feat: add unified memory routing to consolidation service
- Add MemoryRouteClassification and MemoryRoutingResult models - Implement unified classifier (_classify_web_results_unified) that routes web results to: wiki, volatile, file (Paperless), prefetch, or skip - Add routing methods: _route_to_volatile, _route_to_files, _register_prefetch - Update _process_search to use unified classifier instead of separate analysis - Add get_volatile_cache_service factory to dependencies - Wire volatile_service and settings_client into ConsolidationService - Update ConsolidationResult/Response with new routing counters Test fixes: - Fix WikiJSClient fixtures to use api_token instead of username/password - Fix entity linking test assertions to expect full user-namespaced paths - Add sample_unified_classification fixture for new classifier format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -597,7 +597,9 @@ def get_consolidation_service() -> "ConsolidationService":
|
||||
ollama=get_ollama_client(),
|
||||
wiki=get_wikijs_client(),
|
||||
settings=get_settings(),
|
||||
ingestion_service=get_ingestion_service()
|
||||
ingestion_service=get_ingestion_service(),
|
||||
volatile_service=get_volatile_cache_service(),
|
||||
settings_client=get_settings_client(),
|
||||
)
|
||||
|
||||
|
||||
@@ -638,6 +640,17 @@ def get_rag_search_service() -> "RAGSearchService":
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_volatile_cache_service() -> "VolatileCacheService":
|
||||
"""Get VolatileCacheService singleton."""
|
||||
from src.services.volatile_service import VolatileCacheService
|
||||
return VolatileCacheService(
|
||||
qdrant_client=get_qdrant_client(),
|
||||
ollama_client=get_ollama_client(),
|
||||
settings=get_settings()
|
||||
)
|
||||
|
||||
|
||||
# Authentication
|
||||
from fastapi import Security, HTTPException
|
||||
from fastapi.security import HTTPBearer
|
||||
|
||||
@@ -34,6 +34,9 @@ class ConsolidationResult(BaseModel):
|
||||
pages_created: int = 0
|
||||
pages_updated: int = 0
|
||||
entities_added: int = 0
|
||||
volatile_cached: int = 0
|
||||
files_queued: int = 0
|
||||
prefetch_registered: int = 0
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@@ -44,6 +47,53 @@ class ConsolidationResponse(BaseModel):
|
||||
pages_created: int = Field(description="New wiki pages created")
|
||||
pages_updated: int = Field(description="Existing pages updated")
|
||||
entities_added: int = Field(description="New entities added to graph")
|
||||
volatile_cached: int = Field(default=0, description="Items cached to volatile storage")
|
||||
files_queued: int = Field(default=0, description="Files queued for Paperless")
|
||||
prefetch_registered: int = Field(default=0, description="Prefetch patterns registered")
|
||||
errors: List[str] = Field(default=[], description="Error messages")
|
||||
results: List[ConsolidationResult] = Field(description="Per-search results")
|
||||
dry_run: bool = Field(description="Whether this was a dry run")
|
||||
|
||||
|
||||
class MemoryRouteClassification(BaseModel):
|
||||
"""
|
||||
Unified classification of a web result for memory routing.
|
||||
|
||||
Route types:
|
||||
- wiki: Stable reference content → wiki page creation/update
|
||||
- volatile: Ephemeral data (weather, news, prices) → volatile cache
|
||||
- file: Downloadable file (PDF, doc, xls, images) → Paperless ingestion
|
||||
- prefetch: Regularly updated source → scheduler registration
|
||||
- skip: Low value, ads, errors → discard
|
||||
"""
|
||||
url: str
|
||||
title: str
|
||||
route_type: str = Field(description="One of: wiki, volatile, file, prefetch, skip")
|
||||
|
||||
# Wiki routing fields
|
||||
wiki_action: Optional[str] = Field(default=None, description="create or update")
|
||||
wiki_path: Optional[str] = Field(default=None, description="Wiki path for page")
|
||||
wiki_summary: Optional[str] = Field(default=None, description="Summary for wiki page")
|
||||
|
||||
# Volatile routing fields
|
||||
volatile_namespace: Optional[str] = Field(default=None, description="weather, news, financial, etc.")
|
||||
volatile_key: Optional[str] = Field(default=None, description="Cache key")
|
||||
volatile_ttl_hours: Optional[int] = Field(default=None, description="TTL in hours")
|
||||
|
||||
# Prefetch routing fields
|
||||
prefetch_cron: Optional[str] = Field(default=None, description="Cron expression for refresh")
|
||||
prefetch_endpoint: Optional[str] = Field(default=None, description="API endpoint to call")
|
||||
|
||||
# Classification metadata
|
||||
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
||||
reason: str = Field(default="")
|
||||
|
||||
|
||||
class MemoryRoutingResult(BaseModel):
|
||||
"""Aggregated result of memory routing for a search."""
|
||||
wiki_routed: int = 0
|
||||
volatile_cached: int = 0
|
||||
files_queued: int = 0
|
||||
prefetch_registered: int = 0
|
||||
skipped: int = 0
|
||||
classifications: List[MemoryRouteClassification] = []
|
||||
|
||||
@@ -12,7 +12,8 @@ from src.models.consolidation import ConsolidationRequest, ConsolidationResponse
|
||||
from src.services.consolidation_service import ConsolidationService
|
||||
from src.core.dependencies import (
|
||||
Neo4jDep, OllamaDep, WikiJSDep,
|
||||
verify_api_key, get_settings, get_ingestion_service
|
||||
verify_api_key, get_settings, get_ingestion_service,
|
||||
get_volatile_cache_service, get_settings_client,
|
||||
)
|
||||
from src.config import Settings
|
||||
|
||||
@@ -34,7 +35,9 @@ def get_consolidation_service(
|
||||
ollama=ollama_client,
|
||||
wiki=wiki_client,
|
||||
settings=settings,
|
||||
ingestion_service=get_ingestion_service()
|
||||
ingestion_service=get_ingestion_service(),
|
||||
volatile_service=get_volatile_cache_service(),
|
||||
settings_client=get_settings_client(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ from src.services.wiki_page_writer import WikiPageWriter
|
||||
from src.models.consolidation import (
|
||||
SearchQueryInfo,
|
||||
ConsolidationResult,
|
||||
ConsolidationResponse
|
||||
ConsolidationResponse,
|
||||
MemoryRouteClassification,
|
||||
MemoryRoutingResult,
|
||||
)
|
||||
from src.config import Settings
|
||||
|
||||
@@ -41,7 +43,9 @@ class ConsolidationService:
|
||||
ollama: OllamaClient,
|
||||
wiki: WikiJSClient,
|
||||
settings: Settings,
|
||||
ingestion_service: Optional["IngestionService"] = None
|
||||
ingestion_service: Optional["IngestionService"] = None,
|
||||
volatile_service: Optional["VolatileCacheService"] = None,
|
||||
settings_client: Optional["SettingsClient"] = None,
|
||||
):
|
||||
self.neo4j = neo4j
|
||||
self.ollama = ollama
|
||||
@@ -49,6 +53,8 @@ class ConsolidationService:
|
||||
self.settings = settings
|
||||
self.wiki_page_writer = WikiPageWriter(ollama_client=ollama, settings=settings)
|
||||
self.ingestion_service = ingestion_service # Optional to avoid circular dependency
|
||||
self.volatile_service = volatile_service # For ephemeral data caching
|
||||
self.settings_client = settings_client # For prefetch registration
|
||||
|
||||
async def consolidate_knowledge(
|
||||
self,
|
||||
@@ -97,6 +103,9 @@ class ConsolidationService:
|
||||
total_pages_created = 0
|
||||
total_pages_updated = 0
|
||||
total_entities_added = 0
|
||||
total_volatile_cached = 0
|
||||
total_files_queued = 0
|
||||
total_prefetch_registered = 0
|
||||
errors: List[str] = []
|
||||
|
||||
for search in unprocessed:
|
||||
@@ -112,6 +121,9 @@ class ConsolidationService:
|
||||
total_pages_created += result.pages_created
|
||||
total_pages_updated += result.pages_updated
|
||||
total_entities_added += result.entities_added
|
||||
total_volatile_cached += result.volatile_cached
|
||||
total_files_queued += result.files_queued
|
||||
total_prefetch_registered += result.prefetch_registered
|
||||
|
||||
# Mark as processed if not dry run (even if skipped)
|
||||
# This prevents searches from accumulating when they don't meet criteria
|
||||
@@ -141,6 +153,9 @@ class ConsolidationService:
|
||||
pages_created=total_pages_created,
|
||||
pages_updated=total_pages_updated,
|
||||
entities_added=total_entities_added,
|
||||
volatile_cached=total_volatile_cached,
|
||||
files_queued=total_files_queued,
|
||||
prefetch_registered=total_prefetch_registered,
|
||||
errors=errors,
|
||||
results=results,
|
||||
dry_run=dry_run
|
||||
@@ -149,7 +164,8 @@ class ConsolidationService:
|
||||
logger.info(
|
||||
f"Consolidation complete: {processed_count}/{len(unprocessed)} searches, "
|
||||
f"{total_pages_created} pages created, {total_pages_updated} updated, "
|
||||
f"{total_entities_added} entities added"
|
||||
f"{total_entities_added} entities, {total_volatile_cached} volatile, "
|
||||
f"{total_files_queued} files, {total_prefetch_registered} prefetch"
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -213,6 +229,13 @@ class ConsolidationService:
|
||||
) -> Optional[ConsolidationResult]:
|
||||
"""
|
||||
Process a single search query for knowledge consolidation.
|
||||
|
||||
Uses unified memory routing to classify each web result and route to:
|
||||
- wiki: Stable reference content → wiki page creation/update
|
||||
- volatile: Ephemeral data → volatile cache
|
||||
- file: Downloadable documents → Paperless queue
|
||||
- prefetch: Regular updates → scheduler registration
|
||||
- skip: Low value content → discard
|
||||
"""
|
||||
search_id = search['id']
|
||||
query = search['query']
|
||||
@@ -234,97 +257,106 @@ class ConsolidationService:
|
||||
|
||||
logger.info(f"Retrieved {len(web_results)} web results")
|
||||
|
||||
# Analyze web results with Ollama for novel information
|
||||
analysis = await self._analyze_web_results(
|
||||
# Unified classification of all web results
|
||||
routing_result = await self._classify_web_results_unified(
|
||||
query=query,
|
||||
web_results=web_results,
|
||||
keywords=search.get('keywords', []),
|
||||
user=user
|
||||
)
|
||||
|
||||
if not analysis or not analysis.get('has_novel_info'):
|
||||
logger.info("No novel information found")
|
||||
if not routing_result.classifications:
|
||||
logger.info("No classifications returned")
|
||||
return ConsolidationResult(
|
||||
search_id=search_id,
|
||||
query=query
|
||||
)
|
||||
|
||||
# Extract consolidation actions
|
||||
pages_to_create = analysis.get('new_pages', [])
|
||||
pages_to_update = analysis.get('update_pages', [])
|
||||
new_entities = analysis.get('new_entities', [])
|
||||
|
||||
logger.info(
|
||||
f"Analysis: {len(pages_to_create)} new pages, "
|
||||
f"{len(pages_to_update)} updates, {len(new_entities)} entities"
|
||||
f"Routing: {routing_result.wiki_routed} wiki, "
|
||||
f"{routing_result.volatile_cached} volatile, "
|
||||
f"{routing_result.files_queued} files, "
|
||||
f"{routing_result.prefetch_registered} prefetch, "
|
||||
f"{routing_result.skipped} skipped"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
logger.info("[DRY RUN] Would create/update pages and entities")
|
||||
logger.info("[DRY RUN] Would route results to destinations")
|
||||
return ConsolidationResult(
|
||||
search_id=search_id,
|
||||
query=query,
|
||||
pages_created=len(pages_to_create),
|
||||
pages_updated=len(pages_to_update),
|
||||
entities_added=len(new_entities)
|
||||
pages_created=routing_result.wiki_routed,
|
||||
volatile_cached=routing_result.volatile_cached,
|
||||
files_queued=routing_result.files_queued,
|
||||
prefetch_registered=routing_result.prefetch_registered,
|
||||
)
|
||||
|
||||
# Create/update wiki pages
|
||||
# Process each classification
|
||||
pages_created = 0
|
||||
pages_updated = 0
|
||||
entities_added = 0
|
||||
volatile_cached = 0
|
||||
files_queued = 0
|
||||
prefetch_registered = 0
|
||||
|
||||
# Create new pages
|
||||
for page_data in pages_to_create:
|
||||
try:
|
||||
await self._create_or_consolidate_page(
|
||||
user=user,
|
||||
title=page_data.get('title'),
|
||||
path=page_data.get('path'),
|
||||
summary=page_data.get('summary'),
|
||||
source_query=query,
|
||||
web_results=web_results
|
||||
)
|
||||
pages_created += 1
|
||||
logger.info(f"Created page: {page_data.get('title')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create page {page_data.get('title')}: {e}")
|
||||
# Create URL-to-web_result lookup
|
||||
url_to_result = {r['url']: r for r in web_results}
|
||||
|
||||
# Update existing pages
|
||||
for page_data in pages_to_update:
|
||||
try:
|
||||
await self._update_page_with_facts(
|
||||
title=page_data.get('title'),
|
||||
new_facts=page_data.get('new_facts', []),
|
||||
source_url=page_data.get('source_url'),
|
||||
user=user
|
||||
)
|
||||
pages_updated += 1
|
||||
logger.info(f"Updated page: {page_data.get('title')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update page {page_data.get('title')}: {e}")
|
||||
for classification in routing_result.classifications:
|
||||
web_result = url_to_result.get(classification.url, {})
|
||||
|
||||
# Add new entities to graph
|
||||
for entity_data in new_entities:
|
||||
try:
|
||||
await self._add_entity_to_graph(
|
||||
user=user,
|
||||
entity_name=entity_data.get('name'),
|
||||
entity_type=entity_data.get('type'),
|
||||
description=entity_data.get('description'),
|
||||
source_search_id=search_id
|
||||
)
|
||||
entities_added += 1
|
||||
logger.info(f"Added entity: {entity_data.get('name')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add entity {entity_data.get('name')}: {e}")
|
||||
if classification.route_type == 'wiki':
|
||||
# Route to wiki page creation/update
|
||||
try:
|
||||
if classification.wiki_action == 'create':
|
||||
await self._create_or_consolidate_page(
|
||||
user=user,
|
||||
title=classification.title,
|
||||
path=classification.wiki_path or f"reference/{classification.title.lower().replace(' ', '-')}",
|
||||
summary=classification.wiki_summary or '',
|
||||
source_query=query,
|
||||
web_results=[web_result] if web_result else web_results[:3]
|
||||
)
|
||||
pages_created += 1
|
||||
logger.info(f"Created wiki page: {classification.title}")
|
||||
elif classification.wiki_action == 'update':
|
||||
await self._update_page_with_facts(
|
||||
title=classification.title,
|
||||
new_facts=[classification.wiki_summary] if classification.wiki_summary else [],
|
||||
source_url=classification.url,
|
||||
user=user
|
||||
)
|
||||
pages_updated += 1
|
||||
logger.info(f"Updated wiki page: {classification.title}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed wiki routing for {classification.title}: {e}")
|
||||
|
||||
elif classification.route_type == 'volatile':
|
||||
# Route to volatile cache
|
||||
if await self._route_to_volatile(classification, web_result, user):
|
||||
volatile_cached += 1
|
||||
|
||||
elif classification.route_type == 'file':
|
||||
# Route to Paperless queue
|
||||
if await self._route_to_files(classification, web_result, user):
|
||||
files_queued += 1
|
||||
|
||||
elif classification.route_type == 'prefetch':
|
||||
# Register prefetch pattern
|
||||
if await self._register_prefetch(classification, web_result, user):
|
||||
prefetch_registered += 1
|
||||
|
||||
# 'skip' route type - do nothing
|
||||
|
||||
return ConsolidationResult(
|
||||
search_id=search_id,
|
||||
query=query,
|
||||
pages_created=pages_created,
|
||||
pages_updated=pages_updated,
|
||||
entities_added=entities_added
|
||||
entities_added=entities_added,
|
||||
volatile_cached=volatile_cached,
|
||||
files_queued=files_queued,
|
||||
prefetch_registered=prefetch_registered,
|
||||
)
|
||||
|
||||
async def _get_web_results(self, search_id: str) -> List[Dict[str, Any]]:
|
||||
@@ -937,3 +969,305 @@ JSON:"""
|
||||
logger.debug(f"Added entity to graph: {entity_name} ({entity_type})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add entity to graph: {e}")
|
||||
|
||||
async def _classify_web_results_unified(
|
||||
self,
|
||||
query: str,
|
||||
web_results: List[Dict[str, Any]],
|
||||
keywords: List[str],
|
||||
user: str = "jpmschweitzer"
|
||||
) -> MemoryRoutingResult:
|
||||
"""
|
||||
Unified classification of web results for memory routing.
|
||||
|
||||
Each web result is classified into exactly one destination:
|
||||
- wiki: Stable reference content → wiki page creation/update
|
||||
- volatile: Ephemeral data (weather, news, prices) → volatile cache
|
||||
- file: Downloadable file (PDF, doc, xls, images) → Paperless
|
||||
- prefetch: Regularly updated source → scheduler registration
|
||||
- skip: Low value, ads, errors → discard
|
||||
|
||||
Returns:
|
||||
MemoryRoutingResult with classifications for each web result
|
||||
"""
|
||||
# Fetch existing taxonomy structure for wiki path suggestions
|
||||
try:
|
||||
taxonomy_structure = await self.wiki.get_taxonomy_structure(f"users/{user}")
|
||||
existing_paths_info = self._format_taxonomy_for_prompt(taxonomy_structure)
|
||||
logger.info(f"Fetched taxonomy with {len(taxonomy_structure)} categories for user {user}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch taxonomy structure: {e}")
|
||||
existing_paths_info = ""
|
||||
|
||||
# Build classification prompt
|
||||
web_summary = "\n\n".join([
|
||||
f"[{i+1}] Title: {r['title']}\n URL: {r['url']}\n Content: {r['content'][:400]}..."
|
||||
for i, r in enumerate(web_results[:10])
|
||||
])
|
||||
|
||||
prompt = f"""You are a Memory Router for a personal knowledge system. Classify each web result into ONE destination.
|
||||
|
||||
Query: "{query}"
|
||||
Keywords: {', '.join(keywords) if keywords else 'none'}
|
||||
|
||||
Web Results:
|
||||
{web_summary}
|
||||
|
||||
CLASSIFICATION RULES:
|
||||
|
||||
**wiki** - Stable reference content worth documenting permanently:
|
||||
- Factual information about people, places, companies, products
|
||||
- How-to guides, tutorials, technical documentation
|
||||
- Historical facts, biographies, definitions
|
||||
- Content that won't change frequently
|
||||
|
||||
**volatile** - Ephemeral data that changes frequently:
|
||||
- Current weather conditions or forecasts
|
||||
- Latest news headlines or breaking news
|
||||
- Stock prices, exchange rates, crypto prices
|
||||
- Sports scores, live results
|
||||
- Traffic conditions, transit delays
|
||||
- Social media trends, notifications
|
||||
Use namespaces: weather, news, financial, transit, traffic, sports, social, system
|
||||
|
||||
**file** - Downloadable documents:
|
||||
- PDF files (URLs ending in .pdf or containing /pdf/)
|
||||
- Office documents (.doc, .docx, .xls, .xlsx, .ppt)
|
||||
- Images (.jpg, .png, .gif when they're primary content)
|
||||
- CSV/data files
|
||||
- Any direct download link
|
||||
|
||||
**prefetch** - Sources worth checking regularly:
|
||||
- News feeds or RSS sources
|
||||
- API endpoints with live data
|
||||
- Dashboards or status pages
|
||||
- Only if not already captured by volatile
|
||||
|
||||
**skip** - Low value content:
|
||||
- Ads, paywalled content
|
||||
- Error pages, 404s
|
||||
- Duplicate or redundant results
|
||||
- Content not answering the query
|
||||
|
||||
{existing_paths_info}
|
||||
|
||||
Return ONLY valid JSON array:
|
||||
[
|
||||
{{
|
||||
"url": "...",
|
||||
"title": "...",
|
||||
"route_type": "wiki|volatile|file|prefetch|skip",
|
||||
"wiki_action": "create|update",
|
||||
"wiki_path": "category/subcategory/page-name",
|
||||
"wiki_summary": "What to document",
|
||||
"volatile_namespace": "weather|news|financial|...",
|
||||
"volatile_key": "cache-key",
|
||||
"volatile_ttl_hours": 1,
|
||||
"prefetch_cron": "0 * * * *",
|
||||
"prefetch_endpoint": "/volatile/fetch/...",
|
||||
"confidence": 0.9,
|
||||
"reason": "Why this classification"
|
||||
}}
|
||||
]
|
||||
|
||||
Only include fields relevant to the route_type. Set irrelevant fields to null.
|
||||
|
||||
JSON:"""
|
||||
|
||||
try:
|
||||
response = await self.ollama.generate_text(
|
||||
prompt=prompt,
|
||||
model=self.settings.ollama_model,
|
||||
stream=False,
|
||||
temperature=0.0
|
||||
)
|
||||
|
||||
if not response:
|
||||
logger.warning("Empty response from Ollama for classification")
|
||||
return MemoryRoutingResult()
|
||||
|
||||
# Extract JSON array from response
|
||||
response_clean = response.strip()
|
||||
if '[' in response_clean:
|
||||
json_start = response_clean.find('[')
|
||||
json_end = response_clean.rfind(']') + 1
|
||||
response_clean = response_clean[json_start:json_end]
|
||||
|
||||
classifications_raw = json.loads(response_clean)
|
||||
|
||||
# Parse into MemoryRouteClassification objects
|
||||
result = MemoryRoutingResult()
|
||||
for item in classifications_raw:
|
||||
try:
|
||||
classification = MemoryRouteClassification(
|
||||
url=item.get('url', ''),
|
||||
title=item.get('title', ''),
|
||||
route_type=item.get('route_type', 'skip'),
|
||||
wiki_action=item.get('wiki_action'),
|
||||
wiki_path=item.get('wiki_path'),
|
||||
wiki_summary=item.get('wiki_summary'),
|
||||
volatile_namespace=item.get('volatile_namespace'),
|
||||
volatile_key=item.get('volatile_key'),
|
||||
volatile_ttl_hours=item.get('volatile_ttl_hours'),
|
||||
prefetch_cron=item.get('prefetch_cron'),
|
||||
prefetch_endpoint=item.get('prefetch_endpoint'),
|
||||
confidence=item.get('confidence', 0.5),
|
||||
reason=item.get('reason', ''),
|
||||
)
|
||||
result.classifications.append(classification)
|
||||
|
||||
# Count by route type
|
||||
if classification.route_type == 'wiki':
|
||||
result.wiki_routed += 1
|
||||
elif classification.route_type == 'volatile':
|
||||
result.volatile_cached += 1
|
||||
elif classification.route_type == 'file':
|
||||
result.files_queued += 1
|
||||
elif classification.route_type == 'prefetch':
|
||||
result.prefetch_registered += 1
|
||||
else:
|
||||
result.skipped += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse classification item: {e}")
|
||||
|
||||
logger.info(
|
||||
f"Classification complete: {result.wiki_routed} wiki, "
|
||||
f"{result.volatile_cached} volatile, {result.files_queued} files, "
|
||||
f"{result.prefetch_registered} prefetch, {result.skipped} skipped"
|
||||
)
|
||||
return result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse classification response as JSON: {e}")
|
||||
return MemoryRoutingResult()
|
||||
except Exception as e:
|
||||
logger.error(f"Classification failed: {e}", exc_info=True)
|
||||
return MemoryRoutingResult()
|
||||
|
||||
async def _route_to_volatile(
|
||||
self,
|
||||
classification: MemoryRouteClassification,
|
||||
web_result: Dict[str, Any],
|
||||
user: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Route a web result to volatile cache.
|
||||
|
||||
Args:
|
||||
classification: The classification with volatile routing info
|
||||
web_result: The original web result data
|
||||
user: User identifier
|
||||
|
||||
Returns:
|
||||
True if successfully cached, False otherwise
|
||||
"""
|
||||
if not self.volatile_service:
|
||||
logger.warning("Volatile service not configured, skipping volatile routing")
|
||||
return False
|
||||
|
||||
namespace = classification.volatile_namespace or "custom"
|
||||
key = classification.volatile_key or web_result['url'].split('/')[-1]
|
||||
ttl = (classification.volatile_ttl_hours or 1) * 3600 # Convert hours to seconds
|
||||
|
||||
try:
|
||||
# Store the web result content in volatile cache
|
||||
data = {
|
||||
"title": web_result.get('title', ''),
|
||||
"content": web_result.get('content', ''),
|
||||
"url": web_result.get('url', ''),
|
||||
"text": f"{web_result.get('title', '')}: {web_result.get('content', '')[:500]}",
|
||||
}
|
||||
|
||||
await self.volatile_service.store(
|
||||
user=user,
|
||||
namespace=namespace,
|
||||
key=key,
|
||||
data=data,
|
||||
source=web_result.get('url', 'web_search'),
|
||||
ttl=ttl,
|
||||
)
|
||||
|
||||
logger.info(f"Cached to volatile: {namespace}/{key} (ttl={ttl}s)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cache to volatile: {e}")
|
||||
return False
|
||||
|
||||
async def _route_to_files(
|
||||
self,
|
||||
classification: MemoryRouteClassification,
|
||||
web_result: Dict[str, Any],
|
||||
user: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Queue a file for Paperless ingestion.
|
||||
|
||||
Args:
|
||||
classification: The classification with file info
|
||||
web_result: The original web result data
|
||||
user: User identifier
|
||||
|
||||
Returns:
|
||||
True if successfully queued, False otherwise
|
||||
"""
|
||||
# For now, log the file for manual review or future Paperless integration
|
||||
url = web_result.get('url', '')
|
||||
title = web_result.get('title', '')
|
||||
|
||||
logger.info(f"File detected for Paperless: {title} ({url})")
|
||||
|
||||
# TODO: Implement actual Paperless file upload
|
||||
# This would involve:
|
||||
# 1. Download the file
|
||||
# 2. Upload to Paperless via API
|
||||
# 3. Add tags based on classification
|
||||
|
||||
return True # Placeholder - count as queued
|
||||
|
||||
async def _register_prefetch(
|
||||
self,
|
||||
classification: MemoryRouteClassification,
|
||||
web_result: Dict[str, Any],
|
||||
user: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Register a prefetch pattern with the scheduler.
|
||||
|
||||
Args:
|
||||
classification: The classification with prefetch info
|
||||
web_result: The original web result data
|
||||
user: User identifier
|
||||
|
||||
Returns:
|
||||
True if successfully registered, False otherwise
|
||||
"""
|
||||
if not self.settings_client:
|
||||
logger.warning("Settings client not configured, skipping prefetch registration")
|
||||
return False
|
||||
|
||||
cron = classification.prefetch_cron or "0 * * * *" # Default: hourly
|
||||
endpoint = classification.prefetch_endpoint or ""
|
||||
|
||||
if not endpoint:
|
||||
logger.warning(f"No prefetch endpoint specified for {web_result.get('url')}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Store prefetch configuration in settings
|
||||
prefetch_key = f"prefetch.{user}.{classification.volatile_key or 'auto'}"
|
||||
prefetch_config = {
|
||||
"cron": cron,
|
||||
"endpoint": endpoint,
|
||||
"source_url": web_result.get('url', ''),
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
await self.settings_client.set(prefetch_key, prefetch_config, user=user)
|
||||
logger.info(f"Registered prefetch: {prefetch_key} ({cron})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register prefetch: {e}")
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user