# Agents Source: https://docs.expectedparrot.com/en/latest/agents *Agent* objects are used to simulate survey responses for target audiences. They can be 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. Agents can be created individually or as a list, and updated after creation. They work with any question type for single questions or full surveys. **AI agents are not real people.** Agent responses are generated by language models based on their training data. They reflect statistical patterns in that data, not the actual opinions of any demographic group. Use AI-simulated responses for prototyping, hypothesis generation, and pre-testing — and validate with real human data when your goal is to measure actual attitudes or behaviors. Agent information is presented to a model in a system prompt; it is delivered together with a user prompt of information about a given question. In the examples below we show how to access these prompts to inspect them before running a survey and in the results that are generated for a survey. Note, however, that certain models do not take system prompts (e.g., OpenAI’s o1). When using a model that does not take a system prompt, agent information should be included in the user prompt. ## Constructing an Agent An Agent is created by passing a dictionary of traits relevant to the questions, using single values or textual narratives. For example: ```python theme={null} from edsl import Agent agent = Agent( traits = { "persona": "You are an expert in machine learning.", "age": 45, "home_state": "Massachusetts" }) ``` ### Simplified constructor with keyword arguments You can also create agents by passing traits directly as keyword arguments, which provides a more concise syntax: ```python theme={null} from edsl import Agent agent = Agent( persona="You are an expert in machine learning.", age=45, home_state="Massachusetts" ) ``` This is equivalent to the traits dictionary approach but more convenient for simple agent creation. **Note**: 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: ```python theme={null} agent = Agent( name = "Ada", traits = { "persona": "You are an expert in machine learning.", "age": 45, "home_state": "Massachusetts" }) ``` If a name is not passed when the agent is created, an agent\_name field is automatically added to the Results that are generated when a survey is run with the agent. This field is a unique identifier for the agent and can be used to filter or group results by agent. It is *not* used in the prompts for generating responses. If you want to use a name in the prompts for generating responses, you can pass it as a trait: ```python theme={null} agent = Agent( traits = { "first_name": "Ada", "persona": "You are an expert in machine learning.", "age": 45, "home_state": "Massachusetts" }) ``` We can see how the agent information is presented to a model by inspecting the system prompt that is generated when we use an agent with a question: ```python theme={null} from edsl import QuestionFreeText q = QuestionFreeText( question_name = "favorite_food", question_text = "What is your favorite food?" ) job = q.by(agent) # using the agent created above job.prompts().select("user_prompt", "system_prompt") ``` Output: | user\_prompt | system\_prompt | | :- | :- | | What is your favorite food? | You are answering questions as if you were a human. Do not break character. Your traits: \{‘first\_name’: ‘Ada’, ‘persona’: ‘You are an expert in machine learning.’, ‘age’: 45, ‘home\_state’: ‘Massachusetts’} | Note that trying to create two agents with the same name or trying to use a key “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 for multiple agents. For example, here we create a list of agents as an AgentList with different combinations of traits: ```python theme={null} from edsl import AgentList, Agent 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 that can then be used in a survey. Example code for running a survey with the agents: ```python theme={null} from edsl import QuestionFreeText, Survey q = QuestionFreeText( question_name = "favorite_food", question_text = "What is your favorite food?" ) survey = Survey(questions = [q]) results = survey.by(agents).run() ``` This will generate a Results object that contains a Result for each agent’s responses to the survey question. Learn more about working with results in the [Results](/en/latest/results#results) section. ## Generating agents from data An AgentList can be automatically generated from data stored in many source types, including a list, a dictionary, a CSV, TSV or Excel file, a Pandas dataframe, etc. A general method for this is from\_source() which is called on the AgentList class, takes a data source\_type (csv, excel, pandas, etc.) and a data source, and returns an AgentList object. Optional parameters allow you to specify special instructions, a codebook for the traits, and a name\_field for the agents. For example, if you have agent data stored in a CSV file, you can create an AgentList from it using the from\_source() method by specifying source\_type=”csv” and the path to the CSV file: ```python theme={null} from edsl import AgentList agents = AgentList.from_source( source_type="csv", file_or_url="agent_data.csv") # replace with your CSV file path ``` If the data source is a CSV or Excel file, the header row is used as the keys for the traits, and can optionally have a column “name” for the agent names. If a different column name should be used for the agent names, it can be specified with the name\_field parameter: ```python theme={null} from edsl import AgentList agents = AgentList.from_source( source_type="csv", file_or_url="agent_data.csv", # replace with your CSV file path name_field="first_name" # replace with your column name for agent names ) ``` A codebook can also be passed to provide descriptions for the traits. It can be useful for providing context to a model about the traits of an agent. For example, if you have a trait “age” and you want to provide more context about what that means, you could use the codebook to specify that “age” refers to the age of the agent in years: ```python theme={null} from edsl import AgentList codebook = { "age": "The age of the agent in years", "location": "The location of the agent" } agents = AgentList.from_source( source_type="csv", file_or_url="agent_data.csv", # replace with your CSV file path codebook=codebook ) ``` Special instructions can also be passed to modify the default instructions that are used with agent traits in the system prompt. For example, if you want all agents to answer in German, you could use the instructions parameter to specify that: ```python theme={null} from edsl import AgentList agents = AgentList.from_source( source_type="csv", file_or_url="agent_data.csv", # replace with your CSV file path instructions="Answer in German." ) ``` ### From natural language descriptions You can generate agents from natural language descriptions using the `from_vibes()` method. This uses an LLM to interpret your descriptions and create appropriate agent traits: ```python theme={null} from edsl import AgentList # Generate agents from natural language descriptions agents = AgentList.from_vibes([ "A 25-year-old software engineer from San Francisco", "A retired teacher in their 60s from rural Maine", "A college student studying biology" ]) ``` This method is particularly useful when you want to quickly create diverse agents without manually specifying all traits. ### From a list We can create a simple AgentList from a list using the from\_list() method, which takes a single trait\_name and a list of values for it and returns an agent for each value (each agent has a single trait): ```python theme={null} from edsl import AgentList agents = AgentList.from_list(trait_name="age", values=[10, 20, 30, 40]) agents ``` Output: | age | | :- | | 10 | | 20 | | 30 | | 40 | ### From a dictionary We can create a more complex AgentList from a dictionary using the from\_dict() method. It takes a dictionary with a key agent\_list and a list of dictionaries, each of which must have a traits key with a dictionary of traits and an optional name key for the agent’s name: ```python theme={null} from edsl import AgentList data = { "agent_list": [ {"name":"agent1", "traits":{"age": 10, "location": "New York"}}, {"name":"agent2", "traits":{"age": 20, "location": "California"}}, {"name":"agent3", "traits":{"age": 30, "location": "Texas"}}, {"name":"agent4", "traits":{"age": 40, "location": "Florida"}}, {"name":"agent5", "traits":{"age": 50, "location": "Washington"}} ] } agents = AgentList.from_dict(data) agents ``` Output: | location | age | | :- | :- | | New York | 10 | | California | 20 | | Texas | 30 | | Florida | 40 | | Washington | 50 | ### From a CSV file We can also create an AgentList from a CSV file using the from\_csv() method. The CSV file must have a header row of Pythonic keys for the traits, and can optionally have a column “name” for the agent names: ```python theme={null} from edsl import AgentList # Creating a CSV file with agent data to use as an example import pandas as pd data = [ {"name": "Alice", "age": 25, "city": "New York"}, {"name": "Bob", "age": 30, "city": "San Francisco"}, {"name": "Charlie", "age": 35, "city": "Chicago"} ] df = pd.DataFrame(data) df.to_csv("agent_data.csv", index=False) # Creating an AgentList from the CSV file agents = AgentList.from_csv("agent_data.csv") agents ``` Output: | city | age | | :- | :- | | New York | 25 | | San Francisco | 30 | | Chicago | 35 | ## 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. **Note:** This method is only available with local inference. It does not work with remote inference. Example: ```python theme={null} 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: ```python theme={null} 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. The instruction parameter can be used to omit or modify the default instructions that are used with agent traits in the system prompt. For example: ```python theme={null} 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. We can create a Job object to inspect the prompts (user and system) that will be used to generate responses: ```python theme={null} from edsl import QuestionFreeText q = QuestionFreeText( question_name = "favorite_food", question_text = "What is your favorite food?" ) job = q.by(a) # using the agent created above job.prompts().select("user_prompt", "system_prompt") ``` Output: | user\_prompt | system\_prompt | | :- | :- | | What is your favorite food? | Answer in German. Your traits: \{‘age’: 10}. Answer in German. | Learn more about how to use instructions in the [Prompts](/en/latest/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: ```python theme={null} 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: ```python theme={null} 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. ``` We can also use the traits\_presentation\_template together with an instruction and inspect the prompts: ```python theme={null} from edsl import Agent, QuestionFreeText a = Agent( traits = {"age": 10}, traits_presentation_template = "(You are {{ age }} years old.)", instruction = "Answer in German." ) q = QuestionFreeText( question_name = "favorite_food", question_text = "What is your favorite food?" ) job = q.by(a) job.prompts().select("user_prompt", "system_prompt") ``` Output: | user\_prompt | system\_prompt | | :- | :- | | What is your favorite food? | Answer in German.(You are 10 years old.) | **Note**: 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): ```python theme={null} results.select("agent.age", "agent.agent_name", "answer.*") ``` **Note**: Note that the prefix “agent” can also be dropped. The following code is equivalent: ``` results.select("age", "agent_name", "answer.*") ``` We can filter the results by an agent’s traits: ```python theme={null} results.filter("age == 22") ``` We can also call the filter() method on an agent list to filter agents by their traits: ```python theme={null} 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: ```python theme={null} 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") ``` Output: | user\_prompt | | :- | | What is your last name, John? | In templates, `agent.` and `agent[""]` look up agent traits before Python attributes. For example, `{{ agent.prompt }}` renders the value of a trait named `prompt`, even though `Agent` also has a `prompt()` method. If no trait matches, existing attribute access such as `{{ agent.name }}` still works. This precedence applies only during template rendering; Python calls such as `a.prompt()` are unchanged. Learn more about user and system prompts in the [Prompts](/en/latest/prompts) section. ## Accessing agent traits The traits of an agent can be accessed directly: ```python theme={null} from edsl import Agent a = Agent(traits = {'age': 22}) a.traits ``` Output: ```python theme={null} {'age': 22} ``` The traits of an agent can also be accessed as attributes of the agent: ```python theme={null} from edsl import Agent a = Agent(traits = {'age': 22}) a.age ``` Output: ```python theme={null} 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: ```python theme={null} 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: ```python theme={null} results.select("persona", "year", "school", "major", "state", "answer.*") ``` Output: | agent.persona | agent.year | agent.school | agent.major | agent.state | answer.favorite\_courses | answer.attend\_grad\_school | | :- | :- | :- | :- | :- | :- | :- | | You are a sophomore at a community college in upstate New York. | sophomore | community college | biology | New York | \[‘General Biology I’, ‘Organic Chemistry’, ‘Environmental Science’] | Undecided | If multiple agents will be used with a survey, they are passed as a list in the same by call: ```python theme={null} 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.*") ``` Output: | agent.major | agent.year | answer.favorite\_courses | answer.attend\_grad\_school | | :- | :- | :- | :- | | biology | sophomore | \[‘Genetics’, ‘Ecology’, ‘Cell Biology’] | Undecided | | history | junior | \[‘Medieval Europe’, ‘The American Civil War’, ‘Ancient Civilizations’] | Undecided | | mathematics | senior | \[‘Abstract Algebra’, ‘Real Analysis’, ‘Topology’] | Undecided | 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: ```python theme={null} results = survey.by(scenarios).by(agents).by(models).run() # example code - scenarios and models not defined here ``` Learn more about [Scenarios](/en/latest/scenarios), [Language Models](/en/latest/language_models) and [Results](/en/latest/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: ```python theme={null} 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: ```python theme={null} from edsl import Agent a = Agent(traits = {"age": 22}) a.add_trait({"location": "California"}) a ``` Output: | key | value | | :- | :- | | traits:age | 22 | | traits:location | California | ### Removing a trait We can remove a trait from an agent: ```python theme={null} from edsl import Agent a = Agent(traits = {"age": 22, "location": "California"}) a.remove_trait("age") a ``` Output: | key | value | | :- | :- | | 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: ```python theme={null} 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() # create new agent with traits from results a ``` Output: | location | surfing | age | | :- | :- | :- | | California | Sometimes | 22 | **Note**: 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: ```python theme={null} 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() agents ``` Output: | location | surfing | age | | :- | :- | :- | | California | Sometimes | 22 | | New York | Never | 30 | | Texas | Never | 40 | ### Creating AgentList from Results The AgentList.from\_results() method allows you to create an AgentList directly from a Results object. This is useful when you want to create agents based on survey responses, including their original traits and their answers to questions. By default, this method includes all answer columns as traits for the new agents: ```python theme={null} from edsl import AgentList # Create AgentList with all questions included new_agents = AgentList.from_results(results) # The new agents will have traits from the original agents plus all their answers new_agents[0] You can also specify which question responses to include as traits using the question_names parameter: # Include only specific questions as traits new_agents = AgentList.from_results(results, question_names=['surfing', 'age']) # The new agents will have traits from the original agents plus only the specified answers new_agents[0] ``` This is particularly useful when you want to: - Create agents with only certain response patterns - Filter out irrelevant or sensitive question responses - Create more focused agent profiles based on specific survey questions - Reduce the number of traits when only certain responses are needed **Note**: Note that the question\_names parameter affects both answer.\* columns (as traits) and prompt.\* columns (as codebook). Agent traits (from agent.\* columns) are always included. ## Agent class > ### *class* edsl.agents.Agent(*traits: dict | None = None*, *name: str | None = None*, *codebook: dict | None = None*, *instruction: str | None = None*, *trait\_categories: dict\[str, list\[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\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Bases: [`Base`](/en/latest/base#base-class "edsl.base.base_class.Base") A class representing an AI agent with customizable traits that can answer questions. An Agent in EDSL represents an entity with specific characteristics (traits) that can answer questions through various mechanisms. Agents can use language models to generate responses based on their traits, directly answer questions through custom functions, or dynamically adjust their traits based on the questions being asked. Key capabilities: - Store and manage agent characteristics (traits) - Provide instructions on how the agent should answer - Support for custom codebooks to provide human-readable trait descriptions - Integration with multiple question types and language models - Combine agents to create more complex personas - Customize agent behavior through direct answering methods Agents are used in conjunction with Questions, Scenarios, and Surveys to create structured interactions with language models. > #### **init**(*traits: dict | None = None*, *name: str | None = None*, *codebook: dict | None = None*, *instruction: str | None = None*, *trait\_categories: dict\[str, list\[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\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Initialize a new Agent instance with specified traits and capabilities. > #### **Args:** traits: Dictionary of agent characteristics (e.g., \{“age”: 30, “occupation”: “doctor”}) name: Optional name identifier for the agent codebook: Dictionary mapping trait keys to human-readable descriptions for prompts. This provides more descriptive labels for traits when rendering prompts. For example, \{‘age’: ‘Age in years’} would display “Age in years: 30” instead of “age: 30”. instruction: Directive for how the agent should answer questions traits\_presentation\_template: Jinja2 template for formatting traits in prompts dynamic\_traits\_function: Function that can modify traits based on questions dynamic\_traits\_function\_source\_code: Source code string for the dynamic traits function dynamic\_traits\_function\_name: Name of the dynamic traits function answer\_question\_directly\_source\_code: Source code for direct question answering method answer\_question\_directly\_function\_name: Name of the direct answering function The Agent class brings together several key concepts: ### Traits Traits are key-value pairs that define an agent’s characteristics. These are used to construct a prompt that guides the language model on how to respond. Example: >>> a = Agent(traits=\{“age”: 10, “hair”: “brown”, “height”: 5.5}) >>> a.traits \{‘age’: 10, ‘hair’: ‘brown’, ‘height’: 5.5} ### Traits Presentation The traits\_presentation\_template controls how traits are formatted in prompts. It uses Jinja2 templating to insert trait values. Example: >>> a = Agent(traits=\{“age”: 10}, traits\_presentation\_template=”I am a \{`\} year old.”) \>\>\> repr(a.agent_persona) ‘Prompt(text=”””I am a \{{age}`} year old.”””)’ ### Codebooks Codebooks provide human-readable descriptions for traits in prompts. Example: ```python theme={null} >>> 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 Instructions guide how the agent should answer questions. If not provided, a default instruction is used. ```python theme={null} >>> Agent.default_instruction 'You are answering questions as if you were a human. Do not break character.' ``` For details on how these components are used to construct prompts, see `edsl.agents.Invigilator.InvigilatorBase`. > #### add(*other\_agent: A | None = None*, \*, *conflict\_strategy: str = 'numeric'*) → A [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Combine *self* with *other\_agent* and return a new Agent. ### Parameters **other\_agent:** The second agent to merge with *self*. If *None*, *self* is returned unchanged. **conflict\_strategy:** How to handle overlapping trait names. * `"numeric"` (default) – rename conflicting traits coming from *other\_agent* by appending an incrementing suffix (`_1`, `_2` …). This is identical to the behaviour of the `+` operator before this refactor. * `"error"` – raise `edsl.agents.exceptions.AgentCombinationError`. * `"repeated_observation"` – if both agents have the same trait *and* the codebook entry for that trait is identical (or missing in both), merge the two values into a list `[old, new]`. If the codebook entries differ, an `edsl.agents.exceptions.AgentCombinationError` is raised. ### Returns **Agent:** A new agent containing the merged traits / codebooks. > #### add\_canned\_response(*question\_name*, *response*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Add a canned response to the agent. > #### add\_category(*category\_name: str*, *trait\_keys: list\[str] | None = None*) → None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Add a category to the agent > #### add\_direct\_question\_answering\_method(*method: DirectAnswerMethod*, *validate\_response: bool = False*, *translate\_response: bool = False*) → None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Add a method to the agent that can answer a particular question type. See: [/en/latest/agents#agent-direct-answering-methods](/en/latest/agents#agent-direct-answering-methods) > #### **Args:** method: A method that can answer a question directly validate\_response: Whether to validate the response translate\_response: Whether to translate the response Raises: AgentDirectAnswerFunctionError: If the method signature is invalid > #### **Example:** ```python theme={null} >>> 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 | dict\[str, Any]*, *value: Any | None = None*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Add a trait to an agent and return a new agent. > #### **Args:** trait\_name\_or\_dict: Either a trait name string or a dictionary of traits value: The trait value if trait\_name\_or\_dict is a string > #### **Returns:** A new Agent instance with the added trait(s) > #### **Raises:** AgentErrors: If both a dictionary and a value are provided > #### **Example:** ```python theme={null} >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}) >>> a.add_trait("weight", 150) Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5, 'weight': 150}) ``` > #### *property* agent\_persona\*: [Prompt](/en/latest/prompts#edsl.prompts.Prompt "edsl.prompts.Prompt")\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Get the agent’s persona template as a Prompt object. This property provides access to the template that formats the agent’s traits for presentation in prompts. The template is wrapped in a Prompt object that supports rendering with variable substitution. **Returns:** Prompt: A prompt object containing the traits presentation template > #### answer\_question(\*, *question: [QuestionBase](/en/latest/questions#questionbase-class "edsl.questions.QuestionBase")*, *cache: [Cache](/en/latest/data#cache-class "edsl.caching.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*, *key\_lookup: 'KeyLookup' | None = None*) → AgentResponseDict [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Answer a posed question asynchronously. > **Args:** question: The question to answer cache: The cache to use for storing responses scenario: The scenario in which the question is asked survey: The survey context 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 key\_lookup: The key lookup for API credentials > **Returns:** An AgentResponseDict containing the answer > **Example:** ```python theme={null} >>> a = Agent(traits = {}) >>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.") >>> from edsl.questions import QuestionFreeText >>> q = QuestionFreeText.example() >>> a.answer_question(question = q, cache = False).answer 'I am a direct answer.' ``` > **Note:** 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 InvigilatorBase object. > #### answer\_question\_directly\_function\_name *= ''* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### *async* async\_answer\_question(\*, *question: [QuestionBase](/en/latest/questions#questionbase-class "edsl.questions.QuestionBase")*, *cache: [Cache](/en/latest/data#cache-class "edsl.caching.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*, *key\_lookup: 'KeyLookup' | None = None*) → AgentResponseDict [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Answer a posed question asynchronously. > **Args:** question: The question to answer cache: The cache to use for storing responses scenario: The scenario in which the question is asked survey: The survey context 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 key\_lookup: The key lookup for API credentials > **Returns:** An AgentResponseDict containing the answer > **Example:** ```python theme={null} >>> a = Agent(traits = {}) >>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.") >>> from edsl.questions import QuestionFreeText >>> q = QuestionFreeText.example() >>> a.answer_question(question = q, cache = False).answer 'I am a direct answer.' ``` > **Note:** 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 InvigilatorBase object. > #### chat() [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### code() → str [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return the code for the agent. > **Returns:** Python code string to recreate this agent > **Example:** ```python theme={null} >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}) >>> print(a.code()) from edsl.agents import Agent agent = Agent(traits={'age': 10, 'hair': 'brown', 'height': 5.5}) ``` > #### copy() → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create a deep copy of this agent using serialization/deserialization. This method uses to\_dict/from\_dict to create a completely independent copy of the agent, including all its traits, codebook, instructions, and special functions like dynamic traits and direct answering methods. > **Returns:** Agent: A new agent instance that is functionally identical to this one > **Examples:** ```python theme={null} >>> a = Agent(traits={"age": 10, "hair": "brown"}, ... codebook={'age': 'Their age is'}) >>> a2 = a.copy() >>> a2 == a # Functionally equivalent True >>> id(a) == id(a2) # But different objects False ``` Copy preserves direct answering methods: ```python theme={null} >>> def f(self, question, scenario): return "I am a direct answer." >>> a.add_direct_question_answering_method(f) >>> a2 = a.copy() >>> a2.answer_question_directly(None, None) 'I am a direct answer.' ``` > #### create\_invigilator(\*, *question: [QuestionBase](/en/latest/questions#questionbase-class "edsl.questions.QuestionBase")*, *cache: [Cache](/en/latest/data#cache-class "edsl.caching.Cache")*, *survey: 'Survey' | None = None*, *scenario: 'Scenario' | None = None*, *model: 'LanguageModel' | None = None*, *memory\_plan: 'MemoryPlan' | None = None*, *current\_answers: dict | None = None*, *iteration: int = 1*, *raise\_validation\_errors: bool = True*, *key\_lookup: 'KeyLookup' | None = None*) → InvigilatorBase [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) 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, for direct questions, and for LLM questions. > **Args:** question: The question to be asked cache: The cache for storing responses survey: The survey context scenario: The scenario context model: The language model to use memory\_plan: The memory plan to use current\_answers: The current answers iteration: The iteration number raise\_validation\_errors: Whether to raise validation errors key\_lookup: The key lookup for API credentials > **Returns:** An InvigilatorBase instance for handling the question > **Example:** ```python theme={null} >>> a = Agent(traits = {}) >>> inv = a.create_invigilator(question = None, cache = False) >>> type(inv).__name__ 'InvigilatorAI' ``` > **Note:** An invigilator 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\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### drop(\*\*field\_names: str | List\[str]\*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Drop field(s) from the agent. > **Args:** [\*](/en/latest/#id2)field\_names: The name(s) of the field(s) to drop. Can be: * Single field name: drop(“age”) * Multiple field names: drop(“age”, “height”) * List of field names: drop(\[“age”, “height”]) > **Examples:** Drop a single trait from the agent: ```python theme={null} >>> a = Agent(traits={"age": 30, "hair": "brown", "height": 5.5}) >>> a_dropped = a.drop("age") >>> a_dropped.traits {'hair': 'brown', 'height': 5.5} ``` Drop multiple traits using separate arguments: ```python theme={null} >>> a = Agent(traits={"age": 30, "hair": "brown", "height": 5.5}) >>> a_dropped = a.drop("age", "height") >>> a_dropped.traits {'hair': 'brown'} ``` Drop multiple traits using a list: ```python theme={null} >>> a = Agent(traits={"age": 30, "hair": "brown", "height": 5.5}) >>> a_dropped = a.drop(["age", "height"]) >>> a_dropped.traits {'hair': 'brown'} ``` Drop an agent field like name: ```python theme={null} >>> a = Agent(traits={"age": 30}, name="John") >>> a_dropped = a.drop("name") >>> a_dropped.name is None True >>> a_dropped.traits {'age': 30} ``` Error when trying to drop a non-existent field: ```python theme={null} >>> a = Agent(traits={"age": 30}) >>> a.drop("nonexistent") Traceback (most recent call last): ... edsl.agents.exceptions.AgentErrors: ... ``` > #### drop\_trait\_if(*bad\_value: Any*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Drop traits that have a specific bad value. > **Args:** bad\_value: The value to remove from traits > **Returns:** A new Agent instance with the bad value traits removed > **Example:** ```python theme={null} >>> agent = Agent(traits={'age': 30, 'height': None, 'weight': 150}) >>> clean_agent = agent.drop_trait_if(None) >>> clean_agent.traits {'age': 30, 'weight': 150} ``` > #### duplicate() → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create a deep copy of this agent with all its traits and capabilities. This method creates a completely independent copy of the agent, including all its traits, codebook, instructions, and special functions like dynamic traits and direct answering methods. > **Returns:** Agent: A new agent instance that is functionally identical to this one > **Examples:** Create a duplicate agent and verify it’s equal but not the same object: ```python theme={null} >>> a = Agent(traits={"age": 10, "hair": "brown", "height": 5.5}, ... codebook={'age': 'Their age is'}) >>> a2 = a.duplicate() >>> a2 == a # Functionally equivalent True >>> id(a) == id(a2) # But different objects False ``` Duplicating preserves direct answering methods: ```python theme={null} >>> def f(self, question, scenario): return "I am a direct answer." >>> a.add_direct_question_answering_method(f) >>> hasattr(a, "answer_question_directly") True >>> a2 = a.duplicate() >>> a2.answer_question_directly(None, None) 'I am a direct answer.' ``` Duplicating preserves custom instructions: ```python theme={null} >>> a = Agent(traits={'age': 10}, instruction="Have fun!") >>> a2 = a.duplicate() >>> a2.instruction 'Have fun!' ``` > #### *property* dynamic\_traits\_function\*: Callable | None\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) The dynamic traits function if one exists. This property provides backward compatibility for the old attribute access pattern. > **Returns:** The dynamic traits function or None > **Examples:** ```python theme={null} >>> agent = Agent(traits={'age': 30}) >>> agent.dynamic_traits_function is None True >>> def dynamic_func(): return {'age': 25} >>> agent.traits_manager.initialize_dynamic_function(dynamic_func) >>> agent.dynamic_traits_function is not None True ``` > #### *property* dynamic\_traits\_function\_name\*: str\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) The name of the dynamic traits function. This property provides backward compatibility for the old attribute access pattern. > **Returns:** The function name or empty string if no function > **Examples:** ```python theme={null} >>> agent = Agent(traits={'age': 30}) >>> agent.dynamic_traits_function_name '' >>> def my_func(): return {'age': 25} >>> agent.traits_manager.initialize_dynamic_function(my_func) >>> agent.dynamic_traits_function_name 'my_func' ``` > #### *classmethod* example(*randomize: bool = False*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return an example Agent instance. > **Args:** randomize: If True, adds a random string to the value of an example key > **Returns:** An example Agent instance > **Example:** ```python theme={null} >>> Agent.example() Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5}) ``` > #### *classmethod* from\_dict(*agent\_dict: dict\[str, dict | bool | str]*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Deserialize from a dictionary. > **Args:** agent\_dict: Dictionary containing agent data > **Returns:** An Agent instance created from the dictionary > **Example:** ```python theme={null} >>> Agent.from_dict({'name': "Steve", 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}}) Agent(name = """Steve""", traits = {'age': 10, 'hair': 'brown', 'height': 5.5}) ``` > #### *classmethod* from\_result(*result: [Result](/en/latest/results#edsl.results.Result "edsl.results.Result")*, *name: str | None = None*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create an Agent instance from a Result object. The agent’s traits will correspond to the questions asked during the interview (the keys of result.answer) with their respective answers as the values. A simple, readable traits\_presentation\_template is automatically generated so that rendering the agent will look like: ```python theme={null} This person was asked the following questions – here are the answers: Q: A: Q: A: ... ``` > **Args:** result: The Result instance from which to build the agent name: Optional explicit name for the new agent. If omitted, we attempt > to reuse result.agent.name if it exists > **Returns:** A new Agent instance created from the result > **Raises:** TypeError: If result is not a Result object > **Example:** ```python theme={null} >>> from edsl.results import Result >>> # result = Result(...) >>> # agent = Agent.from_result(result) ``` > #### *property* has\_dynamic\_traits\_function\*: bool\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Whether the agent has a dynamic traits function. This property provides backward compatibility for the old attribute access pattern. > **Returns:** True if the agent has a dynamic traits function, False otherwise > **Examples:** ```python theme={null} >>> agent = Agent(traits={'age': 30}) >>> agent.has_dynamic_traits_function False >>> def dynamic_func(): return {'age': 25} >>> agent.traits_manager.initialize_dynamic_function(dynamic_func) >>> agent.has_dynamic_traits_function True ``` > #### instruction [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) ABC for something. > #### *property* invigilator [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Lazily initialize the invigilator to avoid importing language\_models during Survey import > #### keep(\*\*field\_names: str | List\[str]\*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Keep only the specified fields from the agent. > **Args:** [\*](/en/latest/#id4)field\_names: The name(s) of the field(s) to keep. Can be: * Single field name: keep(“age”) * Multiple field names: keep(“age”, “height”) * List of field names: keep(\[“age”, “height”]) > **Examples:** Keep a single trait: ```python theme={null} >>> a = Agent(traits={"age": 30, "hair": "brown", "height": 5.5}) >>> a_kept = a.keep("age") >>> a_kept.traits {'age': 30} ``` Keep multiple traits using separate arguments: ```python theme={null} >>> a = Agent(traits={"age": 30, "hair": "brown", "height": 5.5}) >>> a_kept = a.keep("age", "height") >>> a_kept.traits {'age': 30, 'height': 5.5} ``` Keep multiple traits using a list: ```python theme={null} >>> a = Agent(traits={"age": 30, "hair": "brown", "height": 5.5}) >>> a_kept = a.keep(["age", "height"]) >>> a_kept.traits {'age': 30, 'height': 5.5} ``` Keep agent fields and traits: ```python theme={null} >>> a = Agent(traits={"age": 30, "hair": "brown"}, name="John") >>> a_kept = a.keep("name", "age") >>> a_kept.name 'John' >>> a_kept.traits {'age': 30} ``` Error when trying to keep a non-existent field: ```python theme={null} >>> a = Agent(traits={"age": 30}) >>> a.keep("nonexistent") Traceback (most recent call last): ... edsl.agents.exceptions.AgentErrors: ... ``` > #### name [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Valid agent name descriptor. > #### old\_keep(\**traits: str*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Legacy trait selection method (renamed from select). Note: This method has data integrity issues and is kept for backward compatibility. Use select() or keep() instead, which provide better data consistency. > **Args:** [\*](/en/latest/#id6)traits: The trait names to select > **Returns:** A new Agent instance with only the selected traits > **Example:** ```python theme={null} >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}, codebook = {'age': 'Their age is'}) >>> a.old_keep("age", "height") Agent(traits = {'age': 10, 'height': 5.5}, codebook = {'age': 'Their age is'}) ``` ```python theme={null} >>> a.old_keep("height") Agent(traits = {'height': 5.5}) ``` > #### prompt() → [Prompt](/en/latest/prompts#edsl.prompts.Prompt "edsl.prompts.Prompt") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Generate a formatted prompt containing the agent’s traits. This method renders the agent’s traits presentation template with the agent’s traits and codebook, creating a formatted prompt that can be used in language model requests. The method is dynamic and responsive to changes in the agent’s state: 1. If a custom template was explicitly set during initialization, it will be used 2. If using the default template and the codebook has been updated since initialization, this method will recreate the template to reflect the current codebook values 3. The template is rendered with access to all trait values, the complete traits dictionary, and the codebook The template rendering makes the following variables available: - All individual trait keys (e.g., `{{ age }}`, `{{ occupation }}`) - The full traits dictionary as `{{ traits }}` - The codebook as `{{ codebook }}` > **Returns:** Prompt: A Prompt object containing the rendered template > **Raises:** QuestionScenarioRenderError: If any template variables remain undefined > **Examples:** Basic trait rendering without a codebook: ```python theme={null} >>> agent = Agent(traits={"age": 10, "hair": "brown", "height": 5.5}) >>> agent.prompt() Prompt(text="""Your traits: {'age': 10, 'hair': 'brown', 'height': 5.5}""") ``` Trait rendering with a codebook (more readable format): ```python theme={null} >>> codebook = {"age": "Age in years", "hair": "Hair color"} >>> agent = Agent(traits={"age": 10, "hair": "brown"}, codebook=codebook) >>> print(agent.prompt().text) Your traits: Age in years: 10 Hair color: brown ``` Adding a codebook after initialization updates the rendering: ```python theme={null} >>> agent = Agent(traits={"age": 30, "occupation": "doctor"}) >>> initial_prompt = agent.prompt() >>> "Your traits: {" in initial_prompt.text True >>> agent.codebook = {"age": "Age", "occupation": "Profession"} >>> updated_prompt = agent.prompt() >>> "Age: 30" in updated_prompt.text True >>> "Profession: doctor" in updated_prompt.text True ``` Custom templates can reference any trait directly: ```python theme={null} >>> template = "Profile: {{ age }} year old {{ occupation }}" >>> agent = Agent(traits={"age": 45, "occupation": "teacher"}, ... traits_presentation_template=template) >>> agent.prompt().text 'Profile: 45 year old teacher' ``` > #### remove\_direct\_question\_answering\_method() → None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Remove the direct question answering method. > **Example:** ```python theme={null} >>> 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](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Remove a trait from the agent. > **Args:** trait: The name of the trait to remove > **Returns:** A new Agent instance without the specified trait > **Example:** ```python theme={null} >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}) >>> a.remove_trait("age") Agent(traits = {'hair': 'brown', 'height': 5.5}) ``` > #### rename(*old\_name\_or\_dict: str | dict\[str, str]*, *new\_name: str | None = None*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Rename a trait. > **Args:** old\_name\_or\_dict: The old name of the trait or a dictionary of old names and new names new\_name: The new name of the trait (required if old\_name\_or\_dict is a string) > **Returns:** A new Agent instance with renamed traits > **Raises:** AgentErrors: If invalid arguments are provided > **Example:** ```python theme={null} >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}) >>> newa = a.rename("age", "years") >>> newa == Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5}) True ``` ```python theme={null} >>> newa.rename({'years': 'smage'}) == Agent(traits = {'smage': 10, 'hair': 'brown', 'height': 5.5}) True ``` > #### search\_traits(*search\_string: str*) → RankableItems [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Search the agent’s traits for a string. This method delegates to the traits manager to search through trait descriptions and return ranked matches based on similarity. > **Args:** search\_string: The string to search for in trait descriptions > **Returns:** A ScenarioList containing ranked trait matches > **Examples:** ```python theme={null} >>> from edsl.agents import Agent >>> agent = Agent(traits={"age": 30, "occupation": "doctor"}) >>> results = agent.search_traits("age") >>> len(results) >= 1 True ``` > #### select(\**traits: str*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Select agents with only the referenced traits. This method now uses the robust keep() implementation for better data integrity and consistent handling of codebooks and trait\_categories. > **Args:** [\*](/en/latest/#id8)traits: The trait names to select > **Returns:** A new Agent instance with only the selected traits > **Example:** ```python theme={null} >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}, codebook = {'age': 'Their age is'}) >>> a.select("age", "height") Agent(traits = {'age': 10, 'height': 5.5}, codebook = {'age': 'Their age is'}) ``` ```python theme={null} >>> a.select("height") Agent(traits = {'height': 5.5}) ``` > #### table() → [Dataset](/en/latest/dataset#edsl.dataset.Dataset "edsl.dataset.Dataset") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create a tabular representation of the agent’s traits. This method delegates to the table manager to create a structured Dataset containing trait information. > **Returns:** A Dataset containing trait information > **Example:** ```python theme={null} >>> agent = Agent(traits={'age': 30, 'height': 5.5}, codebook={'age': 'Age in years'}) >>> dataset = agent.table() >>> len(dataset) == 2 True ``` > #### to(*target: 'QuestionBase' | 'Jobs' | 'Survey'*) → [Jobs](/en/latest/jobs#jobs-class "edsl.jobs.Jobs") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Send the agent to a question, job, or survey. > **Args:** target: The question, job, or survey to send the agent to > **Returns:** A Jobs object containing the agent and target > **Example:** ```python theme={null} >>> agent = Agent(traits={'age': 30}) >>> from edsl.questions import QuestionFreeText >>> q = QuestionFreeText(question_name='test', question_text='How are you?') >>> job = agent.to(q) >>> type(job).__name__ 'Jobs' ``` > #### to\_dict(*add\_edsl\_version: bool = True*, *full\_dict: bool = False*) → dict\[str, dict | bool | str] [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Serialize to a dictionary with EDSL info. > **Args:** add\_edsl\_version: Whether to include EDSL version information full\_dict: Whether to include all attributes even if they have default values > **Returns:** A dictionary representation of the agent > **Example:** ```python theme={null} >>> a = Agent(name = "Steve", traits = {"age": 10, "hair": "brown", "height": 5.5}) >>> d = a.to_dict() >>> d['traits'] {'age': 10, 'hair': 'brown', 'height': 5.5} >>> d['name'] 'Steve' >>> d['edsl_class_name'] 'Agent' ``` ```python theme={null} >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}, instruction = "Have fun.") >>> d = a.to_dict() >>> d['traits'] {'age': 10, 'hair': 'brown', 'height': 5.5} >>> d['instruction'] 'Have fun.' >>> d['edsl_class_name'] 'Agent' ``` > #### *property* traits\*: dict\[str, Any]\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Get the agent’s traits, potentially using dynamic generation. This property provides access to the agent’s traits, either from the stored traits dictionary or by calling a dynamic traits function if one is defined. If a dynamic traits function is used, it may take the current question as a parameter to generate context-aware traits. > **Returns:** dict: Dictionary of agent traits (key-value pairs) > **Examples:** ```python theme={null} >>> a = Agent(traits={"age": 10, "hair": "brown", "height": 5.5}) >>> a.traits {'age': 10, 'hair': 'brown', 'height': 5.5} ``` > #### *property* traits\_presentation\_template [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Get the traits presentation template. > #### translate\_traits(*values\_codebook: dict\[str, dict\[Any, Any]]*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Translate traits to a new codebook. > **Args:** values\_codebook: Dictionary mapping trait names to value translation dictionaries > **Returns:** A new Agent instance with translated trait values > **Example:** ```python theme={null} >>> 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}) ``` > #### with\_categories(\**categories: str*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return a new agent with the specified categories ## AgentList class > #### *class* edsl.agents.AgentList(*data: list\[[Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent")] | None = None*, *codebook: dict\[str, str] | None = None*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Bases: `UserList`, [`Base`](/en/latest/base#base-class "edsl.base.base_class.Base"), `AgentListOperationsMixin` A list of Agents with additional functionality for manipulation and analysis. The AgentList class extends Python’s UserList to provide a container for Agent objects with methods for filtering, transforming, and analyzing collections of agents. ```python theme={null} >>> AgentList.example().to_scenario_list().drop('age') ScenarioList([Scenario({'hair': 'brown', 'height': 5.5}), Scenario({'hair': 'brown', 'height': 5.5})]) ``` ```python theme={null} >>> AgentList.example().to_dataset() Dataset([{'age': [22, 22]}, {'hair': ['brown', 'brown']}, {'height': [5.5, 5.5]}]) ``` ```python theme={null} >>> AgentList.example().select('age', 'hair', 'height').to_pandas() age hair height 0 22 brown 5.5 1 22 brown 5.5 ``` > #### **init**(*data: list\[[Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent")] | None = None*, *codebook: dict\[str, str] | None = None*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Initialize a new AgentList. ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5}), ... Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5})]) >>> al AgentList([Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5}), Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5})]) >>> al_with_codebook = AgentList([Agent(traits = {'age': 22})], codebook={'age': 'Age in years'}) >>> al_with_codebook[0].codebook {'age': 'Age in years'} ``` > **Args:** data: A list of Agent objects. If None, creates an empty AgentList. codebook: Optional dictionary mapping trait names to descriptions. If provided, will be applied to all agents in the list. > #### add\_instructions(*instructions: str*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Apply instructions to all agents in the list. This method provides a more intuitive name for setting instructions on all agents, avoiding the need to iterate manually. > **Args:** instructions: The instructions to apply to all agents. > **Returns:** AgentList: Returns self for method chaining. > **Examples:** ```python theme={null} >>> from edsl import Agent, AgentList >>> agents = AgentList([Agent(traits={'age': 30}), Agent(traits={'age': 40})]) >>> agents.add_instructions("Answer as if you were this age") AgentList([Agent(traits = {'age': 30}, instruction = """Answer as if you were this age"""), Agent(traits = {'age': 40}, instruction = """Answer as if you were this age""")]) ``` > #### add\_trait(*trait: str*, *values: List\[Any]*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Adds a new trait to every agent, with values taken from values. > **Parameters:** * **trait** – The name of the trait. * **values** – The valeues(s) of the trait. If a single value is passed, it is used for all agents. ```python theme={null} >>> al = AgentList.example() >>> new_al = al.add_trait('new_trait', 1) >>> new_al.select('new_trait').to_scenario_list().to_list() [1, 1] >>> al.add_trait('new_trait', [1, 2, 3]) Traceback (most recent call last): ... edsl.agents.exceptions.AgentListError: The passed values have to be the same length as the agent list. ... ``` > #### *property* all\_traits\*: list\[str]\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return all traits in the AgentList. >>> from edsl import Agent >>> agent\_1 = Agent(traits = \{‘age’: 22}) >>> agent\_2 = Agent(traits = \{‘hair’: ‘brown’}) >>> al = AgentList(\[agent\_1, agent\_2]) >>> al.all\_traits \[‘age’, ‘hair’] > #### at(*index: int*) → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Get the agent at the specified index position. > #### chart() [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create a chart from the results. > #### clipboard\_data() → str [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return TSV representation of this object for clipboard operations. This method is called by the clipboard() method in the base class to provide a custom format for copying objects to the system clipboard. > **Returns:** str: Tab-separated values representation of the object > #### code(*string=True*) → str | list\[str] [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return code to construct an AgentList. ```python theme={null} >>> al = AgentList.example() >>> print(al.code()) from edsl import Agent from edsl import AgentList agent_list = AgentList([Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5}), Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5})]) ``` > #### *property* codebook\*: dict\[str, str]\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return the codebook for the AgentList. > #### collapse(*warn\_about\_none\_name: bool = True*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) All agents with the same name have their traits combined. ```python theme={null} >>> al = AgentList([Agent(name = 'steve'), Agent(name = 'roxanne')]) >>> al.collapse() AgentList([Agent(name = """steve""", traits = {}), Agent(name = """roxanne""", traits = {})]) >>> al = AgentList([Agent(name = 'steve', traits = {'age': 22}), Agent(name = 'steve', traits = {'hair': 'brown'})]) >>> al.collapse() AgentList([Agent(name = """steve""", traits = {'age': 22, 'hair': 'brown'})]) >>> AgentList.example().collapse(warn_about_none_name = False) AgentList([Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5})]) ``` > #### drop(\*\*field\_names: str | List\[str]\*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Drop field(s) from all agents in the AgentList. > **Args:** [\*](/en/latest/#id10)**field\_names: The name(s) of the field(s) to drop. Can be:** * Single field name: drop(“age”) * Multiple field names: drop(“age”, “height”) * List of field names: drop(\[“age”, “height”]) > **Returns:** AgentList: A new AgentList with the specified fields dropped from all agents. > **Examples:** Drop a single trait from all agents: ```python theme={null} >>> from edsl import Agent, AgentList >>> al = AgentList([Agent(traits={"age": 30, "hair": "brown", "height": 5.5})]) >>> al_dropped = al.drop("age") >>> al_dropped[0].traits {'hair': 'brown', 'height': 5.5} ``` Drop multiple traits using separate arguments: ```python theme={null} >>> al = AgentList([Agent(traits={"age": 30, "hair": "brown", "height": 5.5})]) >>> al_dropped = al.drop("age", "height") >>> al_dropped[0].traits {'hair': 'brown'} ``` Drop multiple traits using a list: ```python theme={null} >>> al = AgentList([Agent(traits={"age": 30, "hair": "brown", "height": 5.5})]) >>> al_dropped = al.drop(["age", "height"]) >>> al_dropped[0].traits {'hair': 'brown'} ``` > #### duplicate() → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create a deep copy of the AgentList. > **Returns:** AgentList: A new AgentList containing copies of all agents. > **Examples:** ```python theme={null} >>> al = AgentList.example() >>> al2 = al.duplicate() >>> al2 == al True >>> id(al2) == id(al) False ``` > #### edit() [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### *classmethod* example(*randomize: bool = False*, *codebook: dict\[str, str] | None = None*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Returns an example AgentList instance. > **Parameters:** * **randomize** – If True, uses Agent’s randomize method. * **codebook** – Optional dictionary mapping trait names to descriptions. ```python theme={null} >>> al = AgentList.example() >>> al AgentList([Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5}), Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5})]) >>> al = AgentList.example(codebook={'age': 'Age in years'}) >>> al[0].codebook {'age': 'Age in years'} ``` > #### filter(*expression: str*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Filter agents based on a boolean expression. > **Args:** expression: A string containing a boolean expression to evaluate against each agent’s traits. > **Returns:** AgentList: A new AgentList containing only agents that satisfy the expression. > **Examples:** ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent(traits = {'a': 1, 'b': 1}), ... Agent(traits = {'a': 1, 'b': 2})]) >>> al.filter("b == 2") AgentList([Agent(traits = {'a': 1, 'b': 2})]) >>> al = AgentList([Agent(traits = {'a': 1, 'b': 1}, name = 'steve'), ... Agent(traits = {'a': 1, 'b': 2}, name = 'roxanne')]) >>> len(al.filter("name == 'steve'")) 1 >>> len(al.filter("name == 'roxanne'")) 1 >>> len(al.filter("name == 'steve' and a == 1")) 1 >>> len(al.filter("name == 'steve' and a == 2")) 0 >>> len(al.filter("name == 'steve' and a == 1 and b == 2")) 0 ``` > #### first() → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Get the first agent in the list. > #### flatten(*field: str*, *keep\_original: bool = False*) → [Dataset](/en/latest/dataset#edsl.dataset.Dataset "edsl.dataset.Dataset") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Expand a field containing dictionaries into separate fields. This method takes a field that contains a list of dictionaries and expands it into multiple fields, one for each key in the dictionaries. This is useful when working with nested data structures or results from extraction operations. > **Parameters:** field: The field containing dictionaries to flatten keep\_original: Whether to retain the original field in the result > **Returns:** A new Dataset with the dictionary keys expanded into separate fields > **Notes:** * Each key in the dictionaries becomes a new field with name pattern “.” * All dictionaries in the field must have compatible structures * If a dictionary is missing a key, the corresponding value will be None * Non-dictionary values in the field will cause a warning > **Examples:** ```python theme={null} >>> from edsl.dataset import Dataset ``` ```python theme={null} # Basic flattening of nested dictionaries >>> Dataset([{‘a’: [{‘a’: 1, ‘b’: 2}]}, {‘c’: [5]}]).flatten(‘a’) Dataset([{‘c’: [5]}, {‘a.a’: [1]}, {‘a.b’: [2]}]) # Works with prefixed fields too >>> Dataset([{‘answer.example’: [{‘a’: 1, ‘b’: 2}]}, {‘c’: [5]}]).flatten(‘answer.example’) Dataset([{‘c’: [5]}, {‘answer.example.a’: [1]}, {‘answer.example.b’: [2]}]) # Keep the original field if needed >>> d = Dataset([{‘a’: [{‘a’: 1, ‘b’: 2}]}, {‘c’: [5]}]) >>> d.flatten(‘a’, keep_original=True) Dataset([{‘a’: [{‘a’: 1, ‘b’: 2}]}, {‘c’: [5]}, {‘a.a’: [1]}, {‘a.b’: [2]}]) # Can also use unambiguous unprefixed field name >>> result = Dataset([{‘answer.pros_cons’: [{‘pros’: [‘Safety’], ‘cons’: [‘Cost’]}]}]).flatten(‘pros_cons’) >>> sorted(result.keys()) == [‘answer.pros_cons.cons’, ‘answer.pros_cons.pros’] True >>> sorted(result.to_dicts()[0].items()) == sorted({‘cons’: [‘Cost’], ‘pros’: [‘Safety’]}.items()) True ``` > #### *classmethod* from\_csv(*file\_path: str*, *name\_field: str | None = None*, *codebook: dict\[str, str] | None = None*, *instructions: str | None = None*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Load AgentList from a CSV file. **Deprecated since version Use:** AgentList.from\_source(‘csv’, …) instead. ```python theme={null} >>> import csv >>> import os >>> with open('/tmp/agents.csv', 'w') as f: ... writer = csv.writer(f) ... _ = writer.writerow(['age', 'hair', 'height']) ... _ = writer.writerow([22, 'brown', 5.5]) >>> al = AgentList.from_csv('/tmp/agents.csv') >>> al = AgentList.from_csv('/tmp/agents.csv', name_field='hair') >>> al = AgentList.from_csv('/tmp/agents.csv', codebook={'age': 'Age in years'}) >>> al = AgentList.from_csv('/tmp/agents.csv', instructions='Answer as a person') >>> os.remove('/tmp/agents.csv') ``` **Parameters:** * **file\_path** – The path to the CSV file. * **name\_field** – The name of the field to use as the agent name. * **codebook** – Optional dictionary mapping trait names to descriptions. * **instructions** – Optional instructions to apply to all created agents. > #### *classmethod* from\_dict(*data: dict*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Deserialize the dictionary back to an AgentList object. > **Parameters:** data: A dictionary representing an AgentList. ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent.example(), Agent.example()]) >>> al2 = AgentList.from_dict(al.to_dict()) >>> al2 == al True >>> example_codebook = {'age': 'Age in years'} >>> al = AgentList([Agent.example()]).set_codebook(example_codebook) >>> al2 = AgentList.from_dict(al.to_dict()) >>> al2[0].codebook == example_codebook True ``` > #### *classmethod* from\_list(*trait\_name: str*, *values: List\[Any]*, *codebook: dict\[str, str] | None = None*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create an AgentList from a list of values. > **Parameters:** * **trait\_name** – The name of the trait. * **values** – A list of values. * **codebook** – Optional dictionary mapping trait names to descriptions. ```python theme={null} >>> AgentList.from_list('age', [22, 23]) AgentList([Agent(traits = {'age': 22}), Agent(traits = {'age': 23})]) >>> al = AgentList.from_list('age', [22], codebook={'age': 'Age in years'}) >>> al[0].codebook {'age': 'Age in years'} ``` > #### *classmethod* from\_results(*results: [Results](/en/latest/results#result-class "edsl.results.Results")*, *question\_names: List\[str] | None = None*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create an AgentList from a Results object. > **Args:** results: The Results object to convert question\_names: Optional list of question names to include. If None, all questions are included. Affects both answer.\* columns (as traits) and prompt.\* columns (as codebook). Agent traits are always included. > **Returns:** AgentList: A new AgentList created from the Results > #### *classmethod* from\_scenario\_list(*scenario\_list: [ScenarioList](/en/latest/scenarios#scenariolist-class "edsl.scenarios.ScenarioList")*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create an AgentList from a ScenarioList. This method supports special fields that map to Agent parameters: - “name”: Will be used as the agent’s name - “agent\_parameters”: A dictionary containing: * “instruction”: The agent’s instruction text * “name”: The agent’s name (overrides the “name” field if present) > **Examples:** ```python theme={null} >>> from edsl import ScenarioList, Scenario >>> # Basic usage with traits >>> s = ScenarioList([Scenario({'age': 22, 'hair': 'brown', 'height': 5.5})]) >>> al = AgentList.from_scenario_list(s) >>> al AgentList([Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5})]) ``` > #### *classmethod* from\_source(*source\_type: str*, \**args*, *instructions: str | None = None*, *codebook: dict\[str, str] | None = None*, *name\_field: str | None = None*, \*\**kwargs*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create an AgentList from a specified source type. This method serves as the main entry point for creating AgentList objects, providing a unified interface for various data sources. > **Args:** **source\_type: The type of source to create an AgentList from.** Valid values include: ‘csv’, ‘tsv’, ‘excel’, ‘pandas’, etc. [\*](/en/latest/#id12)args: Positional arguments to pass to the source-specific method. instructions: Optional instructions to apply to all created agents. codebook: Optional dictionary mapping trait names to descriptions, or a path to a CSV file. If a CSV file is provided, it should have 2 columns: original keys and descriptions. Keys will be automatically converted to pythonic names. name\_field: The name of the field to use as the agent name (for CSV/Excel sources). [\*\*](/en/latest/#id14)kwargs: Additional keyword arguments to pass to the source-specific method. > **Returns:** An AgentList object created from the specified source. > **Examples:** ```python theme={null} >>> # Create agents from a CSV file with instructions >>> # agents = AgentList.from_source( >>> # 'csv', 'agents.csv', >>> # instructions="Answer as if you were the person described" >>> # ) >>> # >>> # Create agents with a CSV codebook file >>> # agents = AgentList.from_source( >>> # 'csv', 'agents.csv', >>> # codebook='codebook.csv' # CSV with keys like "Age in years" -> "age_in_years" >>> # ) ``` > #### *static* get\_codebook(*file\_path: str*) → dict [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Returns a codebook dictionary mapping CSV column names to None. Reads the header row of a CSV file and creates a codebook with field names as keys and None as values. **Args:** file\_path: Path to the CSV file to read. **Returns:** A dictionary with CSV column names as keys and None as values. **Raises:** FileNotFoundError: If the specified file path does not exist. csv.Error: If there is an error reading the CSV file. > #### get\_tabular\_data(*remove\_prefix: bool = False*, *pretty\_labels: dict | None = None*) → Tuple\[List\[str], List\[List]] [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Internal method to get tabular data in a standard format. **Args:** remove\_prefix: Whether to remove the prefix from column names pretty\_labels: Dictionary mapping original column names to pretty labels **Returns:** Tuple containing (header\_row, data\_rows) > #### ggplot2(*ggplot\_code: str*, *shape: str = 'wide'*, *sql: str | None = None*, *remove\_prefix: bool = True*, *debug: bool = False*, *height: float = 4*, *width: float = 6*, *factor\_orders: dict | None = None*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create visualizations using R’s ggplot2 library. This method provides a bridge to R’s powerful ggplot2 visualization library, allowing you to create sophisticated plots directly from EDSL data structures. > **Parameters:** ggplot\_code: R code string containing ggplot2 commands shape: Data shape to use (“wide” or “long”) sql: Optional SQL query to transform data before visualization remove\_prefix: Whether to remove prefixes (like “answer.”) from column names debug: Whether to display debugging information height: Plot height in inches width: Plot width in inches factor\_orders: Dictionary mapping factor variables to their desired order > **Returns:** A plot object that renders in Jupyter notebooks > **Notes:** * Requires R and the ggplot2 package to be installed * Data is automatically converted to a format suitable for ggplot2 * The ggplot2 code should reference column names as they appear after any transformations from the shape and remove\_prefix parameters > **Examples:** ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> # The following would create a plot if R is installed (not shown in doctest): >>> # r.ggplot2(''' >>> # ggplot(df, aes(x=how_feeling)) + >>> # geom_bar() + >>> # labs(title="Distribution of Feelings") >>> # ''') ``` > #### give\_names(\**trait\_keys: str*, *remove\_traits: bool = True*, *separator: str = ','*, *force\_name: bool = False*) → None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Give names to agents based on the values of the specified traits. ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent(traits = {'a': 1, 'b': 1}), ... Agent(traits = {'a': 1, 'b': 2})]) >>> al.give_names('a') >>> al[0].name '1' ``` > #### html(*filename: str | None = None*, *cta: str = 'Open in browser'*, *return\_link: bool = False*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### join(*other: [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList")*, *join\_type: str = 'inner'*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Join this AgentList with another AgentList. > **Args:** other: The other AgentList to join with join\_type: The type of join to perform (“inner”, “left”, or “right”) > **Returns:** AgentList: A new AgentList containing the joined results > **Examples:** ```python theme={null} >>> from edsl import Agent, AgentList >>> al1 = AgentList([Agent(name="John", traits={"age": 30})]) >>> al2 = AgentList([Agent(name="John", traits={"height": 180})]) >>> joined = al1.join(al2) >>> joined[0].traits {'age': 30, 'height': 180} ``` > #### *classmethod* join\_multiple(\*\*agent\_lists: [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList")\*, *join\_type: str = 'inner'*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Join multiple AgentLists together. > **Args:** [\*](/en/latest/#id16)agent\_lists: Variable number of AgentList objects to join join\_type: The type of join to perform (“inner”, “left”, or “right”) > **Returns:** AgentList: A new AgentList containing the joined results > **Raises:** ValueError: If fewer than 2 AgentLists are provided > **Examples:** ```python theme={null} >>> from edsl import Agent, AgentList >>> al1 = AgentList([Agent(name="John", traits={"age": 30})]) >>> al2 = AgentList([Agent(name="John", traits={"height": 180})]) >>> al3 = AgentList([Agent(name="John", traits={"weight": 75})]) >>> joined = AgentList.join_multiple(al1, al2, al3) >>> len(joined) 1 >>> joined[0].traits {'age': 30, 'height': 180, 'weight': 75} ``` > #### keep(\*\*field\_names: str | List\[str]\*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Keep only the specified fields from all agents in the AgentList. > **Args:** [\*](/en/latest/#id18)field\_names: The name(s) of the field(s) to keep. Can be: * Single field name: keep(“age”) * Multiple field names: keep(“age”, “height”) * List of field names: keep(\[“age”, “height”]) > **Returns:** AgentList: A new AgentList with only the specified fields kept for all agents. > **Examples:** Keep a single trait for all agents: ```python theme={null} >>> from edsl import Agent, AgentList >>> al = AgentList([Agent(traits={"age": 30, "hair": "brown", "height": 5.5})]) >>> al_kept = al.keep("age") >>> al_kept[0].traits {'age': 30} ``` Keep multiple traits using separate arguments: ```python theme={null} >>> al = AgentList([Agent(traits={"age": 30, "hair": "brown", "height": 5.5})]) >>> al_kept = al.keep("age", "height") >>> al_kept[0].traits {'age': 30, 'height': 5.5} ``` Keep multiple traits using a list: ```python theme={null} >>> al = AgentList([Agent(traits={"age": 30, "hair": "brown", "height": 5.5})]) >>> al_kept = al.keep(["age", "height"]) >>> al_kept[0].traits {'age': 30, 'height': 5.5} ``` Keep agent fields and traits: ```python theme={null} >>> al = AgentList([Agent(traits={"age": 30, "hair": "brown"}, name="John")]) >>> al_kept = al.keep("name", "age") >>> al_kept[0].name 'John' >>> al_kept[0].traits {'age': 30} ``` > #### last() → [Agent](/en/latest/#edsl.agents.Agent "edsl.agents.agent.Agent") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Get the last agent in the list. > #### make\_tabular(*remove\_prefix: bool*, *pretty\_labels: dict | None = None*) → tuple\[list, List\[list]] [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Turn the results into a tabular format. > **Parameters:** **remove\_prefix** – Whether to remove the prefix from the column names. ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> r.select('how_feeling').make_tabular(remove_prefix = True) (['how_feeling'], [['OK'], ['Great'], ['Terrible'], ['OK']]) >>> r.select('how_feeling').make_tabular(remove_prefix = True, pretty_labels = {'how_feeling': "How are you feeling"}) (['How are you feeling'], [['OK'], ['Great'], ['Terrible'], ['OK']]) ``` > #### *classmethod* manage() [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### *property* names\*: List\[str]\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Returns the names of the agents in the AgentList. > #### num\_observations() [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return the number of observations in the dataset. ```python theme={null} >>> from edsl.results import Results >>> Results.example().num_observations() 4 ``` > #### print\_long() [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Print the results in a long format. >>> from edsl.results import Results >>> r = Results.example() >>> r.select(‘how\_feeling’).print\_long() answer.how\_feeling: OK answer.how\_feeling: Great answer.how\_feeling: Terrible answer.how\_feeling: OK > #### relevant\_columns(*data\_type: str | None = None*, *remove\_prefix: bool = False*) → list [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return the set of keys that are present in the dataset. **Parameters:** * **data\_type** – The data type to filter by. * **remove\_prefix** – Whether to remove the prefix from the column names. ```python expandable theme={null} >>> from ..dataset import Dataset >>> d = Dataset([{'a.b':[1,2,3,4]}]) >>> d.relevant_columns() ['a.b'] >>> d.relevant_columns(remove_prefix=True) ['b'] >>> d = Dataset([{'a':[1,2,3,4]}, {'b':[5,6,7,8]}]) >>> d.relevant_columns() ['a', 'b'] >>> from edsl.results import Results; Results.example().select('how_feeling', 'how_feeling_yesterday').relevant_columns() ['answer.how_feeling', 'answer.how_feeling_yesterday'] >>> from edsl.results import Results >>> sorted(Results.example().select().relevant_columns(data_type = "model")) ['model.canned_response', 'model.inference_service', 'model.model', 'model.model_index', 'model.temperature'] >>> # Testing relevant_columns with invalid data_type raises DatasetValueError - tested in unit tests ``` > #### remove\_prefix() [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Returns a new Dataset with the prefix removed from all column names. The prefix is defined as everything before the first dot (.) in the column name. If removing prefixes would result in duplicate column names, an exception is raised. > **Returns:** Dataset: A new Dataset with prefixes removed from column names > **Raises:** ValueError: If removing prefixes would result in duplicate column names > **Examples:** ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> r.select('how_feeling', 'how_feeling_yesterday').relevant_columns() ['answer.how_feeling', 'answer.how_feeling_yesterday'] >>> r.select('how_feeling', 'how_feeling_yesterday').remove_prefix().relevant_columns() ['how_feeling', 'how_feeling_yesterday'] ``` ```python theme={null} >>> from edsl.dataset import Dataset >>> d = Dataset([{'a.x': [1, 2, 3]}, {'b.x': [4, 5, 6]}]) >>> # d.remove_prefix() ``` \# Testing remove\_prefix with duplicate column names raises DatasetValueError - tested in unit tests > #### remove\_trait(*trait: str*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Remove traits from the AgentList. > **Parameters:** **traits** – The traits to remove. ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent({'age': 22, 'hair': 'brown', 'height': 5.5}), Agent({'age': 22, 'hair': 'brown', 'height': 5.5})]) >>> al.remove_trait('age') AgentList([Agent(traits = {'hair': 'brown', 'height': 5.5}), Agent(traits = {'hair': 'brown', 'height': 5.5})]) ``` > #### rename(*old\_name: str*, *new\_name: str*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Rename a trait across all agents in the list. > **Args:** old\_name: The current name of the trait. new\_name: The new name to assign to the trait. > **Returns:** AgentList: A new AgentList with the renamed trait. > **Examples:** ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent(traits = {'a': 1, 'b': 1}), ... Agent(traits = {'a': 1, 'b': 2})]) >>> al2 = al.rename('a', 'c') >>> assert al2 == AgentList([Agent(traits = {'c': 1, 'b': 1}), ... Agent(traits = {'c': 1, 'b': 2})]) >>> assert al != al2 ``` > #### report(\**fields: str | None*, *top\_n: int | None = None*, *header\_fields: List\[str] | None = None*, *divider: bool = True*, *return\_string: bool = False*, *format: str = 'markdown'*, *filename: str | None = None*) → str | Document | None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Generates a report of the results by iterating through rows. > **Args:** [\*](/en/latest/#id20)fields: The fields to include in the report. If none provided, all fields are used. top\_n: Optional limit on the number of observations to include. header\_fields: Optional list of fields to include in the main header instead of as sections. divider: If True, adds a horizontal rule between observations (markdown only). return\_string: If True, returns the markdown string. If False (default in notebooks), only displays the markdown without returning. format: Output format - either “markdown” or “docx”. filename: If provided and format is “docx”, saves the document to this file. > **Returns:** Depending on format and return\_string: - For markdown: A string if return\_string is True, otherwise None (displays in notebook) - For docx: A docx.Document object, or None if filename is provided (saves to file) Examples: ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> report = r.select('how_feeling').report(return_string=True) >>> "# Observation: 1" in report True >>> doc = r.select('how_feeling').report(format="docx") >>> isinstance(doc, object) True ``` > #### report\_from\_template(*template: str*, \**fields: str | None*, *top\_n: int | None = None*, *remove\_prefix: bool = True*, *return\_string: bool = False*, *format: str = 'text'*, *filename: str | None = None*, *separator: str = 'nn'*, *observation\_title\_template: str | None = None*, *explode: bool = False*, *filestore: bool = False*) → str | Document | List | [FileStore](/en/latest/filestore#edsl.scenarios.FileStore "edsl.scenarios.FileStore") | None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Generates a report using a Jinja2 template for each row in the dataset. This method renders a user-provided Jinja2 template for each observation in the dataset, with template variables populated from the row data. This allows for completely customized report formatting using pandoc for advanced output formats. > **Args:** template: Jinja2 template string to render for each row [\*](/en/latest/#id22)fields: The fields to include in template context. If none provided, all fields are used. top\_n: Optional limit on the number of observations to include. remove\_prefix: Whether to remove type prefixes (e.g., “answer.”) from field names in template context. return\_string: If True, returns the rendered content. If False (default in notebooks), only displays the content without returning. format: Output format - one of “text”, “html”, “pdf”, or “docx”. Formats other than “text” require pandoc. filename: If provided, saves the rendered content to this file. For exploded output, this becomes a template (e.g., “report\_”). separator: String to use between rendered templates for each row (ignored when explode=True). observation\_title\_template: Optional Jinja2 template for observation titles. Defaults to “Observation ” where index is 1-based. Template has access to all row data plus ‘index’ and ‘index0’ variables. explode: If True, creates separate files for each observation instead of one combined file. filestore: If True, wraps the generated file(s) in FileStore object(s). If no filename is provided, creates temporary files. For exploded output, returns a list of FileStore objects. > **Returns:** Depending on explode, format, return\_string, and filestore: - For text format: String content or None (if displayed in notebook) - For html format: HTML string content or None (if displayed in notebook) - For docx format: Document object or None (if saved to file) - For pdf format: PDF bytes or None (if saved to file) - If explode=True: List of created filenames (when filename provided) or list of documents/content - If filestore=True: FileStore object(s) containing the generated file(s) > **Notes:** * Pandoc is required for HTML, PDF, and DOCX output formats * Templates are treated as Markdown for all non-text formats * PDF output uses XeLaTeX engine through pandoc * HTML output includes standalone document structure > **Examples:** ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> template = "Person feels: {{ how_feeling }}" >>> report = r.select('how_feeling').report_from_template(template, return_string=True) >>> "Person feels: OK" in report True >>> "Person feels: Great" in report True ``` ```python theme={null} # Custom observation titles >>> custom_title = “Response {{ index }}: {{ how_feeling }}” >>> report = r.select(‘how_feeling’).report_from_template( … template, observation_title_template=custom_title, return_string=True) >>> “Response 1: OK” in report True # HTML output (requires pandoc) >>> html_report = r.select(‘how_feeling’).report_from_template( … template, format=”html”, return_string=True) # doctest: +SKIP >>> # Creates HTML with proper document structure # PDF output (requires pandoc with XeLaTeX) >>> pdf_report = r.select(‘how_feeling’).report_from_template( … template, format=”pdf”) # doctest: +SKIP >>> # Returns PDF bytes # Basic template functionality >>> template2 = “Feeling: {{ how_feeling }}, Index: {{ index }}” >>> report2 = r.select(‘how_feeling’).report_from_template( … template2, return_string=True, top_n=2) >>> “Feeling: OK, Index: 1” in report2 True ``` > #### sample(*n: int*, *seed: str | None = None*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return a random sample of agents. **Args:** n: The number of agents to sample. seed: Optional seed for the random number generator to ensure reproducibility. **Returns:** AgentList: A new AgentList containing the sampled agents. > #### select(\**traits*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Create a new AgentList with only the specified traits. **Args:** [\*](/en/latest/#id24)traits: Variable number of trait names to keep. **Returns:** AgentList: A new AgentList containing agents with only the selected traits. Examples: ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent(traits = {'a': 1, 'b': 1}), ... Agent(traits = {'a': 1, 'b': 2})]) >>> al.select('a') AgentList([Agent(traits = {'a': 1}), Agent(traits = {'a': 1})]) ``` > #### set\_codebook(*codebook: dict\[str, str]*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Set the codebook for the AgentList. ```python theme={null} >>> from edsl import Agent >>> a = Agent(traits = {'hair': 'brown'}) >>> al = AgentList([a, a]) >>> _ = al.set_codebook({'hair': "Color of hair on driver's license"}) >>> al[0].codebook {'hair': "Color of hair on driver's license"} ``` > **Parameters:** **codebook** – The codebook. > #### set\_dynamic\_traits(*function: Callable*) → None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Set the dynamic traits for all agents in the list. > **Args:** function: The function to set. > #### set\_dynamic\_traits\_from\_question\_map(*q\_to\_traits: dict\[str, list\[str]]*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Configure dynamic traits for each agent from a question→traits mapping (in-place). Each agent will get a dynamic traits function that, when asked a question whose `question_name` is present in `q_to_traits`, returns a dict mapping the corresponding trait name(s) to the agent’s original static value(s) for those trait(s). A warning is emitted if the set of mapped trait names does not exactly equal the set of trait keys present in this AgentList. > **Args:** q\_to\_traits: Mapping from question name to list of trait keys, e.g. `{"geo": ["hometown"], "cuisine": ["food"]}` > **Returns:** AgentList: self (modified in-place). > **Examples:** ```python theme={null} >>> from edsl import Agent, AgentList >>> a_alice = Agent(name="Alice", traits={'hometown': 'Boston', 'food': 'beans'}) >>> a_bob = Agent(name="Bob", traits={'hometown': 'SF', 'food': 'sushi'}) >>> al = AgentList([a_alice, a_bob]) >>> _ = al.set_dynamic_traits_from_question_map({'geo': ['hometown'], 'cuisine': ['food']}) >>> class Q: ... def __init__(self, name): self.question_name = name >>> al[0].dynamic_traits_function(Q('geo'))['hometown'] 'Boston' >>> al[1].dynamic_traits_function(Q('geo'))['hometown'] 'SF' >>> al[0].dynamic_traits_function(Q('cuisine'))['food'] 'beans' ``` > #### set\_instruction(*instruction: str*) → None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Set the instruction for all agents in the list. **Args:** instruction: The instruction to set. > #### set\_traits\_presentation\_template(*traits\_presentation\_template: str*) → None [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Set the traits presentation template for all agents in the list. **Args:** traits\_presentation\_template: The traits presentation template to set. > #### shuffle(*seed: str | None = None*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Randomly shuffle the agents in place. **Args:** seed: Optional seed for the random number generator to ensure reproducibility. **Returns:** AgentList: The shuffled AgentList (self). > #### sql(*query: str*, *transpose: bool = None*, *transpose\_by: str = None*, *remove\_prefix: bool = True*, *shape: str = 'wide'*) → [Dataset](/en/latest/dataset#edsl.dataset.Dataset "edsl.dataset.Dataset") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Execute SQL queries on the dataset. This powerful method allows you to use SQL to query and transform your data, combining the expressiveness of SQL with EDSL’s data structures. It works by creating an in-memory SQLite database from your data and executing the query against it. > **Parameters:** query: SQL query string to execute transpose: Whether to transpose the resulting table (rows become columns) transpose\_by: Column to use as the new index when transposing remove\_prefix: Whether to remove type prefixes (e.g., “answer.”) from column names shape: Data shape to use (“wide” or “long”) * “wide”: Default tabular format with columns for each field * “long”: Melted format with key-value pairs, useful for certain queries > **Returns:** A Dataset object containing the query results > **Notes:** * The data is stored in a table named “self” in the SQLite database * In wide format, column names include their type prefix unless remove\_prefix=True * In long format, the data is melted into columns: row\_number, key, value, data\_type * Complex objects like lists and dictionaries are converted to strings > **Examples:** ```python theme={null} >>> from edsl import Results >>> r = Results.example() ``` ```sql theme={null} # Basic selection >>> len(r.sql(“SELECT * FROM self”, shape=”wide”)) 4 # Filtering with WHERE clause >>> r.sql(“SELECT * FROM self WHERE how_feeling = ‘Great’”).num_observations() 1 # Aggregation >>> r.sql(“SELECT how_feeling, COUNT(*) as count FROM self GROUP BY how_feeling”).keys() [‘how_feeling’, ‘count’] # Using long format >>> len(r.sql(“SELECT * FROM self”, shape=”long”)) 200 ``` > #### table(\**fields*, *tablefmt: str | None = 'rich'*, *pretty\_labels: dict | None = None*) → Any [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### tally(\**fields: str | None*, *top\_n: int | None = None*, *output='Dataset'*) → dict | [Dataset](/en/latest/dataset#edsl.dataset.Dataset "edsl.dataset.Dataset") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Count frequency distributions of values in specified fields. This method tallies the occurrence of unique values within one or more fields, similar to a GROUP BY and COUNT in SQL. When multiple fields are provided, it performs cross-tabulation across those fields. > **Parameters:** [\*](/en/latest/#id26)fields: Field names to tally. If none provided, uses all available fields. top\_n: Optional limit to return only the top N most frequent values. output: Format for results, either “Dataset” (recommended) or “dict”. > **Returns:** By default, returns a Dataset with columns for the field(s) and a ‘count’ column. If output=”dict”, returns a dictionary mapping values to counts. > **Notes:** * For single fields, returns counts of each unique value * For multiple fields, returns counts of each unique combination of values * Results are sorted in descending order by count * Fields can be specified with or without their type prefix > **Examples:** ```python theme={null} >>> from edsl import Results >>> r = Results.example() ``` ```sql theme={null} # Single field frequency count >>> r.select(‘how_feeling’).tally(‘answer.how_feeling’, output=”dict”) {‘OK’: 2, ‘Great’: 1, ‘Terrible’: 1} # Return as Dataset (default) >>> from edsl.dataset import Dataset >>> expected = Dataset([{‘answer.how_feeling’: [‘OK’, ‘Great’, ‘Terrible’]}, {‘count’: [2, 1, 1]}]) >>> r.select(‘how_feeling’).tally(‘answer.how_feeling’, output=”Dataset”) == expected True # Multi-field cross-tabulation - exact output varies based on data >>> result = r.tally(‘how_feeling’, ‘how_feeling_yesterday’) >>> ‘how_feeling’ in result.keys() and ‘how_feeling_yesterday’ in result.keys() and ‘count’ in result.keys() True ``` > #### to(*target: 'Question' | 'Jobs' | 'Survey'*) → [Jobs](/en/latest/jobs#jobs-class "edsl.jobs.Jobs") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) > #### to\_agent\_list(*remove\_prefix: bool = True*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the results to a list of dictionaries, one per agent. > **Parameters:** **remove\_prefix** – Whether to remove the prefix from the column names. ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> r.select('how_feeling').to_agent_list() AgentList([Agent(traits = {'how_feeling': 'OK'}), Agent(traits = {'how_feeling': 'Great'}), Agent(traits = {'how_feeling': 'Terrible'}), Agent(traits = {'how_feeling': 'OK'})]) ``` > #### to\_csv(*filename: str | None = None*, *remove\_prefix: bool = False*, *pretty\_labels: dict | None = None*) → [FileStore](/en/latest/filestore#edsl.scenarios.FileStore "edsl.scenarios.FileStore") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Export the results to a FileStore instance containing CSV data. > #### to\_dataset(*traits\_only: bool = True*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the AgentList to a Dataset. > **Args:** traits\_only: If True, only include agent traits. If False, also include agent parameters like instructions and names. > **Returns:** Dataset: A dataset containing the agents’ traits and optionally their parameters. > **Examples:** ```python theme={null} >>> from edsl import AgentList >>> al = AgentList.example() >>> al.to_dataset() Dataset([{'age': [22, 22]}, {'hair': ['brown', 'brown']}, {'height': [5.5, 5.5]}]) >>> al.to_dataset(traits_only=False) Dataset([{'age': [22, 22]}, {'hair': ['brown', 'brown']}, {'height': [5.5, 5.5]}, {'agent_parameters': [{'instruction': 'You are answering questions as if you were a human. Do not break character.', 'agent_name': None, 'traits_presentation_template': 'Your traits: {{ traits }}'}, {'instruction': 'You are answering questions as if you were a human. Do not break character.', 'agent_name': None, 'traits_presentation_template': 'Your traits: {{ traits }}'}]}]) ``` > #### to\_dict(*sorted=False*, *add\_edsl\_version=True*, *full\_dict=False*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Serialize the AgentList to a dictionary. ```python theme={null} >>> AgentList.example().to_dict(add_edsl_version=False) {'agent_list': [{'traits': {'age': 22, 'hair': 'brown', 'height': 5.5}}, {'traits': {'age': 22, 'hair': 'brown', 'height': 5.5}}]} >>> example_codebook = {'age': 'Age in years'} >>> al = AgentList.example().set_codebook(example_codebook) >>> result = al.to_dict(add_edsl_version=False) >>> 'codebook' in result True >>> result['codebook'] == example_codebook True ``` > #### to\_dicts(*remove\_prefix: bool = True*) → list\[dict] [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the results to a list of dictionaries. **Parameters:** **remove\_prefix** – Whether to remove the prefix from the column names. ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> r.select('how_feeling').to_dicts() [{'how_feeling': 'OK'}, {'how_feeling': 'Great'}, {'how_feeling': 'Terrible'}, {'how_feeling': 'OK'}] ``` > #### to\_docx(*filename: str | None = None*, *remove\_prefix: bool = False*, *pretty\_labels: dict | None = None*) → [FileStore](/en/latest/filestore#edsl.scenarios.FileStore "edsl.scenarios.FileStore") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Export the results to a FileStore instance containing DOCX data. Each row of the dataset will be rendered on its own page, with a 2-column table that lists the keys and associated values for that observation. > #### to\_excel(*filename: str | None = None*, *remove\_prefix: bool = False*, *pretty\_labels: dict | None = None*, *sheet\_name: str | None = None*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Export the results to a FileStore instance containing Excel data. > #### to\_jsonl(*filename: str | None = None*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Export the results to a FileStore instance containing JSONL data. > #### to\_list(*flatten=False*, *remove\_none=False*, *unzipped=False*) → list\[list] [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the results to a list of lists. > **Parameters:** * **flatten** – Whether to flatten the list of lists. * **remove\_none** – Whether to remove None values from the list. ```python theme={null} >>> from edsl.results import Results >>> Results.example().select('how_feeling', 'how_feeling_yesterday') Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'answer.how_feeling_yesterday': ['Great', 'Good', 'OK', 'Terrible']}]) ``` ```python theme={null} >>> Results.example().select('how_feeling', 'how_feeling_yesterday').to_list() [('OK', 'Great'), ('Great', 'Good'), ('Terrible', 'OK'), ('OK', 'Terrible')] ``` ```python theme={null} >>> r = Results.example() >>> r.select('how_feeling').to_list() ['OK', 'Great', 'Terrible', 'OK'] ``` ```python theme={null} >>> from edsl.dataset import Dataset >>> Dataset([{'a.b': [[1, 9], 2, 3, 4]}]).select('a.b').to_list(flatten = True) [1, 9, 2, 3, 4] ``` ```python theme={null} >>> from edsl.dataset import Dataset >>> # Testing to_list flatten with multiple columns raises DatasetValueError - tested in unit tests ``` > #### to\_pandas(*remove\_prefix: bool = False*, *lists\_as\_strings=False*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the results to a pandas DataFrame, ensuring that lists remain as lists. **Args:** remove\_prefix: Whether to remove the prefix from the column names. lists\_as\_strings: Whether to convert lists to strings. > **Returns:** A pandas DataFrame. > #### to\_polars(*remove\_prefix: bool = False*, *lists\_as\_strings=False*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the results to a Polars DataFrame. > **Args:** remove\_prefix: Whether to remove the prefix from the column names. lists\_as\_strings: Whether to convert lists to strings. > **Returns:** A Polars DataFrame. > #### to\_scenario\_list(*remove\_prefix: bool = True*) → list\[dict] [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the results to a list of dictionaries, one per scenario. **Parameters:** **remove\_prefix** – Whether to remove the prefix from the column names. ```python theme={null} >>> from edsl.results import Results >>> r = Results.example() >>> r.select('how_feeling').to_scenario_list() ScenarioList([Scenario({'how_feeling': 'OK'}), Scenario({'how_feeling': 'Great'}), Scenario({'how_feeling': 'Terrible'}), Scenario({'how_feeling': 'OK'})]) ``` > #### to\_sqlite(*filename: str | None = None*, *remove\_prefix: bool = False*, *pretty\_labels: dict | None = None*, *table\_name: str = 'results'*, *if\_exists: str = 'replace'*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Export the results to a SQLite database file. > #### *property* trait\_keys\*: List\[str]\* [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Get the trait keys for the AgentList. > #### translate\_traits(*codebook: dict\[str, str]*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Translate traits to a new codebook. **Parameters:** **codebook** – The new codebook. ```python theme={null} >>> al = AgentList.example() >>> codebook = {'hair': {'brown':'Secret word for green'}} >>> al.translate_traits(codebook) AgentList([Agent(traits = {'age': 22, 'hair': 'Secret word for green', 'height': 5.5}), Agent(traits = {'age': 22, 'hair': 'Secret word for green', 'height': 5.5})]) ``` > #### tree(*node\_order: List\[str] | None = None*) [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Convert the results to a Tree. **Args:** node\_order: The order of the nodes. **Returns:** A Tree object. > #### unpack\_list(*field: str*, *new\_names: List\[str] | None = None*, *keep\_original: bool = True*) → [Dataset](/en/latest/dataset#edsl.dataset.Dataset "edsl.dataset.Dataset") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Unpack list columns into separate columns with provided names or numeric suffixes. For example, if a dataset contains: \[\{'data': \[\[1, 2, 3], \[4, 5, 6]], 'other': \['x', 'y']}] After d.unpack\_list(‘data’), it should become: \[\{'other': \['x', 'y'], 'data\_1': \[1, 4], 'data\_2': \[2, 5], 'data\_3': \[3, 6]}] > **Args:** field: The field containing lists to unpack new\_names: Optional list of names for the unpacked fields. If None, uses numeric suffixes. keep\_original: If True, keeps the original field in the dataset > **Returns:** A new Dataset with unpacked columns > **Examples:** ```python theme={null} >>> from edsl.dataset import Dataset >>> d = Dataset([{'data': [[1, 2, 3], [4, 5, 6]]}]) >>> d.unpack_list('data') Dataset([{'data': [[1, 2, 3], [4, 5, 6]]}, {'data_1': [1, 4]}, {'data_2': [2, 5]}, {'data_3': [3, 6]}]) ``` ```python theme={null} >>> d.unpack_list('data', new_names=['first', 'second', 'third']) Dataset([{'data': [[1, 2, 3], [4, 5, 6]]}, {'first': [1, 4]}, {'second': [2, 5]}, {'third': [3, 6]}]) ``` > #### with\_names(\**trait\_keys: str*, *remove\_traits: bool = True*, *separator: str = ','*, *force\_name: bool = False*) → [AgentList](/en/latest/#edsl.agents.AgentList "edsl.agents.agent_list.AgentList") [\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/agents.py) Return a new AgentList with names based on the values of the specified traits. > **Args:** [\*](/en/latest/#id28)trait\_keys: The trait keys to use for naming remove\_traits: Whether to remove the traits used for naming from the agents separator: The separator to use when joining multiple trait values force\_name: Whether to force naming even if agents already have names > **Returns:** AgentList: A new AgentList with named agents ```python theme={null} >>> from edsl import Agent >>> al = AgentList([Agent(traits = {'a': 1, 'b': 1}), ... Agent(traits = {'a': 1, 'b': 2})]) >>> al_with_names = al.with_names('a') >>> al_with_names[0].name '1' >>> al[0].name is None # Original unchanged True ``` # Managing Keys Source: https://docs.expectedparrot.com/en/latest/api_keys API keys are required to access large language models. To use EDSL with models you can either provide your own keys from service providers (*Anthropic, Azure, Bedrock, Deep Infra, DeepSeek, Google, Groq, Mistral, OpenAI, Perplexity, Together, Xai*) or use an **Expected Parrot key** to access all available models at the Expected Parrot server. See the [model pricing page](http://www.expectedparrot.com/models) for details on current available models and prices. In addition to providing access to available models, your Expected Parrot key also allows you to post and share content for free. This page shows how to store and select keys to use when running surveys on your own computer (*local inference*) or at the Expected Parrot server (*remote inference*). For instructions on using your Expected Parrot key only for accessing the platform, please see [this section](/en/latest/coop) of the documentation. **Special note for Colab users**: If you are using EDSL in a Colab notebook, please see [special instructions](/en/latest/colab_setup) on storing API keys as “secrets” ([Colab Setup](/en/latest/colab_setup)). *Caution: Treat your API keys as sensitive information, like passwords. Never share your keys publicly or upload files containing them to public repositories.* ## Methods There are three methods for storing and managing keys (details on each below): Manage keys from your Expected Parrot account (*recommended*) Store keys in a local file on your computer Set keys in your code (*not recommended*) ### 1. Manage keys from your account This method provides a secure way to store your keys and the most flexibility in choosing how to use them. It requires an Expected Parrot account ([log in/sign up here](https://www.expectedparrot.com/login)) and works with remote-inference. *If you are only running surveys locally, see method 2. below for instructions on storing keys on your computer.* To use this method, go to your [Settings](http://www.expectedparrot.com/home/settings) page and enable the **Run surveys remotely** toggle under Remote Execution: Settings page showing Remote Execution and Remote Error Logging toggles Then go to your [Keys](http://www.expectedparrot.com/home/keys) page and choose whether to add any of your own keys. Use the **Grant access** icon to share a key with other users without sharing the key directly. Use the **Edit** icon to modify settings for any of your keys, or the **Delete** icon to remove them. You can review the current prioritization of your keys at the **Key priority** tab at any time. When you run surveys remotely, your Expected Parrot key is used by default with any models that you have not provided keys for. Keys page showing LLM Keys and key management options ### 2. Store keys in a local file This method allows you to store keys in a private file on your computer and make them available for any surveys that you run, locally or remotely. It also allows you to post local content to Expected Parrot. To use this method: Navigate to your EDSL working directory: \$ cd /path/to/edsl (replace with your actual path) Create a file named .env Add your keys to the file in the following format (omit any keys that you do not have or do not want to use): ```bash expandable theme={null} EXPECTED_PARROT_API_KEY = 'your_key_here' # This key is required for remote inference and interacting with Expected Parrot ANTHROPIC_API_KEY = 'your_key_here' DEEP_INFRA_API_KEY = 'your_key_here' DEEPSEEK_API_KEY = 'your_key_here' GOOGLE_API_KEY = 'your_key_here' GROQ_API_KEY = 'your_key_here' MISTRAL_API_KEY = 'your_key_here' OPENAI_API_KEY = 'your_key_here' PERPLEXITY_API_KEY = 'your_key_here' TOGETHER_API_KEY = 'your_key_here' XAI_API_KEY = 'your_key_here' AWS_ACCESS_KEY_ID = 'your_key_here' AWS_SECRET_ACCESS_KEY = 'your_key_here' AZURE_ENDPOINT_URL_AND_KEY = https://model_1_link:api_key_1,https://model_2_link:api_key_2 ``` Your Expected Parrot key can also be found at the [Keys](http://www.expectedparrot.com/home/keys) page of your account, where you can reset it at any time: Keys page showing Expected Parrot API Key and local .env configuration This key allows you to access all available models at once and use remote-inference to run surveys at the Expected Parrot server. It also allows you to post content to the platform, and to interact with other content that is public or shared with you. If you are using Azure or Bedrock, see a notebook of examples for setting up your keys [here](/en/latest/edsl_with_cloud_providers). ### 3. Set keys in your code *Warning:* This method is not recommended for security reasons. If you include your keys in your code, they can be seen by anyone who has access to it. This is especially risky if you are sharing your code with others or if you are using a version control system like Git. To use this method, store any keys that you want to use as strings in your code in the following format: ```bash expandable theme={null} import os os.environ['EXPECTED_PARROT_API_KEY'] = 'your_key_here' os.environ['ANTHROPIC_API_KEY'] = 'your_key_here' os.environ['DEEP_INFRA_API_KEY'] = 'your_key_here' os.environ['DEEPSEEK_API_KEY'] = 'your_key_here' os.environ['GOOGLE_API_KEY'] = 'your_key_here' os.environ['GROQ_API_KEY'] = 'your_key_here' os.environ['MISTRAL_API_KEY'] = 'your_key_here' os.environ['OPENAI_API_KEY'] = 'your_key_here' os.environ['REPLICATE_API_KEY'] = 'your_key_here' os.environ['TOGETHER_API_KEY'] = 'your_key_here' os.environ['XAI_API_KEY'] = 'your_key_here' os.environ['AWS_ACCESS_KEY_ID'] = 'your_key_here' os.environ['AWS_SECRET_ACCESS_KEY'] = 'your_key_here' os.environ['AZURE_ENDPOINT_URL_AND_KEY'] = https://model_1_link:api_key_1,https://model_2_link:api_key_2 ``` Note that your keys will not persist across sessions and you will need to provide your keys each time you start a new session. ## Credits When you use your Expected Parrot key to access models your account is charged for the costs of API calls to models. (When you use your own keys, service providers will bill you directly.) Please see the [model pricing page](http://www.expectedparrot.com/getting-started/coop-pricing) for information on available models and prices and the credits section for information on purchasing credits and calculating costs. ## Troubleshooting In order to use local inference, you must also have credits available on your account with a service provider in order to run surveys with some models. If you are using remote inference, simply ensure that you have credits on your Expected Parrot account to access all available models. When you run a survey, EDSL checks whether you are using remote or local inference and then checks for the requisite API keys for the models that you have specified to use with the survey. If you do not specify a model to use for a survey, EDSL will attempt to run it with the default model. You can check the current default model by running the following command: ```bash theme={null} from edsl import Model Model() ``` Output: | key | value | | :- | :- | | model | gpt-4o | | parameters:temperature | 0.5 | | parameters:max\_tokens | 1000 | | parameters:top\_p | 1 | | parameters:frequency\_penalty | 0 | | parameters:presence\_penalty | 0 | | parameters:logprobs | False | | parameters:top\_logprobs | 3 | To check all available models: ```python theme={null} from edsl import Model Model.available() ``` To check all available models for a specific provider: ```python theme={null} from edsl import Model Model.available(service="openai") ``` Learn more about available models in the [Language Models](/en/latest/language_models#language-models) section of the documentation. If you attempt to run a survey without storing any API keys, you will get a message with a link to log into your Expected Parrot account and automatically activate remote inference and store your Expected Parrot API key for you. If you provide an invalid API key you will receive an error message AuthenticationError: Incorrect API key provided…. You may also receive an error message if you do not have credits on your account with a service provider. Learn more about handling errors in the [Exceptions & Debugging](/en/latest/exceptions#exceptions) section of the documentation. Please also feel free to reach out to us to help you troubleshoot: * Discord channel: [https://discord.com/invite/mxAYkjfy9m](https://discord.com/invite/mxAYkjfy9m) * Email: [info@expectedparrot.com](mailto:info@expectedparrot.com) # Base Source: https://docs.expectedparrot.com/en/latest/base ## Base class > ### *class* edsl.base.Base[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Bases: `RepresentationMixin`, `PersistenceMixin`, `DiffMethodsMixin`, `HashingMixin`, `ABC` Base class for all classes in the EDSL package. This abstract base class combines several mixins to provide a rich set of functionality to all EDSL objects. It defines the core interface that all EDSL objects must implement, including serialization, deserialization, and code generation. All EDSL classes should inherit from this class to ensure consistent behavior and capabilities across the framework. > #### apply\_command(*command\_name*, *kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) > #### *abstract* code()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Generate Python code that recreates this object. This method must be implemented by all subclasses to provide a way to generate executable Python code that can recreate the object. **Returns:** str: Python code that, when executed, creates an equivalent object > #### *abstract* example()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Create an example instance of this class. This method should be implemented by all subclasses to provide a convenient way to create example objects for testing and demonstration. **Returns:** An instance of the class with sample data > #### *abstract* from\_dict()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Create an instance from a dictionary. This class method must be implemented by all subclasses to provide a standard way to deserialize objects from dictionaries. **Returns:** An instance of the class populated with data from the dictionary > #### get\_description() → str[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Get the description of this object. > #### get\_uuid() → str[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Get the UUID of this object from the Expected Parrot cloud service based on its hash. This method calculates the hash of the object and queries the cloud service to find if there’s an uploaded version with the same content. If found, it returns the UUID of that object. **Returns:** str: The UUID of the object in the cloud service if found **Raises:** **CoopServerResponseError: If the object is not found or there’s an error** communicating with the server > #### inspect()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Create an interactive inspector widget for this object. This method uses the InspectorWidget registry system to find the appropriate inspector widget class for this object’s type and returns an instance of it. **Returns:** InspectorWidget subclass instance: Interactive widget for inspecting this object **Raises:** KeyError: If no inspector widget is registered for this object’s class ImportError: If the widgets module cannot be imported > #### json()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Get a formatted JSON representation of this object. **Returns:** DisplayJSON: A displayable JSON representation > #### keys()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Get the key names in the object’s dictionary representation. This method returns all the keys in the serialized form of the object, excluding metadata keys like version information. **Returns:** list: A list of key names > #### *property* polly\_commands[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Auto-generate commands list from methods decorated with @polly\_command > #### show\_methods(*show\_docstrings=True*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Display all public methods available on this object. This utility method helps explore the capabilities of an object by listing all its public methods and optionally their documentation. **Args:** **show\_docstrings: If True, print method names with docstrings;** if False, return the list of method names **Returns:** **None or list: If show\_docstrings is True, prints methods and returns None.** If show\_docstrings is False, returns a list of method names. > #### store(*d: dict*, *key\_name: str | None = None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Store this object in a dictionary with an optional key. **Args:** d: The dictionary in which to store the object key\_name: Optional key to use (defaults to the length of the dictionary) **Returns:** None > #### *abstract* to\_dict()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Serialize this object to a dictionary. This method must be implemented by all subclasses to provide a standard way to serialize objects to dictionaries. The dictionary should contain all the data needed to reconstruct the object. **Returns:** dict: A dictionary representation of the object > #### to\_json()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Serialize this object to a JSON string. **Returns:** str: A JSON string representation of the object > #### values()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Get the values in the object’s dictionary representation. **Returns:** set: A set containing all the values in the object > #### yaml()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/base.py) Get a formatted YAML representation of this object. **Returns:** DisplayYAML: A displayable YAML representation # Survey logic checklist Source: https://docs.expectedparrot.com/en/latest/checklist This page provides a checklist for reviewing the logic and contents of an EDSL survey and ensuring that it will run as you intend it to. ## Support Please reach out to us with any questions or issues! Send an email to **[info@expectedparrot.com](mailto:info@expectedparrot.com)** or post a message at [Discord](https://discord.com/invite/mxAYkjfy9m). ### How EDSL works At its core, EDSL is built on the concept of [Questions](/en/latest/questions) being answered by AI [Agents](/en/latest/agents), using [Language Models](/en/latest/language_models) to generate responses that are returned as formatted datasets of [Results](/en/latest/results). A typical workflow consists of the following steps: 1. Construct **questions** of different types (multiple choice, free text, etc.) and combine them in a survey. Learn about [question types](/en/latest/questions). 2. *Optional:* Add **data or content** to your questions (from CSVs, PDFs, docs, images, etc.). Learn about [scenarios](/en/latest/scenarios). 3. *Optional:* Add **rules or logic** to specify how questions should be presented (e.g., skip/stop rules, or including context of other questions). Learn about [survey rules](/en/latest/surveys). 4. *Optional:* Design or import **personas** for AI agents to answer the questions. Learn about [agents](/en/latest/agents). 5. Select **language models** to generate responses. EDSL works with hundreds of popular [models](/en/latest/language_models). 6. Run the survey and get a formatted dataset of **results**. Use [built-in methods](/en/latest/results) for analyzing them. 7. *Optional:* Store and share your work at the [Expected Parrot](https://www.expectedparrot.com/login): a platform for AI research that is fully integrated with EDSL. [Learn more](/en/latest/coop). ### Issues checklist Below are some issues to consider before running a survey, with links to examples of EDSL features for handling them. Ensure that the question type you are using is appropriate for the question you are presenting. * Use QuestionYesNo when you only want to allow “Yes” or “No” responses. * Use QuestionMultipleChoice when you want to require that only one option be selected. * Use QuestionCheckBox when you want to allow multiple (or no) selections or specify a number of selections. Ensure that the question options make sense for the question text. * Does the question text ask for options not presented, or a different number of options? * Should the question options include an option for non-responses (e.g., “I do not know.”)? * Are validation constraints appropriate for your use case (e.g., min/max values, required selections)? Survey questions are administered asynchronously by default, meaning that the presentation of one question does not include context of any other questions in the survey unless you specify otherwise. This default functionality reduces costs and runtime. It also allows you to fine-tune and readily compare responses to versions of questions with different contexts. If a question depends on or requires information about other questions in a survey, you need to add a rule specifying the logic to be applied. This can be done in a variety of ways: * Use [piping](/en/latest/surveys) to add components of a question in another question (e.g., insert the answer to a question in the text of a follow-up question). * Use question [memory](/en/latest/surveys) to include the entire context of a question/answer in the presentation of a different question (*“You were previously asked…”*). There are separate rules for adding the context of a single question, a set of questions, all prior questions, or a lagged number of questions. ## Testing a model’s ability to answer a question Running test questions and examining the answers and [“comment” fields](/en/latest/questions) can help you understand whether a question needs to be improved. You can also run questions *about* your questions for [cognitive testing](/en/latest/notebooks/research_methods) (e.g., *“How is the following question confusing: `{{ question }}`”*) ### Tools for integrating data ## Scenario objects EDSL provides many methods for importing and formatting data or content to be used with a survey. This can be particularly useful for surveys that involve [data labeling](/en/latest/notebooks/data_labeling_example) or [data cleaning](/en/latest/notebooks/data_cleaning) tasks, such as using questions to extract information from unstructured texts or apply labels to images or other content. This is done by creating Scenerio \<[/en/latest/scenarios](/en/latest/scenarios)>\`\_ objects representing data or content to be added to survey questions (e.g., *“What is the main topic of this text? \{\{ text }}?”, “What is in this image? \{\{ image }}”*). See [examples](/en/latest/notebooks/question_loop_scenarios) for working with [PDFs](/en/latest/notebooks/scenario_from_pdf), CSVs, [images](/en/latest/notebooks/image_scenario_example), docs, [tables](/en/latest/notebooks/scenario_list_wikipedia) and other data types. You can also use scenarios to store [metadata](/en/latest/notebooks/adding_metadata) for surveys. ## Converting questions Many models are highly capable of reformatting non-EDSL surveys based on a prompt that includes basic instructions or examples of EDSL code. This can be an efficient way to reconstruct an existing survey in EDSL and then modify as needed with above-mentioned methods. See examples: [CES data](/en/latest/notebooks/ces_data_edsl); [GoogleForms](/en/latest/notebooks/google_form_to_edsl). # Citation Source: https://docs.expectedparrot.com/en/latest/citation If you use the EDSL package in your research or projects, please use the following citation: ```bash theme={null} @techreport{Horton2024EDSL, title = {EDSL: Expected Parrot Domain Specific Language for AI Powered Social Science}, author = {John Horton and Robin Horton}, year = {2024}, institution = {Expected Parrot}, type = {Whitepaper} } ``` # Colab Notebooks Source: https://docs.expectedparrot.com/en/latest/colab_notebooks All of the methods for working with notebooks in EDSL are available in Colab. To access these methods in Colab, please first complete the following steps to set up EDSL and connect your Google Drive: ## 1. Store your API key as a secret In Google Colab, your API keys can be stored as “secrets” in lieu of storing them in a *.env* file as you would in other notebook types. For example, you can store your Expected Parrot API key as follows: Storing API key in Google Colab ## 2. Install EDSL Run the following command in a code cell to install the EDSL package: ```python theme={null} pip install edsl ``` ## 3. Access your API key Run the following code to access your API key in your Colab notebook: ```python theme={null} import os from google.colab import userdata os.environ['EXPECTED_PARROT_API_KEY'] = userdata.get('EXPECTED_PARROT_API_KEY') ``` ## 4. Connect your Google Drive To access your files in Google Drive, you need to connect your Google Drive to Colab. Run the following code to do this, and accept the permissions request in the pop-up window: ```python theme={null} from google.colab import drive drive.mount('/content/drive') ``` You will see the following message returned: ``` Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True). ``` ## 5. Access your files Run the following code to see the names of all the files in a Google Drive folder **Note**: Note that you will need to replace the path with the path to your own folder, and you may need to adjust the path to match the structure of your Google Drive (e.g., here the default Google Drive folder name “Colab Notebooks” has been changed to “ColabNotebooks” for convenience in specifying the path): ```python theme={null} import os print(sorted(os.listdir('/content/drive/MyDrive/ColabNotebooks/'))) ``` Sample output: ``` ['colab_to_coop.ipynb'] ``` ## 6. Post a notebook to Expected Parrot Now that you have your notebook ready, you can post it to Expected Parrot using the Notebook object. Modify the path to your notebook file path in your Google Drive as needed: ```python theme={null} from edsl import Notebook notebook = Notebook(path="/content/drive/MyDrive/ColabNotebooks/colab_to_coop.ipynb") notebook.push(description="Posting a Colab notebook to Expected Parrot") ``` Example output: ```json theme={null} {'description': 'Posting a Colab notebook to Expected Parrot', 'object_type': 'notebook', 'url': 'https://www.expectedparrot.com/content/a878656a-317a-4181-a496-3c49f12e38d7', 'uuid': 'a878656a-317a-4181-a496-3c49f12e38d7', 'version': '0.1.36', 'visibility': 'unlisted'} ``` ## 7. Update or edit a notebook at Expected Parrot ```python theme={null} from edsl import Notebook notebook = Notebook(path="/content/drive/MyDrive/ColabNotebooks/colab_to_coop.ipynb") notebook.patch( uuid = "a878656a-317a-4181-a496-3c49f12e38d7", visibility = "public", value = notebook ) ``` Output: ``` {'status': 'success'} ``` ## Example Colab code Posting a Colab notebook to Expected Parrot # Colab Setup Source: https://docs.expectedparrot.com/en/latest/colab_setup All examples, tutorials, and demo notebooks are executable in any Python notebook environment. To run them in Colab, please follow the special instructions below for technical setup: ## 1. Store your API key as a secret In Google Colab, your API keys can be stored as “secrets” in lieu of storing them in a *.env* file as you would in other notebook types. For example, you can store your Expected Parrot API key as follows: Storing API key in Google Colab ## 2. Install EDSL Run the following command in a code cell to install the EDSL package: ```bash theme={null} pip install edsl ``` ## 3. Access your API key Run the following code to access your API key in your Colab notebook: ```python theme={null} import os from google.colab import userdata os.environ['EXPECTED_PARROT_API_KEY'] = userdata.get('EXPECTED_PARROT_API_KEY') ``` ## Example Colab code EDSL methods for creating objects and posting them to Expected Parrot are now available to you in Colab. Here’s a snapshot of how your Colab code might look: Storing and using API key in Google Colab ## Posting Colab notebooks to Expected Parrot Special instructions for connecting your Google Drive to post Colab notebooks to Expected Parrot are available in the [Colab Notebooks](/en/latest/colab_notebooks) section. # Contributing Source: https://docs.expectedparrot.com/en/latest/contributing Thank you for checking out EDSL! We sincerely appreciate feedback and welcome contributions to this project. This page describes some ways to contribute to the project. It includes information on how to report bugs, suggest enhancements, draft new documentation pages, and contribute code. Please also feel free to reach out to us in our Discord channel or by email if you have any questions or need help: [Discord channel](https://discord.com/invite/mxAYkjfy9m) [info@expectedparrot.com](mailto:info@expectedparrot.com) ## Reporting bugs If you find a bug in the project, please let us know and report it on the project’s GitHub issue tracker. Link to the issue tracker: [EDSL GitHub issue tracker](https://github.com/expectedparrot/edsl/issues/new) When reporting a bug, it is helpful to include the following information: * The version of the project you are using. (Run edsl –version to find the version number.) * The operating system you are using. (For example, Windows, macOS, or Linux.) * The Python version you are using. (Run python –version to find the Python version number.) * A detailed description of the bug, including the steps to reproduce it. * If possible, a minimal example that reproduces the bug. If you are able to fix the bug, please consider submitting a pull request with the fix (see below). ## Suggesting enhancements If you have an idea for an enhancement to the project, please let us know and report it on the project’s GitHub issue tracker (see link above). When suggesting an enhancement, it is helpful to include the following information: * A detailed description of the enhancement. * A rationale for the enhancement. * If possible, a sketch of the proposed implementation. If you do not wish to implement the enhancement yourself, please consider submitting a pull request with the changes (see below). ## Drafting new documentation pages If you would like to contribute a new documentation page to the project, please follow these steps: Fork the project on GitHub. Create a new branch for your documentation page. Add the new documentation page to the docs/ directory. Add a link to the new documentation page in the docs/index.rst file. Submit a pull request with your changes. Please follow the project’s documentation conventions when writing documentation (header styles, code blocks, links, etc.). For example, use the reStructuredText markup language and consider copying this or other pages as a template. ## Contributing code If you would like to contribute code to the project, it is helpfup to follow these steps: Fork the project on GitHub. Create a new branch for your changes. Write your code. Write tests for your code. Run the tests. Submit a pull request with your changes. When submitting a pull request, please include the following information: * A description of the changes in the pull request. * A rationale for the changes. * If the changes fix a bug, please include the issue number. When writing tests, please follow the project’s testing conventions. The project uses the unittest testing framework. Thank you! # Expected Parrot Source: https://docs.expectedparrot.com/en/latest/coop Expected Parrot provides a free platform for storing, sharing and collaborating on AI-based research, fully integrated with EDSL via the Coop module. Expected Parrot provides a free platform for creating, storing and sharing AI-based research, and validating it with human respondents. It is fully integrated with EDSL via the *Coop* module, allowing you to post, store and retrieve any objects that you've created with EDSL, together with data, notebooks and other project content. You can also explore public content and collaborate on projects privately with other users: Expected Parrot page view Your Expected Parrot account provides access to special features for running EDSL surveys and storing results remotely at the Expected Parrot server, and launching hybrid human-AI inference jobs interactively. Learn more about these features in the [Remote Inference](/en/latest/remote_inference), [Remote Caching](/en/latest/remote_caching) and [Survey Builder](/en/latest/survey_builder) sections of the documentation. ## Getting started ### 1. Create an account [Sign up](https://www.expectedparrot.com/login) with an email address. Your account comes with an **Expected Parrot API key** that allows you to: * Post and share content * Run surveys remotely * Use any available models with your surveys * Use the universal cache of stored prompts and responses You can inspect your key and reset it at any time at your [Keys](https://www.expectedparrot.com/home/keys) page. ### 2. Your Expected Parrot API key Remote inference is a feature that allows you to run EDSL surveys and inference jobs at the Expected Parrot server, and store results at your account. It is activated by default when you create an account. When remote inference is activated, your survey jobs and results are automatically stored at the Expected Parrot server and accessible at the [Results](https://www.expectedparrot.com/home/remote-inference) page of your account. You can also post any EDSL objects to Expected Parrot from your workspace, includes Results, Surveys, Agents, Scenarios, Notebooks and data files (using the FileStore module). To do this, you first need to store your Expected Parrot API key. If you have activated remote inference (at your [Settings](https://www.expectedparrot.com/home/settings) page), your key is automatically stored. If you are only using EDSL locally, create a file named .env in your EDSL working directory and store your key in it using the following format: ```bash theme={null} EXPECTED_PARROT_API_KEY = 'your_key_here' ``` Learn more about storing API keys in the [Managing Keys](/en/latest/api_keys) section of the documentation. ### 3. Post objects to Expected Parrot Use the `Coop` module and methods to post and share objects (see examples below). You can set the visibility status of an object when you post it or update it later, from your workspace or interactively at your account. There are 3 status options: * *public*: Visible to everyone * *private*: Visible to logged in users that you have granted access * *unlisted*: Visible to anyone with the link but not listed in search results (default) See details on methods for uploading, downloading, updating and deleting content at your account below. ### 4. Explore content Content that you have created with remote inference or posted to Expected Parrot is available at your [Content](https://www.expectedparrot.com/content) page. You can access public content at the [Explore](https://www.expectedparrot.com/login) tab, and copy code and examples to modify or rerun at your workspace. ## Methods When remote inference is activated the results of your surveys are automatically added to your [Content](https://www.expectedparrot.com/content) page. You can modify them from your workspace or interactively at your account. You can also post, update, download and delete any objects using the `Coop` module and methods (see examples below). ### Posting There are 2 methods for uploading/posting an object from your workspace to your account: 1. Call the *push()* method on the object directly. 2. Call the *create()* method on a `Coop` client object and pass it the object. You can optionally pass a description, a convenient alias for the Expected Parrot URL and a *visibility* status (public, private or unlisted (default)) when you create an object. These can be changed at any time. **Direct method** Here we post a question object by calling the *push()* method on it: ```python theme={null} from edsl import QuestionFreeText q = QuestionFreeText( question_name = "example", question_text = "How are you today?" ) q.push( description="This is an example question", # optional alias = "my-example-question", # optional visibility="public" # optional ) ``` We can see the description, alias and visibility status that we specified in the information that is returned: ```json theme={null} {'description': 'This is an example question', 'object_type': 'question', 'url': 'https://www.expectedparrot.com/content/1706e6f9-2675-4433-8b0a-080654a5cd08', 'alias_url': 'https://www.expectedparrot.com/content/RobinHorton/my-example-question', 'uuid': '1706e6f9-2675-4433-8b0a-080654a5cd08', 'version': '0.1.47.dev1', 'visibility': 'public'} ``` **Using a `Coop` client** Here we post the same question by passing it to the *create()* method of a `Coop` client object: ```python theme={null} from edsl import Coop, QuestionFreeText q = QuestionFreeText( question_name = "example", question_text = "How are you today?" ) c = Coop() c.create(q) ``` Here we include a description and visibility status: ```python theme={null} from edsl import Coop, QuestionFreeText q = QuestionFreeText( question_name = "example", question_text = "How are you today?" ) c = Coop() c.create(object=q, description="This is an example question", alias = "another-example-question", visibility="public") ``` This will return the same information about the object as the direct method shown above (with a unique uuid and URL for viewing the object at the Expected Parrot platform). ### Updating There are 3 methods for updating/editing an object to Expected Parrot: 1. Edit the object at the Expected Parrot platform. 2. Call the *patch()* method on the object directly. 3. Call the *patch()* method on a `Coop` client object. For each patch() method, pass the alias\_url or the uuid of the object and the parameter(s) that you want to update: *description*, *visibility*, *alias* and/or *value*. * The *description* parameter is used to update the description of an object, such as a question or survey. * The *visibility* parameter is used to update the visibility of an object: *public*, *private* or *unlisted*. * The *value* parameter is used to update the content of an object, such as the text of a question or the code in a notebook. **At the Expected Parrot platform** You can manually update the description or visibility of an object at the Expected Parrot platform. Navigate to the [Explore](https://www.expectedparrot.com/login) page and open the page view for an object. You can select options to change the **visibility** of the object (*public*, *private* or *unlisted*) or to **edit** the description. **Direct method** Here we update the description and *visibility* of the question we created and uploaded in the examples above by calling the patch() method on it: ```python theme={null} q.patch("https://www.expectedparrot.com/content/RobinHorton/my-example-question", description="This is an updated question", visibility="unlisted") ``` This will return a status message: ```json theme={null} {'status': 'success'} ``` Here we change the question itself by modifying the value parameter: ```python theme={null} from edsl import QuestionMultipleChoice new_q = QuestionMultipleChoice( question_name = "example", question_text = "How are you today?", question_options = ["Good", "Bad", "OK"] ) q.patch("https://www.expectedparrot.com/content/RobinHorton/my-example-question", value=new_q) ``` **Using a `Coop` client** Here we do the same using a `Coop` client object: ```python theme={null} from edsl import Coop c = Coop() c.patch("https://www.expectedparrot.com/content/1706e6f9-2675-4433-8b0a-080654a5cd08", description="This is an updated question", visibility="public") ``` This will return the same status message as above. ### Replicating / Downloading There are a variety of methods for replicating or downloading an object at Expected Parrot: 1. Selecting options to download or copy code at the Expected Parrot platform 2. Calling the *pull()* method on the class of the object 3. Calling the *get()* method on a `Coop` client object **Copy code at the Expected Parrot platform** The platform provides copyable code for downloading or reconstructing an object that has been posted: Navigate to the [Explore](https://www.expectedparrot.com/login) page and select an object Go to the object’s page Select the option to **Download** the object OR Select the option to **Pull** the object using its uuid or **Raw** to get the code for constructing the object. Use this code in your workspace to download the object locally or to reconstruct it. **Class method** Here we download the question posted above by calling the pull() method on the object class (Question) and passing the uuid of the object: ```python theme={null} from edsl import Question q = Question.pull("1706e6f9-2675-4433-8b0a-080654a5cd08") q ``` This will return the object (the multiple choice question question that replaced the free text question): | key | value | | :- | :- | | question\_name | example | | question\_text | How are you today? | | question\_options:0 | Good | | question\_options:1 | Bad | | question\_options:2 | OK | | question\_type | multiple\_choice | **Using a `Coop` client** Here we download the question by calling the get() method on a `Coop` client object: ```python theme={null} from edsl import Coop c = Coop() q = c.get("1706e6f9-2675-4433-8b0a-080654a5cd08") q ``` This will return the same object as above. ### Deleting There are 3 methods for deleting an object from your account: 1. Selecting options to delete at the web app 2. Calling the *delete()* method on the class of the object 3. Calling the *delete()* method on a `Coop` client object **At the Expected Parrot platform** You can manually delete objects at the Expected Parrot platform: Navigate to [Explore](https://www.expectedparrot.com/login) and open an object’s page view (see image above for *Uploading* content) Select the option to **delete** the object: Expected Parrot page view **Directly** Here we delete a question object by calling the *delete()* method on the class of the object (*Question*) and passing the uuid of the object: ```python theme={null} from edsl import Question Question.delete("1234abcd-abcd-1234-abcd-1234abcd1234") # mock uuid ``` This will return a status message: ```json theme={null} {'status': 'success'} ``` **Using a `Coop` client** Here we delete a question by calling the *delete()* method on a `Coop` client object, passing the uuid of the object: ```python theme={null} from edsl import Coop c = Coop() c.delete(uuid="1234abcd-abcd-1234-abcd-1234abcd1234") # mock uuid ``` This will return the same status message as above (so long as the object was not already deleted). ### Projects You can create a project at the Expected Parrot platform by selecting the option to **Create a project** at the top right of the page. You can also create a project by calling the *create\_project()* method on a `Coop` client object: ```python theme={null} from edsl import Coop c = Coop() c.create_project( project_name = "My project", description = "This is a project", visibility = "public" ) ``` # Estimating & Tracking Costs Source: https://docs.expectedparrot.com/en/latest/costs EDSL comes with built-in methods for estimating costs before running your survey jobs, and tracking actual costs after running them. This is useful for budgeting and understanding the costs of running surveys with different models and inference services, and determining which models are most efficient for your research goals. **Note:** When running survey jobs your prompts and responses are cached automatically and can be retrieved again later at no cost. Learn more about remote inference caching in the remote-caching section of the docs. ## Estimating costs When you create a survey, you can estimate the cost of running it before actually running it. This is done by combining the survey with one or more models to create a Job object. The Job object contains all the information needed to run the survey, including the models and inference services used. There are 2 methods of estimating costs (in USD and credits): * Call the estimate\_job\_cost() method on a Job object (a survey combined with one or more models). This will return the total estimated cost in USD, the estimated input and output tokens, and estimated costs and tokens for each inference service and model used. It will also show the number of credits that will be placed on hold if the survey job is run remotely. Once the job has completed the credits are released and final calculated credits are deducted from your account. * Call the remote\_inference\_cost() method on a `Coop` client object and pass it the job. This will return the total estimated cost in USD and credits. *Credits are required to run surveys remotely. Learn more about using credits in the `Credits `\_ section of the docs.* For example: ```python theme={null} from edsl import QuestionMultipleChoice, Survey, Model, ModelList q = QuestionMultipleChoice( question_name = "color", question_text = "Which is your favorite primary color?", question_options = ["red", "blue", "yellow"] ) survey = Survey(questions = [q]) models = ModelList([ Model("gpt-4o", service_name = "openai"), Model("gemini-1.5-flash", service_name = "google") ]) job = survey.by(models) job.estimate_job_cost() ``` This will return: ```json expandable theme={null} {'estimated_total_cost_usd': 0.000697825, 'total_credits_hold': 0.09999999999999999, 'estimated_total_input_tokens': 134, 'estimated_total_output_tokens': 102, 'detailed_costs': [{'inference_service': 'openai', 'model': 'gpt-4o', 'token_type': 'input', 'price_per_million_tokens': 2.5, 'tokens': 67, 'cost_usd': 0.0001675, 'credits_hold': 0.02}, {'inference_service': 'openai', 'model': 'gpt-4o', 'token_type': 'output', 'price_per_million_tokens': 10.0, 'tokens': 51, 'cost_usd': 0.00051, 'credits_hold': 0.06}, {'inference_service': 'google', 'model': 'gemini-1.5-flash', 'token_type': 'input', 'price_per_million_tokens': 0.075, 'tokens': 67, 'cost_usd': 5.025e-06, 'credits_hold': 0.01}, {'inference_service': 'google', 'model': 'gemini-1.5-flash', 'token_type': 'output', 'price_per_million_tokens': 0.3, 'tokens': 51, 'cost_usd': 1.53e-05, 'credits_hold': 0.01}]} ``` Using the `Coop` client object instead: ```python theme={null} from edsl import Coop coop = Coop() coop.remote_inference_cost(job) ``` This will return: ```json theme={null} {'credits_hold': 0.1, 'usd': 0.001} ``` ### Reasoning-model uncertainty Reasoning-token usage depends on the workload and may not be included in a server point estimate. For reasoning models, EDSL labels the returned amount as a lower bound instead of presenting it as an exact prediction. Inspect `estimate_kind`, `warnings`, and the `assumptions.reasoning_tokens` object before using an estimate as a budget: ```bash theme={null} ep jobs cost jobs.ep ``` ```json theme={null} { "usd": 0.01, "credits_hold": 1.0, "estimate_kind": "lower_bound", "lower_bound_usd": 0.01, "expected_usd": null, "upper_bound_usd": null, "warnings": ["The server point estimate does not specify a reasoning-token assumption..."] } ``` `null` expected or upper values mean EDSL does not have enough information to produce those bounds; they do not mean zero additional cost. If the server supplies a richer range, EDSL preserves that range and its assumptions. ## Calculations The above-mentioned methods use the following calculation for each question in a survey to estimate the total cost of the job: 1. **Estimate the input tokens.** * Compute the number of characters in the user\_prompt and system\_prompt, with any Agent and Scenario data piped in. (*Note:* Previous answers cannot be piped in because they are not available until the survey is run; they are left as Jinja-bracketed variables in the prompts for purposes of estimating tokens and costs.) * Apply a piping multiplier of 2 to the number of characters in the user prompt if it has an answer piped in from a previous question (i.e., if the question has Jinja braces). Otherwise, apply a multiplier of 1. * Convert the number of characters into the number of input tokens using a conversion factor of 4 characters per token, rounding down to the nearest whole number. (This approximation was [established by OpenAI](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them).) 2. **Estimate the output tokens.** * Apply a multiplier of 0.75 to the number of input tokens, rounding up to the nearest whole number. 3. **Apply the Expected Parrot rates for the model and inference service.** * Find the model and inference service on the Expected Parrot [Pricing](https://www.expectedparrot.com/getting-started/coop-pricing) page. The rates listed there are used to calculate costs for jobs run using an Expected Parrot API key: *Total cost = (input tokens \* input token rate) + (output tokens \* output token rate)* * Expected Parrot rates may differ from rates charged by the underlying service provider and do not necessarily reflect provider-specific discounts, promotions, time-based pricing, caching discounts, batch pricing, volume pricing, service tiers, or other specialized pricing mechanisms. * If a current price for the model is not available, a default price for the inference service provider is used. If both the model and the inference service provider are not found, the following fallback token rates are applied (you will also see a warning message that a model price was not found): * USD 1.00 per 1M input tokens * USD 1.00 per 1M ouput tokens 4. **Convert the total cost in USD to credits.** * Total cost in credits = total cost in USD \* 100, rounded up to the nearest 1/100th credit. Then sum the costs for all question prompts to get the total cost of the job. A notebook example is available [here](https://www.expectedparrot.com/content/RobinHorton/estimating-job-costs-notebook). ## Tracking costs After running a survey job, you can track the token costs calculated for each question in the raw\_model\_response columns of the Results generated for your survey: * **raw\_model\_response.\\_cost**: The cost of the result for the relevant question, applying the token quanities & prices. * **raw\_model\_response.\\_input\_price\_per\_million\_tokenss**: The price per million input tokens for the relevant question for the relevant model. * **raw\_model\_response.\\_input\_tokens**: The number of input tokens for the relevant question for the relevant model. * **raw\_model\_response.\\_one\_usd\_buys**: The number of identical results for the relevant question that 1USD would cover. * **raw\_model\_response.\\_output\_price\_per\_million\_tokens**: The price per million output tokens for the relevant question for the relevant model. * **raw\_model\_response.\\_output\_tokens**: The number of output tokens for the relevant question for the relevant model. * **raw\_model\_response.\\_raw\_model\_response**: The raw model response for the relevant question. Details can also be viewed at the [Jobs](https://www.expectedparrot.com/home/remote-inference) and [Transactions](https://www.expectedparrot.com/home/transactions) pages of your Expected Parrot account. For example, here we inspect the costs of running the job from above: ```python theme={null} results = job.run() results.select("raw_model_response.*") ``` Output: | model.model | question\_text.read\_question\_text | question\_text.important\_question\_text | raw\_model\_response.read\_input\_price\_per\_million\_tokens | raw\_model\_response.important\_output\_tokens | raw\_model\_response.important\_output\_price\_per\_million\_tokens | raw\_model\_response.read\_one\_usd\_buys | raw\_model\_response.important\_raw\_model\_response | raw\_model\_response.important\_input\_price\_per\_million\_tokens | raw\_model\_response.read\_input\_tokens | raw\_model\_response.read\_raw\_model\_response | raw\_model\_response.important\_one\_usd\_buys | raw\_model\_response.read\_output\_price\_per\_million\_tokens | raw\_model\_response.read\_cost | raw\_model\_response.read\_output\_tokens | raw\_model\_response.important\_input\_tokens | raw\_model\_response.important\_cost | | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | | gemini-1.5-flash | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 0.075000 | 53 | 0.300000 | 42872.461058 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “5nnIt’s, like, a huge deal! The future of the planet is at stake, and that affects everything - from the environment to the economy to, you know, my future. It’s definitely something I worry about.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.2145003372768186, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 128, ‘candidates_token_count’: 53, ‘total_token_count’: 181, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 0.075000 | 95 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “YesnnI’ve read a few articles and some chapters from textbooks for my environmental science classes, which covered climate change extensively. It’s not quite the same as reading a whole book dedicated to the topic, but I’ve definitely learned about it.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.15844399840743453, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 95, ‘candidates_token_count’: 54, ‘total_token_count’: 149, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 39215.691903 | 0.300000 | 0.000023 | 54 | 128 | 0.000025 | | gpt-4o | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 2.500000 | 32 | 10.000000 | 1724.137931 | `{‘id’: ‘chatcmpl-BQaCQLYP5PB3vEeEOElownyIV7jLX’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “5 nClimate change is a critical issue that affects the entire planet and future generations, so I believe it’s very important to address and find solutions for it.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675378, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_f5bdcc3276’, ‘usage’: `{‘completion_tokens’: 32, ‘prompt_tokens’: 131, ‘total_tokens’: 163, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}` }\` | 2.500000 | 96 | `{‘id’: ‘chatcmpl-BQaCTYk259rsV3vcpoA2vi6XP7yhd’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “Yes nI’ve read a few books on climate change as part of my studies to better understand the environmental challenges we face and what actions can be taken to mitigate them.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675381, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_f5bdcc3276’, ‘usage’: `{‘completion_tokens’: 34, ‘prompt_tokens’: 96, ‘total_tokens’: 130, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}}` | 1544.401544 | 10.000000 | 0.000580 | 34 | 131 | 0.000647 | | gemini-1.5-flash | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 0.075000 | 48 | 0.300000 | 52287.589235 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “1nnHouse prices are something I think about, but it’s not something that’s keeping me up at night. It’s more of a long-term consideration than something I’m actively focused on right now.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.22673827409744263, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 128, ‘candidates_token_count’: 48, ‘total_token_count’: 176, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 0.075000 | 95 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “NonnI’m a student, so I haven’t had much time to read books outside of my coursework. House prices aren’t really something I’ve focused on yet.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.12296264171600342, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 95, ‘candidates_token_count’: 40, ‘total_token_count’: 135, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 41666.672500 | 0.300000 | 0.000019 | 40 | 128 | 0.000024 | | gpt-4o | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 2.500000 | 32 | 10.000000 | 2127.659574 | `{‘id’: ‘chatcmpl-BQaCUaNZYyLh3T6gtpHnV8YinocJv’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “3 nAs a student, I’m not in the market to buy a house right now, but I am interested in understanding the housing market for future planning.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675382, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_f5bdcc3276’, ‘usage’: `{‘completion_tokens’: 32, ‘prompt_tokens’: 131, ‘total_tokens’: 163, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}` }\` | 2.500000 | 96 | `{‘id’: ‘chatcmpl-BQaCSxNYx8KL3iJGbA3ARSAoUaxAC’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “No nI haven’t read any books specifically about house prices, but I’ve come across articles and discussions about them.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675380, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_f5bdcc3276’, ‘usage’: `{‘completion_tokens’: 23, ‘prompt_tokens’: 96, ‘total_tokens’: 119, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}` }\` | 1544.401544 | 10.000000 | 0.000470 | 23 | 131 | 0.000647 | | gemini-1.5-flash | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 0.075000 | 55 | 0.300000 | 29304.034247 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “5nnIt’s absolutely crucial. As someone with a global platform, I see firsthand the devastating effects of climate change – from extreme weather events impacting communities to the threats to biodiversity. We need urgent action, and I’m committed to doing my part.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.25197906494140626, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 128, ‘candidates_token_count’: 55, ‘total_token_count’: 183, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 0.075000 | 95 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “YesnnOh honey, I’ve read *so many* books about climate change. It’s a topic I’m incredibly passionate about, and I try to stay informed. Between interviews and red carpets, I always have a stack of books on my nightstand, and lately, a lot of them have been focused on environmental issues. It’s crucial to be aware of what’s happening to our planet.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.29773031870524086, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 95, ‘candidates_token_count’: 90, ‘total_token_count’: 185, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 38314.181794 | 0.300000 | 0.000034 | 90 | 128 | 0.000026 | | gpt-4o | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 2.500000 | 39 | 10.000000 | 1503.759398 | `{‘id’: ‘chatcmpl-BQaCSmgqiBdOgYN7hW2qux0PmZvSe’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “5 nClimate change is one of the most pressing issues of our time, and as a public figure, I believe it’s crucial to use my platform to raise awareness and advocate for meaningful action.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675380, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_d8864f8b6b’, ‘usage’: `{‘completion_tokens’: 39, ‘prompt_tokens’: 133, ‘total_tokens’: 172, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}` }\` | 2.500000 | 98 | `{‘id’: ‘chatcmpl-BQaCQav47AtdUvfbBlWZWtGOzMwXY’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “Yes nAs someone in the public eye, I try to stay informed about important issues like climate change, so I’ve read a few books on the subject to better understand its impact and what can be done.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675378, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_d8864f8b6b’, ‘usage’: `{‘completion_tokens’: 42, ‘prompt_tokens’: 98, ‘total_tokens’: 140, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}` }\` | 1384.083045 | 10.000000 | 0.000665 | 42 | 133 | 0.000723 | | gemini-1.5-flash | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 0.075000 | 59 | 0.300000 | 35180.304746 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “3nnHonestly, it’s something I think about, but it’s not my biggest concern. I mean, a nice place to live is great, but my career and family take precedence. Plus, I have people who handle that kind of thing for me, thankfully!n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.2841725430246127, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 128, ‘candidates_token_count’: 59, ‘total_token_count’: 187, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 0.075000 | 95 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “YesnnI mean, honestly, who *hasn’t* been obsessed with the housing market lately? It’s practically a national pastime at this point! I’ve skimmed a few, mostly for research for a role, but let’s be real, the real estate market is its own kind of wild, unpredictable drama.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.33602206807740975, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 95, ‘candidates_token_count’: 71, ‘total_token_count’: 166, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 36630.042024 | 0.300000 | 0.000028 | 71 | 128 | 0.000027 | | gpt-4o | Have you read any books about `{{ scenario.topic }}` ? | On a scale from 1 to 5, how important to you is `{{ scenario.topic }}` ? | 2.500000 | 31 | 10.000000 | 1904.761905 | `{‘id’: ‘chatcmpl-BQaCRYMuhgJBDz0LtE0dX5Nil89RL’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “3 nAs a celebrity, I have an interest in real estate both as an investment and for personal living spaces, but it’s not my primary focus.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675379, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_d8864f8b6b’, ‘usage’: `{‘completion_tokens’: 31, ‘prompt_tokens’: 133, ‘total_tokens’: 164, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}` }\` | 2.500000 | 98 | `{‘id’: ‘chatcmpl-BQaCRamWhwJssHEfAOQCBR6UdQlcj’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: “No nI haven’t read any books specifically about house prices, but I’ve definitely heard a lot about the market through various conversations and media.”, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None, ‘annotations’: []}}` ], ‘created’: 1745675379, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service\_tier’: ‘default’, ‘system\_fingerprint’: ‘fp\_d8864f8b6b’, ‘usage’: `{‘completion_tokens’: 28, ‘prompt_tokens’: 98, ‘total_tokens’: 126, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}` }\` | 1556.420233 | 10.000000 | 0.000525 | 28 | 133 | 0.000642 | We can see the details at Expected Parrot as well. Your [Jobs page](https://www.expectedparrot.com/home/remote-inference) will show the actual costs of each survey job. Your [Transactions page](https://www.expectedparrot.com/home/transactions) will show the additional information about credits on hold based on cost estimates together with actual costs of each survey job. For more on credits, please see the [Credits](/en/latest/credits) section of the docs. # Credits Source: https://docs.expectedparrot.com/en/latest/credits Credits are required in order to run surveys with models using your Expected Parrot API key. They can be purchased at your account (see details below). Credits are deducted based on the applicable rates published on the Expected Parrot model pricing page. These are the rates Expected Parrot uses to calculate credit consumption and may differ from rates charged by the underlying service provider. Credits must be purchased in advance and are consumed when surveys are run. If your balance is insufficient, you will be prompted to purchase more credits. Details on credit consumption are provided below. *Note:* Credits are *not* required to run surveys with your own keys from service providers or to post and share content at [Expected Parrot](https://www.expectedparrot.com/login). When using your own keys, cost estimates may use the prices listed on the model pricing page, but your actual charges are determined by the applicable service provider and may differ from those estimates. ## Free credits Your Expected Parrot account comes with free credits that you can use to run surveys with your API key. **Are you using EDSL for a research project?** Send an email to [*info@expectedparrot.com*](mailto:info@expectedparrot.com) to request additional free credits! ## Purchasing credits To purchase credits, navigate to the [Credits](https://www.expectedparrot.com/home/credits) page of your account and enter the number of credits that you would like to purchase (1 USD buys 100 credits; the minimum purchase amount is 1 USD): Purchase credits *Note:* Payments are processed by Stripe. ## Using credits When you run a survey with your Expected Parrot API key, the number of credits consumed (and deducted from your balance) is displayed at the [Jobs](https://www.expectedparrot.com/home/remote-inference) page of your account. This number is equal to the sum of the cost in credits of each response in the results. The cost in credits of a response is calculated as follows: * The number of input tokens is multiplied by the applicable input token rate published on the Expected Parrot model pricing page. * The number of output tokens is multiplied by the applicable output token rate published on the Expected Parrot model pricing page. * The total cost in USD is converted to credits (1 USD = 100 credits). * The total cost in credits is rounded up to the nearest 1/100th of a credit. (If you use your own keys from service providers, no credits are consumed.) ### Example calculation * Input tokens: 16 * Output tokens: 45 * Input token rate: USD 2.50 per 1M tokens * Output token rate: USD 10.00 per 1M tokens * Total cost: (16 \* USD 2.50/1,000,000) + (45 \* USD 10.00/1,000,000) = USD 0.00049 * Total credits: 0.05 credits ## Response details & token rates Details about a model’s response are stored in the raw\_model\_response fields of the results dataset. For each question that was run, the following columns will appear in results: * **raw\_model\_response.\\_cost**: The cost in USD calculated for the API call using the applicable Expected Parrot pricing rates. (In the example above, this is USD 0.00049.) * **raw\_model\_response.\\_one\_usd\_buys**: The number of tokens that can be purchased with 1 USD (for reference). * **raw\_model\_response.\\_raw\_model\_response**: A dictionary containing the raw response for the question, which includes the input text and tokens, output text and tokens, and other information about the API call. This dictionary is specific to the language model service provider and may contain additional information about the response. For example, here we run a question with two models and inspect the raw model response information (note that the raw response formats are not identical): ```python theme={null} from edsl import QuestionFreeText, ModelList, Model q = QuestionFreeText( question_name = "rainbow", question_text = "What are the colors of a rainbow?" ) m = ModelList(Model(m) for m in ["gemini-1.5-flash", "gpt-4o"]) results = q.by(m).run() results.select("model", "raw_model_response.*") ``` Output: | model.model | raw\_model\_response.rainbow\_cost | raw\_model\_response.rainbow\_raw\_model\_response | raw\_model\_response.rainbow\_one\_usd\_buys | | :- | :- | :- | :- | | gemini-1.5-flash | 0.000018 | `{‘candidates’: [{‘content’: {‘parts’: [{‘text’: “The colors of a rainbow are typically listed as red, orange, yellow, green, blue, indigo, and violet. However, it’s important to note that these colors blend seamlessly into each other, and the number of distinct colors perceived can vary from person to person.n”}], ‘role’: ‘model’}, ‘finish_reason’: 1, ‘safety_ratings’: [{‘category’: 8, ‘probability’: 1, ‘blocked’: False}, {‘category’: 10, ‘probability’: 1, ‘blocked’: False}, {‘category’: 7, ‘probability’: 1, ‘blocked’: False}, {‘category’: 9, ‘probability’: 1, ‘blocked’: False}], ‘avg_logprobs’: -0.099734950483891, ‘token_count’: 0, ‘grounding_attributions’: []}], ‘usage_metadata’: {‘prompt_token_count’: 8, ‘candidates_token_count’: 57, ‘total_token_count’: 65, ‘cached_content_token_count’: 0}, ‘model_version’: ‘gemini-1.5-flash’}` | 56497.186153 | | gpt-4o | 0.000438 | `{‘id’: ‘chatcmpl-B2OaTCPGFdNY7dju27SxmrLfSWXSE’, ‘choices’: [{‘finish_reason’: ‘stop’, ‘index’: 0, ‘logprobs’: None, ‘message’: {‘content’: ‘The colors of a rainbow, in order, are red, orange, yellow, green, blue, indigo, and violet. These colors can be remembered using the acronym ROYGBIV.’, ‘refusal’: None, ‘role’: ‘assistant’, ‘audio’: None, ‘function_call’: None, ‘tool_calls’: None}}], ‘created’: 1739910869, ‘model’: ‘gpt-4o-2024-08-06’, ‘object’: ‘chat.completion’, ‘service_tier’: ‘default’, ‘system_fingerprint’: ‘fp_523b9b6e5f’, ‘usage’: {‘completion_tokens’: 40, ‘prompt_tokens’: 15, ‘total_tokens’: 55, ‘completion_tokens_details’: {‘accepted_prediction_tokens’: 0, ‘audio_tokens’: 0, ‘reasoning_tokens’: 0, ‘rejected_prediction_tokens’: 0}, ‘prompt_tokens_details’: {‘audio_tokens’: 0, ‘cached_tokens’: 0}}}` | 2285.714286 | In the raw model response information for the response from *gpt-4o*, we can see values for completion\_tokens (output tokens) and prompt\_tokens (input tokens): ```python theme={null} 'completion_tokens': 40, 'prompt_tokens': 16 ``` The total cost of the response is calculated using the applicable rates listed on the Expected Parrot model pricing page (at the time of writing, USD 2.50 per 1M tokens for input and USD 10.00 per 1M tokens for output): ```python theme={null} (16 * USD 2.50/1,000,000) + (40 * USD 10.00/1,000,000) = USD 0.00044 = 0.05 credits (rounded up to the nearest 1/100th credit) ``` In the raw model response information for the response from *gemini-1.5-flash*, we can see values for prompt\_token\_count and candidates\_token\_count (input tokens and output tokens, respectively): ```python theme={null} 'prompt_token_count': 8, 'candidates_token_count': 57 ``` The total cost of the response is calculated using the applicable rates listed on the Expected Parrot model pricing page (at the time of writing, USD 0.08 per 1M tokens for input and USD 0.30 per 1M tokens for output): ```python theme={null} (8 * USD 0.08/1,000,000) + (57 * USD 0.30/1,000,000) = USD 0.00001774 = 0.01 credits (rounded up to the nearest 1/100th credit) ``` This translates to a total of 0.06 credits consumed for the survey. We can see this number of credits consumed at the remote inference page of our Expected Parrot account: Expected Parrot remote inference jobs page We can also navigate to the results page and select the same columns of the results to display: Expected Parrot remote inference jobs page ## Token rates Model token rates used to calculate costs can be viewed on the [model pricing page](https://www.expectedparrot.com/getting-started/coop-pricing). For jobs run using an Expected Parrot API key, the rates published on that page are the rates Expected Parrot uses to calculate credit consumption. Expected Parrot reviews pricing information from service providers regularly, but our published rates do not necessarily reproduce every provider-specific pricing mechanism, including discounts, promotions, time-based pricing, caching discounts, batch pricing, volume pricing, service tiers, or similar adjustments. Published Expected Parrot rates may therefore differ from the rates a service provider would charge for the same request made directly or using your own API key. Provider pricing may change without notice, and Expected Parrot rates may not be updated at the same time. If a current price is unavailable, a default or fallback price may be used as described on the model pricing page. If you believe a published price is missing or out of date, please submit a report using the form on that page. ## Estimating job costs Before running a survey, you can estimate the tokens and costs (in USD and credits) in 2 different ways: * Call the estimate\_job\_cost() method on the Job object (a survey combined with one or more models). This will return the total estimated cost in USD, the total estimated input and output tokens, and estimated costs and tokens for each inference service and model used. * Call the remote\_inference\_cost() method on a `Coop` client object and pass it the job. This will return the estimated cost in credits and USD. (Credits are required to run surveys remotely.) ### Example Here we create a survey and agent, select a model and combine them to create a job. Then we call the above-mentioned methods for estimating costs and show the underlying calculations. The steps below can also be accessed as a notebook at the Expected Parrot platform ([notebook view](https://www.expectedparrot.com/content/c379241a-7039-4505-8d42-4c909a54c6e0)). ```python expandable theme={null} from edsl import QuestionFreeText, Survey, Agent, Model q0 = QuestionFreeText( question_name = "favorite_flower", question_text = "What is the name of your favorite flower?" ) q1 = QuestionFreeText( question_name = "flower_color", question_text = "What color is {{ favorite_flower.answer }}?" ) survey = Survey(questions = [q0, q1]) a = Agent(traits = {"persona":"You are a botanist on Cape Cod."}) m = Model("gpt-4o") job = survey.by(a).by(m) estimated_job_cost = job.estimate_job_cost() estimated_job_cost ``` Output: ```json theme={null} {'estimated_total_cost': 0.0009175000000000001, 'estimated_total_input_tokens': 91, 'estimated_total_output_tokens': 69, 'model_costs': [{'inference_service': 'openai', 'model': 'gpt-4o', 'estimated_cost': 0.0009175000000000001, 'estimated_input_tokens': 91, 'estimated_output_tokens': 69}]} ``` The estimated\_total\_cost is the total cost in USD to run the job, and the estimated\_total\_input\_tokens and estimated\_total\_output\_tokens are the estimated total input and output tokens, respectively for all the prompts in the survey. To get the estimated cost in credits to run the job remotely we can call the remote\_inference\_cost() method on a `Coop` client object and pass it the job: ```python theme={null} from edsl import Coop coop = Coop() estimated_remote_inference_cost = coop.remote_inference_cost(job) # using the job object from above estimated_remote_inference_cost ``` Output: ```json theme={null} {'credits': 0.1, 'usd': 0.00092} ``` ### Formula details Total job costs are estimated by performing the following calculation for each set of question prompts in the survey and summing the results: 1. Estimate the input tokens. * Compute the number of characters in the user\_prompt and system\_prompt, with any Agent and Scenario data piped in. (*Note:* Previous answers cannot be piped in because they are not available until the survey is run; they are left as Jinja-bracketed variables in the prompts for purposes of estimating tokens and costs.) * Apply a piping multiplier of 2 to the number of characters in the user prompt if it has an answer piped in from a previous question (i.e., if the question has Jinja braces that cannot be filled in before the survey is run). Otherwise, apply a multiplier of 1. * Convert the number of characters into the number of input tokens using a conversion factor of 4 characters per token, rounding down to the nearest whole number. (This approximation was [established by OpenAI](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them).) 2. Estimate the output tokens. * Apply a multiplier of 0.75 to the number of input tokens, rounding up to the nearest whole number. 3. Apply the token rates for the model and inference service. * Find the model and inference service for the question in the [model pricing page](https://www.expectedparrot.com/getting-started/coop-pricing): Total cost in USD = (input tokens \* input token rate) + (output tokens \* output token rate) * If a model and inference service are not found, use the following fallback token rates (you will see a warning message that actual model rates were not found): * USD 1.00 per 1M input tokens * USD 1.00 per 1M ouput tokens 4. Convert the total cost in USD to credits. * Total cost in credits = total cost in USD \* 100, rounded up to the nearest 1/100th credit. Then sum the costs for all question prompts to get the total cost of the job. ### Calculations Here we show the calculations for the examples above. We can call the show\_prompts() method on the job object to see the prompts for each question in the survey: ```python theme={null} job.show_prompts() ``` Output: | user\_prompt | system\_prompt | | :- | :- | | What is the name of your favorite flower? | You are answering questions as if you were a human. Do not break character. Your traits: `{‘persona’: ‘You are a botanist on Cape Cod.’}` | | What color is `{{ answer }}`? | You are answering questions as if you were a human. Do not break character. Your traits: `{‘persona’: ‘You are a botanist on Cape Cod.’}` | Here we count the characters in each user prompt and system prompt: ```python theme={null} q0_user_prompt_characters = len("What is the name of your favorite flower?") q0_user_prompt_characters ``` Output: ``` 41 ``` ```python theme={null} q0_system_prompt_characters = len("You are answering questions as if you were a human. Do not break character. Your traits: {'persona': 'You are a botanist on Cape Cod.'}") q0_system_prompt_characters ``` Output: ``` 135 ``` We apply the piping multiplier of 2 to the number of characters in the user prompt for q1 because the answer to q0 is piped in: ```python theme={null} q1_user_prompt_characters = len("What color is {{ answer }}?") * 2 q1_user_prompt_characters ``` Output: ``` 54 ``` The system prompt characters are identical for the single agent used with the questions: ```python theme={null} q1_system_prompt_characters = len("You are answering questions as if you were a human. Do not break character. Your traits: {'persona': 'You are a botanist on Cape Cod.'}") q1_system_prompt_characters ``` Output: ``` 135 ``` Here we estimate the input and output tokens for each set of prompts: ```python theme={null} q0_input_tokens = (q0_user_prompt_characters + q0_system_prompt_characters) // 4 q0_input_tokens ``` Output: ``` 44 ``` ```python theme={null} q0_output_tokens = ceil(0.75 * q0_input_tokens) q0_output_tokens ``` Output: ``` 33 ``` ```python theme={null} q1_input_tokens = (q1_user_prompt_characters + q1_system_prompt_characters) // 4 q1_input_tokens ``` Output: ``` 47 ``` ```python theme={null} q1_output_tokens = ceil(0.75 * q1_input_tokens) q1_output_tokens ``` Output: ``` 36 ``` The total input tokens and output tokens for the job are: ```python theme={null} total_input_tokens = q0_input_tokens + q1_input_tokens total_input_tokens ``` Output: ``` 91 ``` ```python theme={null} total_output_tokens = q0_output_tokens + q1_output_tokens total_output_tokens ``` Output: ``` 69 ``` Next we apply the token rates for the model: ```python theme={null} q0_tokens_cost = (2.50/1000000 * q0_input_tokens) + (10.00/1000000 * q0_output_tokens) q0_tokens_cost ``` Output: ``` 0.00044000000000000007 ``` ```python theme={null} q1_tokens_cost = (2.50/1000000 * q1_input_tokens) + (10.00/1000000 * q1_output_tokens) q1_tokens_cost ``` Output: ``` 0.00047750000000000006 ``` The total cost of the job is: ```python theme={null} total_cost_usd = q0_tokens_cost + q1_tokens_cost total_cost_usd ``` Output: ``` 0.0009175000000000001 ``` We convert the total cost in USD to credits: ```python theme={null} q0_credits = ceil(q0_tokens_cost * 100 * 100) / 100 q0_credits ``` Output: ``` 0.05 ``` ```python theme={null} q1_credits = ceil(q1_tokens_cost * 100 * 100) / 100 q1_credits ``` Output: ``` 0.05 ``` We calculate the total cost in credits: ```python theme={null} total_credits = q0_credits + q1_credits total_credits ``` Output: ``` 0.1 ``` The total cost of the job is 0.00092 USD and 0.1 credits. ## Refunds Please send an email to [*info@expectedparrot.com*](mailto:info@expectedparrot.com) if you have any questions about credits or refunds, or need assistance with your account. # Custom CSS Source: https://docs.expectedparrot.com/en/latest/custom_css CSS classes and HTML structure for styling human-facing surveys with custom CSS. Every question component supported by humanize has `edsl-*` CSS classes so you can customise the survey appearance. ## Applying custom CSS ### When creating a human survey Pass your CSS string in the `humanize_schema` under `survey.custom_css`. ```python theme={null} from edsl import Survey, QuestionFreeText, QuestionMultipleChoice survey = Survey([ QuestionMultipleChoice( question_name="rating", question_text="Rate your experience.", question_options=["Good", "OK", "Bad"], ), QuestionFreeText( question_name="feedback", question_text="Any additional feedback you'd like to share?", ), ]) custom_css = """ .edsl-survey-container { background-color: #f9f9f9; } .edsl-question-text { font-weight: bold; color: #333; } """ humanize_schema = { "questions": { "rating": {"format": {"type": "dropdown"}}, "feedback": {"optional": True}, }, "survey": {"custom_css": custom_css}, } survey.humanize(humanize_schema=humanize_schema) ``` Pass `"survey": {"custom_css": None}` (or omit `custom_css`) to create the survey without any custom CSS. ### Updating CSS on an existing human survey Use `Coop.patch_human_survey_css` to set or clear the CSS on a human survey that already exists. ```python theme={null} from edsl import Coop coop = Coop() # Set or replace CSS coop.patch_human_survey_css( human_survey_uuid="your-human-survey-uuid", css=".edsl-survey-container { background-color: #fff; }", ) # Clear existing CSS coop.patch_human_survey_css( human_survey_uuid="your-human-survey-uuid", css=None, ) ``` ## Survey-level classes | Class | Element | Notes | | - | - | - | | `edsl-survey-container` | Outermost container of the entire survey | Use to set background color for the whole survey. Carries `data-group-name` when the survey is presented by question group; see below | | `edsl-survey-banner` | `
` holding the survey's logo, at the top of the survey | Only present when the survey has a logo. Give it a `background` for a banner; see below | | `edsl-survey-banner-content` | Column inside `edsl-survey-banner` holding the logo | Lined up with `edsl-survey-content` | | `edsl-logo` | The logo `` inside `edsl-survey-banner` | 40px tall by default, width following the image. Resize with `height`; see below | | `edsl-survey-content` | Column holding the page's questions and `edsl-survey-nav` | Questions are spaced by its `gap` | | `edsl-survey-item` | `
` wrapping each question or instruction on the page | Carries `data-question-name` or `data-instruction-name`; see below | | `edsl-instruction` | `
` holding an instruction's text | Inside `edsl-survey-item` | | `edsl-survey-nav` | Row `
` holding the page's navigation buttons | Below the questions | | `edsl-next-button` | `

