From 903e4f47e0df67b211675ced3e9f69b369affe06 Mon Sep 17 00:00:00 2001 From: zhangwenjun-huawei Date: Mon, 26 Oct 2026 11:25:53 +0800 Subject: [PATCH] refactor(negotiation)!: remove deprecated state-machine negotiation APIs The legacy state-machine negotiation demo has been retired since v1.1.0. Remove it so the next release ships only the stateless negotiation content API. - remove A2ATClient/A2ATServer start_negotiation, receive_negotiation and continue_negotiation, plus their client/server orchestration packages - delete a2a_t.negotiation.{common,types,store,runtime,handling,rendering} - drop A2AT_NEGOTIATION_STATE_STORE_TYPE from env templates and tests - align README, developer guides and API references with removal in v1.2.0 - delete the legacy-only tests and add a module/method absence guard Signed-off-by: zhangwenjun-huawei --- README.md | 2 +- README_zh.md | 2 +- a2a-t-sample/env.example | 3 - docs/en/API_Reference.md | 2 +- docs/en/developer_guide.md | 3 +- "docs/zh/API\345\217\202\350\200\203.md" | 2 +- ...00\345\217\221\346\214\207\345\215\227.md" | 2 +- env.example | 4 - package_data/env.example | 4 - src/a2a_t/client/a2at_client.py | 92 +---- src/a2a_t/client/negotiation/__init__.py | 7 - .../negotiation/negotiation_orchestrator.py | 17 - .../negotiation_orchestrator_builder.py | 62 ---- src/a2a_t/negotiation/common/__init__.py | 58 --- src/a2a_t/negotiation/common/constants.py | 9 - src/a2a_t/negotiation/common/enums.py | 26 -- src/a2a_t/negotiation/common/errors.py | 21 -- src/a2a_t/negotiation/common/models.py | 105 ------ src/a2a_t/negotiation/handling/__init__.py | 23 -- .../handling/negotiation_handler.py | 236 ------------ src/a2a_t/negotiation/rendering/__init__.py | 23 -- .../rendering/negotiation_prompt_renderer.py | 30 -- src/a2a_t/negotiation/runtime/__init__.py | 23 -- .../runtime/base_negotiation_orchestrator.py | 68 ---- src/a2a_t/negotiation/store/__init__.py | 27 -- src/a2a_t/negotiation/store/base.py | 17 - src/a2a_t/negotiation/store/factory.py | 50 --- src/a2a_t/negotiation/store/in_memory.py | 26 -- src/a2a_t/negotiation/types/__init__.py | 29 -- src/a2a_t/negotiation/types/base.py | 56 --- src/a2a_t/negotiation/types/feasibility.py | 9 - src/a2a_t/negotiation/types/information.py | 86 ----- src/a2a_t/negotiation/types/target.py | 9 - src/a2a_t/server/a2at_server.py | 95 +---- src/a2a_t/server/negotiation/__init__.py | 7 - .../negotiation/negotiation_orchestrator.py | 17 - .../negotiation_orchestrator_builder.py | 81 ----- tests/client_prompt/test_a2at_client.py | 72 +--- .../test_a2at_client_negotiation_api.py | 58 +-- tests/client_prompt/test_input_limit_gate.py | 2 - tests/client_prompt/test_prompt_side_apis.py | 1 - tests/corpus/assemblers.py | 1 - tests/corpus/live/env_writer.py | 1 - tests/corpus/live/test_live_config.py | 1 - tests/negotiation/test_handling_runtime.py | 341 ------------------ tests/negotiation/test_legacy_api_removed.py | 47 +++ tests/negotiation/test_models_runtime.py | 78 ---- .../negotiation/test_orchestrator_builders.py | 262 -------------- tests/negotiation/test_orchestrators.py | 259 ------------- tests/negotiation/test_parser.py | 69 ---- tests/negotiation/test_store.py | 76 ---- tests/negotiation/test_store_factory.py | 87 ----- tests/negotiation/test_types.py | 238 ------------ tests/prompt_compliance/test_a2at_server.py | 79 +--- .../test_a2at_server_negotiation_api.py | 56 +-- .../test_input_limit_gate.py | 2 - .../test_integration_runtime.py | 6 - tests/test_facade_api_symmetry.py | 3 +- 58 files changed, 76 insertions(+), 2996 deletions(-) delete mode 100644 src/a2a_t/client/negotiation/__init__.py delete mode 100644 src/a2a_t/client/negotiation/negotiation_orchestrator.py delete mode 100644 src/a2a_t/client/negotiation/negotiation_orchestrator_builder.py delete mode 100644 src/a2a_t/negotiation/common/__init__.py delete mode 100644 src/a2a_t/negotiation/common/constants.py delete mode 100644 src/a2a_t/negotiation/common/enums.py delete mode 100644 src/a2a_t/negotiation/common/errors.py delete mode 100644 src/a2a_t/negotiation/common/models.py delete mode 100644 src/a2a_t/negotiation/handling/__init__.py delete mode 100644 src/a2a_t/negotiation/handling/negotiation_handler.py delete mode 100644 src/a2a_t/negotiation/rendering/__init__.py delete mode 100644 src/a2a_t/negotiation/rendering/negotiation_prompt_renderer.py delete mode 100644 src/a2a_t/negotiation/runtime/__init__.py delete mode 100644 src/a2a_t/negotiation/runtime/base_negotiation_orchestrator.py delete mode 100644 src/a2a_t/negotiation/store/__init__.py delete mode 100644 src/a2a_t/negotiation/store/base.py delete mode 100644 src/a2a_t/negotiation/store/factory.py delete mode 100644 src/a2a_t/negotiation/store/in_memory.py delete mode 100644 src/a2a_t/negotiation/types/__init__.py delete mode 100644 src/a2a_t/negotiation/types/base.py delete mode 100644 src/a2a_t/negotiation/types/feasibility.py delete mode 100644 src/a2a_t/negotiation/types/information.py delete mode 100644 src/a2a_t/negotiation/types/target.py delete mode 100644 src/a2a_t/server/negotiation/__init__.py delete mode 100644 src/a2a_t/server/negotiation/negotiation_orchestrator.py delete mode 100644 src/a2a_t/server/negotiation/negotiation_orchestrator_builder.py delete mode 100644 tests/negotiation/test_handling_runtime.py create mode 100644 tests/negotiation/test_legacy_api_removed.py delete mode 100644 tests/negotiation/test_models_runtime.py delete mode 100644 tests/negotiation/test_orchestrator_builders.py delete mode 100644 tests/negotiation/test_orchestrators.py delete mode 100644 tests/negotiation/test_parser.py delete mode 100644 tests/negotiation/test_store.py delete mode 100644 tests/negotiation/test_store_factory.py delete mode 100644 tests/negotiation/test_types.py diff --git a/README.md b/README.md index 42a58687..4d78ebe9 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Confirm the following limitations before use: - The built-in LLM call chain is uniformly exposed as an OpenAI adaptation layer. - The LLM prompt resources are built in and do not support custom extension (`prompts` and `errors` are always read from the built-in resources). -- Negotiation session state travels in the message metadata (the SDK is stateless) and no negotiation state store is provided; the legacy state-machine negotiation APIs (`start_negotiation` / `receive_negotiation` / `continue_negotiation`) are deprecated since 1.1.0. +- Negotiation session state travels in the message metadata (the SDK is stateless) and no negotiation state store is provided; the legacy state-machine negotiation APIs (`start_negotiation` / `receive_negotiation` / `continue_negotiation`) were deprecated since v1.1.0 and removed in v1.2.0. - Bundled resources and language coverage are limited, and remote resource loading such as `registry-center` (the registry center) is not included. - This document mainly introduces the SDK itself and does not cover CLI tools, hosted services, deployment flows, or ready-to-use application solutions. diff --git a/README_zh.md b/README_zh.md index d128bf0d..c1a01be3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -168,7 +168,7 @@ uv run mypy src # 类型检查 - 内置 LLM 调用链对外统一为 OpenAI 适配层。 - 大模型提示词资源已内置,不支持自定义扩展(`prompts` 与 `errors` 始终从内置资源加载)。 -- 协商会话状态随消息 metadata 往返(SDK 无状态),不提供协商状态存储;旧的状态机协商 API(`start_negotiation` / `receive_negotiation` / `continue_negotiation`)自 1.1.0 起废弃。 +- 协商会话状态随消息 metadata 往返(SDK 无状态),不提供协商状态存储;旧的状态机协商 API(`start_negotiation` / `receive_negotiation` / `continue_negotiation`)自 v1.1.0 起废弃,已在 v1.2.0 版本删除。 - 随包资源与语言覆盖有限,不包含 `registry-center`(注册中心)等远程资源加载能力。 - 本文档主要介绍 SDK 本身,不涉及 CLI、托管服务、部署流程或可直接使用的应用方案。 diff --git a/a2a-t-sample/env.example b/a2a-t-sample/env.example index c3d525e9..ab78ca3f 100644 --- a/a2a-t-sample/env.example +++ b/a2a-t-sample/env.example @@ -59,6 +59,3 @@ A2AT_LLM_SESSION_MAX_PER_PROVIDER=100 # the dedicated logger a2a_t.llm.call is enabled for DEBUG; where the lines are printed is # decided by the embedding application's logging framework. A2AT_LLM_DETAIL_LOG_ENABLED=false - -# Negotiation -A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory diff --git a/docs/en/API_Reference.md b/docs/en/API_Reference.md index 8f1880d6..f4ec4617 100644 --- a/docs/en/API_Reference.md +++ b/docs/en/API_Reference.md @@ -72,7 +72,7 @@ The A2A-T SDK exposes only two entry points: the client entry `A2ATClient` (prom - Some APIs involve LLM calls. When using them, control the call frequency and concurrency based on the concurrency the connected model service can provide and your business latency requirements. - APIs involving LLM calls apply length protection to `text` inputs: when the input exceeds `A2AT_INPUT_TEXT_MAX_CHARS` configured in `.env` (the `package_data/.env` or the file pointed to by `env_path`), the error code `input.text_too_long` is reported; the default of this configuration item is 16384 (16×1024). Structured data that does not involve LLM calls is not limited. -- The SDK is stateless: negotiation session state travels with the A2A-T metadata (`negotiationContext`) of each message and is not stored inside the SDK; the legacy `start_negotiation` / `receive_negotiation` / `continue_negotiation` state-machine negotiation APIs are deprecated since 1.1.0, emit a `DeprecationWarning` when called, and will be removed in the next release. +- The SDK is stateless: negotiation session state travels with the A2A-T metadata (`negotiationContext`) of each message and is not stored inside the SDK; the legacy `start_negotiation` / `receive_negotiation` / `continue_negotiation` state-machine negotiation APIs were deprecated since v1.1.0, emitted a `DeprecationWarning` when called, and were removed in v1.2.0. ## 1.3 API Descriptions diff --git a/docs/en/developer_guide.md b/docs/en/developer_guide.md index 0817ddc7..2ea0b343 100644 --- a/docs/en/developer_guide.md +++ b/docs/en/developer_guide.md @@ -87,7 +87,7 @@ flowchart TD 1. Python 3.12+ is required. 2. Complete multi-agent protocol interaction development also requires the `a2a-sdk`, version 1.1.0+. 3. The SDK is stateless: negotiation session state travels with the A2A-T metadata (`negotiationContext`) of each message and is not stored inside the SDK. -4. The legacy state-machine negotiation demo (`start_negotiation` / `receive_negotiation` / `continue_negotiation` and the `negotiation/{types,store,runtime,handling}` packages) is deprecated since 1.1.0 (calls emit a `DeprecationWarning`) and will be removed in the next release. +4. The legacy state-machine negotiation demo (`start_negotiation` / `receive_negotiation` / `continue_negotiation` and the `negotiation/{types,store,runtime,handling}` packages): the state-machine negotiation APIs were deprecated since v1.1.0, emitted a `DeprecationWarning` when called, and were removed in v1.2.0. 5. The A2A-T SDK does not provide an agent HTTP service framework, a registry-center client, or authentication and key management capabilities; these must be integrated by the business system. ## 1.3 Environment Preparation @@ -1521,4 +1521,3 @@ The sample configuration file provided by the SDK is `env.example`; copy it to ` | `A2AT_LLM_SESSION_MAX_PER_PROVIDER` | Maximum number of tracked sessions per provider, default `100` | | `A2AT_LLM_MAX_ATTEMPTS` | Maximum number of attempts for retryable LLM steps, range 1-10 (out-of-range values are clipped), default `3` | | `A2AT_LLM_DETAIL_LOG_ENABLED` | Whether to print the complete LLM request/response messages (not truncated), default `false`; enabling it may print sensitive information or consume log space, use only in the DEBUG phase and keep it off in production; time/token/elapsed-time summary logs are emitted at DEBUG level by the dedicated logger `a2a_t.llm.call`, see 1.6 | -| `A2AT_NEGOTIATION_STATE_STORE_TYPE` | Negotiation state store of the deprecated state-machine negotiation demo (`in_memory`); the 1.1.0 negotiation content API is stateless and does not use this key | \ No newline at end of file diff --git "a/docs/zh/API\345\217\202\350\200\203.md" "b/docs/zh/API\345\217\202\350\200\203.md" index 2937dc3f..57bfc659 100644 --- "a/docs/zh/API\345\217\202\350\200\203.md" +++ "b/docs/zh/API\345\217\202\350\200\203.md" @@ -72,7 +72,7 @@ A2A-T SDK 对外仅提供两个入口:客户端入口 `A2ATClient`(提示词 - 部分接口涉及到LLM调用,使用时需根据对所接模型服务可提供的并发能力,结合业务时限要求,控制使用API时的调用频率和并发。 - 涉及到LLM调用的API针对文本`text`输入会做长度防护,在输入超过`.env`(`package_data/.env` 或 `env_path` 指向的文件)中配置的 `A2AT_INPUT_TEXT_MAX_CHARS` 字符时,会报错误码 `input.text_too_long` ,该配置项默认配置为 16384(16×1024);不涉及LLM调用的结构化数据不受此限制。 -- SDK 无状态:协商会话状态随每条消息的 A2A-T metadata(`negotiationContext`)往返,不保存在 SDK 内部;旧的 `start_negotiation` / `receive_negotiation` / `continue_negotiation` 状态机协商 API 自 1.1.0 起废弃,调用时发出 `DeprecationWarning`,将在下一版本删除。 +- SDK 无状态:协商会话状态随每条消息的 A2A-T metadata(`negotiationContext`)往返,不保存在 SDK 内部;旧的 `start_negotiation` / `receive_negotiation` / `continue_negotiation` 状态机协商 API 自 v1.1.0 起废弃,调用时发出 `DeprecationWarning`,已在 v1.2.0 版本删除。 ## 1.3 API说明 diff --git "a/docs/zh/\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/zh/\345\274\200\345\217\221\346\214\207\345\215\227.md" index 4f0fae37..3c81becd 100644 --- "a/docs/zh/\345\274\200\345\217\221\346\214\207\345\215\227.md" +++ "b/docs/zh/\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -87,6 +87,7 @@ flowchart TD 1. Python 版本要求为 3.12+。 2. 完整的多智能体协议交互开发需要同时引入`a2a-sdk`,要求版本为 1.1.0+。 3. SDK 无状态:协商会话状态随每条消息的 A2A-T metadata(`negotiationContext`)往返,不保存在 SDK 内部。 +4. 旧的状态机协商 demo(`start_negotiation` / `receive_negotiation` / `continue_negotiation` 以及 `negotiation/{types,store,runtime,handling}` 包):状态机协商 API 自 v1.1.0 起废弃,调用时发出 `DeprecationWarning`,已在 v1.2.0 版本删除。 5. A2A-T SDK 不提供 Agent HTTP 服务框架、注册中心客户端、鉴权和密钥管理能力,需由业务系统集成。 ## 1.3 环境准备 @@ -1512,4 +1513,3 @@ SDK提供的样例配置文件为`env.example`,复制为 `package_data/.env` | `A2AT_LLM_SESSION_MAX_PER_PROVIDER` | 单提供方会话跟踪数上限,默认 `100` | | `A2AT_LLM_MAX_ATTEMPTS` | 可重试 LLM 步骤的最大尝试次数,取值范围 1-10(越界自动裁剪),默认 `3` | | `A2AT_LLM_DETAIL_LOG_ENABLED` | 是否打印 LLM 请求/响应的完整报文(不截断),默认 `false`;开启后可能打印敏感信息或占用日志空间,仅限 DEBUG 阶段使用、正式运行必须关闭;时间/Token/耗时等摘要日志由专用 logger `a2a_t.llm.call` 以 DEBUG 级输出,详见 1.6 | -| `A2AT_NEGOTIATION_STATE_STORE_TYPE` | 已废弃状态机协商 demo 的协商状态存储(`in_memory`);1.1.0 协商内容 API 无状态,不使用该键 | diff --git a/env.example b/env.example index 968f6394..342600e1 100644 --- a/env.example +++ b/env.example @@ -59,10 +59,6 @@ A2AT_LLM_MAX_ATTEMPTS=3 # decided by the embedding application's logging framework. A2AT_LLM_DETAIL_LOG_ENABLED=false -# Negotiation -# Selects the negotiation state store implementation (legacy state-machine demo, deprecated). -A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory - # Live-LLM corpus tests (dev only; fully decoupled from the production A2AT_LLM_* keys) # Base URL of the live endpoint used by the live corpus family; the family skips when unset. A2AT_TEST_LLM_BASE_URL= diff --git a/package_data/env.example b/package_data/env.example index 2c84638b..95366e5e 100644 --- a/package_data/env.example +++ b/package_data/env.example @@ -57,7 +57,3 @@ A2AT_LLM_MAX_ATTEMPTS=3 # the dedicated logger a2a_t.llm.call is enabled for DEBUG; where the lines are printed is # decided by the embedding application's logging framework. A2AT_LLM_DETAIL_LOG_ENABLED=false - -# Negotiation -# Selects the negotiation state store implementation (legacy state-machine demo, deprecated). -A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory diff --git a/src/a2a_t/client/a2at_client.py b/src/a2a_t/client/a2at_client.py index 9a471637..0fe1b696 100644 --- a/src/a2a_t/client/a2at_client.py +++ b/src/a2a_t/client/a2at_client.py @@ -6,16 +6,12 @@ twelve negotiation content methods of the shared :class:`~a2a_t.negotiation.generation.content_service.NegotiationContentService` (eight generation plus four validation methods) and the extension-agnostic template queries of the -:class:`~a2a_t.common.prompt_resources.catalog.TemplateQueryService`. The three legacy -``start/receive/continue_negotiation`` methods of the deprecated state-machine negotiation demo -are kept for one release with a :class:`DeprecationWarning` and will be removed in the next -release (port decision D1). +:class:`~a2a_t.common.prompt_resources.catalog.TemplateQueryService`. """ from __future__ import annotations import logging -import warnings from collections.abc import Mapping from pathlib import Path from typing import Any @@ -27,7 +23,6 @@ from a2a_t.core.validation_pipeline import FilledParamData from a2a_t.llm.config_loader import LLMConfigLoader from a2a_t.llm.factory import LLMClientFactory -from a2a_t.negotiation.common.models import ContinueNegotiationInput, StartNegotiationInput from a2a_t.negotiation.content.models import ( NegotiationAbortData, NegotiationEndingData, @@ -37,7 +32,6 @@ from a2a_t.observability.llm_decorator import A2ATLLMClientDecorator from a2a_t.observability.negotiation_metrics import report_terminal_negotiation_rounds -from .negotiation.negotiation_orchestrator_builder import ClientNegotiationOrchestratorBuilder from .prompt_generation.models import PromptGenerationResult from .prompt_generation.prompt_generation_orchestrator_builder import PromptGenerationOrchestratorBuilder @@ -60,17 +54,6 @@ def _default_env_path() -> Path: return Path(__file__).resolve().parents[3] / "package_data" / ".env" -def _deprecation_message(method: str) -> str: - """Return the D1 deprecation message of one legacy negotiation facade method.""" - return ( - f"A2ATClient.{method} is deprecated since 1.1.0 and will be removed in the next release; " - "the state-machine negotiation demo it belongs to is retired. Use the negotiation content " - "API instead: generate_negotiation_{propose,accept,reject,abort}_prompt_from_data / " - "..._from_text and validate_{propose,accept,reject,abort}_prompt_and_data_filling, backed " - "by a2a_t.negotiation.generation.NegotiationContentService." - ) - - def _parse_template_uri(template_uri: str | TemplateUri | None) -> TemplateUri: """Parse the raw template URI string boundary fail-fast (D16, Java ``parseTemplateUri``). @@ -111,10 +94,6 @@ class A2ATClient: hand-written URI strings. Business failures are raised as catalog-coded exceptions of the :mod:`a2a_t.core.errors.exceptions` tree (catch :class:`~a2a_t.core.errors.exceptions.A2ATError` and branch on ``code_str``); programming errors stay ``TypeError`` / ``ValueError``. - - The three legacy ``start/receive/continue_negotiation`` methods of the retired state-machine - negotiation demo are kept for one release with a :class:`DeprecationWarning` and will be - removed in the next release. """ def __init__( @@ -125,18 +104,18 @@ def __init__( ) -> None: """Create one client facade from a ``.env`` file. - The configuration, the LLM client and both orchestrators are resolved eagerly so that a - misconfiguration surfaces at construction instead of on the first call. The negotiation - content service and the template query service are assembled lazily on first use: both - capture resource snapshots at that point, so constructing the facade stays side-effect free - for the prompt-only flows. + The configuration and the LLM client are resolved eagerly so that a misconfiguration + surfaces at construction instead of on the first call. The negotiation content service and + the template query service are assembled lazily on first use: both capture resource + snapshots at that point, so constructing the facade stays side-effect free for the + prompt-only flows. Args: env_path: path of the ``.env`` file carrying the SDK configuration; ``None`` uses the packaged ``package_data/.env`` default. The resolved file must exist — copy ``env.example`` to ``.env`` first (every key is optional, an empty file works). - logger: optional logger injected into the LLM client and the orchestrators; ``None`` - uses each component's module-level logger. + logger: optional logger injected into the LLM client and the prompt generation + orchestrator; ``None`` uses each component's module-level logger. Raises: ConfigFileNotFoundError: when the resolved ``.env`` path does not exist. @@ -155,10 +134,6 @@ def __init__( llm_client=self._llm_client, logger=logger, ) - self._negotiation_orchestrator = ClientNegotiationOrchestratorBuilder().build( - env_path=resolved_env_path, - logger=logger, - ) # Lazily assembled negotiation content service and template query service: both capture # resource snapshots at first use, so constructing the facade stays side-effect free for # the prompt-only flows. Tests may inject either seam before the first call. @@ -381,57 +356,6 @@ def generate_notification_prompt_from_data_with_schema( data, schema, _parse_template_uri(template_uri) ) - def start_negotiation(self, input: StartNegotiationInput) -> dict[str, object]: - """Start a client-side negotiation round. - - Deprecated (D1): the state-machine negotiation demo is retired; this method emits a - :class:`DeprecationWarning`, forwards to the legacy orchestrator unchanged, and will be - removed in the next release. Use the negotiation content API instead — - :meth:`generate_negotiation_propose_prompt_from_data` and its siblings. - - Args: - input: legacy negotiation start input. - - Returns: - the legacy orchestrator's round result as a plain mapping. - """ - warnings.warn(_deprecation_message("start_negotiation"), DeprecationWarning, stacklevel=2) - return self._negotiation_orchestrator.start_negotiation(input) - - def receive_negotiation(self, message: str, context: dict[str, object]) -> dict[str, object]: - """Process a negotiation message received from the remote peer. - - Deprecated (D1): the state-machine negotiation demo is retired; this method forwards to the - legacy orchestrator unchanged and will be removed in the next release. Use the negotiation - content API instead — :meth:`validate_propose_prompt_and_data_filling` and its siblings. - - Args: - message: received negotiation message text. - context: legacy negotiation context mapping. - - Returns: - the legacy orchestrator's round result as a plain mapping. - """ - warnings.warn(_deprecation_message("receive_negotiation"), DeprecationWarning, stacklevel=2) - return self._negotiation_orchestrator.receive_negotiation(message, context) - - def continue_negotiation(self, input: ContinueNegotiationInput) -> dict[str, object]: - """Continue an existing negotiation with a local response. - - Deprecated (D1): the state-machine negotiation demo is retired; this method forwards to the - legacy orchestrator unchanged and will be removed in the next release. Use the negotiation - content API instead — :meth:`generate_negotiation_accept_prompt_from_data` and its - siblings. - - Args: - input: legacy negotiation continuation input. - - Returns: - the legacy orchestrator's round result as a plain mapping. - """ - warnings.warn(_deprecation_message("continue_negotiation"), DeprecationWarning, stacklevel=2) - return self._negotiation_orchestrator.continue_negotiation(input) - # ------------------------------------------------------------------ # Negotiation content generation (from data: deterministic, zero LLM) # ------------------------------------------------------------------ diff --git a/src/a2a_t/client/negotiation/__init__.py b/src/a2a_t/client/negotiation/__init__.py deleted file mode 100644 index 30dd14f0..00000000 --- a/src/a2a_t/client/negotiation/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .negotiation_orchestrator import NegotiationOrchestrator -from .negotiation_orchestrator_builder import ClientNegotiationOrchestratorBuilder - -__all__ = [ - "NegotiationOrchestrator", - "ClientNegotiationOrchestratorBuilder", -] diff --git a/src/a2a_t/client/negotiation/negotiation_orchestrator.py b/src/a2a_t/client/negotiation/negotiation_orchestrator.py deleted file mode 100644 index 9f5f3130..00000000 --- a/src/a2a_t/client/negotiation/negotiation_orchestrator.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from a2a_t.negotiation.common.enums import NegotiationRole -from a2a_t.negotiation.runtime.base_negotiation_orchestrator import BaseNegotiationOrchestrator - - -class NegotiationOrchestrator(BaseNegotiationOrchestrator): - """Bind the shared negotiation runtime to the client role.""" - - def __init__(self, *, handler: Any, logger: Any | None = None) -> None: - super().__init__( - handler=handler, - role=NegotiationRole.CLIENT, - logger=logger, - ) diff --git a/src/a2a_t/client/negotiation/negotiation_orchestrator_builder.py b/src/a2a_t/client/negotiation/negotiation_orchestrator_builder.py deleted file mode 100644 index 1c7c7a4f..00000000 --- a/src/a2a_t/client/negotiation/negotiation_orchestrator_builder.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Any - -from a2a_t.negotiation.common.enums import NegotiationType -from a2a_t.negotiation.handling import NegotiationHandler -from a2a_t.negotiation.rendering import NegotiationPromptRenderer -from a2a_t.negotiation.store import NegotiationStateStoreFactory -from a2a_t.negotiation.types import ( - FeasibilityNegotiationType, - InformationNegotiationType, - TargetNegotiationType, -) - -from .negotiation_orchestrator import NegotiationOrchestrator - - -class ClientNegotiationOrchestratorBuilder: - """Assemble the client-side negotiation runtime.""" - - def __init__( - self, - *, - prompt_renderer_cls: type = NegotiationPromptRenderer, - store_factory: NegotiationStateStoreFactory | None = None, - handler_cls: type = NegotiationHandler, - orchestrator_cls: type = NegotiationOrchestrator, - ) -> None: - self._prompt_renderer_cls = prompt_renderer_cls - self._store_factory = store_factory or NegotiationStateStoreFactory() - self._handler_cls = handler_cls - self._orchestrator_cls = orchestrator_cls - - def build( - self, - *, - env_path: str | Path | None = None, - logger: Any | None = None, - ) -> NegotiationOrchestrator: - """Build a client negotiation orchestrator with all supported negotiation types.""" - prompt_renderer = self._prompt_renderer_cls() - store = self._build_store(env_path=env_path, logger=logger) - handler = self._handler_cls( - negotiation_types={ - NegotiationType.INFORMATION: InformationNegotiationType(prompt_renderer=prompt_renderer), - NegotiationType.TARGET: TargetNegotiationType(prompt_renderer=prompt_renderer), - NegotiationType.FEASIBILITY: FeasibilityNegotiationType(prompt_renderer=prompt_renderer), - }, - store=store, - ) - return self._orchestrator_cls( # type: ignore[no-any-return] - handler=handler, - logger=logger, - ) - - def _build_store(self, *, env_path: str | Path | None, logger: Any | None) -> object: - """Resolve the negotiation state store configured for the client runtime.""" - return self._store_factory.build( - env_path=env_path, - logger=logger, - ) diff --git a/src/a2a_t/negotiation/common/__init__.py b/src/a2a_t/negotiation/common/__init__.py deleted file mode 100644 index f237be2b..00000000 --- a/src/a2a_t/negotiation/common/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Deprecated state-machine negotiation demo package (D1 shim round). - -This package belongs to the retired state-machine negotiation demo mirrored from the old Java -``negotiation.types`` model package. It is kept unchanged for one release and will be removed in -the next release; importing it emits a :class:`DeprecationWarning`. Note that the legacy facade -methods ``start/receive/continue_negotiation`` still consume this package's input types and are -deprecated with it. -""" - -import warnings - -from .constants import NEGOTIATION_T_URI, NEGOTIATION_T_URI_NL, TASK_PROMPT_KEY, TASK_PROMPT_KEY_NL -from .enums import NegotiationRole, NegotiationStatus, NegotiationType -from .errors import ( - NegotiationContextError, - NegotiationInputError, - NegotiationParseError, - NegotiationStateError, - NegotiationTerminalStateError, -) -from .models import ( - ContinueNegotiationInput, - ContinueResult, - NegotiationContext, - NegotiationRecord, - ReceiveResult, - StartNegotiationInput, -) - -__all__ = [ - "ContinueNegotiationInput", - "ContinueResult", - "NEGOTIATION_T_URI", - "NEGOTIATION_T_URI_NL", - "NegotiationContext", - "NegotiationContextError", - "NegotiationInputError", - "NegotiationParseError", - "NegotiationRecord", - "NegotiationRole", - "NegotiationStateError", - "NegotiationStatus", - "NegotiationTerminalStateError", - "NegotiationType", - "ReceiveResult", - "StartNegotiationInput", - "TASK_PROMPT_KEY", - "TASK_PROMPT_KEY_NL", -] - -warnings.warn( - "The a2a_t.negotiation.common package is deprecated since 1.1.0 and will be removed in the " - "next release; the state-machine negotiation demo is retired. Use the negotiation content " - "pipeline instead (a2a_t.negotiation.content / generation / validation, exposed by the " - "A2ATClient and A2ATServer negotiation content methods).", - DeprecationWarning, - stacklevel=2, -) diff --git a/src/a2a_t/negotiation/common/constants.py b/src/a2a_t/negotiation/common/constants.py deleted file mode 100644 index 1bb80fae..00000000 --- a/src/a2a_t/negotiation/common/constants.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -NEGOTIATION_T_URI_NL = "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/NL/v1" -NEGOTIATION_T_URI = "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/v1" -TASK_PROMPT_KEY_NL = "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Task-T/NL/v1" -TASK_PROMPT_KEY = "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Task-T/v1" -# Non-NL URI constants are retained for backward compatibility with external consumers -# that may still use the pre-IG1453 URI format. The SDK internally uses the _NL variants. -MAX_IN_PROGRESS_NEGOTIATION_ROUND = 8 diff --git a/src/a2a_t/negotiation/common/enums.py b/src/a2a_t/negotiation/common/enums.py deleted file mode 100644 index 1789fd62..00000000 --- a/src/a2a_t/negotiation/common/enums.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from enum import Enum - - -class NegotiationType(str, Enum): - """Enumerate the negotiation flows supported by the SDK.""" - - INFORMATION = "information" - TARGET = "target" - FEASIBILITY = "feasibility" - - -class NegotiationRole(str, Enum): - """Describe which side owns the current negotiation state.""" - - CLIENT = "client" - SERVER = "server" - - -class NegotiationStatus(str, Enum): - """Describe the lifecycle state of a negotiation round.""" - - IN_PROGRESS = "in-progress" - AGREED = "agreed" - REJECTED = "rejected" diff --git a/src/a2a_t/negotiation/common/errors.py b/src/a2a_t/negotiation/common/errors.py deleted file mode 100644 index 3fe4db42..00000000 --- a/src/a2a_t/negotiation/common/errors.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - - -class NegotiationInputError(Exception): - """Raised when negotiation input is invalid.""" - - -class NegotiationContextError(Exception): - """Raised when negotiation context is invalid.""" - - -class NegotiationStateError(Exception): - """Raised when local negotiation state is inconsistent.""" - - -class NegotiationTerminalStateError(Exception): - """Raised when a terminal negotiation is continued.""" - - -class NegotiationParseError(Exception): - """Raised when a negotiation message cannot be parsed.""" diff --git a/src/a2a_t/negotiation/common/models.py b/src/a2a_t/negotiation/common/models.py deleted file mode 100644 index ce2ee881..00000000 --- a/src/a2a_t/negotiation/common/models.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from .enums import NegotiationRole, NegotiationStatus, NegotiationType -from .errors import NegotiationContextError - - -@dataclass(slots=True) -class StartNegotiationInput: - """Carry the input required to open a negotiation.""" - - type: NegotiationType - content_text: str - facts: dict[str, object] - - -@dataclass(slots=True) -class ContinueNegotiationInput: - """Carry the input required to continue an active negotiation.""" - - context: "NegotiationContext" - status: NegotiationStatus - content_text: str - - -@dataclass(slots=True) -class NegotiationContext: - """Represent the transport-safe context shared between negotiation rounds.""" - - negotiation_type: NegotiationType - negotiation_id: str - role: NegotiationRole - round: int - status: NegotiationStatus - extra: dict[str, object] - - @classmethod - def from_context(cls, context: dict[str, object]) -> "NegotiationContext": - """Parse and validate a serialized negotiation context payload.""" - try: - negotiation_type = NegotiationType(str(context["negotiationType"])) - negotiation_id = str(context["negotiationId"]) - # role is optional: Java SDK context payload omits it; default to CLIENT. - raw_role = context.get("role", NegotiationRole.CLIENT.value) - role = NegotiationRole(str(raw_role)) - round_value = int(str(context["round"])) - status = NegotiationStatus(str(context["status"])) - # extra is optional: Java SDK defaults it to empty map. - extra = context.get("extra", {}) - except (KeyError, TypeError, ValueError) as error: - raise NegotiationContextError("Invalid negotiation context.") from error - - if not negotiation_id or round_value < 1 or not isinstance(extra, dict): - raise NegotiationContextError("Invalid negotiation context.") - - return cls( - negotiation_type=negotiation_type, - negotiation_id=negotiation_id, - role=role, - round=round_value, - status=status, - extra=dict(extra), - ) - - def to_context(self) -> dict[str, object]: - """Serialize the negotiation context into the transport payload shape.""" - return { - "negotiationType": self.negotiation_type.value, - "negotiationId": self.negotiation_id, - "role": self.role.value, - "round": self.round, - "status": self.status.value, - "extra": dict(self.extra), - } - - -@dataclass(slots=True) -class ReceiveResult: - """Represent the local outcome of processing a received negotiation message.""" - - need_response: bool - facts: dict[str, object] - message: str = "" - - -@dataclass(slots=True) -class ContinueResult: - """Represent the prompt emitted when a negotiation is continued locally.""" - - prompt_text: str - final_task_prompt: str | None - - -@dataclass(slots=True) -class NegotiationRecord: - """Persist the local state accumulated for one negotiation.""" - - context: NegotiationContext - last_message: str | None - last_receive_result: ReceiveResult | None - last_continue_result: ContinueResult | None - last_task_prompt: str | None - created_at: object - updated_at: object diff --git a/src/a2a_t/negotiation/handling/__init__.py b/src/a2a_t/negotiation/handling/__init__.py deleted file mode 100644 index c273e728..00000000 --- a/src/a2a_t/negotiation/handling/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Deprecated state-machine negotiation demo package (D1 shim round). - -This package belongs to the retired state-machine negotiation demo mirrored from the old Java -``negotiation.handler`` package. It is kept unchanged for one release and will be removed in the -next release; importing it emits a :class:`DeprecationWarning`. -""" - -import warnings - -from .negotiation_handler import NegotiationHandler - -__all__ = [ - "NegotiationHandler", -] - -warnings.warn( - "The a2a_t.negotiation.handling package is deprecated since 1.1.0 and will be removed in the " - "next release; the state-machine negotiation demo is retired. Use the negotiation content " - "pipeline instead (a2a_t.negotiation.content / generation / validation, exposed by the " - "A2ATClient and A2ATServer negotiation content methods).", - DeprecationWarning, - stacklevel=2, -) diff --git a/src/a2a_t/negotiation/handling/negotiation_handler.py b/src/a2a_t/negotiation/handling/negotiation_handler.py deleted file mode 100644 index ed71890b..00000000 --- a/src/a2a_t/negotiation/handling/negotiation_handler.py +++ /dev/null @@ -1,236 +0,0 @@ -from __future__ import annotations - -import uuid -from datetime import datetime, timezone - -from a2a_t.negotiation.common.constants import ( - MAX_IN_PROGRESS_NEGOTIATION_ROUND, - NEGOTIATION_T_URI_NL, - TASK_PROMPT_KEY_NL, -) -from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType -from a2a_t.negotiation.common.errors import NegotiationInputError, NegotiationStateError, NegotiationTerminalStateError -from a2a_t.negotiation.common.models import ( - ContinueNegotiationInput, - NegotiationContext, - NegotiationRecord, - ReceiveResult, - StartNegotiationInput, -) -from a2a_t.negotiation.store.base import NegotiationStateStore -from a2a_t.negotiation.types.base import BaseNegotiationType - - -class NegotiationHandler: - """Own negotiation state transitions across start, receive, and continue flows.""" - - def __init__( - self, - *, - negotiation_types: dict[NegotiationType, BaseNegotiationType], - store: NegotiationStateStore, - ) -> None: - self._negotiation_types = dict(negotiation_types) - self._store = store - - def start(self, *, input: StartNegotiationInput, role: NegotiationRole) -> dict[str, object]: - """Start a negotiation and persist the initial local record.""" - negotiation_type_key = self._normalize_negotiation_type(input.type) - context = self._create_start_context( - negotiation_type=negotiation_type_key, - role=role, - ) - negotiation_type = self._get_negotiation_type(negotiation_type_key) - prompt_text = negotiation_type.render_start_prompt(input=input, context=context) - now = self._utc_now() - self._store.save( - NegotiationRecord( - context=context, - last_message=prompt_text, - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at=now, - updated_at=now, - ) - ) - return self._build_result_map( - prompt_text=prompt_text, - context=context, - ) - - def receive(self, *, message: str, context: dict[str, object]) -> dict[str, object]: - """Validate and process a negotiation message received from the remote peer.""" - parsed_context = NegotiationContext.from_context(context) - record = self._store.get(parsed_context.negotiation_id) - if record is None: - if parsed_context.round != 1: - raise NegotiationStateError("Negotiation record is missing for non-initial round.") - now = self._utc_now() - record = NegotiationRecord( - context=parsed_context, - last_message=None, - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at=now, - updated_at=now, - ) - elif record.context.status in {NegotiationStatus.AGREED, NegotiationStatus.REJECTED}: - raise NegotiationTerminalStateError("Cannot receive a terminal negotiation again.") - elif parsed_context.round != record.context.round + 1: - raise NegotiationStateError("Incoming negotiation round is inconsistent with local state.") - elif ( - parsed_context.negotiation_type != record.context.negotiation_type - or parsed_context.role != record.context.role - ): - raise NegotiationStateError("Incoming negotiation context is inconsistent with local state.") - - if ( - parsed_context.status == NegotiationStatus.IN_PROGRESS - and parsed_context.round >= MAX_IN_PROGRESS_NEGOTIATION_ROUND - ): - receive_result = ReceiveResult( - need_response=True, - facts={}, - message="Negotiation reached the maximum in-progress round limit. Please reject it.", - ) - record.context = parsed_context - record.last_message = message - record.last_receive_result = receive_result - record.updated_at = self._utc_now() - self._store.save(record) - return self._build_receive_result_map( - context=parsed_context, - receive_result=receive_result, - ) - - negotiation_type = self._get_negotiation_type(parsed_context.negotiation_type) - receive_result = negotiation_type.process_received_message( - message=message, - context=parsed_context, - record=record, - ) - record.context = parsed_context - record.last_message = message - record.last_receive_result = receive_result - record.updated_at = self._utc_now() - self._store.save(record) - return self._build_receive_result_map( - context=parsed_context, - receive_result=receive_result, - ) - - def continue_(self, *, input: ContinueNegotiationInput) -> dict[str, object]: - """Render the next local negotiation message and persist the advanced state.""" - record = self._store.get(input.context.negotiation_id) - if record is None: - raise NegotiationStateError("Negotiation record is missing.") - if record.context.status in {NegotiationStatus.AGREED, NegotiationStatus.REJECTED}: - raise NegotiationTerminalStateError("Cannot continue a terminal negotiation.") - if ( - input.context.negotiation_type != record.context.negotiation_type - or input.context.negotiation_id != record.context.negotiation_id - or input.context.role != record.context.role - or input.context.round != record.context.round - or input.context.status != record.context.status - ): - # Continue operations must use the exact local snapshot to avoid diverging branches. - raise NegotiationStateError("Negotiation continue context is inconsistent with local state.") - - negotiation_type = self._get_negotiation_type(input.context.negotiation_type) - continue_result = negotiation_type.render_continue_prompt( - record=record, - context=input.context, - status=input.status, - content_text=input.content_text, - ) - next_context = self._create_next_context( - previous=input.context, - status=input.status, - ) - record.context = next_context - record.last_continue_result = continue_result - record.last_task_prompt = input.content_text - record.updated_at = self._utc_now() - self._store.save(record) - return self._build_result_map( - prompt_text=continue_result.prompt_text, - context=next_context, - final_task_prompt=continue_result.final_task_prompt, - ) - - def _get_negotiation_type(self, negotiation_type: NegotiationType) -> BaseNegotiationType: - """Resolve the strategy object that implements one negotiation type.""" - try: - return self._negotiation_types[negotiation_type] - except KeyError as error: - raise NegotiationInputError(f"Unsupported negotiation type: {negotiation_type.value}") from error - - @staticmethod - def _normalize_negotiation_type(value: NegotiationType | str) -> NegotiationType: - """Normalize external negotiation type values into the shared enum.""" - try: - return value if isinstance(value, NegotiationType) else NegotiationType(str(value)) - except ValueError as error: - raise NegotiationInputError(f"Unsupported negotiation type: {value}") from error - - @staticmethod - def _utc_now() -> datetime: - """Return the current UTC timestamp used for negotiation record bookkeeping.""" - return datetime.now(timezone.utc) - - @staticmethod - def _create_start_context(*, negotiation_type: NegotiationType, role: NegotiationRole) -> NegotiationContext: - """Create the initial context for a newly started negotiation.""" - return NegotiationContext( - negotiation_type=negotiation_type, - negotiation_id=str(uuid.uuid4()), - role=role, - round=1, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ) - - @staticmethod - def _create_next_context(*, previous: NegotiationContext, status: NegotiationStatus) -> NegotiationContext: - """Advance a negotiation context to the next round while preserving identity.""" - return NegotiationContext( - negotiation_type=previous.negotiation_type, - negotiation_id=previous.negotiation_id, - role=previous.role, - round=previous.round + 1, - status=status, - extra=dict(previous.extra), - ) - - @staticmethod - def _build_result_map( - *, - prompt_text: str, - context: NegotiationContext, - final_task_prompt: str | None = None, - ) -> dict[str, object]: - """Build the transport payload returned by start and continue operations.""" - negotiation_data: dict[str, object] = {"message": prompt_text} - negotiation_data.update(context.to_context()) - result: dict[str, object] = { - NEGOTIATION_T_URI_NL: negotiation_data, - } - if final_task_prompt is not None: - result[TASK_PROMPT_KEY_NL] = final_task_prompt - return result - - @staticmethod - def _build_receive_result_map( - *, - context: NegotiationContext, - receive_result: ReceiveResult, - ) -> dict[str, object]: - """Build the transport payload returned by receive operations.""" - return { - "context": context.to_context(), - "needResponse": receive_result.need_response, - "facts": dict(receive_result.facts), - "message": receive_result.message, - } diff --git a/src/a2a_t/negotiation/rendering/__init__.py b/src/a2a_t/negotiation/rendering/__init__.py deleted file mode 100644 index eb3148ec..00000000 --- a/src/a2a_t/negotiation/rendering/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Deprecated state-machine negotiation demo package (D1 shim round). - -This package belongs to the retired state-machine negotiation demo mirrored from the old Java -``negotiation.rendering`` package. It is kept unchanged for one release and will be removed in the -next release; importing it emits a :class:`DeprecationWarning`. -""" - -import warnings - -from .negotiation_prompt_renderer import NegotiationPromptRenderer - -__all__ = [ - "NegotiationPromptRenderer", -] - -warnings.warn( - "The a2a_t.negotiation.rendering package is deprecated since 1.1.0 and will be removed in the " - "next release; the state-machine negotiation demo is retired. Use the negotiation content " - "pipeline instead (a2a_t.negotiation.content / generation / validation, exposed by the " - "A2ATClient and A2ATServer negotiation content methods).", - DeprecationWarning, - stacklevel=2, -) diff --git a/src/a2a_t/negotiation/rendering/negotiation_prompt_renderer.py b/src/a2a_t/negotiation/rendering/negotiation_prompt_renderer.py deleted file mode 100644 index ee7b2f93..00000000 --- a/src/a2a_t/negotiation/rendering/negotiation_prompt_renderer.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from a2a_t.negotiation.common.enums import NegotiationType - - -class NegotiationPromptRenderer: - """Render negotiation messages into the transport payload text.""" - - def render(self, *, negotiation_type: NegotiationType, message: str) -> str: - """Render a negotiation message for the given negotiation type.""" - return message - - def render_start(self, *, negotiation_type: NegotiationType, message: str) -> str: - """Render the opening message for a negotiation.""" - return self.render( - negotiation_type=negotiation_type, - message=message, - ) - - def render_continue( - self, - *, - negotiation_type: NegotiationType, - message: str, - ) -> str: - """Render a follow-up message for a negotiation.""" - return self.render( - negotiation_type=negotiation_type, - message=message, - ) diff --git a/src/a2a_t/negotiation/runtime/__init__.py b/src/a2a_t/negotiation/runtime/__init__.py deleted file mode 100644 index fcc6a368..00000000 --- a/src/a2a_t/negotiation/runtime/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Deprecated state-machine negotiation demo package (D1 shim round). - -This package belongs to the retired state-machine negotiation demo mirrored from the old Java -``negotiation.runtime`` package. It is kept unchanged for one release and will be removed in the -next release; importing it emits a :class:`DeprecationWarning`. -""" - -import warnings - -from .base_negotiation_orchestrator import BaseNegotiationOrchestrator - -__all__ = [ - "BaseNegotiationOrchestrator", -] - -warnings.warn( - "The a2a_t.negotiation.runtime package is deprecated since 1.1.0 and will be removed in the " - "next release; the state-machine negotiation demo is retired. Use the negotiation content " - "pipeline instead (a2a_t.negotiation.content / generation / validation, exposed by the " - "A2ATClient and A2ATServer negotiation content methods).", - DeprecationWarning, - stacklevel=2, -) diff --git a/src/a2a_t/negotiation/runtime/base_negotiation_orchestrator.py b/src/a2a_t/negotiation/runtime/base_negotiation_orchestrator.py deleted file mode 100644 index 5951d0eb..00000000 --- a/src/a2a_t/negotiation/runtime/base_negotiation_orchestrator.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Any - -from a2a_t.negotiation.common.enums import NegotiationRole -from a2a_t.negotiation.common.models import ContinueNegotiationInput, StartNegotiationInput - -_LOGGER = logging.getLogger(__name__) - - -class BaseNegotiationOrchestrator: - """Expose the shared negotiation handler behind a role-specific facade.""" - - def __init__(self, *, handler: Any, role: NegotiationRole, logger: Any | None = None) -> None: - self._handler = handler - self._role = role - self._logger = logger if logger is not None else _LOGGER - - def start_negotiation(self, input: StartNegotiationInput) -> dict[str, object]: - """Start a negotiation from the bound local role.""" - self._log_info("negotiation_start_started role=%s type=%s", self._role.value, input.type.value) - result: dict[str, object] = self._handler.start(input=input, role=self._role) - self._log_result("negotiation_start_completed role=%s type=%s id=%s status=%s", result) - return result - - def receive_negotiation(self, message: str, context: dict[str, object]) -> dict[str, object]: - """Process a negotiation message received from the remote peer.""" - self._log_info( - "negotiation_receive_started role=%s type=%s id=%s", - self._role.value, - context.get("negotiationType"), - context.get("negotiationId"), - ) - result: dict[str, object] = self._handler.receive( - message=message, - context=context, - ) - self._log_result("negotiation_receive_completed role=%s type=%s id=%s status=%s", result) - return result - - def continue_negotiation(self, input: ContinueNegotiationInput) -> dict[str, object]: - """Continue a negotiation from the bound local role.""" - self._log_info( - "negotiation_continue_started role=%s type=%s id=%s status=%s", - self._role.value, - input.context.negotiation_type.value, - input.context.negotiation_id, - input.status.value, - ) - result: dict[str, object] = self._handler.continue_(input=input) - self._log_result("negotiation_continue_completed role=%s type=%s id=%s status=%s", result) - return result - - def _log_result(self, message: str, result: dict[str, object]) -> None: - context = result.get("context") - if not isinstance(context, dict): - context = {} - self._log_info( - message, - self._role.value, - context.get("negotiationType"), - context.get("negotiationId"), - context.get("status"), - ) - - def _log_info(self, message: str, *args: object) -> None: - self._logger.info(message, *args) diff --git a/src/a2a_t/negotiation/store/__init__.py b/src/a2a_t/negotiation/store/__init__.py deleted file mode 100644 index 2f55e81b..00000000 --- a/src/a2a_t/negotiation/store/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Deprecated state-machine negotiation demo package (D1 shim round). - -This package belongs to the retired state-machine negotiation demo mirrored from the old Java -``negotiation.store`` package. It is kept unchanged for one release and will be removed in the -next release; importing it emits a :class:`DeprecationWarning`. -""" - -import warnings - -from .base import NegotiationStateStore -from .factory import NegotiationStateStoreFactory -from .in_memory import InMemoryNegotiationStateStore - -__all__ = [ - "InMemoryNegotiationStateStore", - "NegotiationStateStore", - "NegotiationStateStoreFactory", -] - -warnings.warn( - "The a2a_t.negotiation.store package is deprecated since 1.1.0 and will be removed in the " - "next release; the state-machine negotiation demo is retired. Use the negotiation content " - "pipeline instead (a2a_t.negotiation.content / generation / validation, exposed by the " - "A2ATClient and A2ATServer negotiation content methods).", - DeprecationWarning, - stacklevel=2, -) diff --git a/src/a2a_t/negotiation/store/base.py b/src/a2a_t/negotiation/store/base.py deleted file mode 100644 index cc4150de..00000000 --- a/src/a2a_t/negotiation/store/base.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from typing import Protocol - -from a2a_t.negotiation.common.models import NegotiationRecord - - -class NegotiationStateStore(Protocol): - """Describe the persistence contract for negotiation records.""" - - def get(self, negotiation_id: str) -> NegotiationRecord | None: ... - - def save(self, record: NegotiationRecord) -> None: ... - - def delete(self, negotiation_id: str) -> None: ... - - def cleanup_expired(self) -> bool: ... diff --git a/src/a2a_t/negotiation/store/factory.py b/src/a2a_t/negotiation/store/factory.py deleted file mode 100644 index 8a4e8347..00000000 --- a/src/a2a_t/negotiation/store/factory.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Any - -from a2a_t.config.errors import ConfigFileNotFoundError -from a2a_t.config.source import DotEnvConfigSource - -from .in_memory import InMemoryNegotiationStateStore - - -class NegotiationStateStoreFactory: - """Create negotiation state stores from environment-backed configuration.""" - - def build( - self, - *, - env_path: str | Path | None = None, - logger: Any | None = None, - ) -> InMemoryNegotiationStateStore: - """Build the configured store or fall back to the in-memory implementation.""" - if env_path is None: - return InMemoryNegotiationStateStore() - - try: - values = DotEnvConfigSource.load(Path(env_path)) - except ConfigFileNotFoundError: - self._warning(logger, "Negotiation state store env file is missing. Falling back to in_memory store.") - return InMemoryNegotiationStateStore() - except Exception: - self._warning(logger, "Negotiation state store config is invalid. Falling back to in_memory store.") - return InMemoryNegotiationStateStore() - - store_type = values.get("A2AT_NEGOTIATION_STATE_STORE_TYPE", "in_memory").strip() or "in_memory" - if store_type == "in_memory": - return InMemoryNegotiationStateStore() - - # Unsupported store types degrade to the in-memory store so negotiation can still proceed. - self._warning( - logger, - "Negotiation state store type %s is unsupported. Falling back to in_memory store.", - store_type, - ) - return InMemoryNegotiationStateStore() - - @staticmethod - def _warning(logger: Any | None, message: str, *args: object) -> None: - """Emit a warning only when the caller supplied a warning-capable logger.""" - if logger is not None and hasattr(logger, "warning"): - logger.warning(message, *args) diff --git a/src/a2a_t/negotiation/store/in_memory.py b/src/a2a_t/negotiation/store/in_memory.py deleted file mode 100644 index 2c11b0d1..00000000 --- a/src/a2a_t/negotiation/store/in_memory.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from a2a_t.negotiation.common.models import NegotiationRecord - - -class InMemoryNegotiationStateStore: - """Persist negotiation records in process memory.""" - - def __init__(self) -> None: - self._records: dict[str, NegotiationRecord] = {} - - def get(self, negotiation_id: str) -> NegotiationRecord | None: - """Return the negotiation record for the given identifier.""" - return self._records.get(negotiation_id) - - def save(self, record: NegotiationRecord) -> None: - """Persist or replace a negotiation record by its identifier.""" - self._records[record.context.negotiation_id] = record - - def delete(self, negotiation_id: str) -> None: - """Delete a negotiation record when it exists.""" - self._records.pop(negotiation_id, None) - - def cleanup_expired(self) -> bool: - """Report success for the no-op cleanup used by the in-memory store.""" - return True diff --git a/src/a2a_t/negotiation/types/__init__.py b/src/a2a_t/negotiation/types/__init__.py deleted file mode 100644 index 1651924b..00000000 --- a/src/a2a_t/negotiation/types/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Deprecated state-machine negotiation demo package (D1 shim round). - -This package belongs to the retired state-machine negotiation demo mirrored from the old Java -``negotiation.types`` package. It is kept unchanged for one release and will be removed in the -next release; importing it emits a :class:`DeprecationWarning`. -""" - -import warnings - -from .base import BaseNegotiationType -from .feasibility import FeasibilityNegotiationType -from .information import InformationNegotiationType -from .target import TargetNegotiationType - -__all__ = [ - "BaseNegotiationType", - "FeasibilityNegotiationType", - "InformationNegotiationType", - "TargetNegotiationType", -] - -warnings.warn( - "The a2a_t.negotiation.types package is deprecated since 1.1.0 and will be removed in the " - "next release; the state-machine negotiation demo is retired. Use the negotiation content " - "pipeline instead (a2a_t.negotiation.content / generation / validation, exposed by the " - "A2ATClient and A2ATServer negotiation content methods).", - DeprecationWarning, - stacklevel=2, -) diff --git a/src/a2a_t/negotiation/types/base.py b/src/a2a_t/negotiation/types/base.py deleted file mode 100644 index 7dfd92c4..00000000 --- a/src/a2a_t/negotiation/types/base.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from a2a_t.negotiation.common.enums import NegotiationStatus -from a2a_t.negotiation.common.models import ( - ContinueResult, - NegotiationContext, - NegotiationRecord, - ReceiveResult, - StartNegotiationInput, -) -from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - - -class BaseNegotiationType: - """Provide default behavior shared by concrete negotiation type strategies.""" - - def __init__(self, *, prompt_renderer: NegotiationPromptRenderer) -> None: - self._prompt_renderer = prompt_renderer - - def render_start_prompt(self, *, input: StartNegotiationInput, context: NegotiationContext) -> str: - """Render the first prompt emitted when a negotiation starts.""" - return self._prompt_renderer.render_start( - negotiation_type=input.type, - message=input.content_text, - ) - - def process_received_message( - self, - *, - message: str, - context: NegotiationContext, - record: NegotiationRecord | None, - ) -> ReceiveResult: - """Process a received message using the default echo-like behavior.""" - return ReceiveResult( - need_response=context.status == NegotiationStatus.IN_PROGRESS, - facts={}, - message=message, - ) - - def render_continue_prompt( - self, - *, - record: NegotiationRecord, - context: NegotiationContext, - status: NegotiationStatus, - content_text: str, - ) -> ContinueResult: - """Render the next outbound prompt using the default passthrough behavior.""" - return ContinueResult( - prompt_text=self._prompt_renderer.render_continue( - negotiation_type=context.negotiation_type, - message=content_text, - ), - final_task_prompt=None, - ) diff --git a/src/a2a_t/negotiation/types/feasibility.py b/src/a2a_t/negotiation/types/feasibility.py deleted file mode 100644 index 79543635..00000000 --- a/src/a2a_t/negotiation/types/feasibility.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from .base import BaseNegotiationType - - -class FeasibilityNegotiationType(BaseNegotiationType): - """Handle feasibility negotiations with the shared base behavior.""" - - pass diff --git a/src/a2a_t/negotiation/types/information.py b/src/a2a_t/negotiation/types/information.py deleted file mode 100644 index 5dab4825..00000000 --- a/src/a2a_t/negotiation/types/information.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from a2a_t.negotiation.common.enums import NegotiationStatus -from a2a_t.negotiation.common.models import ContinueResult, NegotiationContext, NegotiationRecord, ReceiveResult - -from .base import BaseNegotiationType - - -class InformationNegotiationType(BaseNegotiationType): - """Handle information negotiations that may end in a completed task prompt.""" - - _COMPLETE_MESSAGE = "Task prompt is complete." - - def __init__(self, *, prompt_renderer: Any, prompt_checker: Any | None = None) -> None: - super().__init__(prompt_renderer=prompt_renderer) - self._prompt_checker = prompt_checker - - def process_received_message( - self, - *, - message: str, - context: NegotiationContext, - record: NegotiationRecord | None, - ) -> ReceiveResult: - """Validate received prompt content and decide whether more information is needed.""" - if context.status in {NegotiationStatus.AGREED, NegotiationStatus.REJECTED}: - return ReceiveResult( - need_response=False, - facts={}, - message=message, - ) - - if self._prompt_checker is None: - return super().process_received_message( - message=message, - context=context, - record=None, - ) - - compliance_result = self._prompt_checker.check( - processed_prompt_text=message, - ) - if compliance_result.success: - return ReceiveResult( - need_response=True, - facts={}, - message=self._COMPLETE_MESSAGE, - ) - - failure = compliance_result.failure or {} - failure_stage = str(failure.get("stage") or "") - failure_message = str(failure.get("message") or "").strip() - - if failure_stage == "slot_validation": - return ReceiveResult( - need_response=True, - facts={}, - message=failure_message or message, - ) - - return ReceiveResult( - need_response=True, - facts={}, - message=failure_message or "Task prompt validation failed.", - ) - - def render_continue_prompt( - self, - *, - record: NegotiationRecord, - context: NegotiationContext, - status: NegotiationStatus, - content_text: str, - ) -> ContinueResult: - """Render the next information negotiation prompt and finalize agreed task prompts.""" - prompt_text = self._prompt_renderer.render_continue( - negotiation_type=context.negotiation_type, - message=content_text, - ) - final_task_prompt = content_text if status == NegotiationStatus.AGREED else None - return ContinueResult( - prompt_text=prompt_text, - final_task_prompt=final_task_prompt, - ) diff --git a/src/a2a_t/negotiation/types/target.py b/src/a2a_t/negotiation/types/target.py deleted file mode 100644 index bbfc6b18..00000000 --- a/src/a2a_t/negotiation/types/target.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from .base import BaseNegotiationType - - -class TargetNegotiationType(BaseNegotiationType): - """Handle target negotiations with the shared base behavior.""" - - pass diff --git a/src/a2a_t/server/a2at_server.py b/src/a2a_t/server/a2at_server.py index b331c2e8..8c0b21a4 100644 --- a/src/a2a_t/server/a2at_server.py +++ b/src/a2a_t/server/a2at_server.py @@ -5,16 +5,12 @@ behind an input-length gate), the twelve negotiation content methods of the shared :class:`~a2a_t.negotiation.generation.content_service.NegotiationContentService` (eight generation plus four validation methods) and the extension-agnostic template queries of the -:class:`~a2a_t.common.prompt_resources.catalog.TemplateQueryService`. The three legacy -``start/receive/continue_negotiation`` methods of the deprecated state-machine negotiation demo -are kept for one release with a :class:`DeprecationWarning` and will be removed in the next -release (port decision D1). +:class:`~a2a_t.common.prompt_resources.catalog.TemplateQueryService`. """ from __future__ import annotations import logging -import warnings from collections.abc import Mapping from pathlib import Path from typing import Any @@ -31,7 +27,6 @@ from a2a_t.core.validation_pipeline import FilledParamData from a2a_t.llm.config_loader import LLMConfigLoader from a2a_t.llm.factory import LLMClientFactory -from a2a_t.negotiation.common.models import ContinueNegotiationInput, StartNegotiationInput from a2a_t.negotiation.content.models import ( NegotiationAbortData, NegotiationEndingData, @@ -41,7 +36,6 @@ from a2a_t.observability.llm_decorator import A2ATLLMClientDecorator from a2a_t.observability.negotiation_metrics import report_terminal_negotiation_rounds -from .negotiation.negotiation_orchestrator_builder import ServerNegotiationOrchestratorBuilder from .prompt_compliance.content_validator import InputLimitedContentValidator, build_content_validators from .prompt_compliance.models import PromptComplianceResult from .prompt_compliance.prompt_compliance_orchestrator_builder import PromptComplianceOrchestratorBuilder @@ -65,17 +59,6 @@ def _default_env_path() -> Path: return Path(__file__).resolve().parents[3] / "package_data" / ".env" -def _deprecation_message(method: str) -> str: - """Return the D1 deprecation message of one legacy negotiation facade method.""" - return ( - f"A2ATServer.{method} is deprecated since 1.1.0 and will be removed in the next release; " - "the state-machine negotiation demo it belongs to is retired. Use the negotiation content " - "API instead: generate_negotiation_{propose,accept,reject,abort}_prompt_from_data / " - "..._from_text and validate_{propose,accept,reject,abort}_prompt_and_data_filling, backed " - "by a2a_t.negotiation.generation.NegotiationContentService." - ) - - def _parse_template_uri(template_uri: str | TemplateUri | None) -> TemplateUri: """Parse the raw template URI string boundary fail-fast (D16, Java ``parseTemplateUri``). @@ -118,10 +101,6 @@ class A2ATServer: hand-written URI strings. Business failures are raised as catalog-coded exceptions of the :mod:`a2a_t.core.errors.exceptions` tree (catch :class:`~a2a_t.core.errors.exceptions.A2ATError` and branch on ``code_str``); programming errors stay ``TypeError`` / ``ValueError``. - - The three legacy ``start/receive/continue_negotiation`` methods of the retired state-machine - negotiation demo are kept for one release with a :class:`DeprecationWarning` and will be - removed in the next release. """ def __init__( @@ -132,18 +111,18 @@ def __init__( ) -> None: """Create one server facade from a ``.env`` file. - The configuration, the LLM client and both orchestrators are resolved eagerly so that a - misconfiguration surfaces at construction instead of on the first call. The negotiation - content service, the template query service and the extension content validators are - assembled lazily on first use: each captures resource snapshots at that point, so - constructing the facade stays side-effect free for the compliance-only flows. + The configuration, the LLM client and the prompt compliance orchestrator are resolved + eagerly so that a misconfiguration surfaces at construction instead of on the first call. + The negotiation content service, the template query service and the extension content + validators are assembled lazily on first use: each captures resource snapshots at that + point, so constructing the facade stays side-effect free for the compliance-only flows. Args: env_path: path of the ``.env`` file carrying the SDK configuration; ``None`` uses the packaged ``package_data/.env`` default. The resolved file must exist — copy ``env.example`` to ``.env`` first (every key is optional, an empty file works). - logger: optional logger injected into the LLM client and the orchestrators; ``None`` - uses each component's module-level logger. + logger: optional logger injected into the LLM client and the prompt compliance + orchestrator; ``None`` uses each component's module-level logger. Raises: ConfigFileNotFoundError: when the resolved ``.env`` path does not exist. @@ -162,12 +141,6 @@ def __init__( llm_client=self._llm_client, logger=logger, ) - self._negotiation_orchestrator = ServerNegotiationOrchestratorBuilder().build( - config=self._config, - llm_client=self._llm_client, - env_path=resolved_env_path, - logger=logger, - ) # Lazily assembled negotiation content service and template query service: both capture # resource snapshots at first use, so constructing the facade stays side-effect free for # the compliance-only flows. Tests may inject either seam before the first call. @@ -305,58 +278,6 @@ def validate_auth_prompt_and_data_filling( prompt, schema, _parse_template_uri(template_uri) ) - def start_negotiation(self, input: StartNegotiationInput) -> dict[str, object]: - """Start a server-side negotiation round. - - Deprecated (D1): the state-machine negotiation demo is retired; this method emits a - :class:`DeprecationWarning`, forwards to the legacy orchestrator unchanged, and will be - removed in the next release. Use the negotiation content API instead — - :meth:`generate_negotiation_propose_prompt_from_data` and its siblings. - - Args: - input: legacy negotiation start input. - - Returns: - the legacy orchestrator's round result as a plain mapping. - """ - warnings.warn(_deprecation_message("start_negotiation"), DeprecationWarning, stacklevel=2) - return self._negotiation_orchestrator.start_negotiation(input) - - def receive_negotiation(self, message: str, context: dict[str, object]) -> dict[str, object]: - """Process a negotiation message received from the remote peer. - - Deprecated (D1): the state-machine negotiation demo is retired; this method emits a - :class:`DeprecationWarning`, forwards to the legacy orchestrator unchanged, and will be - removed in the next release. Use the negotiation content API instead — - :meth:`validate_propose_prompt_and_data_filling` and its siblings. - - Args: - message: received negotiation message text. - context: legacy negotiation context mapping. - - Returns: - the legacy orchestrator's round result as a plain mapping. - """ - warnings.warn(_deprecation_message("receive_negotiation"), DeprecationWarning, stacklevel=2) - return self._negotiation_orchestrator.receive_negotiation(message, context) - - def continue_negotiation(self, input: ContinueNegotiationInput) -> dict[str, object]: - """Continue an existing negotiation with a local response. - - Deprecated (D1): the state-machine negotiation demo is retired; this method emits a - :class:`DeprecationWarning`, forwards to the legacy orchestrator unchanged, and will be - removed in the next release. Use the negotiation content API instead — - :meth:`generate_negotiation_accept_prompt_from_data` and its siblings. - - Args: - input: legacy negotiation continuation input. - - Returns: - the legacy orchestrator's round result as a plain mapping. - """ - warnings.warn(_deprecation_message("continue_negotiation"), DeprecationWarning, stacklevel=2) - return self._negotiation_orchestrator.continue_negotiation(input) - # ------------------------------------------------------------------ # Negotiation content generation (from data: deterministic, zero LLM) # ------------------------------------------------------------------ diff --git a/src/a2a_t/server/negotiation/__init__.py b/src/a2a_t/server/negotiation/__init__.py deleted file mode 100644 index e7d2d15a..00000000 --- a/src/a2a_t/server/negotiation/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .negotiation_orchestrator import NegotiationOrchestrator -from .negotiation_orchestrator_builder import ServerNegotiationOrchestratorBuilder - -__all__ = [ - "NegotiationOrchestrator", - "ServerNegotiationOrchestratorBuilder", -] diff --git a/src/a2a_t/server/negotiation/negotiation_orchestrator.py b/src/a2a_t/server/negotiation/negotiation_orchestrator.py deleted file mode 100644 index f6d0fa7a..00000000 --- a/src/a2a_t/server/negotiation/negotiation_orchestrator.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from a2a_t.negotiation.common.enums import NegotiationRole -from a2a_t.negotiation.runtime.base_negotiation_orchestrator import BaseNegotiationOrchestrator - - -class NegotiationOrchestrator(BaseNegotiationOrchestrator): - """Bind the shared negotiation runtime to the server role.""" - - def __init__(self, *, handler: Any, logger: Any | None = None) -> None: - super().__init__( - handler=handler, - role=NegotiationRole.SERVER, - logger=logger, - ) diff --git a/src/a2a_t/server/negotiation/negotiation_orchestrator_builder.py b/src/a2a_t/server/negotiation/negotiation_orchestrator_builder.py deleted file mode 100644 index f5993764..00000000 --- a/src/a2a_t/server/negotiation/negotiation_orchestrator_builder.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Any - -from a2a_t.common.prompt_runtime import PromptRuntimeComponentsBuilder -from a2a_t.config.models import A2ATConfig -from a2a_t.negotiation.common.enums import NegotiationType -from a2a_t.negotiation.handling import NegotiationHandler -from a2a_t.negotiation.rendering import NegotiationPromptRenderer -from a2a_t.negotiation.store import NegotiationStateStoreFactory -from a2a_t.negotiation.types import ( - FeasibilityNegotiationType, - InformationNegotiationType, - TargetNegotiationType, -) -from a2a_t.server.prompt_compliance.prompt_compliance_orchestrator_builder import PromptComplianceOrchestratorBuilder - -from .negotiation_orchestrator import NegotiationOrchestrator - - -class ServerNegotiationOrchestratorBuilder: - """Assemble the server-side negotiation runtime.""" - - def __init__( - self, - *, - prompt_compliance_builder: PromptComplianceOrchestratorBuilder | None = None, - runtime_components_builder: PromptRuntimeComponentsBuilder | None = None, - prompt_renderer_cls: type = NegotiationPromptRenderer, - store_factory: NegotiationStateStoreFactory | None = None, - handler_cls: type = NegotiationHandler, - orchestrator_cls: type = NegotiationOrchestrator, - ) -> None: - self._prompt_compliance_builder = prompt_compliance_builder or PromptComplianceOrchestratorBuilder() - self._runtime_components_builder = runtime_components_builder or PromptRuntimeComponentsBuilder() - self._prompt_renderer_cls = prompt_renderer_cls - self._store_factory = store_factory or NegotiationStateStoreFactory() - self._handler_cls = handler_cls - self._orchestrator_cls = orchestrator_cls - - def build( - self, - *, - config: A2ATConfig, - llm_client: Any, - env_path: str | Path | None = None, - logger: Any | None = None, - ) -> NegotiationOrchestrator: - """Build a server negotiation orchestrator with compliance-aware information flows.""" - components = self._runtime_components_builder.build(config=config) - prompt_checker = self._prompt_compliance_builder.build( - config=config, - llm_client=llm_client, - runtime_components=components, - logger=logger, - ) - prompt_renderer = self._prompt_renderer_cls() - store = self._build_store(env_path=env_path, logger=logger) - handler = self._handler_cls( - negotiation_types={ - NegotiationType.INFORMATION: InformationNegotiationType( - prompt_renderer=prompt_renderer, - prompt_checker=prompt_checker, - ), - NegotiationType.TARGET: TargetNegotiationType(prompt_renderer=prompt_renderer), - NegotiationType.FEASIBILITY: FeasibilityNegotiationType(prompt_renderer=prompt_renderer), - }, - store=store, - ) - return self._orchestrator_cls( # type: ignore[no-any-return] - handler=handler, - logger=logger, - ) - - def _build_store(self, *, env_path: str | Path | None, logger: Any | None) -> object: - """Resolve the negotiation state store configured for the server runtime.""" - return self._store_factory.build( - env_path=env_path, - logger=logger, - ) diff --git a/tests/client_prompt/test_a2at_client.py b/tests/client_prompt/test_a2at_client.py index 7468b4a1..2dcbd81c 100644 --- a/tests/client_prompt/test_a2at_client.py +++ b/tests/client_prompt/test_a2at_client.py @@ -6,8 +6,6 @@ from pathlib import Path from unittest.mock import patch -import pytest - PROJECT_ROOT = Path(__file__).resolve().parents[2] SRC_ROOT = PROJECT_ROOT / "src" @@ -17,8 +15,6 @@ from a2a_t.client.prompt_generation.models import PromptGenerationResult from a2a_t.llm.models import LLMClientConfig -from a2a_t.negotiation.common.enums import NegotiationStatus, NegotiationType -from a2a_t.negotiation.common.models import ContinueNegotiationInput, NegotiationContext, StartNegotiationInput from tests.support import TEST_ENV_PATH, ManagedTempDirTestCase @@ -57,25 +53,6 @@ def build(self, **kwargs: object) -> FakePromptGenerationOrchestrator: return self._orchestrator -class FakeNegotiationOrchestrator: - def __init__(self) -> None: - self.start_calls: list[object] = [] - self.receive_calls: list[dict[str, object]] = [] - self.continue_calls: list[object] = [] - - def start_negotiation(self, input: StartNegotiationInput) -> dict[str, object]: - self.start_calls.append(input) - return {"started": True} - - def receive_negotiation(self, message: str, context: dict[str, object]) -> dict[str, object]: - self.receive_calls.append({"message": message, "context": context}) - return {"received": True} - - def continue_negotiation(self, input: ContinueNegotiationInput) -> dict[str, object]: - self.continue_calls.append(input) - return {"continued": True} - - class A2ATClientTest(unittest.TestCase): def test_constructor_signature_remains_stable(self) -> None: from a2a_t.client.a2at_client import A2ATClient @@ -86,7 +63,7 @@ def test_constructor_signature_remains_stable(self) -> None: self.assertEqual(signature.parameters["env_path"].kind, inspect.Parameter.KEYWORD_ONLY) self.assertEqual(signature.parameters["logger"].kind, inspect.Parameter.KEYWORD_ONLY) - def test_a2at_client_delegates_all_public_methods_with_typed_negotiation_inputs(self) -> None: + def test_a2at_client_delegates_prompt_generation_with_configured_llm(self) -> None: from a2a_t.client.a2at_client import A2ATClient prompt_result = PromptGenerationResult( @@ -96,62 +73,20 @@ def test_a2at_client_delegates_all_public_methods_with_typed_negotiation_inputs( ) prompt_orchestrator = FakePromptGenerationOrchestrator(prompt_result) prompt_builder = FakePromptGenerationBuilder(prompt_orchestrator) - negotiation = FakeNegotiationOrchestrator() llm_config = build_llm_config() llm_client = object() - start_input = StartNegotiationInput( - type=NegotiationType.TARGET, - content_text="Clarify please", - facts={}, - ) - continue_input = ContinueNegotiationInput( - context=NegotiationContext.from_context( - { - "negotiationType": "target", - "negotiationId": "neg-1", - "role": "client", - "round": 1, - "status": "in-progress", - "extra": {}, - } - ), - status=NegotiationStatus.IN_PROGRESS, - content_text="Here is more detail.", - ) - with ( patch("a2a_t.client.a2at_client._default_env_path", return_value=TEST_ENV_PATH), patch("a2a_t.client.a2at_client.LLMConfigLoader.load", return_value=llm_config) as load_llm_config, patch("a2a_t.client.a2at_client.LLMClientFactory.create", return_value=llm_client) as create_llm_client, patch("a2a_t.client.a2at_client.PromptGenerationOrchestratorBuilder", return_value=prompt_builder), - patch("a2a_t.client.a2at_client.ClientNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = negotiation client = A2ATClient() result = client.generate_task_prompt("Analyze Site A.") self.assertIs(result, prompt_result) - # The three legacy state-machine methods keep their forwarding behavior for one - # release and warn on every call (D1 deprecation shim round). - with pytest.warns(DeprecationWarning, match=r"A2ATClient\.\w+ is deprecated since 1\.1\.0"): - self.assertEqual(client.start_negotiation(start_input), {"started": True}) - self.assertEqual( - client.receive_negotiation( - "Clarify intent", - { - "negotiationType": "target", - "negotiationId": "neg-1", - "role": "client", - "round": 1, - "status": "in-progress", - "extra": {}, - }, - ), - {"received": True}, - ) - self.assertEqual(client.continue_negotiation(continue_input), {"continued": True}) load_llm_config.assert_called_once_with(TEST_ENV_PATH) create_llm_client.assert_called_once_with(llm_config.provider, llm_config, logger=None) @@ -160,9 +95,6 @@ def test_a2at_client_delegates_all_public_methods_with_typed_negotiation_inputs( built_llm_client = prompt_builder.calls[0]["llm_client"] self.assertIs(built_llm_client._inner, llm_client) self.assertEqual(prompt_orchestrator.calls, ["Analyze Site A."]) - self.assertEqual(negotiation.start_calls, [start_input]) - self.assertEqual(negotiation.receive_calls[0]["message"], "Clarify intent") - self.assertEqual(negotiation.continue_calls, [continue_input]) class A2ATClientPromptResourceTimingTest(ManagedTempDirTestCase): @@ -209,12 +141,10 @@ def missing_prompts_read_text(self: object, key: object) -> str: return original_read_text(self, key) with ( - patch("a2a_t.client.a2at_client.ClientNegotiationOrchestratorBuilder") as negotiation_builder_cls, patch("a2a_t.client.a2at_client.LLMConfigLoader.load", return_value=build_llm_config()), patch("a2a_t.client.a2at_client.LLMClientFactory.create", return_value=object()), patch.object(PackagedResourceReader, "read_text", missing_prompts_read_text), ): - negotiation_builder_cls.return_value.build.return_value = object() client = A2ATClient(env_path=env_path) result = client.generate_task_prompt("Analyze Site A.") diff --git a/tests/client_prompt/test_a2at_client_negotiation_api.py b/tests/client_prompt/test_a2at_client_negotiation_api.py index 4ac406c9..23e35973 100644 --- a/tests/client_prompt/test_a2at_client_negotiation_api.py +++ b/tests/client_prompt/test_a2at_client_negotiation_api.py @@ -6,16 +6,13 @@ like the service boundary. The from-data legs run against the real packaged templates with no LLM (the factory is patched to a stand-in client), the from-text and validation legs run against a scripted LLM client recording every structured call, and the golden fixtures pin the rendered -messages byte-for-byte. The deprecation shim round (D1) is pinned here too: the three legacy -state-machine facade methods warn, and so does the import of every old negotiation demo package. +messages byte-for-byte. """ from __future__ import annotations -import importlib import sys from pathlib import Path -from types import ModuleType from typing import Any, Callable from unittest.mock import patch @@ -75,17 +72,6 @@ "validate_abort_prompt_and_data_filling", ) -#: The six retired state-machine negotiation demo packages kept for one release (D1). -OLD_NEGOTIATION_PACKAGES = ( - "a2a_t.negotiation.common", - "a2a_t.negotiation.types", - "a2a_t.negotiation.store", - "a2a_t.negotiation.runtime", - "a2a_t.negotiation.handling", - "a2a_t.negotiation.rendering", -) - - class ScriptedLlmClient: """LLM boundary fake replaying scripted payloads and recording every structured call.""" @@ -709,48 +695,6 @@ def test_get_prompt_answers_none_for_unknown_templates(unknown_uri: str, tmp_pat assert client.get_prompt(unknown_uri) is None -# -------------------------------------------------------------------------------------- -# deprecation shim round (D1) -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - ("method", "build_input"), - ( - ("start_negotiation", lambda: object()), - ("receive_negotiation", lambda: ("message", {})), - ("continue_negotiation", lambda: object()), - ), -) -def test_legacy_negotiation_methods_warn_and_still_delegate( - method: str, build_input: Callable[[], Any], tmp_path: Path -) -> None: - client = build_client(write_env(tmp_path, "zh-CN")) - input = build_input() - with patch.object(client._negotiation_orchestrator, method, return_value={"legacy": True}) as delegated: - with pytest.warns(DeprecationWarning, match=f"A2ATClient\\.{method} is deprecated since 1\\.1\\.0"): - - def call() -> Any: - return ( - getattr(client, method)(input) - if method != "receive_negotiation" - else client.receive_negotiation(*input) - ) - - result = call() - - assert result == {"legacy": True} - delegated.assert_called_once() - - -@pytest.mark.parametrize("package_name", OLD_NEGOTIATION_PACKAGES) -def test_importing_an_old_negotiation_package_warns(package_name: str) -> None: - module: ModuleType = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) - - def test_negotiation_api_surface_is_complete() -> None: for method in (*NEGOTIATION_API_METHODS, "get_prompts", "get_prompt"): assert callable(getattr(A2ATClient, method, None)), f"A2ATClient is missing {method}" diff --git a/tests/client_prompt/test_input_limit_gate.py b/tests/client_prompt/test_input_limit_gate.py index 613c36d1..88f97227 100644 --- a/tests/client_prompt/test_input_limit_gate.py +++ b/tests/client_prompt/test_input_limit_gate.py @@ -228,9 +228,7 @@ def test_generate_task_prompt_rejects_oversized_input_at_the_facade_entry(self) patch("a2a_t.client.a2at_client._default_env_path", return_value=env_path), patch("a2a_t.client.a2at_client.LLMConfigLoader.load", return_value=_build_llm_config()), patch("a2a_t.client.a2at_client.LLMClientFactory.create", return_value=object()), - patch("a2a_t.client.a2at_client.ClientNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = object() client = A2ATClient(env_path=env_path) result = client.generate_task_prompt("x" * (DEFAULT_MAX_TEXT_CHARS + 1)) diff --git a/tests/client_prompt/test_prompt_side_apis.py b/tests/client_prompt/test_prompt_side_apis.py index 120960c1..9821ff56 100644 --- a/tests/client_prompt/test_prompt_side_apis.py +++ b/tests/client_prompt/test_prompt_side_apis.py @@ -423,7 +423,6 @@ def test_facade_checks_input_arguments_before_the_template_uri(method_name: str, patch("a2a_t.client.a2at_client.LLMConfigLoader.load", return_value=llm_config), patch("a2a_t.client.a2at_client.LLMClientFactory.create", return_value=object()), patch("a2a_t.client.a2at_client.PromptGenerationOrchestratorBuilder"), - patch("a2a_t.client.a2at_client.ClientNegotiationOrchestratorBuilder"), ): client = A2ATClient() diff --git a/tests/corpus/assemblers.py b/tests/corpus/assemblers.py index e90ee10c..9e812537 100644 --- a/tests/corpus/assemblers.py +++ b/tests/corpus/assemblers.py @@ -238,7 +238,6 @@ def _minimal_env_for(language: str, max_attempts: int) -> Path: "A2AT_LLM_BASE_URL=https://llm.example.test/v1", "A2AT_LLM_API_KEY=corpus-scripted", f"A2AT_LLM_MAX_ATTEMPTS={max_attempts}", - "A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory", "", ) ), diff --git a/tests/corpus/live/env_writer.py b/tests/corpus/live/env_writer.py index 21eced8a..8e46ef37 100644 --- a/tests/corpus/live/env_writer.py +++ b/tests/corpus/live/env_writer.py @@ -64,7 +64,6 @@ def env_file_for(config: LiveLlmConfig) -> Path: f"A2AT_LLM_TEMPERATURE={config.temperature}", f"A2AT_LLM_TIMEOUT_SECONDS={config.timeout_seconds}", f"A2AT_LLM_MAX_ATTEMPTS={MAX_ATTEMPTS}", - "A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory", "", ) ), diff --git a/tests/corpus/live/test_live_config.py b/tests/corpus/live/test_live_config.py index 1e63bcbd..67f20e11 100644 --- a/tests/corpus/live/test_live_config.py +++ b/tests/corpus/live/test_live_config.py @@ -234,7 +234,6 @@ def test_env_file_carries_the_live_bridge_entries() -> None: "A2AT_LLM_TEMPERATURE=0", "A2AT_LLM_TIMEOUT_SECONDS=60", "A2AT_LLM_MAX_ATTEMPTS=3", - "A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory", ] diff --git a/tests/negotiation/test_handling_runtime.py b/tests/negotiation/test_handling_runtime.py deleted file mode 100644 index 18aa5340..00000000 --- a/tests/negotiation/test_handling_runtime.py +++ /dev/null @@ -1,341 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -class NegotiationHandlingRuntimeTest(unittest.TestCase): - def _target_types(self): - from a2a_t.negotiation.common.enums import NegotiationType - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.target import TargetNegotiationType - - return { - NegotiationType.TARGET: TargetNegotiationType(prompt_renderer=NegotiationPromptRenderer()), - } - - def test_handler_start_returns_fixed_key_map_and_saves_record(self) -> None: - from a2a_t.negotiation.common.constants import NEGOTIATION_T_URI_NL - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationType - from a2a_t.negotiation.common.models import StartNegotiationInput - from a2a_t.negotiation.handling.negotiation_handler import NegotiationHandler - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - handler = NegotiationHandler( - negotiation_types=self._target_types(), - store=store, - ) - - payload = handler.start( - input=StartNegotiationInput( - type=NegotiationType.TARGET, - content_text="Please clarify the request.", - facts={"targetItems": [{"name": "intent"}]}, - ), - role=NegotiationRole.CLIENT, - ) - - self.assertIn(NEGOTIATION_T_URI_NL, payload) - negotiation_data = payload[NEGOTIATION_T_URI_NL] - self.assertEqual(negotiation_data["message"], "Please clarify the request.") - self.assertTrue(negotiation_data["negotiationId"]) - self.assertIsNotNone(store.get(negotiation_data["negotiationId"])) - - def test_handler_receive_allows_first_round_without_existing_record(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.handling.negotiation_handler import NegotiationHandler - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - handler = NegotiationHandler( - negotiation_types=self._target_types(), - store=InMemoryNegotiationStateStore(), - ) - - result = handler.receive( - message="Clarify intent", - context={ - "negotiationType": "target", - "negotiationId": "neg-receive", - "role": "client", - "round": 1, - "status": "in-progress", - "extra": {}, - }, - ) - - self.assertTrue(result["needResponse"]) - self.assertEqual(result["context"]["negotiationType"], NegotiationType.TARGET.value) - self.assertEqual(result["context"]["role"], NegotiationRole.CLIENT.value) - self.assertEqual(result["context"]["status"], NegotiationStatus.IN_PROGRESS.value) - self.assertEqual(result["facts"], {}) - self.assertEqual(result["message"], "Clarify intent") - - def test_handler_continue_returns_map_with_incremented_round(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import ( - ContinueNegotiationInput, - NegotiationContext, - NegotiationRecord, - ReceiveResult, - ) - from a2a_t.negotiation.handling.negotiation_handler import NegotiationHandler - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - context = NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-continue", - role=NegotiationRole.CLIENT, - round=1, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ) - store.save( - NegotiationRecord( - context=context, - last_message="old", - last_receive_result=ReceiveResult(need_response=True, facts={"targetItems": []}), - last_continue_result=None, - last_task_prompt=None, - created_at="t1", - updated_at="t1", - ) - ) - handler = NegotiationHandler( - negotiation_types=self._target_types(), - store=store, - ) - - payload = handler.continue_( - input=ContinueNegotiationInput( - context=context, - status=NegotiationStatus.IN_PROGRESS, - content_text="Here is the target.", - ) - ) - - negotiation_data = payload[ - "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/NL/v1" - ] - self.assertEqual(negotiation_data["round"], 2) - self.assertEqual(negotiation_data["message"], "Here is the target.") - - def test_handler_receive_terminal_message_returns_need_response_false(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext, NegotiationRecord - from a2a_t.negotiation.handling.negotiation_handler import NegotiationHandler - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - store.save( - NegotiationRecord( - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-terminal", - role=NegotiationRole.CLIENT, - round=1, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ), - last_message="old", - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at="t1", - updated_at="t1", - ) - ) - handler = NegotiationHandler( - negotiation_types=self._target_types(), - store=store, - ) - - result = handler.receive( - message="Clarify intent", - context={ - "negotiationType": "target", - "negotiationId": "neg-terminal", - "role": "client", - "round": 2, - "status": "agreed", - "extra": {}, - }, - ) - - self.assertFalse(result["needResponse"]) - - def test_handler_receive_rejects_incoming_round_that_skips_local_progress(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.errors import NegotiationStateError - from a2a_t.negotiation.common.models import NegotiationContext, NegotiationRecord - from a2a_t.negotiation.handling.negotiation_handler import NegotiationHandler - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - store.save( - NegotiationRecord( - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-round-skip", - role=NegotiationRole.CLIENT, - round=2, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ), - last_message="old", - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at="t1", - updated_at="t1", - ) - ) - handler = NegotiationHandler( - negotiation_types=self._target_types(), - store=store, - ) - - with self.assertRaises(NegotiationStateError): - handler.receive( - message="Clarify intent", - context={ - "negotiationType": "target", - "negotiationId": "neg-round-skip", - "role": "client", - "round": 4, - "status": "in-progress", - "extra": {}, - }, - ) - - def test_handler_continue_rejects_context_round_mismatch(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.errors import NegotiationStateError - from a2a_t.negotiation.common.models import ContinueNegotiationInput, NegotiationContext, NegotiationRecord - from a2a_t.negotiation.handling.negotiation_handler import NegotiationHandler - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - store.save( - NegotiationRecord( - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-continue-mismatch", - role=NegotiationRole.CLIENT, - round=2, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ), - last_message="old", - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at="t1", - updated_at="t1", - ) - ) - handler = NegotiationHandler( - negotiation_types=self._target_types(), - store=store, - ) - - with self.assertRaises(NegotiationStateError): - handler.continue_( - input=ContinueNegotiationInput( - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-continue-mismatch", - role=NegotiationRole.CLIENT, - round=1, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ), - status=NegotiationStatus.IN_PROGRESS, - content_text="Here is the target.", - ) - ) - - def test_handler_receive_returns_reject_guidance_when_incoming_round_hits_limit(self) -> None: - from a2a_t.negotiation.common.constants import MAX_IN_PROGRESS_NEGOTIATION_ROUND - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext, NegotiationRecord - from a2a_t.negotiation.handling.negotiation_handler import NegotiationHandler - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - previous_round = MAX_IN_PROGRESS_NEGOTIATION_ROUND - 1 - store.save( - NegotiationRecord( - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-round-limit", - role=NegotiationRole.CLIENT, - round=previous_round, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ), - last_message="old", - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at="t1", - updated_at="t1", - ) - ) - handler = NegotiationHandler( - negotiation_types=self._target_types(), - store=store, - ) - - result = handler.receive( - message="Clarify intent", - context={ - "negotiationType": "target", - "negotiationId": "neg-round-limit", - "role": "client", - "round": MAX_IN_PROGRESS_NEGOTIATION_ROUND, - "status": "in-progress", - "extra": {}, - }, - ) - - self.assertTrue(result["needResponse"]) - self.assertEqual( - result["message"], "Negotiation reached the maximum in-progress round limit. Please reject it." - ) - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages this suite pins emit a -# DeprecationWarning when imported and will be removed in the next release. The behavioral -# assertions above stay untouched; this only pins the warning contract of the deprecated -# entry points this file exercises (handling + its store/types/rendering/common dependencies). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "package_name", - ( - "a2a_t.negotiation.handling", - "a2a_t.negotiation.store", - "a2a_t.negotiation.types", - "a2a_t.negotiation.rendering", - "a2a_t.negotiation.common", - ), -) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/negotiation/test_legacy_api_removed.py b/tests/negotiation/test_legacy_api_removed.py new file mode 100644 index 00000000..870f9046 --- /dev/null +++ b/tests/negotiation/test_legacy_api_removed.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import importlib +import sys +import unittest +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +SRC_ROOT = PROJECT_ROOT / "src" + +if str(SRC_ROOT) not in sys.path: + sys.path.insert(0, str(SRC_ROOT)) + +REMOVED_NEGOTIATION_MODULES = ( + "a2a_t.negotiation.common", + "a2a_t.negotiation.types", + "a2a_t.negotiation.store", + "a2a_t.negotiation.runtime", + "a2a_t.negotiation.handling", + "a2a_t.negotiation.rendering", + "a2a_t.client.negotiation", + "a2a_t.server.negotiation", +) + +REMOVED_FACADE_METHODS = ( + "start_negotiation", + "receive_negotiation", + "continue_negotiation", +) + + +class NegotiationLegacyApiRemovedTest(unittest.TestCase): + def test_legacy_state_machine_modules_are_removed(self) -> None: + for module_name in REMOVED_NEGOTIATION_MODULES: + with self.subTest(module_name=module_name): + sys.modules.pop(module_name, None) + with self.assertRaises(ModuleNotFoundError): + importlib.import_module(module_name) + + def test_facades_no_longer_expose_the_legacy_methods(self) -> None: + from a2a_t.client.a2at_client import A2ATClient + from a2a_t.server.a2at_server import A2ATServer + + for facade in (A2ATClient, A2ATServer): + for method in REMOVED_FACADE_METHODS: + with self.subTest(facade=facade.__name__, method=method): + self.assertFalse(hasattr(facade, method)) diff --git a/tests/negotiation/test_models_runtime.py b/tests/negotiation/test_models_runtime.py deleted file mode 100644 index 278e1971..00000000 --- a/tests/negotiation/test_models_runtime.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -class NegotiationModelsRuntimeTest(unittest.TestCase): - def test_negotiation_context_to_context_uses_protocol_field_names(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext - - context = NegotiationContext( - negotiation_type=NegotiationType.INFORMATION, - negotiation_id="neg-1", - role=NegotiationRole.CLIENT, - round=2, - status=NegotiationStatus.IN_PROGRESS, - extra={"x": "y"}, - ) - - self.assertEqual( - context.to_context(), - { - "negotiationType": "information", - "negotiationId": "neg-1", - "role": "client", - "round": 2, - "status": "in-progress", - "extra": {"x": "y"}, - }, - ) - - def test_negotiation_context_from_context_uses_protocol_field_names(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext - - context = NegotiationContext.from_context( - { - "negotiationType": "information", - "negotiationId": "neg-3", - "role": "server", - "round": 4, - "status": "agreed", - "extra": {}, - } - ) - - self.assertEqual(context.negotiation_type, NegotiationType.INFORMATION) - self.assertEqual(context.negotiation_id, "neg-3") - self.assertEqual(context.role, NegotiationRole.SERVER) - self.assertEqual(context.round, 4) - self.assertEqual(context.status, NegotiationStatus.AGREED) - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages this suite pins emit a -# DeprecationWarning when imported and will be removed in the next release. The behavioral -# assertions above stay untouched; this only pins the warning contract of the deprecated -# entry points this file exercises (the common model package). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize("package_name", ("a2a_t.negotiation.common",)) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/negotiation/test_orchestrator_builders.py b/tests/negotiation/test_orchestrator_builders.py deleted file mode 100644 index 7fe7d062..00000000 --- a/tests/negotiation/test_orchestrator_builders.py +++ /dev/null @@ -1,262 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -from a2a_t.config.models import A2ATConfig, PromptRuntimeConfig - - -class FakePromptComplianceBuilder: - def __init__(self, checker: object) -> None: - self._checker = checker - self.calls: list[dict[str, object]] = [] - - def build(self, **kwargs: object) -> object: - self.calls.append(dict(kwargs)) - return self._checker - - -class FakeRuntimeComponentsBuilder: - def __init__(self, components: object) -> None: - self._components = components - self.calls: list[object] = [] - - def build(self, *, config: A2ATConfig) -> object: - self.calls.append(config) - return self._components - - -class FakePromptRenderer: - last_kwargs: dict[str, object] | None = None - - def __init__(self, **kwargs: object) -> None: - type(self).last_kwargs = dict(kwargs) - - -class FakeOrchestrator: - last_kwargs: dict[str, object] | None = None - - def __init__(self, **kwargs: object) -> None: - type(self).last_kwargs = dict(kwargs) - - def start_negotiation(self, input: object) -> dict[str, object]: - return { - "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/NL/v1": { - "role": "fake", - "message": "fake", - } - } - - -class FakeStoreFactory: - def __init__(self) -> None: - self.calls: list[dict[str, object]] = [] - - def build(self, *, env_path=None, logger=None): - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - self.calls.append( - { - "env_path": env_path, - "logger": logger, - } - ) - return InMemoryNegotiationStateStore() - - -class FakePromptChecker: - def check(self, *, processed_prompt_text: str): - from a2a_t.server.prompt_compliance.models import PromptComplianceResult - - return PromptComplianceResult( - success=True, - ) - - -class FakeLogger: - def info(self, message: str, *args: object) -> None: - pass - - -class NegotiationOrchestratorBuilderTest(unittest.TestCase): - def _config(self) -> A2ATConfig: - # Explicit source type: the default flipped to packaged in 1.1.0 (D10 step 2), and these - # builder tests only need the bundled packaged tree either way. - return A2ATConfig( - prompt=PromptRuntimeConfig(source_type="packaged"), - ) - - def test_client_builder_builds_working_orchestrator(self) -> None: - from a2a_t.client.negotiation.negotiation_orchestrator_builder import ClientNegotiationOrchestratorBuilder - from a2a_t.negotiation.common.enums import NegotiationType - from a2a_t.negotiation.common.models import StartNegotiationInput - - store_factory = FakeStoreFactory() - builder = ClientNegotiationOrchestratorBuilder(store_factory=store_factory) - orchestrator = builder.build() - - result = orchestrator.start_negotiation( - StartNegotiationInput( - type=NegotiationType.TARGET, - content_text="Please clarify.", - facts={}, - ) - ) - - self.assertIn( - "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/NL/v1", result - ) - negotiation_data = result[ - "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/NL/v1" - ] - self.assertEqual(negotiation_data["role"], "client") - self.assertEqual(len(store_factory.calls), 1) - - def test_server_builder_builds_working_orchestrator(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationType - from a2a_t.negotiation.common.models import StartNegotiationInput - from a2a_t.server.negotiation.negotiation_orchestrator_builder import ServerNegotiationOrchestratorBuilder - - prompt_checker = FakePromptChecker() - prompt_compliance_builder = FakePromptComplianceBuilder(prompt_checker) - store_factory = FakeStoreFactory() - builder = ServerNegotiationOrchestratorBuilder( - prompt_compliance_builder=prompt_compliance_builder, - store_factory=store_factory, - ) - - orchestrator = builder.build( - config=self._config(), - llm_client=object(), - ) - result = orchestrator.start_negotiation( - StartNegotiationInput( - type=NegotiationType.INFORMATION, - content_text="Need more details.", - facts={}, - ) - ) - - self.assertIn( - "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/NL/v1", result - ) - negotiation_data = result[ - "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/NL/v1" - ] - self.assertEqual(negotiation_data["role"], "server") - self.assertEqual(len(prompt_compliance_builder.calls), 1) - self.assertEqual(len(store_factory.calls), 1) - - def test_server_builder_builds_shared_runtime_once_and_reuses_it(self) -> None: - from a2a_t.server.negotiation.negotiation_orchestrator_builder import ServerNegotiationOrchestratorBuilder - - prompt_checker = FakePromptChecker() - prompt_compliance_builder = FakePromptComplianceBuilder(prompt_checker) - components = type( - "Components", - (), - {}, - )() - runtime_builder = FakeRuntimeComponentsBuilder(components) - builder = ServerNegotiationOrchestratorBuilder( - prompt_compliance_builder=prompt_compliance_builder, - runtime_components_builder=runtime_builder, - prompt_renderer_cls=FakePromptRenderer, - store_factory=FakeStoreFactory(), - ) - - builder.build( - config=self._config(), - llm_client=object(), - ) - - self.assertEqual(len(runtime_builder.calls), 1) - self.assertIs(prompt_compliance_builder.calls[0]["runtime_components"], components) - self.assertNotIn("resource_root", prompt_compliance_builder.calls[0]) - self.assertEqual(FakePromptRenderer.last_kwargs, {}) - - def test_server_builder_passes_logger_to_prompt_compliance_builder(self) -> None: - from a2a_t.server.negotiation.negotiation_orchestrator_builder import ServerNegotiationOrchestratorBuilder - - prompt_checker = FakePromptChecker() - prompt_compliance_builder = FakePromptComplianceBuilder(prompt_checker) - logger = FakeLogger() - builder = ServerNegotiationOrchestratorBuilder( - prompt_compliance_builder=prompt_compliance_builder, - store_factory=FakeStoreFactory(), - ) - - builder.build( - config=self._config(), - llm_client=object(), - logger=logger, - ) - - self.assertIs(prompt_compliance_builder.calls[0]["logger"], logger) - - def test_client_builder_passes_logger_to_orchestrator(self) -> None: - from a2a_t.client.negotiation.negotiation_orchestrator_builder import ClientNegotiationOrchestratorBuilder - - logger = FakeLogger() - builder = ClientNegotiationOrchestratorBuilder( - store_factory=FakeStoreFactory(), - orchestrator_cls=FakeOrchestrator, - ) - - builder.build(logger=logger) - - assert FakeOrchestrator.last_kwargs is not None - self.assertIs(FakeOrchestrator.last_kwargs["logger"], logger) - - def test_server_builder_passes_logger_to_orchestrator(self) -> None: - from a2a_t.server.negotiation.negotiation_orchestrator_builder import ServerNegotiationOrchestratorBuilder - - logger = FakeLogger() - builder = ServerNegotiationOrchestratorBuilder( - prompt_compliance_builder=FakePromptComplianceBuilder(FakePromptChecker()), - store_factory=FakeStoreFactory(), - orchestrator_cls=FakeOrchestrator, - ) - - builder.build( - config=self._config(), - llm_client=object(), - logger=logger, - ) - - assert FakeOrchestrator.last_kwargs is not None - self.assertIs(FakeOrchestrator.last_kwargs["logger"], logger) - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages behind the legacy -# orchestrator builders this suite pins emit a DeprecationWarning when imported and will be -# removed in the next release. The behavioral assertions above stay untouched; this only -# pins the warning contract of the deprecated entry points this file exercises (the common -# and store packages the builders assemble the legacy orchestrators from). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "package_name", - ( - "a2a_t.negotiation.common", - "a2a_t.negotiation.store", - ), -) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/negotiation/test_orchestrators.py b/tests/negotiation/test_orchestrators.py deleted file mode 100644 index daf9b487..00000000 --- a/tests/negotiation/test_orchestrators.py +++ /dev/null @@ -1,259 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -class FakeNegotiationHandler: - def __init__(self) -> None: - self.start_calls: list[dict[str, object]] = [] - self.receive_calls: list[dict[str, object]] = [] - self.continue_calls: list[dict[str, object]] = [] - - def start(self, *, input: object, role: object) -> dict[str, object]: - self.start_calls.append({"input": input, "role": role}) - return {"started": True} - - def receive(self, *, message: str, context: dict[str, object]): - self.receive_calls.append({"message": message, "context": context}) - return { - "context": { - "negotiationType": "target", - "negotiationId": "neg-1", - "role": "client", - "round": 1, - "status": "in-progress", - "extra": {}, - }, - "needResponse": True, - "facts": {"targetItems": []}, - "message": "Please clarify.", - } - - def continue_(self, *, input: object) -> dict[str, object]: - self.continue_calls.append({"input": input}) - return {"continued": True} - - -class FakeLogger: - def __init__(self) -> None: - self.info_messages: list[tuple[str, tuple[object, ...]]] = [] - - def info(self, message: str, *args: object) -> None: - self.info_messages.append((message, args)) - - -class FalsyLogger(FakeLogger): - def __bool__(self) -> bool: - return False - - -class NegotiationOrchestratorTest(unittest.TestCase): - def test_client_orchestrator_start_negotiation_uses_client_role(self) -> None: - from a2a_t.client.negotiation.negotiation_orchestrator import NegotiationOrchestrator - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationType - from a2a_t.negotiation.common.models import StartNegotiationInput - - handler = FakeNegotiationHandler() - orchestrator = NegotiationOrchestrator(handler=handler) - - result = orchestrator.start_negotiation( - StartNegotiationInput( - type=NegotiationType.TARGET, - content_text="Please clarify.", - facts={}, - ) - ) - - self.assertEqual(result, {"started": True}) - self.assertEqual(handler.start_calls[0]["role"], NegotiationRole.CLIENT) - - def test_server_orchestrator_start_negotiation_uses_server_role(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationType - from a2a_t.negotiation.common.models import StartNegotiationInput - from a2a_t.server.negotiation.negotiation_orchestrator import NegotiationOrchestrator - - handler = FakeNegotiationHandler() - orchestrator = NegotiationOrchestrator(handler=handler) - - result = orchestrator.start_negotiation( - StartNegotiationInput( - type=NegotiationType.INFORMATION, - content_text="Need more information.", - facts={}, - ) - ) - - self.assertEqual(result, {"started": True}) - self.assertEqual(handler.start_calls[0]["role"], NegotiationRole.SERVER) - - def test_receive_negotiation_returns_public_dict(self) -> None: - from a2a_t.client.negotiation.negotiation_orchestrator import NegotiationOrchestrator - - orchestrator = NegotiationOrchestrator( - handler=FakeNegotiationHandler(), - ) - - result = orchestrator.receive_negotiation( - "message", - { - "negotiationType": "target", - "negotiationId": "neg-1", - "role": "client", - "round": 1, - "status": "in-progress", - "extra": {}, - }, - ) - - self.assertEqual(result["context"]["negotiationId"], "neg-1") - self.assertEqual(result["needResponse"], True) - self.assertEqual(result["message"], "Please clarify.") - - def test_continue_negotiation_returns_handler_payload(self) -> None: - from a2a_t.client.negotiation.negotiation_orchestrator import NegotiationOrchestrator - from a2a_t.negotiation.common.enums import NegotiationStatus - from a2a_t.negotiation.common.models import ContinueNegotiationInput, NegotiationContext - - handler = FakeNegotiationHandler() - orchestrator = NegotiationOrchestrator( - handler=handler, - ) - - result = orchestrator.continue_negotiation( - ContinueNegotiationInput( - context=NegotiationContext.from_context( - { - "negotiationType": "target", - "negotiationId": "neg-1", - "role": "client", - "round": 1, - "status": "in-progress", - "extra": {}, - } - ), - status=NegotiationStatus.IN_PROGRESS, - content_text="Here is the target.", - ) - ) - - self.assertEqual(result, {"continued": True}) - self.assertEqual(len(handler.continue_calls), 1) - self.assertEqual(handler.continue_calls[0]["input"].context.negotiation_id, "neg-1") - - def test_orchestrator_uses_explicit_logger_even_when_logger_is_falsy(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationType - from a2a_t.negotiation.common.models import StartNegotiationInput - from a2a_t.server.negotiation.negotiation_orchestrator import NegotiationOrchestrator - - logger = FalsyLogger() - orchestrator = NegotiationOrchestrator( - handler=FakeNegotiationHandler(), - logger=logger, - ) - - orchestrator.start_negotiation( - StartNegotiationInput( - type=NegotiationType.INFORMATION, - content_text="Need more information.", - facts={}, - ) - ) - - self.assertIn( - ("negotiation_start_started role=%s type=%s", ("server", "information")), - logger.info_messages, - ) - - def test_orchestrator_logs_lifecycle_events_without_message_content(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import ContinueNegotiationInput, NegotiationContext, StartNegotiationInput - from a2a_t.server.negotiation.negotiation_orchestrator import NegotiationOrchestrator - - logger = FakeLogger() - orchestrator = NegotiationOrchestrator( - handler=FakeNegotiationHandler(), - logger=logger, - ) - start_input = StartNegotiationInput( - type=NegotiationType.INFORMATION, - content_text="Need more information.", - facts={}, - ) - context = { - "negotiationType": "target", - "negotiationId": "neg-1", - "role": "client", - "round": 1, - "status": "in-progress", - "extra": {}, - } - - orchestrator.start_negotiation(start_input) - orchestrator.receive_negotiation("sensitive message", context) - orchestrator.continue_negotiation( - ContinueNegotiationInput( - context=NegotiationContext.from_context(context), - status=NegotiationStatus.IN_PROGRESS, - content_text="sensitive continuation", - ) - ) - - self.assertIn( - ("negotiation_start_started role=%s type=%s", ("server", "information")), - logger.info_messages, - ) - self.assertIn( - ("negotiation_start_completed role=%s type=%s id=%s status=%s", ("server", None, None, None)), - logger.info_messages, - ) - self.assertIn( - ("negotiation_receive_started role=%s type=%s id=%s", ("server", "target", "neg-1")), - logger.info_messages, - ) - self.assertIn( - ( - "negotiation_receive_completed role=%s type=%s id=%s status=%s", - ("server", "target", "neg-1", "in-progress"), - ), - logger.info_messages, - ) - self.assertIn( - ( - "negotiation_continue_started role=%s type=%s id=%s status=%s", - ("server", "target", "neg-1", "in-progress"), - ), - logger.info_messages, - ) - self.assertIn( - ("negotiation_continue_completed role=%s type=%s id=%s status=%s", ("server", None, None, None)), - logger.info_messages, - ) - self.assertFalse(any("sensitive" in message for message, _ in logger.info_messages)) - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages behind the legacy -# orchestrators this suite pins emit a DeprecationWarning when imported and will be removed -# in the next release. The behavioral assertions above stay untouched; this only pins the -# warning contract of the deprecated entry points this file exercises (the common package -# backing the legacy orchestrator transports). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize("package_name", ("a2a_t.negotiation.common",)) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/negotiation/test_parser.py b/tests/negotiation/test_parser.py deleted file mode 100644 index 143118bb..00000000 --- a/tests/negotiation/test_parser.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -class NegotiationContextParseTest(unittest.TestCase): - def test_from_context_returns_negotiation_context(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext - - context = NegotiationContext.from_context( - { - "negotiationType": "information", - "negotiationId": "neg-1", - "role": "client", - "round": 3, - "status": "in-progress", - "extra": {}, - } - ) - - self.assertEqual(context.negotiation_type, NegotiationType.INFORMATION) - self.assertEqual(context.negotiation_id, "neg-1") - self.assertEqual(context.role, NegotiationRole.CLIENT) - self.assertEqual(context.round, 3) - self.assertEqual(context.status, NegotiationStatus.IN_PROGRESS) - - def test_from_context_rejects_invalid_root_fields(self) -> None: - from a2a_t.negotiation.common.errors import NegotiationContextError - from a2a_t.negotiation.common.models import NegotiationContext - - with self.assertRaises(NegotiationContextError): - NegotiationContext.from_context( - { - "negotiationType": "unknown", - "negotiationId": "neg-1", - "role": "client", - "round": 0, - "status": "in-progress", - "extra": {}, - } - ) - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages this suite pins emit a -# DeprecationWarning when imported and will be removed in the next release. The behavioral -# assertions above stay untouched; this only pins the warning contract of the deprecated -# entry points this file exercises (the common model/parser package). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize("package_name", ("a2a_t.negotiation.common",)) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/negotiation/test_store.py b/tests/negotiation/test_store.py deleted file mode 100644 index 51c07ad8..00000000 --- a/tests/negotiation/test_store.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -class InMemoryNegotiationStateStoreTest(unittest.TestCase): - def test_store_saves_gets_deletes_records(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext, NegotiationRecord - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - record = NegotiationRecord( - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-store", - role=NegotiationRole.SERVER, - round=1, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ), - last_message="message", - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at="2026-04-18T00:00:00Z", - updated_at="2026-04-18T00:00:00Z", - ) - - store.save(record) - - self.assertIs(store.get("neg-store"), record) - - store.delete("neg-store") - - self.assertIsNone(store.get("neg-store")) - - def test_cleanup_expired_returns_true(self) -> None: - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - store = InMemoryNegotiationStateStore() - - self.assertTrue(store.cleanup_expired()) - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages this suite pins emit a -# DeprecationWarning when imported and will be removed in the next release. The behavioral -# assertions above stay untouched; this only pins the warning contract of the deprecated -# entry points this file exercises (store + its common dependency). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "package_name", - ( - "a2a_t.negotiation.store", - "a2a_t.negotiation.common", - ), -) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/negotiation/test_store_factory.py b/tests/negotiation/test_store_factory.py deleted file mode 100644 index 01c15b82..00000000 --- a/tests/negotiation/test_store_factory.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -class FakeLogger: - def __init__(self) -> None: - self.messages: list[str] = [] - - def warning(self, message: str, *args: object) -> None: - if args: - message = message % args - self.messages.append(message) - - -class NegotiationStateStoreFactoryTest(unittest.TestCase): - def _env_path(self, name: str) -> Path: - temp_dir = PROJECT_ROOT / ".tmp_tests" / "negotiation_store_factory" - temp_dir.mkdir(parents=True, exist_ok=True) - return temp_dir / name - - def test_build_returns_in_memory_store_for_supported_type(self) -> None: - from a2a_t.negotiation.store.factory import NegotiationStateStoreFactory - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - env_path = self._env_path("supported.env") - env_path.write_text("A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory\n", encoding="utf-8") - - store = NegotiationStateStoreFactory().build(env_path=env_path) - - self.assertIsInstance(store, InMemoryNegotiationStateStore) - - def test_build_falls_back_to_in_memory_when_env_is_missing(self) -> None: - from a2a_t.negotiation.store.factory import NegotiationStateStoreFactory - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - logger = FakeLogger() - store = NegotiationStateStoreFactory().build( - env_path=Path("missing.env"), - logger=logger, - ) - - self.assertIsInstance(store, InMemoryNegotiationStateStore) - self.assertEqual(len(logger.messages), 1) - - def test_build_falls_back_to_in_memory_for_unsupported_type(self) -> None: - from a2a_t.negotiation.store.factory import NegotiationStateStoreFactory - from a2a_t.negotiation.store.in_memory import InMemoryNegotiationStateStore - - logger = FakeLogger() - env_path = self._env_path("unsupported.env") - env_path.write_text("A2AT_NEGOTIATION_STATE_STORE_TYPE=file\n", encoding="utf-8") - - store = NegotiationStateStoreFactory().build( - env_path=env_path, - logger=logger, - ) - - self.assertIsInstance(store, InMemoryNegotiationStateStore) - self.assertEqual(len(logger.messages), 1) - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages this suite pins emit a -# DeprecationWarning when imported and will be removed in the next release. The behavioral -# assertions above stay untouched; this only pins the warning contract of the deprecated -# entry points this file exercises (the store package). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize("package_name", ("a2a_t.negotiation.store",)) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/negotiation/test_types.py b/tests/negotiation/test_types.py deleted file mode 100644 index c370e396..00000000 --- a/tests/negotiation/test_types.py +++ /dev/null @@ -1,238 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -import unittest -from pathlib import Path - -import pytest - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = PROJECT_ROOT / "src" - -if str(SRC_ROOT) not in sys.path: - sys.path.insert(0, str(SRC_ROOT)) - - -class FakePromptChecker: - def __init__(self, result: object) -> None: - self._result = result - self.calls: list[str] = [] - - def check(self, *, processed_prompt_text: str): - self.calls.append(processed_prompt_text) - return self._result - - -class NegotiationTypesTest(unittest.TestCase): - def _prompt_compliance_result(self): - from a2a_t.server.prompt_compliance.models import PromptComplianceFailure, PromptComplianceResult - - return PromptComplianceResult( - success=False, - failure=PromptComplianceFailure( - code="slot.rule_violation", - message="Need more information", - stage="slot_validation", - ), - ) - - def _context(self): - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext - - return NegotiationContext( - negotiation_type=NegotiationType.INFORMATION, - negotiation_id="neg-1", - role=NegotiationRole.SERVER, - round=1, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ) - - def test_information_type_on_client_side_returns_received_facts(self) -> None: - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.information import InformationNegotiationType - - negotiation_type = InformationNegotiationType(prompt_renderer=NegotiationPromptRenderer()) - - result = negotiation_type.process_received_message( - message="prompt", - context=self._context(), - record=None, - ) - - self.assertTrue(result.need_response) - self.assertEqual(result.facts, {}) - self.assertEqual(result.message, "prompt") - - def test_information_type_on_server_side_uses_prompt_checker(self) -> None: - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.information import InformationNegotiationType - - checker = FakePromptChecker(self._prompt_compliance_result()) - negotiation_type = InformationNegotiationType( - prompt_renderer=NegotiationPromptRenderer(), - prompt_checker=checker, - ) - - result = negotiation_type.process_received_message( - message="latest full task prompt", - context=self._context(), - record=None, - ) - - self.assertEqual(checker.calls, ["latest full task prompt"]) - self.assertTrue(result.need_response) - self.assertEqual(result.facts, {}) - self.assertEqual(result.message, "Need more information") - - def test_information_type_on_server_side_returns_completion_message_when_prompt_is_valid(self) -> None: - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.information import InformationNegotiationType - from a2a_t.server.prompt_compliance.models import PromptComplianceResult - - checker = FakePromptChecker( - PromptComplianceResult( - success=True, - ) - ) - negotiation_type = InformationNegotiationType( - prompt_renderer=NegotiationPromptRenderer(), - prompt_checker=checker, - ) - - result = negotiation_type.process_received_message( - message="latest full task prompt", - context=self._context(), - record=None, - ) - - self.assertTrue(result.need_response) - self.assertEqual(result.facts, {}) - self.assertEqual(result.message, "Task prompt is complete.") - - def test_information_type_on_server_side_returns_error_when_prompt_fails_without_negotiation(self) -> None: - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.information import InformationNegotiationType - from a2a_t.server.prompt_compliance.models import PromptComplianceFailure, PromptComplianceResult - - checker = FakePromptChecker( - PromptComplianceResult( - success=False, - failure=PromptComplianceFailure( - code="scenario.not_matched", - message="Task prompt metadata is invalid.", - stage="prompt_parse", - ), - ) - ) - negotiation_type = InformationNegotiationType( - prompt_renderer=NegotiationPromptRenderer(), - prompt_checker=checker, - ) - - result = negotiation_type.process_received_message( - message="latest full task prompt", - context=self._context(), - record=None, - ) - - self.assertTrue(result.need_response) - self.assertEqual(result.facts, {}) - self.assertEqual(result.message, "Task prompt metadata is invalid.") - - def test_information_type_render_continue_returns_final_task_prompt_when_agreed(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationStatus - from a2a_t.negotiation.common.models import NegotiationRecord - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.information import InformationNegotiationType - - negotiation_type = InformationNegotiationType(prompt_renderer=NegotiationPromptRenderer()) - result = negotiation_type.render_continue_prompt( - record=NegotiationRecord( - context=self._context(), - last_message=None, - last_receive_result=None, - last_continue_result=None, - last_task_prompt=None, - created_at="t1", - updated_at="t1", - ), - context=self._context(), - status=NegotiationStatus.AGREED, - content_text="final prompt", - ) - - self.assertEqual(result.final_task_prompt, "final prompt") - self.assertEqual(result.prompt_text, "final prompt") - - def test_target_type_passthroughs_facts(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.target import TargetNegotiationType - - negotiation_type = TargetNegotiationType(prompt_renderer=NegotiationPromptRenderer()) - result = negotiation_type.process_received_message( - message="clarify this", - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-2", - role=NegotiationRole.CLIENT, - round=1, - status=NegotiationStatus.IN_PROGRESS, - extra={}, - ), - record=None, - ) - - self.assertEqual(result.facts, {}) - self.assertEqual(result.message, "clarify this") - - def test_target_type_on_terminal_message_does_not_require_response(self) -> None: - from a2a_t.negotiation.common.enums import NegotiationRole, NegotiationStatus, NegotiationType - from a2a_t.negotiation.common.models import NegotiationContext - from a2a_t.negotiation.rendering.negotiation_prompt_renderer import NegotiationPromptRenderer - from a2a_t.negotiation.types.target import TargetNegotiationType - - negotiation_type = TargetNegotiationType(prompt_renderer=NegotiationPromptRenderer()) - result = negotiation_type.process_received_message( - message="clarify this", - context=NegotiationContext( - negotiation_type=NegotiationType.TARGET, - negotiation_id="neg-3", - role=NegotiationRole.CLIENT, - round=2, - status=NegotiationStatus.AGREED, - extra={}, - ), - record=None, - ) - - self.assertFalse(result.need_response) - self.assertEqual(result.facts, {}) - self.assertEqual(result.message, "clarify this") - - -# -------------------------------------------------------------------------------------- -# Deprecation shim round (D1): the retired state-machine packages this suite pins emit a -# DeprecationWarning when imported and will be removed in the next release. The behavioral -# assertions above stay untouched; this only pins the warning contract of the deprecated -# entry points this file exercises (types + their common/rendering dependencies). -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "package_name", - ( - "a2a_t.negotiation.types", - "a2a_t.negotiation.common", - "a2a_t.negotiation.rendering", - ), -) -def test_importing_a_deprecated_negotiation_package_warns(package_name: str) -> None: - module = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) diff --git a/tests/prompt_compliance/test_a2at_server.py b/tests/prompt_compliance/test_a2at_server.py index 8b77e6ee..22638069 100644 --- a/tests/prompt_compliance/test_a2at_server.py +++ b/tests/prompt_compliance/test_a2at_server.py @@ -6,8 +6,6 @@ from pathlib import Path from unittest.mock import patch -import pytest - PROJECT_ROOT = Path(__file__).resolve().parents[2] SRC_ROOT = PROJECT_ROOT / "src" @@ -17,8 +15,6 @@ from a2a_t.core.errors.catalog import ErrorCatalog from a2a_t.llm.models import LLMClientConfig -from a2a_t.negotiation.common.enums import NegotiationStatus, NegotiationType -from a2a_t.negotiation.common.models import ContinueNegotiationInput, NegotiationContext, StartNegotiationInput from a2a_t.server.prompt_compliance.models import PromptComplianceFailure, PromptComplianceResult from tests.support import TEST_ENV_PATH, ManagedTempDirTestCase @@ -62,25 +58,6 @@ def build(self, **kwargs: object) -> FakePromptComplianceOrchestrator: return self._orchestrator -class FakeNegotiationOrchestrator: - def __init__(self) -> None: - self.start_calls: list[object] = [] - self.receive_calls: list[dict[str, object]] = [] - self.continue_calls: list[object] = [] - - def start_negotiation(self, input: StartNegotiationInput) -> dict[str, object]: - self.start_calls.append(input) - return {"started": True} - - def receive_negotiation(self, message: str, context: dict[str, object]) -> dict[str, object]: - self.receive_calls.append({"message": message, "context": context}) - return {"received": True} - - def continue_negotiation(self, input: ContinueNegotiationInput) -> dict[str, object]: - self.continue_calls.append(input) - return {"continued": True} - - class FakeLogger: def __init__(self) -> None: self.info_messages: list[tuple[str, tuple[object, ...]]] = [] @@ -99,7 +76,7 @@ def test_constructor_signature_remains_stable(self) -> None: self.assertEqual(signature.parameters["env_path"].kind, inspect.Parameter.KEYWORD_ONLY) self.assertEqual(signature.parameters["logger"].kind, inspect.Parameter.KEYWORD_ONLY) - def test_a2at_server_delegates_all_public_methods_with_typed_negotiation_inputs(self) -> None: + def test_a2at_server_delegates_prompt_compliance_with_configured_llm(self) -> None: from a2a_t.server.a2at_server import A2ATServer compliance_result = PromptComplianceResult( @@ -112,38 +89,16 @@ def test_a2at_server_delegates_all_public_methods_with_typed_negotiation_inputs( ) compliance = FakePromptComplianceOrchestrator(compliance_result) compliance_builder = FakePromptComplianceBuilder(compliance) - negotiation = FakeNegotiationOrchestrator() logger = FakeLogger() llm_config = build_llm_config() llm_client = object() - start_input = StartNegotiationInput( - type=NegotiationType.INFORMATION, - content_text="Need more information.", - facts={}, - ) - continue_input = ContinueNegotiationInput( - context=NegotiationContext.from_context( - { - "negotiationType": "information", - "negotiationId": "neg-1", - "role": "server", - "round": 1, - "status": "in-progress", - "extra": {}, - } - ), - status=NegotiationStatus.IN_PROGRESS, - content_text="Need the site name.", - ) with ( patch("a2a_t.server.a2at_server._default_env_path", return_value=TEST_ENV_PATH), patch("a2a_t.server.a2at_server.LLMConfigLoader.load", return_value=llm_config) as load_llm_config, patch("a2a_t.server.a2at_server.LLMClientFactory.create", return_value=llm_client) as create_llm_client, patch("a2a_t.server.a2at_server.PromptComplianceOrchestratorBuilder", return_value=compliance_builder), - patch("a2a_t.server.a2at_server.ServerNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = negotiation server = A2ATServer(logger=logger) self.assertEqual( @@ -157,34 +112,12 @@ def test_a2at_server_delegates_all_public_methods_with_typed_negotiation_inputs( ), ), ) - # The three legacy state-machine methods keep their forwarding behavior for one - # release and warn on every call (D1 deprecation shim round). - with pytest.warns(DeprecationWarning, match=r"A2ATServer\.\w+ is deprecated since 1\.1\.0"): - self.assertEqual(server.start_negotiation(start_input), {"started": True}) - self.assertEqual( - server.receive_negotiation( - "Need more information", - { - "negotiationType": "information", - "negotiationId": "neg-1", - "role": "server", - "round": 1, - "status": "in-progress", - "extra": {}, - }, - ), - {"received": True}, - ) - self.assertEqual(server.continue_negotiation(continue_input), {"continued": True}) load_llm_config.assert_called_once_with(TEST_ENV_PATH) create_llm_client.assert_called_once_with(llm_config.provider, llm_config, logger=logger) - # The builders receive the observability-wrapped client (spec 2.2 A2ATLLMClientDecorator) + # The builder receives the observability-wrapped client (spec 2.2 A2ATLLMClientDecorator) # whose inner is the factory product. self.assertIs(compliance_builder.calls[0]["llm_client"]._inner, llm_client) - self.assertIs( - negotiation_builder_cls.return_value.build.call_args.kwargs["llm_client"]._inner, llm_client - ) self.assertEqual( compliance.calls, [ @@ -193,9 +126,6 @@ def test_a2at_server_delegates_all_public_methods_with_typed_negotiation_inputs( } ], ) - self.assertEqual(negotiation.start_calls, [start_input]) - self.assertEqual(negotiation.receive_calls[0]["message"], "Need more information") - self.assertEqual(negotiation.continue_calls, [continue_input]) self.assertIs(compliance_builder.calls[0]["logger"], logger) def test_check_task_prompt_returns_success_only_when_compliance_passes(self) -> None: @@ -203,16 +133,13 @@ def test_check_task_prompt_returns_success_only_when_compliance_passes(self) -> compliance = FakePromptComplianceOrchestrator(PromptComplianceResult(success=True)) compliance_builder = FakePromptComplianceBuilder(compliance) - negotiation = FakeNegotiationOrchestrator() with ( patch("a2a_t.server.a2at_server._default_env_path", return_value=TEST_ENV_PATH), patch("a2a_t.server.a2at_server.LLMConfigLoader.load", return_value=build_llm_config()), patch("a2a_t.server.a2at_server.LLMClientFactory.create", return_value=object()), patch("a2a_t.server.a2at_server.PromptComplianceOrchestratorBuilder", return_value=compliance_builder), - patch("a2a_t.server.a2at_server.ServerNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = negotiation server = A2ATServer() self.assertEqual( @@ -265,12 +192,10 @@ def missing_prompts_read_text(self: object, key: object) -> str: return original_read_text(self, key) with ( - patch("a2a_t.server.a2at_server.ServerNegotiationOrchestratorBuilder") as negotiation_builder_cls, patch("a2a_t.server.a2at_server.LLMConfigLoader.load", return_value=build_llm_config()), patch("a2a_t.server.a2at_server.LLMClientFactory.create", return_value=object()), patch.object(PackagedResourceReader, "read_text", missing_prompts_read_text), ): - negotiation_builder_cls.return_value.build.return_value = object() server = A2ATServer(env_path=env_path) result = server.check_task_prompt(processed_prompt_text="processed body") diff --git a/tests/prompt_compliance/test_a2at_server_negotiation_api.py b/tests/prompt_compliance/test_a2at_server_negotiation_api.py index a665e2ab..bd1fe9cb 100644 --- a/tests/prompt_compliance/test_a2at_server_negotiation_api.py +++ b/tests/prompt_compliance/test_a2at_server_negotiation_api.py @@ -4,17 +4,14 @@ client facade over the same shared service, so this suite pins the symmetric surface: the from-data legs render the Java golden fixtures byte-for-byte against the packaged templates, the from-text and validation legs run against a scripted LLM client, the raw string template URI -boundary fails fast on every method (D16), every method delegates to the matching service method -with the typed URI, and the deprecation shim round (D1) warns on the three legacy state-machine -facade methods and on the import of every old negotiation demo package. +boundary fails fast on every method (D16), and every method delegates to the matching service +method with the typed URI. """ from __future__ import annotations -import importlib import sys from pathlib import Path -from types import ModuleType from typing import Any, Callable from unittest.mock import patch @@ -73,17 +70,6 @@ "validate_abort_prompt_and_data_filling", ) -#: The six retired state-machine negotiation demo packages kept for one release (D1). -OLD_NEGOTIATION_PACKAGES = ( - "a2a_t.negotiation.common", - "a2a_t.negotiation.types", - "a2a_t.negotiation.store", - "a2a_t.negotiation.runtime", - "a2a_t.negotiation.handling", - "a2a_t.negotiation.rendering", -) - - class ScriptedLlmClient: """LLM boundary fake replaying scripted payloads and recording every structured call.""" @@ -714,44 +700,6 @@ def test_get_prompt_answers_none_for_unknown_templates(unknown_uri: str, tmp_pat assert server.get_prompt(unknown_uri) is None -# -------------------------------------------------------------------------------------- -# deprecation shim round (D1) -# -------------------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - ("method", "build_input"), - ( - ("start_negotiation", lambda: object()), - ("receive_negotiation", lambda: ("message", {})), - ("continue_negotiation", lambda: object()), - ), -) -def test_legacy_negotiation_methods_warn_and_still_delegate( - method: str, build_input: Callable[[], Any], tmp_path: Path -) -> None: - server = build_server(write_env(tmp_path, "zh-CN")) - input = build_input() - with patch.object(server._negotiation_orchestrator, method, return_value={"legacy": True}) as delegated: - with pytest.warns(DeprecationWarning, match=f"A2ATServer\\.{method} is deprecated since 1\\.1\\.0"): - result = ( - getattr(server, method)(input) - if method != "receive_negotiation" - else server.receive_negotiation(*input) - ) - - assert result == {"legacy": True} - delegated.assert_called_once() - - -@pytest.mark.parametrize("package_name", OLD_NEGOTIATION_PACKAGES) -def test_importing_an_old_negotiation_package_warns(package_name: str) -> None: - module: ModuleType = importlib.import_module(package_name) - - with pytest.warns(DeprecationWarning, match=f"{package_name} package is deprecated since 1\\.1\\.0"): - importlib.reload(module) - - def test_negotiation_api_surface_is_complete() -> None: for method in (*NEGOTIATION_API_METHODS, "get_prompts", "get_prompt"): assert callable(getattr(A2ATServer, method, None)), f"A2ATServer is missing {method}" diff --git a/tests/prompt_compliance/test_input_limit_gate.py b/tests/prompt_compliance/test_input_limit_gate.py index c3d78e35..6a127c99 100644 --- a/tests/prompt_compliance/test_input_limit_gate.py +++ b/tests/prompt_compliance/test_input_limit_gate.py @@ -219,9 +219,7 @@ def test_check_task_prompt_rejects_oversized_input_at_the_facade_entry(self) -> patch("a2a_t.server.a2at_server._default_env_path", return_value=env_path), patch("a2a_t.server.a2at_server.LLMConfigLoader.load", return_value=_build_llm_config()), patch("a2a_t.server.a2at_server.LLMClientFactory.create", return_value=object()), - patch("a2a_t.server.a2at_server.ServerNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = object() server = A2ATServer(env_path=env_path) result = server.check_task_prompt(processed_prompt_text="x" * (DEFAULT_MAX_TEXT_CHARS + 1)) diff --git a/tests/prompt_compliance/test_integration_runtime.py b/tests/prompt_compliance/test_integration_runtime.py index d2feb7b1..7ef8e934 100644 --- a/tests/prompt_compliance/test_integration_runtime.py +++ b/tests/prompt_compliance/test_integration_runtime.py @@ -141,9 +141,7 @@ def test_handler_check_task_prompt_succeeds_with_real_shared_components(self) -> "a2a_t.server.a2at_server.PromptComplianceOrchestratorBuilder", return_value=FakePromptComplianceBuilder(service), ), - patch("a2a_t.server.a2at_server.ServerNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = object() server = A2ATServer() result = server.check_task_prompt(processed_prompt_text="processed body") @@ -189,9 +187,7 @@ def test_handler_check_task_prompt_returns_business_constraint_message_for_inval "a2a_t.server.a2at_server.PromptComplianceOrchestratorBuilder", return_value=FakePromptComplianceBuilder(service), ), - patch("a2a_t.server.a2at_server.ServerNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = object() server = A2ATServer() result = server.check_task_prompt(processed_prompt_text="processed body") @@ -255,9 +251,7 @@ def test_handler_check_task_prompt_succeeds_when_optional_subscribe_incident_slo "a2a_t.server.a2at_server.PromptComplianceOrchestratorBuilder", return_value=FakePromptComplianceBuilder(service), ), - patch("a2a_t.server.a2at_server.ServerNegotiationOrchestratorBuilder") as negotiation_builder_cls, ): - negotiation_builder_cls.return_value.build.return_value = object() server = A2ATServer() result = server.check_task_prompt(processed_prompt_text="processed body") diff --git a/tests/test_facade_api_symmetry.py b/tests/test_facade_api_symmetry.py index 735199d4..a312612d 100644 --- a/tests/test_facade_api_symmetry.py +++ b/tests/test_facade_api_symmetry.py @@ -268,8 +268,7 @@ def test_every_client_public_method_is_symmetric_on_the_server(method: str) -> N The seven prompt-side generation APIs are the documented client-only exception of the Java contract and are skipped with their reason; every other client method — the twelve negotiation - content methods, both template queries and the three deprecated state-machine shims — must stay - symmetric. + content methods and both template queries — must stay symmetric. """ _assert_symmetric_signatures(A2ATClient, A2ATServer, method)