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.
- polaris_ai/__init__.py +34 -0
- polaris_ai/_async.py +39 -0
- polaris_ai/_version.py +18 -0
- polaris_ai/a2a/__init__.py +31 -0
- polaris_ai/a2a/_card.py +44 -0
- polaris_ai/a2a/registry.py +240 -0
- polaris_ai/a2a/resolver.py +198 -0
- polaris_ai/a2a/server.py +92 -0
- polaris_ai/agentscope/__init__.py +76 -0
- polaris_ai/agentscope/a2a.py +276 -0
- polaris_ai/agentscope/cards.py +106 -0
- polaris_ai/agentscope/config.py +221 -0
- polaris_ai/agentscope/context.py +195 -0
- polaris_ai/agentscope/mcp.py +598 -0
- polaris_ai/agentscope/middleware.py +103 -0
- polaris_ai/agentscope/runtime.py +291 -0
- polaris_ai/agentscope/skill.py +464 -0
- polaris_ai/langgraph/__init__.py +50 -0
- polaris_ai/langgraph/config.py +160 -0
- polaris_ai/langgraph/context.py +152 -0
- polaris_ai/langgraph/registry.py +258 -0
- polaris_ai/langgraph/remote.py +315 -0
- polaris_ai/langgraph/resolver.py +226 -0
- polaris_ai/log.py +408 -0
- polaris_ai/metrics.py +154 -0
- polaris_python_ai-0.0.1.dev2.dist-info/METADATA +351 -0
- polaris_python_ai-0.0.1.dev2.dist-info/RECORD +30 -0
- polaris_python_ai-0.0.1.dev2.dist-info/WHEEL +5 -0
- polaris_python_ai-0.0.1.dev2.dist-info/licenses/LICENSE +27 -0
- polaris_python_ai-0.0.1.dev2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
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 small Polaris connection factory for LangGraph applications."""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from collections.abc import Iterable
|
|
20
|
+
|
|
21
|
+
from polaris.api.config import init_context_by_config
|
|
22
|
+
from polaris.api.consumer import ConsumerAPI
|
|
23
|
+
from polaris.api.log import LogOptions
|
|
24
|
+
from polaris.api.provider import ProviderAPI
|
|
25
|
+
from polaris.pkg.config.api import Configuration
|
|
26
|
+
|
|
27
|
+
from polaris_ai._async import await_completion
|
|
28
|
+
|
|
29
|
+
DEFAULT_DISCOVERY_ADDRESS = "127.0.0.1:8091"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_address(address: str) -> str:
|
|
33
|
+
if not isinstance(address, str) or not address.strip():
|
|
34
|
+
raise ValueError("Polaris addresses must be nonempty host:port values")
|
|
35
|
+
address = address.strip()
|
|
36
|
+
host, separator, port_text = address.rpartition(":")
|
|
37
|
+
if not separator or not host:
|
|
38
|
+
raise ValueError(f"Invalid Polaris address {address!r}: a port is required")
|
|
39
|
+
try:
|
|
40
|
+
port = int(port_text)
|
|
41
|
+
except ValueError as error:
|
|
42
|
+
raise ValueError(f"Invalid Polaris address {address!r}: port must be an integer") from error
|
|
43
|
+
if not 0 < port < 65536:
|
|
44
|
+
raise ValueError(f"Invalid Polaris address {address!r}: port must be in [1, 65535]")
|
|
45
|
+
if host.startswith("[") and host.endswith("]"):
|
|
46
|
+
host = host[1:-1]
|
|
47
|
+
elif host.count(":") > 0:
|
|
48
|
+
raise ValueError(f"Invalid Polaris address {address!r}: IPv6 hosts require brackets")
|
|
49
|
+
if not host.strip():
|
|
50
|
+
raise ValueError(f"Invalid Polaris address {address!r}: host is empty")
|
|
51
|
+
return f"[{host}]:{port}" if ":" in host else f"{host}:{port}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PolarisLangGraphContext:
|
|
55
|
+
"""Expose Provider and Consumer APIs over one borrowed or owned SDK context."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
namespace: str = "default",
|
|
60
|
+
*,
|
|
61
|
+
address: str | Iterable[str] = DEFAULT_DISCOVERY_ADDRESS,
|
|
62
|
+
connector_token: str = "",
|
|
63
|
+
log_options: LogOptions | None = None,
|
|
64
|
+
sdk_context=None,
|
|
65
|
+
):
|
|
66
|
+
if sdk_context is not None:
|
|
67
|
+
if sdk_context.is_destroy():
|
|
68
|
+
raise ValueError("An active SDKContext is required")
|
|
69
|
+
self._owns_context = False
|
|
70
|
+
self._context = sdk_context
|
|
71
|
+
else:
|
|
72
|
+
addresses = [address] if isinstance(address, str) else list(address)
|
|
73
|
+
if not addresses:
|
|
74
|
+
raise ValueError("At least one discovery address is required")
|
|
75
|
+
config = Configuration()
|
|
76
|
+
config.set_default()
|
|
77
|
+
config.get_global().get_server_connector().addresses = [
|
|
78
|
+
_validate_address(item) for item in addresses
|
|
79
|
+
]
|
|
80
|
+
config.get_global().get_server_connector().token = connector_token
|
|
81
|
+
config.get_provider().get_lossless().enable = False
|
|
82
|
+
self._owns_context = True
|
|
83
|
+
self._context = None
|
|
84
|
+
try:
|
|
85
|
+
self._context = init_context_by_config(config, log_options=log_options)
|
|
86
|
+
except BaseException:
|
|
87
|
+
if self._context is not None:
|
|
88
|
+
self._context.destroy()
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
self.namespace = namespace.strip() or "default"
|
|
92
|
+
self._closed = False
|
|
93
|
+
try:
|
|
94
|
+
self.provider = ProviderAPI(self._context)
|
|
95
|
+
self.consumer = ConsumerAPI(self._context)
|
|
96
|
+
except BaseException:
|
|
97
|
+
self.close()
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_address(
|
|
102
|
+
cls,
|
|
103
|
+
address: str | Iterable[str] = DEFAULT_DISCOVERY_ADDRESS,
|
|
104
|
+
namespace: str = "default",
|
|
105
|
+
*,
|
|
106
|
+
connector_token: str = "",
|
|
107
|
+
log_options: LogOptions | None = None,
|
|
108
|
+
) -> "PolarisLangGraphContext":
|
|
109
|
+
return cls(
|
|
110
|
+
namespace,
|
|
111
|
+
address=address,
|
|
112
|
+
connector_token=connector_token,
|
|
113
|
+
log_options=log_options,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def context(self):
|
|
118
|
+
return self._context
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def owns_context(self) -> bool:
|
|
122
|
+
return self._owns_context
|
|
123
|
+
|
|
124
|
+
def _check_open(self):
|
|
125
|
+
if self._closed or self._context is None or self._context.is_destroy():
|
|
126
|
+
raise RuntimeError("PolarisLangGraphContext is closed")
|
|
127
|
+
|
|
128
|
+
def close(self):
|
|
129
|
+
if self._closed:
|
|
130
|
+
return
|
|
131
|
+
self._closed = True
|
|
132
|
+
self.provider = None
|
|
133
|
+
self.consumer = None
|
|
134
|
+
if self._owns_context and self._context is not None:
|
|
135
|
+
self._context.destroy()
|
|
136
|
+
|
|
137
|
+
async def async_close(self):
|
|
138
|
+
await await_completion(asyncio.to_thread(self.close))
|
|
139
|
+
|
|
140
|
+
def __enter__(self):
|
|
141
|
+
self._check_open()
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def __exit__(self, *_):
|
|
145
|
+
self.close()
|
|
146
|
+
|
|
147
|
+
async def __aenter__(self):
|
|
148
|
+
self._check_open()
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
async def __aexit__(self, *_):
|
|
152
|
+
await self.async_close()
|
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
"""Register LangGraph deployments through the public Polaris Provider API."""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import concurrent.futures
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from threading import Event, RLock
|
|
23
|
+
|
|
24
|
+
from polaris.api.provider import ProviderAPI
|
|
25
|
+
from polaris.pkg.model.server import InstanceDeRegisterRequest, InstanceRegisterRequest
|
|
26
|
+
|
|
27
|
+
from polaris_ai._async import await_completion, drain_task
|
|
28
|
+
from polaris_ai.log import get_ai_logger
|
|
29
|
+
|
|
30
|
+
META_GRAPH_ID = "langgraph.graph-id"
|
|
31
|
+
META_PATH = "langgraph.path"
|
|
32
|
+
META_PROTOCOL = "langgraph.protocol"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class LangGraphTransport:
|
|
37
|
+
"""An already listening LangGraph Server endpoint."""
|
|
38
|
+
|
|
39
|
+
host: str
|
|
40
|
+
port: int
|
|
41
|
+
path: str = "/"
|
|
42
|
+
graph_id: str = ""
|
|
43
|
+
support_tls: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class RegisteredInstance:
|
|
48
|
+
service: str
|
|
49
|
+
host: str
|
|
50
|
+
port: int
|
|
51
|
+
instance_id: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PolarisLangGraphRegistrar:
|
|
55
|
+
"""Track LangGraph registrations and deregister only instances it created."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
sdk_context,
|
|
60
|
+
namespace: str = "default",
|
|
61
|
+
*,
|
|
62
|
+
service_token: str = "",
|
|
63
|
+
ttl: int = 5,
|
|
64
|
+
logger=None,
|
|
65
|
+
):
|
|
66
|
+
if sdk_context is None or sdk_context.is_destroy():
|
|
67
|
+
raise ValueError("An active SDKContext is required")
|
|
68
|
+
if not isinstance(service_token, str):
|
|
69
|
+
raise ValueError("service_token must be a string")
|
|
70
|
+
if type(ttl) is not int or not 0 < ttl <= 0xFFFFFFFF:
|
|
71
|
+
raise ValueError("ttl must be a positive uint32 number of seconds")
|
|
72
|
+
self.namespace = namespace.strip() or "default"
|
|
73
|
+
self.service_token = service_token.strip()
|
|
74
|
+
self.ttl = ttl
|
|
75
|
+
self.logger = logger if logger is not None else get_ai_logger("langgraph")
|
|
76
|
+
self._context = sdk_context
|
|
77
|
+
self._provider = ProviderAPI(sdk_context)
|
|
78
|
+
self._registered: list[RegisteredInstance] = []
|
|
79
|
+
self._lock = RLock()
|
|
80
|
+
self._closed = False
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def registered(self) -> tuple[RegisteredInstance, ...]:
|
|
84
|
+
with self._lock:
|
|
85
|
+
return tuple(self._registered)
|
|
86
|
+
|
|
87
|
+
def _check_open(self):
|
|
88
|
+
if self._closed or self._context.is_destroy():
|
|
89
|
+
raise RuntimeError("LangGraph registrar or SDKContext is closed")
|
|
90
|
+
|
|
91
|
+
def register(
|
|
92
|
+
self,
|
|
93
|
+
service: str,
|
|
94
|
+
transports: Sequence[LangGraphTransport],
|
|
95
|
+
*,
|
|
96
|
+
version: str = "",
|
|
97
|
+
):
|
|
98
|
+
"""Register one instance per LangGraph endpoint."""
|
|
99
|
+
return self._register(service, tuple(transports), version)
|
|
100
|
+
|
|
101
|
+
def _register(self, service, transports, version="", cancelled: Event | None = None):
|
|
102
|
+
if not isinstance(service, str) or not service.strip():
|
|
103
|
+
raise ValueError("service is required")
|
|
104
|
+
service = service.strip()
|
|
105
|
+
with self._lock:
|
|
106
|
+
self._check_open()
|
|
107
|
+
requests = self._requests(service, transports, version.strip())
|
|
108
|
+
created = []
|
|
109
|
+
self.logger.debug(
|
|
110
|
+
"Registering LangGraph service: namespace=%s service=%s endpoints=%d",
|
|
111
|
+
self.namespace,
|
|
112
|
+
service,
|
|
113
|
+
len(requests),
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
for request in requests:
|
|
117
|
+
if cancelled is not None and cancelled.is_set():
|
|
118
|
+
raise concurrent.futures.CancelledError()
|
|
119
|
+
response = self._provider.register_instance(request)
|
|
120
|
+
instance = RegisteredInstance(
|
|
121
|
+
request.service,
|
|
122
|
+
request.host,
|
|
123
|
+
request.port,
|
|
124
|
+
response.instance_id,
|
|
125
|
+
)
|
|
126
|
+
self._registered.append(instance)
|
|
127
|
+
created.append(instance)
|
|
128
|
+
if cancelled is not None and cancelled.is_set():
|
|
129
|
+
raise concurrent.futures.CancelledError()
|
|
130
|
+
self.logger.info(
|
|
131
|
+
"Registered LangGraph service %s/%s with %d endpoint(s)",
|
|
132
|
+
self.namespace,
|
|
133
|
+
service,
|
|
134
|
+
len(created),
|
|
135
|
+
)
|
|
136
|
+
return tuple(created)
|
|
137
|
+
except BaseException:
|
|
138
|
+
self.logger.warning_throttled(
|
|
139
|
+
("register-rollback", service),
|
|
140
|
+
"Rolling back %d LangGraph registration(s) for %s/%s",
|
|
141
|
+
len(created),
|
|
142
|
+
self.namespace,
|
|
143
|
+
service,
|
|
144
|
+
)
|
|
145
|
+
self._deregister(created)
|
|
146
|
+
raise
|
|
147
|
+
|
|
148
|
+
def _requests(self, service, transports, version):
|
|
149
|
+
if not transports:
|
|
150
|
+
raise ValueError("transports must not be empty")
|
|
151
|
+
requests = []
|
|
152
|
+
for transport in transports:
|
|
153
|
+
if not isinstance(transport, LangGraphTransport):
|
|
154
|
+
raise ValueError("transports must contain LangGraphTransport objects")
|
|
155
|
+
if (
|
|
156
|
+
not isinstance(transport.host, str)
|
|
157
|
+
or not transport.host.strip()
|
|
158
|
+
or type(transport.port) is not int
|
|
159
|
+
or not 0 < transport.port < 65536
|
|
160
|
+
or type(transport.support_tls) is not bool
|
|
161
|
+
or not isinstance(transport.path, str)
|
|
162
|
+
or not transport.path.startswith("/")
|
|
163
|
+
or not isinstance(transport.graph_id, str)
|
|
164
|
+
):
|
|
165
|
+
raise ValueError("LangGraphTransport requires host, valid port and absolute path")
|
|
166
|
+
protocol = "https" if transport.support_tls else "http"
|
|
167
|
+
metadata = {
|
|
168
|
+
META_GRAPH_ID: transport.graph_id.strip() or service,
|
|
169
|
+
META_PATH: transport.path,
|
|
170
|
+
META_PROTOCOL: protocol,
|
|
171
|
+
}
|
|
172
|
+
request = InstanceRegisterRequest(
|
|
173
|
+
namespace=self.namespace,
|
|
174
|
+
service=service,
|
|
175
|
+
service_token=self.service_token,
|
|
176
|
+
host=transport.host.strip(),
|
|
177
|
+
port=transport.port,
|
|
178
|
+
version=version,
|
|
179
|
+
protocol=protocol,
|
|
180
|
+
metadata=metadata,
|
|
181
|
+
ttl=self.ttl,
|
|
182
|
+
auto_heartbeat=True,
|
|
183
|
+
)
|
|
184
|
+
request.validate()
|
|
185
|
+
requests.append(request)
|
|
186
|
+
return requests
|
|
187
|
+
|
|
188
|
+
def _deregister(self, instances):
|
|
189
|
+
for instance in instances:
|
|
190
|
+
request = InstanceDeRegisterRequest(
|
|
191
|
+
namespace=self.namespace,
|
|
192
|
+
service=instance.service,
|
|
193
|
+
service_token=self.service_token,
|
|
194
|
+
host=instance.host,
|
|
195
|
+
port=instance.port,
|
|
196
|
+
instance_id=instance.instance_id,
|
|
197
|
+
)
|
|
198
|
+
try:
|
|
199
|
+
self._provider.deregister_instance(request)
|
|
200
|
+
except Exception as error:
|
|
201
|
+
self.logger.warning_throttled(
|
|
202
|
+
("deregister", instance.service, instance.host, instance.port),
|
|
203
|
+
"Failed to deregister LangGraph service %s at %s:%s: %s",
|
|
204
|
+
instance.service,
|
|
205
|
+
instance.host,
|
|
206
|
+
instance.port,
|
|
207
|
+
error,
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
self._registered = [item for item in self._registered if item is not instance]
|
|
211
|
+
|
|
212
|
+
async def async_register(
|
|
213
|
+
self,
|
|
214
|
+
service: str,
|
|
215
|
+
transports: Sequence[LangGraphTransport],
|
|
216
|
+
*,
|
|
217
|
+
version: str = "",
|
|
218
|
+
):
|
|
219
|
+
cancelled = Event()
|
|
220
|
+
task = asyncio.create_task(
|
|
221
|
+
asyncio.to_thread(self._register, service, tuple(transports), version, cancelled)
|
|
222
|
+
)
|
|
223
|
+
try:
|
|
224
|
+
return await asyncio.shield(task)
|
|
225
|
+
except asyncio.CancelledError:
|
|
226
|
+
cancelled.set()
|
|
227
|
+
await drain_task(task)
|
|
228
|
+
if not task.cancelled() and task.exception() is None:
|
|
229
|
+
cleanup = asyncio.create_task(asyncio.to_thread(self._rollback, task.result()))
|
|
230
|
+
await drain_task(cleanup)
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
def _rollback(self, instances):
|
|
234
|
+
with self._lock:
|
|
235
|
+
self._deregister(instances)
|
|
236
|
+
|
|
237
|
+
def close(self):
|
|
238
|
+
"""Retry deregistration for every instance still tracked by this registrar."""
|
|
239
|
+
with self._lock:
|
|
240
|
+
self._closed = True
|
|
241
|
+
self._deregister(tuple(self._registered))
|
|
242
|
+
|
|
243
|
+
async def async_close(self):
|
|
244
|
+
await await_completion(asyncio.to_thread(self.close))
|
|
245
|
+
|
|
246
|
+
def __enter__(self):
|
|
247
|
+
self._check_open()
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def __exit__(self, *_):
|
|
251
|
+
self.close()
|
|
252
|
+
|
|
253
|
+
async def __aenter__(self):
|
|
254
|
+
self._check_open()
|
|
255
|
+
return self
|
|
256
|
+
|
|
257
|
+
async def __aexit__(self, *_):
|
|
258
|
+
await self.async_close()
|