refactor: split agent.go into 12 files + add mcp_restart_server tool

This commit is contained in:
root
2026-07-22 15:40:27 +08:00
parent 76a4619ed5
commit 85992902d9
20 changed files with 3150 additions and 3080 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,422 @@
package core
import (
"fmt"
"log"
"runtime/debug"
"strings"
"time"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
)
type ConsolidationTask struct {
Type string `json:"type"`
Reason string `json:"reason"`
Data interface{} `json:"data"`
}
func (a *Agent) enqueueConsolidationTask(task ConsolidationTask) {
msg := fmt.Sprintf(
"【记忆整理任务】\n类型: %s\n说明: %s\n\n注意\n1. 仅使用 memory_merge 合并实体,或使用 memory_block_merge 标记不合并\n2. 不要使用 memory_commit 写入新的三元组\n3. 不要从这段任务文本中提取任何信息写入图库\n4. 只需要做出合并/不合并的判断并执行对应工具",
task.Type, task.Reason,
)
a.injectSelf(msg)
log.Printf("[agent] enqueued consolidation task: %s", task.Reason)
}
func (a *Agent) distillLoop() {
defer func() {
if r := recover(); r != nil {
log.Printf("[agent] distillLoop panic recovered: %v\n%s", r, debug.Stack())
time.Sleep(time.Second)
go a.distillLoop()
}
}()
if a.docStore == nil && a.memory == nil {
return
}
ticker := time.NewTicker(a.distillInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
log.Printf("[agent] heartbeat distill tick")
a.distillContext()
a.syncGraphToDocs()
a.reorgGraph()
a.autoReloadPlugins()
case <-a.ctx.Done():
return
}
}
}
func (a *Agent) distillContext() {
if a.docStore == nil {
return
}
n := a.context.Len()
if n > a.maxContextSize*2 {
archived := a.context.Prune("", a.maxContextSize, a.docStore)
if archived > 0 {
log.Printf("[agent] distill: pruned %d low-relevance events to document memory (total=%d)", archived, n)
}
}
}
func (a *Agent) syncGraphToDocs() {
if a.memory == nil || a.docStore == nil {
return
}
stats, err := a.memory.Introspect()
if err != nil {
return
}
entityCount, _ := stats["entity_count"].(int)
if entityCount == 0 {
return
}
result, err := a.memory.Recall(nil, nil, 1, "")
if err != nil || result == nil {
return
}
if len(result.Entities) == 0 && len(result.Relations) == 0 {
return
}
var summaryParts []string
summaryParts = append(summaryParts, fmt.Sprintf("图记忆快照: %d 个热点实体", len(result.Entities)))
for _, e := range result.Entities {
summaryParts = append(summaryParts, fmt.Sprintf("- %s (%s, %d次)", e.Name, e.Type, e.MentionCount))
}
if len(result.Relations) > 0 {
summaryParts = append(summaryParts, "关联关系:")
for i, r := range result.Relations {
if i >= 10 {
break
}
summaryParts = append(summaryParts, fmt.Sprintf(" %s →(%s)→ %s", r.SourceName, r.RelationType, r.TargetName))
}
}
summary := fmt.Sprintf("图记忆索引 (%d 实体, %d 关系)", len(result.Entities), len(result.Relations))
content := strings.Join(summaryParts, "\n")
recent := a.docStore.RecentDocs(1)
if len(recent) > 0 && recent[0].Source == "graph" && recent[0].Content == content {
return
}
doc := &document.Doc{
Summary: summary,
Content: content,
Tags: []string{"graph_memory", "auto_sync"},
Entities: extractEntityNames(result.Entities),
Source: "graph",
}
if err := a.docStore.Insert(doc); err != nil {
log.Printf("[agent] graph→doc sync error: %v", err)
} else {
log.Printf("[agent] graph→doc synced: %s", doc.Summary)
}
}
func extractEntityNames(entities []memory.Entity) []string {
names := make([]string, len(entities))
for i, e := range entities {
names[i] = e.Name
}
return names
}
func (a *Agent) reorgGraph() {
if a.memory == nil {
return
}
log.Printf("[agent] graph reorg start")
if a.indexer != nil {
if err := a.indexer.Sync(); err != nil {
log.Printf("[agent] indexer sync error: %v", err)
}
}
if a.docStore != nil {
a.docStore.Reindex()
}
if a.docStore != nil {
coldDocs := a.docStore.FindColdDocs(72*time.Hour, 2)
for _, doc := range coldDocs {
triples := docToTriples(doc)
if len(triples) > 0 {
ec, rc, err := a.memory.Commit(triples, string(a.id)+"_doc_archival", 0)
if err != nil {
log.Printf("[agent] doc→graph archival error: %v", err)
continue
}
log.Printf("[agent] doc→graph: %s → %d entities, %d relations", doc.ID, ec, rc)
a.docStore.Remove(doc.ID)
}
}
}
result, err := a.memory.Recall(nil, nil, 1, "")
if err != nil || result == nil || len(result.Entities) < 2 {
return
}
maxCandidates := 5
candidates := 0
for i := 0; i < len(result.Entities) && candidates < maxCandidates; i++ {
for j := i + 1; j < len(result.Entities) && candidates < maxCandidates; j++ {
ea, eb := result.Entities[i].Name, result.Entities[j].Name
if ea > eb {
ea, eb = eb, ea
}
key := ea + "||" + eb
a.noMergeMu.Lock()
rounds, ok := a.noMergeMarkers[key]
if ok {
rounds--
if rounds <= 0 {
delete(a.noMergeMarkers, key)
} else {
a.noMergeMarkers[key] = rounds
}
}
a.noMergeMu.Unlock()
if ok {
continue
}
sim := entitySimilarity(result.Entities[i].Name, result.Entities[j].Name)
if sim > 0.75 {
candidates++
a.enqueueConsolidationTask(ConsolidationTask{
Type: "entity_merge",
Reason: fmt.Sprintf(
"实体「%s」(类型:%s, 提及%d次) 与「%s」(类型:%s, 提及%d次) 相似度 %.0f%%,可能指代同一事物,请判断是否需要合并",
result.Entities[i].Name, result.Entities[i].Type, result.Entities[i].MentionCount,
result.Entities[j].Name, result.Entities[j].Type, result.Entities[j].MentionCount,
sim*100,
),
Data: map[string]interface{}{
"entity_a": result.Entities[i].Name,
"entity_a_type": result.Entities[i].Type,
"entity_a_mentions": result.Entities[i].MentionCount,
"entity_b": result.Entities[j].Name,
"entity_b_type": result.Entities[j].Type,
"entity_b_mentions": result.Entities[j].MentionCount,
"similarity": sim,
},
})
}
}
}
if candidates > 0 {
log.Printf("[agent] graph reorg: %d merge candidates sent for LLM decision", candidates)
} else {
log.Printf("[agent] graph reorg: no similar entities found")
}
a.evaluateGraphQuality()
}
func (a *Agent) evaluateGraphQuality() {
if a.memory == nil {
return
}
result, err := a.memory.Recall(nil, nil, 1, "")
if err != nil || result == nil || len(result.Relations) == 0 {
return
}
var lowQuality []string
for _, r := range result.Relations {
if (r.SourceName == "用户" || r.SourceName == "AI") &&
(r.RelationType == "提及" || r.RelationType == "回应") {
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」", r.SourceName, r.RelationType, r.TargetName))
continue
}
if r.RelationType == "关联" {
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」(jieba 共现)", r.SourceName, r.RelationType, r.TargetName))
continue
}
if r.Confidence < 0.3 && r.RelationType != "" {
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」(confidence=%.1f)", r.SourceName, r.RelationType, r.TargetName, r.Confidence))
}
}
if len(lowQuality) == 0 {
return
}
batchSize := 10
for i := 0; i < len(lowQuality); i += batchSize {
end := i + batchSize
if end > len(lowQuality) {
end = len(lowQuality)
}
batch := lowQuality[i:end]
a.enqueueConsolidationTask(ConsolidationTask{
Type: "graph_quality",
Reason: fmt.Sprintf(
"图数据库中发现 %d 条低质量关系,请逐条判断是否应该删除(保留 = keep删除 = discard\n%s",
len(batch),
strings.Join(batch, "\n"),
),
Data: map[string]interface{}{
"candidates": batch,
"action": "evaluate_quality",
},
})
}
log.Printf("[agent] graph quality: %d low-quality connection batches sent for LLM evaluation", (len(lowQuality)+batchSize-1)/batchSize)
}
func entitySimilarity(a, b string) float64 {
if a == "" || b == "" {
return 0
}
if a == b {
return 1.0
}
runesA, runesB := []rune(a), []rune(b)
if len(runesA) < 2 || len(runesB) < 2 {
if len(runesA) == len(runesB) && len(runesA) == 1 {
if runesA[0] == runesB[0] {
return 1.0
}
}
return 0
}
setA := make(map[string]bool)
for i := 0; i < len(runesA)-1; i++ {
setA[string(runesA[i:i+2])] = true
}
setB := make(map[string]bool)
for i := 0; i < len(runesB)-1; i++ {
setB[string(runesB[i:i+2])] = true
}
intersect := 0
for bg := range setA {
if setB[bg] {
intersect++
}
}
union := len(setA) + len(setB) - intersect
if union <= 0 {
return 0
}
return float64(intersect) / float64(union)
}
func docToTriples(doc *document.Doc) []memory.Triple {
var triples []memory.Triple
if doc == nil {
return triples
}
triples = append(triples, memory.Triple{
Subject: "文档",
SubjectType: "Concept",
Relation: "主题",
Object: doc.Summary,
ObjectType: "Topic",
Confidence: 1.0,
})
lines := strings.Split(doc.Content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
terms := memory.CutExact(line)
for i := 0; i < len(terms)-1; i++ {
triples = append(triples, memory.Triple{
Subject: terms[i],
SubjectType: "Concept",
Relation: "关联",
Object: terms[i+1],
ObjectType: "Concept",
Confidence: 0.8,
})
}
}
if doc.Source != "" {
triples = append(triples, memory.Triple{
Subject: "文档",
SubjectType: "Concept",
Relation: "来源",
Object: doc.Source,
ObjectType: "Source",
Confidence: 1.0,
})
}
return triples
}
func (a *Agent) emitMemoryCandidate(source, input, response string, toolsUsed []string) {
a.io.EmitOutput("memory", "memory_candidate", map[string]interface{}{
"source": source,
"input": input,
"response": response,
"tools_used": toolsUsed,
"agent_id": string(a.id),
"timestamp": time.Now().Unix(),
})
}
func (a *Agent) processConsolidation(evt *agentIO.InputEvent, input string) {
start := time.Now()
a.currentOutputChannel = "_consolidation_"
stageCtx := a.stageCtxFromInput(input, evt.Source, "")
stageCtx.Extra["output_channel"] = evt.OutputChannel
a.injectSourceContext(stageCtx, evt)
archived := a.context.Prune(input, a.maxContextSize-1, a.docStore)
if archived > 0 {
log.Printf("[agent] consolidation: pruned %d low-relevance events", archived)
}
a.context.Append(ContextEvent{
Timestamp: start,
Source: "system",
Input: input,
})
response, toolsUsed, err := a.process(input, stageCtx)
if err != nil {
log.Printf("[agent] consolidation error: %v", err)
return
}
a.context.Append(ContextEvent{
Timestamp: time.Now(),
Source: "agent",
Input: input,
Response: response,
ToolsUsed: toolsUsed,
})
log.Printf("[agent] consolidation done (%dms, tools=%v)", time.Since(start).Milliseconds(), toolsUsed)
}

