Initial commit: CarrotAssistant backend (Go + Gin + SQLite, OpenAI-compatible agent runtime)
This commit is contained in:
122
internal/server/agentkit.go
Normal file
122
internal/server/agentkit.go
Normal file
@@ -0,0 +1,122 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user