写成屎山了

This commit is contained in:
2026-06-18 08:35:09 +08:00
parent 45822fd333
commit fcd230cb5b
7 changed files with 186 additions and 44 deletions
+5 -4
View File
@@ -18,9 +18,10 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// SystemPromptStore is the interface for getting the system prompt // ToolConfigStore is the interface for getting tool configuration
type SystemPromptStore interface { type ToolConfigStore interface {
GetLLMPrimaryConfigSystemPrompt() (string, error) GetLLMPrimaryConfigSystemPrompt() (string, error)
GetLLMPrimaryConfigEnableTool() (bool, error)
} }
// Config holds the AI service configuration // Config holds the AI service configuration
@@ -28,7 +29,7 @@ type Config struct {
LLMProviders []llm.ProviderConfig LLMProviders []llm.ProviderConfig
DataDir string DataDir string
Enabled bool Enabled bool
SystemPromptStore SystemPromptStore ToolConfigStore ToolConfigStore
} }
// Service manages all AI-related components // Service manages all AI-related components
@@ -97,7 +98,7 @@ func NewService(cfg Config, db *gorm.DB, botSender autoreply.BotSender) (*Servic
convStore, convStore,
msgQueue, msgQueue,
botSender, botSender,
cfg.SystemPromptStore, cfg.ToolConfigStore,
) )
return &Service{ return &Service{
+56 -14
View File
@@ -14,6 +14,8 @@ import (
"meshtastic_mqtt_server/message" "meshtastic_mqtt_server/message"
"meshtastic_mqtt_server/toolmanager" "meshtastic_mqtt_server/toolmanager"
"meshtastic_mqtt_server/toolrouter" "meshtastic_mqtt_server/toolrouter"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
) )
const ( const (
@@ -63,9 +65,10 @@ type BotSender interface {
SendChannelText(ctx context.Context, botID uint64, channelID string, text string) error SendChannelText(ctx context.Context, botID uint64, channelID string, text string) error
} }
// SystemPromptStore is the interface for getting the system prompt // ToolConfigStore is the interface for getting tool configuration
type SystemPromptStore interface { type ToolConfigStore interface {
GetLLMPrimaryConfigSystemPrompt() (string, error) GetLLMPrimaryConfigSystemPrompt() (string, error)
GetLLMPrimaryConfigEnableTool() (bool, error)
} }
// Service manages automatic AI replies for bots // Service manages automatic AI replies for bots
@@ -76,7 +79,7 @@ type Service struct {
convStore *conversation.Store convStore *conversation.Store
msgQueue MessageQueue msgQueue MessageQueue
botSender BotSender botSender BotSender
systemPromptStore SystemPromptStore toolConfigStore ToolConfigStore
running bool running bool
mu sync.Mutex mu sync.Mutex
@@ -92,7 +95,7 @@ func NewService(
convStore *conversation.Store, convStore *conversation.Store,
msgQueue MessageQueue, msgQueue MessageQueue,
botSender BotSender, botSender BotSender,
systemPromptStore SystemPromptStore, toolConfigStore ToolConfigStore,
) *Service { ) *Service {
return &Service{ return &Service{
llmState: llmState, llmState: llmState,
@@ -101,7 +104,7 @@ func NewService(
convStore: convStore, convStore: convStore,
msgQueue: msgQueue, msgQueue: msgQueue,
botSender: botSender, botSender: botSender,
systemPromptStore: systemPromptStore, toolConfigStore: toolConfigStore,
} }
} }
@@ -253,22 +256,61 @@ func (s *Service) processMessage(ctx context.Context, msg QueuedMessage) {
return return
} }
// Get system prompt from primary config // Get system prompt and tool enable flag from primary config
var systemPrompt string var systemPrompt string
if s.systemPromptStore != nil { enableTool := false
systemPrompt, err = s.systemPromptStore.GetLLMPrimaryConfigSystemPrompt() if s.toolConfigStore != nil {
systemPrompt, err = s.toolConfigStore.GetLLMPrimaryConfigSystemPrompt()
if err != nil { if err != nil {
printJSON(map[string]any{"event": "llm_system_prompt_warning", "msg_id": msg.ID, "error": err.Error()}) 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 // Print tool manager status for debugging
augmentedMessages, err := toolrouter.RunAgentToolLoop(procCtx, s.toolRouter, profile, conv.Messages, s.toolMgr, nil) toolCount := 0
_ = augmentedMessages // We'll use this in the future with proper tool support 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 // Run the tool loop to get augmented messages - pass system prompt to tool router
printJSON(map[string]any{"event": "llm_process_completion_start", "msg_id": msg.ID, "has_system_prompt": systemPrompt != ""}) // Tool loop will handle system prompt and tool calling
reply, err := completion.CompleteText(procCtx, profile, systemPrompt, conv.Messages, 512) 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 { if err != nil {
errMsg := fmt.Sprintf("LLM completion failed: %v", err) 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}) printJSON(map[string]any{"event": "llm_process_failed", "msg_id": msg.ID, "step": "llm_completion", "error": errMsg})
+28
View File
@@ -85,3 +85,31 @@ func CompleteText(ctx context.Context, profile *llm.Profile, systemPrompt string
} }
return "", nil 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
}
+12
View File
@@ -170,6 +170,18 @@ func (s *store) GetLLMPrimaryConfigSystemPrompt() (string, error) {
return record.SystemPrompt, nil 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 回复配置 // CreateLLMPrimaryConfig 创建主 AI 回复配置
func (s *store) CreateLLMPrimaryConfig(record *llmPrimaryConfigRecord) error { func (s *store) CreateLLMPrimaryConfig(record *llmPrimaryConfigRecord) error {
if err := s.db.Create(record).Error; err != nil { if err := s.db.Create(record).Error; err != nil {
+1 -1
View File
@@ -300,7 +300,7 @@ func run(cfg *config) error {
LLMProviders: providerConfigs, LLMProviders: providerConfigs,
DataDir: cfg.DataDir, DataDir: cfg.DataDir,
Enabled: cfg.AI.Enabled, Enabled: cfg.AI.Enabled,
SystemPromptStore: store, ToolConfigStore: store,
}, store.db, botSenderAdapter) }, store.db, botSenderAdapter)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to initialize AI service: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: failed to initialize AI service: %v\n", err)
+37 -5
View File
@@ -18,17 +18,21 @@ type Manager struct {
} }
// Load loads tools from the given directory // 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) { 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) entries, err := os.ReadDir(root)
if err != nil { if err != nil {
// Directory doesn't exist, create empty manager if !os.IsNotExist(err) {
if os.IsNotExist(err) {
return &Manager{tools: map[string]agenttool.LoadedTool{}}, nil
}
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 { for _, entry := range entries {
if !entry.IsDir() { if !entry.IsDir() {
continue continue
@@ -57,6 +61,34 @@ func Load(root string, options agenttool.LoadOptions) (*Manager, error) {
manager.tools[toolName] = tool manager.tools[toolName] = tool
manager.order = append(manager.order, toolName) 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 return manager, nil
} }
+29 -2
View File
@@ -18,7 +18,8 @@ import (
const maxAgentToolIterations = 6 const maxAgentToolIterations = 6
// RunAgentToolLoop runs the agent tool calling loop // 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) finalMessages, err := buildArkMessages(chatMessages)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -29,6 +30,16 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, c
} }
tools := availableAgentTools(state, routerProfile, manager, emit) tools := availableAgentTools(state, routerProfile, manager, emit)
if len(tools) == 0 { 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 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}}) 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 { 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 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{ systemMessage := &model.ChatCompletionMessage{
Role: "system", Role: "system",
Content: &model.ChatCompletionMessageContent{ Content: &model.ChatCompletionMessageContent{