Stop War Room A2A loops and unlock full chat history with dates.

Detect repeating topics and pause A2A, load older messages beyond the old ~120 cap, and show clear date/time stamps on every bubble.
This commit is contained in:
Aissa
2026-08-16 23:23:48 +00:00
parent c5ea8cbb63
commit f925042a93
5 changed files with 307 additions and 25 deletions
+23 -3
View File
@@ -572,16 +572,36 @@ async def warroom_open_dm(agent_key: str) -> dict[str, Any]:
@router.get("/warroom/rooms/{room_id}/messages")
async def warroom_list_messages(room_id: int, limit: int = 100, after_id: int | None = None) -> dict[str, Any]:
async def warroom_list_messages(
room_id: int,
limit: int = 500,
after_id: int | None = None,
before_id: int | None = None,
) -> dict[str, Any]:
from app.services import agent_chat
agent_chat.ensure_warroom()
room = agent_chat.get_room(room_id)
if not room:
raise HTTPException(404, "Room niet gevonden")
items = agent_chat.list_messages(room_id, limit=min(limit, 300), after_id=after_id)
lim = min(max(1, limit), 5000)
items = agent_chat.list_messages(
room_id, limit=lim, after_id=after_id, before_id=before_id
)
deleted_ids = agent_chat.list_deleted_ids(room_id) if after_id else []
return {"ok": True, "room": room, "items": items, "deleted_ids": deleted_ids}
total = agent_chat.count_messages(room_id)
oldest = agent_chat.oldest_message_id(room_id)
first_id = int(items[0]["id"]) if items else None
has_more = bool(oldest is not None and first_id is not None and first_id > oldest)
return {
"ok": True,
"room": room,
"items": items,
"deleted_ids": deleted_ids,
"total": total,
"has_more": has_more,
"oldest_id": oldest,
}
@router.post("/warroom/rooms/{room_id}/messages")
+154 -12
View File
@@ -197,13 +197,30 @@ def get_room(room_id: int) -> dict[str, Any] | None:
return _iso(fetch_one("SELECT * FROM warroom_rooms WHERE id = %s", (room_id,)))
def list_messages(room_id: int, *, limit: int = 100, after_id: int | None = None) -> list[dict[str, Any]]:
def list_messages(
room_id: int,
*,
limit: int = 100,
after_id: int | None = None,
before_id: int | None = None,
) -> list[dict[str, Any]]:
"""List messages. Default = newest page. before_id = older page (scroll up)."""
lim = max(1, min(int(limit), 5000))
if after_id:
rows = fetch_all(
"""SELECT * FROM warroom_messages
WHERE room_id = %s AND id > %s AND deleted_at IS NULL
ORDER BY created_at ASC, id ASC LIMIT %s""",
(room_id, after_id, limit),
(room_id, after_id, lim),
)
elif before_id:
rows = fetch_all(
"""SELECT * FROM (
SELECT * FROM warroom_messages
WHERE room_id = %s AND id < %s AND deleted_at IS NULL
ORDER BY created_at DESC, id DESC LIMIT %s
) t ORDER BY created_at ASC, id ASC""",
(room_id, before_id, lim),
)
else:
rows = fetch_all(
@@ -212,11 +229,29 @@ def list_messages(room_id: int, *, limit: int = 100, after_id: int | None = None
WHERE room_id = %s AND deleted_at IS NULL
ORDER BY created_at DESC, id DESC LIMIT %s
) t ORDER BY created_at ASC, id ASC""",
(room_id, limit),
(room_id, lim),
)
return [_iso(r) for r in rows] # type: ignore
def count_messages(room_id: int) -> int:
row = fetch_one(
"SELECT COUNT(*) AS c FROM warroom_messages WHERE room_id = %s AND deleted_at IS NULL",
(room_id,),
)
return int((row or {}).get("c") or 0)
def oldest_message_id(room_id: int) -> int | None:
row = fetch_one(
"""SELECT id FROM warroom_messages
WHERE room_id = %s AND deleted_at IS NULL
ORDER BY id ASC LIMIT 1""",
(room_id,),
)
return int(row["id"]) if row and row.get("id") is not None else None
def delete_message(room_id: int, message_id: int) -> dict[str, Any] | None:
"""Soft-delete a warroom message. Returns the row or None if missing/already deleted."""
row = fetch_one(
@@ -508,8 +543,10 @@ def _soul_system(soul: dict[str, Any], *, room_kind: str) -> str:
accuracy
+ "War Room-regels:\n"
+ "- Max 4 korte zinnen, Nederlands.\n"
+ "- Geen @zelf-prefix / geen herhalingen.\n"
+ "- Peer @mentions verplicht: eindig met @iemand + gerichte vraag of reactie.\n"
+ "- Geen @zelf-prefix.\n"
+ "- GEEN herhalingen: zeg niet opnieuw wat net al besloten/gezegd is.\n"
+ "- Peer @mentions alleen als je écht iets nieuws toevoegt; anders geen @mention-ketting.\n"
+ "- Bij gesloten besluiten (gestopt/klaar): max 1 bevestiging, daarna ander dossier of stop.\n"
+ "- Zeg nooit dat agents uit staan.\n"
+ "- Menselijk & echt: reageer concreet op de vorige spreker (feit, mop of vraag).\n"
+ "- Lichte food-moppen / Foodlinkk-humor mag (respectvol, geen geloof/politiek/grof).\n"
@@ -1200,7 +1237,9 @@ async def post_user_message_stream(
user_focus = (
f"Prioriteit: beantwoord EERST het bericht van {username or 'Aissa'}. "
f"Spreek hem aan als Aissa. "
f"Voor escalatie naar CEO-inbox: @aissa + concrete vraag. Daarna optioneel 1 @mention naar een collega."
f"Voor escalatie naar CEO-inbox: @aissa + concrete vraag. "
f"Geen herhaling van eerdere berichten. Max 3 zinnen. "
f"@mention alleen als je iets nieuws toevoegt."
)
for key in speakers:
async for ev in _emit_speak(key, trig=f"{trigger}\n\n{user_focus}"):
@@ -1220,6 +1259,74 @@ async def post_user_message_stream(
yield {"event": "done", "ok": True, "a2a": True}
def _norm_msg(text: str) -> str:
t = (text or "").lower()
t = re.sub(r"@\w+", " ", t)
t = re.sub(r"[^a-z0-9àáäâèéëêìíïîòóöôùúüû\s]+", " ", t)
t = re.sub(r"\s+", " ", t).strip()
return t
def _token_set(text: str) -> set[str]:
stop = {
"de", "het", "een", "en", "of", "op", "voor", "van", "naar", "met", "zijn", "niet",
"geen", "wel", "dat", "die", "dit", "dan", "ook", "als", "bij", "uit", "aan", "er",
"ik", "je", "we", "jij", "hij", "zij", "aissa", "herman", "even", "check", "checken",
"begrepen", "helder", "prima", "punt", "dus", "nog", "meer", "kun", "kan", "moet",
}
return {w for w in _norm_msg(text).split() if len(w) > 2 and w not in stop}
def _overlap_ratio(a: str, b: str) -> float:
sa, sb = _token_set(a), _token_set(b)
if not sa or not sb:
return 0.0
return len(sa & sb) / max(1, min(len(sa), len(sb)))
def _is_conversation_looping(messages: list[dict], *, min_msgs: int = 4, threshold: float = 0.42) -> bool:
"""True if recent agent messages keep restating the same decision/theme."""
agents = [m for m in messages if m.get("sender_type") == "agent"][-8:]
if len(agents) < min_msgs:
return False
texts = [str(m.get("content") or "") for m in agents]
# pairwise overlap among last messages
hits = 0
pairs = 0
for i in range(len(texts)):
for j in range(i + 1, len(texts)):
pairs += 1
if _overlap_ratio(texts[i], texts[j]) >= threshold:
hits += 1
if pairs and (hits / pairs) >= 0.45:
return True
# keyword lock: same closed decision repeated
lock_words = ("website stopt", "website stop", "geen website", "dielines", "label-mock", "op de nas", "scheelt", "oprui")
recent_join = " ".join(_norm_msg(t) for t in texts[-5:])
lock_hits = sum(1 for w in lock_words if w in recent_join)
return lock_hits >= 3
def _anti_repeat_block(messages: list[dict], *, limit: int = 4) -> str:
agents = [m for m in messages if m.get("sender_type") == "agent"][-limit:]
if not agents:
return ""
lines = ["ANTI-HERHALING (hard):"]
lines.append("- Herhaal NIET wat collega's net al zeiden (besluit, mop, of check-vraag).")
lines.append("- Als het besluit al gevallen is: 1 korte bevestiging MAX, daarna NIEUW onderwerp of stilte.")
lines.append("- Verboden: opnieuw 'website stopt', 'dielines op NAS', 'even checken @product' als dat al 2x gezegd is.")
lines.append("- Lever 1 nieuw feit/actie uit JOUW rol, of zeg niks nieuws en @mention iemand met een écht nieuwe vraag.")
lines.append("Recent al gezegd (NIET napraten):")
for m in agents:
who = m.get("agent_key") or "?"
excerpt = (m.get("content") or "").strip().replace("\n", " ")
if len(excerpt) > 140:
excerpt = excerpt[:139] + "…"
lines.append(f"- @{who}: {excerpt}")
return "\n".join(lines)
async def continue_warroom_stream(
room_id: int,
*,
@@ -1267,7 +1374,40 @@ async def continue_warroom_stream(
if not topic:
topic = "Volg Aissa's input als leidraad; spreek hem aan als Aissa; wissel cockpit-dossier met lichte humor; escaleer alleen met @aissa als echt nodig."
produced = [m for m in recent if m.get("sender_type") == "agent"][-6:]
produced = [m for m in recent if m.get("sender_type") == "agent"][-8:]
anti = _anti_repeat_block(recent, limit=5)
looping = _is_conversation_looping(recent)
# Bij herhalingslus: stop autonome A2A i.p.v. nog meer napraters
if looping:
yield {
"event": "continue_start",
"topic": "Herhaling gedetecteerd — A2A pauzeert dit onderwerp",
}
# Herman mag 1x afronden en doorpakken, daarna klaar
close_trig = (
f"Topic van Aissa: {topic}\n"
f"{anti}\n"
"De War Room herhaalt zich. Jij bent @herman.\n"
"Schrijf MAX 3 zinnen: (1) besluit is al genomen — stop dit onderwerp, "
"(2) noem 1 concreet ander dossier uit cockpit (CRM/retail/pitch), "
"(3) GEEN @mention-ketting over website/packaging/NAS-opruimen.\n"
"Geen mop verplicht. Natuurlijk NL."
)
async for kind_ev, payload in _agent_speak_stream(
room, "herman", trigger=close_trig, hermes_user=hermes_user, in_reply_to=None
):
if kind_ev == "start":
yield {"event": "typing_start", **payload}
elif kind_ev == "token":
yield {"event": "typing", **payload}
elif kind_ev == "done":
yield {"event": "agent", "message": payload}
elif kind_ev == "error":
yield {"event": "system", "message": payload}
yield {"event": "done", "ok": True, "continued": True, "loop_break": True}
return
yield {"event": "continue_start", "topic": topic[:200]}
for turn in range(max(1, min(int(max_turns), 5))):
@@ -1295,16 +1435,18 @@ async def continue_warroom_stream(
src = last
trig = (
f"Autonoom War Room A2A (user kan meelezen). Topic: {topic}\n"
f"Jij bent @{target}. Reageer ECHT op @{src}.\n"
f"Kies passend: (a) 1 cockpit-feit + gerichte vraag, OF (b) korte respectvolle food-mop + @mention terug naar de inhoud.\n"
f"Breng JOUW expertise in (niet herhalen wat @{src} zei). Spreek de CEO aan als Aissa. Altijd @mention 1 collega; escalatie naar CEO-inbox via @aissa. Max 4 zinnen. Natuurlijk NL."
f"{anti}\n"
f"Jij bent @{target}. Reageer op @{src} met NIEUWE info uit jouw rol.\n"
f"Verboden: napraten van hetzelfde besluit. Max 3 zinnen. "
f"@mention alleen bij écht nieuwe vraag. Spreek Aissa aan als Aissa."
)
else:
target, src = nxt[0]
trig = (
f"Autonoom War Room A2A. Topic: {topic}\n"
f"@{src} sprak je aan. Reageer op die inhoud (of mop) alsof je in een echt teamoverleg zit.\n"
f"Geef waarde vanuit JOUW rol (feit/actie/vraag). Trek er 1 collega bij die iets kan toevoegen via @mention — niet alleen Herman. Korte food-humor mag."
f"{anti}\n"
f"@{src} noemde je. Geef 1 NIEUW punt vanuit jouw rol — geen echo.\n"
f"Max 3 zinnen. Geen herhaling van website-stop/NAS-check als dat al gezegd is."
)
async for kind_ev, payload in _agent_speak_stream(