snake-game-27febef5/app/routes.py

132 lines
4.7 KiB
Python

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