Compare commits
4 Commits
1cfebd37be
...
9d04b0b200
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d04b0b200 | |||
| 85a3d1e27d | |||
| 62813cbe2e | |||
| 53488f792d |
2
go.mod
2
go.mod
@@ -1,3 +1,5 @@
|
||||
module ai-agent-scaffold-go
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
3
go.sum
Normal file
3
go.sum
Normal file
@@ -0,0 +1,3 @@
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Application 应用配置顶层结构
|
||||
type Application struct {
|
||||
App AppSection `yaml:"app"`
|
||||
Server ServerSection `yaml:"server"`
|
||||
Agent AgentSection `yaml:"agent"`
|
||||
LLM LLMSection `yaml:"llm"`
|
||||
}
|
||||
|
||||
// AppSection 应用基础信息
|
||||
type AppSection struct {
|
||||
Name string `yaml:"name"`
|
||||
Env string `yaml:"env"`
|
||||
}
|
||||
|
||||
// ServerSection 服务器配置
|
||||
type ServerSection struct {
|
||||
Addr string `yaml:"addr"`
|
||||
}
|
||||
|
||||
// AgentSection Agent 配置路径列表
|
||||
type AgentSection struct {
|
||||
ConfigPaths []string `yaml:"config-paths"`
|
||||
}
|
||||
|
||||
// LLMSection LLM 相关配置
|
||||
type LLMSection struct {
|
||||
RequestTimeout string `yaml:"request-timeout"`
|
||||
}
|
||||
|
||||
const defaultLLMRequestTimeout = 5 * time.Minute
|
||||
|
||||
// RequestTimeoutDuration 解析 LLM 请求超时时间
|
||||
func (s LLMSection) RequestTimeoutDuration() (time.Duration, error) {
|
||||
if s.RequestTimeout == "" {
|
||||
return defaultLLMRequestTimeout, nil
|
||||
}
|
||||
d, err := time.ParseDuration(s.RequestTimeout)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid llm.request-timeout %q: %w", s.RequestTimeout, err)
|
||||
}
|
||||
if d <= 0 {
|
||||
return 0, fmt.Errorf("llm.request-timeout must be positive, got %q", s.RequestTimeout)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// LoadApplication 从指定路径加载应用配置
|
||||
func LoadApplication(path string) (Application, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Application{}, fmt.Errorf("read application config %s: %w", path, err)
|
||||
}
|
||||
var app Application
|
||||
if err := yaml.Unmarshal(data, &app); err != nil {
|
||||
return Application{}, fmt.Errorf("parse application config: %w", err)
|
||||
}
|
||||
// 设置默认值
|
||||
if app.Server.Addr == "" {
|
||||
app.Server.Addr = ":8091"
|
||||
}
|
||||
if app.App.Env == "" {
|
||||
app.App.Env = "local"
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
@@ -1 +1,122 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"ai-agent-scaffold-go/internal/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// agentRoot 对应 YAML 的 ai.agent.config.tables 结构
|
||||
type agentRoot struct {
|
||||
AI struct {
|
||||
Agent struct {
|
||||
Config struct {
|
||||
Tables map[string]model.AiAgentConfigTable `yaml:"tables"`
|
||||
} `yaml:"config"`
|
||||
} `yaml:"agent"`
|
||||
} `yaml:"ai"`
|
||||
}
|
||||
|
||||
// LoadAgentTables 从字节数据加载 Agent 配置表
|
||||
func LoadAgentTables(data []byte) (map[string]model.AiAgentConfigTable, error) {
|
||||
expanded := expandEnvPlaceholders(string(data))
|
||||
var root agentRoot
|
||||
if err := yaml.Unmarshal([]byte(expanded), &root); err != nil {
|
||||
return nil, fmt.Errorf("parse agent config: %w", err)
|
||||
}
|
||||
tables := root.AI.Agent.Config.Tables
|
||||
if len(tables) == 0 {
|
||||
return nil, fmt.Errorf("agent config tables are required")
|
||||
}
|
||||
for name, table := range tables {
|
||||
normalizeDefaults(&table)
|
||||
if err := validateTable(name, table); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables[name] = table
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// LoadAgentTablesFile 从文件路径加载 Agent 配置表
|
||||
func LoadAgentTablesFile(path string) (map[string]model.AiAgentConfigTable, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read agent config %s: %w", path, err)
|
||||
}
|
||||
return LoadAgentTables(data)
|
||||
}
|
||||
|
||||
// envPlaceholderRE 匹配 ${VAR} 和 ${VAR:-default} 格式的环境变量占位符
|
||||
var envPlaceholderRE = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}`)
|
||||
|
||||
// expandEnvPlaceholders 替换字符串中的环境变量占位符
|
||||
func expandEnvPlaceholders(input string) string {
|
||||
return envPlaceholderRE.ReplaceAllStringFunc(input, func(match string) string {
|
||||
groups := envPlaceholderRE.FindStringSubmatch(match)
|
||||
name := groups[1]
|
||||
if value, ok := os.LookupEnv(name); ok && value != "" {
|
||||
return value
|
||||
}
|
||||
if len(groups) > 2 {
|
||||
return groups[2]
|
||||
}
|
||||
return ""
|
||||
})
|
||||
}
|
||||
|
||||
// normalizeDefaults 填充配置默认值
|
||||
func normalizeDefaults(table *model.AiAgentConfigTable) {
|
||||
if table.Module.AiAPI.CompletionsPath == "" {
|
||||
table.Module.AiAPI.CompletionsPath = "v1/chat/completions"
|
||||
}
|
||||
for i := range table.Module.AgentWorkflows {
|
||||
if table.Module.AgentWorkflows[i].MaxIterations == 0 {
|
||||
table.Module.AgentWorkflows[i].MaxIterations = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateTable 校验配置表的必填字段
|
||||
func validateTable(name string, table model.AiAgentConfigTable) error {
|
||||
prefix := "agent table " + name
|
||||
required := map[string]string{
|
||||
"app-name": table.AppName,
|
||||
"agent.agent-id": table.Agent.AgentID,
|
||||
"module.ai-api.base-url": table.Module.AiAPI.BaseURL,
|
||||
"module.ai-api.api-key": table.Module.AiAPI.APIKey,
|
||||
"module.chat-model.model": table.Module.ChatModel.Model,
|
||||
"module.runner.agent-name": table.Module.Runner.AgentName,
|
||||
}
|
||||
for field, value := range required {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fmt.Errorf("%s: %s is required", prefix, field)
|
||||
}
|
||||
}
|
||||
if len(table.Module.Agents) == 0 {
|
||||
return fmt.Errorf("%s: module.agents is required", prefix)
|
||||
}
|
||||
for i, agent := range table.Module.Agents {
|
||||
if strings.TrimSpace(agent.Name) == "" {
|
||||
return fmt.Errorf("%s: module.agents[%d].name is required", prefix, i)
|
||||
}
|
||||
if strings.TrimSpace(agent.Instruction) == "" {
|
||||
return fmt.Errorf("%s: module.agents[%d].instruction is required", prefix, i)
|
||||
}
|
||||
}
|
||||
for i, workflow := range table.Module.AgentWorkflows {
|
||||
switch workflow.Type {
|
||||
case model.WorkflowTypeLoop, model.WorkflowTypeParallel, model.WorkflowTypeSequential:
|
||||
default:
|
||||
return fmt.Errorf("%s: module.agent-workflows[%d].type is invalid: %s", prefix, i, workflow.Type)
|
||||
}
|
||||
if strings.TrimSpace(workflow.Name) == "" {
|
||||
return fmt.Errorf("%s: module.agent-workflows[%d].name is required", prefix, i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1 +1,76 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"ai-agent-scaffold-go/internal/model"
|
||||
)
|
||||
|
||||
// ChatModelAdapter 适配器,将 OpenAIClient 包装为 model.ChatModel
|
||||
type ChatModelAdapter struct {
|
||||
client *OpenAIClient
|
||||
tools []model.Tool
|
||||
}
|
||||
|
||||
// NewChatModelAdapter 创建 ChatModel 适配器
|
||||
func NewChatModelAdapter(client *OpenAIClient, tools []model.Tool) *ChatModelAdapter {
|
||||
return &ChatModelAdapter{client: client, tools: tools}
|
||||
}
|
||||
|
||||
// Generate 实现 model.ChatModel 接口
|
||||
func (m *ChatModelAdapter) Generate(ctx context.Context, messages []model.ChatMessage) (model.ChatReply, error) {
|
||||
return m.client.Generate(ctx, messages, m.toolDefs())
|
||||
}
|
||||
|
||||
// Stream 实现 model.ChatModel 接口
|
||||
func (m *ChatModelAdapter) Stream(ctx context.Context, messages []model.ChatMessage) (<-chan model.ChatStreamEvent, <-chan error) {
|
||||
return m.client.Stream(ctx, messages, m.toolDefs())
|
||||
}
|
||||
|
||||
// Tools 返回注册的工具列表
|
||||
func (m *ChatModelAdapter) Tools() []model.Tool {
|
||||
return m.tools
|
||||
}
|
||||
|
||||
// CallTool 根据名称和参数调用对应的工具
|
||||
func (m *ChatModelAdapter) CallTool(ctx context.Context, name, arguments string) (string, error) {
|
||||
query := extractQuery(arguments)
|
||||
for _, t := range m.tools {
|
||||
if t.Name() == name {
|
||||
return t.Call(ctx, query)
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("tool %q not found", name)
|
||||
}
|
||||
|
||||
// toolDefs 将 model.Tool 转换为 ToolDef 列表
|
||||
func (m *ChatModelAdapter) toolDefs() []ToolDef {
|
||||
defs := make([]ToolDef, 0, len(m.tools))
|
||||
for _, t := range m.tools {
|
||||
defs = append(defs, ToolDef{Name: t.Name(), Description: t.Description()})
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
// extractQuery 从工具调用参数 JSON 中提取 query 字段
|
||||
func extractQuery(arguments string) string {
|
||||
arguments = strings.TrimSpace(arguments)
|
||||
if arguments == "" {
|
||||
return ""
|
||||
}
|
||||
if idx := strings.Index(arguments, `"query"`); idx >= 0 {
|
||||
rest := arguments[idx+7:]
|
||||
if colon := strings.Index(rest, `:`); colon >= 0 {
|
||||
rest = strings.TrimSpace(rest[colon+1:])
|
||||
if strings.HasPrefix(rest, `"`) {
|
||||
rest = rest[1:]
|
||||
if end := strings.Index(rest, `"`); end >= 0 {
|
||||
return rest[:end]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return arguments
|
||||
}
|
||||
|
||||
@@ -1 +1,328 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ai-agent-scaffold-go/internal/model"
|
||||
)
|
||||
|
||||
// OpenAIClient OpenAI 兼容 API 客户端
|
||||
type OpenAIClient struct {
|
||||
httpClient *http.Client
|
||||
completionsURL string
|
||||
apiKey string
|
||||
model string
|
||||
}
|
||||
|
||||
// ToolDef 工具定义,用于传给 LLM 的 tools 参数
|
||||
type ToolDef struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// NewOpenAIClient 创建 OpenAI 客户端
|
||||
func NewOpenAIClient(completionsURL, apiKey, model string, requestTimeout time.Duration) *OpenAIClient {
|
||||
if requestTimeout <= 0 {
|
||||
requestTimeout = 5 * time.Minute
|
||||
}
|
||||
return &OpenAIClient{
|
||||
httpClient: &http.Client{Timeout: requestTimeout},
|
||||
completionsURL: completionsURL,
|
||||
apiKey: apiKey,
|
||||
model: model,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 同步调用 LLM,返回完整回复
|
||||
func (c *OpenAIClient) Generate(ctx context.Context, messages []model.ChatMessage, tools []ToolDef) (model.ChatReply, error) {
|
||||
body, err := buildRequestBody(c.model, messages, tools, false)
|
||||
if err != nil {
|
||||
return model.ChatReply{}, err
|
||||
}
|
||||
resp, err := c.do(ctx, body)
|
||||
if err != nil {
|
||||
return model.ChatReply{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return model.ChatReply{}, fmt.Errorf("openai read body: %w", err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return model.ChatReply{}, fmt.Errorf("openai upstream %d: %s", resp.StatusCode, truncate(string(raw), 400))
|
||||
}
|
||||
|
||||
var parsed openaiCompletion
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return model.ChatReply{}, fmt.Errorf("openai decode: %w", err)
|
||||
}
|
||||
if len(parsed.Choices) == 0 {
|
||||
return model.ChatReply{}, fmt.Errorf("openai response has no choices")
|
||||
}
|
||||
choice := parsed.Choices[0].Message
|
||||
reply := model.ChatReply{Content: choice.Content}
|
||||
for _, tc := range choice.ToolCalls {
|
||||
reply.ToolCalls = append(reply.ToolCalls, model.ChatToolCall{
|
||||
ID: tc.ID,
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
})
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
// Stream 流式调用 LLM,返回事件通道和错误通道
|
||||
func (c *OpenAIClient) Stream(ctx context.Context, messages []model.ChatMessage, tools []ToolDef) (<-chan model.ChatStreamEvent, <-chan error) {
|
||||
events := make(chan model.ChatStreamEvent, 8)
|
||||
errs := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(events)
|
||||
defer close(errs)
|
||||
|
||||
body, err := buildRequestBody(c.model, messages, tools, true)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
resp, err := c.do(ctx, body)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
errs <- fmt.Errorf("openai upstream %d: %s", resp.StatusCode, truncate(string(raw), 400))
|
||||
return
|
||||
}
|
||||
|
||||
toolCallBuf := map[int]*model.ChatToolCall{}
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
emitToolCalls(events, toolCallBuf)
|
||||
events <- model.ChatStreamEvent{Done: true}
|
||||
return
|
||||
}
|
||||
errs <- fmt.Errorf("openai stream read: %w", err)
|
||||
return
|
||||
}
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if line == "" || !strings.HasPrefix(line, "data:") {
|
||||
continue
|
||||
}
|
||||
payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if payload == "[DONE]" {
|
||||
emitToolCalls(events, toolCallBuf)
|
||||
events <- model.ChatStreamEvent{Done: true}
|
||||
return
|
||||
}
|
||||
|
||||
var chunk openaiStreamChunk
|
||||
if err := json.Unmarshal([]byte(payload), &chunk); err != nil {
|
||||
errs <- fmt.Errorf("openai stream decode: %w", err)
|
||||
return
|
||||
}
|
||||
if len(chunk.Choices) == 0 {
|
||||
continue
|
||||
}
|
||||
delta := chunk.Choices[0].Delta
|
||||
|
||||
if delta.Content != "" {
|
||||
select {
|
||||
case events <- model.ChatStreamEvent{Delta: delta.Content}:
|
||||
case <-ctx.Done():
|
||||
errs <- ctx.Err()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range delta.ToolCalls {
|
||||
current, ok := toolCallBuf[tc.Index]
|
||||
if !ok {
|
||||
current = &model.ChatToolCall{}
|
||||
toolCallBuf[tc.Index] = current
|
||||
}
|
||||
if tc.ID != "" {
|
||||
current.ID = tc.ID
|
||||
}
|
||||
if tc.Function.Name != "" {
|
||||
current.Name = tc.Function.Name
|
||||
}
|
||||
if tc.Function.Arguments != "" {
|
||||
current.Arguments += tc.Function.Arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return events, errs
|
||||
}
|
||||
|
||||
// emitToolCalls 将缓冲区中的工具调用合并发送
|
||||
func emitToolCalls(events chan<- model.ChatStreamEvent, buf map[int]*model.ChatToolCall) {
|
||||
if len(buf) == 0 {
|
||||
return
|
||||
}
|
||||
calls := make([]model.ChatToolCall, 0, len(buf))
|
||||
for i := 0; i < len(buf); i++ {
|
||||
if call, ok := buf[i]; ok && call != nil {
|
||||
calls = append(calls, *call)
|
||||
}
|
||||
}
|
||||
if len(calls) > 0 {
|
||||
events <- model.ChatStreamEvent{ToolCalls: calls}
|
||||
}
|
||||
}
|
||||
|
||||
// do 发送 HTTP 请求
|
||||
func (c *OpenAIClient) do(ctx context.Context, body []byte) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.completionsURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openai build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if c.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
}
|
||||
return c.httpClient.Do(req)
|
||||
}
|
||||
|
||||
// buildRequestBody 构建请求体 JSON
|
||||
func buildRequestBody(modelName string, messages []model.ChatMessage, tools []ToolDef, stream bool) ([]byte, error) {
|
||||
payload := map[string]any{
|
||||
"model": modelName,
|
||||
"messages": encodeMessages(messages),
|
||||
"stream": stream,
|
||||
}
|
||||
if len(tools) > 0 {
|
||||
payload["tools"] = encodeTools(tools)
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
// encodeMessages 将 ChatMessage 转换为 OpenAI API 格式
|
||||
func encodeMessages(messages []model.ChatMessage) []map[string]any {
|
||||
encoded := make([]map[string]any, 0, len(messages))
|
||||
for _, m := range messages {
|
||||
entry := map[string]any{"role": string(m.Role)}
|
||||
if m.Content != "" {
|
||||
entry["content"] = m.Content
|
||||
} else if m.Role != model.ChatRoleAssistant || len(m.ToolCalls) == 0 {
|
||||
entry["content"] = ""
|
||||
}
|
||||
if m.Name != "" {
|
||||
entry["name"] = m.Name
|
||||
}
|
||||
if m.ToolCallID != "" {
|
||||
entry["tool_call_id"] = m.ToolCallID
|
||||
}
|
||||
if len(m.ToolCalls) > 0 {
|
||||
calls := make([]map[string]any, 0, len(m.ToolCalls))
|
||||
for _, tc := range m.ToolCalls {
|
||||
calls = append(calls, map[string]any{
|
||||
"id": tc.ID,
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": tc.Name,
|
||||
"arguments": tc.Arguments,
|
||||
},
|
||||
})
|
||||
}
|
||||
entry["tool_calls"] = calls
|
||||
}
|
||||
encoded = append(encoded, entry)
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
||||
// encodeTools 将工具定义转换为 OpenAI API 格式
|
||||
func encodeTools(tools []ToolDef) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
desc := t.Description
|
||||
if desc == "" {
|
||||
desc = "external tool " + t.Name
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": t.Name,
|
||||
"description": desc,
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"query": map[string]any{
|
||||
"type": "string",
|
||||
"description": "text input for the tool",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// truncate 截断字符串
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + "..."
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OpenAI API 响应结构体
|
||||
// ============================================================
|
||||
|
||||
type openaiCompletion struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []openaiToolCallV1 `json:"tool_calls"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
type openaiStreamChunk struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []openaiStreamToolCall `json:"tool_calls"`
|
||||
} `json:"delta"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
type openaiToolCallV1 struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
type openaiStreamToolCall struct {
|
||||
Index int `json:"index"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
50
internal/model/chat.go
Normal file
50
internal/model/chat.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package model
|
||||
|
||||
// ChatRole 消息角色
|
||||
type ChatRole string
|
||||
|
||||
const (
|
||||
ChatRoleSystem ChatRole = "system"
|
||||
ChatRoleUser ChatRole = "user"
|
||||
ChatRoleAssistant ChatRole = "assistant"
|
||||
ChatRoleTool ChatRole = "tool"
|
||||
)
|
||||
|
||||
// ChatMessage 聊天消息
|
||||
type ChatMessage struct {
|
||||
Role ChatRole
|
||||
Content string
|
||||
ToolCallID string
|
||||
Name string
|
||||
ToolCalls []ChatToolCall
|
||||
}
|
||||
|
||||
// ChatToolCall 工具调用请求
|
||||
type ChatToolCall struct {
|
||||
ID string
|
||||
Name string
|
||||
Arguments string
|
||||
}
|
||||
|
||||
// ChatReply 聊天回复
|
||||
type ChatReply struct {
|
||||
Content string
|
||||
ToolCalls []ChatToolCall
|
||||
}
|
||||
|
||||
// ChatStreamEvent 流式事件
|
||||
type ChatStreamEvent struct {
|
||||
Delta string
|
||||
ToolCalls []ChatToolCall
|
||||
Done bool
|
||||
}
|
||||
|
||||
// ChatContent 聊天输入内容
|
||||
type ChatContent struct {
|
||||
Texts []TextPart
|
||||
}
|
||||
|
||||
// TextPart 文本片段
|
||||
type TextPart struct {
|
||||
Message string
|
||||
}
|
||||
@@ -1 +1,67 @@
|
||||
package model
|
||||
|
||||
// WorkflowType 工作流类型
|
||||
type WorkflowType string
|
||||
|
||||
const (
|
||||
WorkflowTypeLoop WorkflowType = "loop"
|
||||
WorkflowTypeParallel WorkflowType = "parallel"
|
||||
WorkflowTypeSequential WorkflowType = "sequential"
|
||||
)
|
||||
|
||||
// AiAgentConfigTable 一个 Agent 配置表的顶层结构,对应 YAML 中 tables 下的每一项
|
||||
type AiAgentConfigTable struct {
|
||||
AppName string `yaml:"app-name" json:"appName"`
|
||||
Agent AgentSummary `yaml:"agent" json:"agent"`
|
||||
Module AgentModule `yaml:"module" json:"module"`
|
||||
}
|
||||
|
||||
// AgentSummary Agent 摘要信息
|
||||
type AgentSummary struct {
|
||||
AgentID string `yaml:"agent-id" json:"agentId"`
|
||||
AgentName string `yaml:"agent-name" json:"agentName"`
|
||||
AgentDesc string `yaml:"agent-desc" json:"agentDesc"`
|
||||
}
|
||||
|
||||
// AgentModule Agent 模块配置,包含 API、模型、Agent 定义、工作流和 Runner
|
||||
type AgentModule struct {
|
||||
AiAPI AiAPIConfig `yaml:"ai-api" json:"aiApi"`
|
||||
ChatModel ChatModelConfig `yaml:"chat-model" json:"chatModel"`
|
||||
Agents []AgentConfig `yaml:"agents" json:"agents"`
|
||||
AgentWorkflows []AgentWorkflowConfig `yaml:"agent-workflows" json:"agentWorkflows"`
|
||||
Runner RunnerConfig `yaml:"runner" json:"runner"`
|
||||
}
|
||||
|
||||
// AiAPIConfig LLM API 连接配置
|
||||
type AiAPIConfig struct {
|
||||
BaseURL string `yaml:"base-url" json:"baseUrl"`
|
||||
APIKey string `yaml:"api-key" json:"apiKey"`
|
||||
CompletionsPath string `yaml:"completions-path" json:"completionsPath"`
|
||||
}
|
||||
|
||||
// ChatModelConfig 聊天模型配置
|
||||
type ChatModelConfig struct {
|
||||
Model string `yaml:"model" json:"model"`
|
||||
}
|
||||
|
||||
// AgentConfig 单个 Agent 的定义
|
||||
type AgentConfig struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Instruction string `yaml:"instruction" json:"instruction"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
OutputKey string `yaml:"output-key" json:"outputKey"`
|
||||
}
|
||||
|
||||
// AgentWorkflowConfig 工作流配置,支持 loop/parallel/sequential 三种类型
|
||||
type AgentWorkflowConfig struct {
|
||||
Type WorkflowType `yaml:"type" json:"type"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
SubAgents []string `yaml:"sub-agents" json:"subAgents"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
MaxIterations int `yaml:"max-iterations" json:"maxIterations"`
|
||||
}
|
||||
|
||||
// RunnerConfig Runner 配置,指定入口 Agent 名称
|
||||
type RunnerConfig struct {
|
||||
AgentName string `yaml:"agent-name" json:"agentName"`
|
||||
}
|
||||
|
||||
61
internal/model/store.go
Normal file
61
internal/model/store.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
import "sync"
|
||||
|
||||
// InMemoryAgentRegistry 基于内存的 Agent 注册表
|
||||
type InMemoryAgentRegistry struct {
|
||||
mu sync.RWMutex
|
||||
agents map[string]RegisteredAgent
|
||||
}
|
||||
|
||||
func NewInMemoryAgentRegistry() *InMemoryAgentRegistry {
|
||||
return &InMemoryAgentRegistry{agents: make(map[string]RegisteredAgent)}
|
||||
}
|
||||
|
||||
func (r *InMemoryAgentRegistry) Register(agent RegisteredAgent) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.agents[agent.AgentID] = agent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryAgentRegistry) Get(agentID string) (RegisteredAgent, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
agent, ok := r.agents[agentID]
|
||||
return agent, ok
|
||||
}
|
||||
|
||||
func (r *InMemoryAgentRegistry) List() []RegisteredAgent {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
agents := make([]RegisteredAgent, 0, len(r.agents))
|
||||
for _, agent := range r.agents {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
// InMemorySessionStore 基于内存的会话存储
|
||||
type InMemorySessionStore struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]string
|
||||
}
|
||||
|
||||
func NewInMemorySessionStore() *InMemorySessionStore {
|
||||
return &InMemorySessionStore{sessions: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (s *InMemorySessionStore) Get(userID, agentID string) (string, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
sessionID, ok := s.sessions[userID+":"+agentID]
|
||||
return sessionID, ok
|
||||
}
|
||||
|
||||
func (s *InMemorySessionStore) Set(userID, agentID, sessionID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.sessions[userID+":"+agentID] = sessionID
|
||||
return nil
|
||||
}
|
||||
@@ -1 +1,54 @@
|
||||
package model
|
||||
|
||||
import "context"
|
||||
|
||||
// 核心接口
|
||||
// Tool 外部工具接口
|
||||
type Tool interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Call(ctx context.Context, input string) (string, error)
|
||||
}
|
||||
|
||||
// ChatModel 聊天模型接口,支持同步生成和流式输出
|
||||
type ChatModel interface {
|
||||
Generate(ctx context.Context, messages []ChatMessage) (ChatReply, error)
|
||||
Stream(ctx context.Context, messages []ChatMessage) (<-chan ChatStreamEvent, <-chan error)
|
||||
}
|
||||
|
||||
// Agent 智能体接口
|
||||
type Agent interface {
|
||||
Name() string
|
||||
Run(ctx context.Context, content ChatContent) (string, error)
|
||||
Stream(ctx context.Context, content ChatContent, out chan<- string) error
|
||||
}
|
||||
|
||||
// Runner 运行器接口,管理会话并执行 Agent
|
||||
type Runner interface {
|
||||
CreateSession(userID string) (string, error)
|
||||
Run(userID, sessionID string, content ChatContent) ([]string, error)
|
||||
Stream(userID, sessionID string, content ChatContent) (<-chan string, <-chan error)
|
||||
}
|
||||
|
||||
// 注册与存储接口
|
||||
// RegisteredAgent 已注册的 Agent 信息
|
||||
type RegisteredAgent struct {
|
||||
AppName string
|
||||
AgentID string
|
||||
AgentName string
|
||||
AgentDesc string
|
||||
Runner Runner
|
||||
}
|
||||
|
||||
// AgentRegistry Agent 注册表接口
|
||||
type AgentRegistry interface {
|
||||
Register(agent RegisteredAgent) error
|
||||
Get(agentID string) (RegisteredAgent, bool)
|
||||
List() []RegisteredAgent
|
||||
}
|
||||
|
||||
// SessionStore 会话存储接口
|
||||
type SessionStore interface {
|
||||
Get(userID, agentID string) (string, bool)
|
||||
Set(userID, agentID, sessionID string) error
|
||||
}
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
CodeSuccess = "0000"
|
||||
InfoSuccess = "success"
|
||||
CodeUnknownError = "0001"
|
||||
InfoUnknownError = "unknown error"
|
||||
CodeIllegalParameter = "0002"
|
||||
InfoIllegalParameter = "illegal parameter"
|
||||
CodeAgentNotFound = "0003"
|
||||
InfoAgentNotFound = "agent not found"
|
||||
)
|
||||
|
||||
@@ -1 +1,17 @@
|
||||
package types
|
||||
|
||||
type AppError struct {
|
||||
Code string
|
||||
Info string
|
||||
}
|
||||
|
||||
func NewAppError(code, info string) *AppError {
|
||||
return &AppError{Code: code, Info: info}
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Code + ": " + e.Info
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user