feat(action): redefine structure of evaluator result to support json schema of openai sdk, and create system prompt for sub-modules of ActionPlanner

This commit is contained in:
2026-04-16 21:38:11 +08:00
parent 062af4b7d2
commit 281984bb05
4 changed files with 128 additions and 31 deletions

View File

@@ -71,9 +71,7 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
protected void doExecute(@NotNull PartnerRunningFlowContext context) { protected void doExecute(@NotNull PartnerRunningFlowContext context) {
String input = context.encodeInputsBlock().encodeToXmlString(); String input = context.encodeInputsBlock().encodeToXmlString();
Result<ExtractorResult> result = actionExtractor.execute(input) Result<ExtractorResult> result = actionExtractor.execute(input)
.onFailure(exp -> { .onFailure(exp -> ExceptionReporterHandler.INSTANCE.report(exp, ContextExceptionReporter.REPORTER_NAME));
ExceptionReporterHandler.INSTANCE.report(exp, ContextExceptionReporter.REPORTER_NAME);
});
if (result.exceptionOrNull() != null) { if (result.exceptionOrNull() != null) {
return; return;
} }
@@ -82,17 +80,16 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
if (tendencies.isEmpty()) { if (tendencies.isEmpty()) {
return; return;
} }
appendTendencyBlock(tendencies, input); appendTendencyBlock(tendencies);
evaluateTendency(context.getSource(), input, extractorResult); evaluateTendency(context.getSource(), input, extractorResult);
} }
private void appendTendencyBlock(List<String> tendencies, String input) { private void appendTendencyBlock(List<String> tendencies) {
input = trimInput(input);
String datetime = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); String datetime = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
cognitionCapability.contextWorkspace().register(new ContextBlock( cognitionCapability.contextWorkspace().register(new ContextBlock(
buildTendenciesEvaluatingFullBlock(tendencies), buildTendenciesEvaluatingFullBlock(tendencies),
buildTendenciesEvaluatingCompactBlock(tendencies, datetime, input), buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
buildTendenciesEvaluatingAbstractBlock(tendencies, datetime, input), buildTendenciesEvaluatingAbstractBlock(tendencies, datetime),
Set.of(ContextBlock.FocusedDomain.ACTION), Set.of(ContextBlock.FocusedDomain.ACTION),
60, 60,
18, 18,
@@ -100,25 +97,24 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
)); ));
} }
private @NotNull BlockContent buildTendenciesEvaluatingAbstractBlock(List<String> tendencies, String datetime, String input) { private @NotNull BlockContent buildTendenciesEvaluatingAbstractBlock(List<String> tendencies, String datetime) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) { return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
@Override @Override
protected void fillXml(@NotNull Document document, @NotNull Element root) { protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "datetime", datetime); appendTextElement(document, root, "datetime", datetime);
appendTextElement(document, root, "related_input", input);
appendTextElement(document, root, "abstract", "There are " + tendencies.size() + " related tendencies in evaluating."); appendTextElement(document, root, "abstract", "There are " + tendencies.size() + " related tendencies in evaluating.");
} }
}; };
} }
private @NotNull BlockContent buildTendenciesEvaluatingCompactBlock(List<String> tendencies, String datetime, String input) { private @NotNull BlockContent buildTendenciesEvaluatingCompactBlock(List<String> tendencies, String datetime) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) { return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
@Override @Override
protected void fillXml(@NotNull Document document, @NotNull Element root) { protected void fillXml(@NotNull Document document, @NotNull Element root) {
int size = tendencies.size(); int size = tendencies.size();
boolean num = size > 3; boolean num = size > 3;
appendTextElement(document, root, "datetime", datetime); appendTextElement(document, root, "datetime", datetime);
appendTextElement(document, root, "related_input", input); appendTextElement(document, root, "state", "Partner is considering whether to do these");
appendTextElement(document, root, "tendencies_count", size); appendTextElement(document, root, "tendencies_count", size);
appendListElement(document, root, num ? "tendencies_truncated" : "tendencies", "tendency", num ? tendencies.subList(0, 3) : tendencies); appendListElement(document, root, num ? "tendencies_truncated" : "tendencies", "tendency", num ? tendencies.subList(0, 3) : tendencies);
} }
@@ -126,10 +122,11 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
} }
private @NotNull BlockContent buildTendenciesEvaluatingFullBlock(List<String> tendencies) { private @NotNull BlockContent buildTendenciesEvaluatingFullBlock(List<String> tendencies) {
return new CommunicationBlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) { return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
@Override @Override
protected void fillXml(@NotNull Document document, @NotNull Element root) { protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendRepeatedElements(document, root, "tendency", tendencies); appendTextElement(document, root, "state", "Partner is considering whether to do these");
appendListElement(document, root, "tendencies", "tendency", tendencies);
} }
}; };
} }
@@ -216,20 +213,19 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) { return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
@Override @Override
protected void fillXml(@NotNull Document document, @NotNull Element root) { protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "waiting_confirm"); appendTextElement(document, root, "state", "need_user_confirm");
appendTextElement(document, root, "action_uuid", executableAction.getUuid()); appendTextElement(document, root, "action_uuid", executableAction.getUuid());
appendTextElement(document, root, "action_type", evaluatorResult.getType()); appendTextElement(document, root, "action_type", evaluatorResult.getType());
appendTextElement(document, root, "tendency", executableAction.getTendency()); appendTextElement(document, root, "tendency", executableAction.getTendency());
appendTextElement(document, root, "reason", executableAction.getReason()); appendTextElement(document, root, "reason", executableAction.getReason());
appendTextElement(document, root, "description", executableAction.getDescription()); appendTextElement(document, root, "description", executableAction.getDescription());
appendTextElement(document, root, "source_user", executableAction.getSource()); appendTextElement(document, root, "source_user", executableAction.getSource());
if (evaluatorResult.getScheduleType() != null) { EvaluatorResult.ScheduleData scheduleData = evaluatorResult.getScheduleData();
appendTextElement(document, root, "schedule_type", evaluatorResult.getScheduleType()); if (scheduleData != null) {
appendTextElement(document, root, "schedule_type", scheduleData.getType());
appendTextElement(document, root, "schedule_content", scheduleData.getContent());
} }
if (evaluatorResult.getScheduleContent() != null) { Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
appendTextElement(document, root, "schedule_content", evaluatorResult.getScheduleContent());
}
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChain();
if (primaryActionChain == null || primaryActionChain.isEmpty()) { if (primaryActionChain == null || primaryActionChain.isEmpty()) {
return; return;
} }
@@ -253,10 +249,10 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
} }
private BlockContent buildPendingCompactBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) { private BlockContent buildPendingCompactBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
return new BlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH) { return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
@Override @Override
protected void fillXml(@NotNull Document document, @NotNull Element root) { protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "waiting_confirm"); appendTextElement(document, root, "state", "need_user_confirm");
appendTextElement(document, root, "related_input", input); appendTextElement(document, root, "related_input", input);
appendTextElement(document, root, "tendency", executableAction.getTendency()); appendTextElement(document, root, "tendency", executableAction.getTendency());
appendTextElement(document, root, "description", executableAction.getDescription()); appendTextElement(document, root, "description", executableAction.getDescription());
@@ -269,7 +265,7 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
return new BlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH) { return new BlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH) {
@Override @Override
protected void fillXml(@NotNull Document document, @NotNull Element root) { protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "waiting_confirm"); appendTextElement(document, root, "state", "need_user_confirm");
appendTextElement(document, root, "related_input", input); appendTextElement(document, root, "related_input", input);
appendTextElement(document, root, "pending_tendency", executableAction.getTendency()); appendTextElement(document, root, "pending_tendency", executableAction.getTendency());
appendTextElement(document, root, "summary", "exists pending action waiting for confirmation"); appendTextElement(document, root, "summary", "exists pending action waiting for confirmation");
@@ -366,8 +362,8 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
evaluatorResult.getReason(), evaluatorResult.getReason(),
evaluatorResult.getDescription(), evaluatorResult.getDescription(),
userId, userId,
evaluatorResult.getScheduleType(), evaluatorResult.getScheduleData().getType(),
evaluatorResult.getScheduleContent() evaluatorResult.getScheduleData().getContent()
); );
case IMMEDIATE -> new ImmediateExecutableAction( case IMMEDIATE -> new ImmediateExecutableAction(
evaluatorResult.getTendency(), evaluatorResult.getTendency(),
@@ -381,7 +377,7 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
private Map<Integer, List<MetaAction>> getActionChain(EvaluatorResult evaluatorResult) { private Map<Integer, List<MetaAction>> getActionChain(EvaluatorResult evaluatorResult) {
Map<Integer, List<MetaAction>> actionChain = new HashMap<>(); Map<Integer, List<MetaAction>> actionChain = new HashMap<>();
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChain(); Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
if (!fixDependencies(primaryActionChain)) { if (!fixDependencies(primaryActionChain)) {
return null; return null;
} }

View File

@@ -25,6 +25,53 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<EvaluatorResult>> implements ActivateModel { public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<EvaluatorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责评估单条行动倾向,并判断它是否值得进入后续行动链。
你会收到:
- 一条结构化上下文消息,其中可能包含当前活跃的行动相关状态、近期交流轨迹、认知相关上下文、以及当前活跃记忆切片;
- 一组 <available_meta_actions>,表示当前可用的 MetaAction 候选;
- 一条最新的 user message其内容就是当前需要评估的单条 tendency。
你的任务:
- 判断该 tendency 是否成立、是否值得推进;
- 判断它更适合立即执行,还是先进入规划;
- 判断是否需要先向用户确认;
- 若可推进,则基于 available_meta_actions 生成 primaryActionChain
- 若当前 tendency 与某个待处理 pending 明确对应,且本轮已完成承接或推进,应正确填写 resolvedPending
- 若当前 tendency 明确包含可调度语义,再填写 scheduleData。
评估原则:
- 结合上下文理解当前 tendency 与近期交流、当前行动状态、活跃记忆之间的关系。
- 若 tendency 与已有正在执行、等待确认、或已明确覆盖的行动完全等价,通常不应重复建立新的行动链。
- 若 tendency 是对已有待确认行动的确认、拒绝、补充条件、修改要求或继续推进,则应优先视为对原有行动状态的承接,而不是无关新任务。
- 若 action 相关上下文中存在等待确认的 block且当前 tendency 明显与其相关,则必须显式承接这一点,不要因为其已存在于上下文中而省略。
- 只有在 tendency 明确具有可执行意义时,才返回 ok=true。
- 若 tendency 更适合直接交流回应,或尚不足以形成行动推进,则返回 ok=false。
- primaryActionChain 中只能使用 available_meta_actions 内出现的 action_key不要编造不存在的动作。
- 不要输出完整执行细节、自然语言计划正文或额外解释,只输出 EvaluatorResult 对应结构。
关于字段:
- ok 表示该 tendency 是否值得进入后续行动推进。
- needConfirm 表示在真正推进前是否必须先得到用户确认。
- type:
- IMMEDIATE: 可直接进入即时执行链;
- PLANNING: 需要先形成或进入规划链,再决定后续执行。
- primaryActionChain 表示按 order 分组的候选动作链,每个元素包含:
- order: 执行顺序
- actionKeys: 该顺序下的 action_key 列表
- reason 用于说明你为何做出该判断,应简洁明确。
- description 用于概括本次行动评估结果,应能帮助后续模块快速理解该 tendency 的推进方向。
- scheduleData 仅在该 tendency 明确包含可调度语义时填写;否则留空。
- scheduleData.type: 一次性计划或周期性计划
- scheduleData.content: 符合 Quartz 标准的 Cron 表达式
- resolvedPending 仅在你能明确判断当前 tendency 已承接某个 pending block 时填写;否则留空。
- 当 ok=false 时type、primaryActionChain、scheduleData、resolvedPending 通常应留空或保持无效默认值,不要强行填充。
输出要求:
- 严格按照 EvaluatorResult 对应结构输出。
- 不要输出结构之外的解释、注释或额外文本。
""";
@InjectCapability @InjectCapability
private ActionCapability actionCapability; private ActionCapability actionCapability;
@@ -107,6 +154,12 @@ public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, Lis
return new ResolvedContext(List.of(content)).encodeToMessage(); return new ResolvedContext(List.of(content)).encodeToMessage();
} }
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull @NotNull
@Override @Override
public String modelKey() { public String modelKey() {

View File

@@ -1,10 +1,12 @@
package work.slhaf.partner.module.action.planner.evaluator.entity; package work.slhaf.partner.module.action.planner.evaluator.entity;
import lombok.Data; import lombok.Data;
import work.slhaf.partner.core.action.entity.SchedulableExecutableAction; import work.slhaf.partner.core.action.entity.Schedulable;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Data @Data
public class EvaluatorResult { public class EvaluatorResult {
@@ -12,9 +14,8 @@ public class EvaluatorResult {
private boolean needConfirm; private boolean needConfirm;
private ResolvedPending resolvedPending; private ResolvedPending resolvedPending;
private ActionType type; private ActionType type;
private String scheduleContent; private ScheduleData scheduleData;
private SchedulableExecutableAction.ScheduleType scheduleType; private List<ChainElement> primaryActionChain;
private Map<Integer, List<String>> primaryActionChain;
private String tendency; private String tendency;
private String reason; private String reason;
private String description; private String description;
@@ -28,4 +29,25 @@ public class EvaluatorResult {
private String blockName; private String blockName;
private String source; private String source;
} }
public Map<Integer, List<String>> getPrimaryActionChainAsMap() {
return primaryActionChain.stream().collect(Collectors.toMap(
ChainElement::getOrder,
ChainElement::getActionKeys,
(oldValue, newValue) -> newValue,
LinkedHashMap::new
));
}
@Data
public static class ScheduleData {
private String content;
private Schedulable.ScheduleType type;
}
@Data
public static class ChainElement {
private Integer order;
private List<String> actionKeys;
}
} }

View File

@@ -16,6 +16,26 @@ import java.util.List;
public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<ExtractorResult>> implements ActivateModel { public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<ExtractorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责从当前输入中提取可能的行动倾向,供后续模块继续评估。
你会收到:
- 一条结构化上下文消息,主要包含 communication 域与 action 域内容;
- 一条最新输入,作为本轮重点分析对象。
规则:
- communication 域主要用于理解当前会话语境、主题延续与用户意图。
- action 域主要用于判断相关行动是否已经在执行、等待确认,或已被覆盖。
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤或工具细节。
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息或条件变化,应避免重复提出。
- 若 action 域中存在等待确认的 block且当前输入与其相关则必须提取出对应的行动倾向不要因为其已存在于上下文中而省略。
- 若当前输入没有明显行动倾向,可返回空结果。
输出要求:
- 严格按照 ExtractorResult 对应结构输出。
- 不要输出结构之外的解释或额外文本。
""";
@InjectCapability @InjectCapability
private CognitionCapability cognitionCapability; private CognitionCapability cognitionCapability;
@@ -39,6 +59,12 @@ public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<Extr
} }
} }
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull @NotNull
@Override @Override
public String modelKey() { public String modelKey() {