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,221 @@
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-side configuration for Polaris AgentScope integrations."""
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 _text_env(value: str) -> str:
36
+ return value.strip()
37
+
38
+
39
+ def _list_env(value: str) -> tuple[str, ...]:
40
+ return tuple(item.strip() for item in value.split(",") if item.strip())
41
+
42
+
43
+ def _int_env(value: str, name: str, default: int) -> int:
44
+ if not value:
45
+ return default
46
+ try:
47
+ return int(value)
48
+ except ValueError as error:
49
+ raise ValueError(f"{name} must be an integer") from error
50
+
51
+
52
+ def _float_env(value: str, name: str, default: float) -> float:
53
+ if not value:
54
+ return default
55
+ try:
56
+ return float(value)
57
+ except ValueError as error:
58
+ raise ValueError(f"{name} must be a number") from error
59
+
60
+
61
+ def _headers_env(value: str) -> tuple[tuple[str, str], ...]:
62
+ result = []
63
+ for item in _list_env(value):
64
+ name, separator, header_value = item.partition("=")
65
+ if not separator or not name.strip():
66
+ raise ValueError("POLARIS_AI_MCP_HEADERS items must be name=value")
67
+ result.append((name.strip(), header_value.strip()))
68
+ return tuple(result)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class PolarisSkillConfig:
73
+ """Read-only Skill repository options."""
74
+
75
+ enabled: bool = True
76
+ addresses: tuple[str, ...] = ()
77
+ names: tuple[str, ...] = ()
78
+ version: str = ""
79
+ list_limit: int = 50
80
+ max_skills: int = 100
81
+ refresh_interval: float = 30.0
82
+ mounted_enabled: bool = False
83
+ mounted_service_name: str = ""
84
+ attach_to_agent: bool = True
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class PolarisA2ARegistryConfig:
89
+ enabled: bool = True
90
+ ttl: int = 5
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class PolarisA2ADiscoveryConfig:
95
+ enabled: bool = True
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class PolarisA2AConfig:
100
+ enabled: bool = True
101
+ registry: PolarisA2ARegistryConfig = field(default_factory=PolarisA2ARegistryConfig)
102
+ discovery: PolarisA2ADiscoveryConfig = field(default_factory=PolarisA2ADiscoveryConfig)
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class PolarisMCPConfig:
107
+ enabled: bool = False
108
+ protocol_version: str = ""
109
+ scheme: str = ""
110
+ timeout: float = 30.0
111
+ filter_contract: bool = True
112
+ headers: tuple[tuple[str, str], ...] = ()
113
+ caller_service: str = ""
114
+ labels: str = ""
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class PolarisAgentConfig:
119
+ """Code-first options for an application; not part of polaris.yaml."""
120
+
121
+ enabled: bool = True
122
+ namespace: str = "default"
123
+ addresses: tuple[str, ...] = ("127.0.0.1:8091",)
124
+ connector_token: str = ""
125
+ service_token: str = ""
126
+ skill: PolarisSkillConfig = field(default_factory=PolarisSkillConfig)
127
+ a2a: PolarisA2AConfig = field(default_factory=PolarisA2AConfig)
128
+ mcp: PolarisMCPConfig = field(default_factory=PolarisMCPConfig)
129
+
130
+ @classmethod
131
+ def from_env(
132
+ cls,
133
+ environ: dict[str, str] | None = None,
134
+ *,
135
+ agent_name: str = "",
136
+ ) -> "PolarisAgentConfig":
137
+ """Load application options with explicit code and SDK env fallbacks.
138
+
139
+ `POLARIS_AI_*` always takes precedence. Existing `POLARIS_*` variables are
140
+ read only as fallbacks so this helper can coexist with SDK examples.
141
+ """
142
+ env = os.environ if environ is None else environ
143
+
144
+ def value(name: str, fallback: str = "") -> str:
145
+ return env.get(name, env.get(fallback, "")).strip()
146
+
147
+ addresses = _list_env(
148
+ value("POLARIS_AI_ADDRESS", "POLARIS_SERVER_ADDRESS")
149
+ or value("POLARIS_AI_ADDRESSES", "POLARIS_SERVER_ADDRESSES")
150
+ ) or ("127.0.0.1:8091",)
151
+ skill_addresses = _list_env(
152
+ value("POLARIS_AI_SKILL_ADDRESS", "POLARIS_SKILL_SERVER_ADDRESS")
153
+ )
154
+
155
+ mounted_service = value("POLARIS_AI_MOUNTED_SKILL_SERVICE")
156
+ if not mounted_service:
157
+ mounted_service = value("POLARIS_AI_A2A_CARD_NAME")
158
+ if not mounted_service:
159
+ mounted_service = value("POLARIS_AI_AGENT_NAME") or agent_name.strip()
160
+
161
+ skill = PolarisSkillConfig(
162
+ enabled=_bool_env(env.get("POLARIS_AI_SKILL_ENABLED", ""), default=True),
163
+ addresses=skill_addresses,
164
+ names=_list_env(value("POLARIS_AI_SKILL_NAMES")),
165
+ version=value("POLARIS_AI_SKILL_VERSION"),
166
+ list_limit=_int_env(
167
+ value("POLARIS_AI_SKILL_LIST_LIMIT"), "POLARIS_AI_SKILL_LIST_LIMIT", 50
168
+ ),
169
+ max_skills=_int_env(
170
+ value("POLARIS_AI_SKILL_MAX_SKILLS"), "POLARIS_AI_SKILL_MAX_SKILLS", 100
171
+ ),
172
+ refresh_interval=_float_env(
173
+ value("POLARIS_AI_SKILL_LIST_REFRESH_INTERVAL_SECONDS"),
174
+ "POLARIS_AI_SKILL_LIST_REFRESH_INTERVAL_SECONDS",
175
+ 30.0,
176
+ ),
177
+ mounted_enabled=_bool_env(
178
+ env.get("POLARIS_AI_MOUNTED_SKILL_ENABLED", ""), default=False
179
+ ),
180
+ mounted_service_name=mounted_service,
181
+ attach_to_agent=_bool_env(
182
+ env.get("POLARIS_AI_SKILL_ATTACH_TO_AGENT", ""), default=True
183
+ ),
184
+ )
185
+ a2a = PolarisA2AConfig(
186
+ enabled=_bool_env(env.get("POLARIS_AI_A2A_ENABLED", ""), default=True),
187
+ registry=PolarisA2ARegistryConfig(
188
+ enabled=_bool_env(
189
+ env.get("POLARIS_AI_A2A_REGISTRY_ENABLED", ""), default=True
190
+ ),
191
+ ttl=_int_env(value("POLARIS_AI_A2A_TTL"), "POLARIS_AI_A2A_TTL", 5),
192
+ ),
193
+ discovery=PolarisA2ADiscoveryConfig(
194
+ enabled=_bool_env(
195
+ env.get("POLARIS_AI_A2A_DISCOVERY_ENABLED", ""), default=True
196
+ ),
197
+ ),
198
+ )
199
+ headers = _headers_env(value("POLARIS_AI_MCP_HEADERS"))
200
+ mcp = PolarisMCPConfig(
201
+ enabled=_bool_env(env.get("POLARIS_AI_MCP_ENABLED", ""), default=False),
202
+ protocol_version=value("POLARIS_AI_MCP_PROTOCOL_VERSION"),
203
+ scheme=value("POLARIS_AI_MCP_SCHEME"),
204
+ timeout=_float_env(value("POLARIS_AI_MCP_TIMEOUT"), "POLARIS_AI_MCP_TIMEOUT", 30.0),
205
+ filter_contract=_bool_env(
206
+ env.get("POLARIS_AI_MCP_FILTER_CONTRACT", ""), default=True
207
+ ),
208
+ headers=headers,
209
+ caller_service=value("POLARIS_AI_MCP_CALLER_SERVICE"),
210
+ labels=value("POLARIS_AI_MCP_LABELS"),
211
+ )
212
+ return cls(
213
+ enabled=_bool_env(env.get("POLARIS_AI_ENABLED", ""), default=True),
214
+ namespace=value("POLARIS_AI_NAMESPACE", "default") or "default",
215
+ addresses=addresses,
216
+ connector_token=value("POLARIS_AI_CONNECTOR_TOKEN", "POLARIS_TOKEN"),
217
+ service_token=value("POLARIS_AI_SERVICE_TOKEN", "POLARIS_SERVICE_TOKEN"),
218
+ skill=skill,
219
+ a2a=a2a,
220
+ mcp=mcp,
221
+ )
@@ -0,0 +1,195 @@
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
+ """Shared Polaris connection factory for AgentScope 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.api.skill import SkillAPI
26
+ from polaris.pkg.config.api import Configuration
27
+
28
+ from polaris_ai._async import await_completion
29
+
30
+ DEFAULT_DISCOVERY_ADDRESS = "127.0.0.1:8091"
31
+ SKILL_ADDRESS_PORT = 8094
32
+
33
+
34
+ def _validate_address(address: str) -> str:
35
+ if not isinstance(address, str) or not address.strip():
36
+ raise ValueError("Polaris addresses must be nonempty host:port values")
37
+ address = address.strip()
38
+ host, separator, port_text = address.rpartition(":")
39
+ if not separator or not host:
40
+ raise ValueError(f"Invalid Polaris address {address!r}: a port is required")
41
+ try:
42
+ port = int(port_text)
43
+ except ValueError as error:
44
+ raise ValueError(f"Invalid Polaris address {address!r}: port must be an integer") from error
45
+ if not 0 < port < 65536:
46
+ raise ValueError(f"Invalid Polaris address {address!r}: port must be in [1, 65535]")
47
+ if host.startswith("[") and host.endswith("]"):
48
+ host = host[1:-1]
49
+ if not host:
50
+ raise ValueError(f"Invalid Polaris address {address!r}: empty IPv6 host")
51
+ elif host.count(":") > 0:
52
+ raise ValueError(f"Invalid Polaris address {address!r}: IPv6 hosts require brackets")
53
+ if not host.strip():
54
+ raise ValueError(f"Invalid Polaris address {address!r}: host is empty")
55
+ return f"[{host}]:{port}" if ":" in host else f"{host}:{port}"
56
+
57
+
58
+ def derive_skill_addresses(addresses: Iterable[str]) -> list[str]:
59
+ """Map discovery endpoints to the standard Skill endpoint on each host."""
60
+ result = []
61
+ for address in addresses:
62
+ normalized = _validate_address(address)
63
+ host, _, _ = normalized.rpartition(":")
64
+ skill_address = f"{host}:{SKILL_ADDRESS_PORT}"
65
+ if skill_address not in result:
66
+ result.append(skill_address)
67
+ if not result:
68
+ raise ValueError("At least one discovery address is required")
69
+ return result
70
+
71
+
72
+ class PolarisAgentContext:
73
+ """Expose shared Provider, Consumer and Skill APIs over one SDK context.
74
+
75
+ A supplied context is borrowed and is never modified or destroyed by this
76
+ factory. When no context is supplied, the factory owns the context it
77
+ creates, disables stat reporting, and derives Skill addresses from the
78
+ discovery addresses. Like Java's PolarisContextManager, it also disables
79
+ the provider lossless lifecycle for this owned context.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ namespace: str = "default",
85
+ *,
86
+ address: str | Iterable[str] = DEFAULT_DISCOVERY_ADDRESS,
87
+ connector_token: str = "",
88
+ skill_address: str | Iterable[str] = "",
89
+ log_options: LogOptions | None = None,
90
+ sdk_context=None,
91
+ ):
92
+ if sdk_context is not None:
93
+ if sdk_context.is_destroy():
94
+ raise ValueError("An active SDKContext is required")
95
+ self._owns_context = False
96
+ self._context = sdk_context
97
+ else:
98
+ if isinstance(address, str):
99
+ addresses = [address]
100
+ else:
101
+ addresses = list(address)
102
+ if not addresses:
103
+ raise ValueError("At least one discovery address is required")
104
+ addresses = [_validate_address(item) for item in addresses]
105
+ if isinstance(skill_address, str):
106
+ skill_addresses = [] if not skill_address else [skill_address]
107
+ else:
108
+ skill_addresses = list(skill_address)
109
+ skill_addresses = [_validate_address(item) for item in skill_addresses]
110
+ if not skill_addresses:
111
+ skill_addresses = derive_skill_addresses(addresses)
112
+
113
+ config = Configuration()
114
+ config.set_default()
115
+ config.get_global().get_server_connector().addresses = addresses
116
+ config.get_global().get_server_connector().token = connector_token
117
+ config.get_global().get_stat_reporter().enable = False
118
+ config.get_provider().get_lossless().enable = False
119
+ config.get_ai().get_skill().get_server_connector().addresses = skill_addresses
120
+ self._owns_context = True
121
+ self._context = None
122
+ try:
123
+ self._context = init_context_by_config(config, log_options=log_options)
124
+ except BaseException:
125
+ if self._context is not None:
126
+ self._context.destroy()
127
+ raise
128
+
129
+ self.namespace = namespace.strip() or "default"
130
+ self._closed = False
131
+ try:
132
+ self.provider = ProviderAPI(self._context)
133
+ self.consumer = ConsumerAPI(self._context)
134
+ self.skill = SkillAPI(self._context)
135
+ except BaseException:
136
+ self.close()
137
+ raise
138
+
139
+ @classmethod
140
+ def from_address(
141
+ cls,
142
+ address: str | Iterable[str] = DEFAULT_DISCOVERY_ADDRESS,
143
+ namespace: str = "default",
144
+ *,
145
+ connector_token: str = "",
146
+ skill_address: str | Iterable[str] = "",
147
+ log_options: LogOptions | None = None,
148
+ ) -> "PolarisAgentContext":
149
+ return cls(
150
+ namespace,
151
+ address=address,
152
+ connector_token=connector_token,
153
+ skill_address=skill_address,
154
+ log_options=log_options,
155
+ )
156
+
157
+ @property
158
+ def context(self):
159
+ return self._context
160
+
161
+ @property
162
+ def owns_context(self) -> bool:
163
+ return self._owns_context
164
+
165
+ def _check_open(self):
166
+ if self._closed or self._context is None or self._context.is_destroy():
167
+ raise RuntimeError("PolarisAgentContext is closed")
168
+
169
+ def close(self):
170
+ """Close owned resources; borrowed contexts remain untouched."""
171
+ if self._closed:
172
+ return
173
+ self._closed = True
174
+ self.provider = None
175
+ self.consumer = None
176
+ self.skill = None
177
+ if self._owns_context and self._context is not None:
178
+ self._context.destroy()
179
+
180
+ async def async_close(self):
181
+ await await_completion(asyncio.to_thread(self.close))
182
+
183
+ def __enter__(self):
184
+ self._check_open()
185
+ return self
186
+
187
+ def __exit__(self, *_):
188
+ self.close()
189
+
190
+ async def __aenter__(self):
191
+ self._check_open()
192
+ return self
193
+
194
+ async def __aexit__(self, *_):
195
+ await self.async_close()