Vectorless Agentic Rag — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Vectorless Agentic Rag (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This project implements vectorless agentic RAG over Confluence using Python, PageIndex, OpenAI gpt-4o-mini, Claude via Pydantic-AI, and FastMCP. An MCP server and Streamlit chatbot are implemented as core functionalities, replacing vector similarity search with LLM reasoning over a hierarchical document tree — no embeddings or reranker needed.
Traditional RAG pipelines answer the question "which chunk is most similar to this query?" using cosine distance or BM25 scores. PageIndex asks a different question: "given the document's table of contents, where does the answer most likely live?"
Traditional (hybrid-RAG):
query → embed → cosine similarity → top-k chunks → rerank → answer
Vectorless (this project):
query → read tree → reason about sections → read section by line → answerThe "index" is a hierarchical JSON tree — a machine-readable table of contents — built once by an LLM analysing the document's structure. At query time, the agent reads this tree, identifies the relevant section by its line_num field, and fetches exactly those lines. No vectors, no BM25, no embeddings matrix.
| Problem with hybrid RAG | Why vectorless avoids it |
|---|---|
| Fixed-size chunking splits sentences, code blocks, and tables across boundaries | PageIndex uses natural section boundaries from the document's own headings |
| "Similar text ≠ relevant text" — embedding similarity conflates topics | The agent reasons about section purpose, not surface similarity |
| In-document cross-references ("see Appendix B") are silently dropped | The agent can follow references by reading the referenced section |
| Multi-turn context is lost — each query is independent | The tree is stable; the agent can re-visit sections across conversation turns |
| Rare technical terms get diluted in dense embeddings | Tree navigation is label-based, not frequency-based |
| Scenario | Why hybrid wins |
|---|---|
| Very large corpora (> 1 M short snippets) | A flat tree becomes unwieldy; BM25 scales better |
| Unstructured content (chat logs, raw emails) | No headings to build a tree from |
| Low-latency requirements (< 200 ms) | Tree navigation requires LLM reasoning at query time |
| Simple keyword lookups | BM25 is faster and cheaper for exact-match queries |
| Dimension | Hybrid RAG | Vectorless RAG |
|---|---|---|
| Index type | BM25 + NumPy embeddings | JSON tree (no vectors) |
| Index build cost | Cheap (embeddings API) | Higher (LLM per section) |
| Query cost | Cheap (cosine + rerank) | Higher (LLM reasoning) |
| Chunking | Fixed 1500-char splits | Natural document sections |
| Cross-reference handling | Broken | Native (agent follows references) |
| Dependencies | OpenAI + Cohere + bm25s | PageIndex + OpenAI (indexing only) |
| MCP port | 8051 | 8052 |
[Confluence API] [Public docs (K8s)]
| |
1-fetch-confluence.py 1-fetch-k8s.py
| |
└──────────┬─────────────┘
↓
docs/<SPACE_KEY>.md
(one Markdown file per space/section;
all pages concatenated as ## sections)
↓
2-build-index.py
(PageIndexClient.index() — LLM builds tree)
↓
workspace/ ← PageIndex JSON trees (no .npy, no BM25)
indexes/meta.json ← space_key → doc_id mapping
↓
┌────────────┴────────────┐
│ 3 tools │
│ list_spaces │ ← discover indexed knowledge areas
│ get_space_structure │ ← read the hierarchical tree
│ read_section │ ← fetch content by line range
└────────────┬────────────┘
↓
┌────────────┴────────────┐
│ 4-agent.py │ pydantic-ai Claude agent
│ 5-mcp-server.py │ FastMCP on port 8052
│ 6-chatbot.py │ Streamlit UI
└─────────────────────────┘4-vectorless-agentic-RAG/
│
├── 1-fetch-confluence.py Fetch Confluence → docs/<SPACE>.md
├── 1-fetch-k8s.py Fetch K8s public docs → docs/K8S_*.md (no Confluence needed)
├── 2-build-index.py Build PageIndex trees → workspace/ + indexes/meta.json
├── 3-search.py Interactive tree explorer (no agent)
├── 4-agent.py pydantic-ai Claude agent with structured output
├── 5-mcp-server.py FastMCP server on port 8052
├── 6-chatbot.py Streamlit chatbot (MCP client)
│
├── utils/
│ ├── confluence.py Confluence REST client + html_to_markdown()
│ └── agent_tools.py list_spaces / get_space_structure / read_section
│
├── docs/ ← created by step 1 (one .md per space)
├── workspace/ ← created by step 2 (PageIndex internal workspace)
├── indexes/ ← created by step 2 (meta.json only — no vectors)
│
├── pyproject.toml
├── .env.example
└── readme.mdPageIndex is not on PyPI. Install from GitHub:
pip install git+https://github.com/VectifyAI/PageIndex.gitOr clone and install in editable mode (recommended for development):
git clone https://github.com/VectifyAI/PageIndex.git
cd PageIndex
pip install -e .
cd ..pip install -e .
# or with uv:
uv sync.envcp .env.example .envRequired keys:
| Variable | Used by | Purpose |
|---|---|---|
CONFLUENCE_BASE_URL | steps 1, 3 | Your Atlassian instance URL |
CONFLUENCE_EMAIL | steps 1, 3 | Atlassian account email |
CONFLUENCE_API_TOKEN | steps 1, 3 | API token from id.atlassian.com |
CONFLUENCE_SPACE_KEYS | step 1 | Comma-separated space keys to index |
OPENAI_API_KEY | step 2 | PageIndex uses OpenAI to build trees |
ANTHROPIC_API_KEY | steps 4–6 | Claude agent for reasoning and retrieval |
No `COHERE_API_KEY` needed. Vectorless RAG has no reranking step.
Option A: Confluence (requires API token)
# First run with empty CONFLUENCE_SPACE_KEYS lists available spaces:
uv run 1-fetch-confluence.py
# After setting CONFLUENCE_SPACE_KEYS=ENG,DOCS in .env:
uv run 1-fetch-confluence.pyOutput: docs/ENG.md, docs/DOCS.md, etc. — one Markdown file per space, containing all pages concatenated as ## Page: {title} sections.
Option B: Kubernetes public docs (no API token needed, good for testing)
uv run 1-fetch-k8s.pyOutput: docs/K8S_CONCEPTS.md, docs/K8S_TASKS.md, etc.
uv run 2-build-index.pyFor each .md in docs/, PageIndex calls the LLM to analyse the document structure and produce a hierarchical JSON tree. Results are cached in workspace/ — subsequent runs skip already-indexed documents.
What gets built:
workspace/ — PageIndex internal tree files (managed by the library)indexes/meta.json — maps space_key → {doc_id, space_name, page_count}Cost estimate: roughly 100–200 LLM calls to gpt-4o-mini per 50-page space, which costs approximately $0.01–0.05 per space. Run once; re-running is free for unchanged documents.
uv run 3-search.pyThis shows the three-step navigation that the agent uses:
list_spaces() — see what's indexedget_space_structure(space_key) — read the hierarchical treeread_section(space_key, "120-180") — fetch content by line rangeUnlike 3-hybrid-search.py in the hybrid-RAG project, there is no query-scoring step. You navigate the tree manually — the same reasoning the agent does automatically.
uv run 4-agent.py "What is our on-call escalation process?"
uv run 4-agent.py "How do we request access to production databases?"
uv run 4-agent.py "What changed in the deployment process last quarter?"The agent:
list_spaces() to discover indexed knowledge areasget_space_structure() to read the document treeread_section() with tight line ranges (e.g. "120-180")ConfluenceAnswer with the answer and citationsuv run 5-mcp-server.pyStarts on http://0.0.0.0:8052/sse. Connect from:
Claude Desktop (%APPDATA%\Claude\claude_desktop_config.json on Windows, ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"confluence-vectorless": {
"url": "http://localhost:8052/sse"
}
}
}Claude Code (.mcp.json in repo root):
{
"mcpServers": {
"confluence-vectorless": {
"type": "sse",
"url": "http://localhost:8052/sse"
}
}
}Cursor → Settings → MCP → Server URL: http://localhost:8052/sse
# In one terminal:
uv run 5-mcp-server.py
# In another terminal:
uv run streamlit run 6-chatbot.pyThese three functions are in utils/agent_tools.py and are shared between the standalone agent (4-agent.py) and the MCP server (5-mcp-server.py).
list_spaces()Reads indexes/meta.json and returns the list of indexed knowledge areas.
[
{"space_key": "ENG", "space_name": "Engineering", "page_count": 47},
{"space_key": "HR", "space_name": "Human Resources", "page_count": 12}
]The agent calls this first to know what's available — identical purpose to list_spaces() in the hybrid-RAG project.
get_space_structure(space_key)Calls PageIndexClient.get_document_structure(doc_id) and returns the full hierarchical tree as JSON. Example structure:
[
{
"name": "Engineering Space",
"description": "Internal engineering documentation",
"line_num": 1,
"sub_nodes": [
{
"name": "Page: Deployment Process",
"description": "How to deploy services to production",
"line_num": 15,
"sub_nodes": [
{
"name": "Prerequisites",
"description": "Required access and tools before deploying",
"line_num": 22,
"sub_nodes": []
},
{
"name": "Rollback procedure",
"description": "Steps to revert a failed deployment",
"line_num": 48,
"sub_nodes": []
}
]
}
]
}
]The agent reasons over this tree to identify the relevant line_num values, then calls read_section() with those lines.
This replaces `hybrid_search()` from the hybrid-RAG project. Instead of a ranked list of similar chunks, the agent gets a map of the entire document and decides where to look.
read_section(space_key, lines)Calls PageIndexClient.get_page_content(doc_id, lines) and returns the raw Markdown content for that line range. Examples:
read_section("ENG", "48-75") # lines 48 to 75
read_section("ENG", "42") # single line
read_section("ENG", "10,20,35") # specific linesThis replaces `get_page_full()` from the hybrid-RAG project. Instead of fetching an entire Confluence page by ID, the agent reads exactly the lines it identified from the tree — typically a 30–60 line section.
When you call client.index(md_path), PageIndex:
#, ##, ###) tocreate an initial outline
section covers and whether it should be split into sub-sections
name, description,line_num, and sub_nodes list
index() calls on the samefilename return the cached doc_id immediately
The LLM used during indexing is configured via PageIndex's config.yaml (defaults to gpt-4o-mini). The retrieval agent (steps 4–6) uses Claude independently — you can swap either model without affecting the other.
docs/ENG.md looks like)<!-- space: ENG | name: Engineering | pages: 47 | fetched: 2024-01-15T10:30:00Z -->
# Engineering Space
> Confluence space **ENG** — 47 pages indexed on 2024-01-15
## Page: Deployment Process
<!-- page_id: 12345 | url: https://... | last_modified: 2024-01-10T08:00:00Z -->
### Overview
All production deployments go through the CI/CD pipeline defined in...
### Prerequisites
You need write access to the `prod-deploy` GitHub environment...
### Rollback procedure
If the healthcheck fails within 5 minutes of deploy...
---
## Page: On-Call Runbook
<!-- page_id: 12346 | url: https://... | last_modified: 2024-01-12T09:00:00Z -->
...Headings within each page are shifted by two levels (h1→###, h2→####) so the page's own ## Page: header is the root of its subtree. This lets PageIndex build a clean three-level hierarchy:
# Space (level 1 — the space)
## Page: ... (level 2 — individual Confluence pages)
### Section heading (level 3 — headings within the page)
#### Subsection (level 4)Both projects expose the same MCP interface and produce ConfluenceAnswer with citations, so you can run the same question through both and compare.
| Aspect | 1-hybrid-agentic-RAG | 4-vectorless-agentic-RAG |
|---|---|---|
| Port | 8051 | 8052 |
| Tool 1 | list_spaces() | list_spaces() |
| Tool 2 | hybrid_search(query) | get_space_structure(space_key) |
| Tool 3 | get_page_full(page_id) | read_section(space_key, lines) |
| Output type | ConfluenceAnswer | ConfluenceAnswer |
| Citation fields | page_id, title, url, quote | space_key, section_title, lines_read, quote |
| Index files | indexes/bm25/, embeddings.npy, meta.json | workspace/, meta.json |
| External APIs | OpenAI (embed) + Cohere (rerank) | OpenAI (index build only) |
| Re-index cost | Free (embeddings already stored) | Free (workspace cached) |
Run them simultaneously — they use different ports and separate index directories, so there is no conflict.
Choose vectorless when:
Stay with hybrid-RAG when:
Re-run step 1 to fetch updated pages, then step 2 to re-index changed documents. PageIndex detects unchanged documents by filename — only re-processed files incur LLM cost.
For continuous sync, schedule a nightly job:
uv run 1-fetch-confluence.py && uv run 2-build-index.pyEach Confluence space becomes one .md file and one PageIndex document. With many spaces, the agent's first call to get_space_structure() must pick the right space. If cross-space queries are common, consider:
and descriptions before calling get_space_structure()
.md file atfetch time so fewer top-level documents need checking
(cheap), then use PageIndex for the selected space's content
| Step | Model | Cost |
|---|---|---|
| Build tree (per 50-page space) | gpt-4o-mini | ~$0.01–0.05 |
| Query — list + structure | claude-sonnet-4-6 | ~$0.002 |
| Query — read section (1–2 calls) | claude-sonnet-4-6 | ~$0.005–0.015 |
| Total per query | ~$0.007–0.017 |
Compare with hybrid-RAG: ~$0.003–0.008 per query (cheaper per query, but requires Cohere API and periodic re-embedding when content changes).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.