223 lines
7.1 KiB
Python
223 lines
7.1 KiB
Python
from pydantic import BaseModel
|
|
from pydantic_ai import Agent, FunctionToolset
|
|
|
|
from haiku.rag.client import HaikuRAG
|
|
from haiku.rag.config.models import AppConfig
|
|
from haiku.rag.tools.context import ToolContext
|
|
from haiku.rag.tools.filters import build_multi_document_filter, combine_filters
|
|
from haiku.rag.tools.session import SESSION_NAMESPACE, SessionState
|
|
from haiku.rag.utils import get_model
|
|
|
|
DOCUMENT_NAMESPACE = "haiku.rag.document"
|
|
|
|
DOCUMENT_SUMMARY_PROMPT = """Generate a summary of the document content provided below.
|
|
|
|
Start with a one-paragraph overview, then list the main topics covered, and highlight any key findings or conclusions.
|
|
|
|
Guidelines:
|
|
- Aim for 1-2 paragraphs for short documents, 3-4 paragraphs for longer ones
|
|
- Focus on factual content and key information
|
|
- Do not include meta-commentary like "This document discusses..." or "The document covers..."
|
|
- Do not speculate beyond what's in the content
|
|
|
|
Document content:
|
|
{content}"""
|
|
|
|
|
|
class DocumentInfo(BaseModel):
|
|
"""Document info for list_documents response."""
|
|
|
|
title: str
|
|
uri: str
|
|
created: str
|
|
|
|
|
|
class DocumentListResponse(BaseModel):
|
|
"""Response from list_documents tool."""
|
|
|
|
documents: list[DocumentInfo]
|
|
page: int
|
|
total_pages: int
|
|
total_documents: int
|
|
|
|
|
|
class DocumentState(BaseModel):
|
|
"""State for document toolset.
|
|
|
|
Tracks documents accessed during tool invocations.
|
|
"""
|
|
|
|
accessed_documents: list[DocumentInfo] = []
|
|
|
|
|
|
async def find_document(client: HaikuRAG, query: str):
|
|
"""Find a document by exact URI, partial URI, or partial title match."""
|
|
# Try exact URI match first
|
|
doc = await client.get_document_by_uri(query)
|
|
if doc is not None:
|
|
return doc
|
|
|
|
escaped_query = query.replace("'", "''")
|
|
# Also try without spaces for matching "TB MED 593" to "tbmed593"
|
|
no_spaces = escaped_query.replace(" ", "")
|
|
|
|
# Try partial URI match (with and without spaces)
|
|
docs = await client.list_documents(
|
|
limit=1,
|
|
filter=f"LOWER(uri) LIKE LOWER('%{escaped_query}%') OR LOWER(uri) LIKE LOWER('%{no_spaces}%')",
|
|
)
|
|
if docs:
|
|
return docs[0]
|
|
|
|
# Try partial title match (with and without spaces)
|
|
docs = await client.list_documents(
|
|
limit=1,
|
|
filter=f"LOWER(title) LIKE LOWER('%{escaped_query}%') OR LOWER(title) LIKE LOWER('%{no_spaces}%')",
|
|
)
|
|
if docs:
|
|
return docs[0]
|
|
|
|
return None
|
|
|
|
|
|
def create_document_toolset(
|
|
client: HaikuRAG,
|
|
config: AppConfig,
|
|
context: ToolContext | None = None,
|
|
base_filter: str | None = None,
|
|
) -> FunctionToolset:
|
|
"""Create a toolset with document management capabilities.
|
|
|
|
Args:
|
|
client: HaikuRAG client for document operations.
|
|
config: Application configuration (used for summarization LLM).
|
|
context: Optional ToolContext for state tracking.
|
|
If provided, accessed documents are tracked in DocumentState.
|
|
If SessionState is registered, it will be used for dynamic
|
|
document filtering.
|
|
base_filter: Optional base SQL WHERE clause applied to list operations.
|
|
|
|
Returns:
|
|
FunctionToolset with list_documents, get_document, summarize_document tools.
|
|
"""
|
|
# Get or create document state if context provided
|
|
state: DocumentState | None = None
|
|
if context is not None:
|
|
state = context.get_or_create(DOCUMENT_NAMESPACE, DocumentState)
|
|
|
|
async def list_documents(page: int = 1) -> DocumentListResponse:
|
|
"""List available documents in the knowledge base.
|
|
|
|
Args:
|
|
page: Page number (default: 1, 50 documents per page)
|
|
|
|
Returns:
|
|
Paginated list of documents with metadata.
|
|
"""
|
|
page_size = 50
|
|
offset = (page - 1) * page_size
|
|
|
|
# Get session filter from session state
|
|
session_filter = None
|
|
if context is not None:
|
|
session_state = context.get_typed(SESSION_NAMESPACE, SessionState)
|
|
if session_state is not None and session_state.document_filter:
|
|
session_filter = build_multi_document_filter(
|
|
session_state.document_filter
|
|
)
|
|
|
|
effective_filter = combine_filters(base_filter, session_filter)
|
|
|
|
docs = await client.list_documents(
|
|
limit=page_size, offset=offset, filter=effective_filter
|
|
)
|
|
total = await client.count_documents(filter=effective_filter)
|
|
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
|
|
return DocumentListResponse(
|
|
documents=[
|
|
DocumentInfo(
|
|
title=doc.title or "Untitled",
|
|
uri=doc.uri or "",
|
|
created=doc.created_at.strftime("%Y-%m-%d"),
|
|
)
|
|
for doc in docs
|
|
],
|
|
page=page,
|
|
total_pages=total_pages,
|
|
total_documents=total,
|
|
)
|
|
|
|
async def get_document(query: str) -> str:
|
|
"""Retrieve a specific document by title or URI.
|
|
|
|
Args:
|
|
query: The document title or URI to look up.
|
|
|
|
Returns:
|
|
Document content and metadata, or not found message.
|
|
"""
|
|
doc = await find_document(client, query)
|
|
|
|
if doc is None:
|
|
return f"Document not found: {query}"
|
|
|
|
# Track accessed document in state
|
|
if state is not None:
|
|
state.accessed_documents.append(
|
|
DocumentInfo(
|
|
title=doc.title or "Untitled",
|
|
uri=doc.uri or "",
|
|
created=doc.created_at.strftime("%Y-%m-%d"),
|
|
)
|
|
)
|
|
|
|
return (
|
|
f"**{doc.title or 'Untitled'}**\n\n"
|
|
f"- ID: {doc.id}\n"
|
|
f"- URI: {doc.uri}\n"
|
|
f"- Created: {doc.created_at.strftime('%Y-%m-%d %H:%M')}\n\n"
|
|
f"**Content:**\n{doc.content}"
|
|
)
|
|
|
|
async def summarize_document(query: str) -> str:
|
|
"""Generate a summary of a specific document.
|
|
|
|
Args:
|
|
query: The document title or URI to summarize.
|
|
|
|
Returns:
|
|
Generated summary or not found message.
|
|
"""
|
|
doc = await find_document(client, query)
|
|
|
|
if doc is None:
|
|
return f"Document not found: {query}"
|
|
|
|
# Track accessed document in state
|
|
if state is not None:
|
|
state.accessed_documents.append(
|
|
DocumentInfo(
|
|
title=doc.title or "Untitled",
|
|
uri=doc.uri or "",
|
|
created=doc.created_at.strftime("%Y-%m-%d"),
|
|
)
|
|
)
|
|
|
|
# Use LLM to generate summary
|
|
summary_model = get_model(config.qa.model, config)
|
|
summary_agent: Agent[None, str] = Agent(
|
|
summary_model,
|
|
output_type=str,
|
|
)
|
|
result = await summary_agent.run(
|
|
DOCUMENT_SUMMARY_PROMPT.format(content=doc.content or "")
|
|
)
|
|
|
|
return f"**Summary of {doc.title or doc.uri}:**\n\n{result.output}"
|
|
|
|
toolset = FunctionToolset()
|
|
toolset.add_function(list_documents)
|
|
toolset.add_function(get_document)
|
|
toolset.add_function(summarize_document)
|
|
return toolset
|