Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ When connected to Claude/Cursor/Windsurf, the following tools are available:
- `archon:rag_search_knowledge_base` - Search knowledge base for relevant content
- `archon:rag_search_code_examples` - Find code snippets in the knowledge base
- `archon:rag_get_available_sources` - List available knowledge sources
- `archon:rag_list_pages_for_source` - List all pages for a given source (browse documentation structure)
- `archon:rag_read_full_page` - Retrieve full page content by page_id or URL

### Project Management

Expand Down
74 changes: 74 additions & 0 deletions migration/0.1.0/010_add_page_metadata_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- =====================================================
-- Add archon_page_metadata table for page-based RAG retrieval
-- =====================================================
-- This migration adds support for storing complete documentation pages
-- alongside chunks for improved agent context retrieval.
--
-- Features:
-- - Full page content storage with metadata
-- - Support for llms-full.txt section-based pages
-- - Foreign key relationship from chunks to pages
-- =====================================================

-- Create archon_page_metadata table
CREATE TABLE IF NOT EXISTS archon_page_metadata (
-- Primary identification
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_id TEXT NOT NULL,
url TEXT NOT NULL,

-- Content
full_content TEXT NOT NULL,

-- Section metadata (for llms-full.txt H1 sections)
section_title TEXT,
section_order INT DEFAULT 0,

-- Statistics
word_count INT NOT NULL,
char_count INT NOT NULL,
chunk_count INT NOT NULL DEFAULT 0,

-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),

-- Flexible metadata storage
metadata JSONB DEFAULT '{}'::jsonb,

-- Constraints
CONSTRAINT archon_page_metadata_url_unique UNIQUE(url),
CONSTRAINT archon_page_metadata_source_fk FOREIGN KEY (source_id)
REFERENCES archon_sources(source_id) ON DELETE CASCADE
);

-- Add page_id foreign key to archon_crawled_pages
-- This links chunks back to their parent page
-- NULLABLE because existing chunks won't have a page_id yet
ALTER TABLE archon_crawled_pages
ADD COLUMN IF NOT EXISTS page_id UUID REFERENCES archon_page_metadata(id) ON DELETE SET NULL;

-- Create indexes for query performance
CREATE INDEX IF NOT EXISTS idx_archon_page_metadata_source_id ON archon_page_metadata(source_id);
CREATE INDEX IF NOT EXISTS idx_archon_page_metadata_url ON archon_page_metadata(url);
CREATE INDEX IF NOT EXISTS idx_archon_page_metadata_section ON archon_page_metadata(source_id, section_title, section_order);
CREATE INDEX IF NOT EXISTS idx_archon_page_metadata_created_at ON archon_page_metadata(created_at);
CREATE INDEX IF NOT EXISTS idx_archon_page_metadata_metadata ON archon_page_metadata USING GIN(metadata);
CREATE INDEX IF NOT EXISTS idx_archon_crawled_pages_page_id ON archon_crawled_pages(page_id);

-- Add comments to document the table structure
COMMENT ON TABLE archon_page_metadata IS 'Stores complete documentation pages for agent retrieval';
COMMENT ON COLUMN archon_page_metadata.source_id IS 'References the source this page belongs to';
COMMENT ON COLUMN archon_page_metadata.url IS 'Unique URL of the page (synthetic for llms-full.txt sections with #anchor)';
COMMENT ON COLUMN archon_page_metadata.full_content IS 'Complete markdown/text content of the page';
COMMENT ON COLUMN archon_page_metadata.section_title IS 'H1 section title for llms-full.txt pages';
COMMENT ON COLUMN archon_page_metadata.section_order IS 'Order of section in llms-full.txt file (0-based)';
COMMENT ON COLUMN archon_page_metadata.word_count IS 'Number of words in full_content';
COMMENT ON COLUMN archon_page_metadata.char_count IS 'Number of characters in full_content';
COMMENT ON COLUMN archon_page_metadata.chunk_count IS 'Number of chunks created from this page';
COMMENT ON COLUMN archon_page_metadata.metadata IS 'Flexible JSON metadata (page_type, knowledge_type, tags, etc)';
COMMENT ON COLUMN archon_crawled_pages.page_id IS 'Foreign key linking chunk to parent page';

-- =====================================================
-- MIGRATION COMPLETE
-- =====================================================
165 changes: 162 additions & 3 deletions python/src/mcp_server/features/rag/rag_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ async def rag_get_available_sources(ctx: Context) -> str:

@mcp.tool()
async def rag_search_knowledge_base(
ctx: Context, query: str, source_id: str | None = None, match_count: int = 5
ctx: Context,
query: str,
source_id: str | None = None,
match_count: int = 5,
return_mode: str = "pages"
) -> str:
"""
Search knowledge base for relevant content using RAG.
Expand All @@ -90,20 +94,31 @@ async def rag_search_knowledge_base(
This is the 'id' field from available sources, NOT a URL or domain name.
Example: "src_1234abcd" not "docs.anthropic.com"
match_count: Max results (default: 5)
return_mode: "pages" (default, full pages with metadata) or "chunks" (raw text chunks)

Returns:
JSON string with structure:
- success: bool - Operation success status
- results: list[dict] - Array of matching documents with content and metadata
- results: list[dict] - Array of pages/chunks with content and metadata
Pages include: page_id, url, title, preview, word_count, chunk_matches
Chunks include: content, metadata, similarity
- return_mode: str - Mode used ("pages" or "chunks")
- reranked: bool - Whether results were reranked
- error: str|null - Error description if success=false

Note: Use "pages" mode for better context (recommended), or "chunks" for raw granular results.
After getting pages, use rag_read_full_page() to retrieve complete page content.
"""
try:
api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)

async with httpx.AsyncClient(timeout=timeout) as client:
request_data = {"query": query, "match_count": match_count}
request_data = {
"query": query,
"match_count": match_count,
"return_mode": return_mode
}
if source_id:
request_data["source"] = source_id

Expand All @@ -115,6 +130,7 @@ async def rag_search_knowledge_base(
{
"success": True,
"results": result.get("results", []),
"return_mode": result.get("return_mode", return_mode),
"reranked": result.get("reranked", False),
"error": None,
},
Expand Down Expand Up @@ -198,5 +214,148 @@ async def rag_search_code_examples(
logger.error(f"Error searching code examples: {e}")
return json.dumps({"success": False, "results": [], "error": str(e)}, indent=2)

@mcp.tool()
async def rag_list_pages_for_source(
ctx: Context, source_id: str, section: str | None = None
) -> str:
"""
List all pages for a given knowledge source.

Use this after rag_get_available_sources() to see all pages in a source.
Useful for browsing documentation structure or finding specific pages.

Args:
source_id: Source ID from rag_get_available_sources() (e.g., "src_1234abcd")
section: Optional filter for llms-full.txt section title (e.g., "# Core Concepts")

Returns:
JSON string with structure:
- success: bool - Operation success status
- pages: list[dict] - Array of page objects with id, url, section_title, word_count
- total: int - Total number of pages
- source_id: str - The source ID that was queried
- error: str|null - Error description if success=false

Example workflow:
1. Call rag_get_available_sources() to get source_id
2. Call rag_list_pages_for_source(source_id) to see all pages
3. Call rag_read_full_page(page_id) to read specific pages
"""
try:
api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)

async with httpx.AsyncClient(timeout=timeout) as client:
params = {"source_id": source_id}
if section:
params["section"] = section

response = await client.get(
urljoin(api_url, "/api/pages"),
params=params
)

if response.status_code == 200:
result = response.json()
return json.dumps(
{
"success": True,
"pages": result.get("pages", []),
"total": result.get("total", 0),
"source_id": result.get("source_id", source_id),
"error": None,
},
indent=2,
)
else:
error_detail = response.text
return json.dumps(
{
"success": False,
"pages": [],
"total": 0,
"source_id": source_id,
"error": f"HTTP {response.status_code}: {error_detail}",
},
indent=2,
)

except Exception as e:
logger.error(f"Error listing pages for source {source_id}: {e}")
return json.dumps(
{
"success": False,
"pages": [],
"total": 0,
"source_id": source_id,
"error": str(e)
},
indent=2
)

@mcp.tool()
async def rag_read_full_page(
ctx: Context, page_id: str | None = None, url: str | None = None
) -> str:
"""
Retrieve full page content from knowledge base.
Use this to get complete page content after RAG search.

Args:
page_id: Page UUID from search results (e.g., "550e8400-e29b-41d4-a716-446655440000")
url: Page URL (e.g., "https://docs.example.com/getting-started")

Note: Provide EITHER page_id OR url, not both.

Returns:
JSON string with structure:
- success: bool
- page: dict with full_content, title, url, metadata
- error: str|null
"""
try:
if not page_id and not url:
return json.dumps(
{"success": False, "error": "Must provide either page_id or url"},
indent=2
)

api_url = get_api_url()
timeout = httpx.Timeout(30.0, connect=5.0)

async with httpx.AsyncClient(timeout=timeout) as client:
if page_id:
response = await client.get(urljoin(api_url, f"/api/pages/{page_id}"))
else:
response = await client.get(
urljoin(api_url, "/api/pages/by-url"),
params={"url": url}
)

if response.status_code == 200:
page_data = response.json()
return json.dumps(
{
"success": True,
"page": page_data,
"error": None,
},
indent=2,
)
else:
error_detail = response.text
return json.dumps(
{
"success": False,
"page": None,
"error": f"HTTP {response.status_code}: {error_detail}",
},
indent=2,
)

except Exception as e:
logger.error(f"Error reading page: {e}")
return json.dumps({"success": False, "page": None, "error": str(e)}, indent=2)

# Log successful registration
logger.info("✓ RAG tools registered (HTTP-based version)")
Loading