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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user