Release v4928.1.4.2 stable

This commit is contained in:
2026-06-09 22:55:58 +01:00
commit 6445044ac6
280 changed files with 41775 additions and 0 deletions

290
app/persistence.py Normal file
View File

@@ -0,0 +1,290 @@
from typing import Any, Dict, Optional, Tuple
import json
from sqlalchemy import text
from app.config import settings
from app.db import engine
from app.schemas import (
ActionDecision,
ActionResult,
AnalyzeRequest,
CurrentState,
UsageInfo,
)
def _json(value) -> str:
if hasattr(value, "model_dump"):
value = value.model_dump()
return json.dumps(value or {}, ensure_ascii=False)
def _uuid_or_none(value):
value = str(value or "").strip()
return value or None
def save_action_run(
*,
request: AnalyzeRequest,
action_decision: ActionDecision,
action_result: ActionResult,
usage: UsageInfo,
needs_review: bool,
model: str,
decision_source: str,
raw_body: Optional[str] = None,
clean_body: Optional[str] = None,
raw_event_id: Optional[str] = None,
) -> Tuple[str, str]:
if not settings.clientflow_persist:
return "", ""
conversation_id = request.conversation_id or "manual"
with engine.begin() as conn:
message_row = conn.execute(text("""
INSERT INTO messages (
raw_event_id,
source_system,
source_event_id,
conversation_id,
contact_id,
direction,
raw_body,
clean_body,
previous_context,
metadata
)
VALUES (
CAST(:raw_event_id AS UUID),
:source_system,
NULL,
:conversation_id,
:contact_id,
'inbound',
:raw_body,
:clean_body,
:previous_context,
CAST(:metadata AS JSONB)
)
RETURNING id::text
"""), {
"raw_event_id": raw_event_id,
"source_system": request.source or "manual",
"conversation_id": conversation_id,
"contact_id": request.contact_id,
"raw_body": raw_body or request.last_customer_message,
"clean_body": clean_body or request.last_customer_message,
"previous_context": request.previous_context,
"metadata": _json({
"current_state": request.current_state.model_dump(),
}),
}).fetchone()
message_id = message_row[0]
run_row = conn.execute(text("""
INSERT INTO action_runs (
message_id,
raw_event_id,
conversation_id,
contact_id,
source_system,
model,
provider,
openrouter_generation_id,
decision_source,
action_decision,
action_result,
prompt_tokens,
completion_tokens,
total_tokens,
cost,
usage,
needs_review
)
VALUES (
CAST(:message_id AS UUID),
CAST(:raw_event_id AS UUID),
:conversation_id,
:contact_id,
:source_system,
:model,
:provider,
:openrouter_generation_id,
:decision_source,
CAST(:action_decision AS JSONB),
CAST(:action_result AS JSONB),
:prompt_tokens,
:completion_tokens,
:total_tokens,
:cost,
CAST(:usage AS JSONB),
:needs_review
)
RETURNING id::text
"""), {
"message_id": message_id,
"raw_event_id": raw_event_id,
"conversation_id": conversation_id,
"contact_id": request.contact_id,
"source_system": request.source or "manual",
"model": model,
"provider": usage.provider,
"openrouter_generation_id": usage.id,
"decision_source": decision_source,
"action_decision": _json(action_decision),
"action_result": _json(action_result),
"prompt_tokens": usage.prompt_tokens,
"completion_tokens": usage.completion_tokens,
"total_tokens": usage.total_tokens,
"cost": usage.cost,
"usage": _json(usage),
"needs_review": needs_review,
}).fetchone()
action_run_id = run_row[0]
if raw_event_id:
conn.execute(text("""
UPDATE raw_events
SET
message_id = CAST(:message_id AS UUID),
action_run_id = CAST(:action_run_id AS UUID)
WHERE id = CAST(:raw_event_id AS UUID)
"""), {
"message_id": message_id,
"action_run_id": action_run_id,
"raw_event_id": raw_event_id,
})
return action_run_id, message_id
def save_raw_event(
source_system: str,
event_type: str | None,
source_event_id: str | None,
conversation_id: str | None,
contact_id: str | None,
payload: dict,
) -> Dict[str, Any]:
with engine.begin() as conn:
row = conn.execute(text("""
INSERT INTO raw_events (
source_system,
event_type,
source_event_id,
conversation_id,
contact_id,
payload
)
VALUES (
:source_system,
:event_type,
:source_event_id,
:conversation_id,
:contact_id,
CAST(:payload AS JSONB)
)
ON CONFLICT (source_system, source_event_id)
WHERE source_event_id IS NOT NULL
DO UPDATE SET
payload = EXCLUDED.payload,
event_type = EXCLUDED.event_type,
conversation_id = COALESCE(EXCLUDED.conversation_id, raw_events.conversation_id),
contact_id = COALESCE(EXCLUDED.contact_id, raw_events.contact_id)
RETURNING
id::text,
processed,
ignored,
message_id::text,
action_run_id::text,
(xmax = 0) AS inserted
"""), {
"source_system": source_system,
"event_type": event_type,
"source_event_id": source_event_id,
"conversation_id": conversation_id,
"contact_id": contact_id,
"payload": _json(payload),
}).mappings().first()
return dict(row or {})
def mark_raw_event_processed(
raw_event_id: str,
action_run_id: str | None = None,
message_id: str | None = None,
ignored: bool = False,
error: str | None = None,
) -> None:
with engine.begin() as conn:
conn.execute(text("""
UPDATE raw_events
SET
processed = TRUE,
ignored = :ignored,
processing_error = :error,
action_run_id = COALESCE(CAST(:action_run_id AS UUID), action_run_id),
message_id = COALESCE(CAST(:message_id AS UUID), message_id),
processed_at = now()
WHERE id = CAST(:raw_event_id AS UUID)
"""), {
"raw_event_id": raw_event_id,
"ignored": ignored,
"error": error,
"action_run_id": _uuid_or_none(action_run_id),
"message_id": _uuid_or_none(message_id),
})
def get_state_for_conversation(conversation_id: str | None) -> CurrentState:
if not conversation_id:
return CurrentState()
with engine.begin() as conn:
row = conn.execute(text("""
SELECT
t.action_code,
t.route,
t.status,
t.action,
t.note,
t.created_at
FROM tasks t
WHERE t.conversation_id = :conversation_id
ORDER BY t.created_at DESC
LIMIT 1
"""), {
"conversation_id": conversation_id,
}).mappings().first()
if not row:
return CurrentState()
return CurrentState(
last_action_code=row.get("action_code") or "desconhecido",
last_route=row.get("route") or "desconhecido",
last_task_status=row.get("status") or "desconhecido",
metadata={
"last_action": row.get("action"),
"last_note": row.get("note"),
"last_created_at": str(row.get("created_at")),
},
)