Agents

Agent objects are used to simulate survey responses for target audiences. They are created with specified traits, such as personas and relevant attributes for a survey, that are used together with language models to generate answers to questions.

Constructing an Agent

An Agent is created by passing a dictionary of traits for a language model to reference in answering questions. Traits can be anything that might be relevant to the questions the agent will be asked, and constructed with single values or textual narratives.

For example:

from edsl import Agent

traits_dict = {
    "persona": "You are an expert in machine learning.",
    "age": 45,
    "home_state": "Massachusetts"
}
a = Agent(traits = traits_dict)

Note that traits= must be named explicitly in the construction, and the traits must use Python identifiers as keys (e.g., home_state but not home state or home-state).

Agent names

We can optionally give an agent a name when it is constructed:

a = Agent(name = "Ada", traits = traits_dict)

If a name is not passed when the agent is created, an agent_name field is automatically added to results when a survey is administered to the agent. This field is a unique identifier for the agent and can be used to filter or group results by agent.

Note that trying to create two agents with the same name or trying to include the name in the traits will raise an error.

Agent lists

Agents can be created collectively and administered a survey together. This is useful for comparing responses across agents.

For example, here we create a list of agents with each combination of listed trait dimensions:

from edsl import Agent, AgentList

ages = [10, 20, 30, 40, 50]
locations = ["New York", "California", "Texas", "Florida", "Washington"]

agents = AgentList(
    Agent(traits = {"age": age, "location": location}) for age, location in zip(ages, locations)
)

This code will create a list of agents with different ages and locations, which can then be used in a survey.

Example code for running a survey with the agents:

from edsl import Survey

survey = Survey.example()

results = survey.by(agents).run()

This will generate a Results object that contains a Result for each agent’s responses to the survey questions. Learn more about working with results in the Results section.

Dynamic traits function

Agents can also be created with a dynamic_traits_function parameter. This function can be used to generate traits dynamically based on the question being asked or the scenario in which the question is asked.

For example:

def dynamic_traits_function(question):
    if question.question_name == "age":
        return {"age": 10}
    elif question.question_name == "hair":
        return {"hair": "brown"}

a = Agent(dynamic_traits_function = dynamic_traits_function)

When the agent is asked a question about age, the agent will return an age of 10. When asked about hair, the agent will return “brown”. This can be useful for creating agents that can answer questions about different topics without including potentially irrelevant traits in the agent’s traits dictionary.

Note that the traits returned by the function are not added to the agent’s traits dictionary.

Agent direct-answering methods

Agents can also be created with a method that can answer a particular question type directly:

from edsl import Agent

a = Agent()

def f(self, question, scenario): return "I am a direct answer."

a.add_direct_question_answering_method(f)
a.answer_question_directly(question = None, scenario = None)

Output:

I am a direct answer.

This can be useful for creating agents that can answer questions directly without needing to use a language model.

Giving an agent instructions

In addition to traits, agents can be given detailed instructions on how to answer questions.

For example:

from edsl import Agent

a = Agent(traits = {"age": 10}, instruction = "Answer in German.")
a.instruction

Output:

Answer in German.

When the agent is assigned to a survey, the special instruction will be added to the prompts for generating responses. The instructions are stored in the instruction field of the agent and can be accessed directly in results.

Learn more about how to use instructions in the Prompts section.

Controlling the presentation of the persona

The traits_presentation_template parameter can be used to create a narrative persona for an agent. This is a template string that can be rendered with the agent’s traits as variables.

For example:

a = Agent(traits = {'age': 22, 'hair': 'brown', 'gender': 'female'},
    traits_presentation_template = "I am a {{ age }} year-old {{ gender }} with {{ hair }} hair.")

a.agent_persona.render(primary_replacement = a.traits)

Output:

I am a 22 year-old female with brown hair.

Note that the trait keys must be valid Python identifiers (e.g., home_state but not home state or home-state). This can be handled by using a dictionary with string keys and values, for example:

from edsl import Agent

codebook = {'age': 'The age of the agent'}

a = Agent(traits = {'age': 22},
    codebook = codebook,
    traits_presentation_template = "{{ codebook['age'] }} is {{ age }}.")

a.agent_persona.render(primary_replacement = a.traits)

Output:

The age of the agent is 22.

Note that it can be helpful to include traits mentioned in the persona as independent keys and values in order to analyze survey results by those dimensions individually. For example, we may want the narrative to include a sentence about the agent’s age, but also be able to readily analyze or filter results by age.