…

``` ### EDSL object ```html theme={null}
Select the agent list to use.
Selected Agent List

My agents - Q1 2025

UUID: a1b2c3d4-…

…

``` ### File upload ```html theme={null}
Upload supporting documents.
…

Upload files

Drag & drop or click to browse

All file types · Max 10 files · Up to 1 GB per file

Files (2)
…

doc1.pdf

24 KB

Uploading…
Done Failed
Name Size Upload Status Actions
… doc1.pdf 24 KB
Done Failed

…

``` ### Free text ```html theme={null}
Tell us about yourself.

…

``` ### Linear scale When humanize format is `radio` (default): ```html theme={null}
Rate from 1 to 5.

…

```
When humanize format is `dropdown`: ```html theme={null}
Rate from 1 to 5.

…

```
### Likert five When humanize format is `radio` (default): ```html theme={null}
How satisfied are you?

…

```
When humanize format is `dropdown`: ```html theme={null}
How satisfied are you?

…

```
### List ```html theme={null}
List your top hobbies.

Please enter one item per line.

…

``` ### Matrix When humanize format is `table` (default): ```html theme={null}
Rate each item.
Price
Poor 1
Price

…

``` Both views are always in the page, and CSS picks which one shows. To keep the table at every width: ```css theme={null} .edsl-matrix-stack-view { display: none; } .edsl-matrix-table-view { display: block; } ``` Row labels take 25% of the table's width by default, and the option columns share the rest. To change that, set `--edsl-matrix-item-column` to a percentage, on `.edsl-matrix-table` or on `.edsl-survey-container`: ```css theme={null} .edsl-matrix-table { --edsl-matrix-item-column: 40%; } ```
When humanize format is `carousel`, rows are shown one at a time: ```html theme={null}
Rate each item.

