132 lines
4.7 KiB
Python
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)
|