mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-10 02:34:20 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from datetime import (
|
||||
datetime,
|
||||
timezone,
|
||||
)
|
||||
from typing import Any
|
||||
from agente_contas_tim.integrations import agent_framework_bridge
|
||||
from agente_contas_tim.constants.ic_tags_enum import (
|
||||
CVNTag,
|
||||
MPITag,
|
||||
VEBTag,
|
||||
)
|
||||
from agente_contas_tim.integrations.agent_event_metadata import build_billing_id_payload
|
||||
from agente_contas_tim.integrations.rct_policy import RCTOperation, rct_tags_for_attempt
|
||||
from agente_contas_tim.observability import get_session_id
|
||||
from agente_contas_tim.workflows.actions.registry import (
|
||||
WorkflowRuntimeContext,
|
||||
workflow_action,
|
||||
)
|
||||
from agente_contas_tim.workflows.runtime_types import ActionResult
|
||||
from agente_contas_tim.workflows.actions.common.helpers import (
|
||||
_emit_ic,
|
||||
_first_text_from_params_or_state,
|
||||
_result_failed_or_missing_data,
|
||||
_runtime_llm_callbacks,
|
||||
_runtime_llm_metadata,
|
||||
_to_dict,
|
||||
)
|
||||
from agente_contas_tim.workflows.actions.invoice_explanation.helpers import (
|
||||
_extract_invoice_explanation_text,
|
||||
_extract_rag_context,
|
||||
_normalizar_invoice_explanation_mensagem,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("agente_contas_tim.workflows.actions.tim_actions")
|
||||
|
||||
|
||||
_INVOICE_EXPLANATION_TRAILER = "Com essa explicação, sanei sua dúvida?"
|
||||
|
||||
|
||||
def _metadata_value(metadata: dict[str, Any], *keys: str) -> Any:
|
||||
for key in keys:
|
||||
value = metadata.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, str) and not value.strip():
|
||||
continue
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _int_or_none(value: Any) -> int | None:
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
raw = str(value).strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _rct_api_metadata_from_result(
|
||||
result: Any,
|
||||
input_state: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
result_metadata = getattr(result, "metadata", None)
|
||||
if not isinstance(result_metadata, dict):
|
||||
return {}
|
||||
|
||||
metadata: dict[str, Any] = {}
|
||||
api_url = _metadata_value(result_metadata, "api_url", "apiUrl")
|
||||
if api_url is not None:
|
||||
metadata["apiUrl"] = str(api_url)
|
||||
|
||||
status_code = _int_or_none(
|
||||
_metadata_value(
|
||||
result_metadata,
|
||||
"api_status_code",
|
||||
"apiStatusCode",
|
||||
"status_code",
|
||||
)
|
||||
)
|
||||
if status_code is not None:
|
||||
metadata["apiStatusCode"] = status_code
|
||||
|
||||
response_payload = _metadata_value(
|
||||
result_metadata,
|
||||
"api_response_payload",
|
||||
"apiResponsePayload",
|
||||
"error_body",
|
||||
)
|
||||
if response_payload is not None:
|
||||
metadata["apiResponsePayload"] = str(response_payload)
|
||||
|
||||
latency_ms = _int_or_none(
|
||||
_metadata_value(result_metadata, "latency_ms", "latencyMs")
|
||||
)
|
||||
if latency_ms is not None:
|
||||
metadata["latencyMs"] = max(0, latency_ms)
|
||||
|
||||
agent_specific_data = build_billing_id_payload(
|
||||
{
|
||||
"invoice_id": str(
|
||||
input_state.get("invoice_id")
|
||||
or input_state.get("current_invoice_number")
|
||||
or ""
|
||||
).strip()
|
||||
}
|
||||
)
|
||||
if agent_specific_data != "{}":
|
||||
metadata["agentSpecificData"] = agent_specific_data
|
||||
return metadata
|
||||
|
||||
|
||||
@workflow_action("invoice_explanation")
|
||||
def invoice_explanation(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
"""Retorna explicacao fixa da fatura."""
|
||||
msisdn = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"msisdn",
|
||||
"Msisdn",
|
||||
"MSISDN",
|
||||
)
|
||||
if not msisdn:
|
||||
return ActionResult.fail("msisdn obrigatório para invoice_explanation")
|
||||
|
||||
result = runtime.factory.create_invoice_explanation(
|
||||
msisdn=msisdn,
|
||||
customer_id=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"customer_id",
|
||||
"customerId",
|
||||
),
|
||||
current_invoice_number=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"current_invoice_number",
|
||||
"currentInvoiceNumber",
|
||||
"invoice_id",
|
||||
"invoiceId",
|
||||
),
|
||||
past_invoice_number=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"past_invoice_number",
|
||||
"pastInvoiceNumber",
|
||||
),
|
||||
current_invoice_due_date=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"current_invoice_due_date",
|
||||
"currentInvoiceDueDate",
|
||||
),
|
||||
past_invoice_due_date=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"past_invoice_due_date",
|
||||
"pastInvoiceDueDate",
|
||||
),
|
||||
channel=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"channel",
|
||||
"Channel",
|
||||
),
|
||||
).execute()
|
||||
if _result_failed_or_missing_data(result, state=state):
|
||||
return ActionResult.fail(
|
||||
result.error or "Falha ao montar explicacao da fatura",
|
||||
**result.metadata,
|
||||
)
|
||||
return ActionResult.ok(_to_dict(result.data), **result.metadata)
|
||||
|
||||
|
||||
@workflow_action("preparar_invoice_explanation")
|
||||
def preparar_invoice_explanation(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
"""Busca a explicacao bruta da fatura via InvoiceExplanationCommand.
|
||||
|
||||
Reaproveita a explicacao gerada na primeira passagem ao re-pausar
|
||||
(caminho OUTRO), ou a explicacao_base recebida do prefetch/cache,
|
||||
evitando reexecutar o command. Nao aplica vocalizacao nem
|
||||
enriquecimento — esses passos ficam em `formatar_invoice_explanation`.
|
||||
"""
|
||||
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
||||
if not isinstance(input_state, dict):
|
||||
input_state = {}
|
||||
|
||||
tentativa_anterior = int(params.get("tentativa_anterior") or 0)
|
||||
if tentativa_anterior == 0:
|
||||
_emit_ic(CVNTag.INICIO_FLUXO, input_state)
|
||||
|
||||
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
|
||||
cached = vars_state.get("preparar", {}) if isinstance(vars_state, dict) else {}
|
||||
explicacao_base = ""
|
||||
if isinstance(cached, dict):
|
||||
previous = cached.get("explicacao_base")
|
||||
if isinstance(previous, str) and previous.strip():
|
||||
explicacao_base = previous.strip()
|
||||
|
||||
metadata: dict[str, Any] = {}
|
||||
if not explicacao_base:
|
||||
explicacao_base = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"explicacao_base",
|
||||
"invoice_explanation_base",
|
||||
"invoiceExplanationBase",
|
||||
)
|
||||
|
||||
if not explicacao_base:
|
||||
msisdn = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"msisdn",
|
||||
"Msisdn",
|
||||
"MSISDN",
|
||||
)
|
||||
if not msisdn:
|
||||
# Pre-validacao falhou antes de chamar o servico → loop de retry
|
||||
_emit_ic(CVNTag.PRE_VALIDACAO_FAIL, input_state)
|
||||
return ActionResult.ok(
|
||||
{
|
||||
"success": False,
|
||||
"service_failed": False,
|
||||
"tentativa": tentativa_anterior + 1,
|
||||
}
|
||||
)
|
||||
|
||||
# Pre-validacao ok → intencao validada antes de chamar o servico
|
||||
_emit_ic(CVNTag.PRE_VALIDACAO_OK, input_state)
|
||||
|
||||
result = runtime.factory.create_invoice_explanation(
|
||||
msisdn=msisdn,
|
||||
customer_id=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"customer_id",
|
||||
"customerId",
|
||||
),
|
||||
current_invoice_number=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"current_invoice_number",
|
||||
"currentInvoiceNumber",
|
||||
"invoice_id",
|
||||
"invoiceId",
|
||||
),
|
||||
past_invoice_number=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"past_invoice_number",
|
||||
"pastInvoiceNumber",
|
||||
),
|
||||
current_invoice_due_date=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"current_invoice_due_date",
|
||||
"currentInvoiceDueDate",
|
||||
),
|
||||
past_invoice_due_date=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"past_invoice_due_date",
|
||||
"pastInvoiceDueDate",
|
||||
),
|
||||
channel=_first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"channel",
|
||||
"Channel",
|
||||
),
|
||||
).execute()
|
||||
if _result_failed_or_missing_data(result, state=state):
|
||||
# Falha no servico → vai direto pro FIM, sem retry
|
||||
_emit_ic(CVNTag.SERVICO_FAIL, input_state)
|
||||
rct_metadata = _rct_api_metadata_from_result(result, input_state)
|
||||
for tag in rct_tags_for_attempt(
|
||||
RCTOperation.BASE_CONHECIMENTO,
|
||||
tentativa_anterior + 1,
|
||||
success=False,
|
||||
):
|
||||
_emit_ic(tag, input_state, extra_metadata=rct_metadata)
|
||||
return ActionResult.ok(
|
||||
{
|
||||
"success": False,
|
||||
"service_failed": True,
|
||||
}
|
||||
)
|
||||
explicacao_base = _extract_invoice_explanation_text(_to_dict(result.data))
|
||||
if not explicacao_base:
|
||||
_emit_ic(CVNTag.SERVICO_FAIL, input_state)
|
||||
rct_metadata = _rct_api_metadata_from_result(result, input_state)
|
||||
for tag in rct_tags_for_attempt(
|
||||
RCTOperation.BASE_CONHECIMENTO,
|
||||
tentativa_anterior + 1,
|
||||
success=False,
|
||||
):
|
||||
_emit_ic(tag, input_state, extra_metadata=rct_metadata)
|
||||
return ActionResult.ok(
|
||||
{
|
||||
"success": False,
|
||||
"service_failed": True,
|
||||
}
|
||||
)
|
||||
_emit_ic(CVNTag.SERVICO_OK, input_state)
|
||||
for tag in rct_tags_for_attempt(
|
||||
RCTOperation.BASE_CONHECIMENTO,
|
||||
tentativa_anterior + 1,
|
||||
success=True,
|
||||
):
|
||||
_emit_ic(tag, input_state)
|
||||
metadata = dict(result.metadata)
|
||||
else:
|
||||
# Cache hit — intencao ja validada sem nova chamada ao servico
|
||||
_emit_ic(CVNTag.PRE_VALIDACAO_OK, input_state)
|
||||
_emit_ic(CVNTag.SERVICO_OK, input_state)
|
||||
for tag in rct_tags_for_attempt(
|
||||
RCTOperation.BASE_CONHECIMENTO,
|
||||
tentativa_anterior + 1,
|
||||
success=True,
|
||||
):
|
||||
_emit_ic(tag, input_state)
|
||||
|
||||
return ActionResult.ok(
|
||||
{
|
||||
"success": True,
|
||||
"explicacao_base": explicacao_base,
|
||||
"tentativa": 0,
|
||||
},
|
||||
**metadata,
|
||||
)
|
||||
|
||||
|
||||
@workflow_action("checar_tentativa_cvn")
|
||||
def checar_tentativa_cvn(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
"""Verifica contador de tentativas do fluxo CVN e emite CVN.004 ou CVN.005."""
|
||||
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
||||
if not isinstance(input_state, dict):
|
||||
input_state = {}
|
||||
tentativa = int(params.get("tentativa") or 1)
|
||||
if tentativa > 2:
|
||||
_emit_ic(CVNTag.LIMITE_TENTATIVAS, input_state)
|
||||
else:
|
||||
_emit_ic(CVNTag.DENTRO_LIMITE, input_state)
|
||||
return ActionResult.ok({"tentativa": tentativa})
|
||||
|
||||
|
||||
@workflow_action("formatar_invoice_explanation")
|
||||
def formatar_invoice_explanation(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
"""Reescreve a explicacao bruta para voz, enriquecendo com juros/multas
|
||||
a partir do invoice_detail, via capability LLM dedicada.
|
||||
|
||||
Recebe `explicacao_base` (obrigatorio) e `invoice_detail` (opcional)
|
||||
nos params. Quando `trailer_override` for informado, instrui o LLM a
|
||||
usar essa frase como fechamento (em vez da pergunta padrao). Util
|
||||
para o caminho de reforco (OUTRO).
|
||||
|
||||
Em caso de falha do LLM, faz fallback para concatenacao simples
|
||||
`explicacao_base + trailer` para nao bloquear o fluxo.
|
||||
"""
|
||||
explicacao_base = str(
|
||||
params.get("explicacao_base", "") or ""
|
||||
).strip()
|
||||
if not explicacao_base:
|
||||
return ActionResult.fail(
|
||||
"explicacao_base obrigatoria para formatar_invoice_explanation"
|
||||
)
|
||||
|
||||
invoice_detail = str(
|
||||
params.get("invoice_detail", "") or ""
|
||||
).strip()
|
||||
trailer_override = str(
|
||||
params.get("trailer_override", "") or ""
|
||||
).strip()
|
||||
fallback_trailer = (
|
||||
trailer_override or _INVOICE_EXPLANATION_TRAILER
|
||||
)
|
||||
|
||||
mensagem = ""
|
||||
if runtime.llm_gateway is not None:
|
||||
try:
|
||||
llm_result = runtime.llm_gateway.execute(
|
||||
capability_id="fluxo_invoice_explanation_reescrita",
|
||||
variables={
|
||||
"explicacao_base": explicacao_base,
|
||||
"invoice_detail": invoice_detail or "(vazio)",
|
||||
"trailer_override": trailer_override,
|
||||
},
|
||||
user_text=explicacao_base,
|
||||
callbacks=_runtime_llm_callbacks(runtime),
|
||||
tags=["workflow_action"],
|
||||
metadata=_runtime_llm_metadata(runtime),
|
||||
)
|
||||
mensagem = str(
|
||||
getattr(llm_result, "content", "") or ""
|
||||
).strip()
|
||||
mensagem = _normalizar_invoice_explanation_mensagem(
|
||||
mensagem,
|
||||
trailer_override=trailer_override,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"formatar_invoice_explanation: falha ao invocar "
|
||||
"capability fluxo_invoice_explanation_reescrita"
|
||||
)
|
||||
|
||||
if not mensagem:
|
||||
mensagem = f"{explicacao_base}\n\n{fallback_trailer}"
|
||||
|
||||
return ActionResult.ok(
|
||||
{
|
||||
"mensagem": mensagem,
|
||||
"await_user_input": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@workflow_action("registrar_atendimento_invoice_explanation")
|
||||
def registrar_atendimento_invoice_explanation(
|
||||
state: dict[str, Any],
|
||||
params: dict[str, Any],
|
||||
runtime: WorkflowRuntimeContext,
|
||||
) -> ActionResult:
|
||||
"""Stub de registro do atendimento de invoice_explanation.
|
||||
|
||||
Implementacao real ainda nao definida; deixa apenas um warning para
|
||||
indicar que o caminho foi exercitado. A resposta ao cliente fica a
|
||||
cargo do orchestrator (LLM) no proximo round.
|
||||
"""
|
||||
resposta = params.get("resposta_usuario")
|
||||
ic = str(params.get("ic", "")).strip()
|
||||
resposta_norm = str(resposta or "").strip().upper()
|
||||
ic_extra = ""
|
||||
cvn_ic = ""
|
||||
if resposta_norm == "SIM":
|
||||
ic_extra = MPITag.EXPLICACAO_SIM
|
||||
cvn_ic = CVNTag.CLIENTE_CONCORDOU
|
||||
elif resposta_norm == "NAO":
|
||||
ic_extra = MPITag.EXPLICACAO_NAO
|
||||
cvn_ic = CVNTag.CLIENTE_NAO_CONCORDOU
|
||||
logger.warning(
|
||||
"registrar_atendimento_invoice_explanation: registro ainda nao "
|
||||
"implementado (resposta_usuario=%s)",
|
||||
resposta,
|
||||
)
|
||||
if cvn_ic:
|
||||
input_state = state.get("input", {}) if isinstance(state, dict) else {}
|
||||
if not isinstance(input_state, dict):
|
||||
input_state = {}
|
||||
_emit_ic(cvn_ic, input_state)
|
||||
if ic_extra:
|
||||
try:
|
||||
input_state = (
|
||||
state.get("input", {}) if isinstance(state, dict) else {}
|
||||
)
|
||||
if not isinstance(input_state, dict):
|
||||
input_state = {}
|
||||
rag_info = _extract_rag_context(state)
|
||||
message_id = _first_text_from_params_or_state(
|
||||
state,
|
||||
params,
|
||||
"message_id",
|
||||
"messageId",
|
||||
)
|
||||
channel_id = str(
|
||||
input_state.get("channel_id")
|
||||
or input_state.get("channelId")
|
||||
or "URA"
|
||||
).strip() or "URA"
|
||||
event_date = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||
customer_message = str(
|
||||
input_state.get("customer_message")
|
||||
or input_state.get("customerMessage")
|
||||
or resposta
|
||||
or ""
|
||||
).strip()
|
||||
metadata = {
|
||||
"sessionId": get_session_id(),
|
||||
"tag": ic_extra,
|
||||
"eventDate": event_date,
|
||||
"uraCallId": str(
|
||||
input_state.get("ura_call_id", "")
|
||||
).strip(),
|
||||
"ani": str(input_state.get("ani", "")).strip(),
|
||||
"gsm": str(input_state.get("msisdn", "")).strip(),
|
||||
"agentId": "contas",
|
||||
"channelId": channel_id,
|
||||
"ragRetrievedDocuments": str(
|
||||
rag_info.get("ragRetrievedDocuments", "")
|
||||
),
|
||||
"ragSelectedDocuments": str(
|
||||
rag_info.get("ragSelectedDocuments", "")
|
||||
),
|
||||
"noMatchRag": bool(rag_info.get("noMatchRag", True)),
|
||||
"llmResponse": str(input_state.get("llm_response", "") or ""),
|
||||
"customerMessage": customer_message,
|
||||
}
|
||||
if message_id:
|
||||
metadata["messageId"] = message_id
|
||||
agent_framework_bridge.event(ic_extra, metadata=metadata)
|
||||
if ic == VEBTag.EXPLICACAO_ACEITA:
|
||||
veb_metadata = dict(metadata)
|
||||
veb_metadata["tag"] = ic
|
||||
agent_framework_bridge.event(ic, metadata=veb_metadata)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"registrar_atendimento_invoice_explanation: falha ao emitir %s",
|
||||
ic_extra,
|
||||
exc_info=True,
|
||||
)
|
||||
payload: dict[str, Any] = {}
|
||||
if ic:
|
||||
payload["ic"] = ic
|
||||
return ActionResult.ok(payload)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'invoice_explanation',
|
||||
'preparar_invoice_explanation',
|
||||
'checar_tentativa_cvn',
|
||||
'formatar_invoice_explanation',
|
||||
'registrar_atendimento_invoice_explanation',
|
||||
]
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from typing import Any
|
||||
|
||||
_INVOICE_EXPLANATION_TRAILER = "Com essa explicação, sanei sua dúvida?"
|
||||
_INVOICE_EXPLANATION_TRAILER_PATTERN = re.compile(
|
||||
r"com essa explica[cç][aã]o,?\s+sanei sua d[uú]vida\??$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_INVOICE_EXPLANATION_PADRAO_SENTINEL = re.compile(
|
||||
r"\s*\(use o padr[aã]o\)\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _normalizar_mensagem_voz(
|
||||
mensagem: str,
|
||||
*,
|
||||
trailer_override: str,
|
||||
trailer_default: str = "",
|
||||
trailer_pattern: re.Pattern[str] | None = None,
|
||||
) -> str:
|
||||
"""Pos-processa a mensagem reescrita pelo LLM.
|
||||
|
||||
Remove o sentinel "(use o padrao)" caso o modelo o tenha emitido e,
|
||||
quando `trailer_override` estiver vazio, garante o `trailer_default`
|
||||
ao final (a menos que o texto ja termine com `trailer_pattern`).
|
||||
"""
|
||||
if not mensagem:
|
||||
return mensagem
|
||||
if trailer_override:
|
||||
return mensagem
|
||||
|
||||
mensagem = _INVOICE_EXPLANATION_PADRAO_SENTINEL.sub("", mensagem).strip()
|
||||
if not trailer_default:
|
||||
return mensagem
|
||||
if trailer_pattern is not None and trailer_pattern.search(mensagem):
|
||||
return mensagem
|
||||
return f"{mensagem} {trailer_default}".strip()
|
||||
|
||||
|
||||
def _normalizar_invoice_explanation_mensagem(
|
||||
mensagem: str,
|
||||
*,
|
||||
trailer_override: str,
|
||||
) -> str:
|
||||
return _normalizar_mensagem_voz(
|
||||
mensagem,
|
||||
trailer_override=trailer_override,
|
||||
trailer_default=_INVOICE_EXPLANATION_TRAILER,
|
||||
trailer_pattern=_INVOICE_EXPLANATION_TRAILER_PATTERN,
|
||||
)
|
||||
|
||||
|
||||
def _extract_rag_context(state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Extrai campos de RAG do estado para inclusão em eventos MPI."""
|
||||
vars_state = state.get("vars", {})
|
||||
if not isinstance(vars_state, dict):
|
||||
return {}
|
||||
|
||||
# Procura por qualquer nó que tenha retornado dados de RAG.
|
||||
# Prioriza os nós conhecidos por realizar busca RAG.
|
||||
for node_id in ("buscar_informacao", "buscar_informacao_rag", "preparar"):
|
||||
node_result = vars_state.get(node_id)
|
||||
if not isinstance(node_result, dict):
|
||||
continue
|
||||
|
||||
# Se o nó já tem os campos formatados, usa eles
|
||||
if "ragRetrievedDocuments" in node_result:
|
||||
return {
|
||||
"ragRetrievedDocuments": node_result.get("ragRetrievedDocuments", ""),
|
||||
"ragSelectedDocuments": node_result.get("ragSelectedDocuments", ""),
|
||||
"noMatchRag": node_result.get("noMatchRag", True),
|
||||
}
|
||||
|
||||
# Caso contrário, tenta construir a partir da lista 'documents'
|
||||
documents = node_result.get("documents")
|
||||
if isinstance(documents, (list, tuple)):
|
||||
retrieved_titles = []
|
||||
selected_titles = []
|
||||
for doc in documents:
|
||||
if not isinstance(doc, dict):
|
||||
continue
|
||||
t = (
|
||||
doc.get("title_proc")
|
||||
or doc.get("title")
|
||||
or doc.get("chunk_texto")
|
||||
or ""
|
||||
)
|
||||
if t:
|
||||
title = str(t)
|
||||
retrieved_titles.append(title)
|
||||
# Threshold de 0.6 para considerar como selecionado (distância menor é melhor)
|
||||
distance = float(doc.get("distance", 1.0))
|
||||
if distance <= 0.6:
|
||||
selected_titles.append(title)
|
||||
|
||||
return {
|
||||
"ragRetrievedDocuments": "|".join(retrieved_titles),
|
||||
"ragSelectedDocuments": "|".join(selected_titles),
|
||||
"noMatchRag": len(documents) == 0,
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_invoice_explanation_text(data: Any) -> str:
|
||||
if isinstance(data, dict):
|
||||
for key in ("mensagem", "message", "explicacao", "texto"):
|
||||
value = data.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
if isinstance(data, str) and data.strip():
|
||||
return data.strip()
|
||||
return ""
|
||||
|
||||
|
||||
__all__ = [
|
||||
'_normalizar_mensagem_voz',
|
||||
'_normalizar_invoice_explanation_mensagem',
|
||||
'_extract_rag_context',
|
||||
'_extract_invoice_explanation_text',
|
||||
]
|
||||
Reference in New Issue
Block a user