mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat: complete HomeAgent architecture v2
- IO abstraction layer with OutputChannel routing and capability validation - Three-layer memory (Context-Document-Graph) with TF-IDF relevance pruning - OneBot V11 QQ protocol plugin with Reverse WebSocket client - Plugin system with hot-reload (SKILL.md + native factories) - Knowledge system with TF-IDF vector indexing - Personality system (personal.md) - Text memory (JSONL with rotation) - Change tracker (overlayfs) with rollback - Lua adapter VM - Design document (DESIGN.md) Module: gitcode.com/JianFeeeee/HomeAgent
This commit is contained in:
509
internal/agent/api/provider.go
Normal file
509
internal/agent/api/provider.go
Normal file
@ -0,0 +1,509 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolCalls []ToolCall `json:"-"`
|
||||
}
|
||||
|
||||
func (m Message) MarshalJSON() ([]byte, error) {
|
||||
raw := map[string]interface{}{
|
||||
"role": m.Role,
|
||||
"content": m.Content,
|
||||
}
|
||||
if m.ReasoningContent != "" {
|
||||
raw["reasoning_content"] = m.ReasoningContent
|
||||
}
|
||||
if m.ToolCallID != "" {
|
||||
raw["tool_call_id"] = m.ToolCallID
|
||||
}
|
||||
if len(m.ToolCalls) > 0 {
|
||||
apiTCs := make([]apiToolCall, len(m.ToolCalls))
|
||||
for i, tc := range m.ToolCalls {
|
||||
argsBytes, _ := json.Marshal(tc.Arguments)
|
||||
apiTCs[i] = apiToolCall{
|
||||
ID: tc.ID,
|
||||
Type: "function",
|
||||
Function: apiFunction{
|
||||
Name: tc.Name,
|
||||
Arguments: string(argsBytes),
|
||||
},
|
||||
}
|
||||
}
|
||||
raw["tool_calls"] = apiTCs
|
||||
}
|
||||
return json.Marshal(raw)
|
||||
}
|
||||
|
||||
type CompletionRequest struct {
|
||||
Model string `json:"model,omitempty"`
|
||||
Messages []Message `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Tools []interface{} `json:"tools,omitempty"`
|
||||
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
||||
ExtraBody map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
func (r *CompletionRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias CompletionRequest
|
||||
data, err := json.Marshal((*Alias)(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(r.ExtraBody) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range r.ExtraBody {
|
||||
raw[k] = v
|
||||
}
|
||||
return json.Marshal(raw)
|
||||
}
|
||||
|
||||
type CompletionResponse struct {
|
||||
Content string `json:"content"`
|
||||
FinishReason string `json:"finish_reason,omitempty"`
|
||||
TokenUsage TokenUsage `json:"token_usage,omitempty"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type TokenUsage struct {
|
||||
Prompt int `json:"prompt"`
|
||||
Completion int `json:"completion"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
}
|
||||
|
||||
type apiToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function apiFunction `json:"function"`
|
||||
}
|
||||
|
||||
type apiFunction struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
type StreamChunk struct {
|
||||
Content string `json:"content"`
|
||||
Done bool `json:"done"`
|
||||
ToolCall *ToolCall `json:"tool_call,omitempty"`
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
Name() string
|
||||
Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error)
|
||||
ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error)
|
||||
}
|
||||
|
||||
type BaseConfig struct {
|
||||
Model string `json:"model"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
}
|
||||
|
||||
type OpenAIProvider struct {
|
||||
cfg BaseConfig
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewOpenAIProvider(cfg BaseConfig) *OpenAIProvider {
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "https://api.openai.com/v1"
|
||||
}
|
||||
if cfg.Temperature == 0 {
|
||||
cfg.Temperature = 0.7
|
||||
}
|
||||
if cfg.MaxTokens == 0 {
|
||||
cfg.MaxTokens = 4096
|
||||
}
|
||||
return &OpenAIProvider{
|
||||
cfg: cfg,
|
||||
client: &http.Client{Timeout: 60 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) Name() string { return "openai" }
|
||||
|
||||
func (p *OpenAIProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||||
if req.Model == "" {
|
||||
req.Model = p.cfg.Model
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(req)
|
||||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/chat/completions", strings.NewReader(string(body)))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||||
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("api call: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var rawResult struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content *string `json:"content"`
|
||||
ReasoningContent *string `json:"reasoning_content"`
|
||||
ToolCalls []rawToolCall `json:"tool_calls"`
|
||||
Role string `json:"role"`
|
||||
} `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rawResult); err != nil {
|
||||
return nil, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
|
||||
if len(rawResult.Choices) == 0 {
|
||||
return nil, fmt.Errorf("no choices returned")
|
||||
}
|
||||
|
||||
ch := rawResult.Choices[0]
|
||||
content := ""
|
||||
if ch.Message.Content != nil {
|
||||
content = *ch.Message.Content
|
||||
}
|
||||
|
||||
var toolCalls []ToolCall
|
||||
for _, tc := range ch.Message.ToolCalls {
|
||||
tc := tc
|
||||
args := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
|
||||
args["_raw"] = tc.Function.Arguments
|
||||
}
|
||||
toolCalls = append(toolCalls, ToolCall{
|
||||
ID: tc.ID,
|
||||
Type: tc.Type,
|
||||
Name: tc.Function.Name,
|
||||
Arguments: args,
|
||||
})
|
||||
}
|
||||
|
||||
return &CompletionResponse{
|
||||
Content: content,
|
||||
FinishReason: ch.FinishReason,
|
||||
TokenUsage: TokenUsage{
|
||||
Prompt: rawResult.Usage.PromptTokens,
|
||||
Completion: rawResult.Usage.CompletionTokens,
|
||||
Total: rawResult.Usage.TotalTokens,
|
||||
},
|
||||
ToolCalls: toolCalls,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||||
req.Stream = true
|
||||
ch := make(chan StreamChunk, 64)
|
||||
|
||||
body, _ := json.Marshal(req)
|
||||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/chat/completions", strings.NewReader(string(body)))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||||
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stream api: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer resp.Body.Close()
|
||||
defer close(ch)
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
for {
|
||||
var line struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"delta"`
|
||||
FinishReason *string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
if err := decoder.Decode(&line); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if len(line.Choices) > 0 {
|
||||
ch <- StreamChunk{
|
||||
Content: line.Choices[0].Delta.Content,
|
||||
Done: line.Choices[0].FinishReason != nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type OllamaProvider struct {
|
||||
cfg BaseConfig
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewOllamaProvider(cfg BaseConfig) *OllamaProvider {
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "http://localhost:11434"
|
||||
}
|
||||
if cfg.Temperature == 0 {
|
||||
cfg.Temperature = 0.7
|
||||
}
|
||||
if cfg.MaxTokens == 0 {
|
||||
cfg.MaxTokens = 4096
|
||||
}
|
||||
return &OllamaProvider{
|
||||
cfg: cfg,
|
||||
client: &http.Client{Timeout: 120 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OllamaProvider) Name() string { return "ollama" }
|
||||
|
||||
func (p *OllamaProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||||
ollamaReq := map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"messages": req.Messages,
|
||||
"stream": false,
|
||||
"options": map[string]interface{}{
|
||||
"temperature": req.Temperature,
|
||||
"num_predict": req.MaxTokens,
|
||||
},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(ollamaReq)
|
||||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/api/chat", strings.NewReader(string(body)))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ollama chat: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
DoneReason string `json:"done_reason"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
|
||||
return &CompletionResponse{
|
||||
Content: result.Message.Content,
|
||||
FinishReason: result.DoneReason,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OllamaProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||||
ch := make(chan StreamChunk, 64)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
ch <- StreamChunk{Done: true}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type LuaAdaptedProvider struct {
|
||||
name string
|
||||
base Provider
|
||||
vm *luaVM.VM
|
||||
adapter string
|
||||
}
|
||||
|
||||
func NewLuaAdaptedProvider(base Provider, vm *luaVM.VM, adapter string) *LuaAdaptedProvider {
|
||||
return &LuaAdaptedProvider{
|
||||
name: fmt.Sprintf("lua_%s", adapter),
|
||||
base: base,
|
||||
vm: vm,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *LuaAdaptedProvider) Name() string { return p.name }
|
||||
|
||||
func (p *LuaAdaptedProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||||
inputMap := map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"messages": messagesToMap(req.Messages),
|
||||
"temperature": req.Temperature,
|
||||
"max_tokens": req.MaxTokens,
|
||||
"stream": false,
|
||||
}
|
||||
|
||||
transformed, err := p.vm.CallTransform(p.adapter, inputMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lua transform: %w", err)
|
||||
}
|
||||
|
||||
transformedReq := &CompletionRequest{
|
||||
Model: getString(transformed, "model"),
|
||||
Temperature: getFloat(transformed, "temperature"),
|
||||
MaxTokens: int(getFloat(transformed, "max_tokens")),
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
if msgs, ok := transformed["messages"].([]interface{}); ok {
|
||||
for _, m := range msgs {
|
||||
if mm, ok := m.(map[string]interface{}); ok {
|
||||
transformedReq.Messages = append(transformedReq.Messages, Message{
|
||||
Role: getString(mm, "role"),
|
||||
Content: getString(mm, "content"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := p.base.Chat(ctx, transformedReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||||
return p.base.ChatStream(ctx, req)
|
||||
}
|
||||
|
||||
type ProviderManager struct {
|
||||
mu sync.RWMutex
|
||||
providers map[string]Provider
|
||||
default_ string
|
||||
}
|
||||
|
||||
func NewProviderManager() *ProviderManager {
|
||||
return &ProviderManager{
|
||||
providers: make(map[string]Provider),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ProviderManager) Register(name string, p Provider) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.providers[name] = p
|
||||
if m.default_ == "" {
|
||||
m.default_ = name
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ProviderManager) SetDefault(name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, ok := m.providers[name]; !ok {
|
||||
return fmt.Errorf("provider %s not found", name)
|
||||
}
|
||||
m.default_ = name
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ProviderManager) Get(name string) Provider {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if name == "" {
|
||||
name = m.default_
|
||||
}
|
||||
return m.providers[name]
|
||||
}
|
||||
|
||||
func (m *ProviderManager) Default() Provider {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.providers[m.default_]
|
||||
}
|
||||
|
||||
func (m *ProviderManager) List() []string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var names []string
|
||||
for n := range m.providers {
|
||||
names = append(names, n)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
type rawToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
func messagesToMap(msgs []Message) []interface{} {
|
||||
result := make([]interface{}, len(msgs))
|
||||
for i, m := range msgs {
|
||||
result[i] = map[string]interface{}{
|
||||
"role": m.Role,
|
||||
"content": m.Content,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
1066
internal/agent/core/agent.go
Normal file
1066
internal/agent/core/agent.go
Normal file
File diff suppressed because it is too large
Load Diff
172
internal/agent/core/context.go
Normal file
172
internal/agent/core/context.go
Normal file
@ -0,0 +1,172 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
)
|
||||
|
||||
// ContextEvent — 单条上下文事件
|
||||
type ContextEvent struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Source string `json:"source"`
|
||||
Input string `json:"input"`
|
||||
Response string `json:"response,omitempty"`
|
||||
ToolsUsed []string `json:"tools_used,omitempty"`
|
||||
Vector vector.Vector `json:"-"` // 缓存向量,避免重复计算
|
||||
}
|
||||
|
||||
// RelevanceContext — 基于相关性的上下文管理,非固定阈值
|
||||
type RelevanceContext struct {
|
||||
mu sync.Mutex
|
||||
events []*ContextEvent
|
||||
veczer *vector.TFIDFVectorizer
|
||||
trained bool
|
||||
}
|
||||
|
||||
func NewRelevanceContext() *RelevanceContext {
|
||||
return &RelevanceContext{
|
||||
veczer: vector.NewTFIDFVectorizer(2),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RelevanceContext) Append(evt ContextEvent) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
evt.Vector = c.veczer.Vectorize(evt.Input + " " + evt.Response)
|
||||
c.events = append(c.events, &evt)
|
||||
|
||||
// 增量训练向量化器
|
||||
c.trained = false
|
||||
}
|
||||
|
||||
// Prune — 基于当前输入计算每条上下文的相关性,归档最不相关的
|
||||
// 返回被归档的事件(转为文档),保留 topK 个最相关的在活跃上下文中
|
||||
func (c *RelevanceContext) Prune(currentInput string, topK int, docStore *document.Store) int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if len(c.events) <= topK {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 确保向量化器已训练
|
||||
c.ensureTrained()
|
||||
|
||||
queryVec := c.veczer.Vectorize(currentInput)
|
||||
|
||||
// 计算每条上下文与当前输入的相关性
|
||||
type scored struct {
|
||||
event *ContextEvent
|
||||
score float64
|
||||
idx int
|
||||
}
|
||||
scoredEvents := make([]scored, len(c.events))
|
||||
for i, evt := range c.events {
|
||||
score := vector.CosineSimilarity(queryVec, evt.Vector)
|
||||
scoredEvents[i] = scored{event: evt, score: score, idx: i}
|
||||
}
|
||||
|
||||
// 按相关性从高到低排序
|
||||
sort.Slice(scoredEvents, func(i, j int) bool {
|
||||
return scoredEvents[i].score > scoredEvents[j].score
|
||||
})
|
||||
|
||||
// 保留 topK 最相关的
|
||||
keep := scoredEvents
|
||||
if len(keep) > topK {
|
||||
keep = keep[:topK]
|
||||
}
|
||||
archive := scoredEvents[topK:]
|
||||
|
||||
// 重建 events 为保留的
|
||||
c.events = make([]*ContextEvent, len(keep))
|
||||
for i, s := range keep {
|
||||
c.events[i] = s.event
|
||||
}
|
||||
|
||||
// 按时间重新排序
|
||||
sort.Slice(c.events, func(i, j int) bool {
|
||||
return c.events[i].Timestamp.Before(c.events[j].Timestamp)
|
||||
})
|
||||
|
||||
// 归档到文档记忆
|
||||
archived := 0
|
||||
if docStore != nil && len(archive) > 0 {
|
||||
entries := make([]document.ContextEntry, len(archive))
|
||||
for i, s := range archive {
|
||||
entries[i] = document.ContextEntry{
|
||||
Timestamp: s.event.Timestamp,
|
||||
Source: s.event.Source,
|
||||
Content: s.event.Input,
|
||||
Response: s.event.Response,
|
||||
}
|
||||
}
|
||||
doc, err := docStore.ContextToDoc("context_archived", entries)
|
||||
if err == nil && doc != nil {
|
||||
archived = len(archive)
|
||||
}
|
||||
}
|
||||
|
||||
return archived
|
||||
}
|
||||
|
||||
// Format — 输出活跃上下文的文本,用于注入 prompt
|
||||
func (c *RelevanceContext) Format() string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if len(c.events) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("【近期事件】\n")
|
||||
for _, e := range c.events {
|
||||
sb.WriteString(fmt.Sprintf("[%s] %s: %s", e.Timestamp.Format("15:04:05"), e.Source, e.Input))
|
||||
if e.Response != "" {
|
||||
sb.WriteString(fmt.Sprintf(" → %s", truncateStr(e.Response, 80)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Recent — 返回最近 n 条
|
||||
func (c *RelevanceContext) Recent(n int) []ContextEvent {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if n <= 0 || n > len(c.events) {
|
||||
n = len(c.events)
|
||||
}
|
||||
result := make([]ContextEvent, n)
|
||||
for i, evt := range c.events[len(c.events)-n:] {
|
||||
result[i] = *evt
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Len — 当前上下文事件数
|
||||
func (c *RelevanceContext) Len() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.events)
|
||||
}
|
||||
|
||||
func (c *RelevanceContext) ensureTrained() {
|
||||
if !c.trained && len(c.events) > 0 {
|
||||
texts := make([]string, len(c.events))
|
||||
for i, evt := range c.events {
|
||||
texts[i] = evt.Input + " " + evt.Response
|
||||
}
|
||||
c.veczer.Train(texts)
|
||||
c.trained = true
|
||||
}
|
||||
}
|
||||
546
internal/agent/io/channel.go
Normal file
546
internal/agent/io/channel.go
Normal file
@ -0,0 +1,546 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeviceType int
|
||||
|
||||
const (
|
||||
DeviceInput DeviceType = 0
|
||||
DeviceOutput DeviceType = 1
|
||||
DeviceIO DeviceType = 2
|
||||
)
|
||||
|
||||
// OutputCapability 定义通道支持的输出格式
|
||||
type OutputCapability int
|
||||
|
||||
const (
|
||||
CapText OutputCapability = 1 << iota // 文本
|
||||
CapFile // 文件
|
||||
CapImage // 图片
|
||||
CapAudio // 音频
|
||||
CapStructured // 结构化数据(JSON/卡片)
|
||||
)
|
||||
|
||||
func (c OutputCapability) Supports(cap OutputCapability) bool {
|
||||
return c&cap != 0
|
||||
}
|
||||
|
||||
func (c OutputCapability) String() string {
|
||||
var flags []string
|
||||
if c&CapText != 0 {
|
||||
flags = append(flags, "text")
|
||||
}
|
||||
if c&CapFile != 0 {
|
||||
flags = append(flags, "file")
|
||||
}
|
||||
if c&CapImage != 0 {
|
||||
flags = append(flags, "image")
|
||||
}
|
||||
if c&CapAudio != 0 {
|
||||
flags = append(flags, "audio")
|
||||
}
|
||||
if c&CapStructured != 0 {
|
||||
flags = append(flags, "structured")
|
||||
}
|
||||
return fmt.Sprintf("%v", flags)
|
||||
}
|
||||
|
||||
type Device interface {
|
||||
Name() string
|
||||
Type() DeviceType
|
||||
Description() string
|
||||
Tools() []ToolDef
|
||||
Execute(tool string, args map[string]interface{}) (interface{}, error)
|
||||
Start() error
|
||||
Stop() error
|
||||
OutputCapabilities() OutputCapability
|
||||
}
|
||||
|
||||
type ToolHandler func(args map[string]interface{}) (interface{}, error)
|
||||
|
||||
type ToolDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
Handler ToolHandler `json:"-"` // 可选:插件工具的直接处理器,Device 通过 Execute() 分发
|
||||
}
|
||||
|
||||
type InputEvent struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Source string `json:"source"`
|
||||
Type string `json:"type"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
ResponseCh chan<- *OutputEvent `json:"-"`
|
||||
OutputChannel string `json:"output_channel"` // 默认输出通道(不传则等于 Source)
|
||||
}
|
||||
|
||||
type OutputEvent struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Target string `json:"target"`
|
||||
Type string `json:"type"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
Done bool `json:"done,omitempty"`
|
||||
OutputChannel string `json:"output_channel"` // 路由到此通道
|
||||
}
|
||||
|
||||
type IOManager struct {
|
||||
mu sync.RWMutex
|
||||
devices map[string]Device
|
||||
inputCh chan *InputEvent
|
||||
outputCh chan *OutputEvent
|
||||
nextReqID int64
|
||||
routes map[string]string // 输入源 → 默认输出通道 e.g. "mic" → "speaker"
|
||||
}
|
||||
|
||||
func NewIOManager() *IOManager {
|
||||
return &IOManager{
|
||||
devices: make(map[string]Device),
|
||||
inputCh: make(chan *InputEvent, 256),
|
||||
outputCh: make(chan *OutputEvent, 256),
|
||||
routes: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterOutputRoute 注册输入源 → 默认输出通道映射
|
||||
// 例如:mic → speaker,voice_input → speaker
|
||||
func (m *IOManager) RegisterOutputRoute(inputSource, outputChannel string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.routes[inputSource] = outputChannel
|
||||
}
|
||||
|
||||
// DefaultOutput 返回输入源的默认输出通道
|
||||
func (m *IOManager) DefaultOutput(source string) string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if ch, ok := m.routes[source]; ok {
|
||||
return ch
|
||||
}
|
||||
return source // 默认等于输入源
|
||||
}
|
||||
|
||||
func (m *IOManager) nextRequestID() string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.nextReqID++
|
||||
return fmt.Sprintf("req_%d_%d", time.Now().UnixNano(), m.nextReqID)
|
||||
}
|
||||
|
||||
func (m *IOManager) UnregisterDevice(name string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.devices, name)
|
||||
for src, dst := range m.routes {
|
||||
if src == name || dst == name {
|
||||
delete(m.routes, src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AtomicSwapDevices 原子化替换全部 IO 设备与路由表
|
||||
// 1. 新设备必须在调用前已完成 Start()
|
||||
// 2. 调用后旧设备立即从路由表中摘除,新请求走向新设备
|
||||
// 3. 返回旧设备列表,由调用方负责 Stop()
|
||||
func (m *IOManager) AtomicSwapDevices(newDevices map[string]Device, newRoutes map[string]string) map[string]Device {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
oldDevices := m.devices
|
||||
m.devices = newDevices
|
||||
|
||||
m.routes = newRoutes
|
||||
|
||||
return oldDevices
|
||||
}
|
||||
|
||||
func (m *IOManager) RegisterDevice(dev Device) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, ok := m.devices[dev.Name()]; ok {
|
||||
return fmt.Errorf("device %s already registered", dev.Name())
|
||||
}
|
||||
m.devices[dev.Name()] = dev
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IOManager) StartAll() error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for name, dev := range m.devices {
|
||||
if err := dev.Start(); err != nil {
|
||||
return fmt.Errorf("start device %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IOManager) StopAll() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for _, dev := range m.devices {
|
||||
dev.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IOManager) InjectInput(source string, eventType string, payload map[string]interface{}) {
|
||||
m.inputCh <- &InputEvent{
|
||||
RequestID: m.nextRequestID(),
|
||||
Source: source,
|
||||
Type: eventType,
|
||||
Payload: payload,
|
||||
OutputChannel: m.DefaultOutput(source),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IOManager) InjectInputSync(source string, eventType string, payload map[string]interface{}) *OutputEvent {
|
||||
ch := make(chan *OutputEvent, 1)
|
||||
m.inputCh <- &InputEvent{
|
||||
RequestID: m.nextRequestID(),
|
||||
Source: source,
|
||||
Type: eventType,
|
||||
Payload: payload,
|
||||
ResponseCh: ch,
|
||||
OutputChannel: m.DefaultOutput(source),
|
||||
}
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (m *IOManager) InjectText(source string, text string) {
|
||||
m.InjectInput(source, "text", map[string]interface{}{
|
||||
"content": text,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *IOManager) InjectTextSync(source string, text string) *OutputEvent {
|
||||
return m.InjectInputSync(source, "text", map[string]interface{}{
|
||||
"content": text,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *IOManager) EmitOutput(target string, outputType string, payload map[string]interface{}) {
|
||||
m.outputCh <- &OutputEvent{
|
||||
RequestID: "",
|
||||
Target: target,
|
||||
Type: outputType,
|
||||
Payload: payload,
|
||||
Done: true,
|
||||
}
|
||||
}
|
||||
|
||||
// EmitOutputTo 通过指定输出通道发送
|
||||
func (m *IOManager) EmitOutputTo(target, outputChannel, outputType string, payload map[string]interface{}) {
|
||||
m.outputCh <- &OutputEvent{
|
||||
RequestID: "",
|
||||
Target: target,
|
||||
Type: outputType,
|
||||
Payload: payload,
|
||||
Done: true,
|
||||
OutputChannel: outputChannel,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IOManager) EmitText(target string, text string) {
|
||||
m.EmitOutput(target, "text", map[string]interface{}{
|
||||
"content": text,
|
||||
})
|
||||
}
|
||||
|
||||
// EmitTextTo 通过指定输出通道发送文本
|
||||
func (m *IOManager) EmitTextTo(target, outputChannel, text string) {
|
||||
m.EmitOutputTo(target, outputChannel, "text", map[string]interface{}{
|
||||
"content": text,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *IOManager) InputChan() <-chan *InputEvent { return m.inputCh }
|
||||
func (m *IOManager) OutputChan() <-chan *OutputEvent { return m.outputCh }
|
||||
|
||||
func (m *IOManager) GetAllTools() []ToolDef {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var tools []ToolDef
|
||||
for _, dev := range m.devices {
|
||||
tools = append(tools, dev.Tools()...)
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
func (m *IOManager) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for _, dev := range m.devices {
|
||||
for _, t := range dev.Tools() {
|
||||
if t.Name == name {
|
||||
return dev.Execute(name, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("tool %s not found", name)
|
||||
}
|
||||
|
||||
func (m *IOManager) ListDevices() []Device {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
list := make([]Device, 0, len(m.devices))
|
||||
for _, d := range m.devices {
|
||||
list = append(list, d)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// ChannelInfo 返回 IOManager 中已注册的所有通道信息
|
||||
type ChannelInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type DeviceType `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Tools []ToolDef `json:"tools"`
|
||||
OutputCaps OutputCapability `json:"output_capabilities"`
|
||||
}
|
||||
|
||||
func (m *IOManager) ListChannels() []ChannelInfo {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var list []ChannelInfo
|
||||
for _, dev := range m.devices {
|
||||
list = append(list, ChannelInfo{
|
||||
Name: dev.Name(),
|
||||
Type: dev.Type(),
|
||||
Description: dev.Description(),
|
||||
Tools: dev.Tools(),
|
||||
OutputCaps: dev.OutputCapabilities(),
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (m *IOManager) GetChannelCapabilities(channel string) OutputCapability {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if dev, ok := m.devices[channel]; ok {
|
||||
return dev.OutputCapabilities()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Microphone
|
||||
type Microphone struct {
|
||||
name string
|
||||
sampleRate int
|
||||
io *IOManager
|
||||
}
|
||||
|
||||
func NewMicrophone(name string, sampleRate int, io *IOManager) *Microphone {
|
||||
return &Microphone{name: name, sampleRate: sampleRate, io: io}
|
||||
}
|
||||
|
||||
func (d *Microphone) Name() string { return d.name }
|
||||
func (d *Microphone) Type() DeviceType { return DeviceInput }
|
||||
func (d *Microphone) OutputCapabilities() OutputCapability { return 0 } // 纯输入
|
||||
func (d *Microphone) Description() string { return fmt.Sprintf("麦克风 (%s, %dHz)", d.name, d.sampleRate) }
|
||||
func (d *Microphone) Start() error { return nil }
|
||||
func (d *Microphone) Stop() error { return nil }
|
||||
|
||||
func (d *Microphone) Tools() []ToolDef {
|
||||
return []ToolDef{{
|
||||
Name: d.name + "_capture",
|
||||
Description: fmt.Sprintf("从 %s 录制音频", d.name),
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"duration": map[string]interface{}{"type": "number", "description": "录制时长(秒)", "default": 3},
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func (d *Microphone) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||||
return map[string]interface{}{"device": d.name, "status": "recorded", "format": "wav", "sample_rate": d.sampleRate}, nil
|
||||
}
|
||||
|
||||
// Speaker
|
||||
type Speaker struct {
|
||||
name string
|
||||
io *IOManager
|
||||
}
|
||||
|
||||
func NewSpeaker(name string, io *IOManager) *Speaker {
|
||||
return &Speaker{name: name, io: io}
|
||||
}
|
||||
|
||||
func (d *Speaker) Name() string { return d.name }
|
||||
func (d *Speaker) Type() DeviceType { return DeviceOutput }
|
||||
func (d *Speaker) OutputCapabilities() OutputCapability { return CapText | CapAudio }
|
||||
func (d *Speaker) Description() string { return fmt.Sprintf("扬声器 (%s)", d.name) }
|
||||
func (d *Speaker) Start() error { return nil }
|
||||
func (d *Speaker) Stop() error { return nil }
|
||||
|
||||
func (d *Speaker) Tools() []ToolDef {
|
||||
return []ToolDef{{
|
||||
Name: d.name + "_speak",
|
||||
Description: fmt.Sprintf("通过 %s 播放语音", d.name),
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"text": map[string]interface{}{"type": "string", "description": "播放文本"},
|
||||
},
|
||||
"required": []string{"text"},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func (d *Speaker) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||||
text, _ := args["text"].(string)
|
||||
return map[string]interface{}{"device": d.name, "status": "playing", "text": text}, nil
|
||||
}
|
||||
|
||||
// Camera
|
||||
type Camera struct {
|
||||
name string
|
||||
io *IOManager
|
||||
}
|
||||
|
||||
func NewCamera(name string, io *IOManager) *Camera {
|
||||
return &Camera{name: name, io: io}
|
||||
}
|
||||
|
||||
func (d *Camera) Name() string { return d.name }
|
||||
func (d *Camera) Type() DeviceType { return DeviceInput }
|
||||
func (d *Camera) OutputCapabilities() OutputCapability { return CapImage } // 可返回图片
|
||||
func (d *Camera) Description() string { return fmt.Sprintf("摄像头 (%s)", d.name) }
|
||||
func (d *Camera) Start() error { return nil }
|
||||
func (d *Camera) Stop() error { return nil }
|
||||
|
||||
func (d *Camera) Tools() []ToolDef {
|
||||
return []ToolDef{
|
||||
{
|
||||
Name: d.name + "_capture",
|
||||
Description: fmt.Sprintf("使用 %s 拍照", d.name),
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"quality": map[string]interface{}{"type": "integer", "description": "质量1-100", "default": 90},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: d.name + "_stream",
|
||||
Description: fmt.Sprintf("控制 %s 视频流", d.name),
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{"type": "string", "enum": []interface{}{"start", "stop"}},
|
||||
},
|
||||
"required": []string{"action"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Camera) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||||
return map[string]interface{}{"device": d.name, "status": "captured"}, nil
|
||||
}
|
||||
|
||||
// RobotArm
|
||||
type RobotArm struct {
|
||||
name string
|
||||
io *IOManager
|
||||
}
|
||||
|
||||
func NewRobotArm(name string, io *IOManager) *RobotArm {
|
||||
return &RobotArm{name: name, io: io}
|
||||
}
|
||||
|
||||
func (d *RobotArm) Name() string { return d.name }
|
||||
func (d *RobotArm) Type() DeviceType { return DeviceIO }
|
||||
func (d *RobotArm) OutputCapabilities() OutputCapability { return CapStructured }
|
||||
func (d *RobotArm) Description() string { return fmt.Sprintf("机械臂 (%s)", d.name) }
|
||||
func (d *RobotArm) Start() error { return nil }
|
||||
func (d *RobotArm) Stop() error { return nil }
|
||||
|
||||
func (d *RobotArm) Tools() []ToolDef {
|
||||
return []ToolDef{
|
||||
{
|
||||
Name: d.name + "_move",
|
||||
Description: fmt.Sprintf("移动 %s 到坐标", d.name),
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"x": map[string]interface{}{"type": "number", "description": "X 轴"},
|
||||
"y": map[string]interface{}{"type": "number", "description": "Y 轴"},
|
||||
"z": map[string]interface{}{"type": "number", "description": "Z 轴"},
|
||||
},
|
||||
"required": []string{"x", "y", "z"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: d.name + "_grip",
|
||||
Description: fmt.Sprintf("控制 %s 夹爪", d.name),
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{"type": "string", "enum": []interface{}{"open", "close"}},
|
||||
},
|
||||
"required": []string{"action"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *RobotArm) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||||
return map[string]interface{}{"device": d.name, "tool": tool, "status": "executed"}, nil
|
||||
}
|
||||
|
||||
// GPIODevice
|
||||
type GPIODevice struct {
|
||||
name string
|
||||
pins []int
|
||||
io *IOManager
|
||||
}
|
||||
|
||||
func NewGPIODevice(name string, pins []int, io *IOManager) *GPIODevice {
|
||||
return &GPIODevice{name: name, pins: pins, io: io}
|
||||
}
|
||||
|
||||
func (d *GPIODevice) Name() string { return d.name }
|
||||
func (d *GPIODevice) Type() DeviceType { return DeviceIO }
|
||||
func (d *GPIODevice) OutputCapabilities() OutputCapability { return CapStructured }
|
||||
func (d *GPIODevice) Description() string { return "GPIO 通用引脚" }
|
||||
func (d *GPIODevice) Start() error { return nil }
|
||||
func (d *GPIODevice) Stop() error { return nil }
|
||||
|
||||
func (d *GPIODevice) Tools() []ToolDef {
|
||||
return []ToolDef{
|
||||
{
|
||||
Name: d.name + "_gpio_write",
|
||||
Description: "设置引脚电平",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"pin": map[string]interface{}{"type": "integer"},
|
||||
"value": map[string]interface{}{"type": "integer", "enum": []interface{}{0, 1}},
|
||||
},
|
||||
"required": []string{"pin", "value"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: d.name + "_gpio_read",
|
||||
Description: "读取引脚电平",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"pin": map[string]interface{}{"type": "integer"},
|
||||
},
|
||||
"required": []string{"pin"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *GPIODevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||||
return map[string]interface{}{"device": d.name, "tool": tool, "status": "ok"}, nil
|
||||
}
|
||||
41
internal/agent/personal.go
Normal file
41
internal/agent/personal.go
Normal file
@ -0,0 +1,41 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Personality struct {
|
||||
Content string
|
||||
Path string
|
||||
}
|
||||
|
||||
func LoadPersonality(path string) (*Personality, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &Personality{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read personal.md: %w", err)
|
||||
}
|
||||
return &Personality{
|
||||
Content: string(data),
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func SavePersonality(path, content string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create personality dir: %w", err)
|
||||
}
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
|
||||
func (p *Personality) InjectPrompt() string {
|
||||
if p.Content == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("【人格设定】\n%s\n", p.Content)
|
||||
}
|
||||
Reference in New Issue
Block a user