Skip to content

Commit 26e1f57

Browse files
authored
feat(ai): add Prompt variables and Skill download support, align with Java SDK, update version to 3.2.0 (#322)
* feat(ai): add Prompt variables and Skill download support, align with Java SDK - Add PromptVariable model with defaultValue support - Enhance Prompt.render() to merge default values from variables - Add Skill download module (model, util, HTTP endpoint) - Add HTTP variables parsing in AiHttpClientProxy - Add Client-Version/User-Agent headers to HTTP requests - Change RpcClient.start() to not throw on gRPC connect failure - Normalize context_path handling with build_context_prefix() - Unify exception types to NacosException in skill download - Bump version to 3.2.0 - Update README/README_CN with Prompt and Skill documentation - Add 32 unit tests for Prompt variables and Skill utilities * docs(ai): add transport mode description for AI Client
1 parent b5e0c70 commit 26e1f57

20 files changed

Lines changed: 896 additions & 35 deletions

README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Supported Nacos version over 3.x
3131
from v2.nacos import NacosNamingService, NacosConfigService, NacosAIService, ClientConfigBuilder, GRPCConfig, \
3232
Instance, SubscribeServiceParam, RegisterInstanceParam, DeregisterInstanceParam, \
3333
BatchRegisterInstanceParam, GetServiceParam, ListServiceParam, ListInstanceParam, ConfigParam
34+
from v2.nacos.ai.model.ai_param import GetPromptParam, SubscribePromptParam, DownloadSkillParam
3435

3536
client_config = (ClientConfigBuilder()
3637
.access_key(os.getenv('NACOS_ACCESS_KEY'))
@@ -323,6 +324,12 @@ client_config = (ClientConfigBuilder()
323324
ai_client = await NacosAIService.create_ai_service(client_config)
324325
```
325326

327+
**Transport Modes:**
328+
329+
- **Prompt** supports both gRPC and HTTP transport. By default, gRPC is used. If the gRPC port is unreachable, the AI client will still start normally (gRPC reconnects asynchronously in the background), and Prompt operations can fall back to HTTP.
330+
- **Skill download** always uses HTTP, regardless of gRPC availability.
331+
- **MCP Server / Agent Card** management uses gRPC.
332+
326333
### MCP Server Management
327334

328335
Nacos provides management capabilities for MCP (Model Context Protocol) Server, including registration, discovery, and subscription, supporting dynamic registration and service discovery of MCP servers.
@@ -549,6 +556,105 @@ await ai_client.unsubscribe_agent_card(
549556
)
550557
```
551558

559+
### Prompt Management
560+
561+
Nacos provides prompt template management capabilities, including retrieval, subscription, and rendering with variable substitution.
562+
563+
#### Get Prompt
564+
565+
```python
566+
from v2.nacos.ai.model.ai_param import GetPromptParam
567+
568+
prompt = await ai_client.get_prompt(
569+
GetPromptParam(prompt_key='my-prompt', version='1.0.0')
570+
)
571+
print(prompt.template)
572+
```
573+
574+
* `param` *GetPromptParam* Parameter for retrieving prompt information.
575+
* `prompt_key` - Key of the prompt to query (required).
576+
* `version` - Version of the prompt (optional).
577+
* `label` - Label of the prompt (optional).
578+
* `return` Prompt if success or an exception will be raised.
579+
580+
#### Render Prompt with Variables
581+
582+
The `Prompt` object supports template rendering with `{{variableName}}` placeholders. Variables defined in the prompt may include default values via `PromptVariable.defaultValue`. When rendering, default values are applied first, then overridden by user-provided values.
583+
584+
```python
585+
# Render the prompt template with variable substitution
586+
result = prompt.render({"name": "Alice", "place": "Nacos"})
587+
print(result) # e.g. "Hello Alice, welcome to Nacos!"
588+
589+
# Variables with defaultValue will be used automatically if not overridden
590+
# For example, if the prompt has a variable: PromptVariable(name="lang", defaultValue="en")
591+
# Calling render without providing "lang" will use "en" as the value
592+
result = prompt.render({"name": "Alice"})
593+
```
594+
595+
* `param` *variables* - A dict of variable name to value mappings (optional). Overrides default values defined in `PromptVariable.defaultValue`.
596+
* `return` Rendered string with all `{{variableName}}` placeholders replaced.
597+
598+
#### Subscribe Prompt
599+
600+
```python
601+
from v2.nacos.ai.model.ai_param import SubscribePromptParam
602+
603+
async def prompt_listener(prompt_key, prompt):
604+
print(f"Prompt changed: {prompt_key}, version: {prompt.version}")
605+
606+
prompt = await ai_client.subscribe_prompt(
607+
SubscribePromptParam(
608+
prompt_key='my-prompt',
609+
version='1.0.0',
610+
subscribe_callback=prompt_listener
611+
)
612+
)
613+
```
614+
615+
* `param` *SubscribePromptParam* Parameter for subscribing to prompt changes.
616+
* `prompt_key` - Key of the prompt to subscribe to (required).
617+
* `version` - Version of the prompt (optional).
618+
* `label` - Label of the prompt (optional).
619+
* `subscribe_callback` - Callback function to handle prompt changes (required).
620+
* `return` Current Prompt if success or an exception will be raised.
621+
622+
#### Unsubscribe Prompt
623+
624+
```python
625+
await ai_client.unsubscribe_prompt(
626+
SubscribePromptParam(
627+
prompt_key='my-prompt',
628+
version='1.0.0',
629+
subscribe_callback=prompt_listener
630+
)
631+
)
632+
```
633+
634+
### Skill Download
635+
636+
Nacos supports downloading skill packages as ZIP archives.
637+
638+
#### Download Skill ZIP
639+
640+
```python
641+
from v2.nacos.ai.model.ai_param import DownloadSkillParam
642+
643+
zip_bytes = await ai_client.download_skill_zip(
644+
DownloadSkillParam(skill_name='my-skill', version='1.0.0')
645+
)
646+
647+
# Save to file
648+
with open('my-skill.zip', 'wb') as f:
649+
f.write(zip_bytes)
650+
```
651+
652+
* `param` *DownloadSkillParam* Parameter for downloading a skill ZIP.
653+
* `skill_name` - Name of the skill (required).
654+
* `version` - Target skill version (optional, defaults to latest).
655+
* `label` - Target skill label, e.g. "latest", "stable" (optional).
656+
* `return` ZIP file content as bytes if success or an exception will be raised.
657+
552658
### Stop AI Client
553659

554660
```python

README_CN.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Python 3.10+
3131
from v2.nacos import NacosNamingService, NacosConfigService, NacosAIService, ClientConfigBuilder, GRPCConfig, \
3232
Instance, SubscribeServiceParam, RegisterInstanceParam, DeregisterInstanceParam, \
3333
BatchRegisterInstanceParam, GetServiceParam, ListServiceParam, ListInstanceParam, ConfigParam
34+
from v2.nacos.ai.model.ai_param import GetPromptParam, SubscribePromptParam, DownloadSkillParam
3435

3536
client_config = (ClientConfigBuilder()
3637
.access_key(os.getenv('NACOS_ACCESS_KEY'))
@@ -319,6 +320,12 @@ client_config = (ClientConfigBuilder()
319320
ai_client = await NacosAIService.create_ai_service(client_config)
320321
```
321322

323+
**传输模式说明:**
324+
325+
- **Prompt** 同时支持 gRPC 和 HTTP 两种传输模式。默认使用 gRPC。如果 gRPC 端口不可达,AI 客户端仍可正常创建(gRPC 会在后台异步重连),Prompt 操作可回退到 HTTP 模式。
326+
- **Skill 下载** 始终使用 HTTP,不依赖 gRPC 连接。
327+
- **MCP Server / Agent Card** 管理使用 gRPC。
328+
322329
### MCP Server 管理
323330

324331
Nacos 提供了对 MCP (Model Context Protocol) Server 的管理能力,包括注册、发现和订阅,支持 MCP Server 的动态注册和服务发现。
@@ -545,6 +552,105 @@ await ai_client.unsubscribe_agent_card(
545552
)
546553
```
547554

555+
### Prompt 管理
556+
557+
Nacos 提供了 Prompt 模板管理能力,包括获取、订阅和变量替换渲染。
558+
559+
#### 获取 Prompt
560+
561+
```python
562+
from v2.nacos.ai.model.ai_param import GetPromptParam
563+
564+
prompt = await ai_client.get_prompt(
565+
GetPromptParam(prompt_key='my-prompt', version='1.0.0')
566+
)
567+
print(prompt.template)
568+
```
569+
570+
* `param` *GetPromptParam* 获取 Prompt 信息的参数
571+
* `prompt_key` - 要查询的 Prompt 键名(必填)
572+
* `version` - Prompt 版本(可选)
573+
* `label` - Prompt 标签(可选)
574+
* `return` 成功时返回 Prompt,失败时抛出异常
575+
576+
#### 使用变量渲染 Prompt
577+
578+
`Prompt` 对象支持使用 `{{variableName}}` 占位符进行模板渲染。Prompt 中定义的变量可以通过 `PromptVariable.defaultValue` 包含默认值。渲染时,先应用默认值,然后被用户提供的值覆盖。
579+
580+
```python
581+
# 使用变量替换渲染 Prompt 模板
582+
result = prompt.render({"name": "Alice", "place": "Nacos"})
583+
print(result) # e.g. "Hello Alice, welcome to Nacos!"
584+
585+
# 如果未覆盖,将自动使用带有 defaultValue 的变量
586+
# 例如,如果 Prompt 有一个变量:PromptVariable(name="lang", defaultValue="en")
587+
# 调用 render 时不提供 "lang" 将使用 "en" 作为值
588+
result = prompt.render({"name": "Alice"})
589+
```
590+
591+
* `param` *variables* - 变量名到值的映射字典(可选)。覆盖 `PromptVariable.defaultValue` 中定义的默认值。
592+
* `return` 替换所有 `{{variableName}}` 占位符后的渲染字符串。
593+
594+
#### 订阅 Prompt
595+
596+
```python
597+
from v2.nacos.ai.model.ai_param import SubscribePromptParam
598+
599+
async def prompt_listener(prompt_key, prompt):
600+
print(f"Prompt changed: {prompt_key}, version: {prompt.version}")
601+
602+
prompt = await ai_client.subscribe_prompt(
603+
SubscribePromptParam(
604+
prompt_key='my-prompt',
605+
version='1.0.0',
606+
subscribe_callback=prompt_listener
607+
)
608+
)
609+
```
610+
611+
* `param` *SubscribePromptParam* 订阅 Prompt 变化的参数
612+
* `prompt_key` - 要订阅的 Prompt 键名(必填)
613+
* `version` - Prompt 版本(可选)
614+
* `label` - Prompt 标签(可选)
615+
* `subscribe_callback` - 处理 Prompt 变化的回调函数(必填)
616+
* `return` 成功时返回当前 Prompt,失败时抛出异常
617+
618+
#### 取消订阅 Prompt
619+
620+
```python
621+
await ai_client.unsubscribe_prompt(
622+
SubscribePromptParam(
623+
prompt_key='my-prompt',
624+
version='1.0.0',
625+
subscribe_callback=prompt_listener
626+
)
627+
)
628+
```
629+
630+
### 技能下载
631+
632+
Nacos 支持以 ZIP 压缩包的形式下载技能包。
633+
634+
#### 下载技能 ZIP
635+
636+
```python
637+
from v2.nacos.ai.model.ai_param import DownloadSkillParam
638+
639+
zip_bytes = await ai_client.download_skill_zip(
640+
DownloadSkillParam(skill_name='my-skill', version='1.0.0')
641+
)
642+
643+
# 保存到文件
644+
with open('my-skill.zip', 'wb') as f:
645+
f.write(zip_bytes)
646+
```
647+
648+
* `param` *DownloadSkillParam* 下载技能 ZIP 的参数
649+
* `skill_name` - 技能名称(必填)
650+
* `version` - 目标技能版本(可选,默认为最新版本)
651+
* `label` - 目标技能标签,例如 "latest"、"stable"(可选)
652+
* `return` 成功时返回 ZIP 文件内容(bytes),失败时抛出异常
653+
548654
### 停止 AI 客户端
549655

550656
```python

requirements.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@ protobuf>=3.20.3
77
psutil>=5.9.5
88
pycryptodome>=3.19.1
99
pydantic>=2.10.4
10-
a2a>=0.44
11-
a2a-sdk>=0.3.20
10+
a2a-sdk>=0.3.20,<1.0.0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def run(self):
5353

5454
setup(
5555
name="nacos-sdk-python",
56-
version="3.2.0b1",
56+
version="3.2.0",
5757
packages=find_packages(
5858
exclude=["test", "*.tests", "*.tests.*", "tests.*", "tests"]),
5959
url="https://github.com/nacos-group/nacos-sdk-python",

test/client_v2_test.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import os
33
import unittest
44
from typing import List
5-
from unittest.mock import AsyncMock
65
from unittest.mock import AsyncMock, MagicMock
76

87
from v2.nacos import ConfigParam
@@ -240,8 +239,6 @@ async def test_auth_login_url_with_standard_context_path(self):
240239
[Regression Test] Verifies that when context_path is '/nacos',
241240
the login URL correctly includes the prefix.
242241
"""
243-
import logging
244-
245242
# 1. Setup config with standard context path
246243
config = ClientConfig(
247244
server_addresses="http://127.0.0.1:8848",
@@ -278,5 +275,42 @@ async def test_auth_login_url_with_standard_context_path(self):
278275
self.assertEqual(called_url, expected_url,
279276
f"URL mismatch for standard context_path. Expected '{expected_url}', but got '{called_url}'")
280277

278+
279+
class TestClientConfigContextPathNormalization(unittest.TestCase):
280+
"""Unit tests for ClientConfig.context_path normalization (Issue #300 follow-up)."""
281+
282+
def test_empty_string_falls_back_to_default(self):
283+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="")
284+
self.assertEqual(cfg.context_path, "/nacos")
285+
286+
def test_none_falls_back_to_default(self):
287+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path=None)
288+
self.assertEqual(cfg.context_path, "/nacos")
289+
290+
def test_default_value(self):
291+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848")
292+
self.assertEqual(cfg.context_path, "/nacos")
293+
294+
def test_missing_leading_slash_is_added(self):
295+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="nacos")
296+
self.assertEqual(cfg.context_path, "/nacos")
297+
298+
def test_trailing_slash_is_stripped(self):
299+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/nacos/")
300+
self.assertEqual(cfg.context_path, "/nacos")
301+
302+
def test_root_is_preserved(self):
303+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/")
304+
self.assertEqual(cfg.context_path, "/")
305+
306+
def test_build_context_prefix_for_root(self):
307+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/")
308+
self.assertEqual(cfg.build_context_prefix(), "")
309+
310+
def test_build_context_prefix_for_standard(self):
311+
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/nacos")
312+
self.assertEqual(cfg.build_context_prefix(), "/nacos")
313+
314+
281315
if __name__ == '__main__':
282316
unittest.main()

0 commit comments

Comments
 (0)