diff --git a/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc index 907d106..cd74785 100644 Binary files a/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc index b2319c6..25ef057 100644 Binary files a/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc index ae2cffe..034108a 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc index 6f6b31c..f451e26 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc index 573ff01..a5123b3 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc index 9e88773..4448806 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc index 9103277..5d34ee7 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc index bfcdc2d..1840490 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc index efc6d74..f4bcd6b 100644 Binary files a/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc b/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc index f788beb..893ae4e 100644 Binary files a/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc index 0be3524..b988b7e 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc index dea6dfa..deb08c3 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc index 7b420cb..92a9594 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc index 697a603..9834e10 100644 Binary files a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc index 089a6b4..68ed373 100644 Binary files a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc index fa681bc..bb18688 100644 Binary files a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/checkpoints/langgraph_saver.py b/libs/agent_framework/src/agent_framework/checkpoints/langgraph_saver.py index f060ccd..468338c 100644 --- a/libs/agent_framework/src/agent_framework/checkpoints/langgraph_saver.py +++ b/libs/agent_framework/src/agent_framework/checkpoints/langgraph_saver.py @@ -25,12 +25,203 @@ from typing import Any, AsyncIterator, Iterator from .checkpoint_repository import create_checkpoint_repository -def _jsonable(value: Any) -> Any: - try: - json.dumps(value, default=str) +def _parse_legacy_json_container(value: Any, expected: type) -> Any: + """Recover containers that older JSON backends persisted as JSON strings. + + This is intentionally field-scoped: ordinary business strings must stay + strings, even if their text happens to look like JSON. + """ + current = value + for _ in range(3): + if isinstance(current, expected): + return current + if not isinstance(current, str): + break + text = current.strip() + if not text: + break + if expected is dict and not text.startswith("{"): + break + if expected is list and not text.startswith("["): + break + try: + current = json.loads(text) + except Exception: + break + return current if isinstance(current, expected) else expected() + + +def _strict_json_value(value: Any, *, path: str = "$") -> Any: + """Convert to repository-safe JSON without ever falling back to ``str``. + + ``default=str`` is unsafe for LangGraph checkpoints: runtime/task objects can + become ordinary strings and later be consumed as typed values by Pregel. + Keep native JSON containers recursively and fail loudly for an unsupported + object instead of corrupting it silently. + """ + if value is None or isinstance(value, (str, int, float, bool)): return value - except TypeError: - return json.loads(json.dumps(value, default=str)) + if isinstance(value, dict): + return { + str(key): _strict_json_value(item, path=f"{path}.{key}") + for key, item in value.items() + } + if isinstance(value, (list, tuple)): + return [ + _strict_json_value(item, path=f"{path}[{idx}]") + for idx, item in enumerate(value) + ] + # Common durable scalar types that JSON does not know natively. + if isinstance(value, uuid.UUID): + return str(value) + try: + from datetime import date, datetime + if isinstance(value, (date, datetime)): + return value.isoformat() + except Exception: + pass + try: + from enum import Enum + if isinstance(value, Enum): + return _strict_json_value(value.value, path=path) + except Exception: + pass + if hasattr(value, "model_dump") and callable(value.model_dump): + return _strict_json_value(value.model_dump(), path=path) + raise TypeError( + f"Checkpoint contém valor não serializável em {path}: " + f"{type(value).__module__}.{type(value).__qualname__}" + ) + + +def _normalize_checkpoint(checkpoint: Any) -> dict[str, Any]: + checkpoint = _parse_legacy_json_container(checkpoint, dict) + if not isinstance(checkpoint, dict): + return {} + out = dict(checkpoint) + out["channel_values"] = _parse_legacy_json_container(out.get("channel_values"), dict) + out["channel_versions"] = _parse_legacy_json_container(out.get("channel_versions"), dict) + raw_seen = _parse_legacy_json_container(out.get("versions_seen"), dict) + out["versions_seen"] = { + str(node): _parse_legacy_json_container(versions, dict) + for node, versions in raw_seen.items() + } + if "pending_sends" in out: + out["pending_sends"] = _parse_legacy_json_container(out.get("pending_sends"), list) + if "updated_channels" in out and isinstance(out.get("updated_channels"), str): + out["updated_channels"] = _parse_legacy_json_container(out.get("updated_channels"), list) + return out + + +def _normalize_metadata(metadata: Any) -> dict[str, Any]: + value = _parse_legacy_json_container(metadata, dict) + return value if isinstance(value, dict) else {} + + +def _normalize_config(config: Any) -> dict[str, Any]: + value = _parse_legacy_json_container(config, dict) + if not isinstance(value, dict): + return {} + out = dict(value) + out["configurable"] = _parse_legacy_json_container(out.get("configurable"), dict) + return out + + +_EPHEMERAL_RUNTIME_KEYS = {"__pregel_runtime", "__pregel_store"} + + +def _strip_runtime_refs(value: Any) -> Any: + """Recursively remove process-local runtime/store references only. + + Checkpoints may legitimately contain LangGraph internal channels whose names + also start with ``__pregel_`` (for example task channels). Those are durable + graph state and must be preserved. The corruption that triggers + ``str.override`` is specifically a runtime/store object captured inside a + nested RunnableConfig and later stringified by the JSON repository. + """ + if isinstance(value, dict): + return { + key: _strip_runtime_refs(item) + for key, item in value.items() + if str(key) not in _EPHEMERAL_RUNTIME_KEYS + } + if isinstance(value, list): + return [_strip_runtime_refs(item) for item in value] + if isinstance(value, tuple): + return tuple(_strip_runtime_refs(item) for item in value) + return value + + +def _durable_config(config: dict[str, Any] | None) -> dict[str, Any]: + """Return a checkpoint-safe copy of a LangGraph RunnableConfig. + + LangGraph injects ephemeral private values such as ``__pregel_runtime`` and + ``__pregel_store`` under ``configurable`` while a graph is running. They are + process-local and must never cross the durable checkpoint boundary. + + The scrub is recursive because task/pending-write config fragments may be + nested below regular config fields in newer LangGraph versions. + """ + if not isinstance(config, dict): + return {} + cleaned = _strip_runtime_refs(config) + if not isinstance(cleaned, dict): + return {} + configurable = cleaned.get("configurable") + if isinstance(configurable, dict): + cleaned = dict(cleaned) + cleaned["configurable"] = { + key: value + for key, value in configurable.items() + if not str(key).startswith("__pregel_") + } + return cleaned + + +def _canonical_checkpoint_config( + payload: dict[str, Any], + request_config: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Rebuild the RunnableConfig returned to LangGraph from durable IDs only. + + Official LangGraph savers do not re-bind the full config that happened to be + present when a checkpoint was written. They reconstruct a fresh config from + ``thread_id``, ``checkpoint_ns`` and ``checkpoint_id``. Doing the same here + prevents a historical/factory-time runtime value from being rebound into a + new execution while remaining backward compatible with existing rows. + """ + requested = _durable_config(request_config) + stored = _durable_config(_normalize_config(payload.get("config")) if isinstance(payload, dict) else None) + req_cfg = requested.get("configurable") if isinstance(requested.get("configurable"), dict) else {} + stored_cfg = stored.get("configurable") if isinstance(stored.get("configurable"), dict) else {} + checkpoint = payload.get("checkpoint") if isinstance(payload, dict) else {} + checkpoint = checkpoint if isinstance(checkpoint, dict) else {} + + thread_id = ( + req_cfg.get("thread_id") + or stored_cfg.get("thread_id") + or payload.get("thread_id") + or "default" + ) + checkpoint_ns = req_cfg.get("checkpoint_ns") + if checkpoint_ns is None: + checkpoint_ns = stored_cfg.get("checkpoint_ns", "") + + requested_checkpoint_id = req_cfg.get("checkpoint_id") + checkpoint_id = ( + requested_checkpoint_id + or payload.get("checkpoint_id") + or checkpoint.get("id") + or stored_cfg.get("checkpoint_id") + ) + + configurable: dict[str, Any] = { + "thread_id": str(thread_id), + "checkpoint_ns": str(checkpoint_ns or ""), + } + if checkpoint_id not in (None, ""): + configurable["checkpoint_id"] = str(checkpoint_id) + return {"configurable": configurable} def _thread_id(config: dict[str, Any] | None) -> str: @@ -85,6 +276,7 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver): """Checkpoint saver nativo para LangGraph usando os repositories do framework.""" def __init__(self, settings, repository=None): + super().__init__() self.settings = settings self.repository = repository or create_checkpoint_repository(settings) self._loop: asyncio.AbstractEventLoop | None = None @@ -100,20 +292,40 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver): with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: return ex.submit(lambda: asyncio.run(coro)).result() - def _make_tuple(self, payload: dict[str, Any] | None): + def _make_tuple( + self, + payload: dict[str, Any] | None, + request_config: dict[str, Any] | None = None, + ): if not payload: return None - config = payload.get("config") or {"configurable": {"thread_id": payload.get("thread_id")}} - checkpoint = payload.get("checkpoint") or {} - metadata = payload.get("metadata") or {} - parent_config = payload.get("parent_config") - pending_writes = _normalize_pending_writes(payload.get("pending_writes") or []) + # Second-stage protection: never re-bind the full persisted RunnableConfig. + # Rebuild only the durable identifiers, as official LangGraph savers do. + config = _canonical_checkpoint_config(payload, request_config) + checkpoint = _strip_runtime_refs(_normalize_checkpoint(payload.get("checkpoint") or {})) + metadata = _strip_runtime_refs(_normalize_metadata(payload.get("metadata") or {})) + raw_parent_config = payload.get("parent_config") + if isinstance(raw_parent_config, dict): + parent_payload = { + "thread_id": payload.get("thread_id"), + "config": raw_parent_config, + "checkpoint_id": (raw_parent_config.get("configurable") or {}).get("checkpoint_id") + if isinstance(raw_parent_config.get("configurable"), dict) + else None, + "checkpoint": {}, + } + parent_config = _canonical_checkpoint_config(parent_payload) + else: + parent_config = None + pending_writes = _normalize_pending_writes( + _strip_runtime_refs(payload.get("pending_writes") or []) + ) try: from langgraph.checkpoint.base import CheckpointTuple return CheckpointTuple(config=config, checkpoint=checkpoint, metadata=metadata, parent_config=parent_config, pending_writes=pending_writes) except Exception: return { - "config": config, + "config": _durable_config(config), "checkpoint": checkpoint, "metadata": metadata, "parent_config": parent_config, @@ -121,7 +333,10 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver): } async def aget_tuple(self, config: dict[str, Any]): - return self._make_tuple(await self.repository.get_latest(_thread_id(config))) + return self._make_tuple( + await self.repository.get_latest(_thread_id(config)), + request_config=config, + ) def get_tuple(self, config: dict[str, Any]): return self._run(self.aget_tuple(config)) @@ -129,20 +344,24 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver): async def aput(self, config: dict[str, Any], checkpoint: dict[str, Any], metadata: dict[str, Any] | None = None, new_versions: dict[str, Any] | None = None): thread_id = _thread_id(config) checkpoint_id = _checkpoint_id(checkpoint) + clean_config = _durable_config(config) + clean_cfg = clean_config.get("configurable") if isinstance(clean_config.get("configurable"), dict) else {} + checkpoint_ns = str(clean_cfg.get("checkpoint_ns") or "") + # Return a fresh canonical config. Never feed process-local/factory-time + # configurable values back into the next LangGraph super-step. next_config = { - **(config or {}), "configurable": { - **((config or {}).get("configurable") or {}), "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, "checkpoint_id": checkpoint_id, - }, + } } await self.repository.put(thread_id, { "thread_id": thread_id, - "config": _jsonable(next_config), - "checkpoint": _jsonable(checkpoint), - "metadata": _jsonable(metadata or {}), - "new_versions": _jsonable(new_versions or {}), + "config": _strict_json_value(next_config, path="$.config"), + "checkpoint": _strict_json_value(_strip_runtime_refs(_normalize_checkpoint(checkpoint)), path="$.checkpoint"), + "metadata": _strict_json_value(_strip_runtime_refs(_normalize_metadata(metadata or {})), path="$.metadata"), + "new_versions": _strict_json_value(_strip_runtime_refs(new_versions or {}), path="$.new_versions"), "checkpoint_id": checkpoint_id, }) return next_config @@ -153,19 +372,46 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver): async def aput_writes(self, config: dict[str, Any], writes: list[tuple[str, Any]], task_id: str, task_path: str = ""): thread_id = _thread_id(config) try: - latest = await self.repository.get_latest(thread_id) or {"thread_id": thread_id, "config": config, "checkpoint": {}, "metadata": {}} + latest = await self.repository.get_latest(thread_id) or {"thread_id": thread_id, "config": _durable_config(config), "checkpoint": {}, "metadata": {}} except: latest = { "thread_id": thread_id, - "config": config, + "config": _durable_config(config), "checkpoint": {}, "metadata": {}, "pending_writes": [], } + if isinstance(latest, dict): + # Do not keep extending a persisted RunnableConfig across super-steps. + # Rebuild the same canonical config that aget_tuple() will expose. + latest["config"] = _canonical_checkpoint_config(latest, config) + if isinstance(latest.get("checkpoint"), dict): + latest["checkpoint"] = _strip_runtime_refs(latest.get("checkpoint")) + if isinstance(latest.get("metadata"), dict): + latest["metadata"] = _strip_runtime_refs(latest.get("metadata")) + if isinstance(latest.get("parent_config"), dict): + parent_payload = { + "thread_id": latest.get("thread_id") or thread_id, + "config": latest.get("parent_config"), + "checkpoint_id": (latest.get("parent_config", {}).get("configurable") or {}).get("checkpoint_id") + if isinstance(latest.get("parent_config", {}).get("configurable"), dict) + else None, + "checkpoint": {}, + } + latest["parent_config"] = _canonical_checkpoint_config(parent_payload) + pending = list(latest.get("pending_writes") or []) for channel, value in writes or []: - pending.append({"task_id": task_id, "task_path": task_path, "channel": channel, "value": _jsonable(value)}) + # Writes may contain nested task/RunnableConfig fragments. Scrub the + # private runtime before the repository's JSON ``default=str`` layer. + durable_value = _strip_runtime_refs(value) + pending.append({ + "task_id": task_id, + "task_path": task_path, + "channel": channel, + "value": _strict_json_value(durable_value, path=f"$.pending_writes[{task_id}].{channel}"), + }) latest["pending_writes"] = pending await self.repository.put(thread_id, latest) diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc index 2856a11..411fc5a 100644 Binary files a/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc index fd10918..05536b0 100644 Binary files a/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc index 9c90361..2f31bcb 100644 Binary files a/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc index 7433adb..74fb001 100644 Binary files a/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc differ 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 51f3e08..23860d7 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 19056a9..2edc15f 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 7209c91..a9157af 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 04f2fb9..39187c0 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 1e4e58a..28ff7e9 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 ca5282b..58271f2 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 46650f6..5663c4b 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 97b40a2..a4f78f1 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 bd73f4d..e30bb5f 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 b0e3656..a73de07 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 3f748c3..a3f7962 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 7f4c449..f8bb661 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 b29a51e..ade4d43 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 8e0d5c6..784931e 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 37afb50..4eb13fa 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 8a12af0..de98b7b 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 6a12982..789cc88 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 b1a0a48..25f6e32 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 0a6c4de..05cedc1 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 daf58a0..308a422 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 86d11da..3f9be8a 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 5031535..4c92a6e 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 bbb1dd2..94358bc 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 dbe933a..5e26da3 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 9cf0d63..f593963 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 3c0e22e..6205861 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 b0fb0ea..ca8b372 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 3d477c6..0fbad9d 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 92e0354..5fd4841 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 4874115..f5c3f71 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 a0040d2..aad36c5 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 b77dc68..87c729a 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 72e1252..19f9644 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 6453cc8..5dc208b 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 50cd18f..a5804aa 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 4a5691a..96c0db9 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 d1242c3..4161dd3 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/prompts/fallback.py b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/fallback.py index ef0a4f3..b0fc7e8 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/fallback.py +++ b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/fallback.py @@ -80,6 +80,13 @@ _REWRITE_INSTRUCTIONS_BY_CODE: dict[str, str] = { "pergunta de confirmação direta e curta, mencionando o serviço ou ação " "pendente. Sem executar nem prometer ação." ), + "FRASEOLOGIA": ( + "Preserve integralmente os fatos, valores, nomes de produtos e o resultado " + "de negócio já informado. Reescreva SOMENTE o trecho apontado como " + "fraseologia inadequada, trocando vocabulário de implementação, processo " + "interno, categoria técnica ou operação por linguagem natural de cliente. " + "Não invente ação, não altere o resultado e não acrescente oferta." + ), } diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/fraseologia.py b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/fraseologia.py index b22e574..3958af7 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/fraseologia.py +++ b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/fraseologia.py @@ -48,8 +48,13 @@ A) Termos e rotulos proibidos (o cliente nao deve ouvi-los): so pelo nome e valor. a menos que seja perguntado diretamente sobre. Alguns itens possuem o nome parecido com códigos, como BEMOBI_GAM ESMENSALM São PERMITIDOS. Pois seu nome do produto é dessa forma. - A3. nomes de ferramentas/tools, JSON, chaves tecnicas, checklist interno ou - raciocinio expostos ao cliente -> falar so o resultado, em linguagem natural. + A3. nomes de ferramentas/tools, JSON, chaves tecnicas, parametros/chaves de + implementacao, checklist interno, estados do workflow ou raciocinio interno + expostos ao cliente -> falar so o resultado ou fazer a pergunta necessaria + em linguagem natural. Exemplos de termos internos proibidos: "subject", + "asset_id", "invoice_id", "tool", "workflow", "route", "intent", + "COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION" e nomes de tools como + "cancelar_vas_avulso" / "contestar_cobranca". A4. Dizer que vai encaminhar uma jornada adequada, dizer que vai encaminhar para um especialista. Preferivel dizer que não pode ajudar sobre isso A5. Dizer que está "fora do escopo". Preferivel dizer "Sobre X não posso ajudar com isso" @@ -64,7 +69,9 @@ B) Construcoes proibidas: B4. orientar o cliente a procurar atendimento ou outro canal: "entre em contato com a central", "ligue para o atendimento", "fale com um atendente", "procure uma loja", "acesse o app/site para resolver" -> resolver a duvida - aqui mesmo, sem encaminhar o cliente para outro canal. + aqui mesmo, sem encaminhar o cliente para outro canal. ATENCAO: pedir para + o cliente tentar ou solicitar novamente NESTA MESMA CONVERSA, sem citar + central, loja, app, site, telefone, atendente ou outro canal, NAO viola B4. C) Ofertas e promessas proibidas (revisao humana — sobrepoe outros rails): C1. oferecer plano mais barato, troca, migracao ou rebaixe de plano (inclusive @@ -72,6 +79,20 @@ C) Ofertas e promessas proibidas (revisao humana — sobrepoe outros rails): C2. conceder ressarcimento em dobro -> usar a fala fixa de ajuste na fatura. NAO marque FRASEOLOGIA (fraseados OBRIGATORIOS — sempre OK): + - perguntas ou pedidos de DADOS DE NEGOCIO que o cliente conhece e que sao + necessarios para continuar o atendimento. Isso NAO expoe raciocinio nem + processo interno. Exemplos SEMPRE OK: "Para prosseguir, informe valor.", + "Qual foi o valor da cobranca?", "Informe a data da cobranca.", + "Qual servico voce deseja cancelar?", "Qual e o nome do produto?". + Nao confunda o nome natural do dado de negocio ("valor", "data", "servico", + "cobranca", "fatura", "produto") com o nome tecnico da chave interna + ("subject", "asset_id", "invoice_id" etc.). + - confirmacoes de uma acao ja em andamento em linguagem natural, por exemplo + "Voce confirma o cancelamento do servico TIM Fashion?", sao interacao normal + com o cliente e NAO constituem exposicao de processo interno. + - em caso de falha tecnica, orientar a repetir a mesma solicitacao aqui mesmo, + por exemplo "Se desejar tentar novamente, solicite o cancelamento novamente", + e permitido; isso NAO e encaminhamento para outro canal. - "incluso no seu plano" / "faz parte do seu plano" / "beneficio incluso". - citar o servico por nome e valor SEM rotulo de origem. - a fala fixa de ressarcimento ("Por aqui, nao consigo seguir com o 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 d9822e6..a71492a 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 e4b2094..f8db266 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 4fb33dd..6bfa669 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 cce3597..522e165 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 6825de9..9cf7e4a 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 2ba9ca2..85dd15e 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 a2818e9..c5f6bb3 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 9938053..72cf505 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 9756229..e4886f3 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 1325f82..3e8b4d3 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 a6ebb2e..2cbd176 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 c6b77b7..e47902a 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 41623f5..021976a 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/output_supervisor.py b/libs/agent_framework/src/agent_framework/guardrails/output_supervisor.py index 3d51a57..0cced3f 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/output_supervisor.py +++ b/libs/agent_framework/src/agent_framework/guardrails/output_supervisor.py @@ -10,6 +10,7 @@ from .rail_result import RailResult from .parallel_executor import ParallelRailExecutor from .llm_rails import LLMOutputGRLRail from .config_loader import load_guardrails_config +from .framework_llm_client import classify_with_framework_llm logger = logging.getLogger("agent_framework.guardrails.output_supervisor") @@ -122,11 +123,81 @@ class OutputSupervisor: ) ) + # FRASEOLOGIA é um rail de wording. Quando ele for o único rail impeditivo, + # não descarte uma resposta factual/grounded: faça uma única reescrita + # cirúrgica, depois submeta o texto reescrito a TODOS os rails novamente. + # A flag no contexto impede loop infinito caso a nova versão continue + # inadequada. + phraseology_block = next( + (r for r in results if str(r.code or "").upper() == "FRASEOLOGIA" and r.action == RailAction.BLOCK), + None, + ) + other_impediments = [ + r for r in results + if r is not phraseology_block and r.action in {RailAction.BLOCK, RailAction.RETRY, RailAction.HANDOVER} + ] + if ( + phraseology_block is not None + and not other_impediments + and int(ctx.get("__phraseology_rewrite_attempt", 0)) < 1 + ): + rewritten = await self._rewrite_phraseology(candidate, phraseology_block, ctx) + if rewritten and rewritten.strip() and rewritten.strip() != candidate.strip(): + rewrite_ctx = dict(ctx) + rewrite_ctx["__phraseology_rewrite_attempt"] = 1 + rewrite_ctx["phraseology_original_candidate"] = candidate + rewrite_ctx["phraseology_original_reason"] = phraseology_block.reason + decision = await self.evaluate(rewritten.strip(), rewrite_ctx) + decision.results.insert(0, RailResult( + code="FRASEOLOGIA_REWRITE", + action=RailAction.OBSERVE, + reason=phraseology_block.reason, + metadata={ + "rewritten": True, + "original_code": "FRASEOLOGIA", + "rewrite_attempt": 1, + }, + )) + decision.metadata = { + **dict(decision.metadata or {}), + "phraseology_rewritten": True, + "phraseology_rewrite_attempts": 1, + } + return decision + decision = self.aggregate(candidate, list(results), ctx) await self._emit_events(results, decision, ctx) await self._emit_final(decision, ctx) return decision + + async def _rewrite_phraseology(self, candidate: str, result: RailResult, context: dict[str, Any]) -> str | None: + """Reescreve apenas wording bloqueado por FRASEOLOGIA. + + A saída é sempre reavaliada por ``evaluate`` antes de ser liberada. Uma + falha do LLM ou uma resposta vazia mantém o comportamento fail-closed. + """ + try: + rewrite_context = { + **dict(context or {}), + "guardrail_code": "FRASEOLOGIA", + "guardrail_reason": result.reason, + } + out = await classify_with_framework_llm( + self.llm, + "FALLBACK", + {"text": candidate, "context": rewrite_context}, + profile_name="grl", + component_name="guardrail.fraseologia.rewrite", + generation_name="guardrail.fraseologia.rewrite", + ) + # O prompt FALLBACK usa ``reason`` como texto final reescrito. + rewritten = str(out.get("reason") or "").strip() + return rewritten or None + except Exception: + logger.exception("output_supervisor.phraseology_rewrite_failed") + return None + def aggregate(self, candidate: str, results: list[RailResult], context: dict[str, Any] | None = None) -> RailDecisionV2: ctx = context or {} final_action = max((r.action for r in results), key=lambda a: _SEVERITY.get(a, 0), default=RailAction.ALLOW) diff --git a/libs/agent_framework/src/agent_framework/guardrails/rails.py b/libs/agent_framework/src/agent_framework/guardrails/rails.py index 87f4d73..4385aaa 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/rails.py +++ b/libs/agent_framework/src/agent_framework/guardrails/rails.py @@ -304,13 +304,55 @@ class PrematureActionRail(Guardrail): class ProactiveOfferRail(Guardrail): - """AOFERTA calibrado: bloqueia oferta proativa não solicitada no output.""" + """AOFERTA calibrado: bloqueia oferta proativa não solicitada no output. + + Estados transacionais determinísticos de continuidade não são uma nova + oferta do agente. Quando o runtime já abriu uma transação e está apenas + coletando parâmetros obrigatórios ou aguardando confirmação, AOFERTA deve + permitir a mensagem sem consultar a LLM. Outros rails de saída (por exemplo + FRASEOLOGIA) continuam sendo executados normalmente pelo pipeline. + """ code = "AOFERTA" stage = "output" + _TRANSACTION_CONTINUATION_STATUSES = { + "COLLECTING_PARAMETERS", + "AWAITING_CONFIRMATION", + } + + @classmethod + def _transaction_continuation_status(cls, ctx: dict[str, Any]) -> str | None: + status = str(ctx.get("transaction_status") or "").strip().upper() + if status in cls._TRANSACTION_CONTINUATION_STATUSES: + return status + + # Compatibilidade com callers que ainda só expõem o estado por meio + # dos resultados das tools. O runtime transacional já grava o status + # nesses resultados; não inferimos pelo texto da resposta. + for result in reversed(list(ctx.get("mcp_results") or ctx.get("tool_result") or [])): + if not isinstance(result, dict): + continue + result_status = str(result.get("transaction_status") or "").strip().upper() + if result_status in cls._TRANSACTION_CONTINUATION_STATUSES: + return result_status + return None async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision: ctx = _ctx(context) + continuation_status = self._transaction_continuation_status(ctx) + if continuation_status: + return RailDecision( + code=self.code, + allowed=True, + reason=f"continuidade_transacional:{continuation_status}", + sanitized_text=text, + metadata={ + "mechanism": "deterministic_transaction_bypass", + "transaction_status": continuation_status, + "calibrated": True, + }, + ) + out = await classify_with_framework_llm( _llm(ctx), "AOFERTA", diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc index ba70cc0..09f9ffe 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc index b69fa7e..f9247b5 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc index 05071d9..4c3f3db 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc index 6f31f4f..8e4c636 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc index 8733137..ba5a5f7 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc index 5b24247..72d653d 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc index 6c6c73d..58bc8a5 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc index 1a0447a..0c59cbd 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc index 595a70f..9ee624a 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc index eaf8835..8cb3e8c 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc index f25e0ed..871ad16 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc index 060f7f7..e2648af 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc index ced5c6b..201d849 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc index 5e8670c..04ec9f9 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc index a7bace4..0c921a8 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc index b55c8a9..be1bf7e 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc index a486198..966e33d 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc index a238f49..8b44c2d 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc index 752144c..9f43626 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc index f65a9e0..9243caa 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc index 89e23e2..2becb01 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc index cd84394..8062ace 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc index 3698cc2..f887600 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc index 2db8c51..05dbb4e 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc index 6adc99c..a1cfcae 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc index 94b3b4a..5cca607 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc index 11b6171..049ad41 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc index c0cca06..a8ce304 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc index 949ef27..6e18c2e 100644 Binary files a/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc index ab679cf..f302fad 100644 Binary files a/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc index 72ac825..046c99a 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc index f2ba742..4c822b0 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc index d93429b..53b9e9d 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc index 59a5556..2ff7251 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc index b1e8e40..ffe0633 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc index bcc0193..2dc36a2 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc index 65dd8b4..fbc7007 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc index 4e41728..eaa3aa8 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc index a1b9044..458ccc5 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc index 82b7d2e..5d7ac01 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc index 7aaaea4..d61cc2d 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc index 3ddcc0c..567886a 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc index f016ac9..987dba7 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc index ab8e5c0..428dcdd 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc index cc59659..08f74ea 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc index 3dbb05c..0639755 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc index 62566a2..41b5b31 100644 Binary files a/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc index a249a40..80c7b09 100644 Binary files a/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc index 3f35524..d667b10 100644 Binary files a/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc b/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc index f8bb2a6..0d68175 100644 Binary files a/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc index ec9d3cd..970155f 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc index 49957a6..a326aa9 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc index 57e1cdc..bd793b6 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc index 83dc9ec..dbfa17d 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc index c096706..a055948 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc index bdb0968..b727ba0 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc index 632eff8..7d0829e 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc index 05e03cf..4d35623 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc index 47f8d97..eb19e90 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc index b827d3a..590a07f 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc index 278abed..4ee49bc 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py index 7c01d37..a731756 100644 --- a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py +++ b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py @@ -54,7 +54,20 @@ class EnterpriseRouter: async def route(self, state: dict[str, Any]) -> RouteDecision: session = (state.get("context") or {}).get("session", {}) or {} - current_state = state.get("next_state") or session.get("metadata", {}).get("workflow_state") + explicit_next_state = state.get("next_state") + tx_status_at_route = str(state.get("transaction_status") or "").strip().upper() + terminal_tx = tx_status_at_route in {"COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"} + + # Um status transacional terminal é a fonte de verdade sobre o latch. Se + # um checkpoint legado/parcial ainda trouxer ``next_state`` da transação + # encerrada, esse valor não pode aprisionar a próxima mensagem na política + # de estado. O workflow_state da sessão continua disponível porque pode + # representar um workflow conversacional independente da transação já + # encerrada. + if terminal_tx and explicit_next_state: + current_state = session.get("metadata", {}).get("workflow_state") + else: + current_state = explicit_next_state or session.get("metadata", {}).get("workflow_state") text = state.get("sanitized_input") or state.get("user_text") or "" # Estados transacionais preservam continuidade para respostas curtas @@ -107,6 +120,20 @@ class EnterpriseRouter: await self._emit(interruption, state) return interruption + # A transação continua ativa e a mensagem NÃO representa mudança de + # intenção. Neste caso a decisão sintética de estado precisa vencer + # route stickiness/continuity. Antes, o código apenas verificava uma + # possível interrupção e, na ausência dela, caía adiante no LLM de + # continuidade. Isso fazia respostas de parâmetro (ex.: ``R$ 71,99``) + # perderem o latch determinístico da transação e reiniciarem a seleção + # da tool. + synthetic.metadata = { + **(synthetic.metadata or {}), + "transaction_state_recovered": True, + } + await self._emit(synthetic, state) + return synthetic + # Mensagens que expressam de forma explícita uma intenção diferente da # intent/agente ativos devem prevalecer sobre a route stickiness. Isso # evita manter um fluxo read-only (por exemplo, tracking) quando o usuário @@ -119,7 +146,6 @@ class EnterpriseRouter: active_agent and keyword_candidate is not None and keyword_candidate.intent != previous_intent - and self._is_explicit_intent_shift(keyword_candidate) ): keyword_candidate.metadata = { **(keyword_candidate.metadata or {}), @@ -167,133 +193,129 @@ class EnterpriseRouter: text: str, state_decision: RouteDecision, ) -> RouteDecision | None: - """Detecta uma nova intenção durante coleta/confirmação transacional. + """Detecta semanticamente mudança de intenção durante uma transação. - A política de estado continua protegendo respostas curtas/ambíguas. Uma - interrupção só é aceita quando o roteador encontra uma intent diferente - da transação ativa. Keywords explícitas são preferidas; na ausência delas, - o LLM router pode decidir, desde que atinja o limiar configurado. - - O método não limpa o estado por conta própria. Ele marca a decisão com - ``transaction_interruption=intent_shift`` para que o runtime feche o latch - transacional de maneira centralizada e auditável. + Não existe lista de palavras para desistência ou mudança de assunto. Uma + interrupção nasce de uma intent diferente resolvida por uma keyword + configurada no ``routing.yaml`` ou, na ausência dela, por uma decisão + semântica do LLM com o contexto da transação pendente. """ active_tx = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {} started_intent = str(active_tx.get("started_from_intent") or "").strip() previous = state.get("route_decision") or {} previous_intent = str(previous.get("intent") or state.get("intent") or started_intent).strip() - # Parameter answers have precedence over intent-shift detection. A reply - # such as ``R$ 71,99`` after the framework asked for ``valor`` is not a - # new billing-explanation intent; it is the missing transaction argument. - # Likewise, short entity-like replies (``TIM Music``) should remain in the - # pending transaction. Only clear question/action language is allowed to - # escape to the router. - missing = [str(x) for x in (state.get("missing_parameters") or []) if str(x).strip()] - if missing and self._looks_like_pending_parameter_answer(text, missing): - return None - candidate = self._route_by_keyword(text) if candidate is not None: - explicit = self._is_explicit_intent_shift(candidate) different = ( candidate.agent != state_decision.agent or (started_intent and candidate.intent != started_intent) or (previous_intent and not previous_intent.startswith("state:") and candidate.intent != previous_intent) ) - if explicit and different: + if different: candidate.metadata = { **(candidate.metadata or {}), "transaction_interruption": "intent_shift", "interrupted_state": state_decision.next_state, "interrupted_agent": state_decision.agent, "interrupted_intent": started_intent or previous_intent, + "interruption_source": "configured_routing", } return candidate - # Uma keyword reconhecida para a própria transação deve continuar - # protegida pelo estado; não há necessidade de chamar o LLM. - if not different: - return None + return None if not (self.enable_llm_router and self.llm is not None): return None - try: - candidate = await self._route_by_llm(text, state) - except Exception as exc: - logger.warning("Falha ao avaliar interrupção de estado transacional via LLM: %s", exc) - return None - different = ( - candidate.agent != state_decision.agent - or (started_intent and candidate.intent != started_intent) - or (previous_intent and not previous_intent.startswith("state:") and candidate.intent != previous_intent) - ) - if not different or candidate.confidence < self.intent_shift_threshold: - return None - - candidate.metadata = { - **(candidate.metadata or {}), - "transaction_interruption": "intent_shift", - "interrupted_state": state_decision.next_state, - "interrupted_agent": state_decision.agent, - "interrupted_intent": started_intent or previous_intent, + allowed = [i for i in self.intents if i.enabled] + allowed_payload = [ + { + "intent": i.name, + "agent": i.agent, + "description": i.description, + "examples": i.examples[:3], + "domain": i.domain, + } + for i in allowed + ] + transaction_context = { + "current_agent": state_decision.agent, + "current_intent": started_intent or previous_intent, + "transaction_status": state.get("transaction_status"), + "tool_name": active_tx.get("tool_name"), + "missing_parameters": list(state.get("missing_parameters") or []), } + system = ( + "Você decide apenas se o turno atual continua a transação ativa ou muda de intenção. " + "Use o significado da mensagem e o contexto transacional; não use palavras isoladas como regra. " + "Se a mensagem responde ao dado/confirmacao pendente, retorne CONTINUE. " + "Se o usuário passou a perseguir outro objetivo, retorne SHIFT e a nova intent permitida. " + "Retorne somente JSON válido com decision, intent, agent, confidence, reason." + ) + user = { + "message": text, + "transaction": transaction_context, + "allowed_intents": allowed_payload, + "session_context": (state.get("context") or {}).get("session", {}), + } + try: + answer = await self.llm.ainvoke( + [ + {"role": "system", "content": system}, + {"role": "user", "content": json.dumps(user, ensure_ascii=False)}, + ], + temperature=0.0, + max_tokens=512, + profile_name="router", + component_name="router", + generation_name="llm.transaction_intent_shift", + ) + data = self._parse_json(answer) + except Exception as exc: + logger.warning("Falha ao avaliar mudança semântica de intent transacional via LLM: %s", exc) + return None + + if str(data.get("decision") or "").strip().upper() != "SHIFT": + return None + confidence = float(data.get("confidence") or 0.0) + if confidence < self.intent_shift_threshold: + return None + + intent_name = str(data.get("intent") or "").strip() + if not intent_name or intent_name == (started_intent or previous_intent): + return None + agent = str(data.get("agent") or self._agent_for_intent(intent_name) or "").strip() + if not agent: + return None + + candidate = RouteDecision( + route=agent, + agent=agent, + intent=intent_name, + confidence=confidence, + reason=str(data.get("reason") or "Mudança semântica de intenção durante transação."), + method="llm", + metadata={ + "transaction_interruption": "intent_shift", + "interrupted_state": state_decision.next_state, + "interrupted_agent": state_decision.agent, + "interrupted_intent": started_intent or previous_intent, + "interruption_source": "semantic_classifier", + "raw_llm_answer": answer[:1000], + }, + domain=self._domain_for_intent(intent_name), + mcp_tools=self._tools_for_intent(intent_name), + ) return candidate - @staticmethod - def _looks_like_pending_parameter_answer(text: str, missing_parameters: list[str]) -> bool: - """Return True when the current turn looks like an answer to a missing field. - - This is intentionally conservative: structured values are recognized by - parameter semantics, while a short entity-like phrase is accepted only if - it does not contain interrogative or explicit topic-shift language. - """ - raw = str(text or "").strip() - if not raw: - return False - normalized = unicodedata.normalize("NFKD", raw.lower()) - normalized = "".join(ch for ch in normalized if not unicodedata.combining(ch)) - normalized = re.sub(r"\s+", " ", normalized).strip() - names = {re.sub(r"[^a-z0-9_]+", "", str(x).lower()) for x in missing_parameters} - - # Explicit conversational shifts/questions must still escape the transaction. - shift_tokens = ( - "quero ", "gostaria ", "preciso ", "me diga", "me informe", "mostre", - "qual ", "quais ", "quando ", "onde ", "como ", "porque ", "por que ", - "esquece", "esqueca", "mudei de assunto", "outra coisa", "voltar para", - "cancelar ", "cancele ", "consultar ", "ver minha", "ver meu", - ) - if "?" in raw or any(tok in normalized for tok in shift_tokens): - return False - - # Common structured parameter families. - if names & {"valor", "value", "amount", "price", "preco", "preço"}: - if re.fullmatch(r"(?:r\$\s*)?[+-]?\d{1,3}(?:[.\s]\d{3})*(?:,\d{1,2})?|(?:r\$\s*)?[+-]?\d+(?:[.,]\d{1,2})?", normalized, flags=re.I): - return True - if names & {"order_id", "pedido", "pedido_id", "invoice_id", "fatura_id", "protocol", "protocolo", "protocolo_id"}: - if re.fullmatch(r"[a-z0-9._/-]{2,64}", normalized, flags=re.I): - return True - if names & {"data", "date", "due_date", "vencimento"}: - if re.fullmatch(r"\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?", normalized): - return True - - # A single short noun/entity phrase is usually the requested subject/reason. - # Keep it in the transaction unless it contains clear routing language. - words = re.findall(r"[a-z0-9$+.,/_-]+", normalized) - if len(missing_parameters) == 1 and 1 <= len(words) <= 5: - return True - return False - @staticmethod def _is_explicit_intent_shift(decision: RouteDecision) -> bool: - """Retorna True para matches explícitos que devem vencer a stickiness. + """Compatibilidade: keyword configurada é um sinal explícito de routing. - A regra é configurável porque usa a keyword já declarada no routing.yaml, - sem listas linguísticas fixas no código. Keywords com quatro ou mais - caracteres são tratadas como sinais explícitos de mudança de intenção. + Não há regra por conteúdo ou tamanho da keyword; o framework confia na + configuração do domínio. """ - matched = str((decision.metadata or {}).get("matched_keyword") or "").strip() - return len(matched) >= 4 + return decision.method == "keyword" and bool(str((decision.metadata or {}).get("matched_keyword") or "").strip()) def _route_by_state(self, current_state: str | None) -> RouteDecision | None: if not current_state: @@ -457,14 +479,26 @@ class EnterpriseRouter: ] system = ( "Você é um roteador de intenções para uma plataforma de agentes. " - "Classifique a mensagem do usuário em uma das intents permitidas. " + "Classifique semanticamente a mensagem do usuário em uma das intents permitidas. " + "Quando houver uma transação ativa, considere a intent que iniciou a transação, " + "o estado transacional e os parâmetros ainda pendentes. Se a mensagem apenas " + "responder ao que está pendente, mantenha a intent da transação. Se o usuário " + "passar a perseguir outro objetivo, classifique a nova intent. " "Retorne somente JSON válido com: intent, agent, confidence, reason. " "Não responda ao usuário final." ) + active_tx = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {} + transaction_context = { + "status": state.get("transaction_status"), + "started_from_intent": active_tx.get("started_from_intent"), + "tool_name": active_tx.get("tool_name"), + "missing_parameters": list(state.get("missing_parameters") or []), + } if active_tx else None user = { "message": text, "allowed_intents": allowed_payload, "session_context": (state.get("context") or {}).get("session", {}), + "transaction_context": transaction_context, } answer = await self.llm.ainvoke( [ 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 8ba4883..8c97908 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 f47313e..9d6a067 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/agent_runtime.py b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py index 7c7ee90..9006bd8 100644 --- a/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py +++ b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py @@ -1231,30 +1231,6 @@ class AgentRuntimeMixin: ) return result - @staticmethod - def _transaction_cancel_requested(text: str) -> bool: - """Reconhece cancelamento explícito de uma transação pendente. - - Mantém o conjunto deliberadamente pequeno para não confundir uma nova - ação de negócio (ex.: "cancelar meu serviço") com o cancelamento do - próprio fluxo pendente. - """ - normalized = " ".join(str(text or "").strip().casefold().split()) - exact = { - "cancelar", "cancele", "cancela", "desistir", "desisto", - "não quero continuar", "nao quero continuar", - "não quero mais", "nao quero mais", - "deixa pra lá", "deixa pra la", - } - if normalized in exact: - return True - contextual = ( - r"^(?:cancele|cancela|cancelar)\s+(?:essa|esta|a|o)?\s*(?:operação|operacao|solicitação|solicitacao|transação|transacao|pedido|processo)(?:\s+anterior)?$", - r"^(?:não|nao)\s+quero\s+mais\s+(?:contestar|continuar|prosseguir)(?:\s+.*)?$", - r"^(?:esqueça|esqueca)\s+(?:essa|esta|a)?\s*(?:solicitação|solicitacao|operação|operacao|transação|transacao)(?:\s+anterior)?$", - ) - return any(re.match(pattern, normalized) for pattern in contextual) - @staticmethod def _confirmation_decision(text: str) -> str | None: normalized = " ".join((text or "").strip().lower().split()) @@ -2072,28 +2048,19 @@ class AgentRuntimeMixin: self._normalize_transaction_lifecycle(state) # Uma transação em coleta/confirmação não pode aprisionar a sessão. O - # EnterpriseRouter sinaliza uma nova intent explícita em metadata; além - # disso, cancelamentos explícitos devem funcionar também durante coleta - # de parâmetros (antes, só funcionavam em AWAITING_CONFIRMATION). + # EnterpriseRouter é a única fonte para interrupção por mudança de intent. + # Não existe interpretação lexical de desistência no runtime: mudou a + # intent, a transação anterior é encerrada e seus latches são limpos. active_before_interruption = self._active_transaction(state) route_meta = (state.get("route_decision") or {}).get("metadata") or {} interruption = str(route_meta.get("transaction_interruption") or "").strip().lower() - cancel_requested = self._transaction_cancel_requested(str(text)) - if active_before_interruption and (interruption == "intent_shift" or cancel_requested): + if active_before_interruption and interruption == "intent_shift": interrupted_tool = active_before_interruption.get("tool_name") self._finish_active_transaction(state, "CANCELLED") state["tool_policy_result"] = { - "action": "cancelled_by_intent_shift" if interruption == "intent_shift" else "cancelled_by_user", + "action": "cancelled_by_intent_shift", "tool_name": interrupted_tool, } - if cancel_requested and interruption != "intent_shift": - return [{ - "ok": True, - "tool_name": interrupted_tool, - "transaction_status": "CANCELLED", - "cancelled": True, - "reason": "user_cancelled_pending_transaction", - }] # Clarificação de resultado de tool tem precedência: reutiliza a mesma tool # e argumentos, alterando apenas o parâmetro escolhido pelo usuário. diff --git a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc index 58bef23..575e044 100644 Binary files a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc index 2f03c93..c0e787c 100644 Binary files a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc index a9b3dd4..d95a6a7 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc index 797c7f4..b389a45 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc index 2742377..15964e2 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc index eea775f..d9cddca 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc index 8fe5fa3..27e8cfa 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc index 4f14383..5d38905 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc index 7c022b3..0eddccb 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/runtime.py b/libs/agent_framework/src/agent_framework/workflows/runtime.py index f818233..d721fb6 100644 --- a/libs/agent_framework/src/agent_framework/workflows/runtime.py +++ b/libs/agent_framework/src/agent_framework/workflows/runtime.py @@ -1,6 +1,8 @@ from __future__ import annotations import inspect +import logging +import traceback from copy import deepcopy from typing import Any from uuid import uuid4 @@ -9,6 +11,8 @@ from .models import WorkflowDefinition, WorkflowPause, WorkflowRunResult from .registry import DEFAULT_WORKFLOW_ACTIONS, WorkflowActionRegistry from .repository import FileWorkflowRepository +logger = logging.getLogger(__name__) + def _resolve(path: Any, state: dict[str, Any]) -> Any: if not isinstance(path, str) or not path.startswith("$."): @@ -88,9 +92,54 @@ def _normalize_resume(value: Any, pause: WorkflowPause) -> Any: return normalized -def _exception_details(exc: Exception) -> dict[str, Any]: - """Preserve structured external-error facts without coupling the framework to a provider.""" - details: dict[str, Any] = {"type": type(exc).__name__} +def _type_shape(value: Any, *, depth: int = 0, max_depth: int = 4) -> Any: + """Return a value-free type map suitable for runtime diagnostics. + + We intentionally do not serialize values here: RunnableConfig may contain + process-local LangGraph objects and customer/business data. The diagnostic + only exposes keys, container sizes and Python type names. + """ + if depth >= max_depth: + return {"type": type(value).__name__} + if isinstance(value, dict): + return { + "type": "dict", + "size": len(value), + "keys": {str(k): _type_shape(v, depth=depth + 1, max_depth=max_depth) for k, v in value.items()}, + } + if isinstance(value, (list, tuple)): + sample = list(value[:5]) if isinstance(value, tuple) else value[:5] + return { + "type": type(value).__name__, + "size": len(value), + "items": [_type_shape(v, depth=depth + 1, max_depth=max_depth) for v in sample], + } + return {"type": type(value).__name__} + + +def _runtime_versions() -> dict[str, str]: + versions: dict[str, str] = {} + try: + from importlib.metadata import version + + for package in ("langgraph", "langgraph-checkpoint", "langchain-core"): + try: + versions[package] = version(package) + except Exception: + pass + except Exception: + pass + return versions + + +def _exception_details(exc: Exception, *, runtime_context: dict[str, Any] | None = None) -> dict[str, Any]: + """Preserve structured error facts plus a traceback for workflow runtime failures.""" + details: dict[str, Any] = { + "type": type(exc).__name__, + "traceback": "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), + } + if runtime_context: + details["runtime_diagnostics"] = runtime_context for attr in ("status_code", "body", "attempts", "code", "metadata"): value = getattr(exc, attr, None) if value not in (None, "", [], {}): @@ -123,6 +172,15 @@ class WorkflowRuntime: self._compiled: dict[tuple[str, int], Any] = {} self._fallback_paused: dict[str, dict[str, Any]] = {} + def _runtime_diagnostics(self, *, graph: Any | None, config: dict[str, Any], phase: str) -> dict[str, Any]: + return { + "phase": phase, + "config_shape": _type_shape(config), + "graph_type": type(graph).__name__ if graph is not None else None, + "checkpointer_type": type(self.checkpointer).__name__ if self.checkpointer is not None else None, + "versions": _runtime_versions(), + } + def _outgoing(self, definition: WorkflowDefinition) -> dict[str, list[Any]]: outgoing: dict[str, list[Any]] = {} for edge in definition.edges: @@ -439,9 +497,13 @@ class WorkflowRuntime: import langgraph # noqa: F401 except ModuleNotFoundError: return await self._run_fallback(definition, initial, start_node=definition.start, execution_id=eid) + phase = "compile" try: graph = self._compile(definition) + phase = "ainvoke" + logger.debug("workflow_langgraph_before_ainvoke diagnostics=%s", self._runtime_diagnostics(graph=graph, config=config, phase=phase)) state = await graph.ainvoke(initial, config=config) + phase = "aget_state" snapshot = await graph.aget_state(config) if getattr(snapshot, "next", None): interrupts = [] @@ -482,7 +544,12 @@ class WorkflowRuntime: workflow_version=definition.version, status="FAILED", error=str(exc), - error_details=_exception_details(exc), + error_details=_exception_details( + exc, + runtime_context=self._runtime_diagnostics( + graph=locals().get("graph"), config=config, phase=locals().get("phase", "unknown") + ), + ), output=dict(partial.get("nodes") or {}), state=partial, trace=list(partial.get("trace") or []), @@ -507,9 +574,13 @@ class WorkflowRuntime: from langgraph.types import Command except ModuleNotFoundError as exc: raise ModuleNotFoundError("langgraph não está instalado") from exc + phase = "compile" try: graph = self._compile(definition) + phase = "ainvoke_resume" + logger.debug("workflow_langgraph_before_resume diagnostics=%s", self._runtime_diagnostics(graph=graph, config=config, phase=phase)) state = await graph.ainvoke(Command(resume=resume_value), config=config) + phase = "aget_state_resume" snapshot = await graph.aget_state(config) if getattr(snapshot, "next", None): interrupts = [] @@ -545,7 +616,12 @@ class WorkflowRuntime: workflow_version=definition.version, status="FAILED", error=str(exc), - error_details=_exception_details(exc), + error_details=_exception_details( + exc, + runtime_context=self._runtime_diagnostics( + graph=locals().get("graph"), config=config, phase=locals().get("phase", "unknown") + ), + ), output=dict(partial.get("nodes") or {}), state=partial, trace=list(partial.get("trace") or []), diff --git a/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc index d7275c4..6ffeb33 100644 Binary files a/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc and b/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_aoferta_transaction_continuation.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_aoferta_transaction_continuation.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000..b047262 Binary files /dev/null and b/tests/__pycache__/test_aoferta_transaction_continuation.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_phraseology_rewrite_revalidation.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_phraseology_rewrite_revalidation.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000..6fa7a4b Binary files /dev/null and b/tests/__pycache__/test_phraseology_rewrite_revalidation.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_route_stickiness_transaction_shift.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_route_stickiness_transaction_shift.cpython-313-pytest-9.0.2.pyc index aebaf3a..3bb1c66 100644 Binary files a/tests/__pycache__/test_route_stickiness_transaction_shift.cpython-313-pytest-9.0.2.pyc and b/tests/__pycache__/test_route_stickiness_transaction_shift.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_transaction_confirmation_customer_facing.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_transaction_confirmation_customer_facing.cpython-313-pytest-9.0.2.pyc index 5f8dc49..29f28f7 100644 Binary files a/tests/__pycache__/test_transaction_confirmation_customer_facing.cpython-313-pytest-9.0.2.pyc and b/tests/__pycache__/test_transaction_confirmation_customer_facing.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_transaction_state_regression_matrix.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_transaction_state_regression_matrix.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000..dd780db Binary files /dev/null and b/tests/__pycache__/test_transaction_state_regression_matrix.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_transaction_state_router_interruption.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_transaction_state_router_interruption.cpython-313-pytest-9.0.2.pyc index a5d8faa..5029b3b 100644 Binary files a/tests/__pycache__/test_transaction_state_router_interruption.cpython-313-pytest-9.0.2.pyc and b/tests/__pycache__/test_transaction_state_router_interruption.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_transactional_tool_flow.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_transactional_tool_flow.cpython-313-pytest-9.0.2.pyc index 643fed4..2abadb1 100644 Binary files a/tests/__pycache__/test_transactional_tool_flow.cpython-313-pytest-9.0.2.pyc and b/tests/__pycache__/test_transactional_tool_flow.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/test_aoferta_transaction_continuation.py b/tests/test_aoferta_transaction_continuation.py new file mode 100644 index 0000000..37bf869 --- /dev/null +++ b/tests/test_aoferta_transaction_continuation.py @@ -0,0 +1,75 @@ +import pytest + +from agent_framework.guardrails.rails import ProactiveOfferRail + + +@pytest.mark.asyncio +@pytest.mark.parametrize("status", ["COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"]) +async def test_aoferta_bypasses_transaction_continuation_without_llm(monkeypatch, status): + async def should_not_run(*args, **kwargs): + raise AssertionError("AOFERTA LLM must not run for transaction continuation") + + monkeypatch.setattr( + "agent_framework.guardrails.rails.classify_with_framework_llm", + should_not_run, + ) + + decision = await ProactiveOfferRail().evaluate( + "Para prosseguir, informe valor.", + {"transaction_status": status}, + ) + + assert decision.allowed is True + assert decision.code == "AOFERTA" + assert decision.metadata["mechanism"] == "deterministic_transaction_bypass" + assert decision.metadata["transaction_status"] == status + + +@pytest.mark.asyncio +async def test_aoferta_detects_transaction_continuation_from_mcp_results(monkeypatch): + async def should_not_run(*args, **kwargs): + raise AssertionError("AOFERTA LLM must not run for transaction continuation") + + monkeypatch.setattr( + "agent_framework.guardrails.rails.classify_with_framework_llm", + should_not_run, + ) + + decision = await ProactiveOfferRail().evaluate( + "Você confirma o cancelamento do serviço TIM Fashion?", + { + "mcp_results": [ + { + "tool_name": "cancelar_vas_avulso", + "awaiting_confirmation": True, + "transaction_status": "AWAITING_CONFIRMATION", + } + ] + }, + ) + + assert decision.allowed is True + assert decision.metadata["transaction_status"] == "AWAITING_CONFIRMATION" + + +@pytest.mark.asyncio +async def test_aoferta_still_calls_llm_outside_transaction_continuation(monkeypatch): + calls = [] + + async def fake_classify(*args, **kwargs): + calls.append((args, kwargs)) + return {"allowed": False, "reason": "oferta proativa"} + + monkeypatch.setattr( + "agent_framework.guardrails.rails.classify_with_framework_llm", + fake_classify, + ) + + decision = await ProactiveOfferRail().evaluate( + "Quer aproveitar e cancelar outro serviço?", + {"transaction_status": "COMPLETED"}, + ) + + assert len(calls) == 1 + assert decision.allowed is False + assert decision.metadata["mechanism"] == "llm_supervisor" diff --git a/tests/test_fraseologia_business_parameter_prompt.py b/tests/test_fraseologia_business_parameter_prompt.py new file mode 100644 index 0000000..d621878 --- /dev/null +++ b/tests/test_fraseologia_business_parameter_prompt.py @@ -0,0 +1,23 @@ +from agent_framework.guardrails.calibrated.prompts.fraseologia import build_fraseologia_prompt + + +def test_fraseologia_prompt_allows_business_parameter_collection(): + prompt = build_fraseologia_prompt("[ContestacaoAgent] Para prosseguir, informe valor.") + assert '"Para prosseguir, informe valor."' in prompt + assert "DADOS DE NEGOCIO" in prompt + assert "NAO expoe raciocinio" in prompt + + +def test_fraseologia_prompt_distinguishes_business_data_from_internal_keys(): + prompt = build_fraseologia_prompt("Informe subject.") + assert '"subject"' in prompt + assert '"asset_id"' in prompt + assert '"invoice_id"' in prompt + assert '"COLLECTING_PARAMETERS"' in prompt + assert '"AWAITING_CONFIRMATION"' in prompt + + +def test_fraseologia_prompt_keeps_natural_transaction_confirmation_allowed(): + prompt = build_fraseologia_prompt("Voce confirma o cancelamento do servico TIM Fashion?") + assert "confirmacoes de uma acao ja em andamento" in prompt + assert "TIM Fashion" in prompt diff --git a/tests/test_phraseology_rewrite_revalidation.py b/tests/test_phraseology_rewrite_revalidation.py new file mode 100644 index 0000000..00d6989 --- /dev/null +++ b/tests/test_phraseology_rewrite_revalidation.py @@ -0,0 +1,114 @@ +import pytest + +from agent_framework.guardrails.base import RailDecision +from agent_framework.guardrails.output_supervisor import OutputSupervisor +from agent_framework.guardrails.rail_action import RailAction + + +class PhraseologyRail: + code = "FRASEOLOGIA" + stage = "output" + + def __init__(self): + self.calls = [] + + async def evaluate(self, text, context): + self.calls.append(text) + blocked = "categoria tratável por esta operação" in text + return RailDecision( + code=self.code, + allowed=not blocked, + reason=( + "remova linguagem interna" if blocked else "" + ), + sanitized_text=text, + metadata={"calibrated": True}, + ) + + +class AllowRail: + code = "AOFERTA" + stage = "output" + + def __init__(self): + self.calls = [] + + async def evaluate(self, text, context): + self.calls.append(text) + return RailDecision( + code=self.code, + allowed=True, + reason="", + sanitized_text=text, + metadata={"calibrated": True}, + ) + + +@pytest.mark.asyncio +async def test_phraseology_block_is_rewritten_once_and_all_rails_are_revalidated(monkeypatch): + phrase = PhraseologyRail() + allow = AllowRail() + + async def fake_classify(llm, task, payload, **kwargs): + assert task == "FALLBACK" + assert payload["context"]["guardrail_code"] == "FRASEOLOGIA" + return { + "allowed": True, + "label": "FALLBACK", + "reason": ( + '[ContestacaoAgent] O item "TIM CTRL Redes Sociais 8.0" no valor de ' + 'R$ 71,99 é um plano da sua fatura e, por isso, não pode ser ' + 'contestado como serviço adicional.' + ), + } + + monkeypatch.setattr( + "agent_framework.guardrails.output_supervisor.classify_with_framework_llm", + fake_classify, + ) + + supervisor = OutputSupervisor( + rails=[allow, phrase], + enable_parallel=False, + llm=object(), + ) + original = ( + '[ContestacaoAgent] O item "TIM CTRL Redes Sociais 8.0" no valor de ' + 'R$ 71,99 é classificado como plano na sua fatura. Não é possível ' + 'contestar esse tipo de cobrança por este canal, pois planos não pertencem ' + 'a uma categoria tratável por esta operação.' + ) + + decision = await supervisor.evaluate(original, {}) + + assert decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE} + assert "categoria tratável por esta operação" not in decision.candidate + assert "TIM CTRL Redes Sociais 8.0" in decision.candidate + assert "R$ 71,99" in decision.candidate + assert len(phrase.calls) == 2 + assert len(allow.calls) == 2 + assert decision.metadata["phraseology_rewritten"] is True + assert any(r.code == "FRASEOLOGIA_REWRITE" for r in decision.results) + + +@pytest.mark.asyncio +async def test_phraseology_rewrite_does_not_loop_when_rewritten_text_is_still_blocked(monkeypatch): + phrase = PhraseologyRail() + + async def fake_classify(llm, task, payload, **kwargs): + return { + "allowed": True, + "label": "FALLBACK", + "reason": payload["text"] + " ", + } + + monkeypatch.setattr( + "agent_framework.guardrails.output_supervisor.classify_with_framework_llm", + fake_classify, + ) + supervisor = OutputSupervisor(rails=[phrase], enable_parallel=False, llm=object()) + original = "planos não pertencem a uma categoria tratável por esta operação" + + decision = await supervisor.evaluate(original, {}) + assert decision.action == RailAction.BLOCK + assert len(phrase.calls) == 1 diff --git a/tests/test_route_stickiness_transaction_shift.py b/tests/test_route_stickiness_transaction_shift.py index 261c677..3ce6135 100644 --- a/tests/test_route_stickiness_transaction_shift.py +++ b/tests/test_route_stickiness_transaction_shift.py @@ -10,9 +10,9 @@ def test_explicit_keyword_shift_preempts_stickiness(): assert EnterpriseRouter._is_explicit_intent_shift(d) is True -def test_short_generic_keyword_does_not_preempt(): +def test_configured_keyword_is_explicit_regardless_of_literal_length(): d = RouteDecision(route="x", agent="x", intent="x", method="keyword", metadata={"matched_keyword": "id"}) - assert EnterpriseRouter._is_explicit_intent_shift(d) is False + assert EnterpriseRouter._is_explicit_intent_shift(d) is True def test_same_agent_explicit_intent_shift_must_preempt_stickiness(): diff --git a/tests/test_transaction_state_regression_matrix.py b/tests/test_transaction_state_regression_matrix.py new file mode 100644 index 0000000..cf63fe0 --- /dev/null +++ b/tests/test_transaction_state_regression_matrix.py @@ -0,0 +1,397 @@ +from types import SimpleNamespace + +import pytest + +from agent_framework.routing.enterprise_router import EnterpriseRouter +from agent_framework.runtime.agent_runtime import AgentRuntimeMixin + + +ROUTING_YAML = """ +router: + fallback_agent: billing_agent + confidence_threshold: 0.70 +state_policies: + - state: COLLECTING_ORDER_PARAMETERS + agent: orders_agent + - state: WAITING_ORDER_CONFIRMATION + agent: orders_agent +intents: + - name: retail_order_cancel + domain: retail + agent: orders_agent + priority: 30 + keywords: [cancelar pedido] + - name: retail_order_tracking + domain: retail + agent: orders_agent + priority: 20 + keywords: [rastrear pedido, pedido] + - name: billing_invoice_explanation + domain: telecom + agent: billing_agent + priority: 40 + keywords: [fatura, vencimento] +""" + + +def _router(tmp_path, *, stickiness=True): + routing = tmp_path / "routing.yaml" + routing.write_text(ROUTING_YAML, encoding="utf-8") + settings = SimpleNamespace( + ROUTING_CONFIG_PATH=str(routing), + ENABLE_LLM_ROUTER=False, + ENABLE_ROUTE_STICKINESS=stickiness, + ) + return EnterpriseRouter(settings) + + +def _active_tx(status="COLLECTING_PARAMETERS", arguments=None): + return { + "tool_name": "cancelar_pedido", + "status": status, + "started_from_intent": "retail_order_cancel", + "arguments": dict(arguments or {"order_id": "PED-1001"}), + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("message", ["PED-1001", "12345", "R$ 71,99", "10/09/2026"]) +async def test_matrix_collecting_parameter_answers_keep_transaction(message, tmp_path): + router = _router(tmp_path) + state = { + "user_text": message, + "sanitized_input": message, + "next_state": "COLLECTING_ORDER_PARAMETERS", + "transaction_status": "COLLECTING_PARAMETERS", + "missing_parameters": ["order_id"], + "active_transaction": _active_tx(arguments={}), + "active_agent": "orders_agent", + "intent": "state:COLLECTING_ORDER_PARAMETERS", + "route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"}, + } + decision = await router.route(state) + assert decision.method == "state" + assert decision.agent == "orders_agent" + assert (decision.metadata or {}).get("transaction_interruption") is None + + +@pytest.mark.asyncio +async def test_matrix_missing_next_state_recovers_active_transaction_before_stickiness(tmp_path): + router = _router(tmp_path) + state = { + "user_text": "PED-1001", + "sanitized_input": "PED-1001", + "next_state": None, + "transaction_status": "COLLECTING_PARAMETERS", + "missing_parameters": ["order_id"], + "selected_tool_call": {"tool_name": "cancelar_pedido", "arguments": {}}, + "active_transaction": _active_tx(arguments={}), + "active_agent": "orders_agent", + "route": "orders_agent", + "route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"}, + } + decision = await router.route(state) + assert decision.method == "state" + assert decision.metadata["transaction_state_recovered"] is True + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "status", + ["COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"], +) +async def test_matrix_terminal_transaction_never_recovers_from_latch(status, tmp_path): + router = _router(tmp_path, stickiness=False) + state = { + "user_text": "quero ver minha fatura", + "sanitized_input": "quero ver minha fatura", + "next_state": None, + "transaction_status": status, + # Deliberately stale latch: terminal status must win. + "active_transaction": _active_tx(status=status), + "selected_tool_call": {"tool_name": "cancelar_pedido", "arguments": {"order_id": "PED-1001"}}, + "active_agent": "orders_agent", + "intent": "retail_order_cancel", + "route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"}, + } + decision = await router.route(state) + assert decision.intent == "billing_invoice_explanation" + assert (decision.metadata or {}).get("transaction_state_recovered") is not True + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "status", + ["COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"], +) +async def test_matrix_terminal_transaction_ignores_stale_next_state(status, tmp_path): + router = _router(tmp_path, stickiness=False) + state = { + "user_text": "quero ver minha fatura", + "sanitized_input": "quero ver minha fatura", + # Simulate a partially persisted/legacy state where terminal status was + # written but the transactional next_state was not cleared. + "next_state": "COLLECTING_ORDER_PARAMETERS", + "transaction_status": status, + "active_transaction": _active_tx(status=status), + "active_agent": "orders_agent", + "intent": "retail_order_cancel", + "route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"}, + } + decision = await router.route(state) + assert decision.intent == "billing_invoice_explanation" + assert decision.agent == "billing_agent" + assert decision.method != "state" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "status,next_state", + [ + ("COLLECTING_PARAMETERS", "COLLECTING_ORDER_PARAMETERS"), + ("AWAITING_CONFIRMATION", "WAITING_ORDER_CONFIRMATION"), + ], +) +async def test_matrix_clear_intent_shift_preempts_active_transaction(status, next_state, tmp_path): + router = _router(tmp_path) + state = { + "user_text": "esquece, quero ver minha fatura", + "sanitized_input": "esquece, quero ver minha fatura", + "next_state": next_state, + "transaction_status": status, + "active_transaction": _active_tx(status=status), + "active_agent": "orders_agent", + "intent": f"state:{next_state}", + "route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"}, + } + decision = await router.route(state) + assert decision.intent == "billing_invoice_explanation" + assert decision.agent == "billing_agent" + assert decision.metadata["transaction_interruption"] == "intent_shift" + + +@pytest.mark.asyncio +async def test_matrix_route_stickiness_without_transaction_preserves_previous_behavior(tmp_path): + router = _router(tmp_path) + # Generic short follow-up: no transaction latch, no explicit new intent. + state = { + "user_text": "e o status?", + "sanitized_input": "e o status?", + "next_state": None, + "transaction_status": None, + "active_transaction": None, + "active_agent": "orders_agent", + "intent": "retail_order_tracking", + "route_decision": { + "route": "orders_agent", + "agent": "orders_agent", + "intent": "retail_order_tracking", + "domain": "retail", + "mcp_tools": [], + }, + "context": {"session": {}}, + } + decision = await router.route(state) + # Semantic continuity may be disabled by settings/profile defaults in the + # isolated test; regardless, transaction recovery must not be involved. + assert (decision.metadata or {}).get("transaction_state_recovered") is not True + + +class _PolicyRouter: + def __init__(self): + self.registry = SimpleNamespace( + tools={"cancelar_pedido": object()}, + get_tool=lambda name: SimpleNamespace(selection_keywords=["cancelar pedido", "cancelar"]), + ) + + def resolve_execution_policy(self, tool_name, arguments=None): + return { + "operation_type": "transactional", + "require_confirmation": True, + "requires": ["order_id"], + "policy_source": "matrix-test", + } + + def validate_execution_policy(self, tool_name, arguments=None): + return True, None, self.resolve_execution_policy(tool_name, arguments) + + +class _Runtime(AgentRuntimeMixin): + def __init__(self, *, call_ok=True): + self.tool_router = _PolicyRouter() + self.calls = [] + self.call_ok = call_ok + + async def _call_mcp_tool(self, tool_name, arguments, state): + self.calls.append((tool_name, dict(arguments))) + return {"ok": self.call_ok, "tool_name": tool_name, "result": {"status": "DONE" if self.call_ok else "ERROR"}} + + +@pytest.mark.asyncio +async def test_matrix_confirmation_yes_completes_and_clears_latches(): + runtime = _Runtime(call_ok=True) + state = { + "user_text": "sim", + "sanitized_input": "sim", + "transaction_status": "AWAITING_CONFIRMATION", + "active_transaction": _active_tx(status="AWAITING_CONFIRMATION"), + "pending_tool_call": {"tool_name": "cancelar_pedido", "arguments": {"order_id": "PED-1001"}}, + } + result = await runtime.execute_tools_for_intent(state, tools=[]) + assert result[-1]["ok"] is True + assert state["transaction_status"] == "COMPLETED" + assert state["active_transaction"] is None + assert state["next_state"] is None + + +@pytest.mark.asyncio +async def test_matrix_confirmation_no_cancels_and_clears_latches(): + runtime = _Runtime(call_ok=True) + state = { + "user_text": "não", + "sanitized_input": "não", + "transaction_status": "AWAITING_CONFIRMATION", + "active_transaction": _active_tx(status="AWAITING_CONFIRMATION"), + "pending_tool_call": {"tool_name": "cancelar_pedido", "arguments": {"order_id": "PED-1001"}}, + } + result = await runtime.execute_tools_for_intent(state, tools=[]) + assert result[-1]["transaction_status"] == "CANCELLED" + assert state["transaction_status"] == "CANCELLED" + assert state["active_transaction"] is None + assert state["next_state"] is None + assert runtime.calls == [] + + +@pytest.mark.asyncio +async def test_matrix_runtime_does_not_cancel_from_literal_words_without_intent_shift(): + runtime = _Runtime(call_ok=True) + state = { + "user_text": "texto livre sem classificação de nova intent", + "sanitized_input": "texto livre sem classificação de nova intent", + "transaction_status": "COLLECTING_PARAMETERS", + "active_transaction": _active_tx(status="COLLECTING_PARAMETERS", arguments={}), + "selected_tool_call": {"tool_name": "cancelar_pedido", "arguments": {}}, + "missing_parameters": ["order_id"], + } + await runtime.execute_tools_for_intent(state, tools=[]) + assert state["transaction_status"] != "CANCELLED" + assert state["active_transaction"] is not None + + +@pytest.mark.asyncio +async def test_matrix_intent_shift_cancels_old_transaction_before_new_tool_path(): + runtime = _Runtime(call_ok=True) + state = { + "user_text": "quero ver minha fatura", + "sanitized_input": "quero ver minha fatura", + "transaction_status": "COLLECTING_PARAMETERS", + "active_transaction": _active_tx(status="COLLECTING_PARAMETERS", arguments={}), + "selected_tool_call": {"tool_name": "cancelar_pedido", "arguments": {}}, + "missing_parameters": ["order_id"], + "route_decision": {"metadata": {"transaction_interruption": "intent_shift"}}, + } + await runtime.execute_tools_for_intent(state, tools=[]) + assert state["transaction_status"] == "CANCELLED" + assert state["active_transaction"] is None + assert state["tool_policy_result"]["action"] == "cancelled_by_intent_shift" + + +@pytest.mark.asyncio +async def test_matrix_intent_shift_clears_arguments_and_restart_starts_from_zero(): + runtime = _Runtime(call_ok=True) + state = { + "user_text": "nova intenção", + "sanitized_input": "nova intenção", + "transaction_status": "COLLECTING_PARAMETERS", + "active_transaction": _active_tx( + status="COLLECTING_PARAMETERS", + arguments={"order_id": "PED-OLD"}, + ), + "selected_tool_call": { + "tool_name": "cancelar_pedido", + "arguments": {"order_id": "PED-OLD"}, + }, + "pending_tool_call": { + "tool_name": "cancelar_pedido", + "arguments": {"order_id": "PED-OLD"}, + }, + "missing_parameters": [], + "route_decision": {"metadata": {"transaction_interruption": "intent_shift"}}, + } + await runtime.execute_tools_for_intent(state, tools=[]) + + assert state["transaction_status"] == "CANCELLED" + assert state["active_transaction"] is None + assert state["selected_tool_call"] == {} + assert state["pending_tool_call"] == {} + assert state["missing_parameters"] == [] + assert state["next_state"] is None + assert state["last_transaction"]["arguments"] == {"order_id": "PED-OLD"} + + # Se a intent antiga voltar depois, o histórico fica apenas em last_transaction; + # nenhum argumento operacional é restaurado para a nova transação. + state["transaction_status"] = None + state["route_decision"] = {} + state["user_text"] = "iniciar novamente" + state["sanitized_input"] = "iniciar novamente" + state["mcp_tools"] = ["cancelar_pedido"] + await runtime.execute_tools_for_intent(state, tools=["cancelar_pedido"]) + assert state.get("selected_tool_call", {}).get("arguments", {}).get("order_id") != "PED-OLD" + assert (state.get("active_transaction") or {}).get("arguments", {}).get("order_id") != "PED-OLD" + + +@pytest.mark.asyncio +async def test_matrix_tool_failure_is_terminal_and_not_reactivated(): + runtime = _Runtime(call_ok=False) + state = { + "user_text": "sim", + "sanitized_input": "sim", + "transaction_status": "AWAITING_CONFIRMATION", + "active_transaction": _active_tx(status="AWAITING_CONFIRMATION"), + "pending_tool_call": {"tool_name": "cancelar_pedido", "arguments": {"order_id": "PED-1001"}}, + } + await runtime.execute_tools_for_intent(state, tools=[]) + assert state["transaction_status"] == "FAILED" + assert state["active_transaction"] is None + assert state["pending_tool_call"] == {} + assert state["next_state"] is None + + +@pytest.mark.parametrize("status", ["COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"]) +def test_matrix_normalization_clears_terminal_operational_latches(status): + runtime = _Runtime() + state = { + "transaction_status": status, + "next_state": "COLLECTING_ORDER_PARAMETERS", + "active_transaction": _active_tx(status=status), + "selected_tool_call": {"tool_name": "cancelar_pedido", "arguments": {"order_id": "PED-1001"}}, + "pending_tool_call": {"tool_name": "cancelar_pedido", "arguments": {"order_id": "PED-1001"}}, + "missing_parameters": ["order_id"], + "confirmation_required": True, + } + runtime._normalize_transaction_lifecycle(state) + assert state["active_transaction"] is None + assert state["selected_tool_call"] == {} + assert state["pending_tool_call"] == {} + assert state["missing_parameters"] == [] + assert state["next_state"] is None + assert state["last_transaction"]["status"] == status + +@pytest.mark.asyncio +@pytest.mark.parametrize("status", ["COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"]) +async def test_matrix_terminal_stale_next_state_does_not_lock_generic_followup(status, tmp_path): + router = _router(tmp_path, stickiness=False) + state = { + "user_text": "ok", + "sanitized_input": "ok", + "next_state": "COLLECTING_ORDER_PARAMETERS", + "transaction_status": status, + "active_transaction": _active_tx(status=status), + "active_agent": "orders_agent", + "intent": "retail_order_cancel", + "route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"}, + "context": {"session": {}}, + } + decision = await router.route(state) + assert decision.method != "state" + assert (decision.metadata or {}).get("transaction_state_recovered") is not True diff --git a/tests/test_transaction_state_router_interruption.py b/tests/test_transaction_state_router_interruption.py index 7cca021..f9eff09 100644 --- a/tests/test_transaction_state_router_interruption.py +++ b/tests/test_transaction_state_router_interruption.py @@ -7,7 +7,7 @@ from agent_framework.routing.enterprise_router import EnterpriseRouter class _LLM: async def ainvoke(self, messages, **kwargs): - return '{"intent":"contas_vas_information","agent":"vas_agent","confidence":0.96,"reason":"nova consulta de serviços"}' + return '{"decision":"SHIFT","intent":"contas_vas_information","agent":"vas_agent","confidence":0.96,"reason":"nova consulta de serviços"}' @pytest.mark.asyncio @@ -86,7 +86,7 @@ intents: class _LowConfidenceLLM: async def ainvoke(self, messages, **kwargs): - return '{"intent":"contas_vas_information","agent":"vas_agent","confidence":0.30,"reason":"incerto"}' + return '{"decision":"SHIFT","intent":"contas_vas_information","agent":"vas_agent","confidence":0.30,"reason":"incerto"}' settings = SimpleNamespace( ROUTING_CONFIG_PATH=str(routing), @@ -320,3 +320,76 @@ intents: decision = await router.route(state) assert (decision.metadata or {}).get("transaction_interruption") is None + +@pytest.mark.asyncio +async def test_missing_next_state_parameter_answer_recovers_transaction_state_before_continuity(tmp_path): + """Regressão: parâmetro de uma transação ativa não pode cair no route-continuity. + + Reproduz o caso Contas de forma genérica: o latch transacional sobreviveu ao + checkpoint, ``next_state`` não, e o usuário fornece exatamente o parâmetro + faltante. A decisão deve continuar determinística (method=state), preservando + a tool ativa para o AgentRuntime completar os argumentos já coletados. + """ + routing = tmp_path / "routing.yaml" + routing.write_text( + """ +router: + fallback_agent: contestacao_agent + confidence_threshold: 0.70 +state_policies: [] +intents: + - name: contas_contestation + agent: contestacao_agent + priority: 20 + keywords: [nao contratei] +""", + encoding="utf-8", + ) + settings = SimpleNamespace( + ROUTING_CONFIG_PATH=str(routing), + ENABLE_LLM_ROUTER=False, + ENABLE_ROUTE_STICKINESS=True, + ) + router = EnterpriseRouter(settings) + state = { + "user_text": "R$ 71,99", + "sanitized_input": "R$ 71,99", + "next_state": None, + "transaction_status": "COLLECTING_PARAMETERS", + "missing_parameters": ["valor"], + "selected_tool_call": { + "tool_name": "contestar_cobranca", + "arguments": {"subject": "Plano Exemplo"}, + }, + "active_agent": "contestacao_agent", + "route": "contestacao_agent", + "route_decision": { + "route": "contestacao_agent", + "agent": "contestacao_agent", + "intent": "contas_contestation", + }, + } + + decision = await router.route(state) + + assert decision.method == "state" + assert decision.agent == "contestacao_agent" + assert decision.intent == "state:COLLECTING_PARAMETERS" + assert decision.next_state == "COLLECTING_PARAMETERS" + assert decision.metadata["transaction_state_recovered"] is True + + +def test_agent_state_declares_durable_transaction_latch(): + """O schema do host deve manter os campos que o AgentRuntime persiste.""" + import importlib.util + from pathlib import Path + + state_path = Path(__file__).parents[2] / "app" / "state.py" + spec = importlib.util.spec_from_file_location("contas_agent_state_v10", state_path) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + + annotations = module.AgentState.__annotations__ + assert "active_transaction" in annotations + assert "last_transaction" in annotations diff --git a/tests/test_transactional_tool_flow.py b/tests/test_transactional_tool_flow.py index 6a38d56..049ff7b 100644 --- a/tests/test_transactional_tool_flow.py +++ b/tests/test_transactional_tool_flow.py @@ -572,8 +572,8 @@ async def test_pre_validation_rejection_clears_collecting_latches_and_is_exposed async def test_collecting_parameters_can_be_cancelled_explicitly(): runtime = _ContestRuntime() state = { - "user_text": "cancele essa operação anterior", - "sanitized_input": "cancele essa operação anterior", + "user_text": "nova intenção classificada pelo router", + "sanitized_input": "nova intenção classificada pelo router", "route": "contestacao_agent", "intent": "state:COLLECTING_CONTESTACAO_PARAMETERS", "transaction_status": "COLLECTING_PARAMETERS", @@ -587,12 +587,12 @@ async def test_collecting_parameters_can_be_cancelled_explicitly(): "selected_tool_call": {"tool_name": "contestar_cobranca", "arguments": {}}, "missing_parameters": ["subject"], "next_state": "COLLECTING_CONTESTACAO_PARAMETERS", + "route_decision": {"metadata": {"transaction_interruption": "intent_shift"}}, } result = await runtime.execute_tools_for_intent(state, tools=[]) - assert result[-1]["transaction_status"] == "CANCELLED" - assert result[-1]["cancelled"] is True + assert result == [] assert state["transaction_status"] == "CANCELLED" assert state["active_transaction"] is None assert state["next_state"] is None diff --git a/tests/unit/__pycache__/test_langgraph_checkpoint_runtime_config.cpython-313-pytest-9.0.2.pyc b/tests/unit/__pycache__/test_langgraph_checkpoint_runtime_config.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000..35dd15d Binary files /dev/null and b/tests/unit/__pycache__/test_langgraph_checkpoint_runtime_config.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/unit/__pycache__/test_langgraph_checkpoint_saver.cpython-313-pytest-9.0.2.pyc b/tests/unit/__pycache__/test_langgraph_checkpoint_saver.cpython-313-pytest-9.0.2.pyc index 3fdd435..648fe5f 100644 Binary files a/tests/unit/__pycache__/test_langgraph_checkpoint_saver.cpython-313-pytest-9.0.2.pyc and b/tests/unit/__pycache__/test_langgraph_checkpoint_saver.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/unit/__pycache__/test_workflow_runtime_diagnostics.cpython-313-pytest-9.0.2.pyc b/tests/unit/__pycache__/test_workflow_runtime_diagnostics.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000..cbeca66 Binary files /dev/null and b/tests/unit/__pycache__/test_workflow_runtime_diagnostics.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/unit/test_langgraph_checkpoint_runtime_config.py b/tests/unit/test_langgraph_checkpoint_runtime_config.py index fd0f6f5..4561861 100644 --- a/tests/unit/test_langgraph_checkpoint_runtime_config.py +++ b/tests/unit/test_langgraph_checkpoint_runtime_config.py @@ -52,7 +52,7 @@ def test_saver_strips_private_runtime_before_put_and_restore() -> None: checkpoint = {"id": "cp1", "v": 1} returned = asyncio.run(saver.aput(config, checkpoint, {}, {})) - assert returned["configurable"] == {"thread_id": "t1", "checkpoint_id": "cp1"} + assert returned["configurable"] == {"thread_id": "t1", "checkpoint_ns": "", "checkpoint_id": "cp1"} assert repo.saved is not None persisted = repo.saved[1] assert "__pregel_runtime" not in persisted["config"]["configurable"] @@ -62,3 +62,134 @@ def test_saver_strips_private_runtime_before_put_and_restore() -> None: restored = saver._make_tuple(persisted) restored_config = restored.config if hasattr(restored, "config") else restored["config"] assert "__pregel_runtime" not in restored_config["configurable"] + + +def test_nested_pregel_runtime_is_removed_from_checkpoint_and_pending_writes() -> None: + repo = _Repo() + saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo) + config = {"configurable": {"thread_id": "t-nested"}} + checkpoint = { + "id": "cp-nested", + "v": 1, + "channel_values": { + "__pregel_tasks": {"durable": True}, + "task": { + "config": { + "configurable": { + "thread_id": "t-nested", + "__pregel_runtime": "Runtime(...) nested", + "business_key": "kept", + } + } + } + }, + } + + asyncio.run(saver.aput(config, checkpoint, {}, {})) + asyncio.run( + saver.aput_writes( + config, + [ + ( + "tasks", + { + "nested": { + "configurable": { + "__pregel_runtime": "Runtime(...) write", + "subject": "TIM Fashion", + } + } + }, + ) + ], + "task-1", + ) + ) + + assert repo.saved is not None + persisted = repo.saved[1] + assert persisted["checkpoint"]["channel_values"]["__pregel_tasks"] == {"durable": True} + task_config = persisted["checkpoint"]["channel_values"]["task"]["config"]["configurable"] + assert "__pregel_runtime" not in task_config + assert task_config["business_key"] == "kept" + pending_cfg = persisted["pending_writes"][0]["value"]["nested"]["configurable"] + assert "__pregel_runtime" not in pending_cfg + assert pending_cfg["subject"] == "TIM Fashion" + + +def test_restore_rebuilds_fresh_canonical_config_instead_of_rebinding_stored_config() -> None: + repo = _Repo() + saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo) + legacy_payload = { + "thread_id": "thread-legacy", + "checkpoint_id": "cp-legacy", + "config": { + "tags": ["old-run"], + "configurable": { + "thread_id": "thread-legacy", + "checkpoint_ns": "wf", + "checkpoint_id": "cp-legacy", + "tenant": "default", + "__pregel_runtime": "stringified-runtime", + "__pregel_store": "stringified-store", + }, + }, + "checkpoint": {"id": "cp-legacy", "v": 1}, + "metadata": {}, + } + asyncio.run(repo.put("thread-legacy", legacy_payload)) + + # Simulate a new invocation. Runtime/private keys supplied by a previous run + # must never be rebound from the persisted checkpoint tuple. + request = { + "tags": ["new-run"], + "configurable": { + "thread_id": "thread-legacy", + "checkpoint_ns": "wf", + "__pregel_runtime": "request-runtime-placeholder", + }, + } + restored = asyncio.run(saver.aget_tuple(request)) + restored_config = restored.config if hasattr(restored, "config") else restored["config"] + + assert restored_config == { + "configurable": { + "thread_id": "thread-legacy", + "checkpoint_ns": "wf", + "checkpoint_id": "cp-legacy", + } + } + assert "tags" not in restored_config + assert "tenant" not in restored_config["configurable"] + assert "__pregel_runtime" not in restored_config["configurable"] + + +def test_legacy_checkpoint_structural_json_strings_are_recovered() -> None: + from agent_framework.checkpoints.langgraph_saver import _normalize_checkpoint, _normalize_config + + legacy = { + "v": 1, + "id": "cp-json", + "channel_values": '{"state":{"ok":true}}', + "channel_versions": '{"state":"0001"}', + "versions_seen": '{"node":"{\\"state\\":\\"0001\\"}"}', + } + normalized = _normalize_checkpoint(legacy) + assert normalized["channel_values"] == {"state": {"ok": True}} + assert normalized["channel_versions"] == {"state": "0001"} + assert normalized["versions_seen"] == {"node": {"state": "0001"}} + + cfg = _normalize_config({"configurable": '{"thread_id":"t1","checkpoint_ns":""}'}) + assert cfg["configurable"] == {"thread_id": "t1", "checkpoint_ns": ""} + + +def test_checkpoint_serialization_never_silently_stringifies_unknown_objects() -> None: + import pytest + from agent_framework.checkpoints.langgraph_saver import _strict_json_value + + class RuntimeLike: + def override(self): + return self + + with pytest.raises(TypeError, match="Checkpoint contém valor não serializável"): + _strict_json_value({"channel_values": {"bad": RuntimeLike()}}, path="$.checkpoint") diff --git a/tests/unit/test_workflow_runtime_diagnostics.py b/tests/unit/test_workflow_runtime_diagnostics.py new file mode 100644 index 0000000..56d1608 --- /dev/null +++ b/tests/unit/test_workflow_runtime_diagnostics.py @@ -0,0 +1,26 @@ +from agent_framework.workflows.runtime import _exception_details, _type_shape + + +def _boom(): + raise AttributeError("'str' object has no attribute 'override'") + + +def test_exception_details_contains_traceback(): + try: + _boom() + except Exception as exc: + details = _exception_details(exc, runtime_context={"phase": "ainvoke"}) + assert details["type"] == "AttributeError" + assert "_boom" in details["traceback"] + assert "override" in details["traceback"] + assert details["runtime_diagnostics"]["phase"] == "ainvoke" + + +def test_type_shape_exposes_types_not_values(): + shape = _type_shape({"configurable": {"thread_id": "secret-thread", "__pregel_runtime": "bad-runtime"}}) + text = repr(shape) + assert "thread_id" in text + assert "__pregel_runtime" in text + assert "secret-thread" not in text + assert "bad-runtime" not in text + assert "str" in text