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,464 @@
|
|
|
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
|
+
"""Read-only published and mounted Skill loaders for AgentScope."""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import io
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
import stat
|
|
23
|
+
import tempfile
|
|
24
|
+
import time
|
|
25
|
+
import zipfile
|
|
26
|
+
from collections.abc import Sequence
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path, PurePosixPath
|
|
29
|
+
from threading import RLock
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import frontmatter
|
|
33
|
+
from agentscope.skill import Skill, SkillLoaderBase
|
|
34
|
+
except ImportError as error:
|
|
35
|
+
raise ImportError(
|
|
36
|
+
"PolarisSkillLoader requires pip install 'polaris-python-ai[agentscope]'."
|
|
37
|
+
) from error
|
|
38
|
+
|
|
39
|
+
from polaris.api.consumer import ConsumerAPI
|
|
40
|
+
from polaris.api.skill import SkillAPI
|
|
41
|
+
from polaris.pkg.model.error import ServerCode
|
|
42
|
+
from polaris.pkg.model.service import ExtendedMetadata, GetAllInstancesRequest
|
|
43
|
+
from polaris.pkg.model.skill import SkillDownloadRequest, SkillListRequest
|
|
44
|
+
|
|
45
|
+
from polaris_ai._async import await_completion
|
|
46
|
+
from polaris_ai.log import get_ai_logger
|
|
47
|
+
|
|
48
|
+
_MAX_ARCHIVE_BYTES = 64 * 1024 * 1024
|
|
49
|
+
_MAX_ARCHIVE_FILES = 2048
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class PolarisSkillRepositoryInfo:
|
|
54
|
+
"""Read-only repository identity exposed to AgentScope applications."""
|
|
55
|
+
|
|
56
|
+
type: str
|
|
57
|
+
location: str
|
|
58
|
+
writeable: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PolarisSkillNotFoundError(LookupError):
|
|
62
|
+
"""The requested Skill does not exist in the selected Polaris repository."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PolarisSkillLoader(SkillLoaderBase):
|
|
66
|
+
"""Load published skills, or only skills mounted on ``service_name``.
|
|
67
|
+
|
|
68
|
+
The supplied SDKContext is borrowed. Extracted assets live until ``close``;
|
|
69
|
+
persistent offline backups remain the responsibility of the SDK SkillAPI.
|
|
70
|
+
``refresh_interval`` is in seconds and request ``timeout`` is in milliseconds.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
sdk_context,
|
|
76
|
+
namespace: str = "default",
|
|
77
|
+
*,
|
|
78
|
+
service_name: str = "",
|
|
79
|
+
version: str = "",
|
|
80
|
+
names: Sequence[str] = (),
|
|
81
|
+
list_limit: int = 50,
|
|
82
|
+
max_skills: int = 100,
|
|
83
|
+
refresh_interval: float = 30.0,
|
|
84
|
+
timeout: int = 0,
|
|
85
|
+
logger=None,
|
|
86
|
+
):
|
|
87
|
+
if sdk_context is None or sdk_context.is_destroy():
|
|
88
|
+
raise ValueError("An active SDKContext is required")
|
|
89
|
+
if (type(list_limit) is not int or list_limit <= 0
|
|
90
|
+
or type(max_skills) is not int or max_skills <= 0):
|
|
91
|
+
raise ValueError("list_limit and max_skills must be positive integers")
|
|
92
|
+
if not isinstance(refresh_interval, (int, float)) or refresh_interval < 0:
|
|
93
|
+
raise ValueError("refresh_interval must be nonnegative")
|
|
94
|
+
if type(timeout) is not int or timeout < 0:
|
|
95
|
+
raise ValueError("timeout must be a nonnegative integer")
|
|
96
|
+
if isinstance(names, str) or any(not isinstance(n, str) or not n.strip() for n in names):
|
|
97
|
+
raise ValueError("names must be a sequence of nonempty skill names")
|
|
98
|
+
if service_name and names:
|
|
99
|
+
raise ValueError("names cannot be combined with service_name")
|
|
100
|
+
self.namespace = namespace.strip() or "default"
|
|
101
|
+
self.service_name = service_name.strip()
|
|
102
|
+
self.version = version.strip()
|
|
103
|
+
self.names = tuple(n.strip() for n in names)
|
|
104
|
+
self.list_limit = list_limit
|
|
105
|
+
self.max_skills = max_skills
|
|
106
|
+
self.refresh_interval = refresh_interval
|
|
107
|
+
self.timeout = timeout
|
|
108
|
+
self.logger = logger if logger is not None else get_ai_logger("skill")
|
|
109
|
+
self._context = sdk_context
|
|
110
|
+
self._skill_api = SkillAPI(sdk_context)
|
|
111
|
+
self._consumer = ConsumerAPI(sdk_context)
|
|
112
|
+
self._refs: tuple[tuple[str, str], ...] = ()
|
|
113
|
+
self._refreshed_at: float | None = None
|
|
114
|
+
self._cache: dict[tuple[str, str], Skill] = {}
|
|
115
|
+
self._lock = asyncio.Lock()
|
|
116
|
+
self._files_lock = RLock()
|
|
117
|
+
self._directory: tempfile.TemporaryDirectory | None = None
|
|
118
|
+
self._closed = False
|
|
119
|
+
|
|
120
|
+
def _check_open(self):
|
|
121
|
+
if self._closed or self._context.is_destroy():
|
|
122
|
+
raise RuntimeError("Skill loader or SDKContext is closed")
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def repository_type(self) -> str:
|
|
126
|
+
return "polaris-mounted" if self.service_name else "polaris"
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def source(self) -> str:
|
|
130
|
+
if self.service_name:
|
|
131
|
+
return f"polaris-mounted:{self.namespace}/{self.service_name}"
|
|
132
|
+
return f"polaris:{self.namespace}"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def location(self) -> str:
|
|
136
|
+
if self.service_name:
|
|
137
|
+
return f"namespace:{self.namespace}/{self.service_name}"
|
|
138
|
+
return f"namespace:{self.namespace}"
|
|
139
|
+
|
|
140
|
+
def is_writeable(self) -> bool:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def get_repository_info(self) -> PolarisSkillRepositoryInfo:
|
|
144
|
+
return PolarisSkillRepositoryInfo(
|
|
145
|
+
type=self.repository_type,
|
|
146
|
+
location=self.location,
|
|
147
|
+
writeable=self.is_writeable(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def list_skills(self) -> list[Skill]:
|
|
151
|
+
"""Return native AgentScope Skills; failed downloads are skipped and retried later."""
|
|
152
|
+
async with self._lock:
|
|
153
|
+
self._check_open()
|
|
154
|
+
self.logger.debug(
|
|
155
|
+
"Listing Polaris Skills: namespace=%s mounted=%s",
|
|
156
|
+
self.namespace, bool(self.service_name),
|
|
157
|
+
)
|
|
158
|
+
await self._refresh_refs()
|
|
159
|
+
self._check_open()
|
|
160
|
+
result = []
|
|
161
|
+
for name, version in self._refs:
|
|
162
|
+
try:
|
|
163
|
+
result.append(await self._load_skill(name, version))
|
|
164
|
+
except Exception as error:
|
|
165
|
+
self._check_open()
|
|
166
|
+
self.logger.warning_throttled(
|
|
167
|
+
("load", name, version),
|
|
168
|
+
"Failed to load skill %s/%s: %s", self.namespace, name, error,
|
|
169
|
+
)
|
|
170
|
+
self.logger.debug(
|
|
171
|
+
"Listed Polaris Skills: namespace=%s requested=%d loaded=%d",
|
|
172
|
+
self.namespace, len(self._refs), len(result),
|
|
173
|
+
)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
async def async_get_skill(self, name: str, *, version: str | None = None) -> Skill:
|
|
177
|
+
"""Load one skill; mounted loaders only accept names in the current mount table."""
|
|
178
|
+
if not isinstance(name, str) or not name.strip():
|
|
179
|
+
raise ValueError("Skill name is required")
|
|
180
|
+
name = name.strip()
|
|
181
|
+
async with self._lock:
|
|
182
|
+
self._check_open()
|
|
183
|
+
resolved = self.version if version is None else version.strip()
|
|
184
|
+
self.logger.debug(
|
|
185
|
+
"Loading Polaris Skill: namespace=%s name=%s version=%s",
|
|
186
|
+
self.namespace, name, resolved,
|
|
187
|
+
)
|
|
188
|
+
if self.service_name:
|
|
189
|
+
await self._refresh_refs()
|
|
190
|
+
mounts = dict(self._refs)
|
|
191
|
+
if name not in mounts:
|
|
192
|
+
raise LookupError(f"Skill {name!r} is not mounted on {self.service_name!r}")
|
|
193
|
+
resolved = mounts[name]
|
|
194
|
+
return await self._load_skill(name, resolved)
|
|
195
|
+
|
|
196
|
+
async def async_list_skill_names(self) -> list[str]:
|
|
197
|
+
"""Refresh repository references and return names without downloading ZIPs."""
|
|
198
|
+
async with self._lock:
|
|
199
|
+
self._check_open()
|
|
200
|
+
await self._refresh_refs()
|
|
201
|
+
self._check_open()
|
|
202
|
+
return [name for name, _ in self._refs]
|
|
203
|
+
|
|
204
|
+
async def async_skill_exists(self, name: str) -> bool:
|
|
205
|
+
"""Check whether a Skill is available in the selected repository."""
|
|
206
|
+
if not isinstance(name, str) or not name.strip():
|
|
207
|
+
return False
|
|
208
|
+
name = name.strip()
|
|
209
|
+
if self.service_name:
|
|
210
|
+
async with self._lock:
|
|
211
|
+
self._check_open()
|
|
212
|
+
await self._refresh_refs()
|
|
213
|
+
self._check_open()
|
|
214
|
+
return name in dict(self._refs)
|
|
215
|
+
try:
|
|
216
|
+
await self.async_get_skill(name)
|
|
217
|
+
except Exception:
|
|
218
|
+
return False
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
async def _refresh_refs(self):
|
|
222
|
+
previous = self._refs
|
|
223
|
+
if self.names:
|
|
224
|
+
refs = tuple((name, self.version) for name in self.names)
|
|
225
|
+
if refs != previous:
|
|
226
|
+
self.logger.info(
|
|
227
|
+
"Configured Polaris Skill references changed: namespace=%s count=%d",
|
|
228
|
+
self.namespace, len(refs),
|
|
229
|
+
)
|
|
230
|
+
self.logger.debug("Configured Polaris Skill references: %s", refs)
|
|
231
|
+
self._refs = refs
|
|
232
|
+
return
|
|
233
|
+
if (self._refreshed_at is not None
|
|
234
|
+
and time.monotonic() - self._refreshed_at < self.refresh_interval
|
|
235
|
+
and (not self.service_name or self._refs)):
|
|
236
|
+
self.logger.debug(
|
|
237
|
+
"Reusing Polaris Skill reference cache: namespace=%s count=%d",
|
|
238
|
+
self.namespace, len(self._refs),
|
|
239
|
+
)
|
|
240
|
+
return
|
|
241
|
+
refs = await self._mounted_refs() if self.service_name else await self._published_refs()
|
|
242
|
+
self._check_open()
|
|
243
|
+
self._refs = tuple(refs)
|
|
244
|
+
self._refreshed_at = time.monotonic()
|
|
245
|
+
if self._refs != previous:
|
|
246
|
+
self.logger.info(
|
|
247
|
+
"Polaris Skill references changed: namespace=%s mounted=%s before=%d after=%d",
|
|
248
|
+
self.namespace, bool(self.service_name), len(previous), len(self._refs),
|
|
249
|
+
)
|
|
250
|
+
self.logger.debug("Polaris Skill references: %s", self._refs)
|
|
251
|
+
|
|
252
|
+
async def _published_refs(self):
|
|
253
|
+
refs = []
|
|
254
|
+
offset = 0
|
|
255
|
+
while len(refs) < self.max_skills:
|
|
256
|
+
response = await self._skill_api.async_list_skills(SkillListRequest(
|
|
257
|
+
namespace=self.namespace, offset=offset, limit=self.list_limit, timeout=self.timeout,
|
|
258
|
+
))
|
|
259
|
+
if response is None or response.code not in (0, 200000):
|
|
260
|
+
raise RuntimeError("Failed to list skills from Polaris")
|
|
261
|
+
resources = response.resources
|
|
262
|
+
self.logger.debug(
|
|
263
|
+
"Listed Polaris Skill page: namespace=%s offset=%d limit=%d "
|
|
264
|
+
"total=%s returned=%d",
|
|
265
|
+
self.namespace, offset, self.list_limit,
|
|
266
|
+
getattr(response, "total", None), len(resources or ()),
|
|
267
|
+
)
|
|
268
|
+
if not resources:
|
|
269
|
+
break
|
|
270
|
+
for resource in resources:
|
|
271
|
+
if resource is None or not resource.name.strip():
|
|
272
|
+
continue
|
|
273
|
+
active = resource.version_info.active_version if resource.version_info else ""
|
|
274
|
+
refs.append((resource.name, self.version or active or ""))
|
|
275
|
+
if len(refs) >= self.max_skills:
|
|
276
|
+
break
|
|
277
|
+
offset += self.list_limit
|
|
278
|
+
if len(resources) < self.list_limit or offset >= response.total:
|
|
279
|
+
break
|
|
280
|
+
return refs
|
|
281
|
+
|
|
282
|
+
async def _mounted_refs(self):
|
|
283
|
+
response = await self._consumer.async_get_all_instances(GetAllInstancesRequest(
|
|
284
|
+
namespace=self.namespace, service=self.service_name, timeout=self.timeout,
|
|
285
|
+
))
|
|
286
|
+
refs = {}
|
|
287
|
+
if response is None:
|
|
288
|
+
return []
|
|
289
|
+
for meta in response.get_extended_metadata():
|
|
290
|
+
if meta.type != ExtendedMetadata.SKILL or meta.agent_skill is None:
|
|
291
|
+
continue
|
|
292
|
+
skill = meta.agent_skill
|
|
293
|
+
raw = skill.name if skill.name.strip() else skill.id
|
|
294
|
+
namespace, separator, name = raw.partition(":")
|
|
295
|
+
if not separator or not namespace.strip() or not name.strip():
|
|
296
|
+
self.logger.warning_throttled(
|
|
297
|
+
("invalid-mount", raw),
|
|
298
|
+
"Invalid mounted skill identity: %r", raw,
|
|
299
|
+
)
|
|
300
|
+
continue
|
|
301
|
+
if namespace.strip() != self.namespace:
|
|
302
|
+
self.logger.warning_throttled(
|
|
303
|
+
("mount-namespace", raw),
|
|
304
|
+
"Skipping mounted skill %s outside namespace %s", raw, self.namespace,
|
|
305
|
+
)
|
|
306
|
+
continue
|
|
307
|
+
refs.setdefault(name.strip(), skill.version.strip() or self.version)
|
|
308
|
+
return list(refs.items())
|
|
309
|
+
|
|
310
|
+
async def _load_skill(self, name, version):
|
|
311
|
+
self._check_open()
|
|
312
|
+
fixed_key = (name, version) if version else None
|
|
313
|
+
if fixed_key is not None and fixed_key in self._cache:
|
|
314
|
+
self.logger.debug(
|
|
315
|
+
"Using cached Polaris Skill: name=%s version=%s", name, version,
|
|
316
|
+
)
|
|
317
|
+
return self._cache[fixed_key]
|
|
318
|
+
self.logger.debug(
|
|
319
|
+
"Downloading Polaris Skill: namespace=%s name=%s version=%s",
|
|
320
|
+
self.namespace, name, version or "<active>",
|
|
321
|
+
)
|
|
322
|
+
response = await self._skill_api.async_download_skill(SkillDownloadRequest(
|
|
323
|
+
namespace=self.namespace, name=name, version=version, timeout=self.timeout, format="zip",
|
|
324
|
+
))
|
|
325
|
+
if response is None:
|
|
326
|
+
raise RuntimeError(f"Failed to download skill {name!r}")
|
|
327
|
+
if response.code == ServerCode.NotFoundResource:
|
|
328
|
+
raise PolarisSkillNotFoundError(
|
|
329
|
+
f"Skill {name!r} was not found in {self.namespace!r}"
|
|
330
|
+
)
|
|
331
|
+
if response.code not in (0, ServerCode.ExecuteSuccess):
|
|
332
|
+
detail = f", info={response.info!r}" if response.info else ""
|
|
333
|
+
raise RuntimeError(
|
|
334
|
+
f"Failed to download skill {name!r} (code={response.code}{detail})"
|
|
335
|
+
)
|
|
336
|
+
if not response.data:
|
|
337
|
+
raise RuntimeError(f"Failed to download skill {name!r} (empty ZIP)")
|
|
338
|
+
actual_version = response.version if isinstance(response.version, str) else ""
|
|
339
|
+
actual_version = actual_version.strip()
|
|
340
|
+
self.logger.debug(
|
|
341
|
+
"Downloaded Polaris Skill: name=%s requestedVersion=%s actualVersion=%s bytes=%d",
|
|
342
|
+
name, version or "<active>", actual_version or "<unknown>", len(response.data),
|
|
343
|
+
)
|
|
344
|
+
if fixed_key is not None:
|
|
345
|
+
cache_key = fixed_key
|
|
346
|
+
else:
|
|
347
|
+
cache_key = (name, actual_version) if actual_version else None
|
|
348
|
+
if cache_key is not None and cache_key in self._cache:
|
|
349
|
+
self.logger.debug(
|
|
350
|
+
"Reusing materialized Polaris Skill: name=%s version=%s",
|
|
351
|
+
name, actual_version or version,
|
|
352
|
+
)
|
|
353
|
+
return self._cache[cache_key]
|
|
354
|
+
# Extraction is isolated from the event loop and close cannot race filesystem writes.
|
|
355
|
+
return await await_completion(asyncio.to_thread(
|
|
356
|
+
self._materialize, cache_key, response.data,
|
|
357
|
+
))
|
|
358
|
+
|
|
359
|
+
def _materialize(self, key, data):
|
|
360
|
+
with self._files_lock:
|
|
361
|
+
self._check_open()
|
|
362
|
+
if key is not None and key in self._cache:
|
|
363
|
+
return self._cache[key]
|
|
364
|
+
if self._directory is None:
|
|
365
|
+
self._directory = tempfile.TemporaryDirectory(prefix="polaris-ai-skills-")
|
|
366
|
+
directory = Path(tempfile.mkdtemp(dir=self._directory.name))
|
|
367
|
+
try:
|
|
368
|
+
root = _extract_archive(data, directory)
|
|
369
|
+
markdown_path = root / "SKILL.md"
|
|
370
|
+
content = frontmatter.loads(markdown_path.read_text(encoding="utf-8-sig"))
|
|
371
|
+
name = content.get("name")
|
|
372
|
+
description = content.get("description")
|
|
373
|
+
if not name or not description:
|
|
374
|
+
raise ValueError("SKILL.md requires name and description front matter")
|
|
375
|
+
skill = Skill(
|
|
376
|
+
name=str(name), description=str(description), dir=str(root),
|
|
377
|
+
markdown=content.content, updated_at=markdown_path.stat().st_mtime,
|
|
378
|
+
)
|
|
379
|
+
if key is not None:
|
|
380
|
+
self._cache[key] = skill
|
|
381
|
+
self.logger.debug(
|
|
382
|
+
"Materialized Polaris Skill: name=%s directory=%s", skill.name, skill.dir,
|
|
383
|
+
)
|
|
384
|
+
return skill
|
|
385
|
+
except BaseException:
|
|
386
|
+
shutil.rmtree(directory)
|
|
387
|
+
raise
|
|
388
|
+
|
|
389
|
+
def close(self):
|
|
390
|
+
"""Remove this loader's temporary assets without destroying the borrowed SDKContext."""
|
|
391
|
+
with self._files_lock:
|
|
392
|
+
self._closed = True
|
|
393
|
+
self.logger.debug(
|
|
394
|
+
"Closing Polaris Skill loader: namespace=%s cached=%d",
|
|
395
|
+
self.namespace, len(self._cache),
|
|
396
|
+
)
|
|
397
|
+
self._cache.clear()
|
|
398
|
+
self._refs = ()
|
|
399
|
+
if self._directory is not None:
|
|
400
|
+
self._directory.cleanup()
|
|
401
|
+
self._directory = None
|
|
402
|
+
|
|
403
|
+
async def async_close(self):
|
|
404
|
+
await await_completion(self._close_async())
|
|
405
|
+
|
|
406
|
+
async def _close_async(self):
|
|
407
|
+
async with self._lock:
|
|
408
|
+
await asyncio.to_thread(self.close)
|
|
409
|
+
|
|
410
|
+
def __enter__(self):
|
|
411
|
+
self._check_open()
|
|
412
|
+
return self
|
|
413
|
+
|
|
414
|
+
def __exit__(self, *_):
|
|
415
|
+
self.close()
|
|
416
|
+
|
|
417
|
+
async def __aenter__(self):
|
|
418
|
+
self._check_open()
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
async def __aexit__(self, *_):
|
|
422
|
+
await self.async_close()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _extract_archive(data: bytes, directory: Path) -> Path:
|
|
426
|
+
if len(data) > _MAX_ARCHIVE_BYTES:
|
|
427
|
+
raise ValueError("Skill ZIP exceeds the 64 MiB limit")
|
|
428
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
429
|
+
entries = archive.infolist()
|
|
430
|
+
if len(entries) > _MAX_ARCHIVE_FILES:
|
|
431
|
+
raise ValueError("Skill ZIP exceeds the 2048 entry limit")
|
|
432
|
+
if sum(entry.file_size for entry in entries) > _MAX_ARCHIVE_BYTES:
|
|
433
|
+
raise ValueError("Expanded Skill ZIP exceeds the 64 MiB limit")
|
|
434
|
+
paths = []
|
|
435
|
+
seen = set()
|
|
436
|
+
for entry in entries:
|
|
437
|
+
raw = entry.orig_filename
|
|
438
|
+
path = PurePosixPath(raw)
|
|
439
|
+
mode = stat.S_IFMT(entry.external_attr >> 16)
|
|
440
|
+
if ("\x00" in raw or "\\" in raw or path.is_absolute() or ".." in path.parts
|
|
441
|
+
or re.match(r"^[A-Za-z]:", raw) or not path.parts
|
|
442
|
+
or mode not in (0, stat.S_IFREG, stat.S_IFDIR)):
|
|
443
|
+
raise ValueError(f"Unsafe Skill ZIP entry: {raw!r}")
|
|
444
|
+
canonical = path.as_posix().casefold()
|
|
445
|
+
if canonical in seen:
|
|
446
|
+
raise ValueError(f"Duplicate Skill ZIP entry: {raw!r}")
|
|
447
|
+
seen.add(canonical)
|
|
448
|
+
paths.append((entry, path))
|
|
449
|
+
definitions = [path for entry, path in paths
|
|
450
|
+
if not entry.is_dir() and path.name == "SKILL.md"]
|
|
451
|
+
if len(definitions) != 1 or len(definitions[0].parts) > 2:
|
|
452
|
+
raise ValueError("Skill ZIP must contain one root or top-level directory SKILL.md")
|
|
453
|
+
root = definitions[0].parent
|
|
454
|
+
if root.parts and any(not path.is_relative_to(root) for _, path in paths):
|
|
455
|
+
raise ValueError("All Skill ZIP assets must be under the same skill root")
|
|
456
|
+
for entry, path in paths:
|
|
457
|
+
target = directory.joinpath(*path.parts)
|
|
458
|
+
if entry.is_dir():
|
|
459
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
else:
|
|
461
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
462
|
+
with archive.open(entry) as source, target.open("xb") as output:
|
|
463
|
+
shutil.copyfileobj(source, output)
|
|
464
|
+
return directory.joinpath(*root.parts)
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
"""Polaris registration, discovery, and RemoteGraph routing for LangGraph."""
|
|
17
|
+
|
|
18
|
+
from polaris_ai._version import __version__
|
|
19
|
+
from .config import (
|
|
20
|
+
PolarisLangGraphConfig,
|
|
21
|
+
PolarisLangGraphDiscoveryConfig,
|
|
22
|
+
PolarisLangGraphRegistryConfig,
|
|
23
|
+
)
|
|
24
|
+
from .context import PolarisLangGraphContext
|
|
25
|
+
from .registry import (
|
|
26
|
+
LangGraphTransport,
|
|
27
|
+
PolarisLangGraphRegistrar,
|
|
28
|
+
RegisteredInstance,
|
|
29
|
+
)
|
|
30
|
+
from .remote import PolarisRemoteGraph
|
|
31
|
+
from .resolver import (
|
|
32
|
+
LangGraphEndpoint,
|
|
33
|
+
LangGraphEndpointNotFoundError,
|
|
34
|
+
PolarisLangGraphResolver,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"LangGraphEndpoint",
|
|
39
|
+
"LangGraphEndpointNotFoundError",
|
|
40
|
+
"LangGraphTransport",
|
|
41
|
+
"PolarisLangGraphConfig",
|
|
42
|
+
"PolarisLangGraphContext",
|
|
43
|
+
"PolarisLangGraphDiscoveryConfig",
|
|
44
|
+
"PolarisLangGraphRegistrar",
|
|
45
|
+
"PolarisLangGraphRegistryConfig",
|
|
46
|
+
"PolarisLangGraphResolver",
|
|
47
|
+
"PolarisRemoteGraph",
|
|
48
|
+
"RegisteredInstance",
|
|
49
|
+
"__version__",
|
|
50
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
"""Application options for Polaris-managed LangGraph deployments."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _bool_env(value: str, *, default: bool) -> bool:
|
|
25
|
+
lowered = value.strip().lower()
|
|
26
|
+
if lowered in ("1", "true", "yes", "on"):
|
|
27
|
+
return True
|
|
28
|
+
if lowered in ("0", "false", "no", "off"):
|
|
29
|
+
return False
|
|
30
|
+
if not lowered:
|
|
31
|
+
return default
|
|
32
|
+
raise ValueError(f"Invalid boolean environment value: {value!r}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _list_env(value: str) -> tuple[str, ...]:
|
|
36
|
+
return tuple(item.strip() for item in value.split(",") if item.strip())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _int_env(value: str, name: str, default: int) -> int:
|
|
40
|
+
if not value:
|
|
41
|
+
return default
|
|
42
|
+
try:
|
|
43
|
+
return int(value)
|
|
44
|
+
except ValueError as error:
|
|
45
|
+
raise ValueError(f"{name} must be an integer") from error
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _headers_env(value: str) -> tuple[tuple[str, str], ...]:
|
|
49
|
+
headers = []
|
|
50
|
+
for item in _list_env(value):
|
|
51
|
+
name, separator, header_value = item.partition("=")
|
|
52
|
+
if not separator or not name.strip():
|
|
53
|
+
raise ValueError("POLARIS_AI_LANGGRAPH_HEADERS items must be name=value")
|
|
54
|
+
headers.append((name.strip(), header_value.strip()))
|
|
55
|
+
return tuple(headers)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class PolarisLangGraphRegistryConfig:
|
|
60
|
+
enabled: bool = True
|
|
61
|
+
ttl: int = 5
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class PolarisLangGraphDiscoveryConfig:
|
|
66
|
+
enabled: bool = True
|
|
67
|
+
timeout: int = 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class PolarisLangGraphConfig:
|
|
72
|
+
"""Code-first settings for registration, discovery, and RemoteGraph calls."""
|
|
73
|
+
|
|
74
|
+
enabled: bool = True
|
|
75
|
+
namespace: str = "default"
|
|
76
|
+
addresses: tuple[str, ...] = ("127.0.0.1:8091",)
|
|
77
|
+
connector_token: str = ""
|
|
78
|
+
service_token: str = ""
|
|
79
|
+
graph_id: str = ""
|
|
80
|
+
service: str = ""
|
|
81
|
+
path: str = "/"
|
|
82
|
+
protocol: str = "http"
|
|
83
|
+
api_key: str = ""
|
|
84
|
+
headers: tuple[tuple[str, str], ...] = ()
|
|
85
|
+
caller_service: str = ""
|
|
86
|
+
labels: str = ""
|
|
87
|
+
registry: PolarisLangGraphRegistryConfig = field(
|
|
88
|
+
default_factory=PolarisLangGraphRegistryConfig,
|
|
89
|
+
)
|
|
90
|
+
discovery: PolarisLangGraphDiscoveryConfig = field(
|
|
91
|
+
default_factory=PolarisLangGraphDiscoveryConfig,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_env(
|
|
96
|
+
cls,
|
|
97
|
+
environ: dict[str, str] | None = None,
|
|
98
|
+
*,
|
|
99
|
+
graph_id: str = "",
|
|
100
|
+
service: str = "",
|
|
101
|
+
) -> "PolarisLangGraphConfig":
|
|
102
|
+
"""Load LangGraph options with ``POLARIS_AI_*`` precedence."""
|
|
103
|
+
env = os.environ if environ is None else environ
|
|
104
|
+
|
|
105
|
+
def value(name: str, fallback: str = "") -> str:
|
|
106
|
+
return env.get(name, env.get(fallback, "")).strip()
|
|
107
|
+
|
|
108
|
+
configured_graph = value("POLARIS_AI_LANGGRAPH_GRAPH_ID") or graph_id.strip()
|
|
109
|
+
configured_service = value("POLARIS_AI_LANGGRAPH_SERVICE") or service.strip()
|
|
110
|
+
addresses = _list_env(
|
|
111
|
+
value("POLARIS_AI_LANGGRAPH_ADDRESS", "POLARIS_SERVER_ADDRESS")
|
|
112
|
+
) or _list_env(
|
|
113
|
+
value("POLARIS_AI_LANGGRAPH_ADDRESSES", "POLARIS_SERVER_ADDRESSES")
|
|
114
|
+
) or _list_env(value("POLARIS_AI_ADDRESS", "POLARIS_SERVER_ADDRESS"))
|
|
115
|
+
return cls(
|
|
116
|
+
enabled=_bool_env(env.get("POLARIS_AI_LANGGRAPH_ENABLED", ""), default=True),
|
|
117
|
+
namespace=(
|
|
118
|
+
value("POLARIS_AI_LANGGRAPH_NAMESPACE", "POLARIS_NAMESPACE")
|
|
119
|
+
or "default"
|
|
120
|
+
),
|
|
121
|
+
addresses=addresses or ("127.0.0.1:8091",),
|
|
122
|
+
connector_token=value(
|
|
123
|
+
"POLARIS_AI_LANGGRAPH_CONNECTOR_TOKEN",
|
|
124
|
+
"POLARIS_TOKEN",
|
|
125
|
+
),
|
|
126
|
+
service_token=value(
|
|
127
|
+
"POLARIS_AI_LANGGRAPH_SERVICE_TOKEN",
|
|
128
|
+
"POLARIS_SERVICE_TOKEN",
|
|
129
|
+
),
|
|
130
|
+
graph_id=configured_graph,
|
|
131
|
+
service=configured_service or configured_graph,
|
|
132
|
+
path=value("POLARIS_AI_LANGGRAPH_PATH") or "/",
|
|
133
|
+
protocol=value("POLARIS_AI_LANGGRAPH_PROTOCOL") or "http",
|
|
134
|
+
api_key=value("POLARIS_AI_LANGGRAPH_API_KEY", "LANGGRAPH_API_KEY"),
|
|
135
|
+
headers=_headers_env(value("POLARIS_AI_LANGGRAPH_HEADERS")),
|
|
136
|
+
caller_service=value("POLARIS_AI_LANGGRAPH_CALLER_SERVICE"),
|
|
137
|
+
labels=value("POLARIS_AI_LANGGRAPH_LABELS"),
|
|
138
|
+
registry=PolarisLangGraphRegistryConfig(
|
|
139
|
+
enabled=_bool_env(
|
|
140
|
+
env.get("POLARIS_AI_LANGGRAPH_REGISTRY_ENABLED", ""),
|
|
141
|
+
default=True,
|
|
142
|
+
),
|
|
143
|
+
ttl=_int_env(
|
|
144
|
+
value("POLARIS_AI_LANGGRAPH_TTL"),
|
|
145
|
+
"POLARIS_AI_LANGGRAPH_TTL",
|
|
146
|
+
5,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
discovery=PolarisLangGraphDiscoveryConfig(
|
|
150
|
+
enabled=_bool_env(
|
|
151
|
+
env.get("POLARIS_AI_LANGGRAPH_DISCOVERY_ENABLED", ""),
|
|
152
|
+
default=True,
|
|
153
|
+
),
|
|
154
|
+
timeout=_int_env(
|
|
155
|
+
value("POLARIS_AI_LANGGRAPH_DISCOVERY_TIMEOUT"),
|
|
156
|
+
"POLARIS_AI_LANGGRAPH_DISCOVERY_TIMEOUT",
|
|
157
|
+
0,
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
)
|