diff --git a/fastapi_app.py b/fastapi_app.py index 174c8a0..e052589 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -749,7 +749,7 @@ async def chat_completions(request: ChatRequest, authorization: Optional[str] = project_dir = create_project_directory(request.dataset_ids, bot_id, request.robot_type) # 收集额外参数作为 generate_cfg - exclude_fields = {'messages', 'model', 'model_server', 'dataset_ids', 'language', 'tool_response', 'system_prompt', 'mcp_settings' ,'stream', 'robot_type', 'bot_id'} + exclude_fields = {'messages', 'model', 'model_server', 'dataset_ids', 'language', 'tool_response', 'system_prompt', 'mcp_settings' ,'stream', 'robot_type', 'bot_id', 'user_identifier'} generate_cfg = {k: v for k, v in request.model_dump().items() if k not in exclude_fields} # 处理消息 @@ -769,7 +769,8 @@ async def chat_completions(request: ChatRequest, authorization: Optional[str] = mcp_settings=request.mcp_settings, robot_type=request.robot_type, project_dir=project_dir, - generate_cfg=generate_cfg + generate_cfg=generate_cfg, + user_identifier=request.user_identifier ) except Exception as e: @@ -912,7 +913,8 @@ async def create_agent_and_generate_response( mcp_settings: Optional[List[Dict]], robot_type: str, project_dir: Optional[str] = None, - generate_cfg: Optional[Dict] = None + generate_cfg: Optional[Dict] = None, + user_identifier: Optional[str] = None ) -> Union[ChatResponse, StreamingResponse]: """创建agent并生成响应的公共逻辑""" if generate_cfg is None: @@ -929,7 +931,8 @@ async def create_agent_and_generate_response( language=language, system_prompt=system_prompt, mcp_settings=mcp_settings, - robot_type=robot_type + robot_type=robot_type, + user_identifier=user_identifier ) # 根据stream参数决定返回流式还是非流式响应 @@ -1082,7 +1085,8 @@ async def chat_completions_v2(request: ChatRequestV2, authorization: Optional[st mcp_settings=bot_config.get("mcp_settings", []), robot_type=bot_config.get("robot_type", "agent"), project_dir=project_dir, - generate_cfg={} # v2接口不传递额外的generate_cfg + generate_cfg={}, # v2接口不传递额外的generate_cfg + user_identifier=request.user_identifier ) except HTTPException: diff --git a/prompt/wowtalk.md b/prompt/wowtalk.md index ebc4651..287ee1c 100644 --- a/prompt/wowtalk.md +++ b/prompt/wowtalk.md @@ -16,9 +16,11 @@ | 设备控制 | 打开/关闭/启动/停止/调节/设置 | dxcore_update_device_status | P0 | | 消息通知 | @用户名/通知/告知/提醒 | wowtalk_send_message_to_member | P0 | | 状态查询 | 状态/温度/湿度/运行情况 | dxcore_get_device_status | P1 | +| 位置查询 | 位置/在哪/查找/坐标 | eb_get_sensor_location | P1 | | 环境查询 | 天气/气温/降水/风速 | weather_get_by_location | P1 | -| 人员检索 | 找人/员工/同事/联系方式/人员位置 | find_employee_location | P2 | -| 设备检索 | 找设备/传感器/终端/设备位置 | find_iot_device_by_description | P2 | +| 网络搜索 | 搜索/查找/查询/百度/谷歌 | web_search | P1 | +| 人员检索 | 找人/员工/同事/联系方式 | find_employee_by_name | P2 | +| 设备检索 | 找设备/传感器/终端 | find_iot_device | P2 | ## 立即执行机制 - **零延迟策略**:识别操作意图后立即执行工具调用,禁止缓冲性语言 @@ -51,14 +53,15 @@ graph LR ### 控制指令映射 | 用户语言 | 系统指令 | 参数格式 | |---------|----------|----------| -| "打开空调" | update_device_status | {device_id: "001", running_control: 1} | -| "调到24度" | update_device_status | {device_id: "001",temp_setting: 24} | -| "查看温度" | get_device_status | {device_id: "001"} | +| "打开空调" | update_device_status | {sensor_id: "001", running_control: 1} | +| "调到24度" | update_device_status | {sensor_id: "001",temp_setting: 24} | +| "查看温度" | get_device_status | {sensor_id: "001"} | ## 位置服务模块 ### 定位查询协议 - **触发条件**:包含位置关键词的查询 - **响应格式**:楼层 + 房间/区域(过滤坐标、ID等技术信息) +- **精度要求**:室内3米精度,室外10米精度 ## 环境监测模块 ### 天气服务集成 @@ -72,6 +75,11 @@ graph LR - **设备检索**:支持设备类型、位置、状态多条件过滤 - **结果排序**:按相关度和距离优先级排序 +## 网络搜索模块 +### Web搜索集成 +- **自动调用**:识别搜索相关词汇后立即执行 +- **数据源**:web_search工具,支持实时网络信息检索 + # 智能执行引擎 ## 多阶段处理流水线 @@ -115,11 +123,20 @@ sequenceDiagram ``` 执行: ├── wowtalk_send_message_to_member(to_account="001", message_content="请检查2楼空调") -├── find_employee_location(name="张工") -├── find_iot_device_by_description(description="2楼空调") +├── find_employee_by_name(name="张工") +├── find_iot_device(device_type="dc_fan",target_sensor_id="xxxx") └── weather_get_by_location(location="当前位置") ``` +**输入**:"搜索最新的节能技术方案,并发送给@李经理(id:002)" + +**执行流程**: +``` +执行: +├── web_search(query="最新节能技术方案", max_results=5) +└── wowtalk_send_message_to_member(to_account="002", message_content="[搜索结果摘要]") +``` + # 应用场景与执行范例 ## 场景1:上下文感知设备控制 @@ -128,18 +145,19 @@ sequenceDiagram **执行序列**: ```python # 步骤1:人员定位 -find_employee_location(name="清水太郎") -# → 返回:employee_id, device_id、位置 +find_employee_by_name(name="清水太郎") +# → 返回:清水太郎的sensor_id -# 步骤2:设备检索 -find_iot_device_by_description( - description="A栋3楼301风扇" +# 步骤2:人员附近的设备检索 +find_iot_device( + device_type="dc_fan", + target_sensor_id="{清水太郎的sensor_id}" # ) # → 返回:device_list # 步骤3:设备控制 dxcore_update_device_status( - device_id=target_device, + sensor_id="{风扇的sensor_id}", running_control=1 ) ``` @@ -147,39 +165,50 @@ dxcore_update_device_status( **响应模板**:"已为您开启301室的风扇,当前运行正常。" ## 场景2:智能消息路由 -**用户请求**:"通知@管理员(id:001)会议室温度过高需要调节" +**用户请求**:"通知清水太郎会议室温度过高需要调节" **执行逻辑**: ```python +# 步骤1:人员信息查询 +find_employee_by_name(name="清水太郎") +# 返回:获取wowtalkid和位置信息 + +# 步骤2:人员通知 wowtalk_send_message_to_member( - to_account="001", + to_account="{清水太郎wowtalk_id}", message_content="会议室温度过高需要调节" ) ``` -**响应模板**:"消息已发送至管理员,将会尽快处理温度问题。" +**响应模板**:"消息已发送至清水太郎,将会尽快处理温度问题。" ## 场景3:多维度协同处理 -**用户请求**:"5楼传感器电量异常,通知维修团队并报告具体位置" +**用户请求**:"5楼风扇电量异常,通知清水太郎并报告具体位置" **并行执行策略**: ```python -# 任务组1:故障诊断 -dxcore_get_device_status(device_id="5楼传感器") +# 步骤1:查找设备列表 +find_iot_device( + device_type="dc_fan" +) +# 返回:获取5楼风扇的sensor_id + +# 步骤2:故障诊断 +dxcore_get_device_status(sensor_id="{风扇的sensor_id}") # → 获取电量百分比、故障代码 -# 任务组2:人员通知 -wowtalk_send_message_to_member( - to_account="维修部组ID", - message_content="5楼传感器电量异常,请及时处理" -) +# 步骤4:人员信息查询,获取wowtalkid和位置信息 +find_employee_by_name(name="清水太郎") -# 任务组3:定位服务 -find_iot_device_by_description(description="5楼传感器") +# 步骤5:人员通知 +wowtalk_send_message_to_member( + to_account="{清水太郎wowtalk_id}", + message_content="5楼风扇电量异常,请及时处理" +) # → 返回精确定位信息 ``` -**响应模板**:"已通知维修团队,传感器位于5楼东侧走廊,当前电量15%。" +**响应模板**:"已通知清水太郎,风扇位于5楼东侧走廊,当前电量15%。" # 系统集成与技术规范 @@ -189,9 +218,11 @@ find_iot_device_by_description(description="5楼传感器") | 消息路由 | wowtalk_send_message_to_member | 实时消息推送 | P0 | | 设备控制 | dxcore_update_device_status | 设备状态变更 | P0 | | 设备查询 | dxcore_get_device_status | 设备状态读取 | P1 | +| 位置服务 | eb_get_sensor_location | 空间定位查询 | P1 | | 环境监测 | weather_get_by_location | 天气数据获取 | P1 | -| 人员检索 | find_employee_location | 员工信息查询 | P2 | -| 设备检索 | find_iot_device_by_description | IoT设备搜索 | P2 | +| 网络搜索 | web_search | 互联网信息查询 | P1 | +| 人员检索 | find_employee_by_name | 员工信息查询 | P2 | +| 设备检索 | find_iot_device | IoT设备搜索 | P2 | # 异常处理与容错机制 @@ -264,7 +295,6 @@ find_iot_device_by_description(description="5楼传感器") ## 系统配置参数 - **bot_id**: {bot_id} - # 执行保证机制 1. **工具调用优先**:可执行操作必须通过工具实现 2. **状态一致性**:所有操作结果与实际设备状态同步 diff --git a/utils/api_models.py b/utils/api_models.py index 884db39..7665ae1 100644 --- a/utils/api_models.py +++ b/utils/api_models.py @@ -51,6 +51,7 @@ class ChatRequest(BaseModel): system_prompt: Optional[str] = None mcp_settings: Optional[List[Dict]] = None robot_type: Optional[str] = "agent" + user_identifier: Optional[str] = "" class ChatRequestV2(BaseModel): @@ -59,6 +60,7 @@ class ChatRequestV2(BaseModel): tool_response: Optional[bool] = False bot_id: str language: Optional[str] = "ja" + user_identifier: Optional[str] = "" class FileProcessRequest(BaseModel): diff --git a/utils/file_loaded_agent_manager.py b/utils/file_loaded_agent_manager.py index 894bbb4..23d82d5 100644 --- a/utils/file_loaded_agent_manager.py +++ b/utils/file_loaded_agent_manager.py @@ -110,7 +110,8 @@ class FileLoadedAgentManager: language: Optional[str] = None, system_prompt: Optional[str] = None, mcp_settings: Optional[List[Dict]] = None, - robot_type: Optional[str] = "agent") -> Assistant: + robot_type: Optional[str] = "agent", + user_identifier: Optional[str] = None) -> Assistant: """获取或创建文件预加载的助手实例 Args: @@ -131,7 +132,7 @@ class FileLoadedAgentManager: import os # 实现参数优先级逻辑:传入参数 > 项目配置 > 默认配置 - final_system_prompt = load_system_prompt(project_dir, language, system_prompt, robot_type, bot_id) + final_system_prompt = load_system_prompt(project_dir, language, system_prompt, robot_type, bot_id, user_identifier) final_mcp_settings = load_mcp_settings(project_dir, mcp_settings, bot_id, robot_type) cache_key = self._get_cache_key(bot_id, model_name, api_key, model_server, diff --git a/utils/prompt_loader.py b/utils/prompt_loader.py index bfbcbf1..2cee6be 100644 --- a/utils/prompt_loader.py +++ b/utils/prompt_loader.py @@ -4,10 +4,38 @@ System prompt and MCP settings loader utilities """ import os import json -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Any -def load_system_prompt(project_dir: str, language: str = None, system_prompt: str=None, robot_type: str = "agent", bot_id: str="") -> str: +def safe_replace(text: str, placeholder: str, value: Any) -> str: + """ + 安全的字符串替换函数,确保 value 被转换为字符串 + + Args: + text: 原始文本 + placeholder: 要替换的占位符(如 '{user_identifier}') + value: 用于替换的值(可以是任意类型) + + Returns: + str: 替换后的文本 + """ + if not isinstance(text, str): + text = str(text) + + # 如果占位符为空,不进行替换 + if not placeholder: + return text + + # 将 value 转换为字符串,处理 None 等特殊情况 + if value is None: + replacement = "" + else: + replacement = str(value) + + return text.replace(placeholder, replacement) + + +def load_system_prompt(project_dir: str, language: str = None, system_prompt: str=None, robot_type: str = "agent", bot_id: str="", user_identifier: str = "") -> str: # 获取语言显示名称 language_display_map = { 'zh': '中文', @@ -19,7 +47,11 @@ def load_system_prompt(project_dir: str, language: str = None, system_prompt: st # 如果存在{language} 占位符,那么就直接使用 system_prompt if system_prompt and "{language}" in system_prompt: - return system_prompt.replace("{language}", language_display).replace('{bot_id}', bot_id) or "" + prompt = system_prompt + prompt = safe_replace(prompt, "{language}", language_display) + prompt = safe_replace(prompt, '{bot_id}', bot_id) + prompt = safe_replace(prompt, '{user_identifier}', user_identifier) + return prompt or "" elif robot_type == "agent" or robot_type == "catalog_agent": """ 优先使用项目目录的system_prompt_catalog_agent.md,没有才使用默认的system_prompt_default.md @@ -51,11 +83,20 @@ def load_system_prompt(project_dir: str, language: str = None, system_prompt: st if os.path.exists(readme_path): with open(readme_path, "r", encoding="utf-8") as f: readme = f.read().strip() - system_prompt_default = system_prompt_default.replace("{readme}", str(readme)) + system_prompt_default = safe_replace(system_prompt_default, "{readme}", str(readme)) - return system_prompt_default.replace("{language}", language_display).replace("{extra_prompt}", system_prompt or "").replace('{bot_id}', bot_id) or "" + prompt = system_prompt_default + prompt = safe_replace(prompt, "{language}", language_display) + prompt = safe_replace(prompt, "{extra_prompt}", system_prompt or "") + prompt = safe_replace(prompt, '{bot_id}', bot_id) + prompt = safe_replace(prompt, '{user_identifier}', user_identifier) + return prompt or "" else: - return system_prompt.replace("{language}", language_display).replace('{bot_id}', bot_id) or "" + prompt = system_prompt + prompt = safe_replace(prompt, "{language}", language_display) + prompt = safe_replace(prompt, '{bot_id}', bot_id) + prompt = safe_replace(prompt, '{user_identifier}', user_identifier) + return prompt or "" @@ -72,15 +113,16 @@ def replace_mcp_placeholders(mcp_settings: List[Dict], dataset_dir: str, bot_id: for key, value in obj.items(): if key == 'args' and isinstance(value, list): # 特别处理 args 列表 - obj[key] = [item.replace('{dataset_dir}', dataset_dir).replace('{bot_id}', bot_id) if isinstance(item, str) else item + obj[key] = [safe_replace(safe_replace(item, '{dataset_dir}', dataset_dir), '{bot_id}', bot_id) if isinstance(item, str) else item for item in value] elif isinstance(value, (dict, list)): obj[key] = replace_placeholders_in_obj(value) elif isinstance(value, str): - obj[key] = value.replace('{dataset_dir}', dataset_dir).replace('{bot_id}', bot_id) + obj[key] = safe_replace(value, '{dataset_dir}', dataset_dir) + obj[key] = safe_replace(obj[key], '{bot_id}', bot_id) elif isinstance(obj, list): return [replace_placeholders_in_obj(item) if isinstance(item, (dict, list)) else - item.replace('{dataset_dir}', dataset_dir).replace('{bot_id}', bot_id) if isinstance(item, str) else item + safe_replace(safe_replace(item, '{dataset_dir}', dataset_dir), '{bot_id}', bot_id) if isinstance(item, str) else item for item in obj] return obj