diff --git a/docs/FAQ.md b/docs/FAQ.md index 83df8c2c..c40e7784 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -80,6 +80,7 @@ VAD: 7、[如何开启声纹识别](./voiceprint-integration.md)
8、[新闻插件源配置指南](./newsnow_plugin_config.md)
9、[知识库ragflow集成指南](./ragflow-integration.md)
+10、[如何部署上下文源](./context-provider-integration.md)
### 11、语音克隆、本地语音部署相关教程 1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)
diff --git a/docs/context-provider-integration.md b/docs/context-provider-integration.md new file mode 100644 index 00000000..e49ab0dc --- /dev/null +++ b/docs/context-provider-integration.md @@ -0,0 +1,220 @@ +# 数据上下文填充功能使用指南 + +## 概述 + +数据上下文填充功能(Context Data Provider)允许小智在唤醒那一刻,获取外部系统的数据,并将其动态注入到大模型的系统提示词(System Prompt)中。 +让其做到唤醒时感知世界某个事物的状态。 + +通过这个功能,在小智唤醒的一刹那,“感知”到: +- 智能家居的传感器状态(温度、湿度、灯光状态等) +- 业务系统的实时数据(服务器负载、健康数据、股票信息等) +- 任何可以通过 HTTP API 获取的文本信息 + +**注意**:该功能只是方便小智在唤醒的时候感知事物的状态,而如果想要小智唤醒后实时获取事物的状态,建议在此功能上再结合MCP工具的调用。 + +## 工作原理 + +1. **配置源**:用户配置一个或多个 HTTP API 地址。 +2. **触发请求**:当系统构建 Prompt 时,如果发现模板中包含 `{{ dynamic_context }}` 占位符,会请求所有配置的 API。 +3. **自动注入**:系统会自动将 API 返回的数据格式化为 Markdown 列表,替换 `{{ dynamic_context }}` 占位符。 + +## 接口规范 + +为了让小智正确解析数据,您的 API 需要满足以下规范: + +- **请求方式**:`GET` +- **请求头**:系统会自动添加 `device_id` 字段到 Request Header。 +- **响应格式**:必须返回 JSON 格式,且包含 `code` 和 `data` 字段。 + +### 响应示例 + +**情况 1:返回键值对** +```json +{ + "code": 0, + "msg": "success", + "data": { + "客厅温度": "26℃", + "客厅湿度": "45%", + "大门状态": "已关闭" + } +} +``` +*注入效果:* +```markdown + +- **客厅温度:** 26℃ +- **客厅湿度:** 45% +- **大门状态:** 已关闭 + +``` + +**情况 2:返回列表** +```json +{ + "code": 0, + "data": [ + "您有10个待办事项", + "当前汽车的行驶速度是100km每小时" + ] +} +``` +*注入效果:* +```markdown + +- 您有10个待办事项 +- 当前汽车的行驶速度是100km每小时 + +``` + +## 配置指南 + +### 方式 1:智控台配置(全模块部署) + +1. 登录智控台,进入**角色配置**页面。 +2. 找到**上下文源**配置项(点击“编辑源”按钮)。 +3. 点击**添加**,输入您的 API 地址。 +4. 如果 API 需要鉴权,可以在**请求头**部分添加 `Authorization` 或其他 Header。 +5. 保存配置。 + +### 方式 2:配置文件配置(单模块部署) + +编辑 `xiaozhi-server/data/.config.yaml` 文件,添加 `context_providers` 配置段: + +```yaml +# 上下文源配置 +context_providers: + - url: "http://api.example.com/data" + headers: + Authorization: "Bearer your-token" + - url: "http://another-api.com/data" +``` + +## 启用功能 + +默认情况下,系统的提示词模板文件(`data/.agent-base-prompt.txt`)中已经预置了 `{{ dynamic_context }}` 占位符,您无需手动添加。 + +**示例:** + +```markdown + +【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】 +- **设备ID:** {{device_id}} +- **当前时间:** {{current_time}} +... +{{ dynamic_context }} + +``` + +**注意**:如果您不需要使用此功能,可以选择**不配置任何上下文源**,也可以从提示词模板文件中**删除** `{{ dynamic_context }}` 占位符。 + +## 附录:Mock 测试服务示例 + +为了方便您测试和开发,我们提供了一个简单的 Python Mock Server 脚本。您可以运行此脚本在本地模拟 API 接口。 + +**mock_api_server.py** + +```python +import http.server +import socketserver +import json +from urllib.parse import urlparse, parse_qs + +# 设置端口号 +PORT = 8081 + +class MockRequestHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + # 解析路径和参数 + parsed_path = urlparse(self.path) + path = parsed_path.path + query = parse_qs(parsed_path.query) + + response_data = {} + status_code = 200 + + print(f"收到请求: {path}, 参数: {query}") + + # Case 1: 模拟健康数据 (返回字典 Dict) + # 路径参数风格: /health + # device_id 从 Header 获取 + if path == "/health": + device_id = self.headers.get("device_id", "unknown_device") + print(f"device_id: {device_id}") + response_data = { + "code": 0, + "msg": "success", + "data": { + "测试设备ID": device_id, + "心率": "80 bpm", + "血压": "120/80 mmHg", + "状态": "良好" + } + } + + # Case 2: 模拟新闻列表 (返回列表 List) + # 无参数: /news/list + elif path == "/news/list": + response_data = { + "code": 0, + "msg": "success", + "data": [ + "今日头条:Python 3.14 发布", + "科技新闻:AI 助手改变生活", + "本地新闻:明日有大雨,记得带伞" + ] + } + + # Case 3: 模拟天气简报 (返回字符串 String) + # 无参数: /weather/simple + elif path == "/weather/simple": + response_data = { + "code": 0, + "msg": "success", + "data": "今日晴转多云,气温 20-25 度,空气质量优,适合出行。" + } + + # Case 4: 模拟设备详情 (Query参数风格) + # 参数风格: /device/info + # device_id 从 Header 获取 + elif path == "/device/info": + device_id = self.headers.get("device_id", "unknown_device") + response_data = { + "code": 0, + "msg": "success", + "data": { + "查询方式": "Header参数", + "设备ID": device_id, + "电量": "85%", + "固件": "v2.0.1" + } + } + + # Case 5: 404 Not Found + else: + status_code = 404 + response_data = {"error": "接口不存在"} + + # 发送响应 + self.send_response(status_code) + self.send_header('Content-type', 'application/json; charset=utf-8') + self.end_headers() + self.wfile.write(json.dumps(response_data, ensure_ascii=False).encode('utf-8')) + +# 启动服务 +# 允许地址重用,防止快速重启报错 +socketserver.TCPServer.allow_reuse_address = True +with socketserver.TCPServer(("", PORT), MockRequestHandler) as httpd: + print(f"==================================================") + print(f"Mock API Server 已启动: http://localhost:{PORT}") + print(f"可用接口列表:") + print(f"1. [字典] http://localhost:{PORT}/health") + print(f"2. [列表] http://localhost:{PORT}/news/list") + print(f"3. [文本] http://localhost:{PORT}/weather/simple") + print(f"4. [参数] http://localhost:{PORT}/device/info") + print(f"==================================================") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n服务已停止") +``` diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index a4ad0863..c09aa141 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -44,6 +44,7 @@ import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentContextProviderService; import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; @@ -64,6 +65,7 @@ public class AgentController { private final AgentChatHistoryService agentChatHistoryService; private final AgentChatAudioService agentChatAudioService; private final AgentPluginMappingService agentPluginMappingService; + private final AgentContextProviderService agentContextProviderService; private final RedisUtils redisUtils; @GetMapping("/list") @@ -135,6 +137,8 @@ public class AgentController { agentChatHistoryService.deleteByAgentId(id, true, true); // 删除关联的插件 agentPluginMappingService.deleteByAgentId(id); + // 删除关联的上下文源配置 + agentContextProviderService.deleteByAgentId(id); // 再删除智能体 agentService.deleteById(id); return new Result<>(); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentContextProviderDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentContextProviderDao.java new file mode 100644 index 00000000..d46ad3ab --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentContextProviderDao.java @@ -0,0 +1,9 @@ +package xiaozhi.modules.agent.dao; + +import org.apache.ibatis.annotations.Mapper; +import xiaozhi.common.dao.BaseDao; +import xiaozhi.modules.agent.entity.AgentContextProviderEntity; + +@Mapper +public interface AgentContextProviderDao extends BaseDao { +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java index 0e3d9bc3..ebb29fbd 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java @@ -69,6 +69,9 @@ public class AgentUpdateDTO implements Serializable { @Schema(description = "排序", example = "1", nullable = true) private Integer sort; + @Schema(description = "上下文源配置", nullable = true) + private List contextProviders; + @Data @Schema(description = "插件函数信息") public static class FunctionInfo implements Serializable { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/ContextProviderDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/ContextProviderDTO.java new file mode 100644 index 00000000..0b8edfd8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/ContextProviderDTO.java @@ -0,0 +1,19 @@ +package xiaozhi.modules.agent.dto; + +import java.io.Serializable; +import java.util.Map; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "上下文源配置DTO") +public class ContextProviderDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "URL地址") + private String url; + + @Schema(description = "请求头") + private Map headers; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java new file mode 100644 index 00000000..937556ef --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java @@ -0,0 +1,43 @@ +package xiaozhi.modules.agent.entity; + +import java.util.Date; +import java.util.List; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import xiaozhi.modules.agent.dto.ContextProviderDTO; + +@Data +@TableName(value = "ai_agent_context_provider", autoResultMap = true) +@Schema(description = "智能体上下文源配置") +public class AgentContextProviderEntity { + + @TableId(type = IdType.ASSIGN_UUID) + @Schema(description = "主键") + private String id; + + @Schema(description = "智能体ID") + private String agentId; + + @Schema(description = "上下文源配置") + @TableField(typeHandler = JacksonTypeHandler.class) + private List contextProviders; + + @Schema(description = "创建者") + private Long creator; + + @Schema(description = "创建时间") + private Date createdAt; + + @Schema(description = "更新者") + private Long updater; + + @Schema(description = "更新时间") + private Date updatedAt; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentContextProviderService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentContextProviderService.java new file mode 100644 index 00000000..da759971 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentContextProviderService.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.agent.service; + +import xiaozhi.common.service.BaseService; +import xiaozhi.modules.agent.entity.AgentContextProviderEntity; + +public interface AgentContextProviderService extends BaseService { + /** + * 根据智能体ID获取上下文源配置 + * @param agentId 智能体ID + * @return 上下文源配置实体 + */ + AgentContextProviderEntity getByAgentId(String agentId); + + /** + * 保存或更新上下文源配置 + * @param entity 实体 + */ + void saveOrUpdateByAgentId(AgentContextProviderEntity entity); + + /** + * 根据智能体ID删除上下文源配置 + * @param agentId 智能体ID + */ + void deleteByAgentId(String agentId); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentContextProviderServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentContextProviderServiceImpl.java new file mode 100644 index 00000000..b68ab8c9 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentContextProviderServiceImpl.java @@ -0,0 +1,35 @@ +package xiaozhi.modules.agent.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + +import xiaozhi.common.service.impl.BaseServiceImpl; +import xiaozhi.modules.agent.dao.AgentContextProviderDao; +import xiaozhi.modules.agent.entity.AgentContextProviderEntity; +import xiaozhi.modules.agent.service.AgentContextProviderService; + +@Service +public class AgentContextProviderServiceImpl extends BaseServiceImpl implements AgentContextProviderService { + + @Override + public AgentContextProviderEntity getByAgentId(String agentId) { + return baseDao.selectOne(new QueryWrapper().eq("agent_id", agentId)); + } + + @Override + public void saveOrUpdateByAgentId(AgentContextProviderEntity entity) { + AgentContextProviderEntity exist = getByAgentId(entity.getAgentId()); + if (exist != null) { + entity.setId(exist.getId()); + updateById(entity); + } else { + insert(entity); + } + } + + @Override + public void deleteByAgentId(String agentId) { + baseDao.delete(new QueryWrapper().eq("agent_id", agentId)); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index cb550b3e..0adf0873 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -32,10 +32,12 @@ import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; +import xiaozhi.modules.agent.entity.AgentContextProviderEntity; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentPluginMapping; import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentContextProviderService; import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; @@ -62,6 +64,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp private final AgentChatHistoryService agentChatHistoryService; private final AgentTemplateService agentTemplateService; private final ModelProviderService modelProviderService; + private final AgentContextProviderService agentContextProviderService; @Override public PageData adminAgentList(Map params) { @@ -85,6 +88,13 @@ public class AgentServiceImpl extends BaseServiceImpl imp agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode()); } } + + // 查询上下文源配置 + AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(id); + if (contextProviderEntity != null) { + agent.setContextProviders(contextProviderEntity.getContextProviders()); + } + // 无需额外查询插件列表,已通过SQL查询出来 return agent; } @@ -331,6 +341,14 @@ public class AgentServiceImpl extends BaseServiceImpl imp agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false); } + // 更新上下文源配置 + if (dto.getContextProviders() != null) { + AgentContextProviderEntity contextEntity = new AgentContextProviderEntity(); + contextEntity.setAgentId(agentId); + contextEntity.setContextProviders(dto.getContextProviders()); + agentContextProviderService.saveOrUpdateByAgentId(contextEntity); + } + boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId()); if (!b) { throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java index d56c8bb4..3da8b71c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import xiaozhi.modules.agent.dto.ContextProviderDTO; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentPluginMapping; @@ -21,4 +22,7 @@ public class AgentInfoVO extends AgentEntity @Schema(description = "插件列表Id") @TableField(typeHandler = JacksonTypeHandler.class) private List functions; + + @Schema(description = "上下文源配置") + private List contextProviders; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index 3ab3d977..7ddb4b82 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -20,10 +20,12 @@ import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.JsonUtils; import xiaozhi.modules.agent.dao.AgentVoicePrintDao; +import xiaozhi.modules.agent.entity.AgentContextProviderEntity; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentPluginMapping; import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentVoicePrintEntity; +import xiaozhi.modules.agent.service.AgentContextProviderService; import xiaozhi.modules.agent.service.AgentMcpAccessPointService; import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; @@ -53,6 +55,7 @@ public class ConfigServiceImpl implements ConfigService { private final TimbreService timbreService; private final AgentPluginMappingService agentPluginMappingService; private final AgentMcpAccessPointService agentMcpAccessPointService; + private final AgentContextProviderService agentContextProviderService; private final VoiceCloneService cloneVoiceService; private final AgentVoicePrintDao agentVoicePrintDao; @@ -178,6 +181,13 @@ public class ConfigServiceImpl implements ConfigService { mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/"); result.put("mcp_endpoint", mcpEndpoint); } + + // 获取上下文源配置 + AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(agent.getId()); + if (contextProviderEntity != null && contextProviderEntity.getContextProviders() != null && !contextProviderEntity.getContextProviders().isEmpty()) { + result.put("context_providers", contextProviderEntity.getContextProviders()); + } + // 获取声纹信息 buildVoiceprintConfig(agent.getId(), result); diff --git a/main/manager-api/src/main/resources/db/changelog/202512041515.sql b/main/manager-api/src/main/resources/db/changelog/202512041515.sql new file mode 100644 index 00000000..5e26b99f --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202512041515.sql @@ -0,0 +1,14 @@ +-- liquibase formatted sql + +-- changeset xiaozhi:202512041515 +CREATE TABLE ai_agent_context_provider ( + id VARCHAR(32) NOT NULL COMMENT '主键', + agent_id VARCHAR(32) NOT NULL COMMENT '智能体ID', + context_providers JSON COMMENT '上下文源配置', + creator BIGINT COMMENT '创建者', + created_at DATETIME COMMENT '创建时间', + updater BIGINT COMMENT '更新者', + updated_at DATETIME COMMENT '更新时间', + PRIMARY KEY (id), + INDEX idx_agent_id (agent_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体上下文源配置表'; diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index 993d2058..b0cdbc63 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -431,3 +431,10 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202512031517.sql + - changeSet: + id: 202512041515 + author: cgd + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202512041515.sql diff --git a/main/manager-web/src/components/ContextProviderDialog.vue b/main/manager-web/src/components/ContextProviderDialog.vue new file mode 100644 index 00000000..21d31278 --- /dev/null +++ b/main/manager-web/src/components/ContextProviderDialog.vue @@ -0,0 +1,329 @@ + + + + + diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index c9ea518c..16e0f19e 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -55,10 +55,24 @@ + +
+ + 已成功添加 {{ currentContextProviders.length }} 个源。如何部署上下文源 + + + 编辑源 + +
+
+ @@ -283,15 +302,17 @@ import Api from "@/apis/api"; import { getServiceUrl } from "@/apis/api"; import RequestService from "@/apis/httpRequest"; import FunctionDialog from "@/components/FunctionDialog.vue"; +import ContextProviderDialog from "@/components/ContextProviderDialog.vue"; import HeaderBar from "@/components/HeaderBar.vue"; import i18n from "@/i18n"; import featureManager from "@/utils/featureManager"; export default { name: "RoleConfigPage", - components: { HeaderBar, FunctionDialog }, + components: { HeaderBar, FunctionDialog, ContextProviderDialog }, data() { return { + showContextProviderDialog: false, form: { agentCode: "", agentName: "", @@ -329,6 +350,7 @@ export default { voiceDetails: {}, // 保存完整的音色信息 showFunctionDialog: false, currentFunctions: [], + currentContextProviders: [], allFunctions: [], originalFunctions: [], playingVoice: false, @@ -370,6 +392,7 @@ export default { paramInfo: item.params, }; }), + contextProviders: this.currentContextProviders, }; Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => { if (data.code === 0) { @@ -486,6 +509,9 @@ export default { }; // 后端只给了最小映射:[{ id, agentId, pluginId }, ...] const savedMappings = data.data.functions || []; + + // 加载上下文配置 + this.currentContextProviders = data.data.contextProviders || []; // 先保证 allFunctions 已经加载(如果没有,则先 fetchAllFunctions) const ensureFuncs = this.allFunctions.length @@ -660,6 +686,12 @@ export default { this.showFunctionDialog = true; } }, + openContextProviderDialog() { + this.showContextProviderDialog = true; + }, + handleUpdateContext(providers) { + this.currentContextProviders = providers; + }, handleUpdateFunctions(selected) { this.currentFunctions = selected; }, @@ -1374,4 +1406,18 @@ export default { height: 32px; margin-left: 8px; } + +.context-provider-item ::v-deep .el-form-item__label { + line-height: 42px !important; +} + +.doc-link { + color: #1677ff; + text-decoration: none; + margin-left: 4px; + + &:hover { + text-decoration: underline; + } +} diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt index 490b23d3..7fd90d21 100644 --- a/main/xiaozhi-server/agent-base-prompt.txt +++ b/main/xiaozhi-server/agent-base-prompt.txt @@ -74,6 +74,7 @@ - **今天农历:** {{lunar_date}} - **用户所在城市:** {{local_address}} - **当地未来7天天气:** {{weather_info}} +{{ dynamic_context }} diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index a4087395..13d9fcb3 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -113,6 +113,14 @@ wakeup_words: # MCP接入点地址,地址格式为:ws://你的mcp接入点ip或者域名:端口号/mcp/?token=你的token # 详细教程 https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md mcp_endpoint: 你的接入点 websocket地址 + +# 数据上下文填充配置 +# 用于在系统提示词中注入动态数据,如健康数据、股票信息等 +context_providers: + - url: "" + headers: + Authorization: "" + # 插件的基础配置 plugins: # 获取天气插件的配置,这里填写你的api_key diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 31bfa0c6..6b34e32f 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -162,7 +162,7 @@ class ConnectionHandler: self.conn_from_mqtt_gateway = False # 初始化提示词管理器 - self.prompt_manager = PromptManager(config, self.logger) + self.prompt_manager = PromptManager(self.config, self.logger) async def handle_connection(self, ws): try: @@ -630,6 +630,8 @@ class ConnectionHandler: self.chat_history_conf = int(private_config["chat_history_conf"]) if private_config.get("mcp_endpoint", None) is not None: self.config["mcp_endpoint"] = private_config["mcp_endpoint"] + if private_config.get("context_providers", None) is not None: + self.config["context_providers"] = private_config["context_providers"] # 使用 run_in_executor 在线程池中执行 initialize_modules,避免阻塞主循环 try: diff --git a/main/xiaozhi-server/core/utils/context_provider.py b/main/xiaozhi-server/core/utils/context_provider.py new file mode 100644 index 00000000..5f317540 --- /dev/null +++ b/main/xiaozhi-server/core/utils/context_provider.py @@ -0,0 +1,64 @@ +import httpx +from typing import Dict, Any, List +from config.logger import setup_logging + +TAG = __name__ + +class ContextDataProvider: + """数据上下文填充,负责从配置的API获取数据""" + + def __init__(self, config: Dict[str, Any], logger=None): + self.config = config + self.logger = logger or setup_logging() + self.context_data = "" + + def fetch_all(self, device_id: str) -> str: + """获取所有配置的上下文数据""" + context_providers = self.config.get("context_providers", []) + if not context_providers: + return "" + + formatted_lines = [] + for provider in context_providers: + url = provider.get("url") + headers = provider.get("headers", {}) + + if not url: + continue + + try: + headers = headers.copy() if isinstance(headers, dict) else {} + # 将 device_id 添加到请求头 + headers["device_id"] = device_id + + # 发送请求 + response = httpx.get(url, headers=headers, timeout=3) + + if response.status_code == 200: + result = response.json() + if isinstance(result, dict): + if result.get("code") == 0: + data = result.get("data") + # 格式化数据 + if isinstance(data, dict): + for k, v in data.items(): + formatted_lines.append(f"- **{k}:** {v}") + elif isinstance(data, list): + for item in data: + formatted_lines.append(f"- {item}") + else: + formatted_lines.append(f"- {data}") + else: + self.logger.bind(tag=TAG).warning(f"API {url} 返回错误码: {result.get('msg')}") + else: + self.logger.bind(tag=TAG).warning(f"API {url} 返回的不是JSON字典") + else: + self.logger.bind(tag=TAG).warning(f"API {url} 请求失败: {response.status_code}") + except Exception as e: + self.logger.bind(tag=TAG).error(f"获取上下文数据 {url} 失败: {e}") + + # 将所有格式化后的行拼接成一个字符串 + self.context_data = "\n".join(formatted_lines) + if self.context_data: + self.logger.bind(tag=TAG).debug(f"已注入动态上下文数据:\n{self.context_data}") + return self.context_data diff --git a/main/xiaozhi-server/core/utils/prompt_manager.py b/main/xiaozhi-server/core/utils/prompt_manager.py index 444b16ee..4d8d56af 100644 --- a/main/xiaozhi-server/core/utils/prompt_manager.py +++ b/main/xiaozhi-server/core/utils/prompt_manager.py @@ -4,7 +4,6 @@ """ import os -import cnlunar from typing import Dict, Any from config.logger import setup_logging from jinja2 import Template @@ -60,6 +59,11 @@ class PromptManager: self.cache_manager = cache_manager self.CacheType = CacheType + + # 初始化数据上下文填充 + from core.utils.context_provider import ContextDataProvider + self.context_provider = ContextDataProvider(config, self.logger) + self.context_data = {} self._load_base_template() @@ -184,6 +188,14 @@ class PromptManager: local_address = self._get_location_info(client_ip) # 获取天气信息(使用全局缓存) self._get_weather_info(conn, local_address) + + # 获取配置的上下文数据 + if hasattr(conn, "device_id") and conn.device_id: + if self.base_prompt_template and "dynamic_context" in self.base_prompt_template: + self.context_data = self.context_provider.fetch_all(conn.device_id) + else: + self.context_data = "" + self.logger.bind(tag=TAG).debug(f"上下文信息更新完成") except Exception as e: @@ -230,6 +242,7 @@ class PromptManager: emojiList=EMOJI_List, device_id=device_id, client_ip=client_ip, + dynamic_context=self.context_data, *args, **kwargs, )