View File

@ -0,0 +1,414 @@
package core
import (
"fmt"
"log"
"runtime/debug"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func (a *Agent) eventLoop() {
defer func() {
if r := recover(); r != nil {
log.Printf("[agent] eventLoop panic recovered: %v\n%s", r, debug.Stack())
time.Sleep(time.Second)
go a.eventLoop()
}
}()
for {
select {
case evt := <-a.io.InputChan():
a.handleInput(evt)
case task := <-a.selfInputCh:
a.handleSelfInput(task)
case <-a.ctx.Done():
return
}
}
}
func (a *Agent) interceptLoop() {
defer func() {
if r := recover(); r != nil {
log.Printf("[agent] interceptLoop panic recovered: %v\n%s", r, debug.Stack())
time.Sleep(time.Second)
go a.interceptLoop()
}
}()
for {
select {
case evt := <-a.io.InputInterruptChan():
text, _ := evt.Payload["content"].(string)
if text == "" {
continue
}
log.Printf("[agent] interrupt from %s/%s: %s", evt.Source, evt.OutputChannel, truncateStr(text, 80))
clone := &agentIO.InputEvent{
RequestID: evt.RequestID,
Source: evt.Source,
Type: evt.Type,
Payload: map[string]interface{}{},
OutputChannel: evt.OutputChannel,
}
for k, v := range evt.Payload {
clone.Payload[k] = v
}
clone.Payload["interrupt"] = true
clone.Payload["interrupt_source"] = evt.Source
clone.Payload["interrupt_channel"] = evt.OutputChannel
a.llmMu.Lock()
hasActiveLLM := a.cancelLLM != nil
if hasActiveLLM {
a.cancelLLM()
log.Printf("[agent] LLM request cancelled by interrupt")
}
a.llmMu.Unlock()
if hasActiveLLM {
if a.currentOutputChannel == "_consolidation_" {
log.Printf("[agent] consolidation interrupted, re-injecting input for %s/%s", evt.Source, evt.OutputChannel)
a.io.InjectInputTo(evt.Source, evt.OutputChannel, "text", map[string]interface{}{
"content": text,
"interrupt": true,
"interrupt_source": evt.Source,
"interrupt_channel": evt.OutputChannel,
})
} else {
select {
case a.interceptCh <- clone:
default:
log.Printf("[agent] intercept channel full, queuing input for %s", evt.Source)
a.io.InjectInputTo(evt.Source, evt.OutputChannel, "text", map[string]interface{}{
"content": text,
"interrupt": true,
"interrupt_source": evt.Source,
"interrupt_channel": evt.OutputChannel,
})
}
}
} else {
a.io.InjectInputTo(evt.Source, evt.OutputChannel, "text", map[string]interface{}{
"content": text,
"interrupt": true,
"interrupt_source": evt.Source,
"interrupt_channel": evt.OutputChannel,
})
}
case <-a.ctx.Done():
return
}
}
}
func (a *Agent) handleSelfInput(task string) {
a.processTextInput(&agentIO.InputEvent{
Source: "system",
Type: "text",
Payload: map[string]interface{}{"content": task},
OutputChannel: "_consolidation_",
}, task)
}
func (a *Agent) handleInput(evt *agentIO.InputEvent) {
switch evt.Type {
case "text":
input, _ := evt.Payload["content"].(string)
if input == "" {
return
}
a.processTextInput(evt, input)
case "image", "audio":
a.processMediaInput(evt)
case "event":
log.Printf("[agent] event from %s: %v", evt.Source, evt.Payload)
case "command":
cmd, _ := evt.Payload["command"].(string)
log.Printf("[agent] command from %s: %s", evt.Source, cmd)
default:
log.Printf("[agent] unknown event type from %s: %s", evt.Source, evt.Type)
}
}
func (a *Agent) processMediaInput(evt *agentIO.InputEvent) {
start := time.Now()
a.pendingMedia = evt.Payload
defer func() { a.pendingMedia = nil }()
a.currentOutputChannel = evt.OutputChannel
if a.currentOutputChannel == "" {
a.currentOutputChannel = evt.Source
}
blocks, fallback := a.mediaToBlocks(evt.Payload, evt.Type, evt.Source)
stageCtx := a.stageCtxFromInput(fallback, evt.Source, "")
stageCtx.Extra = map[string]interface{}{
"media_blocks": blocks,
"media_type": evt.Type,
"input_source": evt.Source,
"output_channel": evt.OutputChannel,
}
a.injectSourceContext(stageCtx, evt)
if a.runStage(sdk.StageOnInput, stageCtx) {
a.emitResponse(evt, *stageCtx.Response)
return
}
a.publishEvent(events.EventRawInput, map[string]interface{}{
"content": evt.Payload,
"source": evt.Source,
})
archived := a.context.Prune(fallback, a.maxContextSize-1, a.docStore)
if archived > 0 {
log.Printf("[agent] pruned %d low-relevance events to document memory", archived)
}
a.context.Append(ContextEvent{
Timestamp: start,
Source: evt.Source,
Input: fallback,
})
response, toolsUsed, err := a.process(fallback, stageCtx)
if err != nil {
log.Printf("[agent] process media error: %v", err)
resp := fmt.Sprintf("处理错误: %v", err)
a.emitResponse(evt, resp)
a.context.Append(ContextEvent{Timestamp: time.Now(), Source: "agent", Input: fallback, Response: resp})
return
}
elapsed := time.Since(start)
log.Printf("[agent] %s from %s → response (%dms, tools=%v)", evt.Type, evt.Source, elapsed.Milliseconds(), toolsUsed)
a.context.Append(ContextEvent{
Timestamp: time.Now(),
Source: "agent",
Input: fallback,
Response: response,
ToolsUsed: toolsUsed,
})
a.emitResponse(evt, response)
if !stageCtx.NoMemory {
a.emitMemoryCandidate(evt.Source, fallback, response, toolsUsed)
}
}
func (a *Agent) mediaToBlocks(payload map[string]interface{}, mediaType string, source string) ([]agentAPI.ContentBlock, string) {
data, _ := payload["data"].(string)
mime, _ := payload["mime"].(string)
url, _ := payload["url"].(string)
alt, _ := payload["alt"].(string)
if alt == "" {
if source == "" {
source = "unknown"
}
alt = fmt.Sprintf("[从 %s 收到了 %s]", source, mediaType)
}
var blocks []agentAPI.ContentBlock
desc := ""
switch mediaType {
case "image":
desc = a.inputCfg.Image.DescribePrompt
if desc == "" {
desc = fmt.Sprintf("从 %s 收到了一张图片,请使用 describe_image 工具查看详情。", source)
}
case "audio":
desc = a.inputCfg.Audio.DescribePrompt
if desc == "" {
desc = fmt.Sprintf("从 %s 收到了一段音频,请使用 transcribe_audio 工具查看内容。", source)
}
}
blocks = append(blocks, agentAPI.ContentBlock{Type: "text", Text: desc})
if data != "" || url != "" {
imgURL := url
if data != "" {
if mime == "" {
mime = "image/png"
}
imgURL = "data:" + mime + ";base64," + data
}
if mediaType == "image" {
blocks = append(blocks, agentAPI.ContentBlock{
Type: "image_url",
ImageURL: &agentAPI.ImageURL{URL: imgURL, Detail: "auto"},
})
} else if mediaType == "audio" {
blocks = append(blocks, agentAPI.ContentBlock{
Type: "audio_url",
AudioURL: &agentAPI.AudioURL{URL: imgURL},
})
}
}
return blocks, alt
}
func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
start := time.Now()
a.currentOutputChannel = evt.OutputChannel
if a.currentOutputChannel == "" {
a.currentOutputChannel = evt.Source
}
if evt.OutputChannel == "_consolidation_" {
a.processConsolidation(evt, input)
return
}
noMemory := false
if v, ok := evt.Payload["no_memory"].(bool); ok {
noMemory = v
}
stageCtx := a.stageCtxFromInput(input, evt.Source, "")
stageCtx.Extra["input_source"] = evt.Source
stageCtx.Extra["output_channel"] = evt.OutputChannel
if noMemory {
stageCtx.NoMemory = true
}
a.injectSourceContext(stageCtx, evt)
if a.runStage(sdk.StageOnInput, stageCtx) {
a.emitResponse(evt, *stageCtx.Response)
return
}
input = stageCtx.RawMessage
a.publishEvent(events.EventRawInput, map[string]interface{}{
"content": input,
"source": evt.Source,
})
archived := a.context.Prune(input, a.maxContextSize-1, a.docStore)
if archived > 0 {
log.Printf("[agent] pruned %d low-relevance events to document memory", archived)
}
a.context.Append(ContextEvent{
Timestamp: start,
Source: evt.Source,
Input: input,
})
response, toolsUsed, err := a.process(input, stageCtx)
if err != nil {
log.Printf("[agent] process error: %v", err)
resp := fmt.Sprintf("处理错误: %v", err)
a.emitResponse(evt, resp)
a.context.Append(ContextEvent{Timestamp: time.Now(), Source: "agent", Input: input, Response: resp})
return
}
elapsed := time.Since(start)
log.Printf("[agent] input from %s → response (%dms, tools=%v)", evt.Source, elapsed.Milliseconds(), toolsUsed)
a.context.Append(ContextEvent{
Timestamp: time.Now(),
Source: "agent",
Input: input,
Response: response,
ToolsUsed: toolsUsed,
})
a.emitResponse(evt, response)
if !stageCtx.NoMemory {
a.emitMemoryCandidate(evt.Source, input, response, toolsUsed)
}
}
func (a *Agent) emitResponse(evt *agentIO.InputEvent, response string) {
stageCtx := &sdk.StageContext{
FinalText: response,
Phase: sdk.StageBeforeOutput,
}
a.runStage(sdk.StageBeforeOutput, stageCtx)
response = stageCtx.FinalText
ch := a.currentOutputChannel
if ch == "" {
ch = evt.OutputChannel
}
if ch == "" {
ch = evt.Source
}
payload := map[string]interface{}{
"content": response,
"request_id": evt.RequestID,
}
if stageCtx.ReasoningContent != "" {
payload["reasoning_content"] = stageCtx.ReasoningContent
}
if stageCtx.TokenUsage != nil {
payload["usage"] = stageCtx.TokenUsage
}
if evt.ResponseCh != nil {
evt.ResponseCh <- &agentIO.OutputEvent{
RequestID: evt.RequestID,
Target: evt.Source,
Type: "text",
Payload: payload,
Done: true,
OutputChannel: ch,
}
}
a.publishEvent(events.EventAgentOutput, map[string]interface{}{
"content": response,
"channel": ch,
"source": evt.Source,
})
stageCtx.Phase = sdk.StageAfterOutput
a.runStage(sdk.StageAfterOutput, stageCtx)
}
func (a *Agent) drainInterrupts() []string {
var out []string
for {
select {
case evt := <-a.interceptCh:
if evt == nil {
continue
}
text, _ := evt.Payload["content"].(string)
if text == "" {
continue
}
source := evt.Source
if source == "" {
source = "unknown"
}
channel := evt.OutputChannel
if channel == "" {
channel = source
}
out = append(out, fmt.Sprintf("[打断消息][来源:%s][输出通道:%s] %s", source, channel, text))
default:
return out
}
}
}