…

```
### Multiple choice When humanize format is `radio` (default): ```html theme={null}
Which fruit do you prefer?

… Please select an option.

```
When humanize format is `dropdown`: ```html theme={null}
Which fruit do you prefer?

…

```
### Multiple choice with other ```html theme={null}
How did you hear about us?

…

``` ### Numerical When humanize format is `input` (default): ```html theme={null}
Enter your age.

…

```
When humanize format is `slider`: ```html theme={null}
On a scale of 0 to 100, how likely are you to recommend?
0 50 100

…

```
### Rank ```html theme={null}
Rank these items from most to least preferred.
Item A
Item B
…
Item C
…

…

``` ### Survey message ```html theme={null}
Thanks! The next few questions are about your work.
``` ### Top K ```html theme={null}
Pick your top 2.

…

``` ### Yes/no When humanize format is `radio` (default): ```html theme={null}
Do you agree?

…

```
When humanize format is `dropdown`: ```html theme={null}
Do you agree?

…

```
## Comment field If a question has `comment` configured in the humanize schema, an additional comment input is rendered under that question. ```html theme={null}
``` # Custom JavaScript Source: https://docs.expectedparrot.com/en/latest/custom_javascript Run your own JavaScript on a human survey's questions, and record what respondents do. Custom JavaScript lets you attach a script to a question in a human survey. The script runs in the respondent's browser once the question is on the page, so you can build interactive tasks, measure timing and behaviour, and record events with the survey. Custom JavaScript is available on approved accounts only. Write to [info@expectedparrot.com](mailto:info@expectedparrot.com) to request access. Until your account is approved, a humanize schema that includes a script is rejected when you save it, and no part of that schema is saved. ## Checking access ```python theme={null} from edsl import Coop coop = Coop() coop.get_custom_js_access() # True once your account is approved ``` Access is per account: once yours is approved, the human surveys you create can include custom JavaScript. ## Adding a script Scripts go in the humanize schema, under `questions[question_name].javascript.hooks`. A hook maps an event to the script that runs on it. The only event is `"question.ready"`, which fires once the question has been rendered and its inputs are on the page. ```python theme={null} from edsl import Survey, QuestionMultipleChoice survey = Survey([ QuestionMultipleChoice( question_name="choice", question_text="Which option do you prefer?", question_options=["A", "B", "C"], ), ]) time_to_first_answer = """ const container = this.getQuestionContainer(); const shownAt = performance.now(); container.addEventListener("change", () => { ep.log("first_answer", { ms: Math.round(performance.now() - shownAt) }); }, { once: true }); """ humanize_schema = { "questions": { "choice": {"javascript": {"hooks": {"question.ready": time_to_first_answer}}}, }, } survey.humanize(humanize_schema=humanize_schema) ``` To add or change a script on a survey that already exists, use `Coop.patch_human_survey_humanize_schema`: ```python theme={null} coop.patch_human_survey_humanize_schema( "your-human-survey-uuid", {"questions": {"choice": {"javascript": {"hooks": {"question.ready": time_to_first_answer}}}}}, ) ``` Set `"javascript": None` on a question to remove its script. Most question types support `javascript`, including survey messages; see [Humanize schema](/en/latest/humanize_schema) for the list. `compute` and `image_generation` questions do not: they run in the background and never appear on the page. ## Writing a hook A hook is the *body* of a function, not a function. Write the statements you want to run; don't wrap them in `function () { ... }`. Inside a hook you have: | Name | What it is | | - | - | | `this.getQuestionContainer()` | The question's element on the page. Find your question's inputs and add your own elements inside it. | | `context.questionName` | The name of the question the hook is running for. | | `context.questionType` | The question's type, for example `"multiple_choice"`. | | `context.isPreview` | `true` when the survey was opened through a preview link, which does not save a response. | | `ep.log(eventName, payload)` | Records an event with the survey. See below. | `context` carries nothing that identifies the respondent. Each question's hook runs once when that question appears. If the same script is attached to several questions, it runs once for each, and `context.questionName` tells them apart. ### Recording events with `ep.log` `ep.log(eventName, payload)` records an event against the respondent's response. Use it for anything you want to analyse later: timings, clicks, which condition a respondent saw. * `eventName` must be a non-empty string. Names longer than 200 characters are cut to 200. * `payload` is optional and must be convertible to JSON: objects, arrays, strings, numbers, booleans and `null`. A payload that isn't (a page element, an object that refers to itself) is not recorded, and a warning appears in the browser console. * A payload larger than 4,000 characters, measured as JSON, is not recorded. * Each response records at most 10,000 events. `ep.log` returns nothing. Events are sent in batches in the background, so don't wait on it. ### What a script may do Scripts can build interface inside the survey, read and write answers, control when respondents can move on, measure behaviour, randomise conditions, read parameters from the page URL, load images, audio, video and other media to show, and use the camera, microphone or location for a task the respondent is told about. A script can also load a well-known, widely used library, such as d3, Chart.js or lodash, from a public CDN like cdnjs, jsDelivr or unpkg, at a pinned version (for example `https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js`, not `d3@latest`). Your own code must still be plain JavaScript written in the script itself. A script can't be saved if it: * Loads code from anywhere else: an unfamiliar or personal site, a paste site, a little-known or custom package, source hosted somewhere other than a named library, or a URL put together while the script runs. * Builds code at runtime, for example by passing a constructed string to `eval`, `new Function`, `setTimeout` or `setInterval`, or by inserting markup that contains a script. * Makes any request to Expected Parrot, or sends respondent data anywhere else. `ep.log` is how data leaves a script. * Reads cookies, tokens, credentials, or storage it didn't create. * Is obfuscated or minified so that what it does can't be read from the source. This applies to your script, not to a well-known library it loads. * Asks for passwords or payment details, or imitates a login or other dialog. * Collects anything unrelated to taking the survey, or uses the camera, microphone or location covertly. * Keeps running after the respondent has finished or left the survey. * Misleads or traps the respondent, for example by opening popups, preventing them from leaving, or hiding the survey's own notices. * Uses the respondent's device for something other than the survey. If a script can't be saved, no part of that humanize schema is saved, and the survey keeps what it had. If you think a script was refused by mistake, write to [info@expectedparrot.com](mailto:info@expectedparrot.com). ### Limits * Each hook can be at most 64,000 characters, and all hooks on one question at most 256,000. * One save can add at most 25 different new scripts. A script used unchanged on several questions counts once, and scripts the survey already has don't count. Save larger surveys in smaller batches. ## Retrieving events Use `Coop.get_all_human_survey_events` to get every event a survey has recorded, oldest first: ```python theme={null} from edsl import Coop coop = Coop() events = coop.get_all_human_survey_events("your-human-survey-uuid") respondent_events = [e for e in events if not e["is_preview"]] ``` Events from preview links are included and marked `is_preview`, so leave them out of an analysis. Events that arrive while the download runs are included, and no event comes back twice. To fetch new events later without downloading everything again, pass the `id` of the last event you downloaded as `after`. Take it from the full download, before filtering out previews: ```python theme={null} last_id = events[-1]["id"] if events else None new_events = coop.get_all_human_survey_events( "your-human-survey-uuid", after=last_id ) ``` Fetching only new events is useful for checking your scripts while a survey is collecting, but it can occasionally miss an event that was still being saved when you downloaded. For analysis, download the full log once fielding has ended, at least an hour after the last response. To fetch one batch at a time, use `Coop.get_human_survey_events`. It takes `after` and `limit` (at most 200, the default), and returns `{"events", "next_cursor", "has_more"}`. Pass `next_cursor` as `after` to get the next batch while `has_more` is `True`. `next_cursor` is returned even when there is nothing more, so you can keep it and use it later. To see how many events a survey has without downloading them, use `Coop.count_human_survey_events`. The count is as of the call; on a survey still collecting, a download started afterwards can return more. Each event includes: | Field | Description | | - | - | | `id` | The event's unique id | | `received_at` | When Expected Parrot received it (ISO 8601, UTC) | | `client_ts` | When the respondent's browser logged it (ISO 8601, UTC) | | `response_uuid` | The response the event belongs to | | `is_preview` | Whether the event came from a preview link | | `log_session_uuid` | The page load the event was logged in. Each time a respondent opens or reloads the survey starts a new session, so one response can have several. | | `session_state` | Whether that session's log is whole. See below. | | `client_seq` | The event's position in its session, counting from 0. Sort by it to get the order events were logged in. A gap means an event in between was not recorded. | | `question_name` | The question whose script logged it | | `event_type` | What kind of event it is. Always `"ep.log"` for now. | | `event_name` | The `eventName` passed to `ep.log` | | `hook_event` | The hook that was running when the event was logged, for example `"question.ready"` | | `source` | What logged the event. Always `"author_script"` for now. | | `payload` | The `payload` passed to `ep.log`, or `None` | `session_state` is one of: * `"complete"`: the session ended normally and no events are missing. * `"truncated"`: some events from this session are known to be missing, for example because they broke the limits above. * `"unknown"`: no events are known to be missing, but the session never signalled that it ended, for example because the respondent's browser closed or lost its connection. Its log may or may not be whole. `session_state` describes the whole session, so every event from one session carries the same value. ## From the command line The `ep` CLI covers the same steps. `--javascript` takes `QUESTION:HOOK=FILE`, naming the question, the hook, and a file holding the script: ```bash theme={null} # Check access ep humanize custom-js-access # Attach a script while building a schema, or to a survey that already exists ep humanize schema create --survey survey.ep --javascript choice:question.ready=choice.js --output humanize.json ep humanize schema set --javascript choice:question.ready=choice.js # Remove a question's script ep humanize schema set --clear-javascript choice # Get the first batch of events, or save every event to a file ep humanize events ep humanize events --all --output events.jsonl # Later, save only the events since the last download ep humanize events --all --after --output new.jsonl # Count events without downloading them ep humanize events --count ``` `--all` requires `--output`, which can end in `.json` or `.jsonl`. Every result includes `next_cursor`, the value to pass as `--after` next time. If a download fails part way, no output file is written. As with the Python methods, use `--after` for checking a survey while it collects, and a full download once fielding has ended for analysis. # Caching LLM Calls Source: https://docs.expectedparrot.com/en/latest/data The *Cache* class is used to store responses from a language model so that they can easily be retrieved, reused and shared. *What is a cache?* ([Wikipedia](https://en.wikipedia.org/wiki/Cache_\(computing\))) ## Why caching? Language model outputs are expensive to create, both in terms of time and money. As such, it is useful to store them in a cache so that they can be shared or reused later. Use cases: * Avoid rerunning questions when a job fails only partially, by only resending unanswered questions to a language model. * Share your cache with others so they can rerun your questions at no cost. * Use a common remote cache to avoid rerunning questions that others have already run. * Build up training data to train or fine-tune a smaller model. * Build up a public repository of questions and responses so others can learn from them. ## How it works A Cache is a dictionary-like object that stores the inputs and outputs of a language model. Specifically, a cache has an attribute, data, that is dictionary-like. The keys of a cache are hashes of the unique inputs to a language model, i.e., the unique combinations of prompts and any parameters used to generate the outputs. The values are CacheEntry objects, which contains the inputs and outputs. A cache can be stored as either a Python in-memory dictionary or a dictionary connected to a SQLite3 database. The default constructor is an in-memory dictionary. If a SQLite3 database is used, a cache will persist automatically between sessions. You can also specify that a cache be used for a specific session, in which case it will not persist between sessions. After a session, the cache will have new entries from any new jobs that have been run during the session. These can be written to a local SQLite3 database, a JSONL file, or a remote server. ## Multiple caches Multiple caches are impacted when a survey is run: * **New cache for results:** A new cache is automatically created for the results of a survey. This cache is specific to the results object, and is attached to it. * **Default cache updated:** A default cache is automatically updated with any new entries from the results. If a new or existing cache object was passed to the run() method, it is the default cache that is updated. Otherwise, either your local cache or your remote cache is updated, depending on whether the job was run locally or remotely. See examples below for more details on how to create and manage caches. ## Generating a new cache for results A new cache is automatically generated whenever results are generated for a question or survey. This cache is specific to the Results object, and is attached to it. It can be accessed using the cache attribute of the results object. For example: ```python theme={null} from edsl import QuestionNumerical, Model m = Model("gemini-1.5-flash") q = QuestionNumerical( question_name = "random", question_text = "Please give me a random number between 1 and 100." ) results = q.by(m).run() results.cache ``` Example output: | model | parameters | system\_prompt | user\_prompt | output | iteration | timestamp | cache\_key | | :- | :- | :- | :- | :- | :- | :- | :- | | gemini-1.5-flash | `{‘temperature’: 0.5, ‘topP’: 1, ‘topK’: 1, ‘maxOutputTokens’: 2048, ‘stopSequences’: []}` | nan | Please give me a random number between 1 and 100. This question requires a numerical response in the form of an integer or decimal (e.g., -12, 0, 1, 2, 3.45, …). Respond with just your number on a single line. If your response is equivalent to zero, report ‘0’ After the answer, put a comment explaining your choice on the next line. | `{“candidates”: [{“content”: {“parts”: [{“text”: “87n# This is a randomly generated number between 1 and 100.n”}], “role”: “model”}, “finish_reason”: 1, “safety_ratings”: [{“category”: 8, “probability”: 1, “blocked”: false}, {“category”: 10, “probability”: 1, “blocked”: false}, {“category”: 7, “probability”: 1, “blocked”: false}, {“category”: 9, “probability”: 1, “blocked”: false}], “avg_logprobs”: -0.03539780080318451, “token_count”: 0, “grounding_attributions”: []}], “usage_metadata”: {“prompt_token_count”: 97, “candidates_token_count”: 20, “total_token_count”: 117, “cached_content_token_count”: 0}}` | 0 | 1737491116 | 7f057154c60a1b9ae343b0634fe7a370 | The results object also includes columns of information about the cache for the results: ```python theme={null} results.columns ``` Output: | | | :- | | agent.agent\_index | | agent.agent\_instruction | | agent.agent\_name | | answer.random | | cache\_keys.random\_cache\_key | | cache\_used.random\_cache\_used | | comment.random\_comment | | generated\_tokens.random\_generated\_tokens | | iteration.iteration | | model.maxOutputTokens | | model.model | | model.model\_index | | model.stopSequences | | model.temperature | | model.topK | | model.topP | | prompt.random\_system\_prompt | | prompt.random\_user\_prompt | | question\_options.random\_question\_options | | question\_text.random\_question\_text | | question\_type.random\_question\_type | | raw\_model\_response.random\_cost | | raw\_model\_response.random\_one\_usd\_buys | | raw\_model\_response.random\_raw\_model\_response | | scenario.scenario\_index | The cache\_keys column contains the cache key for each question. It is a unique identifier for the cache entry, and is a hash of the unique inputs to the language model; if you rerun the same question with the same parameters, it will return the same cache key. It can be used to retrieve the cache entry later, or to add the entry to a different cache. The cache\_used column indicates whether or not each result was retrieved from the default cache (your local cache, your remote cache, or a cache that was passed to the run() method when the results were generated). Whenever a question is run for the first time, the cache\_used column will be False; if the same question is run again with the same cache available, the cache\_used column will be True. For example, here we run a new question and confirm that the default cache was not used (i.e., the cache key for the question is not already in the available cache): ```python theme={null} from edsl import QuestionNumerical, Model m = Model("gemini-1.5-flash") q = QuestionNumerical( question_name = "random", question_text = "Please give me a random number between 1 and 100." ) r1 = q.by(m).run() r1.select("cache_keys.*", "cache_used.*") ``` Output: | cache\_keys.random\_cache\_key | cache\_used.random\_cache\_used | | :- | :- | | 7f057154c60a1b9ae343b0634fe7a370 | False | If we run the question again, the cache key will be the same, but the cache\_used indicator will now be True: ```python theme={null} r2 = q.by(m).run() r2.select("cache_keys.*", "cache_used.*") ``` Output: | cache\_keys.random\_cache\_key | cache\_used.random\_cache\_used | | :- | :- | | 7f057154c60a1b9ae343b0634fe7a370 | True | ## Working with a cache A cache can be passed to the run() method of a survey in order to make the entries available in generating the survey results. This can be useful if you want to add new entries to a specific cache other than your default cache (local or remote). For example: ```python expandable theme={null} from edsl import QuestionFreeText, QuestionNumerical, Survey, Model m = Model("gemini-1.5-flash") # This question is already run, so we can use its cache from above (r1.cache) q1 = QuestionNumerical( question_name = "random", question_text = "Please give me a random number between 1 and 100." ) # This question is new, so the entry will be added to the cache that we use q2 = QuestionFreeText( question_name = "explain", question_text = "How does an AI choose a random number?" ) survey = Survey(questions = [q1,q2]) # The results will include the retrieved entry for q1 and the new entry for q2 r3 = survey.by(m).run(cache = r1.cache) r3.select("cache_keys.*", "cache_used.*") ``` Output: | cache\_keys.random\_cache\_key | cache\_keys.explain\_cache\_key | cache\_used.random\_cache\_used | cache\_used.explain\_cache\_used | | :- | :- | :- | :- | | 7f057154c60a1b9ae343b0634fe7a370 | 6442cf8e6b9812a89bd50bf059f77885 | True | False | **Note**: Note that this is independent of the new cache generated for the results as above, which are accessed by calling r3.cache. ## Instantiating a new cache This code will instantiate a new cache object using a dictionary as the data attribute ### In-memory usage ```python theme={null} from edsl import Cache my_in_memory_cache = Cache() ``` It can then be passed as an object to a run method: ```python theme={null} from edsl import Survey s = Survey.example() results = s.run(cache = my_in_memory_cache) ``` If an in-memory cache is not stored explicitly, the data will be lost when the session is over, unless it is written to a file or remote caching is instantiated. More on this below. ### Local persistence for an in-memory cache ```python theme={null} c = Cache() # a bunch of operations c.write_sqlite_db("example.db") # or c.write_jsonl("example.jsonl") ``` You can then load the cache from the SQLite3 database or JSONL file using Cache methods. ```python theme={null} c = Cache.from_sqlite_db("example.db") # or c = Cache.from_jsonl("example.jsonl") ``` ### SQLite3Dict for transactions Instead of using a dictionary as the data attribute, you can use a special dictionary-like object based on SQLite3. This will persist the cache between sessions. This is the “normal” way that a cache is used for runs where no specic cache is passed. ```python theme={null} from edsl import Cache from edsl.data.SQLiteDict import SQLiteDict my_sqlite_cache = Cache(data = SQLiteDict("example.db")) ``` This will leave a SQLite3 database on the user’s machine at the file, in this case example.db in the current directory. It will persist between sessions and can be loaded using the from\_sqlite\_db method shown above. ### Default SQLite Cache: .edsl\_cache/data.db By default, the cache will be stored in a SQLite3 database at the path .edsl\_cache/data.db. You can interact with this cache directly, e.g., ```python theme={null} sqlite3 .edsl_cache/data.db ``` ### Setting a session cache The set\_session\_cache function is used to set the cache for a session: ```python theme={null} from edsl import Cache, set_session_cache set_session_cache(Cache()) ``` The cache can be set to a specific cache object, or it can be set to a dictionary or SQLite3Dict object. ```python theme={null} from edsl import Cache, set_session_cache from edsl.data import SQLiteDict set_session_cache(Cache(data = SQLiteDict("example.db"))) # or set_session_cache(Cache(data = {})) ``` This will set the cache for the current session, and you do not need to pass the cache object to the run method during the session. The unset\_session\_cache function is used to unset the cache for a session: ```python theme={null} from edsl import unset_session_cache unset_session_cache() ``` This will unset the cache for the current session, and you will need to pass the cache object to the run method during the session. ### Avoiding cache persistence We can avoid cache persistence by passing cache=False to the run method: ```python theme={null} from edsl import QuestionFreeText q = QuestionFreeText.example() results = q.run(cache = False) ``` ## For developers ### Delayed cache-writing: Useful for remote caching Separate from this remote cache syncing, delays can be made in writing to the cache itself. By default, the cache will write to the cache immediately after storing a new entry. However, this can be changed by setting the immediate\_write parameter to False. ```python theme={null} c = Cache(immediate_write = False) ``` This is useful when you want to store entries to the cache only after a block of code has been executed. This is also controlled by using the cache object as a context. ```python theme={null} with c as cache: # readings / writing ... # The cache will be written to the cache persistence layer after the block of code has been executed ``` ## Cache class > ### *class* edsl.caching.Cache(\*, *filename: str | None = None*, *data: SQLiteDict | dict | None = None*, *immediate\_write: bool = True*, *method=None*, *verbose=False*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Bases: [`Base`](/en/latest/base "edsl.base.base_class.Base") Cache for storing and retrieving language model responses. The Cache class manages a collection of CacheEntry objects, providing methods for storing, retrieving, and persisting language model responses. It serves as the core component of EDSL’s caching infrastructure, helping to reduce redundant API calls, save costs, and ensure reproducibility. Cache can use different storage backends: - In-memory dictionary (default) - SQLite database via SQLiteDict - JSON lines file (.jsonl) The cache operates by generating deterministic keys based on the model, parameters, prompts, and iteration number. This allows for efficient lookup of cached responses when identical requests are made. **Attributes:** data (dict or SQLiteDict): The primary storage for cache entries new\_entries (dict): Entries added in the current session fetched\_data (dict): Entries retrieved in the current session filename (str, optional): Path for persistence if provided immediate\_write (bool): Whether to update data immediately (True) or defer (False) **Technical Notes:** * Can be used as a context manager to automatically persist changes on exit * Supports serialization/deserialization via to\_dict/from\_dict methods * Implements set operations (addition, subtraction) for combining caches * Integrates with the broader EDSL caching infrastructure via CacheHandler > #### **init**(\*, *filename: str | None = None*, *data: SQLiteDict | dict | None = None*, *immediate\_write: bool = True*, *method=None*, *verbose=False*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Initialize a new Cache instance. Creates a new cache for storing language model responses. The cache can be initialized with existing data or connected to a persistent storage file. > **Args:** **filename: Path to a persistent storage file (.jsonl or .db). If provided, the cache** will be initialized from this file and changes will be written back to it. Cannot be used together with data parameter. **data: Initial cache data as a dictionary or SQLiteDict. Cannot be used together** with filename parameter. **immediate\_write: If True, new entries are immediately added to the main data store.** If False, they’re kept separate until explicitly written. method: Deprecated. Legacy parameter for backward compatibility. verbose: If True, prints diagnostic information about cache hits and misses. > **Raises:** **CacheError: If both filename and data are provided, or if the filename has an** invalid extension. > **Implementation Notes:** * The cache maintains separate dictionaries for tracking: \* data: The main persistent storage \* new\_entries: Entries added in this session \* fetched\_data: Entries fetched in this session \* new\_entries\_to\_write\_later: Entries to be written if immediate\_write=False * If loading from a file, the appropriate loader method is called based on extension > ### add\_from\_dict(*new\_data: dict\[str, 'CacheEntry']*, *write\_now: bool | None = True*) → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Add entries to the cache from a dictionary. **Parameters:** **write\_now** – Whether to write to the cache immediately (similar to immediate\_write). > ### add\_from\_jsonl(*filename: str*, *write\_now: bool | None = True*) → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Add entries to the cache from a JSONL. **Parameters:** **write\_now** – Whether to write to the cache immediately (similar to immediate\_write). > ### add\_from\_sqlite(*db\_path: str*, *write\_now: bool | None = True*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Add entries to the cache from an SQLite database. **Parameters:** **write\_now** – Whether to write to the cache immediately (similar to immediate\_write). > ### close()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Explicitly close and clean up resources. This method properly disposes of any SQLAlchemy engines and connections to prevent memory leaks. > ### code()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Generate Python code that recreates this object. This method must be implemented by all subclasses to provide a way to generate executable Python code that can recreate the object. **Returns:** str: Python code that, when executed, creates an equivalent object > ### data *= *[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) > ### *classmethod* example(*randomize: bool = False*) → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Create an example Cache instance for testing and demonstration. Creates a Cache object pre-populated with example CacheEntry objects. This method is useful for documentation, testing, and demonstration purposes. > **Args:** **randomize: If True, creates CacheEntry objects with randomized content** for uniqueness. If False, uses consistent example entries. > **Returns:** Cache: A new Cache object containing example CacheEntry objects > **Technical Notes:** * Uses CacheEntry.example() to create sample entries * When randomize=True, generates unique keys for each call * When randomize=False, produces consistent examples for doctests * Creates an in-memory cache (no persistent file) > **Examples:** ```python theme={null} >>> cache = Cache.example() >>> len(cache) > 0 True >>> from edsl.caching.cache_entry import CacheEntry >>> all(isinstance(entry, CacheEntry) for entry in cache.values()) True ``` ```python theme={null} >>> # Create examples with randomized content >>> cache1 = Cache.example(randomize=True) >>> cache2 = Cache.example(randomize=True) >>> # With randomization, keys should be different >>> len(cache1) > 0 and len(cache2) > 0 True ``` > ### fetch(\*\*\*, *model: str*, *parameters: dict*, *system\_prompt: str*, *user\_prompt: str*, *iteration: int*, *validated: bool = False*, *remote\_fetch: bool = False*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Retrieve a cached language model response if available. This method attempts to find a cached response matching the exact input parameters. The combination of model, parameters, prompts, and iteration creates a unique key that identifies a specific language model request. > **Args:** model: Language model identifier (e.g., “gpt-3.5-turbo”) parameters: Model configuration parameters (e.g., temperature, max\_tokens) system\_prompt: The system instructions given to the model user\_prompt: The user query/prompt given to the model iteration: The iteration number for this specific request > **Returns:** tuple: (response, key) where: * response: The cached model output as a string, or None if not found * key: The cache key string generated for this request > **Technical Notes:** * Uses CacheEntry.gen\_key() to generate a consistent hash-based key * Updates self.fetched\_data when a hit occurs to track cache usage * Optionally logs cache hit/miss when verbose=True * The response is returned as a JSON string for consistency * On local cache miss, attempts to fetch from remote universal cache > **Examples:** ```python theme={null} >>> c = Cache() >>> c.fetch(model="gpt-3", parameters="default", system_prompt="Hello", ... user_prompt="Hi", iteration=1)[0] is None True ``` > ### fetch\_input\_example() → dict[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Create an example input for a ‘fetch’ operation. > ### *classmethod* from\_dict(*data*) → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Construct a Cache from a dictionary. > ### *classmethod* from\_jsonl(*jsonlfile: str*, *db\_path: str | None = None*) → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Construct a Cache from a JSONL file. **Parameters:** * **jsonlfile** – The path to the JSONL file of cache entries. * **db\_path** – The path to the SQLite database used to store the cache. * If db\_path is None, the cache will be stored in memory, as a dictionary. * If db\_path is provided, the cache will be stored in an SQLite database. > ### *classmethod* from\_local\_cache() → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Construct a Cache from a local cache file. > ### *classmethod* from\_sqlite\_db(*db\_path: str*) → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Construct a Cache from a SQLite database. > ### *classmethod* from\_url(*db\_path=None*) → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Construct a Cache object from a remote. > ### items()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Return an iterator of (key, value) pairs in the cache. Similar to dict.items(), provides an iterator over all key-value pairs in the cache for easy iteration. **Returns:** zip: An iterator of (key, CacheEntry) tuples > ### keys()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Return a list of all cache keys. Retrieves all cache keys, which are the unique identifiers for each cache entry. > **Returns:** list: A list of string keys in the cache > **Examples:** ```python theme={null} >>> from edsl import Cache >>> Cache.example().keys() ['5513286eb6967abc0511211f0402587d'] ``` > ### new\_entries\_cache() → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Return a new Cache object with the new entries. > ### select(\**fields*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) > ### store(*model: str*, *parameters: str*, *system\_prompt: str*, *user\_prompt: str*, *response: dict*, *iteration: int*, *service: str*, *validated: bool = False*) → str[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Store a new language model response in the cache. Creates a new CacheEntry from the provided parameters and response, then adds it to the cache using a deterministic key derived from the input parameters. > **Args:** model: Language model identifier (e.g., “gpt-3.5-turbo”) parameters: Model configuration parameters (e.g., temperature, max\_tokens) system\_prompt: The system instructions given to the model user\_prompt: The user query/prompt given to the model response: The model’s response as a dictionary iteration: The iteration number for this specific request service: The service provider (e.g., “openai”, “anthropic”) validated: Whether the response has been validated (default: False) > **Returns:** str: The cache key generated for this entry > **Technical Notes:** * Creates a new CacheEntry object to encapsulate the response and metadata * Adds the entry to self.new\_entries to track entries added in this session * Adds the entry to the main data store if immediate\_write=True * Otherwise, stores in new\_entries\_to\_write\_later for deferred writing * The response is stored as a JSON string for consistency and compatibility > **Storage Behavior:** The method’s behavior depends on the immediate\_write setting: - If True: Immediately writes to the main data store (self.data) - If False: Stores in a separate dict for writing later (e.g., at context exit) > **Examples:** ```python theme={null} >>> from edsl import Cache, Model, Question >>> m = Model("test") >>> c = Cache() >>> len(c) 0 >>> results = Question.example("free_text").by(m).run(cache=c, ... disable_remote_cache=True, disable_remote_inference=True) >>> len(c) 1 ``` > ### subset(*keys: list\[str]*) → [Cache](/en/latest/data#cache-class)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Return a subset of the Cache with the specified keys. > ### table(\**fields*, *tablefmt: str | None = None*, *pretty\_labels: dict | None = None*) → str[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) > ### to\_dataset()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Convert this object to a Dataset for advanced data operations. **Returns:** Dataset: A Dataset object containing this object’s data > ### to\_dict(*add\_edsl\_version=True*) → dict[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Serialize the cache to a dictionary for storage or transmission. Converts the Cache object into a plain dictionary format that can be easily serialized to JSON or other formats. Each CacheEntry is also converted to a dictionary using its to\_dict method. > **Args:** **add\_edsl\_version: If True, includes the EDSL version and class name** in the serialized output for compatibility tracking > **Returns:** > **dict: A dictionary representation of the cache with the structure:** ```python theme={null} { “key1”: {cache_entry1_dict}, “key2”: {cache_entry2_dict}, … “edsl_version”: “x.x.x”, # if add_edsl_version=True “edsl_class_name”: “Cache” # if add_edsl_version=True } ``` > **Technical Notes:** * Used by from\_dict for deserialization * Used by **hash** for cache comparison * The version info allows for proper handling of format changes > ### to\_html()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) > ### to\_scenario\_list()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) > ### tree(*node\_list: list\[str] | None = None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) > ### values()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Return a list of all cache entry values. Retrieves all CacheEntry objects stored in the cache. > **Returns:** list: A list of CacheEntry objects > **Examples:** ```python theme={null} >>> from edsl import Cache >>> entries = Cache.example().values() >>> len(entries) 1 >>> entries[0] CacheEntry(model='gpt-3.5-turbo', parameters={'temperature': 0.5}, ...) ``` > ### view() → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) View the Cache in a new browser tab. > ### write(*filename: str | None = None*) → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Write the cache to a file at the specified location. > ### write\_jsonl(*filename: str*) → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Write the cache to a JSONL file. > ### write\_sqlite\_db(*db\_path: str*) → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/caching.py) Write the cache to an SQLite database. # Dataset Source: https://docs.expectedparrot.com/en/latest/dataset The `Dataset` class is a versatile data container for tabular data with powerful manipulation capabilities. It represents data in a column-oriented format, providing methods for analysis, transformation, and visualization.The `Dataset` class is a versatile data container for tabular data with powerful manipulation capabilities. It represents data in a column-oriented format, providing methods for analysis, transformation, and visualization. ## Overview Dataset is a fundamental data structure in EDSL that provides a column-oriented representation of tabular data. It offers methods for manipulating, analyzing, visualizing, and exporting data, similar to tools like pandas or dplyr. Key features: 1. Flexible data manipulation (filtering, sorting, transformation) 2. Visualization capabilities with multiple rendering options 3. Export to various formats (CSV, Excel, Pandas, etc.) 4. Integration with other EDSL components ## Creating Datasets Datasets can be created from various sources: From dictionaries: ```python theme={null} from edsl import Dataset # Create a dataset with two columns d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) ``` From existing EDSL objects: ```python theme={null} # From Results object dataset = results.select('how _feeling', 'agent.status') # From pandas DataFrame import pandas as pd df = pd.DataFrame({'a': [1, 2, 3 ], 'b': [4, 5, 6 ]}) dataset = Dataset.from _pandas _dataframe(df) ``` ## Displaying and Visualizing Data The Dataset class provides multiple ways to display and visualize data: Basic display: ```python theme={null} # Print the dataset dataset.print() # Display only the first few rows dataset.head() # Display only the last few rows dataset.tail() ``` ## Table Display Options You can control the table formatting using the `tablefmt` parameter: ```python theme={null} # Display as an ASCII grid dataset.table(tablefmt ="grid") # Display as pipe-separated values dataset.table(tablefmt ="pipe") # Display as HTML dataset.table(tablefmt ="html") # Display as Markdown dataset.table(tablefmt ="github") # Display as LaTeX dataset.table(tablefmt ="latex") ``` ### Rich Terminal Output The Dataset class supports displaying tables with enhanced formatting using the Rich library, which provides beautiful terminal formatting with colors, styles, and more: ```python theme={null} # Display using Rich formatting dataset.table(tablefmt ="rich") # Alternative syntax dataset.print(format ="rich") ``` This creates a nicely formatted table in the terminal with automatically sized columns, bold headers, and grid lines. Example: ```python theme={null} from edsl import Dataset # Create a dataset d = Dataset( [ {'name': ['Alice', 'Bob', 'Charlie', 'David' ]}, {'age': [25, 32, 45, 19 ]}, {'city': ['New York', 'Los Angeles', 'Chicago', 'Boston' ]} ]) # Display with rich formatting d.table(tablefmt ="rich") ``` ## Data Manipulation The Dataset class provides numerous methods for data manipulation: Filtering: ```python theme={null} # Filter results for a specific condition filtered = dataset.filter("how _feeling == 'Great'") ``` Creating new columns: ```python theme={null} # Create a new column with _sentiment = dataset.mutate("sentiment = 1 if how _feeling == 'Great' else 0") ``` Sorting: ```python theme={null} # Sort by a specific column sorted _data = dataset.order _by("age", reverse =True) ``` Reshaping: ```python theme={null} # Convert to long format long _data = dataset.long() # Convert back to wide format wide _data = long _data.wide() ``` ## Exporting Data Export to various formats: ```python theme={null} # Export to CSV dataset.to _csv("data.csv") # Export to pandas DataFrame df = dataset.to _pandas() # Export to Word document dataset.to _docx("data.docx", title ="My Dataset") ``` ## Dataset Methods > ### *class* edsl.dataset.Dataset(*data: list \[dict \[str, Any ] ] = None*, *print \_parameters: dict | None = None*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Bases: `UserList`, `DatasetOperationsMixin`, `PersistenceMixin`, `HashingMixin` A versatile data container for tabular data with powerful manipulation capabilities. The Dataset class is a fundamental data structure in EDSL that represents tabular data in a column-oriented format. It provides a rich set of methods for data manipulation, transformation, analysis, visualization, and export through the DatasetOperationsMixin. Key features: 1. Column-oriented data structure optimized for LLM experiment results 2. Rich data manipulation API similar to dplyr/pandas (filter, select, mutate, etc.) 3. Visualization capabilities including tables, plots, and reports 4. Export to various formats (CSV, Excel, SQLite, pandas, etc.) 5. Serialization for storage and transport 6. Tree-based data exploration A Dataset typically contains multiple columns, each represented as a dictionary with a single key-value pair. The key is the column name and the value is a list of values for that column. All columns must have the same length. The Dataset class inherits from: - UserList: Provides list-like behavior for storing column data - DatasetOperationsMixin: Provides data manipulation methods - PersistenceMixin: Provides serialization capabilities - HashingMixin: Provides hashing functionality for comparison and storage Datasets are typically created by transforming other EDSL container types like Results, AgentList, or ScenarioList, but can also be created directly from data. > #### collapse(*field: str*, *separator: str | None = None*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Collapse multiple values in a field into a single value using a separator. > **Args:** field: The name of the field to collapse. separator: Optional string to use as a separator between values. > Defaults to a space if not specified. > **Examples:** ```python theme={null} >>> d = Dataset( [{'words': [ ['hello', 'world' ], ['good', 'morning' ] ]} ]) >>> d.collapse('words').data [{'words': [ [ ['hello', 'world' ], ['good', 'morning' ] ] ]} ] ``` ```python theme={null} >>> d = Dataset( [{'numbers': [1, 2, 3 ]} ]) >>> d.collapse('numbers', separator =',').data [{'numbers': ['1,2,3' ]} ] ``` > #### drop(*field \_name*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Returns a new Dataset with the specified field removed. > **Args:** field \_name (str): The name of the field to remove. > **Returns:** Dataset: A new Dataset instance without the specified field. > **Raises:** KeyError: If the field \_name doesn’t exist in the dataset. > **Examples:** ```python theme={null} >>> from .dataset import Dataset >>> d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) >>> d.drop('a') Dataset( [{'b': [4, 5, 6 ]} ]) ``` ```python theme={null} >>> # Testing drop with nonexistent field raises DatasetKeyError - tested in unit tests ``` > #### *classmethod* example(*n: int = None*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return an example dataset. > **Examples:** ```python theme={null} >>> Dataset.example() Dataset( [{'a': [1, 2, 3, 4 ]}, {'b': [4, 3, 2, 1 ]} ]) ``` ```python theme={null} >>> Dataset.example(n =2) Dataset( [{'a': [1, 1 ]}, {'b': [2, 2 ]} ]) ``` > #### expand(*field: str*, *number \_field: bool = False*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Expand a field containing lists into multiple rows. > **Args:** field: The field containing lists to expand number \_field: If True, adds a number field indicating the position in the original list > **Returns:** A new Dataset with the expanded rows > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [ [1, 2, 3 ], [4, 5, 6 ] ]}, {'b': ['x', 'y' ]} ]) >>> d.expand('a').data [{'a': [1, 2, 3, 4, 5, 6 ]}, {'b': ['x', 'x', 'x', 'y', 'y', 'y' ]} ] ``` ```python theme={null} >>> d = Dataset( [{'items': [ ['apple', 'banana' ], ['orange' ] ]}, {'id': [1, 2 ]} ]) >>> d.expand('items', number _field =True).data [{'items': ['apple', 'banana', 'orange' ]}, {'id': [1, 1, 2 ]}, {'items _number': [1, 2, 1 ]} ] ``` > #### expand \_field(*field*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Expand a field in the dataset. Renamed to avoid conflict with the expand method defined earlier. > #### filter(*expression*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Filter the dataset based on a boolean expression. > **Args:** expression: A string expression that evaluates to a boolean value. Can reference column names in the dataset. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3, 4 ]}, {'b': [5, 6, 7, 8 ]} ]) >>> d.filter('a > 2').data [{'a': [3, 4 ]}, {'b': [7, 8 ]} ] ``` ```python theme={null} >>> d = Dataset( [{'x': ['a', 'b', 'c' ]}, {'y': [1, 2, 3 ]} ]) >>> d.filter('y < 3').data [{'x': ['a', 'b' ]}, {'y': [1, 2 ]} ] ``` > #### first() → dict \[str, Any ] [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Get the first value of the first key in the first dictionary. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3, 4 ]}, {'b': [5, 6, 7, 8 ]} ]) >>> d.first() 1 ``` ```python theme={null} >>> d = Dataset( [{'x': ['first', 'second' ]} ]) >>> d.first() 'first' ``` > #### *classmethod* from \_dict(*data: dict*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Convert a dictionary to a dataset. > **Examples:** ```python theme={null} >>> d = Dataset.from _dict({'data': [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]}) >>> isinstance(d, Dataset) True >>> d.data [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ] ``` ```python theme={null} >>> d = Dataset.from _dict({'data': [{'x': ['a', 'b' ]} ]}) >>> d.data [{'x': ['a', 'b' ]} ] ``` > #### *classmethod* from \_edsl \_object(*object*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) > #### *classmethod* from \_pandas \_dataframe(*df*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) > #### get \_sort \_indices(*lst: list \[Any ]*, *reverse: bool = False*) → list \[int ] [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return the indices that would sort the list, using either numpy or pure Python. None values are placed at the end of the sorted list. > **Args:** lst: The list to be sorted reverse: Whether to sort in descending order use \_numpy: Whether to use numpy implementation (falls back to pure Python if numpy is unavailable) > **Returns:** A list of indices that would sort the list > ### head(*n: int = 5*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return the first n observations in the dataset. ```python theme={null} >>> d = Dataset( [{'a.b': [1,2,3,4 ]} ]) >>> d.head(2) Dataset( [{'a.b': [1, 2 ]} ]) ``` > #### keys() → list \[str ] [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return the keys of the dataset. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3, 4 ]}, {'b': [5, 6, 7, 8 ]} ]) >>> d.keys() ['a', 'b' ] ``` ```python theme={null} >>> d = Dataset( [{'x.y': [1, 2 ]}, {'z.w': [3, 4 ]} ]) >>> d.keys() ['x.y', 'z.w' ] ``` > #### latex(\* \* *kwargs*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a LaTeX representation of the dataset. > **Args:** [ \* \*](/en/latest/#id2)kwargs: Additional arguments to pass to the table formatter. > #### long(*exclude \_fields: list \[str ] = None*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Convert the dataset from wide to long format. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2 ], 'b': [3, 4 ]} ]) >>> d.long().data [{'row': [0, 0, 1, 1 ]}, {'key': ['a', 'b', 'a', 'b' ]}, {'value': [1, 3, 2, 4 ]} ] ``` ```python theme={null} >>> d = Dataset( [{'x': [1, 2 ], 'y': [3, 4 ], 'z': [5, 6 ]} ]) >>> d.long(exclude _fields = ['z' ]).data [{'row': [0, 0, 1, 1 ]}, {'key': ['x', 'y', 'x', 'y' ]}, {'value': [1, 3, 2, 4 ]}, {'z': [5, 5, 6, 6 ]} ] ``` > #### merge(*other: [Dataset](/en/latest/dataset#dataset-methods)*, *by \_x*, *by \_y*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Merge the dataset with another dataset on the given keys. > **Examples:** ```python theme={null} >>> d1 = Dataset( [{'key': [1, 2, 3 ]}, {'value1': ['a', 'b', 'c' ]} ]) >>> d2 = Dataset( [{'key': [2, 3, 4 ]}, {'value2': ['x', 'y', 'z' ]} ]) >>> merged = d1.merge(d2, 'key', 'key') >>> len(merged.data [0 ] ['key' ]) 3 ``` ```python theme={null} >>> d1 = Dataset( [{'id': [1, 2 ]}, {'name': ['Alice', 'Bob' ]} ]) >>> d2 = Dataset( [{'id': [2, 3 ]}, {'age': [25, 30 ]} ]) >>> merged = d1.merge(d2, 'id', 'id') >>> len(merged.data [0 ] ['id' ]) 2 ``` > #### order \_by(*sort \_key: str*, *reverse: bool = False*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a new dataset with the observations sorted by the given key. > **Args:** sort \_key: The key to sort the observations by reverse: Whether to sort in reverse order > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [3, 1, 4, 1, 5 ]}, {'b': ['x', 'y', 'z', 'w', 'v' ]} ]) >>> sorted _d = d.order _by('a') >>> sorted _d.data [{'a': [1, 1, 3, 4, 5 ]}, {'b': ['y', 'w', 'x', 'z', 'v' ]} ] ``` ```python theme={null} >>> d = Dataset( [{'a': [3, 1, 4, 1, 5 ]}, {'b': ['x', 'y', 'z', 'w', 'v' ]} ]) >>> sorted _d = d.order _by('a', reverse =True) >>> sorted _d.data [{'a': [5, 4, 3, 1, 1 ]}, {'b': ['v', 'z', 'x', 'y', 'w' ]} ] ``` ```python theme={null} >>> d = Dataset( [{'a': [3, None, 1, 4, None ]}, {'b': ['x', 'y', 'z', 'w', 'v' ]} ]) >>> sorted _d = d.order _by('a') >>> sorted _d.data [{'a': [1, 3, 4, None, None ]}, {'b': ['z', 'x', 'w', 'y', 'v' ]} ] ``` > #### print(*pretty \_labels =None*, \* \* *kwargs*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Print the dataset in a formatted way. > **Args:** pretty \_labels: A dictionary mapping column names to their display names [ \* \*](/en/latest/#id4)kwargs: Additional arguments format: The output format (“html”, “markdown”, “rich”, “latex”) > **Returns:** TableDisplay object > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) >>> display = d.print(format ='rich') >>> display is not None True ``` ```python theme={null} >>> d = Dataset( [{'long _column _name': [1, 2 ]} ]) >>> display = d.print(pretty _labels ={'long _column _name': 'Short'}) >>> display is not None True ``` > #### remove \_prefix() → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Remove the prefix from column names that contain dots. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a.b': [1, 2, 3 ]}, {'c.d': [4, 5, 6 ]} ]) >>> d.remove _prefix().data [{'b': [1, 2, 3 ]}, {'d': [4, 5, 6 ]} ] ``` ```python theme={null} >>> d = Dataset( [{'x.y.z': [1, 2 ]}, {'a.b.c': [3, 4 ]} ]) >>> d.remove _prefix().data [{'y': [1, 2 ]}, {'b': [3, 4 ]} ] ``` > #### rename(*rename \_dic*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Rename columns in the dataset according to the provided dictionary. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) >>> d.rename({'a': 'x', 'b': 'y'}).data [{'x': [1, 2, 3 ]}, {'y': [4, 5, 6 ]} ] ``` ```python theme={null} >>> d = Dataset( [{'old _name': [1, 2 ]} ]) >>> d.rename({'old _name': 'new _name'}).data [{'new _name': [1, 2 ]} ] ``` > #### sample(*n: int = None*, *frac: float = None*, *with \_replacement: bool = True*, *seed: str | int | float = None*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a new dataset with a sample of the observations. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3, 4, 5 ]}, {'b': [6, 7, 8, 9, 10 ]} ]) >>> sampled = d.sample(n =3, seed =42) >>> len(sampled.data [0 ] ['a' ]) 3 ``` ```python theme={null} >>> d = Dataset( [{'x': ['a', 'b', 'c', 'd' ]} ]) >>> sampled = d.sample(frac =0.5, seed =123) >>> len(sampled.data [0 ] ['x' ]) 2 ``` > #### select(\* *keys*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a new dataset with only the selected keys. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3, 4 ]}, {'b': [5, 6, 7, 8 ]}, {'c': [9, 10, 11, 12 ]} ]) >>> d.select('a', 'c').data [{'a': [1, 2, 3, 4 ]}, {'c': [9, 10, 11, 12 ]} ] ``` ```python theme={null} >>> d = Dataset( [{'x': [1, 2 ]}, {'y': [3, 4 ]} ]) >>> d.select('x').data [{'x': [1, 2 ]} ] ``` > #### shuffle(*seed =None*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a new dataset with the observations shuffled. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3, 4 ]}, {'b': [5, 6, 7, 8 ]} ]) >>> shuffled = d.shuffle(seed =42) >>> len(shuffled.data [0 ] ['a' ]) == len(d.data [0 ] ['a' ]) True ``` ```python theme={null} >>> d = Dataset( [{'x': ['a', 'b', 'c' ]} ]) >>> shuffled = d.shuffle(seed =123) >>> set(shuffled.data [0 ] ['x' ]) == set(d.data [0 ] ['x' ]) True ``` > #### summary() → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a summary of the dataset. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) >>> d.summary().data [{'num _observations': [3 ]}, {'keys': [ ['a', 'b' ] ]} ] ``` > #### table(\* *fields*, *tablefmt: str | None = 'rich'*, *max \_rows: int | None = None*, *pretty \_labels =None*, *print \_parameters: dict | None = None*) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) > #### tail(*n: int = 5*) → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return the last n observations in the dataset. ```python theme={null} >>> d = Dataset( [{'a.b': [1,2,3,4 ]} ]) >>> d.tail(2) Dataset( [{'a.b': [3, 4 ]} ]) ``` > #### to(*survey \_or \_question: 'Survey' | 'QuestionBase'*) → Job [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Transform the dataset using a survey or question. > **Args:** survey \_or \_question: Either a Survey or QuestionBase object to apply to the dataset. > **Examples:** ```python theme={null} >>> from edsl import QuestionFreeText >>> from edsl.jobs import Jobs >>> d = Dataset([{'name': ['Alice', 'Bob']}]) >>> q = QuestionFreeText(question_text="How are you, {{ name }}?", question_name="how_feeling") >>> job = d.to(q) >>> isinstance(job, Jobs) True ``` > #### to \_dict() → dict [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Convert the dataset to a dictionary. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) >>> d.to _dict() {'data': [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]} ``` ```python theme={null} >>> d = Dataset( [{'x': ['a', 'b' ]} ]) >>> d.to _dict() {'data': [{'x': ['a', 'b' ]} ]} ``` > #### to \_docx(*output \_file: str*, *title: str = None*) → None [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Convert the dataset to a Word document. > **Args:** output \_file (str): Path to save the Word document title (str, optional): Title for the document > **Examples:** ```python theme={null} >>> import tempfile >>> d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) >>> with tempfile.NamedTemporaryFile(suffix ='.docx') as tmp: ... d.to _docx(tmp.name, title ='Test Document') ... import os ... os.path.exists(tmp.name) True ``` > #### to \_json() [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a JSON representation of the dataset. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ]) >>> d.to _json() [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ] ``` ```python theme={null} >>> d = Dataset( [{'x': ['a', 'b' ]} ]) >>> d.to _json() [{'x': ['a', 'b' ]} ] ``` > #### tree(*node \_order: list \[str ] | None = None*) → Tree [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Return a tree representation of the dataset. ```python theme={null} >>> d = Dataset( [{'a': [1,2,3,4 ]}, {'b': [4,3,2,1 ]} ]) >>> d.tree() Tree(Dataset({'a': [1, 2, 3, 4 ], 'b': [4, 3, 2, 1 ]}), node _order= ['a', 'b' ]) ``` > #### unique() → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Remove duplicate rows from the dataset. > **Returns:** A new Dataset with duplicate rows removed. > **Examples:** ```python theme={null} >>> d = Dataset( [{'a': [1, 2, 3, 1 ]}, {'b': [4, 5, 6, 4 ]} ]) >>> d.unique().data [{'a': [1, 2, 3 ]}, {'b': [4, 5, 6 ]} ] >>> d = Dataset( [{'x': ['a', 'b', 'a' ]}, {'y': [1, 2, 1 ]} ]) >>> d.unique().data [{'x': ['a', 'b' ]}, {'y': [1, 2 ]} ] ``` ```python theme={null} >>> # Dataset with a single column >>> Dataset( [{'value': [1, 2, 3, 2, 1, 3 ]} ]).unique().data [{'value': [1, 2, 3 ]} ] ``` > #### wide() → [Dataset](/en/latest/dataset#dataset-methods) [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Convert a long-format dataset (with row, key, value columns) to wide format. > **Examples:** ```python theme={null} >>> d = Dataset( [{'row': [0, 0, 1, 1 ]}, {'key': ['a', 'b', 'a', 'b' ]}, {'value': [1, 3, 2, 4 ]} ]) >>> d.wide().data [{'a': [1, 2 ]}, {'b': [3, 4 ]} ] ``` ```python theme={null} >>> d = Dataset( [{'row': [0, 0, 1, 1 ]}, {'key': ['x', 'y', 'x', 'y' ]}, {'value': [1, 3, 2, 4 ]}, {'z': [5, 5, 6, 6 ]} ]) >>> d.wide().data [{'x': [1, 2 ]}, {'y': [3, 4 ]}, {'z': [5, 6 ]} ] ``` > #### write(*filename: str*, *tablefmt: str | None = None*) → None [ \[source \]](https://github.com/expectedparrot/edsl/blob/main/edsl/dataset.py) Write the dataset to a file in the specified format. > **Args:** filename: The name of the file to write to. tablefmt: Optional format for the table (e.g., ‘csv’, ‘html’, ‘latex’). # Enums Source: https://docs.expectedparrot.com/en/latest/enums Enums module wrapper for backward compatibility. This module re-exports everything from the edsl.base.enums module. > #### *class* edsl.enums.EnumWithChecks(*value*, *names=None*, \*\*\*, *module=None*, *qualname=None*, *type=None*, *start=1*, *boundary=None*) [source](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) Bases: `Enum` Base class for all enums with checks. > #### **module** *= 'edsl.base.enums'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### *classmethod* is\_value\_valid(*value*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) Check if the value is valid. > #### *class* edsl.enums.InferenceServiceType(*value*, *names=None*, \*\*\*, *module=None*, *qualname=None*, *type=None*, *start=1*, *boundary=None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) Bases: [`EnumWithChecks`](/en/latest "edsl.base.enums.EnumWithChecks") Enum for the inference service types. > #### ANTHROPIC *= 'anthropic'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### AZURE *= 'azure'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### BEDROCK *= 'bedrock'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### DEEPSEEK *= 'deepseek'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### DEEP\_INFRA *= 'deep\_infra'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### GOOGLE *= 'google'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### GROQ *= 'groq'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### MISTRAL *= 'mistral'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### OLLAMA *= 'ollama'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### OPENAI *= 'openai'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### OPENAI\_V2 *= 'openai\_v2'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### OPEN\_ROUTER *= 'open\_router'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### PERPLEXITY *= 'perplexity'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### REPLICATE *= 'replicate'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### TEST *= 'test'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### TOGETHER *= 'together'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### XAI *= 'xai'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **module** *= 'edsl.base.enums'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **new**(*value*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### *class* edsl.enums.QuestionType(*value*, *names=None*, \*\*\*, *module=None*, *qualname=None*, *type=None*, *start=1*, *boundary=None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) Bases: [`EnumWithChecks`](/en/latest "edsl.base.enums.EnumWithChecks") Enum for the question types. > #### BUDGET *= 'budget'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### CHECKBOX *= 'checkbox'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### EXTRACT *= 'extract'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### FREE\_TEXT *= 'free\_text'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### FUNCTIONAL *= 'functional'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### LIKERT\_FIVE *= 'likert\_five'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### LINEAR\_SCALE *= 'linear\_scale'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### LIST *= 'list'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### MULTIPLE\_CHOICE *= 'multiple\_choice'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### NUMERICAL *= 'numerical'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### RANK *= 'rank'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### TOP\_K *= 'top\_k'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### YES\_NO *= 'yes\_no'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **module** *= 'edsl.base.enums'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **new**(*value*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### *class* edsl.enums.TokenPricing(\*\*\*, *model\_name*, *prompt\_token\_price\_per\_k: float*, *completion\_token\_price\_per\_k: float*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) Bases: `object` > #### **dict** *= mappingproxy(\{'**module**': 'edsl.base.enums', '**init**': \, '**eq**': \, 'example': \)>, '**dict**': \, '**weakref**': \, '**doc**': None, '**hash**': None, '**annotations**': })*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **eq**(*other*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) Return self==value. > #### **hash** *= None*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **init**(\*\*\*, *model\_name*, *prompt\_token\_price\_per\_k: float*, *completion\_token\_price\_per\_k: float*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **module** *= 'edsl.base.enums'*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) > #### **weakref**[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) list of weak references to the object > #### *classmethod* example() → [TokenPricing](/en/latest/#edsl.enums.TokenPricing "edsl.base.enums.TokenPricing")[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) Return an example TokenPricing object. > #### edsl.enums.get\_token\_pricing(*model\_name*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/enums.py) # Exceptions & Debugging Source: https://docs.expectedparrot.com/en/latest/exceptions An exception is an error that occurs during the execution of a question or survey. When an exception is raised, EDSL will display a message about the error and an interactive report with more details in a new browser tab. ## Help debugging If you would like help debugging an error that you are encountering, please feel free to share your code, objects and exceptions report with us. An easy way to do this is to post a notebook with your code to the coop and **share the link with us at [info@expectedparrot.com](mailto:info@expectedparrot.com)**. You can use the following code to generate a link to your notebook: ```python theme={null} from edsl import notebook n = Notebook(path="path/to/your/notebook.ipynb") n.push(description="Notebook with code that raises an exception", visibility="private") ``` ## Common exceptions ### Answer validation errors A number of exceptions may indicate that there is a problem with the way that a question has been constructed or answered. For example, you may intend for the answer to be formatted as a list but receive a string instead. Or a question may be unanswered and the model has returned None. These exceptions are typically raised by the Question class and are subclassed from QuestionAnswerValidationError. A useful starting point for debugging these exceptions is to check the Settings class for the Questions model ([https://github.com/expectedparrot/edsl/blob/main/edsl/questions/settings.py](https://github.com/expectedparrot/edsl/blob/main/edsl/questions/settings.py)). The default settings (which can be modified) are as follows: ```text theme={null} MAX_ANSWER_LENGTH = 2000 MAX_EXPRESSION_CONSTRAINT_LENGTH = 1000 MAX_NUM_OPTIONS = 200 MIN_NUM_OPTIONS = 1 MAX_OPTION_LENGTH = 10000 MAX_QUESTION_LENGTH = 100000 ``` ### Missing API key You will receive a MissingAPIKeyError exception if you try to run a question and have not activated [Remote Inference](/en/latest/remote_inference) or stored an API key for the model that you are trying to use. ### Problem with inference provider Some exceptions may indicate that there is a problem with your account with an inference provider, such as insufficient credits. ### API Timeout The maximum number of seconds to wait for an API call to return can be specified in config.py: ```python theme={null} CONFIG_MAP = { ... "EDSL_API_TIMEOUT": { "default": "60", "info": "This env var determines the maximum number of seconds to wait for an API call to return.", }, ... ``` ### Missing packages A ModuleNotFoundError exception will be raised if a required package is not installed. This is more likely to occur when cloning the repository instead of installing the package using pip install edsl. It can typically be remedied by reinstalling your virtual environment or installing the missing package using pip install \. ## Strategies for dealing with exceptions ### Re-try the question If an exception is raised, the question will be re-tried up to a maximum number of attempts. The number of retries can be specified in config.py: ```python theme={null} CONFIG_MAP = { ... "EDSL_MAX_ATTEMPTS": { "default": "5", "info": "This env var determines the maximum number of times to retry a failed API call.", }, ... ``` By default, your successful results are cached, so re-executing a survey will only re-run the questions that were not answered successfully in the previous run. Learn more about working with data. ### Search for the exception message in the documentation The name of the exception that is raised will often provide a clue as to what the problem is. You can search for the exception type in the search bar at the top of the main documentation page to find more information about the exception and how to resolve it. # File Store Source: https://docs.expectedparrot.com/en/latest/filestore *FileStore* is a module for storing and sharing data files at Expected Parrot. It allows you to post and retrieve files of various types to use in EDSL surveys, such as survey data, PDFs, CSVs, docs or images. It can also be used to create Scenario objects for questions or traits for Agent objects from data files stored on the platform. When posting files, the FileStore module will automatically infer the file type from the extension. You can give a file a description and an alias, and set its visibility (public, private or unlisted). **Note**: Scenarios created from FileStore objects cannot be used with question memory rules, and can only be added to questions with the by() method, not the loop() method. This is because the memory rules and loop() method insert the filepath in the question, whereas the by() method inserts the file content when the question is run. See details on these methods at the [Scenarios](/en/latest/scenarios) section of the documentation. The examples below are also available in a [notebook at Expected Parrot](https://www.expectedparrot.com/content/RobinHorton/my-example-filestore-notebook). ## File types The following file types are currently supported by the FileStore module: * docx (Word document) * csv (Comma-separated values) * html (HyperText Markup Language) * json (JavaScript Object Notation) * latex (LaTeX) * md (Markdown) * pdf (Portable Document Format) * png (image) * pptx (PowerPoint) * py (Python) * sql (SQL database) * sqlite (SQLite database) * txt (text) ## Posting a file 1. Import the FileStore constructor and create an object by passing the path to the file. The constructor will automatically infer the file type from the extension. For example: ```python theme={null} from edsl import FileStore fs = FileStore("my_data.csv") # replace with your own file ``` 2. Call the push method to post the file at Expected Parrot. You can optionally pass the following parameters: * description: a string description for the file * alias: a convenient Pythonic reference name for the URL for the object, e.g., my\_example\_csv * visibility: either *public*, *private* or *unlisted* (the default is *unlisted*) ```python theme={null} fs.push(description = "My example CSV file", alias = "my-example-csv-file", visibility = "public") ``` The push method returns a dictionary with the following keys and values (this is the same for any object posted to Expected Parrot): * description: the description you provided, if any * object\_type: the type of object (e.g., scenario, survey, results, agent, notebook; objects posted with FileStore are always scenarios) * url: the URL of the file at Expected Parrot * uuid: the UUID of the file at Expected Parrot * version: the version of the file * visibility: the visibility of the file (*public*, *private* or *unlisted* by default) Example output: ```json theme={null} {'description': 'My example CSV file', 'object_type': 'scenario', 'url': 'https://www.expectedparrot.com/content/17c0e3d3-8d08-4ae0-bc7d-384a56a07e4e', 'uuid': '17c0e3d3-8d08-4ae0-bc7d-384a56a07e4e', 'version': '0.1.47.dev1', 'visibility': 'public'} ``` ## Retrieving a file To retrieve a file, call the pull method on the FileStore constructor and pass it the alias or UUID of the file that you want to retrieve. For the example above, we can retrieve the file with: ```python theme={null} fs = FileStore.pull("https://www.expectedparrot.com/content/RobinHorton/my-example-csv-file") ``` This is equivalent: ```python theme={null} fs = FileStore.pull(csv_info["uuid"]) ``` Once retrieved, a file can be converted into scenarios. To construct a single scenario from a file, use the Scenario constructor and pass the file as a value for a specified key (see image file example below). To construct a list of scenarios from a file, call the from\_csv or from\_pdf method of the ScenarioList constructor and pass the file as an argument (see CSV and PDF examples below). To construct a list of scenarios from multiple files in a directory, you can use the ScenarioList.from\_directory() method, which will wrap each file in a Scenario with a specified key (default is “content”). We can also create agents by calling the from\_csv() method on an AgentList object. ### CSV example Here we create an example CSV and then post it to Expected Parrot using FileStore and retrieve it. Then we use the retrieved file to construct scenarios for questions (you can skip the step to create a CSV and replace with your own file). To create an example CSV file: ```python theme={null} # Sample data data = [ ['Age', 'City', 'Occupation'], [25, 'New York', 'Software Engineer'], [30, 'San Francisco', 'Teacher'], [35, 'Chicago', 'Doctor'], [28, 'Boston', 'Data Scientist'], [45, 'Seattle', 'Architect'] ] # Writing to CSV file with open('data.csv', 'w') as file: for row in data: line = ','.join(str(item) for item in row) file.write(line + 'n') ``` Here we post the file to Expected Parrot and inspect the details: ```python theme={null} from edsl import FileStore fs = FileStore("data.csv") csv_info = fs.push(description = "My example CSV file", alias = "my-example-csv-file", visibility = "public") csv_info # display the URL and uuid of the stored file for retrieving it later ``` Example output: ```json theme={null} {'description': 'My example CSV file', 'object_type': 'scenario', 'url': 'https://www.expectedparrot.com/content/17c0e3d3-8d08-4ae0-bc7d-384a56a07e4e', 'uuid': '17c0e3d3-8d08-4ae0-bc7d-384a56a07e4e', 'version': '0.1.47.dev1', 'visibility': 'public'} ``` Now we can retrieve the file and create scenarios from it: ```python theme={null} fs = FileStore.pull("https://www.expectedparrot.com/content/RobinHorton/my-example-csv-file") # or equivalently fs = FileStore.pull(csv_info["uuid"]) ``` Here we create a ScenarioList object from the CSV file: ```python theme={null} from edsl import ScenarioList scenarios = ScenarioList.from_csv(fs.to_tempfile()) ``` To inspect the scenarios: ```python theme={null} scenarios # display the scenarios ``` Output: | Age | City | Occupation | | :- | :- | :- | | 25 | New York | Software Engineer | | 30 | San Francisco | Teacher | | 35 | Chicago | Doctor | | 28 | Boston | Data Scientist | | 45 | Seattle | Architect | Alternatively, we can create agents from the CSV file: ```python theme={null} from edsl import AgentList agents = AgentList.from_csv(fs.to_tempfile()) ``` Learn more about designing agents and using scenarios in the [Agents](/en/latest/agents) and [Scenarios](/en/latest/scenarios) sections. ### PNG example Here we post and retrieve an image file, and then create a scenario for it. Note that we need to specify the scenario key for the file when we create the scenario. We also need to ensure that we have specified a vision model when using it with a survey (e.g., *gpt-4o*). To post the file: ```python theme={null} from edsl import FileStore fs = FileStore("parrot_logo.png") # replace with your own file png_info = fs.push(description = "My example PNG file", alias = "my-example-png-file", visibility = "public") png_info # display the URL and uuid of the stored file for retrieving it later ``` Example output: ```json theme={null} {'description': 'My example PNG file', 'object_type': 'scenario', 'url': 'https://www.expectedparrot.com/content/b261660e-11a3-4bec-8864-0b6ec76dfbee', 'uuid': 'b261660e-11a3-4bec-8864-0b6ec76dfbee', 'version': '0.1.47.dev1', 'visibility': 'public'} ``` Here we retrieve the file and then create a Scenario object for it with a key for the placeholder in the questions where we want to use the image: ```python theme={null} from edsl import FileStore fs = FileStore.pull("https://www.expectedparrot.com/content/RobinHorton/my-example-png-file") # or equivalently fs = FileStore.pull(png_info["uuid"]) ``` Here we create a Scenario object from the image file: ```python theme={null} from edsl import Scenario image_scenario = Scenario({"parrot_logo": fs}) # specify the key for the image ``` We can verify the key for the scenario object: ```python theme={null} image_scenario.keys() ``` Output: ```python theme={null} ['parrot_logo'] ``` To rename a key: ```python theme={null} image_scenario = image_scenario.rename({"parrot_logo": "logo"}) # key = old name, value = new name image_scenario.keys() ``` Output: ```python theme={null} ['logo'] ``` To use it in a question, the question should be parameterized with the key: ```python theme={null} from edsl import QuestionFreeText q = QuestionFreeText( question_name = "test", question_text = "Describe this logo: {{ logo }}" ) ``` Here we run the question with the scenario object. Note that we need to use a vision model; here we specify the default model for demonstration purposes and add an agent persona: ```python theme={null} from edsl import Model model = Model("gpt-4o") # specify a vision model results = q.by(image_scenario).by(model).run() # run the question with the scenario and model ``` Learn more about selecting models in the [Language Models](/en/latest/language_models) section. Output: | model | scenario.logo | answer.test | | :- | :- | :- | | gpt-4 | FileStore: self.path | The logo features a large, stylized letter “E” in a serif font on the left. Next to it, within square brackets, is a colorful parrot. The parrot has a green body, an orange beak, a pink chest, blue lower body, and gray feet. The design combines a classic typographic element with a vibrant, playful illustration. | ### PDF example Here we download an example PDF from the internet, post and retrieve it from Expected Parrot using FileStore and then convert it into a ScenarioList object with the from\_pdf() method. The default keys are filename, page, text, which can be modified with the rename method. To download a PDF file: ```python theme={null} import requests url = "https://arxiv.org/pdf/2404.11794" response = requests.get(url) with open("automated_social_scientist.pdf", "wb") as file: file.write(response.content) ``` Here we post the file to Expected Parrot and inspect the details: ```python theme={null} from edsl import FileStore fs = FileStore("automated_social_scientist.pdf") pdf_info = fs.push(description = "My example PDF file", visibility = "public") pdf_info # display the URL and uuid of the stored file for retrieving it later ``` Example output: ```json theme={null} {'description': 'My example PDF file', 'object_type': 'scenario', 'url': 'https://www.expectedparrot.com/content/e1770915-7e69-436d-b2ca-f0f92c6f56ba', 'uuid': 'e1770915-7e69-436d-b2ca-f0f92c6f56ba', 'version': '0.1.47.dev1', 'visibility': 'public'} ``` Now we retrieve the file and create a ScenarioList object from it: ```python theme={null} from edsl import FileStore, ScenarioList pdf_file = FileStore.pull(pdf_info["uuid"]) scenarios = ScenarioList.from_pdf(pdf_file.to_tempfile()) ``` To inspect the keys: ```python theme={null} scenarios.parameters ``` Output: ```python theme={null} {'filename', 'page', 'text'} ``` Using the scenarios in a question: ```python theme={null} from edsl import QuestionFreeText q = QuestionFreeText( question_name = "summary", question_text = "Summarize this page: {{ text }}" ) ``` Each result will contain the text from a page of the PDF file, together with columns for the filename and page number. Run results.columns to see all the components of results. ## FileStore class > ### *class* edsl.scenarios.FileStore(*path: str | None = None*, *mime\_type: str | None = None*, *binary: bool | None = None*, *suffix: str | None = None*, *base64\_string: str | None = None*, *external\_locations: Dict\[str, str] | None = None*, *extracted\_text: str | None = None*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Bases: [`Scenario`](/en/latest/scenarios "edsl.scenarios.scenario.Scenario") A specialized Scenario subclass for managing file content and metadata. FileStore provides functionality for working with files in EDSL, handling various file formats with appropriate encoding, storage, and access methods. It extends Scenario to allow files to be included in surveys, questions, and other EDSL components. FileStore supports multiple file formats including text, PDF, Word documents, images, and more. It can load files from local paths or URLs, and provides methods for accessing file content, extracting text, and managing file operations. Key features: - Base64 encoding for portability and serialization - Lazy loading through temporary files when needed - Automatic MIME type detection - Text extraction from various file formats - Format-specific operations through specialized handlers > **Attributes:** \_path (str): The original file path. \_temp\_path (str): Path to any generated temporary file. suffix (str): File extension. binary (bool): Whether the file is binary. mime\_type (str): The file’s MIME type. base64\_string (str): Base64-encoded file content. external\_locations (dict): Dictionary of external locations. extracted\_text (str): Text extracted from the file. > **Examples:** ```python theme={null} >>> import tempfile >>> # Create a text file >>> with tempfile.NamedTemporaryFile(suffix=".txt", mode="w") as f: ... _ = f.write("Hello World") ... _ = f.flush() ... fs = FileStore(f.name) ``` \# The following example works locally but is commented out for CI environments # where dependencies like pandoc may not be available: # >>> # FileStore supports various formats # >>> formats = \[“txt”, “pdf”, “docx”, “pptx”, “md”, “py”, “json”, “csv”, “html”, “png”, “db”] # >>> \_ = \[FileStore.example(format) for format in formats] > #### **init**(*path: str | None = None*, *mime\_type: str | None = None*, *binary: bool | None = None*, *suffix: str | None = None*, *base64\_string: str | None = None*, *external\_locations: Dict\[str, str] | None = None*, *extracted\_text: str | None = None*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Initialize a new FileStore object. This constructor creates a FileStore object from either a file path or a base64-encoded string representation of file content. It handles automatic detection of file properties like MIME type, extracts text content when possible, and manages file encoding. > **Args:** path: Path to the file to load. Can be a local file path or URL. mime\_type: MIME type of the file. If not provided, will be auto-detected. binary: Whether the file is binary. Defaults to False. suffix: File extension. If not provided, will be extracted from the path. base64\_string: Base64-encoded file content. If provided, the file content will be loaded from this string instead of the path. **external\_locations: Dictionary mapping location names to URLs or paths where** the file can also be accessed. **extracted\_text: Pre-extracted text content from the file. If not provided,** text will be extracted automatically if possible. [\*\*](/en/latest/#id1)**kwargs: Additional keyword arguments. ‘filename’ can be used as an** alternative to ‘path’. **Note:** If path is a URL (starts with [http://](http://) or [https://](https://)), the file will be downloaded automatically. *async* async\_upload\_google(*refresh: bool = False*) → dict[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Async version of upload\_google that avoids blocking the event loop. This method uploads a file to Google’s Generative AI service asynchronously, polls for activation status with exponential backoff, and returns the file info. **Args:** refresh: If True, force re-upload even if already uploaded **Returns:** Dictionary containing the Google file information **Raises:** Exception: If upload fails or file activation fails > #### *static* base64\_to\_file(*base64\_string*, *is\_binary=True*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### *static* base64\_to\_text\_file(*base64\_string*) → IO[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### *classmethod* batch\_screenshots(*urls: List\[str]*, \*\**kwargs*) → [ScenarioList](/en/latest/scenarios#scenariolist-class "edsl.scenarios.ScenarioList")[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Take screenshots of multiple URLs concurrently. Args: urls: List of URLs to screenshot [\*\*](/en/latest/#id3)kwargs: Additional arguments passed to screenshot function (full\_page, wait\_until, etc.) **Returns:** ScenarioList containing FileStore objects with their corresponding URLs > #### create\_link(*custom\_filename=None*, *style=None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### download\_link()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### encode\_file\_to\_base64\_string(*file\_path: str*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### *classmethod* example(*example\_type='txt'*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Returns an example Scenario instance. > **Args:** **randomize: If True, adds a random string to the value of the example key** to ensure uniqueness. > **Returns:** A Scenario instance with example data suitable for testing or demonstration. > **Examples:** ```python theme={null} >>> s = Scenario.example() >>> 'persona' in s True >>> s1 = Scenario.example(randomize=True) >>> s2 = Scenario.example(randomize=True) >>> s1.data != s2.data # Should be different due to randomization True ``` > #### extract\_text() → str[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### *classmethod* from\_dict(*d*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Creates a Scenario from a dictionary, with special handling for FileStore objects. This method creates a Scenario using the provided dictionary. It has special handling for dictionary values that represent serialized FileStore objects, which it will deserialize back into proper FileStore instances. > **Args:** d: A dictionary to convert to a Scenario. > **Returns:** A new Scenario containing the provided dictionary data. > **Examples:** ```python theme={null} >>> Scenario.from_dict({"food": "wood chips"}) Scenario({'food': 'wood chips'}) ``` ```python theme={null} >>> # Example with a serialized FileStore >>> from edsl import FileStore >>> file_dict = {"path": "example.txt", "base64_string": "SGVsbG8gV29ybGQ="} >>> s = Scenario.from_dict({"document": file_dict}) >>> isinstance(s["document"], FileStore) True ``` > **Notes:** * Any dictionary values that match the FileStore format will be converted to FileStore objects * The method detects FileStore objects by looking for “base64\_string” and “path” keys * EDSL version information is automatically removed by the @remove\_edsl\_version decorator * This method is commonly used when deserializing scenarios from JSON or other formats > #### *classmethod* from\_url(*url: str*, *download\_path: str | None = None*, *mime\_type: str | None = None*) → [FileStore](/en/latest/#edsl.scenarios.FileStore "edsl.scenarios.file_store.FileStore")[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > **Parameters:** * **url** – The URL of the file to download. * **download\_path** – The path to save the downloaded file. * **mime\_type** – The MIME type of the file. If None, it will be guessed from the file extension. > #### *classmethod* from\_url\_screenshot(*url: str*, \*\**kwargs*) → [FileStore](/en/latest/#edsl.scenarios.FileStore "edsl.scenarios.file_store.FileStore")[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Synchronous wrapper for screenshot functionality > #### get\_image\_dimensions() → tuple[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Get the dimensions (width, height) of an image file. > **Returns:** tuple: A tuple containing the width and height of the image. > **Raises:** ValueError: If the file is not an image or PIL is not installed. > **Examples:** ```python theme={null} >>> fs = FileStore.example("png") >>> width, height = fs.get_image_dimensions() >>> isinstance(width, int) and isinstance(height, int) True ``` > #### get\_video\_metadata() → dict[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Get metadata about a video file such as duration, dimensions, codec, etc. Uses FFmpeg to extract the information if available. > **Returns:** dict: A dictionary containing video metadata, or a dictionary with error information if metadata extraction fails. > **Raises:** ValueError: If the file is not a video. > **Examples:** ```python theme={null} >>> fs = FileStore.example("mp4") >>> metadata = fs.get_video_metadata() >>> isinstance(metadata, dict) True ``` > #### is\_image() → bool[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Check if the file is an image by examining its MIME type. > **Returns:** bool: True if the file is an image, False otherwise. > **Examples:** ```python theme={null} >>> fs = FileStore.example("png") >>> fs.is_image() True >>> fs = FileStore.example("txt") >>> fs.is_image() False ``` > #### is\_video() → bool[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Check if the file is a video by examining its MIME type. > **Returns:** bool: True if the file is a video, False otherwise. > **Examples:** ```python theme={null} >>> fs = FileStore.example("mp4") >>> fs.is_video() True >>> fs = FileStore.example("webm") >>> fs.is_video() True >>> fs = FileStore.example("txt") >>> fs.is_video() False offload(*inplace=False*) → [FileStore](/en/latest/#edsl.scenarios.FileStore "edsl.scenarios.file_store.FileStore")[[source]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) ``` > #### Offloads base64-encoded content from the FileStore by replacing ‘base64\_string’ with ‘offloaded’. This reduces memory usage. > **Args:** inplace (bool): If True, modify the current FileStore. If False, return a new one. > **Returns:** FileStore: The modified FileStore (either self or a new instance). > #### open() → IO[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### *property* path\*: str\*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Returns a valid path to the file content, creating a temporary file if needed. This property ensures that a valid file path is always available for the file content, even if the original file is no longer accessible or if the FileStore was created from a base64 string without a path. If the original path doesn’t exist, it automatically generates a temporary file from the base64 content. > **Returns:** A string containing a valid file path to access the file content. > **Examples:** ```python theme={null} >>> import tempfile, os >>> with tempfile.NamedTemporaryFile(suffix=".txt", mode="w") as f: ... _ = f.write("Hello World") ... _ = f.flush() ... fs = FileStore(f.name) ... os.path.isfile(fs.path) True ``` > **Notes:** * The path may point to a temporary file that will be cleaned up when the Python process exits * Accessing this property may create a new temporary file if needed * This property provides a consistent interface regardless of how the FileStore was created (from file or from base64 string) > #### *classmethod* pull(*url\_or\_uuid: str | UUID*) → [FileStore](/en/latest/#edsl.scenarios.FileStore "edsl.scenarios.file_store.FileStore")[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Pull a FileStore object from Expected Parrot. **Args:** url\_or\_uuid: Either a UUID string or a URL pointing to the object expected\_parrot\_url: Optional URL for the Parrot server **Returns:** FileStore: The pulled FileStore object > #### push(*description: str | None = None*, *alias: str | None = None*, *visibility: str | None = 'unlisted'*, *expected\_parrot\_url: str | None = None*) → dict[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Push the object to Expected Parrot. :param description: The description of the object to push. :param visibility: The visibility of the object to push. > #### save\_to\_gcs\_bucket(*signed\_url: str*) → dict[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Saves the FileStore’s file content to a Google Cloud Storage bucket using a signed URL. **Args:** signed\_url (str): The signed URL for uploading to GCS bucket **Returns:** dict: Response from the GCS upload operation **Raises:** ValueError: If base64\_string is offloaded or missing requests.RequestException: If the upload fails > #### *property* size\*: int\*[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### *property* text[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### to\_pandas()[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Convert the file content to a pandas DataFrame if supported by the file handler. **Returns:** pandas.DataFrame: The data from the file as a DataFrame **Raises:** AttributeError: If the file type’s handler doesn’t support pandas conversion > #### to\_scenario(*key\_name: str | None = None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### to\_tempfile(*suffix=None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### upload\_google(*refresh: bool = False*) → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) > #### view() → None[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Display an interactive visualization of this object. **Returns:** The result of the dataset’s view method > #### write(*filename: str | None = None*) → str[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios.py) Write the file content to disk, either to a specified filename or a temporary file. **Args:** filename (Optional\[str]): The destination filename. If None, creates a temporary file. **Returns:** str: The path to the written file. # Firecrawl Integration Source: https://docs.expectedparrot.com/en/latest/firecrawl The EDSL Firecrawl integration provides seamless access to the Firecrawl web scraping platform, allowing you to scrape, crawl, search, and extract structured data from web content directly into EDSL Scenarios and ScenarioLists. ## Features * **Scrape**: Extract clean content from single URLs or batches of URLs * **Crawl**: Comprehensively crawl entire websites and extract content from all pages * **Search**: Perform web searches and scrape content from search results * **Map URLs**: Fast URL discovery without full content extraction * **Extract**: AI-powered structured data extraction using schemas or natural language prompts All methods return EDSL Scenario or ScenarioList objects, making web data immediately ready for survey research, analysis, and other EDSL workflows. ## Installation Install the required dependencies: ```bash theme={null} pip install firecrawl-py python-dotenv ``` ## Setup Get your Firecrawl API key from [https://firecrawl.dev](https://firecrawl.dev/) and set it as an environment variable: ```bash theme={null} export FIRECRAWL_API_KEY=your_api_key_here ``` Or add it to a .env file in your project: ```bash theme={null} FIRECRAWL_API_KEY=your_api_key_here ``` ## Basic Usage ### Quick Start Examples **Scraping a single URL:** ```python theme={null} from edsl.scenarios.firecrawl_scenario import scrape_url # Scrape a single URL result = scrape_url("https://example.com") print(result["content"]) # Scraped markdown content print(result["title"]) # Page title ``` **Scraping multiple URLs:** ```python theme={null} from edsl.scenarios.firecrawl_scenario import scrape_url # Scrape multiple URLs urls = ["https://example.com", "https://example.org"] results = scrape_url(urls, max_concurrent=5) for result in results: print(f"URL: {result['url']}") print(f"Title: {result['title']}") print(f"Content: {result['content'][:100]}...") ``` **Web search with content extraction:** ```python theme={null} from edsl.scenarios.firecrawl_scenario import search_web # Search and extract content from results results = search_web("python web scraping tutorials") for result in results: print(f"Title: {result['title']}") print(f"URL: {result['url']}") print(f"Content: {result['content'][:100]}...") ``` **Crawling an entire website:** ```python theme={null} from edsl.scenarios.firecrawl_scenario import crawl_website # Crawl a website with limits results = crawl_website( "https://docs.example.com", limit=50, # Max 50 pages max_depth=3, # Max depth of 3 include_paths=["/docs/*"] # Only crawl documentation ) print(f"Crawled {len(results)} pages") ``` **Structured data extraction:** ```python theme={null} from edsl.scenarios.firecrawl_scenario import extract_data # Define what data to extract schema = { "title": "string", "price": "number", "description": "string", "availability": "boolean" } # Extract structured data result = extract_data("https://shop.example.com/product", schema=schema) extracted = result["extracted_data"] print(f"Product: {extracted['title']}") print(f"Price: ${extracted['price']}") ``` ## Class-Based API For more control, use the class-based API: ```python theme={null} ### FirecrawlScenario Class from edsl.scenarios.firecrawl_scenario import FirecrawlScenario # Initialize with API key (optional if set in environment) firecrawl = FirecrawlScenario(api_key="your_key_here") # Use any method result = firecrawl.scrape("https://example.com") ``` ### FirecrawlRequest Class For distributed processing or when you need to separate request creation from execution: ```python theme={null} from edsl.scenarios.firecrawl_scenario import FirecrawlRequest, FirecrawlScenario # Create request without executing (useful for distributed systems) request = FirecrawlRequest(api_key="your_key") request_dict = request.scrape("https://example.com") # Execute the request later (potentially on a different machine) result = FirecrawlScenario.from_request(request_dict) ``` ## Detailed Method Documentation ### Scraping Methods \*\*scrape\_url(url\_or\_urls, max\_concurrent=10, **kwargs)** Scrape content from one or more URLs. **Parameters:** * `url_or_urls`: Single URL string or list of URLs * `max_concurrent`: Maximum concurrent requests for batch processing (default: 10) * `formats`: List of output formats (default: \[“markdown”]) * `only_main_content`: Extract only main content, skip navigation/ads (default: True) * `include_tags`: HTML tags to specifically include * `exclude_tags`: HTML tags to exclude * `headers`: Custom HTTP headers as dictionary * `wait_for`: Time to wait before scraping (milliseconds) * `timeout`: Request timeout (milliseconds) * `actions`: Browser actions to perform before scraping **Returns:** * Single URL: Scenario object with scraped content * Multiple URLs: ScenarioList with Scenario objects \*\*crawl\_website(url, **kwargs)** Crawl an entire website and extract content from all discovered pages. **Parameters:** * `url`: Base URL to start crawling from * `limit`: Maximum number of pages to crawl * `max_depth`: Maximum crawl depth from starting URL * `include_paths`: URL path patterns to include (supports wildcards) * `exclude_paths`: URL path patterns to exclude * `formats`: Output formats for each page (default: \[“markdown”]) * `only_main_content`: Extract only main content (default: True) **Returns:** ScenarioList containing Scenario objects for each crawled page ### Search Methods \*\*search\_web(query\_or\_queries, max\_concurrent=5, **kwargs)** Search the web and extract content from results. **Parameters:** * `query_or_queries`: Single search query or list of queries * `max_concurrent`: Maximum concurrent requests for batch processing (default: 5) * `limit`: Maximum number of search results per query * `sources`: Sources to search (e.g., \[“web”, “news”, “images”]) * `location`: Geographic location for localized results * `formats`: Output formats for scraped content from results **Returns:** ScenarioList containing Scenario objects for each search result \*\*map\_website\_urls(url, **kwargs)** Discover and map all URLs from a website without scraping content (fast URL discovery). **Parameters:** * `url`: Base URL to discover links from * Additional mapping parameters via kwargs **Returns:** ScenarioList containing Scenario objects for each discovered URL ### Extraction Methods \*\*extract\_data(url\_or\_urls, schema=None, prompt=None, **kwargs)** Extract structured data from web pages using AI-powered analysis. **Parameters:** * `url_or_urls`: Single URL string or list of URLs * `schema`: JSON schema defining data structure to extract * `prompt`: Natural language description of what to extract * `max_concurrent`: Maximum concurrent requests for batch processing (default: 5) * `formats`: Output formats for scraped content before extraction **Returns:** * Single URL: Scenario object with extracted structured data * Multiple URLs: ScenarioList with Scenario objects Note: Either `schema` or `prompt` should be provided. Schema takes precedence if both are given. ## Working with Results ### Scenario Fields All methods return Scenario or ScenarioList objects with standardized fields: **Common fields for all methods:** * `url`: The scraped/crawled/searched URL * `title`: Page title (when available) * `description`: Page description (when available) * `content`: Primary content (usually markdown format) * `status_code`: HTTP status code (when available) **Scraping-specific fields:** * `scrape_status`: “success” or “error” * `markdown`: Markdown content * `html`: HTML content (if requested) * `links`: Extracted links (if requested) * `screenshot`: Screenshot data (if requested) * `metadata`: Full page metadata **Search-specific fields:** * `search_query`: The original search query * `search_status`: “success” or “error” * `result_type`: “web”, “news”, or “image” * `position`: Result position in search results **Extraction-specific fields:** * `extract_status`: “success” or “error” * `extracted_data`: The structured data extracted by AI * `extraction_prompt`: The prompt used (if any) * `extraction_schema`: The schema used (if any) **Crawl-specific fields:** * `crawl_status`: “success” or “error” **URL mapping fields:** * `discovered_url`: The discovered URL * `source_url`: The URL it was discovered from * `map_status`: “success” or “error” ## Advanced Usage ### Concurrent Processing All batch methods support concurrent processing to speed up large operations: ```python theme={null} # Scrape 100 URLs with 20 concurrent requests results = scrape_url(urls, max_concurrent=20) # Search multiple queries concurrently queries = ["AI research", "machine learning", "data science"] results = search_web(queries, max_concurrent=3) ``` ### Custom Formats and Options Specify different output formats and extraction options: ```python theme={null} # Get both markdown and HTML content result = scrape_url( "https://example.com", formats=["markdown", "html"], only_main_content=False, include_tags=["article", "main"], exclude_tags=["nav", "footer"] ) # Access different formats print(result["markdown"]) # Markdown content print(result["html"]) # HTML content ``` ### Complex Crawling Scenarios Advanced crawling with path filtering: ```python theme={null} # Crawl documentation site with specific constraints results = crawl_website( "https://docs.example.com", limit=100, max_depth=3, include_paths=["/docs/*", "/api/*", "/tutorials/*"], exclude_paths=["/docs/deprecated/*", "/docs/v1/*"], formats=["markdown", "html"] ) # Filter results by content type api_docs = [r for r in results if "/api/" in r["url"]] tutorials = [r for r in results if "/tutorials/" in r["url"]] ``` ### Schema-Based vs Prompt-Based Extraction **Using JSON schemas for structured extraction:** ```python theme={null} # Define precise data structure product_schema = { "name": "string", "price": "number", "rating": "number", "availability": "boolean", "features": ["string"], "specifications": { "dimensions": "string", "weight": "string", "color": "string" } } result = extract_data("https://shop.example.com/product", schema=product_schema) product_data = result["extracted_data"] ``` **Using natural language prompts:** ```python theme={null} # Extract with natural language result = extract_data( "https://news.example.com/article", prompt="Extract the article headline, author name, publication date, and key topics discussed" ) article_data = result["extracted_data"] print(f"Headline: {article_data['headline']}") print(f"Author: {article_data['author']}") ``` ## Error Handling The integration handles errors gracefully by returning Scenario objects with error information: ```python theme={null} results = scrape_url(["https://valid-url.com", "https://invalid-url.com"]) for result in results: if result.get("scrape_status") == "error": print(f"Error scraping {result['url']}: {result['error']}") else: print(f"Successfully scraped {result['url']}") ``` ## Integration with EDSL Workflows The firecrawl integration seamlessly integrates with other EDSL components: ### Using with Surveys ```python expandable theme={null} from edsl import QuestionFreeText, Survey from edsl.scenarios.firecrawl_scenario import scrape_url # Scrape content to create scenarios scenarios = scrape_url([ "https://news1.com/article1", "https://news2.com/article2" ]) # Create survey questions about the content q1 = QuestionFreeText( question_name="summary", question_text="Summarize the main points of this article: {{ content }}" ) q2 = QuestionFreeText( question_name="sentiment", question_text="What is the overall sentiment of this article?" ) survey = Survey(questions=[q1, q2]) # Run survey with scraped content as scenarios results = survey.by(scenarios).run() ``` ### Content Analysis Pipeline ```python expandable theme={null} from edsl.scenarios.firecrawl_scenario import search_web, extract_data # 1. Search for relevant content search_results = search_web("climate change research 2024", limit=10) # 2. Extract structured data from search results extraction_schema = { "findings": "string", "methodology": "string", "publication_date": "string", "authors": ["string"] } urls = [result["url"] for result in search_results if result["search_status"] == "success"] extracted_data = extract_data(urls, schema=extraction_schema) # 3. Use in EDSL survey for analysis survey = Survey([ QuestionFreeText( question_name="significance", question_text="Rate the significance of these findings: {{ extracted_data }}" ) ]) analysis_results = survey.by(extracted_data).run() ``` ## Distributed Processing For large-scale operations, use the request/response pattern for distributed processing: ```python expandable theme={null} from edsl.scenarios.firecrawl_scenario import ( create_scrape_request, create_search_request, execute_request ) # Create serializable requests (can be sent to workers, APIs, etc.) requests = [ create_scrape_request("https://example1.com"), create_scrape_request("https://example2.com"), create_search_request("machine learning tutorials") ] # Execute requests (potentially on different machines/processes) results = [] for request in requests: result = execute_request(request) results.append(result) ``` ## Best Practices ### Performance Optimization 1. **Use appropriate concurrency limits** - Start with defaults and adjust based on your needs and rate limits 2. **Filter early** - Use include/exclude paths in crawling to avoid unnecessary requests 3. **Choose optimal formats** - Only request formats you actually need 4. **Batch operations** - Process multiple URLs/queries together when possible ### Rate Limiting 1. **Respect rate limits** - Firecrawl has rate limits based on your plan 2. **Adjust concurrency** - Lower max\_concurrent values if you hit rate limits 3. **Monitor costs** - Each request consumes Firecrawl credits ### Content Quality 1. **Use only\_main\_content=True** - Filters out navigation, ads, and other noise 2. **Specify include/exclude tags** - Fine-tune content extraction 3. **Choose appropriate formats** - Markdown for text analysis, HTML for detailed parsing ### Error Resilience 1. **Check status fields** - Always check scrape\_status, search\_status, etc. 2. **Handle partial failures** - Some URLs in a batch may fail while others succeed 3. **Implement retries** - For critical operations, implement retry logic for failed requests ### Cost Management 1. **Use URL mapping first** - Discover URLs with map\_website\_urls before full crawling 2. **Set reasonable limits** - Use limit and max\_depth parameters to control crawl scope 3. **Cache results** - Store results locally to avoid re-scraping the same content ## Troubleshooting ### Common Issues **“FIRECRAWL\_API\_KEY not found” error:** * Ensure your API key is set as an environment variable or in a .env file * Verify the key is valid and active at [https://firecrawl.dev](https://firecrawl.dev/) **Rate limit errors:** * Reduce max\_concurrent parameter * Check your Firecrawl plan limits * Implement delays between requests if needed **Empty or poor quality content:** * Try only\_main\_content=False for more content * Adjust include\_tags/exclude\_tags parameters * Some sites may have anti-scraping measures **Slow performance:** * Increase max\_concurrent for batch operations * Use URL mapping to discover URLs faster than full crawling * Consider using search instead of crawling for content discovery **Memory usage with large crawls:** * Use limit parameter to control crawl size * Process results in batches rather than storing everything in memory * Consider using the request/response pattern for distributed processing ### Getting Help * Check the [Firecrawl documentation](https://docs.firecrawl.dev/) for API-specific issues * Review EDSL documentation for Scenario and ScenarioList usage * Ensure your firecrawl-py package is up to date: `pip install --upgrade firecrawl-py` ## API Reference ### Convenience Functions > #### edsl.scenarios.firecrawl\_scenario.scrape\_url(*url\_or\_urls: str | List\[str]*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Convenience function to scrape single URL or multiple URLs. > #### edsl.scenarios.firecrawl\_scenario.crawl\_website(*url: str*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Convenience function to crawl a website. > #### edsl.scenarios.firecrawl\_scenario.search\_web(*query\_or\_queries: str | List\[str]*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Convenience function to search the web with single query or multiple queries. > #### edsl.scenarios.firecrawl\_scenario.map\_website\_urls(*url: str*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Convenience function to map website URLs. > #### edsl.scenarios.firecrawl\_scenario.extract\_data(*url\_or\_urls: str | List\[str]*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Convenience function to extract structured data from single URL or multiple URLs. ### Classes > #### *class* edsl.scenarios.firecrawl\_scenario.FirecrawlScenario(*api\_key: str | None = None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) EDSL integration for Firecrawl web scraping and data extraction. This class provides methods to use all Firecrawl features and return results as EDSL Scenario and ScenarioList objects. > ##### crawl(*url: str*, *limit: int | None = 10*, *max\_depth: int | None = 3*, *include\_paths: List\[str] | None = None*, *exclude\_paths: List\[str] | None = None*, *formats: List\[str] | None = None*, *only\_main\_content: bool = True*, *scrape\_options: Dict\[str, Any] | None = None*, *return\_credits: bool = False*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Crawl a website and return a ScenarioList with all pages. **Args:** url: Base URL to crawl limit: Maximum number of pages to crawl max\_depth: Maximum crawl depth (now max\_discovery\_depth) include\_paths: URL patterns to include exclude\_paths: URL patterns to exclude formats: List of formats to return for each page only\_main\_content: Whether to extract only main content [\*\*](/en/latest/#id1)kwargs: Additional parameters passed to Firecrawl **Returns:** ScenarioList containing scenarios for each crawled page > ##### extract(*url\_or\_urls: str | List\[str]*, *schema: Dict\[str, Any] | None = None*, *prompt: str | None = None*, *max\_concurrent: int = 5*, *limit: int | None = None*, *scrape\_options: Dict\[str, Any] | None = None*, *return\_credits: bool = False*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Smart extract method that handles both single URLs and batches. **Args:** url\_or\_urls: Single URL string or list of URLs schema: JSON schema for structured extraction prompt: Natural language prompt for extraction max\_concurrent: Maximum concurrent requests for batch processing [\*\*](/en/latest/#id3)kwargs: Additional parameters passed to extract method **Returns:** Scenario object for single URL, ScenarioList for multiple URLs, or (object, credits) if return\_credits=True > #### *classmethod* from\_request(*request\_dict: Dict\[str, Any]*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Execute a serialized Firecrawl request and return results. This method allows for distributed processing where requests are serialized, sent to an API, reconstituted here, executed, and results returned. **Args:** request\_dict: Dictionary containing the serialized request **Returns:** Scenario or ScenarioList depending on the method and input > #### map\_urls(*url: str*, *limit: int | None = None*, *return\_credits: bool = False*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Get all URLs from a website (fast URL discovery). > **Args:** url: Website URL to map limit: Maximum number of URLs to discover and map. If None, discovers all > available linked URLs. Defaults to None. > \*\* [\*\*](/en/latest/#id5)kwargs: Additional parameters passed to Firecrawl **Returns:** ScenarioList containing scenarios for each discovered URL > ##### search(*query\_or\_queries: str | List\[str]*, *max\_concurrent: int = 5*, *limit: int | None = None*, *sources: List\[str] | None = None*, *location: str | None = None*, *scrape\_options: Dict\[str, Any] | None = None*, *return\_credits: bool = False*, \*\**kwargs*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Smart search method that handles both single queries and batches. **Args:** query\_or\_queries: Single query string or list of queries max\_concurrent: Maximum concurrent requests for batch processing [\*\*](/en/latest/#id9)kwargs: Additional parameters passed to search method **Returns:** ScenarioList for both single and multiple queries, or (ScenarioList, credits) if return\_credits=True > #### *class* edsl.scenarios.firecrawl\_scenario.FirecrawlRequest(*api\_key: str | None = None*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Firecrawl request class that can operate in two modes: 1. With API key present: Automatically executes requests via FirecrawlScenario and returns results 2. Without API key: Can be used as a descriptor/placeholder, raises exception when methods are called This supports both direct execution and distributed processing patterns. > ##### crawl(*url: str*, *limit: int | None = 10*, *max\_depth: int | None = 3*, *include\_paths: List\[str] | None = None*, *exclude\_paths: List\[str] | None = None*, *formats: List\[str] | None = None*, *only\_main\_content: bool = True*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Crawl an entire website and extract content from all discovered pages. This method performs comprehensive website crawling, discovering and scraping content from multiple pages within a website. When an API key is present, it executes the crawl and returns a ScenarioList with all pages. Without an API key, it raises an exception. > **Args:** **url: Base URL to start crawling from. Should be a valid HTTP/HTTPS URL.** The crawler will discover and follow links from this starting point. **limit: Maximum number of pages to crawl. If None, crawls all discoverable** pages (subject to other constraints). Use this to control crawl scope. **max\_depth: Maximum crawl depth from the starting URL. Depth 0 is just the** starting page, depth 1 includes pages directly linked from the start, etc. If None, no depth limit is imposed. **include\_paths: List of URL path patterns to include in the crawl. Only URLs** matching these patterns will be crawled. Supports wildcard patterns. **exclude\_paths: List of URL path patterns to exclude from the crawl. URLs** matching these patterns will be skipped. Applied after include\_paths. **formats: List of output formats for each crawled page (e.g., \[“markdown”, “html”]).** Defaults to \[“markdown”] if not specified. only\_main\_content: Whether to extract only main content from each page, skipping navigation, ads, footers, etc. Defaults to True. [\*\*](/en/latest/#id11)kwargs: Additional crawling parameters passed to the Firecrawl API. > **Returns:** **When API key is present:** ScenarioList containing Scenario objects for each crawled page. Each scenario includes the page content, URL, title, and metadata. When API key is missing: Raises ValueError > **Raises:** ValueError: If FIRECRAWL\_API\_KEY is not found in environment, parameters, or .env file. > **Examples:** > **Basic website crawl:** ```python theme={null} >>> firecrawl = FirecrawlRequest(api_key="your_key") >>> results = firecrawl.crawl("https://example.com") >>> print(f"Crawled {len(results)} pages") >>> for page in results: ... print(f"Page: {page['url']} - {page['title']}") ``` > **Limited crawl with constraints:** ```python theme={null} >>> results = firecrawl.crawl( ... "https://docs.example.com", ... limit=50, ... max_depth=3, ... include_paths=["/docs/*", "/api/*"], ... exclude_paths=["/docs/deprecated/*"] ... ) ``` > **Full content crawl with multiple formats:** ```python theme={null} >>> results = firecrawl.crawl( ... "https://blog.example.com", ... formats=["markdown", "html"], ... only_main_content=False, ... limit=100 ... ) >>> for post in results: ... print(f"Title: {post['title']}") ... print(f"Content length: {len(post['content'])}") ``` > #### extract(*url\_or\_urls: str | List\[str]*, *schema: Dict\[str, Any] | None = None*, *prompt: str | None = None*, *max\_concurrent: int = 5*, *limit: int | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Extract structured data from web pages using AI-powered analysis. This method uses AI to extract specific information from web pages based on either a JSON schema or natural language prompt. When an API key is present, it executes the extraction and returns structured data. Without an API key, it raises an exception. > **Args:** **url\_or\_urls: Single URL string or list of URLs to extract data from.** Each URL should be a valid HTTP/HTTPS URL. **schema: JSON schema defining the structure of data to extract. Should be** a dictionary with field names and their types/descriptions. Takes precedence over prompt if both are provided. **prompt: Natural language description of what data to extract. Used when** schema is not provided. Should be clear and specific. **max\_concurrent: Maximum number of concurrent requests when processing multiple** URLs. Only applies when url\_or\_urls is a list. Defaults to 5. **limit: Maximum number of URLs to extract data from when url\_or\_urls is a list.** If None, extracts from all provided URLs. Defaults to None. [\*\*](/en/latest/#id13)**kwargs: Additional extraction parameters:** **formats: List of output formats for the scraped content before extraction** (e.g., \[“markdown”, “html”]). > **Returns:** **When API key is present:** * For single URL: Scenario object containing extracted structured data * For multiple URLs: ScenarioList containing Scenario objects Each result includes the extracted data in the ‘extracted\_data’ field. When API key is missing: Raises ValueError > **Raises:** ValueError: If FIRECRAWL\_API\_KEY is not found in environment, parameters, or .env file. > **Examples:** > **Schema-based extraction:** ```python theme={null} >>> schema = { ... "title": "string", ... "price": "number", ... "description": "string", ... "availability": "boolean" ... } >>> firecrawl = FirecrawlRequest(api_key="your_key") >>> result = firecrawl.extract("https://shop.example.com/product", schema=schema) >>> print(result["extracted_data"]["title"]) ``` > **Prompt-based extraction:** ```python theme={null} >>> result = firecrawl.extract( ... "https://news.example.com/article", ... prompt="Extract the article headline, author, and publication date" ... ) >>> print(result["extracted_data"]) ``` > **Multiple URLs with schema:** ```python theme={null} >>> urls = ["https://shop1.com/item1", "https://shop2.com/item2"] >>> results = firecrawl.extract( ... urls, ... schema={"name": "string", "price": "number"}, ... max_concurrent=3, ... limit=2 ... ) >>> for result in results: ... data = result["extracted_data"] ... print(f"{data['name']}: ${data['price']}") ``` > #### map\_urls(*url: str*, *limit: int | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Discover and map all URLs from a website without scraping content. This method performs fast URL discovery to map the structure of a website without downloading and processing the full content of each page. When an API key is present, it executes the mapping and returns discovered URLs. Without an API key, it raises an exception. > **Args:** **url: Base URL to discover links from. Should be a valid HTTP/HTTPS URL.** The mapper will analyze this page and discover all linked URLs. **limit: Maximum number of URLs to discover and map. If None, discovers all** available linked URLs. Defaults to None. [\*\*](/en/latest/#id15)**kwargs: Additional mapping parameters passed to the Firecrawl API.** Common options may include depth limits or filtering criteria. > **Returns:** **When API key is present:** ScenarioList containing Scenario objects for each discovered URL. Each scenario includes the discovered URL, source URL, and any available metadata (title, description) without full content. When API key is missing: Raises ValueError > **Raises:** ValueError: If FIRECRAWL\_API\_KEY is not found in environment, parameters, or .env file. > **Examples:** > **Basic URL mapping:** ```python theme={null} >>> firecrawl = FirecrawlRequest(api_key="your_key") >>> urls = firecrawl.map_urls("https://example.com") >>> print(f"Discovered {len(urls)} URLs") >>> for url_info in urls: ... print(f"URL: {url_info['discovered_url']}") ... if 'title' in url_info: ... print(f"Title: {url_info['title']}") ``` > **Website structure analysis:** ```python theme={null} >>> urls = firecrawl.map_urls("https://docs.example.com", limit=100) >>> # Group URLs by path pattern >>> doc_urls = [u for u in urls if '/docs/' in u['discovered_url']] >>> api_urls = [u for u in urls if '/api/' in u['discovered_url']] >>> print(f"Documentation pages: {len(doc_urls)}") >>> print(f"API reference pages: {len(api_urls)}") ``` > **Link discovery for targeted crawling:** ```python theme={null} >>> # First map URLs to understand site structure >>> all_urls = firecrawl.map_urls("https://blog.example.com") >>> # Then crawl specific sections >>> blog_posts = [u['discovered_url'] for u in all_urls ... if '/posts/' in u['discovered_url']] >>> # Use discovered URLs for targeted scraping >>> content = firecrawl.scrape(blog_posts[:10]) ``` > #### scrape(*url\_or\_urls: str | List\[str]*, *max\_concurrent: int = 10*, *limit: int | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Scrape content from one or more URLs using Firecrawl. This method extracts clean, structured content from web pages. When an API key is present, it automatically executes the request and returns Scenario objects. Without an API key, it raises an exception when called. > **Args:** **url\_or\_urls: Single URL string or list of URLs to scrape. Each URL should** be a valid HTTP/HTTPS URL. **max\_concurrent: Maximum number of concurrent requests when scraping multiple** URLs. Only applies when url\_or\_urls is a list. Defaults to 10. **limit: Maximum number of URLs to scrape when url\_or\_urls is a list. If None,** scrapes all provided URLs. Defaults to None. [\*\*](/en/latest/#id17)**kwargs: Additional scraping parameters:** **formats: List of output formats (e.g., \[“markdown”, “html”, “links”]).** Defaults to \[“markdown”]. **only\_main\_content: Whether to extract only main content, skipping** navigation, ads, etc. Defaults to True. include\_tags: List of HTML tags to specifically include in extraction. exclude\_tags: List of HTML tags to exclude from extraction. headers: Custom HTTP headers as a dictionary. wait\_for: Time to wait before scraping in milliseconds. timeout: Request timeout in milliseconds. actions: List of browser actions to perform before scraping (e.g., clicking, scrolling). > **Returns:** **When API key is present:** * For single URL: Scenario object containing scraped content * For multiple URLs: ScenarioList containing Scenario objects When API key is missing: Raises ValueError > **Raises:** ValueError: If FIRECRAWL\_API\_KEY is not found in environment, parameters, or .env file. > **Examples:** > **Basic scraping:** ```python theme={null} >>> firecrawl = FirecrawlRequest(api_key="your_key") >>> result = firecrawl.scrape("https://example.com") >>> print(result["content"]) # Scraped markdown content ``` > **Multiple URLs with custom options:** ```python theme={null} >>> urls = ["https://example.com", "https://example.org"] >>> results = firecrawl.scrape( ... urls, ... max_concurrent=5, ... limit=2, ... formats=["markdown", "html"], ... only_main_content=False ... ) ``` > **Descriptor pattern (no API key):** ```python theme={null} >>> class MyClass: ... scraper = FirecrawlRequest() # No exception here >>> # my_instance.scraper.scrape("url") # Would raise ValueError ``` > #### search(*query\_or\_queries: str | List\[str]*, *max\_concurrent: int = 5*, *limit: int | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Search the web and extract content from results using Firecrawl. This method performs web searches and automatically scrapes content from the search results. When an API key is present, it executes the search and returns ScenarioList objects with the results. Without an API key, it raises an exception. > **Args:** > **query\_or\_queries: Single search query string or list of queries to search for.** Each query should be a natural language search term. > **max\_concurrent: Maximum number of concurrent requests when processing multiple** queries. Only applies when query\_or\_queries is a list. Defaults to 5. > **limit: Maximum number of search results to return per query. If None, returns** all available results. Defaults to None. > [\*\*](/en/latest/#id19)**kwargs: Additional search parameters:** **sources: List of sources to search (e.g., \[“web”, “news”, “images”]).** Defaults to \[“web”]. location: Geographic location for localized search results. formats: List of output formats for scraped content from results (e.g., \[“markdown”, “html”]). > **Returns:** **When API key is present:** ScenarioList containing Scenario objects for each search result. Each scenario includes search metadata (query, position, result type) and scraped content from the result URL. When API key is missing: Raises ValueError > **Raises:** **ValueError: If FIRECRAWL\_API\_KEY is not found in environment, parameters,** or .env file. > **Examples:** > **Basic web search:** ```python theme={null} >>> firecrawl = FirecrawlRequest(api_key="your_key") >>> results = firecrawl.search("python web scraping") >>> for result in results: ... print(f"Title: {result['title']}") ... print(f"Content: {result['content'][:100]}...") ``` Multiple queries with options: ```python theme={null} >>> queries = ["AI research", "machine learning trends"] >>> results = firecrawl.search( ... queries, ... limit=5, ... sources=["web", "news"], ... location="US" ... ) ``` News search with custom formatting: ```python theme={null} >>> results = firecrawl.search( ... "climate change news", ... sources=["news"], ... formats=["markdown", "html"] ... ) ``` ### Request Creation Functions > #### edsl.scenarios.firecrawl\_scenario.create\_scrape\_request(*url\_or\_urls: str | List\[str]*, *api\_key: str | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Create a serializable scrape request. > #### edsl.scenarios.firecrawl\_scenario.create\_search\_request(*query\_or\_queries: str | List\[str]*, *api\_key: str | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Create a serializable search request. > #### edsl.scenarios.firecrawl\_scenario.create\_extract\_request(*url\_or\_urls: str | List\[str]*, *api\_key: str | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Create a serializable extract request. > #### edsl.scenarios.firecrawl\_scenario.create\_crawl\_request(*url: str*, *api\_key: str | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Create a serializable crawl request. > #### edsl.scenarios.firecrawl\_scenario.create\_map\_request(*url: str*, *api\_key: str | None = None*, \*\**kwargs*) → Dict\[str, Any][\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Create a serializable map\_urls request. > #### edsl.scenarios.firecrawl\_scenario.execute\_request(*request\_dict: Dict\[str, Any]*)[\[source\]](https://github.com/expectedparrot/edsl/blob/main/edsl/scenarios/firecrawl_scenario.py) Execute a serialized Firecrawl request and return results. # Getting Started Source: https://docs.expectedparrot.com/en/latest/getting_started To use EDSL, you need to install the package and choose how to access language models. ## 1. Installation Run the following command to install the EDSL package: ```bash theme={null} ! uv pip install edsl -q ``` If you have previously installed EDSL, you can check which version you have by running: ```bash theme={null} pip show edsl ``` (Compare it to the version shown at [PyPI](https://pypi.org/project/edsl/).) If it is out of date you can update it with the following command: ```bash theme={null} pip install --upgrade edsl ``` You can also access the source code at [GitHub](https://github.com/expectedparrot/edsl). If you are using EDSL with Google Colab, please see the [Colab Setup](/en/latest/colab_setup) section for special instructions. See [Installation](/en/latest/installation) instructions for more details and troubleshooting tips. ## 2. Create an account [Sign up](https://www.expectedparrot.com/login) for a free account with your email address. This allows you to run surveys remotely and collaborate with other users. Your account comes with \$25 in credits for API calls to any available models. Run the following code to log in to your account and store your Expected Parrot key: ```python theme={null} from edsl import login login() ``` ## 3. Choose where to run surveys You can use EDSL to run surveys locally on your own machine or remotely at the Expected Parrot server. To activate or deactivate remote inference, navigate to the [Settings](https://www.expectedparrot.com/home/settings) page of your account and toggle *remote inference* on or off. Remote inference allows you to use remote caching features and automatically store all your survey results. See the [Remote Inference](/en/latest/remote_inference) and [Remote Caching](/en/latest/remote_caching) sections for details. To run surveys locally you must provide your own keys from service providers. See the next step below for details. ## 4. Manage API keys for language models API keys are required to run surveys with language models. EDSL works with many language models from popular service providers, including Anthropic, Azure, Bedrock, DeepInfra, DeepSeek, Google, Mistral, OpenAI, Perplexity, Together and Xai. Current model pricing and performance information is available [here](https://www.expectedparrot.com/models). When you run a survey you can choose whether to use your own keys from service providers and/or an **Expected Parrot API key** to access all available models at once. There are several ways to manage your keys: * Manage keys at your Expected Parrot account (*recommended*) * Store keys in a .env file on your computer * Store keys directly in your EDSL code (*not recommended for security reasons*) If you have your own keys from service providers you can add them to your [Keys](https://www.expectedparrot.com/home/keys) page. You can also choose whether to grant access to other users and set usage limits. If you are running surveys locally, or only want to post and retrieve content at Expected Parrot without running your surveys remotely, you can store your keys in a .env file on your computer. This file should be in the same directory as your EDSL code and contain the following lines: ``` EXPECTED_PARROT_API_KEY=your_expected_parrot_key # required for remote inference and caching and posting content at Expected Parrot ANTHROPIC_API_KEY=your_anthropic_key GOOGLE_API_KEY=your_google_key OPENAI_API_KEY=your_openai_key # etc. ``` See the [Managing Keys](/en/latest/api_keys) section for details on this method of managing keys. **Note**: Note that if you use your own keys, you will be charged directly by the service provider for API calls. If you use your Expected Parrot API key, credits are deducted from your account based on the model and number of tokens used in the survey. ## 5. Run a survey Read the [Starter Tutorial](/en/latest/starter_tutorial) and [download a notebook](https://www.expectedparrot.com/content/179b3a78-2505-4568-acd9-c09d18953288) to create a survey and run it. See examples for many other use cases and [tips](/en/latest/checklist) on using EDSL effectively in the documentation. ## Support If you have any questions or need help, please send a message to [info@expectedparrot.com](mailto:info@expectedparrot.com). You can also [open at issue at GitHub](https://github.com/expectedparrot/edsl/issues/new?template=Blank+issue) to report bugs or request new features. Please also join our [Discord channel](https://discord.com/invite/mxAYkjfy9m) to ask questions and chat with other users! # Humanize Source: https://docs.expectedparrot.com/en/latest/humanize EDSL comes with built-in methods for generating web versions of your surveys and collecting and analyzing responses from humans. This page covers code methods for launching surveys, gathering human responses, and analyzing results. For interactive survey building, see [Survey Builder](/en/latest/survey_builder). For Prolific recruitment, see [Prolific studies](/en/latest/prolific). For email notifications and scheduled reminders, see [Survey notifications](/en/latest/humanize_notifications). To let AI agents take a survey, see [AI agent access](/en/latest/humanize_agent_access). ## How it works **Create** [Surveys](/en/latest/surveys) with desired types of [Questions](/en/latest/questions). **Humanize** the survey to generate a web-hosted version on Expected Parrot with a respondent link and an admin dashboard. **Share** the respondent link with human participants (or recruit via [Prolific](/en/latest/prolific)). **Collect** responses back as a standard EDSL `Results` object. **Analyze** your human results alongside any LLM results. *** ## Basic usage ```python theme={null} from edsl import Survey, QuestionMultipleChoice, QuestionFreeText, QuestionLinearScale q1 = QuestionMultipleChoice( question_name="frequency", question_text="How often do you work from home?", question_options=["Never", "1-2 days a week", "3-4 days a week", "Every day"], ) q2 = QuestionLinearScale( question_name="satisfaction", question_text="How satisfied are you with your current work arrangement?", question_options=[1, 2, 3, 4, 5], option_labels={1: "Very dissatisfied", 5: "Very satisfied"}, ) q3 = QuestionFreeText( question_name="improvement", question_text="What one change would most improve your day-to-day work experience?", ) survey = Survey([q1, q2, q3]) info = survey.humanize( human_survey_name="Workplace Experience Survey", survey_description="A short survey about remote and in-office work preferences.", survey_visibility="unlisted", ) print(info["respondent_url"]) # share with respondents print(info["admin_url"]) # monitor responses print(info["uuid"]) # save this to retrieve results later ``` ### `.humanize()` parameters | Parameter | Type | Default | Description | | - | - | - | - | | `human_survey_name` | str | `"New survey"` | Display name on Coop | | `survey_description` | str \| None | None | Optional description | | `survey_alias` | str \| None | None | URL-friendly alias | | `survey_visibility` | str | `"private"` | `"private"`, `"public"`, or `"unlisted"` | | `humanize_schema` | dict \| None | None | Controls question styling and optionality — see [Humanize schema](/en/latest/humanize_schema) | | `delivery_map` | DeliveryMap \| None | None | Maps delivery channels to agent trait columns; required for email notifications | ### Return value `survey.humanize()` returns a `Scenario` (dict-like) with these keys: | Key | Description | | - | - | | `uuid` | Human survey UUID — needed to fetch responses later | | `respondent_url` | Public link for respondents | | `admin_url` | Admin dashboard link | | `name` | Survey name | | `n_responses` | Response count (0 at creation) | | `survey_uuid` | UUID of the pushed survey object | | `scenario_list_uuid` | UUID of the pushed scenario list (if any) | | `asset_substitutions` | Old asset uuid → new one, when the schema named assets that were copied into your library. Empty when nothing was copied | **Save the `uuid`** — you need it to retrieve responses. *** ## Previewing before deployment Use `.preview()` to get a preview URL without creating a live survey: ```python theme={null} preview_url = survey.preview() print(preview_url) # With schema customization preview_url = survey.preview(humanize_schema=schema) ``` *** ## Present questions in groups To request one group per page, define contiguous question groups on the survey and pass `humanize_schema={"survey": {"presentation": "group"}}` to `.humanize()`. See [Grouped surveys](/en/latest/humanize_groups) for a complete example, branching behavior, and availability requirements. *** ## Add your logo To show your own logo above the questions, upload the image once and reference it from the humanize schema: ```python theme={null} from edsl import Coop logo = Coop().upload_human_survey_asset("lab_logo.png") survey.humanize( human_survey_name="Customer feedback", humanize_schema={ "survey": { "branding": { "logo": { "source": {"type": "asset", "asset_uuid": logo.uuid}, "alt": "Acme Research logo", } } } }, ) ``` Uploading the same file again returns the asset you already have, so re-running a script does not fill your library with copies. From the CLI, upload once and apply it to a survey that already exists: ```bash theme={null} ep humanize assets upload lab_logo.png ep humanize schema set --logo-asset --logo-alt "Acme Research logo" ``` Or do both in one step: ```bash theme={null} ep humanize schema set --logo-file lab_logo.png --logo-alt "Acme Research logo" ``` See [Humanize schema](/en/latest/humanize_schema) for the `branding` fields, and [Custom CSS](/en/latest/custom_css) for resizing the logo or putting it on a colored banner. *** ## With scenarios When using scenarios, call `.humanize()` on a `Jobs` object (not directly on the survey). You must specify a `scenario_list_method`. This example shows each respondent a different policy description and asks for their reaction: ```python theme={null} from edsl import Survey, QuestionFreeText, QuestionLinearScale, Scenario, ScenarioList scenarios = ScenarioList([ Scenario({"policy": "Four-day workweek with no change in pay"}), Scenario({"policy": "Flexible start times between 7am and 11am"}), Scenario({"policy": "Monthly remote-work stipend of $100"}), ]) q1 = QuestionLinearScale( question_name="support", question_text="How much do you support this policy: '{{ scenario.policy }}'?", question_options=[1, 2, 3, 4, 5], option_labels={1: "Strongly oppose", 5: "Strongly support"}, ) q2 = QuestionFreeText( question_name="reasoning", question_text="What is the main reason for your rating?", ) survey = Survey([q1, q2]) info = survey.by(scenarios).humanize( human_survey_name="Workplace Policy Reactions", scenario_list_method="randomize", ) ``` ### Scenario list methods | Method | Behavior | | - | - | | `"randomize"` | Each respondent gets a random scenario (with replacement) | | `"ordered"` | Scenarios are assigned sequentially to respondents | | `"loop"` | Survey is expanded — every question is repeated for every scenario | | `"single_scenario"` | Exactly one scenario is used for all respondents (list must have length 1) | **Rules:** * If you attach scenarios, you **must** specify `scenario_list_method`. * If you specify a method, you **must** attach scenarios. * Agents and models are **not supported** with humanize — the survey goes to real humans. *** ## With agent lists (for email notifications) Attach an `AgentList` via `.by()` to associate respondents with email addresses. Pass a `DeliveryMap` to tell Coop which agent trait holds the email address. ```python theme={null} from edsl import Survey, Agent, AgentList from edsl.coop.coop_humanize_notifications import ChannelConfig, DeliveryMap agents = AgentList([ Agent(traits={"name": "Jordan", "email": "jordan@example.com"}), Agent(traits={"name": "Taylor", "email": "taylor@example.com"}), Agent(traits={"name": "Morgan", "email": "morgan@example.com"}), ]) info = survey.by(agents).humanize( human_survey_name="Panel Study — Wave 1", agent_list_description="Recruited panel participants", delivery_map=DeliveryMap(email=ChannelConfig(col_name="email")), ) ``` `col_name` must match the trait key that holds each respondent's email address. Once deployed, use [Survey notifications](/en/latest/humanize_notifications) to send invitations, schedule reminders, and track delivery status. *** ## Retrieving responses ```python theme={null} from edsl import Coop coop = Coop() results = coop.get_human_survey_responses("your-human-survey-uuid") results.select("answer.frequency", "answer.satisfaction", "answer.improvement").print() ``` ### Checking response count ```python theme={null} status = coop.get_human_survey("your-human-survey-uuid") print(f"Responses received: {status['n_responses']}") ``` ### Working with file upload responses If your survey includes a `QuestionFileUpload` question, each response's answer for that question is a list of file-info dicts. Use `FileStore.from_file_upload_answer` (single file) or `FileStoreList.from_file_upload_answers` (all files at once) to download and wrap them: ```python theme={null} from edsl import Coop from edsl.scenarios import FileStore, FileStoreList coop = Coop() results = coop.get_human_survey_responses("your-human-survey-uuid") # Each answer is a list of uploaded file dicts uploaded_files = results[0]["answer"]["my_upload_question"] # Download all files as a FileStoreList fsl = FileStoreList.from_file_upload_answers(uploaded_files) # Or download a single file fs = FileStore.from_file_upload_answer(uploaded_files[0]) ``` The returned `FileStore` and `FileStoreList` objects behave like any other — you can inspect, filter, or pass them into further EDSL workflows. *** ## QR code Generate a QR code for the respondent link using `get_human_survey_qr_code()`. Requires the `qrcode` library (`pip install "edsl[full]"` or `pip install "qrcode[pil]"`). ```python theme={null} from edsl import Coop coop = Coop() qr = coop.get_human_survey_qr_code("your-human-survey-uuid") qr.save("qr_code.png") # save to file ``` From the CLI, generate a QR code for the shared respondent URL: ```bash theme={null} ep humanize qr --output survey-qr.png ``` When the human survey has an attached agent list, export the roster together with each respondent's personal link: ```bash theme={null} ep humanize links --output respondent-links.csv ``` Add `--qr-dir` to generate one personal QR code per respondent and record each PNG path in the CSV: ```bash theme={null} ep humanize links \ --output respondent-links.csv \ --qr-dir respondent-qr ``` Personal links and QR codes contain access tokens and act as bearer credentials. Store the export securely and share each link or code only with its intended respondent. The CLI returns only file metadata on stdout, never the links. *** ## Prolific integration For paid recruitment via Prolific, see the [Prolific studies](/en/latest/prolific) page. The short version: ```python theme={null} from edsl import Coop coop = Coop() info = survey.humanize(human_survey_name="Workplace Experience Survey") study = coop.create_prolific_study( human_survey_uuid=info["uuid"], name="Workplace Experience Study", description="A short survey about remote and in-office work preferences. Takes about 3 minutes.", num_participants=200, estimated_completion_time_minutes=3, participant_payment_cents=75, ) # create_prolific_study leaves the study in DRAFT mode — publish to make it live coop.publish_prolific_study(human_survey_uuid=info["uuid"]) ``` # AI agent access Source: https://docs.expectedparrot.com/en/latest/humanize_agent_access Let AI agents take a human survey through an agent link, and control who answers and what guidance they get. A human survey can let AI agents take it: a participant hands their AI agent the survey's agent link, and the agent answers through Expected Parrot's API instead of a browser. Agent access is off until the survey's owner turns it on, and only the owner can read or change it. The agent link is the respondent link with `/respond/human-surveys/` replaced by `/respond/agent/`: ```text theme={null} https://www.expectedparrot.com/respond/agent/ ``` It serves the agent its instructions. Each agent attempt is recorded as a response, alongside browser responses. *** ## The config | Field | Type | Default | Description | | - | - | - | - | | `enabled` | bool | `false` | Whether the agent link accepts new attempts. | | `participation_mode` | string | `"human_assisted"` | Who produces the answers (see below). | | `instructions` | string \| null | `null` | Guidance for agents on the whole survey, at most 4,000 characters. In autonomous mode, this is how the agent should answer. Advisory: nothing enforces it. | | `question_settings` | dict | `{}` | Per-question settings, keyed by question name. Each may have `instructions` (at most 2,000 characters). | ### Participation modes | Mode | Who answers | | - | - | | `human_assisted` | The participant. The agent asks each question and records their answers, and doesn't answer for them. | | `authorized_context` | The agent, from context the participant has authorized it to use, asking them whenever it's unsure. | | `autonomous` | The agent alone, as described in `instructions`. | In `autonomous` mode there's no participant, so: * The agent can't hand a page to a browser (see [What agents can't answer](#what-agents-cant-answer)). * A start with Prolific parameters is refused: autonomous responses can't be used for a paid panel. *** ## Reading and changing the config ```python theme={null} from edsl import Coop coop = Coop() access = coop.get_human_survey_agent_access("your-human-survey-uuid") # {"configured": True, "enabled": True, "participation_mode": "human_assisted", # "instructions": None, "question_settings": {}} ``` `configured` is `False` if agent access has never been set; the other fields are then their defaults. To change it, send only the fields you want to change: ```python theme={null} coop.patch_human_survey_agent_access( "your-human-survey-uuid", { "enabled": True, "participation_mode": "autonomous", "instructions": ( "Keep free-text answers to one or two sentences, and use the " "comment box to flag any answer that's an estimate." ), "question_settings": { "improvements": { "instructions": "Name at least one specific change, not a general comment." } }, }, survey=survey, # optional: check question names against the survey ) ``` The patch is merged into the stored config: * Fields left out are unchanged. * `question_settings` merges by question name: naming `improvements` changes only its settings. * `"instructions": None` clears the survey-wide instructions. * A question set to `None` in `question_settings` has its settings removed, e.g. `{"question_settings": {"improvements": None}}`. It returns the stored config, in the same shape as `get_human_survey_agent_access`. From the CLI, `ep humanize agent-access get ` prints the config, and `patch` changes it: ```bash theme={null} ep humanize agent-access patch --enabled --participation_mode autonomous ep humanize agent-access patch --question_instructions "improvements=Name at least one specific change, not a general comment." --survey survey.ep ``` `patch` also takes the patch as a JSON file with `--config`, and `--survey` checks question names as `survey=` does. *** ## Validation EDSL checks a patch before sending it, and raises `AgentAccessValidationError` if: * It has a key the config doesn't define. * A value is the wrong type or out of range, or `participation_mode` isn't one of the three modes. * `enabled`, `participation_mode` or `question_settings` is `None`. Only `instructions` and a single question's settings can be cleared that way. * With `survey=` (or `--survey`), it gives settings to a question that isn't in the survey. *** ## What agents can't answer Some questions need the participant themselves: * **Voice-only interviews.** In `human_assisted` and `authorized_context` mode, the agent hands the page to the participant: it gives them a one-time link to answer that page in their browser, then carries on. In `autonomous` mode there's no one to hand it to, so the survey can't be completed by an agent. * **Timed questions.** A question with a fixed time limit, in a survey that shows one question per page (where the browser runs the timer), can't be answered by an agent: a time limit can't be enforced over the API. The agent link tells the agent to stop and give the participant the browser link instead. Text interviews and file uploads are answered by the agent itself. *** ## In results Every response carries these fields, so you can tell agent responses apart: | Field | Description | | - | - | | `agent.collection_interface` | `browser`, or `agent_http` for an agent response. | | `agent.participation_mode` | The survey's mode when the attempt started. Empty for browser responses. | | `agent.client_name_self_reported` | The agent's client, as the agent reported it (e.g. `Claude Code`). Not verified. | | `agent.model_name_self_reported` | The agent's model, as the agent reported it. Not verified. | | `agent.handed_off_questions` | The questions the participant answered in their browser through a handoff. Empty for browser responses. | The survey's page on Expected Parrot also has an **AI agents** tab showing the current config. # Grouped surveys Source: https://docs.expectedparrot.com/en/latest/humanize_groups Define question groups and request one group per page in a human survey. Grouped presentation lets you put related questions on the same page. Define the page boundaries on your `Survey`, then pass `{"survey": {"presentation": "group"}}` to `.humanize()`. This guide describes the API introduced in [PR #2623](https://github.com/expectedparrot/edsl/pull/2623). It requires an EDSL version containing that change and a hosted Humanize implementation that supports grouped presentation. The PR adds the schema option and navigation API; it does not include the hosted UI implementation. ## Create a two-page survey This example puts two background questions on the first page and two feedback questions on the second. ```python theme={null} from edsl import Survey, QuestionFreeText, QuestionMultipleChoice survey = Survey([ QuestionMultipleChoice( question_name="work_location", question_text="Where do you usually work?", question_options=["At home", "In an office", "A mix of both"], ), QuestionFreeText( question_name="role", question_text="What is your role?", ), QuestionMultipleChoice( question_name="satisfaction", question_text="How satisfied are you with your work arrangement?", question_options=["Satisfied", "Neutral", "Dissatisfied"], ), QuestionFreeText( question_name="improvement", question_text="What would improve your work arrangement?", ), ]) # First question, last question (inclusive), group name. survey.add_question_group("work_location", "role", "background") survey.add_question_group("satisfaction", "improvement", "feedback") humanize_schema = { "survey": {"presentation": "group"}, "questions": {"improvement": {"optional": True}}, } ``` | Page | Group name | Questions | | - | - | - | | 1 | `background` | `work_location`, `role` | | 2 | `feedback` | `satisfaction`, `improvement` | Preview with the same schema you intend to use when creating the survey: ```python theme={null} preview_url = survey.preview(humanize_schema=humanize_schema) print(preview_url) ``` When ready, create the human survey: ```python theme={null} info = survey.humanize( human_survey_name="Workplace experience", humanize_schema=humanize_schema, ) print(info["respondent_url"]) print(info["admin_url"]) ``` The groups live on the survey; the schema selects how to present them. Adding groups alone does not enable grouped presentation. Omit `presentation`, or set it to `"question"`, for the default question-by-question mode. ## Set group boundaries `survey.add_question_group(start_question, end_question, group_name)` accepts question names or question objects and returns the survey for chaining. * Each group includes both endpoints and every question between them. * Groups must not overlap. A one-question page uses the same question for both endpoints. * Names must be unique Python identifiers, such as `background`, and must not match a question name. * Pages follow question order in the survey. * Assign every question you want displayed to a group. With groups defined, the current grouped navigator skips questions outside those groups. For example, to make a page containing only `work_location`, define its group with `survey.add_question_group("work_location", "work_location", "location_page")` on a survey where that question is not already grouped. ## Plan branching around pages The navigator uses the answers supplied when a page is requested to filter skipped questions. After a page is submitted, it applies the normal navigation rules from that group's **last renderable question** to choose the next page. Put page-level jump or stop rules on that question. If a later question depends on an earlier answer, put the dependent question on a later page so that answer is available when the page is selected. The navigation API does not establish live, within-page updates to conditional questions. For example, starting with the two-page survey above, omit the improvement question for satisfied respondents: ```python theme={null} # Keep the controlling answer on an earlier page. survey.question_groups.clear() survey.add_question_group("work_location", "satisfaction", "background") survey.add_question_group("improvement", "improvement", "improvements") survey.add_skip_rule( "improvement", "{{ satisfaction.answer }} == 'Satisfied'", ) ``` Now the first page collects work location, role, and satisfaction. A satisfied respondent skips the improvements page and reaches the end; other respondents continue to it. Add the rule before previewing or creating the human survey. Additional navigation behavior: * A group with no remaining questions is skipped. * A jump into the middle of a group starts at the target question; earlier questions in that group are omitted. * Relevant instructions before or within the selected group are included by default. ## Inspect navigation locally Survey authors normally define groups and call `.humanize()`. For debugging or implementing a renderer, `survey.next_group()` returns the next page without creating a hosted survey. Using the original two-page example, before the branching modification: ```python theme={null} first_page = survey.next_group() assert first_page["group_name"] == "background" assert first_page["question_names"] == ["work_location", "role"] second_page = survey.next_group( current_group="background", answers={ "work_location.answer": "At home", "role.answer": "Researcher", }, ) assert second_page["group_name"] == "feedback" ``` Pass all answers collected so far, including those from the just-submitted group. Use `include_instructions=False` to return questions only. | Returned field | Meaning | | - | - | | `group_name` | Selected group name, or `None` at the end | | `items` | Question and instruction objects; `[EndOfSurvey]` at the end | | `question_names` | Names of the questions selected for this page | | `is_end` | `True` when the survey has finished | See [Humanize](/en/latest/humanize) for retrieving responses and [Humanize schema](/en/latest/humanize_schema) for styling and other presentation options. # Survey notifications Source: https://docs.expectedparrot.com/en/latest/humanize_notifications Send emails, schedule deliveries, and react to respondent events for human surveys. Human surveys support email delivery to respondents, scheduled send campaigns, and event-triggered callbacks. All of these are managed through `HumanSurveyNotificationHandler`, a convenience wrapper that binds to a specific human survey so you don't have to pass the UUID on every call. The survey must have an agent list with an email delivery channel configured before any notifications can be sent. Set this up when calling `survey.humanize()` by passing `agent_list` and `delivery_map` arguments. ## Getting a handler ```python theme={null} from edsl.coop import HumanSurveyNotificationHandler handler = HumanSurveyNotificationHandler("your-human-survey-uuid") ``` A `Coop` client is created automatically. Pass one explicitly if you need a specific API key or base URL: ```python theme={null} from edsl import Coop from edsl.coop import HumanSurveyNotificationHandler coop = Coop() handler = HumanSurveyNotificationHandler("your-human-survey-uuid", coop=coop) ``` *** ## Statuses Every delivery task moves through two independent status tracks. ### Dispatch status Controls whether the platform has attempted to hand off the message to the downstream channel (e.g. the email provider). | Value | Meaning | | - | - | | `pending` | The task has been created but not yet processed. | | `dispatched` | The message was successfully handed off to the channel. | | `failed` | The dispatch attempt failed (e.g. missing address, provider error). | ### Delivery status Reflects what the email provider reports back after dispatch. | Value | Meaning | | - | - | | `pending` | Waiting for a delivery event from the provider. | | `sent` | The provider accepted the message for delivery. | | `delivered` | The provider confirmed the message reached the recipient. | | `bounced` | The message was rejected by the recipient's mail server. | | `failed` | The provider reported an error sending the email. | ### Response status Tracks whether a respondent has engaged with the survey itself. | Value | Meaning | | - | - | | `not_started` | The respondent has not opened the survey link. | | `completed` | The respondent submitted a complete response. | *** ## Quickstart examples ### Send an email right now Trigger an immediate delivery to all respondents. ```python theme={null} result = handler.send_respondent_email(name="Initial outreach") print(result) # {"delivery_uuid": "...", "routes": [...]} ``` Pass a custom subject, HTML email body, or restrict which respondents receive it: ```python theme={null} from edsl.coop.coop_humanize_notifications import ( HumanizeRespondentFilter, RespondentCondition, ResponseStatus, ) # Only email respondents who haven't started yet condition = RespondentCondition( response_status=[ResponseStatus.not_started] ) respondent_filter = HumanizeRespondentFilter(conditions=[condition]) result = handler.send_respondent_email( name="Reminder to non-starters", subject="Don't forget to complete our survey", delivery_template="

