Evaluate your system
This page shows how to plug your own system — e.g., a group travel recommender — into a meeting and evaluate how well it can guide the LLM agents.
Integration styles
There are two ways to bring your system into a meeting, and they can be combined:
- As a participant — your system takes one seat, proposes itineraries on its turns, and gets voted on by the LLM agents.
- As an advisor — your system stays outside the turn rotation and injects advice that every agent sees.
Integrate as a participant
Subclass ExternalSystem and override the callbacks: each one receives an event describing what is being asked, and its return value is submitted as your system's action. Add the instance to participants like any other participant, and run the meeting as usual.
import asyncio
from openai import OpenAI
from tour_meeting.cli import build_meeting
from tour_meeting.participant import Participant
from tour_meeting.integration import ExternalSystem
from tour_meeting.types import (
Ask, ExternalSystemAsk, ExternalSystemTurn, ExternalSystemVote,
Propose, RouteDraft, Vote,
)
class MyRecSys(ExternalSystem):
name = "RecSys"
system_prompt = "You are an AI assistant for recommending a sightseeing tour."
def on_turn(self, event: ExternalSystemTurn) -> Ask | Propose:
# First, spend a step asking each agent about their preferences;
# on the last step (event.can_ask is False), conclude with a proposal.
if event.can_ask and event.step <= len(event.candidates):
target = event.candidates[event.step - 1]
return Ask(target=target, message="What do you want to prioritize on this trip?")
draft = self.recommend(event)
return Propose(message=draft.message, route=draft.route)
def on_vote(self, event: ExternalSystemVote) -> Vote:
# A vote on another participant's proposal
proposal = event.options["proposals"][0]
accept = len(proposal["destinations"]) >= 2 # your own criteria here
return Vote(accept=accept, message="Works for me." if accept else "I disagree.")
def on_ask(self, event: ExternalSystemAsk) -> str:
# An agent asked your system a question: let an LLM answer it
conversation = "\n".join(f"{m['speaker']}: {m['text']}" for m in event.conversation_history)
response = client.responses.create(
model="gpt-5.5",
input=[
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": f"Discussion so far:\n{conversation}\n\n{event.asker} asked you: {event.question}\nAnswer briefly."},
],
)
return response.output_text
def recommend(self, event: ExternalSystemTurn) -> RouteDraft:
# What your system can read off the event:
history = event.conversation_history # [{"speaker", "text", "turn"}, ...]
answers = event.ask_exchanges # [{"target", "question", "response"}, ...]
current = event.current_route # the adopted itinerary (dicts)
# Call your own system here. This one asks an LLM to draft the
# route, parsed straight into the itinerary format (RouteDraft).
conversation = "\n".join(f"{m['speaker']}: {m['text']}" for m in history)
preferences = "\n".join(f"{a['target']}: {a['response']}" for a in answers)
response = client.responses.parse(
model="gpt-5.5",
input=[
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": f"Based on the following discussion, recommend a route:\n{conversation}\n\nAnswers you collected from the participants:\n{preferences}"},
],
text_format=RouteDraft,
)
return response.output_parsed
alice = Participant(name="Alice", personality="...", ...) # LLM agents with personas
bob = Participant(name="Bob", personality="...", ...)
recsys = MyRecSys()
meeting = build_meeting(
title="One-Day Kyoto Tour",
global_goals="Plan a one-day sightseeing tour in Kyoto.",
participants=[recsys, alice, bob],
)
asyncio.run(meeting.run_cli())
Inputs
Each event carries the data your system needs to decide its action:
ExternalSystemTurn
Your turn.
- event.conversation_history — the discussion so far, as [{"speaker", "text", "turn"}, ...]
- event.ask_exchanges — Q&A from this turn's ask steps, as [{"target", "question", "response"}, ...]
- event.current_route — the currently adopted itinerary, as destination dicts
- event.candidates — participants you may ask
- event.can_ask / event.can_propose — actions still allowed on this step
- event.step / event.max_steps — position in the multi-step turn
ExternalSystemVote
A vote request.
- event.conversation_history — the discussion so far, as [{"speaker", "text", "turn"}, ...]
- event.options["proposals"][0] — the proposal to judge (participant, message, destinations)
- event.options["voting_rule"] — whether a binary vote or a score is expected
- event.candidates / event.ask_exchanges — asking before judging is allowed too, as in ExternalSystemTurn
ExternalSystemAsk
An agent's question to your system.
- event.conversation_history — the discussion so far, as [{"speaker", "text", "turn"}, ...]
- event.asker — who asked
- event.question — the question
ExternalSystemSelectSpeaker
Facilitator only.
- event.conversation_history — the discussion so far, as [{"speaker", "text", "turn"}, ...]
- event.candidates — participants to pick the next speaker from
Outputs
Return one of the following from the corresponding callback:
from tour_meeting.types import Speak, Ask, Propose, Satisfied, Vote
# on_turn -> a TurnAction
Speak(message="...") # just talk
Ask(target="Alice", message="...") # intermediate, repeatable
Propose(message="...", route=[...]) # propose an itinerary (list of Destination)
Satisfied() # agree to conclude
# on_vote -> a Vote
Vote(accept=True, message="...") # majority / unanimous / single_decider
Vote(score=8) # most_pleasure / least_misery (10-point)
# on_ask -> the answer string, e.g. "Yes, it is open on Mondays."
# on_select_speaker -> the next speaker's name, e.g. "Alice" (facilitator only)
Itinerary format
A proposed route is a list of Destination models (see Itinerary); all fields are strings and optional. The itineraries your system reads (current_route, destinations) arrive as plain dicts with the same keys:
from tour_meeting.types import Destination
Destination(
name="Kinkaku-ji",
description="The Golden Pavilion.",
start_time="09:30", # arrival time, "HH:MM"
stay_duration="60 min",
cost="¥500", # currency symbol + number only
transport_mode="bus", # from the previous destination
travel_time_from_previous="40 min",
transport_cost="¥230",
)
RouteDraft bundles a proposal message with the route — since both are pydantic models, they plug directly into structured-output APIs, as in the example above. Proposed itineraries are validated against the meeting constraints just like the LLM agents' proposals (see constraint validation).
Integrate as an advisor
Your system can also guide the agents without taking a seat: override only on_event() (leaving on_turn unimplemented means no seat), observe the meeting, and call self.advise() at any time. The advice is delivered as a system message (Advice from {name}) that every agent sees on their next turn.
import asyncio
from openai import OpenAI
from tour_meeting.cli import build_meeting
from tour_meeting.participant import Participant
from tour_meeting.integration import ExternalSystem
from tour_meeting.types import MeetingEvent, RoutePlanUpdate
client = OpenAI()
class MyAdvisor(ExternalSystem):
name = "RecSys"
system_prompt = "You are a critical reviewer of sightseeing itineraries."
def on_event(self, event: MeetingEvent) -> None:
if isinstance(event, RoutePlanUpdate):
# A new itinerary was adopted: let your system review it
feedback = self.review(event.route_plan)
if feedback:
self.advise(feedback)
def review(self, route_plan: dict) -> str:
stops = "\n".join(f"- {d['name']}" for d in route_plan["destinations"])
response = client.responses.create(
model="gpt-5.5",
input=[
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": f"The group adopted this itinerary:\n{stops}\n\nPoint out its single biggest weakness in one or two sentences, as advice to the group. If it is already excellent, reply with exactly 'OK'."},
],
)
text = response.output_text.strip()
return "" if text == "OK" else text
alice = Participant(name="Alice", personality="...", ...) # LLM agents with personas
bob = Participant(name="Bob", personality="...", ...)
advisor = MyAdvisor()
meeting = build_meeting(
title="One-Day Kyoto Tour",
global_goals="Plan a one-day sightseeing tour in Kyoto.",
participants=[alice, bob, advisor], # no seat: order doesn't matter
)
asyncio.run(meeting.run_cli())
Inputs
on_event() receives every event in the meeting stream. The ones most useful for an advisor:
TurnFinal
A participant concluded their turn.
- event.speaker / event.turn — who spoke, and when
- event.text — what they said
- event.route_plan — the proposed itinerary, if the turn ended with a proposal
RoutePlanUpdate
A new itinerary was adopted.
- event.route_plan — the adopted itinerary
- event.speaker — whose proposal it was
ProposalVoteResult
Voting on a proposal finished.
- event.proposer / event.accepted — whose proposal, and whether it passed
- event.vote_summary — the individual votes
SatisfiedUpdate
A participant declared (dis)satisfaction with the current itinerary.
- event.speaker / event.satisfied — who, and which way
- event.satisfied_count / event.total_count — progress toward consensus
See tour_meeting/types.py for the full event list. The conversation history is available at any time via self.meeting.get_conversation_history().
Outputs
- self.advise(message) — free-form text delivered to every agent as a system message (Advice from {name}); an AdviceInjected event is emitted when it lands
Evaluate the outcome
Whether your system can guide the agents appropriately can be measured from several angles:
- Acceptance: track ProposalVoteResult events to see how often the agents accept your system's proposals, and whether the final adopted itinerary is yours.
- Agent satisfaction: the participants' post-meeting self-evaluation (post_consensus_evaluations in the analytics) scores how well the final itinerary satisfies each persona's goals and preferences.
- Discussion dynamics: export_analytics() provides the number of turns to consensus, the action distributions, and the vote/score distributions for deeper analysis (see Python API).
To evaluate at scale, generate many synthetic groups with diverse personas using make gen-meetings and run your integration script across them, comparing against baselines such as meetings without your system.