feat(project): 项目完结

This commit is contained in:
peakxy
2026-05-30 23:16:49 +08:00
commit 60e7777cbd
97 changed files with 15027 additions and 0 deletions

View 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)
}

View 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)
}

View 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)
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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]

View File

@@ -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)
}

View 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)
}

View 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 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)
}

View 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 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)
}

View 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
}