Import ClientFlow production v4928.1.5.132.4

This commit is contained in:
plx
2026-07-29 13:11:01 +00:00
parent 6445044ac6
commit 261d342057
405 changed files with 48373 additions and 1401 deletions

View File

@@ -254,6 +254,28 @@ def mark_raw_event_processed(
})
def mark_raw_event_error(raw_event_id: str, error: str) -> None:
"""Regista erro de processamento sem marcar o evento como processado.
Isto evita filas silenciosas: o evento deixa de ficar em
processed=false/ignored=false/processing_error=null, mas continua elegível
para recovery explícito com scripts administrativos.
"""
with engine.begin() as conn:
conn.execute(text("""
UPDATE raw_events
SET
processed = FALSE,
ignored = FALSE,
processing_error = :error,
processed_at = now()
WHERE id = CAST(:raw_event_id AS UUID)
"""), {
"raw_event_id": raw_event_id,
"error": str(error or "processing_exception")[:1000],
})
def get_state_for_conversation(conversation_id: str | None) -> CurrentState:
if not conversation_id:
return CurrentState()
@@ -288,3 +310,52 @@ def get_state_for_conversation(conversation_id: str | None) -> CurrentState:
"last_created_at": str(row.get("created_at")),
},
)
def get_recent_chatwoot_public_context(
conversation_id: str | None,
*,
current_source_event_id: str | None = None,
max_messages: int = 2,
) -> list[str]:
"""Devolve as últimas mensagens públicas Chatwoot para contexto LLM.
Usado quando o webhook não traz `conversation.messages`. Lê raw_events já
guardados e exclui a mensagem atual para evitar que o LLM confunda histórico
com o email recebido agora.
"""
if not conversation_id:
return []
with engine.begin() as conn:
rows = conn.execute(text("""
SELECT
source_event_id,
payload #>> '{message_type}' AS message_type,
payload #>> '{content}' AS content,
created_at
FROM raw_events
WHERE source_system = 'chatwoot'
AND event_type = 'message_created'
AND conversation_id = :conversation_id
AND COALESCE(payload #>> '{content}', '') <> ''
AND (CAST(:current_source_event_id AS TEXT) IS NULL OR source_event_id <> CAST(:current_source_event_id AS TEXT))
AND COALESCE(payload #>> '{private}', 'false') <> 'true'
ORDER BY created_at DESC
LIMIT :limit
"""), {
"conversation_id": str(conversation_id),
"current_source_event_id": str(current_source_event_id or "") or None,
"limit": max(1, int(max_messages or 2)),
}).mappings().all()
lines: list[str] = []
for row in reversed(rows):
role = "BLIF" if str(row.get("message_type") or "").lower() == "outgoing" else "Cliente"
content = str(row.get("content") or "")
content = " ".join(content.split())
if len(content) > 360:
content = content[:359].rstrip() + ""
if content:
lines.append(f"- {role}: {content}")
return lines