package server import ( "errors" "fmt" "gorm.io/gorm" "code.littlelan.cn/CarrotAssistant/backend/internal/agent" "code.littlelan.cn/CarrotAssistant/backend/internal/llm" "code.littlelan.cn/CarrotAssistant/backend/internal/skill" "code.littlelan.cn/CarrotAssistant/backend/internal/store" ) // resolveLLMConfig builds the llm.Config for an App: looks up the App's // ModelConfig (or errors), decrypts the API key, and returns a ready client. func (d Deps) resolveLLMConfig(app *store.App) (llm.Config, error) { if app.ModelConfigID == nil { return llm.Config{}, errors.New("应用未配置模型") } var mc store.ModelConfig if err := d.DB.First(&mc, "id = ?", *app.ModelConfigID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return llm.Config{}, errors.New("应用的模型配置不存在") } return llm.Config{}, err } apiKey, err := d.Crypto.Decrypt(mc.APIKeyEncrypted) if err != nil { return llm.Config{}, fmt.Errorf("解密 api key: %w", err) } return llm.Config{ BaseURL: mc.BaseURL, APIKey: apiKey, Model: mc.DefaultModel, MaxTokens: mc.MaxTokens, }, nil } // loadPublishedSkill returns the currently-published skill for an app plus its // parsed content. Returns an error if none is published (the agent cannot run // without one). func (d Deps) loadPublishedSkill(app *store.App) (*store.Skill, *skill.Skill, error) { var row store.Skill err := d.DB.Where("app_id = ? AND status = ?", app.ID, "published"). Order("id DESC").First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, errors.New("应用尚未发布任何 skill") } return nil, nil, err } content, err := skill.NewManager(d.SkillsDir).Load(&row) if err != nil { return nil, nil, fmt.Errorf("读取 skill 文件: %w", err) } return &row, content, nil } // sessionHistory loads a session's messages and converts them to llm.Message // form, prefixed with the skill's system prompt. The returned slice is ready // to hand to agent.NewRunner. func (d Deps) sessionHistory(sessionID string, sk *skill.Skill) ([]llm.Message, error) { var msgs []store.Message if err := d.DB.Where("session_id = ?", sessionID).Order("id ASC").Find(&msgs).Error; err != nil { return nil, err } out := make([]llm.Message, 0, len(msgs)+1) // System prompt first; never replay it from storage. out = append(out, llm.Message{Role: llm.RoleSystem, Content: sk.SystemPrompt}) for _, m := range msgs { out = append(out, toLLMMessage(m)) } return out, nil } // toLLMMessage converts a stored message back into llm.Message, rehydrating // tool calls from the JSON column. func toLLMMessage(m store.Message) llm.Message { msg := llm.Message{ Role: llm.Role(m.Role), Content: m.Content, ToolCallID: m.ToolCallID, } if len(m.ToolCalls) > 0 { var calls []llm.ToolCall // Best-effort decode; a corrupt column should never break the chat. _ = jsonUnmarshal(m.ToolCalls, &calls) msg.ToolCalls = calls } return msg } // persistMessage stores one message row. Used after each assistant turn / // tool result so the conversation survives across requests. func (d Deps) persistMessage(sessionID string, m llm.Message) error { row := store.Message{ SessionID: sessionID, Role: string(m.Role), Content: m.Content, ToolCallID: m.ToolCallID, } if len(m.ToolCalls) > 0 { // Store the calls array directly; toLLMMessage decodes []ToolCall. row.ToolCalls = jsonRaw(m.ToolCalls) } if err := d.DB.Create(&row).Error; err != nil { return err } return d.DB.Model(&store.Session{}).Where("id = ?", sessionID). UpdateColumn("message_count", gorm.Expr("message_count + 1")).Error } // newRunner assembles an agent.Runner for an app + session. func (d Deps) newRunner(app *store.App, sk *skill.Skill, history []llm.Message, onEvent func(agent.Event)) (*agent.Runner, error) { cfg, err := d.resolveLLMConfig(app) if err != nil { return nil, err } client := llm.New(cfg) return agent.NewRunner(client, sk, history, onEvent), nil }