From 46b342655cba9335f98e25b13f83e24ce8bba2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=8B=E9=A3=8E?= Date: Wed, 4 Feb 2026 13:25:50 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=89=88=E6=9C=AC=E8=87=B30.1.3=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E8=BF=BD=E8=B8=AA?= =?UTF-8?q?=E5=92=8C=E5=8F=AF=E8=A7=82=E6=B5=8B=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assistant-agent-autoconfigure/pom.xml | 3 +- .../agent/autoconfigure/CodeactAgent.java | 166 ++++-- .../subagent/BaseAgentTaskTool.java | 76 +-- .../subagent/CodeactSubAgentInterceptor.java | 102 +--- .../subagent/filter/CodeactToolFilter.java | 218 -------- .../subagent/filter/WhitelistMode.java | 57 -- .../subagent/node/CodeGeneratorNode.java | 39 +- assistant-agent-common/pom.xml | 2 +- .../common/constant/CodeactStateKeys.java | 69 --- .../agent/common/tools/CodeactTool.java | 14 +- .../tools/definition/ParameterTree.java | 44 +- assistant-agent-core/pom.xml | 12 +- .../core/executor/GraalCodeExecutor.java | 252 ++++++++- .../agent/core/model/ExecutionRecord.java | 28 + .../agent/core/model/ToolCallRecord.java | 69 +++ ...BaseAgentObservationLifecycleListener.java | 506 +++++++++++++++++ .../CodeactObservationDocumentation.java | 248 +++++++++ .../observation/DefaultObservationState.java | 112 ++++ .../observation/HookObservationHelper.java | 303 ++++++++++ .../InterceptorObservationHelper.java | 186 +++++++ .../core/observation/ObservationState.java | 130 +++++ .../OpenTelemetryObservationHelper.java | 393 +++++++++++++ .../CodeGenerationObservationContext.java | 185 +++++++ .../CodeactExecutionObservationContext.java | 178 ++++++ .../CodeactToolCallObservationContext.java | 190 +++++++ .../context/HookObservationContext.java | 267 +++++++++ .../InterceptorObservationContext.java | 389 +++++++++++++ .../context/ReactPhaseObservationContext.java | 521 ++++++++++++++++++ .../agent/core/tool/ToolRegistryBridge.java | 52 ++ .../tool/view/PythonToolViewRenderer.java | 82 ++- assistant-agent-evaluation/pom.xml | 12 +- .../evaluation/DefaultEvaluationService.java | 63 ++- .../agent/evaluation/EvaluationService.java | 29 + .../builder/EvaluationSuiteBuilder.java | 71 ++- .../evaluator/LLMBasedEvaluator.java | 3 + .../executor/CriterionEvaluationAction.java | 5 + .../GraphBasedEvaluationExecutor.java | 27 +- .../evaluation/model/CriterionResult.java | 13 + ...valuationObservationLifecycleListener.java | 363 ++++++++++++ assistant-agent-extensions/pom.xml | 2 +- .../dynamic/mcp/McpDynamicToolFactory.java | 8 +- .../mcp/McpServerAwareToolCallback.java | 2 +- .../dynamic/naming/NameNormalizer.java | 25 +- .../ExperienceExtensionAutoConfiguration.java | 18 + .../experience/hook/FastIntentReactHook.java | 5 +- .../tool/CommonSenseInjectionTool.java | 36 ++ .../LearningExtensionAutoConfiguration.java | 24 +- .../learning/hook/AfterAgentLearningHook.java | 70 ++- .../internal/AsyncLearningHandler.java | 37 +- .../reply/tools/BaseReplyCodeactTool.java | 14 +- .../reply/tools/ReplyCodeactToolFactory.java | 65 ++- .../tools/UnifiedSearchCodeactTool.java | 8 +- assistant-agent-prompt-builder/pom.xml | 2 +- assistant-agent-start/pom.xml | 9 +- ...ExperienceEvaluationCriterionProvider.java | 12 +- pom.xml | 14 +- 56 files changed, 5120 insertions(+), 710 deletions(-) delete mode 100644 assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/CodeactToolFilter.java delete mode 100644 assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/WhitelistMode.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ToolCallRecord.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/BaseAgentObservationLifecycleListener.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/CodeactObservationDocumentation.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/DefaultObservationState.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/HookObservationHelper.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/InterceptorObservationHelper.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/ObservationState.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/OpenTelemetryObservationHelper.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeGenerationObservationContext.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactExecutionObservationContext.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactToolCallObservationContext.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/HookObservationContext.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/InterceptorObservationContext.java create mode 100644 assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/ReactPhaseObservationContext.java create mode 100644 assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/observation/EvaluationObservationLifecycleListener.java create mode 100644 assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/CommonSenseInjectionTool.java diff --git a/assistant-agent-autoconfigure/pom.xml b/assistant-agent-autoconfigure/pom.xml index 467a811..68c242e 100644 --- a/assistant-agent-autoconfigure/pom.xml +++ b/assistant-agent-autoconfigure/pom.xml @@ -6,11 +6,12 @@ com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 assistant-agent-autoconfigure + 17 17 diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/CodeactAgent.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/CodeactAgent.java index 881c68d..45f70fc 100644 --- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/CodeactAgent.java +++ b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/CodeactAgent.java @@ -175,7 +175,7 @@ public static class CodeactAgentBuilder extends Builder { // ReturnSchemaRegistry (进程内单例) private ReturnSchemaRegistry returnSchemaRegistry; - // ToolRegistryBridgeFactory (用于自定义 ToolRegistryBridge) + // ToolRegistryBridgeFactory (用于自定义工具调用桥接,如可观测性) private ToolRegistryBridgeFactory toolRegistryBridgeFactory; // CodeactTool support (新机制) @@ -195,11 +195,8 @@ public static class CodeactAgentBuilder extends Builder { // Keep reference to ChatModel for code generation private ChatModel chatModel; - // SubAgent system prompt (for Codeact phase code generation) - private String subAgentSystemPrompt; - - // state keys to propagate from parent agent (react phase) to child agent (codeact phase) - private List stateKeysToPropagate = new ArrayList<>(); + // GraphLifecycleListeners (用于可观测性) + private List lifecycleListeners = new ArrayList<>(); public CodeactAgentBuilder() { super(); @@ -248,22 +245,6 @@ public CodeactAgentBuilder subAgentHooks(List hooks) { return this; } - /** - * Set state keys to propagate from parent agent (react phase) to child agent (codeact phase) - */ - public CodeactAgentBuilder stateKeysToPropagate(List keys) { - this.stateKeysToPropagate = keys != null ? new ArrayList<>(keys) : new ArrayList<>(); - return this; - } - - /** - * Set state keys to propagate from parent agent (react phase) to child agent (codeact phase), in variable argument form - */ - public CodeactAgentBuilder stateKeysToPropagate(String... keys) { - this.stateKeysToPropagate = new ArrayList<>(Arrays.asList(keys)); - return this; - } - public CodeactAgentBuilder experienceProvider(ExperienceProvider experienceProvider) { this.experienceProvider = experienceProvider; return this; @@ -279,14 +260,6 @@ public CodeactAgentBuilder fastIntentService(FastIntentService fastIntentService return this; } - /** - * Set custom system prompt for the Codeact sub-agent (code generation phase). - */ - public CodeactAgentBuilder subAgentSystemPrompt(String systemPrompt) { - this.subAgentSystemPrompt = systemPrompt; - return this; - } - /** * Enable/disable initial code generation hook */ @@ -364,8 +337,8 @@ public CodeactAgentBuilder returnSchemaRegistry(ReturnSchemaRegistry registry) { /** * Set the ToolRegistryBridgeFactory for customizing ToolRegistryBridge creation. * - *

If not set, the default factory will be used which creates standard - * ToolRegistryBridge instances. + *

可用于添加可观测性、任务记录等功能。 + * 如果不设置,将使用默认的 ToolRegistryBridge。 * * @param factory the ToolRegistryBridgeFactory to use * @return CodeactAgentBuilder instance for chaining @@ -375,6 +348,35 @@ public CodeactAgentBuilder toolRegistryBridgeFactory(ToolRegistryBridgeFactory f return this; } + /** + * Add a GraphLifecycleListener for observability. + * + *

用于监听 Agent Graph 执行的关键阶段,如 React 阶段开始/结束、节点执行前后等。 + * 可以通过此接口实现日志记录、指标收集、分布式追踪等可观测性功能。 + * + * @param listener the GraphLifecycleListener to add + * @return CodeactAgentBuilder instance for chaining + */ + public CodeactAgentBuilder lifecycleListener(com.alibaba.cloud.ai.graph.GraphLifecycleListener listener) { + if (listener != null) { + this.lifecycleListeners.add(listener); + } + return this; + } + + /** + * Add multiple GraphLifecycleListeners for observability. + * + * @param listeners the list of GraphLifecycleListeners to add + * @return CodeactAgentBuilder instance for chaining + */ + public CodeactAgentBuilder lifecycleListeners(List listeners) { + if (listeners != null) { + this.lifecycleListeners.addAll(listeners); + } + return this; + } + /** * Set the model name for code generation * For example: "qwen-coder-plus", "qwen-max", etc. @@ -507,6 +509,55 @@ public CodeactAgentBuilder methodTools(Object... toolObjects) { return this; } + /** + * Override buildConfig to include lifecycleListeners for observability. + * + * @return CompileConfig with lifecycleListeners included + */ + @Override + protected CompileConfig buildConfig() { + // If compileConfig is already set, use it + if (compileConfig != null) { + // Add additional lifecycleListeners to existing config + if (!lifecycleListeners.isEmpty()) { + for (com.alibaba.cloud.ai.graph.GraphLifecycleListener listener : lifecycleListeners) { + compileConfig.lifecycleListeners().offer(listener); + } + logger.info("CodeactAgentBuilder#buildConfig - reason=添加LifecycleListeners到已有CompileConfig, count={}", + lifecycleListeners.size()); + } + return compileConfig; + } + + // Build new CompileConfig with saver and lifecycleListeners + com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig saverConfig = + com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig.builder() + .register(saver) + .build(); + + CompileConfig.Builder builder = CompileConfig.builder() + .saverConfig(saverConfig) + .recursionLimit(Integer.MAX_VALUE) + .releaseThread(releaseThread); + + // Add ObservationRegistry if available + if (observationRegistry != null) { + builder.observationRegistry(observationRegistry); + } + + // Add all lifecycleListeners + for (com.alibaba.cloud.ai.graph.GraphLifecycleListener listener : lifecycleListeners) { + builder.withLifecycleListener(listener); + } + + if (!lifecycleListeners.isEmpty()) { + logger.info("CodeactAgentBuilder#buildConfig - reason=创建带有LifecycleListeners的CompileConfig, count={}", + lifecycleListeners.size()); + } + + return builder.build(); + } + /** * Build the CodeactAgent */ @@ -562,7 +613,7 @@ public CodeactAgent build() { null, // Will be set by ReactAgent new OverAllState(), // Placeholder this.codeactToolRegistry, // Pass CodeactTool registry - this.toolRegistryBridgeFactory, // Pass custom factory (null will use default) + this.toolRegistryBridgeFactory, // Pass ToolRegistryBridgeFactory for observability this.allowIO, this.allowNativeAccess, this.executionTimeoutMs @@ -684,6 +735,27 @@ public CodeactAgent build() { if (!allTools.isEmpty()) { toolBuilder.toolCallbacks(allTools); } + + // Set toolContext if available (like DefaultBuilder does) + if (toolContext != null && !toolContext.isEmpty()) { + toolBuilder.toolContext(toolContext); + } + + // Enable logging if enabled + if (enableLogging) { + toolBuilder.enableActingLog(true); + } + + // Set exception processor (like DefaultBuilder does) + if (toolExecutionExceptionProcessor == null) { + toolBuilder.toolExecutionExceptionProcessor( + org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor.builder() + .alwaysThrow(false) + .build()); + } else { + toolBuilder.toolExecutionExceptionProcessor(toolExecutionExceptionProcessor); + } + llmNode.setInstruction(instruction); AgentToolNode toolNode = toolBuilder.build(); @@ -923,21 +995,19 @@ private ModelInterceptor createCodeactSubAgentInterceptor() { } return CodeactSubAgentInterceptor.builder() - .defaultModel(codeGenModel) - .defaultCodeactTools(this.codeactTools) - .defaultLanguage(language) - .codeContext(this.codeContext) - .environmentManager(this.environmentManager) - .experienceProvider(this.experienceProvider) - .experienceExtensionProperties(this.experienceExtensionProperties) - .fastIntentService(this.fastIntentService) - .includeDefaultCodeGenerator(true) // 使用默认代码生成器 - .hooks(this.subAgentHooks) // Pass sub-agent hooks - .stateKeysToPropagate(this.stateKeysToPropagate) // 传递需要跨 agent 传递的 state keys - .returnSchemaRegistry(this.codeactToolRegistry != null ? - this.codeactToolRegistry.getReturnSchemaRegistry() : null) - .subAgentSystemPrompt(this.subAgentSystemPrompt) - .build(); + .defaultModel(codeGenModel) + .defaultCodeactTools(this.codeactTools) + .defaultLanguage(language) + .codeContext(this.codeContext) + .environmentManager(this.environmentManager) + .experienceProvider(this.experienceProvider) + .experienceExtensionProperties(this.experienceExtensionProperties) + .fastIntentService(this.fastIntentService) + .includeDefaultCodeGenerator(true) // 使用默认代码生成器 + .hooks(this.subAgentHooks) // Pass sub-agent hooks + .returnSchemaRegistry(this.codeactToolRegistry != null ? + this.codeactToolRegistry.getReturnSchemaRegistry() : null) + .build(); } } } diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/BaseAgentTaskTool.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/BaseAgentTaskTool.java index a489436..a854c9c 100644 --- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/BaseAgentTaskTool.java +++ b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/BaseAgentTaskTool.java @@ -18,17 +18,13 @@ import com.alibaba.cloud.ai.graph.CompiledGraph; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.agent.BaseAgent; -import com.alibaba.cloud.ai.graph.agent.tools.ToolContextConstants; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ToolContext; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; @@ -41,7 +37,6 @@ *

  • 支持 Map<String, BaseAgent> 而不是 Map<String, ReactAgent>
  • *
  • 通过 CompiledGraph.invoke() 调用而不是 agent.call()
  • *
  • 从 OverAllState 中提取结果
  • - *
  • 支持配置 stateKeysToPropagate,将父 agent 的指定 state 传递给子 agent
  • * * * @author Assistant Agent Team @@ -53,22 +48,8 @@ public class BaseAgentTaskTool implements BiFunction subAgents; - /** - * 需要从父 agent 的 OverAllState 传递给子 agent 的 state keys 列表。 - *

    - * 这是一个通用扩展点,允许业务方配置需要跨 agent 传递的状态。 - */ - private final List stateKeysToPropagate; - public BaseAgentTaskTool(Map subAgents) { - this(subAgents, Collections.emptyList()); - } - - public BaseAgentTaskTool(Map subAgents, List stateKeysToPropagate) { this.subAgents = subAgents; - this.stateKeysToPropagate = stateKeysToPropagate != null - ? new ArrayList<>(stateKeysToPropagate) - : Collections.emptyList(); } @Override @@ -90,25 +71,22 @@ public String apply(TaskRequest request, ToolContext toolContext) { // 3. Build inputs from description or structured inputs Map inputs; if (request.structuredInputs != null && !request.structuredInputs.isEmpty()) { - inputs = new HashMap<>(request.structuredInputs); + inputs = request.structuredInputs; logger.info("BaseAgentTaskTool#apply 使用结构化输入: {}", inputs.keySet()); } else { inputs = buildInputsFromDescription(request.description); logger.info("BaseAgentTaskTool#apply 从描述解析输入: {}", inputs.keySet()); } - // 4. Propagate configured state keys from parent agent - propagateParentStateKeys(toolContext, inputs); - - // 5. Invoke the subagent through CompiledGraph + // 4. Invoke the subagent through CompiledGraph CompiledGraph compiledGraph = subAgent.getAndCompileGraph(); Optional resultOpt = compiledGraph.invoke(inputs); - // 6. Extract result from state + // 5. Extract result from state OverAllState resultState = resultOpt.orElseThrow(() -> new IllegalStateException("SubAgent returned empty result")); - // 7. Try to get generated_code or any string result + // 6. Try to get generated_code or any string result String result = resultState.value("generated_code", String.class) .orElseGet(() -> extractAnyStringResult(resultState)); @@ -122,52 +100,6 @@ public String apply(TaskRequest request, ToolContext toolContext) { } } - /** - * 从父 agent 的 OverAllState 中提取配置的 keys,传递给子 agent。 - *

    - * 这是一个通用扩展点,允许业务方将父 agent 的状态(如评估结果)传递给子 agent。 - * - * @param toolContext 工具上下文,包含父 agent 的 OverAllState - * @param inputs 子 agent 的输入 Map,将被追加父 agent 的状态 - */ - private void propagateParentStateKeys(ToolContext toolContext, Map inputs) { - if (stateKeysToPropagate.isEmpty()) { - return; - } - - if (toolContext == null || toolContext.getContext() == null) { - logger.debug("BaseAgentTaskTool#propagateParentStateKeys - toolContext 为空,跳过状态传递"); - return; - } - - Object stateObj = toolContext.getContext().get(ToolContextConstants.AGENT_STATE_CONTEXT_KEY); - if (!(stateObj instanceof OverAllState)) { - logger.debug("BaseAgentTaskTool#propagateParentStateKeys - 父 agent state 不存在或类型不匹配,跳过状态传递"); - return; - } - - OverAllState parentState = (OverAllState) stateObj; - int propagatedCount = 0; - - for (String key : stateKeysToPropagate) { - Optional valueOpt = parentState.value(key); - if (valueOpt.isPresent()) { - Object value = valueOpt.get(); - inputs.put(key, value); - propagatedCount++; - logger.debug("BaseAgentTaskTool#propagateParentStateKeys - 传递状态: key={}, valueType={}", - key, value.getClass().getSimpleName()); - } else { - logger.debug("BaseAgentTaskTool#propagateParentStateKeys - 状态 key={} 在父 agent 中不存在,跳过", key); - } - } - - if (propagatedCount > 0) { - logger.info("BaseAgentTaskTool#propagateParentStateKeys - 完成状态传递: propagatedCount={}, keys={}", - propagatedCount, stateKeysToPropagate); - } - } - /** * 从任务描述构建输入参数 * 尝试解析描述中的结构化信息 diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/CodeactSubAgentInterceptor.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/CodeactSubAgentInterceptor.java index b09e8e6..9a49b08 100644 --- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/CodeactSubAgentInterceptor.java +++ b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/CodeactSubAgentInterceptor.java @@ -74,18 +74,6 @@ public class CodeactSubAgentInterceptor extends ModelInterceptor { private final ExperienceExtensionProperties experienceExtensionProperties; private final FastIntentService fastIntentService; - /** - * 需要从父 agent 传递给子 agent 的 state keys。 - * 通过此配置,业务方可以指定需要跨 agent 传递的状态。 - */ - private final List stateKeysToPropagate; - - /** - * 子 Agent 的系统提示词(用于代码生成阶段)。 - * 如果设置了此字段,将传递给 CodeGeneratorSubAgent 用于定制代码生成行为。 - */ - private final String subAgentSystemPrompt; - private CodeactSubAgentInterceptor(Builder builder) { this.systemPrompt = builder.systemPrompt != null ? builder.systemPrompt : DEFAULT_SYSTEM_PROMPT; this.subAgents = new HashMap<>(builder.subAgents); @@ -96,10 +84,6 @@ private CodeactSubAgentInterceptor(Builder builder) { this.experienceProvider = builder.experienceProvider; this.experienceExtensionProperties = builder.experienceExtensionProperties; this.fastIntentService = builder.fastIntentService; - this.stateKeysToPropagate = builder.stateKeysToPropagate != null - ? new ArrayList<>(builder.stateKeysToPropagate) - : Collections.emptyList(); - this.subAgentSystemPrompt = builder.subAgentSystemPrompt; // 添加默认code-generator和condition-code-generator(对标general-purpose) if (includeDefaultCodeGenerator && builder.defaultModel != null) { @@ -111,8 +95,7 @@ private CodeactSubAgentInterceptor(Builder builder) { builder.defaultLanguage, false, // 不是条件判断函数 builder.hooks, - builder.returnSchemaRegistry, - builder.subAgentSystemPrompt // 传递自定义系统提示词 + builder.returnSchemaRegistry ); this.subAgents.put("code-generator", codeGenAgent); @@ -124,8 +107,7 @@ private CodeactSubAgentInterceptor(Builder builder) { builder.defaultLanguage, true, // 是条件判断函数 builder.hooks, - builder.returnSchemaRegistry, - builder.subAgentSystemPrompt + builder.returnSchemaRegistry ); this.subAgents.put("condition-code-generator", conditionCodeGenAgent); @@ -133,12 +115,7 @@ private CodeactSubAgentInterceptor(Builder builder) { } // 创建内部 BaseAgentTaskTool(对标 SubAgentInterceptor 创建 TaskTool) - // 传入 stateKeysToPropagate,支持将父 agent 的状态传递给子 agent - BaseAgentTaskTool taskTool = new BaseAgentTaskTool(this.subAgents, this.stateKeysToPropagate); - - if (!this.stateKeysToPropagate.isEmpty()) { - logger.info("CodeactSubAgentInterceptor# 配置状态传递: keys={}", this.stateKeysToPropagate); - } + BaseAgentTaskTool taskTool = new BaseAgentTaskTool(this.subAgents); // 创建WriteCodeTool和WriteConditionCodeTool(委托给 BaseAgentTaskTool) CodeFastIntentSupport codeFastIntentSupport = @@ -156,15 +133,6 @@ private CodeactSubAgentInterceptor(Builder builder) { /** * 创建默认代码生成子Agent(对标createGeneralPurposeAgent) - * - * @param model ChatModel 实例 - * @param tools CodeactTool 列表 - * @param interceptors 拦截器列表 - * @param language 编程语言 - * @param isCondition 是否为条件判断函数 - * @param hooks Hook 列表 - * @param returnSchemaRegistry 返回值 Schema 注册表 - * @param customSystemPrompt 自定义系统提示词(可选,为 null 时使用默认提示词) */ private BaseAgent createDefaultCodeGeneratorAgent( ChatModel model, @@ -173,8 +141,7 @@ private BaseAgent createDefaultCodeGeneratorAgent( Language language, boolean isCondition, List hooks, - ReturnSchemaRegistry returnSchemaRegistry, - String customSystemPrompt) { + ReturnSchemaRegistry returnSchemaRegistry) { List modelInterceptors = new ArrayList<>(); if (interceptors != null) { @@ -186,7 +153,7 @@ private BaseAgent createDefaultCodeGeneratorAgent( } if (isCondition) { - CodeGeneratorSubAgent.Builder builder = CodeGeneratorSubAgent.builder() + return CodeGeneratorSubAgent.builder() .name("condition-code-generator") .description("Generate condition function code that returns boolean") .chatModel(model) @@ -195,14 +162,10 @@ private BaseAgent createDefaultCodeGeneratorAgent( .modelInterceptors(modelInterceptors) .hooks(hooks) .isCondition(true) - .returnSchemaRegistry(returnSchemaRegistry); - - if (customSystemPrompt != null && !customSystemPrompt.isEmpty()) { - builder.customSystemPrompt(customSystemPrompt); - } - return builder.build(); + .returnSchemaRegistry(returnSchemaRegistry) + .build(); } else { - CodeGeneratorSubAgent.Builder builder = CodeGeneratorSubAgent.builder() + return CodeGeneratorSubAgent.builder() .name("code-generator") .description("Generate function code based on requirements") .chatModel(model) @@ -211,12 +174,8 @@ private BaseAgent createDefaultCodeGeneratorAgent( .modelInterceptors(modelInterceptors) .hooks(hooks) .isCondition(false) - .returnSchemaRegistry(returnSchemaRegistry); - - if (customSystemPrompt != null && !customSystemPrompt.isEmpty()) { - builder.customSystemPrompt(customSystemPrompt); - } - return builder.build(); + .returnSchemaRegistry(returnSchemaRegistry) + .build(); } } @@ -277,28 +236,11 @@ public static class Builder { private ExperienceExtensionProperties experienceExtensionProperties; private FastIntentService fastIntentService; - /** - * 需要从父 agent 传递给子 agent 的 state keys。 - * 通过此配置,业务方可以指定需要跨 agent 传递的状态。 - */ - private List stateKeysToPropagate; - - /** - * 子 Agent 的自定义系统提示词(用于代码生成阶段)。 - * 如果设置了此字段,将传递给 CodeGeneratorSubAgent 用于定制代码生成行为。 - */ - private String subAgentSystemPrompt; - public Builder systemPrompt(String systemPrompt) { this.systemPrompt = systemPrompt; return this; } - public Builder subAgentSystemPrompt(String systemPrompt) { - this.subAgentSystemPrompt = systemPrompt; - return this; - } - public Builder defaultModel(ChatModel model) { this.defaultModel = model; return this; @@ -354,30 +296,6 @@ public Builder hooks(List hooks) { return this; } - /** - * 配置需要从父 agent 传递给子 agent 的 state keys。 - *

    - * 这是一个通用扩展点,允许业务方将父 agent 的状态传递给 Codeact 子 agent。 - * - * @param keys 需要传递的 state key 列表 - * @return this builder - */ - public Builder stateKeysToPropagate(List keys) { - this.stateKeysToPropagate = keys; - return this; - } - - /** - * 配置需要从父 agent 传递给子 agent 的 state keys(可变参数形式)。 - * - * @param keys 需要传递的 state keys - * @return this builder - */ - public Builder stateKeysToPropagate(String... keys) { - this.stateKeysToPropagate = Arrays.asList(keys); - return this; - } - /** * 添加自定义子Agent(对标addSubAgent) */ diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/CodeactToolFilter.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/CodeactToolFilter.java deleted file mode 100644 index 8c8d292..0000000 --- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/CodeactToolFilter.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.alibaba.assistant.agent.autoconfigure.subagent.filter; - -import com.alibaba.assistant.agent.common.constant.CodeactStateKeys; -import com.alibaba.assistant.agent.common.enums.Language; -import com.alibaba.assistant.agent.common.tools.CodeactTool; -import com.alibaba.assistant.agent.common.tools.CodeactToolMetadata; -import com.alibaba.cloud.ai.graph.OverAllState; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * CodeactTool 筛选器 - * - *

    根据 OverAllState 中的白名单配置筛选可用工具。 - * 支持按工具名称和工具组进行筛选,多种白名单模式可选。 - * - *

    使用方式: - *

    {@code
    - * List filtered = CodeactToolFilter.filter(allTools, state, Language.PYTHON);
    - * }
    - * - * @author Assistant Agent Team - * @since 1.0.0 - * @see WhitelistMode - */ -public final class CodeactToolFilter { - - private static final Logger logger = LoggerFactory.getLogger(CodeactToolFilter.class); - - private CodeactToolFilter() { - // 工具类,禁止实例化 - } - - /** - * 根据 OverAllState 中的白名单配置筛选工具 - * - * @param allTools 全部工具列表 - * @param state OverAllState 实例 - * @param language 目标编程语言(用于语言兼容性过滤) - * @return 筛选后的工具列表 - */ - public static List filter(List allTools, OverAllState state, Language language) { - if (allTools == null || allTools.isEmpty()) { - logger.debug("CodeactToolFilter#filter - 输入工具列表为空,返回空列表"); - return Collections.emptyList(); - } - - // 1. 先按语言兼容性过滤 - List languageCompatible = filterByLanguage(allTools, language); - logger.debug("CodeactToolFilter#filter - 语言过滤后工具数: {}/{}", - languageCompatible.size(), allTools.size()); - - // 2. 读取白名单配置 - List toolNameWhitelist = extractStringList(state, CodeactStateKeys.AVAILABLE_TOOL_NAMES); - List toolGroupWhitelist = extractStringList(state, CodeactStateKeys.AVAILABLE_TOOL_GROUPS); - WhitelistMode mode = extractWhitelistMode(state); - - logger.info("CodeactToolFilter#filter - 白名单配置: mode={}, toolNames={}, toolGroups={}", - mode, toolNameWhitelist, toolGroupWhitelist); - - // 3. 如果没有任何白名单配置,返回语言兼容的全部工具 - if (isEmpty(toolNameWhitelist) && isEmpty(toolGroupWhitelist)) { - logger.info("CodeactToolFilter#filter - 无白名单配置,使用全部工具: count={}", - languageCompatible.size()); - return languageCompatible; - } - - // 4. 根据模式执行筛选 - List filtered = applyWhitelist(languageCompatible, toolNameWhitelist, toolGroupWhitelist, mode); - - logger.info("CodeactToolFilter#filter - 筛选完成: input={}, output={}, filteredTools={}", - languageCompatible.size(), filtered.size(), - filtered.stream().map(CodeactTool::getName).collect(Collectors.toList())); - - return filtered; - } - - /** - * 按语言兼容性过滤 - */ - private static List filterByLanguage(List tools, Language language) { - if (language == null) { - return new ArrayList<>(tools); - } - return tools.stream() - .filter(tool -> { - CodeactToolMetadata meta = tool.getCodeactMetadata(); - return meta.supportedLanguages().contains(language); - }) - .collect(Collectors.toList()); - } - - /** - * 应用白名单筛选 - */ - private static List applyWhitelist( - List tools, - List nameWhitelist, - List groupWhitelist, - WhitelistMode mode) { - - Set nameSet = isEmpty(nameWhitelist) ? null : new HashSet<>(nameWhitelist); - Set groupSet = isEmpty(groupWhitelist) ? null : new HashSet<>(groupWhitelist); - - return tools.stream() - .filter(tool -> matchesWhitelist(tool, nameSet, groupSet, mode)) - .collect(Collectors.toList()); - } - - /** - * 判断工具是否匹配白名单 - */ - private static boolean matchesWhitelist( - CodeactTool tool, - Set nameSet, - Set groupSet, - WhitelistMode mode) { - - String toolName = tool.getName(); - String toolGroup = tool.getCodeactMetadata().targetClassName(); - - boolean matchesName = nameSet == null || nameSet.contains(toolName); - boolean matchesGroup = groupSet == null || - (toolGroup != null && groupSet.contains(toolGroup)); - - switch (mode) { - case INTERSECTION: - // 两个白名单都存在时取交集,只有一个时只检查那一个 - if (nameSet != null && groupSet != null) { - return matchesName && matchesGroup; - } else if (nameSet != null) { - return matchesName; - } else if (groupSet != null) { - return matchesGroup; - } - return true; - - case UNION: - // 取并集:满足任一即可 - if (nameSet != null && groupSet != null) { - return matchesName || matchesGroup; - } else if (nameSet != null) { - return matchesName; - } else if (groupSet != null) { - return matchesGroup; - } - return true; - - case NAME_ONLY: - return nameSet == null || matchesName; - - case GROUP_ONLY: - return groupSet == null || matchesGroup; - - default: - return true; - } - } - - /** - * 从 state 提取字符串列表 - */ - @SuppressWarnings("unchecked") - private static List extractStringList(OverAllState state, String key) { - if (state == null) { - return null; - } - return state.value(key, List.class).orElse(null); - } - - /** - * 从 state 提取白名单模式 - */ - private static WhitelistMode extractWhitelistMode(OverAllState state) { - if (state == null) { - return WhitelistMode.INTERSECTION; - } - return state.value(CodeactStateKeys.WHITELIST_MODE, String.class) - .map(s -> { - try { - return WhitelistMode.valueOf(s.toUpperCase()); - } catch (IllegalArgumentException e) { - logger.warn("CodeactToolFilter#extractWhitelistMode - 无效的模式值: {}, 使用默认值", s); - return WhitelistMode.INTERSECTION; - } - }) - .orElse(WhitelistMode.INTERSECTION); - } - - /** - * 判断列表是否为空 - */ - private static boolean isEmpty(List list) { - return list == null || list.isEmpty(); - } -} diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/WhitelistMode.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/WhitelistMode.java deleted file mode 100644 index d7cffe3..0000000 --- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/filter/WhitelistMode.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.alibaba.assistant.agent.autoconfigure.subagent.filter; - -/** - * 工具白名单模式枚举 - * - *

    定义工具白名单的筛选模式,用于控制名称白名单和组白名单的组合逻辑。 - * - * @author Assistant Agent Team - * @since 1.0.0 - */ -public enum WhitelistMode { - - /** - * 交集模式(默认) - * - *

    当名称白名单和组白名单同时存在时,工具必须同时满足两者。 - * 当只有其中一个存在时,只检查存在的那个。 - */ - INTERSECTION, - - /** - * 并集模式 - * - *

    当名称白名单和组白名单同时存在时,工具满足任一即可。 - * 当只有其中一个存在时,只检查存在的那个。 - */ - UNION, - - /** - * 仅名称模式 - * - *

    仅使用名称白名单进行筛选,忽略组白名单。 - */ - NAME_ONLY, - - /** - * 仅组模式 - * - *

    仅使用组白名单进行筛选,忽略名称白名单。 - */ - GROUP_ONLY -} diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/node/CodeGeneratorNode.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/node/CodeGeneratorNode.java index 270a961..7161706 100644 --- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/node/CodeGeneratorNode.java +++ b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/subagent/node/CodeGeneratorNode.java @@ -15,14 +15,12 @@ */ package com.alibaba.assistant.agent.autoconfigure.subagent.node; -import com.alibaba.assistant.agent.common.constant.CodeactStateKeys; import com.alibaba.assistant.agent.common.enums.Language; import com.alibaba.assistant.agent.common.tools.CodeactTool; import com.alibaba.assistant.agent.common.tools.CodeactToolMetadata; import com.alibaba.assistant.agent.common.tools.definition.ParameterNode; import com.alibaba.assistant.agent.common.tools.definition.ParameterTree; import com.alibaba.assistant.agent.common.tools.definition.ReturnSchema; -import com.alibaba.assistant.agent.autoconfigure.subagent.filter.CodeactToolFilter; import com.alibaba.assistant.agent.core.tool.schema.ReturnSchemaRegistry; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.RunnableConfig; @@ -121,18 +119,15 @@ public Map apply(OverAllState state, RunnableConfig config) { logger.debug("CodeGeneratorNode#apply 提取输入: functionName={}, requirement={}, parameters={}, historyCodeCount={}", functionName, requirement, parameters, historyCode.size()); - // 2. 【新增】根据 state 中的白名单配置筛选工具 - List effectiveTools = filterTools(state); + // 2. 构建系统提示(包含语言规范、可用CodeactTool和历史代码) + String systemPrompt = buildSystemPrompt(language, codeactTools, isCondition, customSystemPrompt, historyCode); - // 3. 构建系统提示(使用筛选后的工具) - String systemPrompt = buildSystemPrompt(language, effectiveTools, isCondition, customSystemPrompt, historyCode); - - // 4. 构建用户消息 + // 3. 构建用户消息 String userMessage = buildUserMessage(requirement, functionName, parameters, isCondition); logger.info("CodeGeneratorNode#apply 构建消息: systemPrompt={}, userMessage={}", systemPrompt, userMessage); - // 5. 构造ModelRequest + // 4. 构造ModelRequest List messages = List.of( new SystemMessage(systemPrompt), new UserMessage(userMessage) @@ -142,20 +137,18 @@ public Map apply(OverAllState state, RunnableConfig config) { .messages(messages) .build(); - // 6. 通过拦截器链调用模型 + // 5. 通过拦截器链调用模型 ModelResponse modelResponse = executeWithInterceptors(modelRequest); - // 7. 提取生成的代码 + // 6. 提取生成的代码 String generatedCode = extractCodeFromResponse(modelResponse); logger.info("CodeGeneratorNode#apply 代码生成成功: functionName={}, codeLength={}", functionName, generatedCode.length()); - // 8. 返回结果(放入outputKey) + // 7. 返回结果(放入outputKey) Map result = new HashMap<>(); result.put(outputKey, generatedCode); - // 可选:将筛选后的工具列表写回 state,用于调试和审计 - result.put(CodeactStateKeys.FILTERED_CODEACT_TOOLS, effectiveTools); return result; } catch (Exception e) { @@ -166,24 +159,6 @@ public Map apply(OverAllState state, RunnableConfig config) { } } - /** - * 根据 state 中的白名单配置筛选工具 - * - *

    使用 {@link CodeactToolFilter} 根据 state 中配置的白名单筛选可用工具。 - * 如果没有配置白名单,则返回全部工具(保持向后兼容)。 - * - * @param state OverAllState 实例 - * @return 筛选后的工具列表 - */ - private List filterTools(OverAllState state) { - List filtered = CodeactToolFilter.filter(codeactTools, state, language); - - logger.info("CodeGeneratorNode#filterTools - 工具筛选完成: total={}, filtered={}", - codeactTools != null ? codeactTools.size() : 0, filtered.size()); - - return filtered; - } - /** * 从state提取需求 */ diff --git a/assistant-agent-common/pom.xml b/assistant-agent-common/pom.xml index 6b92ce5..b676856 100644 --- a/assistant-agent-common/pom.xml +++ b/assistant-agent-common/pom.xml @@ -6,7 +6,7 @@ com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 assistant-agent-common diff --git a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java index 93bd0ba..a244395 100644 --- a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java +++ b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java @@ -28,8 +28,6 @@ private CodeactStateKeys() { // Utility class } - // ==================== 代码生成上下文 ==================== - /** * Key for storing the list of generated codes in the current session * Type: List<GeneratedCode> @@ -65,72 +63,5 @@ private CodeactStateKeys() { * Type: Boolean */ public static final String INITIAL_CODE_GEN_DONE = "initial_code_gen_done"; - - // ==================== 工具白名单配置 ==================== - - /** - * 可用工具名称白名单 - * - *

    类型:List<String> - *

    示例:["search_app", "reply_user", "get_project_info"] - *

    用途:精确指定允许使用的工具,工具名称对应 CodeactTool.getName() - *

    为空或不存在时:不按名称筛选 - * - *

    注意:这里存储的是工具的 name(CodeactTool.getName()),不是独立的 ID。 - * 如果评估时 LLM 输出的是缩写/简短 ID,上层应用需要在写入 state 前将 ID 转换为对应的工具 name。 - */ - public static final String AVAILABLE_TOOL_NAMES = "available_tool_names"; - - /** - * 可用工具组白名单 - * - *

    类型:List<String> - *

    示例:["search", "reply", "app_helper"] - *

    用途:按工具组筛选,组名对应 CodeactToolMetadata.targetClassName() - *

    为空或不存在时:不按组筛选 - */ - public static final String AVAILABLE_TOOL_GROUPS = "available_tool_groups"; - - /** - * 白名单模式 - * - *

    类型:String - *

    可选值: - * - "INTERSECTION"(默认):名称白名单和组白名单取交集 - * - "UNION":名称白名单和组白名单取并集 - * - "NAME_ONLY":仅使用名称白名单 - * - "GROUP_ONLY":仅使用组白名单 - *

    为空或不存在时:默认为 INTERSECTION - */ - public static final String WHITELIST_MODE = "tool_whitelist_mode"; - - // ==================== 工具上下文(只读) ==================== - - /** - * 注入的全部 codeact 工具列表 - * - *

    类型:List<CodeactTool> - *

    由 CodeGeneratorSubAgent.init_context 节点注入 - *

    上层应用可读取此列表进行评估 - */ - public static final String CODEACT_TOOLS = "codeact_tools"; - - /** - * 筛选后的 codeact 工具列表 - * - *

    类型:List<CodeactTool> - *

    由 CodeGeneratorNode 筛选后写入(可选) - *

    用于调试和审计 - */ - public static final String FILTERED_CODEACT_TOOLS = "filtered_codeact_tools"; - - /** - * 编程语言 - * - *

    类型:String - *

    示例:"python", "java" - *

    由 CodeGeneratorSubAgent.init_context 节点注入 - */ - public static final String LANGUAGE = "language"; } diff --git a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/CodeactTool.java b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/CodeactTool.java index 4a0b2bd..fe56225 100644 --- a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/CodeactTool.java +++ b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/CodeactTool.java @@ -81,7 +81,12 @@ default CodeactToolDefinition getCodeactDefinition() { * @return 工具名 */ default String getName() { - return getCodeactDefinition().name(); + CodeactToolDefinition definition = getCodeactDefinition(); + if (definition != null) { + return definition.name(); + } + // 回退到 ToolDefinition + return getToolDefinition().name(); } /** @@ -89,7 +94,12 @@ default String getName() { * @return 工具描述 */ default String getDescription() { - return getCodeactDefinition().description(); + CodeactToolDefinition definition = getCodeactDefinition(); + if (definition != null) { + return definition.description(); + } + // 回退到 ToolDefinition + return getToolDefinition().description(); } /** diff --git a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/definition/ParameterTree.java b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/definition/ParameterTree.java index 3e2075d..c4b5438 100644 --- a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/definition/ParameterTree.java +++ b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/definition/ParameterTree.java @@ -155,7 +155,9 @@ private String formatDefaultValue(ParameterNode param) { return "None"; } if (defaultValue instanceof String) { - return "\"" + defaultValue + "\""; + // 转义字符串中的特殊字符 + String escaped = escapeStringForPython((String) defaultValue); + return "\"" + escaped + "\""; } if (defaultValue instanceof Boolean) { return (Boolean) defaultValue ? "True" : "False"; @@ -163,6 +165,46 @@ private String formatDefaultValue(ParameterNode param) { return String.valueOf(defaultValue); } + /** + * 转义字符串以便在 Python 代码中安全使用。 + * + * @param str 原始字符串 + * @return 转义后的字符串 + */ + private String escapeStringForPython(String str) { + if (str == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (char c : str.toCharArray()) { + switch (c) { + case '\\': + sb.append("\\\\"); + break; + case '"': + sb.append("\\\""); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + if (c < 32) { + // 控制字符用 Unicode 转义 + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + return sb.toString(); + } + /** * 创建构建器实例。 * @return 构建器 diff --git a/assistant-agent-core/pom.xml b/assistant-agent-core/pom.xml index 2d04cb2..ccff481 100644 --- a/assistant-agent-core/pom.xml +++ b/assistant-agent-core/pom.xml @@ -6,7 +6,7 @@ com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 assistant-agent-core @@ -65,6 +65,16 @@ org.slf4j slf4j-api + + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-context + \ No newline at end of file diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/executor/GraalCodeExecutor.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/executor/GraalCodeExecutor.java index ee5fc63..417552b 100644 --- a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/executor/GraalCodeExecutor.java +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/executor/GraalCodeExecutor.java @@ -25,6 +25,7 @@ import com.alibaba.assistant.agent.core.executor.bridge.StateBridge; import com.alibaba.assistant.agent.core.model.ExecutionRecord; import com.alibaba.assistant.agent.core.model.GeneratedCode; +import com.alibaba.assistant.agent.core.model.ToolCallRecord; import com.alibaba.assistant.agent.core.tool.CodeactToolRegistry; import com.alibaba.assistant.agent.core.tool.DefaultToolRegistryBridgeFactory; import com.alibaba.assistant.agent.core.tool.ToolRegistryBridge; @@ -60,6 +61,27 @@ public class GraalCodeExecutor { private static final Logger logger = LoggerFactory.getLogger(GraalCodeExecutor.class); + /** + * 执行结果包装类,包含执行结果和工具调用追踪 + */ + private static class ExecutionResultWrapper { + private final Object result; + private final List callTrace; + + public ExecutionResultWrapper(Object result, List callTrace) { + this.result = result; + this.callTrace = callTrace != null ? callTrace : new ArrayList<>(); + } + + public Object getResult() { + return result; + } + + public List getCallTrace() { + return callTrace; + } + } + private final RuntimeEnvironmentManager environmentManager; private final CodeContext codeContext; @@ -238,12 +260,14 @@ public ExecutionRecord execute(String functionName, Map args, To logger.debug("GraalCodeExecutor#execute 代码长度: {} 字符", finalCode.length()); // Execute with GraalVM - Object result = executeWithGraal(finalCode, toolContext); + ExecutionResultWrapper resultWrapper = executeWithGraal(finalCode, toolContext); record.setSuccess(true); - record.setResult(result != null ? String.valueOf(result) : "null"); + record.setResult(resultWrapper.getResult() != null ? String.valueOf(resultWrapper.getResult()) : "null"); + record.setCallTrace(resultWrapper.getCallTrace()); - logger.info("GraalCodeExecutor#execute 执行成功: result={}", result); + logger.info("GraalCodeExecutor#execute 执行成功: result={}, callTraceSize={}", + resultWrapper.getResult(), resultWrapper.getCallTrace().size()); } catch (Exception e) { record.setSuccess(false); @@ -284,12 +308,13 @@ public ExecutionRecord executeDirect(String code, ToolContext toolContext) { String completeCode = environmentManager.generateImports(codeContext) + "\n" + code; // Execute with GraalVM - Object result = executeWithGraal(completeCode, toolContext); + ExecutionResultWrapper resultWrapper = executeWithGraal(completeCode, toolContext); record.setSuccess(true); - record.setResult(String.valueOf(result)); + record.setResult(String.valueOf(resultWrapper.getResult())); + record.setCallTrace(resultWrapper.getCallTrace()); - logger.info("GraalCodeExecutor#executeDirect 执行成功"); + logger.info("GraalCodeExecutor#executeDirect 执行成功, callTraceSize={}", resultWrapper.getCallTrace().size()); } catch (Exception e) { record.setSuccess(false); @@ -312,13 +337,16 @@ public ExecutionRecord executeDirect(String code, ToolContext toolContext) { * @param toolContext the tool context to pass to ToolRegistryBridgeFactory * @return execution result */ - private Object executeWithGraal(String code, ToolContext toolContext) { + private ExecutionResultWrapper executeWithGraal(String code, ToolContext toolContext) { logger.debug("GraalCodeExecutor#executeWithGraal 创建GraalVM Context, hasToolContext={}", toolContext != null); // Capture output ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream errorStream = new ByteArrayOutputStream(); + // 用于保存ToolRegistryBridge以便获取callTrace + ToolRegistryBridge toolRegistryBridge = null; + try (Context context = Context.newBuilder("python") .allowHostAccess(HostAccess.ALL) .allowIO(allowIO) @@ -336,7 +364,7 @@ private Object executeWithGraal(String code, ToolContext toolContext) { // Inject CodeactTools into Python environment with toolContext if (codeactToolRegistry != null) { - injectCodeactTools(context, codeactToolRegistry, codeContext.getLanguage(), toolContext); + toolRegistryBridge = injectCodeactTools(context, codeactToolRegistry, codeContext.getLanguage(), toolContext); } // Execute code @@ -469,7 +497,11 @@ private Object executeWithGraal(String code, ToolContext toolContext) { javaResult = result.toString(); } - return javaResult; + // 获取工具调用追踪记录 + List callTrace = toolRegistryBridge != null ? toolRegistryBridge.getCallTrace() : new ArrayList<>(); + logger.info("GraalCodeExecutor#executeWithGraal - reason=获取callTrace完成, callTraceSize={}", callTrace.size()); + + return new ExecutionResultWrapper(javaResult, callTrace); } catch (Exception e) { String errors = errorStream.toString(StandardCharsets.UTF_8); @@ -628,7 +660,7 @@ private String getStackTrace(Exception e) { * @param language 编程语言 * @param toolContext 工具上下文 */ - private void injectCodeactTools(Context context, + private ToolRegistryBridge injectCodeactTools(Context context, CodeactToolRegistry registry, Language language, ToolContext toolContext) { @@ -654,7 +686,7 @@ private void injectCodeactTools(Context context, if (tools.isEmpty()) { logger.debug("GraalCodeExecutor#injectCodeactTools - reason=没有支持该语言的工具, language={}", language); - return; + return bridge; } // Group tools by targetClassName @@ -673,12 +705,173 @@ private void injectCodeactTools(Context context, // Generate and execute Python code String pythonCode = generatePythonToolCode(toolsByClass, globalTools, effectiveToolContext); if (pythonCode != null && !pythonCode.isEmpty()) { - logger.debug("GraalCodeExecutor#injectCodeactTools - reason=生成Python工具代码, length={}", pythonCode.length()); - context.eval("python", pythonCode); + logger.info("GraalCodeExecutor#injectCodeactTools - reason=生成Python工具代码, code={}", pythonCode); + try { + context.eval("python", pythonCode); + } catch (Exception e) { + // 执行失败时记录生成的代码以便调试 + // 添加行号便于定位问题 + String[] lines = pythonCode.split("\n"); + StringBuilder numberedCode = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + numberedCode.append(String.format("%4d: %s\n", i + 1, lines[i])); + } + logger.error("GraalCodeExecutor#injectCodeactTools - reason=Python工具代码执行失败, totalLines={}, 生成的代码:\n{}", + lines.length, numberedCode); + throw new RuntimeException("Python工具代码语法错误,代码行数=" + lines.length + ",请查看日志获取完整代码", e); + } } logger.info("GraalCodeExecutor#injectCodeactTools - reason=CodeactTool注入完成, classCount={}, globalToolCount={}", toolsByClass.size(), globalTools.size()); + + return bridge; + } + + /** + * 清理描述字符串,使其在 Python 三引号字符串中安全使用。 + * + *

    主要处理以下问题: + *

      + *
    • 将三引号 """ 替换为转义形式
    • + *
    • 确保反斜杠正确转义
    • + *
    • 移除可能导致问题的控制字符
    • + *
    + * + * @param description 原始描述 + * @return 清理后的描述 + */ + private String sanitizeDescriptionForPython(String description) { + if (description == null || description.isEmpty()) { + return ""; + } + + // 1. 处理三引号 - 将 """ 替换为 \"\"\" (在三引号字符串内转义) + String sanitized = description.replace("\"\"\"", "\\\"\\\"\\\""); + + // 2. 处理单独的反斜杠(但不影响已有的转义序列) + // 只有当反斜杠后面不是常见转义字符时才转义 + // 这里使用简单策略:先不处理,因为三引号字符串中反斜杠问题较少 + + // 3. 移除可能导致问题的控制字符(除了换行和制表符) + StringBuilder sb = new StringBuilder(); + for (char c : sanitized.toCharArray()) { + if (c == '\n' || c == '\r' || c == '\t' || c >= 32) { + sb.append(c); + } + } + + return sb.toString(); + } + + private String formatDocstring(String description, String indent) { + if (description == null) { + return indent; + } + String normalized = description.replace("\r\n", "\n").replace("\r", "\n"); + String[] lines = normalized.split("\n", -1); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + sb.append(indent).append(lines[i]); + if (i < lines.length - 1) { + sb.append("\n"); + } + } + return sb.toString(); + } + + private boolean isPythonKeyword(String name) { + if (name == null || name.isEmpty()) { + return false; + } + switch (name) { + case "False": + case "None": + case "True": + case "and": + case "as": + case "assert": + case "async": + case "await": + case "break": + case "class": + case "continue": + case "def": + case "del": + case "elif": + case "else": + case "except": + case "finally": + case "for": + case "from": + case "global": + case "if": + case "import": + case "in": + case "is": + case "lambda": + case "nonlocal": + case "not": + case "or": + case "pass": + case "raise": + case "return": + case "try": + case "while": + case "with": + case "yield": + return true; + default: + return false; + } + } + + private boolean isValidPythonIdentifier(String name) { + if (name == null || name.isEmpty()) { + return false; + } + if (isPythonKeyword(name)) { + return false; + } + char first = name.charAt(0); + if (!(Character.isLetter(first) || first == '_')) { + return false; + } + for (int i = 1; i < name.length(); i++) { + char c = name.charAt(i); + if (!(Character.isLetterOrDigit(c) || c == '_')) { + return false; + } + } + return true; + } + + private String toSafePythonIdentifier(String name, String fallbackPrefix) { + if (isValidPythonIdentifier(name)) { + return name; + } + String safeSource = name == null ? "" : name; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < safeSource.length(); i++) { + char c = safeSource.charAt(i); + if (Character.isLetterOrDigit(c) || c == '_') { + sb.append(c); + } else { + sb.append('_'); + } + } + if (sb.length() == 0) { + sb.append('_'); + } + char first = sb.charAt(0); + if (!(Character.isLetter(first) || first == '_')) { + sb.insert(0, '_'); + } + String candidate = sb.toString(); + if (isPythonKeyword(candidate)) { + candidate = fallbackPrefix + Math.abs(safeSource.hashCode()); + } + return candidate; } /** @@ -740,7 +933,9 @@ private void generatePythonMethod(StringBuilder code, boolean isClassMethod) { String toolName = tool.getToolDefinition().name(); - String description = tool.getToolDefinition().description(); + String rawDescription = tool.getToolDefinition().description(); + // 处理描述中可能导致 Python 语法错误的字符 + String description = sanitizeDescriptionForPython(rawDescription); // 优先使用 ParameterTree 获取参数信息 ParameterTree parameterTree = tool.getParameterTree(); @@ -750,6 +945,7 @@ private void generatePythonMethod(StringBuilder code, List requiredParams = new ArrayList<>(); List optionalParams = new ArrayList<>(); List allParamNames = new ArrayList<>(); + boolean forceKwargs = false; if (parameterTree != null && parameterTree.hasParameters()) { // 使用 ParameterTree 生成参数签名 @@ -759,6 +955,9 @@ private void generatePythonMethod(StringBuilder code, for (ParameterNode param : parameterTree.getParameters()) { String paramName = param.getName(); allParamNames.add(paramName); + if (!isValidPythonIdentifier(paramName)) { + forceKwargs = true; + } if (param.isRequired()) { requiredParams.add(paramName); } else { @@ -780,9 +979,28 @@ private void generatePythonMethod(StringBuilder code, parameters = "**kwargs"; } } + if (!parameters.equals("**kwargs")) { + String[] params = parameters.split(","); + for (String param : params) { + String paramName = param.trim().split(":")[0].split("=")[0].trim(); + if (!paramName.isEmpty() && !paramName.equals("self") && !isValidPythonIdentifier(paramName)) { + forceKwargs = true; + break; + } + } + } } } + if (forceKwargs) { + parameters = "**kwargs"; + requiredParams.clear(); + optionalParams.clear(); + allParamNames.clear(); + } + + String pythonFunctionName = toSafePythonIdentifier(functionName, "tool_"); + // Generate method/function String indent = isClassMethod ? " " : ""; @@ -792,8 +1010,10 @@ private void generatePythonMethod(StringBuilder code, } // Function definition - code.append(String.format("%sdef %s(%s):\n", indent, functionName, parameters)); - code.append(String.format("%s \"\"\"%s\"\"\"\n", indent, description != null ? description : toolName)); + code.append(String.format("%sdef %s(%s):\n", indent, pythonFunctionName, parameters)); + String docIndent = indent + " "; + String docBody = formatDocstring(description != null ? description : toolName, docIndent); + code.append(String.format("%s \"\"\"\n%s\n%s \"\"\"\n", indent, docBody, indent)); // Function body - call Java tool through proxy code.append(String.format("%s # Call Java CodeactTool\n", indent)); diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ExecutionRecord.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ExecutionRecord.java index 464bb71..51cbb29 100644 --- a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ExecutionRecord.java +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ExecutionRecord.java @@ -19,6 +19,8 @@ import java.io.Serializable; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -77,8 +79,14 @@ public class ExecutionRecord implements Serializable { */ private Map metadata; + /** + * 工具调用追踪记录,记录代码执行过程中调用的工具列表 + */ + private List callTrace; + public ExecutionRecord() { this.executedAt = LocalDateTime.now(); + this.callTrace = new ArrayList<>(); } public ExecutionRecord(String functionName, Language language) { @@ -161,6 +169,25 @@ public void setMetadata(Map metadata) { this.metadata = metadata; } + public List getCallTrace() { + return callTrace; + } + + public void setCallTrace(List callTrace) { + this.callTrace = callTrace; + } + + /** + * 添加一条工具调用记录 + * @param toolName 工具名称 + */ + public void addToolCall(String toolName) { + if (this.callTrace == null) { + this.callTrace = new ArrayList<>(); + } + this.callTrace.add(new ToolCallRecord(this.callTrace.size() + 1, toolName)); + } + @Override public String toString() { return "ExecutionRecord{" + @@ -171,6 +198,7 @@ public String toString() { ", errorMessage='" + errorMessage + '\'' + ", executedAt=" + executedAt + ", durationMs=" + durationMs + + ", callTrace=" + callTrace + '}'; } } diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ToolCallRecord.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ToolCallRecord.java new file mode 100644 index 0000000..7914acf --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/model/ToolCallRecord.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.model; + +import java.io.Serializable; + +/** + * 工具调用记录,用于追踪代码执行过程中调用的工具。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class ToolCallRecord implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 调用顺序(从1开始) + */ + private int order; + + /** + * 工具名称(格式:targetClassName.methodName 或 直接 toolName) + */ + private String tool; + + public ToolCallRecord() { + } + + public ToolCallRecord(int order, String tool) { + this.order = order; + this.tool = tool; + } + + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + public String getTool() { + return tool; + } + + public void setTool(String tool) { + this.tool = tool; + } + + @Override + public String toString() { + return "{\"order\": " + order + ", \"tool\": \"" + tool + "\"}"; + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/BaseAgentObservationLifecycleListener.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/BaseAgentObservationLifecycleListener.java new file mode 100644 index 0000000..eae4af6 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/BaseAgentObservationLifecycleListener.java @@ -0,0 +1,506 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation; + +import com.alibaba.assistant.agent.core.observation.context.HookObservationContext; +import com.alibaba.assistant.agent.core.observation.context.InterceptorObservationContext; +import com.alibaba.assistant.agent.core.observation.context.ReactPhaseObservationContext; +import com.alibaba.cloud.ai.graph.GraphLifecycleListener; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * Agent 可观测性生命周期监听器抽象基类 + *

    + * 提供通用的 OpenTelemetry Span 创建和管理能力,包括: + *

      + *
    • Hook 执行的观测
    • + *
    • Interceptor 执行的观测
    • + *
    • React 阶段(LlmNode/ToolNode)的观测
    • + *
    • 自定义数据注册机制
    • + *
    + *

    + * 已从 Micrometer Observation 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public abstract class BaseAgentObservationLifecycleListener implements GraphLifecycleListener { + + private static final Logger log = LoggerFactory.getLogger(BaseAgentObservationLifecycleListener.class); + + /** + * 状态 key:React 阶段开始时间 + */ + protected static final String REACT_START_TIME_KEY = "_react_start_time_"; + + /** + * 状态 key:ObservationState + */ + protected static final String OBSERVATION_STATE_KEY = "_observation_state_"; + + protected final Tracer tracer; + + /** + * 存储每个 sessionId 的根 Span + */ + protected final ConcurrentHashMap sessionSpans = new ConcurrentHashMap<>(); + + /** + * 存储每个 sessionId 的根 Span Scope + */ + protected final ConcurrentHashMap sessionScopes = new ConcurrentHashMap<>(); + + /** + * 存储每个节点的 Span + */ + protected final ConcurrentHashMap nodeSpans = new ConcurrentHashMap<>(); + + /** + * 存储每个节点的 Span Scope + */ + protected final ConcurrentHashMap nodeScopes = new ConcurrentHashMap<>(); + + /** + * 存储每个 sessionId 的迭代计数器 + */ + protected final ConcurrentHashMap iterationCounters = new ConcurrentHashMap<>(); + + /** + * 存储每个节点的开始时间 + */ + protected final ConcurrentHashMap nodeStartTimes = new ConcurrentHashMap<>(); + + /** + * 存储每个 sessionId 的 ObservationState + */ + protected final ConcurrentHashMap sessionStates = new ConcurrentHashMap<>(); + + protected BaseAgentObservationLifecycleListener(Tracer tracer) { + this.tracer = tracer; + log.info("BaseAgentObservationLifecycleListener# - reason=初始化完成, hasTracer={}", tracer != null); + } + + // ==================== ObservationState Management ==================== + + /** + * 获取或创建会话的 ObservationState + * + * @param sessionId 会话ID + * @return ObservationState + */ + public ObservationState getOrCreateObservationState(String sessionId) { + return sessionStates.computeIfAbsent(sessionId, k -> new DefaultObservationState()); + } + + /** + * 获取会话的 ObservationState + * + * @param sessionId 会话ID + * @return ObservationState,如果不存在返回null + */ + public ObservationState getObservationState(String sessionId) { + return sessionStates.get(sessionId); + } + + /** + * 从 OverAllState 中获取或创建 ObservationState + * + * @param state OverAllState + * @param config RunnableConfig + * @return ObservationState + */ + protected ObservationState getOrCreateObservationState(Map state, RunnableConfig config) { + String sessionId = extractSessionId(config); + + // 先从缓存中获取 + ObservationState obsState = sessionStates.get(sessionId); + if (obsState != null) { + return obsState; + } + + // 尝试从 state 中获取 + Object stateObj = state.get(OBSERVATION_STATE_KEY); + if (stateObj instanceof ObservationState) { + obsState = (ObservationState) stateObj; + sessionStates.put(sessionId, obsState); + return obsState; + } + + // 创建新的并存入 state + obsState = new DefaultObservationState(); + sessionStates.put(sessionId, obsState); + state.put(OBSERVATION_STATE_KEY, obsState); + return obsState; + } + + // ==================== Hook Span ==================== + + /** + * 创建 Hook 的 Span + * + * @param context Hook观测上下文 + * @return Span + */ + protected Span createHookSpan(HookObservationContext context) { + if (tracer == null) { + return null; + } + + String spanName = CodeactObservationDocumentation.SPAN_HOOK + "." + + (context.getHookName() != null ? context.getHookName().toLowerCase() : "unknown"); + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.CONVERSATION_ID, + context.getSessionId() != null ? context.getSessionId() : "unknown") + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.SPAN_KIND_NAME, "CHAIN") + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.OPERATION_NAME, "chain") + .setAttribute(CodeactObservationDocumentation.HookAttributes.NAME, + context.getHookName() != null ? context.getHookName() : "unknown") + .setAttribute(CodeactObservationDocumentation.HookAttributes.POSITION, + context.getHookPosition() != null ? context.getHookPosition() : "unknown") + .startSpan(); + + // 添加自定义数据 + context.getAllCustomData().forEach((key, value) -> { + if (value != null) { + span.setAttribute("codeact.hook.custom." + key, truncate(value.toString(), 500)); + } + }); + + return span; + } + + // ==================== Interceptor Span ==================== + + /** + * 创建 Interceptor 的 Span + * + * @param context Interceptor观测上下文 + * @return Span + */ + protected Span createInterceptorSpan(InterceptorObservationContext context) { + if (tracer == null) { + return null; + } + + String typeName = context.getInterceptorType() != null + ? context.getInterceptorType().name().toLowerCase() + : "unknown"; + String spanName = CodeactObservationDocumentation.SPAN_INTERCEPTOR + "." + typeName + "." + + (context.getInterceptorName() != null ? context.getInterceptorName().toLowerCase() : "unknown"); + + SpanKind spanKind = context.getInterceptorType() == InterceptorObservationContext.InterceptorType.MODEL + ? SpanKind.CLIENT + : SpanKind.INTERNAL; + + String genAiSpanKind = context.getInterceptorType() == InterceptorObservationContext.InterceptorType.MODEL + ? "LLM" + : (context.getInterceptorType() == InterceptorObservationContext.InterceptorType.TOOL + ? "TOOL" + : "CHAIN"); + + String operationName = genAiSpanKind.equals("LLM") ? "chat" + : (genAiSpanKind.equals("TOOL") ? "execute_tool" : "chain"); + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(spanKind) + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.CONVERSATION_ID, + context.getSessionId() != null ? context.getSessionId() : "unknown") + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.SPAN_KIND_NAME, genAiSpanKind) + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.OPERATION_NAME, operationName) + .setAttribute(CodeactObservationDocumentation.InterceptorAttributes.NAME, + context.getInterceptorName() != null ? context.getInterceptorName() : "unknown") + .setAttribute(CodeactObservationDocumentation.InterceptorAttributes.TYPE, typeName) + .startSpan(); + + // Model Interceptor specific attributes + if (context.getModelName() != null) { + span.setAttribute(CodeactObservationDocumentation.InterceptorAttributes.MODEL_NAME, context.getModelName()); + } + + // Tool Interceptor specific attributes + if (context.getToolName() != null) { + span.setAttribute(CodeactObservationDocumentation.InterceptorAttributes.TOOL_NAME, context.getToolName()); + } + + // 添加自定义数据 + context.getAllCustomData().forEach((key, value) -> { + if (value != null) { + span.setAttribute("codeact.interceptor.custom." + key, truncate(value.toString(), 500)); + } + }); + + return span; + } + + // ==================== React Phase Span ==================== + + /** + * 创建 React 阶段的 Span + * + * @param context React阶段观测上下文 + * @return Span + */ + protected Span createReactPhaseSpan(ReactPhaseObservationContext context) { + if (tracer == null) { + return null; + } + + String nodeTypeName = context.getNodeType() != null + ? context.getNodeType().name().toLowerCase() + : "unknown"; + String spanName = CodeactObservationDocumentation.SPAN_REACT + "." + nodeTypeName; + + SpanKind spanKind = context.getNodeType() == ReactPhaseObservationContext.NodeType.LLM + ? SpanKind.CLIENT + : SpanKind.INTERNAL; + + String genAiSpanKind = context.getNodeType() == ReactPhaseObservationContext.NodeType.LLM + ? "LLM" + : (context.getNodeType() == ReactPhaseObservationContext.NodeType.TOOL + ? "TOOL" + : "CHAIN"); + + String operationName = genAiSpanKind.equals("LLM") ? "chat" + : (genAiSpanKind.equals("TOOL") ? "execute_tool" : "chain"); + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(spanKind) + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.CONVERSATION_ID, + context.getSessionId() != null ? context.getSessionId() : "unknown") + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.SPAN_KIND_NAME, genAiSpanKind) + .setAttribute(CodeactObservationDocumentation.GenAIAttributes.OPERATION_NAME, operationName) + .setAttribute(CodeactObservationDocumentation.ReactPhaseAttributes.NODE_TYPE, nodeTypeName) + .startSpan(); + + // Add model name for LLM nodes + if (context.getModelName() != null) { + span.setAttribute(CodeactObservationDocumentation.ReactPhaseAttributes.MODEL_NAME, context.getModelName()); + } + + // Add iteration info + if (context.getIteration() > 0) { + span.setAttribute(CodeactObservationDocumentation.ReactPhaseAttributes.ITERATION, (long) context.getIteration()); + } + + // Add node ID + if (context.getNodeId() != null) { + span.setAttribute(CodeactObservationDocumentation.ReactPhaseAttributes.NODE_ID, context.getNodeId()); + } + + // ==================== LLM Node 特定属性 ==================== + if (context.getNodeType() == ReactPhaseObservationContext.NodeType.LLM) { + // 记录 prompt 消息数量 + if (context.getPromptMessageCount() > 0) { + span.setAttribute("alibaba.llm.prompt_message_count", (long) context.getPromptMessageCount()); + } + + // 记录可用工具列表(只记录工具名) + if (context.getAvailableToolNames() != null && !context.getAvailableToolNames().isEmpty()) { + span.setAttribute("alibaba.llm.available_tools", String.join(",", context.getAvailableToolNames())); + span.setAttribute("alibaba.llm.available_tools_count", (long) context.getAvailableToolNames().size()); + } + + // 记录输入摘要 + if (context.getInputSummary() != null) { + span.setAttribute("alibaba.llm.input_summary", truncate(context.getInputSummary(), 1000)); + } + + // 记录输出摘要 + if (context.getOutputSummary() != null) { + span.setAttribute("alibaba.llm.output_summary", truncate(context.getOutputSummary(), 1000)); + } + + // 记录 token 使用量 + if (context.getInputTokens() > 0) { + span.setAttribute("alibaba.llm.input_tokens", (long) context.getInputTokens()); + } + if (context.getOutputTokens() > 0) { + span.setAttribute("alibaba.llm.output_tokens", (long) context.getOutputTokens()); + } + + // 记录完成原因 + if (context.getFinishReason() != null) { + span.setAttribute("alibaba.llm.finish_reason", context.getFinishReason()); + } + } + + // ==================== Tool Node 特定属性 ==================== + if (context.getNodeType() == ReactPhaseObservationContext.NodeType.TOOL) { + List toolCalls = context.getToolCalls(); + if (toolCalls != null && !toolCalls.isEmpty()) { + // 记录工具调用数量 + span.setAttribute("alibaba.tool.call_count", (long) toolCalls.size()); + + // 记录所有调用的工具名 + String toolNames = toolCalls.stream() + .map(ReactPhaseObservationContext.ToolCallInfo::getToolName) + .filter(name -> name != null) + .collect(Collectors.joining(",")); + span.setAttribute("alibaba.tool.names", toolNames); + + // 记录工具调用总时长 + long totalDuration = toolCalls.stream() + .mapToLong(ReactPhaseObservationContext.ToolCallInfo::getDurationMs) + .sum(); + span.setAttribute("alibaba.tool.total_duration_ms", totalDuration); + + // 记录工具调用结果总长度 + int totalResultLength = toolCalls.stream() + .mapToInt(ReactPhaseObservationContext.ToolCallInfo::getResultLength) + .sum(); + span.setAttribute("alibaba.tool.total_result_length", (long) totalResultLength); + + // 记录每个工具调用的详细信息 + for (int i = 0; i < toolCalls.size() && i < 10; i++) { // 最多记录10个工具调用 + ReactPhaseObservationContext.ToolCallInfo toolCall = toolCalls.get(i); + String prefix = "alibaba.tool." + i + "."; + span.setAttribute(prefix + "name", + toolCall.getToolName() != null ? toolCall.getToolName() : "unknown"); + span.setAttribute(prefix + "duration_ms", toolCall.getDurationMs()); + span.setAttribute(prefix + "success", toolCall.isSuccess()); + if (toolCall.getArguments() != null) { + span.setAttribute(prefix + "arguments", truncate(toolCall.getArguments(), 500)); + } + if (toolCall.getResult() != null) { + span.setAttribute(prefix + "result", truncate(toolCall.getResult(), 500)); + } + if (!toolCall.isSuccess() && toolCall.getErrorMessage() != null) { + span.setAttribute(prefix + "error", truncate(toolCall.getErrorMessage(), 200)); + } + } + } + } + + // 添加自定义数据 + context.getAllCustomData().forEach((key, value) -> { + if (value != null) { + span.setAttribute("codeact.react.custom." + key, truncate(value.toString(), 500)); + } + }); + + return span; + } + + // ==================== Helper Methods ==================== + + /** + * 从 RunnableConfig 中提取 sessionId + */ + protected String extractSessionId(RunnableConfig config) { + if (config == null) { + return "unknown"; + } + return config.threadId().orElse("unknown"); + } + + /** + * 从 RunnableConfig 的 metadata 中提取指定字段 + */ + protected String extractMetadata(RunnableConfig config, String key) { + if (config == null) { + return null; + } + return config.metadata(key) + .map(Object::toString) + .orElse(null); + } + + /** + * 截断字符串 + */ + protected String truncate(String str, int maxLength) { + if (str == null) { + return "null"; + } + if (str.length() <= maxLength) { + return str; + } + return str.substring(0, maxLength) + "...[truncated]"; + } + + /** + * 停止节点 Span 并记录时长 + * + * @param nodeKey 节点Key + * @param durationMs 时长(毫秒) + * @param success 是否成功 + * @param error 错误信息(可选) + */ + protected void stopNodeSpan(String nodeKey, long durationMs, boolean success, Throwable error) { + // 先关闭 Scope + Scope scope = nodeScopes.remove(nodeKey); + if (scope != null) { + scope.close(); + } + + // 再结束 Span + Span span = nodeSpans.remove(nodeKey); + if (span != null) { + span.setAttribute("duration.ms", durationMs); + span.setAttribute("success", success); + + if (error != null) { + span.setStatus(StatusCode.ERROR, error.getMessage()); + span.recordException(error); + } + + span.end(); + } + } + + /** + * 清理会话资源 + * + * @param sessionId 会话ID + */ + protected void cleanupSession(String sessionId) { + // 关闭会话 Scope + Scope sessionScope = sessionScopes.remove(sessionId); + if (sessionScope != null) { + sessionScope.close(); + } + + // 结束会话 Span + Span sessionSpan = sessionSpans.remove(sessionId); + if (sessionSpan != null) { + sessionSpan.end(); + } + + sessionStates.remove(sessionId); + iterationCounters.remove(sessionId); + + // 清理该会话的所有节点 Span + nodeSpans.keySet().removeIf(key -> key.startsWith(sessionId + ":")); + nodeScopes.keySet().removeIf(key -> key.startsWith(sessionId + ":")); + nodeStartTimes.keySet().removeIf(key -> key.startsWith(sessionId + ":")); + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/CodeactObservationDocumentation.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/CodeactObservationDocumentation.java new file mode 100644 index 0000000..e02df8e --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/CodeactObservationDocumentation.java @@ -0,0 +1,248 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation; + +import io.opentelemetry.api.common.AttributeKey; + +/** + * CodeactAgent 可观测性文档定义 + *

    + * 定义通用的、标准化的观测指标,供所有使用 CodeactAgent 的项目复用。 + * 使用 OpenTelemetry AttributeKey 定义标准属性。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public final class CodeactObservationDocumentation { + + private CodeactObservationDocumentation() { + // Utility class + } + + // ==================== Span Names ==================== + + public static final String SPAN_HOOK = "codeact.hook"; + public static final String SPAN_INTERCEPTOR = "codeact.interceptor"; + public static final String SPAN_REACT = "codeact.react"; + public static final String SPAN_EXECUTION = "codeact.execution"; + public static final String SPAN_CODE_GENERATION = "codeact.codegen"; + public static final String SPAN_TOOL_CALL = "codeact.tool.call"; + + // ==================== Hook Attribute Keys ==================== + + /** + * Hook 观测属性键 + */ + public static final class HookAttributes { + /** Hook名称 */ + public static final AttributeKey NAME = AttributeKey.stringKey("codeact.hook.name"); + /** Hook位置(BEFORE_AGENT, AFTER_AGENT, BEFORE_MODEL, AFTER_MODEL) */ + public static final AttributeKey POSITION = AttributeKey.stringKey("codeact.hook.position"); + /** 是否成功 */ + public static final AttributeKey SUCCESS = AttributeKey.booleanKey("codeact.hook.success"); + /** Agent名称 */ + public static final AttributeKey AGENT_NAME = AttributeKey.stringKey("codeact.hook.agent_name"); + /** 会话ID */ + public static final AttributeKey SESSION_ID = AttributeKey.stringKey("codeact.hook.session_id"); + /** 执行时长(毫秒) */ + public static final AttributeKey DURATION_MS = AttributeKey.longKey("codeact.hook.duration_ms"); + /** 错误类型 */ + public static final AttributeKey ERROR_TYPE = AttributeKey.stringKey("codeact.hook.error_type"); + /** 错误信息 */ + public static final AttributeKey ERROR_MESSAGE = AttributeKey.stringKey("codeact.hook.error_message"); + + private HookAttributes() {} + } + + // ==================== Interceptor Attribute Keys ==================== + + /** + * Interceptor 观测属性键 + */ + public static final class InterceptorAttributes { + /** Interceptor名称 */ + public static final AttributeKey NAME = AttributeKey.stringKey("codeact.interceptor.name"); + /** Interceptor类型(MODEL, TOOL) */ + public static final AttributeKey TYPE = AttributeKey.stringKey("codeact.interceptor.type"); + /** 是否成功 */ + public static final AttributeKey SUCCESS = AttributeKey.booleanKey("codeact.interceptor.success"); + /** Agent名称 */ + public static final AttributeKey AGENT_NAME = AttributeKey.stringKey("codeact.interceptor.agent_name"); + /** 会话ID */ + public static final AttributeKey SESSION_ID = AttributeKey.stringKey("codeact.interceptor.session_id"); + /** 执行时长(毫秒) */ + public static final AttributeKey DURATION_MS = AttributeKey.longKey("codeact.interceptor.duration_ms"); + /** 模型名称(仅ModelInterceptor) */ + public static final AttributeKey MODEL_NAME = AttributeKey.stringKey("codeact.interceptor.model_name"); + /** 工具名称(仅ToolInterceptor) */ + public static final AttributeKey TOOL_NAME = AttributeKey.stringKey("codeact.interceptor.tool_name"); + /** 工具参数长度(仅ToolInterceptor) */ + public static final AttributeKey TOOL_ARGUMENTS_LENGTH = AttributeKey.longKey("codeact.interceptor.tool_arguments_length"); + /** 工具结果长度(仅ToolInterceptor) */ + public static final AttributeKey TOOL_RESULT_LENGTH = AttributeKey.longKey("codeact.interceptor.tool_result_length"); + /** 输入Token数(仅ModelInterceptor) */ + public static final AttributeKey INPUT_TOKENS = AttributeKey.longKey("codeact.interceptor.input_tokens"); + /** 输出Token数(仅ModelInterceptor) */ + public static final AttributeKey OUTPUT_TOKENS = AttributeKey.longKey("codeact.interceptor.output_tokens"); + + private InterceptorAttributes() {} + } + + // ==================== React Phase Attribute Keys ==================== + + /** + * React阶段 观测属性键 + */ + public static final class ReactPhaseAttributes { + /** 节点类型(LLM, TOOL) */ + public static final AttributeKey NODE_TYPE = AttributeKey.stringKey("codeact.react.node_type"); + /** 是否成功 */ + public static final AttributeKey SUCCESS = AttributeKey.booleanKey("codeact.react.success"); + /** 模型名称 */ + public static final AttributeKey MODEL_NAME = AttributeKey.stringKey("codeact.react.model_name"); + /** 会话ID */ + public static final AttributeKey SESSION_ID = AttributeKey.stringKey("codeact.react.session_id"); + /** Agent名称 */ + public static final AttributeKey AGENT_NAME = AttributeKey.stringKey("codeact.react.agent_name"); + /** 节点ID */ + public static final AttributeKey NODE_ID = AttributeKey.stringKey("codeact.react.node_id"); + /** 迭代轮次 */ + public static final AttributeKey ITERATION = AttributeKey.longKey("codeact.react.iteration"); + /** 执行时长(毫秒) */ + public static final AttributeKey DURATION_MS = AttributeKey.longKey("codeact.react.duration_ms"); + /** 输入Token数 */ + public static final AttributeKey INPUT_TOKENS = AttributeKey.longKey("codeact.react.input_tokens"); + /** 输出Token数 */ + public static final AttributeKey OUTPUT_TOKENS = AttributeKey.longKey("codeact.react.output_tokens"); + /** 提示消息数 */ + public static final AttributeKey PROMPT_MESSAGE_COUNT = AttributeKey.longKey("codeact.react.prompt_message_count"); + /** 完成原因 */ + public static final AttributeKey FINISH_REASON = AttributeKey.stringKey("codeact.react.finish_reason"); + /** 工具调用数 */ + public static final AttributeKey TOOL_CALLS_COUNT = AttributeKey.longKey("codeact.react.tool_calls_count"); + /** 工具名称列表(逗号分隔) */ + public static final AttributeKey TOOL_NAMES = AttributeKey.stringKey("codeact.react.tool_names"); + + private ReactPhaseAttributes() {} + } + + // ==================== Codeact Execution Attribute Keys ==================== + + /** + * 代码执行 观测属性键 + */ + public static final class ExecutionAttributes { + /** 编程语言 */ + public static final AttributeKey LANGUAGE = AttributeKey.stringKey("codeact.language"); + /** 是否成功 */ + public static final AttributeKey SUCCESS = AttributeKey.booleanKey("codeact.success"); + /** 函数名 */ + public static final AttributeKey FUNCTION_NAME = AttributeKey.stringKey("codeact.function.name"); + /** 参数长度 */ + public static final AttributeKey ARGUMENTS_LENGTH = AttributeKey.longKey("codeact.arguments.length"); + /** 结果长度 */ + public static final AttributeKey RESULT_LENGTH = AttributeKey.longKey("codeact.result.length"); + /** 执行时长(毫秒) */ + public static final AttributeKey DURATION_MS = AttributeKey.longKey("codeact.duration.ms"); + /** 错误类型 */ + public static final AttributeKey ERROR_TYPE = AttributeKey.stringKey("codeact.error.type"); + /** 错误信息 */ + public static final AttributeKey ERROR_MESSAGE = AttributeKey.stringKey("codeact.error.message"); + + private ExecutionAttributes() {} + } + + // ==================== Code Generation Attribute Keys ==================== + + /** + * 代码生成 观测属性键 + */ + public static final class CodeGenerationAttributes { + /** 编程语言 */ + public static final AttributeKey LANGUAGE = AttributeKey.stringKey("codeact.codegen.language"); + /** 模型名称 */ + public static final AttributeKey MODEL_NAME = AttributeKey.stringKey("codeact.codegen.model"); + /** 是否成功 */ + public static final AttributeKey SUCCESS = AttributeKey.booleanKey("codeact.codegen.success"); + /** 函数名 */ + public static final AttributeKey FUNCTION_NAME = AttributeKey.stringKey("codeact.codegen.function.name"); + /** 代码行数 */ + public static final AttributeKey CODE_LINES = AttributeKey.longKey("codeact.codegen.code.lines"); + /** 输入 Token */ + public static final AttributeKey INPUT_TOKENS = AttributeKey.longKey("codeact.codegen.input.tokens"); + /** 输出 Token */ + public static final AttributeKey OUTPUT_TOKENS = AttributeKey.longKey("codeact.codegen.output.tokens"); + /** 执行时长(毫秒) */ + public static final AttributeKey DURATION_MS = AttributeKey.longKey("codeact.codegen.duration.ms"); + + private CodeGenerationAttributes() {} + } + + // ==================== Codeact Tool Call Attribute Keys ==================== + + /** + * Codeact工具调用 观测属性键 + */ + public static final class ToolCallAttributes { + /** 工具名称 */ + public static final AttributeKey TOOL_NAME = AttributeKey.stringKey("codeact.tool.name"); + /** 是否成功 */ + public static final AttributeKey SUCCESS = AttributeKey.booleanKey("codeact.tool.success"); + /** 参数(JSON格式) */ + public static final AttributeKey ARGUMENTS = AttributeKey.stringKey("codeact.tool.arguments"); + /** 参数长度 */ + public static final AttributeKey ARGUMENTS_LENGTH = AttributeKey.longKey("codeact.tool.arguments.length"); + /** 结果长度 */ + public static final AttributeKey RESULT_LENGTH = AttributeKey.longKey("codeact.tool.result.length"); + /** 执行时长(毫秒) */ + public static final AttributeKey DURATION_MS = AttributeKey.longKey("codeact.tool.duration.ms"); + /** 错误类型 */ + public static final AttributeKey ERROR_TYPE = AttributeKey.stringKey("codeact.tool.error.type"); + + private ToolCallAttributes() {} + } + + // ==================== GenAI Semantic Convention Attribute Keys ==================== + + /** + * GenAI 语义约定属性键 + */ + public static final class GenAIAttributes { + /** 会话ID */ + public static final AttributeKey CONVERSATION_ID = AttributeKey.stringKey("gen_ai.conversation.id"); + /** Span 类型(LLM, TOOL, CHAIN, EVALUATOR) */ + public static final AttributeKey SPAN_KIND_NAME = AttributeKey.stringKey("gen_ai.span_kind_name"); + /** 操作名称(chat, execute_tool, chain, evaluate) */ + public static final AttributeKey OPERATION_NAME = AttributeKey.stringKey("gen_ai.operation.name"); + /** Agent 名称 */ + public static final AttributeKey AGENT_NAME = AttributeKey.stringKey("gen_ai.agent.name"); + /** 响应模型 */ + public static final AttributeKey RESPONSE_MODEL = AttributeKey.stringKey("gen_ai.response.model"); + /** 工具名称 */ + public static final AttributeKey TOOL_NAME = AttributeKey.stringKey("gen_ai.tool.name"); + /** 输入 Token 使用量 */ + public static final AttributeKey INPUT_TOKENS = AttributeKey.longKey("gen_ai.usage.input_tokens"); + /** 输出 Token 使用量 */ + public static final AttributeKey OUTPUT_TOKENS = AttributeKey.longKey("gen_ai.usage.output_tokens"); + /** 输入消息 */ + public static final AttributeKey INPUT_MESSAGES = AttributeKey.stringKey("gen_ai.input.messages"); + /** 输出消息 */ + public static final AttributeKey OUTPUT_MESSAGES = AttributeKey.stringKey("gen_ai.output.messages"); + + private GenAIAttributes() {} + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/DefaultObservationState.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/DefaultObservationState.java new file mode 100644 index 0000000..7e64a46 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/DefaultObservationState.java @@ -0,0 +1,112 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 默认的观测状态存储实现 + *

    + * 使用 ConcurrentHashMap 存储观测数据,保证线程安全。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class DefaultObservationState implements ObservationState { + + private final ConcurrentHashMap data = new ConcurrentHashMap<>(); + + public DefaultObservationState() { + } + + /** + * 从现有Map创建ObservationState + * + * @param initialData 初始数据 + */ + public DefaultObservationState(Map initialData) { + if (initialData != null) { + data.putAll(initialData); + } + } + + @Override + public void put(String key, Object value) { + if (key != null && value != null) { + data.put(key, value); + } + } + + @Override + public void putAll(Map dataMap) { + if (dataMap != null) { + dataMap.forEach((key, value) -> { + if (key != null && value != null) { + data.put(key, value); + } + }); + } + } + + @Override + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) data.get(key); + } + + @Override + @SuppressWarnings("unchecked") + public T getOrDefault(String key, T defaultValue) { + Object value = data.get(key); + return value != null ? (T) value : defaultValue; + } + + @Override + public boolean contains(String key) { + return data.containsKey(key); + } + + @Override + public Object remove(String key) { + return data.remove(key); + } + + @Override + public Map getAll() { + return Collections.unmodifiableMap(data); + } + + @Override + public void clear() { + data.clear(); + } + + @Override + public int size() { + return data.size(); + } + + @Override + public String toString() { + return "DefaultObservationState{" + + "size=" + data.size() + + ", keys=" + data.keySet() + + '}'; + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/HookObservationHelper.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/HookObservationHelper.java new file mode 100644 index 0000000..ed807d1 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/HookObservationHelper.java @@ -0,0 +1,303 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation; + +import com.alibaba.assistant.agent.core.observation.context.HookObservationContext; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * Hook 观测辅助工具类 + *

    + * 提供便捷的方法在 Hook 中创建和管理 OpenTelemetry Span。 + *

    + * 已从 Micrometer Observation 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public final class HookObservationHelper { + + private static final Logger log = LoggerFactory.getLogger(HookObservationHelper.class); + + private HookObservationHelper() { + // Utility class + } + + /** + * 为 Hook 执行创建观测并执行操作 + * + * @param tracer OpenTelemetry Tracer + * @param hookName Hook名称 + * @param hookPosition Hook位置(before/after) + * @param sessionId 会话ID + * @param action 要执行的操作 + * @param 返回类型 + * @return 操作结果 + */ + public static T observeHook( + Tracer tracer, + String hookName, + String hookPosition, + String sessionId, + Supplier action) { + + return observeHook(tracer, hookName, hookPosition, sessionId, null, action); + } + + /** + * 为 Hook 执行创建观测并执行操作(带自定义数据) + * + * @param tracer OpenTelemetry Tracer + * @param hookName Hook名称 + * @param hookPosition Hook位置(before/after) + * @param sessionId 会话ID + * @param customData 自定义数据 + * @param action 要执行的操作 + * @param 返回类型 + * @return 操作结果 + */ + public static T observeHook( + Tracer tracer, + String hookName, + String hookPosition, + String sessionId, + Map customData, + Supplier action) { + + if (tracer == null) { + return action.get(); + } + + HookObservationContext context = new HookObservationContext(hookName, hookPosition); + context.setSessionId(sessionId); + if (customData != null) { + context.putAllCustomData(customData); + } + + long startTime = System.currentTimeMillis(); + String spanName = "codeact.hook." + (hookName != null ? hookName.toLowerCase() : "unknown"); + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("gen_ai.conversation.id", sessionId != null ? sessionId : "unknown") + .setAttribute("gen_ai.span_kind_name", "CHAIN") + .setAttribute("gen_ai.operation.name", "chain") + .setAttribute("codeact.hook.name", hookName != null ? hookName : "unknown") + .setAttribute("codeact.hook.position", hookPosition != null ? hookPosition : "unknown") + .startSpan(); + + // 添加自定义数据到 Span + if (customData != null) { + customData.forEach((key, value) -> { + if (value != null) { + span.setAttribute("codeact.hook.custom." + key, truncate(value.toString(), 200)); + } + }); + } + + try (Scope ignored = span.makeCurrent()) { + T result = action.get(); + + long durationMs = System.currentTimeMillis() - startTime; + context.setDurationMs(durationMs); + context.setSuccess(true); + + span.setAttribute("duration.ms", durationMs); + + log.debug("HookObservationHelper#observeHook - reason=Hook执行成功, " + + "hookName={}, hookPosition={}, durationMs={}", hookName, hookPosition, durationMs); + + return result; + + } catch (Exception e) { + long durationMs = System.currentTimeMillis() - startTime; + context.setDurationMs(durationMs); + context.setSuccess(false); + context.setErrorType(e.getClass().getSimpleName()); + context.setErrorMessage(e.getMessage()); + + span.setAttribute("duration.ms", durationMs); + span.setStatus(StatusCode.ERROR, e.getMessage()); + span.recordException(e); + + log.warn("HookObservationHelper#observeHook - reason=Hook执行失败, " + + "hookName={}, hookPosition={}, durationMs={}, errorType={}", + hookName, hookPosition, durationMs, e.getClass().getSimpleName()); + + throw e; + + } finally { + span.end(); + } + } + + /** + * 为异步 Hook 执行创建观测上下文(用于手动管理 Span 生命周期) + * + * @param tracer OpenTelemetry Tracer + * @param hookName Hook名称 + * @param hookPosition Hook位置 + * @param sessionId 会话ID + * @return Span 和 Context 的包装对象 + */ + public static HookObservationScope startHookObservation( + Tracer tracer, + String hookName, + String hookPosition, + String sessionId) { + + if (tracer == null) { + return new HookObservationScope(null, null, null); + } + + HookObservationContext context = new HookObservationContext(hookName, hookPosition); + context.setSessionId(sessionId); + + String spanName = "codeact.hook." + (hookName != null ? hookName.toLowerCase() : "unknown"); + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("gen_ai.conversation.id", sessionId != null ? sessionId : "unknown") + .setAttribute("gen_ai.span_kind_name", "CHAIN") + .setAttribute("gen_ai.operation.name", "chain") + .setAttribute("codeact.hook.name", hookName != null ? hookName : "unknown") + .setAttribute("codeact.hook.position", hookPosition != null ? hookPosition : "unknown") + .startSpan(); + + Scope scope = span.makeCurrent(); + + return new HookObservationScope(span, scope, context); + } + + /** + * 截断字符串 + */ + private static String truncate(String str, int maxLength) { + if (str == null) { + return "null"; + } + if (str.length() <= maxLength) { + return str; + } + return str.substring(0, maxLength) + "..."; + } + + /** + * Hook 观测作用域 + *

    + * 用于手动管理 Span 的生命周期,适用于异步场景。 + */ + public static class HookObservationScope implements AutoCloseable { + private final Span span; + private final Scope scope; + private final HookObservationContext context; + private final long startTime; + + HookObservationScope(Span span, Scope scope, HookObservationContext context) { + this.span = span; + this.scope = scope; + this.context = context; + this.startTime = System.currentTimeMillis(); + } + + /** + * 添加自定义数据 + */ + public HookObservationScope putCustomData(String key, Object value) { + if (context != null && key != null && value != null) { + context.putCustomData(key, value); + if (span != null) { + span.setAttribute("codeact.hook.custom." + key, truncate(value.toString(), 200)); + } + } + return this; + } + + /** + * 标记成功并关闭 + */ + public void success() { + if (span != null && context != null) { + long durationMs = System.currentTimeMillis() - startTime; + context.setDurationMs(durationMs); + context.setSuccess(true); + span.setAttribute("duration.ms", durationMs); + } + close(); + } + + /** + * 标记失败并关闭 + */ + public void failure(Throwable error) { + if (span != null && context != null) { + long durationMs = System.currentTimeMillis() - startTime; + context.setDurationMs(durationMs); + context.setSuccess(false); + if (error != null) { + context.setErrorType(error.getClass().getSimpleName()); + context.setErrorMessage(error.getMessage()); + span.setStatus(StatusCode.ERROR, error.getMessage()); + span.recordException(error); + } + span.setAttribute("duration.ms", durationMs); + } + close(); + } + + @Override + public void close() { + if (span != null) { + span.end(); + } + if (scope != null) { + scope.close(); + } + } + + /** + * 获取上下文 + */ + public HookObservationContext getContext() { + return context; + } + + /** + * 获取 Span + */ + public Span getSpan() { + return span; + } + + /** + * 检查是否为有效的 Scope + */ + public boolean isValid() { + return span != null; + } + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/InterceptorObservationHelper.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/InterceptorObservationHelper.java new file mode 100644 index 0000000..8bf93c7 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/InterceptorObservationHelper.java @@ -0,0 +1,186 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation; + +import com.alibaba.assistant.agent.core.observation.context.InterceptorObservationContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Supplier; + +/** + * Interceptor 观测辅助工具类 + *

    + * 提供便捷的方法在 ModelInterceptor 和 ToolInterceptor 中创建和管理 OpenTelemetry Span。 + *

    + * 已从 Micrometer Observation 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public final class InterceptorObservationHelper { + + private static final Logger log = LoggerFactory.getLogger(InterceptorObservationHelper.class); + + private InterceptorObservationHelper() { + // Utility class + } + + /** + * 为 ModelInterceptor 执行创建观测并执行操作 + * + * @param tracer OpenTelemetry Tracer + * @param interceptorName Interceptor名称 + * @param sessionId 会话ID + * @param modelName 模型名称 + * @param action 要执行的操作 + * @param 返回类型 + * @return 操作结果 + */ + public static T observeModelInterceptor( + Tracer tracer, + String interceptorName, + String sessionId, + String modelName, + Supplier action) { + + InterceptorObservationContext context = new InterceptorObservationContext( + interceptorName, InterceptorObservationContext.InterceptorType.MODEL); + context.setSessionId(sessionId); + context.setModelName(modelName); + + return observeInterceptor(tracer, context, action); + } + + /** + * 为 ToolInterceptor 执行创建观测并执行操作 + * + * @param tracer OpenTelemetry Tracer + * @param interceptorName Interceptor名称 + * @param sessionId 会话ID + * @param toolName 工具名称 + * @param toolArguments 工具参数 + * @param action 要执行的操作 + * @param 返回类型 + * @return 操作结果 + */ + public static T observeToolInterceptor( + Tracer tracer, + String interceptorName, + String sessionId, + String toolName, + String toolArguments, + Supplier action) { + + InterceptorObservationContext context = new InterceptorObservationContext( + interceptorName, InterceptorObservationContext.InterceptorType.TOOL); + context.setSessionId(sessionId); + context.setToolName(toolName); + context.setToolArguments(toolArguments); + + return observeInterceptor(tracer, context, action); + } + + /** + * 通用的 Interceptor 观测执行方法 + */ + private static T observeInterceptor( + Tracer tracer, + InterceptorObservationContext context, + Supplier action) { + + if (tracer == null) { + return action.get(); + } + + long startTime = System.currentTimeMillis(); + String spanName = buildSpanName(context); + + SpanKind spanKind = context.getInterceptorType() == InterceptorObservationContext.InterceptorType.MODEL + ? SpanKind.CLIENT + : SpanKind.INTERNAL; + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(spanKind) + .setAttribute("gen_ai.conversation.id", + context.getSessionId() != null ? context.getSessionId() : "unknown") + .setAttribute("gen_ai.span_kind_name", + context.getInterceptorType() == InterceptorObservationContext.InterceptorType.MODEL + ? "LLM" : "TOOL") + .setAttribute("gen_ai.operation.name", + context.getInterceptorType() == InterceptorObservationContext.InterceptorType.MODEL + ? "chat" : "execute_tool") + .setAttribute("codeact.interceptor.name", + context.getInterceptorName() != null ? context.getInterceptorName() : "unknown") + .setAttribute("codeact.interceptor.type", + context.getInterceptorType() != null ? context.getInterceptorType().name() : "unknown") + .startSpan(); + + try (Scope ignored = span.makeCurrent()) { + T result = action.get(); + + long durationMs = System.currentTimeMillis() - startTime; + context.setDurationMs(durationMs); + context.setSuccess(true); + + span.setAttribute("duration.ms", durationMs); + + log.debug("InterceptorObservationHelper#observeInterceptor - reason=Interceptor执行成功, " + + "interceptorName={}, durationMs={}", context.getInterceptorName(), durationMs); + + return result; + + } catch (Exception e) { + long durationMs = System.currentTimeMillis() - startTime; + context.setDurationMs(durationMs); + context.setSuccess(false); + context.setErrorType(e.getClass().getSimpleName()); + context.setErrorMessage(e.getMessage()); + + span.setAttribute("duration.ms", durationMs); + span.setStatus(StatusCode.ERROR, e.getMessage()); + span.recordException(e); + + log.warn("InterceptorObservationHelper#observeInterceptor - reason=Interceptor执行失败, " + + "interceptorName={}, durationMs={}, errorType={}", + context.getInterceptorName(), durationMs, e.getClass().getSimpleName()); + + throw e; + + } finally { + span.end(); + } + } + + /** + * 构建 Span 名称 + */ + private static String buildSpanName(InterceptorObservationContext context) { + String typeName = context.getInterceptorType() != null + ? context.getInterceptorType().name().toLowerCase() + : "unknown"; + String interceptorName = context.getInterceptorName() != null + ? context.getInterceptorName().toLowerCase() + : "unknown"; + return "codeact.interceptor." + typeName + "." + interceptorName; + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/ObservationState.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/ObservationState.java new file mode 100644 index 0000000..6370715 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/ObservationState.java @@ -0,0 +1,130 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation; + +import java.util.Map; + +/** + * 观测状态存储接口 + *

    + * 允许 Hook 和 Interceptor 在执行过程中注册自定义观测数据, + * 这些数据会被收集并记录到 Observation 中。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public interface ObservationState { + + /** + * 状态Key常量:当前轮次任务ID + */ + String KEY_CURRENT_ROUND_TASK_ID = "current_round_task_id"; + + /** + * 状态Key常量:用户输入 + */ + String KEY_INPUT = "input"; + + /** + * 状态Key常量:消息历史 + */ + String KEY_MESSAGES = "messages"; + + /** + * 状态Key常量:会话ID + */ + String KEY_SESSION_ID = "session_id"; + + /** + * 状态Key常量:租户ID + */ + String KEY_TENANT_ID = "tenant_id"; + + /** + * 状态Key常量:用户ID + */ + String KEY_USER_ID = "user_id"; + + /** + * 注册观测数据 + * + * @param key 数据键,建议使用有意义的前缀(如 hook.xxx 或 interceptor.xxx) + * @param value 数据值 + */ + void put(String key, Object value); + + /** + * 批量注册观测数据 + * + * @param data 数据Map + */ + void putAll(Map data); + + /** + * 获取观测数据 + * + * @param key 数据键 + * @param 期望的类型 + * @return 数据值,如果不存在返回null + */ + T get(String key); + + /** + * 获取观测数据,如果不存在则返回默认值 + * + * @param key 数据键 + * @param defaultValue 默认值 + * @param 期望的类型 + * @return 数据值,如果不存在返回默认值 + */ + T getOrDefault(String key, T defaultValue); + + /** + * 检查是否存在指定键的数据 + * + * @param key 数据键 + * @return 是否存在 + */ + boolean contains(String key); + + /** + * 移除指定键的数据 + * + * @param key 数据键 + * @return 被移除的值,如果不存在返回null + */ + Object remove(String key); + + /** + * 获取所有观测数据 + * + * @return 所有观测数据的不可变视图 + */ + Map getAll(); + + /** + * 清空所有观测数据 + */ + void clear(); + + /** + * 获取数据数量 + * + * @return 数据数量 + */ + int size(); +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/OpenTelemetryObservationHelper.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/OpenTelemetryObservationHelper.java new file mode 100644 index 0000000..851107a --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/OpenTelemetryObservationHelper.java @@ -0,0 +1,393 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation; + +import com.alibaba.assistant.agent.core.observation.context.HookObservationContext; +import com.alibaba.assistant.agent.core.observation.context.InterceptorObservationContext; +import com.alibaba.assistant.agent.core.observation.context.ReactPhaseObservationContext; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * OpenTelemetry 可观测性辅助类 + *

    + * 提供统一的 Span 创建和管理能力,替代原有的 Micrometer Observation 实现。 + *

    + * 主要功能: + *

      + *
    • Hook 执行的 Span 创建和管理
    • + *
    • Interceptor 执行的 Span 创建和管理
    • + *
    • React 阶段(LlmNode/ToolNode)的 Span 创建和管理
    • + *
    • Context 传播和 Scope 管理
    • + *
    + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class OpenTelemetryObservationHelper { + + private static final Logger log = LoggerFactory.getLogger(OpenTelemetryObservationHelper.class); + + private final Tracer tracer; + + /** + * 存储活跃的 Span(按唯一标识) + */ + private final ConcurrentHashMap activeSpans = new ConcurrentHashMap<>(); + + public OpenTelemetryObservationHelper(Tracer tracer) { + this.tracer = tracer; + log.info("OpenTelemetryObservationHelper# - reason=初始化完成"); + } + + // ==================== Hook Span ==================== + + /** + * 开始 Hook Span + * + * @param spanKey 唯一标识(如 sessionId:hookName) + * @param context Hook观测上下文 + * @return 创建的 Span + */ + public Span startHookSpan(String spanKey, HookObservationContext context) { + String spanName = "codeact.hook." + (context.getHookName() != null + ? context.getHookName().toLowerCase() + : "unknown"); + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(SpanKind.INTERNAL) + .setAllAttributes(context.toAttributes()) + .startSpan(); + + Scope scope = span.makeCurrent(); + activeSpans.put(spanKey, new SpanHolder(span, scope)); + + log.debug("OpenTelemetryObservationHelper#startHookSpan - reason=开始Hook Span, " + + "spanKey={}, hookName={}", spanKey, context.getHookName()); + + return span; + } + + /** + * 结束 Hook Span + * + * @param spanKey 唯一标识 + * @param context 更新后的上下文 + * @param error 异常(可选) + */ + public void endHookSpan(String spanKey, HookObservationContext context, Throwable error) { + SpanHolder holder = activeSpans.remove(spanKey); + if (holder == null) { + log.warn("OpenTelemetryObservationHelper#endHookSpan - reason=未找到Span, spanKey={}", spanKey); + return; + } + + try { + Span span = holder.span; + + // 添加结束时的属性 + if (context.getDurationMs() > 0) { + span.setAttribute("duration.ms", context.getDurationMs()); + } + span.setAttribute("codeact.hook.success", context.isSuccess()); + + if (error != null) { + span.setStatus(StatusCode.ERROR, error.getMessage()); + span.recordException(error); + } else if (!context.isSuccess()) { + span.setStatus(StatusCode.ERROR, context.getErrorMessage()); + } + + span.end(); + } finally { + holder.scope.close(); + } + + log.debug("OpenTelemetryObservationHelper#endHookSpan - reason=结束Hook Span, " + + "spanKey={}, success={}", spanKey, context.isSuccess()); + } + + // ==================== Interceptor Span ==================== + + /** + * 开始 Interceptor Span + * + * @param spanKey 唯一标识 + * @param context Interceptor观测上下文 + * @return 创建的 Span + */ + public Span startInterceptorSpan(String spanKey, InterceptorObservationContext context) { + String typeName = context.getInterceptorType() != null + ? context.getInterceptorType().name().toLowerCase() + : "unknown"; + String spanName = "codeact.interceptor." + typeName + "." + + (context.getInterceptorName() != null ? context.getInterceptorName().toLowerCase() : "unknown"); + + SpanKind kind = context.getInterceptorType() == InterceptorObservationContext.InterceptorType.MODEL + ? SpanKind.CLIENT + : SpanKind.INTERNAL; + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(kind) + .setAllAttributes(context.toAttributes()) + .startSpan(); + + Scope scope = span.makeCurrent(); + activeSpans.put(spanKey, new SpanHolder(span, scope)); + + log.debug("OpenTelemetryObservationHelper#startInterceptorSpan - reason=开始Interceptor Span, " + + "spanKey={}, interceptorName={}", spanKey, context.getInterceptorName()); + + return span; + } + + /** + * 结束 Interceptor Span + * + * @param spanKey 唯一标识 + * @param context 更新后的上下文 + * @param error 异常(可选) + */ + public void endInterceptorSpan(String spanKey, InterceptorObservationContext context, Throwable error) { + SpanHolder holder = activeSpans.remove(spanKey); + if (holder == null) { + log.warn("OpenTelemetryObservationHelper#endInterceptorSpan - reason=未找到Span, spanKey={}", spanKey); + return; + } + + try { + Span span = holder.span; + + // 添加结束时的属性 + if (context.getDurationMs() > 0) { + span.setAttribute("duration.ms", context.getDurationMs()); + } + span.setAttribute("codeact.interceptor.success", context.isSuccess()); + + // Token usage for model interceptors + if (context.getInputTokens() > 0) { + span.setAttribute("gen_ai.usage.input_tokens", (long) context.getInputTokens()); + } + if (context.getOutputTokens() > 0) { + span.setAttribute("gen_ai.usage.output_tokens", (long) context.getOutputTokens()); + } + + if (error != null) { + span.setStatus(StatusCode.ERROR, error.getMessage()); + span.recordException(error); + } else if (!context.isSuccess()) { + span.setStatus(StatusCode.ERROR, context.getErrorMessage()); + } + + span.end(); + } finally { + holder.scope.close(); + } + + log.debug("OpenTelemetryObservationHelper#endInterceptorSpan - reason=结束Interceptor Span, " + + "spanKey={}, success={}", spanKey, context.isSuccess()); + } + + // ==================== React Phase Span ==================== + + /** + * 开始 React Phase Span + * + * @param spanKey 唯一标识 + * @param context React阶段观测上下文 + * @return 创建的 Span + */ + public Span startReactPhaseSpan(String spanKey, ReactPhaseObservationContext context) { + String nodeTypeName = context.getNodeType() != null + ? context.getNodeType().name().toLowerCase() + : "unknown"; + String spanName = "codeact.react." + nodeTypeName; + + SpanKind kind = context.getNodeType() == ReactPhaseObservationContext.NodeType.LLM + ? SpanKind.CLIENT + : SpanKind.INTERNAL; + + Span span = tracer.spanBuilder(spanName) + .setSpanKind(kind) + .setAllAttributes(context.toAttributes()) + .startSpan(); + + Scope scope = span.makeCurrent(); + activeSpans.put(spanKey, new SpanHolder(span, scope)); + + log.debug("OpenTelemetryObservationHelper#startReactPhaseSpan - reason=开始React Phase Span, " + + "spanKey={}, nodeType={}", spanKey, nodeTypeName); + + return span; + } + + /** + * 结束 React Phase Span + * + * @param spanKey 唯一标识 + * @param context 更新后的上下文 + * @param error 异常(可选) + */ + public void endReactPhaseSpan(String spanKey, ReactPhaseObservationContext context, Throwable error) { + SpanHolder holder = activeSpans.remove(spanKey); + if (holder == null) { + log.warn("OpenTelemetryObservationHelper#endReactPhaseSpan - reason=未找到Span, spanKey={}", spanKey); + return; + } + + try { + Span span = holder.span; + + // 添加结束时的属性 + if (context.getDurationMs() > 0) { + span.setAttribute("duration.ms", context.getDurationMs()); + } + span.setAttribute("codeact.react.success", context.isSuccess()); + + // Token usage for LLM nodes + if (context.getInputTokens() > 0) { + span.setAttribute("gen_ai.usage.input_tokens", (long) context.getInputTokens()); + } + if (context.getOutputTokens() > 0) { + span.setAttribute("gen_ai.usage.output_tokens", (long) context.getOutputTokens()); + } + if (context.getFinishReason() != null) { + span.setAttribute("gen_ai.response.finish_reasons", context.getFinishReason()); + } + + if (error != null) { + span.setStatus(StatusCode.ERROR, error.getMessage()); + span.recordException(error); + } else if (!context.isSuccess()) { + span.setStatus(StatusCode.ERROR, context.getErrorMessage()); + } + + span.end(); + } finally { + holder.scope.close(); + } + + log.debug("OpenTelemetryObservationHelper#endReactPhaseSpan - reason=结束React Phase Span, " + + "spanKey={}, success={}", spanKey, context.isSuccess()); + } + + // ==================== Generic Span Operations ==================== + + /** + * 在指定的 Span 作用域内执行操作 + * + * @param spanName Span名称 + * @param attributes Span属性 + * @param action 要执行的操作 + * @param 返回值类型 + * @return 操作结果 + */ + public T withSpan(String spanName, Attributes attributes, Supplier action) { + Span span = tracer.spanBuilder(spanName) + .setSpanKind(SpanKind.INTERNAL) + .setAllAttributes(attributes) + .startSpan(); + + try (Scope ignored = span.makeCurrent()) { + T result = action.get(); + return result; + } catch (Exception e) { + span.setStatus(StatusCode.ERROR, e.getMessage()); + span.recordException(e); + throw e; + } finally { + span.end(); + } + } + + /** + * 在指定的 Span 作用域内执行操作(无返回值) + * + * @param spanName Span名称 + * @param attributes Span属性 + * @param action 要执行的操作 + */ + public void withSpanVoid(String spanName, Attributes attributes, Runnable action) { + Span span = tracer.spanBuilder(spanName) + .setSpanKind(SpanKind.INTERNAL) + .setAllAttributes(attributes) + .startSpan(); + + try (Scope ignored = span.makeCurrent()) { + action.run(); + } catch (Exception e) { + span.setStatus(StatusCode.ERROR, e.getMessage()); + span.recordException(e); + throw e; + } finally { + span.end(); + } + } + + /** + * 获取当前活跃的 Span 数量 + */ + public int getActiveSpanCount() { + return activeSpans.size(); + } + + /** + * 清理指定 session 相关的所有 Span + * + * @param sessionIdPrefix session前缀 + */ + public void cleanupSession(String sessionIdPrefix) { + activeSpans.entrySet().removeIf(entry -> { + if (entry.getKey().startsWith(sessionIdPrefix)) { + SpanHolder holder = entry.getValue(); + try { + holder.span.end(); + } finally { + holder.scope.close(); + } + log.debug("OpenTelemetryObservationHelper#cleanupSession - reason=清理Span, spanKey={}", + entry.getKey()); + return true; + } + return false; + }); + } + + /** + * Span 持有者,包含 Span 和对应的 Scope + */ + private static class SpanHolder { + final Span span; + final Scope scope; + + SpanHolder(Span span, Scope scope) { + this.span = span; + this.scope = scope; + } + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeGenerationObservationContext.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeGenerationObservationContext.java new file mode 100644 index 0000000..d933ce9 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeGenerationObservationContext.java @@ -0,0 +1,185 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation.context; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +import static com.alibaba.assistant.agent.core.observation.CodeactObservationDocumentation.CodeGenerationAttributes; + +/** + * Codeact 代码生成观测上下文 + *

    + * 存储代码生成过程中的观测数据,包括模型、函数名、Token使用情况等。 + *

    + * 已从 Micrometer Observation.Context 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class CodeGenerationObservationContext { + + private String functionName; + private String language; + private String modelName; + private int codeLines; + private int inputTokens; + private int outputTokens; + private long durationMs; + private boolean success; + private String errorType; + private String errorMessage; + + public CodeGenerationObservationContext() { + } + + public CodeGenerationObservationContext(String language, String modelName) { + this.language = language; + this.modelName = modelName; + } + + // Getters and Setters + + public String getFunctionName() { + return functionName; + } + + public CodeGenerationObservationContext setFunctionName(String functionName) { + this.functionName = functionName; + return this; + } + + public String getLanguage() { + return language; + } + + public CodeGenerationObservationContext setLanguage(String language) { + this.language = language; + return this; + } + + public String getModelName() { + return modelName; + } + + public CodeGenerationObservationContext setModelName(String modelName) { + this.modelName = modelName; + return this; + } + + public int getCodeLines() { + return codeLines; + } + + public CodeGenerationObservationContext setCodeLines(int codeLines) { + this.codeLines = codeLines; + return this; + } + + public int getInputTokens() { + return inputTokens; + } + + public CodeGenerationObservationContext setInputTokens(int inputTokens) { + this.inputTokens = inputTokens; + return this; + } + + public int getOutputTokens() { + return outputTokens; + } + + public CodeGenerationObservationContext setOutputTokens(int outputTokens) { + this.outputTokens = outputTokens; + return this; + } + + public long getDurationMs() { + return durationMs; + } + + public CodeGenerationObservationContext setDurationMs(long durationMs) { + this.durationMs = durationMs; + return this; + } + + public boolean isSuccess() { + return success; + } + + public CodeGenerationObservationContext setSuccess(boolean success) { + this.success = success; + return this; + } + + public String getErrorType() { + return errorType; + } + + public CodeGenerationObservationContext setErrorType(String errorType) { + this.errorType = errorType; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public CodeGenerationObservationContext setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + /** + * 构建 OpenTelemetry Attributes + * + * @return Attributes + */ + public Attributes toAttributes() { + AttributesBuilder builder = Attributes.builder(); + + if (language != null) { + builder.put(CodeGenerationAttributes.LANGUAGE, language); + } + if (modelName != null) { + builder.put(CodeGenerationAttributes.MODEL_NAME, modelName); + } + builder.put(CodeGenerationAttributes.SUCCESS, success); + if (functionName != null) { + builder.put(CodeGenerationAttributes.FUNCTION_NAME, functionName); + } + builder.put(CodeGenerationAttributes.CODE_LINES, (long) codeLines); + builder.put(CodeGenerationAttributes.INPUT_TOKENS, (long) inputTokens); + builder.put(CodeGenerationAttributes.OUTPUT_TOKENS, (long) outputTokens); + builder.put(CodeGenerationAttributes.DURATION_MS, durationMs); + + return builder.build(); + } + + @Override + public String toString() { + return "CodeGenerationObservationContext{" + + "functionName='" + functionName + '\'' + + ", language='" + language + '\'' + + ", modelName='" + modelName + '\'' + + ", codeLines=" + codeLines + + ", inputTokens=" + inputTokens + + ", outputTokens=" + outputTokens + + ", durationMs=" + durationMs + + ", success=" + success + + '}'; + } +} diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactExecutionObservationContext.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactExecutionObservationContext.java new file mode 100644 index 0000000..0a0ee2e --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactExecutionObservationContext.java @@ -0,0 +1,178 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation.context; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +import static com.alibaba.assistant.agent.core.observation.CodeactObservationDocumentation.ExecutionAttributes; + +/** + * Codeact 代码执行观测上下文 + *

    + * 存储代码执行过程中的观测数据,包括函数名、参数、结果等。 + *

    + * 已从 Micrometer Observation.Context 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class CodeactExecutionObservationContext { + + private String functionName; + private String language; + private String arguments; + private int argumentsLength; + private String result; + private int resultLength; + private long durationMs; + private boolean success; + private String errorType; + private String errorMessage; + + public CodeactExecutionObservationContext() { + } + + public CodeactExecutionObservationContext(String functionName, String language) { + this.functionName = functionName; + this.language = language; + } + + // Getters and Setters + + public String getFunctionName() { + return functionName; + } + + public CodeactExecutionObservationContext setFunctionName(String functionName) { + this.functionName = functionName; + return this; + } + + public String getLanguage() { + return language; + } + + public CodeactExecutionObservationContext setLanguage(String language) { + this.language = language; + return this; + } + + public String getArguments() { + return arguments; + } + + public CodeactExecutionObservationContext setArguments(String arguments) { + this.arguments = arguments; + this.argumentsLength = arguments != null ? arguments.length() : 0; + return this; + } + + public int getArgumentsLength() { + return argumentsLength; + } + + public String getResult() { + return result; + } + + public CodeactExecutionObservationContext setResult(String result) { + this.result = result; + this.resultLength = result != null ? result.length() : 0; + return this; + } + + public int getResultLength() { + return resultLength; + } + + public long getDurationMs() { + return durationMs; + } + + public CodeactExecutionObservationContext setDurationMs(long durationMs) { + this.durationMs = durationMs; + return this; + } + + public boolean isSuccess() { + return success; + } + + public CodeactExecutionObservationContext setSuccess(boolean success) { + this.success = success; + return this; + } + + public String getErrorType() { + return errorType; + } + + public CodeactExecutionObservationContext setErrorType(String errorType) { + this.errorType = errorType; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public CodeactExecutionObservationContext setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + /** + * 构建 OpenTelemetry Attributes + * + * @return Attributes + */ + public Attributes toAttributes() { + AttributesBuilder builder = Attributes.builder(); + + if (language != null) { + builder.put(ExecutionAttributes.LANGUAGE, language); + } + builder.put(ExecutionAttributes.SUCCESS, success); + if (functionName != null) { + builder.put(ExecutionAttributes.FUNCTION_NAME, functionName); + } + builder.put(ExecutionAttributes.ARGUMENTS_LENGTH, (long) argumentsLength); + builder.put(ExecutionAttributes.RESULT_LENGTH, (long) resultLength); + builder.put(ExecutionAttributes.DURATION_MS, durationMs); + if (errorType != null) { + builder.put(ExecutionAttributes.ERROR_TYPE, errorType); + } + if (errorMessage != null) { + builder.put(ExecutionAttributes.ERROR_MESSAGE, errorMessage); + } + + return builder.build(); + } + + @Override + public String toString() { + return "CodeactExecutionObservationContext{" + + "functionName='" + functionName + '\'' + + ", language='" + language + '\'' + + ", argumentsLength=" + argumentsLength + + ", resultLength=" + resultLength + + ", durationMs=" + durationMs + + ", success=" + success + + ", errorType='" + errorType + '\'' + + '}'; + } +} diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactToolCallObservationContext.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactToolCallObservationContext.java new file mode 100644 index 0000000..f999605 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/CodeactToolCallObservationContext.java @@ -0,0 +1,190 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation.context; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +import static com.alibaba.assistant.agent.core.observation.CodeactObservationDocumentation.ToolCallAttributes; +import static com.alibaba.assistant.agent.core.observation.CodeactObservationDocumentation.GenAIAttributes; + +/** + * Codeact 工具调用观测上下文 + *

    + * 存储在代码执行过程中调用工具的观测数据。 + *

    + * 已从 Micrometer Observation.Context 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class CodeactToolCallObservationContext { + + private String sessionId; + private String toolName; + private String arguments; + private int argumentsLength; + private String result; + private int resultLength; + private long durationMs; + private boolean success; + private String errorType; + private String errorMessage; + + public CodeactToolCallObservationContext() { + } + + public CodeactToolCallObservationContext(String toolName) { + this.toolName = toolName; + } + + // Getters and Setters + + public String getSessionId() { + return sessionId; + } + + public CodeactToolCallObservationContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + public String getToolName() { + return toolName; + } + + public CodeactToolCallObservationContext setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + public String getArguments() { + return arguments; + } + + public CodeactToolCallObservationContext setArguments(String arguments) { + this.arguments = arguments; + this.argumentsLength = arguments != null ? arguments.length() : 0; + return this; + } + + public int getArgumentsLength() { + return argumentsLength; + } + + public String getResult() { + return result; + } + + public CodeactToolCallObservationContext setResult(String result) { + this.result = result; + this.resultLength = result != null ? result.length() : 0; + return this; + } + + public int getResultLength() { + return resultLength; + } + + public long getDurationMs() { + return durationMs; + } + + public CodeactToolCallObservationContext setDurationMs(long durationMs) { + this.durationMs = durationMs; + return this; + } + + public boolean isSuccess() { + return success; + } + + public CodeactToolCallObservationContext setSuccess(boolean success) { + this.success = success; + return this; + } + + public String getErrorType() { + return errorType; + } + + public CodeactToolCallObservationContext setErrorType(String errorType) { + this.errorType = errorType; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public CodeactToolCallObservationContext setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + /** + * 构建 OpenTelemetry Attributes + * + * @return Attributes + */ + public Attributes toAttributes() { + AttributesBuilder builder = Attributes.builder(); + + if (sessionId != null) { + builder.put(GenAIAttributes.CONVERSATION_ID, sessionId); + } + if (toolName != null) { + builder.put(ToolCallAttributes.TOOL_NAME, toolName); + } + builder.put(ToolCallAttributes.SUCCESS, success); + if (arguments != null) { + builder.put(ToolCallAttributes.ARGUMENTS, truncate(arguments, 500)); + } + builder.put(ToolCallAttributes.ARGUMENTS_LENGTH, (long) argumentsLength); + builder.put(ToolCallAttributes.RESULT_LENGTH, (long) resultLength); + builder.put(ToolCallAttributes.DURATION_MS, durationMs); + if (errorType != null) { + builder.put(ToolCallAttributes.ERROR_TYPE, errorType); + } + + return builder.build(); + } + + /** + * 截断字符串 + */ + private String truncate(String str, int maxLength) { + if (str == null) { + return null; + } + if (str.length() <= maxLength) { + return str; + } + return str.substring(0, maxLength) + "...[truncated]"; + } + + @Override + public String toString() { + return "CodeactToolCallObservationContext{" + + "toolName='" + toolName + '\'' + + ", argumentsLength=" + argumentsLength + + ", resultLength=" + resultLength + + ", durationMs=" + durationMs + + ", success=" + success + + ", errorType='" + errorType + '\'' + + '}'; + } +} diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/HookObservationContext.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/HookObservationContext.java new file mode 100644 index 0000000..99cd345 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/HookObservationContext.java @@ -0,0 +1,267 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation.context; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +import java.util.HashMap; +import java.util.Map; + +/** + * Hook 观测上下文 + *

    + * 存储 Hook 执行过程中的观测数据,支持自定义数据注册。 + *

    + * 已从 Micrometer Observation.Context 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class HookObservationContext { + + // 标准属性键定义 + public static final AttributeKey HOOK_NAME = AttributeKey.stringKey("codeact.hook.name"); + public static final AttributeKey HOOK_POSITION = AttributeKey.stringKey("codeact.hook.position"); + public static final AttributeKey AGENT_NAME = AttributeKey.stringKey("gen_ai.agent.name"); + public static final AttributeKey SESSION_ID = AttributeKey.stringKey("gen_ai.conversation.id"); + public static final AttributeKey DURATION_MS = AttributeKey.longKey("duration.ms"); + public static final AttributeKey SUCCESS = AttributeKey.booleanKey("codeact.hook.success"); + public static final AttributeKey ERROR_TYPE = AttributeKey.stringKey("error.type"); + public static final AttributeKey ERROR_MESSAGE = AttributeKey.stringKey("error.message"); + + private String hookName; + private String hookPosition; + private String agentName; + private String sessionId; + private long durationMs; + private boolean success = true; + private String errorType; + private String errorMessage; + + /** + * 自定义数据存储,允许Hook在执行过程中注册定制数据 + */ + private final Map customData = new HashMap<>(); + + public HookObservationContext() { + } + + public HookObservationContext(String hookName, String hookPosition) { + this.hookName = hookName; + this.hookPosition = hookPosition; + } + + // ==================== Getters and Setters ==================== + + public String getHookName() { + return hookName; + } + + public HookObservationContext setHookName(String hookName) { + this.hookName = hookName; + return this; + } + + public String getHookPosition() { + return hookPosition; + } + + public HookObservationContext setHookPosition(String hookPosition) { + this.hookPosition = hookPosition; + return this; + } + + public String getAgentName() { + return agentName; + } + + public HookObservationContext setAgentName(String agentName) { + this.agentName = agentName; + return this; + } + + public String getSessionId() { + return sessionId; + } + + public HookObservationContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + public long getDurationMs() { + return durationMs; + } + + public HookObservationContext setDurationMs(long durationMs) { + this.durationMs = durationMs; + return this; + } + + public boolean isSuccess() { + return success; + } + + public HookObservationContext setSuccess(boolean success) { + this.success = success; + return this; + } + + public String getErrorType() { + return errorType; + } + + public HookObservationContext setErrorType(String errorType) { + this.errorType = errorType; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public HookObservationContext setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + // ==================== Custom Data Methods ==================== + + /** + * 注册自定义数据 + * + * @param key 数据键 + * @param value 数据值 + * @return this + */ + public HookObservationContext putCustomData(String key, Object value) { + this.customData.put(key, value); + return this; + } + + /** + * 批量注册自定义数据 + * + * @param data 数据Map + * @return this + */ + public HookObservationContext putAllCustomData(Map data) { + if (data != null) { + this.customData.putAll(data); + } + return this; + } + + /** + * 获取自定义数据 + * + * @param key 数据键 + * @return 数据值,如果不存在返回null + */ + @SuppressWarnings("unchecked") + public T getCustomData(String key) { + return (T) this.customData.get(key); + } + + /** + * 获取所有自定义数据 + * + * @return 自定义数据的不可变视图 + */ + public Map getAllCustomData() { + return Map.copyOf(customData); + } + + /** + * 检查是否存在指定键的自定义数据 + * + * @param key 数据键 + * @return 是否存在 + */ + public boolean hasCustomData(String key) { + return customData.containsKey(key); + } + + @Override + public String toString() { + return "HookObservationContext{" + + "hookName='" + hookName + '\'' + + ", hookPosition='" + hookPosition + '\'' + + ", agentName='" + agentName + '\'' + + ", sessionId='" + sessionId + '\'' + + ", durationMs=" + durationMs + + ", success=" + success + + ", errorType='" + errorType + '\'' + + ", customDataKeys=" + customData.keySet() + + '}'; + } + + /** + * 将上下文转换为 OpenTelemetry Attributes + * + * @return Attributes 对象 + */ + public Attributes toAttributes() { + AttributesBuilder builder = Attributes.builder(); + + if (hookName != null) { + builder.put(HOOK_NAME, hookName); + } + if (hookPosition != null) { + builder.put(HOOK_POSITION, hookPosition); + } + if (agentName != null) { + builder.put(AGENT_NAME, agentName); + } + if (sessionId != null) { + builder.put(SESSION_ID, sessionId); + } + if (durationMs > 0) { + builder.put(DURATION_MS, durationMs); + } + builder.put(SUCCESS, success); + if (errorType != null) { + builder.put(ERROR_TYPE, errorType); + } + if (errorMessage != null) { + builder.put(ERROR_MESSAGE, errorMessage); + } + + // 添加自定义数据 + for (Map.Entry entry : customData.entrySet()) { + if (entry.getValue() != null) { + String key = "codeact.hook.custom." + entry.getKey(); + Object value = entry.getValue(); + if (value instanceof String) { + builder.put(AttributeKey.stringKey(key), (String) value); + } else if (value instanceof Long) { + builder.put(AttributeKey.longKey(key), (Long) value); + } else if (value instanceof Double) { + builder.put(AttributeKey.doubleKey(key), (Double) value); + } else if (value instanceof Boolean) { + builder.put(AttributeKey.booleanKey(key), (Boolean) value); + } else { + builder.put(AttributeKey.stringKey(key), String.valueOf(value)); + } + } + } + + return builder.build(); + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/InterceptorObservationContext.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/InterceptorObservationContext.java new file mode 100644 index 0000000..646221c --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/InterceptorObservationContext.java @@ -0,0 +1,389 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation.context; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +import java.util.HashMap; +import java.util.Map; + +/** + * Interceptor 观测上下文 + *

    + * 存储 Interceptor 执行过程中的观测数据,支持自定义数据注册。 + * 适用于 ModelInterceptor 和 ToolInterceptor。 + *

    + * 已从 Micrometer Observation.Context 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class InterceptorObservationContext { + + // 标准属性键定义 + public static final AttributeKey INTERCEPTOR_NAME_KEY = AttributeKey.stringKey("codeact.interceptor.name"); + public static final AttributeKey INTERCEPTOR_TYPE_KEY = AttributeKey.stringKey("codeact.interceptor.type"); + public static final AttributeKey AGENT_NAME_KEY = AttributeKey.stringKey("gen_ai.agent.name"); + public static final AttributeKey SESSION_ID_KEY = AttributeKey.stringKey("gen_ai.conversation.id"); + public static final AttributeKey DURATION_MS_KEY = AttributeKey.longKey("duration.ms"); + public static final AttributeKey SUCCESS_KEY = AttributeKey.booleanKey("codeact.interceptor.success"); + public static final AttributeKey MODEL_NAME_KEY = AttributeKey.stringKey("gen_ai.response.model"); + public static final AttributeKey INPUT_TOKENS_KEY = AttributeKey.longKey("gen_ai.usage.input_tokens"); + public static final AttributeKey OUTPUT_TOKENS_KEY = AttributeKey.longKey("gen_ai.usage.output_tokens"); + public static final AttributeKey TOOL_NAME_KEY = AttributeKey.stringKey("gen_ai.tool.name"); + + /** + * 拦截器类型枚举 + */ + public enum InterceptorType { + MODEL, + TOOL + } + + private String interceptorName; + private InterceptorType interceptorType; + private String agentName; + private String sessionId; + private long durationMs; + private boolean success = true; + private String errorType; + private String errorMessage; + + // Model Interceptor specific fields + private String modelName; + private int inputTokens; + private int outputTokens; + private int messageCount; + + // Tool Interceptor specific fields + private String toolName; + private String toolArguments; + private int toolArgumentsLength; + private String toolResult; + private int toolResultLength; + + /** + * 自定义数据存储,允许Interceptor在执行过程中注册定制数据 + */ + private final Map customData = new HashMap<>(); + + public InterceptorObservationContext() { + } + + public InterceptorObservationContext(String interceptorName, InterceptorType interceptorType) { + this.interceptorName = interceptorName; + this.interceptorType = interceptorType; + } + + // ==================== Common Getters and Setters ==================== + + public String getInterceptorName() { + return interceptorName; + } + + public InterceptorObservationContext setInterceptorName(String interceptorName) { + this.interceptorName = interceptorName; + return this; + } + + public InterceptorType getInterceptorType() { + return interceptorType; + } + + public InterceptorObservationContext setInterceptorType(InterceptorType interceptorType) { + this.interceptorType = interceptorType; + return this; + } + + public String getAgentName() { + return agentName; + } + + public InterceptorObservationContext setAgentName(String agentName) { + this.agentName = agentName; + return this; + } + + public String getSessionId() { + return sessionId; + } + + public InterceptorObservationContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + public long getDurationMs() { + return durationMs; + } + + public InterceptorObservationContext setDurationMs(long durationMs) { + this.durationMs = durationMs; + return this; + } + + public boolean isSuccess() { + return success; + } + + public InterceptorObservationContext setSuccess(boolean success) { + this.success = success; + return this; + } + + public String getErrorType() { + return errorType; + } + + public InterceptorObservationContext setErrorType(String errorType) { + this.errorType = errorType; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public InterceptorObservationContext setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + // ==================== Model Interceptor Getters and Setters ==================== + + public String getModelName() { + return modelName; + } + + public InterceptorObservationContext setModelName(String modelName) { + this.modelName = modelName; + return this; + } + + public int getInputTokens() { + return inputTokens; + } + + public InterceptorObservationContext setInputTokens(int inputTokens) { + this.inputTokens = inputTokens; + return this; + } + + public int getOutputTokens() { + return outputTokens; + } + + public InterceptorObservationContext setOutputTokens(int outputTokens) { + this.outputTokens = outputTokens; + return this; + } + + public int getMessageCount() { + return messageCount; + } + + public InterceptorObservationContext setMessageCount(int messageCount) { + this.messageCount = messageCount; + return this; + } + + // ==================== Tool Interceptor Getters and Setters ==================== + + public String getToolName() { + return toolName; + } + + public InterceptorObservationContext setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + public String getToolArguments() { + return toolArguments; + } + + public InterceptorObservationContext setToolArguments(String toolArguments) { + this.toolArguments = toolArguments; + this.toolArgumentsLength = toolArguments != null ? toolArguments.length() : 0; + return this; + } + + public int getToolArgumentsLength() { + return toolArgumentsLength; + } + + public String getToolResult() { + return toolResult; + } + + public InterceptorObservationContext setToolResult(String toolResult) { + this.toolResult = toolResult; + this.toolResultLength = toolResult != null ? toolResult.length() : 0; + return this; + } + + public int getToolResultLength() { + return toolResultLength; + } + + // ==================== Custom Data Methods ==================== + + /** + * 注册自定义数据 + * + * @param key 数据键 + * @param value 数据值 + * @return this + */ + public InterceptorObservationContext putCustomData(String key, Object value) { + this.customData.put(key, value); + return this; + } + + /** + * 批量注册自定义数据 + * + * @param data 数据Map + * @return this + */ + public InterceptorObservationContext putAllCustomData(Map data) { + if (data != null) { + this.customData.putAll(data); + } + return this; + } + + /** + * 获取自定义数据 + * + * @param key 数据键 + * @return 数据值,如果不存在返回null + */ + @SuppressWarnings("unchecked") + public T getCustomData(String key) { + return (T) this.customData.get(key); + } + + /** + * 获取所有自定义数据 + * + * @return 自定义数据的不可变视图 + */ + public Map getAllCustomData() { + return Map.copyOf(customData); + } + + /** + * 检查是否存在指定键的自定义数据 + * + * @param key 数据键 + * @return 是否存在 + */ + public boolean hasCustomData(String key) { + return customData.containsKey(key); + } + + @Override + public String toString() { + return "InterceptorObservationContext{" + + "interceptorName='" + interceptorName + '\'' + + ", interceptorType=" + interceptorType + + ", agentName='" + agentName + '\'' + + ", sessionId='" + sessionId + '\'' + + ", durationMs=" + durationMs + + ", success=" + success + + ", modelName='" + modelName + '\'' + + ", toolName='" + toolName + '\'' + + ", customDataKeys=" + customData.keySet() + + '}'; + } + + /** + * 将上下文转换为 OpenTelemetry Attributes + * + * @return Attributes 对象 + */ + public Attributes toAttributes() { + AttributesBuilder builder = Attributes.builder(); + + if (interceptorName != null) { + builder.put(INTERCEPTOR_NAME_KEY, interceptorName); + } + if (interceptorType != null) { + builder.put(INTERCEPTOR_TYPE_KEY, interceptorType.name()); + } + if (agentName != null) { + builder.put(AGENT_NAME_KEY, agentName); + } + if (sessionId != null) { + builder.put(SESSION_ID_KEY, sessionId); + } + if (durationMs > 0) { + builder.put(DURATION_MS_KEY, durationMs); + } + builder.put(SUCCESS_KEY, success); + + // Model specific + if (modelName != null) { + builder.put(MODEL_NAME_KEY, modelName); + } + if (inputTokens > 0) { + builder.put(INPUT_TOKENS_KEY, (long) inputTokens); + } + if (outputTokens > 0) { + builder.put(OUTPUT_TOKENS_KEY, (long) outputTokens); + } + + // Tool specific + if (toolName != null) { + builder.put(TOOL_NAME_KEY, toolName); + } + + // Error info + if (errorType != null) { + builder.put(AttributeKey.stringKey("error.type"), errorType); + } + if (errorMessage != null) { + builder.put(AttributeKey.stringKey("error.message"), errorMessage); + } + + // 添加自定义数据 + for (Map.Entry entry : customData.entrySet()) { + if (entry.getValue() != null) { + String key = "codeact.interceptor.custom." + entry.getKey(); + Object value = entry.getValue(); + if (value instanceof String) { + builder.put(AttributeKey.stringKey(key), (String) value); + } else if (value instanceof Long) { + builder.put(AttributeKey.longKey(key), (Long) value); + } else if (value instanceof Integer) { + builder.put(AttributeKey.longKey(key), ((Integer) value).longValue()); + } else if (value instanceof Double) { + builder.put(AttributeKey.doubleKey(key), (Double) value); + } else if (value instanceof Boolean) { + builder.put(AttributeKey.booleanKey(key), (Boolean) value); + } else { + builder.put(AttributeKey.stringKey(key), String.valueOf(value)); + } + } + } + + return builder.build(); + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/ReactPhaseObservationContext.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/ReactPhaseObservationContext.java new file mode 100644 index 0000000..d0801d1 --- /dev/null +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/observation/context/ReactPhaseObservationContext.java @@ -0,0 +1,521 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.core.observation.context; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * React 阶段观测上下文 + *

    + * 存储 React 阶段(LlmNode 和 ToolNode)执行过程中的观测数据。 + *

    + * 已从 Micrometer Observation.Context 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class ReactPhaseObservationContext { + + // 标准属性键定义 + public static final AttributeKey SESSION_ID_KEY = AttributeKey.stringKey("gen_ai.conversation.id"); + public static final AttributeKey AGENT_NAME_KEY = AttributeKey.stringKey("gen_ai.agent.name"); + public static final AttributeKey ITERATION_KEY = AttributeKey.longKey("codeact.react.iteration"); + public static final AttributeKey NODE_TYPE_KEY = AttributeKey.stringKey("codeact.react.node_type"); + public static final AttributeKey NODE_ID_KEY = AttributeKey.stringKey("codeact.react.node_id"); + public static final AttributeKey DURATION_MS_KEY = AttributeKey.longKey("duration.ms"); + public static final AttributeKey SUCCESS_KEY = AttributeKey.booleanKey("codeact.react.success"); + public static final AttributeKey MODEL_NAME_KEY = AttributeKey.stringKey("gen_ai.response.model"); + public static final AttributeKey INPUT_TOKENS_KEY = AttributeKey.longKey("gen_ai.usage.input_tokens"); + public static final AttributeKey OUTPUT_TOKENS_KEY = AttributeKey.longKey("gen_ai.usage.output_tokens"); + public static final AttributeKey FINISH_REASON_KEY = AttributeKey.stringKey("gen_ai.response.finish_reasons"); + public static final AttributeKey TOOL_CALLS_COUNT_KEY = AttributeKey.longKey("codeact.react.tool_calls_count"); + + /** + * Node 类型枚举 + */ + public enum NodeType { + LLM, + TOOL, + UNKNOWN + } + + private String sessionId; + private String agentName; + private int iteration; + private NodeType nodeType; + private String nodeId; + private long durationMs; + private boolean success = true; + private String errorType; + private String errorMessage; + + // LlmNode specific fields + private String modelName; + private int inputTokens; + private int outputTokens; + private int promptMessageCount; + private int responseMessageCount; + private String finishReason; + + /** + * LLM 可调用的工具名称列表 + */ + private List availableToolNames = new ArrayList<>(); + + /** + * LLM 输入消息内容(摘要) + */ + private String inputSummary; + + /** + * LLM 输出消息内容(摘要) + */ + private String outputSummary; + + // ToolNode specific fields + private List toolCalls = new ArrayList<>(); + + /** + * 自定义数据存储 + */ + private final Map customData = new HashMap<>(); + + public ReactPhaseObservationContext() { + } + + public ReactPhaseObservationContext(String sessionId, NodeType nodeType, String nodeId) { + this.sessionId = sessionId; + this.nodeType = nodeType; + this.nodeId = nodeId; + } + + // ==================== Common Getters and Setters ==================== + + public String getSessionId() { + return sessionId; + } + + public ReactPhaseObservationContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + public String getAgentName() { + return agentName; + } + + public ReactPhaseObservationContext setAgentName(String agentName) { + this.agentName = agentName; + return this; + } + + public int getIteration() { + return iteration; + } + + public ReactPhaseObservationContext setIteration(int iteration) { + this.iteration = iteration; + return this; + } + + public NodeType getNodeType() { + return nodeType; + } + + public ReactPhaseObservationContext setNodeType(NodeType nodeType) { + this.nodeType = nodeType; + return this; + } + + public String getNodeId() { + return nodeId; + } + + public ReactPhaseObservationContext setNodeId(String nodeId) { + this.nodeId = nodeId; + return this; + } + + public long getDurationMs() { + return durationMs; + } + + public ReactPhaseObservationContext setDurationMs(long durationMs) { + this.durationMs = durationMs; + return this; + } + + public boolean isSuccess() { + return success; + } + + public ReactPhaseObservationContext setSuccess(boolean success) { + this.success = success; + return this; + } + + public String getErrorType() { + return errorType; + } + + public ReactPhaseObservationContext setErrorType(String errorType) { + this.errorType = errorType; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public ReactPhaseObservationContext setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + // ==================== LlmNode Getters and Setters ==================== + + public String getModelName() { + return modelName; + } + + public ReactPhaseObservationContext setModelName(String modelName) { + this.modelName = modelName; + return this; + } + + public int getInputTokens() { + return inputTokens; + } + + public ReactPhaseObservationContext setInputTokens(int inputTokens) { + this.inputTokens = inputTokens; + return this; + } + + public int getOutputTokens() { + return outputTokens; + } + + public ReactPhaseObservationContext setOutputTokens(int outputTokens) { + this.outputTokens = outputTokens; + return this; + } + + public int getPromptMessageCount() { + return promptMessageCount; + } + + public ReactPhaseObservationContext setPromptMessageCount(int promptMessageCount) { + this.promptMessageCount = promptMessageCount; + return this; + } + + public int getResponseMessageCount() { + return responseMessageCount; + } + + public ReactPhaseObservationContext setResponseMessageCount(int responseMessageCount) { + this.responseMessageCount = responseMessageCount; + return this; + } + + public String getFinishReason() { + return finishReason; + } + + public ReactPhaseObservationContext setFinishReason(String finishReason) { + this.finishReason = finishReason; + return this; + } + + public List getAvailableToolNames() { + return availableToolNames; + } + + public ReactPhaseObservationContext setAvailableToolNames(List availableToolNames) { + this.availableToolNames = availableToolNames != null ? availableToolNames : new ArrayList<>(); + return this; + } + + public String getInputSummary() { + return inputSummary; + } + + public ReactPhaseObservationContext setInputSummary(String inputSummary) { + this.inputSummary = inputSummary; + return this; + } + + public String getOutputSummary() { + return outputSummary; + } + + public ReactPhaseObservationContext setOutputSummary(String outputSummary) { + this.outputSummary = outputSummary; + return this; + } + + // ==================== ToolNode Getters and Setters ==================== + + public List getToolCalls() { + return toolCalls; + } + + public ReactPhaseObservationContext setToolCalls(List toolCalls) { + this.toolCalls = toolCalls; + return this; + } + + public ReactPhaseObservationContext addToolCall(ToolCallInfo toolCall) { + this.toolCalls.add(toolCall); + return this; + } + + // ==================== Custom Data Methods ==================== + + public ReactPhaseObservationContext putCustomData(String key, Object value) { + this.customData.put(key, value); + return this; + } + + public ReactPhaseObservationContext putAllCustomData(Map data) { + if (data != null) { + this.customData.putAll(data); + } + return this; + } + + @SuppressWarnings("unchecked") + public T getCustomData(String key) { + return (T) this.customData.get(key); + } + + public Map getAllCustomData() { + return Map.copyOf(customData); + } + + // ==================== Tool Call Info ==================== + + /** + * 工具调用信息 + */ + public static class ToolCallInfo { + private String toolName; + private String arguments; + private int argumentsLength; + private String result; + private int resultLength; + private long durationMs; + private boolean success; + private String errorType; + private String errorMessage; + + public ToolCallInfo() { + } + + public ToolCallInfo(String toolName) { + this.toolName = toolName; + } + + // Getters and Setters + + public String getToolName() { + return toolName; + } + + public ToolCallInfo setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + public String getArguments() { + return arguments; + } + + public ToolCallInfo setArguments(String arguments) { + this.arguments = arguments; + this.argumentsLength = arguments != null ? arguments.length() : 0; + return this; + } + + public int getArgumentsLength() { + return argumentsLength; + } + + public String getResult() { + return result; + } + + public ToolCallInfo setResult(String result) { + this.result = result; + this.resultLength = result != null ? result.length() : 0; + return this; + } + + public int getResultLength() { + return resultLength; + } + + public long getDurationMs() { + return durationMs; + } + + public ToolCallInfo setDurationMs(long durationMs) { + this.durationMs = durationMs; + return this; + } + + public boolean isSuccess() { + return success; + } + + public ToolCallInfo setSuccess(boolean success) { + this.success = success; + return this; + } + + public String getErrorType() { + return errorType; + } + + public ToolCallInfo setErrorType(String errorType) { + this.errorType = errorType; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public ToolCallInfo setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + @Override + public String toString() { + return "ToolCallInfo{" + + "toolName='" + toolName + '\'' + + ", argumentsLength=" + argumentsLength + + ", resultLength=" + resultLength + + ", durationMs=" + durationMs + + ", success=" + success + + '}'; + } + } + + @Override + public String toString() { + return "ReactPhaseObservationContext{" + + "sessionId='" + sessionId + '\'' + + ", agentName='" + agentName + '\'' + + ", iteration=" + iteration + + ", nodeType=" + nodeType + + ", nodeId='" + nodeId + '\'' + + ", durationMs=" + durationMs + + ", success=" + success + + ", modelName='" + modelName + '\'' + + ", toolCallsCount=" + toolCalls.size() + + '}'; + } + + /** + * 将上下文转换为 OpenTelemetry Attributes + * + * @return Attributes 对象 + */ + public Attributes toAttributes() { + AttributesBuilder builder = Attributes.builder(); + + if (sessionId != null) { + builder.put(SESSION_ID_KEY, sessionId); + } + if (agentName != null) { + builder.put(AGENT_NAME_KEY, agentName); + } + if (iteration > 0) { + builder.put(ITERATION_KEY, (long) iteration); + } + if (nodeType != null) { + builder.put(NODE_TYPE_KEY, nodeType.name()); + } + if (nodeId != null) { + builder.put(NODE_ID_KEY, nodeId); + } + if (durationMs > 0) { + builder.put(DURATION_MS_KEY, durationMs); + } + builder.put(SUCCESS_KEY, success); + + // LLM specific + if (modelName != null) { + builder.put(MODEL_NAME_KEY, modelName); + } + if (inputTokens > 0) { + builder.put(INPUT_TOKENS_KEY, (long) inputTokens); + } + if (outputTokens > 0) { + builder.put(OUTPUT_TOKENS_KEY, (long) outputTokens); + } + if (finishReason != null) { + builder.put(FINISH_REASON_KEY, finishReason); + } + + // Tool calls + if (!toolCalls.isEmpty()) { + builder.put(TOOL_CALLS_COUNT_KEY, (long) toolCalls.size()); + } + + // Error info + if (errorType != null) { + builder.put(AttributeKey.stringKey("error.type"), errorType); + } + if (errorMessage != null) { + builder.put(AttributeKey.stringKey("error.message"), errorMessage); + } + + // 添加自定义数据 + for (Map.Entry entry : customData.entrySet()) { + if (entry.getValue() != null) { + String key = "codeact.react.custom." + entry.getKey(); + Object value = entry.getValue(); + if (value instanceof String) { + builder.put(AttributeKey.stringKey(key), (String) value); + } else if (value instanceof Long) { + builder.put(AttributeKey.longKey(key), (Long) value); + } else if (value instanceof Integer) { + builder.put(AttributeKey.longKey(key), ((Integer) value).longValue()); + } else if (value instanceof Double) { + builder.put(AttributeKey.doubleKey(key), (Double) value); + } else if (value instanceof Boolean) { + builder.put(AttributeKey.booleanKey(key), (Boolean) value); + } else { + builder.put(AttributeKey.stringKey(key), String.valueOf(value)); + } + } + } + + return builder.build(); + } +} + diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/ToolRegistryBridge.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/ToolRegistryBridge.java index e06853a..e9e2f89 100644 --- a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/ToolRegistryBridge.java +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/ToolRegistryBridge.java @@ -16,11 +16,15 @@ package com.alibaba.assistant.agent.core.tool; import com.alibaba.assistant.agent.common.tools.CodeactTool; +import com.alibaba.assistant.agent.core.model.ToolCallRecord; import com.alibaba.assistant.agent.core.tool.schema.ReturnSchemaRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ToolContext; +import java.util.ArrayList; +import java.util.List; + /** * ToolRegistry Bridge - 供 Python 调用的 Java 对象。 * @@ -38,6 +42,11 @@ public class ToolRegistryBridge { private final ToolContext toolContext; + /** + * 工具调用追踪记录 + */ + private final List callTrace = new ArrayList<>(); + /** * 构造函数。 * @param registry 工具注册表 @@ -61,6 +70,9 @@ public String callTool(String toolName, String argsJson) { toolContext != null, toolContext != null && toolContext.getContext() != null ? toolContext.getContext().keySet() : "null"); + // 记录工具调用到追踪列表 + recordToolCall(toolName); + try { // 从注册表获取工具 CodeactTool tool = registry.getTool(toolName) @@ -118,5 +130,45 @@ private void observeReturnSchema(String toolName, String resultJson, boolean suc } } + /** + * 记录工具调用。 + * @param toolName 工具名称 + */ + private void recordToolCall(String toolName) { + // 获取工具的targetClassName来构建完整的工具标识 + String toolIdentifier = toolName; + try { + CodeactTool tool = registry.getTool(toolName).orElse(null); + if (tool != null && tool.getCodeactMetadata() != null) { + String targetClassName = tool.getCodeactMetadata().targetClassName(); + if (targetClassName != null && !targetClassName.isEmpty()) { + toolIdentifier = targetClassName + "." + toolName; + } + } + } catch (Exception e) { + logger.warn("ToolRegistryBridge#recordToolCall - reason=获取工具元数据失败, toolName={}", toolName); + } + + ToolCallRecord record = new ToolCallRecord(callTrace.size() + 1, toolIdentifier); + callTrace.add(record); + logger.info("ToolRegistryBridge#recordToolCall - reason=记录工具调用, order={}, tool={}", record.getOrder(), record.getTool()); + } + + /** + * 获取工具调用追踪记录。 + * @return 工具调用记录列表 + */ + public List getCallTrace() { + return new ArrayList<>(callTrace); + } + + /** + * 清空工具调用追踪记录。 + */ + public void clearCallTrace() { + callTrace.clear(); + logger.debug("ToolRegistryBridge#clearCallTrace - reason=清空调用追踪记录"); + } + } diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/view/PythonToolViewRenderer.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/view/PythonToolViewRenderer.java index 7e42acf..2767314 100644 --- a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/view/PythonToolViewRenderer.java +++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/view/PythonToolViewRenderer.java @@ -124,31 +124,8 @@ public String renderClassStub(String className, String classDescription, List优先从 codeInvocationTemplate 中提取,与 GraalCodeExecutor.generatePythonMethod 保持一致。 + * 这确保了 LLM 生成的代码中的方法名与实际注入到 Python 环境中的方法名一致。 + * + * @param tool CodeactTool 实例 + * @return 方法名 + */ + private String extractMethodName(CodeactTool tool) { + // 获取 ParameterTree 判断是否有结构化参数 + ParameterTree parameterTree = tool.getParameterTree(); + + // 如果没有结构化参数,尝试从 codeInvocationTemplate 提取方法名 + // 这与 GraalCodeExecutor.generatePythonMethod 的逻辑一致 + if (parameterTree == null || !parameterTree.hasParameters()) { + String invocationTemplate = tool.getCodeactMetadata() != null + ? tool.getCodeactMetadata().codeInvocationTemplate() + : null; + + if (invocationTemplate != null && invocationTemplate.contains("(")) { + int parenIndex = invocationTemplate.indexOf('('); + String extractedName = invocationTemplate.substring(0, parenIndex).trim(); + if (!extractedName.isEmpty()) { + logger.debug("PythonToolViewRenderer#extractMethodName - reason=从codeInvocationTemplate提取方法名, " + + "methodName={}, toolClass={}", extractedName, tool.getClass().getSimpleName()); + return extractedName; + } + } + } + + // 回退到 ToolDefinition.name() + String methodName = null; + if (tool.getToolDefinition() != null) { + methodName = tool.getToolDefinition().name(); + logger.debug("PythonToolViewRenderer#extractMethodName - reason=从ToolDefinition获取名称, name={}, toolClass={}", + methodName, tool.getClass().getSimpleName()); + } + + // 如果还是空,尝试 getName() + if (methodName == null || methodName.isEmpty()) { + methodName = tool.getName(); + logger.debug("PythonToolViewRenderer#extractMethodName - reason=尝试getName, name={}", methodName); + } + + // 最后的保底 + if (methodName == null || methodName.isEmpty()) { + methodName = "unknown_method"; + logger.warn("PythonToolViewRenderer#extractMethodName - reason=工具名为空使用默认名称, toolClass={}", + tool.getClass().getSimpleName()); + } + + return methodName; + } + } diff --git a/assistant-agent-evaluation/pom.xml b/assistant-agent-evaluation/pom.xml index 2a84b46..e9f0be9 100644 --- a/assistant-agent-evaluation/pom.xml +++ b/assistant-agent-evaluation/pom.xml @@ -6,7 +6,7 @@ com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 assistant-agent-evaluation @@ -50,6 +50,16 @@ slf4j-api + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-context + + com.alibaba.cloud.ai spring-ai-alibaba-dashscope diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/DefaultEvaluationService.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/DefaultEvaluationService.java index 23bbe15..de5bf39 100644 --- a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/DefaultEvaluationService.java +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/DefaultEvaluationService.java @@ -19,6 +19,10 @@ import com.alibaba.assistant.agent.evaluation.model.EvaluationContext; import com.alibaba.assistant.agent.evaluation.model.EvaluationResult; import com.alibaba.assistant.agent.evaluation.model.EvaluationSuite; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -58,24 +62,69 @@ public DefaultEvaluationService(GraphBasedEvaluationExecutor executor, ExecutorS @Override public EvaluationResult evaluate(EvaluationSuite suite, EvaluationContext context) { - logger.info("Starting evaluation for suite: {}", suite.getName()); + return evaluate(suite, context, null); + } + + @Override + public EvaluationResult evaluate(EvaluationSuite suite, EvaluationContext context, + Span parentSpan) { + logger.info("DefaultEvaluationService#evaluate - reason=开始评估, suite={}, hasParent={}", + suite.getName(), parentSpan != null); + + // 设置 parent Span 到 ThreadLocal + if (parentSpan != null) { + com.alibaba.assistant.agent.evaluation.observation.EvaluationObservationLifecycleListener + .setParentSpan(parentSpan); + } try { - // Execute evaluation - EvaluationResult result = executor.execute(suite, context); + // Execute evaluation with parent span + EvaluationResult result = executor.execute(suite, context, parentSpan); - logger.info("Evaluation completed for suite: {}", suite.getName()); + logger.info("DefaultEvaluationService#evaluate - reason=评估完成, suite={}", suite.getName()); return result; } catch (Exception e) { - logger.error("Error executing evaluation for suite: {}", suite.getName(), e); + logger.error("DefaultEvaluationService#evaluate - reason=评估失败, suite={}", suite.getName(), e); throw new RuntimeException("Evaluation execution failed", e); + } finally { + // 清理 ThreadLocal + if (parentSpan != null) { + com.alibaba.assistant.agent.evaluation.observation.EvaluationObservationLifecycleListener + .clearParentSpan(); + } } } @Override public CompletableFuture evaluateAsync(EvaluationSuite suite, EvaluationContext context) { - return CompletableFuture.supplyAsync(() -> evaluate(suite, context), asyncExecutor); + return evaluateAsync(suite, context, null); + } + + @Override + public CompletableFuture evaluateAsync(EvaluationSuite suite, EvaluationContext context, + Span parentSpan) { + // 捕获当前的 parent Span 和 Context,在异步执行时传递 + final Span capturedParent = parentSpan; + final Context capturedContext = parentSpan != null ? Context.current().with(parentSpan) : Context.current(); + + return CompletableFuture.supplyAsync(() -> { + // 在异步线程中恢复 trace context + try (Scope ignored = capturedContext.makeCurrent()) { + // 设置 parent Span 到 ThreadLocal,供 EvaluationObservationLifecycleListener 使用 + if (capturedParent != null) { + com.alibaba.assistant.agent.evaluation.observation.EvaluationObservationLifecycleListener + .setParentSpan(capturedParent); + } + try { + return executor.execute(suite, context, capturedParent); + } finally { + // 清理 ThreadLocal + com.alibaba.assistant.agent.evaluation.observation.EvaluationObservationLifecycleListener + .clearParentSpan(); + } + } + }, asyncExecutor); } @Override @@ -93,7 +142,7 @@ public void registerSuite(EvaluationSuite suite) { throw new IllegalArgumentException("Suite ID cannot be null or empty"); } suiteRegistry.put(suite.getId(), suite); - logger.info("Registered evaluation suite: {}", suite.getId()); + logger.info("DefaultEvaluationService#registerSuite - reason=注册评估套件, suiteId={}", suite.getId()); } /** diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/EvaluationService.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/EvaluationService.java index c68c0f4..c20e6ea 100644 --- a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/EvaluationService.java +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/EvaluationService.java @@ -18,6 +18,7 @@ import com.alibaba.assistant.agent.evaluation.model.EvaluationContext; import com.alibaba.assistant.agent.evaluation.model.EvaluationResult; import com.alibaba.assistant.agent.evaluation.model.EvaluationSuite; +import io.opentelemetry.api.trace.Span; import java.util.concurrent.CompletableFuture; @@ -38,6 +39,20 @@ public interface EvaluationService { */ EvaluationResult evaluate(EvaluationSuite suite, EvaluationContext context); + /** + * Execute evaluation synchronously with parent Span for tracing + * + * @param suite Evaluation suite to execute + * @param context Evaluation context + * @param parentSpan Parent span for establishing span hierarchy + * @return Evaluation result + */ + default EvaluationResult evaluate(EvaluationSuite suite, EvaluationContext context, + Span parentSpan) { + // Default implementation ignores parentSpan for backward compatibility + return evaluate(suite, context); + } + /** * Execute evaluation asynchronously * @@ -47,6 +62,20 @@ public interface EvaluationService { */ CompletableFuture evaluateAsync(EvaluationSuite suite, EvaluationContext context); + /** + * Execute evaluation asynchronously with parent Span for tracing + * + * @param suite Evaluation suite to execute + * @param context Evaluation context + * @param parentSpan Parent span for establishing span hierarchy + * @return CompletableFuture of evaluation result + */ + default CompletableFuture evaluateAsync(EvaluationSuite suite, EvaluationContext context, + Span parentSpan) { + // Default implementation ignores parentSpan for backward compatibility + return evaluateAsync(suite, context); + } + /** * Load a pre-configured evaluation suite by ID * diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/builder/EvaluationSuiteBuilder.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/builder/EvaluationSuiteBuilder.java index afd392e..8131576 100644 --- a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/builder/EvaluationSuiteBuilder.java +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/builder/EvaluationSuiteBuilder.java @@ -21,10 +21,14 @@ import com.alibaba.assistant.agent.evaluation.model.EvaluationCriterion; import com.alibaba.assistant.agent.evaluation.model.EvaluationSuite; import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.GraphLifecycleListener; import com.alibaba.cloud.ai.graph.KeyStrategy; import com.alibaba.cloud.ai.graph.StateGraph; import com.alibaba.cloud.ai.graph.exception.GraphStateException; import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; import java.util.ArrayList; import java.util.Collections; @@ -47,6 +51,9 @@ public class EvaluationSuiteBuilder { private final EvaluatorRegistry evaluatorRegistry; private final BatchAggregationStrategyRegistry aggregationStrategyRegistry; private ExecutorService executorService; + private final List lifecycleListeners = new ArrayList<>(); + private Tracer tracer; + private Span parentSpan; public EvaluationSuiteBuilder(String id, EvaluatorRegistry evaluatorRegistry) { this(id, evaluatorRegistry, new BatchAggregationStrategyRegistry(), null); @@ -80,6 +87,54 @@ public EvaluationSuiteBuilder executorService(ExecutorService executorService) { return this; } + /** + * 添加 GraphLifecycleListener 用于可观测性 + * + * @param listener GraphLifecycleListener + * @return this + */ + public EvaluationSuiteBuilder lifecycleListener(GraphLifecycleListener listener) { + if (listener != null) { + this.lifecycleListeners.add(listener); + } + return this; + } + + /** + * 添加多个 GraphLifecycleListeners + * + * @param listeners GraphLifecycleListeners + * @return this + */ + public EvaluationSuiteBuilder lifecycleListeners(List listeners) { + if (listeners != null) { + this.lifecycleListeners.addAll(listeners); + } + return this; + } + + /** + * 设置 Tracer 用于可观测性 + * + * @param tracer Tracer + * @return this + */ + public EvaluationSuiteBuilder tracer(Tracer tracer) { + this.tracer = tracer; + return this; + } + + /** + * 设置 parent Span,所有评估项的 span 都会继承此父 span + * + * @param parentSpan 父 Span + * @return this + */ + public EvaluationSuiteBuilder parentSpan(Span parentSpan) { + this.parentSpan = parentSpan; + return this; + } + public EvaluationSuiteBuilder name(String name) { suite.setName(name); @@ -237,8 +292,20 @@ public EvaluationSuite build() { stateGraph.addEdge(previousJoinNode, StateGraph.END); } - // Compile graph; keep representation inside suite - CompiledGraph compiled = stateGraph.compile(); + // Build CompileConfig with lifecycleListeners for observability + CompileConfig.Builder configBuilder = CompileConfig.builder(); + + // Add ObservationRegistry if available + + // Add all lifecycleListeners + for (GraphLifecycleListener listener : lifecycleListeners) { + configBuilder.withLifecycleListener(listener); + } + + CompileConfig compileConfig = configBuilder.build(); + + // Compile graph with config + CompiledGraph compiled = stateGraph.compile(compileConfig); suite.setCompiledGraph(compiled); } catch (GraphStateException e) { diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/evaluator/LLMBasedEvaluator.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/evaluator/LLMBasedEvaluator.java index a80cba6..9304e0f 100644 --- a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/evaluator/LLMBasedEvaluator.java +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/evaluator/LLMBasedEvaluator.java @@ -65,6 +65,9 @@ public CriterionResult evaluate(CriterionExecutionContext executionContext) { // Build prompt String promptText = buildPrompt(executionContext); + // Store prompt for observability + result.setRawPrompt(promptText); + logger.debug("Evaluating criterion {} with LLM, prompt: {}", executionContext.getCriterion().getName(), promptText); diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/CriterionEvaluationAction.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/CriterionEvaluationAction.java index 5939f86..aa6a7ff 100644 --- a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/CriterionEvaluationAction.java +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/CriterionEvaluationAction.java @@ -132,6 +132,11 @@ public Map apply(OverAllState state) { updates.put(criterion.getName() + "_value", result.getValue()); } + // 注册结果到静态存储,供 EvaluationObservationLifecycleListener 读取 + // 因为 after() 回调中的 state 可能是快照,不包含刚写入的数据 + com.alibaba.assistant.agent.evaluation.observation.EvaluationObservationLifecycleListener + .registerCriterionResult(criterion.getName(), result); + logger.info("Criterion {} completed with result: {}", criterion.getName(), result.getValue()); } catch (Exception e) { diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/GraphBasedEvaluationExecutor.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/GraphBasedEvaluationExecutor.java index 96a046f..84d46d9 100644 --- a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/GraphBasedEvaluationExecutor.java +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/executor/GraphBasedEvaluationExecutor.java @@ -20,10 +20,12 @@ import com.alibaba.assistant.agent.evaluation.model.EvaluationResult; import com.alibaba.assistant.agent.evaluation.model.EvaluationCriterion; import com.alibaba.assistant.agent.evaluation.model.EvaluationSuite; +import com.alibaba.assistant.agent.evaluation.observation.EvaluationObservationLifecycleListener; import com.alibaba.cloud.ai.graph.CompiledGraph; import com.alibaba.cloud.ai.graph.NodeOutput; import com.alibaba.cloud.ai.graph.RunnableConfig; import com.alibaba.cloud.ai.graph.StateGraph; +import io.opentelemetry.api.trace.Span; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,6 +75,19 @@ public ExecutorService getExecutorService() { * Execute evaluation for a suite using its compiled graph. */ public EvaluationResult execute(EvaluationSuite suite, EvaluationContext context) { + return execute(suite, context, null); + } + + /** + * Execute evaluation for a suite using its compiled graph with parent Span. + * + * @param suite Evaluation suite to execute + * @param context Evaluation context + * @param parentSpan Parent span for establishing span hierarchy + * @return Evaluation result + */ + public EvaluationResult execute(EvaluationSuite suite, EvaluationContext context, + Span parentSpan) { EvaluationResult result = new EvaluationResult(); result.setSuiteId(suite.getId()); result.setSuiteName(suite.getName()); @@ -90,6 +105,13 @@ public EvaluationResult execute(EvaluationSuite suite, EvaluationContext context initialData.put("suite", suite); initialData.put("evaluationContext", context); + // 将 parent Span 存入 ThreadLocal,而不是 initialData + // 因为 Span 对象不能被序列化 + if (parentSpan != null) { + EvaluationObservationLifecycleListener.setParentSpan(parentSpan); + logger.debug("GraphBasedEvaluationExecutor#execute - reason=设置parentSpan到ThreadLocal"); + } + // Use CompiledGraph.invokeAndGetOutput to execute the graph // This follows graph-core best practices and ensures we get the final state // Important: graph-core uses ParallelNode for fan-out edges (e.g. multiple START children, @@ -102,6 +124,7 @@ public EvaluationResult execute(EvaluationSuite suite, EvaluationContext context for (String nodeId : parallelNodeIds) { configBuilder.addParallelNodeExecutor(nodeId, executorService); } + RunnableConfig config = configBuilder.build(); Optional outputOpt = compiledGraph.invokeAndGetOutput(initialData, config); @@ -136,6 +159,8 @@ public EvaluationResult execute(EvaluationSuite suite, EvaluationContext context } finally { result.setEndTimeMillis(System.currentTimeMillis()); + // 清理 ThreadLocal,避免内存泄漏 + EvaluationObservationLifecycleListener.clearParentSpan(); } return result; @@ -218,7 +243,7 @@ private EvaluationResult.EvaluationStatistics calculateStatistics( */ public void shutdown() { if (shouldShutdownExecutor && executorService != null) { - logger.info("Shutting down ExecutorService"); + logger.info("GraphBasedEvaluationExecutor#shutdown - reason=关闭ExecutorService"); executorService.shutdown(); } } diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/model/CriterionResult.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/model/CriterionResult.java index 5487b77..f26eb0b 100644 --- a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/model/CriterionResult.java +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/model/CriterionResult.java @@ -49,6 +49,11 @@ public class CriterionResult { */ private String rawResponse; + /** + * Raw prompt sent to LLM (for observability) + */ + private String rawPrompt; + /** * Error message if status is ERROR */ @@ -111,6 +116,14 @@ public void setRawResponse(String rawResponse) { this.rawResponse = rawResponse; } + public String getRawPrompt() { + return rawPrompt; + } + + public void setRawPrompt(String rawPrompt) { + this.rawPrompt = rawPrompt; + } + public String getErrorMessage() { return errorMessage; } diff --git a/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/observation/EvaluationObservationLifecycleListener.java b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/observation/EvaluationObservationLifecycleListener.java new file mode 100644 index 0000000..7417bf4 --- /dev/null +++ b/assistant-agent-evaluation/src/main/java/com/alibaba/assistant/agent/evaluation/observation/EvaluationObservationLifecycleListener.java @@ -0,0 +1,363 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.assistant.agent.evaluation.observation; + +import com.alibaba.assistant.agent.evaluation.model.CriterionResult; +import com.alibaba.cloud.ai.graph.GraphLifecycleListener; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 评估 Graph 的可观测性生命周期监听器 + *

    + * 为每个评估项(criterion)创建独立的 OpenTelemetry Span,记录: + *

      + *
    • 评估项名称
    • + *
    • LLM 输入(prompt)
    • + *
    • LLM 输出(response)
    • + *
    • 评估结果
    • + *
    • 执行时长
    • + *
    + *

    + * 已从 Micrometer Observation 迁移到 OpenTelemetry 原生 API。 + * + * @author Assistant Agent Team + * @since 1.0.0 + */ +public class EvaluationObservationLifecycleListener implements GraphLifecycleListener { + + private static final Logger log = LoggerFactory.getLogger(EvaluationObservationLifecycleListener.class); + + /** + * 在 RunnableConfig metadata 中存储 parent Span 的 key + * @deprecated 使用 ThreadLocal 代替,因为 Span 对象不能被序列化 + */ + @Deprecated + public static final String PARENT_SPAN_KEY = "_evaluation_parent_span_"; + + /** + * InheritableThreadLocal 存储 parent Span + * 使用 InheritableThreadLocal 确保子线程(异步执行)能继承 parent + * 因为 Span 对象包含复杂的引用链,不能放入会被序列化的 Map 中 + */ + private static final InheritableThreadLocal PARENT_SPAN_HOLDER = new InheritableThreadLocal<>(); + + /** + * 设置 parent Span(由 GraphBasedEvaluationExecutor 调用) + */ + public static void setParentSpan(Span parentSpan) { + PARENT_SPAN_HOLDER.set(parentSpan); + } + + /** + * 获取 parent Span + */ + public static Span getParentSpanFromThreadLocal() { + return PARENT_SPAN_HOLDER.get(); + } + + /** + * 清理 parent Span(执行完成后由 GraphBasedEvaluationExecutor 调用) + */ + public static void clearParentSpan() { + PARENT_SPAN_HOLDER.remove(); + } + + private final Tracer tracer; + + /** + * 默认的 parent Span(在构造时设置) + */ + private final Span defaultParentSpan; + + /** + * 存储每个节点的 Span + */ + private final ConcurrentHashMap nodeSpans = new ConcurrentHashMap<>(); + + /** + * 存储每个节点的 Scope + */ + private final ConcurrentHashMap nodeScopes = new ConcurrentHashMap<>(); + + /** + * 存储每个节点的开始时间 + */ + private final ConcurrentHashMap nodeStartTimes = new ConcurrentHashMap<>(); + + /** + * 存储每个节点的 parent Scope(用于正确的父子关系) + */ + private final ConcurrentHashMap parentScopes = new ConcurrentHashMap<>(); + + /** + * 静态存储 criterion 结果(按 nodeKey 分组) + * 因为 after() 中的 state 可能是快照,不包含刚刚写入的结果 + */ + private static final ConcurrentHashMap CRITERION_RESULT_STORE = new ConcurrentHashMap<>(); + + /** + * 注册 criterion 结果(由 CriterionEvaluationAction 调用) + * + * @param criterionName criterion 名称 + * @param result 结果 + */ + public static void registerCriterionResult(String criterionName, CriterionResult result) { + if (criterionName != null && result != null) { + CRITERION_RESULT_STORE.put(criterionName, result); + log.debug("EvaluationObservationLifecycleListener#registerCriterionResult - " + + "reason=注册结果, criterionName={}, status={}", criterionName, result.getStatus()); + } + } + + /** + * 获取并清除 criterion 结果 + * + * @param criterionName criterion 名称 + * @return 结果 + */ + public static CriterionResult getAndClearCriterionResult(String criterionName) { + return CRITERION_RESULT_STORE.remove(criterionName); + } + + public EvaluationObservationLifecycleListener(Tracer tracer) { + this(tracer, null); + } + + /** + * 构造函数,支持设置 parent Span + * + * @param tracer OpenTelemetry Tracer + * @param parentSpan 父 Span,所有评估项的 span 都会继承此父 span + */ + public EvaluationObservationLifecycleListener(Tracer tracer, Span parentSpan) { + this.tracer = tracer; + this.defaultParentSpan = parentSpan; + log.info("EvaluationObservationLifecycleListener# - reason=初始化完成, hasTracer={}, hasParent={}", + tracer != null, parentSpan != null); + } + + @Override + public void onStart(String nodeId, Map state, RunnableConfig config) { + // 评估 Graph 开始时不需要特殊处理 + if (nodeId != null && nodeId.equalsIgnoreCase("__start__")) { + log.debug("EvaluationObservationLifecycleListener#onStart - reason=评估Graph开始"); + } + } + + @Override + public void onComplete(String nodeId, Map state, RunnableConfig config) { + // 评估 Graph 完成时不需要特殊处理 + if (nodeId != null && nodeId.equalsIgnoreCase("__end__")) { + log.debug("EvaluationObservationLifecycleListener#onComplete - reason=评估Graph完成"); + } + } + + @Override + public void onError(String nodeId, Map state, Throwable ex, RunnableConfig config) { + log.error("EvaluationObservationLifecycleListener#onError - reason=评估执行出错, nodeId={}", nodeId, ex); + + // 停止对应节点的 Span + String nodeKey = getNodeKey(nodeId, config); + stopSpan(nodeKey, ex); + } + + @Override + public void before(String nodeId, Map state, RunnableConfig config, Long curTime) { + // 跳过汇聚节点和系统节点 + if (nodeId == null || nodeId.startsWith("join_") || nodeId.startsWith("__")) { + return; + } + + if (tracer == null) { + return; + } + + String nodeKey = getNodeKey(nodeId, config); + nodeStartTimes.put(nodeKey, System.currentTimeMillis()); + + // 获取 parent Span + Span parentSpan = getParentSpan(state); + + // 关键:如果有 parent,需要先将其设置为当前 context + Context parentContext = Context.current(); + if (parentSpan != null) { + parentContext = parentContext.with(parentSpan); + } + + // 创建 criterion 评估的 Span + Span span = tracer.spanBuilder("codeact.evaluation.criterion") + .setParent(parentContext) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("gen_ai.span_kind_name", "EVALUATOR") + .setAttribute("gen_ai.operation.name", "evaluate") + .setAttribute("codeact.evaluation.criterion_name", nodeId) + .startSpan(); + + // 打开当前 span 的 scope + Scope scope = span.makeCurrent(); + + nodeSpans.put(nodeKey, span); + nodeScopes.put(nodeKey, scope); + + log.debug("EvaluationObservationLifecycleListener#before - reason=评估项开始执行, criterionName={}", nodeId); + } + + /** + * 获取 parent Span + * 优先从 ThreadLocal 中获取,其次使用构造时传入的默认值 + */ + private Span getParentSpan(Map state) { + // 先尝试从 ThreadLocal 中获取 + Span fromThreadLocal = PARENT_SPAN_HOLDER.get(); + if (fromThreadLocal != null) { + return fromThreadLocal; + } + // 使用默认的 parent Span + return defaultParentSpan; + } + + @Override + public void after(String nodeId, Map state, RunnableConfig config, Long curTime) { + // 跳过汇聚节点和系统节点 + if (nodeId == null || nodeId.startsWith("join_") || nodeId.startsWith("__")) { + return; + } + + String nodeKey = getNodeKey(nodeId, config); + + // 计算执行时长 + Long startTime = nodeStartTimes.remove(nodeKey); + long durationMs = startTime != null ? System.currentTimeMillis() - startTime : 0; + + // 先关闭当前 span 的 scope + Scope scope = nodeScopes.remove(nodeKey); + if (scope != null) { + scope.close(); + } + + // 获取并停止 Span + Span span = nodeSpans.remove(nodeKey); + if (span != null) { + span.setAttribute("duration.ms", durationMs); + + // 优先从静态存储获取结果(解决 state 快照问题) + CriterionResult result = getAndClearCriterionResult(nodeId); + + // 如果静态存储没有,尝试从 state 获取 + if (result == null) { + String resultKey = nodeId + "_result"; + Object resultObj = state.get(resultKey); + if (resultObj instanceof CriterionResult) { + result = (CriterionResult) resultObj; + } + } + + if (result != null) { + // 记录评估结果 + span.setAttribute("codeact.evaluation.status", + result.getStatus() != null ? result.getStatus().name() : "UNKNOWN"); + + if (result.getValue() != null) { + span.setAttribute("codeact.evaluation.value", + truncate(result.getValue().toString(), 500)); + } + + if (result.getReason() != null) { + span.setAttribute("codeact.evaluation.reason", + truncate(result.getReason(), 500)); + } + + if (result.getRawPrompt() != null) { + // LLM 输入 + span.setAttribute("gen_ai.input.messages", + truncate(result.getRawPrompt(), 2000)); + } + + if (result.getRawResponse() != null) { + // LLM 输出 + span.setAttribute("gen_ai.output.messages", + truncate(result.getRawResponse(), 2000)); + } + + if (result.getErrorMessage() != null) { + span.setAttribute("codeact.evaluation.error", + truncate(result.getErrorMessage(), 500)); + } + + log.info("EvaluationObservationLifecycleListener#after - reason=评估项执行完成, " + + "criterionName={}, status={}, durationMs={}", + nodeId, result.getStatus(), durationMs); + } else { + log.warn("EvaluationObservationLifecycleListener#after - reason=评估结果未找到, criterionName={}", nodeId); + } + + span.end(); + } + } + + /** + * 生成节点唯一标识 + */ + private String getNodeKey(String nodeId, RunnableConfig config) { + String threadId = config != null ? config.threadId().orElse("default") : "default"; + return threadId + ":" + nodeId; + } + + /** + * 停止 Span(出错时调用) + */ + private void stopSpan(String nodeKey, Throwable error) { + Scope scope = nodeScopes.remove(nodeKey); + if (scope != null) { + scope.close(); + } + + Span span = nodeSpans.remove(nodeKey); + if (span != null) { + if (error != null) { + span.setStatus(StatusCode.ERROR, error.getMessage()); + span.recordException(error); + span.setAttribute("error.type", error.getClass().getSimpleName()); + } + span.end(); + } + } + + /** + * 截断字符串 + */ + private String truncate(String str, int maxLength) { + if (str == null) { + return "null"; + } + if (str.length() <= maxLength) { + return str; + } + return str.substring(0, maxLength) + "...[truncated]"; + } +} + diff --git a/assistant-agent-extensions/pom.xml b/assistant-agent-extensions/pom.xml index 42ccf7a..2159e75 100644 --- a/assistant-agent-extensions/pom.xml +++ b/assistant-agent-extensions/pom.xml @@ -6,7 +6,7 @@ com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 assistant-agent-extensions diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpDynamicToolFactory.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpDynamicToolFactory.java index e8f2f1c..aadac5a 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpDynamicToolFactory.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpDynamicToolFactory.java @@ -147,8 +147,8 @@ private CodeactTool createToolFromCallback(ToolCallback callback, ObjectMapper o String inputSchema = definition.inputSchema(); // 类名:使用配置的 server 名称或默认前缀 - // 归一化后的类名(如 my-mcp-server -> my_mcp_server_tools) - String targetClassName = nameNormalizer.normalizeClassName(defaultTargetClassNamePrefix) + "_tools"; + // 归一化后的类名(如 my-mcp-server -> my_mcp_server) + String targetClassName = nameNormalizer.normalizeClassName(defaultTargetClassNamePrefix); String targetClassDescription = defaultTargetClassDescription; // 优先检查是否实现了 McpServerAwareToolCallback 接口 @@ -157,7 +157,7 @@ private CodeactTool createToolFromCallback(ToolCallback callback, ObjectMapper o String displayName = serverAwareCallback.getServerDisplayName(); if (serverName != null && !serverName.isEmpty()) { - targetClassName = nameNormalizer.normalizeClassName(serverName) + "_tools"; + targetClassName = nameNormalizer.normalizeClassName(serverName); targetClassDescription = displayName != null ? displayName : serverName; // 使用原始工具名而不是 ToolDefinition 中的名称 toolName = serverAwareCallback.getToolName(); @@ -171,7 +171,7 @@ else if (!serverSpecs.isEmpty()) { for (Map.Entry entry : serverSpecs.entrySet()) { McpServerSpec spec = entry.getValue(); // 使用 serverSpec 中的名称 - targetClassName = nameNormalizer.normalizeClassName(spec.getServerName()) + "_tools"; + targetClassName = nameNormalizer.normalizeClassName(spec.getServerName()); targetClassDescription = spec.getDescription(); break; // 目前只支持单个 server,使用第一个 } diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpServerAwareToolCallback.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpServerAwareToolCallback.java index 5a4eafb..da12dc8 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpServerAwareToolCallback.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/mcp/McpServerAwareToolCallback.java @@ -38,7 +38,7 @@ public interface McpServerAwareToolCallback extends ToolCallback { * 获取 MCP Server 名称。 * *

    此名称将用于生成 Python 类名(经过归一化处理)。 - * 例如 "aone-app-center" 会被转换为 "AoneAppCenter" 类。 + * 例如 "server-center" 会被转换为 "ServerCenter" 类。 * * @return MCP Server 名称,不能为 null */ diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/naming/NameNormalizer.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/naming/NameNormalizer.java index bd480b7..4a5009f 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/naming/NameNormalizer.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/naming/NameNormalizer.java @@ -120,15 +120,15 @@ else if (c == '_' || c == '-' || c == ' ' || c == '.' || c == '·') { // 常见分隔符转为下划线 sb.append('_'); } + else if (Pinyin.isChinese(c)) { + // 中文字符转为拼音(优先使用 Pinyin 库的判断,更准确) + String pinyin = Pinyin.toPinyin(c).toLowerCase(); + sb.append(pinyin); + logger.debug("NameNormalizer#normalizeToIdentifier - reason=中文转拼音, char={}, pinyin={}", c, pinyin); + } else if (isChinese(c)) { - // 中文字符转为拼音 - String pinyin = toPinyin(c); - if (pinyin != null && !pinyin.isEmpty()) { - sb.append(pinyin); - } - else { - sb.append('_'); - } + // 其他 CJK 相关字符(标点等)转为下划线 + sb.append('_'); } else { // 其他特殊字符转为下划线 @@ -171,15 +171,6 @@ private boolean isChinese(char c) { || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION; } - /** - * 将单个中文字符转换为拼音。 - */ - private String toPinyin(char c) { - if (Pinyin.isChinese(c)) { - return Pinyin.toPinyin(c).toLowerCase(); - } - return null; - } /** * 确保名称唯一。 diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java index 16fe7f7..7b6101d 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java @@ -4,6 +4,7 @@ import com.alibaba.assistant.agent.extension.experience.hook.CommonSenseExperienceModelHook; import com.alibaba.assistant.agent.extension.experience.hook.FastIntentReactHook; import com.alibaba.assistant.agent.extension.experience.hook.ReactExperienceAgentHook; +import com.alibaba.assistant.agent.extension.experience.tool.CommonSenseInjectionTool; import com.alibaba.assistant.agent.extension.experience.fastintent.FastIntentService; import com.alibaba.assistant.agent.extension.experience.internal.InMemoryExperienceProvider; import com.alibaba.assistant.agent.extension.experience.internal.InMemoryExperienceRepository; @@ -126,4 +127,21 @@ public CommonSenseExperienceModelHook commonSenseExperienceModelHook(ExperienceP log.info("ExperienceExtensionAutoConfiguration#commonSenseExperienceModelHook - reason=creating common sense prompt model hook bean"); return new CommonSenseExperienceModelHook(experienceProvider, properties); } + + /** + * 配置常识经验注入假工具 + * + *

    这个工具与 CommonSenseExperienceModelHook 配套使用。 + * Hook 会注入 AssistantMessage + ToolResponseMessage 配对来模拟工具调用, + * 注册这个工具可以让 ReactAgent 的路由逻辑正确识别和处理。 + */ + @Bean + @ConditionalOnProperty(prefix = "spring.ai.alibaba.codeact.extension.experience", + name = "common-experience-enabled", + havingValue = "true", + matchIfMissing = true) + public CommonSenseInjectionTool commonSenseInjectionTool() { + log.info("ExperienceExtensionAutoConfiguration#commonSenseInjectionTool - reason=creating common sense injection tool bean"); + return new CommonSenseInjectionTool(); + } } diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java index 940b294..5798770 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java @@ -124,9 +124,8 @@ public CompletableFuture> beforeAgent(OverAllState state, Ru Optional bestOpt = fastIntentService.selectBestMatch(experiences, ctx); if (bestOpt.isEmpty()) { log.debug("FastIntentReactHook#beforeAgent - reason=no matched experience"); - return CompletableFuture.completedFuture(Map.of( - "jump_to", JumpTo.model - )); + // 不设置 jump_to,让正常流程继续(否则会导致无限循环到 model) + return CompletableFuture.completedFuture(Map.of()); } Experience best = bestOpt.get(); diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/CommonSenseInjectionTool.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/CommonSenseInjectionTool.java new file mode 100644 index 0000000..e979903 --- /dev/null +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/CommonSenseInjectionTool.java @@ -0,0 +1,36 @@ +package com.alibaba.assistant.agent.extension.experience.tool; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.tool.annotation.Tool; + +/** + * 常识经验注入假工具 + * + *

    这个工具不执行任何实际操作,仅用于支持 CommonSenseExperienceModelHook + * 通过 AssistantMessage + ToolResponseMessage 配对方式注入常识经验。 + * + *

    注册这个工具可以让 ReactAgent 的路由逻辑正确识别和处理经验注入。 + * + * @author Assistant Agent Team + */ +public class CommonSenseInjectionTool { + + private static final Logger log = LoggerFactory.getLogger(CommonSenseInjectionTool.class); + + /** + * 常识经验注入方法 - 这是一个占位工具,实际不会被调用 + * + *

    Hook 会预先构造 AssistantMessage(toolCall) + ToolResponseMessage 配对, + * 模拟已经完成的工具调用,所以这个方法不会被真正执行。 + * + * @return 空字符串(实际不会被调用) + */ + @Tool(name = "common_sense_injection", + description = "内部工具:用于注入常识经验到对话上下文。此工具由系统自动调用,无需用户手动触发。") + public String inject() { + log.warn("CommonSenseInjectionTool#inject - reason=此工具不应被直接调用,仅作为占位工具存在"); + return ""; + } +} + diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/config/LearningExtensionAutoConfiguration.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/config/LearningExtensionAutoConfiguration.java index 87eea42..c88df68 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/config/LearningExtensionAutoConfiguration.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/config/LearningExtensionAutoConfiguration.java @@ -41,6 +41,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -48,6 +50,7 @@ import org.springframework.context.annotation.Configuration; import java.util.List; +import java.util.concurrent.ExecutorService; /** * 学习模块自动配置类 @@ -69,13 +72,30 @@ public LearningExtensionAutoConfiguration() { /** * 配置异步学习处理器 + *

    + * 如果提供了名为 "learningAsyncExecutor" 的 ExecutorService Bean, + * 则使用该自定义 ExecutorService(支持 trace 上下文传递等特性)。 + * 否则使用默认的线程池配置。 + * + * @param properties 学习扩展配置 + * @param customExecutor 可选的自定义 ExecutorService(名为 "learningAsyncExecutor") + * @return AsyncLearningHandler */ @Bean @ConditionalOnMissingBean - public AsyncLearningHandler asyncLearningHandler(LearningExtensionProperties properties) { + public AsyncLearningHandler asyncLearningHandler( + LearningExtensionProperties properties, + @Autowired(required = false) @Qualifier("learningAsyncExecutor") ExecutorService customExecutor) { + if (customExecutor != null) { + log.info( + "LearningExtensionAutoConfiguration#asyncLearningHandler - reason=creating async learning handler with custom executor, executorType={}", + customExecutor.getClass().getSimpleName()); + return new AsyncLearningHandler(customExecutor); + } + LearningExtensionProperties.AsyncConfig asyncConfig = properties.getAsync(); log.info( - "LearningExtensionAutoConfiguration#asyncLearningHandler - reason=creating async learning handler, threadPoolSize={}, queueCapacity={}", + "LearningExtensionAutoConfiguration#asyncLearningHandler - reason=creating async learning handler with default executor, threadPoolSize={}, queueCapacity={}", asyncConfig.getThreadPoolSize(), asyncConfig.getQueueCapacity()); return new AsyncLearningHandler(asyncConfig.getThreadPoolSize(), asyncConfig.getQueueCapacity()); } diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/hook/AfterAgentLearningHook.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/hook/AfterAgentLearningHook.java index 01e24d3..fa5948c 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/hook/AfterAgentLearningHook.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/hook/AfterAgentLearningHook.java @@ -18,6 +18,10 @@ import com.alibaba.assistant.agent.common.hook.AgentPhase; import com.alibaba.assistant.agent.common.hook.HookPhases; +import com.alibaba.assistant.agent.common.hook.AgentPhase; +import com.alibaba.assistant.agent.common.hook.HookPhases; +import com.alibaba.assistant.agent.core.observation.HookObservationHelper; +import com.alibaba.assistant.agent.core.observation.ObservationState; import com.alibaba.assistant.agent.extension.learning.internal.DefaultLearningContext; import com.alibaba.assistant.agent.extension.learning.internal.DefaultLearningTask; import com.alibaba.assistant.agent.extension.learning.model.LearningContext; @@ -71,8 +75,13 @@ public CompletableFuture> afterAgent(OverAllState state, Run try { log.info("AfterAgentLearningHook#afterAgent - reason=agent execution completed, starting learning process"); + // 注册自定义观测数据到 ObservationState + registerObservationData(state, "hook.input.learningType", learningType); + registerObservationData(state, "hook.input.triggerSource", LearningTriggerSource.AFTER_AGENT.name()); + // 1. 从state中提取对话历史 List conversationHistory = extractConversationHistory(state); + registerObservationData(state, "hook.input.conversationHistorySize", conversationHistory.size()); // 2. 构建学习上下文 LearningContext context = DefaultLearningContext.builder() @@ -92,36 +101,52 @@ public CompletableFuture> afterAgent(OverAllState state, Run // 4. 判断是否应该触发学习 if (!learningStrategy.shouldTriggerLearning(triggerContext)) { log.info("AfterAgentLearningHook#afterAgent - reason=strategy decided not to trigger learning"); + registerObservationData(state, "hook.output.status", "NOT_TRIGGERED"); + registerObservationData(state, "hook.output.triggered", false); return CompletableFuture.completedFuture(Map.of()); } - // 4. 构建学习任务 + // 注册学习触发信息 + registerObservationData(state, "hook.output.triggered", true); + + // 5. 构建学习任务 LearningTask task = DefaultLearningTask.builder() .learningType(learningType) .triggerSource(LearningTriggerSource.AFTER_AGENT) .context(context) .build(); - // 5. 执行学习(异步或同步) + registerObservationData(state, "hook.output.taskId", task.getId()); + + // 6. 执行学习(异步或同步) if (learningStrategy.shouldExecuteAsync(task)) { log.info( "AfterAgentLearningHook#afterAgent - reason=executing learning asynchronously, taskId={}", task.getId()); + registerObservationData(state, "hook.output.executionMode", "ASYNC"); + learningExecutor.executeAsync(task).exceptionally(ex -> { log.error( "AfterAgentLearningHook#afterAgent - reason=async learning execution failed, taskId={}", task.getId(), ex); return null; }); + registerObservationData(state, "hook.output.status", "ASYNC_SUBMITTED"); } else { log.debug("AfterAgentLearningHook#afterAgent - reason=executing learning synchronously, taskId={}", task.getId()); + registerObservationData(state, "hook.output.executionMode", "SYNC"); + LearningResult result = learningExecutor.execute(task); if (!result.isSuccess()) { log.warn( "AfterAgentLearningHook#afterAgent - reason=learning execution failed, taskId={}, failureReason={}", task.getId(), result.getFailureReason()); + registerObservationData(state, "hook.output.status", "FAILED"); + registerObservationData(state, "hook.output.failureReason", truncate(result.getFailureReason(), 200)); + } else { + registerObservationData(state, "hook.output.status", "SUCCESS"); } } @@ -129,6 +154,8 @@ public CompletableFuture> afterAgent(OverAllState state, Run catch (Exception e) { // 学习失败不影响主流程 log.error("AfterAgentLearningHook#afterAgent - reason=learning hook failed", e); + registerObservationData(state, "hook.output.status", "ERROR"); + registerObservationData(state, "hook.output.errorType", e.getClass().getSimpleName()); } return CompletableFuture.completedFuture(Map.of()); @@ -154,5 +181,42 @@ private List extractConversationHistory(OverAllState state) { } } -} + /** + * 注册自定义观测数据到 ObservationState + *

    + * 这些数据会被 CodeactAgentObservationLifecycleListener 收集并记录到 Observation 中。 + * + * @param state OverAllState + * @param key 数据键,建议使用 "hook." 前缀 + * @param value 数据值 + */ + private void registerObservationData(OverAllState state, String key, Object value) { + try { + // 尝试获取 ObservationState + Object obsStateObj = state.value("_observation_state_").orElse(null); + if (obsStateObj != null) { + // 使用反射调用 put 方法,避免直接依赖 ObservationState 类 + java.lang.reflect.Method putMethod = obsStateObj.getClass().getMethod("put", String.class, Object.class); + putMethod.invoke(obsStateObj, key, value); + log.debug("AfterAgentLearningHook#registerObservationData - reason=注册观测数据成功, key={}", key); + } + } catch (Exception e) { + // 静默失败,不影响主流程 + log.debug("AfterAgentLearningHook#registerObservationData - reason=注册观测数据失败, key={}, error={}", key, e.getMessage()); + } + } + /** + * 截断字符串 + */ + private String truncate(String str, int maxLength) { + if (str == null) { + return "null"; + } + if (str.length() <= maxLength) { + return str; + } + return str.substring(0, maxLength) + "..."; + } + +} diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/internal/AsyncLearningHandler.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/internal/AsyncLearningHandler.java index f75e347..2075f6d 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/internal/AsyncLearningHandler.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/internal/AsyncLearningHandler.java @@ -24,6 +24,8 @@ /** * 异步学习处理器 * 管理异步学习任务的执行,包括线程池管理和任务拒绝策略 + *

    + * 支持自定义 ExecutorService 注入,允许调用方传入支持 trace 上下文传递的线程池。 * * @author Assistant Agent Team * @since 1.0.0 @@ -33,7 +35,14 @@ public class AsyncLearningHandler { private static final Logger log = LoggerFactory.getLogger(AsyncLearningHandler.class); private final ExecutorService executorService; + private final boolean externalExecutor; + /** + * 使用默认线程池配置创建 AsyncLearningHandler + * + * @param threadPoolSize 线程池大小 + * @param queueCapacity 队列容量 + */ public AsyncLearningHandler(int threadPoolSize, int queueCapacity) { this.executorService = new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadFactory() { @@ -44,12 +53,30 @@ public Thread newThread(Runnable r) { return new Thread(r, "learning-async-" + (count++)); } }, new ThreadPoolExecutor.CallerRunsPolicy()); + this.externalExecutor = false; log.info( - "AsyncLearningHandler#constructor - reason=async learning handler initialized, threadPoolSize={}, queueCapacity={}", + "AsyncLearningHandler#constructor - reason=async learning handler initialized with default executor, threadPoolSize={}, queueCapacity={}", threadPoolSize, queueCapacity); } + /** + * 使用自定义 ExecutorService 创建 AsyncLearningHandler + *

    + * 允许调用方传入支持 trace 上下文传递的线程池(如 TraceAwareExecutorService), + * 确保异步任务的 traceId 与父线程保持一致。 + * + * @param executorService 自定义的 ExecutorService + */ + public AsyncLearningHandler(ExecutorService executorService) { + this.executorService = executorService; + this.externalExecutor = true; + + log.info( + "AsyncLearningHandler#constructor - reason=async learning handler initialized with custom executor, executorType={}", + executorService.getClass().getSimpleName()); + } + /** * 异步执行任务 * @param task 任务 @@ -70,8 +97,16 @@ public CompletableFuture executeAsync(Callable task) { /** * 关闭线程池 + *

    + * 注意:如果使用的是外部传入的 ExecutorService,则不会关闭它, + * 由外部调用方负责管理其生命周期。 */ public void shutdown() { + if (externalExecutor) { + log.info("AsyncLearningHandler#shutdown - reason=skipping shutdown for external executor"); + return; + } + log.info("AsyncLearningHandler#shutdown - reason=shutting down async learning handler"); executorService.shutdown(); try { diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/BaseReplyCodeactTool.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/BaseReplyCodeactTool.java index f2d8c17..8ef3582 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/BaseReplyCodeactTool.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/BaseReplyCodeactTool.java @@ -106,13 +106,14 @@ public BaseReplyCodeactTool(String toolName, String description, ReplyChannelDef @Override public String call(String toolInput) { + log.debug("BaseReplyCodeactTool#call(单参数) - reason=框架调用了单参数版本, toolName={}", toolName); return call(toolInput, null); } @Override public String call(String toolInput, ToolContext toolContext) { - log.debug("BaseReplyCodeactTool#call - reason=开始执行回复工具, toolName={}, toolInput={}", toolName, - toolInput); + log.debug("BaseReplyCodeactTool#call - reason=开始执行回复工具, toolName={}, hasToolContext={}", + toolName, toolContext != null); try { // 解析 JSON 输入参数 @@ -480,15 +481,6 @@ private ChannelExecutionContext buildExecutionContext(ToolContext toolContext) { ToolContextHelper.getFromMetadata(toolContext, "user_id").ifPresent(builder::userId); ToolContextHelper.getFromMetadata(toolContext, "trace_id").ifPresent(builder::traceId); - // 从 metadata 获取所有扩展字段,放入 extensions - // 这样业务方可以通过 extensions 传递自定义数据 - ToolContextHelper.getAllMetadata(toolContext).ifPresent(metadata -> { - for (Map.Entry entry : metadata.entrySet()) { - String key = entry.getKey(); - builder.extension(key, entry.getValue()); - } - }); - return builder.build(); } diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/ReplyCodeactToolFactory.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/ReplyCodeactToolFactory.java index 436a19e..98c040d 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/ReplyCodeactToolFactory.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/reply/tools/ReplyCodeactToolFactory.java @@ -110,12 +110,69 @@ public ReplyCodeactTool createTool(ReplyToolConfig config) { /** * 构建参数模式。 + *

    优先使用配置中的参数定义,如果配置中没有,则从渠道定义中获取。 */ private ParameterSchema buildParameterSchema(ReplyToolConfig config, ReplyChannelDefinition channel) { - // 从配置和渠道定义中构建参数模式 - return ParameterSchema.builder() - .parameter("message", ParameterSchema.ParameterType.STRING, true, "要发送的消息内容") - .build(); + // 1. 优先使用配置中的参数定义 + if (config.getParameters() != null && !config.getParameters().isEmpty()) { + ParameterSchema.Builder builder = ParameterSchema.builder(); + for (ReplyToolConfig.ParameterConfig paramConfig : config.getParameters()) { + ParameterSchema.ParameterType type = convertToParameterType(paramConfig.getType()); + ParameterSchema.ParameterDef def = new ParameterSchema.ParameterDef( + paramConfig.getName(), + type, + paramConfig.isRequired(), + paramConfig.getDescription() + ); + def.setDefaultValue(paramConfig.getDefaultValue()); + def.setEnumValues(paramConfig.getEnumValues()); + builder.parameter(def); + } + log.debug("ReplyCodeactToolFactory#buildParameterSchema - reason=使用配置中的参数定义, toolName={}, paramCount={}", + config.getToolName(), config.getParameters().size()); + return builder.build(); + } + + // 2. 如果配置中没有参数定义,从渠道定义中获取 + ParameterSchema channelSchema = channel.getSupportedParameters(); + if (channelSchema != null) { + log.debug("ReplyCodeactToolFactory#buildParameterSchema - reason=使用渠道定义的参数, toolName={}, channelCode={}", + config.getToolName(), config.getChannelCode()); + return channelSchema; + } + + // 3. 如果都没有,返回空的参数模式 + log.warn("ReplyCodeactToolFactory#buildParameterSchema - reason=未找到参数定义将使用空参数模式, toolName={}, channelCode={}", + config.getToolName(), config.getChannelCode()); + return ParameterSchema.builder().build(); + } + + /** + * 将字符串类型转换为 ParameterType。 + */ + private ParameterSchema.ParameterType convertToParameterType(String type) { + if (type == null) { + return ParameterSchema.ParameterType.STRING; + } + String lowerType = type.toLowerCase(); + switch (lowerType) { + case "integer": + case "int": + case "long": + return ParameterSchema.ParameterType.INTEGER; + case "boolean": + case "bool": + return ParameterSchema.ParameterType.BOOLEAN; + case "array": + case "list": + return ParameterSchema.ParameterType.ARRAY; + case "object": + case "map": + return ParameterSchema.ParameterType.OBJECT; + case "string": + default: + return ParameterSchema.ParameterType.STRING; + } } /** diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/search/tools/UnifiedSearchCodeactTool.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/search/tools/UnifiedSearchCodeactTool.java index 5467c99..12b4394 100644 --- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/search/tools/UnifiedSearchCodeactTool.java +++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/search/tools/UnifiedSearchCodeactTool.java @@ -37,13 +37,7 @@ import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.definition.ToolDefinition; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; /** diff --git a/assistant-agent-prompt-builder/pom.xml b/assistant-agent-prompt-builder/pom.xml index 3d8d8b4..15657d8 100644 --- a/assistant-agent-prompt-builder/pom.xml +++ b/assistant-agent-prompt-builder/pom.xml @@ -6,7 +6,7 @@ com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 assistant-agent-prompt-builder diff --git a/assistant-agent-start/pom.xml b/assistant-agent-start/pom.xml index 3b18432..96ea8c6 100644 --- a/assistant-agent-start/pom.xml +++ b/assistant-agent-start/pom.xml @@ -6,7 +6,7 @@ com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 assistant-agent-start @@ -43,6 +43,13 @@ python-community pom + + + + org.junit.jupiter + junit-jupiter + test + diff --git a/assistant-agent-start/src/main/java/com/alibaba/assistant/agent/start/config/ExperienceEvaluationCriterionProvider.java b/assistant-agent-start/src/main/java/com/alibaba/assistant/agent/start/config/ExperienceEvaluationCriterionProvider.java index 705b99e..239f1ba 100644 --- a/assistant-agent-start/src/main/java/com/alibaba/assistant/agent/start/config/ExperienceEvaluationCriterionProvider.java +++ b/assistant-agent-start/src/main/java/com/alibaba/assistant/agent/start/config/ExperienceEvaluationCriterionProvider.java @@ -72,7 +72,7 @@ public List getReactPhaseCriteria() { .reasoningPolicy(ReasoningPolicy.NONE) .evaluatorType(EvaluatorType.LLM_BASED) .evaluatorRef("llm-based") - .contextBindings("context.userInput") + .contextBindings("context.input.userInput") .build(); // 2. 模糊程度判断 Criterion @@ -97,7 +97,7 @@ public List getReactPhaseCriteria() { .reasoningPolicy(ReasoningPolicy.NONE) .evaluatorType(EvaluatorType.LLM_BASED) .evaluatorRef("llm-based") - .contextBindings("userInput") + .contextBindings("context.input.userInput") .build(); EvaluationCriterion reactExperienceRetrieval = EvaluationCriterionBuilder @@ -107,7 +107,7 @@ public List getReactPhaseCriteria() { .evaluatorType(EvaluatorType.RULE_BASED) .evaluatorRef("react_experience_evaluator") .dependsOn("enhanced_user_input") - .contextBindings("userInput") + .contextBindings("context.input.userInput") .build(); return List.of(enhancedUserInput, isFuzzy, reactExperienceRetrieval); @@ -133,7 +133,7 @@ public List getCodeActPhaseCriteria() { .reasoningPolicy(ReasoningPolicy.NONE) .evaluatorType(EvaluatorType.LLM_BASED) .evaluatorRef("llm-based") - .contextBindings("userInput") + .contextBindings("context.input.userInput") .build(); // 2. purpose 判断 Criterion @@ -159,7 +159,7 @@ public List getCodeActPhaseCriteria() { .reasoningPolicy(ReasoningPolicy.NONE) .evaluatorType(EvaluatorType.LLM_BASED) .evaluatorRef("llm-based") - .contextBindings("userInput") + .contextBindings("context.input.userInput") .build(); EvaluationCriterion codeactExperienceRetrieval = EvaluationCriterionBuilder @@ -169,7 +169,7 @@ public List getCodeActPhaseCriteria() { .evaluatorType(EvaluatorType.RULE_BASED) .evaluatorRef("codeact_experience_evaluator") .dependsOn("enhanced_user_input") - .contextBindings("userInput") + .contextBindings("context.input.userInput") .build(); return List.of(enhancedUserInput, purpose, codeactExperienceRetrieval); diff --git a/pom.xml b/pom.xml index 2b54164..7fd349b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.alibaba.agent.assistant assistant-agent - 0.1.3-SNAPSHOT + 0.1.3 pom assistant-agent-autoconfigure @@ -24,8 +24,9 @@ 24.2.1 3.4.8 1.1.0.0 - 1.1.0.0 + 1.1.2.0 1.1.0 + 1.35.0 @@ -120,6 +121,15 @@ pom import + + + + io.opentelemetry + opentelemetry-bom + ${opentelemetry.version} + pom + import + From cca0e5c5fd0cdb0f8bc35e8d78ff05e69c6abd34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=8B=E9=A3=8E?= Date: Wed, 4 Feb 2026 14:37:28 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20CHANGELOG.md?= =?UTF-8?q?=20=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E8=87=B3=200.1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 87 +++++++++- README.md | 467 ++++----------------------------------------------- README_zh.md | 455 ++++--------------------------------------------- 3 files changed, 141 insertions(+), 868 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2c541..a1ccf59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## [1.0.0] - YYYY-MM-DD +## [0.1.3] - 2026-02-04 + +### Added + +#### Observation & Traceability +- **OpenTelemetry Observability Support**: Full OpenTelemetry native API integration + - `BaseAgentObservationLifecycleListener`: Base lifecycle listener for Agent observability + - `CodeactObservationDocumentation`: Standardized observation metrics definition (Hook, Interceptor, React, Execution, CodeGen, ToolCall) + - `EvaluationObservationLifecycleListener`: Observability listener for Evaluation Graph +- **Tool Call Tracing**: New tool call recording and tracing capabilities + - `ToolCallRecord`: Tool call record model with call order and tool name + - `ExecutionRecord.callTrace`: Tool call trace list during code execution + - `ToolRegistryBridge`: Tool registry bridge +- **Observation Helper Classes**: + - `HookObservationHelper`: Hook execution observation helper + - `InterceptorObservationHelper`: Interceptor execution observation helper + - `OpenTelemetryObservationHelper`: General OpenTelemetry observation helper +- **Observation Contexts**: + - `HookObservationContext`: Hook observation context + - `InterceptorObservationContext`: Interceptor observation context + - `ReactPhaseObservationContext`: React phase observation context + - `CodeGenerationObservationContext`: Code generation observation context + - `CodeactExecutionObservationContext`: Code execution observation context + - `CodeactToolCallObservationContext`: Tool call observation context + +#### Prompt Contributor Module +- **PromptContributor Mechanism Refactoring**: Replaced PromptBuilder/PromptManager with a more flexible Prompt contribution system + - `PromptContributor`: Prompt contributor interface + - `PromptContributorManager`: Prompt contributor manager interface + - `DefaultPromptContributorManager`: Default implementation with priority sorting and dynamic registration + - `PromptContributorContext`: Context interface + - `OverAllStatePromptContributorContext`: OverAllState-based context implementation +- **Evaluation-based Prompt Contribution**: + - `EvaluationBasedPromptContributor`: Abstract base class for generating Prompts based on evaluation results + - `PromptContributorModelHook`: Abstract base class for integrating PromptContributor into ModelHook + - `ReactPromptContributorModelHook`: Prompt contribution Hook for React phase + - `CodeactPromptContributorModelHook`: Prompt contribution Hook for Codeact phase +- **Auto Configuration**: `PromptContributorAutoConfiguration` provides out-of-the-box configuration + +#### Other Enhancements +- `ParameterTree`: Enhanced parameter tree definition capabilities +- `CommonSenseInjectionTool`: Common sense injection tool +- `ToolContextHelper`: Tool context helper class +- `CodeactStateKeys`: Codeact state key constants + +### Changed +- **GraalCodeExecutor**: Enhanced code executor with tool call tracing support +- **PythonToolViewRenderer**: Enhanced Python tool view rendering capabilities +- **EvaluationService**: Support for parent Span for distributed tracing +- **EvaluationSuiteBuilder**: Enhanced evaluation suite building capabilities +- **ReplyCodeactToolFactory**: Optimized reply tool factory implementation +- **AfterAgentLearningHook**: Enhanced learning Hook implementation +- **AsyncLearningHandler**: Optimized async learning handler +- **CodeactAgent**: Refactored to support new observation and Prompt contribution mechanism + +### Removed +- `PromptBuilder`: Replaced by PromptContributor +- `PromptManager`: Replaced by PromptContributorManager +- `PromptInjectionInterceptor`: Replaced by PromptContributorModelHook +- `CodeactToolFilter`: Tool filter +- `WhitelistMode`: Whitelist mode enum + +--- + +## [0.1.2] - 2026-01-XX + +### Added +- Baidu Qianfan intelligent search API integration +- HookPhases annotation support +- McpServerAwareToolCallback interface for enhanced MCP dynamic tool creation + +### Fixed +- Fixed context binding path in evaluation criteria +- Fixed null pointer exception in UnifiedSearchCodeactTool + +--- + +## [0.1.0] - Initial Release ### Added @@ -89,9 +166,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Version History Summary -| Version | Date | Description | -|---------|------|-------------| -| 1.0.0 | TBD | Initial open source release | +| Version | Date | Description | +|---------|------------|-----------------------------------------------------------------------------| +| 0.1.3 | 2026-02-04 | Enhanced tool call tracing and observability, refactored Prompt Contributor module, bug fixes | +| 0.1.2 | 2026-01-XX | Baidu Qianfan search integration, HookPhases annotation, bug fixes | +| 0.1.0 | TBD | Initial open source release | --- diff --git a/README.md b/README.md index 4457146..ca2117d 100644 --- a/README.md +++ b/README.md @@ -205,455 +205,46 @@ public class MyKnowledgeSearchProvider implements SearchProvider { > 📖 For more details, refer to: [Knowledge Search Module Documentation](assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/search/README.md) -## 🧩 Core Module Introduction +## 🧩 Core Modules -### Evaluation Module +For detailed documentation on each module, please visit our [Documentation Site](https://java2ai.com/agents/assistantagent/quick-start). -**Role**: Multi-dimensional intent recognition framework that performs multi-layer trait recognition through Evaluation Graph. +### Core Modules -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Evaluation Graph Example │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ User Input: "Query today's orders" │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Layer 1 (parallel execution) │ │ -│ │ ┌────────────┐ ┌────────────┐ │ │ -│ │ │ Is Vague? │ │ Rewrite │ │ │ -│ │ │ clear/vague│ │ (enhance) │ │ │ -│ │ └─────┬──────┘ └─────┬──────┘ │ │ -│ └─────────┼──────────────────────┼────────────────────────┘ │ -│ │ │ │ -│ └──────────┬───────────┘ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Layer 2 (based on rewritten content, parallel) │ │ -│ │ ┌──────────┐ ┌───────────┐ ┌───────────┐ │ │ -│ │ │Experience│ │ Tool │ │ Knowledge │ │ │ -│ │ │available │ │ Available │ │ Available │ │ │ -│ │ │ yes/no │ │ yes/no │ │ yes/no │ │ │ -│ │ └──────────┘ └───────────┘ └───────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────┐ │ -│ │ Integrate evaluation│ │ -│ │ results from │ │ -│ │ different dimensions│ │ -│ │ → Pass to modules │ │ -│ └─────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` +| Module | Description | Documentation | +|--------|-------------|---------------| +| **Evaluation** | Multi-dimensional intent recognition through Evaluation Graph with LLM and rule-based engines | [Quick Start](https://java2ai.com/agents/assistantagent/features/evaluation/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/evaluation/advanced) | +| **Prompt Builder** | Dynamic prompt assembly based on evaluation results and runtime context | [Quick Start](https://java2ai.com/agents/assistantagent/features/prompt-builder/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/prompt-builder/advanced) | -**Core Capabilities**: -- **Dual Evaluation Engines**: - - **LLM Evaluation**: Complex semantic judgment through large models. Users can fully customize evaluation prompts (`customPrompt`), or use default prompt assembly (supports `description`, `workingMechanism`, `fewShots` configurations) - - **Rule-based Evaluation**: Implement rule logic through Java functions. Users can customize `Function` to execute any rule judgment, suitable for threshold detection, format validation, exact matching, etc. -- **Custom Dependencies**: Evaluation items can declare dependencies via `dependsOn`. The system automatically builds an evaluation graph for topological execution - items without dependencies run in parallel, items with dependencies run sequentially. Subsequent evaluation items can access results from preceding items. -- **Evaluation Results**: Support `BOOLEAN`, `ENUM`, `SCORE`, `JSON`, `TEXT` and other types, passed to Prompt Builder to drive dynamic assembly +### Tool Extensions ---- +| Module | Description | Documentation | +|--------|-------------|---------------| +| **MCP Tools** | Integration with Model Context Protocol servers for tool ecosystem reuse | [Quick Start](https://java2ai.com/agents/assistantagent/features/mcp/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/mcp/advanced) | +| **Dynamic HTTP Tools** | REST API integration through OpenAPI specification | [Quick Start](https://java2ai.com/agents/assistantagent/features/dynamic-http/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/dynamic-http/advanced) | +| **Custom CodeAct Tools** | Build custom tools using the CodeactTool interface | [Quick Start](https://java2ai.com/agents/assistantagent/features/custom-codeact-tool/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/custom-codeact-tool/advanced) | -### Prompt Builder Module +### Intelligence Capabilities -**Role**: Dynamically assemble prompts sent to the model based on evaluation results and runtime context. Example: +| Module | Description | Documentation | +|--------|-------------|---------------| +| **Experience** | Accumulate and reuse historical successful execution experiences with FastIntent support | [Quick Start](https://java2ai.com/agents/assistantagent/features/experience/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/experience/advanced) | +| **Learning** | Automatically extract valuable experiences from Agent execution history | [Quick Start](https://java2ai.com/agents/assistantagent/features/learning/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/learning/advanced) | +| **Search** | Multi-source unified search engine for knowledge-based Q&A | [Quick Start](https://java2ai.com/agents/assistantagent/features/search/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/search/advanced) | -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Prompt Builder - Conditional Dynamic Generation │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Evaluation Results Input: │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Vague: yes │ Experience: yes │ Tools: yes │ Knowledge: no│ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Custom PromptBuilder Condition Matching │ │ -│ │ │ │ -│ │ vague=yes ────▶ inject [Clarification Prompt] │ │ -│ │ vague=no ────▶ inject [Direct Execution Prompt] │ │ -│ │ │ │ -│ │ experience=yes ──▶ inject [Historical Experience Reference] │ │ -│ │ tools=yes ──▶ inject [Tool Usage Instructions] │ │ -│ │ knowledge=yes ──▶ inject [Relevant Knowledge Snippets] │ │ -│ │ │ │ -│ │ Combo 1: vague + no tools + no knowledge ──▶ [Ask User Prompt]│ │ -│ │ Combo 2: clear + tools + experience ──▶ [Fast Execute Prompt] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Final Dynamic Prompt: │ │ -│ │ [System Prompt] + [Clarification Guide] + [Experience] + │ │ -│ │ [Tool Instructions] + [User Query] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────┐ │ -│ │ LLM │ │ -│ └──────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` +### Interaction Capabilities -**Core Capabilities**: -- Multiple PromptBuilders execute in priority order -- Each Builder decides whether to contribute and what content based on evaluation results -- Support custom Builders for business-specific prompt logic -- Non-intrusive, intercepts at model invocation level - -**Comparison with Traditional Approaches**: - -| Comparison | Traditional Approach | Evaluation + PromptBuilder | -|------------|---------------------|---------------------------| -| **Prompt Length** | Need to enumerate handling instructions for various situations ("when encountering situation A..., when encountering situation B..."), prompts become bloated | Through pre-evaluation to identify scenarios, only inject context needed for current scenario, prompts are shorter and more precise | -| **Agent Behavior Controllability** | Relies on model's "understanding" of lengthy instructions, prone to misjudgment | Behavior driven by evaluation results, reducing model misjudgment, more controllable | -| **Extension Flexibility** | Adding new scenarios requires modifying prompts, difficult to maintain | Modify relevant evaluation items and PromptBuilder based on business needs | -| **Code Architecture** | Evaluation logic coupled with prompts | Evaluation logic decoupled from prompt templates, separation of concerns, independent maintenance and iteration | - ---- - -### Learning Module - -**Role**: Automatically extract and save valuable experiences from Agent execution history. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Learning Module Workflow │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ Agent Execution Process │ │ -│ │ │ │ -│ │ Input ──▶ Reasoning ──▶ Code Gen ──▶ Execute ──▶ Output │ │ -│ │ │ │ │ │ │ │ │ -│ │ └───────────┴────────────┴───────────┴──────────┘ │ │ -│ │ │ │ │ -│ └────────────────────────────┼──────────────────────────────────────┘ │ -│ ▼ │ -│ ┌──────────────────────────────┐ │ -│ │ Learning Context Capture │ │ -│ │ - User Input │ │ -│ │ - Reasoning Steps │ │ -│ │ - Generated Code │ │ -│ │ - Execution Result │ │ -│ └───────────┬──────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ Learning Extractors Analysis │ │ -│ │ │ │ -│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ -│ │ │ Experience │ │ Pattern │ │ Error │ │ │ -│ │ │ Extractor │ │ Extractor │ │ Extractor │ │ │ -│ │ │Success Mode│ │Common Mode │ │Failure Mode│ │ │ -│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ -│ └────────┼───────────────┼───────────────┼─────────────────────┘ │ -│ │ │ │ │ -│ └───────────────┼───────────────┘ │ -│ ▼ │ -│ ┌────────────────┐ │ -│ │ Persist & │ ──▶ Available for future tasks │ -│ │ Store │ │ -│ └────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**Core Capabilities**: -- **After-Agent Learning**: Extract experiences after each Agent execution -- **After-Model Learning**: Extract experiences after each model call -- **Tool Interceptor**: Extract experiences from tool invocations -- **Offline Learning**: Batch analyze historical data to extract patterns -- **Learning Process**: Capture execution context → Extractor analysis and recognition → Generate experience records → Persist for subsequent reuse - ---- - -### Experience Module - -**Role**: Accumulate and reuse historical successful execution experiences. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Experience Module Workflow │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ [Scenario 1: Experience Accumulation] │ -│ │ -│ User: "Query order status" ──▶ Agent Success ──▶ ┌─────────────┐ │ -│ │ Save: │ │ -│ │ - ReAct Exp │ │ -│ │ - Code Exp │ │ -│ │ - Common Exp│ │ -│ └─────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────┐ │ -│ │ Experience DB │ │ -│ └────────────────┘ │ -│ │ -│ [Scenario 2: Experience Reuse] │ │ -│ │ │ -│ User: "Query my order status" ◀── Match Similar ◀──────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ Agent references history, faster decision + │ │ -│ │ generates correct code │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ [Scenario 3: FastIntent Quick Response] │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Experience DB │ │ -│ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ -│ │ │ Experience A │ │ Experience B │ │ │ -│ │ │ (Normal) │ │ (✓ FastIntent configured) │ │ │ -│ │ │ No FastIntent config│ │ Condition: prefix "View │ │ │ -│ │ │ → Inject to prompt │ │ *sales" │ │ │ -│ │ │ for LLM reference │ │ Action: Call sales API │ │ │ -│ │ └─────────────────────┘ └───────────┬────────────────┘ │ │ -│ └─────────────────────────────────────────────┼───────────────────┘ │ -│ │ Condition matched │ -│ ▼ │ -│ User: "View today's sales" ──▶ Match Exp B ──▶ Skip LLM, execute │ -│ FastIntent directly │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**Core Capabilities**: -- **Multiple Experience Types**: Code generation experience, ReAct decision experience, common sense experience, providing historical reference for similar tasks -- **Flexible Reuse**: Experiences can be injected into prompts or used for FastIntent matching -- **Lifecycle Management**: Support experience creation, update, and deletion -- **FastIntent Quick Response**: - - Experience must explicitly configure `fastIntentConfig` to enable - - When matching configured conditions, skip full LLM reasoning and directly execute pre-recorded tool calls or code -- Support multi-condition matching: message prefix, regex, metadata, state, etc. - ---- - -### Trigger Module - -**Role**: Create and manage scheduled tasks or event-triggered Agent executions. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Trigger Module Capabilities │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ [Scheduled Trigger] │ -│ │ -│ User: "Send me daily sales report at 9am" │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Agent creates │ │ Scheduler │ │ Auto Execute │ │ -│ │ Cron trigger │────▶│ 0 9 * * * │────▶│ Generate report│ │ -│ │ (self-schedule)│ │ │ │ Send notify │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -│ [Delayed Trigger] │ -│ │ -│ User: "Remind me about the meeting in 30 minutes" │ -│ │ │ -│ ▼ │ -│ ┌──────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Agent creates │ │ After 30min │ │ Send reminder │ │ -│ │ one-time trigger│────▶│ fire │────▶│ "Time to meet" │ │ -│ └──────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -│ [Callback Trigger] │ -│ │ -│ User: "Help me with xx when xx condition is met" │ -│ │ -│ External System: Send event to Webhook │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Receive │ │ Trigger Agent │ │ Process event │ │ -│ │ Webhook event │────▶│ execute task │────▶│ Return response│ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**Core Capabilities**: -- `TIME_CRON` Trigger: Support Cron expression for scheduled task triggers -- `TIME_ONCE` Trigger: Support one-time delayed trigger -- `CALLBACK` Trigger: Support callback event trigger -- Agent can autonomously create triggers through tools, achieving "self-scheduling" - ---- +| Module | Description | Documentation | +|--------|-------------|---------------| +| **Reply Channel** | Multi-channel message reply with routing support | [Quick Start](https://java2ai.com/agents/assistantagent/features/reply/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/reply/advanced) | +| **Trigger** | Scheduled tasks, delayed execution, and event callback triggers | [Quick Start](https://java2ai.com/agents/assistantagent/features/trigger/quickstart) | [Advanced](https://java2ai.com/agents/assistantagent/features/trigger/advanced) | -### Reply Channel Module - -**Role**: Provide flexible message reply capability, supporting multiple output channels. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Reply Channel Module Capabilities │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Agent needs to reply to user │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Channel Router │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ├──────────────┬──────────────┬──────────────┐ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ -│ │ DEFAULT │ │ IDE_CARD │ │ IM_NOTIFY │ │ WEBHOOK │ │ -│ │ Text Reply │ │ Card Display│ │ Push Notify │ │ JSON Push │ │ -│ └─────┬──────┘ └─────┬───────┘ └─────┬───────┘ └─────┬──────┘ │ -│ │ │ │ │ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌──────────┐ ┌───────────┐ ┌────────────┐ ┌──────────┐ │ -│ │ Console │ │ IDE │ │ IM │ │ External │ │ -│ │ Terminal │ │ Rich Card │ │(Extendable)│ │ System │ │ -│ └──────────┘ └───────────┘ └────────────┘ └──────────┘ │ -│ │ -│ [Usage Example] │ -│ │ -│ User: "Send results after analysis" │ -│ │ │ -│ ▼ │ -│ Agent: send_message(text="Analysis results...") │ -│ │ │ -│ ▼ │ -│ User receives: "📊 Analysis Results: ..." │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**Core Capabilities**: -- **Multi-channel Routing**: Agent can choose different channels to reply based on scenario -- **Configuration-driven**: Dynamically generate reply tools, no coding required -- **Sync/Async Support**: Support both synchronous and asynchronous reply modes -- **Unified Interface**: Shield underlying implementation differences -- **Built-in Demo Channel**: `IDE_TEXT` (for demonstration) -- **Extendable Channels** (by implementing `ReplyChannelDefinition` SPI): e.g. `IDE_CARD`, `IM_NOTIFICATION` (DingTalk/Feishu/WeCom), `WEBHOOK_JSON`, etc. - requires custom implementation - ---- - -### Dynamic Tools Module - -**Role**: Provide highly extensible tool system, enabling Agent to call various external tools to complete tasks. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Tool Extension Architecture │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Agent needs to execute operation │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────────────┐ │ -│ │ CodeactTool System │ │ -│ └──────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ├─────────────┬─────────────┬─────────────┬──────────────┐ │ -│ ▼ ▼ ▼ ▼ ▼ │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────┐ │ -│ │ MCP │ │ HTTP │ │ Search │ │ Trigger │ │Custom│ │ -│ │ Tools │ │ API │ │ Tools │ │ Tools │ │Tools │ │ -│ │ │ │ Tools │ │ │ │ │ │ │ │ -│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └──┬───┘ │ -│ │ │ │ │ │ │ -│ ▼ ▼ ▼ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌─────┐ │ -│ │ Any MCP │ │ REST API │ │ Knowledge │ │ Scheduled│ │ ... │ │ -│ │ Server │ │ OpenAPI │ │ Search │ │ Tasks │ │ │ │ -│ └──────────┘ └──────────┘ │ Project │ │ Callbacks│ └─────┘ │ -│ │ Search │ └──────────┘ │ -│ └───────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**Core Capabilities**: -- **MCP Tool Support**: One-click integration with any MCP Server, reuse MCP tool ecosystem -- **HTTP API Support**: Integrate REST APIs through OpenAPI specification, call existing enterprise interfaces -- **Built-in Tool Types**: Search, Reply, Trigger, Learning, etc. -- **Custom Tool SPI**: Implement `CodeactTool` interface to easily extend new tools - ---- - -### Knowledge Search Module - -**Role**: Multi-source unified search engine, providing knowledge support for Agent Q&A and decision-making. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Multi-Source Search Architecture │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ User Question: "How to configure database connection pool?" │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Unified Search Interface │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ├────────────────┬────────────────┬────────────────┐ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ -│ │ Knowledge │ │ Project │ │ Web │ │ Custom │ │ -│ │ Provider │ │ Provider │ │ Provider │ │Provider│ │ -│ │ (Primary) │ │ (Optional) │ │ (Optional) │ │ (SPI) │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └───┬────┘ │ -│ │ │ │ │ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ -│ │ FAQ / Docs │ │ Source Code │ │ Web Articles │ │ ... │ │ -│ │ Q&A History │ │ Config Files │ │ Tech Forums │ │ │ │ -│ │ Team Notes │ │ Logs │ │ │ │ │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ └────────┘ │ -│ │ │ │ │ │ -│ └─────────────────┴─────────────────┴──────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────┐ │ -│ │ Aggregate & Rank │ │ -│ │ → Inject into Prompt │ │ -│ └────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**Core Capabilities**: -- **Unified Search Interface**: `SearchProvider` SPI, supports pluggable data sources -- **Demo Providers**: Built-in Mock implementations for Knowledge, Project, Web (for demonstration and testing only) -- **Custom Extension**: Implement `SearchProvider` interface to connect any data source (databases, vector stores, APIs) -- **Result Aggregation**: Support configurable ranking strategies -- **Business Value**: Connect enterprise knowledge base to provide accurate answers, support answer traceability, reduce manual customer service pressure - -**Configuration Example**: - -```yaml -spring: - ai: - alibaba: - codeact: - extension: - search: - enabled: true - knowledge-search-enabled: true # Knowledge base (Mock implementation by default) - project-search-enabled: false # Project code (Mock implementation by default) - web-search-enabled: false # Web search (Mock implementation by default) - default-top-k: 5 - search-timeout-ms: 5000 -``` +### Additional Resources -> 💡 The above search features provide Mock implementations by default for demonstration and testing. For production use, implement `SearchProvider` SPI to connect actual data sources. +| Resource | Link | +|----------|------| +| Quick Start Guide | [AssistantAgent Quick Start](https://java2ai.com/agents/assistantagent/quick-start) | +| Secondary Development Guide | [Development Guide](https://java2ai.com/agents/assistantagent/secondary-development) | --- diff --git a/README_zh.md b/README_zh.md index 5239016..3fb9b2b 100644 --- a/README_zh.md +++ b/README_zh.md @@ -200,443 +200,46 @@ public class MyKnowledgeSearchProvider implements SearchProvider { > 📖 更多细节请参考:[知识检索模块文档](assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/search/README.md) -## 🧩 核心模块介绍 +## 🧩 核心模块 -### 评估模块(Evaluation) +各模块的详细文档请访问 [文档站点](https://java2ai.com/agents/assistantagent/quick-start)。 -**作用**:多维度意图识别框架,通过评估图(Graph)对信息进行多层次特质识别。 +### 核心模块 -``` -┌──────────────────────────────────────────────────────────────────┐ -│ 评估图 (Evaluation Graph) 示例 │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ 用户输入: "查询今日订单" │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Layer 1 (并行执行) │ │ -│ │ ┌────────────┐ ┌────────────┐ │ │ -│ │ │ 是否模糊? │ │ 输入改写 │ │ │ -│ │ │ 清晰/模糊 │ │(增强) │ │ │ -│ │ └─────┬──────┘ └─────┬──────┘ │ │ -│ └─────────┼──────────────────────┼────────────────────────┘ │ -│ │ │ │ -│ └──────────┬───────────┘ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Layer 2 (基于改写内容,并行执行) │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ -│ │ │ 检索经验 │ │ 匹配工具 │ │ 搜索知识 │ │ │ -│ │ │ 有/无 │ │ 有/无 │ │ 有/无 │ │ │ -│ │ └──────────┘ └──────────┘ └──────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────┐ │ -│ │ 整合不同维度评估结果 │ │ -│ │ → 传递给后续模块 │ │ -│ └────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - -**核心能力**: -- **双评估引擎**: - - **LLM 评估**:通过大模型进行复杂语义判断,用户可完全自定义评估 Prompt(`customPrompt`),也可使用默认 Prompt 组装(支持 `description`、`workingMechanism`、`fewShots` 等配置) - - **Rule-based 评估**:通过 Java 函数实现规则逻辑,用户自定义 `Function` 执行任意规则判断,适合阈值检测、格式校验、精确匹配等场景 -- **依赖关系自定义**:评估项可通过 `dependsOn` 声明前置依赖,系统自动构建评估图按拓扑执行,无依赖项并行、有依赖项顺序执行,后续评估项可访问前置评估项的结果 -- **评估结果**:支持 `BOOLEAN`、`ENUM`、`SCORE`、`JSON`、`TEXT` 等类型,传递给 Prompt Builder 驱动动态组装 - ---- - -### Prompt Builder 模块 - -**作用**:根据评估结果和运行时上下文,动态组装发送给模型的 Prompt。示例: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Prompt Builder - 条件化动态生成 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 评估结果输入: │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ 模糊: 是 │ 经验: 有 │ 工具: 有 │ 知识: 无 │ │ -│ └────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ 自定义 PromptBuilder 条件匹配 │ │ -│ │ │ │ -│ │ 模糊=是 ──────▶ 注入 [澄清引导 Prompt] │ │ -│ │ 模糊=否 ──────▶ 注入 [直接执行 Prompt] │ │ -│ │ │ │ -│ │ 经验=有 ──────▶ 注入 [历史经验参考] │ │ -│ │ 工具=有 ──────▶ 注入 [工具使用说明] │ │ -│ │ 知识=有 ──────▶ 注入 [相关知识片段] │ │ -│ │ │ │ -│ │ 组合示例1: 模糊+无工具+无知识 ──▶ [追问用户 Prompt] │ │ -│ │ 组合示例2: 清晰+有工具+有经验 ──▶ [快速执行 Prompt] │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ 最终动态 Prompt: │ │ -│ │ [系统提示] + [澄清引导] + [历史经验] + [工具说明] + [用户问题] │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────┐ │ -│ │ 模型 │ │ -│ └──────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**核心能力**: -- 多个 PromptBuilder 按优先级顺序执行 -- 每个 Builder 根据评估结果决定是否贡献、贡献什么内容 -- 支持自定义 Builder,根据业务需求定制 Prompt 逻辑 -- 非侵入式,在模型调用层拦截 - -**对比传统方案**: - -| 对比维度 | 传统方案 | 评估 + PromptBuilder | -|---------|---------|------------------------------------| -| **Prompt 长度** | 需要穷举各种情况的处理指令("遇到 A 情况应该...,遇到 B 情况应该..."),Prompt 臃肿 | 通过前置评估识别场景,仅注入当前场景所需的上下文,Prompt 更短更精确 | -| **Agent 行为可控性** | 依赖模型对冗长指令的"理解",容易误判 | 行为由评估结果驱动,减少模型误判,更可控 | -| **扩展灵活性** | 新增场景需修改 Prompt,维护困难 | 根据业务需求修改相关评估项与PromptBuilder | -| **代码架构** | 评估逻辑与 Prompt 耦合在一起 | 评估逻辑与 Prompt 模板解耦,关注点分离,独立维护和迭代 | - ---- - -### 学习模块(Learning) - -**作用**:从 Agent 执行历史中自动提取并保存有价值的经验。 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 学习模块工作流程 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ Agent 执行过程 │ │ -│ │ │ │ -│ │ 输入 ──▶ 推理 ──▶ 代码生成 ──▶ 执行 ──▶ 输出 │ │ -│ │ │ │ │ │ │ │ │ -│ │ └────────┴──────────┴─────────┴────────┘ │ │ -│ │ │ │ │ -│ └────────────────────────┼───────────────────────────────────────────┘ │ -│ ▼ │ -│ ┌────────────────────────┐ │ -│ │ 学习上下文捕获 │ │ -│ │ - 用户输入 │ │ -│ │ - 中间推理步骤 │ │ -│ │ - 生成的代码 │ │ -│ │ - 执行结果 │ │ -│ └───────────┬────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ 学习提取器分析 │ │ -│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ -│ │ │ 经验提取器 │ │ 模式提取器 │ │ 错误提取器 │ │ │ -│ │ │ 成功模式 │ │ 通用模式 │ │ 失败教训 │ │ │ -│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ -│ └────────┼───────────────┼───────────────┼─────────────────────┘ │ -│ │ │ │ │ -│ └───────────────┼───────────────┘ │ -│ ▼ │ -│ ┌────────────────┐ │ -│ │ 持久化存储 │ ──▶ 供后续任务参考使用 │ -│ └────────────────┘ │ -│ │ -└────────────────────────────────────────────────────────────────────────┘ -``` - -**核心能力**: -- **After-Agent 学习**:每次 Agent 运行完成后提取经验 -- **After-Model 学习**:每次模型调用后提取经验 -- **Tool Interceptor**:从工具调用中提取经验 -- **离线学习**:批量分析历史数据提取模式 -- **学习过程**:捕获执行上下文 → 提取器分析识别 → 生成经验记录 → 持久化存储供后续复用 - ---- - -### 经验模块(Experience) - -**作用**:积累和复用历史成功执行经验。 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 经验模块工作示意 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 【场景1: 经验积累】 │ -│ │ -│ 用户: "查询订单状态" ──▶ Agent 成功执行 ──▶ ┌────────────────┐ │ -│ │ 保存经验: │ │ -│ │ - React决策经验 │ │ -│ │ - Code经验 │ │ -│ │ - 常识经验 │ │ -│ └────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────┐ │ -│ │ 经验库 │ │ -│ └────────────────┘ │ -│ │ -│ 【场景2: 经验复用】 | │ -│ │ │ -│ 用户: "查询我的订单状态" ◀──── 匹配相似经验 ◀────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ Agent 参考历史经验,更快决策+生成正确代码 │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ 【场景3: 快速意图响应】 │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 经验库 │ │ -│ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ -│ │ │ 经验A (普通) │ │ 经验B (✓ 已配置快速意图) │ │ │ -│ │ │ 无快速意图配置 │ │ 条件: 前缀匹配"查看*销量" │ │ │ -│ │ │ → 注入prompt供llm参考│ │ 动作: 调用销量查询API │ │ │ -│ │ └─────────────────────┘ └───────────┬────────────────┘ │ │ -│ └─────────────────────────────────────────────┼───────────────────┘ │ -│ │ 条件命中 │ -│ ▼ │ -│ 用户: "查看今日销量" ──▶ 匹配经验B快速意图 ──▶ 跳过LLM,直接执行 │ -│ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**核心能力**: -- **多类型经验**:代码生成经验、ReAct 决策经验、常识经验,为类似任务提供历史参考 -- **灵活复用**:经验可注入 Prompt 或用于快速意图匹配 -- **生命周期管理**:支持经验的创建、更新、删除 -- **快速意图响应**: - - 经验需显式配置 `fastIntentConfig` 才能启用 - - 匹配已配置条件时,跳过 LLM 完整推理,直接执行预记录的工具调用或代码 - - 支持多条件匹配:消息前缀、正则、元数据、状态等 - ---- - -### 触发器模块(Trigger) - -**作用**:创建和管理定时任务或事件触发的 Agent 执行。 +| 模块 | 说明 | 文档 | +|------|------|------| +| **评估模块** | 通过评估图(Graph)进行多维度意图识别,支持 LLM 和规则引擎 | [快速开始](https://java2ai.com/agents/assistantagent/features/evaluation/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/evaluation/advanced) | +| **Prompt Builder** | 根据评估结果和运行时上下文动态组装 Prompt | [快速开始](https://java2ai.com/agents/assistantagent/features/prompt-builder/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/prompt-builder/advanced) | -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 触发器模块能力示意 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 【定时触发】 │ -│ │ -│ 用户: "每天早上9点给我发送销售日报" │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Agent 创建 │ │ 调度器 │ │ 自动执行 │ │ -│ │ Cron 触发器 │────▶│ 0 9 * * * │────▶│ 生成日报 │ │ -│ │ (自我调度) │ │ │ │ 发送通知 │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -│ 【延迟触发】 │ -│ │ -│ 用户: "30分钟后提醒我开会" │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Agent 创建 │ │ 30分钟后 │ │ 发送提醒 │ │ -│ │ 一次性触发器 │────▶│ 触发 │────▶│ "该开会了" │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -│ 【回调触发】 │ -│ │ -│ 用户: "满足xx条件时帮我xx" │ -│ │ -│ 外部系统: 发送事件到 Webhook │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ 接收回调 │ │ 触发 Agent │ │ 处理事件 │ │ -│ │ Webhook 事件 │────▶│ 执行任务 │────▶│ 返回响应 │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` +### 工具扩展 -**核心能力**: -- `TIME_CRON`触发器:支持 Cron 表达式定时触发任务 -- `TIME_ONCE`触发器:支持一次性延迟触发 -- `CALLBACK`触发器:支持回调事件触发 -- Agent 可通过工具自主创建触发器,实现"自我调度" +| 模块 | 说明 | 文档 | +|------|------|------| +| **MCP 工具** | 接入 Model Context Protocol 服务器,复用 MCP 工具生态 | [快速开始](https://java2ai.com/agents/assistantagent/features/mcp/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/mcp/advanced) | +| **动态 HTTP 工具** | 通过 OpenAPI 规范接入 REST API | [快速开始](https://java2ai.com/agents/assistantagent/features/dynamic-http/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/dynamic-http/advanced) | +| **自定义 CodeAct 工具** | 通过 CodeactTool 接口构建自定义工具 | [快速开始](https://java2ai.com/agents/assistantagent/features/custom-codeact-tool/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/custom-codeact-tool/advanced) | ---- +### 智能能力 -### 回复渠道模块(Reply Channel) +| 模块 | 说明 | 文档 | +|------|------|------| +| **经验模块** | 积累和复用历史成功执行经验,支持快速意图响应 | [快速开始](https://java2ai.com/agents/assistantagent/features/experience/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/experience/advanced) | +| **学习模块** | 从 Agent 执行历史中自动提取有价值的经验 | [快速开始](https://java2ai.com/agents/assistantagent/features/learning/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/learning/advanced) | +| **搜索模块** | 多数据源统一检索引擎,支持知识问答 | [快速开始](https://java2ai.com/agents/assistantagent/features/search/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/search/advanced) | -**作用**:提供灵活的消息回复能力,支持多种输出渠道。 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 回复渠道模块能力示意 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Agent 需要向用户回复消息 │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 回复渠道路由 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ├──────────────┬──────────────┬──────────────┐ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ -│ │ DEFAULT │ │ IDE_CARD │ │ IM_NOTIFY │ │ WEBHOOK │ │ -│ │ 文本回复 │ │ 卡片展示 │ │ 消息推送 │ │ JSON推送 │ │ -│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ -│ │ │ │ │ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ 控制台 │ │ IDE │ │ IM │ │ 第三方 │ │ -│ │ 终端回复 │ │ 富文本卡片 │ │ (可扩展) │ │ 系统 │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -│ 【使用示例】 │ -│ │ -│ 用户: "分析完成后发送结果" │ -│ │ │ -│ ▼ │ -│ Agent: send_message(text="分析结果...") │ -│ │ │ -│ ▼ │ -│ 用户收到消息: "📊 分析结果: ..." │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**核心能力**: -- **多渠道路由**:Agent 可根据场景选择不同渠道回复 -- **配置驱动**:动态生成回复工具,无需编码 -- **同步异步支持**:支持同步和异步回复模式 -- **统一接口**:屏蔽底层实现差异 -- **内置示例渠道**:`IDE_TEXT`(演示用) -- **可扩展渠道**(通过实现 `ReplyChannelDefinition` SPI):如 `IDE_CARD`、`IM_NOTIFICATION`(钉钉/飞书/企微)、`WEBHOOK_JSON` 等,需用户自行实现 - ---- - -### 工具扩展模块(Dynamic Tools) - -**作用**:提供高度可扩展的工具体系,让 Agent 能够调用各类外部工具完成任务。 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 工具扩展架构 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Agent 需要执行操作 │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────────────┐ │ -│ │ CodeactTool 工具体系 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ├─────────────┬─────────────┬─────────────┬──────────────┐ │ -│ ▼ ▼ ▼ ▼ ▼ │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────┐ │ -│ │ MCP │ │ HTTP │ │ Search │ │ Trigger │ │ 自定义 │ │ -│ │ Tools │ │ API │ │ Tools │ │ Tools │ │ Tools │ │ -│ │ │ │ Tools │ │ │ │ │ │ │ │ -│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └───┬───┘ │ -│ │ │ │ │ │ │ -│ ▼ ▼ ▼ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │ -│ │ 任意 MCP │ │ REST API │ │ 知识检索 │ │ 定时任务 │ │ ... │ │ -│ │ Server │ │ OpenAPI │ │ 项目搜索 │ │ 事件回调 │ │ │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -**核心能力**: -- **MCP 工具支持**:一键接入任意 MCP Server,复用 MCP 工具生态 -- **HTTP API 支持**:通过 OpenAPI 规范接入 REST API,调用企业现有接口 -- **内置工具类型**:搜索(Search)、回复(Reply)、触发器(Trigger)、学习(Learning)等 -- **自定义工具 SPI**:实现 `CodeactTool` 接口,轻松扩展新工具 - ---- +### 交互能力 -### 知识检索模块(Knowledge Search) +| 模块 | 说明 | 文档 | +|------|------|------| +| **回复渠道** | 多渠道消息回复,支持渠道路由 | [快速开始](https://java2ai.com/agents/assistantagent/features/reply/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/reply/advanced) | +| **触发器** | 定时任务、延迟执行、事件回调触发 | [快速开始](https://java2ai.com/agents/assistantagent/features/trigger/quickstart) | [高级特性](https://java2ai.com/agents/assistantagent/features/trigger/advanced) | -**作用**:多数据源统一检索引擎,为 Agent 的问答和决策提供知识支撑。 +### 更多资源 -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 多数据源检索架构 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 用户问题: "如何配置数据库连接池?" │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 统一检索接口 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ├────────────────┬────────────────┬────────────────┐ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────┐ │ -│ │ 知识库 │ │ 项目 │ │ Web │ │ 自定义 │ │ -│ │ Provider │ │ Provider │ │ Provider │ │Provider │ │ -│ │ (主要) │ │ (可选) │ │ (可选) │ │ (SPI) │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └───┬─────┘ │ -│ │ │ │ │ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ -│ │ FAQ / 文档 │ │ 源代码 │ │ 网络文章 │ │ ... │ │ -│ │ 历史问答 │ │ 配置文件 │ │ 技术论坛 │ │ │ │ -│ │ 团队笔记 │ │ 日志 │ │ │ │ │ │ -│ └──────────────┘ └─────────────┘ └───────────────┘ └────────┘ │ -│ │ │ │ │ │ -│ └─────────────────┴─────────────────┴──────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────┐ │ -│ │ 聚合排序 │ │ -│ │ → 注入 Prompt │ │ -│ └────────────────────────┘ │ -│ │ -└────────────────────────────────────────────────────────────────────────┘ -``` - -**核心能力**: -- **统一检索接口**:`SearchProvider` SPI,支持可插拔数据源 -- **演示 Provider**:内置知识库、项目、Web 的 Mock 实现(仅供演示和测试) -- **自定义扩展**:通过实现 `SearchProvider` 接口,接入任意数据源(数据库、向量库、API) -- **结果聚合**:支持可配置的排序策略 -- **业务价值**:接入企业知识库提供准确答案、支持答案溯源、降低人工客服压力 - -**配置示例**: - -```yaml -spring: - ai: - alibaba: - codeact: - extension: - search: - enabled: true - knowledge-search-enabled: true # 知识库(默认 Mock 实现) - project-search-enabled: false # 项目代码(默认 Mock 实现) - web-search-enabled: false # Web 搜索(默认 Mock 实现) - default-top-k: 5 - search-timeout-ms: 5000 -``` - -> 💡 以上搜索功能默认提供 Mock 实现供演示测试。生产环境需实现 `SearchProvider` SPI 接入实际数据源。 +| 资源 | 链接 | +|------|------| +| 快速开始指南 | [AssistantAgent 快速开始](https://java2ai.com/agents/assistantagent/quick-start) | +| 二次开发指南 | [开发指南](https://java2ai.com/agents/assistantagent/secondary-development) | ---