Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Supported Nacos version over 3.x
from v2.nacos import NacosNamingService, NacosConfigService, NacosAIService, ClientConfigBuilder, GRPCConfig, \
Instance, SubscribeServiceParam, RegisterInstanceParam, DeregisterInstanceParam, \
BatchRegisterInstanceParam, GetServiceParam, ListServiceParam, ListInstanceParam, ConfigParam
from v2.nacos.ai.model.ai_param import GetPromptParam, SubscribePromptParam, DownloadSkillParam

client_config = (ClientConfigBuilder()
.access_key(os.getenv('NACOS_ACCESS_KEY'))
Expand Down Expand Up @@ -323,6 +324,12 @@ client_config = (ClientConfigBuilder()
ai_client = await NacosAIService.create_ai_service(client_config)
```

**Transport Modes:**

- **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.
- **Skill download** always uses HTTP, regardless of gRPC availability.
- **MCP Server / Agent Card** management uses gRPC.

### MCP Server Management

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

### Prompt Management

Nacos provides prompt template management capabilities, including retrieval, subscription, and rendering with variable substitution.

#### Get Prompt

```python
from v2.nacos.ai.model.ai_param import GetPromptParam

prompt = await ai_client.get_prompt(
GetPromptParam(prompt_key='my-prompt', version='1.0.0')
)
print(prompt.template)
```

* `param` *GetPromptParam* Parameter for retrieving prompt information.
* `prompt_key` - Key of the prompt to query (required).
* `version` - Version of the prompt (optional).
* `label` - Label of the prompt (optional).
* `return` Prompt if success or an exception will be raised.

#### Render Prompt with Variables

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.

```python
# Render the prompt template with variable substitution
result = prompt.render({"name": "Alice", "place": "Nacos"})
print(result) # e.g. "Hello Alice, welcome to Nacos!"

# Variables with defaultValue will be used automatically if not overridden
# For example, if the prompt has a variable: PromptVariable(name="lang", defaultValue="en")
# Calling render without providing "lang" will use "en" as the value
result = prompt.render({"name": "Alice"})
```

* `param` *variables* - A dict of variable name to value mappings (optional). Overrides default values defined in `PromptVariable.defaultValue`.
* `return` Rendered string with all `{{variableName}}` placeholders replaced.

#### Subscribe Prompt

```python
from v2.nacos.ai.model.ai_param import SubscribePromptParam

async def prompt_listener(prompt_key, prompt):
print(f"Prompt changed: {prompt_key}, version: {prompt.version}")

prompt = await ai_client.subscribe_prompt(
SubscribePromptParam(
prompt_key='my-prompt',
version='1.0.0',
subscribe_callback=prompt_listener
)
)
```

* `param` *SubscribePromptParam* Parameter for subscribing to prompt changes.
* `prompt_key` - Key of the prompt to subscribe to (required).
* `version` - Version of the prompt (optional).
* `label` - Label of the prompt (optional).
* `subscribe_callback` - Callback function to handle prompt changes (required).
* `return` Current Prompt if success or an exception will be raised.

#### Unsubscribe Prompt

```python
await ai_client.unsubscribe_prompt(
SubscribePromptParam(
prompt_key='my-prompt',
version='1.0.0',
subscribe_callback=prompt_listener
)
)
```

### Skill Download

Nacos supports downloading skill packages as ZIP archives.

#### Download Skill ZIP

```python
from v2.nacos.ai.model.ai_param import DownloadSkillParam

zip_bytes = await ai_client.download_skill_zip(
DownloadSkillParam(skill_name='my-skill', version='1.0.0')
)

# Save to file
with open('my-skill.zip', 'wb') as f:
f.write(zip_bytes)
```

* `param` *DownloadSkillParam* Parameter for downloading a skill ZIP.
* `skill_name` - Name of the skill (required).
* `version` - Target skill version (optional, defaults to latest).
* `label` - Target skill label, e.g. "latest", "stable" (optional).
* `return` ZIP file content as bytes if success or an exception will be raised.

### Stop AI Client

```python
Expand Down
106 changes: 106 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Python 3.10+
from v2.nacos import NacosNamingService, NacosConfigService, NacosAIService, ClientConfigBuilder, GRPCConfig, \
Instance, SubscribeServiceParam, RegisterInstanceParam, DeregisterInstanceParam, \
BatchRegisterInstanceParam, GetServiceParam, ListServiceParam, ListInstanceParam, ConfigParam
from v2.nacos.ai.model.ai_param import GetPromptParam, SubscribePromptParam, DownloadSkillParam

client_config = (ClientConfigBuilder()
.access_key(os.getenv('NACOS_ACCESS_KEY'))
Expand Down Expand Up @@ -319,6 +320,12 @@ client_config = (ClientConfigBuilder()
ai_client = await NacosAIService.create_ai_service(client_config)
```

**传输模式说明:**

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

### MCP Server 管理

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

### Prompt 管理

Nacos 提供了 Prompt 模板管理能力,包括获取、订阅和变量替换渲染。

#### 获取 Prompt

```python
from v2.nacos.ai.model.ai_param import GetPromptParam

prompt = await ai_client.get_prompt(
GetPromptParam(prompt_key='my-prompt', version='1.0.0')
)
print(prompt.template)
```

* `param` *GetPromptParam* 获取 Prompt 信息的参数
* `prompt_key` - 要查询的 Prompt 键名(必填)
* `version` - Prompt 版本(可选)
* `label` - Prompt 标签(可选)
* `return` 成功时返回 Prompt,失败时抛出异常

#### 使用变量渲染 Prompt

`Prompt` 对象支持使用 `{{variableName}}` 占位符进行模板渲染。Prompt 中定义的变量可以通过 `PromptVariable.defaultValue` 包含默认值。渲染时,先应用默认值,然后被用户提供的值覆盖。

```python
# 使用变量替换渲染 Prompt 模板
result = prompt.render({"name": "Alice", "place": "Nacos"})
print(result) # e.g. "Hello Alice, welcome to Nacos!"

# 如果未覆盖,将自动使用带有 defaultValue 的变量
# 例如,如果 Prompt 有一个变量:PromptVariable(name="lang", defaultValue="en")
# 调用 render 时不提供 "lang" 将使用 "en" 作为值
result = prompt.render({"name": "Alice"})
```

* `param` *variables* - 变量名到值的映射字典(可选)。覆盖 `PromptVariable.defaultValue` 中定义的默认值。
* `return` 替换所有 `{{variableName}}` 占位符后的渲染字符串。

#### 订阅 Prompt

```python
from v2.nacos.ai.model.ai_param import SubscribePromptParam

async def prompt_listener(prompt_key, prompt):
print(f"Prompt changed: {prompt_key}, version: {prompt.version}")

prompt = await ai_client.subscribe_prompt(
SubscribePromptParam(
prompt_key='my-prompt',
version='1.0.0',
subscribe_callback=prompt_listener
)
)
```

* `param` *SubscribePromptParam* 订阅 Prompt 变化的参数
* `prompt_key` - 要订阅的 Prompt 键名(必填)
* `version` - Prompt 版本(可选)
* `label` - Prompt 标签(可选)
* `subscribe_callback` - 处理 Prompt 变化的回调函数(必填)
* `return` 成功时返回当前 Prompt,失败时抛出异常

#### 取消订阅 Prompt

```python
await ai_client.unsubscribe_prompt(
SubscribePromptParam(
prompt_key='my-prompt',
version='1.0.0',
subscribe_callback=prompt_listener
)
)
```

### 技能下载

Nacos 支持以 ZIP 压缩包的形式下载技能包。

#### 下载技能 ZIP

```python
from v2.nacos.ai.model.ai_param import DownloadSkillParam

zip_bytes = await ai_client.download_skill_zip(
DownloadSkillParam(skill_name='my-skill', version='1.0.0')
)

# 保存到文件
with open('my-skill.zip', 'wb') as f:
f.write(zip_bytes)
```

* `param` *DownloadSkillParam* 下载技能 ZIP 的参数
* `skill_name` - 技能名称(必填)
* `version` - 目标技能版本(可选,默认为最新版本)
* `label` - 目标技能标签,例如 "latest"、"stable"(可选)
* `return` 成功时返回 ZIP 文件内容(bytes),失败时抛出异常

### 停止 AI 客户端

```python
Expand Down
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ protobuf>=3.20.3
psutil>=5.9.5
pycryptodome>=3.19.1
pydantic>=2.10.4
a2a>=0.44
a2a-sdk>=0.3.20
a2a-sdk>=0.3.20,<1.0.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def run(self):

setup(
name="nacos-sdk-python",
version="3.2.0b1",
version="3.2.0",
packages=find_packages(
exclude=["test", "*.tests", "*.tests.*", "tests.*", "tests"]),
url="https://github.com/nacos-group/nacos-sdk-python",
Expand Down
40 changes: 37 additions & 3 deletions test/client_v2_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import os
import unittest
from typing import List
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock

from v2.nacos import ConfigParam
Expand Down Expand Up @@ -240,8 +239,6 @@ async def test_auth_login_url_with_standard_context_path(self):
[Regression Test] Verifies that when context_path is '/nacos',
the login URL correctly includes the prefix.
"""
import logging

# 1. Setup config with standard context path
config = ClientConfig(
server_addresses="http://127.0.0.1:8848",
Expand Down Expand Up @@ -278,5 +275,42 @@ async def test_auth_login_url_with_standard_context_path(self):
self.assertEqual(called_url, expected_url,
f"URL mismatch for standard context_path. Expected '{expected_url}', but got '{called_url}'")


class TestClientConfigContextPathNormalization(unittest.TestCase):
"""Unit tests for ClientConfig.context_path normalization (Issue #300 follow-up)."""

def test_empty_string_falls_back_to_default(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="")
self.assertEqual(cfg.context_path, "/nacos")

def test_none_falls_back_to_default(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path=None)
self.assertEqual(cfg.context_path, "/nacos")

def test_default_value(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848")
self.assertEqual(cfg.context_path, "/nacos")

def test_missing_leading_slash_is_added(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="nacos")
self.assertEqual(cfg.context_path, "/nacos")

def test_trailing_slash_is_stripped(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/nacos/")
self.assertEqual(cfg.context_path, "/nacos")

def test_root_is_preserved(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/")
self.assertEqual(cfg.context_path, "/")

def test_build_context_prefix_for_root(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/")
self.assertEqual(cfg.build_context_prefix(), "")

def test_build_context_prefix_for_standard(self):
cfg = ClientConfig(server_addresses="http://127.0.0.1:8848", context_path="/nacos")
self.assertEqual(cfg.build_context_prefix(), "/nacos")


if __name__ == '__main__':
unittest.main()
Loading
Loading