diff --git a/docs/en/API_Reference.md b/docs/en/API_Reference.md index f4ec461..5d36f7c 100644 --- a/docs/en/API_Reference.md +++ b/docs/en/API_Reference.md @@ -73,6 +73,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 were deprecated since v1.1.0, emitted a `DeprecationWarning` when called, and were removed in v1.2.0. +- The private template-query methods `_get_prompts` / `_get_prompt` on `A2ATClient` and `A2ATServer` are SDK-internal interfaces. They are not part of the public API, are not guaranteed to be compatible across releases, and may be changed or removed without notice; do not call them directly. ## 1.3 API Descriptions 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 57bfc65..b223371 100644 --- "a/docs/zh/API\345\217\202\350\200\203.md" +++ "b/docs/zh/API\345\217\202\350\200\203.md" @@ -73,6 +73,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 自 v1.1.0 起废弃,调用时发出 `DeprecationWarning`,已在 v1.2.0 版本删除。 +- `A2ATClient` 和 `A2ATServer` 的 `_get_prompts` / `_get_prompt` 为 SDK 内部接口,不属于公开 API,不保证跨版本兼容性,可能随时变更或删除,请勿直接调用。 ## 1.3 API说明 diff --git a/src/a2a_t/client/a2at_client.py b/src/a2a_t/client/a2at_client.py index 0fe1b69..33bc82a 100644 --- a/src/a2a_t/client/a2at_client.py +++ b/src/a2a_t/client/a2at_client.py @@ -5,8 +5,7 @@ ``..._from_data_with_schema``, each bypassing scenario recognition for one addressed template), 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`. +(eight generation plus four validation methods). """ from __future__ import annotations @@ -72,9 +71,9 @@ def _parse_template_uri(template_uri: str | TemplateUri | None) -> TemplateUri: class A2ATClient: - """Client-side facade of the A2A-T SDK: prompt generation, negotiation, and template queries. + """Client-side facade of the A2A-T SDK: prompt generation and negotiation. - The facade is the single client entry point of the SDK and exposes four groups of operations: + The facade is the single client entry point of the SDK and exposes three groups of operations: - **Scenario-recognition prompt generation** — :meth:`generate_task_prompt` normalizes the caller input, recognizes the scenario, extracts the slots with one LLM call and renders the @@ -86,8 +85,6 @@ class A2ATClient: - **Negotiation content** — the twelve Negotiation-T methods: eight message-generation methods (``generate_negotiation_{propose,accept,reject,abort}_prompt_from_{data,text}``) and four message-validation methods (``validate_{...}_prompt_and_data_filling``). - - **Template queries** — :meth:`get_prompts` and :meth:`get_prompt`, the extension-agnostic - catalog queries that never throw. Every method that addresses a template takes the template URI in its raw string spelling and parses it fail-fast; use the constants of :mod:`a2a_t.core.standard_templates` instead of @@ -774,10 +771,10 @@ def validate_abort_prompt_and_data_filling( ) # ------------------------------------------------------------------ - # Template queries + # Internal template queries # ------------------------------------------------------------------ - def get_prompts(self) -> list[PromptTemplate]: + def _get_prompts(self) -> list[PromptTemplate]: """List every template available for the configured language across all A2A-T extensions. This query never throws: the extension directories are discovered from the configured @@ -791,7 +788,7 @@ def get_prompts(self) -> list[PromptTemplate]: """ return self._template_queries().get_prompts() - def get_prompt(self, template_uri: str | TemplateUri) -> PromptTemplate | None: + def _get_prompt(self, template_uri: str | TemplateUri) -> PromptTemplate | None: """Load one template by its URI, regardless of the extension. This query never throws for a well-formed template URI: a missing template yields ``None`` diff --git a/src/a2a_t/server/a2at_server.py b/src/a2a_t/server/a2at_server.py index 8c0b21a..99f3acb 100644 --- a/src/a2a_t/server/a2at_server.py +++ b/src/a2a_t/server/a2at_server.py @@ -4,8 +4,7 @@ (``validate_{task,notification,auth}_prompt_and_data_filling``, the shared core validation pipeline 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`. +(eight generation plus four validation methods). """ from __future__ import annotations @@ -77,9 +76,9 @@ def _parse_template_uri(template_uri: str | TemplateUri | None) -> TemplateUri: class A2ATServer: - """Server-side facade of the A2A-T SDK: prompt compliance, validation, negotiation, and queries. + """Server-side facade of the A2A-T SDK: prompt compliance, validation, and negotiation. - The facade is the single server entry point of the SDK and exposes four groups of operations: + The facade is the single server entry point of the SDK and exposes three groups of operations: - **Task prompt compliance** — :meth:`check_task_prompt` validates one processed task prompt against the scenario, template and slot constraints and reports the outcome in the returned @@ -93,8 +92,6 @@ class A2ATServer: (``generate_negotiation_{propose,accept,reject,abort}_prompt_from_{data,text}``) and four message-validation methods (``validate_{...}_prompt_and_data_filling``), identical to the client surface because both peers of a negotiation generate and validate messages. - - **Template queries** — :meth:`get_prompts` and :meth:`get_prompt`, the extension-agnostic - catalog queries that never throw. Every method that addresses a template takes the template URI in its raw string spelling and parses it fail-fast; use the constants of :mod:`a2a_t.core.standard_templates` instead of @@ -696,10 +693,10 @@ def validate_abort_prompt_and_data_filling( ) # ------------------------------------------------------------------ - # Template queries + # Internal template queries # ------------------------------------------------------------------ - def get_prompts(self) -> list[PromptTemplate]: + def _get_prompts(self) -> list[PromptTemplate]: """List every template available for the configured language across all A2A-T extensions. This query never throws: the extension directories are discovered from the configured @@ -713,7 +710,7 @@ def get_prompts(self) -> list[PromptTemplate]: """ return self._template_queries().get_prompts() - def get_prompt(self, template_uri: str | TemplateUri) -> PromptTemplate | None: + def _get_prompt(self, template_uri: str | TemplateUri) -> PromptTemplate | None: """Load one template by its URI, regardless of the extension. This query never throws for a well-formed template URI: a missing template yields ``None`` diff --git a/tests/client_prompt/test_a2at_client_negotiation_api.py b/tests/client_prompt/test_a2at_client_negotiation_api.py index 23e3597..77d4a01 100644 --- a/tests/client_prompt/test_a2at_client_negotiation_api.py +++ b/tests/client_prompt/test_a2at_client_negotiation_api.py @@ -2,7 +2,7 @@ The suite pins the facade surface added in P7: the twelve negotiation content methods delegating to the shared :class:`~a2a_t.negotiation.generation.content_service.NegotiationContentService` -plus the template queries, with the raw string template URI boundary failing fast (D16) exactly +plus the internal template queries, with the raw string template URI boundary failing fast (D16) exactly 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 @@ -433,11 +433,11 @@ def _boundary_calls(client: A2ATClient) -> dict[str, Callable[..., Any]]: "validate_abort_prompt_and_data_filling": lambda uri: client.validate_abort_prompt_and_data_filling( "message", abort_context, {"type": "object"}, uri ), - "get_prompt": client.get_prompt, + "_get_prompt": client._get_prompt, } -@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "get_prompt")) +@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "_get_prompt")) @pytest.mark.parametrize("malformed_uri", _MALFORMED_URIS, ids=["one-segment", "blank", "two-segments"]) def test_template_uri_boundary_rejects_malformed_uris(method_name: str, malformed_uri: str, tmp_path: Path) -> None: client = build_client(write_env(tmp_path, "zh-CN")) @@ -446,7 +446,7 @@ def test_template_uri_boundary_rejects_malformed_uris(method_name: str, malforme _boundary_calls(client)[method_name](malformed_uri) -@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "get_prompt")) +@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "_get_prompt")) def test_template_uri_boundary_rejects_a_null_uri(method_name: str, tmp_path: Path) -> None: client = build_client(write_env(tmp_path, "zh-CN")) @@ -622,28 +622,28 @@ def get_prompt(self, template_uri: TemplateUri) -> PromptTemplate | None: return None -def test_template_queries_delegate_to_the_query_service(tmp_path: Path) -> None: +def test_private_template_queries_delegate_to_the_query_service(tmp_path: Path) -> None: client = build_client(write_env(tmp_path, "zh-CN")) recording = RecordingQueryService() client._template_query_service = recording - assert client.get_prompts() == [] - assert client.get_prompt(INFORMATION_NEGOTIATION_PROPOSE_URI) is None + assert client._get_prompts() == [] + assert client._get_prompt(INFORMATION_NEGOTIATION_PROPOSE_URI) is None assert recording.prompts_calls == 1 assert recording.prompt_calls == [INFORMATION_NEGOTIATION_PROPOSE] # -------------------------------------------------------------------------------------- -# template queries over the packaged tree +# internal template queries over the packaged tree # -------------------------------------------------------------------------------------- @pytest.mark.parametrize("language", LANGUAGES) -def test_get_prompts_lists_every_bundled_template_sorted_by_uri(language: str, tmp_path: Path) -> None: +def test_private_get_prompts_lists_every_bundled_template_sorted_by_uri(language: str, tmp_path: Path) -> None: client = build_client(write_env(tmp_path, language)) - prompts = client.get_prompts() + prompts = client._get_prompts() assert len(prompts) == 13 uris = [template.template_uri.uri for template in prompts] @@ -668,10 +668,10 @@ def test_get_prompts_lists_every_bundled_template_sorted_by_uri(language: str, t "Authorization-T/authorization-policy-management/v1", ), ) -def test_get_prompt_loads_one_bundled_template(template_uri: str, language: str, tmp_path: Path) -> None: +def test_private_get_prompt_loads_one_bundled_template(template_uri: str, language: str, tmp_path: Path) -> None: client = build_client(write_env(tmp_path, language)) - template = client.get_prompt(template_uri) + template = client._get_prompt(template_uri) assert template is not None assert template.template_uri == TemplateUri.parse(template_uri) @@ -689,12 +689,12 @@ def test_get_prompt_loads_one_bundled_template(template_uri: str, language: str, "Task-T/network-layer/unknown-scenario/v1", ), ) -def test_get_prompt_answers_none_for_unknown_templates(unknown_uri: str, tmp_path: Path) -> None: +def test_private_get_prompt_answers_none_for_unknown_templates(unknown_uri: str, tmp_path: Path) -> None: client = build_client(write_env(tmp_path, "zh-CN")) - assert client.get_prompt(unknown_uri) is None + assert client._get_prompt(unknown_uri) is None def test_negotiation_api_surface_is_complete() -> None: - for method in (*NEGOTIATION_API_METHODS, "get_prompts", "get_prompt"): + for method in NEGOTIATION_API_METHODS: assert callable(getattr(A2ATClient, method, None)), f"A2ATClient is missing {method}" diff --git a/tests/prompt_compliance/test_a2at_server_negotiation_api.py b/tests/prompt_compliance/test_a2at_server_negotiation_api.py index bd1fe9c..3cd9528 100644 --- a/tests/prompt_compliance/test_a2at_server_negotiation_api.py +++ b/tests/prompt_compliance/test_a2at_server_negotiation_api.py @@ -1,6 +1,6 @@ """Negotiation content API of the server facade (port of Java ``A2ATServerNegotiationApiTest``). -The server facade exposes the same twelve negotiation content methods and template queries as the +The server facade exposes the same twelve negotiation content methods and internal template queries as the 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 @@ -439,11 +439,11 @@ def _boundary_calls(server: A2ATServer) -> dict[str, Callable[..., Any]]: "validate_abort_prompt_and_data_filling": lambda uri: server.validate_abort_prompt_and_data_filling( "message", abort_context, {"type": "object"}, uri ), - "get_prompt": server.get_prompt, + "_get_prompt": server._get_prompt, } -@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "get_prompt")) +@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "_get_prompt")) @pytest.mark.parametrize("malformed_uri", _MALFORMED_URIS, ids=["one-segment", "blank", "two-segments"]) def test_template_uri_boundary_rejects_malformed_uris(method_name: str, malformed_uri: str, tmp_path: Path) -> None: server = build_server(write_env(tmp_path, "zh-CN")) @@ -452,7 +452,7 @@ def test_template_uri_boundary_rejects_malformed_uris(method_name: str, malforme _boundary_calls(server)[method_name](malformed_uri) -@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "get_prompt")) +@pytest.mark.parametrize("method_name", (*NEGOTIATION_API_METHODS, "_get_prompt")) def test_template_uri_boundary_rejects_a_null_uri(method_name: str, tmp_path: Path) -> None: server = build_server(write_env(tmp_path, "zh-CN")) @@ -627,28 +627,28 @@ def get_prompt(self, template_uri: TemplateUri) -> PromptTemplate | None: return None -def test_template_queries_delegate_to_the_query_service(tmp_path: Path) -> None: +def test_private_template_queries_delegate_to_the_query_service(tmp_path: Path) -> None: server = build_server(write_env(tmp_path, "zh-CN")) recording = RecordingQueryService() server._template_query_service = recording - assert server.get_prompts() == [] - assert server.get_prompt(INFORMATION_NEGOTIATION_PROPOSE_URI) is None + assert server._get_prompts() == [] + assert server._get_prompt(INFORMATION_NEGOTIATION_PROPOSE_URI) is None assert recording.prompts_calls == 1 assert recording.prompt_calls == [INFORMATION_NEGOTIATION_PROPOSE] # -------------------------------------------------------------------------------------- -# template queries over the packaged tree +# internal template queries over the packaged tree # -------------------------------------------------------------------------------------- @pytest.mark.parametrize("language", LANGUAGES) -def test_get_prompts_lists_every_bundled_template_sorted_by_uri(language: str, tmp_path: Path) -> None: +def test_private_get_prompts_lists_every_bundled_template_sorted_by_uri(language: str, tmp_path: Path) -> None: server = build_server(write_env(tmp_path, language)) - prompts = server.get_prompts() + prompts = server._get_prompts() assert len(prompts) == 13 uris = [template.template_uri.uri for template in prompts] @@ -673,10 +673,10 @@ def test_get_prompts_lists_every_bundled_template_sorted_by_uri(language: str, t "Authorization-T/authorization-policy-management/v1", ), ) -def test_get_prompt_loads_one_bundled_template(template_uri: str, language: str, tmp_path: Path) -> None: +def test_private_get_prompt_loads_one_bundled_template(template_uri: str, language: str, tmp_path: Path) -> None: server = build_server(write_env(tmp_path, language)) - template = server.get_prompt(template_uri) + template = server._get_prompt(template_uri) assert template is not None assert template.template_uri == TemplateUri.parse(template_uri) @@ -694,12 +694,12 @@ def test_get_prompt_loads_one_bundled_template(template_uri: str, language: str, "Task-T/network-layer/unknown-scenario/v1", ), ) -def test_get_prompt_answers_none_for_unknown_templates(unknown_uri: str, tmp_path: Path) -> None: +def test_private_get_prompt_answers_none_for_unknown_templates(unknown_uri: str, tmp_path: Path) -> None: server = build_server(write_env(tmp_path, "zh-CN")) - assert server.get_prompt(unknown_uri) is None + assert server._get_prompt(unknown_uri) is None def test_negotiation_api_surface_is_complete() -> None: - for method in (*NEGOTIATION_API_METHODS, "get_prompts", "get_prompt"): + for method in NEGOTIATION_API_METHODS: assert callable(getattr(A2ATServer, method, None)), f"A2ATServer is missing {method}" diff --git a/tests/test_facade_api_symmetry.py b/tests/test_facade_api_symmetry.py index a312612..c09c1ca 100644 --- a/tests/test_facade_api_symmetry.py +++ b/tests/test_facade_api_symmetry.py @@ -1,8 +1,8 @@ """Facade API symmetry guard (port of Java ``NegotiationFacadeApiSymmetryTest``). The Java sample module guards the negotiation content-layer API surface of the two high-level -facades with reflection: the client and the server facade expose the negotiation generation, query -and validation APIs as one symmetric surface — identical method names, parameter types and return +facades with reflection: the client and the server facade expose the negotiation generation and +validation APIs as one symmetric surface — identical method names, parameter types and return types — and the guard fails when one facade drifts from the other. This port replaces the reflection sweep with ``inspect.signature`` parametrized tests (plan section 三、P7): for every guarded method the two facades must agree on the parameter names, their order, kinds, annotations @@ -74,8 +74,8 @@ #: The retired negotiation-scoped query methods the Java guard keeps absent from both facades. REMOVED_NEGOTIATION_QUERY_METHODS: tuple[str, ...] = ("get_negotiation_prompts", "get_negotiation_prompt") -#: The extension-agnostic query methods both facades expose with one signature. -CROSS_EXTENSION_QUERY_METHODS: tuple[str, ...] = ("get_prompts", "get_prompt") +#: The internal template query methods both facades expose with one signature. +INTERNAL_TEMPLATE_QUERY_METHODS: tuple[str, ...] = ("_get_prompts", "_get_prompt") #: Name shape of the negotiation content API family, used to catch family drift beyond the closed #: list (a new ``generate_negotiation_*`` or negotiation ``validate_*_prompt_and_data_filling`` @@ -233,28 +233,26 @@ def test_negotiation_api_family_is_exactly_the_twelve_methods(facade_name: str, @pytest.mark.parametrize("method", REMOVED_NEGOTIATION_QUERY_METHODS) def test_removed_negotiation_query_method_is_absent(facade_name: str, facade: type, method: str) -> None: """Neither facade exposes the removed negotiation-scoped query methods.""" - assert not hasattr(facade, method), ( - f"{facade_name} must not expose the removed {method} query method; " - "use the extension-agnostic get_prompts / get_prompt instead" - ) + assert not hasattr(facade, method), f"{facade_name} must not expose the removed {method} query method" # -------------------------------------------------------------------------------------- -# The cross-extension query methods (Java bothFacadesExposeTheSameCrossExtensionQueryMethods) +# The internal template query methods (not part of the public facade surface) # -------------------------------------------------------------------------------------- -@pytest.mark.parametrize("method", CROSS_EXTENSION_QUERY_METHODS) -def test_cross_extension_query_method_is_symmetric(method: str) -> None: - """The extension-agnostic template queries exist on the client and match on the server.""" +@pytest.mark.parametrize("method", INTERNAL_TEMPLATE_QUERY_METHODS) +def test_internal_template_query_method_is_symmetric(method: str) -> None: + """The internal template queries exist on the client and match on the server.""" assert callable(getattr(A2ATClient, method, None)), f"A2ATClient is missing {method}" _assert_symmetric_signatures(A2ATClient, A2ATServer, method) -def test_client_exposes_exactly_the_cross_extension_query_methods() -> None: - """The client facade exposes both cross-extension query methods and no other one.""" - exposed = {method for method in _public_methods(A2ATClient) if method in CROSS_EXTENSION_QUERY_METHODS} - assert exposed == set(CROSS_EXTENSION_QUERY_METHODS) +@pytest.mark.parametrize("facade_name,facade", _FACADES) +def test_template_query_methods_are_not_public(facade_name: str, facade: type) -> None: + """The template query methods stay private on both facades.""" + for method in ("get_prompts", "get_prompt"): + assert not hasattr(facade, method), f"{facade_name} must not expose the public {method} method" # -------------------------------------------------------------------------------------- @@ -386,14 +384,14 @@ def test_both_facades_generate_the_identical_negotiation_message( @pytest.mark.parametrize("language", LANGUAGES) def test_both_facades_list_the_same_template_catalog(language: str, tmp_path: Path) -> None: - """Both facades built from one config answer the template queries with the same catalog.""" + """Both facades built from one config answer the internal template queries with the same catalog.""" client, server = _build_facades(_write_env(tmp_path, language)) - client_prompts = client.get_prompts() - server_prompts = server.get_prompts() + client_prompts = client._get_prompts() + server_prompts = server._get_prompts() assert [prompt.template_uri.uri for prompt in client_prompts] == [ prompt.template_uri.uri for prompt in server_prompts ] assert client_prompts == server_prompts - assert client.get_prompt("Negotiation-T/common/abort/v1") == server.get_prompt("Negotiation-T/common/abort/v1") + assert client._get_prompt("Negotiation-T/common/abort/v1") == server._get_prompt("Negotiation-T/common/abort/v1")