Hi! Just a reminder to complete our survey.

", respondent_filter=respondent_filter, ) ``` *** ### Schedule a one-time email Schedule a single delivery at a specific time. `run_at` must be a timezone-aware datetime or an ISO 8601 string. ```python theme={null} from datetime import datetime, timezone run_at = datetime(2026, 6, 1, 9, 0, tzinfo=timezone.utc) schedule = handler.create_one_time_schedule( name="June 1st reminder", run_at=run_at, ) print(schedule["schedule_uuid"]) ``` Update the time before it fires: ```python theme={null} handler.update_one_time_schedule( schedule_uuid=schedule["schedule_uuid"], run_at="2026-06-02T09:00:00+00:00", ) ``` *** ### Schedule recurring emails (cron) Send emails on a repeating schedule using standard cron syntax. Provide either `max_jobs` (fire at most N times) or `deadline` (stop firing after this datetime) as the termination condition. Use `start_at` to delay when the schedule begins firing; omit it to start immediately. ```python theme={null} from datetime import datetime, timezone # Every Monday at 9 AM Eastern, up to 4 times schedule = handler.create_cron_schedule( name="Weekly Monday reminder", cron_expression="0 9 * * MON", timezone="America/New_York", max_jobs=4, ) ``` With a deadline instead: ```python theme={null} schedule = handler.create_cron_schedule( name="Daily reminders through end of month", cron_expression="0 8 * * *", timezone="America/Chicago", deadline=datetime(2026, 6, 30, 23, 59, tzinfo=timezone.utc), ) ``` With a delayed start: ```python theme={null} schedule = handler.create_cron_schedule( name="Weekly reminders starting next month", cron_expression="0 9 * * MON", timezone="America/New_York", max_jobs=4, start_at=datetime(2026, 6, 1, tzinfo=timezone.utc), ) ``` Pause and resume a schedule: ```python theme={null} handler.deactivate_schedule(schedule["schedule_uuid"]) handler.activate_schedule(schedule["schedule_uuid"]) ``` *** ### React when someone completes the survey (callback) A **respondent** is a person with a corresponding entry in the survey's agent list. Each respondent has a unique survey link and a tracked response status. Submissions from people outside the agent list (e.g. anonymous links) are not associated with a respondent. `create_callback` sends emails to respondents in your agent list. Two respondent event types are supported: | Event | When it fires | | - | - | | `"human_survey_respondent.completed"` | When a respondent's status is set to completed. | | `"human_survey_respondent.response_submitted"` | Each time a respondent submits a response. | ```python theme={null} callback = handler.create_callback( name="Completion thank-you", callback_type="human_survey_respondent.completed", ) print(callback["callback_uuid"]) ``` Limit how many times the callback fires (e.g. only notify for the first 100 completions): ```python theme={null} callback = handler.create_callback( name="First 100 completions", callback_type="human_survey_respondent.completed", max_fires=100, ) ``` ### Notify the owner on every new response Use `create_owner_response_callback` to email the survey owner each time a response is submitted. This fires for all responses, including anonymous ones — not just respondents in the agent list. ```python theme={null} callback = handler.create_owner_response_callback(name="New response notification") print(callback["callback_uuid"]) ``` ### Send a transcript after each submission Use `create_transcript_callback` to automatically email a transcript each time a response is submitted. Pass `recipient` to control who receives it: * `"respondent"` (default) — emails each respondent a copy of their own answers. * `"owner"` — emails the survey owner a copy of the answers. Fires for all submissions, including anonymous ones. ```python theme={null} # Email the respondent their own transcript callback = handler.create_transcript_callback( name="Send respondent transcript on submission", recipient="respondent", ) # Email the owner a transcript on every submission callback = handler.create_transcript_callback( name="Send owner transcript on submission", recipient="owner", ) print(callback["callback_uuid"]) ``` Activate and deactivate: ```python theme={null} handler.deactivate_callback(callback["callback_uuid"]) handler.activate_callback(callback["callback_uuid"]) ``` *** ### Filter which respondents receive a delivery `HumanizeRespondentFilter` lets you compose conditions to target specific subsets of respondents. Conditions can be nested with `and` / `or` operators. ```python theme={null} from edsl.coop.coop_humanize_notifications import ( HumanizeRespondentFilter, RespondentCondition, DeliveryStatus, DispatchStatus, ResponseStatus, FilterOperator, ) # Target respondents who were never contacted never_contacted = RespondentCondition(never_contacted=True) # OR respondents whose last email bounced bounced = RespondentCondition( most_recent_delivery_status=[DeliveryStatus.bounced] ) # Combine: never contacted OR previously bounced respondent_filter = HumanizeRespondentFilter( operator=FilterOperator.or_, conditions=[never_contacted, bounced], ) handler.send_respondent_email( name="Re-engagement", respondent_filter=respondent_filter, ) ``` **Available condition fields:** | Field | Type | Filters to respondents where… | | - | - | - | | `respondent_uuids` | `list[str]` | UUID is in the provided list | | `response_status` | `list[ResponseStatus]` | Survey response status matches | | `never_contacted` | `bool` | Has never received a delivery | | `any_dispatch_status` | `list[DispatchStatus]` | Any past task has this dispatch status | | `any_delivery_status` | `list[DeliveryStatus]` | Any past task has this delivery status | | `most_recent_dispatch_status` | `list[DispatchStatus]` | Most recent task has this dispatch status | | `most_recent_delivery_status` | `list[DeliveryStatus]` | Most recent task has this delivery status | *** ### Check delivery status List all delivery jobs for the survey: ```python theme={null} deliveries = handler.list_deliveries() for d in deliveries["deliveries"]: print(d["delivery_uuid"], d["status"], d["sent_count"], d["failed_count"]) ``` Inspect a specific delivery job: ```python theme={null} delivery = handler.get_delivery(delivery_uuid="...") print(delivery) # { # "delivery_uuid": "...", # "status": "completed", # "total_respondents": 50, # "processed_respondents": 50, # "sent_count": 48, # "failed_count": 2, # "started_at": "...", # "completed_at": "..." # } ``` Drill into individual tasks (one per respondent per delivery): ```python theme={null} tasks = handler.list_delivery_tasks(delivery_uuid="...") for task in tasks["tasks"]: print(task["identifier"], task["dispatch_status"], task["delivery_status"]) ``` Get a single delivery task by UUID: ```python theme={null} task = handler.get_delivery_task(task_uuid="...") print(task) # { # "task_uuid": "...", # "channel": "email", # "identifier": "respondent@example.com", # "dispatch_status": "dispatched", # "delivery_status": "delivered", # "respondent": {"respondent_uuid": "...", "response_status": "completed"}, # ... # } ``` *** ## Route types Each delivery or schedule sends email to survey respondents via a `RespondentEmailRouteConfig` route. A delivery template is always required; convenience methods like `send_respondent_email` default to the built-in invitation template when none is provided. `RespondentEmailRouteConfig` options: | Field | Description | | - | - | | `delivery_template` | The email body. Pass an HTML string to use a custom template, or omit to use the built-in invitation template. | | `respondent_filter` | `HumanizeRespondentFilter` to restrict which respondents receive the email. | | `subject` | Email subject line (1-200 characters). Uses the default subject when omitted. | ### Template variables Custom HTML templates support the following variables, which are filled in per-respondent at send time: | Variable | Value | | - | - | | `{{ survey_name }}` | The name of the human survey. | | `{{ url }}` | The respondent's unique survey link (includes their access token). | Example: ```python theme={null} handler.send_respondent_email( name="Initial outreach", subject="You're invited to take our survey", delivery_template="""

