From 229243e8321622efc250ef35f7b75f83102c5a42 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 27 Feb 2026 16:54:39 +0800 Subject: [PATCH] Add wait_for_response functionality to ToolResource and related components. Update API models, schemas, and routers to support new parameter. Enhance UI components to manage wait_for_response state, ensuring proper integration across the application. --- api/app/models.py | 1 + api/app/routers/assistants.py | 14 +- api/app/routers/tools.py | 24 ++++ api/app/schemas.py | 2 + engine/core/duplex_pipeline.py | 29 ++++ web/pages/Assistants.tsx | 243 +++++++++++++++++++++++++++------ web/pages/ToolLibrary.tsx | 30 ++++ web/services/backendApi.ts | 3 + web/types.ts | 1 + 9 files changed, 303 insertions(+), 44 deletions(-) diff --git a/api/app/models.py b/api/app/models.py index 7ceabaf..2b553ee 100644 --- a/api/app/models.py +++ b/api/app/models.py @@ -98,6 +98,7 @@ class ToolResource(Base): http_timeout_ms: Mapped[int] = mapped_column(Integer, default=10000) parameter_schema: Mapped[dict] = mapped_column(JSON, default=dict) parameter_defaults: Mapped[dict] = mapped_column(JSON, default=dict) + wait_for_response: Mapped[bool] = mapped_column(default=False) enabled: Mapped[bool] = mapped_column(default=True) is_system: Mapped[bool] = mapped_column(default=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) diff --git a/api/app/routers/assistants.py b/api/app/routers/assistants.py index bbed34a..6ce52de 100644 --- a/api/app/routers/assistants.py +++ b/api/app/routers/assistants.py @@ -22,7 +22,13 @@ from ..schemas import ( AssistantOpenerAudioGenerateRequest, AssistantOpenerAudioOut, ) -from .tools import TOOL_REGISTRY, TOOL_CATEGORY_MAP, TOOL_PARAMETER_DEFAULTS, _ensure_tool_resource_schema +from .tools import ( + TOOL_REGISTRY, + TOOL_CATEGORY_MAP, + TOOL_PARAMETER_DEFAULTS, + TOOL_WAIT_FOR_RESPONSE_DEFAULTS, + _ensure_tool_resource_schema, +) router = APIRouter(prefix="/assistants", tags=["Assistants"]) @@ -142,6 +148,11 @@ def _resolve_runtime_tools(db: Session, selected_tool_ids: List[str], warnings: ) defaults_raw = resource.parameter_defaults if resource else TOOL_PARAMETER_DEFAULTS.get(tool_id) defaults = dict(defaults_raw) if isinstance(defaults_raw, dict) else {} + wait_for_response = ( + bool(resource.wait_for_response) + if resource + else bool(TOOL_WAIT_FOR_RESPONSE_DEFAULTS.get(tool_id, False)) + ) if not resource and tool_id not in TOOL_REGISTRY: warnings.append(f"Tool resource not found: {tool_id}") @@ -160,6 +171,7 @@ def _resolve_runtime_tools(db: Session, selected_tool_ids: List[str], warnings: }, "displayName": display_name or tool_id, "toolId": tool_id, + "waitForResponse": wait_for_response, } if defaults: runtime_tool["defaultArgs"] = defaults diff --git a/api/app/routers/tools.py b/api/app/routers/tools.py index b0087a7..5c9efa3 100644 --- a/api/app/routers/tools.py +++ b/api/app/routers/tools.py @@ -98,6 +98,17 @@ TOOL_REGISTRY = { "required": ["msg"] } }, + "text_msg_prompt": { + "name": "文本消息提示", + "description": "显示一条文本弹窗提示", + "parameters": { + "type": "object", + "properties": { + "msg": {"type": "string", "description": "提示文本内容"} + }, + "required": ["msg"] + } + }, } TOOL_CATEGORY_MAP = { @@ -109,6 +120,7 @@ TOOL_CATEGORY_MAP = { "increase_volume": "system", "decrease_volume": "system", "voice_message_prompt": "system", + "text_msg_prompt": "system", } TOOL_ICON_MAP = { @@ -120,6 +132,7 @@ TOOL_ICON_MAP = { "increase_volume": "Volume2", "decrease_volume": "Volume2", "voice_message_prompt": "Volume2", + "text_msg_prompt": "Terminal", } TOOL_HTTP_DEFAULTS = { @@ -130,6 +143,10 @@ TOOL_PARAMETER_DEFAULTS = { "decrease_volume": {"step": 1}, } +TOOL_WAIT_FOR_RESPONSE_DEFAULTS = { + "text_msg_prompt": True, +} + def _normalize_parameter_schema(value: Any, *, tool_id: Optional[str] = None) -> Dict[str, Any]: if not isinstance(value, dict): @@ -177,6 +194,9 @@ def _ensure_tool_resource_schema(db: Session) -> None: if "parameter_defaults" not in columns: db.execute(text("ALTER TABLE tool_resources ADD COLUMN parameter_defaults JSON")) altered = True + if "wait_for_response" not in columns: + db.execute(text("ALTER TABLE tool_resources ADD COLUMN wait_for_response BOOLEAN DEFAULT 0")) + altered = True if altered: db.commit() @@ -222,6 +242,7 @@ def _seed_default_tools_if_empty(db: Session) -> None: http_timeout_ms=int(http_defaults.get("http_timeout_ms") or 10000), parameter_schema=_normalize_parameter_schema(payload.get("parameters"), tool_id=tool_id), parameter_defaults=_normalize_parameter_defaults(TOOL_PARAMETER_DEFAULTS.get(tool_id)), + wait_for_response=bool(TOOL_WAIT_FOR_RESPONSE_DEFAULTS.get(tool_id, False)), enabled=True, is_system=True, )) @@ -311,6 +332,7 @@ def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db) http_timeout_ms=max(1000, int(data.http_timeout_ms or 10000)), parameter_schema=parameter_schema, parameter_defaults=parameter_defaults, + wait_for_response=bool(data.wait_for_response) if data.category == "system" else False, enabled=data.enabled, is_system=False, ) @@ -342,6 +364,8 @@ def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depend update_data["parameter_schema"] = _normalize_parameter_schema(update_data.get("parameter_schema"), tool_id=id) if "parameter_defaults" in update_data: update_data["parameter_defaults"] = _normalize_parameter_defaults(update_data.get("parameter_defaults")) + if new_category != "system": + update_data["wait_for_response"] = False for field, value in update_data.items(): setattr(item, field, value) diff --git a/api/app/schemas.py b/api/app/schemas.py index dbdb806..91b81a5 100644 --- a/api/app/schemas.py +++ b/api/app/schemas.py @@ -241,6 +241,7 @@ class ToolResourceBase(BaseModel): http_timeout_ms: int = 10000 parameter_schema: Dict[str, Any] = Field(default_factory=dict) parameter_defaults: Dict[str, Any] = Field(default_factory=dict) + wait_for_response: bool = False enabled: bool = True @@ -259,6 +260,7 @@ class ToolResourceUpdate(BaseModel): http_timeout_ms: Optional[int] = None parameter_schema: Optional[Dict[str, Any]] = None parameter_defaults: Optional[Dict[str, Any]] = None + wait_for_response: Optional[bool] = None enabled: Optional[bool] = None diff --git a/engine/core/duplex_pipeline.py b/engine/core/duplex_pipeline.py index 1af94b0..3bc4163 100644 --- a/engine/core/duplex_pipeline.py +++ b/engine/core/duplex_pipeline.py @@ -314,6 +314,7 @@ class DuplexPipeline: 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._runtime_tool_wait_for_response: Dict[str, bool] = {} 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() @@ -337,6 +338,7 @@ class DuplexPipeline: 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._runtime_tool_wait_for_response = self._resolved_tool_wait_for_response_map() self._initial_greeting_emitted = False if self._server_tool_executor is None: @@ -441,12 +443,14 @@ class DuplexPipeline: 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._runtime_tool_wait_for_response = self._resolved_tool_wait_for_response_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 = {} + self._runtime_tool_wait_for_response = {} if self.llm_service and hasattr(self.llm_service, "set_knowledge_config"): self.llm_service.set_knowledge_config(self._resolved_knowledge_config()) @@ -1577,6 +1581,25 @@ class DuplexPipeline: result[name] = dict(raw_defaults) return result + def _resolved_tool_wait_for_response_map(self) -> Dict[str, bool]: + result: Dict[str, bool] = {} + 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 + raw_wait = item.get("waitForResponse") + if raw_wait is None: + raw_wait = item.get("wait_for_response") + if isinstance(raw_wait, bool): + result[name] = raw_wait + return result + def _resolved_tool_id_map(self) -> Dict[str, str]: result: Dict[str, str] = {} for item in self._runtime_tools: @@ -1647,6 +1670,9 @@ class DuplexPipeline: 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_wait_for_response(self, tool_name: str) -> bool: + return bool(self._runtime_tool_wait_for_response.get(tool_name, False)) + 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: @@ -1873,6 +1899,8 @@ class DuplexPipeline: 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 + wait_for_response = self._tool_wait_for_response(tool_name) + enriched_tool_call["wait_for_response"] = wait_for_response call_id = str(enriched_tool_call.get("id") or "").strip() fn_payload = ( dict(enriched_tool_call.get("function")) @@ -1906,6 +1934,7 @@ class DuplexPipeline: tool_name=tool_name, tool_id=tool_id, tool_display_name=tool_display_name, + wait_for_response=wait_for_response, arguments=tool_arguments, executor=executor, timeout_ms=int(self._TOOL_WAIT_TIMEOUT_SECONDS * 1000), diff --git a/web/pages/Assistants.tsx b/web/pages/Assistants.tsx index 5962c82..6cf8fe6 100644 --- a/web/pages/Assistants.tsx +++ b/web/pages/Assistants.tsx @@ -1717,6 +1717,9 @@ const DEBUG_CLIENT_TOOLS = [ { id: 'text_msg_prompt', name: 'text_msg_prompt', description: '文本消息提示' }, ] as const; const DEBUG_CLIENT_TOOL_ID_SET = new Set(DEBUG_CLIENT_TOOLS.map((item) => item.id)); +const DEBUG_CLIENT_TOOL_WAIT_DEFAULTS: Record = { + text_msg_prompt: true, +}; type DynamicVariableEntry = { id: string; @@ -1981,7 +1984,16 @@ export const DebugDrawer: React.FC<{ const [inputText, setInputText] = useState(''); const [isLoading, setIsLoading] = useState(false); const [callStatus, setCallStatus] = useState<'idle' | 'calling' | 'active'>('idle'); - const [textPromptDialog, setTextPromptDialog] = useState<{ open: boolean; message: string }>({ open: false, message: '' }); + const [textPromptDialog, setTextPromptDialog] = useState<{ + open: boolean; + message: string; + pendingResult?: { + toolCallId: string; + toolName: string; + toolDisplayName: string; + waitForResponse: boolean; + }; + }>({ open: false, message: '' }); const [textSessionStarted, setTextSessionStarted] = useState(false); const [wsStatus, setWsStatus] = useState<'disconnected' | 'connecting' | 'ready' | 'error'>('disconnected'); const [wsError, setWsError] = useState(''); @@ -2043,6 +2055,7 @@ export const DebugDrawer: React.FC<{ const assistantResponseIndexByIdRef = useRef>(new Map()); const pendingTtfbByResponseIdRef = useRef>(new Map()); const interruptedResponseIdsRef = useRef>(new Set()); + const interruptedDropNoticeKeysRef = useRef>(new Set()); const audioCtxRef = useRef(null); const playbackTimeRef = useRef(0); const activeAudioSourcesRef = useRef>(new Set()); @@ -2088,6 +2101,9 @@ export const DebugDrawer: React.FC<{ return null; } const isClientTool = debugClientTool ? true : (item?.category || 'query') === 'system'; + const waitForResponse = isClientTool + ? (item?.waitForResponse ?? DEBUG_CLIENT_TOOL_WAIT_DEFAULTS[toolId] ?? false) + : false; const parameterSchema = (item?.parameterSchema && typeof item.parameterSchema === 'object') ? item.parameterSchema : getDefaultToolParameters(toolId); @@ -2097,6 +2113,7 @@ export const DebugDrawer: React.FC<{ return { type: 'function', executor: isClientTool ? 'client' : 'server', + waitForResponse, ...(parameterDefaults && Object.keys(parameterDefaults).length > 0 ? { defaultArgs: parameterDefaults } : {}), function: { name: toolId, @@ -2112,6 +2129,7 @@ export const DebugDrawer: React.FC<{ assistantResponseIndexByIdRef.current.clear(); pendingTtfbByResponseIdRef.current.clear(); interruptedResponseIdsRef.current.clear(); + interruptedDropNoticeKeysRef.current.clear(); }; const extractResponseId = (payload: any): string | undefined => { @@ -2120,6 +2138,23 @@ export const DebugDrawer: React.FC<{ return responseId || undefined; }; + const noteInterruptedDrop = (responseId: string, kind: 'ttfb' | 'delta' | 'final') => { + const key = `${responseId}:${kind}`; + if (interruptedDropNoticeKeysRef.current.has(key)) return; + interruptedDropNoticeKeysRef.current.add(key); + if (interruptedDropNoticeKeysRef.current.size > 256) { + const oldest = interruptedDropNoticeKeysRef.current.values().next().value as string | undefined; + if (oldest) interruptedDropNoticeKeysRef.current.delete(oldest); + } + setMessages((prev) => [ + ...prev, + { + role: 'tool', + text: `drop stale ${kind} from interrupted response ${responseId}`, + }, + ]); + }; + // Initialize useEffect(() => { if (isOpen) { @@ -2364,6 +2399,64 @@ export const DebugDrawer: React.FC<{ clearPlaybackQueue(); }; + const emitClientToolResult = (resultPayload: any, toolDisplayName?: string) => { + const ws = wsRef.current; + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: 'tool_call.results', + results: [resultPayload], + }) + ); + } + const statusCode = Number(resultPayload?.status?.code || 500); + const statusMessage = String(resultPayload?.status?.message || 'error'); + const displayName = toolDisplayName || String(resultPayload?.name || 'unknown_tool'); + const resultText = + statusCode === 200 && typeof resultPayload?.output?.result === 'number' + ? `result ${displayName} = ${resultPayload.output.result}` + : `result ${displayName} status=${statusCode} ${statusMessage}`; + setMessages((prev) => [ + ...prev, + { + role: 'tool', + text: resultText, + }, + ]); + }; + + const closeTextPromptDialog = (action: 'confirm' | 'dismiss') => { + let pending: + | { + toolCallId: string; + toolName: string; + toolDisplayName: string; + waitForResponse: boolean; + } + | undefined; + let message = ''; + setTextPromptDialog((prev) => { + pending = prev.pendingResult; + message = prev.message; + return { open: false, message: '' }; + }); + if (pending?.waitForResponse) { + emitClientToolResult( + { + tool_call_id: pending.toolCallId, + name: pending.toolName, + output: { + message: 'text_prompt_closed', + action, + msg: message, + }, + status: { code: 200, message: 'ok' }, + }, + pending.toolDisplayName + ); + } + }; + const scheduleQueuedPlayback = (ctx: AudioContext) => { const queue = queuedAudioBuffersRef.current; if (queue.length === 0) return; @@ -2492,6 +2585,9 @@ export const DebugDrawer: React.FC<{ }; const handleHangup = () => { + if (textPromptDialog.open) { + closeTextPromptDialog('dismiss'); + } stopVoiceCapture(); stopMedia(); closeWs(); @@ -2941,7 +3037,10 @@ export const DebugDrawer: React.FC<{ if (!Number.isFinite(maybeTtfb) || maybeTtfb < 0) return; const ttfbMs = Math.round(maybeTtfb); const responseId = extractResponseId(payload); - if (responseId && interruptedResponseIdsRef.current.has(responseId)) return; + if (responseId && interruptedResponseIdsRef.current.has(responseId)) { + noteInterruptedDrop(responseId, 'ttfb'); + return; + } if (responseId) { const indexed = assistantResponseIndexByIdRef.current.get(responseId); if (typeof indexed === 'number') { @@ -2994,6 +3093,9 @@ export const DebugDrawer: React.FC<{ parsedArgs = {}; } } + const waitForResponse = Boolean( + payload?.wait_for_response ?? toolCall?.wait_for_response ?? toolCall?.waitForResponse ?? false + ); const resultPayload: any = { tool_call_id: toolCallId, name: toolName, @@ -3004,31 +3106,21 @@ export const DebugDrawer: React.FC<{ resultPayload.output = { message: `Client tool '${toolName}' is disabled in debug settings` }; resultPayload.status = { code: 503, message: 'tool_disabled' }; } - const sendToolResult = () => { - ws.send( - JSON.stringify({ - type: 'tool_call.results', - results: [resultPayload], - }) - ); - const statusCode = Number(resultPayload?.status?.code || 500); - const statusMessage = String(resultPayload?.status?.message || 'error'); - const resultText = - statusCode === 200 && typeof resultPayload?.output?.result === 'number' - ? `result ${toolDisplayName} = ${resultPayload.output.result}` - : `result ${toolDisplayName} status=${statusCode} ${statusMessage}`; - setMessages((prev) => [ - ...prev, - { - role: 'tool', - text: resultText, - }, - ]); - }; try { if (resultPayload.status.code === 503) { // Keep disabled result as-is. } else if (toolName === 'turn_on_camera') { + if (!waitForResponse) { + emitClientToolResult( + { + tool_call_id: toolCallId, + name: toolName, + output: { message: 'camera_on_dispatched' }, + status: { code: 200, message: 'ok' }, + }, + toolDisplayName + ); + } navigator.mediaDevices .getUserMedia({ video: selectedCamera ? { deviceId: { exact: selectedCamera } } : true, @@ -3037,20 +3129,36 @@ export const DebugDrawer: React.FC<{ .then((stream) => { if (videoRef.current) videoRef.current.srcObject = stream; streamRef.current = stream; - resultPayload.output = { - message: 'camera_on', - tracks: stream.getVideoTracks().length, - }; - resultPayload.status = { code: 200, message: 'ok' }; - sendToolResult(); + if (waitForResponse) { + emitClientToolResult( + { + tool_call_id: toolCallId, + name: toolName, + output: { + message: 'camera_on', + tracks: stream.getVideoTracks().length, + }, + status: { code: 200, message: 'ok' }, + }, + toolDisplayName + ); + } }) .catch((err) => { - resultPayload.output = { - message: `Client tool '${toolName}' failed`, - error: err instanceof Error ? err.message : String(err), - }; - resultPayload.status = { code: 500, message: 'client_tool_failed' }; - sendToolResult(); + if (waitForResponse) { + emitClientToolResult( + { + tool_call_id: toolCallId, + name: toolName, + output: { + message: `Client tool '${toolName}' failed`, + error: err instanceof Error ? err.message : String(err), + }, + status: { code: 500, message: 'client_tool_failed' }, + }, + toolDisplayName + ); + } }); return; } else if (toolName === 'turn_off_camera') { @@ -3085,6 +3193,36 @@ export const DebugDrawer: React.FC<{ const utterance = new SpeechSynthesisUtterance(msg); utterance.lang = 'zh-CN'; window.speechSynthesis.cancel(); + if (waitForResponse) { + utterance.onend = () => { + emitClientToolResult( + { + tool_call_id: toolCallId, + name: toolName, + output: { message: 'voice_prompt_completed', msg }, + status: { code: 200, message: 'ok' }, + }, + toolDisplayName + ); + }; + utterance.onerror = (event) => { + emitClientToolResult( + { + tool_call_id: toolCallId, + name: toolName, + output: { + message: 'voice_prompt_failed', + msg, + error: String(event.error || 'speech_error'), + }, + status: { code: 500, message: 'client_tool_failed' }, + }, + toolDisplayName + ); + }; + window.speechSynthesis.speak(utterance); + return; + } window.speechSynthesis.speak(utterance); resultPayload.output = { message: 'voice_prompt_sent', msg }; resultPayload.status = { code: 200, message: 'ok' }; @@ -3098,9 +3236,22 @@ export const DebugDrawer: React.FC<{ resultPayload.output = { message: "Missing required argument 'msg'" }; resultPayload.status = { code: 422, message: 'invalid_arguments' }; } else { - setTextPromptDialog({ open: true, message: msg }); - resultPayload.output = { message: 'text_prompt_shown', msg }; - resultPayload.status = { code: 200, message: 'ok' }; + setTextPromptDialog({ + open: true, + message: msg, + pendingResult: { + toolCallId: toolCallId, + toolName, + toolDisplayName, + waitForResponse, + }, + }); + if (!waitForResponse) { + resultPayload.output = { message: 'text_prompt_shown', msg }; + resultPayload.status = { code: 200, message: 'ok' }; + } else { + return; + } } } } catch (err) { @@ -3110,7 +3261,7 @@ export const DebugDrawer: React.FC<{ }; resultPayload.status = { code: 500, message: 'client_tool_failed' }; } - sendToolResult(); + emitClientToolResult(resultPayload, toolDisplayName); } return; } @@ -3221,7 +3372,10 @@ export const DebugDrawer: React.FC<{ const delta = String(payload.text || ''); if (!delta) return; const responseId = extractResponseId(payload); - if (responseId && interruptedResponseIdsRef.current.has(responseId)) return; + if (responseId && interruptedResponseIdsRef.current.has(responseId)) { + noteInterruptedDrop(responseId, 'delta'); + return; + } setMessages((prev) => { let idx = assistantDraftIndexRef.current; if (idx === null || !prev[idx] || prev[idx].role !== 'model') { @@ -3288,7 +3442,10 @@ export const DebugDrawer: React.FC<{ if (type === 'assistant.response.final') { const finalText = String(payload.text || ''); const responseId = extractResponseId(payload); - if (responseId && interruptedResponseIdsRef.current.has(responseId)) return; + if (responseId && interruptedResponseIdsRef.current.has(responseId)) { + noteInterruptedDrop(responseId, 'final'); + return; + } setMessages((prev) => { let idx = assistantDraftIndexRef.current; assistantDraftIndexRef.current = null; @@ -3813,7 +3970,7 @@ export const DebugDrawer: React.FC<{
-
diff --git a/web/pages/ToolLibrary.tsx b/web/pages/ToolLibrary.tsx index 4c990e2..84074d8 100644 --- a/web/pages/ToolLibrary.tsx +++ b/web/pages/ToolLibrary.tsx @@ -184,6 +184,7 @@ export const ToolLibraryPage: React.FC = () => { const [toolCategory, setToolCategory] = useState<'system' | 'query'>('system'); const [toolIcon, setToolIcon] = useState('Wrench'); const [toolEnabled, setToolEnabled] = useState(true); + const [toolWaitForResponse, setToolWaitForResponse] = useState(false); const [toolHttpMethod, setToolHttpMethod] = useState<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>('GET'); const [toolHttpUrl, setToolHttpUrl] = useState(''); const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}'); @@ -215,6 +216,7 @@ export const ToolLibraryPage: React.FC = () => { setToolCategory('system'); setToolIcon('Wrench'); setToolEnabled(true); + setToolWaitForResponse(false); setToolHttpMethod('GET'); setToolHttpUrl(''); setToolHttpHeadersText('{}'); @@ -231,6 +233,7 @@ export const ToolLibraryPage: React.FC = () => { setToolCategory(tool.category); setToolIcon(tool.icon || 'Wrench'); setToolEnabled(tool.enabled ?? true); + setToolWaitForResponse(Boolean(tool.waitForResponse)); setToolHttpMethod((tool.httpMethod || 'GET') as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'); setToolHttpUrl(tool.httpUrl || ''); setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2)); @@ -269,6 +272,15 @@ export const ToolLibraryPage: React.FC = () => { {tool.category === 'system' ? 'SYSTEM' : 'QUERY'} + {tool.category === 'system' && ( + + {tool.waitForResponse ? 'WAIT' : 'NO-WAIT'} + + )} ID: {tool.id}

{tool.description}

@@ -371,6 +383,7 @@ export const ToolLibraryPage: React.FC = () => { httpTimeoutMs: toolHttpTimeoutMs, parameterSchema: parsedParameterSchema, parameterDefaults: parsedParameterDefaults, + waitForResponse: toolCategory === 'system' ? toolWaitForResponse : false, enabled: toolEnabled, }); setTools((prev) => prev.map((item) => (item.id === updated.id ? updated : item))); @@ -387,6 +400,7 @@ export const ToolLibraryPage: React.FC = () => { httpTimeoutMs: toolHttpTimeoutMs, parameterSchema: parsedParameterSchema, parameterDefaults: parsedParameterDefaults, + waitForResponse: toolCategory === 'system' ? toolWaitForResponse : false, enabled: toolEnabled, }); setTools((prev) => [created, ...prev]); @@ -573,6 +587,22 @@ export const ToolLibraryPage: React.FC = () => { /> + {toolCategory === 'system' && ( +
+ +

+ 勾选后,模型会在客户端工具回传结果后再继续回复;不勾选则可立即继续回复。 +

+
+ )} +
Tool Parameters
diff --git a/web/services/backendApi.ts b/web/services/backendApi.ts index 31d793c..c310b8e 100644 --- a/web/services/backendApi.ts +++ b/web/services/backendApi.ts @@ -125,6 +125,7 @@ const mapTool = (raw: AnyRecord): Tool => ({ httpTimeoutMs: Number(readField(raw, ['httpTimeoutMs', 'http_timeout_ms'], 10000)), parameterSchema: readField(raw, ['parameterSchema', 'parameter_schema'], {}), parameterDefaults: readField(raw, ['parameterDefaults', 'parameter_defaults'], {}), + waitForResponse: Boolean(readField(raw, ['waitForResponse', 'wait_for_response'], false)), isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)), enabled: Boolean(readField(raw, ['enabled'], true)), isCustom: !Boolean(readField(raw, ['isSystem', 'is_system'], false)), @@ -571,6 +572,7 @@ export const createTool = async (data: Partial): Promise => { http_timeout_ms: data.httpTimeoutMs ?? 10000, parameter_schema: data.parameterSchema || {}, parameter_defaults: data.parameterDefaults || {}, + wait_for_response: data.waitForResponse ?? false, enabled: data.enabled ?? true, }; const response = await apiRequest('/tools/resources', { method: 'POST', body: payload }); @@ -589,6 +591,7 @@ export const updateTool = async (id: string, data: Partial): Promise http_timeout_ms: data.httpTimeoutMs, parameter_schema: data.parameterSchema, parameter_defaults: data.parameterDefaults, + wait_for_response: data.waitForResponse, enabled: data.enabled, }; const response = await apiRequest(`/tools/resources/${id}`, { method: 'PUT', body: payload }); diff --git a/web/types.ts b/web/types.ts index ec33eef..ab45028 100644 --- a/web/types.ts +++ b/web/types.ts @@ -199,6 +199,7 @@ export interface Tool { httpTimeoutMs?: number; parameterSchema?: Record; parameterDefaults?: Record; + waitForResponse?: boolean; isCustom?: boolean; isSystem?: boolean; enabled?: boolean;