feat(agent): add test frontend and Docker stack

This commit is contained in:
2026-09-16 21:42:05 +02:00
parent c8b55fbd38
commit 4f5107a46a
16 changed files with 1065 additions and 71 deletions
+27 -2
View File
@@ -43,14 +43,39 @@ python -m agent.cli eval
python -m agent.cli eval --answers --json-out data/eval-report.json
# 4) HTTP-API + Test-Chat
python -m agent.cli serve # http://127.0.0.1:8080 (/v1/ask, /v1/health, /v1/reindex)
python -m agent.cli serve # http://127.0.0.1:8080/ (UI + v1-API)
# Zielhost im Tailscale-Netz (PV_API_KEY muss gesetzt sein)
python -m agent.cli serve --host 100.103.83.12
# UI: http://100.103.83.12:8080/
```
Das dependency-freie Frontend unter `/` zeigt Health, Antwortstatus, Quellen,
Konflikte und Rückfragen. Der Service-Key wird nur im Browser eingegeben und
optional ausschließlich für den aktuellen Tab gespeichert.
Der stabile v1-Vertrag, Bearer-Authentisierung, Fehlersemantik und die
Datenschutzgrenze für die spätere Odoo-Anbindung sind in `docs/API.md`
dokumentiert. v1 ist zustandslos und akzeptiert keine Mandanten-, Mitarbeiter-
oder Abrechnungsobjekte.
## Docker Compose auf `100.103.83.12`
Der Stack in `compose.yaml` verbindet den Agenten mit dem bereits vorhandenen
externen Docker-Netz `ollama-default`; Ollama wird nicht dupliziert. Setup und
Smoke-Tests: `docs/DOCKER.md`.
```bash
cp .env.example .env # API-Key und Ollama-DNS-Alias setzen
docker compose build
docker compose up -d
```
UI: `http://100.103.83.12:8080/`. `data/` wird schreibbar und
`wissensbasis/` read-only eingebunden; weder Index noch lokale Quellkorpora
landen im Image.
## Konfiguration (Umgebungsvariablen)
| Variable | Default | Bedeutung |
@@ -216,7 +241,7 @@ agent/
api.py FastAPI v1 (/v1/ask, /v1/health, /v1/reindex), Auth + Request-IDs
cli.py ingest | ask | eval | serve
eval/ goldset.yaml + evaluate.py
web/index.html Minimaler Test-Chat
web/ Same-origin Test-Frontend (HTML, CSS, JavaScript)
tools/ Intake + Registry (build_registry.py, ingest_sources.py)
tests/ 64 Tests (offline, Fake-Ollama)
data/ index.db (gitignored)
+32
View File
@@ -12,9 +12,11 @@ import secrets
import threading
import uuid
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Literal
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, ConfigDict, Field
@@ -27,6 +29,8 @@ from .retrieve import Retriever
API_VERSION = "v1"
DATA_SCOPE = "knowledge_base_only"
_REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
WEB_DIR = Path(__file__).resolve().parent.parent / "web"
_WEB_ASSETS = {"app.js", "styles.css"}
logger = logging.getLogger(__name__)
bearer = HTTPBearer(auto_error=False)
bearer_credentials = Depends(bearer)
@@ -170,9 +174,37 @@ async def add_request_id(request: Request, call_next):
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "no-referrer"
if request.url.path == "/" or request.url.path.startswith("/assets/"):
response.headers["Content-Security-Policy"] = (
"default-src 'self'; base-uri 'none'; form-action 'self'; "
"frame-ancestors 'none'; img-src 'self' data:; "
"script-src 'self'; style-src 'self'; connect-src 'self'"
)
return response
@app.get("/", include_in_schema=False, response_class=FileResponse)
def frontend() -> FileResponse:
return FileResponse(
WEB_DIR / "index.html",
media_type="text/html",
headers={"Cache-Control": "no-store"},
)
@app.get("/assets/{asset_name}", include_in_schema=False, response_class=FileResponse)
def frontend_asset(asset_name: str) -> FileResponse:
if asset_name not in _WEB_ASSETS:
raise HTTPException(status_code=404, detail="Asset nicht gefunden.")
return FileResponse(
WEB_DIR / asset_name,
headers={"Cache-Control": "public, max-age=3600"},
)
def _request_id(request: Request) -> str:
return getattr(request.state, "request_id", uuid.uuid4().hex)