feat(project): 项目完结
This commit is contained in:
47
internal/domain/agent/model/command.go
Normal file
47
internal/domain/agent/model/command.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package model
|
||||
|
||||
type ArmoryCommand struct {
|
||||
Table AiAgentConfigTable
|
||||
}
|
||||
|
||||
type ChatCommand struct {
|
||||
AgentID string
|
||||
UserID string
|
||||
SessionID string
|
||||
Message string
|
||||
Content ChatContent
|
||||
}
|
||||
|
||||
type ChatContent struct {
|
||||
Texts []TextPart
|
||||
Files []FilePart
|
||||
InlineDatas []InlineDataPart
|
||||
}
|
||||
|
||||
type TextPart struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type FilePart struct {
|
||||
FileURI string
|
||||
MimeType string
|
||||
}
|
||||
|
||||
type InlineDataPart struct {
|
||||
Bytes []byte
|
||||
MimeType string
|
||||
}
|
||||
|
||||
type RegisteredAgent struct {
|
||||
AppName string
|
||||
AgentID string
|
||||
AgentName string
|
||||
AgentDesc string
|
||||
Runner Runner
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
96
internal/domain/agent/model/config.go
Normal file
96
internal/domain/agent/model/config.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package model
|
||||
|
||||
type WorkflowType string
|
||||
|
||||
const (
|
||||
WorkflowTypeLoop WorkflowType = "loop"
|
||||
WorkflowTypeParallel WorkflowType = "parallel"
|
||||
WorkflowTypeSequential WorkflowType = "sequential"
|
||||
)
|
||||
|
||||
type AiAgentConfigTable struct {
|
||||
AppName string `yaml:"app-name" json:"appName"`
|
||||
Agent AgentSummary `yaml:"agent" json:"agent"`
|
||||
Module AgentModule `yaml:"module" json:"module"`
|
||||
}
|
||||
|
||||
type AgentSummary struct {
|
||||
AgentID string `yaml:"agent-id" json:"agentId"`
|
||||
AgentName string `yaml:"agent-name" json:"agentName"`
|
||||
AgentDesc string `yaml:"agent-desc" json:"agentDesc"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type AiAPIConfig struct {
|
||||
BaseURL string `yaml:"base-url" json:"baseUrl"`
|
||||
APIKey string `yaml:"api-key" json:"apiKey"`
|
||||
CompletionsPath string `yaml:"completions-path" json:"completionsPath"`
|
||||
EmbeddingsPath string `yaml:"embeddings-path" json:"embeddingsPath"`
|
||||
}
|
||||
|
||||
type ChatModelConfig struct {
|
||||
Model string `yaml:"model" json:"model"`
|
||||
ToolMCPList []ToolMCPConfig `yaml:"tool-mcp-list" json:"toolMcpList"`
|
||||
ToolSkillsList []ToolSkillsConfig `yaml:"tool-skills-list" json:"toolSkillsList"`
|
||||
}
|
||||
|
||||
type ToolMCPConfig struct {
|
||||
SSE *SSEServerParameters `yaml:"sse,omitempty" json:"sse,omitempty"`
|
||||
Stdio *StdioServerParameters `yaml:"stdio,omitempty" json:"stdio,omitempty"`
|
||||
Local *LocalToolParameters `yaml:"local,omitempty" json:"local,omitempty"`
|
||||
}
|
||||
|
||||
type SSEServerParameters struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
BaseURI string `yaml:"base-uri" json:"baseUri"`
|
||||
SSEEndpoint string `yaml:"sse-endpoint" json:"sseEndpoint"`
|
||||
RequestTimeout int `yaml:"request-timeout" json:"requestTimeout"`
|
||||
}
|
||||
|
||||
type StdioServerParameters struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
RequestTimeout int `yaml:"request-timeout" json:"requestTimeout"`
|
||||
ServerParameters ServerParameters `yaml:"server-parameters" json:"serverParameters"`
|
||||
}
|
||||
|
||||
type ServerParameters struct {
|
||||
Command string `yaml:"command" json:"command"`
|
||||
Args []string `yaml:"args" json:"args"`
|
||||
Env map[string]string `yaml:"env" json:"env"`
|
||||
}
|
||||
|
||||
type LocalToolParameters struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
}
|
||||
|
||||
type ToolSkillsConfig struct {
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Path string `yaml:"path" json:"path"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type RunnerConfig struct {
|
||||
AgentName string `yaml:"agent-name" json:"agentName"`
|
||||
PluginNameList []string `yaml:"plugin-name-list" json:"pluginNameList"`
|
||||
}
|
||||
167
internal/domain/agent/ports/ports.go
Normal file
167
internal/domain/agent/ports/ports.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
)
|
||||
|
||||
type ModelProvider interface {
|
||||
NewAPI(ctx context.Context, config model.AiAPIConfig) (ModelAPI, error)
|
||||
NewChatModel(ctx context.Context, api ModelAPI, config model.ChatModelConfig, tools []Tool) (ChatModel, error)
|
||||
}
|
||||
|
||||
type ModelAPI interface{}
|
||||
|
||||
type ChatRole string
|
||||
|
||||
const (
|
||||
ChatRoleSystem ChatRole = "system"
|
||||
ChatRoleUser ChatRole = "user"
|
||||
ChatRoleAssistant ChatRole = "assistant"
|
||||
ChatRoleTool ChatRole = "tool"
|
||||
)
|
||||
|
||||
type ChatMessage struct {
|
||||
Role ChatRole
|
||||
Content string
|
||||
ToolCallID string
|
||||
Name string
|
||||
ToolCalls []ChatToolCall
|
||||
}
|
||||
|
||||
type ChatToolCall struct {
|
||||
ID string
|
||||
Name string
|
||||
Arguments string
|
||||
}
|
||||
|
||||
type ChatReply struct {
|
||||
Content string
|
||||
ToolCalls []ChatToolCall
|
||||
}
|
||||
|
||||
type ChatStreamEvent struct {
|
||||
Delta string
|
||||
ToolCalls []ChatToolCall
|
||||
Done bool
|
||||
}
|
||||
|
||||
type ChatModel interface {
|
||||
Generate(ctx context.Context, messages []ChatMessage) (ChatReply, error)
|
||||
Stream(ctx context.Context, messages []ChatMessage) (<-chan ChatStreamEvent, <-chan error)
|
||||
Tools() []Tool
|
||||
}
|
||||
|
||||
type Tool interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type ToolDescriptor interface {
|
||||
Description() string
|
||||
}
|
||||
|
||||
type MCPToolFactory interface {
|
||||
BuildTools(ctx context.Context, config model.ToolMCPConfig) ([]Tool, error)
|
||||
}
|
||||
|
||||
type SkillFactory interface {
|
||||
BuildTools(ctx context.Context, config model.ToolSkillsConfig) ([]Tool, error)
|
||||
}
|
||||
|
||||
type ToolRouter interface {
|
||||
CallTool(ctx context.Context, name, arguments string) (string, error)
|
||||
}
|
||||
|
||||
type AgentFactory interface {
|
||||
NewLLMAgent(ctx context.Context, config model.AgentConfig, chatModel ChatModel) (Agent, error)
|
||||
NewLoopAgent(ctx context.Context, config model.AgentWorkflowConfig, subAgents []Agent) (Agent, error)
|
||||
NewParallelAgent(ctx context.Context, config model.AgentWorkflowConfig, subAgents []Agent) (Agent, error)
|
||||
NewSequentialAgent(ctx context.Context, config model.AgentWorkflowConfig, subAgents []Agent) (Agent, error)
|
||||
}
|
||||
|
||||
type Agent interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type RunnerPlugin interface {
|
||||
Name() string
|
||||
OnUserMessage(ctx context.Context, appName, userID, sessionID string, agent Agent, content model.ChatContent) error
|
||||
BeforeAgent(ctx context.Context, appName, userID, sessionID string, agent Agent) error
|
||||
}
|
||||
|
||||
type RunnerFactory interface {
|
||||
NewRunner(ctx context.Context, appName string, agent Agent, pluginNames []string) (model.Runner, error)
|
||||
}
|
||||
|
||||
type AgentRegistry interface {
|
||||
Register(agent model.RegisteredAgent) error
|
||||
Get(agentID string) (model.RegisteredAgent, bool)
|
||||
List() []model.RegisteredAgent
|
||||
}
|
||||
|
||||
type SessionStore interface {
|
||||
Get(userID, agentID string) (string, bool)
|
||||
Set(userID, agentID, sessionID string) error
|
||||
}
|
||||
|
||||
type InMemoryAgentRegistry struct {
|
||||
mu sync.RWMutex
|
||||
agents map[string]model.RegisteredAgent
|
||||
}
|
||||
|
||||
func NewInMemoryAgentRegistry() *InMemoryAgentRegistry {
|
||||
return &InMemoryAgentRegistry{agents: make(map[string]model.RegisteredAgent)}
|
||||
}
|
||||
|
||||
func (r *InMemoryAgentRegistry) Register(agent model.RegisteredAgent) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.agents[agent.AgentID] = agent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryAgentRegistry) Get(agentID string) (model.RegisteredAgent, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
agent, ok := r.agents[agentID]
|
||||
return agent, ok
|
||||
}
|
||||
|
||||
func (r *InMemoryAgentRegistry) List() []model.RegisteredAgent {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
agents := make([]model.RegisteredAgent, 0, len(r.agents))
|
||||
for _, agent := range r.agents {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
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[sessionKey(userID, agentID)]
|
||||
return sessionID, ok
|
||||
}
|
||||
|
||||
func (s *InMemorySessionStore) Set(userID, agentID, sessionID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.sessions[sessionKey(userID, agentID)] = sessionID
|
||||
return nil
|
||||
}
|
||||
|
||||
func sessionKey(userID, agentID string) string {
|
||||
return userID + ":" + agentID
|
||||
}
|
||||
29
internal/domain/agent/service/armory/agent_node.go
Normal file
29
internal/domain/agent/service/armory/agent_node.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
"ai-agent-scaffold-go/internal/domain/shared/tree"
|
||||
)
|
||||
|
||||
type AgentNode struct {
|
||||
agentFactory ports.AgentFactory
|
||||
next Handler
|
||||
}
|
||||
|
||||
func NewAgentNode(agentFactory ports.AgentFactory, next Handler) AgentNode {
|
||||
return AgentNode{agentFactory: agentFactory, next: next}
|
||||
}
|
||||
|
||||
func (n AgentNode) Apply(ctx context.Context, command model.ArmoryCommand, dynamic *DynamicContext) (model.RegisteredAgent, error) {
|
||||
for _, config := range command.Table.Module.Agents {
|
||||
agent, err := n.agentFactory.NewLLMAgent(ctx, config, dynamic.ChatModel)
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
dynamic.AddAgent(agent)
|
||||
}
|
||||
return tree.Route(ctx, n.next, command, dynamic)
|
||||
}
|
||||
27
internal/domain/agent/service/armory/ai_api_node.go
Normal file
27
internal/domain/agent/service/armory/ai_api_node.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
"ai-agent-scaffold-go/internal/domain/shared/tree"
|
||||
)
|
||||
|
||||
type AiAPINode struct {
|
||||
modelProvider ports.ModelProvider
|
||||
next Handler
|
||||
}
|
||||
|
||||
func NewAiAPINode(modelProvider ports.ModelProvider, next Handler) AiAPINode {
|
||||
return AiAPINode{modelProvider: modelProvider, next: next}
|
||||
}
|
||||
|
||||
func (n AiAPINode) Apply(ctx context.Context, command model.ArmoryCommand, dynamic *DynamicContext) (model.RegisteredAgent, error) {
|
||||
api, err := n.modelProvider.NewAPI(ctx, command.Table.Module.AiAPI)
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
dynamic.ModelAPI = api
|
||||
return tree.Route(ctx, n.next, command, dynamic)
|
||||
}
|
||||
54
internal/domain/agent/service/armory/chat_model_node.go
Normal file
54
internal/domain/agent/service/armory/chat_model_node.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
"ai-agent-scaffold-go/internal/domain/shared/tree"
|
||||
)
|
||||
|
||||
type ChatModelNode struct {
|
||||
modelProvider ports.ModelProvider
|
||||
mcpFactory ports.MCPToolFactory
|
||||
skillFactory ports.SkillFactory
|
||||
next Handler
|
||||
}
|
||||
|
||||
func NewChatModelNode(
|
||||
modelProvider ports.ModelProvider,
|
||||
mcpFactory ports.MCPToolFactory,
|
||||
skillFactory ports.SkillFactory,
|
||||
next Handler,
|
||||
) ChatModelNode {
|
||||
return ChatModelNode{
|
||||
modelProvider: modelProvider,
|
||||
mcpFactory: mcpFactory,
|
||||
skillFactory: skillFactory,
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
func (n ChatModelNode) Apply(ctx context.Context, command model.ArmoryCommand, dynamic *DynamicContext) (model.RegisteredAgent, error) {
|
||||
var tools []ports.Tool
|
||||
for _, config := range command.Table.Module.ChatModel.ToolMCPList {
|
||||
built, err := n.mcpFactory.BuildTools(ctx, config)
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
tools = append(tools, built...)
|
||||
}
|
||||
for _, config := range command.Table.Module.ChatModel.ToolSkillsList {
|
||||
built, err := n.skillFactory.BuildTools(ctx, config)
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
tools = append(tools, built...)
|
||||
}
|
||||
chatModel, err := n.modelProvider.NewChatModel(ctx, dynamic.ModelAPI, command.Table.Module.ChatModel, tools)
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
dynamic.ChatModel = chatModel
|
||||
return tree.Route(ctx, n.next, command, dynamic)
|
||||
}
|
||||
87
internal/domain/agent/service/armory/context.go
Normal file
87
internal/domain/agent/service/armory/context.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
)
|
||||
|
||||
type DynamicContext struct {
|
||||
mu sync.RWMutex
|
||||
ModelAPI ports.ModelAPI
|
||||
ChatModel ports.ChatModel
|
||||
agentGroup map[string]ports.Agent
|
||||
currentStepIndex int
|
||||
currentWorkflow *model.AgentWorkflowConfig
|
||||
values map[string]any
|
||||
}
|
||||
|
||||
func NewDynamicContext() *DynamicContext {
|
||||
return &DynamicContext{
|
||||
agentGroup: make(map[string]ports.Agent),
|
||||
values: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DynamicContext) AddAgent(agent ports.Agent) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.agentGroup[agent.Name()] = agent
|
||||
}
|
||||
|
||||
func (c *DynamicContext) Agent(name string) (ports.Agent, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
agent, ok := c.agentGroup[name]
|
||||
return agent, ok
|
||||
}
|
||||
|
||||
func (c *DynamicContext) QueryAgentList(names []string) []ports.Agent {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
agents := make([]ports.Agent, 0, len(names))
|
||||
for _, name := range names {
|
||||
if agent, ok := c.agentGroup[name]; ok {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
func (c *DynamicContext) AddCurrentStepIndex() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.currentStepIndex++
|
||||
}
|
||||
|
||||
func (c *DynamicContext) CurrentStepIndex() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.currentStepIndex
|
||||
}
|
||||
|
||||
func (c *DynamicContext) SetCurrentWorkflow(workflow *model.AgentWorkflowConfig) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.currentWorkflow = workflow
|
||||
}
|
||||
|
||||
func (c *DynamicContext) CurrentWorkflow() *model.AgentWorkflowConfig {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.currentWorkflow
|
||||
}
|
||||
|
||||
func (c *DynamicContext) SetValue(key string, value any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.values[key] = value
|
||||
}
|
||||
|
||||
func (c *DynamicContext) Value(key string) (any, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
value, ok := c.values[key]
|
||||
return value, ok
|
||||
}
|
||||
32
internal/domain/agent/service/armory/factory/factory.go
Normal file
32
internal/domain/agent/service/armory/factory/factory.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/service/armory"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/service/armory/workflow"
|
||||
)
|
||||
|
||||
type DefaultFactory struct {
|
||||
root armory.Handler
|
||||
}
|
||||
|
||||
func NewDefaultFactory(
|
||||
modelProvider ports.ModelProvider,
|
||||
mcpFactory ports.MCPToolFactory,
|
||||
skillFactory ports.SkillFactory,
|
||||
agentFactory ports.AgentFactory,
|
||||
runnerFactory ports.RunnerFactory,
|
||||
registry ports.AgentRegistry,
|
||||
) *DefaultFactory {
|
||||
runner := armory.NewRunnerNode(runnerFactory, registry)
|
||||
workflowNode := workflow.NewAgentWorkflowNode(agentFactory, runner)
|
||||
agent := armory.NewAgentNode(agentFactory, workflowNode)
|
||||
chatModel := armory.NewChatModelNode(modelProvider, mcpFactory, skillFactory, agent)
|
||||
api := armory.NewAiAPINode(modelProvider, chatModel)
|
||||
root := armory.NewRootNode(api)
|
||||
return &DefaultFactory{root: root}
|
||||
}
|
||||
|
||||
func (f *DefaultFactory) ArmoryStrategyHandler() armory.Handler {
|
||||
return f.root
|
||||
}
|
||||
20
internal/domain/agent/service/armory/root_node.go
Normal file
20
internal/domain/agent/service/armory/root_node.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/shared/tree"
|
||||
)
|
||||
|
||||
type RootNode struct {
|
||||
next Handler
|
||||
}
|
||||
|
||||
func NewRootNode(next Handler) RootNode {
|
||||
return RootNode{next: next}
|
||||
}
|
||||
|
||||
func (n RootNode) Apply(ctx context.Context, command model.ArmoryCommand, dynamic *DynamicContext) (model.RegisteredAgent, error) {
|
||||
return tree.Route(ctx, n.next, command, dynamic)
|
||||
}
|
||||
44
internal/domain/agent/service/armory/runner_node.go
Normal file
44
internal/domain/agent/service/armory/runner_node.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
)
|
||||
|
||||
type RunnerNode struct {
|
||||
runnerFactory ports.RunnerFactory
|
||||
registry ports.AgentRegistry
|
||||
}
|
||||
|
||||
func NewRunnerNode(runnerFactory ports.RunnerFactory, registry ports.AgentRegistry) RunnerNode {
|
||||
return RunnerNode{runnerFactory: runnerFactory, registry: registry}
|
||||
}
|
||||
|
||||
func (n RunnerNode) Apply(ctx context.Context, command model.ArmoryCommand, dynamic *DynamicContext) (model.RegisteredAgent, error) {
|
||||
runnerConfig := command.Table.Module.Runner
|
||||
if runnerConfig.AgentName == "" {
|
||||
return model.RegisteredAgent{}, fmt.Errorf("runner.agent-name is required")
|
||||
}
|
||||
agent, ok := dynamic.Agent(runnerConfig.AgentName)
|
||||
if !ok {
|
||||
return model.RegisteredAgent{}, fmt.Errorf("runner agent %q not found", runnerConfig.AgentName)
|
||||
}
|
||||
runner, err := n.runnerFactory.NewRunner(ctx, command.Table.AppName, agent, runnerConfig.PluginNameList)
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
registered := model.RegisteredAgent{
|
||||
AppName: command.Table.AppName,
|
||||
AgentID: command.Table.Agent.AgentID,
|
||||
AgentName: command.Table.Agent.AgentName,
|
||||
AgentDesc: command.Table.Agent.AgentDesc,
|
||||
Runner: runner,
|
||||
}
|
||||
if err := n.registry.Register(registered); err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
return registered, nil
|
||||
}
|
||||
25
internal/domain/agent/service/armory/service.go
Normal file
25
internal/domain/agent/service/armory/service.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
handler Handler
|
||||
}
|
||||
|
||||
func NewService(handler Handler) *Service {
|
||||
return &Service{handler: handler}
|
||||
}
|
||||
|
||||
func (s *Service) AcceptArmoryAgents(ctx context.Context, tables map[string]model.AiAgentConfigTable) error {
|
||||
for _, table := range tables {
|
||||
_, err := s.handler.Apply(ctx, model.ArmoryCommand{Table: table}, NewDynamicContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
8
internal/domain/agent/service/armory/support.go
Normal file
8
internal/domain/agent/service/armory/support.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/shared/tree"
|
||||
)
|
||||
|
||||
type Handler = tree.Handler[model.ArmoryCommand, *DynamicContext, model.RegisteredAgent]
|
||||
@@ -0,0 +1,67 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/service/armory"
|
||||
"ai-agent-scaffold-go/internal/domain/shared/tree"
|
||||
)
|
||||
|
||||
type workflowBuilder interface {
|
||||
Build(context.Context, model.AgentWorkflowConfig, []ports.Agent) (ports.Agent, error)
|
||||
}
|
||||
|
||||
type AgentWorkflowNode struct {
|
||||
next armory.Handler
|
||||
builders map[model.WorkflowType]workflowBuilder
|
||||
buildOrder []model.WorkflowType
|
||||
}
|
||||
|
||||
func NewAgentWorkflowNode(agentFactory ports.AgentFactory, next armory.Handler) *AgentWorkflowNode {
|
||||
buildOrder := []model.WorkflowType{
|
||||
model.WorkflowTypeLoop,
|
||||
model.WorkflowTypeParallel,
|
||||
model.WorkflowTypeSequential,
|
||||
}
|
||||
return &AgentWorkflowNode{
|
||||
next: next,
|
||||
builders: map[model.WorkflowType]workflowBuilder{
|
||||
model.WorkflowTypeLoop: NewLoopNode(agentFactory),
|
||||
model.WorkflowTypeParallel: NewParallelNode(agentFactory),
|
||||
model.WorkflowTypeSequential: NewSequentialNode(agentFactory),
|
||||
},
|
||||
buildOrder: buildOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *AgentWorkflowNode) Apply(ctx context.Context, command model.ArmoryCommand, dynamic *armory.DynamicContext) (model.RegisteredAgent, error) {
|
||||
workflows := command.Table.Module.AgentWorkflows
|
||||
if dynamic.CurrentStepIndex() >= len(workflows) {
|
||||
dynamic.SetCurrentWorkflow(nil)
|
||||
return tree.Route(ctx, n.next, command, dynamic)
|
||||
}
|
||||
|
||||
workflow := workflows[dynamic.CurrentStepIndex()]
|
||||
dynamic.SetCurrentWorkflow(&workflow)
|
||||
dynamic.AddCurrentStepIndex()
|
||||
|
||||
agent, err := n.build(ctx, workflow, dynamic.QueryAgentList(workflow.SubAgents))
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, err
|
||||
}
|
||||
dynamic.AddAgent(agent)
|
||||
return n.Apply(ctx, command, dynamic)
|
||||
}
|
||||
|
||||
func (n *AgentWorkflowNode) build(ctx context.Context, workflow model.AgentWorkflowConfig, subAgents []ports.Agent) (ports.Agent, error) {
|
||||
for _, workflowType := range n.buildOrder {
|
||||
if workflow.Type != workflowType {
|
||||
continue
|
||||
}
|
||||
return n.builders[workflowType].Build(ctx, workflow, subAgents)
|
||||
}
|
||||
return nil, fmt.Errorf("agentWorkflow type is error: %s", workflow.Type)
|
||||
}
|
||||
20
internal/domain/agent/service/armory/workflow/loop_node.go
Normal file
20
internal/domain/agent/service/armory/workflow/loop_node.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
)
|
||||
|
||||
type LoopNode struct {
|
||||
agentFactory ports.AgentFactory
|
||||
}
|
||||
|
||||
func NewLoopNode(agentFactory ports.AgentFactory) LoopNode {
|
||||
return LoopNode{agentFactory: agentFactory}
|
||||
}
|
||||
|
||||
func (n LoopNode) Build(ctx context.Context, workflow model.AgentWorkflowConfig, subAgents []ports.Agent) (ports.Agent, error) {
|
||||
return n.agentFactory.NewLoopAgent(ctx, workflow, subAgents)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
)
|
||||
|
||||
type ParallelNode struct {
|
||||
agentFactory ports.AgentFactory
|
||||
}
|
||||
|
||||
func NewParallelNode(agentFactory ports.AgentFactory) ParallelNode {
|
||||
return ParallelNode{agentFactory: agentFactory}
|
||||
}
|
||||
|
||||
func (n ParallelNode) Build(ctx context.Context, workflow model.AgentWorkflowConfig, subAgents []ports.Agent) (ports.Agent, error) {
|
||||
return n.agentFactory.NewParallelAgent(ctx, workflow, subAgents)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
)
|
||||
|
||||
type SequentialNode struct {
|
||||
agentFactory ports.AgentFactory
|
||||
}
|
||||
|
||||
func NewSequentialNode(agentFactory ports.AgentFactory) SequentialNode {
|
||||
return SequentialNode{agentFactory: agentFactory}
|
||||
}
|
||||
|
||||
func (n SequentialNode) Build(ctx context.Context, workflow model.AgentWorkflowConfig, subAgents []ports.Agent) (ports.Agent, error) {
|
||||
return n.agentFactory.NewSequentialAgent(ctx, workflow, subAgents)
|
||||
}
|
||||
117
internal/domain/agent/service/chat/service.go
Normal file
117
internal/domain/agent/service/chat/service.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"ai-agent-scaffold-go/internal/domain/agent/model"
|
||||
"ai-agent-scaffold-go/internal/domain/agent/ports"
|
||||
"ai-agent-scaffold-go/pkg/types"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
registry ports.AgentRegistry
|
||||
sessions ports.SessionStore
|
||||
}
|
||||
|
||||
func NewService(registry ports.AgentRegistry, sessions ports.SessionStore) *Service {
|
||||
return &Service{registry: registry, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *Service) QueryAgentConfigList() []model.AgentSummary {
|
||||
registered := s.registry.List()
|
||||
sort.Slice(registered, func(i, j int) bool {
|
||||
return registered[i].AgentID < registered[j].AgentID
|
||||
})
|
||||
agents := make([]model.AgentSummary, 0, len(registered))
|
||||
for _, agent := range registered {
|
||||
agents = append(agents, model.AgentSummary{
|
||||
AgentID: agent.AgentID,
|
||||
AgentName: agent.AgentName,
|
||||
AgentDesc: agent.AgentDesc,
|
||||
})
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
func (s *Service) CreateSession(agentID, userID string) (string, error) {
|
||||
if sessionID, ok := s.sessions.Get(userID, agentID); ok {
|
||||
return sessionID, nil
|
||||
}
|
||||
registered, ok := s.registry.Get(agentID)
|
||||
if !ok || registered.Runner == nil {
|
||||
return "", types.NewAppError(types.CodeAgentNotFound, types.InfoAgentNotFound)
|
||||
}
|
||||
sessionID, err := registered.Runner.CreateSession(userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := s.sessions.Set(userID, agentID, sessionID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
func (s *Service) HandleMessage(agentID, userID, sessionID, message string) ([]string, error) {
|
||||
content := model.ChatContent{Texts: []model.TextPart{{Message: message}}}
|
||||
return s.HandleCommand(model.ChatCommand{
|
||||
AgentID: agentID,
|
||||
UserID: userID,
|
||||
SessionID: sessionID,
|
||||
Message: message,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) HandleCommand(command model.ChatCommand) ([]string, error) {
|
||||
registered, sessionID, err := s.resolveRunnerSession(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registered.Runner.Run(command.UserID, sessionID, command.Content)
|
||||
}
|
||||
|
||||
func (s *Service) HandleMessageStream(agentID, userID, sessionID, message string) (<-chan string, <-chan error) {
|
||||
return s.HandleCommandStream(model.ChatCommand{
|
||||
AgentID: agentID,
|
||||
UserID: userID,
|
||||
SessionID: sessionID,
|
||||
Message: message,
|
||||
Content: model.ChatContent{Texts: []model.TextPart{{Message: message}}},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) HandleCommandStream(command model.ChatCommand) (<-chan string, <-chan error) {
|
||||
registered, sessionID, err := s.resolveRunnerSession(command)
|
||||
if err != nil {
|
||||
outputs := make(chan string)
|
||||
errs := make(chan error, 1)
|
||||
errs <- err
|
||||
close(outputs)
|
||||
close(errs)
|
||||
return outputs, errs
|
||||
}
|
||||
return registered.Runner.Stream(command.UserID, sessionID, command.Content)
|
||||
}
|
||||
|
||||
func (s *Service) resolveRunnerSession(command model.ChatCommand) (model.RegisteredAgent, string, error) {
|
||||
registered, ok := s.registry.Get(command.AgentID)
|
||||
if !ok || registered.Runner == nil {
|
||||
return model.RegisteredAgent{}, "", types.NewAppError(types.CodeAgentNotFound, types.InfoAgentNotFound)
|
||||
}
|
||||
sessionID := command.SessionID
|
||||
if sessionID == "" {
|
||||
var err error
|
||||
sessionID, err = s.CreateSession(command.AgentID, command.UserID)
|
||||
if err != nil {
|
||||
return model.RegisteredAgent{}, "", err
|
||||
}
|
||||
}
|
||||
if len(command.Content.Texts) == 0 && command.Message != "" {
|
||||
command.Content.Texts = []model.TextPart{{Message: command.Message}}
|
||||
}
|
||||
if len(command.Content.Texts) == 0 && len(command.Content.Files) == 0 && len(command.Content.InlineDatas) == 0 {
|
||||
return model.RegisteredAgent{}, "", fmt.Errorf("chat content is required")
|
||||
}
|
||||
return registered, sessionID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user