View File

@ -0,0 +1,95 @@
package core
import (
"context"
"fmt"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
)
func (a *Agent) mediaDataURL(defaultMime string) string {
if a.pendingMedia == nil {
return ""
}
data, _ := a.pendingMedia["data"].(string)
mime, _ := a.pendingMedia["mime"].(string)
url, _ := a.pendingMedia["url"].(string)
if data != "" {
if mime == "" {
mime = defaultMime
}
return "data:" + mime + ";base64," + data
}
return url
}
func (a *Agent) mediaRequest(p agentAPI.Provider, mime, emptyPendingMsg, emptyDataMsg, prompt, resultPrefix string, maxTokens int, blockType string, detail string) string {
if a.pendingMedia == nil {
return emptyPendingMsg
}
url := a.mediaDataURL(mime)
if url == "" {
return emptyDataMsg
}
msg := agentAPI.Message{
Role: "user",
Blocks: []agentAPI.ContentBlock{
{Type: "text", Text: prompt},
},
}
if blockType == "image_url" {
msg.Blocks = append(msg.Blocks, agentAPI.ContentBlock{
Type: "image_url",
ImageURL: &agentAPI.ImageURL{URL: url, Detail: detail},
})
} else {
msg.Blocks = append(msg.Blocks, agentAPI.ContentBlock{
Type: "audio_url",
AudioURL: &agentAPI.AudioURL{URL: url},
})
}
return a.mediaChat(p, msg, resultPrefix, maxTokens)
}
func (a *Agent) mediaChat(p agentAPI.Provider, msg agentAPI.Message, resultPrefix string, maxTokens int) string {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
resp, err := p.Chat(ctx, &agentAPI.CompletionRequest{
Messages: []agentAPI.Message{msg},
MaxTokens: maxTokens,
})
if err != nil {
return fmt.Sprintf("%s失败: %v", resultPrefix, err)
}
return fmt.Sprintf("[%s] %s", resultPrefix, resp.Content)
}
func (a *Agent) executeDescribeImage(tc agentAPI.ToolCall) string {
providerName, _ := tc.Arguments["provider"].(string)
p := a.providerManager.Get(providerName)
if p == nil {
p = a.provider
}
detail, _ := tc.Arguments["detail"].(string)
if detail == "" {
detail = "high"
}
return a.mediaRequest(p, "image/png", "没有待处理的图片数据", "图片数据为空",
a.inputCfg.Image.DescribePrompt, "图片描述", 2048, "image_url", detail)
}
func (a *Agent) executeTranscribeAudio(tc agentAPI.ToolCall) string {
providerName, _ := tc.Arguments["provider"].(string)
p := a.providerManager.Get(providerName)
if p == nil {
p = a.provider
}
return a.mediaRequest(p, "audio/wav", "没有待处理的音频数据", "音频数据为空",
a.inputCfg.Audio.DescribePrompt, "音频转写", 2048, "audio_url", "")
}
func (a *Agent) executeOCRImage(tc agentAPI.ToolCall) string {
return a.mediaRequest(a.provider, "image/png", "没有待处理的图片数据", "图片数据为空",
a.inputCfg.Image.OCRPrompt, "OCR 结果", 4096, "image_url", "high")
}

View File