Hi there,

You've been invited to take {{ survey_name }}.

Click here to begin

""", ) ``` Update the subject and template on an existing respondent email route on a schedule: ```python theme={null} handler.patch_schedule_respondent_email_route( schedule_uuid="...", route_uuid="...", subject="A quick reminder about our survey", delivery_template="

Hi! Please take a moment to complete our survey.

", ) ``` Update the subject and template on an existing respondent email route on a callback: ```python theme={null} handler.patch_callback_respondent_email_route( callback_uuid="...", route_uuid="...", subject="Thanks for completing our survey", delivery_template="

Hi! Thank you for completing {{ survey_name }}.

", ) ``` *** ## Managing respondents List all respondents and their current response status: ```python theme={null} respondents = handler.get_respondents() for r in respondents["respondents"]: print(r["respondent_uuid"], r["response_status"]) ``` Check and update the agent list delivery configuration (the trait column that holds each respondent's email address): ```python theme={null} from edsl.coop.coop_humanize_notifications import ChannelConfig, DeliveryMap # See current config config = handler.get_agent_list() print(config["agent_list_config"]) # Update the email column mapping handler.patch_agent_list( delivery_map=DeliveryMap(email=ChannelConfig(col_name="email_address")) ) ``` # Humanize schema Source: https://docs.expectedparrot.com/en/latest/humanize_schema Reference for the humanize schema: a dict that configures how survey questions are presented in web surveys with human respondents. When creating a human survey or preview (e.g. `survey.humanize()` or `survey.preview()`), you can pass a **humanize schema** to control styling and optionality per question. The schema is validated against your survey before use. ## Fields Map of question name (string) -> `HumanizeQuestionSchema`. Optional survey-level options (e.g. custom CSS). `"question"` requests question-by-question presentation (the default); `"group"` requests one question group per page. Define groups on the survey with `add_question_group()` and include every question you want displayed. See [Grouped surveys](/en/latest/humanize_groups) for setup, navigation behavior, and availability requirements for PR #2623. Question time limits (`time_limit`) are ignored under `"group"`. Optional custom CSS for the survey. Can be `null` or omitted. See [Custom CSS](/en/latest/custom_css) for the `edsl-*` CSS classes and HTML structure per question type. How the respondent is shown their position in the survey. Omit to keep the progress bar, which is what surveys render by default. Which progress indicator to render. * `"bar"` — a filled bar, optionally labelled (default). * `"hidden"` — no progress indicator anywhere in the survey. * `"steps"` — markers joined by a connector, one per step, with the current step highlighted. **`bar` only.** The label beneath the bar. Set to `null` to keep the bar and drop the label. Must be `"percent"` — the share of the survey completed, e.g. `"33%"`. **`steps` only.** Whether each marker carries its step number (`"number"`) or is a bare dot (`"dot"`). **`steps` only.** The steps, in order. At least two are required. A step is a *boundary*, not a bucket: it covers every survey item from the end of the previous step through its `complete_after`. Questions can be added or removed inside a step without touching this config, and items a respondent skips past inside a step don't move the marker. Each step is an object with: * `label` (string, optional) — the word under the marker, 1–60 characters. Omit or set to `null` to render the marker alone. * `complete_after` (string, optional) — the name of the survey item (question or instruction) after which this step is complete. Only the final step may omit it, which runs that step to the end of the survey; every earlier step must name an item. The same item must not be named by two steps. The author's brand on the respondent page. Omit or set to `null` for no banner, which is how a survey without this field renders. The logo drawn in the survey banner, above the questions. Set to `null` to remove it. Upload the image first, with `Coop().upload_human_survey_asset("lab_logo.png")` or `ep humanize assets upload lab_logo.png`, and reference the uuid it returns. A schema never carries image bytes. There is no size field: the banner picks a default height. Style `.edsl-logo` in `custom_css` to change it. See [Custom CSS](/en/latest/custom_css). Where the image comes from. Must be `{"type": "asset", "asset_uuid": ""}`, naming an image in your asset library. Alt text for screen readers, up to 200 characters. Required, so that leaving it out is a decision rather than an accident. Use an empty string to mark the logo decorative, which renders it with `alt=""`. Where the logo sits in the banner: `"left"`, `"center"`, or `"right"`. ## HumanizeQuestionSchema (per question) Each `questions[question_name]` value is a `HumanizeQuestionSchema` object. Whether the question is optional. Default is `false` when omitted. **Supported question types:** `free_text`, `budget`, `checkbox`, `checkbox_with_other`, `file_upload`, `interview`, `likert_five`, `linear_scale`, `list`, `matrix`, `multiple_choice`, `multiple_choice_with_other`, `numerical`, `rank`, `top_k`, `yes_no`. Note: `optional` currently has no effect for `interview` and `rank`. A limit on how long the respondent may spend answering the question. Omit or set to `null` for no limit. While the limit runs, a countdown is shown above the question. When it reaches zero the answer locks: the respondent can no longer change it and clicks **Next** to continue. They are not moved on automatically. Whatever they had entered is submitted as their answer, and a question they had not answered is recorded as unanswered, even if it is not `optional`. A time limit applies only while the question is alone on its page. When `survey.presentation` is `"group"`, every question's `time_limit` is ignored, including a question that is the only one in its group. The countdown runs in the respondent's browser, so reloading the page starts it again. **Supported question types:** `free_text`, `budget`, `checkbox`, `checkbox_with_other`, `file_upload`, `likert_five`, `linear_scale`, `list`, `matrix`, `multiple_choice`, `multiple_choice_with_other`, `numerical`, `rank`, `top_k`, `yes_no`. How long the respondent has. Must be `{"type": "fixed", "seconds": }`, the same number of seconds for every respondent. `seconds` must be between 30 and 7200 (two hours). `type` is required, even though `"fixed"` is its only value. Display format. Varies by question type: * **MC-style** (`likert_five`, `linear_scale`, `multiple_choice`, `yes_no`): `"radio"` (list of radio buttons) or `"dropdown"` (single `