Implemented agent streaming
This commit is contained in:
30
README.md
30
README.md
@@ -123,7 +123,7 @@ Example profile: src/test/resources/llama3.3-70b-azure.properties
|
|||||||
|
|
||||||
Client service: clientService.getClient(ModelCategory.MEDIUM);
|
Client service: clientService.getClient(ModelCategory.MEDIUM);
|
||||||
Client usage details: CompletionClientTest and AzureAiClientTest
|
Client usage details: CompletionClientTest and AzureAiClientTest
|
||||||
Agentic flow: AgentTest#agenticFlow
|
Agentic flow: AgentTest#agenticFlow and AgentTest#streamedAgenticFlow
|
||||||
|
|
||||||
## Completions and Streaming
|
## Completions and Streaming
|
||||||
|
|
||||||
@@ -143,10 +143,6 @@ You can intercept every Conversation and Completion by implementing CompletionIn
|
|||||||
This is for example useful for accounting purposes or to implement a circuit breaker.
|
This is for example useful for accounting purposes or to implement a circuit breaker.
|
||||||
The client will autowire all implementations and intercept requests accordingly.
|
The client will autowire all implementations and intercept requests accordingly.
|
||||||
|
|
||||||
# Agentic Flows
|
|
||||||
|
|
||||||
This library contains a graph executor for implementing comprehensive processes using agent-based design patterns. See class AgentTest for examples.
|
|
||||||
|
|
||||||
# Grader
|
# Grader
|
||||||
|
|
||||||
The library includes a grader component that evaluates the correctness of agent responses.
|
The library includes a grader component that evaluates the correctness of agent responses.
|
||||||
@@ -155,6 +151,30 @@ Example:
|
|||||||
|
|
||||||
assertTrue(Grader.assessScoreBinary("Does the following blog post mention the feature " + feature, resultContext.getStringValue("blogPost"), this.clientService));
|
assertTrue(Grader.assessScoreBinary("Does the following blog post mention the feature " + feature, resultContext.getStringValue("blogPost"), this.clientService));
|
||||||
|
|
||||||
|
# Agentic Flows
|
||||||
|
|
||||||
|
This library contains a graph executor for implementing comprehensive processes using agent-based design patterns.
|
||||||
|
|
||||||
|
You can implement your Agents either by creating an Agent record or by extending BaseAgent - choose the approach that best fits your needs.
|
||||||
|
The agentic flow is started with an AgentContext and an initial agent.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
final var context = new AgentContext(List.of(new Agent1(), new Agent2()));
|
||||||
|
context.getContext().setStringValue("key", "Payload here");
|
||||||
|
context.getContext().setStringValues("key2", List.of("All context keys", "support collections", "for aggregation purposes"));
|
||||||
|
context.getContext().setValue("key3", new RandomDatatype());
|
||||||
|
final var resultContexts = agentExecutorService.walk("Agent1", context)
|
||||||
|
final var resultContext = resultContexts.getFirst(); // You may receive multiple contexts if your graph branches out
|
||||||
|
LOGGER.info(resultContext.getStringValue("blogPost") + "\n\n" + resultContext.getGraph().toString() + "\n" + resultContext.getRequestStatistic().toString());
|
||||||
|
|
||||||
|
Streaming is also supported.
|
||||||
|
|
||||||
|
See class AgentTest for examples.
|
||||||
|
|
||||||
|
# Agent implementations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Announcement generated by AgentTest#agenticFlow
|
# Announcement generated by AgentTest#agenticFlow
|
||||||
|
|
||||||
## Introducing Advanced Agent Orchestration: Dynamic Conversations & Parallel Processing
|
## Introducing Advanced Agent Orchestration: Dynamic Conversations & Parallel Processing
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.responsehandler.AgentResponseHandler;
|
||||||
import de.extio.lmlib.profile.ModelCategory;
|
import de.extio.lmlib.profile.ModelCategory;
|
||||||
|
|
||||||
public record Agent(String name,
|
public record Agent(String name,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.extio.lmlib.client.Completion;
|
import de.extio.lmlib.client.Completion;
|
||||||
@@ -14,23 +15,31 @@ import de.extio.lmlib.client.Conversation;
|
|||||||
|
|
||||||
public final class AgentContext {
|
public final class AgentContext {
|
||||||
|
|
||||||
private final Map<String, ? extends BaseAgent> agents;
|
// Exposed
|
||||||
|
|
||||||
private final Map<String, List<? extends Object>> context;
|
private final Map<String, List<? extends Object>> context;
|
||||||
|
|
||||||
|
private volatile boolean streaming;
|
||||||
|
|
||||||
|
private volatile boolean error;
|
||||||
|
|
||||||
|
private volatile boolean skipNextCompletion;
|
||||||
|
|
||||||
|
private volatile Conversation conversation;
|
||||||
|
|
||||||
private final AgentRequestStatistic requestStatistic;
|
private final AgentRequestStatistic requestStatistic;
|
||||||
|
|
||||||
private final List<String> graph = Collections.synchronizedList(new ArrayList<>());
|
private final List<String> graph = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
private volatile Conversation conversation;
|
// Internal
|
||||||
|
|
||||||
|
private final Map<String, ? extends BaseAgent> agents;
|
||||||
|
|
||||||
private volatile AgentNext nextAgent;
|
private volatile AgentNext nextAgent;
|
||||||
|
|
||||||
private volatile Completion lastCompletion;
|
private volatile Completion lastCompletion;
|
||||||
|
|
||||||
private volatile boolean error;
|
private Consumer<AgentContext> agentContextUpdateConsumer;
|
||||||
|
|
||||||
private volatile boolean skipNextCompletion;
|
|
||||||
|
|
||||||
public AgentContext(final Collection<? extends BaseAgent> agents) {
|
public AgentContext(final Collection<? extends BaseAgent> agents) {
|
||||||
this.agents = agents.stream().collect(Collectors.toMap(BaseAgent::name, agent -> agent));
|
this.agents = agents.stream().collect(Collectors.toMap(BaseAgent::name, agent -> agent));
|
||||||
@@ -55,8 +64,10 @@ public final class AgentContext {
|
|||||||
this.nextAgent = other.nextAgent;
|
this.nextAgent = other.nextAgent;
|
||||||
this.requestStatistic = other.requestStatistic;
|
this.requestStatistic = other.requestStatistic;
|
||||||
this.lastCompletion = other.lastCompletion;
|
this.lastCompletion = other.lastCompletion;
|
||||||
|
this.agentContextUpdateConsumer = other.agentContextUpdateConsumer;
|
||||||
this.error = other.error;
|
this.error = other.error;
|
||||||
this.skipNextCompletion = other.skipNextCompletion;
|
this.skipNextCompletion = other.skipNextCompletion;
|
||||||
|
this.streaming = other.streaming;
|
||||||
synchronized (other.graph) {
|
synchronized (other.graph) {
|
||||||
this.graph.addAll(other.graph);
|
this.graph.addAll(other.graph);
|
||||||
}
|
}
|
||||||
@@ -111,7 +122,7 @@ public final class AgentContext {
|
|||||||
return this.context;
|
return this.context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, ? extends BaseAgent> getAgents() {
|
Map<String, ? extends BaseAgent> getAgents() {
|
||||||
return this.agents;
|
return this.agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,11 +138,11 @@ public final class AgentContext {
|
|||||||
this.conversation = conversation;
|
this.conversation = conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AgentNext getNextAgent() {
|
AgentNext getNextAgent() {
|
||||||
return this.nextAgent;
|
return this.nextAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setNextAgent(final AgentNext next) {
|
void setNextAgent(final AgentNext next) {
|
||||||
this.nextAgent = next;
|
this.nextAgent = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,11 +150,11 @@ public final class AgentContext {
|
|||||||
return this.graph;
|
return this.graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Completion getLastCompletion() {
|
Completion getLastCompletion() {
|
||||||
return this.lastCompletion;
|
return this.lastCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLastCompletion(final Completion lastCompletion) {
|
void setLastCompletion(final Completion lastCompletion) {
|
||||||
this.lastCompletion = lastCompletion;
|
this.lastCompletion = lastCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,4 +174,19 @@ public final class AgentContext {
|
|||||||
this.skipNextCompletion = skipNextCompletion;
|
this.skipNextCompletion = skipNextCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Consumer<AgentContext> getAgentContextUpdateConsumer() {
|
||||||
|
return agentContextUpdateConsumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAgentContextUpdateConsumer(final Consumer<AgentContext> agentContextUpdateConsumer) {
|
||||||
|
this.agentContextUpdateConsumer = agentContextUpdateConsumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStreaming() {
|
||||||
|
return streaming;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreaming(final boolean streaming) {
|
||||||
|
this.streaming = streaming;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -45,28 +46,41 @@ public class AgentExecutorService implements InitializingBean, DisposableBean {
|
|||||||
this.agentExecutorService.close();
|
this.agentExecutorService.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AgentContext> walkGraph(final String firstAgent, final AgentContext context) {
|
public List<AgentContext> walk(final String firstAgent, final AgentContext context) {
|
||||||
|
return this.walk(firstAgent, context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AgentContext> walk(final String firstAgent, final AgentContext context, final Consumer<AgentContext> agentContextUpdateConsumer) {
|
||||||
final var agent = context.getAgents().get(firstAgent);
|
final var agent = context.getAgents().get(firstAgent);
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
throw new IllegalArgumentException("Agent not found: " + firstAgent);
|
throw new IllegalArgumentException("Agent not found: " + firstAgent);
|
||||||
}
|
}
|
||||||
LOGGER.debug("Starting agent: {}", agent.name());
|
|
||||||
return this.walkGraph(agent, context);
|
return this.walk(agent, context, agentContextUpdateConsumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AgentContext> walkGraph(final BaseAgent agent, final AgentContext context) {
|
public List<AgentContext> walk(final BaseAgent agent, final AgentContext context) {
|
||||||
|
return this.walk(agent, context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AgentContext> walk(final BaseAgent agent, final AgentContext context, final Consumer<AgentContext> agentContextUpdateConsumer) {
|
||||||
LOGGER.debug("Agent: {}", agent.name());
|
LOGGER.debug("Agent: {}", agent.name());
|
||||||
|
|
||||||
Client client = null;
|
Client client = null;
|
||||||
if (agent.agentType() != AgentType.PROCESSING_ONLY) {
|
if (agent.agentType() != AgentType.PROCESSING_ONLY) {
|
||||||
client = this.clientService.getClient(agent.modelCategory());
|
client = this.clientService.getClient(agent.modelCategory());
|
||||||
}
|
}
|
||||||
|
context.setAgentContextUpdateConsumer(agentContextUpdateConsumer);
|
||||||
|
|
||||||
final var branches = agent.execute(client, this.agentExecutorService, context);
|
final var branches = agent.execute(client, this.agentExecutorService, context);
|
||||||
|
|
||||||
final var responses = Collections.synchronizedList(new ArrayList<AgentContext>());
|
final var responses = Collections.synchronizedList(new ArrayList<AgentContext>());
|
||||||
final var branchTasks = new ArrayList<CompletableFuture<?>>(branches.size());
|
final var branchTasks = new ArrayList<CompletableFuture<?>>(branches.size());
|
||||||
for (final var branchContext : branches) {
|
for (final var branchContext : branches) {
|
||||||
|
if (agentContextUpdateConsumer != null) {
|
||||||
|
agentContextUpdateConsumer.accept(branchContext);
|
||||||
|
}
|
||||||
|
|
||||||
if (!branchContext.isError() && branchContext.getNextAgent() != null) {
|
if (!branchContext.isError() && branchContext.getNextAgent() != null) {
|
||||||
final var branchAgent = context.getAgents().get(branchContext.getNextAgent().name());
|
final var branchAgent = context.getAgents().get(branchContext.getNextAgent().name());
|
||||||
if (branchAgent == null) {
|
if (branchAgent == null) {
|
||||||
@@ -75,7 +89,7 @@ public class AgentExecutorService implements InitializingBean, DisposableBean {
|
|||||||
|
|
||||||
branchTasks.add(CompletableFuture.runAsync(() -> {
|
branchTasks.add(CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
responses.addAll(this.walkGraph(branchAgent, branchContext));
|
responses.addAll(this.walk(branchAgent, branchContext, agentContextUpdateConsumer));
|
||||||
}
|
}
|
||||||
catch (final Exception e) {
|
catch (final Exception e) {
|
||||||
LOGGER.error("Error executing agent", e);
|
LOGGER.error("Error executing agent", e);
|
||||||
|
|||||||
@@ -4,4 +4,7 @@ public final record AgentNext(String name, Boolean gradingPassed) {
|
|||||||
|
|
||||||
public static final AgentNext END = new AgentNext(null, null);
|
public static final AgentNext END = new AgentNext(null, null);
|
||||||
|
|
||||||
|
public static AgentNext end(final AgentContext context) {
|
||||||
|
return END;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package de.extio.lmlib.agent;
|
|
||||||
|
|
||||||
import de.extio.lmlib.client.Completion;
|
|
||||||
|
|
||||||
public interface AgentResponseHandler {
|
|
||||||
|
|
||||||
boolean handle(Split split, Completion completion);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.responsehandler.AgentResponseHandler;
|
||||||
|
import de.extio.lmlib.agent.responsehandler.StreamedAgentResponseHandler;
|
||||||
|
import de.extio.lmlib.agent.responsehandler.TextAgentResponseHandler;
|
||||||
import de.extio.lmlib.client.Client;
|
import de.extio.lmlib.client.Client;
|
||||||
import de.extio.lmlib.client.Completion;
|
import de.extio.lmlib.client.Completion;
|
||||||
import de.extio.lmlib.client.Conversation;
|
import de.extio.lmlib.client.Conversation;
|
||||||
@@ -93,15 +96,29 @@ public interface BaseAgent {
|
|||||||
final var conversation = this.setupConversation(split);
|
final var conversation = this.setupConversation(split);
|
||||||
LOGGER.debug("Conversation: {}", conversation);
|
LOGGER.debug("Conversation: {}", conversation);
|
||||||
|
|
||||||
Completion completion = null;
|
|
||||||
boolean parseable = false;
|
boolean parseable = false;
|
||||||
for (int i = 0; i < 2; i++) {
|
for (int i = 0; i < 2; i++) {
|
||||||
|
Completion completion = null;
|
||||||
|
if (split.context().isStreaming()) {
|
||||||
|
if (this.responseHandler() instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
|
||||||
|
streamedResponseHandler.beforeStream(split.context());
|
||||||
|
}
|
||||||
|
completion = client.streamConversation(this.modelCategory(), conversation, chunk -> {
|
||||||
|
if (this.responseHandler() instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
|
||||||
|
if (streamedResponseHandler.handleChunk(chunk, split.context()) && split.context().getAgentContextUpdateConsumer() != null) {
|
||||||
|
split.context().getAgentContextUpdateConsumer().accept(split.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
completion = client.conversation(this.modelCategory(), conversation);
|
completion = client.conversation(this.modelCategory(), conversation);
|
||||||
|
}
|
||||||
split.context().setLastCompletion(completion);
|
split.context().setLastCompletion(completion);
|
||||||
split.context().getRequestStatistic().add(completion);
|
split.context().getRequestStatistic().add(completion);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.responseHandler().handle(split, completion)) {
|
if (!this.responseHandler().handle(completion, split.context())) {
|
||||||
split.context().getGraph().add("⚠");
|
split.context().getGraph().add("⚠");
|
||||||
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response());
|
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response());
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package de.extio.lmlib.agent;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import de.extio.lmlib.client.Completion;
|
|
||||||
|
|
||||||
public class TextAgentResponseHandler implements AgentResponseHandler {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(TextAgentResponseHandler.class);
|
|
||||||
|
|
||||||
private final String key;
|
|
||||||
|
|
||||||
private final Function<String, String> transformer;
|
|
||||||
|
|
||||||
public TextAgentResponseHandler(final String key) {
|
|
||||||
this(key, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TextAgentResponseHandler(final String key, final Function<String, String> transformer) {
|
|
||||||
this.key = key;
|
|
||||||
this.transformer = transformer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(final Split split, final Completion completion) {
|
|
||||||
var response = completion.response();
|
|
||||||
LOGGER.debug("Response: {}", response);
|
|
||||||
if (this.transformer != null) {
|
|
||||||
response = this.transformer.apply(response);
|
|
||||||
}
|
|
||||||
split.context().getContext().put(this.key, List.of(response));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.extio.lmlib.agent;
|
package de.extio.lmlib.agent.responsehandler;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -6,6 +6,7 @@ import java.util.function.Function;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.AgentContext;
|
||||||
import de.extio.lmlib.client.Completion;
|
import de.extio.lmlib.client.Completion;
|
||||||
|
|
||||||
public class AccumulateTextAgentResponseHandler implements AgentResponseHandler {
|
public class AccumulateTextAgentResponseHandler implements AgentResponseHandler {
|
||||||
@@ -25,14 +26,15 @@ public class AccumulateTextAgentResponseHandler implements AgentResponseHandler
|
|||||||
this.transformer = transformer;
|
this.transformer = transformer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(final Split split, final Completion completion) {
|
public boolean handle(final Completion completion, final AgentContext context) {
|
||||||
var response = completion.response();
|
var response = completion.response();
|
||||||
LOGGER.debug("Response: {}", response);
|
LOGGER.debug("Response: {}", response);
|
||||||
if (this.transformer != null) {
|
if (this.transformer != null) {
|
||||||
response = this.transformer.apply(response);
|
response = this.transformer.apply(response);
|
||||||
}
|
}
|
||||||
((ArrayList<Object>) split.context().getContext().computeIfAbsent(this.key, k -> new ArrayList<>())).add(response);
|
((ArrayList<Object>) context.getContext().computeIfAbsent(this.key, k -> new ArrayList<>())).add(response);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.extio.lmlib.agent.responsehandler;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.AgentContext;
|
||||||
|
import de.extio.lmlib.client.Completion;
|
||||||
|
|
||||||
|
public interface AgentResponseHandler {
|
||||||
|
|
||||||
|
boolean handle(Completion completion, AgentContext context);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.extio.lmlib.agent;
|
package de.extio.lmlib.agent.responsehandler;
|
||||||
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -13,7 +13,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.AgentContext;
|
||||||
import de.extio.lmlib.client.Completion;
|
import de.extio.lmlib.client.Completion;
|
||||||
|
import de.extio.lmlib.client.Conversation;
|
||||||
|
|
||||||
public class JsonAgentResponseHandler implements AgentResponseHandler {
|
public class JsonAgentResponseHandler implements AgentResponseHandler {
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ public class JsonAgentResponseHandler implements AgentResponseHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(final Split split, final Completion completion) {
|
public boolean handle(final Completion completion, final AgentContext context) {
|
||||||
final int firstCurly = completion.response().indexOf("{");
|
final int firstCurly = completion.response().indexOf("{");
|
||||||
final int lastCurly = completion.response().lastIndexOf("}");
|
final int lastCurly = completion.response().lastIndexOf("}");
|
||||||
final var response = completion.response().substring(firstCurly, lastCurly + 1);
|
final var response = completion.response().substring(firstCurly, lastCurly + 1);
|
||||||
@@ -53,6 +55,7 @@ public class JsonAgentResponseHandler implements AgentResponseHandler {
|
|||||||
}
|
}
|
||||||
catch (final Exception e) {
|
catch (final Exception e) {
|
||||||
LOGGER.warn("Cannot parse json response: {}", completion.response());
|
LOGGER.warn("Cannot parse json response: {}", completion.response());
|
||||||
|
addJsonResponseErrorPrompt(context);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,18 +66,26 @@ public class JsonAgentResponseHandler implements AgentResponseHandler {
|
|||||||
if (entry.getValue().isArray()) {
|
if (entry.getValue().isArray()) {
|
||||||
final var items = new LinkedHashSet<>();
|
final var items = new LinkedHashSet<>();
|
||||||
entry.getValue().elements().forEachRemaining(item -> items.add(item.asText()));
|
entry.getValue().elements().forEachRemaining(item -> items.add(item.asText()));
|
||||||
split.context().getContext().put(entry.getKey(), List.copyOf(items));
|
context.getContext().put(entry.getKey(), List.copyOf(items));
|
||||||
hasResponse = true;
|
hasResponse = true;
|
||||||
LOGGER.debug("Response: {} {}", entry.getKey(), items);
|
LOGGER.debug("Response: {} {}", entry.getKey(), items);
|
||||||
}
|
}
|
||||||
else if (entry.getValue().isTextual() || entry.getValue().isNumber() || entry.getValue().isBoolean()) {
|
else if (entry.getValue().isTextual() || entry.getValue().isNumber() || entry.getValue().isBoolean()) {
|
||||||
split.context().getContext().put(entry.getKey(), List.of(entry.getValue().asText()));
|
context.setStringValue(entry.getKey(), entry.getValue().asText());
|
||||||
hasResponse = true;
|
hasResponse = true;
|
||||||
LOGGER.debug("Response: {} {}", entry.getKey(), entry.getValue().asText());
|
LOGGER.debug("Response: {} {}", entry.getKey(), entry.getValue().asText());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasResponse) {
|
||||||
|
addJsonResponseErrorPrompt(context);
|
||||||
|
}
|
||||||
return hasResponse;
|
return hasResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addJsonResponseErrorPrompt(final AgentContext context) {
|
||||||
|
final var turn = context.getConversation().getConversation().getLast();
|
||||||
|
context.getConversation().replaceTurn(new Conversation.Turn(turn.type(), turn.text() + "\n\nThe previous response could not be fully processed or validated. Please make sure to format the response in valid JSON syntax with properly escaped characters."));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.extio.lmlib.agent.responsehandler;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.AgentContext;
|
||||||
|
|
||||||
|
public interface StreamedAgentResponseHandler extends AgentResponseHandler {
|
||||||
|
|
||||||
|
void beforeStream(AgentContext context);
|
||||||
|
|
||||||
|
boolean handleChunk(String chunk, AgentContext context);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package de.extio.lmlib.agent.responsehandler;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.AgentContext;
|
||||||
|
import de.extio.lmlib.client.Completion;
|
||||||
|
|
||||||
|
public class TextAgentResponseHandler implements StreamedAgentResponseHandler {
|
||||||
|
|
||||||
|
public static final String UPDATE_KEY = "chunkUpdateKey";
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(TextAgentResponseHandler.class);
|
||||||
|
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
private final String chunkKey;
|
||||||
|
|
||||||
|
private final Function<String, String> transformer;
|
||||||
|
|
||||||
|
private final Function<String, String> chunkTransformer;
|
||||||
|
|
||||||
|
public TextAgentResponseHandler(final String key) {
|
||||||
|
this(key, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextAgentResponseHandler(final String key, final Function<String, String> transformer, final Function<String, String> chunkTransformer) {
|
||||||
|
this.key = key;
|
||||||
|
this.chunkKey = key.concat("_chunk");
|
||||||
|
this.transformer = transformer;
|
||||||
|
this.chunkTransformer = chunkTransformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(final Completion completion, final AgentContext context) {
|
||||||
|
var response = completion.response();
|
||||||
|
LOGGER.debug("Response: {}", response);
|
||||||
|
if (this.transformer != null) {
|
||||||
|
response = this.transformer.apply(response);
|
||||||
|
}
|
||||||
|
context.setStringValue(this.key, response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeStream(final AgentContext context) {
|
||||||
|
context.setStringValue(this.key, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleChunk(final String chunk_, final AgentContext context) {
|
||||||
|
var chunk = chunk_;
|
||||||
|
if (this.chunkTransformer != null) {
|
||||||
|
chunk = this.chunkTransformer.apply(chunk);
|
||||||
|
}
|
||||||
|
context.setStringValue(this.chunkKey, chunk);
|
||||||
|
context.setStringValue(this.key, Objects.requireNonNullElse(context.getStringValue(this.key), "".concat(chunk)));
|
||||||
|
context.setStringValue(UPDATE_KEY, this.key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -20,10 +22,11 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
|||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
|
||||||
|
import de.extio.lmlib.agent.responsehandler.JsonAgentResponseHandler;
|
||||||
|
import de.extio.lmlib.agent.responsehandler.TextAgentResponseHandler;
|
||||||
import de.extio.lmlib.client.ClientService;
|
import de.extio.lmlib.client.ClientService;
|
||||||
import de.extio.lmlib.grader.Grader;
|
import de.extio.lmlib.grader.Grader;
|
||||||
import de.extio.lmlib.profile.ModelCategory;
|
import de.extio.lmlib.profile.ModelCategory;
|
||||||
|
|
||||||
import io.netty.util.internal.ThreadLocalRandom;
|
import io.netty.util.internal.ThreadLocalRandom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +70,7 @@ public class AgentTest {
|
|||||||
"Generate a list of 5 distinct functional features. In scope is business logic, not code mechanics. Provide no preamble and no explanation. Return the response in JSON format with the following field: { \"features\": [\"Feature 1\", \"Feature 2\", ... ] }",
|
"Generate a list of 5 distinct functional features. In scope is business logic, not code mechanics. Provide no preamble and no explanation. Return the response in JSON format with the following field: { \"features\": [\"Feature 1\", \"Feature 2\", ... ] }",
|
||||||
new JsonAgentResponseHandler(),
|
new JsonAgentResponseHandler(),
|
||||||
null,
|
null,
|
||||||
context -> context.getContext().get("features").forEach(feature -> LOGGER.info(feature.toString())),
|
context -> context.getStringValues("features").forEach(feature -> LOGGER.info(feature)),
|
||||||
null,
|
null,
|
||||||
context -> new AgentNext("FeatureAnalyzer", null)),
|
context -> new AgentNext("FeatureAnalyzer", null)),
|
||||||
|
|
||||||
@@ -98,16 +101,16 @@ public class AgentTest {
|
|||||||
{{featureDescription}}""",
|
{{featureDescription}}""",
|
||||||
new TextAgentResponseHandler("blogPost"),
|
new TextAgentResponseHandler("blogPost"),
|
||||||
context -> {
|
context -> {
|
||||||
context.getContext().put("topics", List.of(String.join(", ", context.getContext().get("features").stream().map(String::valueOf).toArray(size -> new String[size]))));
|
context.setStringValues("topics", List.of(String.join(", ", context.getStringValues("features").stream().toArray(size -> new String[size]))));
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
context -> AgentNext.END));
|
AgentNext::end));
|
||||||
|
|
||||||
final var context = new AgentContext(agents);
|
final var context = new AgentContext(agents);
|
||||||
context.getContext().put("code", List.of(Files.readString(Path.of("src/main/java/de/extio/lmlib/agent/BaseAgent.java"))));
|
context.getContext().put("code", List.of(Files.readString(Path.of("src/main/java/de/extio/lmlib/agent/BaseAgent.java"))));
|
||||||
|
|
||||||
final var resultContexts = this.agentExecutor.walkGraph(agents.get("CodeSummarizer"), context);
|
final var resultContexts = this.agentExecutor.walk(agents.get("CodeSummarizer"), context);
|
||||||
|
|
||||||
final var resultContext = resultContexts.getFirst();
|
final var resultContext = resultContexts.getFirst();
|
||||||
LOGGER.info(resultContext.getContext().get("blogPost").toString() + "\n\n" + resultContext.getGraph().toString() + "\n" + resultContext.getRequestStatistic().toString());
|
LOGGER.info(resultContext.getContext().get("blogPost").toString() + "\n\n" + resultContext.getGraph().toString() + "\n" + resultContext.getRequestStatistic().toString());
|
||||||
@@ -117,10 +120,48 @@ public class AgentTest {
|
|||||||
assertEquals(1, resultContext.getContext().get("blogPost").size());
|
assertEquals(1, resultContext.getContext().get("blogPost").size());
|
||||||
assertEquals(8, resultContext.getRequestStatistic().getRequests().get());
|
assertEquals(8, resultContext.getRequestStatistic().getRequests().get());
|
||||||
for (final var feature : resultContext.getContext().get("features")) {
|
for (final var feature : resultContext.getContext().get("features")) {
|
||||||
assertTrue(Grader.assessScoreBinary("Does the following blog post mention the feature " + feature, resultContext.getContext().get("blogPost").getFirst().toString(), this.clientService));
|
assertTrue(Grader.assessScoreBinary("Does the following blog post mention the feature " + feature, resultContext.getStringValue("blogPost"), this.clientService));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void streamedAgenticFlow() throws IOException {
|
||||||
|
final var agents = Map.of(
|
||||||
|
"CodeSummarizer",
|
||||||
|
new Agent("CodeSummarizer",
|
||||||
|
AgentType.START_CONVERSATION,
|
||||||
|
ModelCategory.MEDIUM,
|
||||||
|
"Generate a summary of the following Java source code.",
|
||||||
|
"The source code is:\n{{code}}",
|
||||||
|
new TextAgentResponseHandler("summary"),
|
||||||
|
null,
|
||||||
|
context -> {
|
||||||
|
context.setStringValue("summary", context.getStringValue("summary") + "\npostprocessed");
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
AgentNext::end));
|
||||||
|
|
||||||
|
final var context = new AgentContext(agents);
|
||||||
|
context.setStreaming(true);
|
||||||
|
context.getContext().put("code", List.of(Files.readString(Path.of("src/main/java/de/extio/lmlib/agent/BaseAgent.java"))));
|
||||||
|
|
||||||
|
final var lastStreamedSummary = new AtomicReference<String>();
|
||||||
|
final var resultContexts = this.agentExecutor.walk(agents.get("CodeSummarizer"), context, contextUpdate -> {
|
||||||
|
if ("summary".equals(contextUpdate.getStringValue(TextAgentResponseHandler.UPDATE_KEY))) { // UPDATE_KEY helps you to identify the streamed chunk
|
||||||
|
System.out.print(contextUpdate.getStringValue("summary_chunk")); // Single chunks can be accessed via _chunk suffix, depends on the StreamedAgentResponseHandler implementation
|
||||||
|
lastStreamedSummary.set(contextUpdate.getStringValue("summary")); // The current key value can also be accessed (e.g. concatenated, depends on the StreamedAgentResponseHandler implementation)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(1, resultContexts.size());
|
||||||
|
assertEquals(lastStreamedSummary.get(), resultContexts.getFirst().getStringValue("summary"));
|
||||||
|
assertTrue(resultContexts.getFirst().getStringValue("summary").endsWith("postprocessed"));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
LOGGER.info("### final result");
|
||||||
|
LOGGER.info(resultContexts.getFirst().toString());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void doubleBranch() {
|
void doubleBranch() {
|
||||||
final var agents = Map.of(
|
final var agents = Map.of(
|
||||||
@@ -187,7 +228,7 @@ public class AgentTest {
|
|||||||
|
|
||||||
final var context = new AgentContext(agents);
|
final var context = new AgentContext(agents);
|
||||||
|
|
||||||
final var resultContexts = this.agentExecutor.walkGraph(agents.get("FirstLevel"), context);
|
final var resultContexts = this.agentExecutor.walk(agents.get("FirstLevel"), context);
|
||||||
|
|
||||||
assertEquals(25, resultContexts.size());
|
assertEquals(25, resultContexts.size());
|
||||||
assertTrue(resultContexts.getFirst().getRequestStatistic().getRequests().get() >= 56);
|
assertTrue(resultContexts.getFirst().getRequestStatistic().getRequests().get() >= 56);
|
||||||
|
|||||||
Reference in New Issue
Block a user