@ -0,0 +1,124 @@
package core
import (
"fmt"
"strings"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string {
channel := strings.TrimPrefix(tc.Name, "output_send__")
if channel == "" {
return "工具名称格式: output_send__{channel}"
}
content, _ := tc.Arguments["content"].(string)
if content == "" {
return "content 不能为空"
}
caps := a.io.GetChannelCapabilities(channel)
if caps == 0 {
return fmt.Sprintf("通道 [%s] 不存在或不可用。可用输出工具列表见 output_list_channels", channel)
}
if !caps.Supports(agentIO.CapText) {
return fmt.Sprintf("通道 [%s] 不支持文本输出(能力: %s", channel, caps.String())
}
stageCtx := &sdk.StageContext{
FinalText: content,
Phase: sdk.StageBeforeOutput,
}
a.runStage(sdk.StageBeforeOutput, stageCtx)
if stageCtx.Response != nil {
return fmt.Sprintf("输出被插件拦截: %s", *stageCtx.Response)
}
if stageCtx.FinalText == "" {
return "输出被插件清空"
}
args := map[string]interface{}{
"payload": stageCtx.FinalText,
"type": "text",
}
if dev := a.io.GetDevice(channel); dev != nil {
result, err := dev.Execute("output", args)
if err != nil {
return fmt.Sprintf("通过 [%s] 通道发送失败: %v", channel, err)
}
return fmt.Sprintf("已通过 [%s] 通道发送: %v", channel, result)
}
a.io.EmitTextTo("agent_io", channel, stageCtx.FinalText)
return fmt.Sprintf("已通过 [%s] 通道发送", channel)
}
func (a *Agent) executeOutputSendHelp(tc agentAPI.ToolCall) string {
suffix := strings.TrimPrefix(tc.Name, "output_send__")
channel := strings.TrimSuffix(suffix, "_help")
if channel == "" {
return "工具名称格式: output_send__{channel}_help"
}
dev := a.io.GetDevice(channel)
if dev == nil {
return fmt.Sprintf("通道 [%s] 不存在", channel)
}
caps := a.io.GetChannelCapabilities(channel)
capStr := "无"
if caps != 0 {
capStr = caps.String()
}
desc := dev.Description()
if desc == "" {
desc = channel + " 输出通道"
}
return fmt.Sprintf(`通道 [%s]
描述: %s
能力: %s
【参数说明】
payload — 消息载荷必填。type=text 时直接填文字type=file/image 时填 URL 或路径
meta — JSON 对象,发送所需的元数据(可选,取决于通道是否需要路由信息)
type — 载荷类型(必填),枚举值见下方
【type 枚举】
- text — 文本消息
- voice — 语音消息
- image — 图片
- file — 文件
【meta JSON 格式】
由通道描述定义,通常包含:
- "group_id" 群号(群聊时必填)
- "user_id" 目标用户 QQ 号(私聊时必填)
- "reply_to" 回复某条消息 ID可选
示例: output_send__%s(payload="你好", meta="{\"group_id\": 123456789}", type="text")`, channel, desc, capStr, channel)
}
func (a *Agent) executeOutputListChannels() string {
channels := a.io.ListChannels()
if len(channels) == 0 {
return "没有可用通道"
}
var parts []string
parts = append(parts, "可用通道:")
for _, ch := range channels {
if ch.OutputCaps == 0 {
continue
}
parts = append(parts, fmt.Sprintf(" - %s: [%s] %s", ch.Name, ch.OutputCaps.String(), ch.Description))
for _, t := range ch.Tools {
parts = append(parts, fmt.Sprintf(" 工具: %s - %s", t.Name, t.Description))
}
}
return strings.Join(parts, "\n")
}

View File

@ -0,0 +1,52 @@
package core
import (
"fmt"
"log"
"strings"
)
func (a *Agent) executePluginReload() string {
if a.pluginReg == nil {
return "插件系统未启用"
}
msg, err := a.pluginReg.Reload(a.pluginDir)
if err != nil {
return fmt.Sprintf("插件重载失败: %v", err)
}
return msg
}
func (a *Agent) autoReloadPlugins() {
if a.pluginReg == nil {
return
}
for _, name := range a.pluginHealth.pendingReloads() {
if !a.pluginReg.AutoRestartEnabled(name) {
log.Printf("[agent] skip auto-reload plugin %s: auto-restart disabled by plugin", name)
continue
}
log.Printf("[agent] auto-reloading unhealthy plugin: %s", name)
if a.stageHost != nil {
a.stageHost.UnregisterPluginTools(name)
}
if err := a.pluginReg.ReloadOne(name); err != nil {
log.Printf("[agent] auto-reload plugin %s failed: %v", name, err)
} else {
a.pluginHealth.markReloaded(name)
log.Printf("[agent] plugin %s reloaded successfully", name)
}
}
}
func (a *Agent) resolveToolPlugin(name string) string {
if a.stageHost != nil {
if plugin := a.stageHost.ToolPlugin(name); plugin != "" {
return plugin
}
}
if idx := strings.IndexByte(name, '_'); idx > 0 {
return name[:idx]
}
return "core"
}

View File

@ -0,0 +1,421 @@
package core
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"sort"
"strings"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response string, toolsUsed []string, err error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.provider == nil {
return "", nil, fmt.Errorf("agent: no LLM provider configured")
}
memContext := a.buildMemoryContext(input)
sysPrompt := a.buildSystemPrompt(memContext, input)
tools := a.buildToolDefs()
msgs := a.buildMessages(sysPrompt, input)
if blocks, ok := stageCtx.Extra["media_blocks"].([]agentAPI.ContentBlock); ok && len(blocks) > 0 {
if len(msgs) > 0 {
msgs[len(msgs)-1].Blocks = blocks
}
}
log.Printf("[agent] tool call loop start, %d tools, %d context events, personality=%t, docs=%d",
len(tools), a.context.Len(),
a.personality != nil && a.personality.Content != "",
a.docStoreSize())
if a.runStage(sdk.StagePreAction, stageCtx) {
return *stageCtx.Response, toolsUsed, nil
}
if len(stageCtx.ContextMsgs) > 0 {
for _, m := range stageCtx.ContextMsgs {
role, _ := m["role"].(string)
content, _ := m["content"].(string)
if role != "" {
msgs = append(msgs, agentAPI.Message{Role: role, Content: content})
}
}
}
for turn := 0; ; turn++ {
for _, interrupt := range a.drainInterrupts() {
msgs = append(msgs, agentAPI.Message{
Role: "system",
Content: interrupt,
})
}
eb := map[string]interface{}{}
if !a.thinkingEnabled {
eb["thinking"] = map[string]interface{}{"type": "disabled"}
}
req := &agentAPI.CompletionRequest{
Messages: msgs,
MaxTokens: 4096,
Tools: tools,
ToolChoice: "auto",
ExtraBody: eb,
}
var providers []agentAPI.Provider
if a.providerManager != nil {
allProviders := a.providerManager.OrderedProviders()
providers = make([]agentAPI.Provider, 0, len(allProviders))
for _, p := range allProviders {
if a.providerManager.IsAvailable(p.Name()) {
providers = append(providers, p)
}
}
}
if len(providers) == 0 {
providers = []agentAPI.Provider{a.provider}
}
var resp *agentAPI.CompletionResponse
var llmErr error
for pi, fbProvider := range providers {
if pi > 0 {
log.Printf("[agent] LLM fallback: trying provider %q (fallback #%d/%d)",
fbProvider.Name(), pi, len(providers)-1)
}
fCtx, fCancel := context.WithCancel(a.ctx)
a.llmMu.Lock()
a.cancelLLM = fCancel
a.llmMu.Unlock()
resp, llmErr = fbProvider.Chat(fCtx, req)
a.llmMu.Lock()
a.cancelLLM = nil
a.llmMu.Unlock()
fCancel()
if llmErr == nil {
a.providerManager.ResetAvailability(fbProvider.Name())
if fbProvider != a.provider {
a.provider = fbProvider
log.Printf("[agent] switched active provider to %q after fallback",
fbProvider.Name())
}
break
}
if errors.Is(llmErr, context.Canceled) {
break
}
var pe *agentAPI.ProviderError
if errors.As(llmErr, &pe) && (pe.StatusCode == 401 || pe.StatusCode == 403) {
a.providerManager.ReportStatus(fbProvider.Name(), pe.StatusCode)
log.Printf("[agent] provider %q marked unavailable (HTTP %d)", fbProvider.Name(), pe.StatusCode)
} else {
a.providerManager.MarkUnavailable(fbProvider.Name())
}
log.Printf("[agent] provider %q failed: %v", fbProvider.Name(), llmErr)
}
if llmErr != nil {
if errors.Is(llmErr, context.Canceled) && a.ctx.Err() == nil {
if a.currentOutputChannel == "_consolidation_" {
return "", toolsUsed, fmt.Errorf("interrupted by user input")
}
continue
}
return "", toolsUsed, fmt.Errorf("all %d providers failed, last error: %w",
len(providers), llmErr)
}
stageCtx.LLMText = resp.Content
stageCtx.ReasoningContent = resp.ReasoningContent
stageCtx.TokenUsage = map[string]int{
"prompt_tokens": resp.TokenUsage.Prompt,
"completion_tokens": resp.TokenUsage.Completion,
"total_tokens": resp.TokenUsage.Total,
}
stageCtx.ToolCalls = convertToolCalls(resp.ToolCalls)
for i := range stageCtx.ToolCalls {
if stageCtx.ToolCalls[i].Plugin == "" {
stageCtx.ToolCalls[i].Plugin = a.resolveToolPlugin(stageCtx.ToolCalls[i].Name)
}
}
if a.runStage(sdk.StagePostAction, stageCtx) {
return *stageCtx.Response, toolsUsed, nil
}
resp.Content = stageCtx.LLMText
resp.ToolCalls = convertBackToolCalls(stageCtx.ToolCalls)
chainPayload := map[string]interface{}{
"content": resp.Content,
"reasoning": resp.ReasoningContent,
"tool_calls": resp.ToolCalls,
"phase": "intermediate",
"turn": turn,
}
if resp.TokenUsage.Total > 0 {
chainPayload["usage"] = map[string]int{
"prompt": resp.TokenUsage.Prompt,
"completion": resp.TokenUsage.Completion,
"total": resp.TokenUsage.Total,
}
}
a.publishEvent(events.EventAgentLLMChain, chainPayload)
if len(resp.ToolCalls) == 0 {
return resp.Content, toolsUsed, nil
}
for _, tc := range resp.ToolCalls {
if len(a.interceptCh) > 0 {
for _, interrupt := range a.drainInterrupts() {
msgs = append(msgs, agentAPI.Message{Role: "system", Content: interrupt})
}
a.publishEvent(events.EventToolCall, map[string]interface{}{
"tool": tc.Name,
"plugin": a.resolveToolPlugin(tc.Name),
"args": tc.Arguments,
"status": "interrupted",
"reason": "user interrupt before execution",
})
break
}
toolsUsed = append(toolsUsed, tc.Name)
pluginName := a.resolveToolPlugin(tc.Name)
log.Printf("[agent] executing tool: %s (plugin=%s, id=%s)", tc.Name, pluginName, tc.ID)
sdkTC := sdk.ToolCall{ID: tc.ID, Name: tc.Name, Plugin: pluginName, Arguments: tc.Arguments}
stageCtx.ToolCalls = []sdk.ToolCall{sdkTC}
stageCtx.ToolResults = nil
if a.runStage(sdk.StageBeforeToolcall, stageCtx) {
result := fmt.Sprintf("工具 %s 已被插件拒绝", tc.Name)
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{tc}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
a.publishEvent(events.EventToolCall, map[string]interface{}{
"tool": tc.Name,
"plugin": pluginName,
"args": tc.Arguments,
"result": result,
"status": "denied",
})
continue
}
tc.Arguments = stageCtx.ToolCalls[0].Arguments
if pluginName != "" && !a.pluginHealth.isHealthy(pluginName) {
result := fmt.Sprintf("插件 %s 处于崩溃状态,已跳过执行,等待自动恢复重载", pluginName)
log.Printf("[agent] skip tool %s: plugin %s unhealthy", tc.Name, pluginName)
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{tc}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
continue
}
result := a.executeToolCall(tc)
log.Printf("[agent] tool %s result: %s", tc.Name, truncateStr(result, 100))
stageCtx.ToolResults = []sdk.ToolResult{{CallID: tc.ID, Name: tc.Name, Plugin: pluginName, Success: true, Result: result}}
a.runStage(sdk.StageAfterToolcall, stageCtx)
if len(stageCtx.ToolResults) > 0 {
if r, ok := stageCtx.ToolResults[0].Result.(string); ok {
result = r
}
}
argsJSON, _ := json.Marshal(tc.Arguments)
a.recordToolCall(tc.Name, string(argsJSON), result)
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{tc}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
a.publishEvent(events.EventToolCall, map[string]interface{}{
"tool": tc.Name,
"plugin": pluginName,
"args": tc.Arguments,
"result": result,
"status": "ok",
})
if len(a.interceptCh) > 0 {
for _, interrupt := range a.drainInterrupts() {
msgs = append(msgs, agentAPI.Message{Role: "system", Content: interrupt})
}
break
}
}
}
}
func convertToolCalls(tcs []agentAPI.ToolCall) []sdk.ToolCall {
if tcs == nil {
return nil
}
result := make([]sdk.ToolCall, len(tcs))
for i, tc := range tcs {
result[i] = sdk.ToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments}
}
return result
}
func convertBackToolCalls(tcs []sdk.ToolCall) []agentAPI.ToolCall {
if tcs == nil {
return nil
}
result := make([]agentAPI.ToolCall, len(tcs))
for i, tc := range tcs {
result[i] = agentAPI.ToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments}
}
return result
}
func (a *Agent) docStoreSize() int {
if a.docStore == nil {
return 0
}
s := a.docStore.Stats()
if n, ok := s["doc_count"]; ok {
if ni, ok := n.(int); ok {
return ni
}
}
return 0
}
func (a *Agent) recordToolCall(name, args, result string) {
a.toolCallRingMu.Lock()
defer a.toolCallRingMu.Unlock()
if len(args) > 200 {
args = args[:200] + "..."
}
var resultStub string
var fullResult string
if len(a.toolCallRing) < 5 {
fullResult = result
}
if len(result) > 80 {
resultStub = result[:80] + "..."
} else {
resultStub = result
}
rec := ToolCallRecord{
Timestamp: time.Now(),
Name: name,
Args: args,
ResultStub: resultStub,
FullResult: fullResult,
}
if len(a.toolCallRing) >= a.toolCallRingMax {
a.toolCallRing = a.toolCallRing[1:]
}
a.toolCallRing = append(a.toolCallRing, rec)
}
func (a *Agent) formatToolCallRing() string {
a.toolCallRingMu.Lock()
defer a.toolCallRingMu.Unlock()
if len(a.toolCallRing) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("【已执行工具记录(最近40条)】\n")
start := 0
if len(a.toolCallRing) > 40 {
start = len(a.toolCallRing) - 40
}
for i, rec := range a.toolCallRing[start:] {
if len(rec.FullResult) > 0 {
sb.WriteString(fmt.Sprintf(" [%d] %s: %s(%s)=%s\n", i+1,
rec.Timestamp.Format("15:04:05"), rec.Name, rec.Args,
truncateStr(rec.FullResult, 120)))
} else {
sb.WriteString(fmt.Sprintf(" [%d] %s: %s(%s) → (已缓存,具体结果通过文本记忆层获取)\n", i+1,
rec.Timestamp.Format("15:04:05"), rec.Name, rec.Args))
}
}
return sb.String()
}
func (a *Agent) formatMergedTimeline() string {
a.context.mu.Lock()
events := make([]*ContextEvent, len(a.context.events))
copy(events, a.context.events)
a.context.mu.Unlock()
a.toolCallRingMu.Lock()
ring := make([]ToolCallRecord, len(a.toolCallRing))
copy(ring, a.toolCallRing)
a.toolCallRingMu.Unlock()
if len(events) == 0 && len(ring) == 0 {
return ""
}
type timelineEntry struct {
ts time.Time
label string
text string
}
entries := make([]timelineEntry, 0, len(events)+len(ring))
for _, e := range events {
text := fmt.Sprintf("[对话] %s: %s", e.Source, e.Input)
if len(e.ToolsUsed) > 0 {
text += fmt.Sprintf(" → 调用工具: %s", strings.Join(e.ToolsUsed, ", "))
}
if e.Response != "" {
text += fmt.Sprintf(" → %s", truncateStr(e.Response, 120))
}
entries = append(entries, timelineEntry{ts: e.Timestamp, label: "对话", text: text})
}
for _, r := range ring {
text := fmt.Sprintf("[工具] %s(%s)", r.Name, r.Args)
if r.FullResult != "" {
text += fmt.Sprintf(" = %s", truncateStr(r.FullResult, 120))
} else {
text += " → (结果已缓存,可通过文本记忆层获取)"
}
entries = append(entries, timelineEntry{ts: r.Timestamp, label: "工具", text: text})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].ts.Before(entries[j].ts)
})
var sb strings.Builder
sb.WriteString("【对话时序】\n")
for _, e := range entries {
sb.WriteString(fmt.Sprintf("[%s] %s\n", e.ts.Format("15:04:05"), e.text))
}
return sb.String()
}
func (a *Agent) buildMessages(sysPrompt, input string) []agentAPI.Message {
msgs := []agentAPI.Message{{Role: "system", Content: sysPrompt}}
if ctxStr := a.formatMergedTimeline(); ctxStr != "" {
msgs = append(msgs, agentAPI.Message{Role: "system", Content: ctxStr})
}
msgs = append(msgs, agentAPI.Message{Role: "user", Content: input})
return msgs
}

