nvidia-nat-a2a 1.4.0a20251207__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nvidia-nat-a2a might be problematic. Click here for more details.

nat/meta/pypi.md ADDED
@@ -0,0 +1,36 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ SPDX-License-Identifier: Apache-2.0
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ -->
17
+
18
+ ![NVIDIA NeMo Agent Toolkit](https://media.githubusercontent.com/media/NVIDIA/NeMo-Agent-Toolkit/refs/heads/main/docs/source/_static/banner.png "NeMo Agent toolkit banner image")
19
+
20
+
21
+ # NVIDIA NeMo Agent Toolkit A2A Subpackage
22
+ Subpackage for A2A Protocol integration in NeMo Agent toolkit.
23
+
24
+ This package provides A2A (Agent-to-Agent) Protocol functionality, allowing NeMo Agent toolkit workflows to connect to remote A2A agents and invoke their skills as functions. This package includes both the client and server components of the A2A protocol.
25
+
26
+ ## Features
27
+ ### Client
28
+ - Connect to remote A2A agents via HTTP with JSON-RPC transport
29
+ - Discover agent capabilities through Agent Cards
30
+ - Submit tasks to remote agents with async execution
31
+
32
+ ### Server
33
+ - Serve A2A agents via HTTP with JSON-RPC transport
34
+ - Support for A2A agent executor pattern
35
+
36
+ For more information about the NVIDIA NeMo Agent Toolkit, please visit the [NeMo Agent Toolkit GitHub Repo](https://github.com/NVIDIA/NeMo-Agent-Toolkit).
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
@@ -0,0 +1,328 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from collections.abc import AsyncGenerator
20
+ from datetime import timedelta
21
+ from uuid import uuid4
22
+
23
+ import httpx
24
+
25
+ from a2a.client import A2ACardResolver
26
+ from a2a.client import Client
27
+ from a2a.client import ClientConfig
28
+ from a2a.client import ClientEvent
29
+ from a2a.client import ClientFactory
30
+ from a2a.types import AgentCard
31
+ from a2a.types import Message
32
+ from a2a.types import Part
33
+ from a2a.types import Role
34
+ from a2a.types import Task
35
+ from a2a.types import TextPart
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class A2ABaseClient:
41
+ """
42
+ Minimal A2A client for connecting to an A2A agent.
43
+
44
+ Args:
45
+ base_url: The base URL of the A2A agent
46
+ task_timeout: Timeout for task operations (default: 300 seconds)
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ base_url: str,
52
+ agent_card_path: str = "/.well-known/agent-card.json",
53
+ task_timeout: timedelta = timedelta(seconds=300),
54
+ streaming: bool = True,
55
+ ):
56
+ self._base_url = base_url
57
+ self._agent_card_path = agent_card_path
58
+ self._task_timeout = task_timeout
59
+ self._streaming = streaming
60
+
61
+ self._httpx_client: httpx.AsyncClient | None = None
62
+ self._client: Client | None = None
63
+ self._agent_card: AgentCard | None = None
64
+
65
+ @property
66
+ def base_url(self) -> str:
67
+ return self._base_url
68
+
69
+ @property
70
+ def agent_card(self) -> AgentCard | None:
71
+ return self._agent_card
72
+
73
+ async def __aenter__(self):
74
+ if self._httpx_client is not None or self._client is not None:
75
+ raise RuntimeError("A2ABaseClient already initialized")
76
+
77
+ # 1) Create httpx client explicitly
78
+ self._httpx_client = httpx.AsyncClient(timeout=httpx.Timeout(self._task_timeout.total_seconds()))
79
+
80
+ # 2) Resolve agent card
81
+ await self._resolve_agent_card()
82
+ if not self._agent_card:
83
+ raise RuntimeError("Agent card not resolved")
84
+
85
+ # 3) Create A2A client
86
+ client_config = ClientConfig(
87
+ httpx_client=self._httpx_client,
88
+ streaming=self._streaming,
89
+ )
90
+ factory = ClientFactory(client_config)
91
+ self._client = factory.create(self._agent_card)
92
+
93
+ logger.info("Connected to A2A agent at %s", self._base_url)
94
+ return self
95
+
96
+ async def __aexit__(self, exc_type, exc_value, traceback):
97
+ # Close A2A client first (if it exposes aclose)
98
+ if self._client is not None:
99
+ aclose = getattr(self._client, "aclose", None)
100
+ if aclose is not None:
101
+ try:
102
+ await aclose()
103
+ except Exception:
104
+ logger.warning("Error while closing A2A client", exc_info=True)
105
+
106
+ # Then close httpx client
107
+ if self._httpx_client is not None:
108
+ try:
109
+ await self._httpx_client.aclose()
110
+ except Exception:
111
+ logger.warning("Error while closing HTTPX client", exc_info=True)
112
+
113
+ self._httpx_client = None
114
+ self._client = None
115
+ self._agent_card = None
116
+
117
+ async def _resolve_agent_card(self):
118
+ """Fetch the agent card from the A2A agent."""
119
+ if not self._httpx_client:
120
+ raise RuntimeError("httpx_client is not initialized")
121
+
122
+ try:
123
+ resolver = A2ACardResolver(httpx_client=self._httpx_client,
124
+ base_url=self._base_url,
125
+ agent_card_path=self._agent_card_path)
126
+ logger.info("Fetching agent card from: %s%s", self._base_url, self._agent_card_path)
127
+ self._agent_card = await resolver.get_agent_card()
128
+ logger.info("Successfully fetched public agent card")
129
+ # TODO: add support for authenticated extended agent card
130
+ except Exception as e:
131
+ logger.error("Failed to fetch agent card: %s", e, exc_info=True)
132
+ raise RuntimeError(f"Failed to fetch agent card from {self._base_url}") from e
133
+
134
+ async def send_message(self,
135
+ message_text: str,
136
+ task_id: str | None = None,
137
+ context_id: str | None = None) -> AsyncGenerator[ClientEvent | Message, None]:
138
+ """
139
+ Send a message to the agent and stream response events.
140
+
141
+ This is the low-level A2A protocol method that yields events as they arrive.
142
+ For simpler usage, prefer the high-level agent function registered by this client.
143
+
144
+ Args:
145
+ message_text: The message text to send
146
+ task_id: Optional task ID to continue an existing conversation
147
+ context_id: Optional context ID for the conversation
148
+
149
+ Yields:
150
+ ClientEvent | Message: The agent's response events as they arrive.
151
+ ClientEvent is a tuple of (Task, UpdateEvent | None)
152
+ Message is a direct message response
153
+ """
154
+ if not self._client:
155
+ raise RuntimeError("A2ABaseClient not initialized")
156
+
157
+ text_part = TextPart(text=message_text)
158
+ parts: list[Part] = [Part(root=text_part)]
159
+ message = Message(role=Role.user, parts=parts, message_id=uuid4().hex, task_id=task_id, context_id=context_id)
160
+
161
+ async for response in self._client.send_message(message):
162
+ yield response
163
+
164
+ async def get_task(self, task_id: str, history_length: int | None = None) -> Task:
165
+ """
166
+ Get the status and details of a specific task.
167
+
168
+ This is an A2A protocol operation for retrieving task information.
169
+
170
+ Args:
171
+ task_id: The unique identifier of the task
172
+ history_length: Optional limit on the number of history messages to retrieve
173
+
174
+ Returns:
175
+ Task: The task object with current status and history
176
+ """
177
+ if not self._client:
178
+ raise RuntimeError("A2ABaseClient not initialized")
179
+
180
+ from a2a.types import TaskQueryParams
181
+ params = TaskQueryParams(id=task_id, history_length=history_length)
182
+ return await self._client.get_task(params)
183
+
184
+ async def cancel_task(self, task_id: str) -> Task:
185
+ """
186
+ Cancel a running task.
187
+
188
+ This is an A2A protocol operation for canceling tasks.
189
+
190
+ Args:
191
+ task_id: The unique identifier of the task to cancel
192
+
193
+ Returns:
194
+ Task: The task object with updated status
195
+ """
196
+ if not self._client:
197
+ raise RuntimeError("A2ABaseClient not initialized")
198
+
199
+ from a2a.types import TaskIdParams
200
+ params = TaskIdParams(id=task_id)
201
+ return await self._client.cancel_task(params)
202
+
203
+ async def send_message_streaming(self,
204
+ message_text: str,
205
+ task_id: str | None = None,
206
+ context_id: str | None = None) -> AsyncGenerator[ClientEvent | Message, None]:
207
+ """
208
+ Send a message to the agent and stream response events (alias for send_message).
209
+
210
+ This method provides an explicit streaming interface that mirrors the A2A SDK pattern.
211
+ It is functionally identical to send_message(), which already streams events.
212
+
213
+ Args:
214
+ message_text: The message text to send
215
+ task_id: Optional task ID to continue an existing conversation
216
+ context_id: Optional context ID for the conversation
217
+
218
+ Yields:
219
+ ClientEvent | Message: The agent's response events as they arrive.
220
+ """
221
+ async for event in self.send_message(message_text, task_id=task_id, context_id=context_id):
222
+ yield event
223
+
224
+ def extract_text_from_parts(self, parts: list) -> list[str]:
225
+ """
226
+ Extract text content from A2A message parts.
227
+
228
+ Args:
229
+ parts: List of A2A Part objects
230
+
231
+ Returns:
232
+ List of text strings extracted from the parts
233
+ """
234
+ text_parts = []
235
+ for part in parts:
236
+ # Handle Part wrapper (RootModel)
237
+ if hasattr(part, 'root'):
238
+ part_content = part.root
239
+ else:
240
+ part_content = part
241
+
242
+ # Extract text from TextPart
243
+ if hasattr(part_content, 'text'):
244
+ text_parts.append(part_content.text)
245
+
246
+ return text_parts
247
+
248
+ def extract_text_from_task(self, task) -> str:
249
+ """
250
+ Extract text response from an A2A Task object.
251
+
252
+ This method understands the A2A protocol structure and extracts the final
253
+ text response from a completed task, prioritizing artifacts over history.
254
+
255
+ Args:
256
+ task: A2A Task object
257
+
258
+ Returns:
259
+ Extracted text response or status message
260
+
261
+ Priority order:
262
+ 1. Check task status (return error/progress if not completed)
263
+ 2. Extract from task.artifacts (structured output)
264
+ 3. Fallback to last agent message in task.history
265
+ """
266
+ from a2a.types import TaskState
267
+
268
+ # Check task status
269
+ if task.status and task.status.state != TaskState.completed:
270
+ # Task not completed - return status message or indicate in progress
271
+ if task.status.state == TaskState.failed:
272
+ return f"Task failed: {task.status.message or 'Unknown error'}"
273
+ return f"Task in progress (state: {task.status.state})"
274
+
275
+ # Priority 1: Extract from artifacts (structured output)
276
+ if task.artifacts:
277
+ # Get text from all artifacts
278
+ all_text = []
279
+ for artifact in task.artifacts:
280
+ if artifact.parts:
281
+ text_parts = self.extract_text_from_parts(artifact.parts)
282
+ if text_parts:
283
+ all_text.extend(text_parts)
284
+ if all_text:
285
+ return " ".join(all_text)
286
+
287
+ # Priority 2: Fallback to history (conversational messages)
288
+ if task.history:
289
+ # Get the last agent message from history
290
+ for msg in reversed(task.history):
291
+ if msg.role.value == 'agent': # Get last agent message
292
+ text_parts = self.extract_text_from_parts(msg.parts)
293
+ if text_parts:
294
+ return " ".join(text_parts)
295
+
296
+ return "No response"
297
+
298
+ def extract_text_from_events(self, events: list) -> str:
299
+ """
300
+ Extract text response from a list of A2A events.
301
+
302
+ This is a convenience method that handles both Message and ClientEvent types.
303
+
304
+ Args:
305
+ events: List of A2A events (ClientEvent or Message objects)
306
+
307
+ Returns:
308
+ Extracted text response
309
+ """
310
+ from a2a.types import Message as A2AMessage
311
+
312
+ if not events:
313
+ return "No response"
314
+
315
+ # Get the last event
316
+ last_event = events[-1]
317
+
318
+ # If it's a Message, extract text from parts
319
+ if isinstance(last_event, A2AMessage):
320
+ text_parts = self.extract_text_from_parts(last_event.parts)
321
+ return " ".join(text_parts) if text_parts else str(last_event)
322
+
323
+ # If it's a ClientEvent (Task, TaskStatusUpdateEvent), extract from task
324
+ if isinstance(last_event, tuple):
325
+ task, _ = last_event
326
+ return self.extract_text_from_task(task)
327
+
328
+ return str(last_event)
@@ -0,0 +1,69 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from datetime import timedelta
17
+
18
+ from pydantic import Field
19
+ from pydantic import HttpUrl
20
+
21
+ from nat.data_models.component_ref import AuthenticationRef
22
+ from nat.data_models.function import FunctionGroupBaseConfig
23
+
24
+
25
+ class A2AClientConfig(FunctionGroupBaseConfig, name="a2a_client"):
26
+ """Configuration for A2A client function group.
27
+
28
+ This configuration enables NAT workflows to connect to remote A2A agents
29
+ and publish the primary agent function and helper functions.
30
+
31
+ Attributes:
32
+ url: The base URL of the A2A agent (e.g., https://agent.example.com)
33
+ agent_card_path: Path to the agent card (default: /.well-known/agent-card.json)
34
+ task_timeout: Maximum time to wait for task completion (default: 300 seconds)
35
+ include_skills_in_description: Include skill details in high-level function description (default: True)
36
+ streaming: Whether to enable streaming support for the A2A client (default: False)
37
+ auth_provider: Optional reference to NAT auth provider for authentication
38
+ """
39
+
40
+ url: HttpUrl = Field(
41
+ ...,
42
+ description="Base URL of the A2A agent",
43
+ )
44
+
45
+ agent_card_path: str = Field(
46
+ default='/.well-known/agent-card.json',
47
+ description="Path to the agent card",
48
+ )
49
+
50
+ task_timeout: timedelta = Field(
51
+ default=timedelta(seconds=300),
52
+ description="Maximum time to wait for task completion",
53
+ )
54
+
55
+ include_skills_in_description: bool = Field(
56
+ default=True,
57
+ description="Include skill details in the high-level agent function description. "
58
+ "Set to False for shorter descriptions (useful for token optimization). "
59
+ "Skills are always available via get_skills() helper.",
60
+ )
61
+
62
+ # streaming is disabled by default because of AIQ-2496
63
+ streaming: bool = Field(
64
+ default=False,
65
+ description="Whether to enable streaming support for the A2A client",
66
+ )
67
+
68
+ auth_provider: str | AuthenticationRef | None = Field(default=None,
69
+ description="Reference to authentication provider")