Phase 1 delivers the complete durability and storage layer:
- WAL with crash recovery: Append-only journal with BLAKE3 checksums,
fsync guarantees, and proper seek-to-EOF on reopen
- Storage engine: sled-backed KVStore with scan_prefix for range queries
- Content-addressed storage: H:{hash}, V:{hash}, E:{hash} key patterns
- Ingestor: Background worker tailing WAL, writing to KV with 8-byte
aligned record headers for rkyv zero-copy deserialization
- Comprehensive tests: 31 tests covering crash recovery, round-trips,
and multi-cycle durability
New crates: stemedb-wal, stemedb-storage, stemedb-ingest
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
604 lines
17 KiB
Markdown
604 lines
17 KiB
Markdown
# Building a Multi-Agent Chat Room with ADK-Go
|
|
|
|
This guide shows how to create a chat room where multiple AI agents with different perspectives and personalities discuss topics together, providing users with diverse viewpoints.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The multi-agent chat room simulates a discussion panel:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ DISCUSSION MODERATOR │
|
|
│ Orchestrates conversation, ensures balance │
|
|
├────────────┬────────────┬────────────┬────────────┬────────────┤
|
|
│ OPTIMIST │ SKEPTIC │ ANALYST │ CREATIVE │ PRAGMATIC │
|
|
│ │ │ │ │ │
|
|
│ Sees │ Questions │ Examines │ Explores │ Focuses on │
|
|
│ potential │ assumptions│ data/facts │ new ideas │ feasibility│
|
|
└────────────┴────────────┴────────────┴────────────┴────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Project Setup
|
|
|
|
```bash
|
|
mkdir multi-agent-chatroom && cd multi-agent-chatroom
|
|
go mod init chatroom
|
|
go get google.golang.org/adk
|
|
```
|
|
|
|
---
|
|
|
|
## Core Implementation
|
|
|
|
### Agent Personas
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"google.golang.org/adk/agent"
|
|
"google.golang.org/adk/agent/llmagent"
|
|
"google.golang.org/adk/agent/loopagent"
|
|
"google.golang.org/adk/agent/sequentialagent"
|
|
"google.golang.org/adk/cmd/launcher/adk"
|
|
"google.golang.org/adk/cmd/launcher/full"
|
|
"google.golang.org/adk/model/gemini"
|
|
"google.golang.org/adk/server/restapi/services"
|
|
"google.golang.org/adk/tool"
|
|
"google.golang.org/adk/tool/functiontool"
|
|
"google.golang.org/adk/tool/geminitool"
|
|
"google.golang.org/genai"
|
|
)
|
|
|
|
// Persona definitions
|
|
type Persona struct {
|
|
Name string
|
|
Role string
|
|
Instruction string
|
|
Style string
|
|
}
|
|
|
|
var personas = []Persona{
|
|
{
|
|
Name: "optimist",
|
|
Role: "The Optimist",
|
|
Instruction: `You are an optimistic thinker who sees potential and opportunities.
|
|
|
|
PERSPECTIVE:
|
|
- Focus on positive outcomes and possibilities
|
|
- Highlight benefits and advantages
|
|
- Encourage bold thinking and innovation
|
|
- Find silver linings in challenges
|
|
|
|
STYLE:
|
|
- Enthusiastic but not naive
|
|
- Back optimism with reasons
|
|
- Acknowledge risks but emphasize how they can be overcome
|
|
- Use phrases like "The exciting part is...", "This opens up...", "Imagine if..."`,
|
|
},
|
|
{
|
|
Name: "skeptic",
|
|
Role: "The Skeptic",
|
|
Instruction: `You are a constructive skeptic who questions assumptions.
|
|
|
|
PERSPECTIVE:
|
|
- Identify potential problems and risks
|
|
- Question unstated assumptions
|
|
- Play devil's advocate
|
|
- Demand evidence for claims
|
|
|
|
STYLE:
|
|
- Critical but not cynical
|
|
- Ask probing questions
|
|
- Point out what could go wrong
|
|
- Use phrases like "But have we considered...", "What evidence shows...", "The risk is..."`,
|
|
},
|
|
{
|
|
Name: "analyst",
|
|
Role: "The Analyst",
|
|
Instruction: `You are a data-driven analyst who examines facts objectively.
|
|
|
|
PERSPECTIVE:
|
|
- Focus on data, statistics, and evidence
|
|
- Compare with historical examples
|
|
- Identify patterns and trends
|
|
- Quantify when possible
|
|
|
|
STYLE:
|
|
- Objective and methodical
|
|
- Reference specific numbers and studies
|
|
- Present multiple scenarios
|
|
- Use phrases like "The data suggests...", "Historically...", "Studies show..."`,
|
|
},
|
|
{
|
|
Name: "creative",
|
|
Role: "The Creative",
|
|
Instruction: `You are a creative thinker who explores unconventional ideas.
|
|
|
|
PERSPECTIVE:
|
|
- Think outside the box
|
|
- Make unexpected connections
|
|
- Propose innovative solutions
|
|
- Challenge conventional wisdom
|
|
|
|
STYLE:
|
|
- Imaginative and bold
|
|
- Use analogies and metaphors
|
|
- Propose "what if" scenarios
|
|
- Use phrases like "What if we...", "Here's a wild idea...", "Imagine combining..."`,
|
|
},
|
|
{
|
|
Name: "pragmatist",
|
|
Role: "The Pragmatist",
|
|
Instruction: `You are a practical thinker focused on real-world implementation.
|
|
|
|
PERSPECTIVE:
|
|
- Focus on feasibility and execution
|
|
- Consider resources, time, and constraints
|
|
- Identify concrete next steps
|
|
- Balance idealism with reality
|
|
|
|
STYLE:
|
|
- Grounded and action-oriented
|
|
- Ask "how" questions
|
|
- Propose specific steps
|
|
- Use phrases like "Practically speaking...", "To make this work...", "The first step would be..."`,
|
|
},
|
|
}
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
|
|
model, err := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{
|
|
APIKey: os.Getenv("GOOGLE_API_KEY"),
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
chatroom, err := buildChatRoom(model)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
l := full.NewLauncher()
|
|
cfg := &adk.Config{AgentLoader: services.NewSingleAgentLoader(chatroom)}
|
|
if err := l.Execute(ctx, cfg, os.Args[1:]); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Building the Chat Room
|
|
|
|
```go
|
|
func buildChatRoom(model *gemini.Model) (agent.Agent, error) {
|
|
// Create tools for agent interaction
|
|
speakTool, err := functiontool.New(
|
|
functiontool.Config{
|
|
Name: "speak",
|
|
Description: "Add your contribution to the discussion",
|
|
},
|
|
speak,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
respondToTool, err := functiontool.New(
|
|
functiontool.Config{
|
|
Name: "respond_to",
|
|
Description: "Respond directly to another participant's point",
|
|
},
|
|
respondTo,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create participant agents
|
|
var participants []agent.Agent
|
|
for _, p := range personas {
|
|
participant, err := createParticipant(model, p, []tool.Tool{speakTool, respondToTool})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
participants = append(participants, participant)
|
|
}
|
|
|
|
// Create moderator that orchestrates the discussion
|
|
moderator, err := createModerator(model, participants)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return moderator, nil
|
|
}
|
|
|
|
func createParticipant(model *gemini.Model, p Persona, tools []tool.Tool) (agent.Agent, error) {
|
|
instruction := fmt.Sprintf(`You are %s in a multi-perspective discussion panel.
|
|
|
|
%s
|
|
|
|
DISCUSSION CONTEXT:
|
|
Topic: {discussion_topic}
|
|
|
|
Previous contributions:
|
|
{discussion_history}
|
|
|
|
YOUR TASK:
|
|
Provide your unique perspective on the topic. Consider what others have said and either:
|
|
1. Build on their points with your perspective
|
|
2. Respectfully challenge or question their views
|
|
3. Introduce a new angle they haven't considered
|
|
|
|
Keep your response focused (2-4 sentences). Use the speak tool to contribute.
|
|
If directly responding to someone, use respond_to tool.`, p.Role, p.Instruction)
|
|
|
|
return llmagent.New(llmagent.Config{
|
|
Name: p.Name,
|
|
Model: model,
|
|
Description: fmt.Sprintf("%s - %s", p.Role, p.Style),
|
|
Instruction: instruction,
|
|
Tools: tools,
|
|
OutputKey: fmt.Sprintf("%s_contribution", p.Name),
|
|
})
|
|
}
|
|
|
|
func createModerator(model *gemini.Model, participants []agent.Agent) (agent.Agent, error) {
|
|
// Opening agent - frames the discussion
|
|
opener, err := llmagent.New(llmagent.Config{
|
|
Name: "opener",
|
|
Model: model,
|
|
Instruction: `You are a discussion moderator. Frame the topic for the panel.
|
|
|
|
Topic from user: {user_input}
|
|
|
|
Create a brief, engaging introduction that:
|
|
1. Restates the topic clearly
|
|
2. Highlights why it's worth discussing
|
|
3. Poses 1-2 key questions for the panel
|
|
|
|
Keep it to 2-3 sentences. Be neutral and inviting.`,
|
|
OutputKey: "discussion_topic",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Discussion round - all participants contribute
|
|
discussionRound, err := createDiscussionRound(model, participants)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Synthesizer - summarizes and identifies key insights
|
|
synthesizer, err := llmagent.New(llmagent.Config{
|
|
Name: "synthesizer",
|
|
Model: model,
|
|
Instruction: `You are summarizing a multi-perspective discussion.
|
|
|
|
TOPIC: {discussion_topic}
|
|
|
|
CONTRIBUTIONS:
|
|
{discussion_history}
|
|
|
|
Create a balanced summary that:
|
|
1. Highlights the key points from each perspective
|
|
2. Notes areas of agreement and disagreement
|
|
3. Identifies the most compelling insights
|
|
4. Suggests questions for further exploration
|
|
|
|
Be fair to all viewpoints. Present the synthesis conversationally.`,
|
|
OutputKey: "synthesis",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build full pipeline: Open → Discuss (loop) → Synthesize
|
|
pipeline, err := sequentialagent.New(sequentialagent.Config{
|
|
AgentConfig: agent.Config{
|
|
Name: "discussion_chatroom",
|
|
Description: "Multi-perspective discussion panel",
|
|
SubAgents: []agent.Agent{opener, discussionRound, synthesizer},
|
|
},
|
|
})
|
|
|
|
return pipeline, err
|
|
}
|
|
|
|
func createDiscussionRound(model *gemini.Model, participants []agent.Agent) (agent.Agent, error) {
|
|
// History tracker - runs before each participant
|
|
historyUpdater, err := llmagent.New(llmagent.Config{
|
|
Name: "history_updater",
|
|
Model: model,
|
|
Instruction: `Update the discussion history with recent contributions.
|
|
|
|
Current history:
|
|
{discussion_history}
|
|
|
|
New contributions to add:
|
|
- Optimist: {optimist_contribution}
|
|
- Skeptic: {skeptic_contribution}
|
|
- Analyst: {analyst_contribution}
|
|
- Creative: {creative_contribution}
|
|
- Pragmatist: {pragmatist_contribution}
|
|
|
|
Output the combined, formatted discussion history.`,
|
|
OutputKey: "discussion_history",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build round: each participant speaks, then history updates
|
|
roundAgents := append(participants, historyUpdater)
|
|
|
|
// Create a sequential round
|
|
round, err := sequentialagent.New(sequentialagent.Config{
|
|
AgentConfig: agent.Config{
|
|
Name: "discussion_round",
|
|
SubAgents: roundAgents,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Loop for multiple rounds of discussion
|
|
multiRound, err := loopagent.New(loopagent.Config{
|
|
AgentConfig: agent.Config{
|
|
Name: "multi_round_discussion",
|
|
SubAgents: []agent.Agent{round},
|
|
},
|
|
MaxIterations: 2, // Two rounds of discussion
|
|
})
|
|
|
|
return multiRound, err
|
|
}
|
|
```
|
|
|
|
### Speaking Tools
|
|
|
|
```go
|
|
type SpeakInput struct {
|
|
Message string `json:"message" jsonschema:"Your contribution to the discussion"`
|
|
}
|
|
|
|
type SpeakOutput struct {
|
|
Delivered bool `json:"delivered"`
|
|
Speaker string `json:"speaker"`
|
|
}
|
|
|
|
func speak(ctx tool.Context, input SpeakInput) SpeakOutput {
|
|
// Get the current agent's name from context
|
|
agentName := "participant"
|
|
|
|
// Store in discussion log
|
|
state := ctx.Session().State()
|
|
var log []string
|
|
if existing := state.Get("temp:discussion_log"); existing != nil {
|
|
if l, ok := existing.([]string); ok {
|
|
log = l
|
|
}
|
|
}
|
|
|
|
entry := fmt.Sprintf("[%s]: %s", strings.ToUpper(agentName), input.Message)
|
|
log = append(log, entry)
|
|
state.Set("temp:discussion_log", log)
|
|
|
|
return SpeakOutput{Delivered: true, Speaker: agentName}
|
|
}
|
|
|
|
type RespondToInput struct {
|
|
Target string `json:"target" jsonschema:"Who you're responding to (optimist, skeptic, analyst, creative, pragmatist)"`
|
|
Message string `json:"message" jsonschema:"Your response"`
|
|
}
|
|
|
|
type RespondToOutput struct {
|
|
Delivered bool `json:"delivered"`
|
|
}
|
|
|
|
func respondTo(ctx tool.Context, input RespondToInput) RespondToOutput {
|
|
state := ctx.Session().State()
|
|
var log []string
|
|
if existing := state.Get("temp:discussion_log"); existing != nil {
|
|
if l, ok := existing.([]string); ok {
|
|
log = l
|
|
}
|
|
}
|
|
|
|
entry := fmt.Sprintf("[RESPONSE to %s]: %s", strings.ToUpper(input.Target), input.Message)
|
|
log = append(log, entry)
|
|
state.Set("temp:discussion_log", log)
|
|
|
|
return RespondToOutput{Delivered: true}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Alternative: Real-Time Debate Format
|
|
|
|
For more dynamic back-and-forth:
|
|
|
|
```go
|
|
func buildDebateRoom(model *gemini.Model) (agent.Agent, error) {
|
|
// Pro side
|
|
proDebater, _ := llmagent.New(llmagent.Config{
|
|
Name: "pro_debater",
|
|
Model: model,
|
|
Instruction: `You are arguing IN FAVOR of the proposition.
|
|
|
|
Proposition: {proposition}
|
|
Opponent's last argument: {con_argument}
|
|
|
|
Build a compelling case FOR the proposition. If responding to opponent:
|
|
- Acknowledge their point
|
|
- Counter with evidence or logic
|
|
- Strengthen your position
|
|
|
|
Be persuasive but fair. 2-3 sentences max.`,
|
|
OutputKey: "pro_argument",
|
|
})
|
|
|
|
// Con side
|
|
conDebater, _ := llmagent.New(llmagent.Config{
|
|
Name: "con_debater",
|
|
Model: model,
|
|
Instruction: `You are arguing AGAINST the proposition.
|
|
|
|
Proposition: {proposition}
|
|
Opponent's last argument: {pro_argument}
|
|
|
|
Build a compelling case AGAINST the proposition. If responding to opponent:
|
|
- Acknowledge their point
|
|
- Counter with evidence or logic
|
|
- Strengthen your position
|
|
|
|
Be persuasive but fair. 2-3 sentences max.`,
|
|
OutputKey: "con_argument",
|
|
})
|
|
|
|
// Debate round
|
|
round, _ := sequentialagent.New(sequentialagent.Config{
|
|
AgentConfig: agent.Config{
|
|
Name: "debate_round",
|
|
SubAgents: []agent.Agent{proDebater, conDebater},
|
|
},
|
|
})
|
|
|
|
// Multiple rounds
|
|
debate, _ := loopagent.New(loopagent.Config{
|
|
AgentConfig: agent.Config{
|
|
Name: "debate",
|
|
SubAgents: []agent.Agent{round},
|
|
},
|
|
MaxIterations: 3,
|
|
})
|
|
|
|
// Judge
|
|
judge, _ := llmagent.New(llmagent.Config{
|
|
Name: "judge",
|
|
Model: model,
|
|
Instruction: `You are an impartial debate judge.
|
|
|
|
Proposition: {proposition}
|
|
|
|
PRO arguments: {pro_argument}
|
|
CON arguments: {con_argument}
|
|
|
|
Evaluate the debate:
|
|
1. Strongest argument from each side
|
|
2. Key points of clash
|
|
3. Which side was more persuasive and why
|
|
4. What was missing from both sides
|
|
|
|
Be balanced and specific in your assessment.`,
|
|
OutputKey: "verdict",
|
|
})
|
|
|
|
// Full debate pipeline
|
|
return sequentialagent.New(sequentialagent.Config{
|
|
AgentConfig: agent.Config{
|
|
Name: "debate_room",
|
|
SubAgents: []agent.Agent{debate, judge},
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Adding Research Capability
|
|
|
|
Let agents search for evidence during discussion:
|
|
|
|
```go
|
|
func createResearchParticipant(model *gemini.Model, p Persona) (agent.Agent, error) {
|
|
instruction := fmt.Sprintf(`You are %s in a research-backed discussion.
|
|
|
|
%s
|
|
|
|
Topic: {discussion_topic}
|
|
Previous points: {discussion_history}
|
|
|
|
You have access to web search. Use it to:
|
|
- Find evidence supporting your perspective
|
|
- Discover relevant statistics or studies
|
|
- Check facts before stating them
|
|
|
|
Provide your perspective backed by what you find. Cite sources.`, p.Role, p.Instruction)
|
|
|
|
return llmagent.New(llmagent.Config{
|
|
Name: p.Name,
|
|
Model: model,
|
|
Description: p.Role,
|
|
Instruction: instruction,
|
|
Tools: []tool.Tool{
|
|
geminitool.GoogleSearch{},
|
|
},
|
|
OutputKey: fmt.Sprintf("%s_contribution", p.Name),
|
|
})
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Running the Chat Room
|
|
|
|
```bash
|
|
# Console mode
|
|
go run main.go
|
|
|
|
# Example prompts:
|
|
# > Should companies require employees to return to the office?
|
|
# > Is social media beneficial or harmful to society?
|
|
# > Should AI-generated art be considered "real" art?
|
|
# > What's the best approach to addressing climate change?
|
|
|
|
# Web UI mode
|
|
go run main.go web api webui
|
|
```
|
|
|
|
---
|
|
|
|
## Sample Output
|
|
|
|
**User**: Should universities require students to take a gap year before college?
|
|
|
|
**Moderator**: _Today we're discussing whether universities should mandate a gap year before enrollment. This touches on questions of maturity, experience, and educational readiness. What perspectives should we consider?_
|
|
|
|
**Optimist**: _A gap year could be transformative! Students return with real-world experience, clearer goals, and renewed motivation. Countries like Denmark and Israel show this works—their students often outperform peers who went straight to university._
|
|
|
|
**Skeptic**: _But who can afford a year without income or education? This could widen inequality—wealthy students travel while others fall behind. And what about momentum? Many students who delay never return._
|
|
|
|
**Analyst**: _The data is mixed. A 2015 study showed gap year students had 0.1-0.4 higher GPAs, but selection bias is significant—motivated students choose gap years. Only 3% of American students take them compared to 25% in the UK._
|
|
|
|
**Creative**: _What if we reimagined the gap year entirely? Instead of a break, it could be a structured "first year" of hands-on learning—apprenticeships, service, exploration—that counts toward the degree._
|
|
|
|
**Pragmatist**: _Making it mandatory seems unworkable. Better approach: make gap years easier and more supported. Deferred admission, structured programs, financial aid that covers gap activities._
|
|
|
|
**Synthesis**: _The panel found common ground on gap years' potential value but diverged on implementation. The Skeptic raised valid equity concerns that the Pragmatist tried to address. The Creative's hybrid model generated interest. Key insight: the question isn't whether gap years help, but how to make them accessible and productive for all students._
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Balance perspectives**: Ensure no single viewpoint dominates
|
|
2. **Keep contributions focused**: 2-4 sentences per turn prevents rambling
|
|
3. **Encourage engagement**: Agents should reference each other's points
|
|
4. **Vary the format**: Debate, panel, roundtable each serve different purposes
|
|
5. **Let users guide depth**: Allow follow-up questions to explore specific angles
|