""" app/routers/ingest.py API-Endpunkte für WP-11 (Discovery & Persistence). """ import os import time import logging from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import Optional, Dict, Any from app.core.ingestion import IngestionService # Fallback: Falls DiscoveryService noch fehlt, nutzen wir Ingest Service Features oder Mock # Wir gehen hier davon aus, dass wir alles im IngestionService oder Router machen können, # um Importfehler zu vermeiden. from app.core.retriever import Retriever from app.models.dto import QueryRequest logger = logging.getLogger(__name__) router = APIRouter() # --- DTOs --- class AnalyzeRequest(BaseModel): text: str type: str = "concept" class SaveRequest(BaseModel): markdown_content: str filename: Optional[str] = None folder: str = "00_Inbox" class SaveResponse(BaseModel): status: str file_path: str note_id: str stats: Dict[str, Any] # --- Endpoints --- @router.post("/analyze") async def analyze_draft(req: AnalyzeRequest): """ WP-11 Intelligence: Liefert Link-Vorschläge. Implementiert direkt hier, um Abhängigkeiten zu reduzieren. """ try: retriever = Retriever() suggestions = [] query_text = req.text[:400] if not query_text.strip(): return {"suggestions": []} # 1. Semantic Search # Safe async call check if hasattr(retriever.search, '__await__'): hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid")) else: hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid")) seen_titles = set() for hit in hits_result.results: # Titel ermitteln title = hit.payload.get("note_id") or hit.node_id if not title or title in seen_titles: continue seen_titles.add(title) edge_kind = "related_to" if req.type == "project": edge_kind = "depends_on" if req.type == "decision": edge_kind = "references" # Score Threshold if hit.total_score > 0.4: # Etwas toleranter suggestions.append({ "target_title": title, "target_id": hit.node_id, "suggested_markdown": f"[[rel:{edge_kind} {title}]]", "reason": f"Semantisch ähnlich ({hit.total_score:.2f})", "type": "semantic" }) return {"suggestions": suggestions} except Exception as e: logger.error(f"Analyze failed: {e}", exc_info=True) # Kein 500er werfen, lieber leere Liste, damit UI nicht crasht return {"suggestions": [], "error": str(e)} @router.post("/save", response_model=SaveResponse) async def save_note(req: SaveRequest): """ WP-11 Persistence: Speichert Markdown physisch und indiziert es sofort. """ try: vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault") abs_vault_root = os.path.abspath(vault_root) if not os.path.exists(abs_vault_root): os.makedirs(abs_vault_root, exist_ok=True) final_filename = req.filename if not final_filename: final_filename = f"draft_{int(time.time())}.md" ingest_service = IngestionService() logger.info(f"Saving {final_filename} to {req.folder}") # --- AWAIT WICHTIG! --- # Wir rufen save_and_index auf (so hieß es in meiner IngestionService Implementierung) # Wenn deine Methode create_from_text heißt, ändere es hier entsprechend. # Ich nutze hier save_and_index als Standard aus WP-11. if hasattr(ingest_service, 'save_and_index'): result = await ingest_service.save_and_index(req.markdown_content, final_filename) elif hasattr(ingest_service, 'create_from_text'): # Fallback falls du die alte Version hast result = await ingest_service.create_from_text(req.markdown_content, final_filename, abs_vault_root, req.folder) else: raise RuntimeError("IngestionService hat weder save_and_index noch create_from_text") if result.get("status") == "error": raise HTTPException(status_code=500, detail=result.get("error")) return SaveResponse( status="success", file_path=result.get("file_path") or result.get("path", "unknown"), note_id=result.get("note_id", "unknown"), stats=result.get("stats", {}) ) except HTTPException as he: raise he except Exception as e: logger.error(f"Save failed: {e}", exc_info=True) raise HTTPException(status_code=500, detail=f"Save failed: {str(e)}")