Implement runtime tool ID and display name mapping in DuplexPipeline. Enhance Assistants and ToolLibrary components to utilize new mappings for improved tool identification and display. Update DebugDrawer to reflect changes in tool display names during interactions.
This commit is contained in:
@@ -94,6 +94,17 @@ def _normalize_runtime_tool_schema(tool_id: str, raw_schema: Any) -> Dict[str, A
|
||||
return schema
|
||||
|
||||
|
||||
def _compose_runtime_system_prompt(base_prompt: Optional[str]) -> str:
|
||||
raw = str(base_prompt or "").strip()
|
||||
tool_policy = (
|
||||
"Tool usage policy:\n"
|
||||
"- Tool function names/IDs are internal and must never be shown to users.\n"
|
||||
"- When users ask which tools are available, describe capabilities in natural language.\n"
|
||||
"- Do not expose raw tool call payloads, IDs, or executor details."
|
||||
)
|
||||
return f"{raw}\n\n{tool_policy}" if raw else tool_policy
|
||||
|
||||
|
||||
def _resolve_runtime_tools(db: Session, selected_tool_ids: List[str], warnings: List[str]) -> List[Dict[str, Any]]:
|
||||
_ensure_tool_resource_schema(db)
|
||||
ids = [str(tool_id).strip() for tool_id in selected_tool_ids if str(tool_id).strip()]
|
||||
@@ -115,6 +126,11 @@ def _resolve_runtime_tools(db: Session, selected_tool_ids: List[str], warnings:
|
||||
continue
|
||||
|
||||
category = str(resource.category if resource else TOOL_CATEGORY_MAP.get(tool_id, "query"))
|
||||
display_name = (
|
||||
str(resource.name or tool_id).strip()
|
||||
if resource
|
||||
else str(TOOL_REGISTRY.get(tool_id, {}).get("name") or tool_id).strip()
|
||||
)
|
||||
description = (
|
||||
str(resource.description or resource.name or "").strip()
|
||||
if resource
|
||||
@@ -135,9 +151,15 @@ def _resolve_runtime_tools(db: Session, selected_tool_ids: List[str], warnings:
|
||||
"executor": "client" if category == "system" else "server",
|
||||
"function": {
|
||||
"name": tool_id,
|
||||
"description": description or tool_id,
|
||||
"description": (
|
||||
f"Display name: {display_name}. {description}".strip()
|
||||
if display_name
|
||||
else (description or tool_id)
|
||||
),
|
||||
"parameters": schema,
|
||||
},
|
||||
"displayName": display_name or tool_id,
|
||||
"toolId": tool_id,
|
||||
}
|
||||
if defaults:
|
||||
runtime_tool["defaultArgs"] = defaults
|
||||
@@ -149,7 +171,7 @@ def _resolve_runtime_tools(db: Session, selected_tool_ids: List[str], warnings:
|
||||
def _resolve_runtime_metadata(db: Session, assistant: Assistant) -> tuple[Dict[str, Any], List[str]]:
|
||||
warnings: List[str] = []
|
||||
metadata: Dict[str, Any] = {
|
||||
"systemPrompt": assistant.prompt or "",
|
||||
"systemPrompt": _compose_runtime_system_prompt(assistant.prompt),
|
||||
"firstTurnMode": assistant.first_turn_mode or "bot_first",
|
||||
"greeting": assistant.opener or "",
|
||||
"generatedOpenerEnabled": bool(assistant.generated_opener_enabled),
|
||||
|
||||
@@ -288,6 +288,8 @@ class DuplexPipeline:
|
||||
self._runtime_tools: List[Any] = list(raw_default_tools)
|
||||
self._runtime_tool_executor: Dict[str, str] = {}
|
||||
self._runtime_tool_default_args: Dict[str, Dict[str, Any]] = {}
|
||||
self._runtime_tool_id_map: Dict[str, str] = {}
|
||||
self._runtime_tool_display_names: Dict[str, str] = {}
|
||||
self._pending_tool_waiters: Dict[str, asyncio.Future] = {}
|
||||
self._early_tool_results: Dict[str, Dict[str, Any]] = {}
|
||||
self._completed_tool_call_ids: set[str] = set()
|
||||
@@ -309,6 +311,8 @@ class DuplexPipeline:
|
||||
|
||||
self._runtime_tool_executor = self._resolved_tool_executor_map()
|
||||
self._runtime_tool_default_args = self._resolved_tool_default_args_map()
|
||||
self._runtime_tool_id_map = self._resolved_tool_id_map()
|
||||
self._runtime_tool_display_names = self._resolved_tool_display_name_map()
|
||||
self._initial_greeting_emitted = False
|
||||
|
||||
if self._server_tool_executor is None:
|
||||
@@ -411,10 +415,14 @@ class DuplexPipeline:
|
||||
self._runtime_tools = tools_payload
|
||||
self._runtime_tool_executor = self._resolved_tool_executor_map()
|
||||
self._runtime_tool_default_args = self._resolved_tool_default_args_map()
|
||||
self._runtime_tool_id_map = self._resolved_tool_id_map()
|
||||
self._runtime_tool_display_names = self._resolved_tool_display_name_map()
|
||||
elif "tools" in metadata:
|
||||
self._runtime_tools = []
|
||||
self._runtime_tool_executor = {}
|
||||
self._runtime_tool_default_args = {}
|
||||
self._runtime_tool_id_map = {}
|
||||
self._runtime_tool_display_names = {}
|
||||
|
||||
if self.llm_service and hasattr(self.llm_service, "set_knowledge_config"):
|
||||
self.llm_service.set_knowledge_config(self._resolved_knowledge_config())
|
||||
@@ -1496,6 +1504,47 @@ class DuplexPipeline:
|
||||
result[name] = dict(raw_defaults)
|
||||
return result
|
||||
|
||||
def _resolved_tool_id_map(self) -> Dict[str, str]:
|
||||
result: Dict[str, str] = {}
|
||||
for item in self._runtime_tools:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
fn = item.get("function")
|
||||
if isinstance(fn, dict) and fn.get("name"):
|
||||
alias = str(fn.get("name")).strip()
|
||||
else:
|
||||
alias = str(item.get("name") or "").strip()
|
||||
if not alias:
|
||||
continue
|
||||
tool_id = str(item.get("toolId") or item.get("tool_id") or alias).strip()
|
||||
if tool_id:
|
||||
result[alias] = tool_id
|
||||
return result
|
||||
|
||||
def _resolved_tool_display_name_map(self) -> Dict[str, str]:
|
||||
result: Dict[str, str] = {}
|
||||
for item in self._runtime_tools:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
fn = item.get("function")
|
||||
if isinstance(fn, dict) and fn.get("name"):
|
||||
name = str(fn.get("name")).strip()
|
||||
else:
|
||||
name = str(item.get("name") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
display_name = str(
|
||||
item.get("displayName")
|
||||
or item.get("display_name")
|
||||
or name
|
||||
).strip()
|
||||
if display_name:
|
||||
result[name] = display_name
|
||||
tool_id = str(item.get("toolId") or item.get("tool_id") or "").strip()
|
||||
if tool_id:
|
||||
result[tool_id] = display_name
|
||||
return result
|
||||
|
||||
def _resolved_tool_allowlist(self) -> List[str]:
|
||||
names: set[str] = set()
|
||||
for item in self._runtime_tools:
|
||||
@@ -1519,6 +1568,12 @@ class DuplexPipeline:
|
||||
return str(fn.get("name") or "").strip()
|
||||
return ""
|
||||
|
||||
def _tool_id_for_name(self, tool_name: str) -> str:
|
||||
return str(self._runtime_tool_id_map.get(tool_name) or tool_name).strip()
|
||||
|
||||
def _tool_display_name(self, tool_name: str) -> str:
|
||||
return str(self._runtime_tool_display_names.get(tool_name) or tool_name).strip()
|
||||
|
||||
def _tool_executor(self, tool_call: Dict[str, Any]) -> str:
|
||||
name = self._tool_name(tool_call)
|
||||
if name and name in self._runtime_tool_executor:
|
||||
@@ -1556,6 +1611,7 @@ class DuplexPipeline:
|
||||
status_message = str(status.get("message") or "") if status else ""
|
||||
tool_call_id = str(result.get("tool_call_id") or result.get("id") or "")
|
||||
tool_name = str(result.get("name") or "unknown_tool")
|
||||
tool_display_name = self._tool_display_name(tool_name) or tool_name
|
||||
ok = bool(200 <= status_code < 300)
|
||||
retryable = status_code >= 500 or status_code in {429, 408}
|
||||
error: Optional[Dict[str, Any]] = None
|
||||
@@ -1568,6 +1624,7 @@ class DuplexPipeline:
|
||||
return {
|
||||
"tool_call_id": tool_call_id,
|
||||
"tool_name": tool_name,
|
||||
"tool_display_name": tool_display_name,
|
||||
"ok": ok,
|
||||
"error": error,
|
||||
"status": {"code": status_code, "message": status_message},
|
||||
@@ -1575,6 +1632,7 @@ class DuplexPipeline:
|
||||
|
||||
async def _emit_tool_result(self, result: Dict[str, Any], source: str) -> None:
|
||||
tool_name = str(result.get("name") or "unknown_tool")
|
||||
tool_display_name = self._tool_display_name(tool_name) or tool_name
|
||||
call_id = str(result.get("tool_call_id") or result.get("id") or "")
|
||||
status = result.get("status") if isinstance(result.get("status"), dict) else {}
|
||||
status_code = int(status.get("code") or 0) if status else 0
|
||||
@@ -1592,6 +1650,7 @@ class DuplexPipeline:
|
||||
source=source,
|
||||
tool_call_id=normalized["tool_call_id"],
|
||||
tool_name=normalized["tool_name"],
|
||||
tool_display_name=normalized["tool_display_name"],
|
||||
ok=normalized["ok"],
|
||||
error=normalized["error"],
|
||||
result=result,
|
||||
@@ -1733,6 +1792,8 @@ class DuplexPipeline:
|
||||
enriched_tool_call = dict(tool_call)
|
||||
enriched_tool_call["executor"] = executor
|
||||
tool_name = self._tool_name(enriched_tool_call) or "unknown_tool"
|
||||
tool_id = self._tool_id_for_name(tool_name)
|
||||
tool_display_name = self._tool_display_name(tool_name) or tool_name
|
||||
call_id = str(enriched_tool_call.get("id") or "").strip()
|
||||
fn_payload = (
|
||||
dict(enriched_tool_call.get("function"))
|
||||
@@ -1764,6 +1825,8 @@ class DuplexPipeline:
|
||||
trackId=self.track_audio_out,
|
||||
tool_call_id=call_id,
|
||||
tool_name=tool_name,
|
||||
tool_id=tool_id,
|
||||
tool_display_name=tool_display_name,
|
||||
arguments=tool_arguments,
|
||||
executor=executor,
|
||||
timeout_ms=int(self._TOOL_WAIT_TIMEOUT_SECONDS * 1000),
|
||||
@@ -1881,6 +1944,7 @@ class DuplexPipeline:
|
||||
continue
|
||||
executor = str(call.get("executor") or "server").strip().lower()
|
||||
tool_name = self._tool_name(call) or "unknown_tool"
|
||||
tool_id = self._tool_id_for_name(tool_name)
|
||||
logger.info(f"[Tool] execute start name={tool_name} call_id={call_id} executor={executor}")
|
||||
if executor == "client":
|
||||
result = await self._wait_for_single_tool_result(call_id)
|
||||
@@ -1888,9 +1952,18 @@ class DuplexPipeline:
|
||||
tool_results.append(result)
|
||||
continue
|
||||
|
||||
call_for_executor = dict(call)
|
||||
fn_for_executor = (
|
||||
dict(call_for_executor.get("function"))
|
||||
if isinstance(call_for_executor.get("function"), dict)
|
||||
else None
|
||||
)
|
||||
if isinstance(fn_for_executor, dict):
|
||||
fn_for_executor["name"] = tool_id
|
||||
call_for_executor["function"] = fn_for_executor
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
self._server_tool_executor(call),
|
||||
self._server_tool_executor(call_for_executor),
|
||||
timeout=self._SERVER_TOOL_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -2913,6 +2913,7 @@ export const DebugDrawer: React.FC<{
|
||||
const toolCall = payload?.tool_call || {};
|
||||
const toolCallId = String(toolCall?.id || '').trim();
|
||||
const toolName = String(toolCall?.function?.name || toolCall?.name || 'unknown_tool');
|
||||
const toolDisplayName = String(payload?.tool_display_name || toolCall?.displayName || toolName);
|
||||
const executor = String(toolCall?.executor || 'server').toLowerCase();
|
||||
const rawArgs = String(toolCall?.function?.arguments || '');
|
||||
const argText = rawArgs.length > 160 ? `${rawArgs.slice(0, 160)}...` : rawArgs;
|
||||
@@ -2920,7 +2921,7 @@ export const DebugDrawer: React.FC<{
|
||||
...prev,
|
||||
{
|
||||
role: 'tool',
|
||||
text: `call ${toolName} executor=${executor}${argText ? ` args=${argText}` : ''}`,
|
||||
text: `call ${toolDisplayName} executor=${executor}${argText ? ` args=${argText}` : ''}`,
|
||||
},
|
||||
]);
|
||||
if (executor === 'client' && toolCallId && ws.readyState === WebSocket.OPEN) {
|
||||
@@ -2950,8 +2951,8 @@ export const DebugDrawer: React.FC<{
|
||||
const statusMessage = String(resultPayload?.status?.message || 'error');
|
||||
const resultText =
|
||||
statusCode === 200 && typeof resultPayload?.output?.result === 'number'
|
||||
? `result ${toolName} = ${resultPayload.output.result}`
|
||||
: `result ${toolName} status=${statusCode} ${statusMessage}`;
|
||||
? `result ${toolDisplayName} = ${resultPayload.output.result}`
|
||||
: `result ${toolDisplayName} status=${statusCode} ${statusMessage}`;
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -3025,14 +3026,15 @@ export const DebugDrawer: React.FC<{
|
||||
if (type === 'assistant.tool_result') {
|
||||
const result = payload?.result || {};
|
||||
const toolName = String(result?.name || 'unknown_tool');
|
||||
const toolDisplayName = String(payload?.tool_display_name || toolName);
|
||||
const statusCode = Number(result?.status?.code || 500);
|
||||
const statusMessage = String(result?.status?.message || 'error');
|
||||
const source = String(payload?.source || 'server');
|
||||
const output = result?.output;
|
||||
const resultText =
|
||||
statusCode === 200
|
||||
? `result ${toolName} source=${source} ${JSON.stringify(output)}`
|
||||
: `result ${toolName} source=${source} status=${statusCode} ${statusMessage}`;
|
||||
? `result ${toolDisplayName} source=${source} ${JSON.stringify(output)}`
|
||||
: `result ${toolDisplayName} source=${source} status=${statusCode} ${statusMessage}`;
|
||||
setMessages((prev) => [...prev, { role: 'tool', text: resultText }]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
const [editingTool, setEditingTool] = useState<Tool | null>(null);
|
||||
|
||||
const [toolName, setToolName] = useState('');
|
||||
const [toolId, setToolId] = useState('');
|
||||
const [toolDesc, setToolDesc] = useState('');
|
||||
const [toolCategory, setToolCategory] = useState<'system' | 'query'>('system');
|
||||
const [toolIcon, setToolIcon] = useState('Wrench');
|
||||
@@ -209,6 +210,7 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
const openAdd = () => {
|
||||
setEditingTool(null);
|
||||
setToolName('');
|
||||
setToolId('');
|
||||
setToolDesc('');
|
||||
setToolCategory('system');
|
||||
setToolIcon('Wrench');
|
||||
@@ -224,6 +226,7 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
const openEdit = (tool: Tool) => {
|
||||
setEditingTool(tool);
|
||||
setToolName(tool.name);
|
||||
setToolId(tool.id);
|
||||
setToolDesc(tool.description || '');
|
||||
setToolCategory(tool.category);
|
||||
setToolIcon(tool.icon || 'Wrench');
|
||||
@@ -314,6 +317,10 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
alert('请填写工具名称');
|
||||
return;
|
||||
}
|
||||
if (!editingTool && toolId.trim() && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(toolId.trim())) {
|
||||
alert('工具 ID 不合法,请使用字母/数字/下划线,且不能以数字开头');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
@@ -369,6 +376,7 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
setTools((prev) => prev.map((item) => (item.id === updated.id ? updated : item)));
|
||||
} else {
|
||||
const created = await createTool({
|
||||
id: toolId.trim() || undefined,
|
||||
name: toolName.trim(),
|
||||
description: toolDesc,
|
||||
category: toolCategory,
|
||||
@@ -542,6 +550,19 @@ export const ToolLibraryPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具 ID (函数名)</label>
|
||||
<Input
|
||||
value={toolId}
|
||||
onChange={(e) => setToolId(e.target.value)}
|
||||
placeholder="例如: voice_message_prompt(留空自动生成)"
|
||||
disabled={Boolean(editingTool)}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{editingTool ? '已创建工具的 ID 不可修改。' : '建议客户端工具填写稳定 ID,避免随机 tool_xxx 导致前端无法识别。'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具描述 (给 AI 的说明)</label>
|
||||
<textarea
|
||||
|
||||
Reference in New Issue
Block a user