| """ |
| Knowledge Summary Service |
| |
| Provides lightweight summary data for knowledge items to minimize data transfer. |
| Optimized for frequent polling and card displays. |
| """ |
|
|
| from typing import Any |
|
|
| from ...config.logfire_config import safe_logfire_error, safe_logfire_info |
| from .knowledge_repository import KnowledgeRepository |
|
|
|
|
| class KnowledgeSummaryService: |
| """ |
| Service for providing lightweight knowledge item summaries. |
| Designed for efficient polling with minimal data transfer. |
| """ |
|
|
| def __init__(self, supabase_client): |
| """ |
| Initialize the knowledge summary service. |
| |
| Args: |
| supabase_client: The Supabase client for database operations |
| """ |
| self.supabase = supabase_client |
| self.repository = KnowledgeRepository(supabase_client) |
|
|
| async def get_summaries( |
| self, |
| page: int = 1, |
| per_page: int = 20, |
| knowledge_type: str | None = None, |
| search: str | None = None, |
| ) -> dict[str, Any]: |
| """ |
| Get lightweight summaries of knowledge items. |
| |
| Returns only essential data needed for card displays: |
| - Basic metadata (title, url, type, tags) |
| - Counts only (no actual content) |
| - Minimal processing overhead |
| |
| Args: |
| page: Page number (1-based) |
| per_page: Items per page |
| knowledge_type: Optional filter by knowledge type |
| search: Optional search term |
| |
| Returns: |
| Dict with minimal item summaries and pagination info |
| """ |
| try: |
| safe_logfire_info(f"Fetching knowledge summaries | page={page} | per_page={per_page}") |
|
|
| |
| query = self.supabase.from_("archon_sources").select( |
| "source_id, title, summary, metadata, source_url, created_at, updated_at" |
| ) |
|
|
| |
| if knowledge_type: |
| query = query.contains("metadata", {"knowledge_type": knowledge_type}) |
|
|
| if search: |
| search_pattern = f"%{search}%" |
| query = query.or_(f"title.ilike.{search_pattern},summary.ilike.{search_pattern}") |
|
|
| |
| count_query = self.supabase.from_("archon_sources").select("*", count="exact", head=True) |
|
|
| if knowledge_type: |
| count_query = count_query.contains("metadata", {"knowledge_type": knowledge_type}) |
|
|
| if search: |
| search_pattern = f"%{search}%" |
| count_query = count_query.or_(f"title.ilike.{search_pattern},summary.ilike.{search_pattern}") |
|
|
| count_result = count_query.execute() |
| total = count_result.count if hasattr(count_result, "count") else 0 |
|
|
| |
| start_idx = (page - 1) * per_page |
| query = query.range(start_idx, start_idx + per_page - 1) |
| query = query.order("updated_at", desc=True) |
|
|
| |
| result = query.execute() |
| sources = result.data if result.data else [] |
|
|
| |
| source_ids = [s["source_id"] for s in sources] |
|
|
| |
| summaries = [] |
|
|
| if source_ids: |
| |
| doc_counts = await self.repository.get_document_counts_batch(source_ids) |
|
|
| |
| code_counts = await self.repository.get_code_example_counts_batch(source_ids) |
|
|
| |
| first_urls = await self.repository.get_first_urls_batch(source_ids) |
|
|
| |
| for source in sources: |
| source_id = source["source_id"] |
| metadata = source.get("metadata", {}) |
|
|
| |
| |
| source_url = source.get("source_url") |
| if source_url: |
| first_url = source_url |
| else: |
| first_url = first_urls.get(source_id, f"source://{source_id}") |
|
|
| source_type = metadata.get("source_type", "file" if first_url.startswith("file://") else "url") |
|
|
| |
| |
| knowledge_type_val = metadata.get("knowledge_type") |
| if not knowledge_type_val: |
| |
| |
| safe_logfire_info( |
| f"Knowledge type not found in metadata for {source_id}, defaulting to technical" |
| ) |
| knowledge_type_val = "technical" |
|
|
| summary = { |
| "source_id": source_id, |
| "title": source.get("title", source.get("summary", "Untitled")), |
| "url": first_url, |
| "status": "active", |
| "document_count": doc_counts.get(source_id, 0), |
| "code_examples_count": code_counts.get(source_id, 0), |
| "knowledge_type": knowledge_type_val, |
| "source_type": source_type, |
| "created_at": source.get("created_at"), |
| "updated_at": source.get("updated_at"), |
| "metadata": metadata, |
| } |
| summaries.append(summary) |
|
|
| safe_logfire_info(f"Knowledge summaries fetched | count={len(summaries)} | total={total}") |
|
|
| return { |
| "items": summaries, |
| "total": total, |
| "page": page, |
| "per_page": per_page, |
| "pages": (total + per_page - 1) // per_page if per_page > 0 else 0, |
| } |
|
|
| except Exception as e: |
| safe_logfire_error(f"Failed to get knowledge summaries | error={str(e)}") |
| raise |
|
|
| async def get_item_chunks( |
| self, source_id: str, page: int = 1, per_page: int = 50, domain_filter: str | None = None |
| ) -> tuple[bool, dict[str, Any]]: |
| """ |
| Get document chunks for a specific knowledge item with pagination and optional domain filtering. |
| """ |
| safe_logfire_info( |
| f"Fetching chunks via repository for source_id: {source_id}, page={page}, per_page={per_page}, domain_filter={domain_filter}" |
| ) |
| return await self.repository.get_item_chunks(source_id, page, per_page, domain_filter) |
|
|