Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 41 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,69 @@
# Multi-Agent System

A sample application demonstrating how to build a multi-agent system using Akka and an LLM model.
A sample application demonstrating how to build a multi-agent system using Akka and an AI model. An Autonomous Agent coordinator delegates dynamically to specialized worker agents, with the model deciding which workers to consult for each request.

## Overview

This project illustrates an Agentic workflow for a multi-agent system using Akka. The system:
This project illustrates a multi-agent system built around the Autonomous Agent component. The system:

- Processes user queries and determines the appropriate agents to handle the request
- Plans the execution order of the selected agents
- Executes the plan by interacting with the agents in sequence
- Summarizes the results from all agents into a final response
- Receives an activity-suggestion request for a user
- Personalizes the request with any stored user preferences
- Hands the request to an Autonomous Agent coordinator that delegates dynamically to a weather agent and an activity agent
- Returns a typed answer once the coordinator's task completes
- Evaluates each completed task with a custom LLM-as-judge and the built-in toxicity evaluator; verdicts are logged and surfaced through metrics and traces

### Akka components

This sample leverages specific Akka components:

- **Workflow**: Manages the user query process, handling the sequential steps of agent selection, plan creation, execution, and summarization.
- **EventSourced Entity**: Maintains the session memory, storing the sequence of interactions between the user and the system.
- **HTTP Endpoint**: Serves the application endpoints for interacting with the multi-agent system (`/activities`).
- **Autonomous Agent (`ActivityCoordinator`)**: Accepts a `SuggestActivities` task and declares `Delegation` to the worker agents. The runtime drives its decision loop until the task completes.
- **Agent (`WeatherAgent`, `ActivityAgent`)**: Plain request-based agents that the coordinator delegates to. Each exposes a `query` method whose parameter type is serialized into a tool schema for the coordinator's model. `WeatherAgent.query` takes a `String`; `ActivityAgent.query` takes an `AgentRequest` record so it can look up the user's preferences with the userId.
- **Agent (`EvaluatorAgent`)**: An LLM-as-judge agent that evaluates the coordinator's answer against the original (preference-aware) request.
- **EventSourced Entity (`PreferencesEntity`)**: Holds the user's preferences.
- **Consumer (`EvaluationConsumer`)**: Subscribes to the runtime's task entity events. On task completion it runs `EvaluatorAgent` and the built-in `ToxicityEvaluator`, logging the verdicts.
- **HTTP Endpoint (`ActivityEndpoint`)**: Exposes the `/activities` and `/preferences` routes.

### Other

- **LLM model**: The system uses an LLM model to assist in agent selection, plan creation, and summarization. The LLM is integrated with tools specific to each agent's domain of expertise.
- **AI model**: The coordinator uses an AI model to choose which workers to consult, what to ask them, and how to synthesize the final answer. The evaluator uses a model to judge each answer.

## Example flow

