129 lines
6.5 KiB
Python
129 lines
6.5 KiB
Python
"""Chat-Komfort: Ergebnistabellen mit CSV und Diagramm, Antwort neu erzeugen, Export, Suche."""
|
||
|
||
import pytest
|
||
from sqlalchemy import func, select
|
||
|
||
from app.chat.models import Message
|
||
from app.core import charts
|
||
from app.identity import Principal
|
||
from app.tasks.models import TaskRun
|
||
from app.tasks.worker import Worker
|
||
from app.tools import service as tools
|
||
from helpers import create_workspace
|
||
|
||
CSV = b"region,umsatz,monat\nNord,100,2025-01\nSued,250,2025-01\nNord,50,2025-02\n"
|
||
QUERY = "SELECT region, sum(umsatz) AS summe FROM umsatz GROUP BY 1 ORDER BY 1"
|
||
|
||
|
||
@pytest.fixture
|
||
def ws(browser, people, db):
|
||
anna = browser("anna")
|
||
workspace_id = create_workspace(anna, people["team_demo"], "Analyse")
|
||
anna.post(f"/workspaces/{workspace_id}/members", {"username": "ben", "role": "member"})
|
||
anna.post(f"/workspaces/{workspace_id}/uploads", files={"file": ("umsatz.csv", CSV)})
|
||
admin = Principal(user_id=people["anna"], username="anna", display_name="Anna", is_admin=True,
|
||
team_ids=frozenset({people["team_demo"]}))
|
||
with db.session() as session:
|
||
tools.classify_suggested(session, admin)
|
||
chat_id = int(anna.post(f"/workspaces/{workspace_id}/chats").headers["location"].rsplit("/", 1)[1])
|
||
return {"anna": anna, "ben": browser("ben"), "dora": browser("dora"), "id": workspace_id, "chat_id": chat_id}
|
||
|
||
|
||
def ask(ws, text: str = "Umsatz je Region?"):
|
||
ws["anna"].post(f"/chats/{ws['chat_id']}/messages", {"content": text}, headers={"HX-Request": "true"})
|
||
|
||
|
||
def test_ergebnis_mit_tabelle_csv_und_diagramm(ws, db, settings, fake_provider):
|
||
fake_provider.script = [[("data_query", {"sql": QUERY}), ("data_query", {"sql": "DELETE FROM umsatz"})],
|
||
"Nord 150, Sued 250."]
|
||
ask(ws)
|
||
Worker(db, settings).run_once()
|
||
page = ws["ben"].get(f"/chats/{ws['chat_id']}").text
|
||
# Nur die erfolgreiche Abfrage hat ein Ergebnis
|
||
assert page.count("Abfrageergebnis:") == 1
|
||
assert "Abfrageergebnis: 2 Zeilen, 2 Spalten · mit Diagramm" in page
|
||
assert '<svg class="aiw-chart-svg"' in page and "Nord: summe 150" in page
|
||
call_id = int(page.split('href="/tool-calls/')[1].split(".csv")[0])
|
||
response = ws["ben"].get(f"/tool-calls/{call_id}.csv")
|
||
assert response.headers["content-type"].startswith("text/csv")
|
||
assert response.content.decode("utf-8-sig").splitlines() == ["region;summe", "Nord;150", "Sued;250"]
|
||
assert ws["dora"].get(f"/tool-calls/{call_id}.csv").status_code == 404
|
||
|
||
|
||
def test_antwort_neu_erzeugen(ws, db, settings, fake_provider):
|
||
fake_provider.script = ["Erste Antwort."]
|
||
ask(ws)
|
||
Worker(db, settings).run_once()
|
||
page = ws["ben"].get(f"/chats/{ws['chat_id']}").text
|
||
assert "Antwort neu erzeugen" in page
|
||
fake_provider.script = ["Zweite Antwort."]
|
||
response = ws["ben"].post(f"/chats/{ws['chat_id']}/regenerate")
|
||
assert response.status_code == 303
|
||
# Während der neue Run läuft: kein zweites Mal
|
||
assert ws["ben"].post(f"/chats/{ws['chat_id']}/regenerate").status_code == 400
|
||
Worker(db, settings).run_once()
|
||
page = ws["ben"].get(f"/chats/{ws['chat_id']}").text
|
||
assert "Zweite Antwort." in page and "Erste Antwort." not in page
|
||
with db.session() as session:
|
||
assert session.scalar(select(func.count()).select_from(Message)) == 2
|
||
assert session.scalar(select(func.count()).select_from(TaskRun)) == 2
|
||
# Die Frage ging erneut an das Modell, ohne die verworfene Antwort
|
||
last_input = fake_provider.calls[-1]["input"]
|
||
assert last_input[-1] == {"role": "user", "content": "Umsatz je Region?"}
|
||
assert ws["dora"].post(f"/chats/{ws['chat_id']}/regenerate").status_code == 404
|
||
|
||
|
||
def test_erneut_versuchen_nach_fehler(ws, db, settings, fake_provider):
|
||
ask(ws)
|
||
with db.session() as session:
|
||
run = session.scalar(select(TaskRun))
|
||
run.status = "failed"
|
||
page = ws["anna"].get(f"/chats/{ws['chat_id']}").text
|
||
assert "Erneut versuchen" in page
|
||
fake_provider.script = ["Jetzt geht es."]
|
||
ws["anna"].post(f"/chats/{ws['chat_id']}/regenerate")
|
||
Worker(db, settings).run_once()
|
||
assert "Jetzt geht es." in ws["anna"].get(f"/chats/{ws['chat_id']}").text
|
||
|
||
|
||
def test_export_als_markdown(ws, db, settings, fake_provider):
|
||
fake_provider.script = [[("data_query", {"sql": QUERY})], "Nord **150**."]
|
||
ask(ws)
|
||
Worker(db, settings).run_once()
|
||
response = ws["ben"].get(f"/chats/{ws['chat_id']}/export.md")
|
||
assert response.headers["content-type"].startswith("text/markdown")
|
||
assert 'filename="Umsatz-je-Region.md"' in response.headers["content-disposition"]
|
||
text = response.text
|
||
assert text.startswith("# Umsatz je Region?") and "Workspace: Analyse" in text
|
||
assert "## Frage – Anna" in text and "Nord **150**." in text and "`data_query`" in text
|
||
assert ws["dora"].get(f"/chats/{ws['chat_id']}/export.md").status_code == 404
|
||
|
||
|
||
def test_suche_in_chats(ws, db, settings, fake_provider):
|
||
fake_provider.script = ["Die Marge liegt bei 12 %."]
|
||
ask(ws, "Wie hoch ist die Marge?")
|
||
Worker(db, settings).run_once()
|
||
other = int(ws["anna"].post(f"/workspaces/{ws['id']}/chats").headers["location"].rsplit("/", 1)[1])
|
||
ws["anna"].post(f"/chats/{other}/rename", {"title": "Lagerbestand"})
|
||
base = f"/workspaces/{ws['id']}"
|
||
page = ws["ben"].get(f"{base}?q=12 %").text.split('id="chats"')[1]
|
||
assert "Wie hoch ist die Marge?" in page and "Lagerbestand" not in page and "Die Marge liegt bei 12 %" in page
|
||
page = ws["ben"].get(f"{base}?q=lager").text
|
||
assert "Lagerbestand" in page and "Marge" not in page.split('id="chats"')[1]
|
||
# % ist kein Platzhalter
|
||
assert "Kein Chat enthält" in ws["ben"].get(f"{base}?q=%25%25%25").text
|
||
|
||
|
||
def test_diagramm_auswahl():
|
||
assert charts.plan(["region", "summe"], [["Nord", 1], ["Süd", 2]]).kind == "bar"
|
||
assert charts.plan(["monat", "a", "b"], [["2025-01", 1, 2], ["2025-02", 2, 3], ["2025-03", 4, 1]]).kind == "line"
|
||
assert charts.plan(["jahr", "umsatz"], [[2023, 1], [2024, 2], [2025, 3]]).kind == "line"
|
||
# Ungeeignet: nur Text, nur eine Spalte, zu viele Zeilen
|
||
assert charts.plan(["a", "b"], [["x", "y"]]) is None
|
||
assert charts.plan(["a"], [[1]]) is None
|
||
assert charts.plan(["k", "v"], [[str(i), i] for i in range(charts.MAX_ROWS + 1)]) is None
|
||
# Höchstens drei Reihen; negative Werte erlaubt
|
||
found = charts.plan(["k", "a", "b", "c", "d"], [["x", 1, -2, 3, 4]])
|
||
assert found.values == [1, 2, 3]
|
||
svg = charts.render(["name", "wert"], [["<script>alert(1)</script>", 5], ["b", -3]])
|
||
assert "<script>" not in svg and "<script>" in svg
|