feat(rag): global tuning manager — /tuning.html + PUT /api/steering/{id}: create, edit, list, delete steering notes without a chat

This commit is contained in:
2026-08-25 13:46:32 -04:00
parent fcde1fd37b
commit 589e26dbe9
16 changed files with 1697 additions and 21 deletions
+3
View File
@@ -57,6 +57,7 @@ def test_suggestions_honors_bor_suggestions_env_override(monkeypatch) -> None:
("/sources.html", "Knowledge base"),
("/document.html", "Brain of Reese"), # phase 10: viewer page
("/login.html", "Sign in"), # phase 16: admin sign-in page
("/tuning.html", "Global Tuning"), # phase 27: global tuning page
],
)
def test_html_pages_served_locally_no_cdn(client, path: str, marker: str) -> None:
@@ -78,6 +79,7 @@ def test_styles_and_js_served(client) -> None:
assert client.get("/assets/document.js").status_code == 200 # phase 10: viewer page
assert client.get("/assets/login.js").status_code == 200 # phase 16: login page
assert client.get("/assets/document-modal.js").status_code == 200 # phase 26: modal module
assert client.get("/assets/tuning.js").status_code == 200 # phase 27: tuning page
# Emoji code points banned from UI chrome (phase 08): the pictograph
@@ -108,6 +110,7 @@ def _find_emoji(text: str) -> list[str]:
"/sources.html",
"/document.html",
"/login.html", # phase 16
"/tuning.html", # phase 27
"/assets/app.js",
"/assets/sources.js",
"/assets/markdown.js",
@@ -22,9 +22,11 @@ from sqlalchemy import select, text
from test_chat_api import FakeRagLLM, _stream_chat
from app.api import chat as chat_api
from app.api.steering import load_steering_notes
from app.main import app as fastapi_app
from app.models import SteeringNote
from app.rag.importer import import_sources
from app.rag.prompts import build_steering_section
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "docs"
QUESTION = "How is my Kubernetes cluster set up?"
@@ -131,6 +133,92 @@ def test_create_enforces_2000_char_limit(admin_client: TestClient) -> None:
assert len(r.json()["note"]) == 2000
# ---------- update (PUT) — phase 27: edit a note in place ----------
def test_update_note_returns_200_and_replaces_text(admin_client: TestClient, db) -> None:
created = admin_client.post("/api/steering", json={"note": NOTE}).json()
r = admin_client.put(f"/api/steering/{created['id']}", json={"note": f" {NOTE}-v2 "})
assert r.status_code == 200
body = r.json()
assert body["id"] == created["id"]
assert body["note"] == f"{NOTE}-v2" # trimmed, full replacement
# Editing does not redate the note.
assert datetime.fromisoformat(body["created_at"]) == datetime.fromisoformat(
created["created_at"]
)
row = db.get(SteeringNote, uuid.UUID(created["id"]))
assert row is not None
assert row.note == f"{NOTE}-v2"
def test_update_is_reflected_in_list_load_and_prompt(admin_client: TestClient, db) -> None:
base = datetime.now(UTC)
db.add_all(
[
SteeringNote(note="oldest note", created_at=base),
SteeringNote(note="newest note", created_at=base + timedelta(hours=1)),
]
)
db.commit()
oldest = db.scalars(select(SteeringNote).order_by(SteeringNote.created_at.asc())).first()
assert oldest is not None
updated = "oldest note — updated"
assert admin_client.put(f"/api/steering/{oldest.id}", json={"note": updated}).status_code == 200
# The GET list keeps its newest-first order and carries the new text.
body = admin_client.get("/api/steering").json()
assert [n["note"] for n in body["notes"]] == ["newest note", updated]
# The chat path reads the updated note back, oldest first… (expire the
# test session's identity map so the fresh DB values are loaded).
db.expire_all()
notes = load_steering_notes(db)
assert notes == [updated, "newest note"]
# …and it appears, in order, in the <tuning> prompt section.
section = build_steering_section(notes)
assert f"1. {updated}" in section
assert "2. newest note" in section
assert section.index("1. ") < section.index("2. ")
def test_update_unknown_note_returns_404(admin_client: TestClient) -> None:
r = admin_client.put(f"/api/steering/{uuid.uuid4()}", json={"note": "x"})
assert r.status_code == 404
assert r.json() == {"detail": "steering note not found"}
def test_update_rejects_invalid_bodies(admin_client: TestClient) -> None:
created = admin_client.post("/api/steering", json={"note": NOTE}).json()
note_id = created["id"]
assert admin_client.put(f"/api/steering/{note_id}", json={"note": ""}).status_code == 422
assert admin_client.put(f"/api/steering/{note_id}", json={"note": " "}).status_code == 422
assert (
admin_client.put(f"/api/steering/{note_id}", json={"note": "x" * 2001}).status_code == 422
)
# The 2000-char boundary passes and is stored as-is.
r = admin_client.put(f"/api/steering/{note_id}", json={"note": "y" * 2000})
assert r.status_code == 200
assert len(r.json()["note"]) == 2000
def test_update_anonymous_returns_403(admin_client: TestClient, db) -> None:
created = admin_client.post("/api/steering", json={"note": NOTE}).json()
anon = TestClient(fastapi_app) # fresh jar: truly anonymous
r = anon.put(f"/api/steering/{created['id']}", json={"note": "anonymous edit"})
assert r.status_code == 403
assert r.json() == {"detail": "admin only"}
# The text is untouched, in both the API and the chat path.
body = admin_client.get("/api/steering").json()
assert [n["note"] for n in body["notes"]] == [NOTE]
assert load_steering_notes(db) == [NOTE]
# ---------- chat turn: note reaches the system prompt ----------