```mermaid
sequenceDiagram
participant User
participant HTTPEndpoint as HTTP Endpoint
participant Workflow as Akka Workflow
participant Selector as Agent Selector
participant Planner as Execution Planner
participant Task as Task Entity
participant Coordinator as ActivityCoordinator
participant WeatherAgent as Weather Agent
participant ActivityAgent as Activity Agent
participant Summarizer as Summarizer
participant EvalConsumer as EvaluationConsumer

User->>HTTPEndpoint: "I do not work today. I am in Madrid. What should I do? Beware of the weather"
HTTPEndpoint->>Workflow: Create new workflow instance
Note over Workflow: Initialize multi-agent query process
HTTPEndpoint-->>User: Return activity id for follow up
User->>HTTPEndpoint: POST /activities/alice "I am in Madrid..."
HTTPEndpoint->>Coordinator: runSingleTask(SUGGEST_ACTIVITIES.instructions(message))
Coordinator-->>HTTPEndpoint: taskId
HTTPEndpoint-->>User: 201 Created (taskId in body + Location)

Workflow->>Selector: Select appropriate agents
Selector-->>Workflow: Return selected agents (e.g., WeatherAgent, ActivityAgent)
Note over Coordinator: Model decides which workers to delegate to

Workflow->>Planner: Create execution plan
Planner-->>Workflow: Return ordered plan (e.g., WeatherAgent -> ActivityAgent)
Coordinator->>WeatherAgent: delegate "What is the weather in Madrid?"
WeatherAgent-->>Coordinator: "Rainy in Madrid"

loop Execute plan
Workflow->>WeatherAgent: "What is the weather in Madrid?"
WeatherAgent-->>Workflow: "Rainy in Madrid"
Coordinator->>ActivityAgent: delegate "Suggest activities for a rainy day in Madrid"
ActivityAgent-->>Coordinator: "Visit the Prado Museum or enjoy local cafes"

Workflow->>ActivityAgent: "Suggest activities for a day in Madrid"
ActivityAgent-->>Workflow: "Visit the Prado Museum or enjoy local cafes"
end
Note over Coordinator: Synthesize final answer
Coordinator->>Task: complete with result

Workflow->>Summarizer: Summarize responses
Summarizer-->>Workflow: "The weather in Madrid is rainy today, so you might want to explore indoor attractions..."
Task-->>EvalConsumer: TaskCompleted event
EvalConsumer->>EvalConsumer: run EvaluatorAgent + ToxicityEvaluator (log verdicts)

User->>HTTPEndpoint: Send query by session id
HTTPEndpoint->>Workflow: Request result from workflow
Workflow-->>HTTPEndpoint: Return result
HTTPEndpoint-->>User: Deliver response
User->>HTTPEndpoint: GET /activities/alice/{taskId}
HTTPEndpoint->>Task: get snapshot
HTTPEndpoint-->>User: typed answer
```

Note that the agents selected depend on the user's query and the available agents. Thus, the flow varies arbitrarily depending on the Planner agent reasoning and the steps selected to respond to the query. Also, another important aspect is that all agents share the same context and thus, when answering a question can take into consideration the context of the previous interactions with other agents.
The set of workers the coordinator consults depends on the user's query and the worker descriptions. Different requests will lead the model to consult only one worker, both, or even loop back for follow-ups.

## Running the application

Expand Down Expand Up @@ -118,24 +117,18 @@ mvn compile exec:java

With the application running, you can test the system using the following endpoints:

* Start a new session:
* Start a new task:
```shell
curl -i -XPOST --location "http://localhost:9000/activities/alice/1" \
curl -i -XPOST --location "http://localhost:9000/activities/alice" \
--header "Content-Type: application/json" \
--data '{"message": "I do not work tomorrow. I am in Madrid. What should I do? Beware of the weather"}'
```

The system will process the query, select the appropriate agents, and return a response.
The endpoint personalizes the request with any stored preferences and calls `runSingleTask` on the coordinator. The response body and `Location` header carry the task id.

* Retrieve the response for a specific session:
* Retrieve the response for a specific task:
```shell
curl -i -XGET --location "http://localhost:9000/activities/alice/1"
```

You can also retrieve for all previous suggestions for a user:

```shell
curl -i -XGET --location "http://localhost:9000/activities/alice"
curl -i -XGET --location "http://localhost:9000/activities/alice/{taskId}"
```

