diff --git a/src/main/java/de/extio/lmlib/agent/Agent.java b/src/main/java/de/extio/lmlib/agent/Agent.java index d607885..26ceded 100755 --- a/src/main/java/de/extio/lmlib/agent/Agent.java +++ b/src/main/java/de/extio/lmlib/agent/Agent.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; @@ -24,7 +25,7 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor private static final Logger LOGGER = LoggerFactory.getLogger(Agent.class); public List execute(final Client client, final ExecutorService agentExecutorService, final AgentContext context) { - final var retContexts = Collections.synchronizedList(new ArrayList()); + final var finishedSplits = Collections.synchronizedList(new ArrayList()); context.setNextAgent(null); @@ -36,57 +37,76 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor final var tasks = new ArrayList>(splits.size()); for (final var split : splits) { tasks.add(CompletableFuture.runAsync(() -> { - split.context().getGraph().add(this.modelCategory.getShortName()); - split.context().getGraph().add(this.name()); - - final var conversation = this.setupConversation(split); - LOGGER.debug("Conversation: {}", conversation); - - Completion completion = null; - boolean parseable = false; - for (int i = 0; i < 2; i++) { - completion = client.conversation(this.modelCategory, conversation); - split.context().getRequestStatistic().add(completion); + try { + if (this.agentType() != AgentType.PROCESSING_ONLY) { + split.context().getGraph().add(this.modelCategory.getShortName()); + } + split.context().getGraph().add(this.name()); - try { - if (!this.responseHandler.handle(split, completion)) { - context.getGraph().add("⚠"); - LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response()); - continue; - } + if (this.agentType != AgentType.PROCESSING_ONLY) { + final var conversation = this.setupConversation(split); + LOGGER.debug("Conversation: {}", conversation); - parseable = true; - break; + Completion completion = null; + boolean parseable = false; + for (int i = 0; i < 2; i++) { + completion = client.conversation(this.modelCategory, conversation); + split.context().setLastCompletion(completion); + split.context().getRequestStatistic().add(completion); + + try { + if (!this.responseHandler.handle(split, completion)) { + context.getGraph().add("⚠"); + LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response()); + continue; + } + + parseable = true; + break; + } + catch (final Exception e) { + context.getGraph().add("⚠"); + LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e); + continue; + } + } + if (!parseable) { + LOGGER.warn("{} Response is still not parseable after last attempt", this.name()); + context.getGraph().add("☢"); + return; + } } - catch (final Exception e) { - context.getGraph().add("⚠"); - LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e); - continue; + + if (this.postProcessor != null) { + this.postProcessor.accept(split.context()); } + + finishedSplits.add(split); } - if (!parseable) { - LOGGER.warn("{} Response is still not parseable after last attempt", this.name()); - context.getGraph().add("☢"); - return; + catch (final Exception ex) { + LOGGER.error("Error during agent execution", ex); } - - if (this.postProcessor != null) { - this.postProcessor.accept(split.context()); - } - - this.chooseNextAgent(conversation, completion, split.context()); - - retContexts.add(split.context()); }, agentExecutorService)); } CompletableFuture.allOf(tasks.toArray(new CompletableFuture[0])).join(); + var retContexts = finishedSplits + .stream() + .sorted((s0, s1) -> Integer.compare(s0.index(), s1.index())) + .map(Split::context) + .toList(); + if (this.merger != null) { - final var mergedContexts = this.merger.apply(retContexts); - mergedContexts.forEach(c -> c.getGraph().add(retContexts.size() + "↣" + mergedContexts.size())); - return mergedContexts; + final int size = retContexts.size(); + retContexts = this.merger.apply(retContexts); + for (final var retContext : retContexts) { + retContext.getGraph().add(size + "↣" + retContexts.size()); + } } + + retContexts.forEach(this::chooseNextAgent); + return retContexts; } @@ -112,7 +132,7 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor } private List applyTemplate(final AgentContext context) { - String text = this.textTemplate; + String text = this.textTemplate != null ? this.textTemplate : ""; for (final var entry : context.getContext().entrySet()) { final var k = "{{" + entry.getKey() + "}}"; if (text.contains(k)) { @@ -120,31 +140,39 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor } } + var branchedEntriesMinCntValues = 0; + final var branchedEntries = new ArrayList>>(); for (final var entry : context.getContext().entrySet()) { final var k = "[[" + entry.getKey() + "]]"; if (text.contains(k)) { - final var result = new ArrayList(); - - int i = 0; - for (final var value : entry.getValue()) { - final var nextText = text.replace(k, value.toString()); - - final var nextContext = new HashMap<>(context.getContext()); - nextContext.put(entry.getKey(), List.of(value)); - - final var nextAgentContext = new AgentContext(context, nextContext); - if (entry.getValue().size() > 1) { - nextAgentContext.getGraph().add("⇉ " + ++i + "/" + entry.getValue().size()); - } - - result.add(new Split(nextText, nextAgentContext)); - } - - return result; // TODO: Multiple splits not supported yet + branchedEntries.add(entry); + branchedEntriesMinCntValues = branchedEntriesMinCntValues == 0 ? entry.getValue().size() : Math.min(branchedEntriesMinCntValues, entry.getValue().size()); } } + if (!branchedEntries.isEmpty()) { + final var result = new ArrayList(); + + for (int i = 0; i < branchedEntriesMinCntValues; i++) { + String nextText = text; + final var nextContext = new HashMap<>(context.getContext()); + for (final var entry : branchedEntries) { + final var k = "[[" + entry.getKey() + "]]"; + nextText = nextText.replace(k, entry.getValue().get(i).toString()); + nextContext.put(entry.getKey(), List.of(entry.getValue().get(i))); + } + + final var nextAgentContext = new AgentContext(context, nextContext); + if (branchedEntriesMinCntValues > 1) { + nextAgentContext.getGraph().add("⇉ " + (i + 1) + "/" + branchedEntriesMinCntValues); + } + + result.add(new Split(nextText, nextAgentContext, i)); + } + + return result; + } - return List.of(new Split(text, context)); + return List.of(new Split(text, context, 0)); } private Conversation setupConversation(final Split split) { @@ -158,7 +186,7 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor split.context().setConversation(conversation); } else { - if (conversation.getConversation().get(0).type() == TurnType.SYSTEM) { + if (conversation.getConversation().get(0).type() == TurnType.SYSTEM && this.agentType() != AgentType.CONVERSATION_WITH_SYSTEM_PROMPT) { final var mergedSysAndUser = conversation.getConversation().get(0).text() + "\n" + conversation.getConversation().get(1).text(); final var newConversation = Conversation.create(mergedSysAndUser); @@ -176,7 +204,7 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor return conversation; } - private void chooseNextAgent(final Conversation conversation, final Completion completion, final AgentContext context) { + private void chooseNextAgent(final AgentContext context) { context.setNextAgent(this.chooseNext.apply(context)); if (context.getNextAgent() != null) { @@ -194,11 +222,6 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor return; } - String answer = completion.response(); - if (context.getNextAgent().transformResponse() != null) { - answer = context.getNextAgent().transformResponse().apply(answer); - } - final Agent nextAgent = context.getAgents().get(context.getNextAgent().name()); switch (nextAgent) { case null -> { @@ -207,11 +230,6 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor context.setNextAgent(null); } - case final Agent a when context.getNextAgent().responseAsUserPrompt() == Boolean.TRUE -> { - context.getGraph().add("↺"); - conversation.replaceTurn(new Conversation.Turn(TurnType.USER, answer)); - } - case final Agent a when a.agentType() == AgentType.COMPLETION -> { context.getGraph().add("→"); context.setConversation(null); @@ -222,9 +240,15 @@ public record Agent(String name, AgentType agentType, ModelCategory modelCategor context.setConversation(null); } - case final Agent a when a.agentType() == AgentType.CONVERSATION -> { + case final Agent a when a.agentType() == AgentType.CONVERSATION || a.agentType() == AgentType.CONVERSATION_WITH_SYSTEM_PROMPT -> { context.getGraph().add("↴"); - conversation.addTurn(new Conversation.Turn(TurnType.ASSISTANT, answer)); + if (context.getConversation().getConversation().getLast().type() != TurnType.ASSISTANT) { + context.getConversation().addTurn(new Conversation.Turn(TurnType.ASSISTANT, context.getLastCompletion().response())); + } + } + + case final Agent a when a.agentType() == AgentType.PROCESSING_ONLY -> { + context.getGraph().add("○"); } default -> { diff --git a/src/main/java/de/extio/lmlib/agent/AgentContext.java b/src/main/java/de/extio/lmlib/agent/AgentContext.java index 418d464..2c56028 100755 --- a/src/main/java/de/extio/lmlib/agent/AgentContext.java +++ b/src/main/java/de/extio/lmlib/agent/AgentContext.java @@ -6,13 +6,14 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import de.extio.lmlib.client.Completion; import de.extio.lmlib.client.Conversation; public final class AgentContext { private final Map agents; - private final Map> context; + private final Map> context; private final AgentRequestStatistic requestStatistic; @@ -22,6 +23,8 @@ public final class AgentContext { private volatile AgentNext nextAgent; + private volatile Completion lastCompletion; + public AgentContext(final Map agents) { this.agents = agents; this.context = new ConcurrentHashMap<>(); @@ -32,18 +35,19 @@ public final class AgentContext { this(other, new ConcurrentHashMap<>(other.context)); } - public AgentContext(final AgentContext other, final Map> context) { + public AgentContext(final AgentContext other, final Map> context) { this.agents = other.agents; this.context = context; this.conversation = Conversation.copy(other.conversation); this.nextAgent = other.nextAgent; this.requestStatistic = other.requestStatistic; + this.lastCompletion = other.lastCompletion; synchronized (other.graph) { this.graph.addAll(other.graph); } } - public Map> getContext() { + public Map> getContext() { return this.context; } @@ -75,4 +79,12 @@ public final class AgentContext { return this.graph; } + public Completion getLastCompletion() { + return this.lastCompletion; + } + + public void setLastCompletion(final Completion lastCompletion) { + this.lastCompletion = lastCompletion; + } + } diff --git a/src/main/java/de/extio/lmlib/agent/AgentExecutorService.java b/src/main/java/de/extio/lmlib/agent/AgentExecutorService.java index 692e151..7956fb2 100755 --- a/src/main/java/de/extio/lmlib/agent/AgentExecutorService.java +++ b/src/main/java/de/extio/lmlib/agent/AgentExecutorService.java @@ -60,7 +60,12 @@ public class AgentExecutorService implements InitializingBean, DisposableBean { } branchTasks.add(CompletableFuture.runAsync(() -> { - responses.addAll(this.walkGraph(branchAgent, branchContext)); + try { + responses.addAll(this.walkGraph(branchAgent, branchContext)); + } + catch (Exception e) { + LOGGER.error("Error executing agent", e); + } }, this.branchExecutorService)); } else { diff --git a/src/main/java/de/extio/lmlib/agent/AgentNext.java b/src/main/java/de/extio/lmlib/agent/AgentNext.java index 03b12ed..a1a7259 100755 --- a/src/main/java/de/extio/lmlib/agent/AgentNext.java +++ b/src/main/java/de/extio/lmlib/agent/AgentNext.java @@ -1,7 +1,5 @@ package de.extio.lmlib.agent; -import java.util.function.Function; - -public final record AgentNext(String name, Boolean gradingPassed, Boolean responseAsUserPrompt, Function transformResponse) { +public final record AgentNext(String name, Boolean gradingPassed) { } diff --git a/src/main/java/de/extio/lmlib/agent/AgentType.java b/src/main/java/de/extio/lmlib/agent/AgentType.java index 23f6eaf..6e81663 100755 --- a/src/main/java/de/extio/lmlib/agent/AgentType.java +++ b/src/main/java/de/extio/lmlib/agent/AgentType.java @@ -1,5 +1,5 @@ package de.extio.lmlib.agent; public enum AgentType { - COMPLETION, START_CONVERSATION, CONVERSATION; + COMPLETION, START_CONVERSATION, CONVERSATION, CONVERSATION_WITH_SYSTEM_PROMPT, PROCESSING_ONLY; } diff --git a/src/main/java/de/extio/lmlib/agent/JsonAgentResponseHandler.java b/src/main/java/de/extio/lmlib/agent/JsonAgentResponseHandler.java index fa7e2cf..2a44f48 100755 --- a/src/main/java/de/extio/lmlib/agent/JsonAgentResponseHandler.java +++ b/src/main/java/de/extio/lmlib/agent/JsonAgentResponseHandler.java @@ -1,13 +1,15 @@ package de.extio.lmlib.agent; -import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -17,7 +19,28 @@ public class JsonAgentResponseHandler implements AgentResponseHandler { private static final Logger LOGGER = LoggerFactory.getLogger(JsonAgentResponseHandler.class); - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; + + public JsonAgentResponseHandler() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS, false); + this.objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_MISSING_VALUES, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS, true); + this.objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + } @Override public boolean handle(final Split split, final Completion completion) { @@ -38,7 +61,7 @@ public class JsonAgentResponseHandler implements AgentResponseHandler { while (fields.hasNext()) { final var entry = fields.next(); if (entry.getValue().isArray()) { - final var items = new HashSet<>(); + final var items = new LinkedHashSet<>(); entry.getValue().elements().forEachRemaining(item -> items.add(item.asText())); split.context().getContext().put(entry.getKey(), List.copyOf(items)); hasResponse = true; diff --git a/src/main/java/de/extio/lmlib/agent/Split.java b/src/main/java/de/extio/lmlib/agent/Split.java index 3a35bc1..2782168 100644 --- a/src/main/java/de/extio/lmlib/agent/Split.java +++ b/src/main/java/de/extio/lmlib/agent/Split.java @@ -1,5 +1,5 @@ package de.extio.lmlib.agent; -record Split(String text, AgentContext context) { +record Split(String text, AgentContext context, int index) { } diff --git a/src/test/java/de/extio/lmlib/agent/AgentTest.java b/src/test/java/de/extio/lmlib/agent/AgentTest.java index 7baae66..7fd1137 100644 --- a/src/test/java/de/extio/lmlib/agent/AgentTest.java +++ b/src/test/java/de/extio/lmlib/agent/AgentTest.java @@ -54,7 +54,7 @@ public class AgentTest { null, null, null, - context -> new AgentNext("FeatureIdentifier", null, null, null)), + context -> new AgentNext("FeatureIdentifier", null)), "FeatureIdentifier", new Agent("FeatureIdentifier", @@ -66,7 +66,7 @@ public class AgentTest { null, context -> context.getContext().get("features").forEach(feature -> LOGGER.info(feature.toString())), null, - context -> new AgentNext("FeatureAnalyzer", null, null, null)), + context -> new AgentNext("FeatureAnalyzer", null)), "FeatureAnalyzer", new Agent("FeatureAnalyzer", @@ -81,7 +81,7 @@ public class AgentTest { null, null, contexts -> Agent.mergeContexts(List.of("features", "featureDescription"), contexts), - context -> new AgentNext("AnswerGenerator", null, null, null)), + context -> new AgentNext("AnswerGenerator", null)), "AnswerGenerator", new Agent("AnswerGenerator", @@ -99,7 +99,7 @@ public class AgentTest { }, null, null, - context -> new AgentNext(null, null, null, null))); + context -> new AgentNext(null, null))); final var context = new AgentContext(agents); context.getContext().put("code", List.of(Files.readString(Path.of("src/main/java/de/extio/lmlib/agent/Agent.java")))); @@ -131,7 +131,7 @@ public class AgentTest { null, null, null, - context -> new AgentNext("SecondLevel", null, null, null)), + context -> new AgentNext("SecondLevel", null)), "SecondLevel", new Agent("SecondLevel", @@ -143,7 +143,7 @@ public class AgentTest { null, null, null, - context -> new AgentNext("Calculator", null, null, null)), + context -> new AgentNext("Calculator", null)), "Calculator", new Agent("Calculator", @@ -155,7 +155,7 @@ public class AgentTest { null, null, null, - context -> new AgentNext("Grader", null, null, null)), + context -> new AgentNext("Grader", null)), "Grader", new Agent("Grader", @@ -179,7 +179,7 @@ public class AgentTest { context.getContext().put("random", List.of(String.valueOf(ThreadLocalRandom.current().nextInt(10)))); } } - return new AgentNext(pass ? null : "Calculator", pass, null, null); + return new AgentNext(pass ? null : "Calculator", pass); })); final var context = new AgentContext(agents); diff --git a/src/test/resources/llama3-local.properties b/src/test/resources/llama3-local.properties index 028d63c..f56566c 100755 --- a/src/test/resources/llama3-local.properties +++ b/src/test/resources/llama3-local.properties @@ -7,5 +7,5 @@ topP=1.0 maxContinuations=1 modelProvider=OAI_TEXT_COMPLETION modelName= -url=http://cruncher4:5001 +url=http://localhost:5001 apiKey=