bugfix: Invoice details
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,6 +19,10 @@ class InvoiceContext:
|
||||
complete_invoices: dict[str, Any] | None = None
|
||||
billing_analysis: dict[str, Any] | None = None
|
||||
invoice_detail: Any = None
|
||||
invoice_amount: str = ""
|
||||
invoice_amount_open: str = ""
|
||||
invoice_period: str = ""
|
||||
invoice_emissao: str = ""
|
||||
customer_id: str = ""
|
||||
error: str | None = None
|
||||
cache_hit: bool = False
|
||||
@@ -33,6 +37,10 @@ class InvoiceContext:
|
||||
"complete_invoices_payload": self.complete_invoices,
|
||||
"billing_analysis": self.billing_analysis,
|
||||
"invoice_detail": self.invoice_detail,
|
||||
"invoice_amount": self.invoice_amount,
|
||||
"invoice_amount_open": self.invoice_amount_open,
|
||||
"invoice_period": self.invoice_period,
|
||||
"invoice_emissao": self.invoice_emissao,
|
||||
"customer_id": self.customer_id,
|
||||
"invoice_context_error": self.error,
|
||||
"invoice_context_cache_hit": self.cache_hit,
|
||||
@@ -42,6 +50,55 @@ class InvoiceContext:
|
||||
}
|
||||
|
||||
|
||||
def extract_invoice_summary_context(detail: Any) -> dict[str, str]:
|
||||
"""Extrai o resumo semântico da fatura detalhada.
|
||||
|
||||
Mantém a paridade com o prefetch do Contas original: ``bill_pdf`` devolve
|
||||
um envelope com ``parsed_content`` e o parser coloca o total em
|
||||
``total_geral``. Workflows e agentes não precisam conhecer a estrutura do
|
||||
PDF para consumir ``invoice_amount``/``invoice_amount_open``.
|
||||
"""
|
||||
parsed = detail
|
||||
if isinstance(detail, dict) and isinstance(detail.get("parsed_content"), dict):
|
||||
parsed = detail["parsed_content"]
|
||||
if not isinstance(parsed, dict):
|
||||
return {}
|
||||
|
||||
resumo = parsed.get("Fatura Resumo")
|
||||
if not isinstance(resumo, list):
|
||||
resumo = []
|
||||
|
||||
invoice_period = ""
|
||||
invoice_emissao = ""
|
||||
invoice_amount = ""
|
||||
parsed_total = parsed.get("total_geral")
|
||||
if parsed_total is not None and str(parsed_total).strip():
|
||||
invoice_amount = str(parsed_total).strip()
|
||||
|
||||
for item in resumo:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
desc = str(item.get("desc", "") or "").strip().casefold()
|
||||
if desc in {"período", "periodo"}:
|
||||
invoice_period = str(item.get("period", "") or "").strip()
|
||||
elif desc in {"emissão", "emissao"}:
|
||||
invoice_emissao = str(item.get("emissao", "") or "").strip()
|
||||
if not invoice_amount and bool(item.get("is_total")) and str(item.get("value", "")).strip():
|
||||
invoice_amount = str(item.get("value", "")).strip()
|
||||
if not invoice_amount and "total" in desc and str(item.get("value", "")).strip():
|
||||
invoice_amount = str(item.get("value", "")).strip()
|
||||
|
||||
context: dict[str, str] = {}
|
||||
if invoice_period:
|
||||
context["invoice_period"] = invoice_period
|
||||
if invoice_emissao:
|
||||
context["invoice_emissao"] = invoice_emissao
|
||||
if invoice_amount:
|
||||
context["invoice_amount"] = invoice_amount
|
||||
context["invoice_amount_open"] = invoice_amount
|
||||
return context
|
||||
|
||||
|
||||
class InvoiceContextService:
|
||||
"""Session-scoped invoice prefetch backed by the framework cache.
|
||||
|
||||
@@ -155,6 +212,7 @@ class InvoiceContextService:
|
||||
|
||||
detail = None
|
||||
detail_ms = 0.0
|
||||
summary: dict[str, str] = {}
|
||||
if include_detail and resolved_invoice_id:
|
||||
detail, detail_error, detail_ms = await self._timed_to_thread(
|
||||
"invoice_detail", self.client.bill_pdf, msisdn, resolved_invoice_id, customer_id,
|
||||
@@ -163,6 +221,8 @@ class InvoiceContextService:
|
||||
if detail_error:
|
||||
errors["invoice_detail"] = detail_error
|
||||
error_parts.append(f"invoice_detail: {detail_error}")
|
||||
else:
|
||||
summary = extract_invoice_summary_context(detail)
|
||||
|
||||
metadata = {
|
||||
"cache_hit": False,
|
||||
@@ -177,7 +237,12 @@ class InvoiceContextService:
|
||||
msisdn=msisdn, invoice_id=resolved_invoice_id,
|
||||
complete_invoices=complete if isinstance(complete, dict) else None,
|
||||
billing_analysis=billing if isinstance(billing, dict) else None,
|
||||
invoice_detail=detail, customer_id=customer_id,
|
||||
invoice_detail=detail,
|
||||
invoice_amount=summary.get("invoice_amount", ""),
|
||||
invoice_amount_open=summary.get("invoice_amount_open", ""),
|
||||
invoice_period=summary.get("invoice_period", ""),
|
||||
invoice_emissao=summary.get("invoice_emissao", ""),
|
||||
customer_id=customer_id,
|
||||
error="; ".join(error_parts) or None, cache_hit=False, business_events=events, errors=errors, metadata=metadata,
|
||||
)
|
||||
|
||||
@@ -219,7 +284,10 @@ class InvoiceContextService:
|
||||
await self.cache.set(key, {
|
||||
"msisdn": ctx.msisdn, "invoice_id": ctx.invoice_id,
|
||||
"complete_invoices": ctx.complete_invoices, "billing_analysis": ctx.billing_analysis,
|
||||
"invoice_detail": ctx.invoice_detail, "customer_id": ctx.customer_id,
|
||||
"invoice_detail": ctx.invoice_detail,
|
||||
"invoice_amount": ctx.invoice_amount, "invoice_amount_open": ctx.invoice_amount_open,
|
||||
"invoice_period": ctx.invoice_period, "invoice_emissao": ctx.invoice_emissao,
|
||||
"customer_id": ctx.customer_id,
|
||||
"error": ctx.error, "cache_hit": False,
|
||||
"errors": dict(ctx.errors or {}), "metadata": dict(ctx.metadata or {}),
|
||||
"_fetched_at": time.time(),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -36,8 +36,29 @@ class ContasDomainService:
|
||||
partial = [p for p in products if needle in str(p.get("name") or p.get("description") or "").lower() or str(p.get("name") or p.get("description") or "").lower() in needle]
|
||||
return partial[0] if partial else None
|
||||
|
||||
def consultar_faturas(self, *, msisdn: str, **_: Any) -> Any:
|
||||
return self.client.consultar_faturas(msisdn)
|
||||
def consultar_faturas(self, *, msisdn: str, **args: Any) -> Any:
|
||||
"""Consulta faturas e expõe o resumo semântico quando pré-carregado.
|
||||
|
||||
A fonte de ``invoice_amount`` continua sendo a fatura detalhada (Bill PDF),
|
||||
como no Contas original. O MCP apenas apresenta esse dado junto do retorno
|
||||
de Complete Invoices; não altera o contrato do backend TIM nem inventa valor.
|
||||
"""
|
||||
complete = args.get("complete_invoices_payload")
|
||||
if not isinstance(complete, dict):
|
||||
complete = self.client.consultar_faturas(msisdn)
|
||||
if not isinstance(complete, dict):
|
||||
return complete
|
||||
|
||||
result = dict(complete)
|
||||
for key in ("invoice_amount", "invoice_amount_open", "invoice_period", "invoice_emissao"):
|
||||
value = args.get(key)
|
||||
if value not in (None, ""):
|
||||
result[key] = value
|
||||
if args.get("invoice_id") not in (None, ""):
|
||||
result.setdefault("invoice_id", args.get("invoice_id"))
|
||||
if args.get("customer_id") not in (None, ""):
|
||||
result.setdefault("customer_id", args.get("customer_id"))
|
||||
return result
|
||||
|
||||
def consultar_plano(self, *, msisdn: str, **args: Any) -> dict[str, Any]:
|
||||
"""Retorna somente os planos presentes na evidência de billing analysis.
|
||||
|
||||
BIN
app/extensions/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/extensions/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/extensions/__pycache__/tim_guardrails.cpython-313.pyc
Normal file
BIN
app/extensions/__pycache__/tim_guardrails.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/extensions/__pycache__/tim_judges.cpython-313.pyc
Normal file
BIN
app/extensions/__pycache__/tim_judges.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/extensions/tim_prompts/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/extensions/tim_prompts/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/extensions/tim_prompts/__pycache__/aluc.cpython-313.pyc
Normal file
BIN
app/extensions/tim_prompts/__pycache__/aluc.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/extensions/tim_prompts/__pycache__/revprec.cpython-313.pyc
Normal file
BIN
app/extensions/tim_prompts/__pycache__/revprec.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/extensions/tim_prompts/__pycache__/rqlt.cpython-313.pyc
Normal file
BIN
app/extensions/tim_prompts/__pycache__/rqlt.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -71,7 +71,7 @@ class ToolCall(BaseModel):
|
||||
|
||||
|
||||
TOOLS: dict[str, dict[str, Any]] = {
|
||||
"consultar_faturas": {"description": "Consulta faturas do cliente.", "input_schema": {"msisdn": "string"}},
|
||||
"consultar_faturas": {"description": "Consulta faturas do cliente, incluindo valor total quando disponível na fatura detalhada.", "input_schema": {"msisdn": "string"}},
|
||||
"consultar_plano": {"description": "Consulta exclusivamente o plano ou planos contratados presentes na fatura.", "input_schema": {"msisdn": "string"}},
|
||||
"invoice_explanation": {"description": "Executa o workflow de explicação de fatura com pause/resume pelo WorkflowRuntime do framework.", "input_schema": {"msisdn": "string"}},
|
||||
"consultar_vas": {"description": "Consulta VAS ativos.", "input_schema": {"msisdn": "string"}},
|
||||
@@ -307,17 +307,22 @@ def _preflight_subject(name: str, args: dict[str, Any]) -> dict[str, Any] | None
|
||||
return None
|
||||
|
||||
async def _enrich_invoice_context(name: str, args: dict[str, Any]) -> None:
|
||||
if name not in {"consultar_plano", "invoice_explanation", "cancelar_vas_avulso", "tratar_vas_estrategico", "contestar_cobranca"}:
|
||||
if name not in {"consultar_faturas", "consultar_plano", "invoice_explanation", "cancelar_vas_avulso", "tratar_vas_estrategico", "contestar_cobranca"}:
|
||||
return
|
||||
msisdn = str(args.get("msisdn") or "").strip()
|
||||
if not msisdn:
|
||||
return
|
||||
# If all required evidence is already present, preserve it exactly.
|
||||
if isinstance(args.get("complete_invoices_payload"), dict) and isinstance(args.get("billing_analysis"), dict):
|
||||
# If all required evidence is already present, preserve it exactly. For
|
||||
# consultar_faturas/invoice_explanation the semantic amount also matters, so
|
||||
# a cached pair without detail must still be enriched.
|
||||
has_base = isinstance(args.get("complete_invoices_payload"), dict) and isinstance(args.get("billing_analysis"), dict)
|
||||
needs_detail = name in {"consultar_faturas", "invoice_explanation", "cancelar_vas_avulso", "contestar_cobranca"}
|
||||
has_detail = isinstance(args.get("invoice_detail"), dict) or args.get("invoice_amount") not in (None, "")
|
||||
if has_base and (not needs_detail or has_detail):
|
||||
return
|
||||
session_id = str(args.get("session_id") or args.get("original_session_id") or args.get("conversation_key") or "").strip()
|
||||
invoice_id = str(args.get("invoice_id") or args.get("current_invoice_number") or "").strip()
|
||||
include_detail = name in {"cancelar_vas_avulso", "contestar_cobranca"} and not isinstance(args.get("invoice_detail"), dict)
|
||||
include_detail = name in {"consultar_faturas", "invoice_explanation", "cancelar_vas_avulso", "contestar_cobranca"} and not isinstance(args.get("invoice_detail"), dict)
|
||||
try:
|
||||
ctx = await get_invoice_context_service().get(
|
||||
session_id=session_id,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user