View File

@ -0,0 +1,179 @@
package core
import (
"fmt"
"log"
"strings"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
)
func (a *Agent) executeSpawnChild(tc agentAPI.ToolCall) string {
task, _ := tc.Arguments["task"].(string)
if task == "" {
return "请提供 task 参数"
}
a.childMu.Lock()
a.childNextID++
taskID := fmt.Sprintf("child_%d", a.childNextID)
a.childMu.Unlock()
go a.runChildTask(taskID, task)
return fmt.Sprintf("子任务已启动ID: %s完成后会自动通知你届时请使用 child_result 工具查看输出", taskID)
}
func (a *Agent) runChildTask(taskID, task string) {
if a.provider == nil {
log.Printf("[child] %s failed: no LLM provider configured", taskID)
return
}
log.Printf("[child] %s started: %s", taskID, truncateStr(task, 80))
sysPrompt := fmt.Sprintf(`你是 HomeAgent 的子任务助手。
请完成以下任务。完成即可,无需保留记忆或查询历史。
任务: %s`, task)
msgs := []agentAPI.Message{
{Role: "system", Content: sysPrompt},
{Role: "user", Content: task},
}
allTools := a.buildToolDefs()
childTools := make([]interface{}, 0, len(allTools))
for _, t := range allTools {
toolMap, ok := t.(map[string]interface{})
if !ok {
continue
}
fn, ok := toolMap["function"].(map[string]interface{})
if !ok {
continue
}
name, _ := fn["name"].(string)
if strings.HasPrefix(name, "output_send__") || name == "output_list_channels" || name == "spawn_child" || name == "plgreload" {
continue
}
childTools = append(childTools, t)
}
var finalResult string
for turn := 0; turn < 5; turn++ {
eb := map[string]interface{}{}
if !a.thinkingEnabled {
eb["thinking"] = map[string]interface{}{"type": "disabled"}
}
req := &agentAPI.CompletionRequest{
Messages: msgs,
MaxTokens: 4096,
Tools: childTools,
ToolChoice: "auto",
ExtraBody: eb,
}
resp, err := a.provider.Chat(a.ctx, req)
if err != nil {
finalResult = fmt.Sprintf("子 Agent 执行失败: %v", err)
break
}
if len(resp.ToolCalls) == 0 {
finalResult = resp.Content
break
}
for _, ct := range resp.ToolCalls {
var result string
switch {
case strings.HasPrefix(ct.Name, "output_send__") || ct.Name == "output_list_channels":
result = fmt.Sprintf("子 Agent 不允许调用输出工具: %s", ct.Name)
case ct.Name == "spawn_child" || ct.Name == "plgreload":
result = fmt.Sprintf("子 Agent 不允许调用系统工具: %s", ct.Name)
default:
result = a.executeToolCall(ct)
}
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{ct}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: ct.ID, Content: result})
}
}
if finalResult == "" {
finalResult = "子 Agent 执行超时(超过 5 轮)"
}
a.childMu.Lock()
a.childResults[taskID] = finalResult
a.childMu.Unlock()
log.Printf("[child] %s done: %s", taskID, truncateStr(finalResult, 100))
notification := fmt.Sprintf("子任务 %s 已完成,请调用 child_result 工具查看输出", taskID)
select {
case a.selfInputCh <- notification:
default:
log.Printf("[child] self input channel full, dropping notification for %s", taskID)
}
}
func (a *Agent) executeChildResultTool(tc agentAPI.ToolCall) string {
taskID, _ := tc.Arguments["task_id"].(string)
if taskID == "" {
return "请提供 task_id 参数"
}
a.childMu.Lock()
result, ok := a.childResults[taskID]
if !ok {
a.childMu.Unlock()
a.childMu.Lock()
_, exists := a.childResults[taskID]
a.childMu.Unlock()
if !exists {
return fmt.Sprintf("子任务 %s 不存在或已过期", taskID)
}
}
delete(a.childResults, taskID)
a.childMu.Unlock()
return fmt.Sprintf("【子任务 %s 结果】\n%s", taskID, result)
}
func (a *Agent) executeLLMTool(tc agentAPI.ToolCall) string {
if a.providerManager == nil {
return "LLM 源管理器不可用"
}
switch tc.Name {
case "llm_list_sources":
sources := a.providerManager.List()
if len(sources) == 0 {
return "没有可用的 LLM 源"
}
parts := []string{"可用 LLM 源:"}
for _, name := range sources {
mark := " "
if p := a.providerManager.Get(""); p != nil && p.Name() == name {
mark = "→"
}
parts = append(parts, fmt.Sprintf(" %s %s", mark, name))
}
return strings.Join(parts, "\n")
case "llm_set_source":
name, _ := tc.Arguments["name"].(string)
if name == "" {
return "请提供源名称"
}
if err := a.providerManager.SetDefault(name); err != nil {
return fmt.Sprintf("切换失败: %v", err)
}
a.provider = a.providerManager.Get(name)
return fmt.Sprintf("已切换到 LLM 源: %s", name)
default:
return fmt.Sprintf("未知的 LLM 工具: %s", tc.Name)
}
}

View File

@ -0,0 +1,72 @@
package core
import (
"fmt"
"log"
"runtime/debug"
"time"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func (a *Agent) runStage(stage sdk.Stage, ctx *sdk.StageContext) bool {
if a.stageHost == nil {
return false
}
ctx.Phase = stage
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[agent] stage %q plugin panic: %v\n%s", stage, r, debug.Stack())
}
}()
a.stageHost.RunStage(stage, ctx)
}()
return ctx.Response != nil
}
func (a *Agent) publishEvent(evtType events.EventType, payload map[string]interface{}) {
if a.eventBus == nil {
return
}
a.eventBus.Publish(&events.Event{
Type: evtType,
Source: string(a.id),
Payload: payload,
Timestamp: time.Now().Unix(),
})
}
func (a *Agent) stageCtxFromInput(input, userID, groupID string) *sdk.StageContext {
return &sdk.StageContext{
RawMessage: input,
UserID: userID,
GroupID: groupID,
Phase: sdk.StageOnInput,
Extra: make(map[string]interface{}),
}
}
func (a *Agent) injectSourceContext(stageCtx *sdk.StageContext, evt *agentIO.InputEvent) {
if stageCtx == nil || evt == nil {
return
}
source := evt.Source
if source == "" {
source = "unknown"
}
channel := evt.OutputChannel
if channel == "" {
channel = source
}
content := fmt.Sprintf("当前输入来源: %s默认输出通道: %s。", source, channel)
if flag, _ := evt.Payload["interrupt"].(bool); flag {
content = fmt.Sprintf("这是一条打断输入。来源: %s默认输出通道: %s。", source, channel)
}
stageCtx.ContextMsgs = append(stageCtx.ContextMsgs, map[string]interface{}{
"role": "system",
"content": content,
})
}

View File

