feat(phase3): add The Librarian agent with library-desk integration
Library-Desk API Client: - Async HTTP client with httpx for library-desk API - HybridRAG search (vector + graph + web) - Wiki operations (search, get, list, create, update) - Smart page creation with HybridRAG research - Semantic vector search and knowledge graph queries - Dossier browsing and health checks Librarian Tools (11 total): - Research: hybrid_search, search_wiki, get_wiki_page, semantic_search - Browse: list_dossiers, get_dossier_pages, explore_knowledge_graph - Graph: find_related_entities - Write: create_wiki_page, update_wiki_page, smart_create_wiki_page Agent: - PydanticAI agent with research assistant personality - System prompt with research and writing workflows - Streaming support via run_librarian_stream() Capability: - LIBRARIAN_CAPABILITY definition for Household Registry - Automatic registration on startup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,701 @@
|
||||
"""
|
||||
Librarian tools for PydanticAI agent.
|
||||
|
||||
These tools wrap the library-desk API and are registered with
|
||||
The Librarian agent for research and knowledge management tasks.
|
||||
"""
|
||||
from src.agents.librarian.client import LibraryDeskClient
|
||||
from src.core.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HybridRAG Search
|
||||
# ============================================================================
|
||||
|
||||
async def hybrid_search(
|
||||
query: str,
|
||||
include_web: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Search across all knowledge sources using HybridRAG.
|
||||
|
||||
This is the primary research tool, combining:
|
||||
- Vector search (semantic similarity over documents)
|
||||
- Knowledge graph (entities and relationships)
|
||||
- Web search (current information from SearXNG)
|
||||
|
||||
Results are fused and re-ranked by relevance.
|
||||
|
||||
Args:
|
||||
query: Natural language research query
|
||||
include_web: Whether to include web results (default: True)
|
||||
|
||||
Returns:
|
||||
Formatted search results with sources and context
|
||||
|
||||
Examples:
|
||||
hybrid_search("How does Docker orchestration work with Kubernetes?")
|
||||
hybrid_search("What projects use Neo4j?", include_web=False)
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
response = await client.hybrid_search(
|
||||
query=query,
|
||||
web_limit=5 if include_web else 0,
|
||||
)
|
||||
|
||||
if not response.results:
|
||||
return f"No results found for '{query}'"
|
||||
|
||||
# Format results
|
||||
output_parts = [f"## Search Results for: {query}\n"]
|
||||
|
||||
# Add keywords if extracted
|
||||
if response.keywords:
|
||||
output_parts.append(f"**Keywords:** {', '.join(response.keywords)}")
|
||||
|
||||
# Add related dossiers
|
||||
if response.related_dossiers:
|
||||
output_parts.append(
|
||||
f"**Related Dossiers:** {', '.join(response.related_dossiers)}"
|
||||
)
|
||||
|
||||
output_parts.append("")
|
||||
|
||||
# Add results
|
||||
for i, result in enumerate(response.results, 1):
|
||||
source_icon = {
|
||||
"vector": "📄",
|
||||
"graph": "🔗",
|
||||
"web": "🌐",
|
||||
}.get(result.source, "•")
|
||||
|
||||
output_parts.append(
|
||||
f"{i}. {source_icon} **{result.title}** (score: {result.score:.2f})"
|
||||
)
|
||||
if result.url:
|
||||
output_parts.append(f" URL: {result.url}")
|
||||
output_parts.append(f" {result.content[:300]}...")
|
||||
output_parts.append("")
|
||||
|
||||
logger.info(
|
||||
"librarian_hybrid_search",
|
||||
query=query,
|
||||
result_count=len(response.results),
|
||||
)
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_hybrid_search_error", error=str(e), query=query)
|
||||
return f"Error searching: {str(e)}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Wiki Operations
|
||||
# ============================================================================
|
||||
|
||||
async def search_wiki(
|
||||
query: str,
|
||||
limit: int = 10,
|
||||
) -> str:
|
||||
"""
|
||||
Search the personal wiki for relevant pages.
|
||||
|
||||
Performs full-text search over wiki page titles, descriptions,
|
||||
and content. Use this for finding specific documents.
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
limit: Maximum results (default: 10)
|
||||
|
||||
Returns:
|
||||
List of matching wiki pages with paths and descriptions
|
||||
|
||||
Examples:
|
||||
search_wiki("docker setup guide")
|
||||
search_wiki("architecture", limit=5)
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
results = await client.search_wiki(query=query, limit=limit)
|
||||
|
||||
if not results:
|
||||
return f"No wiki pages found for '{query}'"
|
||||
|
||||
output_parts = [f"## Wiki Search: {query}\n"]
|
||||
|
||||
for i, page in enumerate(results, 1):
|
||||
output_parts.append(f"{i}. **{page.title}**")
|
||||
output_parts.append(f" Path: {page.path}")
|
||||
if page.description:
|
||||
output_parts.append(f" {page.description}")
|
||||
output_parts.append("")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_wiki_search_error", error=str(e))
|
||||
return f"Error searching wiki: {str(e)}"
|
||||
|
||||
|
||||
async def get_wiki_page(
|
||||
page_id: int,
|
||||
) -> str:
|
||||
"""
|
||||
Get the full content of a wiki page.
|
||||
|
||||
Use this after searching to read the complete content
|
||||
of a specific page.
|
||||
|
||||
Args:
|
||||
page_id: The page ID from search results
|
||||
|
||||
Returns:
|
||||
Full page content including title, path, and markdown content
|
||||
|
||||
Examples:
|
||||
get_wiki_page(42)
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
page = await client.get_wiki_page(page_id=page_id)
|
||||
|
||||
output_parts = [
|
||||
f"# {page.title}",
|
||||
f"**Path:** {page.path}",
|
||||
]
|
||||
|
||||
if page.description:
|
||||
output_parts.append(f"**Description:** {page.description}")
|
||||
|
||||
if page.tags:
|
||||
output_parts.append(f"**Tags:** {', '.join(page.tags)}")
|
||||
|
||||
output_parts.append("")
|
||||
output_parts.append(page.content or "(No content)")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_get_page_error", error=str(e), page_id=page_id)
|
||||
return f"Error getting page {page_id}: {str(e)}"
|
||||
|
||||
|
||||
async def list_dossiers() -> str:
|
||||
"""
|
||||
List all research dossiers (tag collections).
|
||||
|
||||
Dossiers are collections of wiki pages grouped by tag.
|
||||
Use this to discover what knowledge collections exist.
|
||||
|
||||
Returns:
|
||||
List of dossiers with page counts
|
||||
|
||||
Examples:
|
||||
list_dossiers()
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
dossiers = await client.list_dossiers()
|
||||
|
||||
if not dossiers:
|
||||
return "No dossiers found"
|
||||
|
||||
output_parts = ["## Research Dossiers\n"]
|
||||
|
||||
for dossier in dossiers:
|
||||
output_parts.append(
|
||||
f"- **{dossier.name}** ({dossier.page_count} pages)"
|
||||
)
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_list_dossiers_error", error=str(e))
|
||||
return f"Error listing dossiers: {str(e)}"
|
||||
|
||||
|
||||
async def get_dossier_pages(
|
||||
dossier_name: str,
|
||||
limit: int = 20,
|
||||
) -> str:
|
||||
"""
|
||||
Get all pages in a dossier.
|
||||
|
||||
Retrieves pages tagged with the specified dossier name.
|
||||
|
||||
Args:
|
||||
dossier_name: Name of the dossier/tag
|
||||
limit: Maximum pages to return
|
||||
|
||||
Returns:
|
||||
List of pages in the dossier
|
||||
|
||||
Examples:
|
||||
get_dossier_pages("projects")
|
||||
get_dossier_pages("architecture", limit=10)
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
pages = await client.list_wiki_pages(tag=dossier_name, limit=limit)
|
||||
|
||||
if not pages:
|
||||
return f"No pages found in dossier '{dossier_name}'"
|
||||
|
||||
output_parts = [f"## Dossier: {dossier_name}\n"]
|
||||
|
||||
for page in pages:
|
||||
output_parts.append(f"- **{page.title}** ({page.path})")
|
||||
if page.description:
|
||||
output_parts.append(f" {page.description}")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_get_dossier_error", error=str(e))
|
||||
return f"Error getting dossier: {str(e)}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Semantic Search
|
||||
# ============================================================================
|
||||
|
||||
async def semantic_search(
|
||||
query: str,
|
||||
limit: int = 10,
|
||||
) -> str:
|
||||
"""
|
||||
Perform semantic (vector) search over documents.
|
||||
|
||||
Finds documents similar in meaning to the query,
|
||||
even if they don't contain the exact words.
|
||||
|
||||
Args:
|
||||
query: Natural language query
|
||||
limit: Maximum results
|
||||
|
||||
Returns:
|
||||
Matching document chunks with similarity scores
|
||||
|
||||
Examples:
|
||||
semantic_search("containerization best practices")
|
||||
semantic_search("how to handle authentication")
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
results = await client.semantic_search(query=query, limit=limit)
|
||||
|
||||
if not results:
|
||||
return f"No semantically similar content found for '{query}'"
|
||||
|
||||
output_parts = [f"## Semantic Search: {query}\n"]
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
output_parts.append(
|
||||
f"{i}. **{result.page_title}** (score: {result.score:.2f})"
|
||||
)
|
||||
output_parts.append(f" Path: {result.page_path}")
|
||||
output_parts.append(f" {result.chunk_text[:200]}...")
|
||||
output_parts.append("")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_semantic_search_error", error=str(e))
|
||||
return f"Error in semantic search: {str(e)}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Knowledge Graph
|
||||
# ============================================================================
|
||||
|
||||
async def explore_knowledge_graph(
|
||||
entity_type: str = "Document",
|
||||
limit: int = 20,
|
||||
) -> str:
|
||||
"""
|
||||
Explore entities in the knowledge graph.
|
||||
|
||||
Lists nodes of a specific type to understand what's
|
||||
in the knowledge base.
|
||||
|
||||
Args:
|
||||
entity_type: Type of entity (Document, Person, Project, Concept, Technology)
|
||||
limit: Maximum nodes to return
|
||||
|
||||
Returns:
|
||||
List of entities with their properties
|
||||
|
||||
Examples:
|
||||
explore_knowledge_graph("Person")
|
||||
explore_knowledge_graph("Technology", limit=50)
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
nodes = await client.list_graph_nodes(
|
||||
node_type=entity_type,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
if not nodes:
|
||||
return f"No {entity_type} nodes found in knowledge graph"
|
||||
|
||||
output_parts = [f"## Knowledge Graph: {entity_type} Entities\n"]
|
||||
|
||||
for node in nodes:
|
||||
name = node.properties.get("name", node.properties.get("title", node.id))
|
||||
output_parts.append(f"- **{name}**")
|
||||
|
||||
# Show a few key properties
|
||||
for key in ["description", "url", "path"]:
|
||||
if key in node.properties:
|
||||
output_parts.append(f" {key}: {node.properties[key]}")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_explore_graph_error", error=str(e))
|
||||
return f"Error exploring knowledge graph: {str(e)}"
|
||||
|
||||
|
||||
async def find_related_entities(
|
||||
entity_name: str,
|
||||
) -> str:
|
||||
"""
|
||||
Find entities related to a given concept or entity.
|
||||
|
||||
Queries the knowledge graph to find documents, people,
|
||||
and concepts connected to the specified entity.
|
||||
|
||||
Args:
|
||||
entity_name: Name of the entity to find relationships for
|
||||
|
||||
Returns:
|
||||
Related entities and their relationships
|
||||
|
||||
Examples:
|
||||
find_related_entities("Docker")
|
||||
find_related_entities("Kubernetes")
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
# Find entities mentioning or related to the search term
|
||||
cypher = """
|
||||
MATCH (n)
|
||||
WHERE toLower(n.name) CONTAINS toLower($name)
|
||||
OR toLower(n.title) CONTAINS toLower($name)
|
||||
OPTIONAL MATCH (n)-[r]-(related)
|
||||
RETURN n, collect(DISTINCT {type: type(r), node: related})[0..10] as relationships
|
||||
LIMIT 10
|
||||
"""
|
||||
|
||||
results = await client.query_graph(
|
||||
cypher,
|
||||
parameters={"name": entity_name},
|
||||
)
|
||||
|
||||
if not results:
|
||||
return f"No entities found related to '{entity_name}'"
|
||||
|
||||
output_parts = [f"## Entities Related to: {entity_name}\n"]
|
||||
|
||||
for record in results:
|
||||
node = record.get("n", {})
|
||||
relationships = record.get("relationships", [])
|
||||
|
||||
name = node.get("name", node.get("title", "Unknown"))
|
||||
labels = node.get("labels", [])
|
||||
|
||||
output_parts.append(f"### {name}")
|
||||
if labels:
|
||||
output_parts.append(f"Type: {', '.join(labels)}")
|
||||
|
||||
if relationships:
|
||||
output_parts.append("**Connections:**")
|
||||
for rel in relationships[:5]: # Limit to 5 relationships
|
||||
rel_type = rel.get("type", "RELATED_TO")
|
||||
related_node = rel.get("node", {})
|
||||
related_name = related_node.get(
|
||||
"name", related_node.get("title", "Unknown")
|
||||
)
|
||||
output_parts.append(f" - {rel_type} → {related_name}")
|
||||
|
||||
output_parts.append("")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_find_related_error", error=str(e))
|
||||
return f"Error finding related entities: {str(e)}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Wiki Write Operations
|
||||
# ============================================================================
|
||||
|
||||
async def update_wiki_page(
|
||||
page_id: int,
|
||||
content: str | None = None,
|
||||
title: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
description: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing wiki page.
|
||||
|
||||
Supports partial updates - only specify the fields you want to change.
|
||||
Changes trigger automatic vector re-indexing and knowledge graph updates.
|
||||
|
||||
Use this for:
|
||||
- Correcting information in a page
|
||||
- Adding content to an existing page
|
||||
- Updating tags to organize pages into dossiers
|
||||
- Fixing descriptions or titles
|
||||
|
||||
Args:
|
||||
page_id: ID of the page to update (get from search_wiki results)
|
||||
content: New markdown content (optional - only if changing content)
|
||||
title: New title (optional - only if renaming)
|
||||
tags: New tag list (optional - replaces existing tags)
|
||||
description: New description (optional)
|
||||
|
||||
Returns:
|
||||
Confirmation with updated page details
|
||||
|
||||
Examples:
|
||||
update_wiki_page(42, content="# Updated Content\\n\\nNew information here")
|
||||
update_wiki_page(42, tags=["projects", "devops"]) # Add to dossiers
|
||||
update_wiki_page(42, description="Updated description")
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
page = await client.update_wiki_page(
|
||||
page_id=page_id,
|
||||
content=content,
|
||||
title=title,
|
||||
tags=tags,
|
||||
description=description,
|
||||
)
|
||||
|
||||
# Build update summary
|
||||
updated_fields = []
|
||||
if content is not None:
|
||||
updated_fields.append("content")
|
||||
if title is not None:
|
||||
updated_fields.append("title")
|
||||
if tags is not None:
|
||||
updated_fields.append("tags")
|
||||
if description is not None:
|
||||
updated_fields.append("description")
|
||||
|
||||
output_parts = [
|
||||
f"## Page Updated: {page.title}",
|
||||
f"**Path:** {page.path}",
|
||||
f"**Updated fields:** {', '.join(updated_fields)}",
|
||||
]
|
||||
|
||||
if page.tags:
|
||||
output_parts.append(f"**Tags:** {', '.join(page.tags)}")
|
||||
|
||||
output_parts.append("\n*Vector embeddings and knowledge graph will be updated automatically.*")
|
||||
|
||||
logger.info(
|
||||
"librarian_update_page",
|
||||
page_id=page_id,
|
||||
updated_fields=updated_fields,
|
||||
)
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_update_page_error", error=str(e), page_id=page_id)
|
||||
return f"Error updating page {page_id}: {str(e)}"
|
||||
|
||||
|
||||
async def create_wiki_page(
|
||||
title: str,
|
||||
path: str,
|
||||
content: str,
|
||||
tags: list[str],
|
||||
description: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
Create a new wiki page with user-provided content.
|
||||
|
||||
Use this when:
|
||||
- User provides specific content to add
|
||||
- Creating simple notes or reminders
|
||||
- The content is already known/composed
|
||||
|
||||
For research-backed pages where you need to gather information first,
|
||||
use smart_create_wiki_page instead.
|
||||
|
||||
Args:
|
||||
title: Page title
|
||||
path: Page path (e.g., "/projects/my-project" or "/notes/meeting-2024")
|
||||
content: Markdown content for the page
|
||||
tags: List of tags/dossiers (e.g., ["projects", "devops"])
|
||||
description: Short description of the page
|
||||
|
||||
Returns:
|
||||
Confirmation with created page details
|
||||
|
||||
Examples:
|
||||
create_wiki_page(
|
||||
title="SSL Renewal Reminder",
|
||||
path="/reminders/ssl-renewal",
|
||||
content="# SSL Renewal\\n\\nRemember to renew SSL cert on Jan 15",
|
||||
tags=["reminders", "infrastructure"],
|
||||
description="Certificate renewal reminder"
|
||||
)
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
page = await client.create_wiki_page(
|
||||
title=title,
|
||||
path=path,
|
||||
content=content,
|
||||
tags=tags,
|
||||
description=description,
|
||||
)
|
||||
|
||||
output_parts = [
|
||||
f"## Page Created: {page.title}",
|
||||
f"**ID:** {page.id}",
|
||||
f"**Path:** {page.path}",
|
||||
]
|
||||
|
||||
if page.tags:
|
||||
output_parts.append(f"**Tags:** {', '.join(page.tags)}")
|
||||
|
||||
if page.description:
|
||||
output_parts.append(f"**Description:** {page.description}")
|
||||
|
||||
output_parts.append("\n*Vector embeddings and knowledge graph will be updated automatically.*")
|
||||
|
||||
logger.info(
|
||||
"librarian_create_page",
|
||||
page_id=page.id,
|
||||
title=title,
|
||||
path=path,
|
||||
)
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_create_page_error", error=str(e), title=title)
|
||||
return f"Error creating page: {str(e)}"
|
||||
|
||||
|
||||
async def smart_create_wiki_page(
|
||||
topic: str,
|
||||
tags: list[str],
|
||||
path: str | None = None,
|
||||
include_web_research: bool = True,
|
||||
include_wiki_search: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Create a wiki page with automatic research and content synthesis.
|
||||
|
||||
This is the RECOMMENDED way to create pages about topics. It will:
|
||||
1. Search existing wiki, knowledge graph, and web for relevant information
|
||||
2. Use an LLM to synthesize findings into well-structured content
|
||||
3. Create the page with proper source attribution
|
||||
4. Automatically link entities bidirectionally in the knowledge graph
|
||||
|
||||
Use this when:
|
||||
- User says "Create a page about X"
|
||||
- User says "Add information about X to the wiki"
|
||||
- You need to research a topic before writing
|
||||
- The topic would benefit from existing knowledge context
|
||||
|
||||
Args:
|
||||
topic: The topic to research and create a page about
|
||||
tags: List of tags/dossiers for categorization
|
||||
path: Optional custom path (auto-generated from topic if not provided)
|
||||
include_web_research: Whether to search the web (default: True)
|
||||
include_wiki_search: Whether to search existing wiki (default: True)
|
||||
|
||||
Returns:
|
||||
Summary of created page with research statistics
|
||||
|
||||
Examples:
|
||||
smart_create_wiki_page("Docker Compose", tags=["technology", "devops"])
|
||||
smart_create_wiki_page("Home network architecture", tags=["infrastructure"], include_web_research=False)
|
||||
"""
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
response = await client.smart_create_wiki_page(
|
||||
topic=topic,
|
||||
tags=tags,
|
||||
path=path,
|
||||
include_web_research=include_web_research,
|
||||
include_wiki_search=include_wiki_search,
|
||||
)
|
||||
|
||||
page = response.page
|
||||
research = response.research_summary
|
||||
linking = response.entity_linking
|
||||
|
||||
output_parts = [
|
||||
f"## Page Created: {page.title}",
|
||||
f"**ID:** {page.id}",
|
||||
f"**Path:** {page.path}",
|
||||
]
|
||||
|
||||
if page.tags:
|
||||
output_parts.append(f"**Tags:** {', '.join(page.tags)}")
|
||||
|
||||
# Research summary
|
||||
output_parts.append("\n### Research Summary")
|
||||
output_parts.append(f"- **Wiki results used:** {research.wiki_results}")
|
||||
output_parts.append(f"- **Web results used:** {research.web_results}")
|
||||
output_parts.append(f"- **Graph entities found:** {research.graph_entities}")
|
||||
output_parts.append(f"- **Keywords extracted:** {research.keywords_extracted}")
|
||||
output_parts.append(f"- **Total sources:** {response.sources_used}")
|
||||
output_parts.append(f"- **Research time:** {research.timing_ms}ms")
|
||||
|
||||
# Entity linking
|
||||
if linking.forward_links > 0 or linking.backward_links > 0:
|
||||
output_parts.append("\n### Knowledge Graph Updates")
|
||||
output_parts.append(f"- **Forward links created:** {linking.forward_links}")
|
||||
output_parts.append(f"- **Backward links created:** {linking.backward_links}")
|
||||
output_parts.append(f"- **Related pages updated:** {linking.pages_updated}")
|
||||
|
||||
logger.info(
|
||||
"librarian_smart_create",
|
||||
topic=topic,
|
||||
page_id=page.id,
|
||||
sources_used=response.sources_used,
|
||||
)
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("librarian_smart_create_error", error=str(e), topic=topic)
|
||||
return f"Error creating page about '{topic}': {str(e)}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tool Collection for Registration
|
||||
# ============================================================================
|
||||
|
||||
# All tools available to The Librarian
|
||||
LIBRARIAN_TOOLS = [
|
||||
# Research tools
|
||||
hybrid_search,
|
||||
search_wiki,
|
||||
get_wiki_page,
|
||||
list_dossiers,
|
||||
get_dossier_pages,
|
||||
semantic_search,
|
||||
explore_knowledge_graph,
|
||||
find_related_entities,
|
||||
# Write tools
|
||||
create_wiki_page,
|
||||
update_wiki_page,
|
||||
smart_create_wiki_page,
|
||||
]
|
||||
Reference in New Issue
Block a user