Add manual opener tool calls to Assistant model and API

- Introduced `manual_opener_tool_calls` field in the Assistant model to support custom tool calls.
- Updated AssistantBase and AssistantUpdate schemas to include the new field.
- Implemented normalization and migration logic for handling manual opener tool calls in the API.
- Enhanced runtime metadata to include manual opener tool calls in responses.
- Updated tests to validate the new functionality and ensure proper handling of tool calls.
- Refactored tool ID normalization to support legacy tool names for backward compatibility.
This commit is contained in:
Xin Wang
2026-03-02 12:34:42 +08:00
parent b5cdb76e52
commit 00b88c5afa
14 changed files with 806 additions and 74 deletions

View File

@@ -21,6 +21,7 @@ class TestAssistantAPI:
data = response.json()
assert data["name"] == sample_assistant_data["name"]
assert data["opener"] == sample_assistant_data["opener"]
assert data["manualOpenerToolCalls"] == []
assert data["prompt"] == sample_assistant_data["prompt"]
assert data["language"] == sample_assistant_data["language"]
assert data["voiceOutputEnabled"] is True
@@ -67,6 +68,9 @@ class TestAssistantAPI:
"prompt": "You are an updated assistant.",
"speed": 1.5,
"voiceOutputEnabled": False,
"manualOpenerToolCalls": [
{"toolName": "text_msg_prompt", "arguments": {"msg": "请选择服务类型"}}
],
}
response = client.put(f"/api/assistants/{assistant_id}", json=update_data)
assert response.status_code == 200
@@ -75,6 +79,9 @@ class TestAssistantAPI:
assert data["prompt"] == "You are an updated assistant."
assert data["speed"] == 1.5
assert data["voiceOutputEnabled"] is False
assert data["manualOpenerToolCalls"] == [
{"toolName": "text_msg_prompt", "arguments": {"msg": "请选择服务类型"}}
]
def test_delete_assistant(self, client, sample_assistant_data):
"""Test deleting an assistant"""
@@ -205,6 +212,7 @@ class TestAssistantAPI:
"voice": voice_id,
"prompt": "runtime prompt",
"opener": "runtime opener",
"manualOpenerToolCalls": [{"toolName": "text_msg_prompt", "arguments": {"msg": "欢迎"}}],
"speed": 1.1,
})
assistant_resp = client.post("/api/assistants", json=sample_assistant_data)
@@ -217,8 +225,10 @@ class TestAssistantAPI:
assert payload["assistantId"] == assistant_id
metadata = payload["sessionStartMetadata"]
assert metadata["systemPrompt"] == "runtime prompt"
assert metadata["systemPrompt"].startswith("runtime prompt")
assert "Tool usage policy:" in metadata["systemPrompt"]
assert metadata["greeting"] == "runtime opener"
assert metadata["manualOpenerToolCalls"] == [{"toolName": "text_msg_prompt", "arguments": {"msg": "欢迎"}}]
assert metadata["services"]["llm"]["model"] == sample_llm_model_data["model_name"]
assert metadata["services"]["asr"]["model"] == sample_asr_model_data["model_name"]
assert metadata["services"]["asr"]["baseUrl"] == sample_asr_model_data["base_url"]
@@ -239,8 +249,10 @@ class TestAssistantAPI:
assert payload["assistantId"] == assistant_id
assert payload["assistant"]["assistantId"] == assistant_id
assert payload["assistant"]["configVersionId"].startswith(f"asst_{assistant_id}_")
assert payload["assistant"]["systemPrompt"] == sample_assistant_data["prompt"]
assert payload["sessionStartMetadata"]["systemPrompt"] == sample_assistant_data["prompt"]
assert payload["assistant"]["systemPrompt"].startswith(sample_assistant_data["prompt"])
assert "Tool usage policy:" in payload["assistant"]["systemPrompt"]
assert payload["sessionStartMetadata"]["systemPrompt"].startswith(sample_assistant_data["prompt"])
assert "Tool usage policy:" in payload["sessionStartMetadata"]["systemPrompt"]
assert payload["sessionStartMetadata"]["history"]["assistantId"] == assistant_id
def test_runtime_config_resolves_selected_tools_into_runtime_definitions(self, client, sample_assistant_data):
@@ -263,6 +275,30 @@ class TestAssistantAPI:
assert by_name["calculator"]["function"]["parameters"]["type"] == "object"
assert "expression" in by_name["calculator"]["function"]["parameters"]["properties"]
def test_runtime_config_normalizes_legacy_voice_message_prompt_tool_id(self, client, sample_assistant_data):
sample_assistant_data["tools"] = ["voice_message_prompt"]
sample_assistant_data["manualOpenerToolCalls"] = [
{"toolName": "voice_message_prompt", "arguments": {"msg": "您好"}}
]
assistant_resp = client.post("/api/assistants", json=sample_assistant_data)
assert assistant_resp.status_code == 200
assistant_payload = assistant_resp.json()
assistant_id = assistant_payload["id"]
assert assistant_payload["tools"] == ["voice_msg_prompt"]
assert assistant_payload["manualOpenerToolCalls"] == [
{"toolName": "voice_msg_prompt", "arguments": {"msg": "您好"}}
]
runtime_resp = client.get(f"/api/assistants/{assistant_id}/runtime-config")
assert runtime_resp.status_code == 200
metadata = runtime_resp.json()["sessionStartMetadata"]
tools = metadata["tools"]
by_name = {item["function"]["name"]: item for item in tools}
assert "voice_msg_prompt" in by_name
assert metadata["manualOpenerToolCalls"] == [
{"toolName": "voice_msg_prompt", "arguments": {"msg": "您好"}}
]
def test_runtime_config_text_mode_when_voice_output_disabled(self, client, sample_assistant_data):
sample_assistant_data["voiceOutputEnabled"] = False
assistant_resp = client.post("/api/assistants", json=sample_assistant_data)

View File

@@ -21,6 +21,7 @@ class TestToolsAPI:
assert "turn_off_camera" in tools
assert "increase_volume" in tools
assert "decrease_volume" in tools
assert "voice_msg_prompt" in tools
assert "calculator" in tools
def test_get_tool_detail(self, client):
@@ -36,6 +37,14 @@ class TestToolsAPI:
response = client.get("/api/tools/list/non-existent-tool")
assert response.status_code == 404
def test_get_tool_detail_legacy_alias(self, client):
"""Legacy tool id should resolve to canonical tool detail."""
response = client.get("/api/tools/list/voice_message_prompt")
assert response.status_code == 200
data = response.json()
assert data["name"] == "语音消息提示"
assert "msg" in data["parameters"]["properties"]
def test_health_check(self, client):
"""Test health check endpoint"""
response = client.get("/api/tools/health")
@@ -281,6 +290,7 @@ class TestToolResourceCRUD:
assert payload["total"] >= 1
ids = [item["id"] for item in payload["list"]]
assert "calculator" in ids
assert "voice_msg_prompt" in ids
calculator = next((item for item in payload["list"] if item["id"] == "calculator"), None)
assert calculator is not None
assert calculator["parameter_schema"]["type"] == "object"