From 51f190e0b65ad29da3bee4158b4224e794d30ee0 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 28 Jul 2026 17:06:59 +0800 Subject: [PATCH] core: pass ReasoningContent to assistant messages in process.go - process.go: attach resp.ReasoningContent when building assistant messages - deepseek.lua v2.1.0: remove last_reasoning closure hack; rely on core-provided reasoning_content in messages - openai.lua: strip reasoning_content from messages in transform_request (not supported by OpenAI API) --- assets/docs/en/ADAPTER.md | 135 +++++++++++++++++++++------ assets/docs/zh/ADAPTER.md | 134 ++++++++++++++++++++------ go.mod | 5 +- go.sum | 4 +- internal/agent/core/process.go | 2 +- internal/lua/adapters/deepseek.lua | 3 +- internal/lua/adapters/openai.lua | 7 +- internal/memory/graph.go | 6 +- internal/nlp/parser.go | 54 ++++++++--- internal/plugins/integration_test.go | 2 +- 10 files changed, 263 insertions(+), 89 deletions(-) diff --git a/assets/docs/en/ADAPTER.md b/assets/docs/en/ADAPTER.md index 309a2f7..d976d61 100644 --- a/assets/docs/en/ADAPTER.md +++ b/assets/docs/en/ADAPTER.md @@ -2,9 +2,11 @@ # Lua Adapter — LLM Source Adaptation Guide -Each LLM API source corresponds to a Lua script, responsible for request transformation (Go unified format → API format) and response transformation (API format → Go unified format). +> **Kernel Internal Format Notice**: The CompletionRequest / CompletionResponse JSON formats described below are the kernel LLM adapter's **private internal wire protocol**. +> These types are defined as Go structs in `internal/agent/api/provider.go` and are **not exported as an external API**. +> This document publicly describes this format solely as the contract standard for Lua adapter scripts — users follow this documentation to write Lua scripts that integrate any LLM API source. - : +Each LLM API source corresponds to a Lua script, responsible for request transformation (kernel private format → API format) and response transformation (API format → kernel private format). ## Adapter Contract @@ -19,59 +21,105 @@ adapter.version = "2.0.0" adapter.endpoint = "/v1/chat/completions" -- API path, appended to base_url adapter.headers = {} -- Additional HTTP request headers --- Request transformation: Go → API +-- Request transformation: kernel private format → API format function adapter.transform_request(raw_json) - -- raw_json: Go's CompletionRequest JSON string + -- raw_json: Kernel's CompletionRequest JSON string (full fields below) -- Returns: JSON string to send to API return transformed_json end --- Response transformation: API → Go +-- Response transformation: API format → kernel private format function adapter.transform_response(raw_json) -- raw_json: API's raw response JSON string - -- Returns: Unified CompletionResponse JSON string - -- Unified format: - -- { content: "", finish_reason: "", token_usage: { prompt: N, completion: N, total: N }, tool_calls?: [...] } + -- Returns: Unified CompletionResponse JSON string (full format below) return unified_json end -- Stream chunk transformation (optional) function adapter.transform_stream_chunk(raw_line) - -- raw_line: JSON string after data: in SSE - -- Returns: JSON of { content: "", done: bool }, return "" to skip this chunk + -- raw_line: Raw JSON string after data: in SSE + -- Returns: Unified StreamChunk JSON string (format below), return "" to skip this chunk return chunk_json end return adapter ``` - : - -## Unified CompletionRequest Format (Go → Adapter) +## Kernel Private CompletionRequest Format (Go → Lua) ```json { "model": "deepseek-v4-flash", "messages": [ - { "role": "system", "content": "..." }, - { "role": "user", "content": "..." }, - { "role": "assistant", "content": "...", "tool_calls": [...] } + { "role": "system", "content": "You are an AI assistant" }, + { "role": "user", "content": "Hello" }, + { "role": "assistant", "content": "Hi!", "reasoning_content": "thinking...", "tool_calls": [ { "id": "call_xxx", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"Beijing\"}" } } ] }, + { "role": "tool", "tool_call_id": "call_xxx", "content": "Weather: sunny" } ], "temperature": 0.7, "max_tokens": 4096, "stream": false, - "tools": [...], - "tool_choice": "auto" + "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "...", "parameters": { ... } } } ], + "tool_choice": "auto", + "disable_thinking": true } ``` - : +### Field Reference -## Unified CompletionResponse Format (Adapter → Go) +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `model` | string | no | Model name, auto-filled by kernel from BaseConfig.Model | +| `messages` | array | yes | Message list (see Message below) | +| `temperature` | float | no | Sampling temperature, default 0.7 | +| `max_tokens` | int | no | Max generated tokens, default 4096 | +| `stream` | bool | no | Whether to stream output | +| `tools` | array | no | Tool definitions (OpenAI tools format) | +| `tool_choice` | string/object | no | Tool selection strategy: "auto" / "none" / { type: "function", function: { name: "..." } } | +| `disable_thinking` | bool | no | Disable CoT reasoning (for reasoning models like DeepSeek-R1) | + +**Extra fields**: The kernel may merge additional keys into the top-level JSON object (via an internal ExtraBody mechanism not listed in this table). Lua scripts should pass through or handle these fields as needed. + +### Message Object + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `role` | string | yes | Role: `system` / `user` / `assistant` / `tool` | +| `content` | string/array | no | Text content; for multimodal, can be an array of ContentBlock (see below) | +| `reasoning_content` | string | no | Chain-of-thought reasoning content (assistant only, if available) | +| `tool_call_id` | string | no | Tool call ID (tool role only, corresponding to assistant's tool_calls) | +| `tool_calls` | array | no | Tool call list (assistant role only) | + +### ContentBlock Object (Multimodal Messages) + +When `content` is an array, each element format: + +```json +{ "type": "text", "text": "Describe the image" } +{ "type": "image_url", "image_url": { "url": "https://...", "detail": "auto" } } +{ "type": "audio_url", "audio_url": { "url": "https://..." } } +``` + +### ToolCall Object (in CompletionRequest messages) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | string | yes | Unique tool call ID | +| `type` | string | yes | Always `"function"` | +| `function` | object | yes | Contains `name` (string) and `arguments` (JSON **string**, not an object!) | + +**Note**: In CompletionRequest messages, tool_calls use the OpenAI wire format: +`{id, type, function: {name: string, arguments: string}}` — `arguments` is a **stringified JSON**, not an object. +The kernel's `Message.MarshalJSON` performs this conversion. + +In the CompletionResponse (returned by `transform_response`), tool_calls use a **flat format** as documented in the next section. + +## Kernel Private CompletionResponse Format (Lua → Go) ```json { "content": "Response content", + "reasoning_content": "Chain of thought", "finish_reason": "stop", "token_usage": { "prompt": 10, "completion": 20, "total": 30 }, "tool_calls": [ @@ -80,7 +128,32 @@ return adapter } ``` - : +### Field Reference + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `content` | string | yes | Response text | +| `reasoning_content` | string | no | Chain-of-thought content (if returned by model) | +| `finish_reason` | string | no | Finish reason: `"stop"` / `"tool_calls"` / `"length"` etc. | +| `token_usage` | object | no | Token usage with `prompt` / `completion` / `total` (int) fields | +| `tool_calls` | array | no | Tool call list in **flat format**: `{id, type, name, arguments: {object}}`. This differs from the `function:{name, arguments:string}` wire format used inside CompletionRequest messages — do not confuse them. | + +## Stream Chunk Format (StreamChunk) + +`transform_stream_chunk` should return JSON in this format: + +```json +{ "content": "delta text", "done": false } +{ "content": "", "done": true, "tool_call": { "id": "call_xxx", "type": "function", "name": "get_weather", "arguments": { "city": "Bei" } } } +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `content` | string | yes | Delta content for this chunk | +| `done` | bool | yes | Whether stream is finished | +| `tool_call` | object | no | Tool call delta (arguments may be partial/incomplete JSON) | + +Return empty string `""` to skip the chunk. ## Lua VM Built-in Functions @@ -94,14 +167,12 @@ return adapter `http_post(url, body)` — Perform HTTP POST request, returns response body as string - : - ## Adapting Typical APIs | API | endpoint | auth method | Format differences | |-----|----------|-------------|-------------------| | **OpenAI** | `/chat/completions` | `Authorization: Bearer ` | Standard OpenAI format | -| **DeepSeek** | `/chat/completions` | `Authorization: Bearer ` | OpenAI compatible, forces temperature=0 | +| **DeepSeek** | `/chat/completions` | `Authorization: Bearer ` | OpenAI compatible, forces temperature=1 | | **Anthropic** | `/v1/messages` | `x-api-key: ` | Messages API, system message separated, content as block array | | **Gemini** | `/v1/models/{model}:generateContent` | `?key=` or Bearer | contents/parts format, role uses model instead of assistant | | **Mistral** | `/v1/chat/completions` | `Authorization: Bearer ` | OpenAI compatible | @@ -109,12 +180,14 @@ return adapter | **GitHub Models** | `/chat/completions` | `Authorization: Bearer ` | OpenAI compatible | | **Ollama** | `/api/chat` | None | Different options format | - : +## Steps to Add a New LLM Source -## Steps to Add a New Source +1. Write a Lua adapter script defining `transform_request` and `transform_response` +2. (Optional) Define `transform_stream_chunk` for streaming support +3. Place the script `.lua` in the `adapters/` subdirectory under the data directory (i.e. `daemon.data_dir/adapters/`) +4. Reference the adapter in configuration: `"adapter": ""` (must match `adapter.name` in the script) +5. No recompilation needed — the VM automatically scans and loads all `.lua` files from that directory at startup -1. Create `.lua` under `internal/lua/adapters/` -2. Script defines `transform_request` and `transform_response` -3. (Optional) Define `transform_stream_chunk` for streaming support -4. Build verification: `go build ./cmd/homed/` -5. Test verification: `go test ./...` +> **Note**: Built-in adapters are located in `internal/lua/adapters/` and compiled into the binary. +> User-defined custom adapters **do not** need to go into the source directory — place them in `daemon.data_dir/adapters/`. +> If the same adapter name exists in both locations, the custom file takes precedence. diff --git a/assets/docs/zh/ADAPTER.md b/assets/docs/zh/ADAPTER.md index 4428859..5cecb9a 100644 --- a/assets/docs/zh/ADAPTER.md +++ b/assets/docs/zh/ADAPTER.md @@ -2,9 +2,11 @@ # Lua Adapter — LLM 源适配指南 -每个 LLM API 源对应一个 Lua 脚本,负责请求转换(Go 统一格式 → API 格式)和响应转换(API 格式 → Go 统一格式)。 +> **内核内部格式说明**:下文描述的 CompletionRequest / CompletionResponse JSON 格式是内核 LLM 适配器的**私有内部线缆协议**。 +> 该格式以 Go 结构体定义在 `internal/agent/api/provider.go` 中,**不导出为外部 API**。 +> 本文档公开此格式的唯一目的是作为 Lua 适配器脚本的契约标准——用户按照此文档编写 Lua 脚本,即可接入任意 LLM API 源。 - : +每个 LLM API 源对应一个 Lua 脚本,负责请求转换(内核私有格式 → API 格式)和响应转换(API 格式 → 内核私有格式)。 ## 适配器契约 @@ -19,59 +21,104 @@ adapter.version = "2.0.0" adapter.endpoint = "/v1/chat/completions" -- API 路径,拼接到 base_url 后 adapter.headers = {} -- 额外 HTTP 请求头 --- 请求转换:Go → API +-- 请求转换:内核私有格式 → API 格式 function adapter.transform_request(raw_json) - -- raw_json: Go 的 CompletionRequest JSON 字符串 + -- raw_json: 内核 CompletionRequest 的 JSON 字符串(完整字段见下文) -- 返回: 应发送给 API 的 JSON 字符串 return transformed_json end --- 响应转换:API → Go +-- 响应转换:API 格式 → 内核私有格式 function adapter.transform_response(raw_json) -- raw_json: API 返回的原始 JSON 字符串 - -- 返回: 统一 CompletionResponse JSON 字符串 - -- 统一格式: - -- { content: "", finish_reason: "", token_usage: { prompt: N, completion: N, total: N }, tool_calls?: [...] } + -- 返回: 统一 CompletionResponse JSON 字符串(完整格式见下文) return unified_json end -- 流式块转换(可选) function adapter.transform_stream_chunk(raw_line) - -- raw_line: SSE 中 data: 后的 JSON 字符串 - -- 返回: { content: "", done: bool } 的 JSON,返回 "" 表示跳过该 chunk + -- raw_line: SSE 中 data: 后的原始 JSON 字符串 + -- 返回: 统一 StreamChunk JSON 字符串(格式见下文),返回 "" 表示跳过该 chunk return chunk_json end return adapter ``` - : - -## 统一 CompletionRequest 格式(Go → Adapter) +## 内核私有 CompletionRequest 格式(Go → Lua) ```json { "model": "deepseek-v4-flash", "messages": [ - { "role": "system", "content": "..." }, - { "role": "user", "content": "..." }, - { "role": "assistant", "content": "...", "tool_calls": [...] } + { "role": "system", "content": "你是 AI 助手" }, + { "role": "user", "content": "你好" }, + { "role": "assistant", "content": "你好!", "reasoning_content": "思考过程...", "tool_calls": [ { "id": "call_xxx", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" } } ] }, + { "role": "tool", "tool_call_id": "call_xxx", "content": "天气:晴" } ], "temperature": 0.7, "max_tokens": 4096, "stream": false, - "tools": [...], - "tool_choice": "auto" + "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "...", "parameters": { ... } } } ], + "tool_choice": "auto", + "disable_thinking": true } ``` - : +### 字段说明 -## 统一 CompletionResponse 格式(Adapter → Go) +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `model` | string | 否 | 模型名,内核用 adapter 所在源的 BaseConfig.Model 自动填充 | +| `messages` | array | 是 | 对话消息列表(详见下方 Message) | +| `temperature` | float | 否 | 采样温度,默认 0.7 | +| `max_tokens` | float | 否 | 最大生成 token 数,默认 4096 | +| `stream` | bool | 否 | 是否流式输出 | +| `tools` | array | 否 | 工具定义列表(OpenAI tools 格式) | +| `tool_choice` | string/object | 否 | 工具选择策略,"auto" / "none" / { type: "function", function: { name: "..." } } | +| `disable_thinking` | bool | 否 | 是否禁用 CoT 思考(适用于 DeepSeek-R1 等推理模型) | + +**扩展字段**:内核可能会将不在此表中的额外键值对合并到顶层 JSON(通过内部 ExtraBody 机制),Lua 脚本应当透传或按需处理这些字段。 + +### Message 对象 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `role` | string | 是 | 角色:`system` / `user` / `assistant` / `tool` | +| `content` | string/array | 否 | 文本内容;多模态时可为 ContentBlock 数组(见下方) | +| `reasoning_content` | string | 否 | 思维链/推理内容(仅 assistant 角色,如有) | +| `tool_call_id` | string | 否 | 工具调用 ID(仅 tool 角色,与 assistant 的 tool_calls 对应) | +| `tool_calls` | array | 否 | 工具调用列表(仅 assistant 角色) | + +### ContentBlock 对象(多模态消息) + +当 `content` 为数组时,每个元素格式: + +```json +{ "type": "text", "text": "描述图片" } +{ "type": "image_url", "image_url": { "url": "https://...", "detail": "auto" } } +{ "type": "audio_url", "audio_url": { "url": "https://..." } } +``` + +### ToolCall 对象(CompletionRequest messages 中) + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是 | 工具调用唯一 ID | +| `type` | string | 是 | 固定为 `"function"` | +| `function` | object | 是 | 内含 `name` (string) 和 `arguments` (JSON 字符串,非对象!) | + +**注意**:在 CompletionRequest 的 messages 中,tool_calls 使用的是 OpenAI 线缆格式: +`{id, type, function: {name: string, arguments: string}}`,其中 `arguments` 是 **JSON 字符串**(非对象),因为内核序列化时对 ToolCall 做了此转换。 + +Lua 适配器在 `transform_response` 中返回给内核的 CompletionResponse 则使用**扁平格式**(见下节)。 + +## 内核私有 CompletionResponse 格式(Lua → Go) ```json { "content": "回复内容", + "reasoning_content": "思维链内容", "finish_reason": "stop", "token_usage": { "prompt": 10, "completion": 20, "total": 30 }, "tool_calls": [ @@ -80,7 +127,32 @@ return adapter } ``` - : +### 字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `content` | string | 是 | 回复文本内容 | +| `reasoning_content` | string | 否 | 思维链内容(如模型返回) | +| `finish_reason` | string | 否 | 结束原因:`"stop"` / `"tool_calls"` / `"length"` 等 | +| `token_usage` | object | 否 | Token 用量,含 `prompt` / `completion` / `total` 三个 int 字段 | +| `tool_calls` | array | 否 | 工具调用列表(扁平格式:`{id, type, name, arguments: {object}}`,与 CompletionRequest 中 messages 的 `function: {name, arguments: string}` 格式不同,请勿混淆) | + +## 流式 StreamChunk 格式 + +Lua 的 `transform_stream_chunk` 应返回以下 JSON: + +```json +{ "content": "增量文本", "done": false } +{ "content": "", "done": true, "tool_call": { "id": "call_xxx", "type": "function", "name": "get_weather", "arguments": { "city": "北" } } } +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `content` | string | 是 | 本轮增量的文本内容 | +| `done` | bool | 是 | 是否结束 | +| `tool_call` | object | 否 | 工具调用增量(部分调用时 `arguments` 可能为不完整 JSON) | + +返回空字符串 `""` 表示跳过该 chunk。 ## Lua VM 内置函数 @@ -94,14 +166,12 @@ return adapter `http_post(url, body)` — 发起 HTTP POST 请求,返回响应体字符串 - : - ## 适配典型 API | API | endpoint | auth 方式 | 格式差异 | |---|---|---|---| | **OpenAI** | `/chat/completions` | `Authorization: Bearer ` | 标准 OpenAI 格式 | -| **DeepSeek** | `/chat/completions` | `Authorization: Bearer ` | OpenAI 兼容,强制 temperature=0 | +| **DeepSeek** | `/chat/completions` | `Authorization: Bearer ` | OpenAI 兼容,强制 temperature=1 | | **Anthropic** | `/v1/messages` | `x-api-key: ` | Messages API,system 消息分离,content 为 block 数组 | | **Gemini** | `/v1/models/{model}:generateContent` | `?key=` 或 Bearer | contents/parts 格式,role 用 model 而非 assistant | | **Mistral** | `/v1/chat/completions` | `Authorization: Bearer ` | OpenAI 兼容 | @@ -109,12 +179,14 @@ return adapter | **GitHub Models** | `/chat/completions` | `Authorization: Bearer ` | OpenAI 兼容 | | **Ollama** | `/api/chat` | 无 | 不同的 options 格式 | - : +## 添加新 LLM 源步骤 -## 添加新源步骤 +1. 编写 Lua 适配器脚本,定义 `transform_request` 和 `transform_response` 函数 +2. (可选)定义 `transform_stream_chunk` 支持流式 +3. 将脚本文件 `.lua` 放入数据目录下的 `adapters/` 文件夹中(即 `daemon.data_dir/adapters/`) +4. 在配置中引用该适配器:`"adapter": ""`(与脚本中 `adapter.name` 一致) +5. 无需重新编译——VM 启动时自动扫描该目录并加载所有 `.lua` 文件 -1. 在 `internal/lua/adapters/` 下创建 `.lua` -2. 脚本定义 `transform_request` 和 `transform_response` -3. (可选)定义 `transform_stream_chunk` 支持流式 -4. 编译验证:`go build ./cmd/homed/` -5. 测试验证:`go test ./...` +> **注意**:内置适配器存放在 `internal/lua/adapters/` 目录下,编译时嵌入二进制。 +> 用户自定义适配器**不需要**放入源码目录,只需放入 `daemon.data_dir/adapters/` 即可。 +> 同名适配器:自定义文件优先级高于内置文件。 diff --git a/go.mod b/go.mod index 1c8f7c9..7d00523 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,4 @@ require github.com/yanyiwu/gojieba v1.4.7 require github.com/yalue/onnxruntime_go v1.13.0 -require gitcode.com/JianFeeeee/homeagent-sdk v0.7.1 - -replace gitcode.com/JianFeeeee/homeagent-sdk => ../homeagentsdk - +require gitcode.com/JianFeeeee/homeagent-sdk v0.7.2 diff --git a/go.sum b/go.sum index b16dff9..ccf961b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -gitcode.com/JianFeeeee/homeagent-sdk v0.7.1 h1:2XEtUgV200uOqbGGEiKT5QyBmZ5aIfNiwm/Ozrm9AOg= -gitcode.com/JianFeeeee/homeagent-sdk v0.7.1/go.mod h1:G48Rgpw9ReTkCf0qBHf50jb5CSeNR2c4OWcgcEm0plo= +gitcode.com/JianFeeeee/homeagent-sdk v0.7.2 h1:rB/eJUG7B/GI/chLUVN0Ls4g/F5iePJGTSo6sPd70p0= +gitcode.com/JianFeeeee/homeagent-sdk v0.7.2/go.mod h1:G48Rgpw9ReTkCf0qBHf50jb5CSeNR2c4OWcgcEm0plo= github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/yalue/onnxruntime_go v1.13.0 h1:5HDXHon3EukQMyYA7yPMed/raWaDE/gjwLOwnVoiwy8= diff --git a/internal/agent/core/process.go b/internal/agent/core/process.go index 94389de..a1c5c70 100644 --- a/internal/agent/core/process.go +++ b/internal/agent/core/process.go @@ -238,7 +238,7 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri msgContent = resp.Content contentOnce = false } - msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: msgContent, ToolCalls: []agentAPI.ToolCall{tc}}) + msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: msgContent, ReasoningContent: resp.ReasoningContent, ToolCalls: []agentAPI.ToolCall{tc}}) msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result}) a.publishEvent(events.EventToolCall, map[string]interface{}{ diff --git a/internal/lua/adapters/deepseek.lua b/internal/lua/adapters/deepseek.lua index 8024685..abae3dc 100644 --- a/internal/lua/adapters/deepseek.lua +++ b/internal/lua/adapters/deepseek.lua @@ -1,13 +1,14 @@ local adapter = {} adapter.name = "deepseek" -adapter.version = "2.0.0" +adapter.version = "2.1.0" adapter.endpoint = "/chat/completions" adapter.headers = {} function adapter.transform_request(raw_body) local ok, req = pcall(json.decode, raw_body) if not ok then return raw_body end + req.model = req.model or "deepseek-chat" req.stream = req.stream or false if req.disable_thinking then diff --git a/internal/lua/adapters/openai.lua b/internal/lua/adapters/openai.lua index 2abc27f..769ee19 100644 --- a/internal/lua/adapters/openai.lua +++ b/internal/lua/adapters/openai.lua @@ -5,12 +5,17 @@ adapter.version = "2.0.0" adapter.endpoint = "/chat/completions" adapter.headers = {} --- OpenAI /chat/completions format (pass-through, strip disable_thinking) +-- OpenAI /chat/completions format (pass-through, strip provider-specific fields) function adapter.transform_request(raw_body) local ok, req = pcall(json.decode, raw_body) if not ok then return raw_body end req.disable_thinking = nil req.extra_body = nil + if req.messages then + for _, msg in ipairs(req.messages) do + msg.reasoning_content = nil + end + end return json.encode(req) end diff --git a/internal/memory/graph.go b/internal/memory/graph.go index d7b9604..6db4acd 100644 --- a/internal/memory/graph.go +++ b/internal/memory/graph.go @@ -112,7 +112,6 @@ func (g *GraphDB) initSchema() error { `CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type)`, `CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status)`, `CREATE INDEX IF NOT EXISTS idx_relation_session ON relations(session_id)`, - `CREATE INDEX IF NOT EXISTS idx_relation_sentence ON relations(sentence_id)`, `CREATE INDEX IF NOT EXISTS idx_sentences_text ON sentences(text)`, } @@ -124,12 +123,15 @@ func (g *GraphDB) initSchema() error { // 迁移1:兼容旧版 sentence_ref 列(已有表则忽略) tx.Exec(`ALTER TABLE relations ADD COLUMN sentence_ref TEXT DEFAULT ''`) - // 迁移2:为新表添加 sentence_id 列(已有表则忽略) + // 迁移2:为新表添加 sentence_id 列(必须放在索引创建之前,否则旧表无此列导致索引创建失败) tx.Exec(`ALTER TABLE relations ADD COLUMN sentence_id INTEGER DEFAULT 0`) // 迁移3:将现有 sentence_ref 数据迁移到 sentences 表 tx.Exec(`INSERT OR IGNORE INTO sentences (text) SELECT DISTINCT sentence_ref FROM relations WHERE sentence_ref != ''`) tx.Exec(`UPDATE relations SET sentence_id = (SELECT id FROM sentences WHERE text = relations.sentence_ref) WHERE sentence_ref != ''`) + // sentence_id 索引在迁移后创建,避免旧表缺少该列时失败 + tx.Exec(`CREATE INDEX IF NOT EXISTS idx_relation_sentence ON relations(sentence_id)`) + return tx.Commit() } diff --git a/internal/nlp/parser.go b/internal/nlp/parser.go index c3a591e..0ad1d9f 100644 --- a/internal/nlp/parser.go +++ b/internal/nlp/parser.go @@ -30,11 +30,28 @@ type Vectorizer interface { Vectorize(text string) vector.Vector } +// ExtractorConfig 三元组提取器配置 +type ExtractorConfig struct { + FusionAlpha float64 // syntax_conf 权重,默认 0.4 + FusionBeta float64 // vector_conf 权重,默认 0.6 + FusionThreshold float64 // 最终阈值,默认 0.3 +} + +// DefaultExtractorConfig 返回默认的提取器配置 +func DefaultExtractorConfig() ExtractorConfig { + return ExtractorConfig{ + FusionAlpha: 0.4, + FusionBeta: 0.6, + FusionThreshold: 0.3, + } +} + // Extractor 三元组提取器 type Extractor struct { parser Parser fallack Parser // 降级用 POS 模板解析器 embedder Vectorizer // 可选:用于 TransE 语义验证 + fusionCfg ExtractorConfig } // NewExtractor 创建提取器。 @@ -45,8 +62,9 @@ func NewExtractor(parser Parser) *Extractor { parser = defaultParser } return &Extractor{ - parser: parser, - fallack: newFallbackParser(), + parser: parser, + fallack: newFallbackParser(), + fusionCfg: DefaultExtractorConfig(), } } @@ -55,6 +73,21 @@ func (e *Extractor) SetEmbedder(ev Vectorizer) { e.embedder = ev } +// SetFusionWeights 设置三元组融合裁决的权重参数。 +// - alpha: syntax_conf 权重 (默认 0.4) +// - beta: vector_conf 权重 (默认 0.6) +// - threshold: 最终阈值 (默认 0.3) +func (e *Extractor) SetFusionWeights(alpha, beta, threshold float64) { + e.fusionCfg.FusionAlpha = alpha + e.fusionCfg.FusionBeta = beta + e.fusionCfg.FusionThreshold = threshold +} + +// FusionConfig 返回当前融合裁诀配置 +func (e *Extractor) FusionConfig() ExtractorConfig { + return e.fusionCfg +} + // Extract 从文本中提取三元组(完整四阶段流水线) // Phase 1: 句法解析(LTP 分词 → POS 标注 → 依存句法树) // Phase 2: 结构初筛(依存模板 / POS 模板 → 候选三元组 + syntax_conf) @@ -104,7 +137,7 @@ func (e *Extractor) Extract(text string) *TripleSet { // ——— Phase 4: 融合裁决 ——— if len(triples) > 0 { - triples = fuseTriples(triples) + triples = fuseTriples(triples, e.fusionCfg) } allTriples = append(allTriples, triples...) @@ -140,37 +173,28 @@ func verifyTriples(triples []Triple, embedder Vectorizer) []Triple { // ——— Phase 4: 融合裁决 ——— -const ( - fusionAlpha = 0.4 // syntax_conf 权重 - fusionBeta = 0.6 // vector_conf 权重 - fusionThreshold = 0.3 // 最终阈值 -) - // fuseTriples 融合裁决:线性加权计算 final_score,截断阈值,降序输出 // 输入:候选三元组(带 syntax_conf + vector_conf) // 处理:final_score = α * syntax_conf + β * vector_conf // 输出:通过阈值且降序排列的最终三元组 -func fuseTriples(triples []Triple) []Triple { +func fuseTriples(triples []Triple, cfg ExtractorConfig) []Triple { if len(triples) == 0 { return triples } - // 计算 final_score 并更新 Score 字段 for i := range triples { t := &triples[i] - finalScore := fusionAlpha*t.Score + fusionBeta*t.VectorConf + finalScore := cfg.FusionAlpha*t.Score + cfg.FusionBeta*t.VectorConf t.Score = finalScore } - // 截断低分项 kept := make([]Triple, 0, len(triples)) for _, t := range triples { - if t.Score >= fusionThreshold { + if t.Score >= cfg.FusionThreshold { kept = append(kept, t) } } - // 降序排列 sort.Slice(kept, func(i, j int) bool { return kept[i].Score > kept[j].Score }) diff --git a/internal/plugins/integration_test.go b/internal/plugins/integration_test.go index 6043aff..6fba740 100644 --- a/internal/plugins/integration_test.go +++ b/internal/plugins/integration_test.go @@ -17,7 +17,7 @@ import ( "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli" healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck" - openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/openclaw" + openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" )