写成屎山了

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
+8 -7
View File
@@ -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{
+68 -26
View File
@@ -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})
+28
View File
@@ -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
}
+12
View File
@@ -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 {
+4 -4
View File
@@ -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)
+37 -5
View File
@@ -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
}
+29 -2
View File
@@ -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{