mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
feat(agent): add test frontend and Docker stack
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user