Creating a Discussion
The simplest task you can accomplish with this library is to create a small discussion between LLMs.
This guide will teach you the basic setup of the library. You will understand how to setup models, user-agents and how to coordinate them in a discussion. By the end of htis guide, you will be able to run a small discussion with a moderator and save it to the disk for persistence.
Basics
The Model
SynDisco can theoretically support any LLM, as long as it is wrapped in a BaseModel wrapper. The BaseModel class is a very simple interface with one method. This method gives the underlying LLM input, and returns its output to the library.
There already exists a TransformersModel class which handles models from the transformers python library. In 90% of your applications, this will be enough. We can load a TransformersModel using the following code:
[2]:
import syndisco
llm = syndisco.TransformersModel(
model_path="unsloth/Llama-3.2-3B-Instruct-bnb-4bit",
name="test_model",
max_out_tokens=100,
)
This will download a small LLM from huggingface. You can substitute the model_path for any similar model in HuggingFace supporting the Transformers library.
Creating personas
All actors can be defined by a persona, aka a set of attributes that define them. These attributes can be age, ethnicity, and even include special instructions on how they should behave.
Creating a persona programmatically is simple:
[3]:
persona_data = [
{
"username": "Emma35",
"age": 38,
"sex": "female",
"education_level": "Bachelor's",
"sexual_orientation": "Heterosexual",
"demographic_group": "Latino",
"current_employment": "Registered Nurse",
"special_instructions": "",
"personality_characteristics": [
"compassionate",
"patient",
"diligent",
"overwhelmed",
],
},
{
"username": "Giannis",
"age": 21,
"sex": "male",
"education_level": "College",
"sexual_orientation": "Pansexual",
"demographic_group": "White",
"current_employment": "Game Developer",
"special_instructions": "",
"personality_characteristics": [
"strategic",
"meticulous",
"nerdy",
"hyper-focused",
],
},
]
personas = [syndisco.Persona(**data) for data in persona_data]
for persona in personas:
print(persona)
{"username": "Emma35", "age": 38, "sex": "female", "sexual_orientation": "Heterosexual", "demographic_group": "Latino", "current_employment": "Registered Nurse", "education_level": "Bachelor's", "special_instructions": "", "personality_characteristics": ["compassionate", "patient", "diligent", "overwhelmed"]}
{"username": "Giannis", "age": 21, "sex": "male", "sexual_orientation": "Pansexual", "demographic_group": "White", "current_employment": "Game Developer", "education_level": "College", "special_instructions": "", "personality_characteristics": ["strategic", "meticulous", "nerdy", "hyper-focused"]}
Since creating a lot of distinct users is essential in running large-scale experiments, users are usually defined in JSON format. That way, you can change anything without touching your code!
Here is an applied example of how to mass-define user personas through JSON files.
Creating the user-agents
Having a persona and a model we can finally create an actor. The actor will personify the selected persona using the model to talk.
Besides a persona and a model, the actors will also need instructions and a context. By convention, all actors share the same context, and all user-agents share the same instructions. Personalized instructions are defined in the actor’s persona.
[4]:
CONTEXT = "You are taking part in an online conversation"
INSTRUCTIONS = "Act like a human would"
actors = [
syndisco.Actor(
model=llm,
persona=p,
actor_type=syndisco.ActorType.USER,
context=CONTEXT,
instructions=INSTRUCTIONS
)
for p in personas
]
Managing turn-taking
In real-life discussions, who gets to speak at each point in time is determined by complex social dynamics, which are difficult to realistically model. However, there are ways with which we can simulate these dynamics.
SynDisco uses the TurnManager class to model turn taking. Two implementations are available by default: Round Robin, and Random Weighted
Round Robinis the simplest, most intuitive way to model turn-taking; everyone gets to talk once per round. Once everyone talks once, they get to talk again in the same sequence
[5]:
rrobin_turn_manager = syndisco.RoundRobin(["John", "Mary", "Howard", "Todd"])
for i in range(10):
print(next(rrobin_turn_manager))
John
Mary
Howard
Todd
John
Mary
Howard
Todd
John
Mary
RandomWeightedon the other hand throws a weighted coin on each round. If the coin flip succedes, the previous user gets to respond. If not, another user is selected at random
[6]:
rweighted_turn_manager = syndisco.RandomWeighted(
names=["John", "Mary", "Howard", "Todd"], p_respond=0.5
)
for i in range(10):
print(next(rweighted_turn_manager))
John
Howard
John
Howard
Todd
Howard
John
Todd
John
Todd
Generating a discussion
Let’s start with the most basic task; a single discussion between two user-agents.
Since we only have two users, a RoundRobin approach where each user takes a turn sequentially is sufficient.
Now we can run a simple discussion
[7]:
turn_manager = syndisco.RoundRobin([actor.get_name() for actor in actors])
conv = syndisco.Discussion(next_turn_manager=turn_manager, users=actors)
conv.begin()
The following generation flags are not valid and may be ignored: ['temperature', 'top_p']. Set `TRANSFORMERS_VERBOSITY=info` for more details.
User Emma35 posted:
"Hey everyone, I just got back from a long shift at the hospital and
I'm exhausted. Anyone else have a crazy day at work? I feel like I've
been running non-stop since morning. How was your day?"
User Giannis posted:
{"role": "Giannis", "content": "Hey Emma, sorry to hear that you're
exhausted. I had a pretty standard day at the office, just working on
some bug fixes for our new game. Nothing too crazy, but I did have to
troubleshoot a weird issue with the physics engine. Nothing a good
night's sleep couldn't fix, though. How about you, did anything
interesting happen at the hospital?"
User Emma35 posted:
"Hey Giannis, thanks for the concern! I actually had a pretty crazy
day at the hospital. We had a code blue come in and I had to help with
the resuscitation. It was a tough situation, but we managed to get the
patient stabilized. I'm just glad everyone made it through okay. I'm
looking forward to a quiet night at home now. How was the game
development going? Did you guys make any progress on the new level?"
User Giannis posted:
"Hey Emma, I'm glad to hear that the patient is okay. That must have
been a really intense situation. As for the game development, we
actually made some good progress on the new level. We're still
tweaking the difficulty curve, but it's starting to feel like it's
coming together. I'm thinking of adding some more environmental
hazards to make it more challenging. What do you think? Should we add
more obstacles or keep it relatively straightforward?"
User Emma35 posted:
"Hey Giannis, I'm glad to hear that the patient is okay. That must
have been a really intense situation. As for the game development, I
think adding more environmental hazards would definitely make the
level more challenging and exciting. I've always been a fan of games
with obstacles to navigate around, it adds an extra layer of strategy
and makes the gameplay more engaging. Plus, it's always fun to see
players figure out creative ways to overcome the challenges. What kind
of environmental hazards were you
Driving a discussion step by step
Discussion implements Python’s iterator protocol, so you are not limited to running the whole conversation at once with begin(). You can drive it one turn at a time, which is useful when you want to inspect or react to each entry before the next speaker is prompted — for example to apply early stopping, live logging, or conditional branching.
Because Discussion is its own iterator (__iter__ returns self), it is single-pass: once StopIteration is raised the instance is exhausted and should not be reused.
Calling next() manually
Calling iter() on a Discussion performs the one-time setup (registering participant names and inserting any seed opinions). Each subsequent next() prompts one speaker and returns the new log entry as a plain dict.
[8]:
turn_manager = syndisco.RoundRobin([actor.get_name() for actor in actors])
conv = syndisco.Discussion(
next_turn_manager=turn_manager, users=actors, conv_len=4
)
it = iter(conv) # one-time setup happens here
first_entry = next(it)
print("First speaker: ", first_entry["name"])
print("Their message: ", first_entry["text"], "...")
second_entry = next(it)
print("\nSecond speaker:", second_entry["name"])
print("Their message: ", second_entry["text"], "...")
First speaker: Emma35
Their message: "Hey everyone, I just got back from a long shift at the hospital and I'm exhausted. Anyone else have a crazy day at work? I feel like I've been running non-stop since morning. How was your day?" ...
Second speaker: Giannis
Their message: {"role": "Giannis", "content": "Hey Emma, sorry to hear that you're exhausted. I had a pretty standard day at the office, just working on some bug fixes for our new game. Nothing too crazy, but I did have to troubleshoot a weird issue with the physics engine. Nothing a good night's sleep couldn't fix, though. How about you, did anything interesting happen at the hospital?" ...
Using a for loop
The same iterator works directly in a for loop. Here we stop early if a particular word appears in an entry — something that is impossible with begin().
[9]:
turn_manager = syndisco.RoundRobin([actor.get_name() for actor in actors])
conv = syndisco.Discussion(
next_turn_manager=turn_manager, users=actors, conv_len=10
)
STOP_WORD = "hospital"
for entry in conv:
print(f"{entry['name']}: {entry['text']}...")
if STOP_WORD in entry["text"].lower():
print(f"\nStop word '{STOP_WORD}' detected — halting early.")
break
print(f"\nTurns completed so far: {conv._steps_taken}")
Emma35: "Hey everyone, I just got back from a long shift at the hospital and I'm exhausted. Anyone else have a crazy day at work? I feel like I've been running non-stop since morning. How was your day?"...
Stop word 'hospital' detected — halting early.
Turns completed so far: 1
Congratulations, you can now run fully synthetic discussions! You may want to experiment with adding more than 2 users or testing more realistic turn taking procedures (for example, check out the RandomWeighted turn manager).
Working with Logs
All log state is held in Discussion.logs, an instance of Logs. This object supports the standard sequence interface, so you can index, slice, and iterate over it directly - no need to call to_dict() just to inspect entries.
[10]:
# Run a fresh discussion so the logs are fully populated
turn_manager = syndisco.RoundRobin([actor.get_name() for actor in actors])
conv = syndisco.Discussion(next_turn_manager=turn_manager, users=actors, conv_len=4)
conv.begin(verbose=False)
logs = conv.logs
print(f"Total entries : {len(logs)}")
print(f"First entry : {logs[0]}")
print(f"Last entry : {logs[-1]}")
print()
# Iterate directly — each item is a dict with 'name', 'text', 'model'
for entry in logs:
print(f"[{entry['name']}] {entry['text'][:60]}...")
Total entries : 4
First entry : {'name': 'Emma35', 'text': '"Hey everyone, I just got back from a long shift at the hospital and I\'m exhausted. Anyone else have a crazy day at work? I feel like I\'ve been running non-stop since morning. How was your day?"', 'model': 'test_model', 'prompt': '{"context": "You are taking part in an online conversation", "instructions": "Act like a human would", "type": "1", "persona": {"username": "Emma35", "age": 38, "sex": "female", "sexual_orientation": "Heterosexual", "demographic_group": "Latino", "current_employment": "Registered Nurse", "education_level": "Bachelor\'s", "special_instructions": "", "personality_characteristics": ["compassionate", "patient", "diligent", "overwhelmed"]}}'}
Last entry : {'name': 'Giannis', 'text': '"Hey Emma, I\'m glad to hear that the patient is okay. That must have been a really intense situation. As for the game development, we actually made some good progress on the new level. We\'re still tweaking the difficulty curve, but it\'s starting to feel like it\'s coming together. I\'m thinking of adding some more environmental hazards to make it more challenging. What do you think? Should we add more obstacles or keep it relatively straightforward?"', 'model': 'test_model', 'prompt': '{"context": "You are taking part in an online conversation", "instructions": "Act like a human would", "type": "1", "persona": {"username": "Giannis", "age": 21, "sex": "male", "sexual_orientation": "Pansexual", "demographic_group": "White", "current_employment": "Game Developer", "education_level": "College", "special_instructions": "", "personality_characteristics": ["strategic", "meticulous", "nerdy", "hyper-focused"]}}'}
[Emma35] "Hey everyone, I just got back from a long shift at the hosp...
[Giannis] {"role": "Giannis", "content": "Hey Emma, sorry to hear that...
[Emma35] "Hey Giannis, thanks for the concern! I actually had a prett...
[Giannis] "Hey Emma, I'm glad to hear that the patient is okay. That m...
Exporting and importing discussions with Logs
All persistent state in a Discussion is managed by a Logs object. This class you inspect, serialize, or pass saved conversations to an Annotation job without re-running the discussion.
using Logs.from_lists().
from_lists accepts three parallel lists — names, texts, and (optionally) model labels — and is the canonical way to construct a Logs from an external source.
[11]:
import json
import tempfile
tp = tempfile.NamedTemporaryFile(suffix=".json", delete=False)
logs = conv.get_logs()
logs.export(tp.name)
with open(tp.name) as f:
data = json.load(f)
print(json.dumps(data, indent=4))
{
"timestamp": "26-04-03-12-51",
"logs": [
{
"name": "Emma35",
"text": "\"Hey everyone, I just got back from a long shift at the hospital and I'm exhausted. Anyone else have a crazy day at work? I feel like I've been running non-stop since morning. How was your day?\"",
"model": "test_model",
"prompt": "{\"context\": \"You are taking part in an online conversation\", \"instructions\": \"Act like a human would\", \"type\": \"1\", \"persona\": {\"username\": \"Emma35\", \"age\": 38, \"sex\": \"female\", \"sexual_orientation\": \"Heterosexual\", \"demographic_group\": \"Latino\", \"current_employment\": \"Registered Nurse\", \"education_level\": \"Bachelor's\", \"special_instructions\": \"\", \"personality_characteristics\": [\"compassionate\", \"patient\", \"diligent\", \"overwhelmed\"]}}"
},
{
"name": "Giannis",
"text": "{\"role\": \"Giannis\", \"content\": \"Hey Emma, sorry to hear that you're exhausted. I had a pretty standard day at the office, just working on some bug fixes for our new game. Nothing too crazy, but I did have to troubleshoot a weird issue with the physics engine. Nothing a good night's sleep couldn't fix, though. How about you, did anything interesting happen at the hospital?\"",
"model": "test_model",
"prompt": "{\"context\": \"You are taking part in an online conversation\", \"instructions\": \"Act like a human would\", \"type\": \"1\", \"persona\": {\"username\": \"Giannis\", \"age\": 21, \"sex\": \"male\", \"sexual_orientation\": \"Pansexual\", \"demographic_group\": \"White\", \"current_employment\": \"Game Developer\", \"education_level\": \"College\", \"special_instructions\": \"\", \"personality_characteristics\": [\"strategic\", \"meticulous\", \"nerdy\", \"hyper-focused\"]}}"
},
{
"name": "Emma35",
"text": "\"Hey Giannis, thanks for the concern! I actually had a pretty crazy day at the hospital. We had a code blue come in and I had to help with the resuscitation. It was a tough situation, but we managed to get the patient stabilized. I'm just glad everyone made it through okay. I'm looking forward to a quiet night at home now. How was the game development going? Did you guys make any progress on the new level?\"",
"model": "test_model",
"prompt": "{\"context\": \"You are taking part in an online conversation\", \"instructions\": \"Act like a human would\", \"type\": \"1\", \"persona\": {\"username\": \"Emma35\", \"age\": 38, \"sex\": \"female\", \"sexual_orientation\": \"Heterosexual\", \"demographic_group\": \"Latino\", \"current_employment\": \"Registered Nurse\", \"education_level\": \"Bachelor's\", \"special_instructions\": \"\", \"personality_characteristics\": [\"compassionate\", \"patient\", \"diligent\", \"overwhelmed\"]}}"
},
{
"name": "Giannis",
"text": "\"Hey Emma, I'm glad to hear that the patient is okay. That must have been a really intense situation. As for the game development, we actually made some good progress on the new level. We're still tweaking the difficulty curve, but it's starting to feel like it's coming together. I'm thinking of adding some more environmental hazards to make it more challenging. What do you think? Should we add more obstacles or keep it relatively straightforward?\"",
"model": "test_model",
"prompt": "{\"context\": \"You are taking part in an online conversation\", \"instructions\": \"Act like a human would\", \"type\": \"1\", \"persona\": {\"username\": \"Giannis\", \"age\": 21, \"sex\": \"male\", \"sexual_orientation\": \"Pansexual\", \"demographic_group\": \"White\", \"current_employment\": \"Game Developer\", \"education_level\": \"College\", \"special_instructions\": \"\", \"personality_characteristics\": [\"strategic\", \"meticulous\", \"nerdy\", \"hyper-focused\"]}}"
}
],
"id": "b6d2e9c0424b0217b6433ec93450ac811eb456b5f02ede8783b13625e82d6649"
}
[12]:
logs2 = syndisco.Logs.from_file(tp.name)
print(logs2)
{
"timestamp": "26-04-03-12-51",
"logs": [
{
"name": "Emma35",
"text": "\"Hey everyone, I just got back from a long shift at the hospital and I'm exhausted. Anyone else have a crazy day at work? I feel like I've been running non-stop since morning. How was your day?\"",
"model": "test_model",
"prompt": ""
},
{
"name": "Giannis",
"text": "{\"role\": \"Giannis\", \"content\": \"Hey Emma, sorry to hear that you're exhausted. I had a pretty standard day at the office, just working on some bug fixes for our new game. Nothing too crazy, but I did have to troubleshoot a weird issue with the physics engine. Nothing a good night's sleep couldn't fix, though. How about you, did anything interesting happen at the hospital?\"",
"model": "test_model",
"prompt": ""
},
{
"name": "Emma35",
"text": "\"Hey Giannis, thanks for the concern! I actually had a pretty crazy day at the hospital. We had a code blue come in and I had to help with the resuscitation. It was a tough situation, but we managed to get the patient stabilized. I'm just glad everyone made it through okay. I'm looking forward to a quiet night at home now. How was the game development going? Did you guys make any progress on the new level?\"",
"model": "test_model",
"prompt": ""
},
{
"name": "Giannis",
"text": "\"Hey Emma, I'm glad to hear that the patient is okay. That must have been a really intense situation. As for the game development, we actually made some good progress on the new level. We're still tweaking the difficulty curve, but it's starting to feel like it's coming together. I'm thinking of adding some more environmental hazards to make it more challenging. What do you think? Should we add more obstacles or keep it relatively straightforward?\"",
"model": "test_model",
"prompt": ""
}
],
"id": "163bc0db2cb8cef56593c289519ef634230dda045a73160854bd97f694904cbb"
}
[13]:
names=["Alice", "Bob", "Alice"]
texts=[
"What do you think about the new policy?",
"Honestly, I think it goes too far.",
"I can see that perspective, though I'm not sure I agree.",
]
handcrafted = syndisco.Logs()
for name, text in zip(names, texts):
handcrafted.append(name, text)
print(handcrafted)
{
"timestamp": "26-04-03-12-51",
"logs": [
{
"name": "Alice",
"text": "What do you think about the new policy?",
"model": "hardcoded",
"prompt": ""
},
{
"name": "Bob",
"text": "Honestly, I think it goes too far.",
"model": "hardcoded",
"prompt": ""
},
{
"name": "Alice",
"text": "I can see that perspective, though I'm not sure I agree.",
"model": "hardcoded",
"prompt": ""
}
],
"id": "c148dc4f3a8d305962c48bd967dcb5aaa85ee136fafb7acf2b1479be5b596d74"
}