@ -0,0 +1,549 @@
package core
import (
"fmt"
"log"
"runtime/debug"
"strings"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
)
func (a *Agent) executeToolCall(tc agentAPI.ToolCall) (ret string) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("[agent] tool %s panic: %v\n%s", tc.Name, r, stack)
if pluginName := a.resolveToolPlugin(tc.Name); pluginName != "" {
if a.pluginHealth.recordCrash(pluginName) {
log.Printf("[agent] plugin %s exceeded crash threshold, scheduling reload", pluginName)
}
}
ret = fmt.Sprintf("工具 %s 执行崩溃: %v", tc.Name, r)
}
}()
done := make(chan string, 1)
go func() {
done <- a.executeToolCallInner(tc)
}()
select {
case result := <-done:
return result
case <-time.After(60 * time.Second):
log.Printf("[agent] tool %s timed out after 60s", tc.Name)
return fmt.Sprintf("工具 %s 执行超时60秒已取消", tc.Name)
}
}
func (a *Agent) executeToolCallInner(tc agentAPI.ToolCall) string {
switch {
case strings.HasPrefix(tc.Name, "memory_"):
return a.executeMemoryTool(tc)
case strings.HasPrefix(tc.Name, "social_"):
return a.executeSocialTool(tc)
case strings.HasPrefix(tc.Name, "knowledge_"):
return a.executeKnowledgeTool(tc)
case strings.HasPrefix(tc.Name, "doc_"):
return a.executeDocTool(tc)
case strings.HasPrefix(tc.Name, "output_send__") && strings.HasSuffix(tc.Name, "_help"):
return a.executeOutputSendHelp(tc)
case strings.HasPrefix(tc.Name, "output_send__"):
return a.executeOutputSendTool(tc)
case tc.Name == "output_list_channels":
return a.executeOutputListChannels()
case tc.Name == "plgreload":
return a.executePluginReload()
case tc.Name == "spawn_child":
return a.executeSpawnChild(tc)
case tc.Name == "child_result":
return a.executeChildResultTool(tc)
case strings.HasPrefix(tc.Name, "llm_"):
return a.executeLLMTool(tc)
case tc.Name == "describe_image":
return a.executeDescribeImage(tc)
case tc.Name == "transcribe_audio":
return a.executeTranscribeAudio(tc)
case tc.Name == "ocr_image":
return a.executeOCRImage(tc)
}
if a.stageHost != nil {
if result, err := a.stageHost.ExecuteTool(tc.Name, tc.Arguments); err == nil {
return fmt.Sprintf("%v", result)
} else if !strings.Contains(err.Error(), "not found in any plugin") {
return fmt.Sprintf("工具 %s 执行失败: %v", tc.Name, err)
}
}
if a.tracker != nil {
a.tracker.PreAction(tc.Name)
}
result, err := a.io.ExecuteTool(tc.Name, tc.Arguments)
if a.tracker != nil {
if cs := a.tracker.PostAction(tc.Name); cs != nil && len(cs.Files) > 0 {
log.Printf("[agent] tool %s changed %d files (changeset: %s)", tc.Name, len(cs.Files), cs.ID)
}
}
if err != nil {
return fmt.Sprintf("工具 %s 执行失败: %v", tc.Name, err)
}
return fmt.Sprintf("%v", result)
}
func (a *Agent) executeMemoryTool(tc agentAPI.ToolCall) string {
if a.memory == nil {
if tc.Name == "memory_document_query" {
return a.executeDocTool(tc)
}
return "图记忆系统不可用"
}
switch tc.Name {
case "memory_recall":
query, _ := tc.Arguments["query_intent"].(string)
depth, _ := tc.Arguments["depth"].(float64)
if depth <= 0 {
depth = 2
}
if query == "" {
return "请输入查询关键词"
}
result, err := a.memory.Recall(strings.Split(query, ","), nil, int(depth), "")
if err != nil {
return fmt.Sprintf("记忆检索失败: %v", err)
}
if len(result.Entities) == 0 && len(result.Relations) == 0 {
return "未找到相关记忆"
}
if a.indexer != nil {
names := make([]string, len(result.Entities))
for i, e := range result.Entities {
names[i] = e.Name
}
a.indexer.MarkRecalled(names...)
}
var parts []string
parts = append(parts, fmt.Sprintf("找到 %d 个相关实体:", len(result.Entities)))
for _, e := range result.Entities {
parts = append(parts, fmt.Sprintf("- %s (提及%d次, 类型:%s)", e.Name, e.MentionCount, e.Type))
}
parts = append(parts, fmt.Sprintf("找到 %d 条关系:", len(result.Relations)))
for i, r := range result.Relations {
if i >= 10 {
parts = append(parts, "...更多关系被截断")
break
}
parts = append(parts, fmt.Sprintf("- %s →(%s)→ %s", r.SourceName, r.RelationType, r.TargetName))
}
return strings.Join(parts, "\n")
case "memory_block_merge":
entityA, _ := tc.Arguments["entity_a"].(string)
entityB, _ := tc.Arguments["entity_b"].(string)
rounds, _ := tc.Arguments["rounds"].(float64)
if entityA == "" || entityB == "" || rounds <= 0 {
return "entity_a、entity_b 和 rounds 不能为空"
}
if entityA > entityB {
entityA, entityB = entityB, entityA
}
key := entityA + "||" + entityB
a.noMergeMu.Lock()
a.noMergeMarkers[key] = int(rounds)
a.noMergeMu.Unlock()
return fmt.Sprintf("已标记「%s」与「%s」在 %d 轮内不合并", entityA, entityB, int(rounds))
case "memory_commit":
triplesData, ok := tc.Arguments["triples"].([]interface{})
if !ok {
return "参数格式错误,需要 triples 数组"
}
var triples []memory.Triple
for _, td := range triplesData {
if m, ok := td.(map[string]interface{}); ok {
t := memory.Triple{
Subject: getString(m, "subject"),
Relation: getString(m, "relation"),
Object: getString(m, "object"),
}
if t.Subject != "" && t.Relation != "" && t.Object != "" {
triples = append(triples, t)
}
}
}
if len(triples) == 0 {
return "没有有效的三元组"
}
ec, rc, err := a.memory.Commit(triples, string(a.id), 0)
if err != nil {
return fmt.Sprintf("记忆写入失败: %v", err)
}
return fmt.Sprintf("已写入 %d 个实体和 %d 条关系", ec, rc)
case "memory_introspect":
stats, err := a.memory.Introspect()
if err != nil {
return fmt.Sprintf("查询失败: %v", err)
}
return fmt.Sprintf("记忆统计: %v", stats)
case "memory_document_query":
return a.executeDocTool(tc)
case "memory_merge":
source, _ := tc.Arguments["source"].(string)
target, _ := tc.Arguments["target"].(string)
if source == "" || target == "" {
return "source 和 target 不能为空"
}
count, err := a.memory.MergeEntities(source, target)
if err != nil {
return fmt.Sprintf("合并失败: %v", err)
}
return fmt.Sprintf("已将「%s」合并到「%s」source 已彻底删除,%d 条关系已重定向", source, target, count)
case "memory_delete_entity":
name, _ := tc.Arguments["name"].(string)
if name == "" {
return "name 不能为空"
}
if err := a.memory.DeleteEntity(name); err != nil {
return fmt.Sprintf("删除失败: %v", err)
}
return fmt.Sprintf("已彻底删除实体「%s」及其所有关联关系", name)
case "memory_purge":
criteria := make(map[string]string)
if v, ok := tc.Arguments["subject_contains"].(string); ok && v != "" {
criteria["subject_contains"] = v
}
if v, ok := tc.Arguments["relation_type"].(string); ok && v != "" {
criteria["relation_type"] = v
}
if v, ok := tc.Arguments["target_contains"].(string); ok && v != "" {
criteria["target_contains"] = v
}
mode, _ := tc.Arguments["mode"].(string)
if mode == "" {
mode = "soft"
}
n, err := a.memory.Purge(criteria, mode)
if err != nil {
return fmt.Sprintf("删除图记忆失败: %v", err)
}
textRemoved := 0
if a.textMem != nil {
if subj, ok := criteria["subject_contains"]; ok && subj != "" {
textRemoved, _ = a.textMem.PurgeByFilter(func(evt text.Event) bool {
return strings.Contains(evt.Source, subj) || strings.Contains(evt.Input, subj) || strings.Contains(evt.Response, subj)
})
}
}
parts := []string{fmt.Sprintf("已%s删除 %d 条图记忆关系", mode, n)}
if textRemoved > 0 {
parts = append(parts, fmt.Sprintf("清理 %d 条文本记忆日志", textRemoved))
}
return strings.Join(parts, "")
case "memory_edit":
oldSubject, _ := tc.Arguments["old_subject"].(string)
oldRelation, _ := tc.Arguments["old_relation"].(string)
oldObject, _ := tc.Arguments["old_object"].(string)
if oldSubject == "" || oldRelation == "" || oldObject == "" {
return "old_subject、old_relation、old_object 不能为空"
}
newSubject, _ := tc.Arguments["new_subject"].(string)
newRelation, _ := tc.Arguments["new_relation"].(string)
newObject, _ := tc.Arguments["new_object"].(string)
if newSubject == "" && newRelation == "" && newObject == "" {
return "至少提供一个新值new_subject / new_relation / new_object"
}
if newSubject == "" {
newSubject = oldSubject
}
if newRelation == "" {
newRelation = oldRelation
}
if newObject == "" {
newObject = oldObject
}
n, err := a.memory.Purge(map[string]string{
"subject_contains": oldSubject,
"relation_type": oldRelation,
"target_contains": oldObject,
}, "hard")
if err != nil {
return fmt.Sprintf("编辑图记忆失败(删除旧记录): %v", err)
}
triples := []memory.Triple{{
Subject: newSubject,
Relation: newRelation,
Object: newObject,
}}
ec, rc, err := a.memory.Commit(triples, string(a.id), 0)
if err != nil {
return fmt.Sprintf("编辑图记忆失败(写入新记录): %v", err)
}
textReplaced := 0
if a.textMem != nil && oldSubject != "" {
textReplaced, _ = a.textMem.ReplaceByFilter(
func(evt text.Event) bool {
return strings.Contains(evt.Input, oldSubject) || strings.Contains(evt.Response, oldSubject)
},
func(evt text.Event) text.Event {
evt.Input = strings.ReplaceAll(evt.Input, oldSubject, newSubject)
evt.Response = strings.ReplaceAll(evt.Response, oldSubject, newSubject)
return evt
},
)
}
result := fmt.Sprintf("已编辑记忆:删除 %d 条旧关系,写入 %d 个实体 + %d 条新关系", n, ec, rc)
if textReplaced > 0 {
result += fmt.Sprintf(",更新 %d 条文本记忆日志", textReplaced)
}
return result
default:
return fmt.Sprintf("未知的记忆工具: %s", tc.Name)
}
}
func (a *Agent) executeSocialTool(tc agentAPI.ToolCall) string {
if a.social == nil {
return "人物关系网不可用social store 未初始化)"
}
switch tc.Name {
case "person_query":
name, _ := tc.Arguments["name"].(string)
if name == "" {
return "请输入人物名称"
}
profile, err := a.social.GetPerson(name)
if err != nil {
return fmt.Sprintf("查询人物失败: %v", err)
}
var parts []string
parts = append(parts, fmt.Sprintf("▎%s 的档案", name))
if len(profile.Traits) > 0 {
parts = append(parts, "【特质】")
for k, v := range profile.Traits {
parts = append(parts, fmt.Sprintf(" %s: %s", k, v))
}
}
if len(profile.Relations) > 0 {
parts = append(parts, "【社交关系】")
for _, r := range profile.Relations {
parts = append(parts, fmt.Sprintf(" %s —(%s)—→ %s", name, r.Relation, r.Person))
}
}
if len(profile.Traits) == 0 && len(profile.Relations) == 0 {
parts = append(parts, " (尚无记录)")
}
return strings.Join(parts, "\n")
case "person_set_trait":
name, _ := tc.Arguments["name"].(string)
trait, _ := tc.Arguments["trait"].(string)
value, _ := tc.Arguments["value"].(string)
if name == "" || trait == "" || value == "" {
return "name、trait、value 都不能为空"
}
if err := a.social.SetTrait(name, trait, value); err != nil {
return fmt.Sprintf("设置特质失败: %v", err)
}
return fmt.Sprintf("已记录:%s 的 %s = %s", name, trait, value)
case "person_relate":
personA, _ := tc.Arguments["person_a"].(string)
relation, _ := tc.Arguments["relation"].(string)
personB, _ := tc.Arguments["person_b"].(string)
if personA == "" || relation == "" || personB == "" {
return "person_a、relation、person_b 都不能为空"
}
if err := a.social.AddRelation(personA, relation, personB); err != nil {
return fmt.Sprintf("建立关系失败: %v", err)
}
return fmt.Sprintf("已记录:%s —(%s)—→ %s", personA, relation, personB)
case "person_network":
name, _ := tc.Arguments["name"].(string)
depth := int(getFloat(tc.Arguments, "depth"))
if depth <= 0 {
depth = 2
}
if name == "" {
return "请输入人物名称"
}
profiles, err := a.social.GetNetwork(name, depth)
if err != nil {
return fmt.Sprintf("查询社交网络失败: %v", err)
}
if len(profiles) == 0 {
return fmt.Sprintf("未找到 %s 的社交网络", name)
}
var parts []string
parts = append(parts, fmt.Sprintf("▎%s 的社交网络(%d 度)", name, depth))
for _, p := range profiles {
if p.Name == name {
continue
}
parts = append(parts, fmt.Sprintf(" · %s", p.Name))
for k, v := range p.Traits {
parts = append(parts, fmt.Sprintf(" %s: %s", k, v))
}
for _, r := range p.Relations {
if r.Person != name {
parts = append(parts, fmt.Sprintf(" —(%s)—→ %s", r.Relation, r.Person))
}
}
}
return strings.Join(parts, "\n")
default:
return fmt.Sprintf("未知的人物工具: %s", tc.Name)
}
}
func (a *Agent) executeKnowledgeTool(tc agentAPI.ToolCall) string {
if a.knowledge == nil {
return "知识库不可用"
}
switch tc.Name {
case "knowledge_search":
query, _ := tc.Arguments["query"].(string)
topK := int(getFloat(tc.Arguments, "top_k"))
if topK <= 0 {
topK = 5
}
if query == "" {
return "请输入查询关键词"
}
results := a.knowledge.Search(query, topK)
if len(results) == 0 {
return "未找到相关知识"
}
var parts []string
for i, k := range results {
if i >= topK {
break
}
label := k.Name
if k.Category != "" {
label = k.Category + "/" + k.Name
}
parts = append(parts, fmt.Sprintf("[%s]\n%s", label, truncateStr(k.Content, 200)))
}
return strings.Join(parts, "\n---\n")
case "knowledge_create":
name, _ := tc.Arguments["name"].(string)
content, _ := tc.Arguments["content"].(string)
if name == "" || content == "" {
return "name 和 content 不能为空"
}
if err := a.knowledge.Add(name, content); err != nil {
return fmt.Sprintf("知识创建失败: %v", err)
}
return fmt.Sprintf("知识「%s」已创建并向量化索引%d 字符)", name, len(content))
case "knowledge_list":
tree := a.knowledge.BuildTree()
return formatTree(tree, 0)
case "knowledge_delete":
name, _ := tc.Arguments["name"].(string)
if name == "" {
return "name 不能为空"
}
if err := a.knowledge.Remove(name); err != nil {
return fmt.Sprintf("知识删除失败: %v", err)
}
return fmt.Sprintf("知识「%s」已删除", name)
default:
return fmt.Sprintf("未知的知识工具: %s", tc.Name)
}
}
func (a *Agent) executeDocTool(tc agentAPI.ToolCall) string {
if a.docStore == nil {
return "文档记忆不可用"
}
switch tc.Name {
case "doc_query":
query, _ := tc.Arguments["query"].(string)
topK := int(getFloat(tc.Arguments, "top_k"))
if topK <= 0 {
topK = 3
}
if query == "" {
return "请输入查询内容"
}
docs := a.docStore.Consume(query, topK)
if len(docs) == 0 {
return "未找到相关文档记忆"
}
var parts []string
var refs []string
for i, d := range docs {
parts = append(parts, fmt.Sprintf("[%d] %s (来源: %s)", i+1, d.Summary, d.Source))
if len(d.Tags) > 0 {
parts = append(parts, " 标签: "+strings.Join(d.Tags, ", "))
}
content := d.Content
if len(content) > 2000 {
content = content[:2000] + "..."
}
a.context.Append(ContextEvent{
Timestamp: d.CreatedAt,
Source: "cold_storage",
Input: fmt.Sprintf("加载文档记忆: %s", query),
Response: content,
})
refs = append(refs, fmt.Sprintf("#%d(%s)", i+1, d.Summary))
}
return fmt.Sprintf("已加载 %d 篇文档记忆: %s\n(完整内容参见对话时序中 cold_storage 事件)",
len(docs), strings.Join(refs, ", "))
case "doc_commit":
content, _ := tc.Arguments["content"].(string)
summary, _ := tc.Arguments["summary"].(string)
if content == "" {
return "content 不能为空"
}
if summary == "" {
summary = truncateStr(content, 100)
}
tagsRaw, _ := tc.Arguments["tags"].([]interface{})
var tags []string
for _, t := range tagsRaw {
if s, ok := t.(string); ok {
tags = append(tags, s)
}
}
doc := &document.Doc{
Summary: summary,
Content: content,
Tags: tags,
Source: "manual",
}
if err := a.docStore.Insert(doc); err != nil {
return fmt.Sprintf("文档写入失败: %v", err)
}
return fmt.Sprintf("文档已提交 (id: %s, 摘要: %s)", doc.ID, summary)
default:
return fmt.Sprintf("未知的文档工具: %s", tc.Name)
}
}

