写成屎山了
This commit is contained in:
+8
-7
@@ -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
@@ -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})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user