Files
Dumitru Picalau 77bd2864da feat(ai): read requests in any language for the code checks (#137)
A message the English gate doesn't read as English gets one typed
request-frame call (an English restatement plus kind, count, weight
direction and the features asked for). The code checks read it; the
model still reads the user's own words. Settings › AI assistant ›
Chat › Language you write in overrides the guess.
2026-10-27 06:06:00 +01:00

170 lines
7.4 KiB
Python

"""Requests in another language get one request-frame call, and the code
checks read it (`engine/ai/lang.py`, research note 2026-10-27)."""
from __future__ import annotations
import json
from engine.ai import config as ai_config
from tests.wp_api.test_ai_api import _chat
from tests.wp_api.test_ai_chats import _on
from tests.wp_api.test_ai_narrow_api import _acts
from tests.wp_api.test_ai_plan_items import _offered
from tests.wp_api.test_ai_prefix_stable import _on as _on_typed
from wp_api import _ai_lang
GERMAN = "Füge drei Wetteroptionen hinzu, jede mit Negativen"
FRAME = {"language": "de", "english": "Add three weather options, each with negatives",
"kind": "change", "count": 3, "weight": "none", "factor": None,
"wants": ["negatives"]}
WEATHER = {"name": "Weather", "var": "weather",
"options": [{"text": "rain"}, {"text": "snow"}, {"text": "fog"}]}
def _frames(fake_llm, n=0):
return [x for x in fake_llm.seen[n:] if (x["body"].get("response_format") or {})
.get("json_schema", {}).get("name") == "request_frame"]
def _told(fake_llm, n):
return " ".join(str(m["content"]) for x in fake_llm.seen[n:]
for m in x["body"]["messages"] if m["role"] == "user")
async def test_a_german_change_ask_keeps_its_change_tools(wp_client, fake_llm):
# Without the frame "Füge drei Wetteroptionen hinzu" read as small talk
# and the shortlist offered no change tool.
_ai_lang._CACHE.clear()
_on_typed(fake_llm)
fake_llm.overrides["request_frame"] = FRAME
fake_llm.chat_steps.append({"message": "Done.", "calls": []})
n = len(fake_llm.seen)
await _chat(wp_client, "Füge drei Wetteroptionen hinzu")
assert len(_frames(fake_llm, n)) == 1
offered = _offered(_acts(fake_llm, n)[0])
assert "create_wildcard" in offered and "react" not in offered
async def test_the_request_check_reads_the_frame(wp_client, fake_llm):
_ai_lang._CACHE.clear()
_on(fake_llm, model_size="small", tool_format="json")
fake_llm.overrides["request_frame"] = FRAME
fake_llm.chat_steps.extend([
{"message": "", "calls": [{"tool": "create_wildcard", "args": json.dumps(WEATHER)}]},
{"message": "Proposed Weather.", "calls": []},
{"message": "Proposed Weather.", "calls": []},
])
n = len(fake_llm.seen)
await _chat(wp_client, GERMAN)
told = _told(fake_llm, len(fake_llm.seen) - 1)
assert told.count("check the cards against the request") == 1
# The model reads the user's own words, never the restatement.
assert f'The request: "{GERMAN}"' in told
assert FRAME["english"] not in _told(fake_llm, n + 1)
async def test_english_asks_get_no_frame_unless_settings_say_so(wp_client, fake_llm):
_ai_lang._CACHE.clear()
_on(fake_llm)
fake_llm.chat_steps.extend([{"message": "Hi!", "calls": []}] * 3)
n = len(fake_llm.seen)
await _chat(wp_client, "make a weather wildcard please")
assert not _frames(fake_llm, n)
ai_config.update({"request_language": "english"})
await _chat(wp_client, "Mach eine Wetter-Wildcard bitte")
assert not _frames(fake_llm, n)
ai_config.update({"request_language": "other"})
fake_llm.overrides["request_frame"] = FRAME
await _chat(wp_client, "make a weather wildcard please")
assert len(_frames(fake_llm, n)) == 1
async def test_a_frame_is_asked_once_per_text(wp_client, fake_llm):
_ai_lang._CACHE.clear()
_on(fake_llm)
fake_llm.overrides["request_frame"] = FRAME
fake_llm.chat_steps.extend([{"message": "Gern.", "calls": []}] * 2)
n = len(fake_llm.seen)
await _chat(wp_client, GERMAN)
await _chat(wp_client, GERMAN)
assert len(_frames(fake_llm, n)) == 1
async def test_an_unusable_frame_leaves_the_english_checks(wp_client, fake_llm):
_ai_lang._CACHE.clear()
_on(fake_llm)
fake_llm.overrides["request_frame"] = {"english": ""}
fake_llm.chat_steps.append({"message": "Gern.", "calls": []})
out = await _chat(wp_client, GERMAN)
assert out["message"] == "Gern."
assert not _ai_lang._CACHE
def _draft(instruction, **extra):
return {"instruction": instruction, "count": 10, **extra,
"wildcard": {"name": "Weather", "var_binding": "weather", "payload": {
"options": [{"id": "a1", "value": "rain", "weight": 1}]}}}
async def test_the_draft_panel_takes_the_count_a_german_ask_names(wp_client, fake_llm):
_ai_lang._CACHE.clear()
_on(fake_llm)
fake_llm.overrides["request_frame"] = {**FRAME, "count": 6}
fake_llm.overrides["wildcard_options"] = {"options": [
{"text": t, "weight": 1, "tags": [], "negative": ""}
for t in ("Sandsturm", "Hitze", "klarer Himmel", "Wind", "Dunst", "Gewitter")]}
resp = await wp_client.post("/wp/api/ai/wildcard/draft", json=_draft(
"Entwirf sechs Wüstenwetter-Optionen", count_auto=True))
assert resp.status == 200, await resp.text()
assert (await resp.json())["count"] == 6
# A count set by hand stays.
resp = await wp_client.post("/wp/api/ai/wildcard/draft", json=_draft(
"Entwirf sechs Wüstenwetter-Optionen"))
assert (await resp.json())["count"] == 10
async def test_a_german_draft_keeps_the_negatives_it_asks_for(wp_client, fake_llm):
# "mit Negativen" never matched the English check that keeps negatives,
# so a German ask's negatives were stripped.
_ai_lang._CACHE.clear()
_on(fake_llm)
fake_llm.overrides["request_frame"] = {
**FRAME, "english": "Add desert weather options with negatives", "count": None}
fake_llm.overrides["wildcard_options"] = {"options": [
{"text": "Sandsturm", "weight": 1, "tags": [], "negative": "Regen"}]}
resp = await wp_client.post("/wp/api/ai/wildcard/draft", json=_draft(
"Füge Wüstenwetter-Optionen mit Negativen hinzu"))
assert resp.status == 200, await resp.text()
[opt] = (await resp.json())["options"]
assert opt["negative"] == "Regen"
async def test_the_setting_is_checked(wp_client, fake_llm):
_on(fake_llm)
resp = await wp_client.put("/wp/api/ai/config", json={"request_language": "klingon"})
assert resp.status == 400
resp = await wp_client.put("/wp/api/ai/config", json={"request_language": "other"})
assert resp.status == 200, await resp.text()
assert (await resp.json())["request_language"] == "other"
async def test_a_german_say_so_lowers_a_rating(wp_client, fake_llm):
# The safety guard's say-so check reads English words, so it reads the
# restatement too.
_ai_lang._CACHE.clear()
_on(fake_llm)
resp = await wp_client.post("/wp/api/modules", json={
"type": "wildcard", "name": "Pose", "description": "", "category_id": None,
"tags": ["nsfw"], "content_rating": "nsfw",
"payload": {"var_binding": "pose", "sub_categories": [], "options": [
{"id": "aaaa0001", "value": "sitting", "weight": 1, "sub_categories": []}]}})
assert resp.status == 201, await resp.text()
pose = (await resp.json())["id"]
fake_llm.overrides["request_frame"] = {
**FRAME, "english": "Pose is not nsfw, mark it safe", "count": None, "wants": []}
call = {"tool": "update_info", "args": json.dumps({"id": pose, "rating": "safe"})}
fake_llm.chat_steps.extend([{"message": "", "calls": [call]},
{"message": "Marked it safe.", "calls": []}])
body = await _chat(wp_client, "Pose ist nicht jugendgefährdend, stuf es als harmlos ein")
[card] = body["proposals"]
assert card["op"]["body"]["content_rating"] == "safe"