View File

@ -0,0 +1,608 @@
package core
import (
"fmt"
"strings"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
func (a *Agent) buildMemoryContext(input string) string {
if a.indexer == nil {
return ""
}
injected := a.indexer.BuildContext(input)
return a.indexer.FormatContext(injected)
}
func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
prompt := a.systemPrompt
if prompt == "" {
prompt = "你是小宅HomeAgent 的看板娘,一个家政型 AI 管家助手。绝不用 Unicode emoji只用颜文字表达情感句尾带语气词。WebUI 概览页展示你的立绘。"
}
if a.personality != nil {
if pp := a.personality.InjectPrompt(); pp != "" {
prompt += "\n\n" + pp
}
}
if memContext != "" {
prompt += "\n\n" + memContext
}
prompt += "\n\n【记忆清理指令】当用户要求整理或清理记忆时你必须实际调用 memory_ 工具执行操作,不能只回复文本。先用 memory_introspect 查看概况,再用 memory_recall 获取详情。有同义实体则用 memory_merge 合并source 会被彻底删除),有无用噪音实体则用 memory_delete_entity 直接删除,也可用 memory_purge 批量清理,用 memory_edit 修正错误,用 memory_block_merge 标记不合并。如果工具执行成功,把结果告知用户;不要只描述计划而不执行。"
if a.docStore != nil {
docs := a.docStore.Query(userInput, 3)
if len(docs) > 0 {
var parts []string
parts = append(parts, "【相关记忆文档】")
for i, d := range docs {
parts = append(parts, fmt.Sprintf(" [%d] %s", i+1, d.Summary))
}
prompt += "\n\n" + strings.Join(parts, "\n")
}
}
prompt += "\n\n【输出规则】你有多组输出门工具type=output每个对应一个输出通道。回复用户时必须调用对应的 output_send__{通道名} 工具。\n"
prompt += "- payload 参数是消息载荷文本直接填文字type 指定载荷类型text/voice/image/filemeta 是 JSON 发送元数据(群号/用户号等)。\n"
prompt += "- 用 output_send__{通道名}_help 查看该通道的 meta 格式和 type 枚举。\n"
prompt += "- 同一轮对话中可多次调用输出门工具。长消息应当分多次发出,而不是一口气发完。\n"
prompt += "- 直接返回纯文本不会到达任何用户端。"
if a.skills != nil {
if sp := a.skills.GetInjectedPrompt(); sp != "" {
prompt += "\n\n" + sp
}
}
if a.indexer != nil {
prompt += "\n\n" + a.indexer.BuildToolPrompt()
}
prompt += a.buildToolCatalog()
return prompt
}
func cleanParams(params map[string]interface{}) map[string]interface{} {
if params == nil {
return nil
}
cleaned := make(map[string]interface{}, len(params))
for k, v := range params {
cleaned[k] = v
}
if req, ok := cleaned["required"]; ok {
switch v := req.(type) {
case []interface{}:
if len(v) == 0 {
delete(cleaned, "required")
}
case []string:
if len(v) == 0 {
delete(cleaned, "required")
}
}
}
return cleaned
}
func (a *Agent) buildToolCatalog() string {
defs := a.buildToolDefs()
if len(defs) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("\n\n【可用工具列表】")
seen := make(map[string]bool)
for _, d := range defs {
t, ok := d.(map[string]interface{})
if !ok {
continue
}
fn, ok := t["function"].(map[string]interface{})
if !ok {
continue
}
name, _ := fn["name"].(string)
if name == "" || seen[name] {
continue
}
seen[name] = true
desc, _ := fn["description"].(string)
sb.WriteString(fmt.Sprintf("\n- %s", name))
if desc != "" {
if len(desc) > 80 {
desc = desc[:80] + "..."
}
sb.WriteString(": " + desc)
}
}
return sb.String()
}
func (a *Agent) buildToolDefs() []interface{} {
var tools []interface{}
if a.io != nil {
for _, td := range a.io.GetAllTools() {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": td.Name,
"description": td.Description,
"parameters": cleanParams(td.Parameters),
},
})
}
}
if a.stageHost != nil {
for _, td := range a.stageHost.GetToolDefs() {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": td.Name,
"description": td.Description,
"parameters": cleanParams(td.Parameters),
},
})
}
}
if a.indexer != nil {
for _, td := range a.indexer.GetToolDefinitions() {
tools = append(tools, td)
}
}
if a.memory != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "memory_merge",
"description": "【记忆清理】合并两个同义实体。将所有关系从 source 重定向到 target然后彻底删除 source。注意实体删除后不可恢复合并前请确认语义一致。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"source": map[string]interface{}{"type": "string", "description": "被合并的实体名(合并后消失)"},
"target": map[string]interface{}{"type": "string", "description": "保留的实体名"},
},
"required": []string{"source", "target"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "memory_delete_entity",
"description": "【记忆清理】彻底删除指定实体及其所有关联关系。用于清理无用的噪音实体,如 mentionCount=0 的孤立实体、distiller 自动产生的垃圾节点、确认无用的旧数据。此操作不可恢复。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "要删除的实体名称"},
},
"required": []string{"name"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "memory_block_merge",
"description": "【记忆清理】标记两个实体在指定轮次内不尝试合并,用于阻止误判。当 LLM 判断两个实体虽然相似但不是同一事物时,使用此工具阻止后续心跳自动推送合并候选。每次心跳扫描双方计数各减一,归零后恢复候选资格。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"entity_a": map[string]interface{}{"type": "string", "description": "第一个实体名"},
"entity_b": map[string]interface{}{"type": "string", "description": "第二个实体名"},
"rounds": map[string]interface{}{"type": "integer", "description": "阻止轮次数(每次心跳各减一,归零后恢复)"},
},
"required": []string{"entity_a", "entity_b", "rounds"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "memory_purge",
"description": "【记忆清理】删除记忆库中符合条件的垃圾关系和数据。当用户要求整理记忆时,用 memory_introspect 发现低质量实体后,用此工具批量删除。如 @merged 后缀的残留实体、mentionCount=0 的孤立实体、distiller 自动生成的噪音关系等。支持软删soft和物理删除hard。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"subject_contains": map[string]interface{}{"type": "string", "description": "主体名包含的关键词,如 '@merged' 可清理已合并残留"},
"relation_type": map[string]interface{}{"type": "string", "description": "关系类型,如 '提及'、'回应'"},
"target_contains": map[string]interface{}{"type": "string", "description": "客体名包含的关键词"},
"mode": map[string]interface{}{"type": "string", "description": "soft标记删除/ hard物理删除", "default": "soft"},
},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "memory_edit",
"description": "【记忆清理】编辑单条记忆关系:删除旧的 relation 并写入新的。用于修正错误的实体名或关系类型。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"old_subject": map[string]interface{}{"type": "string", "description": "旧主体名"},
"old_relation": map[string]interface{}{"type": "string", "description": "旧关系类型"},
"old_object": map[string]interface{}{"type": "string", "description": "旧客体名"},
"new_subject": map[string]interface{}{"type": "string", "description": "新主体名(不填则不变)"},
"new_relation": map[string]interface{}{"type": "string", "description": "新关系类型(不填则不变)"},
"new_object": map[string]interface{}{"type": "string", "description": "新客体名(不填则不变)"},
},
"required": []string{"old_subject", "old_relation", "old_object"},
},
},
})
}
if a.knowledge != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "knowledge_search",
"description": "搜索知识库。输入查询关键词,返回相关知识内容。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{"type": "string", "description": "查询关键词"},
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 5},
},
"required": []string{"query"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "knowledge_list",
"description": "列出知识库中所有知识分类。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
})
}
if a.knowledge != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "knowledge_create",
"description": "创建新知识。将知识写入知识库knowledge/目录),自动向量化索引。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "知识名称(用作目录名)"},
"content": map[string]interface{}{"type": "string", "description": "知识内容,支持 Markdown"},
},
"required": []string{"name", "content"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "knowledge_delete",
"description": "删除知识库中的指定知识条目。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "要删除的知识名称"},
},
"required": []string{"name"},
},
},
})
}
if a.docStore != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "doc_query",
"description": "查询文档记忆。输入查询内容,返回相关文档摘要。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{"type": "string", "description": "查询内容"},
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 3},
},
"required": []string{"query"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "doc_commit",
"description": "提交一条文档记忆。将重要信息显式写入文档记忆层。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"content": map[string]interface{}{"type": "string", "description": "文档内容"},
"summary": map[string]interface{}{"type": "string", "description": "摘要(可选)"},
"tags": map[string]interface{}{
"type": "array",
"description": "标签列表",
"items": map[string]interface{}{"type": "string"},
},
},
"required": []string{"content"},
},
},
})
}
if a.social != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "person_query",
"description": "查询指定人物的完整档案(特质+社交关系)。用于了解一个人的性格、喜好、背景和社交圈。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
},
"required": []string{"name"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "person_set_trait",
"description": "记录/更新一个人的特质性格、喜好、习惯等。例如person_set_trait(name=\"张三\", trait=\"喜欢\", value=\"红色\")。如果该特质已存在则覆盖。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
"trait": map[string]interface{}{"type": "string", "description": "特质名称,如:喜欢、性格、职业、年龄"},
"value": map[string]interface{}{"type": "string", "description": "特质值红色、开朗、工程师、25岁"},
},
"required": []string{"name", "trait", "value"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "person_relate",
"description": "记录两个人之间的社交关系。例如person_relate(person_a=\"张三\", relation=\"朋友\", person_b=\"李四\")。关系是双向的。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"person_a": map[string]interface{}{"type": "string", "description": "人物A"},
"relation": map[string]interface{}{"type": "string", "description": "关系类型,如:朋友、家人、同事、邻居、同学"},
"person_b": map[string]interface{}{"type": "string", "description": "人物B"},
},
"required": []string{"person_a", "relation", "person_b"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "person_network",
"description": "查询某人的社交网络(多度关系)。显示该人物周围的相关人物及其关系和特质。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
"depth": map[string]interface{}{"type": "integer", "description": "关系深度默认2", "default": 2},
},
"required": []string{"name"},
},
},
})
}
if a.pluginReg != nil && a.pluginDir != "" {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "plgreload",
"description": "重载 plugins/ 目录的所有插件。扫描目录变更,原子化替换 IO 设备。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
})
}
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "spawn_child",
"description": "启动一个异步子 Agent 执行独立任务。子 Agent 后台运行,不阻塞当前对话。完成后系统会自动通知你,届时请调用 child_result 工具查看输出。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"task": map[string]interface{}{
"type": "string",
"description": "要子 Agent 完成的任务描述。请描述清晰、完整,包含所有必要背景。",
},
},
"required": []string{"task"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "child_result",
"description": "查询异步子 Agent 的执行结果。当收到'子任务已完成'的通知后,调用此工具获取输出。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"task_id": map[string]interface{}{
"type": "string",
"description": "spawn_child 返回的任务 ID如 child_1",
},
},
"required": []string{"task_id"},
},
},
})
if a.providerManager != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "llm_list_sources",
"description": "列出所有可用的 LLM 源(如 deepseek、openai、ollama每个源有对应的 Lua 适配器和配置。如需切换 LLM 源,请使用 llm_set_source。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "llm_set_source",
"description": "切换当前 LLM 源到指定名称。变更立即生效,后续对话将使用新的 LLM 源。源名称可通过 llm_list_sources 查看。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
"description": "LLM 源名称(如 deepseek、openai、ollama",
},
},
"required": []string{"name"},
},
},
})
}
channels := a.io.ListChannels()
for _, ch := range channels {
if ch.Type != agentIO.DeviceOutput && ch.Type != agentIO.DeviceIO {
continue
}
capStr := a.io.GetChannelCapabilities(ch.Name).String()
desc := ch.Description
if desc == "" {
desc = ch.Name + " 输出通道"
}
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "output_send__" + ch.Name,
"description": desc + "。能力: " + capStr + "。payload 为消息载荷meta 为 JSON 发送元数据type 为载荷类型。用 _help 查看 meta 格式和 type 枚举。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"payload": map[string]interface{}{
"type": "string",
"description": "消息载荷。type=text 时填文字type=file/image 时填 URL 或路径",
},
"meta": map[string]interface{}{
"type": "string",
"description": "JSON 对象,包含发送所需的元数据。用 output_send__" + ch.Name + "_help 查看 meta 格式",
},
"type": map[string]interface{}{
"type": "string",
"description": "载荷类型,用 channel._help 查看支持的枚举值",
},
},
"required": []string{"payload", "type"},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "output_send__" + ch.Name + "_help",
"description": "查看 " + ch.Name + " 输出通道的 meta 格式说明和 type 枚举",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
})
}
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "output_list_channels",
"description": "列出所有可用输出通道及其能力(如 text/file/image/audio和对应的输出门工具名称。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
})
if a.pendingMedia != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "describe_image",
"description": "描述当前用户上传的图片内容。使用配置的多模态模型或默认 LLM 进行识别。调用此工具后你将获得图片的详细文字描述。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"provider": map[string]interface{}{
"type": "string",
"description": "可选:用于图片描述的 LLM 源名称,不填则使用默认模型",
},
"detail": map[string]interface{}{
"type": "string",
"description": "描述详细程度: high / low / auto",
"default": "high",
},
},
},
},
})
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "transcribe_audio",
"description": "转写当前用户上传的音频内容为文字。使用配置的多模态模型或默认 LLM 进行语音识别。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"provider": map[string]interface{}{
"type": "string",
"description": "可选:用于音频转写的 LLM 源名称,不填则使用默认模型",
},
},
},
},
})
if a.inputCfg.Image.OCREnabled {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "ocr_image",
"description": "对当前用户上传的图片执行 OCR 文字识别,提取图片中的文字内容。适用于截图、文档照片、菜单等场景。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"language": map[string]interface{}{
"type": "string",
"description": "OCR 语言(如 chi_sim+eng默认自动",
},
},
},
},
})
}
}
return tools
}

