mirror of
http://100.103.83.12:3003/fegger/pv-agent.git
synced 2026-09-17 15:46:23 +00:00
feat(agent): add index bootstrap and answer feedback
This commit is contained in:
@@ -91,6 +91,7 @@ def test_frontend_assets_use_v1_api_and_unknown_assets_are_hidden():
|
||||
assert script.status_code == 200
|
||||
assert styles.status_code == 200
|
||||
assert 'fetch("/v1/ask"' in script.text
|
||||
assert 'fetch("/v1/ratings"' in script.text
|
||||
assert "innerHTML" not in script.text
|
||||
assert missing.status_code == 404
|
||||
|
||||
@@ -172,6 +173,47 @@ def test_v1_extracts_grounded_conflict_and_clarification(monkeypatch):
|
||||
assert body["alternatives"] == []
|
||||
|
||||
|
||||
def test_v1_rating_is_persisted_for_logged_answer(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr("agent.api.answer_question", lambda *args, **kwargs: answer_result())
|
||||
with TestClient(app) as client:
|
||||
app.state.rag.cfg = Config(
|
||||
api_key="service-secret",
|
||||
audit_enabled=True,
|
||||
audit_db_path=str(tmp_path / "audit.db"),
|
||||
)
|
||||
app.state.rag.client = FakeClient()
|
||||
app.state.rag.retriever = FakeRetriever()
|
||||
headers = {
|
||||
"Authorization": "Bearer service-secret",
|
||||
"X-Request-ID": "rated-answer-1",
|
||||
}
|
||||
answer = client.post("/v1/ask", json={"question": "Was gilt?"}, headers=headers)
|
||||
assert answer.status_code == 200
|
||||
assert answer.json()["ratings_enabled"] is True
|
||||
|
||||
rating = client.post(
|
||||
"/v1/ratings",
|
||||
json={
|
||||
"request_id": "rated-answer-1",
|
||||
"rating": "up",
|
||||
"feedback": "Hilfreich und nachvollziehbar",
|
||||
},
|
||||
headers={"Authorization": "Bearer service-secret"},
|
||||
)
|
||||
assert rating.status_code == 200
|
||||
assert rating.json()["accepted"] is True
|
||||
row = app.state.rag.audit.recent()[0]
|
||||
assert row["rating"] == "up"
|
||||
assert row["feedback"] == "Hilfreich und nachvollziehbar"
|
||||
|
||||
missing = client.post(
|
||||
"/v1/ratings",
|
||||
json={"request_id": "missing-answer", "rating": "down"},
|
||||
headers={"Authorization": "Bearer service-secret"},
|
||||
)
|
||||
assert missing.status_code == 404
|
||||
|
||||
|
||||
def test_health_does_not_expose_internal_ollama_url():
|
||||
with TestClient(app) as client:
|
||||
configure_state(api_key="service-secret")
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Tests für Index-Bootstrap sowie Interaktions- und Bewertungsprotokoll."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.audit import AuditStore
|
||||
from agent.bootstrap import ensure_index, index_is_ready
|
||||
from agent.config import Config
|
||||
|
||||
|
||||
def interaction_payload() -> dict:
|
||||
return {
|
||||
"api_version": "v1",
|
||||
"request_id": "test-request-1",
|
||||
"status": "answered",
|
||||
"question": "Was gilt?",
|
||||
"answer": "Belegte Antwort [lb-min-01].",
|
||||
"refused": False,
|
||||
"verified": True,
|
||||
"citations": ["lb-min-01"],
|
||||
"sources": [{"id": "lb-min-01", "title": "Quelle"}],
|
||||
"conflicts": [],
|
||||
"planned_queries": [{"text": "Was gilt?"}],
|
||||
"model": "test-model",
|
||||
"latency_ms": 42,
|
||||
"n_context": 1,
|
||||
"regenerations": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_audit_persists_interaction_and_rating(tmp_path):
|
||||
cfg = Config(
|
||||
audit_enabled=True,
|
||||
audit_db_path=str(tmp_path / "audit.db"),
|
||||
audit_retention_days=30,
|
||||
)
|
||||
store = AuditStore(cfg)
|
||||
try:
|
||||
store.record_interaction(interaction_payload())
|
||||
store.record_rating("test-request-1", "down", "Quelle war nicht passend")
|
||||
rows = store.recent()
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["question"] == "Was gilt?"
|
||||
assert rows[0]["answer"].startswith("Belegte Antwort")
|
||||
assert rows[0]["citations"] == ["lb-min-01"]
|
||||
assert rows[0]["rating"] == "down"
|
||||
assert rows[0]["feedback"] == "Quelle war nicht passend"
|
||||
|
||||
|
||||
def test_audit_can_omit_free_text_and_still_log_metadata(tmp_path, capsys):
|
||||
cfg = Config(
|
||||
audit_enabled=True,
|
||||
audit_db_path=str(tmp_path / "audit.db"),
|
||||
audit_log_content=False,
|
||||
audit_stdout=True,
|
||||
)
|
||||
store = AuditStore(cfg)
|
||||
try:
|
||||
store.record_interaction(interaction_payload())
|
||||
store.record_rating("test-request-1", "up", "soll nicht gespeichert werden")
|
||||
row = store.recent()[0]
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert row["question"] is None
|
||||
assert row["answer"] is None
|
||||
assert row["feedback"] is None
|
||||
assert "Was gilt?" not in output
|
||||
assert "soll nicht gespeichert werden" not in output
|
||||
assert '"event": "agent_interaction"' in output
|
||||
assert '"event": "agent_rating"' in output
|
||||
|
||||
|
||||
def test_rating_requires_existing_interaction(tmp_path):
|
||||
store = AuditStore(Config(audit_db_path=str(tmp_path / "audit.db")))
|
||||
try:
|
||||
with pytest.raises(KeyError):
|
||||
store.record_rating("missing", "up", None)
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
|
||||
def test_bootstrap_recognizes_existing_mini_index(mini_index):
|
||||
assert index_is_ready(mini_index) is True
|
||||
dense_cfg = dataclasses.replace(mini_index, embed_off=False)
|
||||
assert index_is_ready(dense_cfg) is False
|
||||
|
||||
|
||||
def test_bootstrap_builds_missing_index(mini_cfg, tmp_path):
|
||||
cfg = dataclasses.replace(mini_cfg, db_path=str(tmp_path / "new-index.db"))
|
||||
assert index_is_ready(cfg) is False
|
||||
result = ensure_index(cfg)
|
||||
assert result["event"] == "index_bootstrap_completed"
|
||||
assert index_is_ready(cfg) is True
|
||||
assert ensure_index(cfg)["event"] == "index_ready"
|
||||
@@ -13,10 +13,10 @@ def load_compose() -> dict:
|
||||
def test_compose_uses_external_ollama_network_and_tailscale_bind():
|
||||
compose = load_compose()
|
||||
service = compose["services"]["pv-agent"]
|
||||
assert service["networks"] == ["ollama-default"]
|
||||
assert compose["networks"]["ollama-default"] == {
|
||||
assert service["networks"] == ["ollama"]
|
||||
assert compose["networks"]["ollama"] == {
|
||||
"external": True,
|
||||
"name": "ollama-default",
|
||||
"name": "ollama_default",
|
||||
}
|
||||
assert "100.103.83.12:${PV_PORT:-8080}:8080" in service["ports"]
|
||||
assert service["environment"]["OLLAMA_URL"] == (
|
||||
@@ -33,6 +33,9 @@ def test_compose_requires_auth_and_limits_container_privileges():
|
||||
assert service["cap_drop"] == ["ALL"]
|
||||
assert "./data:/app/data" in service["volumes"]
|
||||
assert "./wissensbasis:/app/wissensbasis:ro" in service["volumes"]
|
||||
assert service["environment"]["PV_AUDIT_ENABLED"] == "${PV_AUDIT_ENABLED:-true}"
|
||||
assert service["environment"]["PV_AUDIT_DB_PATH"] == "/app/data/audit.db"
|
||||
assert service["logging"]["options"] == {"max-size": "50m", "max-file": "5"}
|
||||
|
||||
|
||||
def test_docker_context_excludes_secrets_and_runtime_data():
|
||||
@@ -44,4 +47,5 @@ def test_docker_context_excludes_secrets_and_runtime_data():
|
||||
assert "requirements-runtime.txt" in dockerfile
|
||||
assert "COPY agent/" in dockerfile
|
||||
assert "COPY web/" in dockerfile
|
||||
assert "python -m agent.bootstrap" in dockerfile
|
||||
assert "COPY ." not in dockerfile
|
||||
|
||||
Reference in New Issue
Block a user