mindnet/app/routers/ingest.py
2025-12-11 12:04:08 +01:00

138 lines
4.8 KiB
Python

"""
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)}")