Preferences can be added with:
Expand All @@ -149,6 +142,8 @@ curl -i localhost:9000/preferences/alice \
}'
```

Preferences are read by the endpoint on each new request and inlined into the task instructions, so subsequent suggestions take them into account. The `EvaluationConsumer` runs on every completed task; inspect the service logs to see its verdicts.

## Deployment

You can use the [Akka Console](https://console.akka.io) to create a project and deploy this service.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>io.akka</groupId>
<artifactId>akka-javasdk-parent</artifactId>
<version>3.5.19</version>
<version>3.6.0</version>
</parent>

<groupId>io.akka.ai</groupId>
Expand Down
104 changes: 18 additions & 86 deletions src/main/java/demo/multiagent/api/ActivityEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@
import akka.javasdk.annotations.http.Post;
import akka.javasdk.client.ComponentClient;
import akka.javasdk.http.HttpResponses;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import demo.multiagent.application.ActivityView;
import demo.multiagent.application.AgentTeamWorkflow;
import demo.multiagent.application.AgentTeamWorkflow.AgentTeamNotification;
import demo.multiagent.application.ActivityCoordinator;
import demo.multiagent.application.ActivityTasks;
import demo.multiagent.application.PreferencesEntity;
import java.util.List;
import java.util.UUID;

// Opened up for access from the public internet to make the service easy to try out.
Expand All @@ -27,57 +23,33 @@ public record Request(String message) {}

public record AddPreference(String preference) {}

public record ActivitiesList(List<Suggestion> suggestions) {
static ActivitiesList fromView(ActivityView.ActivityEntries entries) {
return new ActivitiesList(
entries.entries().stream().map(Suggestion::fromView).toList()
);
}
}

public record Suggestion(String userQuestion, String answer) {
static Suggestion fromView(ActivityView.ActivityEntry entry) {
return new Suggestion(entry.userQuestion(), entry.finalAnswer());
}
}

private final ComponentClient componentClient;

public ActivityEndpoint(ComponentClient componentClient) {
this.componentClient = componentClient;
}

@Post("/activities/{userId}/{sessionId}")
public HttpResponse suggestActivities(String userId, String sessionId, Request request) {
var res = componentClient
.forWorkflow(sessionId)
.method(AgentTeamWorkflow::start)
.invoke(new AgentTeamWorkflow.Request(userId, request.message()));

return HttpResponses.created(res, "/activities/" + userId + "/" + sessionId);
}
@Post("/activities/{userId}")
public HttpResponse suggestActivities(String userId, Request request) {
var instructions = "User: " + userId + "\n\n" + request.message(); // <1>

@Get("/activities/{userId}/{sessionId}")
public HttpResponse getAnswer(String userId, String sessionId) {
var res = componentClient
.forWorkflow(sessionId)
.method(AgentTeamWorkflow::getAnswer)
.invoke();
var taskId = componentClient
.forAutonomousAgent(ActivityCoordinator.class, UUID.randomUUID().toString())
.runSingleTask(ActivityTasks.SUGGEST_ACTIVITIES.instructions(instructions)); // <2>

if (res.isEmpty()) return HttpResponses.notFound(
"Answer for '" + sessionId + "' not available (yet)"
);
else return HttpResponses.ok(res);
return HttpResponses.created(taskId, "/activities/" + userId + "/" + taskId);
}

@Get("/activities/{userId}")
public ActivitiesList listActivities(String userId) {
var viewResult = componentClient
.forView()
.method(ActivityView::getActivities)
.invoke(userId);
@Get("/activities/{userId}/{taskId}")
public HttpResponse getAnswer(String userId, String taskId) {
var snapshot = componentClient.forTask(taskId).get(ActivityTasks.SUGGEST_ACTIVITIES); // <3>

return ActivitiesList.fromView(viewResult);
return snapshot
.result()
.<HttpResponse>map(HttpResponses::ok)
.orElseGet(
() -> HttpResponses.notFound("Answer for '" + taskId + "' not available (yet)")
);
}

@Post("/preferences/{userId}")
Expand All @@ -89,44 +61,4 @@ public HttpResponse addPreference(String userId, AddPreference request) {

return HttpResponses.created();
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes(
{
@JsonSubTypes.Type(value = UpdateEvent.StatusUpdate.class, name = "S"),
@JsonSubTypes.Type(value = UpdateEvent.LlmResponseStart.class, name = "LS"),
@JsonSubTypes.Type(value = UpdateEvent.LlmResponseDelta.class, name = "LD"),
@JsonSubTypes.Type(value = UpdateEvent.LlmResponseEnd.class, name = "LE"),
}
)
public sealed interface UpdateEvent {
record StatusUpdate(String msg) implements UpdateEvent {}

record LlmResponseStart() implements UpdateEvent {}

record LlmResponseDelta(String response) implements UpdateEvent {}

record LlmResponseEnd() implements UpdateEvent {}
}

@Get("/updates/{sessionId}")
public HttpResponse updates(String sessionId) {
return HttpResponses.serverSentEvents(
componentClient
.forWorkflow(sessionId)
.notificationStream(AgentTeamWorkflow::updates)
.source()
.map(notification ->
switch (notification) {
case AgentTeamNotification.LlmResponseDelta llmResponseDelta -> new UpdateEvent.LlmResponseDelta(
llmResponseDelta.response()
);
case AgentTeamNotification.LlmResponseEnd __ -> new UpdateEvent.LlmResponseEnd();
case AgentTeamNotification.LlmResponseStart __ -> new UpdateEvent.LlmResponseStart();
case AgentTeamNotification.StatusUpdate statusUpdate -> new UpdateEvent.StatusUpdate(
statusUpdate.msg()
);
})
);
}
}
21 changes: 0 additions & 21 deletions src/main/java/demo/multiagent/api/UiEndpoint.java

This file was deleted.

14 changes: 8 additions & 6 deletions src/main/java/demo/multiagent/application/ActivityAgent.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
package demo.multiagent.application;

import akka.javasdk.agent.Agent;
import akka.javasdk.annotations.AgentRole;
import akka.javasdk.annotations.Component;
import akka.javasdk.client.ComponentClient;
import demo.multiagent.domain.AgentRequest;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
id = "activity-agent",
name = "Activity Agent",
description = """
An agent that suggests activities in the real world. Like for example,
a team building activity, sports, an indoor or outdoor game,
board games, a city trip, etc.
An agent that suggests activities in the real world. Like for example, \
a team building activity, sports, an indoor or outdoor game, \
board games, a city trip, etc.\
"""
)
@AgentRole("worker")
public class ActivityAgent extends Agent {


private static final String SYSTEM_MESSAGE =
"""
You are an activity agent. Your job is to suggest activities in the real world.
Expand All @@ -32,13 +31,16 @@ public class ActivityAgent extends Agent {
Start the error response with ERROR.
""".stripIndent();

private static final Logger logger = LoggerFactory.getLogger(ActivityAgent.class);

private final ComponentClient componentClient;

public ActivityAgent(ComponentClient componentClient) {
this.componentClient = componentClient;
}

public Effect<String> query(AgentRequest request) {
logger.info("Invoked for user [{}] with: {}", request.userId(), request.message());
var allPreferences = componentClient
.forEventSourcedEntity(request.userId())
.method(PreferencesEntity::getPreferences)
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/demo/multiagent/application/ActivityCoordinator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package demo.multiagent.application;

import akka.javasdk.agent.autonomous.AgentDefinition;
import akka.javasdk.agent.autonomous.AutonomousAgent;
import akka.javasdk.agent.autonomous.capability.Delegation;
import akka.javasdk.agent.autonomous.capability.TaskAcceptance;
import akka.javasdk.annotations.Component;

@Component(
id = "activity-coordinator",
description = """
Coordinates worker agents to suggest real-world activities for a user. \
Decides whether to consult the weather agent, the activity agent, or both, \
and synthesizes their results into a single suggestion.\
"""
)
public class ActivityCoordinator extends AutonomousAgent { // <1>

@Override
public AgentDefinition definition() {
return define()
.instructions(
"""
When delegating to the activity agent, include the userId from the task header \
(the "User: <userId>" line) in the request so the agent can fetch the user's \
preferences.\
"""
) // <2>
.capability(TaskAcceptance.of(ActivityTasks.SUGGEST_ACTIVITIES).maxIterationsPerTask(5)) // <3>
.capability(Delegation.to(WeatherAgent.class, ActivityAgent.class)); // <4>
}
}
Loading
Loading