From fcd230cb5bf3030b1c06b3e662b1c8c8da90550a Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 18 Jun 2026 08:35:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=99=E6=88=90=E5=B1=8E=E5=B1=B1=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai/service.go | 15 ++++--- autoreply/service.go | 94 +++++++++++++++++++++++++++++----------- completion/completion.go | 28 ++++++++++++ llm_store.go | 12 +++++ main.go | 8 ++-- toolmanager/manager.go | 42 +++++++++++++++--- toolrouter/loop.go | 31 ++++++++++++- 7 files changed, 186 insertions(+), 44 deletions(-) diff --git a/ai/service.go b/ai/service.go index 1a31362..8752d55 100644 --- a/ai/service.go +++ b/ai/service.go @@ -18,17 +18,18 @@ import ( "gorm.io/gorm" ) -// SystemPromptStore is the interface for getting the system prompt -type SystemPromptStore interface { +// ToolConfigStore is the interface for getting tool configuration +type ToolConfigStore interface { GetLLMPrimaryConfigSystemPrompt() (string, error) + GetLLMPrimaryConfigEnableTool() (bool, error) } // Config holds the AI service configuration type Config struct { - LLMProviders []llm.ProviderConfig - DataDir string - Enabled bool - SystemPromptStore SystemPromptStore + LLMProviders []llm.ProviderConfig + DataDir string + Enabled bool + ToolConfigStore ToolConfigStore } // Service manages all AI-related components @@ -97,7 +98,7 @@ func NewService(cfg Config, db *gorm.DB, botSender autoreply.BotSender) (*Servic convStore, msgQueue, botSender, - cfg.SystemPromptStore, + cfg.ToolConfigStore, ) return &Service{ diff --git a/autoreply/service.go b/autoreply/service.go index 44184a0..a277e86 100644 --- a/autoreply/service.go +++ b/autoreply/service.go @@ -14,6 +14,8 @@ import ( "meshtastic_mqtt_server/message" "meshtastic_mqtt_server/toolmanager" "meshtastic_mqtt_server/toolrouter" + + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" ) const ( @@ -63,20 +65,21 @@ type BotSender interface { SendChannelText(ctx context.Context, botID uint64, channelID string, text string) error } -// SystemPromptStore is the interface for getting the system prompt -type SystemPromptStore interface { +// ToolConfigStore is the interface for getting tool configuration +type ToolConfigStore interface { GetLLMPrimaryConfigSystemPrompt() (string, error) + GetLLMPrimaryConfigEnableTool() (bool, error) } // Service manages automatic AI replies for bots type Service struct { - llmState *llm.State - toolRouter *toolrouter.State - toolMgr *toolmanager.Manager - convStore *conversation.Store - msgQueue MessageQueue - botSender BotSender - systemPromptStore SystemPromptStore + llmState *llm.State + toolRouter *toolrouter.State + toolMgr *toolmanager.Manager + convStore *conversation.Store + msgQueue MessageQueue + botSender BotSender + toolConfigStore ToolConfigStore running bool mu sync.Mutex @@ -92,16 +95,16 @@ func NewService( convStore *conversation.Store, msgQueue MessageQueue, botSender BotSender, - systemPromptStore SystemPromptStore, + toolConfigStore ToolConfigStore, ) *Service { return &Service{ - llmState: llmState, - toolRouter: toolRouter, - toolMgr: toolMgr, - convStore: convStore, - msgQueue: msgQueue, - botSender: botSender, - systemPromptStore: systemPromptStore, + llmState: llmState, + toolRouter: toolRouter, + toolMgr: toolMgr, + convStore: convStore, + msgQueue: msgQueue, + botSender: botSender, + toolConfigStore: toolConfigStore, } } @@ -253,22 +256,61 @@ func (s *Service) processMessage(ctx context.Context, msg QueuedMessage) { return } - // Get system prompt from primary config + // Get system prompt and tool enable flag from primary config var systemPrompt string - if s.systemPromptStore != nil { - systemPrompt, err = s.systemPromptStore.GetLLMPrimaryConfigSystemPrompt() + enableTool := false + if s.toolConfigStore != nil { + systemPrompt, err = s.toolConfigStore.GetLLMPrimaryConfigSystemPrompt() if err != nil { printJSON(map[string]any{"event": "llm_system_prompt_warning", "msg_id": msg.ID, "error": err.Error()}) } + enableTool, err = s.toolConfigStore.GetLLMPrimaryConfigEnableTool() + if err != nil { + printJSON(map[string]any{"event": "llm_enable_tool_warning", "msg_id": msg.ID, "error": err.Error()}) + } } - // Run the tool loop to get augmented messages - augmentedMessages, err := toolrouter.RunAgentToolLoop(procCtx, s.toolRouter, profile, conv.Messages, s.toolMgr, nil) - _ = augmentedMessages // We'll use this in the future with proper tool support + // Print tool manager status for debugging + toolCount := 0 + if s.toolMgr != nil { + tools := s.toolMgr.Tools() + toolCount = len(tools) + toolNames := make([]string, 0, toolCount) + for _, t := range tools { + toolNames = append(toolNames, t.Name()) + } + printJSON(map[string]any{ + "event": "llm_tool_manager_status", + "msg_id": msg.ID, + "tool_count": toolCount, + "tool_names": toolNames, + "enable_tool": enableTool, + }) + } - // For now, use simple completion since we don't have tools registered yet - printJSON(map[string]any{"event": "llm_process_completion_start", "msg_id": msg.ID, "has_system_prompt": systemPrompt != ""}) - reply, err := completion.CompleteText(procCtx, profile, systemPrompt, conv.Messages, 512) + // Run the tool loop to get augmented messages - pass system prompt to tool router + // Tool loop will handle system prompt and tool calling + var augmentedMessages []*model.ChatCompletionMessage + if enableTool && toolCount > 0 { + augmentedMessages, err = toolrouter.RunAgentToolLoop(procCtx, s.toolRouter, profile, systemPrompt, conv.Messages, s.toolMgr, nil) + if err != nil { + printJSON(map[string]any{"event": "llm_tool_loop_warning", "msg_id": msg.ID, "error": err.Error()}) + // Continue with original messages if tool loop fails + } + } + + printJSON(map[string]any{"event": "llm_process_completion_start", "msg_id": msg.ID, "has_system_prompt": systemPrompt != "", "augmented_messages": len(augmentedMessages)}) + + // Use augmented messages from tool loop (already includes system prompt and tool results) + // If augmented messages is empty or nil, fallback to original messages with system prompt + var reply string + if len(augmentedMessages) > 0 { + // Use augmented messages from tool loop (already converted to model.ChatCompletionMessage) + reply, err = completion.CompleteTextWithArkMessages(procCtx, profile, augmentedMessages, 512) + } else { + // Fallback to simple completion + reply, err = completion.CompleteText(procCtx, profile, systemPrompt, conv.Messages, 512) + } if err != nil { errMsg := fmt.Sprintf("LLM completion failed: %v", err) printJSON(map[string]any{"event": "llm_process_failed", "msg_id": msg.ID, "step": "llm_completion", "error": errMsg}) diff --git a/completion/completion.go b/completion/completion.go index 8458883..3b9fc8a 100644 --- a/completion/completion.go +++ b/completion/completion.go @@ -85,3 +85,31 @@ func CompleteText(ctx context.Context, profile *llm.Profile, systemPrompt string } return "", nil } + +// CompleteTextWithArkMessages completes a text prompt using already converted Ark messages +// This is used when messages have already been converted (e.g. after tool loop) +func CompleteTextWithArkMessages(ctx context.Context, profile *llm.Profile, arkMessages []*model.ChatCompletionMessage, maxTokens int) (string, error) { + if profile == nil || profile.Client == nil { + return "", fmt.Errorf("llm profile or client is nil") + } + + req := model.CreateChatCompletionRequest{ + Model: profile.Config.Model, + Messages: arkMessages, + MaxTokens: &maxTokens, + } + + resp, err := profile.Client.CreateChatCompletion(ctx, req) + if err != nil { + return "", fmt.Errorf("text completion failed: %w", err) + } + + if len(resp.Choices) == 0 { + return "", fmt.Errorf("no completion choices returned") + } + + if resp.Choices[0].Message.Content != nil && resp.Choices[0].Message.Content.StringValue != nil { + return *resp.Choices[0].Message.Content.StringValue, nil + } + return "", nil +} diff --git a/llm_store.go b/llm_store.go index c4426c2..f797359 100644 --- a/llm_store.go +++ b/llm_store.go @@ -170,6 +170,18 @@ func (s *store) GetLLMPrimaryConfigSystemPrompt() (string, error) { return record.SystemPrompt, nil } +// GetLLMPrimaryConfigEnableTool 获取是否启用工具调用 +func (s *store) GetLLMPrimaryConfigEnableTool() (bool, error) { + record, err := s.GetLLMPrimaryConfig() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return record.EnableTool, nil +} + // CreateLLMPrimaryConfig 创建主 AI 回复配置 func (s *store) CreateLLMPrimaryConfig(record *llmPrimaryConfigRecord) error { if err := s.db.Create(record).Error; err != nil { diff --git a/main.go b/main.go index ca26d7c..881a4d6 100644 --- a/main.go +++ b/main.go @@ -297,10 +297,10 @@ func run(cfg *config) error { ) aiService, err = ai.NewService(ai.Config{ - LLMProviders: providerConfigs, - DataDir: cfg.DataDir, - Enabled: cfg.AI.Enabled, - SystemPromptStore: store, + LLMProviders: providerConfigs, + DataDir: cfg.DataDir, + Enabled: cfg.AI.Enabled, + ToolConfigStore: store, }, store.db, botSenderAdapter) if err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to initialize AI service: %v\n", err) diff --git a/toolmanager/manager.go b/toolmanager/manager.go index a06c6a9..3b313e0 100644 --- a/toolmanager/manager.go +++ b/toolmanager/manager.go @@ -18,17 +18,21 @@ type Manager struct { } // Load loads tools from the given directory +// If directory doesn't exist or is empty, automatically loads all registered tools func Load(root string, options agenttool.LoadOptions) (*Manager, error) { + manager := &Manager{tools: map[string]agenttool.LoadedTool{}} + + // Try to read directory entries, err := os.ReadDir(root) if err != nil { - // Directory doesn't exist, create empty manager - if os.IsNotExist(err) { - return &Manager{tools: map[string]agenttool.LoadedTool{}}, nil + if !os.IsNotExist(err) { + return nil, fmt.Errorf("failed to read tools directory: %w", err) } - return nil, fmt.Errorf("failed to read tools directory: %w", err) + // Directory doesn't exist, continue to load all registered tools + entries = []os.DirEntry{} } - manager := &Manager{tools: map[string]agenttool.LoadedTool{}} + // Load tools from directory if they exist for _, entry := range entries { if !entry.IsDir() { continue @@ -57,6 +61,34 @@ func Load(root string, options agenttool.LoadOptions) (*Manager, error) { manager.tools[toolName] = tool manager.order = append(manager.order, toolName) } + + // If no tools loaded from directory, automatically load all registered tools + if len(manager.tools) == 0 { + registeredTools := agenttool.Names() + for _, name := range registeredTools { + descriptor, ok := agenttool.Lookup(name) + if !ok { + continue + } + // Use empty path for tools that don't require configuration files + tool, err := descriptor.Load("", options) + if err != nil { + continue + } + if tool == nil { + continue + } + toolName := strings.ToLower(strings.TrimSpace(tool.Name())) + if toolName == "" { + toolName = name + } + if _, ok := manager.tools[toolName]; ok { + continue + } + manager.tools[toolName] = tool + manager.order = append(manager.order, toolName) + } + } return manager, nil } diff --git a/toolrouter/loop.go b/toolrouter/loop.go index a0445b3..663ed01 100644 --- a/toolrouter/loop.go +++ b/toolrouter/loop.go @@ -18,7 +18,8 @@ import ( const maxAgentToolIterations = 6 // RunAgentToolLoop runs the agent tool calling loop -func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, chatMessages []message.ChatMessage, manager *toolmanager.Manager, emit stream.EmitFunc) ([]*model.ChatCompletionMessage, error) { +// systemPrompt is the primary system prompt from LLM config +func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, systemPrompt string, chatMessages []message.ChatMessage, manager *toolmanager.Manager, emit stream.EmitFunc) ([]*model.ChatCompletionMessage, error) { finalMessages, err := buildArkMessages(chatMessages) if err != nil { return nil, err @@ -29,6 +30,16 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, c } tools := availableAgentTools(state, routerProfile, manager, emit) if len(tools) == 0 { + // No tools available, add system prompt and return + if strings.TrimSpace(systemPrompt) != "" { + systemMessage := &model.ChatCompletionMessage{ + Role: "system", + Content: &model.ChatCompletionMessageContent{ + StringValue: &systemPrompt, + }, + } + finalMessages = append([]*model.ChatCompletionMessage{systemMessage}, finalMessages...) + } return finalMessages, nil } @@ -50,9 +61,25 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, c emit(stream.Frame{Type: "trace", Tool: "agent_tools", Stage: "prepare", Status: "success", Message: "已准备可用工具", Data: map[string]any{"tools": availableNames, "tool_descriptions": toolDescriptions}}) } if state == nil || state.cfg == nil { + // No tool router config, but we have tools - use primary system prompt + if strings.TrimSpace(systemPrompt) != "" { + systemMessage := &model.ChatCompletionMessage{ + Role: "system", + Content: &model.ChatCompletionMessageContent{ + StringValue: &systemPrompt, + }, + } + finalMessages = append([]*model.ChatCompletionMessage{systemMessage}, finalMessages...) + decisionMessages = append([]*model.ChatCompletionMessage{systemMessage}, decisionMessages...) + } return finalMessages, nil } - if prompt := strings.TrimSpace(state.cfg.SystemPrompt); prompt != "" { + // Use tool router system prompt if available, otherwise fall back to primary system prompt + prompt := strings.TrimSpace(state.cfg.SystemPrompt) + if prompt == "" { + prompt = strings.TrimSpace(systemPrompt) + } + if prompt != "" { systemMessage := &model.ChatCompletionMessage{ Role: "system", Content: &model.ChatCompletionMessageContent{