mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 16:53:04 +08:00
refactor(framework): add Result chain APIs and align runtime exception handling
This commit is contained in:
@@ -14,6 +14,7 @@ import work.slhaf.partner.core.action.exception.ActionLookupException;
|
|||||||
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
|
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
|
||||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||||
import work.slhaf.partner.framework.agent.config.ConfigCenter;
|
import work.slhaf.partner.framework.agent.config.ConfigCenter;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||||
@@ -184,31 +185,32 @@ public class ActionCore implements StateSerializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
|
private void applyInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
|
||||||
boolean rebuildCleanTag = false;
|
boolean[] rebuildCleanTag = {false};
|
||||||
|
|
||||||
interventions.sort(Comparator.comparingInt(MetaIntervention::getOrder));
|
interventions.sort(Comparator.comparingInt(MetaIntervention::getOrder));
|
||||||
|
|
||||||
for (MetaIntervention intervention : interventions) {
|
for (MetaIntervention intervention : interventions) {
|
||||||
Result<List<MetaAction>> actionsResult = resolveInterventionActions(intervention);
|
Result<List<MetaAction>> actionsResult = resolveInterventionActions(intervention);
|
||||||
if (actionsResult.isFailure()) {
|
actionsResult
|
||||||
reportLookupFailure(actionsResult.exceptionOrNull());
|
.onFailure(ExceptionReporterHandler.INSTANCE::report)
|
||||||
continue;
|
.onSuccess(actions -> {
|
||||||
}
|
|
||||||
List<MetaAction> actions = actionsResult.getOrNull();
|
|
||||||
|
|
||||||
switch (intervention.getType()) {
|
switch (intervention.getType()) {
|
||||||
case InterventionType.APPEND -> handleAppend(executableAction, intervention.getOrder(), actions);
|
case InterventionType.APPEND ->
|
||||||
case InterventionType.INSERT -> handleInsert(executableAction, intervention.getOrder(), actions);
|
handleAppend(executableAction, intervention.getOrder(), actions);
|
||||||
case InterventionType.DELETE -> handleDelete(executableAction, intervention.getOrder(), actions);
|
case InterventionType.INSERT ->
|
||||||
|
handleInsert(executableAction, intervention.getOrder(), actions);
|
||||||
|
case InterventionType.DELETE ->
|
||||||
|
handleDelete(executableAction, intervention.getOrder(), actions);
|
||||||
case InterventionType.CANCEL -> handleCancel(executableAction);
|
case InterventionType.CANCEL -> handleCancel(executableAction);
|
||||||
case InterventionType.REBUILD -> {
|
case InterventionType.REBUILD -> {
|
||||||
if (!rebuildCleanTag) {
|
if (!rebuildCleanTag[0]) {
|
||||||
cleanActionData(executableAction);
|
cleanActionData(executableAction);
|
||||||
rebuildCleanTag = true;
|
rebuildCleanTag[0] = true;
|
||||||
}
|
}
|
||||||
handleRebuild(executableAction, intervention.getOrder(), actions);
|
handleRebuild(executableAction, intervention.getOrder(), actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -217,23 +219,14 @@ public class ActionCore implements StateSerializable {
|
|||||||
List<MetaAction> actions = new ArrayList<>();
|
List<MetaAction> actions = new ArrayList<>();
|
||||||
for (String actionKey : intervention.getActions()) {
|
for (String actionKey : intervention.getActions()) {
|
||||||
Result<MetaAction> metaActionResult = loadMetaAction(actionKey);
|
Result<MetaAction> metaActionResult = loadMetaAction(actionKey);
|
||||||
if (metaActionResult.isFailure()) {
|
AgentRuntimeException failure = metaActionResult.onSuccess(actions::add).exceptionOrNull();
|
||||||
Throwable throwable = metaActionResult.exceptionOrNull();
|
if (failure != null) {
|
||||||
return Result.failure(throwable == null
|
return Result.failure(failure);
|
||||||
? new ActionLookupException("Meta action lookup failed: " + actionKey, actionKey, "META_ACTION")
|
|
||||||
: throwable);
|
|
||||||
}
|
}
|
||||||
actions.add(metaActionResult.getOrNull());
|
|
||||||
}
|
}
|
||||||
return Result.success(actions);
|
return Result.success(actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reportLookupFailure(Throwable throwable) {
|
|
||||||
if (throwable instanceof ActionLookupException lookupException) {
|
|
||||||
ExceptionReporterHandler.INSTANCE.report(lookupException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在未进入执行阶段的行动单元组新增新的行动
|
* 在未进入执行阶段的行动单元组新增新的行动
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.w3c.dom.Element;
|
|||||||
import work.slhaf.partner.core.action.ActionCapability;
|
import work.slhaf.partner.core.action.ActionCapability;
|
||||||
import work.slhaf.partner.core.action.entity.Action;
|
import work.slhaf.partner.core.action.entity.Action;
|
||||||
import work.slhaf.partner.core.action.entity.ExecutableAction;
|
import work.slhaf.partner.core.action.entity.ExecutableAction;
|
||||||
|
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
|
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
|
||||||
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||||
@@ -15,7 +16,7 @@ import work.slhaf.partner.core.cognition.BlockContent;
|
|||||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||||
import work.slhaf.partner.core.cognition.ContextWorkspace;
|
import work.slhaf.partner.core.cognition.ContextWorkspace;
|
||||||
import work.slhaf.partner.framework.agent.exception.AgentException;
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||||
@@ -310,13 +311,9 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
|||||||
List<String> actions = requireActions(params, type);
|
List<String> actions = requireActions(params, type);
|
||||||
ExecutableAction target = requireTargetAction(targetId);
|
ExecutableAction target = requireTargetAction(targetId);
|
||||||
Result<Void> validationResult = validateActionKeys(actions);
|
Result<Void> validationResult = validateActionKeys(actions);
|
||||||
if (validationResult.isFailure()) {
|
AgentRuntimeException validationFailure = validationResult.onFailure(ExceptionReporterHandler.INSTANCE::report).exceptionOrNull();
|
||||||
reportFailure(validationResult.exceptionOrNull());
|
if (validationFailure != null) {
|
||||||
Throwable throwable = validationResult.exceptionOrNull();
|
return JSONObject.of("ok", false, "result", validationFailure.getLocalizedMessage()).toJSONString();
|
||||||
return JSONObject.of(
|
|
||||||
"ok", false,
|
|
||||||
"result", throwable == null ? "Intervention action validation failed" : throwable.getLocalizedMessage()
|
|
||||||
).toJSONString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MetaIntervention intervention = new MetaIntervention();
|
MetaIntervention intervention = new MetaIntervention();
|
||||||
@@ -398,24 +395,13 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
|||||||
|
|
||||||
private Result<Void> validateActionKeys(List<String> actions) {
|
private Result<Void> validateActionKeys(List<String> actions) {
|
||||||
for (String actionKey : actions) {
|
for (String actionKey : actions) {
|
||||||
Result<work.slhaf.partner.core.action.entity.MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
|
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
|
||||||
if (metaActionResult.isFailure()) {
|
AgentRuntimeException failure = metaActionResult.exceptionOrNull();
|
||||||
Throwable throwable = metaActionResult.exceptionOrNull();
|
if (failure != null) {
|
||||||
return Result.failure(throwable == null
|
return Result.failure(failure);
|
||||||
? new work.slhaf.partner.core.action.exception.ActionLookupException(
|
|
||||||
"Meta action lookup failed: " + actionKey,
|
|
||||||
actionKey,
|
|
||||||
"META_ACTION"
|
|
||||||
) : throwable);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reportFailure(Throwable throwable) {
|
|
||||||
if (throwable instanceof AgentException agentException) {
|
|
||||||
ExceptionReporterHandler.INSTANCE.report(agentException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import work.slhaf.partner.core.action.ActionCore;
|
|||||||
import work.slhaf.partner.core.action.entity.*;
|
import work.slhaf.partner.core.action.entity.*;
|
||||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||||
import work.slhaf.partner.framework.agent.exception.AgentException;
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||||
@@ -314,14 +314,15 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
|||||||
|
|
||||||
val executingStage = actionData.getExecutingStage();
|
val executingStage = actionData.getExecutingStage();
|
||||||
|
|
||||||
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription());
|
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription())
|
||||||
if (extractorInputResult.isFailure()) {
|
.onFailure(ExceptionReporterHandler.INSTANCE::report);
|
||||||
reportFailure(extractorInputResult.exceptionOrNull());
|
AgentRuntimeException exception = extractorInputResult.exceptionOrNull();
|
||||||
Throwable throwable = extractorInputResult.exceptionOrNull();
|
if (exception != null) {
|
||||||
failureReason = buildAttemptFailureReason("参数提取失败", throwable == null ? null : throwable.getLocalizedMessage());
|
failureReason = exception.getMessage();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
val extractorInput = extractorInputResult.getOrNull();
|
|
||||||
|
ExtractorInput extractorInput = extractorInputResult.getOrThrow();
|
||||||
ExtractorResult extractorResult = paramsExtractor.execute(extractorInput);
|
ExtractorResult extractorResult = paramsExtractor.execute(extractorInput);
|
||||||
|
|
||||||
if (extractorResult == null || !extractorResult.isOk()) {
|
if (extractorResult == null || !extractorResult.isOk()) {
|
||||||
@@ -508,21 +509,12 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String resolveHistoryDescription(String actionKey) {
|
private String resolveHistoryDescription(String actionKey) {
|
||||||
Result<MetaActionInfo> metaActionInfoResult = actionCapability.loadMetaActionInfo(actionKey);
|
return actionCapability.loadMetaActionInfo(actionKey)
|
||||||
if (metaActionInfoResult.isFailure()) {
|
.onFailure(ExceptionReporterHandler.INSTANCE::report)
|
||||||
reportFailure(metaActionInfoResult.exceptionOrNull());
|
.fold(
|
||||||
return actionKey;
|
metaActionInfo -> metaActionInfo.getDescription().isBlank() ? actionKey : metaActionInfo.getDescription(),
|
||||||
}
|
exception -> actionKey
|
||||||
MetaActionInfo metaActionInfo = metaActionInfoResult.getOrNull();
|
);
|
||||||
return metaActionInfo == null || metaActionInfo.getDescription().isBlank()
|
|
||||||
? actionKey
|
|
||||||
: metaActionInfo.getDescription();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportFailure(Throwable throwable) {
|
|
||||||
if (throwable instanceof AgentException agentException) {
|
|
||||||
ExceptionReporterHandler.INSTANCE.report(agentException);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private record MetaActionsListeningRecord(AtomicBoolean accepting, int phase) {
|
private record MetaActionsListeningRecord(AtomicBoolean accepting, int phase) {
|
||||||
@@ -540,21 +532,16 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) {
|
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) {
|
||||||
Result<MetaActionInfo> metaActionInfoResult = actionCapability.loadMetaActionInfo(actionKey);
|
return actionCapability.loadMetaActionInfo(actionKey).fold(
|
||||||
if (metaActionInfoResult.isFailure()) {
|
metaActionInfo -> {
|
||||||
Throwable throwable = metaActionInfoResult.exceptionOrNull();
|
|
||||||
return Result.failure(throwable == null
|
|
||||||
? new work.slhaf.partner.core.action.exception.ActionLookupException(
|
|
||||||
"Meta action description not found for action key: " + actionKey,
|
|
||||||
actionKey,
|
|
||||||
"META_ACTION_INFO"
|
|
||||||
) : throwable);
|
|
||||||
}
|
|
||||||
ExtractorInput input = new ExtractorInput();
|
ExtractorInput input = new ExtractorInput();
|
||||||
input.setMetaActionInfo(metaActionInfoResult.getOrNull());
|
input.setMetaActionInfo(metaActionInfo);
|
||||||
input.setTargetActionId(uuid);
|
input.setTargetActionId(uuid);
|
||||||
input.setTargetActionDesc(description);
|
input.setTargetActionDesc(description);
|
||||||
return Result.success(input);
|
return Result.success(input);
|
||||||
|
},
|
||||||
|
Result::failure
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction) {
|
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction) {
|
||||||
|
|||||||
@@ -34,14 +34,16 @@ public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Ext
|
|||||||
resolveTaskMessage(input)
|
resolveTaskMessage(input)
|
||||||
);
|
);
|
||||||
Result<ExtractorResult> result = formattedChat(messages, ExtractorResult.class);
|
Result<ExtractorResult> result = formattedChat(messages, ExtractorResult.class);
|
||||||
if (result.isFailure()) {
|
return result.fold(
|
||||||
log.error("ParamsExtractor解析结果失败", result.exceptionOrNull());
|
extractorResult -> extractorResult,
|
||||||
|
exception -> {
|
||||||
|
log.error("ParamsExtractor解析结果失败", exception);
|
||||||
ExtractorResult fallback = new ExtractorResult();
|
ExtractorResult fallback = new ExtractorResult();
|
||||||
fallback.setOk(false);
|
fallback.setOk(false);
|
||||||
fallback.setParams(new HashMap<>());
|
fallback.setParams(new HashMap<>());
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
return result.getOrThrow();
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Message resolveTaskMessage(ExtractorInput input) {
|
private Message resolveTaskMessage(ExtractorInput input) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import work.slhaf.partner.core.cognition.BlockContent;
|
|||||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||||
import work.slhaf.partner.core.cognition.CommunicationBlockContent;
|
import work.slhaf.partner.core.cognition.CommunicationBlockContent;
|
||||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||||
@@ -384,11 +385,12 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
|||||||
List<MetaAction> metaActions = new ArrayList<>();
|
List<MetaAction> metaActions = new ArrayList<>();
|
||||||
for (String actionKey : entry.getValue()) {
|
for (String actionKey : entry.getValue()) {
|
||||||
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
|
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
|
||||||
if (metaActionResult.isFailure()) {
|
AgentRuntimeException failure = metaActionResult.onSuccess(metaActions::add)
|
||||||
reportFailure(metaActionResult);
|
.onFailure(ExceptionReporterHandler.INSTANCE::report)
|
||||||
|
.exceptionOrNull();
|
||||||
|
if (failure != null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
metaActions.add(metaActionResult.getOrNull());
|
|
||||||
}
|
}
|
||||||
actionChain.put(entry.getKey(), metaActions);
|
actionChain.put(entry.getKey(), metaActions);
|
||||||
}
|
}
|
||||||
@@ -408,12 +410,14 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
|||||||
List<String> actionKeys = primaryActionChain.get(fixedOrder);
|
List<String> actionKeys = primaryActionChain.get(fixedOrder);
|
||||||
for (String actionKey : actionKeys) {
|
for (String actionKey : actionKeys) {
|
||||||
// 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖
|
// 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖
|
||||||
Result<MetaActionInfo> metaActionInfoResult = actionCapability.loadMetaActionInfo(actionKey);
|
|
||||||
if (metaActionInfoResult.isFailure()) {
|
Result<MetaActionInfo> infoResult = actionCapability.loadMetaActionInfo(actionKey)
|
||||||
reportFailure(metaActionInfoResult);
|
.onFailure(ExceptionReporterHandler.INSTANCE::report);
|
||||||
|
if (infoResult.exceptionOrNull() != null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MetaActionInfo metaActionInfo = metaActionInfoResult.getOrNull();
|
|
||||||
|
MetaActionInfo metaActionInfo = infoResult.getOrThrow();
|
||||||
Set<String> preActions = metaActionInfo.getPreActions();
|
Set<String> preActions = metaActionInfo.getPreActions();
|
||||||
boolean preActionsExist = preActions.isEmpty();
|
boolean preActionsExist = preActions.isEmpty();
|
||||||
if (!preActionsExist) {
|
if (!preActionsExist) {
|
||||||
@@ -444,12 +448,6 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reportFailure(Result<?> result) {
|
|
||||||
if (result.exceptionOrNull() instanceof work.slhaf.partner.framework.agent.exception.AgentException agentException) {
|
|
||||||
ExceptionReporterHandler.INSTANCE.report(agentException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fixOrder(Map<Integer, List<String>> primaryActionChain) {
|
private void fixOrder(Map<Integer, List<String>> primaryActionChain) {
|
||||||
Map<Integer, List<String>> tempChain = new HashMap<>(primaryActionChain);
|
Map<Integer, List<String>> tempChain = new HashMap<>(primaryActionChain);
|
||||||
primaryActionChain.clear();
|
primaryActionChain.clear();
|
||||||
|
|||||||
@@ -66,15 +66,14 @@ public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, Lis
|
|||||||
messages,
|
messages,
|
||||||
EvaluatorResult.class
|
EvaluatorResult.class
|
||||||
);
|
);
|
||||||
if (result.isFailure()) {
|
result
|
||||||
log.error("ActionEvaluator评估失败: {}", tendency, result.exceptionOrNull());
|
.onFailure(exception -> log.error("ActionEvaluator评估失败: {}", tendency, exception))
|
||||||
return;
|
.onSuccess(evaluatorResult -> {
|
||||||
}
|
|
||||||
EvaluatorResult evaluatorResult = result.getOrThrow();
|
|
||||||
evaluatorResult.setTendency(tendency);
|
evaluatorResult.setTendency(tendency);
|
||||||
synchronized (evaluatorResults) {
|
synchronized (evaluatorResults) {
|
||||||
evaluatorResults.add(evaluatorResult);
|
evaluatorResults.add(evaluatorResult);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ public class ActionExtractor extends AbstractAgentModule.Sub<String, ExtractorRe
|
|||||||
new Message(Message.Character.USER, input)
|
new Message(Message.Character.USER, input)
|
||||||
);
|
);
|
||||||
Result<ExtractorResult> result = formattedChat(messages, ExtractorResult.class);
|
Result<ExtractorResult> result = formattedChat(messages, ExtractorResult.class);
|
||||||
if (result.isSuccess()) {
|
return result.fold(
|
||||||
return result.getOrThrow();
|
extractorResult -> extractorResult,
|
||||||
}
|
exception -> {
|
||||||
log.error("提取信息出错", result.exceptionOrNull());
|
log.error("提取信息出错", exception);
|
||||||
return new ExtractorResult();
|
return new ExtractorResult();
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import work.slhaf.partner.core.cognition.*;
|
import work.slhaf.partner.core.cognition.*;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||||
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer;
|
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
import work.slhaf.partner.framework.agent.support.Result;
|
|
||||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
@@ -67,11 +67,11 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
|||||||
|
|
||||||
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
|
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
|
||||||
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
|
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
|
||||||
Result<kotlin.Unit> result = this.streamChat(buildChatMessages(runningFlowContext), consumer);
|
this.streamChat(buildChatMessages(runningFlowContext), consumer)
|
||||||
if (result.isFailure()) {
|
.onFailure(exception -> {
|
||||||
log.error("Streaming response failed", result.exceptionOrNull());
|
ExceptionReporterHandler.INSTANCE.report(exception);
|
||||||
consumer.onDelta(INTERRUPTED_MARKER);
|
consumer.onDelta(INTERRUPTED_MARKER);
|
||||||
}
|
});
|
||||||
updateChatMessages(runningFlowContext, consumer.collectResponse());
|
updateChatMessages(runningFlowContext, consumer.collectResponse());
|
||||||
updateContext();
|
updateContext();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAg
|
|||||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
import work.slhaf.partner.framework.agent.support.Result;
|
|
||||||
import work.slhaf.partner.module.TaskBlock;
|
import work.slhaf.partner.module.TaskBlock;
|
||||||
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
|
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
|
||||||
import work.slhaf.partner.module.memory.selector.evaluator.entity.EvaluatorBatchInput;
|
import work.slhaf.partner.module.memory.selector.evaluator.entity.EvaluatorBatchInput;
|
||||||
@@ -65,16 +64,15 @@ public class SliceSelectEvaluator extends AbstractAgentModule.Sub<EvaluatorInput
|
|||||||
contextMessage,
|
contextMessage,
|
||||||
resolveTaskMessage(batchInput)
|
resolveTaskMessage(batchInput)
|
||||||
);
|
);
|
||||||
Result<EvaluatorBatchResult> batchResult = formattedChat(messages, EvaluatorBatchResult.class);
|
formattedChat(messages, EvaluatorBatchResult.class)
|
||||||
if (batchResult.isFailure()) {
|
.onFailure(exception -> log.debug("切片评估失败,已跳过当前切片", exception))
|
||||||
log.debug("切片评估失败,已跳过当前切片", batchResult.exceptionOrNull());
|
.onSuccess(evaluatorBatchResult -> {
|
||||||
return;
|
if (evaluatorBatchResult.isPassed()) {
|
||||||
}
|
|
||||||
if (batchResult.getOrThrow().isPassed()) {
|
|
||||||
synchronized (result) {
|
synchronized (result) {
|
||||||
result.add(slice);
|
result.add(slice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,15 +43,19 @@ public class MemorySelectExtractor extends AbstractAgentModule.Sub<ExtractorInpu
|
|||||||
messages,
|
messages,
|
||||||
ExtractorResult.class
|
ExtractorResult.class
|
||||||
);
|
);
|
||||||
if (result.isSuccess()) {
|
extractorResult = result.fold(
|
||||||
extractorResult = result.getOrThrow();
|
value -> {
|
||||||
log.debug("[MemorySelectExtractor] 主题提取结果: {}", extractorResult);
|
log.debug("[MemorySelectExtractor] 主题提取结果: {}", value);
|
||||||
} else {
|
return value;
|
||||||
log.error("[MemorySelectExtractor] 主题提取出错: ", result.exceptionOrNull());
|
},
|
||||||
extractorResult = new ExtractorResult();
|
exception -> {
|
||||||
extractorResult.setRecall(false);
|
log.error("[MemorySelectExtractor] 主题提取出错: ", exception);
|
||||||
extractorResult.setMatches(List.of());
|
ExtractorResult fallback = new ExtractorResult();
|
||||||
|
fallback.setRecall(false);
|
||||||
|
fallback.setMatches(List.of());
|
||||||
|
return fallback;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return fix(extractorResult);
|
return fix(extractorResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import work.slhaf.partner.core.action.entity.*;
|
import work.slhaf.partner.core.action.entity.*;
|
||||||
import work.slhaf.partner.core.action.exception.ActionLookupException;
|
import work.slhaf.partner.core.action.exception.ActionLookupException;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
import work.slhaf.partner.framework.agent.support.Result;
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
|
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
|
||||||
|
|
||||||
@@ -350,15 +351,12 @@ class ActionCoreTest {
|
|||||||
actionCore.registerMetaActions(Map.of("builtin::demo", metaActionInfo));
|
actionCore.registerMetaActions(Map.of("builtin::demo", metaActionInfo));
|
||||||
|
|
||||||
Result<MetaAction> success = actionCore.loadMetaAction("builtin::demo");
|
Result<MetaAction> success = actionCore.loadMetaAction("builtin::demo");
|
||||||
assertTrue(success.isSuccess());
|
assertEquals("demo", success.fold(MetaAction::getName, ex -> fail(ex.getMessage())));
|
||||||
assertEquals("demo", success.getOrNull().getName());
|
|
||||||
|
|
||||||
Result<MetaAction> failure = actionCore.loadMetaAction("builtin::missing");
|
Result<MetaAction> failure = actionCore.loadMetaAction("builtin::missing");
|
||||||
assertTrue(failure.isFailure());
|
assertInstanceOf(ActionLookupException.class, assertThrows(AgentRuntimeException.class, failure::getOrThrow));
|
||||||
assertInstanceOf(ActionLookupException.class, failure.exceptionOrNull());
|
|
||||||
|
|
||||||
Result<MetaActionInfo> infoFailure = actionCore.loadMetaActionInfo("builtin::missing");
|
Result<MetaActionInfo> infoFailure = actionCore.loadMetaActionInfo("builtin::missing");
|
||||||
assertTrue(infoFailure.isFailure());
|
assertInstanceOf(ActionLookupException.class, assertThrows(AgentRuntimeException.class, infoFailure::getOrThrow));
|
||||||
assertInstanceOf(ActionLookupException.class, infoFailure.exceptionOrNull());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.openai.core.http.StreamResponse;
|
|||||||
import com.openai.models.chat.completions.*;
|
import com.openai.models.chat.completions.*;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
import work.slhaf.partner.framework.agent.exception.ModelInvokeException;
|
import work.slhaf.partner.framework.agent.exception.ModelInvokeException;
|
||||||
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer;
|
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
@@ -160,21 +161,14 @@ public class OpenAiCompatibleProvider extends ModelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private <T> Result<T> executeWithRetry(String failureMessage, ThrowingSupplier<T> supplier) {
|
private <T> Result<T> executeWithRetry(String failureMessage, ThrowingSupplier<T> supplier) {
|
||||||
Exception lastFailure = null;
|
AgentRuntimeException lastFailure = null;
|
||||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||||
Result<T> result = Result.runCatching(supplier::get);
|
Result<T> result = Result.runCatching(supplier::get);
|
||||||
if (result.isSuccess()) {
|
AgentRuntimeException failure = result.exceptionOrNull();
|
||||||
|
if (failure == null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
Throwable throwable = result.exceptionOrNull();
|
lastFailure = failure;
|
||||||
if (throwable instanceof Exception exception) {
|
|
||||||
lastFailure = exception;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (throwable instanceof Error error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return Result.failure(invokeException(failureMessage, throwable));
|
|
||||||
}
|
}
|
||||||
return Result.failure(invokeException(failureMessage, lastFailure));
|
return Result.failure(invokeException(failureMessage, lastFailure));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
package work.slhaf.partner.framework.agent.support
|
package work.slhaf.partner.framework.agent.support
|
||||||
|
|
||||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException
|
||||||
|
import java.util.function.Consumer
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
class Result<T> private constructor(
|
class Result<T> private constructor(
|
||||||
private val value: T?,
|
private val value: T?,
|
||||||
private val exception: Throwable?
|
private val exception: AgentRuntimeException?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun isSuccess(): Boolean = exception == null
|
|
||||||
|
|
||||||
fun isFailure(): Boolean = exception != null
|
|
||||||
|
|
||||||
fun getOrNull(): T? = value
|
|
||||||
|
|
||||||
fun exceptionOrNull(): Throwable? = exception
|
|
||||||
|
|
||||||
fun getOrThrow(): T {
|
fun getOrThrow(): T {
|
||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return value as T
|
return value as T
|
||||||
}
|
}
|
||||||
when (exception) {
|
throw exception
|
||||||
is AgentRuntimeException, is Error -> throw exception
|
|
||||||
else -> throw AgentRuntimeException(exception.localizedMessage, exception)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOrDefault(defaultValue: T): T {
|
fun getOrDefault(defaultValue: T): T {
|
||||||
@@ -35,6 +26,35 @@ class Result<T> private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun exceptionOrNull(): AgentRuntimeException? = exception
|
||||||
|
|
||||||
|
fun onSuccess(consumer: Consumer<in T>): Result<T> {
|
||||||
|
if (exception == null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
consumer.accept(value as T)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFailure(consumer: Consumer<in AgentRuntimeException>): Result<T> {
|
||||||
|
if (exception != null) {
|
||||||
|
consumer.accept(exception)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <R> fold(
|
||||||
|
onSuccess: Function<in T, out R>,
|
||||||
|
onFailure: Function<in AgentRuntimeException, out R>
|
||||||
|
): R {
|
||||||
|
return if (exception == null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
onSuccess.apply(value as T)
|
||||||
|
} else {
|
||||||
|
onFailure.apply(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return if (exception == null) {
|
return if (exception == null) {
|
||||||
"Result.success($value)"
|
"Result.success($value)"
|
||||||
@@ -44,7 +64,7 @@ class Result<T> private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun interface ThrowingSupplier<T> {
|
fun interface ThrowingSupplier<T> {
|
||||||
@Throws(Throwable::class)
|
@Throws(Exception::class)
|
||||||
fun get(): T
|
fun get(): T
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,19 +73,19 @@ class Result<T> private constructor(
|
|||||||
fun <T> success(value: T): Result<T> = Result(value, null)
|
fun <T> success(value: T): Result<T> = Result(value, null)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun <T> failure(exception: Throwable): Result<T> = Result(null, exception)
|
fun <T> failure(exception: AgentRuntimeException): Result<T> = Result(null, exception)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun <T> runCatching(block: ThrowingSupplier<T>): Result<T> {
|
fun <T> runCatching(block: ThrowingSupplier<T>): Result<T> {
|
||||||
return try {
|
return try {
|
||||||
success(block.get())
|
success(block.get())
|
||||||
} catch (throwable: Throwable) {
|
} catch (exception: Exception) {
|
||||||
failure(
|
failure(
|
||||||
when (throwable) {
|
when (exception) {
|
||||||
is AgentRuntimeException, is Error -> throwable
|
is AgentRuntimeException -> exception
|
||||||
else -> AgentRuntimeException(
|
else -> AgentRuntimeException(
|
||||||
throwable.message ?: "Unexpected runtime failure.",
|
exception.message ?: "Unexpected runtime failure.",
|
||||||
throwable
|
exception
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package work.slhaf.partner.framework.agent.support;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ResultTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepAgentRuntimeExceptionInFailure() {
|
||||||
|
AgentRuntimeException exception = new AgentRuntimeException("runtime failure");
|
||||||
|
|
||||||
|
Result<String> result = Result.failure(exception);
|
||||||
|
|
||||||
|
assertSame(exception, result.exceptionOrNull());
|
||||||
|
AtomicReference<AgentRuntimeException> failure = new AtomicReference<>();
|
||||||
|
result.onFailure(failure::set);
|
||||||
|
assertSame(exception, failure.get());
|
||||||
|
AgentRuntimeException thrown = assertThrows(AgentRuntimeException.class, result::getOrThrow);
|
||||||
|
assertSame(exception, thrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWrapCheckedExceptionInRunCatching() {
|
||||||
|
Result<String> result = Result.runCatching(() -> {
|
||||||
|
throw new java.io.IOException("io failure");
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(result.exceptionOrNull());
|
||||||
|
assertInstanceOf(java.io.IOException.class, result.exceptionOrNull().getCause());
|
||||||
|
AgentRuntimeException thrown = assertThrows(AgentRuntimeException.class, result::getOrThrow);
|
||||||
|
assertInstanceOf(java.io.IOException.class, thrown.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepAgentRuntimeExceptionInRunCatching() {
|
||||||
|
AgentRuntimeException exception = new AgentRuntimeException("runtime failure");
|
||||||
|
|
||||||
|
Result<String> result = Result.runCatching(() -> {
|
||||||
|
throw exception;
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSame(exception, result.exceptionOrNull());
|
||||||
|
AgentRuntimeException thrown = assertThrows(AgentRuntimeException.class, result::getOrThrow);
|
||||||
|
assertSame(exception, thrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNullExceptionForSuccess() {
|
||||||
|
assertNull(Result.success("ok").exceptionOrNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotCatchErrorInRunCatching() {
|
||||||
|
AssertionError error = new AssertionError("boom");
|
||||||
|
|
||||||
|
AssertionError thrown = assertThrows(AssertionError.class, () -> Result.runCatching(() -> {
|
||||||
|
throw error;
|
||||||
|
}));
|
||||||
|
|
||||||
|
assertSame(error, thrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleSuccessAndFailureBranchesWithChainApis() {
|
||||||
|
AtomicReference<String> successValue = new AtomicReference<>();
|
||||||
|
AtomicReference<AgentRuntimeException> failureValue = new AtomicReference<>();
|
||||||
|
AgentRuntimeException exception = new AgentRuntimeException("runtime failure");
|
||||||
|
|
||||||
|
Result.success("ok")
|
||||||
|
.onSuccess(successValue::set)
|
||||||
|
.onFailure(failureValue::set);
|
||||||
|
|
||||||
|
assertEquals("ok", successValue.get());
|
||||||
|
assertNull(failureValue.get());
|
||||||
|
|
||||||
|
successValue.set(null);
|
||||||
|
Result.<String>failure(exception)
|
||||||
|
.onSuccess(successValue::set)
|
||||||
|
.onFailure(failureValue::set);
|
||||||
|
|
||||||
|
assertNull(successValue.get());
|
||||||
|
assertSame(exception, failureValue.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFoldResult() {
|
||||||
|
String success = Result.success("ok").fold(
|
||||||
|
value -> "success:" + value,
|
||||||
|
ex -> "failure:" + ex.getMessage()
|
||||||
|
);
|
||||||
|
String failure = Result.<String>failure(new AgentRuntimeException("bad")).fold(
|
||||||
|
value -> "success:" + value,
|
||||||
|
ex -> "failure:" + ex.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("success:ok", success);
|
||||||
|
assertEquals("failure:bad", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user