diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc index d0b2b907..bc07267e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc index 3609a1ec..50fd70a4 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc index 750a7f1f..868b18d1 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc index abe2d9d7..5297e5ad 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc index 1c44d7be..6a077d2e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc index 3e231685..70803d59 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc index 863d19cd..c62f7b42 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc index e4bd26ef..dd9c3d8e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc index 06340931..49c70be6 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc index 3dc9f8ef..93052a6e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc index da395c68..863f5c04 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc index 4a31c5dc..67dcd0c3 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc index 8cb01ecc..0dbd89ea 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc index 57f8c454..715230e5 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc index 20682da7..4d7ec529 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc index e88ed6e5..ac2b9d08 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc index 2e692edb..c5bcaafc 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc index 7d1f44a9..89c78ab4 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc index 9772a2de..df23f355 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc index caf4f1bd..b6bfcaa0 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc index fcdc277a..8b9f7599 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc index a36bce59..fb48f8ca 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc index daa206a9..1b50e7c1 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc index 24c0300f..a1c0ae30 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc index 84a4672d..85e68340 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc index d2948c87..014d27e9 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc index f78d1ba0..1adbe28e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc index ce28e29f..4c253913 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc index d7b0e3d3..4c834efd 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc index f18b89eb..ae92b20e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc index 87681d34..e757574e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc index dbf77992..cdb2534f 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc index 528d2668..d785a3a9 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc index 384cd688..c586b640 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc index dde7d464..6b2041d1 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc index 84f78f39..50fcb153 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc index 9600db02..ec8f20bf 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc index 679d83d1..8f3fac0e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc index 5001ab46..e93d71ee 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc index 93af9183..659cca02 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc index 5d65f585..6c7e3841 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc index 2d893db1..f1573488 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc index 7152ce1a..e437bea9 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc index 26340d55..b62a84fc 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc index 19b310af..aa2be86e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc index d82a04f7..15dcb19f 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc index a00ad760..313b48ab 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc index e72c6d27..86c6d7e1 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc index 65efbd1a..72d4bc1b 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc index 32b0d827..85ecb207 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/rails.py b/libs/agent_framework/src/agent_framework/guardrails/rails.py index fcfb31b3..9a32e5bb 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/rails.py +++ b/libs/agent_framework/src/agent_framework/guardrails/rails.py @@ -323,10 +323,28 @@ class CoherenceRail(Guardrail): "data": out, }, ) - out = await classify_with_framework_llm( - _llm(ctx), "COER", {"text": text or "", "context": ctx}, - profile_name="guardrail", component_name="guardrail.coer", generation_name="guardrail.coer", - ) + try: + out = await classify_with_framework_llm( + _llm(ctx), "COER", {"text": text or "", "context": ctx}, + profile_name="guardrail", component_name="guardrail.coer", generation_name="guardrail.coer", + ) + except Exception as exc: + # COER is a conversational-coherence rail, not a security boundary. + # If its semantic classifier/provider is unavailable, the graph must + # continue and let the normal router/workflow clarification logic own + # the turn instead of surfacing an infrastructure exception. + return RailDecision( + code=self.code, + allowed=True, + reason="Classificador de coerência indisponível; continuidade delegada ao runtime", + sanitized_text=text, + metadata={ + "mechanism": "infrastructure_fail_open", + "calibrated": True, + "error_type": type(exc).__name__, + "error": str(exc)[:500], + }, + ) return RailDecision( code=self.code, allowed=bool(out.get("allowed", True)), reason=str(out.get("reason") or out.get("label") or "COER avaliado"), diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc index da962e44..53122589 100644 Binary files a/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc index a5193168..9603442b 100644 Binary files a/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc index 48591afd..b797d4f4 100644 Binary files a/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py index 10b05ee4..aeaabeca 100644 --- a/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py +++ b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py @@ -12,7 +12,7 @@ from typing import Any, Iterable, Mapping from agent_framework.memory.summary_memory import MemoryContext, render_recent_messages -from agent_framework.runtime.transaction_parameters import extract_transaction_parameters, parse_transaction_confirmation +from agent_framework.runtime.transaction_parameters import extract_transaction_parameters, reconcile_transaction_parameters, parse_transaction_confirmation from agent_framework.workflows.input_contract import match_expected_input @@ -1466,6 +1466,153 @@ class AgentRuntimeMixin: cfg = self._tool_config(tool_name) return str(getattr(cfg, "description", "") or "") if cfg is not None else "" + @staticmethod + def _transaction_context_newest_first(context: Any) -> str: + """Normalize bounded conversation text into newest->oldest message blocks. + + Role-labelled multi-line messages are kept intact. This avoids reversing + individual lines inside an assistant explanation while still honoring the + temporal search order required by parameter reconciliation. + """ + raw = str(context or "").strip() + if not raw: + return "" + blocks: list[str] = [] + current: list[str] = [] + for line in raw.splitlines(): + stripped = line.strip() + if not stripped: + continue + low = stripped.lower() + starts_role = low.startswith(("user:", "assistant:", "system:", "cliente:", "agente:")) + if starts_role and current: + blocks.append(" ".join(current)) + current = [stripped] + else: + current.append(stripped) + if current: + blocks.append(" ".join(current)) + if not blocks: + blocks = [line.strip() for line in raw.splitlines() if line.strip()] + newest = list(reversed(blocks)) + return "\n".join(f"history:{i+1}: {block}" for i, block in enumerate(newest)) + + async def _reconcile_transaction_parameters( + self, + state: dict[str, Any], + *, + tool_name: str, + parameter_names: list[str], + known_arguments: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Reconcile a coherent required-parameter set from text and tool schema.""" + # Compatibility/extensibility: if a domain/runtime subclass explicitly + # overrides the legacy extractor, honor that override and translate its + # candidates into reconciliation decisions. The base framework path below + # remains the coherent temporal reconciler. + override = getattr(type(self), "_extract_transaction_parameters", None) + if override is not None and override is not AgentRuntimeMixin._extract_transaction_parameters: + route_meta_compat = ((state.get("route_decision") or {}).get("metadata") or {}) if isinstance(state.get("route_decision"), dict) else {} + compat_fields = list(parameter_names) if route_meta_compat.get("transaction_parameter_values") else [ + name for name in parameter_names if (known_arguments or {}).get(name) in _EMPTY_VALUES + ] + extracted = await override( + self, state, tool_name=tool_name, missing_parameters=compat_fields, + known_arguments=dict(known_arguments or {}), + ) + values = dict(extracted or {}) + decisions = {name: ("resolved" if name in values else ("preserve" if (known_arguments or {}).get(name) not in _EMPTY_VALUES else "unresolved")) for name in parameter_names} + provenance = {name: ("current" if name in values else "state") for name in parameter_names if decisions[name] != "unresolved"} + return {"values": values, "decisions": decisions, "provenance": provenance, "clear_fields": []} + + active = self._active_transaction(state) or {} + schema = active.get("parameter_schema") if isinstance(active.get("parameter_schema"), dict) else None + if not schema: + policy = self._resolve_tool_execution_policy(tool_name, known_arguments or {}) + schema = self._transaction_parameter_schema(tool_name, policy) + description = str(active.get("tool_description") or self._transaction_tool_description(tool_name) or "") + route_meta = ((state.get("route_decision") or {}).get("metadata") or {}) if isinstance(state.get("route_decision"), dict) else {} + contextual_reentry = bool(route_meta.get("contextual_reentry")) + text = (route_meta.get("original_input") if contextual_reentry else None) or state.get("sanitized_input") or state.get("user_text") or "" + conversational_context = route_meta.get("relevant_conversation_context") if contextual_reentry else None + if not str(conversational_context or "").strip(): + conversational_context = active.get("parameter_conversational_context") + if contextual_reentry and not str(conversational_context or "").strip(): + effective = str(route_meta.get("contextual_reentry_input") or "") + prefix = "CONTEXTO DA SOLICITAÇÃO IMEDIATAMENTE ANTERIOR:\n" + suffix = "\n\nCONTINUAÇÃO ATUAL DO CLIENTE:\n" + if prefix in effective and suffix in effective: + conversational_context = effective.split(prefix, 1)[1].split(suffix, 1)[0].strip() + ordered_context = self._transaction_context_newest_first(conversational_context) + cached = route_meta.get("transaction_parameter_values") + effective_known = dict(known_arguments or {}) + if isinstance(cached, dict): + for key, value in cached.items(): + if str(key) in set(str(x) for x in parameter_names) and value not in _EMPTY_VALUES: + effective_known[str(key)] = value + return await reconcile_transaction_parameters( + getattr(self, "llm", None), + text=str(text), + tool_name=tool_name, + parameter_names=[str(x) for x in parameter_names], + known_arguments=effective_known, + parameter_schema=schema, + tool_description=description, + conversational_context=ordered_context, + ) + + async def _extract_transaction_parameters_current_only( + self, + state: dict[str, Any], + *, + tool_name: str, + missing_parameters: list[str], + known_arguments: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Collect pending parameters from the current utterance only. + + This is the normal COLLECTING_PARAMETERS path. Temporal conversation + history is deliberately excluded here; `_reconcile_transaction_parameters` + is the fallback when this direct collection cannot satisfy or validate the + declarative parameter contract. + """ + if not missing_parameters: + return {} + + # Preserve the framework extension contract: domain/test runtimes that + # explicitly override the legacy collector remain the authoritative + # implementation of the *traditional* first pass. The temporal fallback + # is still owned by the base runtime and is invoked only after failure. + override = getattr(type(self), "_extract_transaction_parameters", None) + if override is not None and override is not AgentRuntimeMixin._extract_transaction_parameters: + return await override( + self, + state, + tool_name=tool_name, + missing_parameters=list(missing_parameters), + known_arguments=dict(known_arguments or {}), + ) + + active = self._active_transaction(state) or {} + schema = active.get("parameter_schema") if isinstance(active.get("parameter_schema"), dict) else None + if not schema: + policy = self._resolve_tool_execution_policy(tool_name, known_arguments or {}) + schema = self._transaction_parameter_schema(tool_name, policy) + description = str(active.get("tool_description") or self._transaction_tool_description(tool_name) or "") + route_meta = ((state.get("route_decision") or {}).get("metadata") or {}) if isinstance(state.get("route_decision"), dict) else {} + text = route_meta.get("original_input") if route_meta.get("contextual_reentry") else None + text = text or state.get("sanitized_input") or state.get("user_text") or "" + return await extract_transaction_parameters( + getattr(self, "llm", None), + text=str(text), + tool_name=tool_name, + missing_parameters=[str(x) for x in missing_parameters], + known_arguments=dict(known_arguments or {}), + parameter_schema=schema, + tool_description=description, + conversational_context="", + ) + async def _extract_transaction_parameters( self, state: dict[str, Any], @@ -1535,6 +1682,7 @@ class AgentRuntimeMixin: suffix = "\n\nCONTINUAÇÃO ATUAL DO CLIENTE:\n" if prefix in effective and suffix in effective: conversational_context = effective.split(prefix, 1)[1].split(suffix, 1)[0].strip() + ordered_context = self._transaction_context_newest_first(conversational_context) extracted = await extract_transaction_parameters( getattr(self, "llm", None), text=str(text), @@ -1543,9 +1691,42 @@ class AgentRuntimeMixin: known_arguments={**dict(known_arguments or {}), **reused}, parameter_schema=schema, tool_description=description, - conversational_context=str(conversational_context or ""), + conversational_context=ordered_context, ) - return {**reused, **extracted} + + # Compatibility/resilience fallback: if a provider returns only a partial + # flat extraction, scan bounded prior text source-by-source from newest to + # oldest for still-unresolved fields. This preserves temporal order and + # remains schema-driven; the main COLLECTING_PARAMETERS path uses the + # coherent set reconciler above. + combined = {**reused, **extracted} + unresolved = [name for name in missing_parameters if name not in combined] + if unresolved and str(conversational_context or "").strip(): + source_lines = self._transaction_context_newest_first(conversational_context).splitlines() + for source_line in source_lines: + if not unresolved: + break + source_text = source_line.split(": ", 1)[1] if ": " in source_line else source_line + low_source = source_text.lower() + for role_prefix in ("user: ", "assistant: ", "cliente: ", "agente: ", "system: "): + if low_source.startswith(role_prefix): + source_text = source_text[len(role_prefix):] + break + recovered = await extract_transaction_parameters( + getattr(self, "llm", None), + text=source_text, + tool_name=tool_name, + missing_parameters=list(unresolved), + known_arguments={**dict(known_arguments or {}), **combined}, + parameter_schema=schema, + tool_description=description, + conversational_context="", + ) + for key, value in recovered.items(): + if key in unresolved and value not in _EMPTY_VALUES: + combined[key] = value + unresolved = [name for name in unresolved if name not in combined] + return combined def _transactional_action_match(self, text: str, tools: list[str] | None = None) -> str | None: """Detecta solicitação transacional usando metadados de tools.yaml. @@ -2827,23 +3008,48 @@ class AgentRuntimeMixin: policy = self._resolve_tool_execution_policy(tool_name, previous_args) missing_before = self._missing_required_arguments(policy, previous_args) - # Parâmetros TRANSACIONAIS são interpretados exclusivamente pelo - # extrator LLM genérico. Durante COLLECTING_PARAMETERS, a fala atual - # também pode CORRIGIR um required field já coletado em turno anterior - # (ex.: valor=19,99 e o cliente diz "desculpa, é 14,99" enquanto - # subject ainda está pendente). Por isso o contrato editável do turno - # é o conjunto completo de ``requires``; somente as chaves realmente - # extraídas pela LLM sobrescrevem ``previous_args``. Campos não citados - # permanecem intactos. Isso preserva parameter-before-intent-shift sem - # tornar valores antigos imutáveis por acidente. - editable_required = [str(name) for name in (policy.get("requires") or [])] - extracted = await self._extract_transaction_parameters( + # Primeira tentativa: coleta tradicional usando SOMENTE a fala + # atual. O histórico não deve participar enquanto a mensagem corrente + # conseguir preencher o contrato normalmente. + arguments = dict(previous_args) + extracted_current = await self._extract_transaction_parameters_current_only( state, tool_name=tool_name, - missing_parameters=editable_required, + missing_parameters=missing_before, known_arguments=previous_args, ) - arguments = {**previous_args, **extracted} + arguments.update(dict(extracted_current or {})) + state["transaction_parameter_collection"] = { + "tool_name": tool_name, + "mode": "current_turn", + "resolved_fields": sorted(str(k) for k in (extracted_current or {}).keys()), + } + + # Segunda opção: somente se a coleta tradicional ainda deixar + # required fields em aberto, reconcilie temporalmente usando o + # schema/descrições declarados em tools.yaml e o contexto bounded + # newest->oldest preservado na transação. + missing_after_current = self._missing_required_arguments(policy, arguments) + if missing_after_current: + required_fields = [str(name) for name in (policy.get("requires") or [])] + reconciliation = await self._reconcile_transaction_parameters( + state, + tool_name=tool_name, + parameter_names=required_fields, + known_arguments=arguments, + ) + for field in reconciliation.get("clear_fields") or []: + arguments.pop(str(field), None) + arguments.update(dict(reconciliation.get("values") or {})) + state["transaction_parameter_reconciliation"] = { + "tool_name": tool_name, + "trigger": "traditional_collection_unresolved", + "decisions": dict(reconciliation.get("decisions") or {}), + "provenance": dict(reconciliation.get("provenance") or {}), + "clear_fields": list(reconciliation.get("clear_fields") or []), + } + else: + state.pop("transaction_parameter_reconciliation", None) # Argumentos estruturados já presentes no contexto são aceitos de # forma genérica (não são parsing textual). Para required fields, @@ -2890,7 +3096,65 @@ class AgentRuntimeMixin: state, tool_name=tool_name, arguments=arguments, policy=policy, emit_events=emit_events ) if pre_validation_result is not None: - return [pre_validation_result] + # A pre-validation é a fronteira autoritativa da coleta + # tradicional. Se ela rejeitar um candidato de forma recuperável + # (NEEDS_PARAMETER), ainda não repromptamos o usuário: fazemos UMA + # tentativa de reconciliação temporal com o histórico bounded e + # com a semântica declarada em tools.yaml. Isso cobre referências + # como "é a de R$ 19,99" sem tornar o reconciliador o caminho + # primário de COLLECTING_PARAMETERS. + pre_meta = state.get("transaction_pre_validation") + pre_meta = pre_meta if isinstance(pre_meta, dict) else {} + retry_parameter = str(pre_meta.get("parameter") or "").strip() + recoverable = ( + str(pre_meta.get("status") or "").upper() == "NEEDS_PARAMETER" + and bool(retry_parameter) + and state.get("transaction_status") == "COLLECTING_PARAMETERS" + ) + if recoverable: + recovered_selected = dict(self._active_transaction(state) or state.get("selected_tool_call") or {}) + recovered_arguments = dict(recovered_selected.get("arguments") or {}) + required_fields = [str(name) for name in (policy.get("requires") or [])] + reconciliation = await self._reconcile_transaction_parameters( + state, + tool_name=tool_name, + parameter_names=required_fields, + known_arguments=recovered_arguments, + ) + for field in reconciliation.get("clear_fields") or []: + recovered_arguments.pop(str(field), None) + recovered_arguments.update(dict(reconciliation.get("values") or {})) + state["transaction_parameter_reconciliation"] = { + "tool_name": tool_name, + "trigger": "prevalidation_needs_parameter", + "rejected_parameter": retry_parameter, + "decisions": dict(reconciliation.get("decisions") or {}), + "provenance": dict(reconciliation.get("provenance") or {}), + "clear_fields": list(reconciliation.get("clear_fields") or []), + } + retry_policy = self._resolve_tool_execution_policy(tool_name, recovered_arguments) + retry_missing = self._missing_required_arguments(retry_policy, recovered_arguments) + if retry_parameter not in retry_missing: + state["selected_tool_call"] = {"tool_name": tool_name, "arguments": recovered_arguments} + self._set_active_transaction( + state, tool_name=tool_name, arguments=recovered_arguments, status="COLLECTING_PARAMETERS" + ) + state["missing_parameters"] = retry_missing + pre_validation_retry = await self._run_transaction_pre_validation( + state, + tool_name=tool_name, + arguments=recovered_arguments, + policy=retry_policy, + emit_events=emit_events, + ) + if pre_validation_retry is None: + arguments = recovered_arguments + policy = retry_policy + pre_validation_result = None + else: + return [pre_validation_retry] + if pre_validation_result is not None: + return [pre_validation_result] tool_name, policy, force_confirmation = self._apply_prevalidated_transaction_decision( state, tool_name=tool_name, arguments=arguments, policy=policy diff --git a/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py b/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py index 1a394f79..88cfbeb6 100644 --- a/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py +++ b/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py @@ -69,69 +69,77 @@ def parse_transaction_confirmation(text: str) -> str | None: return None -async def extract_transaction_parameters( +async def reconcile_transaction_parameters( llm: Any, *, text: str, tool_name: str, - missing_parameters: list[str], + parameter_names: list[str], known_arguments: Mapping[str, Any] | None = None, parameter_schema: Mapping[str, Any] | None = None, tool_description: str | None = None, conversational_context: str | None = None, ) -> dict[str, Any]: - """Extract values for pending transactional parameters using the LLM only. + """Rebuild a coherent parameter set from text, newest to oldest. - This component intentionally contains no domain/entity regexes and no - knowledge of parameter names such as ``order_id`` or ``reason``. The - transaction runtime supplies the pending parameter names and optional schema; - the LLM only interprets the current user turn. State/control-flow decisions - remain deterministic outside this function. + The framework is intentionally field/domain neutral. Meaning comes from the + declarative tool schema (normally tools.yaml) plus the conversation text. + The LLM may resolve a field, preserve a previously known value, explicitly + clear a stale value whose context was superseded, or leave a field unresolved. + Returned provenance is interpretive metadata only; authoritative business + validation still belongs to the configured pre-validation/tool layer. """ - pending = [str(name) for name in (missing_parameters or []) if str(name).strip()] + names = [str(name) for name in (parameter_names or []) if str(name).strip()] message = str(text or "").strip() - if not pending or not message or llm is None: - return {} + if not names or not message or llm is None: + return {"values": {}, "decisions": {}, "provenance": {}, "clear_fields": []} schema = dict(parameter_schema or {}) known = { str(key): value for key, value in dict(known_arguments or {}).items() - if value not in _EMPTY_VALUES and str(key) not in pending + if value not in _EMPTY_VALUES } field_spec = { name: { "type": schema.get(name, "string") if not isinstance(schema.get(name), dict) else schema.get(name, {}).get("type", "string"), "description": None if not isinstance(schema.get(name), dict) else schema.get(name, {}).get("description"), } - for name in pending + for name in names + } + output_shape = { + "fields": { + name: {"decision": "resolved|preserve|clear|unresolved", "value": None, "source": "current|history:N|state"} + for name in names + } } - output_shape = {name: None for name in pending} prompt = ( - "Você extrai parâmetros PENDENTES de uma transação ativa. " - "Sua única tarefa é interpretar a mensagem atual e devolver valores para os parâmetros pendentes. " - "Não decida roteamento, intenção, confirmação ou execução da transação.\n\n" - "REGRAS OBRIGATÓRIAS:\n" - "1. Extraia SOMENTE parâmetros listados em pending_parameters.\n" - "2. Não invente valores e não transforme uma nova solicitação/intenção do usuário em valor de parâmetro.\n" - "3. Se nenhum parâmetro pendente foi realmente informado, devolva null para todos.\n" - "4. Se houver apenas um parâmetro pendente, uma resposta contendo apenas um valor pode ser associada a ele quando isso for semanticamente inequívoco.\n" - "5. Se houver vários parâmetros pendentes, extraia todos os que estiverem presentes no mesmo turno.\n" - "6. O nome do parâmetro não precisa aparecer literalmente na fala. Associe semanticamente o valor usando o nome da transação e os metadados disponíveis para cada campo.\n" - "7. Para cada parâmetro, considere o nome técnico, o tipo quando disponível e principalmente a descrição semântica quando disponível. A ausência de tipo ou descrição NÃO impede a extração.\n" - "8. Se a mensagem deixar clara a correspondência entre um trecho e um parâmetro, preencha-o mesmo que o usuário não cite o nome técnico do campo.\n" - "9. Não use conhecimento externo para completar valores ausentes e não transforme aproximações ou suposições em fatos.\n" - "10. conversational_context, quando presente, serve SOMENTE para resolver referências da mensagem atual (por exemplo: 'a de 14,99' apontando para um item citado imediatamente antes). Não trate texto do contexto como uma nova afirmação do cliente nem como evidência de negócio.\n" - "11. Quando a mensagem atual identifica um valor OU nome e o contexto imediatamente anterior contém uma única entidade compatível, você pode preencher essa entidade e os atributos pendentes inequivocamente associados a ela como CANDIDATOS. Exemplo genérico: se a fala identifica uma entidade e o contexto associa unicamente essa entidade a um valor requerido, o valor pode ser retornado como candidato. A validação autoritativa ocorrerá depois; não invente se houver ambiguidade.\n" - "12. Em caso de dúvida razoável sobre a correspondência ou o valor, prefira null.\n" - "13. Responda SOMENTE JSON válido, sem markdown, sem explicação e sem chaves extras.\n\n" + "Você é o conciliador temporal de parâmetros de uma transação ativa. " + "Reconstrua UM CONJUNTO COERENTE de parâmetros; não extraia cada campo de forma isolada. " + "Não decida roteamento, confirmação nem execução.\n\n" + "CONTRATO OBRIGATÓRIO:\n" + "1. O significado de cada campo vem EXCLUSIVAMENTE de parameter_schema e transaction_description, considerando principalmente a descrição semântica quando disponível. O framework não conhece conceitos de domínio por nome de campo.\n" + "2. A ausência de tipo ou descrição NÃO impede a extração quando o restante do contrato e o texto forem semanticamente suficientes.\n" + "3. A busca textual é temporal, em ordem decrescente: user_message é a fonte mais nova; depois conversational_context já vem do texto mais novo para o mais antigo.\n" + "3. Textos do usuário e textos explicativos do assistente podem ser usados para interpretar referências e relações entre campos. Não trate texto do contexto como uma nova afirmação do cliente; eles são contexto, não evidência autoritativa de negócio.\n" + "4. Para cada campo escolha: resolved = um texto determina novo valor; preserve = nenhum texto mais novo invalida o valor conhecido; clear = existe mudança textual mais nova que torna o valor conhecido incompatível/sem vínculo seguro com o novo contexto e ainda não há substituto; unresolved = não há valor conhecido nem candidato textual seguro.\n" + "5. Se uma fonte mais nova muda uma entidade, objeto, escopo ou outro contexto que dava sentido a campos extraídos anteriormente, REAVALIE os demais campos como conjunto. Não combine automaticamente um atributo antigo com um contexto novo só porque ambos existem.\n" + "6. Um valor de texto anterior pode continuar válido após uma mudança somente quando os textos, considerados em conjunto, sustentarem de forma inequívoca que ele ainda pertence ao contexto mais recente. Caso contrário use clear para o campo dependente.\n" + "7. Se a nova menção é inválida no mundo real, NÃO volte silenciosamente ao valor antigo. Ainda assim devolva o candidato textual mais recente como resolved; a pre-validation autoritativa é responsável por rejeitá-lo.\n" + "8. known_arguments é fallback de estado. Use preserve somente quando nenhum texto mais novo o contradiz ou rompe sua associação contextual.\n" + "9. Uma expressão só pode preencher um campo se satisfizer semanticamente type/description desse campo. Não transfira um trecho para outro campo apenas por proximidade lexical.\n" + "10. Se um texto recente corrige explicitamente informação anterior, a correção prevalece para os campos que o schema permite inferir; os demais devem ser reavaliados quanto à coerência com a correção.\n" + "11. Em ambiguidade razoável, prefira clear/unresolved a inventar uma associação. Em caso de dúvida razoável sobre a correspondência ou o valor, prefira null.\n" + "12. Responda SOMENTE JSON válido no formato pedido, sem markdown e sem chaves extras.\n\n" f"transaction_tool: {tool_name}\n" f"transaction_description: {tool_description or ''}\n" - f"pending_parameters: {json.dumps(pending, ensure_ascii=False)}\n" + f"parameter_names: {json.dumps(names, ensure_ascii=False)}\n" + f"pending_parameters: {json.dumps(names, ensure_ascii=False)}\n" f"parameter_schema: {json.dumps(field_spec, ensure_ascii=False, default=str)}\n" f"known_arguments: {json.dumps(known, ensure_ascii=False, default=str)}\n" - f"conversational_context: {str(conversational_context or '').strip()}\n" + "conversation_sources_newest_to_oldest:\n" f"user_message: {message}\n" + f"conversational_context: {str(conversational_context or '').strip()}\n" f"Formato obrigatório: {json.dumps(output_shape, ensure_ascii=False)}" ) @@ -144,44 +152,98 @@ async def extract_transaction_parameters( temperature=0.0, ) except TypeError: - # Compatibilidade com doubles/testes e providers mínimos que aceitam - # apenas messages. response = await llm.ainvoke([{"role": "user", "content": prompt}]) except Exception as exc: - logger.warning( - "transaction.parameter.llm_extract_failed tool=%s pending=%s error=%s", - tool_name, - pending, - exc, - ) - return {} + logger.warning("transaction.parameter.reconcile_failed tool=%s fields=%s error=%s", tool_name, names, exc) + return {"values": {}, "decisions": {}, "provenance": {}, "clear_fields": []} raw = _response_text(response).strip() try: payload = parse_json_object(raw) except (TypeError, ValueError): - logger.warning( - "transaction.parameter.llm_invalid_structured_output tool=%s pending=%s raw=%r", - tool_name, - pending, - raw[:240], - ) - return {} + logger.warning("transaction.parameter.reconcile_invalid_output tool=%s raw=%r", tool_name, raw[:240]) + return {"values": {}, "decisions": {}, "provenance": {}, "clear_fields": []} if not isinstance(payload, dict): - return {} + return {"values": {}, "decisions": {}, "provenance": {}, "clear_fields": []} - extracted: dict[str, Any] = {} - for name in pending: - value = payload.get(name) + # Backward compatibility with existing providers/test doubles that return a + # flat {field: value} object. Non-null flat values mean ``resolved``. + fields = payload.get("fields") if isinstance(payload.get("fields"), dict) else None + if fields is None: + fields = {} + for name in names: + flat_value = payload.get(name) + if flat_value in _EMPTY_VALUES: + decision = "preserve" if name in known else "unresolved" + source = "state" if decision == "preserve" else "" + else: + decision = "resolved" + source = "current" + fields[name] = {"decision": decision, "value": flat_value, "source": source} + + values: dict[str, Any] = {} + decisions: dict[str, str] = {} + provenance: dict[str, str] = {} + clear_fields: list[str] = [] + for name in names: + item = fields.get(name) if isinstance(fields, dict) else None + if not isinstance(item, dict): + item = {"decision": "unresolved", "value": None, "source": ""} + decision = str(item.get("decision") or "unresolved").strip().lower() + if decision not in {"resolved", "preserve", "clear", "unresolved"}: + decision = "unresolved" + decisions[name] = decision + source = str(item.get("source") or "").strip() + if source: + provenance[name] = source + if decision == "clear": + clear_fields.append(name) + continue + if decision == "preserve": + if name in known: + values[name] = known[name] + continue + if decision != "resolved": + continue declared = field_spec.get(name, {}).get("type", "string") - coerced = _coerce(value, declared) + coerced = _coerce(item.get("value"), declared) if coerced not in _EMPTY_VALUES: - extracted[name] = coerced + values[name] = coerced + else: + decisions[name] = "unresolved" logger.info( - "transaction.parameter.llm_extracted tool=%s pending=%s consumed=%s", - tool_name, - pending, - sorted(extracted), + "transaction.parameter.reconciled tool=%s decisions=%s resolved=%s clear=%s provenance=%s", + tool_name, decisions, sorted(values), clear_fields, provenance, ) - return extracted + return {"values": values, "decisions": decisions, "provenance": provenance, "clear_fields": clear_fields} + + +async def extract_transaction_parameters( + llm: Any, + *, + text: str, + tool_name: str, + missing_parameters: list[str], + known_arguments: Mapping[str, Any] | None = None, + parameter_schema: Mapping[str, Any] | None = None, + tool_description: str | None = None, + conversational_context: str | None = None, +) -> dict[str, Any]: + """Compatibility facade returning only resolved candidates. + + New runtime code should use :func:`reconcile_transaction_parameters` when it + needs preserve/clear/provenance decisions. Keeping this facade avoids + breaking routers and existing extensions that only need candidate extraction. + """ + result = await reconcile_transaction_parameters( + llm, + text=text, + tool_name=tool_name, + parameter_names=missing_parameters, + known_arguments=known_arguments, + parameter_schema=parameter_schema, + tool_description=tool_description, + conversational_context=conversational_context, + ) + return dict(result.get("values") or {})