Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -13,7 +13,9 @@ from app.config import settings
|
||||
from app.chatwoot_client import add_private_note
|
||||
from app.message_cleaner import extract_chatwoot_content
|
||||
from app.persistence import (
|
||||
get_recent_chatwoot_public_context,
|
||||
get_state_for_conversation,
|
||||
mark_raw_event_error,
|
||||
mark_raw_event_processed,
|
||||
save_raw_event,
|
||||
)
|
||||
@@ -112,7 +114,7 @@ def extract_message_content_for_context(message: Dict[str, Any]) -> str:
|
||||
def extract_recent_conversation_context(
|
||||
payload: Dict[str, Any],
|
||||
current_message_id: Optional[str],
|
||||
max_messages: int = 4,
|
||||
max_messages: int = 2,
|
||||
) -> List[str]:
|
||||
"""Extrai as últimas mensagens relevantes antes da mensagem atual.
|
||||
|
||||
@@ -174,13 +176,20 @@ def build_triage_previous_context(payload: Dict[str, Any], extracted: Dict[str,
|
||||
recent_lines = extract_recent_conversation_context(
|
||||
payload=payload,
|
||||
current_message_id=extracted.get("source_event_id"),
|
||||
max_messages=4,
|
||||
max_messages=2,
|
||||
)
|
||||
if not recent_lines:
|
||||
recent_lines = get_recent_chatwoot_public_context(
|
||||
extracted.get("conversation_id"),
|
||||
current_source_event_id=extracted.get("source_event_id"),
|
||||
max_messages=2,
|
||||
)
|
||||
|
||||
if recent_lines:
|
||||
lines.append("Histórico recente, apenas para contexto:")
|
||||
lines.append("Últimos 2 emails públicos antes da mensagem atual, apenas para contexto:")
|
||||
lines.extend(recent_lines)
|
||||
else:
|
||||
lines.append("Histórico recente: não disponível no webhook.")
|
||||
lines.append("Últimos 2 emails públicos: não disponível no webhook nem em raw_events.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -341,36 +350,17 @@ def validate_chatwoot_signature(request: Request, raw_body: bytes) -> None:
|
||||
if not hmac.compare_digest(expected_signature, received_signature):
|
||||
raise HTTPException(status_code=401, detail="invalid chatwoot signature")
|
||||
|
||||
@router.post("/chatwoot")
|
||||
async def chatwoot_webhook(request: Request) -> Dict[str, Any]:
|
||||
async def process_saved_chatwoot_raw_event(raw_event_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Processa um raw_event Chatwoot já persistido.
|
||||
|
||||
raw_body = await request.body()
|
||||
validate_chatwoot_signature(request, raw_body)
|
||||
|
||||
try:
|
||||
payload = json.loads(raw_body.decode('utf-8') or '{}')
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(status_code=400, detail='invalid json payload')
|
||||
v4928.1.5.25: o Chatwoot email webhook coloca o sentido da mensagem
|
||||
em ``payload.message_type``. Nos emails recebidos o ``sender.type`` pode
|
||||
vir vazio, por isso a ingestão não pode depender de ``sender.type`` do contacto.
|
||||
Esta função é partilhada pelo webhook online e pelo script de recovery para
|
||||
evitar que raw_events fiquem em ``processed=false`` sem erro visível.
|
||||
"""
|
||||
extracted = extract_chatwoot_event(payload)
|
||||
|
||||
raw_event_info = save_raw_event(
|
||||
source_system="chatwoot",
|
||||
event_type=extracted["event_type"],
|
||||
source_event_id=extracted["source_event_id"],
|
||||
conversation_id=extracted["conversation_id"],
|
||||
contact_id=extracted["contact_id"],
|
||||
payload=payload,
|
||||
)
|
||||
raw_event_id = raw_event_info["id"]
|
||||
|
||||
if raw_event_info.get("processed"):
|
||||
return {
|
||||
"status": "duplicate_ignored",
|
||||
"raw_event_id": raw_event_id,
|
||||
"message_id": raw_event_info.get("message_id"),
|
||||
"action_run_id": raw_event_info.get("action_run_id"),
|
||||
}
|
||||
|
||||
if not extracted["content"]:
|
||||
mark_raw_event_processed(
|
||||
raw_event_id=raw_event_id,
|
||||
@@ -381,6 +371,7 @@ async def chatwoot_webhook(request: Request) -> Dict[str, Any]:
|
||||
"status": "ignored",
|
||||
"reason": "empty_content",
|
||||
"raw_event_id": raw_event_id,
|
||||
"message_type": extracted.get("message_type"),
|
||||
}
|
||||
|
||||
if extracted["is_outgoing"]:
|
||||
@@ -396,10 +387,13 @@ async def chatwoot_webhook(request: Request) -> Dict[str, Any]:
|
||||
|
||||
completed = auto_complete_result.get("status") == "auto_completed"
|
||||
|
||||
# Mensagens enviadas manualmente no Chatwoot sem task pendente são
|
||||
# normais. Devem ser ignoradas como informação operacional, não como
|
||||
# processing_error, para não poluir System health.
|
||||
mark_raw_event_processed(
|
||||
raw_event_id=raw_event_id,
|
||||
ignored=not completed,
|
||||
error=None if completed else f"outgoing: {auto_complete_result.get('status')}",
|
||||
error=None,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -451,9 +445,55 @@ async def chatwoot_webhook(request: Request) -> Dict[str, Any]:
|
||||
"task_id": response.task_id,
|
||||
"conversation_id": extracted["conversation_id"],
|
||||
"contact_id": extracted["contact_id"],
|
||||
"message_type": extracted.get("message_type"),
|
||||
"action_decision": response.action_decision.model_dump(),
|
||||
"action_result": response.action_result.model_dump(),
|
||||
"needs_review": response.needs_review,
|
||||
"usage": response.usage.model_dump(),
|
||||
"chatwoot_note": chatwoot_note_result,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/chatwoot")
|
||||
async def chatwoot_webhook(request: Request) -> Dict[str, Any]:
|
||||
|
||||
raw_body = await request.body()
|
||||
validate_chatwoot_signature(request, raw_body)
|
||||
|
||||
try:
|
||||
payload = json.loads(raw_body.decode('utf-8') or '{}')
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(status_code=400, detail='invalid json payload')
|
||||
extracted = extract_chatwoot_event(payload)
|
||||
if not isinstance(payload, dict) or not payload:
|
||||
raise HTTPException(status_code=422, detail="empty chatwoot payload")
|
||||
if not extracted.get("conversation_id") and not extracted.get("source_event_id"):
|
||||
raise HTTPException(status_code=422, detail="chatwoot payload without conversation/message id")
|
||||
if not extracted.get("content"):
|
||||
raise HTTPException(status_code=422, detail="chatwoot payload without message content")
|
||||
|
||||
raw_event_info = save_raw_event(
|
||||
source_system="chatwoot",
|
||||
event_type=extracted["event_type"],
|
||||
source_event_id=extracted["source_event_id"],
|
||||
conversation_id=extracted["conversation_id"],
|
||||
contact_id=extracted["contact_id"],
|
||||
payload=payload,
|
||||
)
|
||||
raw_event_id = raw_event_info["id"]
|
||||
|
||||
if raw_event_info.get("processed"):
|
||||
return {
|
||||
"status": "duplicate_ignored",
|
||||
"raw_event_id": raw_event_id,
|
||||
"message_id": raw_event_info.get("message_id"),
|
||||
"action_run_id": raw_event_info.get("action_run_id"),
|
||||
}
|
||||
|
||||
try:
|
||||
return await process_saved_chatwoot_raw_event(raw_event_id, payload)
|
||||
except Exception as exc:
|
||||
# Não deixar eventos em pending silencioso. O recovery script pode
|
||||
# reprocessar com --include-errors depois de corrigida a causa.
|
||||
mark_raw_event_error(raw_event_id=raw_event_id, error=f"processing_exception: {type(exc).__name__}: {exc}")
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user