feat: 初始化 sundynix-agentix 分层式 AI Agent 平台脚手架
5 层 + 1 条 NATS 零拷贝消息总线的 monorepo(Monolith First → Microservices Morph B)。 纵向主干(任务流 + Token 流回流)已真实跑通,横向各层能力为带注释的桩。 已贯通(real code): - sundynix-shared: 共享契约 + JetStream/core NATS 真实收发(bus) + 内嵌 NATS(devnats) + e2e 测试 - sundynix-gateway: Gin 接入 + DSL 解析组装 + NATS Publish + SSE 流式输出 - sundynix-dispatcher: NATS 消费 + Eino Orchestrator 流式回流 + 熔断器 + LLM Pool 占位流式 - 链路: HTTP POST → DSL → sundynix.tasks.* → Dispatcher → Token 经 sundynix.streams.<id> 回流 → SSE - 基础设施: docker-compose(nats/postgres/redis/neo4j/milvus) + Makefile(make demo/e2e) 待填(桩): - Eino 图编排 compose.NewGraph、LLM Pool 接 vLLM/Ollama - Gateway store 换真实 pgx/redis - sundynix-mcp-go: Bleve+Milvus+Neo4j 混合检索 / UniOffice / 外部 API - sundynix-mcp-py: gVisor 沙箱 / MinerU(PaddleOCR) / Docker 解释器 - sundynix-desktop: React Flow 画布 → DSL 导出 → SSE 展示
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
// Package eino 封装基于 CloudWeGo Eino 的 Agent 图编排引擎。
|
||||
package eino
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
)
|
||||
|
||||
// TokenSink 是 Token 流回流出口(由 NATS bus 实现)。
|
||||
type TokenSink interface {
|
||||
PublishToken(taskID string, token []byte) error
|
||||
CompleteStream(taskID string) error
|
||||
}
|
||||
|
||||
// Orchestrator 将 DSL 图编译为 Eino Graph 并驱动执行。
|
||||
type Orchestrator struct {
|
||||
pool *llm.Pool
|
||||
breaker *harness.CircuitBreaker
|
||||
sink TokenSink
|
||||
}
|
||||
|
||||
func NewOrchestrator(pool *llm.Pool, breaker *harness.CircuitBreaker, sink TokenSink) *Orchestrator {
|
||||
return &Orchestrator{pool: pool, breaker: breaker, sink: sink}
|
||||
}
|
||||
|
||||
// Handle 消费一个任务:编译图 → 流式推理 → 经 sink 把 Token 回流到 sundynix.streams.<id>。
|
||||
func (o *Orchestrator) Handle(ctx context.Context, t *contract.Task) error {
|
||||
if !o.breaker.Allow() {
|
||||
log.Printf("[eino] circuit open, drop task %s", t.ID)
|
||||
return nil
|
||||
}
|
||||
log.Printf("[eino] task %s received (graph=%d bytes), streaming tokens...", t.ID, len(t.Graph))
|
||||
|
||||
// TODO: compose.NewGraph(...) 编译 DSL;此处 prompt 占位为图原文。
|
||||
// 工具节点经 NATS 调用第 5 层 MCP(sundynix.tools.go.* / sundynix.tools.py.*)。
|
||||
prompt := string(t.Graph)
|
||||
|
||||
n := 0
|
||||
err := o.pool.Stream(ctx, prompt, func(tok []byte) {
|
||||
if perr := o.sink.PublishToken(t.ID, tok); perr != nil {
|
||||
log.Printf("[eino] publish token failed: %v", perr)
|
||||
return
|
||||
}
|
||||
n++
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[eino] task %s stream error: %v", t.ID, err)
|
||||
}
|
||||
|
||||
if cerr := o.sink.CompleteStream(t.ID); cerr != nil {
|
||||
log.Printf("[eino] complete stream failed: %v", cerr)
|
||||
}
|
||||
log.Printf("[eino] task %s done, %d tokens streamed", t.ID, n)
|
||||
o.breaker.Report(err == nil)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package harness
|
||||
|
||||
// CircuitBreaker 实现熔断降级中心:后端异常时熔断并切换降级策略。
|
||||
type CircuitBreaker struct{ /* state, counters */ }
|
||||
|
||||
func NewCircuitBreaker() *CircuitBreaker { return &CircuitBreaker{} }
|
||||
|
||||
// Allow 判定当前是否放行请求。
|
||||
func (c *CircuitBreaker) Allow() bool { return true } // TODO: half-open / open 状态机
|
||||
|
||||
// Report 上报一次调用结果以驱动状态机。
|
||||
func (c *CircuitBreaker) Report(success bool) {} // TODO
|
||||
@@ -0,0 +1,15 @@
|
||||
// Package harness 提供 LLM 自动化评测与熔断降级能力。
|
||||
package harness
|
||||
|
||||
import "context"
|
||||
|
||||
// Evaluator 实现 LLM 自动化评测(质量打分 / 回归对比)。
|
||||
type Evaluator struct{}
|
||||
|
||||
func NewEvaluator() *Evaluator { return &Evaluator{} }
|
||||
|
||||
// Score 对一次推理输出打分。
|
||||
func (e *Evaluator) Score(ctx context.Context, input, output string) (float64, error) {
|
||||
// TODO: LLM-as-judge / 规则评测
|
||||
return 0, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package llm 抽象 LLM Pool(vLLM / Ollama 集群)的负载均衡与流式推理。
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Pool 维护后端 LLM 实例列表与路由策略。
|
||||
type Pool struct{ /* backends []Backend */ }
|
||||
|
||||
func NewPool() *Pool { return &Pool{} }
|
||||
|
||||
// 占位参数:模拟真实后端的 TTFT(首 token 延迟) 与逐 token 间隔。
|
||||
const (
|
||||
timeToFirstToken = 700 * time.Millisecond
|
||||
interTokenDelay = 60 * time.Millisecond
|
||||
)
|
||||
|
||||
// Stream 选择一个后端进行流式推理,逐 Token 回调 onToken。
|
||||
// 当前为占位实现:把对 prompt 的确定性回复按 token 流式返回,
|
||||
// 真实接入 vLLM/Ollama 时替换为后端 streaming API 即可(回调签名不变)。
|
||||
func (p *Pool) Stream(ctx context.Context, prompt string, onToken func([]byte)) error {
|
||||
// TODO: 选路 (least-load / 模型亲和) → 调 vLLM/Ollama streaming API
|
||||
reply := buildReply(prompt)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(timeToFirstToken): // 模拟 TTFT
|
||||
}
|
||||
|
||||
for _, tok := range tokenize(reply) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
onToken([]byte(tok))
|
||||
time.Sleep(interTokenDelay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildReply 占位:真实实现应由 DSL 编排出的对话上下文驱动后端生成。
|
||||
func buildReply(prompt string) string {
|
||||
p := strings.TrimSpace(prompt)
|
||||
if len(p) > 40 {
|
||||
p = p[:40] + "…"
|
||||
}
|
||||
return "已编排执行该 Agent 图,输入摘要: " + p
|
||||
}
|
||||
|
||||
// tokenize 占位分词:按 rune 切,保证多字节中文也能逐字流式。
|
||||
func tokenize(s string) []string {
|
||||
out := make([]string, 0, len(s))
|
||||
for _, r := range s {
|
||||
out = append(out, string(r))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Package nats 是调度器对共享 bus 的薄封装(消费任务 / 回写 Token)。
|
||||
package nats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
)
|
||||
|
||||
// TaskHandler 处理单个任务。
|
||||
type TaskHandler func(ctx context.Context, t *contract.Task) error
|
||||
|
||||
// Subscriber 包装共享 bus,向调度器暴露消费能力。
|
||||
type Subscriber struct {
|
||||
inner *sharedbus.Bus
|
||||
}
|
||||
|
||||
// MustConnect 接入 NATS 并确保任务流存在(消费者声明在 Consume 时完成)。
|
||||
func MustConnect(url string) *Subscriber {
|
||||
inner, err := sharedbus.Connect(url)
|
||||
if err != nil {
|
||||
log.Fatalf("[dispatcher/nats] connect: %v", err)
|
||||
}
|
||||
if err := inner.EnsureTaskStream(context.Background()); err != nil {
|
||||
log.Fatalf("[dispatcher/nats] ensure stream: %v", err)
|
||||
}
|
||||
log.Printf("[dispatcher/nats] connected %s", url)
|
||||
return &Subscriber{inner: inner}
|
||||
}
|
||||
|
||||
// ConsumeTasks 从 sundynix.tasks.* 持续消费任务(队列组负载均衡),阻塞至 ctx 取消。
|
||||
func (s *Subscriber) ConsumeTasks(ctx context.Context, h TaskHandler) error {
|
||||
stop, err := s.inner.ConsumeTasks(ctx, func(c context.Context, t *contract.Task) error {
|
||||
return h(c, t)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stop()
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// PublishToken / CompleteStream 让 Subscriber 满足 eino.TokenSink,
|
||||
// 把推理 Token 回流到 sundynix.streams.<taskID>。
|
||||
func (s *Subscriber) PublishToken(taskID string, token []byte) error {
|
||||
return s.inner.PublishToken(taskID, token)
|
||||
}
|
||||
|
||||
func (s *Subscriber) CompleteStream(taskID string) error {
|
||||
return s.inner.CompleteStream(taskID)
|
||||
}
|
||||
|
||||
func (s *Subscriber) Close() { s.inner.Close() }
|
||||
Reference in New Issue
Block a user