"""API routes. Define your API endpoints here. All routes are prefixed with /api. Built-in AI endpoints (via Druppie SDK): POST /api/ai/chat — LLM chat completion (body: {prompt, system?}) [module-llm] POST /api/ai/ocr — OCR text extraction (body: {image_url}) [module-vision] POST /api/ai/search — Web search (body: {query}) [module-web] RAG endpoints (vectors stored in THIS app's own database): POST /api/rag/index — embed + store documents (body: {documents: [...]}) POST /api/rag/search — semantic similarity search (body: {query}) Example adding your own: @api.route('/items', methods=['GET']) def list_items(): db = next(get_db()) items = db.query(Item).all() return jsonify([{'id': str(i.id), 'name': i.name} for i in items]) """ from flask import Blueprint, jsonify, request from druppie_sdk import DruppieClient from app.database import get_db from app.rag import RAG api = Blueprint("api", __name__) druppie = DruppieClient() RAG_INDEX = "knowledge-base" @api.route("/info") def info(): from app.config import settings return jsonify(app_name=settings.app_name) # --------------------------------------------------------------------------- # AI endpoints — via Druppie SDK (calls module-llm and module-vision) # --------------------------------------------------------------------------- @api.route("/ai/chat", methods=["POST"]) def ai_chat_endpoint(): """LLM chat completion. Body: {"prompt": "...", "system": "..."}""" data = request.get_json(silent=True) if not data or "prompt" not in data: return jsonify(error="Missing required field: prompt"), 400 result = druppie.call("llm", "chat", { "prompt": data["prompt"], "system": data.get("system", "You are a helpful assistant."), }) return jsonify(answer=result.get("answer", "")) @api.route("/ai/ocr", methods=["POST"]) def ai_ocr_endpoint(): """OCR text extraction. Body: {"image_url": "https://..."}""" data = request.get_json(silent=True) if not data or "image_url" not in data: return jsonify(error="Missing required field: image_url"), 400 result = druppie.call("vision", "ocr", {"image_source": data["image_url"]}) return jsonify(text=result.get("text", "")) @api.route("/ai/search", methods=["POST"]) def ai_search_endpoint(): """Web search. Body: {"query": "search terms"}""" data = request.get_json(silent=True) if not data or "query" not in data: return jsonify(error="Missing required field: query"), 400 result = druppie.call("web", "search_web", {"query": data["query"]}) return jsonify(result) # --------------------------------------------------------------------------- # RAG endpoints — worked example of the embed → store → search loop # # Vectors live in THIS app's own Postgres (pgvector); embeddings are # generated by the stateless module-llm `embed` tool via the SDK. There is # no shared vectorstore — each app owns its own vectors. The `RAG` helper # (app/rag.py) handles chunking, the embed call, storage, and search. # --------------------------------------------------------------------------- @api.route("/rag/index", methods=["POST"]) def rag_index_endpoint(): """Embed and store documents in the app's own database. Body: {"documents": [{"content": "...", "source_name": "...", "source_page": 1}, ...]} For each document the RAG helper chunks the text, calls module-llm `embed` to turn each chunk into a vector, and stores the chunk + vector in this app's `vector_chunks` table (pgvector). """ data = request.get_json(silent=True) if not data or not data.get("documents"): return jsonify(error="Missing required field: documents"), 400 db = next(get_db()) rag = RAG(db, druppie) rag.create_index(RAG_INDEX) result = rag.index_documents(RAG_INDEX, data["documents"]) return jsonify(result) @api.route("/rag/search", methods=["POST"]) def rag_search_endpoint(): """Semantic similarity search over the stored documents. Body: {"query": "natural-language question", "top_k": 5} The query is embedded with the same module-llm `embed` tool, then matched against the stored chunks with pgvector's cosine distance (`embedding <=> :qvec`). Returns the top-k chunks with their source metadata so the caller can build a cited answer. """ data = request.get_json(silent=True) if not data or "query" not in data: return jsonify(error="Missing required field: query"), 400 db = next(get_db()) rag = RAG(db, druppie) results = rag.search(RAG_INDEX, data["query"], top_k=data.get("top_k", 5)) return jsonify(results=results)