"""cAIc routers - /api/ingest terminal command RAG hook.""" import hashlib import hmac import logging import uuid from datetime import datetime, timezone import httpx from fastapi import APIRouter, HTTPException, Request from fastapi.responses import JSONResponse from config import COMPLETIONS_API_KEY from crypto import encrypt_text from eviction import maybe_evict from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION log = logging.getLogger("caic") router = APIRouter() def _check_api_key(request: Request): auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing Bearer token") token = auth[7:].strip() if not hmac.compare_digest(token.encode(), COMPLETIONS_API_KEY.encode()): raise HTTPException(status_code=401, detail="Invalid API key") @router.post("/api/ingest") async def ingest_content(request: Request): _check_api_key(request) body = await request.json() content = (body.get("content") or "").strip() if not content: raise HTTPException(status_code=422, detail="content is required") source = str(body.get("source", "external")).strip() or "external" metadata = body.get("metadata") or {} chunks = chunk_text(content) if not chunks: raise HTTPException(status_code=422, detail="content produced no chunks") ingested = 0 async with httpx.AsyncClient() as client: for i, chunk in enumerate(chunks): embed_resp = await client.post( f"{EMBED_URL}/api/embeddings", json={"model": EMBED_MODEL, "prompt": chunk}, timeout=30.0, ) if embed_resp.status_code != 200: log.warning(f"Ingest embedding failed for chunk {i}: {embed_resp.status_code}") continue vector = embed_resp.json()["embedding"] chunk_hash = hashlib.md5(chunk.encode("utf-8")).hexdigest()[:12] point_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"ingest-{source}-{chunk_hash}-{i}")) payload = {"text": encrypt_text(chunk), "source": source, "ingest_date": datetime.now(timezone.utc).isoformat(), "type": "ingest"} payload.update(metadata) upsert_resp = await client.put( f"{QDRANT_URL}/collections/{RAG_COLLECTION}/points?wait=true", json={"points": [{"id": point_id, "vector": vector, "payload": payload}]}, timeout=30.0, ) if upsert_resp.status_code in (200, 201): ingested += 1 else: log.warning(f"Ingest Qdrant upsert failed for chunk {i}: {upsert_resp.status_code}") if ingested > 0: evicted = await maybe_evict() if evicted: log.info(f"Evicted {evicted} vectors after ingest") return {"chunks_ingested": ingested, "source": source, "message": f"Ingested {ingested} chunks from {source}"}