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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user