mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
Merge branch 'main' into WebMenu
This commit is contained in:
@@ -80,6 +80,7 @@ VAD:
|
||||
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
9、[知识库ragflow集成指南](./ragflow-integration.md)<br/>
|
||||
10、[如何部署上下文源](./context-provider-integration.md)<br/>
|
||||
|
||||
### 11、语音克隆、本地语音部署相关教程
|
||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||
|
||||
@@ -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
|
||||
<context>
|
||||
- **客厅温度:** 26℃
|
||||
- **客厅湿度:** 45%
|
||||
- **大门状态:** 已关闭
|
||||
</context>
|
||||
```
|
||||
|
||||
**情况 2:返回列表**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
"您有10个待办事项",
|
||||
"当前汽车的行驶速度是100km每小时"
|
||||
]
|
||||
}
|
||||
```
|
||||
*注入效果:*
|
||||
```markdown
|
||||
<context>
|
||||
- 您有10个待办事项
|
||||
- 当前汽车的行驶速度是100km每小时
|
||||
</context>
|
||||
```
|
||||
|
||||
## 配置指南
|
||||
|
||||
### 方式 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
|
||||
<context>
|
||||
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||
- **设备ID:** {{device_id}}
|
||||
- **当前时间:** {{current_time}}
|
||||
...
|
||||
{{ dynamic_context }}
|
||||
</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服务已停止")
|
||||
```
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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<AgentContextProviderEntity> {
|
||||
}
|
||||
@@ -69,6 +69,9 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "排序", example = "1", nullable = true)
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "上下文源配置", nullable = true)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Data
|
||||
@Schema(description = "插件函数信息")
|
||||
public static class FunctionInfo implements Serializable {
|
||||
|
||||
@@ -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<String, Object> headers;
|
||||
}
|
||||
+43
@@ -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<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
+25
@@ -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<AgentContextProviderEntity> {
|
||||
/**
|
||||
* 根据智能体ID获取上下文源配置
|
||||
* @param agentId 智能体ID
|
||||
* @return 上下文源配置实体
|
||||
*/
|
||||
AgentContextProviderEntity getByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 保存或更新上下文源配置
|
||||
* @param entity 实体
|
||||
*/
|
||||
void saveOrUpdateByAgentId(AgentContextProviderEntity entity);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除上下文源配置
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteByAgentId(String agentId);
|
||||
}
|
||||
+35
@@ -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<AgentContextProviderDao, AgentContextProviderEntity> implements AgentContextProviderService {
|
||||
|
||||
@Override
|
||||
public AgentContextProviderEntity getByAgentId(String agentId) {
|
||||
return baseDao.selectOne(new QueryWrapper<AgentContextProviderEntity>().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<AgentContextProviderEntity>().eq("agent_id", agentId));
|
||||
}
|
||||
}
|
||||
+18
@@ -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<AgentDao, AgentEntity> imp
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -85,6 +88,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> 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<AgentDao, AgentEntity> 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);
|
||||
|
||||
@@ -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<AgentPluginMapping> functions;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
}
|
||||
|
||||
+10
@@ -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);
|
||||
|
||||
|
||||
@@ -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='智能体上下文源配置表';
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="dialogVisible"
|
||||
width="900px"
|
||||
title="编辑源"
|
||||
:close-on-click-modal="false"
|
||||
custom-class="context-provider-dialog"
|
||||
append-to-body
|
||||
>
|
||||
<div class="dialog-content">
|
||||
<el-empty v-if="localProviders.length === 0" description="暂无上下文API">
|
||||
<el-button type="primary" icon="el-icon-plus" @click="addProvider(0)">添加</el-button>
|
||||
</el-empty>
|
||||
|
||||
<div
|
||||
v-for="(provider, pIndex) in localProviders"
|
||||
:key="pIndex"
|
||||
class="provider-item"
|
||||
>
|
||||
<el-card class="provider-card" shadow="hover" :body-style="{ padding: '15px 20px' }">
|
||||
<!-- URL Row -->
|
||||
<div class="input-row">
|
||||
<span class="label-text">接口地址</span>
|
||||
<el-input
|
||||
v-model="provider.url"
|
||||
placeholder="http://api.example.com/data"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
></el-input>
|
||||
</div>
|
||||
|
||||
<!-- Headers Section -->
|
||||
<div class="headers-section">
|
||||
<div class="label-text" style="margin-top: 6px;">请求头</div>
|
||||
<div class="headers-list">
|
||||
<div
|
||||
v-for="(header, hIndex) in provider.headers"
|
||||
:key="hIndex"
|
||||
class="header-row"
|
||||
>
|
||||
<el-input
|
||||
v-model="header.key"
|
||||
placeholder="Key"
|
||||
size="small"
|
||||
style="width: 180px;"
|
||||
></el-input>
|
||||
<span class="separator">:</span>
|
||||
<el-input
|
||||
v-model="header.value"
|
||||
placeholder="Value"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
></el-input>
|
||||
|
||||
<div class="row-controls">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
circle
|
||||
size="mini"
|
||||
plain
|
||||
@click="addHeader(pIndex, hIndex + 1)"
|
||||
></el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-minus"
|
||||
circle
|
||||
size="mini"
|
||||
plain
|
||||
@click="removeHeader(pIndex, hIndex)"
|
||||
></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Empty Headers State -->
|
||||
<div v-if="provider.headers.length === 0" class="header-row empty-header">
|
||||
<span class="no-header-text">暂无 Headers</span>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="addHeader(pIndex, 0)"
|
||||
>添加 Header</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Provider Block Controls (Right Side) -->
|
||||
<div class="block-controls">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
circle
|
||||
size="medium"
|
||||
@click="addProvider(pIndex + 1)"
|
||||
></el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-minus"
|
||||
circle
|
||||
size="medium"
|
||||
@click="removeProvider(pIndex)"
|
||||
></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContextProviderDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
providers: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localProviders: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.visible;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
this.initLocalData();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initLocalData() {
|
||||
// 深拷贝并将 headers 对象转换为数组
|
||||
this.localProviders = this.providers.map(p => {
|
||||
const headers = p.headers || {};
|
||||
return {
|
||||
url: p.url || '',
|
||||
headers: Object.entries(headers).map(([key, value]) => ({ key, value }))
|
||||
};
|
||||
});
|
||||
|
||||
// 如果为空,添加一个默认块
|
||||
if (this.localProviders.length === 0) {
|
||||
this.localProviders.push({ url: '', headers: [{ key: '', value: '' }] });
|
||||
}
|
||||
},
|
||||
addProvider(index) {
|
||||
this.localProviders.splice(index, 0, {
|
||||
url: '',
|
||||
headers: [{ key: '', value: '' }]
|
||||
});
|
||||
},
|
||||
removeProvider(index) {
|
||||
this.localProviders.splice(index, 1);
|
||||
},
|
||||
addHeader(pIndex, hIndex) {
|
||||
this.localProviders[pIndex].headers.splice(hIndex, 0, { key: '', value: '' });
|
||||
},
|
||||
removeHeader(pIndex, hIndex) {
|
||||
this.localProviders[pIndex].headers.splice(hIndex, 1);
|
||||
},
|
||||
handleConfirm() {
|
||||
const result = this.localProviders
|
||||
.filter(p => p.url.trim() !== '')
|
||||
.map(p => {
|
||||
const headersObj = {};
|
||||
p.headers.forEach(h => {
|
||||
if (h.key.trim()) {
|
||||
headersObj[h.key.trim()] = h.value;
|
||||
}
|
||||
});
|
||||
return {
|
||||
url: p.url.trim(),
|
||||
headers: headersObj
|
||||
};
|
||||
});
|
||||
|
||||
this.$emit('confirm', result);
|
||||
this.dialogVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px 25px;
|
||||
}
|
||||
|
||||
.dialog-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.dialog-content::-webkit-scrollbar-thumb {
|
||||
background: #dcdfe6;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.dialog-content::-webkit-scrollbar-track {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-left: 4px solid #409EFF; /* 左侧强调色 */
|
||||
background-color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.block-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
width: 60px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
line-height: 32px; /* 垂直居中对齐 */
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.headers-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.headers-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: #fcfcfc;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.headers-list:hover {
|
||||
border-color: #c0c4cc;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: #909399;
|
||||
font-weight: bold;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.row-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.header-row:hover .row-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-header {
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.no-header-text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -55,10 +55,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="上下文源:" class="context-provider-item">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<span style="color: #606266; font-size: 13px;">
|
||||
已成功添加 {{ currentContextProviders.length }} 个源。<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/master/docs/context-provider-integration.md" target="_blank" class="doc-link">如何部署上下文源</a>
|
||||
</span>
|
||||
<el-button
|
||||
class="edit-function-btn"
|
||||
size="small"
|
||||
@click="openContextProviderDialog"
|
||||
>
|
||||
编辑源
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('roleConfig.roleIntroduction') + ':'">
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="9"
|
||||
rows="8"
|
||||
resize="none"
|
||||
:placeholder="$t('roleConfig.pleaseEnterContent')"
|
||||
v-model="form.systemPrompt"
|
||||
@@ -71,7 +85,7 @@
|
||||
<el-form-item :label="$t('roleConfig.memoryHis') + ':'">
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="6"
|
||||
rows="4"
|
||||
resize="none"
|
||||
v-model="form.summaryMemory"
|
||||
maxlength="2000"
|
||||
@@ -275,6 +289,11 @@
|
||||
@update-functions="handleUpdateFunctions"
|
||||
@dialog-closed="handleDialogClosed"
|
||||
/>
|
||||
<context-provider-dialog
|
||||
:visible.sync="showContextProviderDialog"
|
||||
:providers="currentContextProviders"
|
||||
@confirm="handleUpdateContext"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
- **今天农历:** {{lunar_date}}
|
||||
- **用户所在城市:** {{local_address}}
|
||||
- **当地未来7天天气:** {{weather_info}}
|
||||
{{ dynamic_context }}
|
||||
</context>
|
||||
|
||||
<memory>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user