123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
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
|
|
}
|