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
2 changes: 1 addition & 1 deletion assistant-agent-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.agent.assistant</groupId>
<artifactId>assistant-agent</artifactId>
<version>0.2.6</version>
<version>0.2.7</version>
</parent>

<artifactId>assistant-agent-autoconfigure</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion assistant-agent-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.agent.assistant</groupId>
<artifactId>assistant-agent</artifactId>
<version>0.2.6</version>
<version>0.2.7</version>
</parent>

<artifactId>assistant-agent-common</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion assistant-agent-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.agent.assistant</groupId>
<artifactId>assistant-agent</artifactId>
<version>0.2.6</version>
<version>0.2.7</version>
</parent>

<artifactId>assistant-agent-core</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion assistant-agent-evaluation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.agent.assistant</groupId>
<artifactId>assistant-agent</artifactId>
<version>0.2.6</version>
<version>0.2.7</version>
</parent>

<artifactId>assistant-agent-evaluation</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion assistant-agent-extensions/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.agent.assistant</groupId>
<artifactId>assistant-agent</artifactId>
<version>0.2.6</version>
<version>0.2.7</version>
</parent>

<artifactId>assistant-agent-extensions</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,11 @@ private ExperienceQueryContext buildQueryContext(OverAllState state, RunnableCon
}
if (state != null) {
state.value("tenant_id", String.class).ifPresent(context::setTenantId);
state.value("user_id", String.class).ifPresent(context::setTenantId);
state.value("user_id", String.class).ifPresent(context::setUserId);
}
if (config != null) {
config.metadata("tenant_id").ifPresent(id -> context.setTenantId(String.valueOf(id)));
config.metadata("user_id").ifPresent(id -> context.setTenantId(String.valueOf(id)));
config.metadata("user_id").ifPresent(id -> context.setUserId(String.valueOf(id)));
}
return context;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
public class BaseReplyCodeactTool implements ReplyCodeactTool {

private static final Logger log = LoggerFactory.getLogger(BaseReplyCodeactTool.class);
private static final String USER_ID_METADATA_KEY = "user_id";
private static final String TRACE_ID_METADATA_KEY = "trace_id";
private static final String CHANNEL_TYPE_METADATA_KEY = "channel_type";
private static final String CHANNEL_ID_METADATA_KEY = "channel_id";

private final String toolName;

Expand Down Expand Up @@ -480,12 +484,23 @@ private ChannelExecutionContext buildExecutionContext(ToolContext toolContext) {
log.warn("BaseReplyCodeactTool#buildExecutionContext - reason=未能获取sessionId(threadId), toolName={}", toolName);
}

// 从 metadata 获取 userId 和 traceId
ToolContextHelper.getFromMetadata(toolContext, "user_id").ifPresent(builder::userId);
ToolContextHelper.getFromMetadata(toolContext, "trace_id").ifPresent(builder::traceId);
// 从 metadata 获取基础上下文字段
ToolContextHelper.getFromMetadata(toolContext, USER_ID_METADATA_KEY).ifPresent(builder::userId);
ToolContextHelper.getFromMetadata(toolContext, TRACE_ID_METADATA_KEY).ifPresent(builder::traceId);

// 将 channel 相关信息注入 extensions,供 reply 渠道实现自行读取。
copyMetadataToExtension(toolContext, builder, CHANNEL_TYPE_METADATA_KEY);
copyMetadataToExtension(toolContext, builder, CHANNEL_ID_METADATA_KEY);

return builder.build();
}

}
private void copyMetadataToExtension(ToolContext toolContext, ChannelExecutionContext.Builder builder, String key) {
ToolContextHelper.getFromMetadata(toolContext, key).ifPresent(value -> {
builder.extension(key, value);
log.debug("BaseReplyCodeactTool#buildExecutionContext - reason=注入metadata到extensions, toolName={}, key={}",
toolName, key);
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private ToolDefinition buildToolDefinition() {
},
"schedule_value": {
"type": "string",
"description": "调度值:CRON模式为cron表达式;FIXED_DELAY/FIXED_RATE模式为毫秒数(如3分钟=180000);ONE_TIME模式为ISO时间戳"
"description": "调度值:CRON模式为6位cron表达式(秒 分 时 日 月 周,例如每天凌晨2点为 0 0 2 * * *);FIXED_DELAY/FIXED_RATE模式为毫秒数(如3分钟=180000);ONE_TIME模式为ISO时间戳"
},
"condition_function": {
"type": "string",
Expand Down Expand Up @@ -225,7 +225,7 @@ private CodeactToolDefinition buildCodeactDefinition() {
.addParameter(ParameterNode.builder()
.name("schedule_value")
.type(ParameterType.STRING)
.description("调度值:CRON模式为cron表达式;FIXED_DELAY/FIXED_RATE模式为毫秒数(如3分钟=180000);ONE_TIME模式为ISO时间戳")
.description("调度值:CRON模式为6位cron表达式(秒 分 时 日 月 周,例如每天凌晨2点为 0 0 2 * * *);FIXED_DELAY/FIXED_RATE模式为毫秒数(如3分钟=180000);ONE_TIME模式为ISO时间戳")
.required(true)
.build())
.addParameter(ParameterNode.builder()
Expand Down Expand Up @@ -267,11 +267,11 @@ private CodeactToolMetadata buildCodeactMetadata() {
result = trigger_tools.subscribe_trigger(
name="daily_task",
schedule_mode="CRON",
schedule_value="0 0 * * *",
schedule_value="0 0 0 * * *",
execute_function="print('Daily execution')"
)
print(f"Trigger ID: {result['trigger_id']}")
""", "创建一个每天午夜执行的触发器,并打印触发器 ID"))
""", "创建一个每天午夜执行的触发器(cron格式为6位:秒 分 时 日 月 周),并打印触发器 ID"))
.addFewShot(new CodeExample("创建一个3分钟后提醒的触发器", """
# 3分钟 = 3 * 60 * 1000 = 180000毫秒
delay_ms = 3 * 60 * 1000 # 分钟 * 秒 * 毫秒
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.alibaba.assistant.agent.extension.reply.tools;

import com.alibaba.assistant.agent.common.tools.ReplyCodeactTool;
import com.alibaba.assistant.agent.extension.reply.config.ReplyToolConfig;
import com.alibaba.assistant.agent.extension.reply.model.ChannelExecutionContext;
import com.alibaba.assistant.agent.extension.reply.model.ParameterSchema;
import com.alibaba.assistant.agent.extension.reply.model.ReplyResult;
import com.alibaba.assistant.agent.extension.reply.spi.ReplyChannelDefinition;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.tools.ToolContextConstants;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.model.ToolContext;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

class BaseReplyCodeactToolTest {

@Test
void shouldCopyChannelMetadataIntoExecutionContextExtensions() {
AtomicReference<ChannelExecutionContext> capturedContext = new AtomicReference<>();
BaseReplyCodeactTool tool = new BaseReplyCodeactTool(
"send_dingtalk_card",
"Send a DingTalk card",
new CapturingReplyChannel(capturedContext),
new ReplyToolConfig(),
null,
ReplyCodeactTool.ReplyChannelType.PRIMARY);

RunnableConfig runnableConfig = RunnableConfig.builder()
.threadId("session_123")
.addMetadata("user_id", "user_456")
.addMetadata("trace_id", "trace_789")
.addMetadata("channel_type", "DINGTALK_GROUP")
.addMetadata("channel_id", "cid_001")
.build();
Map<String, Object> contextMap = new HashMap<>();
contextMap.put(ToolContextConstants.AGENT_CONFIG_CONTEXT_KEY, runnableConfig);

tool.call("{}", new ToolContext(contextMap));

ChannelExecutionContext context = capturedContext.get();
assertNotNull(context);
assertEquals("session_123", context.getSessionId());
assertEquals("user_456", context.getUserId());
assertEquals("trace_789", context.getTraceId());
assertEquals("DINGTALK_GROUP", context.getExtension("channel_type"));
assertEquals("cid_001", context.getExtension("channel_id"));
}

private static final class CapturingReplyChannel implements ReplyChannelDefinition {

private final AtomicReference<ChannelExecutionContext> capturedContext;

private CapturingReplyChannel(AtomicReference<ChannelExecutionContext> capturedContext) {
this.capturedContext = capturedContext;
}

@Override
public String getChannelCode() {
return "DINGTALK";
}

@Override
public String getDescription() {
return "test";
}

@Override
public ParameterSchema getSupportedParameters() {
return null;
}

@Override
public ReplyResult execute(ChannelExecutionContext context, Map<String, Object> params) {
capturedContext.set(context);
return ReplyResult.success("ok");
}
}
}
2 changes: 1 addition & 1 deletion assistant-agent-management/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.agent.assistant</groupId>
<artifactId>assistant-agent</artifactId>
<version>0.2.6</version>
<version>0.2.7</version>
</parent>

<artifactId>assistant-agent-management</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
import com.alibaba.assistant.agent.management.internal.SkillPackageParser;
import com.alibaba.assistant.agent.management.model.ExperienceVO;
import com.alibaba.assistant.agent.management.model.SkillImportConflictStrategy;
import com.alibaba.assistant.agent.management.model.SkillPackage;
import com.alibaba.assistant.agent.management.model.SkillPackageImportResult;
import com.alibaba.assistant.agent.management.spi.SkillExchangeService;
Expand Down Expand Up @@ -51,10 +52,15 @@ public ResponseEntity<ExperienceVO> preview(@RequestBody Map<String, String> bod
}

@PostMapping("/import-package")
public ResponseEntity<SkillPackageImportResult> importPackage(@RequestParam("file") MultipartFile file) throws IOException {
public ResponseEntity<SkillPackageImportResult> importPackage(
@RequestParam("file") MultipartFile file,
@RequestParam(name = "conflictStrategy", required = false) SkillImportConflictStrategy conflictStrategy) throws IOException {
SkillPackage pkg = packageParser.parseAuto(file.getInputStream());
SkillPackageImportResult result = service.importSkillPackage(pkg);
return ResponseEntity.status(HttpStatus.CREATED).body(result);
SkillPackageImportResult result = service.importSkillPackage(pkg, conflictStrategy);
// 检测到同名冲突且未指定策略时返回 200(含 conflict 字段);正常导入返回 201
HttpStatus status = result.hasConflict() && result.getImportedId() == null
? HttpStatus.OK : HttpStatus.CREATED;
return ResponseEntity.status(status).body(result);
}

@PostMapping("/preview-package")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.alibaba.assistant.agent.extension.experience.model.ReferenceEntry;
import com.alibaba.assistant.agent.extension.experience.spi.ExperienceRepository;
import com.alibaba.assistant.agent.management.model.ExperienceVO;
import com.alibaba.assistant.agent.management.model.SkillImportConflictStrategy;
import com.alibaba.assistant.agent.management.model.SkillPackage;
import com.alibaba.assistant.agent.management.model.SkillPackageImportResult;
import com.alibaba.assistant.agent.management.spi.ReferenceSummarizer;
Expand Down Expand Up @@ -239,7 +240,13 @@ public ExperienceVO previewSkillImport(String skillMarkdown) {

@Override
public SkillPackageImportResult importSkillPackage(SkillPackage skillPackage) {
PreparedSkillImport prepared = prepareSkillPackageImport(skillPackage);
return importSkillPackage(skillPackage, null);
}

@Override
public SkillPackageImportResult importSkillPackage(SkillPackage skillPackage,
SkillImportConflictStrategy conflictStrategy) {
PreparedSkillImport prepared = prepareSkillPackageImport(skillPackage, conflictStrategy);
if (prepared.reactExperience == null) {
return prepared.result;
}
Expand All @@ -259,7 +266,8 @@ public SkillPackageImportResult importSkillPackage(SkillPackage skillPackage) {

@Override
public SkillPackageImportResult previewSkillPackageImport(SkillPackage skillPackage) {
PreparedSkillImport prepared = prepareSkillPackageImport(skillPackage);
// 预览阶段以 KEEP_BOTH 策略生成预览(不实际落库),冲突信息由 prepareSkillPackageImport 回填
PreparedSkillImport prepared = prepareSkillPackageImport(skillPackage, SkillImportConflictStrategy.KEEP_BOTH);
Comment thread
AQing-527 marked this conversation as resolved.
if (prepared.reactExperience == null) {
return prepared.result;
}
Expand All @@ -271,19 +279,53 @@ public SkillPackageImportResult previewSkillPackageImport(SkillPackage skillPack
return prepared.result;
}

private PreparedSkillImport prepareSkillPackageImport(SkillPackage skillPackage) {
private PreparedSkillImport prepareSkillPackageImport(SkillPackage skillPackage,
SkillImportConflictStrategy conflictStrategy) {
SkillPackageImportResult result = new SkillPackageImportResult();
if (!skillPackage.hasSkillMd()) {
result.addWarning("No SKILL.md found in package");
return new PreparedSkillImport(result, null, null);
}

Experience existingReact = findExistingReactExperienceByPackage(skillPackage).orElse(null);
Map<String, String> existingDescriptionsByHash = buildDescriptionCache(existingReact);

// 提前解析 frontmatter 中的 name,回退用 react 经验名做同名查找
Experience reactExperience = parseSkillMarkdown(skillPackage.getSkillMdContent());
enrichExperienceFromPackage(reactExperience, skillPackage);

if (existingReact == null) {
existingReact = findExistingReactExperienceByName(reactExperience.getName()).orElse(null);
}

// 同名冲突时始终回传 conflict 信息;仅当调用方未指定策略时才跳过落库
if (existingReact != null) {
String existingToolId = null;
List<String> related = existingReact.getRelatedExperiences();
if (related != null) {
for (String relatedId : related) {
if (relatedId != null && relatedId.startsWith("cli:")) {
existingToolId = relatedId;
break;
}
}
}
result.setConflict(new SkillPackageImportResult.ConflictInfo(
existingReact.getId(), existingReact.getName(), existingToolId));
if (conflictStrategy == null) {
return new PreparedSkillImport(result, null, null);
}
}

Map<String, String> existingDescriptionsByHash = buildDescriptionCache(existingReact);

// REPLACE 策略:复用已存在经验的 ID,使 repository.save 走更新路径
if (existingReact != null && conflictStrategy == SkillImportConflictStrategy.REPLACE) {
reactExperience.setId(existingReact.getId());
if (existingReact.getCreatedAt() != null) {
reactExperience.setCreatedAt(existingReact.getCreatedAt());
}
}

List<ReferenceEntry> references = new ArrayList<>();
List<AssetEntry> assets = new ArrayList<>();
buildReferencesAndAssets(skillPackage, references, assets, existingDescriptionsByHash);
Expand Down Expand Up @@ -319,6 +361,15 @@ private Optional<Experience> findExistingReactExperienceByPackage(SkillPackage s
.findFirst();
}

private Optional<Experience> findExistingReactExperienceByName(String name) {
if (name == null || name.isBlank()) {
return Optional.empty();
}
return repository.findAllByType(ExperienceType.REACT).stream()
.filter(e -> name.equalsIgnoreCase(e.getName()))
.findFirst();
}

private Map<String, String> buildDescriptionCache(Experience existing) {
Map<String, String> cache = new HashMap<>();
if (existing == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.alibaba.assistant.agent.management.model;

/**
* 同名 skill 导入时的冲突处理策略。
*
* <ul>
* <li>{@link #REPLACE}:复用已存在经验的 ID,原地更新内容、references、assets、关联工具等</li>
* <li>{@link #KEEP_BOTH}:忽略已存在经验,生成全新 ID 写入</li>
* </ul>
*
* <p>当未指定策略(参数为 {@code null})时,导入流程在检测到同名 REACT 经验后将不落库,
* 而是把已存在经验信息回填到 {@code SkillPackageImportResult.conflict} 中,由调用方
* (管理后台前端)提示用户选择处理方式。
*/
public enum SkillImportConflictStrategy {

REPLACE,

KEEP_BOTH
}
Loading
Loading