polaris-python-ai 0.0.1.dev2__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.
@@ -0,0 +1,92 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python-ai available.
2
+ #
3
+ # Copyright (C) 2026 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (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
+ # https://opensource.org/licenses/BSD-3-Clause
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
+ """Lifecycle glue between an already started A2A server and Polaris."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import inspect
23
+
24
+ from polaris_ai._async import await_completion
25
+
26
+
27
+ class PolarisA2AServerRegistration:
28
+ """Register an A2A endpoint after its server becomes ready.
29
+
30
+ ``server_ready`` accepts an ``asyncio.Event``, an awaitable, or a callable
31
+ returning an awaitable. Closing the registration closes the registrar, so
32
+ normal exceptions and cancellation unwind through the context manager and
33
+ trigger deregistration.
34
+ """
35
+
36
+ def __init__(self, registrar, agent_card, transports, *, server_ready=None):
37
+ if registrar is None:
38
+ raise ValueError("A PolarisA2ARegistrar is required")
39
+ if agent_card is None:
40
+ raise ValueError("An AgentCard is required")
41
+ self._registrar = registrar
42
+ self._agent_card = agent_card
43
+ self._transports = tuple(transports)
44
+ self._server_ready = server_ready
45
+ self._lock = asyncio.Lock()
46
+ self._registered = False
47
+ self._closed = False
48
+
49
+ @property
50
+ def registered(self) -> bool:
51
+ return self._registered
52
+
53
+ async def _wait_for_server(self):
54
+ ready = self._server_ready
55
+ if ready is None:
56
+ return
57
+ if isinstance(ready, asyncio.Event):
58
+ await ready.wait()
59
+ return
60
+ if inspect.isawaitable(ready):
61
+ await ready
62
+ return
63
+ if callable(ready):
64
+ result = ready()
65
+ if inspect.isawaitable(result):
66
+ await result
67
+ return
68
+ raise TypeError("server_ready must be an asyncio.Event, awaitable, or callable")
69
+
70
+ async def async_start(self):
71
+ async with self._lock:
72
+ if self._closed:
73
+ raise RuntimeError("Polaris A2A server registration is closed")
74
+ if self._registered:
75
+ return self
76
+ await self._wait_for_server()
77
+ await self._registrar.async_register(self._agent_card, self._transports)
78
+ self._registered = True
79
+ return self
80
+
81
+ async def async_close(self):
82
+ async with self._lock:
83
+ self._closed = True
84
+ await await_completion(self._registrar.async_close())
85
+ if not self._registrar.registered:
86
+ self._registered = False
87
+
88
+ async def __aenter__(self):
89
+ return await self.async_start()
90
+
91
+ async def __aexit__(self, *_):
92
+ await self.async_close()
@@ -0,0 +1,76 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python-ai available.
2
+ #
3
+ # Copyright (C) 2026 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (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
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ """AgentScope integrations; optional dependencies load only when requested."""
17
+
18
+ from importlib import import_module
19
+
20
+ from polaris_ai._version import __version__
21
+
22
+ __all__ = [
23
+ "PolarisSkillLoader",
24
+ "PolarisAgentContext",
25
+ "PolarisA2AAgent",
26
+ "PolarisAgentConfig",
27
+ "PolarisAgentRuntime",
28
+ "PolarisDynamicSkillMiddleware",
29
+ "PolarisMCPResolver",
30
+ "PolarisMCPClientPool",
31
+ "PolarisAgentCardTemplate",
32
+ "build_agent_card",
33
+ "PolarisA2ARegistrar",
34
+ "PolarisA2AResolver",
35
+ "PolarisA2AServerRegistration",
36
+ "A2ATransport",
37
+ "AgentCardNotFoundError",
38
+ "RegisteredInstance",
39
+ "PolarisSkillNotFoundError",
40
+ "PolarisSkillRepositoryInfo",
41
+ "PolarisAILogOptions",
42
+ "PolarisAILogger",
43
+ "__version__",
44
+ ]
45
+
46
+
47
+ def __getattr__(name: str):
48
+ if name not in __all__:
49
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
50
+ if name == "PolarisSkillLoader":
51
+ module = ".skill"
52
+ elif name == "PolarisAgentContext":
53
+ module = ".context"
54
+ elif name == "PolarisA2AAgent":
55
+ module = ".a2a"
56
+ elif name in ("PolarisAgentConfig",):
57
+ module = ".config"
58
+ elif name in ("PolarisAgentRuntime",):
59
+ module = ".runtime"
60
+ elif name in ("PolarisDynamicSkillMiddleware",):
61
+ module = ".middleware"
62
+ elif name in ("PolarisMCPResolver", "PolarisMCPClientPool"):
63
+ module = ".mcp"
64
+ elif name in ("PolarisAgentCardTemplate", "build_agent_card"):
65
+ module = ".cards"
66
+ elif name == "PolarisA2AServerRegistration":
67
+ module = "polaris_ai.a2a.server"
68
+ elif name in ("PolarisSkillNotFoundError", "PolarisSkillRepositoryInfo"):
69
+ module = ".skill"
70
+ elif name in ("PolarisAILogOptions", "PolarisAILogger"):
71
+ module = "polaris_ai.log"
72
+ else:
73
+ module = "polaris_ai.a2a"
74
+ value = getattr(import_module(module, __name__), name)
75
+ globals()[name] = value
76
+ return value
@@ -0,0 +1,276 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python-ai available.
2
+ #
3
+ # Copyright (C) 2026 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (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
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ """A resilient wrapper around AgentScope's native A2A client."""
17
+
18
+ import asyncio
19
+ import time
20
+
21
+ from polaris.pkg.model.service import InstanceType
22
+
23
+ from polaris_ai._async import await_completion
24
+ from polaris_ai.log import get_ai_logger
25
+ from polaris_ai.metrics import PolarisCallReporter
26
+
27
+
28
+ class PolarisA2AAgent:
29
+ """Resolve once, then rebuild the native A2A client only when needed.
30
+
31
+ A card is resolved before the first call. Later calls reuse the native
32
+ AgentScope ``A2AAgent`` unless the caller invalidates it or a network
33
+ failure occurs before any response event has been delivered. Business
34
+ errors are propagated without switching instances. The wrapper owns the
35
+ supplied resolver and closes it together with the current client.
36
+ """
37
+
38
+ def __init__(self, resolver, agent_name: str, *, agent_factory=None, reporter=None,
39
+ caller_service: str = "", labels: str = "", logger=None):
40
+ if resolver is None:
41
+ raise ValueError("A PolarisA2AResolver is required")
42
+ if not isinstance(agent_name, str) or not agent_name.strip():
43
+ raise ValueError("agent_name is required")
44
+ if agent_factory is None:
45
+ from agentscope.agent import A2AAgent
46
+
47
+ agent_factory = A2AAgent
48
+ self._resolver = resolver
49
+ self._agent_name = agent_name.strip()
50
+ self._agent_factory = agent_factory
51
+ self._reporter = reporter
52
+ self.logger = logger if logger is not None else get_ai_logger("a2a")
53
+ if self._reporter is None and getattr(resolver, "consumer", None) is not None:
54
+ self._reporter = PolarisCallReporter(
55
+ resolver.consumer,
56
+ getattr(resolver, "namespace", "default"),
57
+ caller_service=caller_service,
58
+ labels=labels,
59
+ )
60
+ self._agent = None
61
+ self._card = None
62
+ self._lock = asyncio.Lock()
63
+ self._closed = False
64
+
65
+ @property
66
+ def name(self) -> str:
67
+ return self._agent_name
68
+
69
+ @property
70
+ def card(self):
71
+ return self._card
72
+
73
+ @property
74
+ def agent(self):
75
+ return self._agent
76
+
77
+ def _check_open(self):
78
+ if self._closed:
79
+ raise RuntimeError("Polaris A2A agent is closed")
80
+
81
+ @staticmethod
82
+ def _is_timeout(error: BaseException) -> bool:
83
+ if isinstance(error, (asyncio.TimeoutError, TimeoutError)):
84
+ return True
85
+ try:
86
+ from a2a.client.errors import A2AClientTimeoutError
87
+ except ImportError:
88
+ return False
89
+ return isinstance(error, A2AClientTimeoutError)
90
+
91
+ @staticmethod
92
+ def _is_retryable(error: BaseException) -> bool:
93
+ if PolarisA2AAgent._is_timeout(error) or isinstance(error, OSError):
94
+ return True
95
+ try:
96
+ from a2a.client.errors import A2AClientTimeoutError
97
+ except ImportError:
98
+ return False
99
+ if isinstance(error, A2AClientTimeoutError):
100
+ return True
101
+ try:
102
+ from a2a.client.errors import A2AClientError
103
+ except ImportError:
104
+ return False
105
+ return isinstance(error, A2AClientError) and str(error).startswith("Network communication error")
106
+
107
+ async def _ensure_agent(self):
108
+ self._check_open()
109
+ if self._agent is None:
110
+ self.logger.debug("Resolving A2A agent client: name=%s", self._agent_name)
111
+ self._card = await self._resolver.async_resolve(self._agent_name)
112
+ self._agent = self._agent_factory(self._card)
113
+ await self._agent.__aenter__()
114
+ self.logger.debug("Created A2A agent client: name=%s", self._agent_name)
115
+ return self._agent
116
+
117
+ async def _close_agent(self):
118
+ agent = self._agent
119
+ if agent is None:
120
+ return
121
+ self.logger.debug("Closing A2A agent client: name=%s", self._agent_name)
122
+ await await_completion(agent.aclose())
123
+ if self._agent is agent:
124
+ self._agent = None
125
+ self._card = None
126
+
127
+ async def _replace_agent(self):
128
+ self.logger.warning_throttled(
129
+ ("rebuild", self._agent_name),
130
+ "Rebuilding A2A agent client after network failure: name=%s",
131
+ self._agent_name,
132
+ )
133
+ await self._close_agent()
134
+ await self._ensure_agent()
135
+
136
+ async def reply(self, inputs):
137
+ async with self._lock:
138
+ self.logger.debug("Sending non-streaming A2A request: name=%s", self._agent_name)
139
+ agent = await self._ensure_agent()
140
+ started_at = time.monotonic()
141
+ final_message = None
142
+ saw_remote_event = False
143
+ try:
144
+ async for event in agent.reply_stream(inputs, yield_final_msg=True):
145
+ if not _is_local_reply_start_event(event):
146
+ saw_remote_event = True
147
+ if isinstance(event, _message_type()):
148
+ final_message = event
149
+ if final_message is None:
150
+ raise RuntimeError("A2A agent did not produce a final message")
151
+ await self._report_call(started_at, success=True)
152
+ self.logger.debug("Completed non-streaming A2A request: name=%s",
153
+ self._agent_name)
154
+ return final_message
155
+ except BaseException as error:
156
+ await self._report_call(
157
+ started_at, success=False,
158
+ timeout=self._is_timeout(error),
159
+ )
160
+ if saw_remote_event or not self._is_retryable(error):
161
+ self.logger.debug(
162
+ "A2A request failed without retry: name=%s error=%r",
163
+ self._agent_name, error,
164
+ )
165
+ raise
166
+ await self._replace_agent()
167
+ retry_started = time.monotonic()
168
+ try:
169
+ self.logger.debug("Retrying A2A request: name=%s", self._agent_name)
170
+ reply = await self._agent.reply(inputs)
171
+ except BaseException as retry_error:
172
+ await self._report_call(
173
+ retry_started, success=False,
174
+ timeout=self._is_timeout(retry_error),
175
+ )
176
+ raise
177
+ await self._report_call(retry_started, success=True)
178
+ self.logger.debug("Retried A2A request succeeded: name=%s", self._agent_name)
179
+ return reply
180
+
181
+ async def reply_stream(self, inputs, *, yield_final_msg: bool = False):
182
+ async with self._lock:
183
+ self.logger.debug("Sending streaming A2A request: name=%s", self._agent_name)
184
+ agent = await self._ensure_agent()
185
+ started_at = time.monotonic()
186
+ yielded = False
187
+ succeeded = False
188
+ try:
189
+ async for event in agent.reply_stream(inputs, yield_final_msg=yield_final_msg):
190
+ yielded = True
191
+ yield event
192
+ succeeded = True
193
+ except BaseException as error:
194
+ await self._report_call(
195
+ started_at, success=False,
196
+ timeout=self._is_timeout(error),
197
+ )
198
+ if yielded or not self._is_retryable(error):
199
+ self.logger.debug(
200
+ "A2A stream failed without retry: name=%s yielded=%s error=%r",
201
+ self._agent_name, yielded, error,
202
+ )
203
+ raise
204
+ await self._replace_agent()
205
+ async for event in self._agent.reply_stream(inputs, yield_final_msg=yield_final_msg):
206
+ yield event
207
+ succeeded = True
208
+ finally:
209
+ if succeeded:
210
+ await self._report_call(started_at, success=True)
211
+ self.logger.debug("Completed streaming A2A request: name=%s",
212
+ self._agent_name)
213
+
214
+ async def _report_call(self, started_at, *, success, timeout=False):
215
+ if self._reporter is None:
216
+ return
217
+ endpoint = self._resolver.selected_endpoint(self._agent_name)
218
+ if endpoint is None:
219
+ return
220
+ host, port = endpoint
221
+ await self._reporter.report(
222
+ service=self._agent_name,
223
+ host=host,
224
+ port=port,
225
+ method="message/send",
226
+ instance_type=InstanceType.A2A,
227
+ started_at=started_at,
228
+ success=success,
229
+ timeout=timeout,
230
+ )
231
+
232
+ async def observe(self, msgs):
233
+ async with self._lock:
234
+ agent = await self._ensure_agent()
235
+ await agent.observe(msgs)
236
+
237
+ async def invalidate(self):
238
+ """Close the current client and make the next call resolve again."""
239
+ async with self._lock:
240
+ self._check_open()
241
+ self.logger.debug("Invalidating A2A agent client: name=%s", self._agent_name)
242
+ await self._close_agent()
243
+ self._resolver.invalidate(self._agent_name)
244
+
245
+ async def aclose(self):
246
+ self._closed = True
247
+ self.logger.debug("Closing A2A agent wrapper: name=%s", self._agent_name)
248
+ await await_completion(self._close_all())
249
+
250
+ async def _close_all(self):
251
+ async with self._lock:
252
+ try:
253
+ await self._close_agent()
254
+ finally:
255
+ await self._resolver.async_close()
256
+
257
+ async def __aenter__(self):
258
+ self._check_open()
259
+ return self
260
+
261
+ async def __aexit__(self, *_):
262
+ await self.aclose()
263
+
264
+
265
+ def _message_type():
266
+ from agentscope.message import Msg
267
+
268
+ return Msg
269
+
270
+
271
+ def _is_local_reply_start_event(event):
272
+ try:
273
+ from agentscope.event import ReplyStartEvent
274
+ except ImportError:
275
+ return False
276
+ return isinstance(event, ReplyStartEvent)
@@ -0,0 +1,106 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python-ai available.
2
+ #
3
+ # Copyright (C) 2026 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (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
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ """Reusable native AgentCard construction for AgentScope applications."""
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Sequence
21
+ from dataclasses import dataclass, field
22
+
23
+ try:
24
+ from a2a.types import (
25
+ AgentCapabilities,
26
+ AgentCard,
27
+ AgentInterface,
28
+ AgentSkill,
29
+ )
30
+ except ImportError as error:
31
+ raise ImportError(
32
+ "AgentCard construction requires pip install 'polaris-python-ai[a2a]'."
33
+ ) from error
34
+
35
+
36
+ def _agent_skill(skill) -> AgentSkill:
37
+ if isinstance(skill, AgentSkill):
38
+ return skill
39
+ name = getattr(skill, "name", "")
40
+ description = getattr(skill, "description", "")
41
+ if not name:
42
+ raise ValueError("Agent Skill requires a name")
43
+ return AgentSkill(id=name, name=name, description=description, tags=["polaris"])
44
+
45
+
46
+ def build_agent_card(
47
+ *,
48
+ name: str,
49
+ url: str,
50
+ description: str = "",
51
+ version: str = "1.0.0",
52
+ protocol_binding: str = "JSONRPC",
53
+ protocol_version: str = "1.0",
54
+ skills: Sequence = (),
55
+ input_modes: Sequence[str] = ("text/plain",),
56
+ output_modes: Sequence[str] = ("text/plain",),
57
+ streaming: bool = True,
58
+ ) -> AgentCard:
59
+ """Build a native A2A card while preserving framework-owned types."""
60
+ if not isinstance(name, str) or not name.strip():
61
+ raise ValueError("Agent name is required")
62
+ if not isinstance(url, str) or not url.strip():
63
+ raise ValueError("Agent URL is required")
64
+ return AgentCard(
65
+ name=name.strip(),
66
+ description=description,
67
+ version=version,
68
+ supported_interfaces=[AgentInterface(
69
+ url=url,
70
+ protocol_binding=protocol_binding,
71
+ protocol_version=protocol_version,
72
+ )],
73
+ capabilities=AgentCapabilities(streaming=streaming),
74
+ default_input_modes=list(input_modes),
75
+ default_output_modes=list(output_modes),
76
+ skills=[_agent_skill(skill) for skill in skills],
77
+ )
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class PolarisAgentCardTemplate:
82
+ """Reusable card defaults; only endpoint and runtime Skills vary per Agent."""
83
+
84
+ description: str = ""
85
+ version: str = "1.0.0"
86
+ protocol_binding: str = "JSONRPC"
87
+ protocol_version: str = "1.0"
88
+ input_modes: tuple[str, ...] = ("text/plain",)
89
+ output_modes: tuple[str, ...] = ("text/plain",)
90
+ streaming: bool = True
91
+ skills: tuple = field(default_factory=tuple)
92
+
93
+ def build(self, name: str, url: str, *, skills: Sequence = ()) -> AgentCard:
94
+ selected = tuple(skills) if skills else self.skills
95
+ return build_agent_card(
96
+ name=name,
97
+ url=url,
98
+ description=self.description,
99
+ version=self.version,
100
+ protocol_binding=self.protocol_binding,
101
+ protocol_version=self.protocol_version,
102
+ skills=selected,
103
+ input_modes=self.input_modes,
104
+ output_modes=self.output_modes,
105
+ streaming=self.streaming,
106
+ )