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,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()