Like many in the tech sphere, I’ve been captivated by the rapid advancements in Artificial Intelligence, particularly Large Language Models. While interacting with a single LLM is fascinating, the true power, I believe, lies in enabling multiple AIs to collaborate, compete, and interact autonomously..
This belief led me to build something ambitious: a fully autonomous, local-only AI Debating Society — a system where agents can engage in structured debates, grounded in real knowledge, without ever calling the cloud.
In Part 1, I built a minimal debate system using Ollama.
In Part 2, I introduced a local RAG pipeline using LangChain, ChromaDB, and Ollama.
Now in Part 3, the system gets a major upgrade:
Full Code:Here
A complete Streamlit-based User Interface for better usability.
Strong backend/frontend separation.
Threading to support real-time interactions without UI freezing.
UI panels for agents, judge, history, and real-time updates.
Let me take you through this final stage of the build from backend modularity to frontend interactivity.
Phase 1: Laying the Foundational Bricks — The Core Architecture
Every robust system starts with clean modular design. I split the system into multiple Python files for clarity and flexibility:
-config.py: Central settings for everything — debate topics, LLM models, agent types, RAG parameters, prompts, and more.
-debate_state.py: Tracks debate history, stage, and stores all interactions in a clean data structure.
-rag_pipeline.py: Manages document ingestion, chunking, embedding, and similarity search using ChromaDB.
-agents.py: Contains the logic for all AI agents — their roles, prompt handling, RAG integration, and interaction rules.
-app.py: The Streamlit frontend with state handling, UI rendering, and backend triggering via buttons and session state.
config.py (The Project’s DNA):
# config.py
# --- Debate Configuration ---
DEBATE_TOPIC = "Should autonomous vehicles be implemented on a large scale within the next decade?"
NUMBER_OF_REBUTTAL_ROUNDS = 2 # Number of times each side gets to respond after opening statements
# --- Ollama Model Configuration ---
# Using dolphin-phi:latest for debate agents and summarization
DEFAULT_MODEL = 'dolphin-phi:latest'
SUMMARY_MODEL = DEFAULT_MODEL
# --- RAG Configuration ---
# Directory containing your PDF documents
KB_DIRECTORY = "./knowledge" # Create a folder named 'knowledge' in your project directory and put PDFs there
# Path where the ChromaDB vector store will be saved/loaded
VECTOR_STORE_PATH = "./chroma_db"
# Ollama model to use for creating embeddings (e.g., nomic-embed-text)
EMBEDDING_MODEL = 'nomic-embed-text'
# Chunk size and overlap for splitting documents
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
# Flag to indicate if RAG should be enabled
ENABLE_RAG = True
RETRIEVER_K = 3 # Number of relevant documents to retrieve for RAG
# --- Agent Configuration ---
# --- Agent Configuration ---
# AGENTS_CONFIG will be built dynamically by the UI, but we keep this
# placeholder structure or a base if needed.
# Let's define the POOL of possible names and photo paths instead.
SOUTH_INDIAN_NAMES = [
"Ramesh", "Sita", "Krishna", "Lakshmi", "Raj", "Meena",
"Arjun", "Priya", "Vikram", "Anjali", "Gopal", "Shanti",
"Mohan", "Lalita", "Anand", "Radha", "Vivek", "Kavitha",
"Sanjay", "Divya"
]
# Add placeholder image paths for your agents (make sure these files exist in ./images)
# You need at least as many photos as the maximum number of agents you might field (e.g., 5 pairs + 1 judge = 11)
AGENT_PHOTO_PATHS = [
"D:\project\mulit agent\Images\5b658e1c-adec-4304-be53-fcad1c34d816.jpg",
"D:\project\mulit agent\Images\259d5c05-4f36-475a-877e-5bbd277e5127.jpg",
"D:\project\mulit agent\Images\85a26cbf-d583-47c8-b7da-67cf739b43ab.jpg",
"D:\project\mulit agent\Images\bae93a3f-099c-46c8-ad4f-77777d6a195c.jpg",
"D:\project\mulit agent\Images\cf245133-496d-4278-a1d6-ef88a0e4d6e0.jpg",
"D:\project\mulit agent\Images\0cced509-f35c-4f0f-88ce-1cc474a8234c.jpg",
"D:\project\mulit agent\Images\ec22cc1c-d385-4ef8-87d3-8fd64f86d20d.jpg",
"D:\project\mulit agent\Images\ebcb1f39-f063-45d7-a617-6e6c463fe77d.jpg",
"D:\project\mulit agent\Images\a8bafc9b-fc5b-4e96-9f0a-51ae736a84cf.jpg",
"D:\project\mulit agent\Images\44166327-6f69-47e4-9072-74bab30ac5ea.jpg",
"D:\project\mulit agent\Images\j.jpg", # A specific one for the judge perhaps
]
# --- Agent Prompts (Base Instructions) ---
AGENT_SYSTEM_PROMPTS = {
'DebateOrchestrator': (
"You are a neutral debate moderator. Your role is to introduce the topic, "
"call on speakers, maintain order, and conclude the debate. Do not offer your own opinions "
"or arguments. Just manage the flow and report the arguments presented by the agents."
),
'Summarizer': (
"You are a neutral summarization assistant. Your task is to read the provided debate history "
"and produce a concise, impartial summary of the key arguments made by each side. "
"Do not add external information or offer opinions. Focus on capturing the main points from both the Affirmative and Negative teams."
),
'DebateAgent': ( # Base prompt for both Affirmative and Negative
"You are an AI debater participating in a structured debate. "
"Your goal is to present compelling arguments for your assigned stance on the topic, "
"and respectfully rebut the points made by the opposing side. "
"Base your response on the provided debate summary and *relevant information from the knowledge base* if available. " # Added RAG instruction
"Be clear, logical, and focus on the arguments. Follow the format shown in the examples "
"and keep your response within the requested token limit."
),
'AffirmativeAgent': (
"You are an AI debater on the AFFIRMATIVE team. Your task is to argue STRONGLY in favor of the debate motion: '{topic}'. "
"Present arguments supporting this position and defend it against the Negative team's points. "
"Remember the base instructions for an AI debater."
),
'NegativeAgent': (
"You are an AI debater on the NEGATIVE team. Your task is to argue STRONGLY against the debate motion: '{topic}'. "
"Present arguments opposing this position and defend it against the Affirmative team's points. "
"Remember the base instructions for an AI debater."
),
'JudgeAgent': (
"You are an AI judge observing a debate on the topic: '{topic}'. "
"Your sole role is to provide a brief, impartial summary of the key points made by each side *based only on the provided debate summary*. "
"Do not add external information, offer opinions, or declare a winner. "
"Summarize the arguments as shown in the example, using a list format. Keep your response within the requested token limit." # RAG context not needed for judge summary
)
}
# --- Specific Prompts for Debate Stages ---
STAGE_PROMPTS = {
'opening_statement': (
"{retrieved_context}" # Placeholder for RAG context
"Deliver your opening statement for the topic: '{topic}'. "
"Provide your main arguments as a numbered list of 3 to 4 concise points, referencing the provided information if relevant." # Reference RAG context
),
'rebuttal': (
"{retrieved_context}" # Placeholder for RAG context
"Here is a summary of the debate history so far:\n\n{summary}\n\n"
"It is your turn to offer a rebuttal. Respond to the points made by the opposing team. "
"Counter their claims and defend your own position based on the summary and *relevant provided information*. " # Reference RAG context
"Provide your rebuttal points as a numbered list of 2 to 3 concise points."
),
'closing_statement': (
"{retrieved_context}" # Placeholder for RAG context
"Here is a summary of the debate history so far:\n\n{summary}\n\n"
"Deliver your closing statement. Summarize your main arguments and explain why your stance on the topic is the most compelling, referencing points in the summary and *relevant provided information* if helpful. " # Reference RAG context
"Provide your summary points as a numbered list of 2 to 3 concise points."
),
'judge_analysis': (
"Here is a summary of the debate history:\n\n{summary}\n\n"
"Provide a *brief*, impartial summary of the key arguments from the Affirmative team and the key arguments from the Negative team based *only* on the summary above. "
"Format your response exactly as shown in the example, using headings and bullet points."
)
}
# --- Prompt Template for Summarization ---
SUMMARY_PROMPT_TEMPLATE = (
"Please provide a concise, neutral summary of the following debate history. "
"Include the main arguments and counter-arguments presented by both the Affirmative and Negative teams:\n\n"
"{debate_history}"
"\n\nProvide the summary in a few sentences or a short paragraph."
)
# --- Max Tokens Configuration ---
MAX_TOKENS_PER_STAGE = {
'opening_statement': 300, # Increased slightly to accommodate potential context
'rebuttal': 250, # Increased slightly to accommodate potential context
'closing_statement': 250, # Increased slightly to accommodate potential context
'judge_analysis': 200 # Judge summary should still be concise
}
MAX_SUMMARY_TOKENS = 100
# --- Few-Shot Examples ---
# Update examples to show the *expected* format when context is present.
# We'll include a placeholder indicating where context *would* be.
# The LLM learns the format from these examples.
PROMPT_EXAMPLES = {
'opening_statement': [
{'role': 'user', 'content': (
"Relevant information from knowledge base:\n\n[Context Placeholder]\n\n" # Indicate where context goes
"Deliver your opening statement for the topic: 'Should pineapple belong on pizza?'. Provide your main arguments as a numbered list of 3 to 4 concise points."
)},
{'role': 'assistant', 'content': (
"Here is my opening statement:\n"
"1. Pineapple adds a delicious sweet and tangy contrast to savory toppings.\n"
"2. Its juiciness helps prevent the pizza from being too dry.\n"
"3. It's a popular topping enjoyed by millions worldwide, indicating broad appeal.\n"
"4. Pairing fruit with savory dishes is common in many cuisines." # Example output doesn't need to explicitly use context if the point is general knowledge
)}
],
'rebuttal': [
{'role': 'user', 'content': (
"Relevant information from knowledge base:\n\n[Context Placeholder]\n\n" # Indicate where context goes
"Here is a summary of the debate history so far:\n\n"
"Summary: Affirmative argued for safety, efficiency. Negative argued against based on risks, job losses. Most recently, Negative claimed AV tech isn't ready and job losses are certain.\n\n"
"It is your turn to offer a rebuttal... Based on the summary and *relevant provided information*, respond to the points made by the opposing side in their most recent arguments. Provide your rebuttal points as a numbered list of 2 to 3 concise points."
)},
{'role': 'assistant', 'content': (
"Here is my rebuttal:\n"
"1. The claim that AV tech isn't ready ignores the rapid advancements and testing already underway by leading companies. [Reference info from context if possible]\n" # Added note for potential reference
"2. While job displacement is a concern, history shows technological shifts create new jobs, and focus should be on transition support, not halting progress."
)}
],
'closing_statement': [
{'role': 'user', 'content': (
"Relevant information from knowledge base:\n\n[Context Placeholder]\n\n" # Indicate where context goes
"Here is a summary of the debate history so far:\n\n"
"Summary: Affirmative argued safety, efficiency, accessibility benefits. Negative countered with safety risks, job losses, infrastructure costs. Rebuttals exchanged points on tech readiness, economic transition, and regulatory progress.\n\n"
"Deliver your closing statement... Provide your summary points as a numbered list of 2 to 3 concise points, referencing the summary and *relevant provided information* if helpful." # Reference RAG context
)},
{'role': 'assistant', 'content': (
"In closing, I reiterate my main points:\n"
"1. The potential safety and efficiency gains from AVs are transformative. [Reference info from context if possible]\n" # Added note for potential reference
"2. While challenges exist, they are surmountable with continued development and thoughtful policy, paving the way for significant societal benefits."
)}
],
'judge_analysis': [
# Judge doesn't get RAG context in this design, so no placeholder needed here
{'role': 'user', 'content': (
"Here is a summary of the debate history:\n\n"
"Summary: Affirmative highlighted safety from reducing human error, efficiency in traffic, and accessibility. Negative emphasized current safety risks, potential job losses, and infrastructure/regulatory hurdles. Rebuttals debated technological maturity and economic transition.\n\n"
"Provide a brief, impartial summary of the key arguments from the Affirmative team and the key arguments from the Negative team based *only* on the summary above. Format your response exactly as shown in the example, using headings and bullet points."
)},
{'role': 'assistant', 'content': (
"Affirmative Key Points:\n"
"- AVs enhance safety by eliminating human error.\n"
"- They improve efficiency and reduce congestion.\n"
"- They offer increased accessibility.\n\n"
"Negative Key Points:\n"
"- AV technology is not yet sufficiently safe or reliable.\n"
"- Large-scale implementation will cause significant job losses.\n"
"- Infrastructure and regulatory challenges are major hurdles."
)}
]
}
debate_state.py (The Debate’s Memory):
# debate_state.py
class DebateState:
"""Holds the state of the debate, including the topic and history."""
def __init__(self, topic: str):
self.topic = topic
self.history = [] # List of dictionaries: [{'agent': name, 'role': role, 'argument': text}]
def add_argument(self, agent_name: str, agent_role: str, argument: str):
"""Adds an argument to the debate history."""
self.history.append({
'agent': agent_name,
'role': agent_role,
'argument': argument
})
def get_history_text(self) -> str:
"""Returns the full debate history as formatted text."""
history_text = f"Debate Topic: {self.topic}\n\n-- Debate History --\n"
if not self.history:
history_text += "No arguments yet.\n"
else:
for entry in self.history:
history_text += f"[{entry['role']} - {entry['agent']}]:\n{entry['argument']}\n\n"
history_text += "-- End of History --\n"
return history_text
def get_last_argument_text(self, from_role: str) -> str or None:
"""Returns the text of the last argument from a specific role."""
for entry in reversed(self.history):
if entry['role'] == from_role:
return entry['argument']
return None
def get_full_history_for_prompt(self) -> list:
"""Returns history formatted for Ollama's chat message list."""
messages = []
# Add context from history, maybe last few turns or all depending on desired context length
# For now, let's just pass the whole history as a user message in the prompt string
# or include relevant previous messages directly if we manage history more granularly for the API
# For simplicity with current STAGE_PROMPTS, we'll format it as a string in the prompt.
return self.get_history_text() # Or a more structured format if needed
Phase 2: Giving Agents a Voice — Connecting to Local LLMs
Using Ollama, I ran LLMs like llama3, mistral, and gemma locally. The agents communicate by sending structured prompts and receiving text responses.
The base Agent class handles all model calls. This abstraction allowed roles like AffirmativeAgent, NegativeAgent, and JudgeAgent to inherit behavior and customize it based on role and stage.
A major lesson: don’t mix UI logic with backend processing. This means no Streamlit calls outside app.py. The agent logic is clean, pure Python, which keeps it portable and testable.
agents.py (Agent Definitions and LLM Handling):
# agents.py
# Import necessary libraries
import ollama
import time
import random
import queue
import concurrent.futures
from typing import Union # Import Union for type hinting
# Import necessary components for RAG context formatting and type hinting
from langchain.schema import Document
from langchain_core.retrievers import BaseRetriever # Type hint for retriever object
# Import configuration settings and debate state
from config import (
AGENT_SYSTEM_PROMPTS, STAGE_PROMPTS, DEFAULT_MODEL, SUMMARY_MODEL,
DEBATE_TOPIC, PROMPT_EXAMPLES, SUMMARY_PROMPT_TEMPLATE,
MAX_TOKENS_PER_STAGE, MAX_SUMMARY_TOKENS,
ENABLE_RAG, RETRIEVER_K # Ensure RETRIEVER_K is imported
)
from debate_state import DebateState # Import DebateState for type hinting
# --- Base Agent Class ---
class Agent:
"""Base class for all agents in the system, handling Ollama interaction."""
# __init__ signature: 2 positional (self, name, role_type), then keyword-only (*)
def __init__(self, name: str, role_type: str, *, model: str = DEFAULT_MODEL, retriever: Union[BaseRetriever, None] = None, agent_photo: Union[str, None] = None):
self.name = name
self.role_type = role_type
self.model = model
self.retriever = retriever # This can be None if not provided
self.agent_photo = agent_photo # Path to the agent's photo
# Get system prompt from config
self.system_prompt_template = AGENT_SYSTEM_PROMPTS.get(role_type)
if self.system_prompt_template is None:
self.system_prompt = ""
# Removed Streamlit warning print here
else:
try:
# Format system prompt with topic if template uses {topic}
self.system_prompt = self.system_prompt_template.format(topic=DEBATE_TOPIC)
except KeyError:
# Use template directly if it doesn't use {topic}
self.system_prompt = self.system_prompt_template
# Method to interact with the LLM (Ollama)
# Takes user_prompt, stage (for examples/tokens), max_tokens, and retrieved_context
def generate_response(self, user_prompt: str, stage: Union[str, None] = None, max_tokens: int = -1, retrieved_context: str = "") -> str:
"""Sends a prompt to the Ollama model and returns the raw response text."""
messages = []
if self.system_prompt:
messages.append({'role': 'system', 'content': self.system_prompt})
# Add few-shot examples if available for this stage
if stage and stage in PROMPT_EXAMPLES:
# Format examples to include the context placeholder if needed
formatted_examples = []
for example in PROMPT_EXAMPLES[stage]:
formatted_example = {'role': example['role']}
if example['role'] == 'user':
# Include the context placeholder in the example user prompt if RAG is conceptually used here
formatted_example['content'] = "Relevant information from knowledge base:\n\n[Context Placeholder]\n\n" + example['content']
else: # assistant role
# Assistant example response doesn't include the placeholder
formatted_example['content'] = example['content']
# CORRECTED: Append the single formatted_example to the list
formatted_examples.append(formatted_example)
messages.extend(formatted_examples) # Add the list of formatted examples
# Add the actual user prompt for the current turn
full_user_prompt = user_prompt
# Add retrieved context to the actual user prompt if RAG is enabled and context is provided
if retrieved_context and ENABLE_RAG:
full_user_prompt = f"Relevant information from knowledge base:\n\n{retrieved_context}\n\n" + user_prompt
messages.append({'role': 'user', 'content': full_user_prompt})
# Set Ollama options (like max_tokens)
options = {}
if max_tokens > 0:
options['num_predict'] = max_tokens
try:
# Make the Ollama chat call
response = ollama.chat(model=self.model, messages=messages, stream=False, options=options)
return response['message']['content'].strip()
except ollama.ResponseError as e:
# Return an error message string on Ollama failure
return f"ERROR: Agent failed to generate response due to Ollama error: {e}"
except Exception as e:
# Return an error message string on other exceptions
return f"ERROR: Agent failed due to an unexpected error: {e}"
# The base Agent class does NOT implement the 'act' method.
# Subclasses that need to participate in a debate turn MUST implement their own 'act' method.
# Keeping a placeholder here to avoid errors when checking if method exists
def act(self, debate_state: DebateState, stage: str, *args, **kwargs):
"""Placeholder act method. Should be implemented by subclasses."""
raise NotImplementedError(f"Agent type '{self.role_type}' must implement 'act' method.")
# --- Base Class for Debating Agents ---
# Inherits from Agent
class DebateAgent(Agent):
"""Base class for debating agents (Affirmative/Negative)."""
# __init__ signature: 3 positional (self, name, role_type, stance), then keyword-only (*)
def __init__(self, name: str, role_type: str, stance: str, *, model: str = DEFAULT_MODEL, retriever: Union[BaseRetriever, None] = None, agent_photo: Union[str, None] = None):
# Call the parent Agent's __init__. Pass name and role_type positionally, then keywords.
# Stance is specific to DebateAgent, not passed to Agent parent.
super().__init__(name, role_type, model=model, retriever=retriever, agent_photo=agent_photo)
self.stance = stance # Store the agent's stance ('Affirmative' or 'Negative')
# THIS IS THE ACT METHOD FOR ALL DEBATING AGENTS (Affirmative and Negative inherit this)
# It handles retrieving RAG context and formatting the prompt for debate stages.
def act(self, debate_state: DebateState, stage: str, debate_summary: Union[str, None] = None) -> str:
"""Generates an argument based on the debate stage, summary, and retrieved context."""
# Get the prompt template for the current stage from config
prompt_template = STAGE_PROMPTS.get(stage)
if not prompt_template:
return f"ERROR: Unknown debate stage '{stage}'"
retrieved_context = ""
# Define which stages require RAG retrieval
stages_using_rag = ['opening_statement', 'rebuttal', 'closing_statement']
# Only retrieve if RAG is enabled, retriever is available for this agent, and the current stage uses RAG
if ENABLE_RAG and self.retriever and stage in stages_using_rag:
# Formulate a query for the retriever based on debate context and agent's task
query = f"Provide information relevant to debating the topic: '{debate_state.topic}' from the {self.stance} perspective."
if debate_summary and stage != 'opening_statement':
# If in rebuttal, query might also be based on recent points from summary
query += f" Specifically, provide information to rebut points made by the opposing side related to: {debate_summary[:200]}..." # Add part of summary to query
elif stage == 'closing_statement' and debate_summary:
query += f" Specifically, provide information supporting key {self.stance} arguments summarized as: {debate_summary[:200]}..."
# print(f"--- {self.name} ({self.role_type}) querying KB for stage '{stage}' with query: {query[:100]}... ---", flush=True) # Removed Streamlit print
try:
# Retrieve top K documents using the retriever
# k is configured in rag_pipeline/config.py and passed when retriever is created
relevant_docs = self.retriever.get_relevant_documents(query)
if relevant_docs:
# Format retrieved documents into a string for the prompt
retrieved_context = "Relevant Information:\n" + "\n---\n".join([f"Source: {doc.metadata.get('source', 'N/A')}\nContent: {doc.page_content}" for doc in relevant_docs]) + "\n---\n"
# print(f"--- Retrieved {len(relevant_docs)} documents for {self.name} ---", flush=True) # Removed Streamlit print
else:
# print(f"--- No relevant documents found for {self.name}'s query ---", flush=True) # Removed Streamlit print
pass # No context found
except Exception as e:
# Return an internal error indicator if KB retrieval fails
# print(f"Error during KB retrieval for {self.name}: {e}", flush=True) # Removed Streamlit print
retrieved_context = f"ERROR_KB_RETRIEVAL: {e}"
# --- Format the user prompt for the LLM ---
prompt_args = {
'topic': debate_state.topic,
# Pass the retrieved_context string. If empty, the template will handle the placeholder.
'retrieved_context': retrieved_context if retrieved_context else "[No relevant information found from knowledge base.]\n\n"
}
# Only add summary to prompt_args if the stage template uses the {summary} placeholder AND summary is provided
# The prompt template for rebuttal and closing statements uses {summary}
if stage in ['rebuttal', 'closing_statement'] and debate_summary is not None:
prompt_args['summary'] = debate_summary
# Note: JudgeAgent overrides act, its prompt only needs summary and doesn't use RAG context
# Format the user prompt text using the appropriate template and the collected arguments
try: # Add try-except for prompt formatting errors
user_prompt_text = prompt_template.format(**prompt_args)
except KeyError as e:
return f"ERROR_PROMPT_FORMAT: Missing key in prompt args: {e}. Prompt template: {prompt_template}"
except Exception as e:
return f"ERROR_PROMPT_FORMAT: An unexpected error occurred during prompt formatting: {e}. Prompt template: {prompt_template}"
# Get the max tokens limit for this specific stage from config
max_tokens = MAX_TOKENS_PER_STAGE.get(stage, -1)
# Call the generate_response method from the parent Agent class
# Pass the formatted prompt, stage, max_tokens, and the retrieved_context string
argument = self.generate_response(user_prompt_text, stage=stage, max_tokens=max_tokens, retrieved_context=retrieved_context)
return argument
# --- Specific Debating Agent Classes ---
# Inherit from DebateAgent (and thus inherit the act method from DebateAgent)
class AffirmativeAgent(DebateAgent):
"""Agent arguing for the debate motion."""
# __init__ signature: 1 positional (self, name), then keyword-only (*)
def __init__(self, name: str, *, model: str = DEFAULT_MODEL, retriever: Union[BaseRetriever, None] = None, agent_photo: Union[str, None] = None):
# Call DebateAgent's __init__. Pass name positionally, role_type and stance positionally, then keywords.
# DebateAgent.__init__ expects (name, role_type, stance) positionally
super().__init__(name, 'AffirmativeAgent', 'Affirmative', model=model, retriever=retriever, agent_photo=agent_photo)
class NegativeAgent(DebateAgent):
"""Agent arguing against the debate motion."""
# __init__ signature: 1 positional (self, name), then keyword-only (*)
def __init__(self, name: str, *, model: str = DEFAULT_MODEL, retriever: Union[BaseRetriever, None] = None, agent_photo: Union[str, None] = None):
# Call DebateAgent's __init__. Pass name positionally, role_type and stance positionally, then keywords.
super().__init__(name, 'NegativeAgent', 'Negative', model=model, retriever=retriever, agent_photo=agent_photo)
# --- Judge Agent Class ---
# Inherits from Agent (does NOT inherit from DebateAgent, has its own act method)
class JudgeAgent(Agent):
"""Agent providing analysis at the end of the debate."""
# __init__ signature: 1 positional (self, name), then keyword-only (*)
def __init__(self, name: str, *, model: str = DEFAULT_MODEL, retriever: Union[BaseRetriever, None] = None, agent_photo: Union[str, None] = None):
# Call the parent Agent's __init__. Pass name and role_type positionally, then keywords.
# Judge doesn't use RAG for its output generation task, so we pass retriever=None to Agent init
super().__init__(name, 'JudgeAgent', model=model, retriever=None, agent_photo=agent_photo)
# THIS IS THE ACT METHOD SPECIFICALLY FOR THE JUDGE AGENT
# It overrides the base Agent.act (which raises NotImplementedError)
# It handles getting summary and formatting prompt for judge analysis.
def act(self, debate_state: DebateState, stage: str, debate_summary: Union[str, None] = None) -> str:
"""Analyzes the debate history and provides commentary based on summary."""
# Judge's act method only needs the stage name 'judge_analysis' and the summary
# Check if the stage is correct, although Orchestrator should call with 'judge_analysis'
if stage != 'judge_analysis':
# This should not happen if Orchestrator is correct
return f"ERROR: Judge act called with incorrect stage: '{stage}'"
# Get the prompt template for judge analysis
prompt_template = STAGE_PROMPTS.get(stage)
if not prompt_template:
return f"ERROR: Judge analysis prompt template not found."
# Judge prompt specifically uses the summary, requires summary to be provided
if debate_summary is None:
return "ERROR: Judge could not get summary."
# Format the user prompt for the LLM using the template and summary
try: # Add try-except for prompt formatting errors
user_prompt = prompt_template.format(summary=debate_summary)
except KeyError as e:
return f"ERROR_PROMPT_FORMAT: Missing key in prompt args: {e}. Prompt template: {prompt_template}"
except Exception as e:
return f"ERROR_PROMPT_FORMAT: An unexpected error occurred during prompt formatting: {e}. Prompt template: {prompt_template}"
# Get the max tokens limit for the judge stage
max_tokens = MAX_TOKENS_PER_STAGE.get(stage, -1)
# Call generate_response. Judge's task doesn't involve retrieving RAG context
# for its output, so retrieved_context is an empty string.
analysis = self.generate_response(user_prompt, stage=stage, max_tokens=max_tokens, retrieved_context="")
return analysis
# --- Debate Orchestrator Class ---
# Inherits from Agent (Orchestrator is a type of agent in the system)
class DebateOrchestrator(Agent):
"""Manages the flow of the debate."""
# __init__ signature: 3 positional (self, name, debate_state, agents), then keyword-only (*)
def __init__(self, name: str, debate_state: DebateState, agents: list[Agent], *, model: str = 'llama3'):
# Call parent Agent's __init__. Pass name positionally, role_type ('DebateOrchestrator') positionally, then keywords.
# Orchestrator doesn't need retriever or agent_photo for its base Agent identity
super().__init__(name, 'DebateOrchestrator', model=model, retriever=None, agent_photo=None)
# Store positional arguments here
self.debate_state = debate_state
self.agents = agents # Store the full list of agent instances
# Separate agents by role type
self.affirmative_agents = [a for a in self.agents if isinstance(a, AffirmativeAgent)]
self.negative_agents = [a for a in self.agents if isinstance(a, NegativeAgent)]
self.judge_agent = next((a for a in self.agents if isinstance(a, JudgeAgent)), None)
if not self.affirmative_agents or not self.negative_agents:
# Removed Streamlit error here, raise Python ValueError
raise ValueError("Must have at least one Affirmative and one Negative agent configured.")
self.turn_delay_seconds = 1 # Delay between turns
self.summary_model = SUMMARY_MODEL
self.summary_system_prompt = AGENT_SYSTEM_PROMPTS.get('Summarizer').format() if AGENT_SYSTEM_PROMPTS.get('Summarizer') else ""
# Method to generate a summary of debate history (used internally by orchestrator)
def _generate_summary(self) -> str:
"""Generates a summary of the current debate history using an LLM."""
# print messages to console
print("\n--- Orchestrator is summarizing debate history... ---", flush=True)
history_text = self.debate_state.get_history_text()
if not history_text.strip() or "-- Debate History --\nNo arguments yet.\n\n-- End of History --" in history_text:
return "No debate history to summarize yet."
user_prompt = SUMMARY_PROMPT_TEMPLATE.format(debate_history=history_text)
messages = []
if self.summary_system_prompt:
messages.append({'role': 'system', 'content': self.summary_system_prompt})
messages.append({'role': 'user', 'content': user_prompt})
options = {}
if MAX_SUMMARY_TOKENS > 0:
options['num_predict'] = MAX_SUMMARY_TOKENS
try:
# Use the generate_response method from the base Agent class for summary generation
# The orchestrator is an Agent, so it can call its own generate_response
summary = self.generate_response(user_prompt, stage='summary', max_tokens=MAX_SUMMARY_TOKENS, retrieved_context="")
print("--- Summary Generated ---", flush=True)
return summary
except Exception as e:
print(f"Error during summarization from Ollama: {e}", flush=True)
return f"ERROR: Failed to generate summary due to error: {e}"
# Main method to run the debate flow
# This is a generator function that yields events back to the UI
def run_debate(self, num_rebuttal_rounds: int):
"""Runs the full debate sequence, yielding output for the UI."""
# Yield messages for the UI
yield {"type": "status", "message": "Starting Debate...", "topic": self.debate_state.topic}
yield {"type": "stage", "stage_name": "Opening Statements"}
# Opening Statements Stage
# Affirmative Team's Opening Statements
for agent in self.affirmative_agents:
# Yield status indicating who is speaking
yield {"type": "status", "message": f"{agent.name} ({agent.role_type}) speaking..."}
# Call the agent's act method
argument_text = agent.act(self.debate_state, 'opening_statement', debate_summary=None) # Opening needs no summary
# Add argument to debate history if not an error
if not argument_text.startswith("ERROR:"):
self.debate_state.add_argument(agent.name, agent.role_type, argument_text)
# Yield the argument for the UI
yield {"type": "argument", "agent_name": agent.name, "agent_role": agent.role_type, "argument": argument_text, "agent_photo": agent.agent_photo}
# Optional: Add a small pause between agents within a stage
# time.sleep(self.turn_delay_seconds)
# Negative Team's Opening Statements
for agent in self.negative_agents:
yield {"type": "status", "message": f"{agent.name} ({agent.role_type}) speaking..."}
argument_text = agent.act(self.debate_state, 'opening_statement', debate_summary=None)
if not argument_text.startswith("ERROR:"):
self.debate_state.add_argument(agent.name, agent.role_type, argument_text)
yield {"type": "argument", "agent_name": agent.name, "agent_role": agent.role_type, "argument": argument_text, "agent_photo": agent.agent_photo}
# Optional: Add a small pause
# time.sleep(self.turn_delay_seconds)
# Rebuttal Rounds Stage
for i in range(num_rebuttal_rounds):
yield {"type": "stage", "stage_name": f"--- Rebuttal Round {i+1} ---"}
# Summarize debate history before each rebuttal round
yield {"type": "status", "message": "Orchestrator summarizing debate..."}
self.current_summary = self._generate_summary()
yield {"type": "status", "message": "Summary Generated."}
# Check if summarization failed
if isinstance(self.current_summary, str) and self.current_summary.startswith("ERROR:"):
yield {"type": "status", "message": f"Skipping remaining debate due to summarization error: {self.current_summary}"}
break # Stop the debate loop if summarization fails
# Affirmative Team's Rebuttals
for agent in self.affirmative_agents:
yield {"type": "status", "message": f"{agent.name} ({agent.role_type}) speaking..."}
# Pass the current debate summary to the agent's act method
argument_text = agent.act(self.debate_state, 'rebuttal', debate_summary=self.current_summary)
if not argument_text.startswith("ERROR:"):
self.debate_state.add_argument(agent.name, agent.role_type, argument_text)
yield {"type": "argument", "agent_name": agent.name, "agent_role": agent.role_type, "argument": argument_text, "agent_photo": agent.agent_photo}
# Optional: Add a pause
# time.sleep(self.turn_delay_seconds)
# Negative Team's Rebuttals
for agent in self.negative_agents:
yield {"type": "status", "message": f"{agent.name} ({agent.role_type}) speaking..."}
argument_text = agent.act(self.debate_state, 'rebuttal', debate_summary=self.current_summary)
if not argument_text.startswith("ERROR:"):
self.debate_state.add_argument(agent.name, agent.role_type, argument_text)
yield {"type": "argument", "agent_name": agent.name, "agent_role": agent.role_type, "argument": argument_text, "agent_photo": agent.agent_photo}
# Optional: Add a pause
# time.sleep(self.turn_delay_seconds)
# Closing Statements Stage
yield {"type": "stage", "stage_name": "Closing Statements"}
# Summarize debate history before closing statements
yield {"type": "status", "message": "Orchestrator summarizing debate..."}
self.current_summary = self._generate_summary()
yield {"type": "status", "message": "Summary Generated."}
# Check if summarization failed
if isinstance(self.current_summary, str) and self.current_summary.startswith("ERROR:"):
yield {"type": "status", "message": f"Skipping closing statements and judge due to summarization error: {self.current_summary}"}
else:
# Affirmative Team's Closing Statements
for agent in self.affirmative_agents:
yield {"type": "status", "message": f"{agent.name} ({agent.role_type}) speaking..."}
# Pass the current debate summary
argument_text = agent.act(self.debate_state, 'closing_statement', debate_summary=self.current_summary)
if not argument_text.startswith("ERROR:"):
self.debate_state.add_argument(agent.name, agent.role_type, argument_text)
yield {"type": "argument", "agent_name": agent.name, "agent_role": agent.role_type, "argument": argument_text, "agent_photo": agent.agent_photo}
# Optional: Add a pause
# time.sleep(self.turn_delay_seconds)
# Negative Team's Closing Statements
for agent in self.negative_agents:
yield {"type": "status", "message": f"{agent.name} ({agent.role_type}) speaking..."}
# Pass the current debate summary
argument_text = agent.act(self.debate_state, 'closing_statement', debate_summary=self.current_summary)
if not argument_text.startswith("ERROR:"):
self.debate_state.add_argument(agent.name, agent.role_type, argument_text)
yield {"type": "argument", "agent_name": agent.name, "agent_role": agent.role_type, "argument": argument_text, "agent_photo": agent.agent_photo}
# Optional: Add a pause
# time.sleep(self.turn_delay_seconds)
# Judge Analysis Stage (Optional)
if self.judge_agent:
yield {"type": "stage", "stage_name": "Judge Analysis"}
# Summarize debate history for the judge's analysis
yield {"type": "status", "message": "Orchestrator summarizing debate for Judge..."}
final_summary = self._generate_summary() # Generate final summary
yield {"type": "status", "message": "Summary Generated for Judge."}
# Check if summarization failed for the judge
if isinstance(final_summary, str) and final_summary.startswith("ERROR:"):
yield {"type": "status", "message": f"Skipping judge analysis due to summarization error: {final_summary}"}
else:
# Call the Judge agent's act method, passing the final summary
analysis = self.judge_agent.act(self.debate_state, stage='judge_analysis', debate_summary=final_summary)
# Yield the judge's analysis argument
yield {"type": "argument", "agent_name": self.judge_agent.name, "agent_role": self.judge_agent.role_type, "argument": analysis, "agent_photo": self.judge_agent.agent_photo}
# End of Debate
yield {"type": "status", "message": "Debate Concluded."}
Phase 3: Mastering the Art of Argumentation — Taming LLM Behavior
Debate agents initially responded with verbose, repetitive, and sometimes vague arguments.
Here’s how I addressed that:
-Token Limit Control: Using num_predict to limit output length.
-Few-Shot Prompting: Examples were provided in prompts to guide structure.
-Context Summarization: Instead of passing full history, I created a _generate_summary() function in the orchestrator to condense all previous arguments into a short, focused context.
This not only made the responses more concise and structured but also helped each agent remain on-topic across multiple turns.
config.py (Prompt Examples, Limits, Summarization):
# --- Agent Prompts (Base Instructions) ---
AGENT_SYSTEM_PROMPTS = {
'DebateOrchestrator': (
"You are a neutral debate moderator. Your role is to introduce the topic, "
"call on speakers, maintain order, and conclude the debate. Do not offer your own opinions "
"or arguments. Just manage the flow and report the arguments presented by the agents."
),
'Summarizer': (
"You are a neutral summarization assistant. Your task is to read the provided debate history "
"and produce a concise, impartial summary of the key arguments made by each side. "
"Do not add external information or offer opinions. Focus on capturing the main points from both the Affirmative and Negative teams."
),
'DebateAgent': ( # Base prompt for both Affirmative and Negative
"You are an AI debater participating in a structured debate. "
"Your goal is to present compelling arguments for your assigned stance on the topic, "
"and respectfully rebut the points made by the opposing side. "
"Base your response on the provided debate summary and *relevant information from the knowledge base* if available. " # Added RAG instruction
"Be clear, logical, and focus on the arguments. Follow the format shown in the examples "
"and keep your response within the requested token limit."
),
'AffirmativeAgent': (
"You are an AI debater on the AFFIRMATIVE team. Your task is to argue STRONGLY in favor of the debate motion: '{topic}'. "
"Present arguments supporting this position and defend it against the Negative team's points. "
"Remember the base instructions for an AI debater."
),
'NegativeAgent': (
"You are an AI debater on the NEGATIVE team. Your task is to argue STRONGLY against the debate motion: '{topic}'. "
"Present arguments opposing this position and defend it against the Affirmative team's points. "
"Remember the base instructions for an AI debater."
),
'JudgeAgent': (
"You are an AI judge observing a debate on the topic: '{topic}'. "
"Your sole role is to provide a brief, impartial summary of the key points made by each side *based only on the provided debate summary*. "
"Do not add external information, offer opinions, or declare a winner. "
"Summarize the arguments as shown in the example, using a list format. Keep your response within the requested token limit." # RAG context not needed for judge summary
)
}
# --- Specific Prompts for Debate Stages ---
STAGE_PROMPTS = {
'opening_statement': (
"{retrieved_context}" # Placeholder for RAG context
"Deliver your opening statement for the topic: '{topic}'. "
"Provide your main arguments as a numbered list of 3 to 4 concise points, referencing the provided information if relevant." # Reference RAG context
),
'rebuttal': (
"{retrieved_context}" # Placeholder for RAG context
"Here is a summary of the debate history so far:\n\n{summary}\n\n"
"It is your turn to offer a rebuttal. Respond to the points made by the opposing team. "
"Counter their claims and defend your own position based on the summary and *relevant provided information*. " # Reference RAG context
"Provide your rebuttal points as a numbered list of 2 to 3 concise points."
),
'closing_statement': (
"{retrieved_context}" # Placeholder for RAG context
"Here is a summary of the debate history so far:\n\n{summary}\n\n"
"Deliver your closing statement. Summarize your main arguments and explain why your stance on the topic is the most compelling, referencing points in the summary and *relevant provided information* if helpful. " # Reference RAG context
"Provide your summary points as a numbered list of 2 to 3 concise points."
),
'judge_analysis': (
"Here is a summary of the debate history:\n\n{summary}\n\n"
"Provide a *brief*, impartial summary of the key arguments from the Affirmative team and the key arguments from the Negative team based *only* on the summary above. "
"Format your response exactly as shown in the example, using headings and bullet points."
)
}
# --- Prompt Template for Summarization ---
SUMMARY_PROMPT_TEMPLATE = (
"Please provide a concise, neutral summary of the following debate history. "
"Include the main arguments and counter-arguments presented by both the Affirmative and Negative teams:\n\n"
"{debate_history}"
"\n\nProvide the summary in a few sentences or a short paragraph."
)
# --- Max Tokens Configuration ---
MAX_TOKENS_PER_STAGE = {
'opening_statement': 300, # Increased slightly to accommodate potential context
'rebuttal': 250, # Increased slightly to accommodate potential context
'closing_statement': 250, # Increased slightly to accommodate potential context
'judge_analysis': 200 # Judge summary should still be concise
}
MAX_SUMMARY_TOKENS = 100
# --- Few-Shot Examples ---
# Update examples to show the *expected* format when context is present.
# We'll include a placeholder indicating where context *would* be.
# The LLM learns the format from these examples.
PROMPT_EXAMPLES = {
'opening_statement': [
{'role': 'user', 'content': (
"Relevant information from knowledge base:\n\n[Context Placeholder]\n\n" # Indicate where context goes
"Deliver your opening statement for the topic: 'Should pineapple belong on pizza?'. Provide your main arguments as a numbered list of 3 to 4 concise points."
)},
{'role': 'assistant', 'content': (
"Here is my opening statement:\n"
"1. Pineapple adds a delicious sweet and tangy contrast to savory toppings.\n"
"2. Its juiciness helps prevent the pizza from being too dry.\n"
"3. It's a popular topping enjoyed by millions worldwide, indicating broad appeal.\n"
"4. Pairing fruit with savory dishes is common in many cuisines." # Example output doesn't need to explicitly use context if the point is general knowledge
)}
],
'rebuttal': [
{'role': 'user', 'content': (
"Relevant information from knowledge base:\n\n[Context Placeholder]\n\n" # Indicate where context goes
"Here is a summary of the debate history so far:\n\n"
"Summary: Affirmative argued for safety, efficiency. Negative argued against based on risks, job losses. Most recently, Negative claimed AV tech isn't ready and job losses are certain.\n\n"
"It is your turn to offer a rebuttal... Based on the summary and *relevant provided information*, respond to the points made by the opposing side in their most recent arguments. Provide your rebuttal points as a numbered list of 2 to 3 concise points."
)},
{'role': 'assistant', 'content': (
"Here is my rebuttal:\n"
"1. The claim that AV tech isn't ready ignores the rapid advancements and testing already underway by leading companies. [Reference info from context if possible]\n" # Added note for potential reference
"2. While job displacement is a concern, history shows technological shifts create new jobs, and focus should be on transition support, not halting progress."
)}
],
'closing_statement': [
{'role': 'user', 'content': (
"Relevant information from knowledge base:\n\n[Context Placeholder]\n\n" # Indicate where context goes
"Here is a summary of the debate history so far:\n\n"
"Summary: Affirmative argued safety, efficiency, accessibility benefits. Negative countered with safety risks, job losses, infrastructure costs. Rebuttals exchanged points on tech readiness, economic transition, and regulatory progress.\n\n"
"Deliver your closing statement... Provide your summary points as a numbered list of 2 to 3 concise points, referencing the summary and *relevant provided information* if helpful." # Reference RAG context
)},
{'role': 'assistant', 'content': (
"In closing, I reiterate my main points:\n"
"1. The potential safety and efficiency gains from AVs are transformative. [Reference info from context if possible]\n" # Added note for potential reference
"2. While challenges exist, they are surmountable with continued development and thoughtful policy, paving the way for significant societal benefits."
)}
],
'judge_analysis': [
# Judge doesn't get RAG context in this design, so no placeholder needed here
{'role': 'user', 'content': (
"Here is a summary of the debate history:\n\n"
"Summary: Affirmative highlighted safety from reducing human error, efficiency in traffic, and accessibility. Negative emphasized current safety risks, potential job losses, and infrastructure/regulatory hurdles. Rebuttals debated technological maturity and economic transition.\n\n"
"Provide a brief, impartial summary of the key arguments from the Affirmative team and the key arguments from the Negative team based *only* on the summary above. Format your response exactly as shown in the example, using headings and bullet points."
)},
{'role': 'assistant', 'content': (
"Affirmative Key Points:\n"
"- AVs enhance safety by eliminating human error.\n"
"- They improve efficiency and reduce congestion.\n"
"- They offer increased accessibility.\n\n"
"Negative Key Points:\n"
"- AV technology is not yet sufficiently safe or reliable.\n"
"- Large-scale implementation will cause significant job losses.\n"
"- Infrastructure and regulatory challenges are major hurdles."
)}
]
}
Phase 4: Grounding Arguments in Knowledge — The RAG Pipeline
To make the debates fact-based, I integrated a local Retrieval-Augmented Generation pipeline using LangChain and ChromaDB.
Here’s the RAG flow:
1.Load PDFs **using PyPDFLoader.
2.Split text** with RecursiveCharacterTextSplitter.
3.Embed using nomic-embed-text (locally with Ollama).
4.Store in ChromaDB (disk-persisted).
5.Retrieve relevant chunks based on query context (topic, stance, stage).
6.Inject into prompts before the agent generates its argument.
The agents now speak not only with structure but also with evidence.
rag_pipeline.py (Knowledge Engine):
# rag_pipeline.py
import os
import time
import ollama
from typing import Union # <-- Import Union
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.retrievers import BaseRetriever
from config import KB_DIRECTORY, VECTOR_STORE_PATH, EMBEDDING_MODEL, CHUNK_SIZE, CHUNK_OVERLAP, RETRIEVER_K
def load_documents(directory: str):
# ... (same as before)
print(f"Loading documents from {directory}...", flush=True)
if not os.path.exists(directory):
print(f"Knowledge base directory not found: {directory}", flush=True)
return []
loader = DirectoryLoader(directory, glob="*.pdf", loader_cls=PyPDFLoader)
documents = loader.load()
print(f"Loaded {len(documents)} documents.", flush=True)
return documents
def split_text_into_chunks(documents, chunk_size: int, chunk_overlap: int):
# ... (same as before)
print(f"Splitting documents into chunks (size={chunk_size}, overlap={chunk_overlap})...", flush=True)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap
)
chunks = text_splitter.split_documents(documents)
print(f"Created {len(chunks)} chunks.", flush=True)
return chunks
def create_embeddings(embedding_model: str):
# ... (same as before)
print(f"Creating embeddings model using Ollama: {embedding_model}...", flush=True)
try:
embeddings = OllamaEmbeddings(model=embedding_model)
print("Embeddings model created successfully.", flush=True)
return embeddings
except Exception as e:
print(f"Error creating Ollama embeddings model '{embedding_model}': {e}", flush=True)
print("Please ensure Ollama is running and the embedding model is pulled.", flush=True)
return None
def create_vector_store(chunks, embeddings, vector_store_path: str):
# ... (same as before)
print(f"Creating vector store at {vector_store_path}...", flush=True)
if not embeddings:
print("Embeddings model is not available. Cannot create vector store.", flush=True)
return None
try:
vector_store = Chroma.from_documents(chunks, embeddings, persist_directory=vector_store_path)
# vector_store.persist() # Deprecated in newer Chroma
print("Vector store created and persisted.", flush=True)
return vector_store
except Exception as e:
print(f"Error creating vector store: {e}", flush=True)
return None
def load_vector_store(embeddings, vector_store_path: str):
# ... (same as before)
print(f"Loading vector store from {vector_store_path}...", flush=True)
if not os.path.exists(vector_store_path):
print("Vector store directory not found. Cannot load.", flush=True)
return None
if not embeddings:
print("Embeddings model is not available. Cannot load vector store.", flush=True)
return None
try:
vector_store = Chroma(persist_directory=vector_store_path, embedding_function=embeddings)
print("Vector store loaded successfully.", flush=True)
return vector_store
except Exception as e:
print(f"Error loading vector store: {e}", flush=True)
return None
# Correct the type hint here: BaseRetriever | None becomes Union[BaseRetriever, None]
def get_retriever(vector_store) -> Union[BaseRetriever, None]: # <-- Use Union
"""Gets a retriever object from the vector store using configured k."""
if not vector_store:
return None
print(f"Creating retriever with k={RETRIEVER_K}...", flush=True)
try:
retriever = vector_store.as_retriever(search_kwargs={"k": RETRIEVER_K})
return retriever
except Exception as e:
print(f"Error creating retriever: {e}", flush=True)
return None
def index_knowledge_base(kb_directory: str,
vector_store_path: str,
embedding_model: str,
chunk_size: int,
chunk_overlap: int):
# ... (same as before)
print("Starting knowledge base indexing/loading...", flush=True)
embeddings = create_embeddings(embedding_model)
if not embeddings:
print("Embedding model creation failed. Cannot index/load knowledge base.", flush=True)
return None
if os.path.exists(vector_store_path):
print("Vector store directory found. Attempting to load...", flush=True)
vector_store = load_vector_store(embeddings, vector_store_path)
if vector_store:
print("Vector store loaded successfully. Skipping indexing.", flush=True)
return vector_store
print("Vector store not found or loading failed. Indexing documents...", flush=True)
documents = load_documents(kb_directory)
if not documents:
print("No documents found in KB directory to index.", flush=True)
return None
chunks = split_text_into_chunks(documents, chunk_size, chunk_overlap)
vector_store = create_vector_store(chunks, embeddings, vector_store_path)
if vector_store:
print("Verifying created vector store...", flush=True)
verified_store = load_vector_store(embeddings, vector_store_path)
if not verified_store:
print("Warning: Could not load the newly created vector store.", flush=True)
return verified_store
return None
# Removed __main__ block from part 2
Phase 5: Bringing It All to Life — The Streamlit UI
Now the fun part — watching the debate unfold live in the browser.
Streamlit UI Features:
-Sidebar: Choose topic, round count, and enable/disable RAG.
-Agent Display: Circular images, names, role badges, and dynamic status.
-Judge Display: Centered and updated with verdict after final round.
-Chat History Panel: Arguments rendered as messages in a chat-like UI.
Left Sidebar:
-Set topic
-Choose number of rounds
-Toggle RAG
-Select models
Center Panel:
-Visual display of agents (with circular photos and South Indian names 😉)
-Status: “Thinking”, “Speaking”, etc.
-Role icons and styling
Right Panel:
-Chat history view of debate, formatted like a messaging app
-Judge comments, summaries, and final verdicts clearly shown
Key Challenges:
-Blocking Calls: LLM calls froze the UI.
✅ Solved using ThreadPoolExecutor to run agents in background.
-**Real-time Feel: **Instead of printing entire debate at once, I used a generator from the orchestrator and yielded one event per round.
✅ This gave it a step-by-step feel, with each agent “thinking” and “speaking” in turn.
-Custom Styling: Injected CSS for chat bubbles, layout structure, and spacing.
app.py (Streamlit UI Logic):
# app.py
import streamlit as st
import time
import os
import random
from collections import deque
import base64
import concurrent.futures
import queue
# Import backend components
from config import (
DEBATE_TOPIC, NUMBER_OF_REBUTTAL_ROUNDS,
DEFAULT_MODEL, SUMMARY_MODEL,
ENABLE_RAG, KB_DIRECTORY, VECTOR_STORE_PATH, EMBEDDING_MODEL, CHUNK_SIZE, CHUNK_OVERLAP, RETRIEVER_K,
AGENT_PHOTO_PATHS, SOUTH_INDIAN_NAMES,
)
from debate_state import DebateState
from agents import Agent, DebateOrchestrator, AffirmativeAgent, NegativeAgent, JudgeAgent # Ensure Agent is imported
from rag_pipeline import index_knowledge_base, get_retriever
# --- Streamlit App Configuration ---
st.set_page_config(layout="wide", page_title="Autonomous AI Debating Society")
# --- Helper function to get base64 encoded image ---
def get_image_base64(image_path):
try:
if image_path and os.path.exists(image_path):
with open(image_path, "rb") as img_file:
return base64.b64encode(img_file.read()).decode('utf-8')
else:
print(f"Image file not found or path is invalid: {image_path}", flush=True)
return None
except Exception as e:
print(f"Error encoding image {image_path}: {e}", flush=True)
return None
# Inject custom CSS
st.markdown("""
<style>
/* --- General Styles --- */
/* Adjust padding of the main content area */
.main .block-container {
padding-top: 20px;
padding-right: 10px; /* Reduced padding for side-by-side columns */
padding-left: 10px; /* Reduced padding */
padding-bottom: 20px;
}
/* Hide default Streamlit header/title in the main area */
.main .stApp > header {
display: none;
}
/* --- Top Elements (Spanning or in Left Column) --- */
/* Style for the main title element */
.main-app-title {
text-align: center;
font-size: 3em; /* <-- Increased title font size */
font-weight: bold;
margin-bottom: 10px;
}
/* Style for the main status message below the title */
.main-status-message {
text-align: center;
font-style: italic;
color: gray;
font-size: 0.9em;
margin-bottom: 20px;
}
body[data-theme="dark"] .main-status-message { color: #aaa; }
/* --- Layout: Sidebar | Main Content Area (Split into Agent Viz + Chat Panel) --- */
/* Style for the left main column (Agent Viz + Top Elements) */
.agent-viz-column {
padding-right: 10px;
}
body[data-theme="dark"] .agent-viz-column { }
/* --- Agent Visualization Area (Within the left main column) --- */
.agent-viz-area {
text-align: center;
margin-top: 20px;
margin-bottom: 20px;
padding: 10px;
}
/* Individual agent container (within the visualization area) */
.agent-container {
text-align: center;
margin: 10px;
display: inline-block;
vertical-align: top;
width: 120px;
}
/* Agent Photo (Within agent-container) */
.agent-container .agent-circle-img img {
border-radius: 50%;
width: 90px;
height: 90px;
object-fit: cover;
margin-bottom: 5px;
border: 3px solid #4CAF50;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
body[data-theme="dark"] .agent-container .agent-circle-img img {
border: 3px solid #7cb342;
box-shadow: 0 2px 5px rgba(255,255,255,0.1);
}
/* Agent Name (Below photo) */
.agent-name-text {
font-size: 0.9em;
font-weight: bold;
min-height: 1.2em;
text-align: center;
}
/* Status Text (Below name) */
.agent-status-text {
font-size: 0.8em;
font-style: italic;
color: gray;
min-height: 1.2em;
text-align: center;
}
body[data-theme="dark"] .agent-status-text {
color: #aaa;
}
/* --- Chat History Area (Right Main Column) --- */
/* Style the entire right column to look like a panel */
.chat-panel-column {
border-left: 1px solid #ccc;
padding-left: 20px;
background-color: #f0f0f0; /* Light background for the panel */
display: flex;
flex-direction: column;
border-radius: 5px;
/* Remove default padding on the column itself if needed */
/* padding: 0 !important; */
}
body[data-theme="dark"] .right-main-column {
border-left: 1px solid #666;
background-color: #333;
}
/* Chat header within the chat panel */
.chat-panel-column h2 {
text-align: center !important;
margin-bottom: 10px;
}
/* Style the chat container within the chat panel */
.chat-widget-container {
border: 1px solid #ccc;
border-radius: 10px;
padding: 10px;
background-color: #f9f9f9;
margin-top: 0; /* Space handled by h2 margin-bottom */
width: 100%;
flex-grow: 1;
min-height: 0;
/* overflow-y handled by Streamlit's container(height=...) */
}
body[data-theme="dark"] .chat-widget-container {
border: 1px solid #666;
background-color: #0e1117;
}
/* Chat Message Layout (Inside Chat History Container) */
.chat-message {
display: flex; /* Use flexbox */
margin-bottom: 15px;
width: 100%;
/* Removed margin-left/right: auto from here */
}
/* Chat Avatar */
.chat-message .avatar {
width: 45px;
height: 45px;
border-radius: 50%;
object-fit: cover;
margin-right: 10px; /* Space between avatar and text */
border: 2px solid #bbb;
flex-shrink: 0;
}
body[data-theme="dark"] .chat-message .avatar { border: 2px solid #777; }
/* Message Content (Name + Bubble) */
.chat-message .message-content {
flex-grow: 1; /* Let content take up space */
min-width: 0; /* Prevent overflow */
}
/* Agent Name in Chat */
.chat-message .message-content strong {
display: block;
margin-bottom: 2px;
font-size: 0.9em;
}
/* --- Chat Bubble Styling and Alignment --- */
.agent-text-bubble {
background-color: #e0e0e0;
border-radius: 15px;
padding: 10px 15px;
margin-bottom: 5px;
/* Control bubble width */
max-width: calc(100% - 60px); /* Max width considering avatar + margin */
display: inline-block; /* Make bubble take only necessary width */
word-wrap: break-word;
color: #000;
line-height: 1.5;
/* Removed default margins */
}
.agent-text-bubble.affirmative { background-color: #a5d6a7; }
.agent-text-bubble.negative { background-color: #ef9a9a; }
.agent-text-bubble.judge { background-color: #ce93d8; }
/* Default (Negative/Judge): Avatar Left, Bubble Left */
.chat-message:not(.affirmative) {
flex-direction: row; /* Avatar then content */
justify-content: flex-start; /* Align to the start (left) */
}
.chat-message:not(.affirmative) .message-content {
text-align: left; /* Name and bubble content aligned left */
}
.chat-message:not(.affirmative) .message-content .agent-text-bubble {
/* Bubble aligns left within its content div */
margin-right: auto; /* Push bubble to the left (optional, depends on other styles) */
margin-left: 0;
text-align: left;
}
/* Affirmative: Avatar Right, Bubble Right */
.chat-message.affirmative {
flex-direction: row-reverse; /* Content then avatar */
justify-content: flex-start; /* Align to the start (which is the right after reversing) */
}
.chat-message.affirmative .avatar {
margin-left: 10px; /* Space between content and avatar */
margin-right: 0;
}
.chat-message.affirmative .message-content {
text-align: right; /* Name and bubble content aligned right */
}
.chat-message.affirmative .message-content .agent-text-bubble {
/* Bubble aligns right within its content div */
margin-left: auto; /* Push bubble to the right */
margin-right: 0;
text-align: left; /* Keep text aligned left inside the bubble */
}
/* --- Utility Styles --- */
/* Status Messages (Used in chat history) */
.status-message {
font-size: 0.9em;
font-style: italic;
color: gray;
text-align: center;
margin: 5px 0;
}
body[data-theme="dark"] .status-message { color: #aaa; }
/* Stage Separators (Used in chat history) */
.stage-separator {
font-weight: bold;
color: #555;
text-align: center;
margin: 10px 0;
border-bottom: 1px solid #ccc;
line-height: 0.1em;
}
body[data-theme="dark"] .stage-separator { color: #aaa; border-bottom-color: #666;}
.stage-separator span {
background:#fff;
padding:0 10px;
}
body[data-theme="dark"] .stage-separator span { background:#0e1117; }
/* Style for the two white rectangles */
.white-rectangle {
background-color: white;
height: 60px;
margin: 15px 0;
border: 1px solid #ddd;
border-radius: 5px;
width: 100%;
}
body[data-theme="dark"] .white-rectangle {
background-color: #eee;
border: 1px solid #555;
}
/* --- Dark Mode Adjustments --- */
body[data-theme="dark"] .agent-text-bubble { background-color: #444; color: #eee; }
body[data-theme="dark"] .agent-text-bubble.affirmative { background-color: #385d4a; }
body[data-theme="dark"] .agent-text-bubble.negative { background-color: #68d43; }
body[data-theme="dark"] .agent-text-bubble.judge { background-color: #5a436b; }
</style>
""", unsafe_allow_html=True)
# --- Session State Initialization ---
if 'debate_started' not in st.session_state:
st.session_state.debate_started = False
if 'debate_finished' not in st.session_state:
st.session_state.debate_finished = False
if 'orchestrator' not in st.session_state:
st.session_state.orchestrator = None
if 'debate_history' not in st.session_state:
st.session_state.debate_history = deque(maxlen=500)
if 'retriever' not in st.session_state:
st.session_state.retriever = None
if 'agent_configs' not in st.session_state:
st.session_state.agent_configs = []
if 'status_message' not in st.session_state:
st.session_state.status_message = "Configure and start the debate."
if 'agent_statuses' not in st.session_state:
st.session_state.agent_statuses = {}
# --- Session State for Threading ---
if 'debate_result_queue' not in st.session_state:
st.session_state.debate_result_queue = queue.Queue()
if 'debate_step_processing' not in st.session_state:
st.session_state.debate_step_processing = False
# --- Function to run one step of the debate generator in a thread ---
def run_debate_step(generator, result_queue):
"""Gets the next item from the generator and puts it in the queue."""
try:
item = next(generator, None)
time.sleep(0.01)
result_queue.put({"item": item, "error": None})
except Exception as e:
result_queue.put({"item": None, "error": e})
# --- Dynamic Agent Configuration ---
def create_dynamic_agent_configs(num_pairs, include_judge):
total_debaters = num_pairs * 2
total_agents = total_debaters + (1 if include_judge else 0)
if total_agents == 0:
st.warning("No agents configured. Adjust number of pairs or include judge.")
return []
if total_agents > len(SOUTH_INDIAN_NAMES):
st.warning(f"Not enough names provided. Need {total_agents} but only have {len(SOUTH_INDIAN_NAMES)}. Reusing names.")
selected_names = [SOUTH_INDIAN_NAMES[i % len(SOUTH_INDIAN_NAMES)] for i in range(total_agents)]
else:
selected_names = random.sample(SOUTH_INDIAN_NAMES, total_agents)
random.shuffle(selected_names)
available_photos = [f"images/{f}" for f in os.listdir("images") if os.path.isdir("images") and f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp'))]
if not available_photos:
st.error("No image files found in the ./images directory!")
return []
if total_agents > len(available_photos):
st.warning(f"Not enough valid image files found in ./images. Need {total_agents} but only found {len(available_photos)}. Reusing photos.")
selected_photos = [available_photos[i % len(available_photos)] for i in range(total_agents)]
else:
selected_photos = random.sample(available_photos, total_agents)
random.shuffle(selected_photos)
agent_configs = []
name_idx = 0
photo_idx = 0
# Create Debaters (Affirmative/Negative)
aff_debaters_cfg = []
neg_debaters_cfg = []
for i in range(num_pairs):
aff_name = selected_names[name_idx]
aff_photo = selected_photos[photo_idx]
aff_debaters_cfg.append({
'type': 'AffirmativeAgent', 'name': aff_name,
'model': DEFAULT_MODEL, 'agent_photo': aff_photo
})
name_idx += 1
photo_idx += 1
neg_name = selected_names[name_idx]
neg_photo = selected_photos[photo_idx]
neg_debaters_cfg.append({
'type': 'NegativeAgent', 'name': neg_name,
'model': DEFAULT_MODEL, 'agent_photo': neg_photo
})
name_idx += 1
photo_idx += 1
agent_configs.extend(aff_debaters_cfg)
agent_configs.extend(neg_debaters_cfg)
# Create Judge (if included)
judge_cfg = None
if include_judge:
judge_photo_candidates = [p for p in available_photos if 'judge' in os.path.basename(p).lower()]
judge_photo = judge_photo_candidates[0] if judge_photo_candidates else (random.choice(available_photos) if available_photos else None)
if judge_photo:
judge_cfg = {
'type': 'JudgeAgent', 'name': "Judge", # Explicitly set Judge name to "Judge"
'model': DEFAULT_MODEL, 'optional': True, 'agent_photo': judge_photo
}
agent_configs.append(judge_cfg)
else:
st.warning("Could not assign a photo to the Judge agent. Judge will not be included.")
# Initialize agent statuses dictionary based on the created configs
st.session_state.agent_statuses = {cfg['name']: "Waiting..." for cfg in agent_configs}
return agent_configs
# --- RAG Setup Function ---
# ... (Keep setup_rag function as is)
def setup_rag(enable):
st.session_state.status_message = "Setting up Knowledge Base (RAG)..."
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message})
st.rerun()
if enable:
try:
kb_dir_exists_and_not_empty = os.path.exists(KB_DIRECTORY) and os.path.isdir(KB_DIRECTORY) and len(os.listdir(KB_DIRECTORY)) > 0
if not kb_dir_exists_and_not_empty:
st.session_state.status_message = f"RAG enabled, but '{KB_DIRECTORY}' is empty or missing. Cannot setup KB."
st.session_state.retriever = None
print(f"RAG Setup failed: KB directory empty or missing {KB_DIRECTORY}")
else:
vector_store = index_knowledge_base(
kb_directory=KB_DIRECTORY,
vector_store_path=VECTOR_STORE_PATH,
embedding_model=EMBEDDING_MODEL,
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP
)
if vector_store:
st.session_state.retriever = get_retriever(vector_store)
if not st.session_state.retriever:
st.session_state.status_message = "Failed to get retriever from vector store. RAG will be disabled."
st.session_state.retriever = None
else:
st.session_state.status_message = "Knowledge Base Setup Complete."
else:
st.session_state.status_message = "Knowledge base indexing failed or no documents found. RAG will be disabled."
st.session_state.retriever = None
except Exception as e:
st.session_state.status_message = f"An error occurred during RAG setup: {e}. RAG will be disabled."
st.session_state.retriever = None
print(f"RAG Setup Error: {e}")
else:
st.session_state.status_message = "RAG disabled by user configuration."
st.session_state.retriever = None
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message})
st.rerun()
# --- Sidebar for controls ---
with st.sidebar:
st.header("Debate Configuration")
st.session_state.topic_input = st.text_area("Debate Topic", st.session_state.get('topic_input', DEBATE_TOPIC), height=100)
st.session_state.rounds_input = st.slider("Number of Rebuttal Rounds", 0, 5, st.session_state.get('rounds_input', NUMBER_OF_REBUTTAL_ROUNDS))
st.session_state.num_agent_pairs = st.slider("Number of Debating Pairs (Affirmative/Negative)", 1, 5, st.session_state.get('num_agent_pairs', 2))
st.session_state.include_judge = st.checkbox("Include Judge Agent", st.session_state.get('include_judge', True))
st.header("Advanced Settings")
st.session_state.enable_rag_toggle = st.checkbox("Enable Knowledge Base (RAG)", st.session_state.get('enable_rag_toggle', ENABLE_RAG))
st.write(f"KB Directory: `{KB_DIRECTORY}`")
st.write(f"Vector Store: `{VECTOR_STORE_PATH}`")
st.write(f"Embedding Model: `{EMBEDDING_MODEL}`")
kb_dir_exists_and_not_empty = os.path.exists(KB_DIRECTORY) and os.path.isdir(KB_DIRECTORY) and len(os.listdir(KB_DIRECTORY)) > 0
if st.button("Setup Knowledge Base", disabled = not kb_dir_exists_and_not_empty):
setup_rag(st.session_state.enable_rag_toggle)
if st.session_state.enable_rag_toggle:
if kb_dir_exists_and_not_empty:
if st.session_state.retriever is None:
st.warning("RAG enabled, but KB not loaded. Click 'Setup Knowledge Base'.")
else:
st.success("RAG enabled and KB loaded.")
else:
st.warning(f"RAG enabled, but '{KB_DIRECTORY}' is empty or missing. Add PDF documents and click 'Setup Knowledge Base'.")
else:
st.info("RAG is disabled.")
total_required_photos = (st.session_state.num_agent_pairs * 2) + (1 if st.session_state.include_judge else 0)
available_photos_count = len([f for f in os.listdir("images") if os.path.isdir("images") and f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp'))]) if os.path.exists("images") else 0
can_create_agents = (st.session_state.num_agent_pairs * 2 + (1 if st.session_state.include_judge else 0)) > 0 and available_photos_count > 0
start_button_disabled = st.session_state.debate_started or (st.session_state.enable_rag_toggle and kb_dir_exists_and_not_empty and st.session_state.retriever is None) or not can_create_agents or st.session_state.debate_step_processing
if st.button("Start Debate", disabled=start_button_disabled):
st.session_state.debate_started = True
st.session_state.debate_finished = False
st.session_state.debate_history = deque(maxlen=500) # Clear history
st.session_state.status_message = "Initializing debate..."
st.session_state.debate_step_processing = False # Ensure this is reset
st.session_state.agent_configs = create_dynamic_agent_configs(
st.session_state.num_agent_pairs,
st.session_state.include_judge
)
if not st.session_state.agent_configs: # Check if config creation failed (due to names/photos)
st.session_state.status_message = "Agent configuration failed. Cannot start debate."
st.session_state.debate_started = False
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message})
st.rerun()
else:
all_agents = []
retriever_to_pass = st.session_state.retriever if st.session_state.enable_rag_toggle else None
agent_map = {
'AffirmativeAgent': AffirmativeAgent,
'NegativeAgent': NegativeAgent,
'JudgeAgent': JudgeAgent,
}
# Agent statuses are initialized in create_dynamic_agent_configs now
# st.session_state.agent_statuses = {cfg['name']: "Waiting..." for cfg in st.session_state.agent_configs}
try:
for agent_cfg in st.session_state.agent_configs:
agent_type = agent_cfg['type']
AgentClass = agent_map[agent_type]
agent_name = agent_cfg['name']
agent_model = agent_cfg['model']
agent_photo = agent_cfg['agent_photo']
if agent_type in ['AffirmativeAgent', 'NegativeAgent']:
agent_instance = AgentClass(agent_name, model=agent_model, retriever=retriever_to_pass, agent_photo=agent_photo)
elif agent_type == 'JudgeAgent':
agent_instance = AgentClass(agent_name, model=agent_model, agent_photo=agent_photo)
else:
raise ValueError(f"Unknown agent type {agent_type}")
all_agents.append(agent_instance)
debate_state = DebateState(topic=st.session_state.topic_input)
st.session_state.orchestrator = DebateOrchestrator(
name="The Moderator",
debate_state=debate_state,
agents=all_agents,
model=DEFAULT_MODEL
)
st.session_state.debate_generator = st.session_state.orchestrator.run_debate(st.session_state.rounds_input)
st.session_state.debate_step_processing = False # Ensure this is False initially
st.rerun()
except Exception as e:
st.session_state.status_message = f"Error initializing debate or agents: {e}"
st.session_state.debate_started = False
st.session_state.orchestrator = None
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message + " -- Setup failed."})
print(f"Initialization Error: {e}")
st.rerun()
if st.session_state.debate_started:
if st.button("Stop Debate"):
st.session_state.debate_started = False
st.session_state.status_message = "Debate manually stopped."
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message})
st.session_state.agent_statuses = {name: "Stopped" for name in st.session_state.agent_statuses}
st.session_state.debate_step_processing = False
st.rerun()
if st.button("Clear Debate History"):
st.session_state.debate_history = deque(maxlen=500)
st.session_state.debate_finished = False
st.session_state.debate_started = False
st.session_state.status_message = "Debate history cleared."
st.session_state.orchestrator = None
st.session_state.agent_statuses = {}
st.session_state.debate_step_processing = False
st.rerun()
# --- Main Content Area (Contains everything to the right of the sidebar) ---
# This area will be split into two columns: Agent Visualization (Left) and Chat Panel (Right)
# Use st.columns to create these two main columns after the sidebar
# Adjust the ratios based on desired width
# Try [1, 1] for equal width initially, or [2, 1] if you want agents wider
agent_viz_col, chat_panel_col = st.columns([2, 1])
# --- Left Main Column: Agent Visualization Area + Top Elements ---
with agent_viz_col:
# Place Title, Status, Topic, Rule here in the left column
st.markdown("<div class='main-app-title'>Autonomous AI Debating Society</div>", unsafe_allow_html=True)
st.markdown(f"<p class='main-status-message'>{st.session_state.status_message}</p>", unsafe_allow_html=True)
st.header(st.session_state.get('topic_input', DEBATE_TOPIC))
st.markdown("---")
# Add the two horizontal white rectangles placeholder from the image
#st.markdown("<div class='white-rectangle'></div>", unsafe_allow_html=True)
#st.markdown("<div class='white-rectangle'></div>", unsafe_allow_html=True)
# Create a container for the agent photos and statuses visualization below rectangles
# This is the area with the 2x2 + 1 layout
st.markdown("<div class='agent-viz-area'>", unsafe_allow_html=True)
# Separate debaters from the judge for layout within this area
aff_debaters_cfg = [a for a in st.session_state.agent_configs if a['type'] == 'AffirmativeAgent']
neg_debaters_cfg = [a for a in st.session_state.agent_configs if a['type'] == 'NegativeAgent']
judge_cfg = next((a for a in st.session_state.agent_configs if a['type'] == 'JudgeAgent'), None)
# Display Debaters (Affirmative on left, Negative on right within this area's columns)
num_aff_debaters = len(aff_debaters_cfg)
num_neg_debaters = len(neg_debaters_cfg)
if num_aff_debaters > 0 or num_neg_debaters > 0:
debater_sides_cols = st.columns([1, 1])
with debater_sides_cols[0]: # Affirmative Side Column (Left part of agent-viz-area)
st.markdown("<div style='text-align: center; font-weight: bold;'>Affirmative Team</div>", unsafe_allow_html=True)
for agent_cfg in aff_debaters_cfg:
status_text = st.session_state.agent_statuses.get(agent_cfg['name'], "Waiting...")
st.markdown(f"<div class='agent-status-text'>{status_text}</div>", unsafe_allow_html=True)
photo_src = agent_cfg.get('agent_photo', '')
base64_img_string = get_image_base64(photo_src)
if base64_img_string: img_tag = f'<img src="data:image/png;base64,{base64_img_string}">'
else: img_tag = '<div style="width: 90px; height: 90px; border: 1px solid red; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 0.7em;">Img Error</div>'
st.markdown(f"<div class='agent-container'>{img_tag}<div class='agent-name-text'>{agent_cfg['name']}</div></div>", unsafe_allow_html=True)
with debater_sides_cols[1]: # Negative Side Column (Right part of agent-viz-area)
st.markdown("<div style='text-align: center; font-weight: bold;'>Negative Team</div>", unsafe_allow_html=True)
for agent_cfg in neg_debaters_cfg:
status_text = st.session_state.agent_statuses.get(agent_cfg['name'], "Waiting...")
st.markdown(f"<div class='agent-status-text'>{status_text}</div>", unsafe_allow_html=True)
photo_src = agent_cfg.get('agent_photo', '')
base64_img_string = get_image_base64(photo_src)
if base64_img_string: img_tag = f'<img src="data:image/png;base64,{base64_img_string}">'
else: img_tag = '<div style="width: 90px; height: 90px; border: 1px solid red; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 0.7em;">Img Error</div>'
st.markdown(f"<div class='agent-container'>{img_tag}<div class='agent-name-text'>{agent_cfg['name']}</div></div>", unsafe_allow_html=True)
# Display Judge Centered Below Debaters within the Agent Display Area
if judge_cfg:
center_judge_cols = st.columns([1, 2, 1])
with center_judge_cols[1]:
st.markdown("<div style='text-align: center; font-weight: bold; margin-top: 20px;'>Judge</div>", unsafe_allow_html=True)
status_text = st.session_state.agent_statuses.get(judge_cfg['name'], "Waiting...")
st.markdown(f"<div class='agent-status-text' style='text-align: center;'>{status_text}</div>", unsafe_allow_html=True)
photo_src = judge_cfg.get('agent_photo', '')
base64_img_string = get_image_base64(photo_src)
if base64_img_string:
img_tag = f'<img src="data:image/png;base64,{base64_img_string}">'
else:
img_tag = '<div style="width: 90px; height: 90px; border: 1px solid red; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 0.7em;">Img Error</div>'
st.markdown(f"<div class='agent-container'>{img_tag}<div class='agent-name-text'>{judge_cfg['name']}</div></div>", unsafe_allow_html=True)
# Close the agent display area wrapper div
st.markdown("</div>", unsafe_allow_html=True)
# --- Right Main Column: Chat History Area (Styled as a Panel) ---
with chat_panel_col: # This column will contain the chat header and container
st.markdown("<div class='chat-panel-column'></div>", unsafe_allow_html=True)
st.header("Debate Chat")
chat_container = st.container(height=1000, border=True, key="chat_history_container")
with chat_container:
for item in reversed(st.session_state.debate_history):
# Initialize bubble_class and avatar_img_tag for each item in the loop
bubble_class = ""
avatar_img_tag = '<div class="avatar" style="border: 1px solid red; border-radius: 50%; flex-shrink: 0; display: flex; justify-content: center; align-items: center; font-size: 0.6em;">Err</div>'
if item["type"] == "status":
st.markdown(f"<div class='status-message'>{item['message']}</div>", unsafe_allow_html=True)
elif item["type"] == "stage":
st.markdown(f"<div class='stage-separator'><span>{item['stage_name']}</span></div>", unsafe_allow_html=True)
elif item["type"] == "message":
photo = item.get("photo", '')
if 'affirmative' in item['role'].lower():
bubble_class = "affirmative"
elif 'negative' in item['role'].lower():
bubble_class = "negative"
elif 'judge' in item['role'].lower():
bubble_class = "judge"
base64_avatar_string = get_image_base64(photo)
if base64_avatar_string:
avatar_img_tag = f'<img src="data:image/png;base64,{base64_avatar_string}" class="avatar">'
else:
pass
if item["type"] == "message": # Only render message HTML for items of type 'message'
message_div_class = f"chat-message {bubble_class}"
st.markdown(f"""
<div class="{message_div_class}">
{avatar_img_tag}
<div class="message-content">
<strong>{item['name']}</strong>
<div class="agent-text-bubble {bubble_class}">
{item['text']}
</div>
</div>
</div>
""", unsafe_allow_html=True)
# --- Debate Loop ---
if st.session_state.debate_started and st.session_state.orchestrator and st.session_state.debate_generator and not st.session_state.debate_step_processing:
st.session_state.debate_step_processing = True
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(run_debate_step, st.session_state.debate_generator, st.session_state.debate_result_queue)
# --- Processing Results from Thread ---
if not st.session_state.debate_result_queue.empty():
try:
result = st.session_state.debate_result_queue.get(timeout=0.01)
if result["item"] is not None:
st.session_state.debate_step_processing = False
if result["error"]:
error = result["error"]
st.session_state.debate_started = False
st.session_state.debate_step_processing = False
st.session_state.status_message = f"An error occurred during the debate turn: {error}"
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message + " -- Debate ended."})
print(f"Debate Thread Error: {error}", flush=True)
st.session_state.agent_statuses = {name: "Error" for name in st.session_state.agent_statuses}
st.rerun()
else: # Successfully got an item from the generator
yielded_item = result["item"]
if yielded_item is not None:
item_type = yielded_item.get("type")
agent_name = yielded_item.get("agent_name")
if item_type == "status":
st.session_state.status_message = yielded_item.get("message", "")
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message}) # Store as status
# Update agent status display based on the message
if "speaking..." in yielded_item.get("message", ""):
speaking_name = yielded_item.get("message").split(" speaking...")[0]
if speaking_name in st.session_state.agent_statuses:
for name in st.session_state.agent_statuses:
if name == speaking_name:
st.session_state.agent_statuses[name] = "Speaking..."
elif st.session_state.debate_started:
st.session_state.agent_statuses[name] = "Listening..."
else:
pass
elif "summarizing debate" in yielded_item.get("message", ""):
st.session_state.agent_statuses = {name: "Summarizing..." for name in st.session_state.agent_statuses}
elif "Summary Generated" in yielded_item.get("message", ""):
st.session_state.agent_statuses = {name: "Listening..." for name in st.session_state.agent_statuses}
elif item_type == "stage":
st.session_state.status_message = f"Stage: {yielded_item.get('stage_name', 'Unknown')}"
st.session_state.debate_history.append({"type": "stage", "stage_name": yielded_item.get("stage_name", "Unknown")}) # Store as stage
st.session_state.agent_statuses = {name: "Listening..." for name in st.session_state.agent_statuses}
elif item_type == "argument":
agent_name = yielded_item.get("agent_name")
agent_role = yielded_item.get("agent_role")
argument_text = yielded_item.get("argument", "")
agent_photo = yielded_item.get("agent_photo")
st.session_state.status_message = f"{agent_name} ({agent_role}) finished speaking."
st.session_state.debate_history.append({
"type": "message",
"name": agent_name,
'role': agent_role,
'text': argument_text,
'photo': agent_photo
})
if agent_name in st.session_state.agent_statuses:
st.session_state.agent_statuses[agent_name] = "Waiting..."
elif item_type == "summary":
pass # Handled by status messages
st.rerun()
else:
st.session_state.debate_started = False
st.session_state.debate_finished = True
st.session_state.debate_step_processing = False
st.session_state.status_message = "Debate concluded."
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message})
st.session_state.agent_statuses = {name: "Finished" for name in st.session_state.agent_statuses}
st.rerun()
except queue.Empty:
pass
except Exception as e:
st.session_state.debate_started = False
st.session_state.debate_step_processing = False
st.session_state.status_message = f"An error occurred while processing debate item: {e}"
st.session_state.debate_history.append({"type": "status", "message": st.session_state.status_message + " -- Debate ended."})
print(f"Debate Item Processing Error: {e}", flush=True)
st.session_state.agent_statuses = {name: "Error" for name in st.session_state.agent_statuses}
st.rerun()
# --- End of UI Rendering ---
if st.session_state.debate_finished:
st.balloons()
# --- Optional: Display Streamlit version for debugging ---
# st.sidebar.write(f"Streamlit Version: {st.__version__}")
Conclusion: A Society in Action and Lessons Learned
Building this Autonomous AI Debating System with local tools, knowledge integration, and a functional UI was both complex and rewarding.
Key Takeaways:
-Modular Codebase: Makes large systems manageable and extensible
-Local LLMs Are Practical: Tools like Ollama are fast and dev-friendly
-Prompt Engineering Matters: Small tweaks lead to major improvements
-RAG Unlocks Real Value: Turning generic LLMs into domain experts
-Concurrency Enables UX: Threading + Streamlit = smooth frontend
-Visuals Change Everything: A UI makes this a tool, not just a script
This project now feels like a real platform — something I can demo, improve, and even expand into more advanced multi-agent simulations.
Try It Out
All code is fully open-source and modular. You can clone the repo, run it locally, and modify everything — from the agents’ personalities to how they access knowledge.
👉 GitHub link:Here
👉 Blog Part 1:Here
👉 Blog Part 2:Here
What’s Next?
I’m considering adding:
-Voice synthesis + recognition **for speaking agents.
-Web-based user debate** (human vs. AI).
-Agent memory tuning across multiple debates.
Until then, feel free to explore the full system. All code is open-source and works entirely offline.
If you’re interested in building intelligent systems that think together instead of alone — this is a great playground to start.
Top comments (0)