Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,11 @@ private String buildSystemPrompt(Language language, List<CodeactTool> codeactToo
sb.append("2. 函数名和参数必须与要求完全一致\n");
sb.append("3. 调用工具使用 实例名.方法名() 格式\n");
sb.append("4. 只返回纯代码,不要 ```python 标记\n");
sb.append("5. ⚠️【禁止生成注释】不要生成任何注释,包括:函数注释(docstring)、行注释(#开头)、多行注释。直接输出纯净的可执行代码。\n");
if (isCondition) {
sb.append("5. 条件函数必须返回 True 或 False\n");
sb.append("6. 条件函数必须返回 True 或 False\n");
} else {
sb.append("5. 【重要】每个函数必须有 return 语句返回结果\n");
sb.append("6. 【重要】每个函数必须有 return 语句返回结果\n");
sb.append(" - 查询/搜索类:return 搜索结果\n");
sb.append(" - 处理/计算类:return 处理结果\n");
sb.append(" - 通知/回复类:先执行操作,再 return 操作结果或状态\n");
Expand Down Expand Up @@ -616,15 +617,15 @@ private ReturnSchema getReturnSchema(CodeactTool tool) {

// 优先从 registry 获取(包含观测到的 schema)
if (returnSchemaRegistry != null) {
logger.info("CodeGeneratorNode#getReturnSchema - reason=开始查询schema, registryHashCode={}, toolName={}, allToolsWithSchema={}",
logger.debug("CodeGeneratorNode#getReturnSchema - reason=开始查询schema, registryHashCode={}, toolName={}, allToolsWithSchema={}",
System.identityHashCode(returnSchemaRegistry), toolName, returnSchemaRegistry.getToolsWithSchema());
ReturnSchema observed = returnSchemaRegistry.getSchema(toolName).orElse(null);
if (observed != null) {
logger.info("CodeGeneratorNode#getReturnSchema - reason=从registry获取到schema, toolName={}, sampleCount={}, hasSuccessShape={}",
logger.debug("CodeGeneratorNode#getReturnSchema - reason=从registry获取到schema, toolName={}, sampleCount={}, hasSuccessShape={}",
toolName, observed.getSampleCount(), observed.getSuccessShape() != null);
return observed;
} else {
logger.info("CodeGeneratorNode#getReturnSchema - reason=registry中未找到schema, toolName={}", toolName);
logger.debug("CodeGeneratorNode#getReturnSchema - reason=registry中未找到schema, toolName={}", toolName);
}
} else {
logger.warn("CodeGeneratorNode#getReturnSchema - reason=returnSchemaRegistry为null, toolName={}", toolName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.ToolCallback;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -438,8 +441,24 @@ private String generateCustomVariables(ToolContext toolContext) {
return sb.toString();
}

/**
* 用于序列化复杂对象的 ObjectMapper
*/
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

/**
* 将 Java 对象转换为 Python 字面量
*
* <p>支持的类型转换:
* <ul>
* <li>null -> None</li>
* <li>String -> Python 字符串(支持多行)</li>
* <li>Number -> Python 数字</li>
* <li>Boolean -> True/False</li>
* <li>List -> Python 列表</li>
* <li>Map -> Python 字典</li>
* <li>其他对象 -> 尝试 JSON 序列化后解析为 Python 对象</li>
* </ul>
*/
@SuppressWarnings("unchecked")
private String toPythonLiteral(Object value) {
Expand Down Expand Up @@ -486,8 +505,38 @@ private String toPythonLiteral(Object value) {
sb.append("}");
return sb.toString();
}
// 其他类型转为字符串
return "\"" + value.toString().replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
// 其他复杂对象类型:尝试使用 Jackson 序列化为 JSON,然后在 Python 中解析
// 这样可以正确处理 Attachment 等 POJO 对象,而不是简单地调用 toString()
return convertComplexObjectToPythonLiteral(value);
}

/**
* 将复杂对象转换为 Python 字面量
*
* <p>先尝试使用 Jackson 将对象序列化为 JSON,然后转换为 Python 字典/列表格式。
* 如果序列化失败,则退化为字符串表示。
*
* @param value 要转换的对象
* @return Python 字面量表示
*/
private String convertComplexObjectToPythonLiteral(Object value) {
try {
// 使用 Jackson 将对象序列化为 JSON 字符串
String jsonStr = JSON_MAPPER.writeValueAsString(value);

// 将 JSON 反序列化为 Map 或 List,然后递归转换为 Python 字面量
// 这样可以复用已有的 Map/List 处理逻辑
Object parsed = JSON_MAPPER.readValue(jsonStr, Object.class);

// 递归调用 toPythonLiteral 处理反序列化后的对象(Map 或 List)
return toPythonLiteral(parsed);
} catch (JsonProcessingException e) {
// JSON 序列化失败,退化为字符串表示
logger.warn("GraalCodeExecutor#convertComplexObjectToPythonLiteral - reason=JSON序列化失败, " +
"valueType={}, error={}, 退化为toString()", value.getClass().getName(), e.getMessage());
String strValue = value.toString();
return "\"" + strValue.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the JSON-serialization fallback, the string is wrapped with normal quotes but newlines aren’t handled. If value.toString() contains '\n', the generated Python literal becomes invalid code. Reuse the same multiline-safe escaping logic as the String branch (triple quotes when needed) for this fallback path too.

Suggested change
return "\"" + strValue.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
// 使用通用的字符串转换逻辑,确保多行字符串也能安全转换为 Python 字面量
return toPythonLiteral(strValue);

Copilot uses AI. Check for mistakes.
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public Optional<ReturnSchema> getSchema(String toolName) {
return Optional.empty();
}
ReturnSchema schema = mergedSchemas.get(toolName);
logger.info("DefaultReturnSchemaRegistry#getSchema - reason=查询schema, hashCode={}, toolName={}, found={}, allTools={}",
logger.debug("DefaultReturnSchemaRegistry#getSchema - reason=查询schema, hashCode={}, toolName={}, found={}, allTools={}",
System.identityHashCode(this), toolName, schema != null, mergedSchemas.keySet());
return Optional.ofNullable(schema);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,44 @@ public EvaluationCriterionBuilder multimodalForImages(String attachmentsPath, St
return this;
}

/**
* Set the timeout for this criterion execution (in milliseconds).
* When timeout occurs, the criterion returns TIMEOUT status with defaultValue.
*
* @param timeoutMs timeout in milliseconds
* @return this builder
*/
public EvaluationCriterionBuilder timeoutMs(long timeoutMs) {
criterion.setTimeoutMs(timeoutMs);
return this;
}

/**
* Set the default value to use when the criterion times out or errors.
* This ensures evaluation can continue even when individual criteria fail.
*
* @param defaultValue the default value to use on timeout/error
* @return this builder
*/
public EvaluationCriterionBuilder defaultValue(Object defaultValue) {
criterion.setDefaultValue(defaultValue);
return this;
}

/**
* Configure timeout and default value together.
* A convenience method for setting both timeout and fallback behavior.
*
* @param timeoutMs timeout in milliseconds
* @param defaultValue the default value to use on timeout/error
* @return this builder
*/
public EvaluationCriterionBuilder withTimeout(long timeoutMs, Object defaultValue) {
criterion.setTimeoutMs(timeoutMs);
criterion.setDefaultValue(defaultValue);
return this;
}

/**
* Ensure the dependent criterion is in the dependsOn list
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;

import java.util.HashMap;
Expand All @@ -48,11 +49,17 @@ public class LLMBasedEvaluator implements Evaluator {
private final ChatModel chatModel;
private final String evaluatorId;
private final ObjectMapper objectMapper;
private final ChatOptions chatOptions;

public LLMBasedEvaluator(ChatModel chatModel, String evaluatorId) {
this(chatModel, evaluatorId, null);
}

public LLMBasedEvaluator(ChatModel chatModel, String evaluatorId, ChatOptions chatOptions) {
this.chatModel = chatModel;
this.evaluatorId = evaluatorId;
this.objectMapper = new ObjectMapper();
this.chatOptions = chatOptions;
}

@Override
Expand All @@ -71,8 +78,8 @@ public CriterionResult evaluate(CriterionExecutionContext executionContext) {
logger.debug("Evaluating criterion {} with LLM, prompt: {}, chatModel: {} ({})",
executionContext.getCriterion().getName(), promptText, chatModel.getClass().getSimpleName(), chatModel);

// Call LLM
Prompt prompt = new Prompt(promptText);
// Call LLM with optional ChatOptions
Prompt prompt = chatOptions != null ? new Prompt(promptText, chatOptions) : new Prompt(promptText);
ChatResponse chatResponse = chatModel.call(prompt);
String response = chatResponse.getResult().getOutput().getText();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.content.Media;

Expand All @@ -49,6 +50,7 @@ public class MultimodalLLMBasedEvaluator extends LLMBasedEvaluator {
private static final Logger logger = LoggerFactory.getLogger(MultimodalLLMBasedEvaluator.class);

private final ChatModel multimodalChatModel;
private final ChatOptions multimodalChatOptions;

/**
* 用于将 Map 转换为 MediaConvertible 实现类的 ObjectMapper
Expand All @@ -69,7 +71,21 @@ public class MultimodalLLMBasedEvaluator extends LLMBasedEvaluator {
* @param evaluatorId 评估器ID
*/
public MultimodalLLMBasedEvaluator(ChatModel textModel, ChatModel multimodalModel, String evaluatorId) {
this(textModel, multimodalModel, evaluatorId, createDefaultObjectMapper());
this(textModel, multimodalModel, evaluatorId, null, null, createDefaultObjectMapper());
}

/**
* 构造函数(带 ChatOptions)
*
* @param textModel 纯文本模型,用于普通评估
* @param multimodalModel 多模态模型,用于处理图片等多模态输入
* @param evaluatorId 评估器ID
* @param textChatOptions 纯文本模型的ChatOptions(可选)
* @param multimodalChatOptions 多模态模型的ChatOptions(可选)
*/
public MultimodalLLMBasedEvaluator(ChatModel textModel, ChatModel multimodalModel, String evaluatorId,
ChatOptions textChatOptions, ChatOptions multimodalChatOptions) {
this(textModel, multimodalModel, evaluatorId, textChatOptions, multimodalChatOptions, createDefaultObjectMapper());
}

/**
Expand All @@ -93,8 +109,24 @@ private static ObjectMapper createDefaultObjectMapper() {
*/
public MultimodalLLMBasedEvaluator(ChatModel textModel, ChatModel multimodalModel,
String evaluatorId, ObjectMapper objectMapper) {
super(textModel, evaluatorId);
this(textModel, multimodalModel, evaluatorId, null, null, objectMapper);
}

/**
* 构造函数(完整参数)
*
* @param textModel 纯文本模型,用于普通评估
* @param multimodalModel 多模态模型,用于处理图片等多模态输入
* @param evaluatorId 评估器ID
* @param textChatOptions 纯文本模型的ChatOptions(可选)
* @param multimodalChatOptions 多模态模型的ChatOptions(可选)
* @param objectMapper 用于类型转换的 ObjectMapper
*/
public MultimodalLLMBasedEvaluator(ChatModel textModel, ChatModel multimodalModel, String evaluatorId,
ChatOptions textChatOptions, ChatOptions multimodalChatOptions, ObjectMapper objectMapper) {
super(textModel, evaluatorId, textChatOptions);
this.multimodalChatModel = multimodalModel;
this.multimodalChatOptions = multimodalChatOptions;
this.objectMapper = objectMapper != null ? objectMapper : new ObjectMapper();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor falls back to new ObjectMapper() when the provided mapper is null, but the class relies on a mapper configured to ignore unknown properties (see createDefaultObjectMapper()). Using a plain ObjectMapper here can reintroduce the deserialization failures this class is designed to avoid; default to createDefaultObjectMapper() instead.

Suggested change
this.objectMapper = objectMapper != null ? objectMapper : new ObjectMapper();
this.objectMapper = objectMapper != null ? objectMapper : createDefaultObjectMapper();

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -316,8 +348,10 @@ protected CriterionResult evaluateWithMultimodal(CriterionExecutionContext conte
.media(mediaList)
.build();

// 调用多模态模型
Prompt prompt = new Prompt(List.of(userMessage));
// 调用多模态模型(支持自定义ChatOptions)
Prompt prompt = multimodalChatOptions != null
? new Prompt(List.of(userMessage), multimodalChatOptions)
: new Prompt(List.of(userMessage));
ChatResponse chatResponse = multimodalChatModel.call(prompt);
String responseText = chatResponse.getResult().getOutput().getText();

Expand Down
Loading