Files
smartmate/backend/newAgent/node/deliver.go
Losita bf1f1defa5 Version: 0.9.14.dev.260410
后端:
  1. LLM 客户端从 newAgent/llm 提升为 infra/llm 基础设施层
     - 删除 backend/newAgent/llm/(ark.go / ark_adapter.go / client.go / json.go)
     - 等价迁移至 backend/infra/llm/,所有 newAgent node 与 service 统一改引用 infrallm
     - 消除 newAgent 对模型客户端的私有依赖,为 memory / websearch 等多模块复用铺路
  2. RAG 基础设施完成可运行态接入(factory / runtime / observer / service 四层成型)
     - 新建 backend/infra/rag/factory.go / runtime.go / observe.go / observer.go /
  service.go:工厂创建、运行时生命周期、轻量观测接口、检索服务门面
     - 更新 infra/rag/config/config.go:补齐 Milvus / Embed / Reranker 全部配置项与默认值
     - 更新 infra/rag/embed/eino_embedder.go:增强 Eino embedding 适配,支持 BaseURL / APIKey 环境变量 / 超时 /
  维度等参数
     - 更新 infra/rag/store/milvus_store.go:完整实现 Milvus 向量存储(建集合 / 建 Index / Upsert / Search /
  Delete),支持 COSINE / L2 / IP 度量
     - 更新 infra/rag/core/pipeline.go:适配 Runtime 接口,Pipeline 由 factory 注入而非手动拼装
     - 更新 infra/rag/corpus/memory_corpus.go / vector_store.go:对接 Memory 模块数据源与 Store 接口扩展
  3. Memory 模块从 Day1 骨架升级为 Day2 完整可运行态
     - 新建 memory/module.go:统一门面 Module,对外封装 EnqueueExtract / ReadService / ManageService / WithTx /
  StartWorker,启动层只依赖这一个入口
     - 新建 memory/orchestrator/llm_write_orchestrator.go:LLM 驱动的记忆抽取编排器,替代原 mock 抽取
     - 新建 memory/service/read_service.go:按用户开关过滤 + 轻量重排 + 访问时间刷新的读取链路
     - 新建 memory/service/manage_service.go:记忆管理面能力(列出 / 软删除 / 开关读写),删除同步写审计日志
     - 新建 memory/service/common.go:服务层公共工具
     - 新建 memory/worker/loop.go:后台轮询循环 RunPollingLoop,定时抢占 pending 任务并推进
     - 新建 memory/utils/audit.go / settings.go:审计日志构造、用户设置过滤等纯函数
     - 更新 memory/model/item.go / job.go / settings.go / config.go / status.go:补齐 DTO 字段与状态常量
     - 更新 memory/repo/item_repo.go / job_repo.go / audit_repo.go / settings_repo.go:补齐 CRUD 与查询能力
     - 更新 memory/worker/runner.go:Runner 对接 Module 与 LLM 抽取器,任务状态机完整化
     - 更新 memory/README.md:同步模块现状说明
  4. newAgent 接入 Memory 读取注入与工具注册依赖预埋
     - 新建 service/agentsvc/agent_memory.go:定义 MemoryReader 接口 + injectMemoryContext,在 graph
  执行前统一补充记忆上下文
     - 更新 service/agentsvc/agent.go:新增 memoryReader 字段与 SetMemoryReader 方法
     - 更新 service/agentsvc/agent_newagent.go:调用 injectMemoryContext 注入 pinned block,检索失败仅降级不阻断主链路
     - 更新 newAgent/tools/registry.go:新增 DefaultRegistryDeps(含 RAGRuntime),工具注册表支持依赖注入
  5. 启动流程与事件处理器接线更新
     - 更新 cmd/start.go:初始化 RAG Runtime → Memory Module → 注册事件处理器 → 启动 Worker 后台轮询
     - 更新 service/events/memory_extract_requested.go:改用 memory.Module.WithTx(tx) 统一门面,事件处理器不再直接依赖
  repo/service 内部包
  6. 缓存插件与配置同步
     - 更新 middleware/cache_deleter.go:静默忽略 MemoryJob / MemoryItem / MemoryAuditLog / MemoryUserSetting
  等新模型,避免日志刷屏;清理冗余注释
     - 更新 config.example.yaml:补齐 rag / memory / websearch 配置段及默认值
     - 更新 go.mod / go.sum:新增 eino-ext/openai / json-patch / go-openai 依赖
  前端:无 仓库:无
2026-04-10 23:17:38 +08:00