View File

@ -0,0 +1,69 @@
package core
import (
"fmt"
"strings"
"unicode/utf8"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
)
func getString(m map[string]interface{}, key string) string {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
func getFloat(m map[string]interface{}, key string) float64 {
if v, ok := m[key]; ok {
switch n := v.(type) {
case float64:
return n
case int:
return float64(n)
}
}
return 0
}
func truncateStr(s string, max int) string {
if utf8.RuneCountInString(s) <= max {
return s
}
var truncated int
for i := range s {
if truncated >= max {
return s[:i] + "..."
}
truncated++
}
return s
}
func formatTree(node *knowledge.TreeIndex, depth int) string {
var sb strings.Builder
indent := strings.Repeat(" ", depth)
for _, child := range node.Children {
sb.WriteString(fmt.Sprintf("%s%s/\n", indent, child.Name))
sb.WriteString(formatTree(child, depth+1))
}
for _, item := range node.Items {
preview := item.Preview
if len([]rune(preview)) > 60 {
preview = string([]rune(preview)[:60]) + "..."
}
tags := ""
if len(item.Tags) > 0 {
tags = " [" + strings.Join(item.Tags, ", ") + "]"
}
sb.WriteString(fmt.Sprintf("%s· %s%s\n", indent, item.Name, tags))
sb.WriteString(fmt.Sprintf("%s %s\n", indent, preview))
}
if sb.Len() == 0 {
sb.WriteString("(空)")
}
return sb.String()
}