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

@@ -14,6 +14,19 @@ from ..schemas import ToolResourceCreate, ToolResourceOut, ToolResourceUpdate
router = APIRouter(prefix="/tools", tags=["Tools & Autotest"])
TOOL_ID_ALIASES: Dict[str, str] = {
# legacy -> canonical
"voice_message_prompt": "voice_msg_prompt",
}
def normalize_tool_id(tool_id: Optional[str]) -> str:
raw = str(tool_id or "").strip()
if not raw:
return ""
return TOOL_ID_ALIASES.get(raw, raw)
# ============ Available Tools ============
TOOL_REGISTRY = {
"calculator": {
@@ -87,7 +100,7 @@ TOOL_REGISTRY = {
"required": []
}
},
"voice_message_prompt": {
"voice_msg_prompt": {
"name": "语音消息提示",
"description": "播报一条语音提示消息",
"parameters": {
@@ -180,7 +193,8 @@ TOOL_CATEGORY_MAP = {
"turn_off_camera": "system",
"increase_volume": "system",
"decrease_volume": "system",
"voice_message_prompt": "system",
"voice_msg_prompt": "system",
"voice_message_prompt": "system", # backward compatibility
"text_msg_prompt": "system",
"voice_choice_prompt": "system",
"text_choice_prompt": "system",
@@ -194,7 +208,8 @@ TOOL_ICON_MAP = {
"turn_off_camera": "CameraOff",
"increase_volume": "Volume2",
"decrease_volume": "Volume2",
"voice_message_prompt": "Volume2",
"voice_msg_prompt": "Volume2",
"voice_message_prompt": "Volume2", # backward compatibility
"text_msg_prompt": "Terminal",
"voice_choice_prompt": "Volume2",
"text_choice_prompt": "Terminal",
@@ -284,9 +299,49 @@ def _validate_query_http_config(*, category: str, tool_id: Optional[str], http_u
raise HTTPException(status_code=400, detail="http_url is required for query tools (except calculator/code_interpreter)")
def _migrate_legacy_system_tool_ids(db: Session) -> None:
"""Rename legacy built-in system tool IDs to their canonical IDs."""
changed = False
for legacy_id, canonical_id in TOOL_ID_ALIASES.items():
if legacy_id == canonical_id:
continue
legacy_item = (
db.query(ToolResource)
.filter(ToolResource.id == legacy_id)
.first()
)
if not legacy_item or not bool(legacy_item.is_system):
continue
canonical_item = (
db.query(ToolResource)
.filter(ToolResource.id == canonical_id)
.first()
)
if canonical_item:
db.delete(legacy_item)
changed = True
continue
legacy_item.id = canonical_id
legacy_item.updated_at = datetime.utcnow()
changed = True
if changed:
db.commit()
def _seed_default_tools_if_empty(db: Session) -> None:
"""Ensure built-in tools exist in tool_resources without overriding custom edits."""
_ensure_tool_resource_schema(db)
_migrate_legacy_system_tool_ids(db)
existing_system_count = (
db.query(ToolResource.id)
.filter(ToolResource.is_system.is_(True))
.count()
)
if existing_system_count > 0:
return
existing_ids = {
str(item[0])
for item in db.query(ToolResource.id).all()
@@ -335,9 +390,10 @@ def list_available_tools():
@router.get("/list/{tool_id}")
def get_tool_detail(tool_id: str):
"""获取工具详情"""
if tool_id not in TOOL_REGISTRY:
canonical_tool_id = normalize_tool_id(tool_id)
if canonical_tool_id not in TOOL_REGISTRY:
raise HTTPException(status_code=404, detail="Tool not found")
return TOOL_REGISTRY[tool_id]
return TOOL_REGISTRY[canonical_tool_id]
# ============ Tool Resource CRUD ============
@@ -369,6 +425,10 @@ def get_tool_resource(id: str, db: Session = Depends(get_db)):
"""获取单个工具资源详情。"""
_seed_default_tools_if_empty(db)
item = db.query(ToolResource).filter(ToolResource.id == id).first()
if not item:
canonical_id = normalize_tool_id(id)
if canonical_id and canonical_id != id:
item = db.query(ToolResource).filter(ToolResource.id == canonical_id).first()
if not item:
raise HTTPException(status_code=404, detail="Tool resource not found")
return item
@@ -378,7 +438,7 @@ def get_tool_resource(id: str, db: Session = Depends(get_db)):
def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)):
"""创建自定义工具资源。"""
_seed_default_tools_if_empty(db)
candidate_id = (data.id or "").strip()
candidate_id = normalize_tool_id((data.id or "").strip())
if candidate_id and db.query(ToolResource).filter(ToolResource.id == candidate_id).first():
raise HTTPException(status_code=400, detail="Tool ID already exists")
@@ -413,7 +473,10 @@ def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)
def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depends(get_db)):
"""更新工具资源。"""
_seed_default_tools_if_empty(db)
canonical_id = normalize_tool_id(id)
item = db.query(ToolResource).filter(ToolResource.id == id).first()
if not item and canonical_id and canonical_id != id:
item = db.query(ToolResource).filter(ToolResource.id == canonical_id).first()
if not item:
raise HTTPException(status_code=404, detail="Tool resource not found")
@@ -421,14 +484,14 @@ def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depend
new_category = update_data.get("category", item.category)
new_http_url = update_data.get("http_url", item.http_url)
_validate_query_http_config(category=new_category, tool_id=id, http_url=new_http_url)
_validate_query_http_config(category=new_category, tool_id=item.id, http_url=new_http_url)
if "http_method" in update_data:
update_data["http_method"] = _normalize_http_method(update_data.get("http_method"))
if "http_timeout_ms" in update_data and update_data.get("http_timeout_ms") is not None:
update_data["http_timeout_ms"] = max(1000, int(update_data["http_timeout_ms"]))
if "parameter_schema" in update_data:
update_data["parameter_schema"] = _normalize_parameter_schema(update_data.get("parameter_schema"), tool_id=id)
update_data["parameter_schema"] = _normalize_parameter_schema(update_data.get("parameter_schema"), tool_id=item.id)
if "parameter_defaults" in update_data:
update_data["parameter_defaults"] = _normalize_parameter_defaults(update_data.get("parameter_defaults"))
if new_category != "system":
@@ -447,7 +510,10 @@ def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depend
def delete_tool_resource(id: str, db: Session = Depends(get_db)):
"""删除工具资源。"""
_seed_default_tools_if_empty(db)
canonical_id = normalize_tool_id(id)
item = db.query(ToolResource).filter(ToolResource.id == id).first()
if not item and canonical_id and canonical_id != id:
item = db.query(ToolResource).filter(ToolResource.id == canonical_id).first()
if not item:
raise HTTPException(status_code=404, detail="Tool resource not found")
db.delete(item)