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:

traits_dict = {
    "persona": "You are a 45-old-woman living in Massachusetts...",
    "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:

agent = Agent(name = "Robin", 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. Here we create a list of agents with each combination of listed trait dimensions:

ages = [10, 20, 30, 40, 50]
locations = ["New York", "California", "Texas", "Florida", "Washington"]
agents = [Agent(traits = {"age": age, "location": location}) for age, location in zip(ages, locations)]

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:

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)

This code will return:

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:

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

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.

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)

This code will return:

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:

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)

This code will return:

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:

results.select("agent.age", ...).print()

And this code will let us filter the results by the agent’s age:

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

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"
)

jobs = q.by(a)
print(jobs.prompts().select('user_prompt').first().text)

This code will output the text of the prompt for the question:

You are being asked the following question: What is your last name, John?
Return a valid JSON formatted like this:
{"answer": "<put free text answer here>"}

Accessing agent traits

The traits of an agent can be accessed directly:

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

This code will return:

{'age': 22}

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

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

This code will return:

22

Simulating agent responses

As with question scenarios and language models, an agent is assigned to a survey using the by method when the survey is run:

agent = Agent(traits = {...})
results = survey.by(agent).run()

This will generate a Results object that contains a Result for each agent’s responses to the survey questions. If multiple agents will be used with a survey, they are passed as a list in the same by call:

agents = [AgentList(...)]
results = survey.by(agents).run()

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()

Learn more about Scenarios, Language Models and Results.

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

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.

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.