loosecanvas / scripts /playwright_chat_turn.py
Joshua Sundance Bailey
loosecanvas: local AI thought-mapping canvas with a trust-tagged knowledge graph
6d1438c
Raw
History Blame Contribute Delete
3.93 kB
"""Browser harness for the LIVE chat turn (requires llama.cpp on LLAMA_CPP_PORT).
Loads a fixture, sends one message, and waits for the streamed assistant reply +
any canvas change the agent's tools produce. Captures chatbot text, node delta,
and a screenshot. This is the only harness that needs the LLM server up.
.venv/Scripts/python.exe scripts/playwright_chat_turn.py ["prompt"]
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from playwright.sync_api import sync_playwright
BASE = "http://127.0.0.1:8000/"
OUT = Path(__file__).resolve().parent.parent / ".playwright-out"
OUT.mkdir(exist_ok=True)
PROMPT = (
sys.argv[1]
if len(sys.argv) > 1
else "Reveal the hidden concepts about overfitting and explain how they relate."
)
NODE_IDS = "() => (window.__cy ? window.__cy.nodes().map(n => n.id()) : [])"
# Gradio renders bot bubbles with data-testid="bot"; fall back to any .bot text.
BOT_TEXT = """
() => {
const els = [...document.querySelectorAll('[data-testid=\"bot\"], .bot.message, .message.bot')];
return els.map(e => e.innerText.trim()).filter(Boolean).join('\\n---\\n');
}
"""
def main() -> int:
console: list[str] = []
report: dict[str, object] = {"base": BASE, "prompt": PROMPT}
failures: list[str] = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1400, "height": 1000})
page.on("console", lambda m: console.append(f"{m.type}: {m.text}"))
page.on("pageerror", lambda e: console.append(f"pageerror: {e}"))
page.add_init_script("window.__PLAYWRIGHT_TEST = true;")
page.goto(BASE, wait_until="domcontentloaded")
page.wait_for_selector("#lc-canvas", timeout=30_000)
page.get_by_role("button", name="Load graph").click()
page.wait_for_function(
"() => window.__cy && window.__cy.nodes().length > 0", timeout=15_000
)
nodes_before = page.evaluate(NODE_IDS)
report["nodes_before"] = sorted(nodes_before)
# Send a turn. The msg textbox carries the "Ask about the graph…" placeholder.
box = page.get_by_placeholder("Ask about the graph", exact=False)
box.click()
box.fill(PROMPT)
page.get_by_role("button", name="Send", exact=True).click()
# The turn streams; the LLM at 64K ctx may take a while. Wait up to 180s for a
# bot bubble to appear, then a short settle for streaming + canvas patch.
bot_text = ""
try:
page.wait_for_function(
'() => { const e=[...document.querySelectorAll(\'[data-testid=\\"bot\\"], '
".bot.message, .message.bot')]; return e.some(x => x.innerText.trim().length > 0); }",
timeout=180_000,
)
except Exception as exc: # noqa: BLE001
failures.append(f"no assistant reply within 180s: {exc}")
page.wait_for_timeout(4_000)
bot_text = page.evaluate(BOT_TEXT)
report["assistant_reply"] = bot_text[:1500]
report["assistant_reply_chars"] = len(bot_text)
if not bot_text.strip():
failures.append("assistant reply empty")
nodes_after = page.evaluate(NODE_IDS)
report["nodes_after"] = sorted(nodes_after)
report["nodes_added"] = sorted(set(nodes_after) - set(nodes_before))
report["nodes_removed"] = sorted(set(nodes_before) - set(nodes_after))
page.screenshot(path=str(OUT / "04_chat_turn.png"), full_page=True)
browser.close()
report["console_tail"] = console[-25:]
report["failures"] = failures
report["ok"] = not failures
(OUT / "chat_report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
print(json.dumps(report, indent=2))
return 0 if not failures else 1
if __name__ == "__main__":
sys.exit(main())