Skip to main content
Glama
README.md
# agent-skills

Modular RAG skills for a self-hosted LLM agent, backed by Qdrant.

Most "chat with your Obsidian vault" projects embed a vector store inside the
Obsidian app and stop there. This is a different shape. Any knowledge source (an
Obsidian vault, a folder of notes, a reference set) becomes a *skill* the agent
can search, all behind one interface. It targets a standalone agent (Ollama, an
agent gateway, an MCP client) instead of living inside the note-taking app.

The Obsidian vault indexer is the main worked example. It is one skill, not the
whole project.

## The core idea: a skill is three things

```
skill = (Qdrant collection) + (manifest: where the docs live + how to chunk) + (retrieval hook)
```

Everything downstream is generic. Adding a knowledge skill is a manifest, not a
rewrite:

```python
NOTES = SkillManifest(name="work_notes", source_glob="~/notes/work/**/*.md")
register(NOTES)          # chunk -> embed -> upsert
retrieve(NOTES.name, q)  # embed query -> search -> context block
```

## Architecture

```
  sources            ingestion              Qdrant                 agent
  vault /     ---->  chunk + embed  ---->  one collection  ---->  gateway / MCP
  notes /            (via Ollama)          per skill              injects context,
  reference sets                                                  then answers
```

## What the vault skill handles

The Obsidian loader (`agent_skills/vault.py`) does not treat notes as flat text:

- YAML frontmatter is parsed into payload metadata, not embedded as prose.
- `[[wikilinks]]` are flattened to their display text for the embedding, and
  kept as a `links` list in the payload for graph-aware retrieval.
- `#tags` (inline and frontmatter) become filterable payload metadata.
- Image embeds (`![[...]]`) are dropped before embedding.
- Incremental sync: each note is SHA-256 hashed, only changed notes are
  re-embedded, deleted notes are purged. The collection is not wiped per run.
- Deterministic chunk IDs (`uuid5(path:index)`) so edits overwrite cleanly
  instead of duplicating.

## Quickstart

```bash
pip install -r requirements.txt

# 1. bring up Qdrant locally
docker run -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant

# 2. pull an embedding model in Ollama
ollama pull nomic-embed-text

# 3. set VAULT_PATH in agent_skills/config.py, then index
python examples/index_vault.py sync

# 4. query
python examples/index_vault.py ask "how did I configure the network?"
```

## Add your own skill

1. Copy `examples/register_notes.py`.
2. Write a `SkillManifest` pointing at your docs.
3. `register()` it. It is now a collection the agent queries like any other.

## Plug into an agent (MCP)

`examples/mcp_server.py` exposes retrieval as MCP tools (`list_skills`,
`search_vault`, `search_skill`), so an MCP client can query the knowledge. The
surface is read-only: it retrieves, it does not write or take actions.

```bash
pip install "mcp[cli]"
python examples/mcp_server.py          # stdio, for local MCP clients
```

For a local client (Claude Code, Cursor, Claude Desktop), point its MCP config
at the script:

```json
{
  "mcpServers": {
    "agent-skills": {
      "command": "python",
      "args": ["/absolute/path/to/examples/mcp_server.py"]
    }
  }
}
```

For a networked gateway, switch the transport to `sse` at the bottom of
`mcp_server.py` and point the gateway at `http://<host>:8000/sse`. The agent
calls `search_vault(...)`, the tool returns a context block, and the gateway
feeds it into the model's context.

## Config

Everything lives in `agent_skills/config.py`: Ollama URL, embedding model and
dimension, Qdrant host/port, chunk size and overlap. Swap the embedder freely,
but keep `EMBED_DIM` matched to the model or the upsert will reject.

## Notes

- Local-first. Embeddings run on your own Ollama host; nothing leaves the network.
- Retrieval only. This gives an agent knowledge to reason over. It does not hand
  the agent tools to act on the world.
- Not an Obsidian plugin. It reads the vault as files; it does not run in the app.
- Early stage: the code is organized and syntax-clean, but run it against a live
  Qdrant and Ollama before relying on it.

## License

MIT. See [LICENSE](LICENSE).