feat: security(CORS/API key), OpenAI-compatible endpoint, Paperless hook indexing; .env support

This commit is contained in:
hyungi
2025-08-13 07:36:27 +09:00
parent 325dde803d
commit d17ec57b2e
6 changed files with 164 additions and 6 deletions

View File

@@ -1,15 +1,31 @@
from __future__ import annotations
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Dict, Any
from .config import settings
from .ollama_client import OllamaClient
from .index_store import JsonlIndex
from .security import require_api_key
from .paperless_client import PaperlessClient
from .utils import chunk_text
app = FastAPI(title="Local AI Server", version="0.1.0")
app = FastAPI(title="Local AI Server", version="0.2.1")
# CORS
import os
cors_origins = os.getenv("CORS_ORIGINS", "*")
origins = [o.strip() for o in cors_origins.split(",") if o.strip()] or ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
ollama = OllamaClient(settings.ollama_host)
index = JsonlIndex(settings.index_path)
@@ -137,8 +153,48 @@ class PaperlessHook(BaseModel):
@app.post("/paperless/hook")
def paperless_hook(hook: PaperlessHook) -> Dict[str, Any]:
# NOTE: 확장 지점 - paperless API를 조회하여 문서 텍스트/메타데이터를 받아
# scripts/embed_ollama.py와 동일 로직으로 인덱스를 업데이트할 수 있습니다.
return {"status": "ack", "document_id": hook.document_id}
def paperless_hook(hook: PaperlessHook, _: None = Depends(require_api_key)) -> Dict[str, Any]:
# Fetch text from Paperless and upsert into index
client = PaperlessClient(settings.paperless_base_url, settings.paperless_token)
text = client.get_document_text(hook.document_id)
parts = chunk_text(text)
model = settings.embedding_model
from .index_store import IndexRow
to_append = []
for i, t in enumerate(parts):
vec = ollama.embeddings(model, t)
to_append.append(IndexRow(id=f"paperless:{hook.document_id}:{i}", text=t, vector=vec, source="paperless"))
added = index.append(to_append)
return {"status": "indexed", "document_id": hook.document_id, "chunks": added}
# OpenAI-compatible chat completions (minimal)
class ChatCompletionsRequest(BaseModel):
model: str | None = None
messages: List[Dict[str, str]]
temperature: float | None = None
max_tokens: int | None = None
@app.post("/v1/chat/completions")
def chat_completions(req: ChatCompletionsRequest, _: None = Depends(require_api_key)) -> Dict[str, Any]:
chosen = req.model or settings.base_model
opts: Dict[str, Any] = {}
if req.temperature is not None:
opts["temperature"] = req.temperature
# Note: Ollama ignores max_tokens field; left here for interface similarity
resp = ollama.chat(chosen, req.messages, stream=False, options=opts)
# Minimal OpenAI-like response shape
return {
"id": "chatcmpl-local",
"object": "chat.completion",
"model": chosen,
"choices": [
{
"index": 0,
"message": resp.get("message", {"role": "assistant", "content": resp.get("response", "")}),
"finish_reason": resp.get("done_reason", "stop"),
}
],
}