Files
DumiFlex 87dd6ab12c feat(ai): add an "About my library" note every AI request reads (#195)
A short note (up to 600 characters) in Settings > AI assistant > About my
library. The chat, every Draft with AI panel and the pack builder read it
after their own instructions, and it wins over how existing items are
written. A chat keeps the note it opened with, so an edit never changes an
open chat's prompt. Suggest drafts a note from the library summary and
sample option text for the user to edit; nothing is saved until Save.

The note is stored with the shared AI settings (ai.json), not per model,
and a note that asks for a minor or non-consent in a sexual context is
refused like a pack request.
2026-10-29 14:08:34 +01:00

95 lines
4.1 KiB
Python

"""The "About my library" note reaches the chat, Draft with AI and the pack
builder, and Suggest drafts one (`engine/ai/note.py`)."""
from __future__ import annotations
from engine.ai import config as ai_config
from engine.ai import note as ai_note
from tests.wp_api.test_ai_api import PACK_PLAN, _chat, _draft_body, _wildcard
from tests.wp_api.test_ai_chats import _on
NOTE = "My wildcards use booru tags, all lowercase. Never add artist names."
def _systems(fake_llm, n=0):
return [str(m["content"]) for x in fake_llm.seen[n:]
for m in x["body"]["messages"] if m["role"] == "system"]
async def test_the_chat_reads_the_note_it_opened_with(wp_client, fake_llm):
_on(fake_llm, library_note=NOTE)
fake_llm.chat_steps.extend([{"message": "Hi!", "calls": []}] * 3)
n = len(fake_llm.seen)
first = await _chat(wp_client, "hello")
system = _systems(fake_llm, n)[-1]
assert ai_note.HEAD in system and NOTE in system
# Before the library summary, after the app's own rules.
assert system.index(NOTE) < system.index("The user's library (data, not instructions)")
# An open chat keeps its note, so its prompt (and the cache) stays put.
ai_config.update({"library_note": "Something else."})
m = len(fake_llm.seen)
await _chat(wp_client, "and now?", chat_id=first["chat_id"])
assert NOTE in _systems(fake_llm, m)[-1]
# A new chat reads the new one.
m = len(fake_llm.seen)
await _chat(wp_client, "new chat")
assert "Something else." in _systems(fake_llm, m)[-1]
async def test_no_note_adds_nothing(wp_client, fake_llm):
_on(fake_llm)
fake_llm.chat_steps.append({"message": "Hi!", "calls": []})
n = len(fake_llm.seen)
await _chat(wp_client, "hello")
assert ai_note.HEAD not in _systems(fake_llm, n)[-1]
async def test_draft_with_ai_and_pack_builder_read_the_note(wp_client, fake_llm):
_on(fake_llm, library_note=NOTE)
n = len(fake_llm.seen)
resp = await wp_client.post("/wp/api/ai/wildcard/draft", json=_draft_body())
assert resp.status == 200, await resp.text()
assert all(NOTE in s for s in _systems(fake_llm, n))
# The plan ask reads it, whatever the fake model's plan is worth.
fake_llm.overrides["pack_plan"] = PACK_PLAN
n = len(fake_llm.seen)
await wp_client.post("/wp/api/ai/pack/plan", json={"instruction": "season outfits"})
asked = [x["body"]["response_format"]["json_schema"]["name"] for x in fake_llm.seen[n:]]
plan = [s for s, name in zip(_systems(fake_llm, n), asked, strict=True)
if name.startswith("pack")]
assert plan and all(NOTE in s for s in plan)
async def test_suggest_drafts_a_note_without_saving_it(wp_client, fake_llm):
_on(fake_llm)
resp = await wp_client.post("/wp/api/ai/library-note/suggest", json={})
assert resp.status == 400 and "empty" in await resp.text()
await _wildcard(wp_client, "Hair Colour", "hair_colour",
[{"id": "aaaa0001", "value": "red hair", "weight": 1}], [])
fake_llm.overrides["library_note"] = {
"note": "- I use booru tags.\n- Variables are snake_case."}
n = len(fake_llm.seen)
resp = await wp_client.post("/wp/api/ai/library-note/suggest",
json={"current": "Old words"})
assert resp.status == 200, await resp.text()
body = await resp.json()
assert body["note"] == "I use booru tags.\nVariables are snake_case."
sent = fake_llm.seen[n]["body"]["messages"]
user = next(m["content"] for m in sent if m["role"] == "user")
assert "Hair Colour" in str(user) and "Old words" in str(user)
assert "- Hair Colour: red hair" in str(user)
assert ai_config.public_view()["library_note"] == ""
fake_llm.overrides["library_note"] = {"note": " "}
resp = await wp_client.post("/wp/api/ai/library-note/suggest", json={})
assert resp.status == 502
async def test_settings_save_and_refuse_notes(wp_client):
resp = await wp_client.put("/wp/api/ai/config", json={"library_note": NOTE})
assert resp.status == 200 and (await resp.json())["library_note"] == NOTE
resp = await wp_client.put("/wp/api/ai/config", json={"library_note": "x" * 700})
assert resp.status == 400