AgentFactory.java 5.06 KB
package com.xly.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.AgentIdentity;
import com.xly.agent.EventLogChatMemory;
import com.xly.agent.ReActAgent;
import com.xly.service.ErpClient;
import com.xly.service.EventProjectionService;
import com.xly.service.FormRenderService;
import com.xly.service.FormResolverService;
import com.xly.service.LedgerService;
import com.xly.service.OpService;
import com.xly.service.PreviewService;
import com.xly.service.SkillService;
import com.xly.service.SystemPromptService;
import com.xly.tool.ErpReadTool;
import com.xly.tool.FormCollectTool;
import com.xly.tool.InteractionTool;
import com.xly.tool.KgQueryTool;
import com.xly.tool.PreviewChangeTool;
import com.xly.tool.UseSkillTool;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.service.AiServices;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
 * 按「每次请求的身份」组装单一 ReAct agent(架构 §5/§7 的 per-call context)。
 *
 * <p>为什么按请求新建:LangChain4j 流式工具执行发生在模型 HTTP 回调线程,而非控制器工作线程,
 * ThreadLocal 不可靠。于是把 {@link AgentIdentity}(透传 token + 权限集)直接注入到**每请求新建**的
 * 工具实例里(ErpReadTool/PreviewChangeTool/FormCollectTool),从根上保证鉴权与 token 正确。
 *
 * <p>固定 7 工具:useSkill/findForms/readFormData/lookupRecord/collectForm/previewChange/askUser。
 * **模型可用的工具全部只读/只渲染**(rearch3 §1/§3):写路径 = 预览卡/表单 → 用户点按钮 →
 * 确定性端点写 ai_op_queue,模型碰不到任何写入口。
 * 无状态、全局的工具({@link KgQueryTool}、{@link InteractionTool})是单例、跨请求复用。
 */
@Component
public class AgentFactory {

    private final StreamingChatModel streamingModel;
    private final LedgerService ledger;
    private final EventProjectionService projection;
    private final SystemPromptService systemPromptService;

    private final ErpClient erp;
    private final FormResolverService resolver;
    private final FormRenderService render;
    private final PreviewService previews;
    private final ObjectMapper mapper;

    private final KgQueryTool kgQueryTool;
    private final InteractionTool interactionTool;
    private final SkillService skillService;

    public AgentFactory(@Qualifier("agentStreamingModel") StreamingChatModel streamingModel,
                        LedgerService ledger, EventProjectionService projection,
                        SystemPromptService systemPromptService,
                        ErpClient erp, FormResolverService resolver, FormRenderService render,
                        PreviewService previews, OpService ops,
                        ObjectMapper mapper, KgQueryTool kgQueryTool, InteractionTool interactionTool,
                        SkillService skillService) {
        this.streamingModel = streamingModel;
        this.ledger = ledger;
        this.projection = projection;
        this.systemPromptService = systemPromptService;
        this.erp = erp;
        this.resolver = resolver;
        this.render = render;
        this.previews = previews;
        this.mapper = mapper;
        this.kgQueryTool = kgQueryTool;
        this.interactionTool = interactionTool;
        this.skillService = skillService;
        // 流程卡的「已提交待办」行读 ai_op_queue.sStatus 只读进度(rearch3 §3)
        projection.setOpStatusLookup(ops::statusLabel);
    }

    /**
     * 组装 ReAct agent:固定 7 工具 + 唯一 system prompt。
     * {@code maxSequentialToolsInvocations} 作为循环护栏,杜绝失控 ReAct 循环。
     *
     * @param convId           会话 id(useSkill/previewChange 落事件需要)
     * @param internalUserTurn true=本次的用户消息是系统注入话术(护栏重试),落账时带 internal 标记,
     *                         前端历史不显示
     */
    public ReActAgent build(AgentIdentity identity, String convId, boolean internalUserTurn) {
        Object[] tools = new Object[]{kgQueryTool, interactionTool,
                new UseSkillTool(skillService, ledger, convId),
                new ErpReadTool(erp, resolver, identity),
                new PreviewChangeTool(previews, identity, convId),
                new FormCollectTool(render, resolver, identity, mapper)};

        return AiServices.builder(ReActAgent.class)
                .streamingChatModel(streamingModel)
                .tools(tools)
                .maxSequentialToolsInvocations(8) // 循环护栏:防止 askUser/工具无限自我循环
                // 事件账本记忆:逐条 append 原子落账,读取为四段式 token 记账投影(见 EventLogChatMemory)
                .chatMemoryProvider(memoryId -> new EventLogChatMemory(
                        String.valueOf(memoryId), ledger, projection, identity, internalUserTurn))
                .systemMessageProvider(memoryId -> systemPromptService.prompt())
                .build();
    }
}