The following code will include the agent’s age as a column of a table with any other selected components (e.g., agent name and all the answers):

results.select("agent.age", "agent.agent_name", "answer.*").print()

Note that the prefix “agent” can also be dropped. The following code is equivalent:

results.select("age", "agent_name", "answer.*").print()

We can filter the results by an agent’s traits:

results.filter("age == 22").print()

We can also call the filter() method on an agent list to filter agents by their traits:

middle_aged_agents = agents.filter("40 <= age <= 60")

Using agent traits in prompts

The traits of an agent can be used in the prompts of questions.

For example:

from edsl import Agent, QuestionFreeText

a = Agent(traits = {'first_name': 'John'})

q = QuestionFreeText(
    question_text = 'What is your last name, {{ agent.first_name }}?',
    question_name = "exmaple"
)

job = q.by(a)
job.prompts().select("user_prompt").print(format="rich")

Output:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ user_prompt                   ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ What is your last name, John? │
└───────────────────────────────┘

Learn more about user and system prompts in the Prompts section.

Accessing agent traits

The traits of an agent can be accessed directly:

from edsl import Agent

a = Agent(traits = {'age': 22})
a.traits

Output:

{'age': 22}

The traits of an agent can also be accessed as attributes of the agent:

from edsl import Agent

a = Agent(traits = {'age': 22})
a.age

Output:

22

Simulating agent responses

When a survey is run, agents can be assigned to it using the by method, which can be chained with other components like scenarios and models:

from edsl import Agent, QuestionList, QuestionMultipleChoice, Survey

agent = Agent(
    name = "college student",
    traits = {
        "persona": "You are a sophomore at a community college in upstate New York.",
        "year": "sophomore",
        "school": "community college",
        "major": "biology",
        "state": "New York"
    }
)

q1 = QuestionList(
    question_name = "favorite_courses",
    question_text = "What are the names of your 3 favorite courses?",
    max_list_items = 3
)

q2 = QuestionMultipleChoice(
    question_name = "attend_grad_school",
    question_text = "Do you plan to attend grad school?",
    question_options = ["Yes", "No", "Undecided"]
)

survey = Survey([q1, q2])

results = survey.by(agent).run()

This will generate a Results object that contains a Result for each agent’s responses to the survey questions. We can select and inspect components of the results, such as the agent’s traits and their answers:

results.select("persona", "year", "school", "major", "state", "answer.*").print()

Output:

┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ agent             ┃ agent     ┃ agent             ┃ agent   ┃ agent    ┃ answer            ┃ answer             ┃
┃ .persona          ┃ .year     ┃ .school           ┃ .major  ┃ .state   ┃ .favorite_courses ┃ .attend_grad_scho… ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ You are a         │ sophomore │ community college │ biology │ New York │ ['General Biology │ Undecided          │
│ sophomore at a    │           │                   │         │          │ II', 'Organic     │                    │
│ community college │           │                   │         │          │ Chemistry I',     │                    │
│ in upstate New    │           │                   │         │          │ 'Environmental    │                    │
│ York.             │           │                   │         │          │ Science']         │                    │
└───────────────────┴───────────┴───────────────────┴─────────┴──────────┴───────────────────┴────────────────────┘

If multiple agents will be used with a survey, they are passed as a list in the same by call:

from edsl import Agent, AgentList

agents = AgentList([
    Agent(traits = {"major": "biology", "year": "sophomore"}),
    Agent(traits = {"major": "history", "year": "junior"}),
    Agent(traits = {"major": "mathematics", "year": "senior"}),
])

results = survey.by(agents).run() # using the same survey as above

results.select("major", "year", "answer.*").print()

Output:

┏━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ agent       ┃ agent     ┃ answer                                                          ┃ answer              ┃
┃ .major      ┃ .year     ┃ .favorite_courses                                               ┃ .attend_grad_school ┃
┡━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ biology     │ sophomore │ ['Genetics', 'Ecology', 'Cell Biology']                         │ Undecided           │
├─────────────┼───────────┼─────────────────────────────────────────────────────────────────┼─────────────────────┤
│ mathematics │ senior    │ ['Real Analysis', 'Abstract Algebra', 'Topology']               │ Undecided           │
├─────────────┼───────────┼─────────────────────────────────────────────────────────────────┼─────────────────────┤
│ history     │ junior    │ ['History of Ancient Civilizations', 'Medieval European         │ Undecided           │
│             │           │ History', 'History of Modern Political Thought']                │                     │
└─────────────┴───────────┴─────────────────────────────────────────────────────────────────┴─────────────────────┘

If scenarios and/or models are also specified for a survey, each component type is added in a separate by call that can be chained in any order with the run method appended last:

results = survey.by(scenarios).by(agents).by(models).run() # example code - scenarios and models not defined here

Learn more about Scenarios, Language Models and Results.

Updating agents

Agents can be updated after they are created.

Changing a trait

Here we create an agent and then change one of its traits:

from edsl import Agent

a = Agent(traits = {"age": 22})
a.age = 23
a.age

Output:

23

Adding a trait

We can also add a new trait to an agent:

from edsl import Agent

a = Agent(traits = {"age": 22})

a.add_trait({"location": "California"})
a

Output:

{
    "traits": {
        "age": 22,
        "location": "California"
    }
}

Removing a trait

We can remove a trait from an agent:

from edsl import Agent

a = Agent(traits = {"age": 22, "location": "California"})

a.remove_trait("age")
a

Output:

{
    "traits": {
        "location": "California"
    }
}

Using survey responses as new agent traits

After running a survey, we can use the responses to create new traits for an agent:

from edsl import Agent, QuestionMultipleChoice, Survey

a = Agent(traits = {"age": 22, "location": "California"})

q = QuestionMultipleChoice(
    question_name = "surfing"
    question_text = "How often do you go surfing?",
    question_options = ["Never", "Sometimes", "Often"]
)

survey = Survey([q])
results = survey.by(a).run()

a = results.select("age", "location", "surfing").to_agent_list()[0] # create new agent with traits from results

Output:

{
    "traits": {
        "age": 22,
        "location": "California",
        "surfing": "Sometimes"
    }
}

Note that in the example above we simply replaced the original agent by selecting the first agent from the agent list that we created. This can be useful for creating agents that evolve over time based on their experiences or responses to surveys.

Here we use the same method to update multiple agents at once:

from edsl import Agent, QuestionMultipleChoice, Survey, AgentList

agents = AgentList([
    Agent(traits = {"age": 22, "location": "California"}),
    Agent(traits = {"age": 30, "location": "New York"}),
    Agent(traits = {"age": 40, "location": "Texas"}),
])

q = QuestionMultipleChoice(
    question_name = "surfing",
    question_text = "How often do you go surfing?",
    question_options = ["Never", "Sometimes", "Often"]
)

survey = Survey([q])
results = survey.by(agents).run()

agents = results.select("age", "location", "surfing").to_agent_list()

Output:

[
    {
        "traits": {
            "age": 22,
            "location": "California",
            "surfing": "Sometimes"
        },
        "edsl_version": "0.1.36.dev1",
        "edsl_class_name": "Agent"
    },
    {
        "traits": {
            "age": 40,
            "location": "Texas",
            "surfing": "Never"
        },
        "edsl_version": "0.1.36.dev1",
        "edsl_class_name": "Agent"
    },
    {
        "traits": {
            "age": 30,
            "location": "New York",
            "surfing": "Never"
        },
        "edsl_version": "0.1.36.dev1",
        "edsl_class_name": "Agent"
    }
]

Agent class

An Agent is an AI agent that can reference a set of traits in answering questions.

class edsl.agents.Agent.Agent(traits: dict | None = None, name: str | None = None, codebook: dict | None = None, instruction: str | None = None, traits_presentation_template: str | None = None, dynamic_traits_function: Callable | None = None, dynamic_traits_function_source_code: str | None = None, dynamic_traits_function_name: str | None = None, answer_question_directly_source_code: str | None = None, answer_question_directly_function_name: str | None = None)[source]

Bases: Base

An Agent that can answer questions.

__init__(traits: dict | None = None, name: str | None = None, codebook: dict | None = None, instruction: str | None = None, traits_presentation_template: str | None = None, dynamic_traits_function: Callable | None = None, dynamic_traits_function_source_code: str | None = None, dynamic_traits_function_name: str | None = None, answer_question_directly_source_code: str | None = None, answer_question_directly_function_name: str | None = None)[source]

Initialize a new instance of Agent.

Parameters:
  • traits – A dictionary of traits that the agent has. The keys need to be valid identifiers.

  • name – A name for the agent

  • codebook – A codebook mapping trait keys to trait descriptions.

  • instruction – Instructions for the agent in how to answer questions.

  • trait_presentation_template – A template for how to present the agent’s traits.

  • dynamic_traits_function – A function that returns a dictionary of traits.

The traits parameter is a dictionary of traits that the agent has. These traits are used to construct a prompt that is presented to the LLM. In the absence of a traits_presentation_template, the default is used. This is a template that is used to present the agent’s traits to the LLM. See edsl.prompts.library.agent_persona.AgentPersona for more information.

Example usage:

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.traits
{'age': 10, 'hair': 'brown', 'height': 5.5}

These traits are used to construct a prompt that is presented to the LLM.

In the absence of a traits_presentation_template, the default is used.

>>> a = Agent(traits = {"age": 10}, traits_presentation_template = "I am a {{age}} year old.")
>>> repr(a.agent_persona)
'Prompt(text="""I am a {{age}} year old.""")'

When this is rendered for presentation to the LLM, it will replace the {{age}} with the actual age. it is also possible to use the codebook to provide a more human-readable description of the trait. Here is an example where we give a prefix to the age trait (namely the age):

>>> traits = {"age": 10, "hair": "brown", "height": 5.5}
>>> codebook = {'age': 'Their age is'}
>>> a = Agent(traits = traits, codebook = codebook, traits_presentation_template = "This agent is Dave. {{codebook['age']}} {{age}}")
>>> d = a.traits | {'codebook': a.codebook}
>>> a.agent_persona.render(d)
Prompt(text="""This agent is Dave. Their age is 10""")

Instructions

The agent can also have instructions. These are instructions that are given to the agent when answering questions.

>>> Agent.default_instruction
'You are answering questions as if you were a human. Do not break character.'

See see how these are used to actually construct the prompt that is presented to the LLM, see edsl.agents.Invigilator.InvigilatorBase.

add_direct_question_answering_method(method: Callable, validate_response: bool = False, translate_response: bool = False) None[source]

Add a method to the agent that can answer a particular question type.

Parameters:

method – A method that can answer a question directly.

Example usage:

>>> a = Agent()
>>> def f(self, question, scenario): return "I am a direct answer."
>>> a.add_direct_question_answering_method(f)
>>> a.answer_question_directly(question = None, scenario = None)
'I am a direct answer.'
add_trait(trait_name_or_dict: str, value: Any | None = None) Agent[source]

Adds a trait to an agent and returns that agent

property agent_persona: Prompt[source]
answer_question(*, question: QuestionBase, cache: Cache, scenario: 'Scenario' | None = None, survey: 'Survey' | None = None, model: 'LanguageModel' | None = None, debug: bool = False, memory_plan: 'MemoryPlan' | None = None, current_answers: dict | None = None, iteration: int = 0) AgentResponseDict[source]

Answer a posed question.

Parameters:
  • question – The question to answer.

  • scenario – The scenario in which the question is asked.

  • model – The language model to use.

  • debug – Whether to run in debug mode.

  • memory_plan – The memory plan to use.

  • current_answers – The current answers.

  • iteration – The iteration number.

>>> a = Agent(traits = {})
>>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.")
>>> from edsl import QuestionFreeText
>>> q = QuestionFreeText.example()
>>> a.answer_question(question = q, cache = False).answer
'I am a direct answer.'

This is a function where an agent returns an answer to a particular question. However, there are several different ways an agent can answer a question, so the actual functionality is delegated to an edsl.agents.InvigilatorBase: object.

answer_question_directly_function_name = ''[source]
async async_answer_question(*, question: QuestionBase, cache: Cache, scenario: 'Scenario' | None = None, survey: 'Survey' | None = None, model: 'LanguageModel' | None = None, debug: bool = False, memory_plan: 'MemoryPlan' | None = None, current_answers: dict | None = None, iteration: int = 0) AgentResponseDict[source]

Answer a posed question.

Parameters:
  • question – The question to answer.

  • scenario – The scenario in which the question is asked.

  • model – The language model to use.

  • debug – Whether to run in debug mode.

  • memory_plan – The memory plan to use.

  • current_answers – The current answers.

  • iteration – The iteration number.

>>> a = Agent(traits = {})
>>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.")
>>> from edsl import QuestionFreeText
>>> q = QuestionFreeText.example()
>>> a.answer_question(question = q, cache = False).answer
'I am a direct answer.'

This is a function where an agent returns an answer to a particular question. However, there are several different ways an agent can answer a question, so the actual functionality is delegated to an edsl.agents.InvigilatorBase: object.

code() str[source]

Return the code for the agent.

Example usage:

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> print(a.code())
from edsl import Agent
agent = Agent(traits={'age': 10, 'hair': 'brown', 'height': 5.5})
create_invigilator(*, question: QuestionBase, cache: Cache, survey: 'Survey' | None = None, scenario: Scenario | None = None, model: LanguageModel | None = None, debug: bool = False, memory_plan: MemoryPlan | None = None, current_answers: dict | None = None, iteration: int = 1, sidecar_model=None, raise_validation_errors: bool = True) InvigilatorBase[source]

Create an Invigilator.

An invigilator is an object that is responsible for administering a question to an agent. There are several different types of invigilators, depending on the type of question and the agent. For example, there are invigilators for functional questions (i.e., question is of type edsl.questions.QuestionFunctional:), for direct questions, and for LLM questions.

>>> a = Agent(traits = {})
>>> a.create_invigilator(question = None, cache = False)
InvigilatorAI(...)

An invigator is an object that is responsible for administering a question to an agent and recording the responses.

default_instruction = 'You are answering questions as if you were a human. Do not break character.'[source]
dynamic_traits_function_name = ''[source]
classmethod example(randomize: bool = False) Agent[source]

Returns an example Agent instance.

Parameters:

randomize – If True, adds a random string to the value of an example key.

classmethod from_dict(agent_dict: dict[str, dict | bool]) Agent[source]

Deserialize from a dictionary.

Example usage:

>>> Agent.from_dict({'name': "Steve", 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}})
Agent(name = """Steve""", traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
has_dynamic_traits_function = False[source]
instruction[source]

ABC for something.

name[source]

ABC for something.

print() None[source]

Print the object to the console.

prompt() str[source]

Return the prompt for the agent.

Example usage:

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.prompt()
Prompt(text="""Your traits: {'age': 10, 'hair': 'brown', 'height': 5.5}""")
remove_direct_question_answering_method() None[source]

Remove the direct question answering method.

Example usage:

>>> a = Agent()
>>> def f(self, question, scenario): return "I am a direct answer."
>>> a.add_direct_question_answering_method(f)
>>> a.remove_direct_question_answering_method()
>>> hasattr(a, "answer_question_directly")
False
remove_trait(trait: str) Agent[source]

Remove a trait from the agent.

Example usage:

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.remove_trait("age")
Agent(traits = {'hair': 'brown', 'height': 5.5})
rename(old_name: str, new_name: str) Agent[source]

Rename a trait.

Example usage:

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.rename("age", "years") == Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5})
True
rich_print()[source]