224 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagentnode
import (
"context"
"fmt"
"strings"
"time"
"github.com/cloudwego/eino/schema"
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
)
const (
deliverStageName = "deliver"
deliverStatusBlockID = "deliver.status"
deliverSpeakBlockID = "deliver.speak"
)
// DeliverNodeInput 描述交付节点单轮运行所需的最小依赖。
//
// 职责边界:
// 1. 只负责生成交付总结并推送给用户,不负责后续流程推进;
// 2. RuntimeState 提供计划步骤和执行状态;
// 3. ConversationContext 提供执行阶段的对话历史;
// 4. 交付完成后标记流程结束。
type DeliverNodeInput struct {
RuntimeState *newagentmodel.AgentRuntimeState
ConversationContext *newagentmodel.ConversationContext
Client *infrallm.Client
ChunkEmitter *newagentstream.ChunkEmitter
}
// RunDeliverNode 执行一轮交付节点逻辑。
//
// 核心职责:
// 1. 调 LLM 基于原始计划 + 执行历史生成交付总结;
// 2. 伪流式推送总结给用户;
// 3. 写入对话历史,保证上下文连续;
// 4. 标记流程结束。
//
// 降级策略:
// 1. LLM 调用失败时,回退到机械格式化总结,不中断流程;
// 2. 机械总结包含计划步骤列表和完成进度。
func RunDeliverNode(ctx context.Context, input DeliverNodeInput) error {
runtimeState, conversationContext, emitter, err := prepareDeliverNodeInput(input)
if err != nil {
return err
}
flowState := runtimeState.EnsureCommonState()
// 1. 推送交付阶段状态,让前端知道正在生成总结。
if err := emitter.EmitStatus(
deliverStatusBlockID,
deliverStageName,
"summarizing",
"正在生成交付总结。",
false,
); err != nil {
return fmt.Errorf("交付阶段状态推送失败: %w", err)
}
// 2. 调 LLM 生成交付总结。
summary := generateDeliverSummary(ctx, input.Client, flowState, conversationContext)
// 3. 伪流式推送总结。
if strings.TrimSpace(summary) != "" {
if err := emitter.EmitPseudoAssistantText(
ctx,
deliverSpeakBlockID,
deliverStageName,
summary,
newagentstream.DefaultPseudoStreamOptions(),
); err != nil {
return fmt.Errorf("交付总结推送失败: %w", err)
}
conversationContext.AppendHistory(schema.AssistantMessage(summary, nil))
}
// 4. 推送最终完成状态。
_ = emitter.EmitStatus(
deliverStatusBlockID,
deliverStageName,
"done",
"本轮流程已结束。",
true,
)
return nil
}
// generateDeliverSummary 尝试调用 LLM 生成交付总结,失败时降级到机械格式化。
func generateDeliverSummary(
ctx context.Context,
client *infrallm.Client,
flowState *newagentmodel.CommonState,
conversationContext *newagentmodel.ConversationContext,
) string {
if flowState != nil {
switch {
case flowState.IsAborted():
return normalizeSpeak(buildAbortSummary(flowState))
case flowState.IsExhaustedTerminal():
return normalizeSpeak(buildExhaustedSummary(flowState))
}
}
if client == nil {
return buildMechanicalSummary(flowState)
}
messages := newagentprompt.BuildDeliverMessages(flowState, conversationContext)
result, err := client.GenerateText(
ctx,
messages,
infrallm.GenerateOptions{
Temperature: 0.5,
MaxTokens: 800,
Thinking: infrallm.ThinkingModeDisabled,
Metadata: map[string]any{
"stage": deliverStageName,
},
},
)
if err != nil || result == nil || strings.TrimSpace(result.Text) == "" {
return buildMechanicalSummary(flowState)
}
return normalizeSpeak(result.Text)
}
// buildAbortSummary 生成“流程已终止”的统一交付文案。
//
// 说明:
// 1. 第二轮开始abort 的用户可见文案由终止方提前写入 CommonState
// 2. deliver 不再重新猜测或改写业务异常,只做最终收口;
// 3. 若历史快照缺失 user_message则回退到一份通用说明避免前端收到空白结果。
func buildAbortSummary(state *newagentmodel.CommonState) string {
if state == nil || state.TerminalOutcome == nil {
return "本轮流程已终止。"
}
if msg := strings.TrimSpace(state.TerminalOutcome.UserMessage); msg != "" {
return msg
}
return "本轮流程已终止,请根据当前提示检查后再继续。"
}
// buildExhaustedSummary 生成“轮次耗尽”的统一收口文案。
func buildExhaustedSummary(state *newagentmodel.CommonState) string {
if state == nil {
return "本轮执行已达到安全轮次上限,当前先停止继续操作。"
}
prefix := "本轮执行已达到安全轮次上限,当前先停止继续操作。"
if state.TerminalOutcome != nil && strings.TrimSpace(state.TerminalOutcome.UserMessage) != "" {
prefix = strings.TrimSpace(state.TerminalOutcome.UserMessage)
}
if !state.HasPlan() {
return prefix
}
return prefix + "\n\n" + strings.TrimSpace(buildMechanicalSummary(state))
}
// buildMechanicalSummary 在 LLM 不可用时,机械拼接一份最小可用总结。
func buildMechanicalSummary(state *newagentmodel.CommonState) string {
if state == nil {
return "任务流程已结束。"
}
var sb strings.Builder
current, total := state.PlanProgress()
if !state.HasPlan() {
return "任务流程已结束。"
}
if state.IsExhaustedTerminal() {
sb.WriteString(fmt.Sprintf("任务因执行轮次耗尽提前结束,已完成 %d/%d 步。\n", current, total))
} else {
sb.WriteString("所有计划步骤已执行完毕。\n")
}
sb.WriteString("\n执行情况\n")
for i, step := range state.PlanSteps {
marker := "[ ]"
if i < current {
marker = "[x]"
}
sb.WriteString(fmt.Sprintf("%s %s\n", marker, strings.TrimSpace(step.Content)))
}
if state.IsExhaustedTerminal() && current < total {
sb.WriteString("\n如需继续完成剩余步骤可以告诉我继续。")
}
return sb.String()
}
// prepareDeliverNodeInput 校验并准备交付节点的运行态依赖。
func prepareDeliverNodeInput(input DeliverNodeInput) (
*newagentmodel.AgentRuntimeState,
*newagentmodel.ConversationContext,
*newagentstream.ChunkEmitter,
error,
) {
if input.RuntimeState == nil {
return nil, nil, nil, fmt.Errorf("deliver node: runtime state 不能为空")
}
input.RuntimeState.EnsureCommonState()
if input.ConversationContext == nil {
input.ConversationContext = newagentmodel.NewConversationContext("")
}
if input.ChunkEmitter == nil {
input.ChunkEmitter = newagentstream.NewChunkEmitter(
newagentstream.NoopPayloadEmitter(), "", "", time.Now().Unix(),
)
}
return input.RuntimeState, input.ConversationContext, input.ChunkEmitter, nil
}