feat(wechat): 关注公众号后自动回复欢迎语(被动回复,可后台配置) #12
@@ -71,7 +71,7 @@ export async function me(): Promise<AuthUser | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- 模型配置(id 为雪花字符串)----
|
// ---- 模型配置(id 为雪花字符串)----
|
||||||
export type Kind = "chat" | "embedding";
|
export type Kind = "chat" | "voice" | "embedding";
|
||||||
|
|
||||||
export interface Model {
|
export interface Model {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ModelManager } from "../components/ModelManager";
|
import { ModelManager } from "../components/ModelManager";
|
||||||
|
import type { Kind } from "../api";
|
||||||
|
|
||||||
|
// 模型配置页:工作主力(chat) / JARVIS 语音(voice) / 向量化(embedding) 三 Tab 统一管理。
|
||||||
|
// 工作与语音分开:编排/报告用强模型(chat),JARVIS 语音对话用快模型(voice, 低时延)——各配各激活。
|
||||||
|
const TABS: { key: Kind; label: string }[] = [
|
||||||
|
{ key: "chat", label: "工作主力模型" },
|
||||||
|
{ key: "voice", label: "JARVIS 语音模型" },
|
||||||
|
{ key: "embedding", label: "向量化模型" },
|
||||||
|
];
|
||||||
|
|
||||||
// 模型配置页:Chat / Embedding 双 Tab 统一管理。
|
|
||||||
export function ModelsPage() {
|
export function ModelsPage() {
|
||||||
const [tab, setTab] = useState<"chat" | "embedding">("chat");
|
const [tab, setTab] = useState<Kind>("chat");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -11,46 +19,46 @@ export function ModelsPage() {
|
|||||||
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-gray-150 pb-4">
|
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-gray-150 pb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-semibold text-gray-800">模型管理</h3>
|
<h3 className="text-base font-semibold text-gray-800">模型管理</h3>
|
||||||
<p className="text-xs text-gray-400">配置全平台的对话模型(LLM)与向量化模型(Embedding)</p>
|
<p className="text-xs text-gray-400">工作主力模型跑编排/报告(要强)· JARVIS 语音模型跑实时对话(要快)· 向量化模型跑 RAG</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex rounded-lg border border-gray-200 bg-gray-50/50 p-1">
|
<div className="flex rounded-lg border border-gray-200 bg-gray-50/50 p-1">
|
||||||
<button
|
{TABS.map((t) => (
|
||||||
onClick={() => setTab("chat")}
|
<button
|
||||||
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-all ${
|
key={t.key}
|
||||||
tab === "chat"
|
onClick={() => setTab(t.key)}
|
||||||
? "bg-white text-violet-700 shadow-sm"
|
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-all ${
|
||||||
: "text-gray-500 hover:text-gray-700"
|
tab === t.key ? "bg-white text-violet-700 shadow-sm" : "text-gray-500 hover:text-gray-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
对话模型 (Chat)
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
<button
|
))}
|
||||||
onClick={() => setTab("embedding")}
|
|
||||||
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-all ${
|
|
||||||
tab === "embedding"
|
|
||||||
? "bg-white text-violet-700 shadow-sm"
|
|
||||||
: "text-gray-500 hover:text-gray-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
向量化模型 (Embedding)
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 模型管理组件 */}
|
{/* 模型管理组件 */}
|
||||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||||
{tab === "chat" ? (
|
{tab === "chat" && (
|
||||||
<ModelManager
|
<ModelManager
|
||||||
kind="chat"
|
kind="chat"
|
||||||
title="对话模型配置 (chat → Dispatcher)"
|
title="工作主力模型 (chat → Dispatcher 编排/报告)"
|
||||||
|
baseUrlHint="https://api.deepseek.com"
|
||||||
|
modelHint="deepseek-v4-pro"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab === "voice" && (
|
||||||
|
<ModelManager
|
||||||
|
kind="voice"
|
||||||
|
title="JARVIS 语音模型 (voice → 语音对话,选快模型抢首字时延)"
|
||||||
baseUrlHint="https://api.deepseek.com"
|
baseUrlHint="https://api.deepseek.com"
|
||||||
modelHint="deepseek-chat"
|
modelHint="deepseek-chat"
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
|
{tab === "embedding" && (
|
||||||
<ModelManager
|
<ModelManager
|
||||||
kind="embedding"
|
kind="embedding"
|
||||||
title="向量化模型配置 (embedding → mcp-go RAG)"
|
title="向量化模型 (embedding → mcp-go RAG)"
|
||||||
baseUrlHint="https://dashscope.aliyuncs.com/compatible-mode/v1"
|
baseUrlHint="https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
modelHint="text-embedding-v3"
|
modelHint="text-embedding-v3"
|
||||||
/>
|
/>
|
||||||
@@ -59,4 +67,3 @@ export function ModelsPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ func main() {
|
|||||||
|
|
||||||
natsURL := envOr("NATS_URL", "nats://localhost:4222")
|
natsURL := envOr("NATS_URL", "nats://localhost:4222")
|
||||||
|
|
||||||
pool := llm.NewPool() // LLM Pool: vLLM / Ollama 集群
|
pool := llm.NewPool() // 工作主力模型池(chat)
|
||||||
|
voicePool := llm.NewPool() // JARVIS 语音模型池(voice;未配置则空、语音任务回落 pool)
|
||||||
breaker := harness.NewCircuitBreaker() // Harness: 熔断降级中心
|
breaker := harness.NewCircuitBreaker() // Harness: 熔断降级中心
|
||||||
// Harness: LLM 自动化评测(规则 + LLM-as-judge,模型就绪时启用)。
|
// Harness: LLM 自动化评测(规则 + LLM-as-judge,模型就绪时启用)。
|
||||||
llmChat := func(ctx context.Context, sys, user string) (string, error) {
|
llmChat := func(ctx context.Context, sys, user string) (string, error) {
|
||||||
@@ -53,6 +54,12 @@ func main() {
|
|||||||
log.Printf("[dispatcher] subscribe model config: %v", err)
|
log.Printf("[dispatcher] subscribe model config: %v", err)
|
||||||
}
|
}
|
||||||
go sub.FetchModelConfigWithRetry(context.Background(), pool.SetConfig)
|
go sub.FetchModelConfigWithRetry(context.Background(), pool.SetConfig)
|
||||||
|
// 语音模型(JARVIS)配置:同机制热更新 + 后台重试。未配置语音模型时拉不到、voicePool 保持空,
|
||||||
|
// 语音任务在 agentPool 里透明回落工作模型池——不影响现有功能。
|
||||||
|
if _, err := sub.SubscribeVoiceConfigUpdated(voicePool.SetConfig); err != nil {
|
||||||
|
log.Printf("[dispatcher] subscribe voice model config: %v", err)
|
||||||
|
}
|
||||||
|
go sub.FetchVoiceConfigWithRetry(context.Background(), voicePool.SetConfig)
|
||||||
|
|
||||||
// Prompt 控制面:拉激活集覆盖内置默认 + 订阅热更新(管理端激活某版即生效,不重启)。
|
// Prompt 控制面:拉激活集覆盖内置默认 + 订阅热更新(管理端激活某版即生效,不重启)。
|
||||||
go sub.FetchPromptsWithRetry(context.Background(), prompts.ApplyOverrides)
|
go sub.FetchPromptsWithRetry(context.Background(), prompts.ApplyOverrides)
|
||||||
@@ -66,8 +73,9 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[dispatcher] build eino graph: %v", err)
|
log.Fatalf("[dispatcher] build eino graph: %v", err)
|
||||||
}
|
}
|
||||||
orch.SetGuardian(guardian) // 输入护栏 Tier2
|
orch.SetGuardian(guardian) // 输入护栏 Tier2
|
||||||
orch.SetUsageSink(sub) // 成本护栏:token 用量回写网关累计/计费
|
orch.SetUsageSink(sub) // 成本护栏:token 用量回写网关累计/计费
|
||||||
|
orch.SetVoicePool(voicePool) // JARVIS 语音任务用快模型(未配置则回落工作模型)
|
||||||
|
|
||||||
// HITL 持久化中断/恢复:开 checkpoint 存储 + 审批决定流,审批节点改走中断模型
|
// HITL 持久化中断/恢复:开 checkpoint 存储 + 审批决定流,审批节点改走中断模型
|
||||||
// (compose.Interrupt 落盘释放 goroutine、抗 dispatcher 重启)。任一步失败则降级回阻塞模型。
|
// (compose.Interrupt 落盘释放 goroutine、抗 dispatcher 重启)。任一步失败则降级回阻塞模型。
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type boardSnapshot struct {
|
|||||||
UID string `json:"uid,omitempty"`
|
UID string `json:"uid,omitempty"`
|
||||||
SID string `json:"sid,omitempty"`
|
SID string `json:"sid,omitempty"`
|
||||||
Query string `json:"query,omitempty"`
|
Query string `json:"query,omitempty"`
|
||||||
|
UseVoice bool `json:"use_voice,omitempty"`
|
||||||
Profile string `json:"profile,omitempty"`
|
Profile string `json:"profile,omitempty"`
|
||||||
History []*schema.Message `json:"history,omitempty"`
|
History []*schema.Message `json:"history,omitempty"`
|
||||||
KB string `json:"kb,omitempty"`
|
KB string `json:"kb,omitempty"`
|
||||||
@@ -35,6 +36,7 @@ func (b *board) MarshalJSON() ([]byte, error) {
|
|||||||
UID: b.uid,
|
UID: b.uid,
|
||||||
SID: b.sid,
|
SID: b.sid,
|
||||||
Query: b.query,
|
Query: b.query,
|
||||||
|
UseVoice: b.useVoice,
|
||||||
Profile: b.profile,
|
Profile: b.profile,
|
||||||
History: b.history,
|
History: b.history,
|
||||||
KB: b.kb,
|
KB: b.kb,
|
||||||
@@ -56,6 +58,7 @@ func (b *board) UnmarshalJSON(data []byte) error {
|
|||||||
b.uid = s.UID
|
b.uid = s.UID
|
||||||
b.sid = s.SID
|
b.sid = s.SID
|
||||||
b.query = s.Query
|
b.query = s.Query
|
||||||
|
b.useVoice = s.UseVoice
|
||||||
b.profile = s.Profile
|
b.profile = s.Profile
|
||||||
b.history = s.History
|
b.history = s.History
|
||||||
b.kb = s.KB
|
b.kb = s.KB
|
||||||
|
|||||||
@@ -56,9 +56,10 @@ func (o *Orchestrator) execComposeGraph(ctx context.Context, t *contract.Task, t
|
|||||||
flow, ferr := dsl.Parse(t.Graph)
|
flow, ferr := dsl.Parse(t.Graph)
|
||||||
plan := dsl.Compile(t.Graph)
|
plan := dsl.Compile(t.Graph)
|
||||||
b := &board{
|
b := &board{
|
||||||
uid: meta(t, contract.MetaUserID),
|
uid: meta(t, contract.MetaUserID),
|
||||||
sid: meta(t, contract.MetaSessionID),
|
sid: meta(t, contract.MetaSessionID),
|
||||||
query: plan.Query,
|
query: plan.Query,
|
||||||
|
useVoice: meta(t, contract.MetaModelProfile) == contract.ModelProfileVoice, // 语音任务 → 走语音模型池
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无图/空图:退化为 compose 单轮对话。
|
// 无图/空图:退化为 compose 单轮对话。
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// START → ChatModel 节点 → END,编译为 Runnable 后流式执行;可观测经 callbacks 桥到 ExecEvent。
|
// START → ChatModel 节点 → END,编译为 Runnable 后流式执行;可观测经 callbacks 桥到 ExecEvent。
|
||||||
// 模型未就绪 / 编译失败时降级回 runAgent(同样的流式回流,保证不回归)。
|
// 模型未就绪 / 编译失败时降级回 runAgent(同样的流式回流,保证不回归)。
|
||||||
func (o *Orchestrator) runComposeConversation(ctx context.Context, taskID string, b *board, system string, tr *execTracer, node, label string) {
|
func (o *Orchestrator) runComposeConversation(ctx context.Context, taskID string, b *board, system string, tr *execTracer, node, label string) {
|
||||||
cm := o.pool.ChatModel()
|
cm := o.agentPool(b).ChatModel() // 语音任务走语音模型池
|
||||||
if cm == nil {
|
if cm == nil {
|
||||||
o.runAgent(ctx, taskID, b, system, tr, node, label) // 无模型 → 降级桩
|
o.runAgent(ctx, taskID, b, system, tr, node, label) // 无模型 → 降级桩
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistS
|
|||||||
usedFuncNames := map[string]bool{}
|
usedFuncNames := map[string]bool{}
|
||||||
for i, spec := range specs {
|
for i, spec := range specs {
|
||||||
sys := firstNonEmpty(spec.System, defaultAgentSystem) + specialistCondensedSuffix
|
sys := firstNonEmpty(spec.System, defaultAgentSystem) + specialistCondensedSuffix
|
||||||
run := o.specialistRunner(ctx, spec, sys, byName)
|
run := o.specialistRunner(ctx, b, spec, sys, byName)
|
||||||
if run == nil {
|
if run == nil {
|
||||||
tr.info("coordinator", "system", "专家跳过", "无可用模型:"+spec.Name)
|
tr.info("coordinator", "system", "专家跳过", "无可用模型:"+spec.Name)
|
||||||
continue
|
continue
|
||||||
@@ -175,7 +175,7 @@ func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistS
|
|||||||
|
|
||||||
// specialistRunner 构造专家执行闭包:带工具且模型支持函数调用→react.Agent.Generate;否则→ChatModel.Generate。
|
// specialistRunner 构造专家执行闭包:带工具且模型支持函数调用→react.Agent.Generate;否则→ChatModel.Generate。
|
||||||
// 无可用模型返回 nil(该专家被跳过)。
|
// 无可用模型返回 nil(该专家被跳过)。
|
||||||
func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec, sys string, byName map[string]tool.BaseTool) func(context.Context, string) (string, error) {
|
func (o *Orchestrator) specialistRunner(ctx context.Context, b *board, spec specialistSpec, sys string, byName map[string]tool.BaseTool) func(context.Context, string) (string, error) {
|
||||||
var tools []tool.BaseTool
|
var tools []tool.BaseTool
|
||||||
for _, tn := range spec.Tools {
|
for _, tn := range spec.Tools {
|
||||||
if t, ok := byName[tn]; ok {
|
if t, ok := byName[tn]; ok {
|
||||||
@@ -183,7 +183,7 @@ func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(tools) > 0 {
|
if len(tools) > 0 {
|
||||||
if tcm := o.pool.ToolCallingModel(); tcm != nil {
|
if tcm := o.agentPool(b).ToolCallingModel(); tcm != nil {
|
||||||
ag, err := react.NewAgent(ctx, &react.AgentConfig{
|
ag, err := react.NewAgent(ctx, &react.AgentConfig{
|
||||||
ToolCallingModel: tcm,
|
ToolCallingModel: tcm,
|
||||||
ToolsConfig: compose.ToolsNodeConfig{Tools: tools},
|
ToolsConfig: compose.ToolsNodeConfig{Tools: tools},
|
||||||
@@ -202,7 +202,7 @@ func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec
|
|||||||
}
|
}
|
||||||
// 工具型专家但模型不支持函数调用 → 退纯对话(下方)
|
// 工具型专家但模型不支持函数调用 → 退纯对话(下方)
|
||||||
}
|
}
|
||||||
cm := o.pool.ChatModel()
|
cm := o.agentPool(b).ChatModel()
|
||||||
if cm == nil {
|
if cm == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@ func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec
|
|||||||
// Anthropic 配方(分解→定制简报→并行派发→综合)。无 ToolCallingModel / 0 可用专家 → 降级单 agent。
|
// Anthropic 配方(分解→定制简报→并行派发→综合)。无 ToolCallingModel / 0 可用专家 → 降级单 agent。
|
||||||
func (o *Orchestrator) runCoordinator(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) {
|
func (o *Orchestrator) runCoordinator(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) {
|
||||||
specs := parseSpecialists(n.Config)
|
specs := parseSpecialists(n.Config)
|
||||||
tcm := o.pool.ToolCallingModel()
|
tcm := o.agentPool(b).ToolCallingModel()
|
||||||
if tcm == nil || len(specs) == 0 {
|
if tcm == nil || len(specs) == 0 {
|
||||||
tr.info(node, "system", "协调者降级", "模型不支持函数调用或未配置专家,退回普通对话")
|
tr.info(node, "system", "协调者降级", "模型不支持函数调用或未配置专家,退回普通对话")
|
||||||
o.runAgent(ctx, taskID, b, system, tr, node, labelOf(n, "多智能体协调"))
|
o.runAgent(ctx, taskID, b, system, tr, node, labelOf(n, "多智能体协调"))
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const defaultAgentSystem = "你是 sundynix-agentix 平台的 AI 助手。"
|
|||||||
type board struct {
|
type board struct {
|
||||||
uid, sid string
|
uid, sid string
|
||||||
query string
|
query string
|
||||||
|
useVoice bool // 该任务用 JARVIS 语音模型池(网关 Meta[model_profile]==voice)
|
||||||
profile string
|
profile string
|
||||||
history []*schema.Message
|
history []*schema.Message
|
||||||
kb string // 最近一个检索节点的 owner 作用域库名(供 map 并行各项检索)
|
kb string // 最近一个检索节点的 owner 作用域库名(供 map 并行各项检索)
|
||||||
@@ -178,10 +179,11 @@ func (o *Orchestrator) runAgent(ctx context.Context, taskID string, b *board, sy
|
|||||||
var reasoning strings.Builder
|
var reasoning strings.Builder
|
||||||
onReasoning := func(s string) { reasoning.WriteString(s) }
|
onReasoning := func(s string) { reasoning.WriteString(s) }
|
||||||
var err error
|
var err error
|
||||||
if o.pool.Ready() {
|
pool := o.agentPool(b) // 语音任务走语音模型池(否则工作池)
|
||||||
err = o.pool.ChatStream(ctx, toChatMessages(msgs), send, onReasoning)
|
if pool.Ready() {
|
||||||
|
err = pool.ChatStream(ctx, toChatMessages(msgs), send, onReasoning)
|
||||||
} else {
|
} else {
|
||||||
err = o.pool.StreamText(ctx, replyFor(msgs), func(tok []byte) { send(string(tok)) })
|
err = pool.StreamText(ctx, replyFor(msgs), func(tok []byte) { send(string(tok)) })
|
||||||
}
|
}
|
||||||
if rc := reasoning.String(); rc != "" {
|
if rc := reasoning.String(); rc != "" {
|
||||||
tr.info(node, "model", "推理过程", fmt.Sprintf("思考 %d 字:%s", len([]rune(rc)), truncate(rc, 200)))
|
tr.info(node, "model", "推理过程", fmt.Sprintf("思考 %d 字:%s", len([]rune(rc)), truncate(rc, 200)))
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ const specialistTimeout = 3 * time.Minute
|
|||||||
// Orchestrator 把每个 DSL 任务动态编译为 Eino 图并执行(记忆召回 → 工具节点 → 注入 → 流式)。
|
// Orchestrator 把每个 DSL 任务动态编译为 Eino 图并执行(记忆召回 → 工具节点 → 注入 → 流式)。
|
||||||
type Orchestrator struct {
|
type Orchestrator struct {
|
||||||
pool LLM
|
pool LLM
|
||||||
|
voicePool LLM // JARVIS 语音模型池(可为 nil → 语音任务回落 pool)
|
||||||
breaker *harness.CircuitBreaker
|
breaker *harness.CircuitBreaker
|
||||||
eval *harness.Evaluator
|
eval *harness.Evaluator
|
||||||
sink TokenSink
|
sink TokenSink
|
||||||
@@ -122,6 +123,19 @@ func NewOrchestrator(pool LLM, breaker *harness.CircuitBreaker, eval *harness.Ev
|
|||||||
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec, status: status, approval: approval, evalSink: evalSink}, nil
|
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec, status: status, approval: approval, evalSink: evalSink}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetVoicePool 注入 JARVIS 语音模型池(可选)。注入且就绪时,标了 model_profile==voice 的任务
|
||||||
|
// 用它跑,抢首字时延;不注入或未就绪则语音任务透明回落工作模型池(o.pool)。
|
||||||
|
func (o *Orchestrator) SetVoicePool(p LLM) { o.voicePool = p }
|
||||||
|
|
||||||
|
// agentPool 按黑板选本任务该用的模型池:语音任务且语音池就绪 → 语音池;否则工作池。
|
||||||
|
// 只用于面向用户的 agent 生成(对话/协作);报告/护栏/记忆抽取等固定走工作池。
|
||||||
|
func (o *Orchestrator) agentPool(b *board) LLM {
|
||||||
|
if b != nil && b.useVoice && o.voicePool != nil && o.voicePool.Ready() {
|
||||||
|
return o.voicePool
|
||||||
|
}
|
||||||
|
return o.pool
|
||||||
|
}
|
||||||
|
|
||||||
// SetGuardian 注入输入护栏 Tier2 的 LLM 分类器(可选;不注入则灰区任务直接放行执行)。
|
// SetGuardian 注入输入护栏 Tier2 的 LLM 分类器(可选;不注入则灰区任务直接放行执行)。
|
||||||
func (o *Orchestrator) SetGuardian(c *harness.Classifier) { o.guard = c }
|
func (o *Orchestrator) SetGuardian(c *harness.Classifier) { o.guard = c }
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ func (o *Orchestrator) discoverTools(subject func(string) string, b *board, task
|
|||||||
// runReactAgent 执行带"自主工具"的 agent 节点:模型在 ReAct 循环里自行决定调哪些 MCP 工具。
|
// runReactAgent 执行带"自主工具"的 agent 节点:模型在 ReAct 循环里自行决定调哪些 MCP 工具。
|
||||||
// 模型不支持函数调用 / 无工具时降级回普通 runAgent。最终答复流式回流;工具调用由适配器落轨迹。
|
// 模型不支持函数调用 / 无工具时降级回普通 runAgent。最终答复流式回流;工具调用由适配器落轨迹。
|
||||||
func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) {
|
func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) {
|
||||||
tcm := o.pool.ToolCallingModel()
|
tcm := o.agentPool(b).ToolCallingModel()
|
||||||
tools := o.agentTools(b, taskID, tr)
|
tools := o.agentTools(b, taskID, tr)
|
||||||
if tcm == nil || len(tools) == 0 {
|
if tcm == nil || len(tools) == 0 {
|
||||||
tr.info(node, "system", "ReAct 降级", "模型不支持函数调用或无可用工具,退回普通对话")
|
tr.info(node, "system", "ReAct 降级", "模型不支持函数调用或无可用工具,退回普通对话")
|
||||||
|
|||||||
@@ -146,6 +146,16 @@ func (s *Subscriber) FetchModelConfigWithRetry(ctx context.Context, apply func(*
|
|||||||
s.inner.RequestConfigWithRetry(ctx, contract.ConfigKindChat, apply)
|
s.inner.RequestConfigWithRetry(ctx, contract.ConfigKindChat, apply)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscribeVoiceConfigUpdated 订阅 JARVIS 语音模型配置热更新(可空——未配置语音模型时不生效)。
|
||||||
|
func (s *Subscriber) SubscribeVoiceConfigUpdated(onUpdate func(*contract.ModelConfig)) (func() error, error) {
|
||||||
|
return s.inner.SubscribeConfigUpdated(contract.ConfigKindVoice, onUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchVoiceConfigWithRetry 后台重试拉取初始语音模型配置(未配置则一直拿不到,语音任务回落工作模型)。
|
||||||
|
func (s *Subscriber) FetchVoiceConfigWithRetry(ctx context.Context, apply func(*contract.ModelConfig)) {
|
||||||
|
s.inner.RequestConfigWithRetry(ctx, contract.ConfigKindVoice, apply)
|
||||||
|
}
|
||||||
|
|
||||||
// FetchPromptsWithRetry 后台重试拉取初始激活 prompt 集(覆盖内置默认)。
|
// FetchPromptsWithRetry 后台重试拉取初始激活 prompt 集(覆盖内置默认)。
|
||||||
func (s *Subscriber) FetchPromptsWithRetry(ctx context.Context, apply func(map[string]string)) {
|
func (s *Subscriber) FetchPromptsWithRetry(ctx context.Context, apply func(map[string]string)) {
|
||||||
s.inner.RequestPromptsWithRetry(ctx, apply)
|
s.inner.RequestPromptsWithRetry(ctx, apply)
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ func main() {
|
|||||||
bus := nats.MustConnect(natsURL) // 接入 NATS 零拷贝骨干网 + 声明任务流
|
bus := nats.MustConnect(natsURL) // 接入 NATS 零拷贝骨干网 + 声明任务流
|
||||||
defer bus.Close()
|
defer bus.Close()
|
||||||
|
|
||||||
// 配置控制面:按 kind 响应消费方(Dispatcher=chat / mcp-go=embedding)的配置请求。
|
// 配置控制面:按 kind 响应消费方(Dispatcher=chat/voice / mcp-go=embedding)的配置请求。
|
||||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding, contract.ConfigKindVoice} {
|
||||||
k := kind
|
k := kind
|
||||||
if _, err := bus.ServeConfig(k, func() *contract.ModelConfig {
|
if _, err := bus.ServeConfig(k, func() *contract.ModelConfig {
|
||||||
return db.ActiveConfig(context.Background(), k) // chat 含 Fallbacks(其它模型作备用)
|
return db.ActiveConfig(context.Background(), k) // chat 含 Fallbacks(其它模型作备用)
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ func (h *Handler) TestModel(c *gin.Context) {
|
|||||||
// broadcastActive 重新广播各 kind 当前激活配置,触发对应消费方热更新。
|
// broadcastActive 重新广播各 kind 当前激活配置,触发对应消费方热更新。
|
||||||
// chat 配置带 Fallbacks(其它已登记 chat 模型作备用),dispatcher 据此重建 failover 链。
|
// chat 配置带 Fallbacks(其它已登记 chat 模型作备用),dispatcher 据此重建 failover 链。
|
||||||
func (h *Handler) broadcastActive(ctx context.Context) {
|
func (h *Handler) broadcastActive(ctx context.Context) {
|
||||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding, contract.ConfigKindVoice} {
|
||||||
if cfg := h.db.ActiveConfig(ctx, kind); cfg != nil {
|
if cfg := h.db.ActiveConfig(ctx, kind); cfg != nil {
|
||||||
// 广播失败 = dispatcher/mcp-go 拿不到新配置,症状是"控制台改了模型却不生效",
|
// 广播失败 = dispatcher/mcp-go 拿不到新配置,症状是"控制台改了模型却不生效",
|
||||||
// 而改配置的人这边一切正常。必须留痕,否则只能靠猜。
|
// 而改配置的人这边一切正常。必须留痕,否则只能靠猜。
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func (s *voiceSession) submitVoiceTask(transcript, graphOverride string) (string
|
|||||||
task.Meta[contract.MetaUserID] = s.uid
|
task.Meta[contract.MetaUserID] = s.uid
|
||||||
task.Meta[contract.MetaTenantID] = billingTenant
|
task.Meta[contract.MetaTenantID] = billingTenant
|
||||||
task.Meta[contract.MetaSessionID] = s.sessionID
|
task.Meta[contract.MetaSessionID] = s.sessionID
|
||||||
|
task.Meta[contract.MetaModelProfile] = contract.ModelProfileVoice // 语音任务走 JARVIS 快模型(未配则回落工作模型)
|
||||||
|
|
||||||
if err := s.h.launchCore(ctx, s.uid, task); err != nil {
|
if err := s.h.launchCore(ctx, s.uid, task); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -153,8 +153,14 @@ const (
|
|||||||
|
|
||||||
// 配置控制面按 kind 寻址:sundynix.config.<kind>.get / .updated。
|
// 配置控制面按 kind 寻址:sundynix.config.<kind>.get / .updated。
|
||||||
// Gateway 持有配置,消费方(Dispatcher/mcp-go)经 NATS 取用/订阅变更。
|
// Gateway 持有配置,消费方(Dispatcher/mcp-go)经 NATS 取用/订阅变更。
|
||||||
ConfigKindChat = "chat" // 对话模型(Dispatcher 用)
|
ConfigKindChat = "chat" // 工作主力对话模型(Dispatcher 默认用,要强)
|
||||||
ConfigKindEmbedding = "embedding" // 向量模型(mcp-go RAG 用)
|
ConfigKindEmbedding = "embedding" // 向量模型(mcp-go RAG 用)
|
||||||
|
ConfigKindVoice = "voice" // JARVIS 语音对话模型(Dispatcher 给语音任务用,要快/低时延)
|
||||||
|
|
||||||
|
// MetaModelProfile 指定该任务用哪档模型:为 ModelProfileVoice 时 Dispatcher 走语音模型池
|
||||||
|
// (未配置语音模型则透明回落工作模型)。语音任务由网关置此标记,与 intent==report 同类路由。
|
||||||
|
MetaModelProfile = "model_profile"
|
||||||
|
ModelProfileVoice = "voice"
|
||||||
|
|
||||||
// 报告生成:Task.Meta[MetaIntent]==IntentReport 时,Dispatcher 走专用多步编排
|
// 报告生成:Task.Meta[MetaIntent]==IntentReport 时,Dispatcher 走专用多步编排
|
||||||
// (规划大纲 → 各章节并行检索+撰写 → 汇聚 → 渲染 Word),而非通用对话图。
|
// (规划大纲 → 各章节并行检索+撰写 → 汇聚 → 渲染 Word),而非通用对话图。
|
||||||
|
|||||||
Reference in New Issue
Block a user