Display an object as a rich table.

Example usage:

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.rich_print()
<rich.table.Table object at ...>
select(*traits: str) Agent[source]

Selects agents with only the references traits

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.select("age", "height")
Agent(traits = {'age': 10, 'height': 5.5})
>>> a.select("age")
Agent(traits = {'age': 10})
to_dict() dict[str, dict | bool][source]

Serialize to a dictionary.

Example usage:

>>> a = Agent(name = "Steve", traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.to_dict()
{'name': 'Steve', 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}, 'edsl_version': '...', 'edsl_class_name': 'Agent'}
property traits: dict[str, str][source]

An agent’s traits, which is a dictionary.

The agent could have a a dynamic traits function (dynamic_traits_function) that returns a dictionary of traits when called. This function can also take a question as an argument. If so, the dynamic traits function is called and the result is returned. Otherwise, the traits are returned.

Example:

>>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
>>> a.traits
{'age': 10, 'hair': 'brown', 'height': 5.5}
translate_traits(values_codebook: dict) Agent[source]

Translate traits to a new codebook.

>>> a = Agent(traits = {"age": 10, "hair": 1, "height": 5.5})
>>> a.translate_traits({"hair": {1:"brown"}})
Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
Parameters:

values_codebook – The new codebook.

AgentList class

A list of Agent objects.

Example usage:

al = AgentList([Agent.example(), Agent.example()])
len(al)
2
AgentList.__init__(data: list['Agent'] | None = None)[source]

Initialize a new AgentList.

Parameters:

data – A list of Agents.

classmethod AgentList.example(randomize: bool = False) AgentList[source]

Returns an example AgentList instance.

Parameters:

randomize – If True, uses Agent’s randomize method.

classmethod AgentList.from_dict(data: dict) AgentList[source]

Deserialize the dictionary back to an AgentList object.

Param:

data: A dictionary representing an AgentList.

>>> from edsl.agents.Agent import Agent
>>> al = AgentList([Agent.example(), Agent.example()])
>>> al2 = AgentList.from_dict(al.to_dict())
>>> al2 == al
True
AgentList.rich_print() Table[source]

Display an object as a rich table.

AgentList.to_dict()[source]

Return dictionary of AgentList to serialization.