mindnet/app/routers/ingest.py
2025-12-11 10:46:37 +01:00

140 lines
4.5 KiB
Python

"""
app/routers/ingest.py
API-Endpunkte für WP-11 (Discovery & Persistence).
Fixed Async/Await Issues.
"""
import os
import time
import logging
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
from app.core.ingestion import IngestionService
from app.core.retriever import Retriever
from app.models.dto import QueryRequest
# Logger Konfiguration
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 via Retriever.
"""
try:
# Wir nutzen den Retriever direkt, statt eines extra DiscoveryServices
retriever = Retriever()
suggestions = []
# 1. Suche nach ähnlichen Inhalten (Semantic)
query_text = req.text[:400]
if not query_text.strip():
return {"suggestions": []}
# Check ob Retriever async ist (in v2.3 oft ja)
if hasattr(retriever.search, '__await__'):
hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid"))
else:
# Fallback sync
hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid"))
seen_titles = set()
for hit in hits_result.results:
title = hit.payload.get("note_id") or hit.node_id
if not title or title in seen_titles: continue
seen_titles.add(title)
# Simple Edge Logic
edge_kind = "related_to"
if req.type == "project": edge_kind = "depends_on"
if req.type == "decision": edge_kind = "references"
if hit.total_score > 0.65:
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)
raise HTTPException(status_code=500, detail=f"Analysis failed: {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:
# 1. Pfad-Setup
vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault")
abs_vault_root = os.path.abspath(vault_root)
if not os.path.exists(abs_vault_root):
# Versuche ihn zu erstellen, falls er fehlt
os.makedirs(abs_vault_root, exist_ok=True)
# 2. Filename
final_filename = req.filename
if not final_filename:
final_filename = f"draft_{int(time.time())}.md"
# 3. Ingestion Service
ingest_service = IngestionService()
logger.info(f"Saving {final_filename} to {req.folder}")
# --- DER FIX: AWAIT HINZUFÜGEN ---
# Da ingestion.py auf async umgestellt wurde, MÜSSEN wir hier warten.
result = await ingest_service.create_from_text(
markdown_content=req.markdown_content,
filename=final_filename,
vault_root=abs_vault_root,
folder=req.folder
)
# Fehlerprüfung auf dem Dictionary
if result.get("status") == "error":
raise HTTPException(status_code=500, detail=result.get("error"))
return SaveResponse(
status="success",
file_path=result.get("path", "unknown"),
note_id=result.get("note_id", "unknown"),
stats={
"chunks": result.get("chunks_count", 0),
"edges": result.get("edges_count", 0)
}
)
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)}")