edsl 0.1.37.dev2__py3-none-any.whl → 0.1.37.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. edsl/Base.py +303 -303
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +48 -48
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +804 -804
  7. edsl/agents/AgentList.py +345 -345
  8. edsl/agents/Invigilator.py +222 -222
  9. edsl/agents/InvigilatorBase.py +305 -305
  10. edsl/agents/PromptConstructor.py +312 -312
  11. edsl/agents/__init__.py +3 -3
  12. edsl/agents/descriptors.py +86 -86
  13. edsl/agents/prompt_helpers.py +129 -129
  14. edsl/auto/AutoStudy.py +117 -117
  15. edsl/auto/StageBase.py +230 -230
  16. edsl/auto/StageGenerateSurvey.py +178 -178
  17. edsl/auto/StageLabelQuestions.py +125 -125
  18. edsl/auto/StagePersona.py +61 -61
  19. edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
  20. edsl/auto/StagePersonaDimensionValues.py +74 -74
  21. edsl/auto/StagePersonaDimensions.py +69 -69
  22. edsl/auto/StageQuestions.py +73 -73
  23. edsl/auto/SurveyCreatorPipeline.py +21 -21
  24. edsl/auto/utilities.py +224 -224
  25. edsl/base/Base.py +289 -289
  26. edsl/config.py +149 -149
  27. edsl/conjure/AgentConstructionMixin.py +152 -152
  28. edsl/conjure/Conjure.py +62 -62
  29. edsl/conjure/InputData.py +659 -659
  30. edsl/conjure/InputDataCSV.py +48 -48
  31. edsl/conjure/InputDataMixinQuestionStats.py +182 -182
  32. edsl/conjure/InputDataPyRead.py +91 -91
  33. edsl/conjure/InputDataSPSS.py +8 -8
  34. edsl/conjure/InputDataStata.py +8 -8
  35. edsl/conjure/QuestionOptionMixin.py +76 -76
  36. edsl/conjure/QuestionTypeMixin.py +23 -23
  37. edsl/conjure/RawQuestion.py +65 -65
  38. edsl/conjure/SurveyResponses.py +7 -7
  39. edsl/conjure/__init__.py +9 -9
  40. edsl/conjure/naming_utilities.py +263 -263
  41. edsl/conjure/utilities.py +201 -201
  42. edsl/conversation/Conversation.py +238 -238
  43. edsl/conversation/car_buying.py +58 -58
  44. edsl/conversation/mug_negotiation.py +81 -81
  45. edsl/conversation/next_speaker_utilities.py +93 -93
  46. edsl/coop/PriceFetcher.py +54 -54
  47. edsl/coop/__init__.py +2 -2
  48. edsl/coop/coop.py +827 -824
  49. edsl/coop/utils.py +131 -131
  50. edsl/data/Cache.py +527 -527
  51. edsl/data/CacheEntry.py +228 -228
  52. edsl/data/CacheHandler.py +149 -149
  53. edsl/data/RemoteCacheSync.py +97 -97
  54. edsl/data/SQLiteDict.py +292 -292
  55. edsl/data/__init__.py +4 -4
  56. edsl/data/orm.py +10 -10
  57. edsl/data_transfer_models.py +73 -73
  58. edsl/enums.py +173 -173
  59. edsl/exceptions/__init__.py +50 -50
  60. edsl/exceptions/agents.py +40 -40
  61. edsl/exceptions/configuration.py +16 -16
  62. edsl/exceptions/coop.py +10 -10
  63. edsl/exceptions/data.py +14 -14
  64. edsl/exceptions/general.py +34 -34
  65. edsl/exceptions/jobs.py +33 -33
  66. edsl/exceptions/language_models.py +63 -63
  67. edsl/exceptions/prompts.py +15 -15
  68. edsl/exceptions/questions.py +91 -91
  69. edsl/exceptions/results.py +26 -26
  70. edsl/exceptions/surveys.py +34 -34
  71. edsl/inference_services/AnthropicService.py +87 -87
  72. edsl/inference_services/AwsBedrock.py +120 -115
  73. edsl/inference_services/AzureAI.py +217 -217
  74. edsl/inference_services/DeepInfraService.py +18 -18
  75. edsl/inference_services/GoogleService.py +156 -156
  76. edsl/inference_services/GroqService.py +20 -20
  77. edsl/inference_services/InferenceServiceABC.py +147 -147
  78. edsl/inference_services/InferenceServicesCollection.py +74 -74
  79. edsl/inference_services/MistralAIService.py +123 -123
  80. edsl/inference_services/OllamaService.py +18 -18
  81. edsl/inference_services/OpenAIService.py +224 -224
  82. edsl/inference_services/TestService.py +89 -89
  83. edsl/inference_services/TogetherAIService.py +170 -170
  84. edsl/inference_services/models_available_cache.py +118 -118
  85. edsl/inference_services/rate_limits_cache.py +25 -25
  86. edsl/inference_services/registry.py +39 -39
  87. edsl/inference_services/write_available.py +10 -10
  88. edsl/jobs/Answers.py +56 -56
  89. edsl/jobs/Jobs.py +1135 -1112
  90. edsl/jobs/__init__.py +1 -1
  91. edsl/jobs/buckets/BucketCollection.py +63 -63
  92. edsl/jobs/buckets/ModelBuckets.py +65 -65
  93. edsl/jobs/buckets/TokenBucket.py +248 -248
  94. edsl/jobs/interviews/Interview.py +661 -661
  95. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  96. edsl/jobs/interviews/InterviewExceptionEntry.py +182 -182
  97. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  98. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  99. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  100. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  101. edsl/jobs/interviews/ReportErrors.py +66 -66
  102. edsl/jobs/interviews/interview_status_enum.py +9 -9
  103. edsl/jobs/runners/JobsRunnerAsyncio.py +338 -338
  104. edsl/jobs/runners/JobsRunnerStatus.py +332 -332
  105. edsl/jobs/tasks/QuestionTaskCreator.py +242 -242
  106. edsl/jobs/tasks/TaskCreators.py +64 -64
  107. edsl/jobs/tasks/TaskHistory.py +441 -441
  108. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  109. edsl/jobs/tasks/task_status_enum.py +163 -163
  110. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  111. edsl/jobs/tokens/TokenUsage.py +34 -34
  112. edsl/language_models/LanguageModel.py +718 -718
  113. edsl/language_models/ModelList.py +102 -102
  114. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  115. edsl/language_models/__init__.py +2 -2
  116. edsl/language_models/fake_openai_call.py +15 -15
  117. edsl/language_models/fake_openai_service.py +61 -61
  118. edsl/language_models/registry.py +137 -137
  119. edsl/language_models/repair.py +156 -156
  120. edsl/language_models/unused/ReplicateBase.py +83 -83
  121. edsl/language_models/utilities.py +64 -64
  122. edsl/notebooks/Notebook.py +259 -259
  123. edsl/notebooks/__init__.py +1 -1
  124. edsl/prompts/Prompt.py +353 -353
  125. edsl/prompts/__init__.py +2 -2
  126. edsl/questions/AnswerValidatorMixin.py +289 -289
  127. edsl/questions/QuestionBase.py +616 -616
  128. edsl/questions/QuestionBaseGenMixin.py +161 -161
  129. edsl/questions/QuestionBasePromptsMixin.py +266 -266
  130. edsl/questions/QuestionBudget.py +227 -227
  131. edsl/questions/QuestionCheckBox.py +359 -359
  132. edsl/questions/QuestionExtract.py +183 -183
  133. edsl/questions/QuestionFreeText.py +114 -114
  134. edsl/questions/QuestionFunctional.py +159 -159
  135. edsl/questions/QuestionList.py +231 -231
  136. edsl/questions/QuestionMultipleChoice.py +286 -286
  137. edsl/questions/QuestionNumerical.py +153 -153
  138. edsl/questions/QuestionRank.py +324 -324
  139. edsl/questions/Quick.py +41 -41
  140. edsl/questions/RegisterQuestionsMeta.py +71 -71
  141. edsl/questions/ResponseValidatorABC.py +174 -174
  142. edsl/questions/SimpleAskMixin.py +73 -73
  143. edsl/questions/__init__.py +26 -26
  144. edsl/questions/compose_questions.py +98 -98
  145. edsl/questions/decorators.py +21 -21
  146. edsl/questions/derived/QuestionLikertFive.py +76 -76
  147. edsl/questions/derived/QuestionLinearScale.py +87 -87
  148. edsl/questions/derived/QuestionTopK.py +91 -91
  149. edsl/questions/derived/QuestionYesNo.py +82 -82
  150. edsl/questions/descriptors.py +418 -418
  151. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  152. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  153. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  154. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  155. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  156. edsl/questions/prompt_templates/question_list.jinja +17 -17
  157. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  158. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  159. edsl/questions/question_registry.py +147 -147
  160. edsl/questions/settings.py +12 -12
  161. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  162. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  163. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  164. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  165. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  166. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  167. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  168. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  169. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  170. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  171. edsl/questions/templates/list/question_presentation.jinja +5 -5
  172. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  173. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  174. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  175. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  176. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  177. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  178. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  179. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  180. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  181. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  182. edsl/results/Dataset.py +293 -293
  183. edsl/results/DatasetExportMixin.py +693 -693
  184. edsl/results/DatasetTree.py +145 -145
  185. edsl/results/Result.py +435 -435
  186. edsl/results/Results.py +1160 -1160
  187. edsl/results/ResultsDBMixin.py +238 -238
  188. edsl/results/ResultsExportMixin.py +43 -43
  189. edsl/results/ResultsFetchMixin.py +33 -33
  190. edsl/results/ResultsGGMixin.py +121 -121
  191. edsl/results/ResultsToolsMixin.py +98 -98
  192. edsl/results/Selector.py +118 -118
  193. edsl/results/__init__.py +2 -2
  194. edsl/results/tree_explore.py +115 -115
  195. edsl/scenarios/FileStore.py +458 -458
  196. edsl/scenarios/Scenario.py +510 -510
  197. edsl/scenarios/ScenarioHtmlMixin.py +59 -59
  198. edsl/scenarios/ScenarioList.py +1101 -1101
  199. edsl/scenarios/ScenarioListExportMixin.py +52 -52
  200. edsl/scenarios/ScenarioListPdfMixin.py +261 -261
  201. edsl/scenarios/__init__.py +4 -4
  202. edsl/shared.py +1 -1
  203. edsl/study/ObjectEntry.py +173 -173
  204. edsl/study/ProofOfWork.py +113 -113
  205. edsl/study/SnapShot.py +80 -80
  206. edsl/study/Study.py +528 -528
  207. edsl/study/__init__.py +4 -4
  208. edsl/surveys/DAG.py +148 -148
  209. edsl/surveys/Memory.py +31 -31
  210. edsl/surveys/MemoryPlan.py +244 -244
  211. edsl/surveys/Rule.py +324 -324
  212. edsl/surveys/RuleCollection.py +387 -387
  213. edsl/surveys/Survey.py +1772 -1772
  214. edsl/surveys/SurveyCSS.py +261 -261
  215. edsl/surveys/SurveyExportMixin.py +259 -259
  216. edsl/surveys/SurveyFlowVisualizationMixin.py +121 -121
  217. edsl/surveys/SurveyQualtricsImport.py +284 -284
  218. edsl/surveys/__init__.py +3 -3
  219. edsl/surveys/base.py +53 -53
  220. edsl/surveys/descriptors.py +56 -56
  221. edsl/surveys/instructions/ChangeInstruction.py +47 -47
  222. edsl/surveys/instructions/Instruction.py +51 -51
  223. edsl/surveys/instructions/InstructionCollection.py +77 -77
  224. edsl/templates/error_reporting/base.html +23 -23
  225. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  226. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  227. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  228. edsl/templates/error_reporting/interview_details.html +115 -115
  229. edsl/templates/error_reporting/interviews.html +9 -9
  230. edsl/templates/error_reporting/overview.html +4 -4
  231. edsl/templates/error_reporting/performance_plot.html +1 -1
  232. edsl/templates/error_reporting/report.css +73 -73
  233. edsl/templates/error_reporting/report.html +117 -117
  234. edsl/templates/error_reporting/report.js +25 -25
  235. edsl/tools/__init__.py +1 -1
  236. edsl/tools/clusters.py +192 -192
  237. edsl/tools/embeddings.py +27 -27
  238. edsl/tools/embeddings_plotting.py +118 -118
  239. edsl/tools/plotting.py +112 -112
  240. edsl/tools/summarize.py +18 -18
  241. edsl/utilities/SystemInfo.py +28 -28
  242. edsl/utilities/__init__.py +22 -22
  243. edsl/utilities/ast_utilities.py +25 -25
  244. edsl/utilities/data/Registry.py +6 -6
  245. edsl/utilities/data/__init__.py +1 -1
  246. edsl/utilities/data/scooter_results.json +1 -1
  247. edsl/utilities/decorators.py +77 -77
  248. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  249. edsl/utilities/interface.py +627 -627
  250. edsl/utilities/repair_functions.py +28 -28
  251. edsl/utilities/restricted_python.py +70 -70
  252. edsl/utilities/utilities.py +391 -391
  253. {edsl-0.1.37.dev2.dist-info → edsl-0.1.37.dev4.dist-info}/LICENSE +21 -21
  254. {edsl-0.1.37.dev2.dist-info → edsl-0.1.37.dev4.dist-info}/METADATA +1 -1
  255. edsl-0.1.37.dev4.dist-info/RECORD +279 -0
  256. edsl-0.1.37.dev2.dist-info/RECORD +0 -279
  257. {edsl-0.1.37.dev2.dist-info → edsl-0.1.37.dev4.dist-info}/WHEEL +0 -0
edsl/agents/Agent.py CHANGED
@@ -1,804 +1,804 @@
1
- """An Agent is an AI agent that can reference a set of traits in answering questions."""
2
-
3
- from __future__ import annotations
4
- import copy
5
- import inspect
6
- import types
7
- from typing import Callable, Optional, Union, Any
8
- from uuid import uuid4
9
- from edsl.Base import Base
10
-
11
- from edsl.prompts import Prompt
12
- from edsl.exceptions import QuestionScenarioRenderError
13
-
14
- from edsl.exceptions.agents import (
15
- AgentCombinationError,
16
- AgentDirectAnswerFunctionError,
17
- AgentDynamicTraitsFunctionError,
18
- )
19
-
20
- from edsl.agents.descriptors import (
21
- TraitsDescriptor,
22
- CodebookDescriptor,
23
- InstructionDescriptor,
24
- NameDescriptor,
25
- )
26
- from edsl.utilities.decorators import (
27
- sync_wrapper,
28
- add_edsl_version,
29
- remove_edsl_version,
30
- )
31
- from edsl.data_transfer_models import AgentResponseDict
32
- from edsl.utilities.restricted_python import create_restricted_function
33
-
34
-
35
- class Agent(Base):
36
- """An Agent that can answer questions."""
37
-
38
- default_instruction = """You are answering questions as if you were a human. Do not break character."""
39
-
40
- _traits = TraitsDescriptor()
41
- codebook = CodebookDescriptor()
42
- instruction = InstructionDescriptor()
43
- name = NameDescriptor()
44
- dynamic_traits_function_name = ""
45
- answer_question_directly_function_name = ""
46
- has_dynamic_traits_function = False
47
-
48
- def __init__(
49
- self,
50
- traits: Optional[dict] = None,
51
- name: Optional[str] = None,
52
- codebook: Optional[dict] = None,
53
- instruction: Optional[str] = None,
54
- traits_presentation_template: Optional[str] = None,
55
- dynamic_traits_function: Optional[Callable] = None,
56
- dynamic_traits_function_source_code: Optional[str] = None,
57
- dynamic_traits_function_name: Optional[str] = None,
58
- answer_question_directly_source_code: Optional[str] = None,
59
- answer_question_directly_function_name: Optional[str] = None,
60
- ):
61
- """Initialize a new instance of Agent.
62
-
63
- :param traits: A dictionary of traits that the agent has. The keys need to be valid identifiers.
64
- :param name: A name for the agent
65
- :param codebook: A codebook mapping trait keys to trait descriptions.
66
- :param instruction: Instructions for the agent in how to answer questions.
67
- :param trait_presentation_template: A template for how to present the agent's traits.
68
- :param dynamic_traits_function: A function that returns a dictionary of traits.
69
-
70
- The `traits` parameter is a dictionary of traits that the agent has.
71
- These traits are used to construct a prompt that is presented to the LLM.
72
- In the absence of a `traits_presentation_template`, the default is used.
73
- This is a template that is used to present the agent's traits to the LLM.
74
- See :py:class:`edsl.prompts.library.agent_persona.AgentPersona` for more information.
75
-
76
- Example usage:
77
-
78
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
79
- >>> a.traits
80
- {'age': 10, 'hair': 'brown', 'height': 5.5}
81
-
82
- These traits are used to construct a prompt that is presented to the LLM.
83
-
84
- In the absence of a `traits_presentation_template`, the default is used.
85
-
86
- >>> a = Agent(traits = {"age": 10}, traits_presentation_template = "I am a {{age}} year old.")
87
- >>> repr(a.agent_persona)
88
- 'Prompt(text=\"""I am a {{age}} year old.\""")'
89
-
90
- When this is rendered for presentation to the LLM, it will replace the `{{age}}` with the actual age.
91
- it is also possible to use the `codebook` to provide a more human-readable description of the trait.
92
- Here is an example where we give a prefix to the age trait (namely the age):
93
-
94
- >>> traits = {"age": 10, "hair": "brown", "height": 5.5}
95
- >>> codebook = {'age': 'Their age is'}
96
- >>> a = Agent(traits = traits, codebook = codebook, traits_presentation_template = "This agent is Dave. {{codebook['age']}} {{age}}")
97
- >>> d = a.traits | {'codebook': a.codebook}
98
- >>> a.agent_persona.render(d)
99
- Prompt(text=\"""This agent is Dave. Their age is 10\""")
100
-
101
- Instructions
102
- ------------
103
- The agent can also have instructions. These are instructions that are given to the agent when answering questions.
104
-
105
- >>> Agent.default_instruction
106
- 'You are answering questions as if you were a human. Do not break character.'
107
-
108
- See see how these are used to actually construct the prompt that is presented to the LLM, see :py:class:`edsl.agents.Invigilator.InvigilatorBase`.
109
-
110
- """
111
- self.name = name
112
- self._traits = traits or dict()
113
- self.codebook = codebook or dict()
114
- if instruction is None:
115
- self.instruction = self.default_instruction
116
- else:
117
- self.instruction = instruction
118
- # self.instruction = instruction or self.default_instruction
119
- self.dynamic_traits_function = dynamic_traits_function
120
-
121
- # Deal with dynamic traits function
122
- if self.dynamic_traits_function:
123
- self.dynamic_traits_function_name = self.dynamic_traits_function.__name__
124
- self.has_dynamic_traits_function = True
125
- else:
126
- self.has_dynamic_traits_function = False
127
-
128
- if dynamic_traits_function_source_code:
129
- self.dynamic_traits_function_name = dynamic_traits_function_name
130
- self.dynamic_traits_function = create_restricted_function(
131
- dynamic_traits_function_name, dynamic_traits_function
132
- )
133
-
134
- # Deal with direct answer function
135
- if answer_question_directly_source_code:
136
- self.answer_question_directly_function_name = (
137
- answer_question_directly_function_name
138
- )
139
- protected_method = create_restricted_function(
140
- answer_question_directly_function_name,
141
- answer_question_directly_source_code,
142
- )
143
- bound_method = types.MethodType(protected_method, self)
144
- setattr(self, "answer_question_directly", bound_method)
145
-
146
- self._check_dynamic_traits_function()
147
-
148
- self.current_question = None
149
-
150
- if traits_presentation_template is not None:
151
- self.traits_presentation_template = traits_presentation_template
152
- else:
153
- self.traits_presentation_template = """Your traits: {{ traits }}"""
154
-
155
- @property
156
- def agent_persona(self) -> Prompt:
157
- return Prompt(text=self.traits_presentation_template)
158
-
159
- def prompt(self) -> str:
160
- """Return the prompt for the agent.
161
-
162
- Example usage:
163
-
164
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
165
- >>> a.prompt()
166
- Prompt(text=\"""Your traits: {'age': 10, 'hair': 'brown', 'height': 5.5}\""")
167
- """
168
- replacement_dict = (
169
- self.traits | {"traits": self.traits} | {"codebook": self.codebook}
170
- )
171
- if undefined := self.agent_persona.undefined_template_variables(
172
- replacement_dict
173
- ):
174
- raise QuestionScenarioRenderError(
175
- f"Agent persona still has variables that were not rendered: {undefined}"
176
- )
177
- else:
178
- return self.agent_persona.render(replacement_dict)
179
-
180
- def _check_dynamic_traits_function(self) -> None:
181
- """Check whether dynamic trait function is valid.
182
-
183
- This checks whether the dynamic traits function is valid.
184
- """
185
- if self.has_dynamic_traits_function:
186
- sig = inspect.signature(self.dynamic_traits_function)
187
- if "question" in sig.parameters:
188
- if len(sig.parameters) > 1:
189
- raise AgentDynamicTraitsFunctionError(
190
- f"The dynamic traits function {self.dynamic_traits_function} has too many parameters. It should only have one parameter: 'question'."
191
- )
192
- else:
193
- if len(sig.parameters) > 0:
194
- raise AgentDynamicTraitsFunctionError(
195
- f"""The dynamic traits function {self.dynamic_traits_function} has too many parameters. It should have no parameters or
196
- just a single parameter: 'question'."""
197
- )
198
-
199
- @property
200
- def traits(self) -> dict[str, str]:
201
- """An agent's traits, which is a dictionary.
202
-
203
- The agent could have a a dynamic traits function (`dynamic_traits_function`) that returns a dictionary of traits
204
- when called. This function can also take a `question` as an argument.
205
- If so, the dynamic traits function is called and the result is returned.
206
- Otherwise, the traits are returned.
207
-
208
- Example:
209
-
210
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
211
- >>> a.traits
212
- {'age': 10, 'hair': 'brown', 'height': 5.5}
213
-
214
- """
215
- if self.has_dynamic_traits_function:
216
- sig = inspect.signature(self.dynamic_traits_function)
217
- if "question" in sig.parameters:
218
- return self.dynamic_traits_function(question=self.current_question)
219
- else:
220
- return self.dynamic_traits_function()
221
- else:
222
- return self._traits
223
-
224
- def rename(self, old_name: str, new_name: str) -> Agent:
225
- """Rename a trait.
226
-
227
- Example usage:
228
-
229
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
230
- >>> a.rename("age", "years") == Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5})
231
- True
232
- """
233
- self.traits[new_name] = self.traits.pop(old_name)
234
- return self
235
-
236
- def __getitem__(self, key):
237
- """Allow for accessing traits using the bracket notation.
238
-
239
- Example:
240
-
241
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
242
- >>> a['traits']['age']
243
- 10
244
-
245
- """
246
- return getattr(self, key)
247
-
248
- def remove_direct_question_answering_method(self) -> None:
249
- """Remove the direct question answering method.
250
-
251
- Example usage:
252
-
253
- >>> a = Agent()
254
- >>> def f(self, question, scenario): return "I am a direct answer."
255
- >>> a.add_direct_question_answering_method(f)
256
- >>> a.remove_direct_question_answering_method()
257
- >>> hasattr(a, "answer_question_directly")
258
- False
259
- """
260
- if hasattr(self, "answer_question_directly"):
261
- delattr(self, "answer_question_directly")
262
-
263
- def add_direct_question_answering_method(
264
- self,
265
- method: Callable,
266
- validate_response: bool = False,
267
- translate_response: bool = False,
268
- ) -> None:
269
- """Add a method to the agent that can answer a particular question type.
270
-
271
- :param method: A method that can answer a question directly.
272
-
273
- Example usage:
274
-
275
- >>> a = Agent()
276
- >>> def f(self, question, scenario): return "I am a direct answer."
277
- >>> a.add_direct_question_answering_method(f)
278
- >>> a.answer_question_directly(question = None, scenario = None)
279
- 'I am a direct answer.'
280
- """
281
- if hasattr(self, "answer_question_directly"):
282
- import warnings
283
-
284
- warnings.warn(
285
- "Warning: overwriting existing answer_question_directly method"
286
- )
287
-
288
- self.validate_response = validate_response
289
- self.translate_response = translate_response
290
-
291
- signature = inspect.signature(method)
292
- for argument in ["question", "scenario", "self"]:
293
- if argument not in signature.parameters:
294
- raise AgentDirectAnswerFunctionError(
295
- f"The method {method} does not have a '{argument}' parameter."
296
- )
297
- bound_method = types.MethodType(method, self)
298
- setattr(self, "answer_question_directly", bound_method)
299
- self.answer_question_directly_function_name = bound_method.__name__
300
-
301
- def create_invigilator(
302
- self,
303
- *,
304
- question: "QuestionBase",
305
- cache: "Cache",
306
- survey: Optional["Survey"] = None,
307
- scenario: Optional[Scenario] = None,
308
- model: Optional[LanguageModel] = None,
309
- debug: bool = False,
310
- memory_plan: Optional[MemoryPlan] = None,
311
- current_answers: Optional[dict] = None,
312
- iteration: int = 1,
313
- sidecar_model=None,
314
- raise_validation_errors: bool = True,
315
- ) -> "InvigilatorBase":
316
- """Create an Invigilator.
317
-
318
- An invigilator is an object that is responsible for administering a question to an agent.
319
- There are several different types of invigilators, depending on the type of question and the agent.
320
- For example, there are invigilators for functional questions (i.e., question is of type :class:`edsl.questions.QuestionFunctional`:), for direct questions, and for LLM questions.
321
-
322
- >>> a = Agent(traits = {})
323
- >>> a.create_invigilator(question = None, cache = False)
324
- InvigilatorAI(...)
325
-
326
- An invigator is an object that is responsible for administering a question to an agent and
327
- recording the responses.
328
- """
329
- from edsl import Model, Scenario
330
-
331
- cache = cache
332
- self.current_question = question
333
- model = model or Model()
334
- scenario = scenario or Scenario()
335
- invigilator = self._create_invigilator(
336
- question=question,
337
- scenario=scenario,
338
- survey=survey,
339
- model=model,
340
- debug=debug,
341
- memory_plan=memory_plan,
342
- current_answers=current_answers,
343
- iteration=iteration,
344
- cache=cache,
345
- sidecar_model=sidecar_model,
346
- raise_validation_errors=raise_validation_errors,
347
- )
348
- if hasattr(self, "validate_response"):
349
- invigilator.validate_response = self.validate_response
350
- if hasattr(self, "translate_response"):
351
- invigilator.translate_response = self.translate_response
352
- return invigilator
353
-
354
- async def async_answer_question(
355
- self,
356
- *,
357
- question: "QuestionBase",
358
- cache: "Cache",
359
- scenario: Optional["Scenario"] = None,
360
- survey: Optional["Survey"] = None,
361
- model: Optional["LanguageModel"] = None,
362
- debug: bool = False,
363
- memory_plan: Optional["MemoryPlan"] = None,
364
- current_answers: Optional[dict] = None,
365
- iteration: int = 0,
366
- ) -> AgentResponseDict:
367
- """
368
- Answer a posed question.
369
-
370
- :param question: The question to answer.
371
- :param scenario: The scenario in which the question is asked.
372
- :param model: The language model to use.
373
- :param debug: Whether to run in debug mode.
374
- :param memory_plan: The memory plan to use.
375
- :param current_answers: The current answers.
376
- :param iteration: The iteration number.
377
-
378
- >>> a = Agent(traits = {})
379
- >>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.")
380
- >>> from edsl import QuestionFreeText
381
- >>> q = QuestionFreeText.example()
382
- >>> a.answer_question(question = q, cache = False).answer
383
- 'I am a direct answer.'
384
-
385
- This is a function where an agent returns an answer to a particular question.
386
- However, there are several different ways an agent can answer a question, so the
387
- actual functionality is delegated to an :class:`edsl.agents.InvigilatorBase`: object.
388
- """
389
- invigilator = self.create_invigilator(
390
- question=question,
391
- cache=cache,
392
- scenario=scenario,
393
- survey=survey,
394
- model=model,
395
- debug=debug,
396
- memory_plan=memory_plan,
397
- current_answers=current_answers,
398
- iteration=iteration,
399
- )
400
- response: AgentResponseDict = await invigilator.async_answer_question()
401
- return response
402
-
403
- answer_question = sync_wrapper(async_answer_question)
404
-
405
- def _create_invigilator(
406
- self,
407
- question: "QuestionBase",
408
- cache: Optional["Cache"] = None,
409
- scenario: Optional["Scenario"] = None,
410
- model: Optional["LanguageModel"] = None,
411
- survey: Optional["Survey"] = None,
412
- debug: bool = False,
413
- memory_plan: Optional["MemoryPlan"] = None,
414
- current_answers: Optional[dict] = None,
415
- iteration: int = 0,
416
- sidecar_model=None,
417
- raise_validation_errors: bool = True,
418
- ) -> "InvigilatorBase":
419
- """Create an Invigilator."""
420
- from edsl import Model
421
- from edsl import Scenario
422
-
423
- model = model or Model()
424
- scenario = scenario or Scenario()
425
-
426
- from edsl.agents.Invigilator import (
427
- InvigilatorHuman,
428
- InvigilatorFunctional,
429
- InvigilatorAI,
430
- InvigilatorBase,
431
- )
432
-
433
- if cache is None:
434
- from edsl.data.Cache import Cache
435
-
436
- cache = Cache()
437
-
438
- if debug:
439
- raise NotImplementedError("Debug mode is not yet implemented.")
440
- # use the question's _simulate_answer method
441
- # invigilator_class = InvigilatorDebug
442
- elif hasattr(question, "answer_question_directly"):
443
- # It's a functional question and the answer only depends on the agent's traits & the scenario
444
- invigilator_class = InvigilatorFunctional
445
- elif hasattr(self, "answer_question_directly"):
446
- # this of the case where the agent has a method that can answer the question directly
447
- # this occurrs when 'answer_question_directly' has been given to the
448
- # which happens when the agent is created from an existing survey
449
- invigilator_class = InvigilatorHuman
450
- else:
451
- # this means an LLM agent will be used. This is the standard case.
452
- invigilator_class = InvigilatorAI
453
-
454
- if sidecar_model is not None:
455
- # this is the case when a 'simple' model is being used
456
- from edsl.agents.Invigilator import InvigilatorSidecar
457
-
458
- invigilator_class = InvigilatorSidecar
459
-
460
- invigilator = invigilator_class(
461
- self,
462
- question=question,
463
- scenario=scenario,
464
- survey=survey,
465
- model=model,
466
- memory_plan=memory_plan,
467
- current_answers=current_answers,
468
- iteration=iteration,
469
- cache=cache,
470
- sidecar_model=sidecar_model,
471
- raise_validation_errors=raise_validation_errors,
472
- )
473
- return invigilator
474
-
475
- def select(self, *traits: str) -> Agent:
476
- """Selects agents with only the references traits
477
-
478
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
479
-
480
-
481
- >>> a.select("age", "height")
482
- Agent(traits = {'age': 10, 'height': 5.5})
483
-
484
- >>> a.select("age")
485
- Agent(traits = {'age': 10})
486
-
487
- """
488
-
489
- if len(traits) == 1:
490
- traits_to_select = [list(traits)[0]]
491
- else:
492
- traits_to_select = list(traits)
493
-
494
- return Agent(traits={trait: self.traits[trait] for trait in traits_to_select})
495
-
496
- ################
497
- # Dunder Methods
498
- ################
499
- def __add__(self, other_agent: Optional[Agent] = None) -> Agent:
500
- """
501
- Combine two agents by joining their traits.
502
-
503
- The agents must not have overlapping traits.
504
-
505
- Example usage:
506
-
507
- >>> a1 = Agent(traits = {"age": 10})
508
- >>> a2 = Agent(traits = {"height": 5.5})
509
- >>> a1 + a2
510
- Agent(traits = {'age': 10, 'height': 5.5})
511
- >>> a1 + a1
512
- Traceback (most recent call last):
513
- ...
514
- edsl.exceptions.agents.AgentCombinationError: The agents have overlapping traits: {'age'}.
515
- """
516
- if other_agent is None:
517
- return self
518
- elif common_traits := set(self.traits.keys()) & set(other_agent.traits.keys()):
519
- raise AgentCombinationError(
520
- f"The agents have overlapping traits: {common_traits}."
521
- )
522
- else:
523
- new_agent = Agent(traits=copy.deepcopy(self.traits))
524
- new_agent.traits.update(other_agent.traits)
525
- return new_agent
526
-
527
- def __eq__(self, other: Agent) -> bool:
528
- """Check if two agents are equal.
529
-
530
- This only checks the traits.
531
- >>> a1 = Agent(traits = {"age": 10})
532
- >>> a2 = Agent(traits = {"age": 10})
533
- >>> a1 == a2
534
- True
535
- >>> a3 = Agent(traits = {"age": 11})
536
- >>> a1 == a3
537
- False
538
- """
539
- return self.data == other.data
540
-
541
- def __getattr__(self, name):
542
- # This will be called only if 'name' is not found in the usual places
543
- # breakpoint()
544
- if name == "has_dynamic_traits_function":
545
- return self.has_dynamic_traits_function
546
-
547
- if name in self._traits:
548
- return self._traits[name]
549
- raise AttributeError(
550
- f"'{type(self).__name__}' object has no attribute '{name}'"
551
- )
552
-
553
- def __getstate__(self):
554
- state = self.__dict__.copy()
555
- # Include any additional state that needs to be serialized
556
- return state
557
-
558
- def __setstate__(self, state):
559
- self.__dict__.update(state)
560
- # Ensure _traits is initialized if it's missing
561
- if "_traits" not in self.__dict__:
562
- self._traits = {}
563
-
564
- def print(self) -> None:
565
- from rich import print_json
566
- import json
567
-
568
- print_json(json.dumps(self.to_dict()))
569
-
570
- def __repr__(self) -> str:
571
- """Return representation of Agent."""
572
- class_name = self.__class__.__name__
573
- items = [
574
- f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
575
- for k, v in self.data.items()
576
- if k != "question_type"
577
- ]
578
- return f"{class_name}({', '.join(items)})"
579
-
580
- def _repr_html_(self):
581
- from edsl.utilities.utilities import data_to_html
582
-
583
- return data_to_html(self.to_dict())
584
-
585
- #######################
586
- # SERIALIZATION METHODS
587
- #######################
588
- @property
589
- def data(self) -> dict:
590
- """Format the data for serialization.
591
-
592
- TODO: Warn if has dynamic traits function or direct answer function that cannot be serialized.
593
- TODO: Add ability to have coop-hosted functions that are serializable.
594
- """
595
-
596
- raw_data = {
597
- k.replace("_", "", 1): v
598
- for k, v in self.__dict__.items()
599
- if k.startswith("_")
600
- }
601
- if hasattr(self, "set_instructions"):
602
- if not self.set_instructions:
603
- raw_data.pop("instruction")
604
- if self.codebook == {}:
605
- raw_data.pop("codebook")
606
- if self.name == None:
607
- raw_data.pop("name")
608
-
609
- if hasattr(self, "dynamic_traits_function"):
610
- raw_data.pop(
611
- "dynamic_traits_function", None
612
- ) # in case dynamic_traits_function will appear with _ in self.__dict__
613
- dynamic_traits_func = self.dynamic_traits_function
614
- if dynamic_traits_func:
615
- func = inspect.getsource(dynamic_traits_func)
616
- raw_data["dynamic_traits_function_source_code"] = func
617
- raw_data[
618
- "dynamic_traits_function_name"
619
- ] = self.dynamic_traits_function_name
620
- if hasattr(self, "answer_question_directly"):
621
- raw_data.pop(
622
- "answer_question_directly", None
623
- ) # in case answer_question_directly will appear with _ in self.__dict__
624
- answer_question_directly_func = self.answer_question_directly
625
- # print(answer_question_directly_func)
626
- # print(type(answer_question_directly_func), flush=True)
627
-
628
- if (
629
- answer_question_directly_func
630
- and raw_data.get("answer_question_directly_source_code", None) != None
631
- ):
632
- raw_data["answer_question_directly_source_code"] = inspect.getsource(
633
- answer_question_directly_func
634
- )
635
- raw_data[
636
- "answer_question_directly_function_name"
637
- ] = self.answer_question_directly_function_name
638
-
639
- return raw_data
640
-
641
- def __hash__(self) -> int:
642
- from edsl.utilities.utilities import dict_hash
643
-
644
- return dict_hash(self._to_dict())
645
-
646
- def _to_dict(self) -> dict[str, Union[dict, bool]]:
647
- """Serialize to a dictionary."""
648
- return self.data
649
-
650
- @add_edsl_version
651
- def to_dict(self) -> dict[str, Union[dict, bool]]:
652
- """Serialize to a dictionary.
653
-
654
- Example usage:
655
-
656
- >>> a = Agent(name = "Steve", traits = {"age": 10, "hair": "brown", "height": 5.5})
657
- >>> a.to_dict()
658
- {'name': 'Steve', 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}, 'edsl_version': '...', 'edsl_class_name': 'Agent'}
659
- """
660
- return self._to_dict()
661
-
662
- @classmethod
663
- @remove_edsl_version
664
- def from_dict(cls, agent_dict: dict[str, Union[dict, bool]]) -> Agent:
665
- """Deserialize from a dictionary.
666
-
667
- Example usage:
668
-
669
- >>> Agent.from_dict({'name': "Steve", 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}})
670
- Agent(name = \"""Steve\""", traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
671
-
672
- """
673
- return cls(**agent_dict)
674
-
675
- ################
676
- # DISPLAY Methods
677
- ################
678
-
679
- def _table(self) -> tuple[dict, list]:
680
- """Prepare generic table data."""
681
- table_data = []
682
- for attr_name, attr_value in self.__dict__.items():
683
- table_data.append({"Attribute": attr_name, "Value": repr(attr_value)})
684
- column_names = ["Attribute", "Value"]
685
- return table_data, column_names
686
-
687
- def add_trait(self, trait_name_or_dict: str, value: Optional[Any] = None) -> Agent:
688
- """Adds a trait to an agent and returns that agent"""
689
- if isinstance(trait_name_or_dict, dict) and value is None:
690
- self.traits.update(trait_name_or_dict)
691
- return self
692
-
693
- if isinstance(trait_name_or_dict, dict) and value:
694
- raise ValueError(f"You passed a dict: {trait_name_or_dict}")
695
-
696
- if isinstance(trait_name_or_dict, str):
697
- trait = trait_name_or_dict
698
- self.traits[trait] = value
699
- return self
700
-
701
- raise Exception("Something is not right with adding")
702
-
703
- def remove_trait(self, trait: str) -> Agent:
704
- """Remove a trait from the agent.
705
-
706
- Example usage:
707
-
708
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
709
- >>> a.remove_trait("age")
710
- Agent(traits = {'hair': 'brown', 'height': 5.5})
711
- """
712
- _ = self.traits.pop(trait)
713
- return self
714
-
715
- def translate_traits(self, values_codebook: dict) -> Agent:
716
- """Translate traits to a new codebook.
717
-
718
- >>> a = Agent(traits = {"age": 10, "hair": 1, "height": 5.5})
719
- >>> a.translate_traits({"hair": {1:"brown"}})
720
- Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
721
-
722
- :param values_codebook: The new codebook.
723
- """
724
- for key, value in self.traits.items():
725
- if key in values_codebook:
726
- self.traits[key] = values_codebook[key][value]
727
- return self
728
-
729
- def rich_print(self):
730
- """Display an object as a rich table.
731
-
732
- Example usage:
733
-
734
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
735
- >>> a.rich_print()
736
- <rich.table.Table object at ...>
737
- """
738
- from rich.table import Table
739
-
740
- table_data, column_names = self._table()
741
- table = Table(title=f"{self.__class__.__name__} Attributes")
742
- for column in column_names:
743
- table.add_column(column, style="bold")
744
-
745
- for row in table_data:
746
- row_data = [row[column] for column in column_names]
747
- table.add_row(*row_data)
748
-
749
- return table
750
-
751
- @classmethod
752
- def example(cls, randomize: bool = False) -> Agent:
753
- """
754
- Returns an example Agent instance.
755
-
756
- :param randomize: If True, adds a random string to the value of an example key.
757
- """
758
- addition = "" if not randomize else str(uuid4())
759
- return cls(traits={"age": 22, "hair": f"brown{addition}", "height": 5.5})
760
-
761
- def code(self) -> str:
762
- """Return the code for the agent.
763
-
764
- Example usage:
765
-
766
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
767
- >>> print(a.code())
768
- from edsl import Agent
769
- agent = Agent(traits={'age': 10, 'hair': 'brown', 'height': 5.5})
770
- """
771
- return f"from edsl import Agent\nagent = Agent(traits={self.traits})"
772
-
773
-
774
- def main():
775
- """
776
- Give an example of usage.
777
-
778
- WARNING: Consume API credits
779
- """
780
- from edsl.agents import Agent
781
- from edsl.questions import QuestionMultipleChoice
782
-
783
- # a simple agent
784
- agent = Agent(traits={"age": 10, "hair": "brown", "height": 5.5})
785
- agent.traits
786
- agent.print()
787
- # combining two agents
788
- agent = Agent(traits={"age": 10}) + Agent(traits={"height": 5.5})
789
- agent.traits
790
- # Agent -> Job using the to() method
791
- agent = Agent(traits={"allergies": "peanut"})
792
- question = QuestionMultipleChoice(
793
- question_text="Would you enjoy a PB&J?",
794
- question_options=["Yes", "No"],
795
- question_name="food_preference",
796
- )
797
- job = question.by(agent)
798
- results = job.run()
799
-
800
-
801
- if __name__ == "__main__":
802
- import doctest
803
-
804
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ """An Agent is an AI agent that can reference a set of traits in answering questions."""
2
+
3
+ from __future__ import annotations
4
+ import copy
5
+ import inspect
6
+ import types
7
+ from typing import Callable, Optional, Union, Any
8
+ from uuid import uuid4
9
+ from edsl.Base import Base
10
+
11
+ from edsl.prompts import Prompt
12
+ from edsl.exceptions import QuestionScenarioRenderError
13
+
14
+ from edsl.exceptions.agents import (
15
+ AgentCombinationError,
16
+ AgentDirectAnswerFunctionError,
17
+ AgentDynamicTraitsFunctionError,
18
+ )
19
+
20
+ from edsl.agents.descriptors import (
21
+ TraitsDescriptor,
22
+ CodebookDescriptor,
23
+ InstructionDescriptor,
24
+ NameDescriptor,
25
+ )
26
+ from edsl.utilities.decorators import (
27
+ sync_wrapper,
28
+ add_edsl_version,
29
+ remove_edsl_version,
30
+ )
31
+ from edsl.data_transfer_models import AgentResponseDict
32
+ from edsl.utilities.restricted_python import create_restricted_function
33
+
34
+
35
+ class Agent(Base):
36
+ """An Agent that can answer questions."""
37
+
38
+ default_instruction = """You are answering questions as if you were a human. Do not break character."""
39
+
40
+ _traits = TraitsDescriptor()
41
+ codebook = CodebookDescriptor()
42
+ instruction = InstructionDescriptor()
43
+ name = NameDescriptor()
44
+ dynamic_traits_function_name = ""
45
+ answer_question_directly_function_name = ""
46
+ has_dynamic_traits_function = False
47
+
48
+ def __init__(
49
+ self,
50
+ traits: Optional[dict] = None,
51
+ name: Optional[str] = None,
52
+ codebook: Optional[dict] = None,
53
+ instruction: Optional[str] = None,
54
+ traits_presentation_template: Optional[str] = None,
55
+ dynamic_traits_function: Optional[Callable] = None,
56
+ dynamic_traits_function_source_code: Optional[str] = None,
57
+ dynamic_traits_function_name: Optional[str] = None,
58
+ answer_question_directly_source_code: Optional[str] = None,
59
+ answer_question_directly_function_name: Optional[str] = None,
60
+ ):
61
+ """Initialize a new instance of Agent.
62
+
63
+ :param traits: A dictionary of traits that the agent has. The keys need to be valid identifiers.
64
+ :param name: A name for the agent
65
+ :param codebook: A codebook mapping trait keys to trait descriptions.
66
+ :param instruction: Instructions for the agent in how to answer questions.
67
+ :param trait_presentation_template: A template for how to present the agent's traits.
68
+ :param dynamic_traits_function: A function that returns a dictionary of traits.
69
+
70
+ The `traits` parameter is a dictionary of traits that the agent has.
71
+ These traits are used to construct a prompt that is presented to the LLM.
72
+ In the absence of a `traits_presentation_template`, the default is used.
73
+ This is a template that is used to present the agent's traits to the LLM.
74
+ See :py:class:`edsl.prompts.library.agent_persona.AgentPersona` for more information.
75
+
76
+ Example usage:
77
+
78
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
79
+ >>> a.traits
80
+ {'age': 10, 'hair': 'brown', 'height': 5.5}
81
+
82
+ These traits are used to construct a prompt that is presented to the LLM.
83
+
84
+ In the absence of a `traits_presentation_template`, the default is used.
85
+
86
+ >>> a = Agent(traits = {"age": 10}, traits_presentation_template = "I am a {{age}} year old.")
87
+ >>> repr(a.agent_persona)
88
+ 'Prompt(text=\"""I am a {{age}} year old.\""")'
89
+
90
+ When this is rendered for presentation to the LLM, it will replace the `{{age}}` with the actual age.
91
+ it is also possible to use the `codebook` to provide a more human-readable description of the trait.
92
+ Here is an example where we give a prefix to the age trait (namely the age):
93
+
94
+ >>> traits = {"age": 10, "hair": "brown", "height": 5.5}
95
+ >>> codebook = {'age': 'Their age is'}
96
+ >>> a = Agent(traits = traits, codebook = codebook, traits_presentation_template = "This agent is Dave. {{codebook['age']}} {{age}}")
97
+ >>> d = a.traits | {'codebook': a.codebook}
98
+ >>> a.agent_persona.render(d)
99
+ Prompt(text=\"""This agent is Dave. Their age is 10\""")
100
+
101
+ Instructions
102
+ ------------
103
+ The agent can also have instructions. These are instructions that are given to the agent when answering questions.
104
+
105
+ >>> Agent.default_instruction
106
+ 'You are answering questions as if you were a human. Do not break character.'
107
+
108
+ See see how these are used to actually construct the prompt that is presented to the LLM, see :py:class:`edsl.agents.Invigilator.InvigilatorBase`.
109
+
110
+ """
111
+ self.name = name
112
+ self._traits = traits or dict()
113
+ self.codebook = codebook or dict()
114
+ if instruction is None:
115
+ self.instruction = self.default_instruction
116
+ else:
117
+ self.instruction = instruction
118
+ # self.instruction = instruction or self.default_instruction
119
+ self.dynamic_traits_function = dynamic_traits_function
120
+
121
+ # Deal with dynamic traits function
122
+ if self.dynamic_traits_function:
123
+ self.dynamic_traits_function_name = self.dynamic_traits_function.__name__
124
+ self.has_dynamic_traits_function = True
125
+ else:
126
+ self.has_dynamic_traits_function = False
127
+
128
+ if dynamic_traits_function_source_code:
129
+ self.dynamic_traits_function_name = dynamic_traits_function_name
130
+ self.dynamic_traits_function = create_restricted_function(
131
+ dynamic_traits_function_name, dynamic_traits_function
132
+ )
133
+
134
+ # Deal with direct answer function
135
+ if answer_question_directly_source_code:
136
+ self.answer_question_directly_function_name = (
137
+ answer_question_directly_function_name
138
+ )
139
+ protected_method = create_restricted_function(
140
+ answer_question_directly_function_name,
141
+ answer_question_directly_source_code,
142
+ )
143
+ bound_method = types.MethodType(protected_method, self)
144
+ setattr(self, "answer_question_directly", bound_method)
145
+
146
+ self._check_dynamic_traits_function()
147
+
148
+ self.current_question = None
149
+
150
+ if traits_presentation_template is not None:
151
+ self.traits_presentation_template = traits_presentation_template
152
+ else:
153
+ self.traits_presentation_template = """Your traits: {{ traits }}"""
154
+
155
+ @property
156
+ def agent_persona(self) -> Prompt:
157
+ return Prompt(text=self.traits_presentation_template)
158
+
159
+ def prompt(self) -> str:
160
+ """Return the prompt for the agent.
161
+
162
+ Example usage:
163
+
164
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
165
+ >>> a.prompt()
166
+ Prompt(text=\"""Your traits: {'age': 10, 'hair': 'brown', 'height': 5.5}\""")
167
+ """
168
+ replacement_dict = (
169
+ self.traits | {"traits": self.traits} | {"codebook": self.codebook}
170
+ )
171
+ if undefined := self.agent_persona.undefined_template_variables(
172
+ replacement_dict
173
+ ):
174
+ raise QuestionScenarioRenderError(
175
+ f"Agent persona still has variables that were not rendered: {undefined}"
176
+ )
177
+ else:
178
+ return self.agent_persona.render(replacement_dict)
179
+
180
+ def _check_dynamic_traits_function(self) -> None:
181
+ """Check whether dynamic trait function is valid.
182
+
183
+ This checks whether the dynamic traits function is valid.
184
+ """
185
+ if self.has_dynamic_traits_function:
186
+ sig = inspect.signature(self.dynamic_traits_function)
187
+ if "question" in sig.parameters:
188
+ if len(sig.parameters) > 1:
189
+ raise AgentDynamicTraitsFunctionError(
190
+ f"The dynamic traits function {self.dynamic_traits_function} has too many parameters. It should only have one parameter: 'question'."
191
+ )
192
+ else:
193
+ if len(sig.parameters) > 0:
194
+ raise AgentDynamicTraitsFunctionError(
195
+ f"""The dynamic traits function {self.dynamic_traits_function} has too many parameters. It should have no parameters or
196
+ just a single parameter: 'question'."""
197
+ )
198
+
199
+ @property
200
+ def traits(self) -> dict[str, str]:
201
+ """An agent's traits, which is a dictionary.
202
+
203
+ The agent could have a a dynamic traits function (`dynamic_traits_function`) that returns a dictionary of traits
204
+ when called. This function can also take a `question` as an argument.
205
+ If so, the dynamic traits function is called and the result is returned.
206
+ Otherwise, the traits are returned.
207
+
208
+ Example:
209
+
210
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
211
+ >>> a.traits
212
+ {'age': 10, 'hair': 'brown', 'height': 5.5}
213
+
214
+ """
215
+ if self.has_dynamic_traits_function:
216
+ sig = inspect.signature(self.dynamic_traits_function)
217
+ if "question" in sig.parameters:
218
+ return self.dynamic_traits_function(question=self.current_question)
219
+ else:
220
+ return self.dynamic_traits_function()
221
+ else:
222
+ return self._traits
223
+
224
+ def rename(self, old_name: str, new_name: str) -> Agent:
225
+ """Rename a trait.
226
+
227
+ Example usage:
228
+
229
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
230
+ >>> a.rename("age", "years") == Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5})
231
+ True
232
+ """
233
+ self.traits[new_name] = self.traits.pop(old_name)
234
+ return self
235
+
236
+ def __getitem__(self, key):
237
+ """Allow for accessing traits using the bracket notation.
238
+
239
+ Example:
240
+
241
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
242
+ >>> a['traits']['age']
243
+ 10
244
+
245
+ """
246
+ return getattr(self, key)
247
+
248
+ def remove_direct_question_answering_method(self) -> None:
249
+ """Remove the direct question answering method.
250
+
251
+ Example usage:
252
+
253
+ >>> a = Agent()
254
+ >>> def f(self, question, scenario): return "I am a direct answer."
255
+ >>> a.add_direct_question_answering_method(f)
256
+ >>> a.remove_direct_question_answering_method()
257
+ >>> hasattr(a, "answer_question_directly")
258
+ False
259
+ """
260
+ if hasattr(self, "answer_question_directly"):
261
+ delattr(self, "answer_question_directly")
262
+
263
+ def add_direct_question_answering_method(
264
+ self,
265
+ method: Callable,
266
+ validate_response: bool = False,
267
+ translate_response: bool = False,
268
+ ) -> None:
269
+ """Add a method to the agent that can answer a particular question type.
270
+
271
+ :param method: A method that can answer a question directly.
272
+
273
+ Example usage:
274
+
275
+ >>> a = Agent()
276
+ >>> def f(self, question, scenario): return "I am a direct answer."
277
+ >>> a.add_direct_question_answering_method(f)
278
+ >>> a.answer_question_directly(question = None, scenario = None)
279
+ 'I am a direct answer.'
280
+ """
281
+ if hasattr(self, "answer_question_directly"):
282
+ import warnings
283
+
284
+ warnings.warn(
285
+ "Warning: overwriting existing answer_question_directly method"
286
+ )
287
+
288
+ self.validate_response = validate_response
289
+ self.translate_response = translate_response
290
+
291
+ signature = inspect.signature(method)
292
+ for argument in ["question", "scenario", "self"]:
293
+ if argument not in signature.parameters:
294
+ raise AgentDirectAnswerFunctionError(
295
+ f"The method {method} does not have a '{argument}' parameter."
296
+ )
297
+ bound_method = types.MethodType(method, self)
298
+ setattr(self, "answer_question_directly", bound_method)
299
+ self.answer_question_directly_function_name = bound_method.__name__
300
+
301
+ def create_invigilator(
302
+ self,
303
+ *,
304
+ question: "QuestionBase",
305
+ cache: "Cache",
306
+ survey: Optional["Survey"] = None,
307
+ scenario: Optional[Scenario] = None,
308
+ model: Optional[LanguageModel] = None,
309
+ debug: bool = False,
310
+ memory_plan: Optional[MemoryPlan] = None,
311
+ current_answers: Optional[dict] = None,
312
+ iteration: int = 1,
313
+ sidecar_model=None,
314
+ raise_validation_errors: bool = True,
315
+ ) -> "InvigilatorBase":
316
+ """Create an Invigilator.
317
+
318
+ An invigilator is an object that is responsible for administering a question to an agent.
319
+ There are several different types of invigilators, depending on the type of question and the agent.
320
+ For example, there are invigilators for functional questions (i.e., question is of type :class:`edsl.questions.QuestionFunctional`:), for direct questions, and for LLM questions.
321
+
322
+ >>> a = Agent(traits = {})
323
+ >>> a.create_invigilator(question = None, cache = False)
324
+ InvigilatorAI(...)
325
+
326
+ An invigator is an object that is responsible for administering a question to an agent and
327
+ recording the responses.
328
+ """
329
+ from edsl import Model, Scenario
330
+
331
+ cache = cache
332
+ self.current_question = question
333
+ model = model or Model()
334
+ scenario = scenario or Scenario()
335
+ invigilator = self._create_invigilator(
336
+ question=question,
337
+ scenario=scenario,
338
+ survey=survey,
339
+ model=model,
340
+ debug=debug,
341
+ memory_plan=memory_plan,
342
+ current_answers=current_answers,
343
+ iteration=iteration,
344
+ cache=cache,
345
+ sidecar_model=sidecar_model,
346
+ raise_validation_errors=raise_validation_errors,
347
+ )
348
+ if hasattr(self, "validate_response"):
349
+ invigilator.validate_response = self.validate_response
350
+ if hasattr(self, "translate_response"):
351
+ invigilator.translate_response = self.translate_response
352
+ return invigilator
353
+
354
+ async def async_answer_question(
355
+ self,
356
+ *,
357
+ question: "QuestionBase",
358
+ cache: "Cache",
359
+ scenario: Optional["Scenario"] = None,
360
+ survey: Optional["Survey"] = None,
361
+ model: Optional["LanguageModel"] = None,
362
+ debug: bool = False,
363
+ memory_plan: Optional["MemoryPlan"] = None,
364
+ current_answers: Optional[dict] = None,
365
+ iteration: int = 0,
366
+ ) -> AgentResponseDict:
367
+ """
368
+ Answer a posed question.
369
+
370
+ :param question: The question to answer.
371
+ :param scenario: The scenario in which the question is asked.
372
+ :param model: The language model to use.
373
+ :param debug: Whether to run in debug mode.
374
+ :param memory_plan: The memory plan to use.
375
+ :param current_answers: The current answers.
376
+ :param iteration: The iteration number.
377
+
378
+ >>> a = Agent(traits = {})
379
+ >>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.")
380
+ >>> from edsl import QuestionFreeText
381
+ >>> q = QuestionFreeText.example()
382
+ >>> a.answer_question(question = q, cache = False).answer
383
+ 'I am a direct answer.'
384
+
385
+ This is a function where an agent returns an answer to a particular question.
386
+ However, there are several different ways an agent can answer a question, so the
387
+ actual functionality is delegated to an :class:`edsl.agents.InvigilatorBase`: object.
388
+ """
389
+ invigilator = self.create_invigilator(
390
+ question=question,
391
+ cache=cache,
392
+ scenario=scenario,
393
+ survey=survey,
394
+ model=model,
395
+ debug=debug,
396
+ memory_plan=memory_plan,
397
+ current_answers=current_answers,
398
+ iteration=iteration,
399
+ )
400
+ response: AgentResponseDict = await invigilator.async_answer_question()
401
+ return response
402
+
403
+ answer_question = sync_wrapper(async_answer_question)
404
+
405
+ def _create_invigilator(
406
+ self,
407
+ question: "QuestionBase",
408
+ cache: Optional["Cache"] = None,
409
+ scenario: Optional["Scenario"] = None,
410
+ model: Optional["LanguageModel"] = None,
411
+ survey: Optional["Survey"] = None,
412
+ debug: bool = False,
413
+ memory_plan: Optional["MemoryPlan"] = None,
414
+ current_answers: Optional[dict] = None,
415
+ iteration: int = 0,
416
+ sidecar_model=None,
417
+ raise_validation_errors: bool = True,
418
+ ) -> "InvigilatorBase":
419
+ """Create an Invigilator."""
420
+ from edsl import Model
421
+ from edsl import Scenario
422
+
423
+ model = model or Model()
424
+ scenario = scenario or Scenario()
425
+
426
+ from edsl.agents.Invigilator import (
427
+ InvigilatorHuman,
428
+ InvigilatorFunctional,
429
+ InvigilatorAI,
430
+ InvigilatorBase,
431
+ )
432
+
433
+ if cache is None:
434
+ from edsl.data.Cache import Cache
435
+
436
+ cache = Cache()
437
+
438
+ if debug:
439
+ raise NotImplementedError("Debug mode is not yet implemented.")
440
+ # use the question's _simulate_answer method
441
+ # invigilator_class = InvigilatorDebug
442
+ elif hasattr(question, "answer_question_directly"):
443
+ # It's a functional question and the answer only depends on the agent's traits & the scenario
444
+ invigilator_class = InvigilatorFunctional
445
+ elif hasattr(self, "answer_question_directly"):
446
+ # this of the case where the agent has a method that can answer the question directly
447
+ # this occurrs when 'answer_question_directly' has been given to the
448
+ # which happens when the agent is created from an existing survey
449
+ invigilator_class = InvigilatorHuman
450
+ else:
451
+ # this means an LLM agent will be used. This is the standard case.
452
+ invigilator_class = InvigilatorAI
453
+
454
+ if sidecar_model is not None:
455
+ # this is the case when a 'simple' model is being used
456
+ from edsl.agents.Invigilator import InvigilatorSidecar
457
+
458
+ invigilator_class = InvigilatorSidecar
459
+
460
+ invigilator = invigilator_class(
461
+ self,
462
+ question=question,
463
+ scenario=scenario,
464
+ survey=survey,
465
+ model=model,
466
+ memory_plan=memory_plan,
467
+ current_answers=current_answers,
468
+ iteration=iteration,
469
+ cache=cache,
470
+ sidecar_model=sidecar_model,
471
+ raise_validation_errors=raise_validation_errors,
472
+ )
473
+ return invigilator
474
+
475
+ def select(self, *traits: str) -> Agent:
476
+ """Selects agents with only the references traits
477
+
478
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
479
+
480
+
481
+ >>> a.select("age", "height")
482
+ Agent(traits = {'age': 10, 'height': 5.5})
483
+
484
+ >>> a.select("age")
485
+ Agent(traits = {'age': 10})
486
+
487
+ """
488
+
489
+ if len(traits) == 1:
490
+ traits_to_select = [list(traits)[0]]
491
+ else:
492
+ traits_to_select = list(traits)
493
+
494
+ return Agent(traits={trait: self.traits[trait] for trait in traits_to_select})
495
+
496
+ ################
497
+ # Dunder Methods
498
+ ################
499
+ def __add__(self, other_agent: Optional[Agent] = None) -> Agent:
500
+ """
501
+ Combine two agents by joining their traits.
502
+
503
+ The agents must not have overlapping traits.
504
+
505
+ Example usage:
506
+
507
+ >>> a1 = Agent(traits = {"age": 10})
508
+ >>> a2 = Agent(traits = {"height": 5.5})
509
+ >>> a1 + a2
510
+ Agent(traits = {'age': 10, 'height': 5.5})
511
+ >>> a1 + a1
512
+ Traceback (most recent call last):
513
+ ...
514
+ edsl.exceptions.agents.AgentCombinationError: The agents have overlapping traits: {'age'}.
515
+ """
516
+ if other_agent is None:
517
+ return self
518
+ elif common_traits := set(self.traits.keys()) & set(other_agent.traits.keys()):
519
+ raise AgentCombinationError(
520
+ f"The agents have overlapping traits: {common_traits}."
521
+ )
522
+ else:
523
+ new_agent = Agent(traits=copy.deepcopy(self.traits))
524
+ new_agent.traits.update(other_agent.traits)
525
+ return new_agent
526
+
527
+ def __eq__(self, other: Agent) -> bool:
528
+ """Check if two agents are equal.
529
+
530
+ This only checks the traits.
531
+ >>> a1 = Agent(traits = {"age": 10})
532
+ >>> a2 = Agent(traits = {"age": 10})
533
+ >>> a1 == a2
534
+ True
535
+ >>> a3 = Agent(traits = {"age": 11})
536
+ >>> a1 == a3
537
+ False
538
+ """
539
+ return self.data == other.data
540
+
541
+ def __getattr__(self, name):
542
+ # This will be called only if 'name' is not found in the usual places
543
+ # breakpoint()
544
+ if name == "has_dynamic_traits_function":
545
+ return self.has_dynamic_traits_function
546
+
547
+ if name in self._traits:
548
+ return self._traits[name]
549
+ raise AttributeError(
550
+ f"'{type(self).__name__}' object has no attribute '{name}'"
551
+ )
552
+
553
+ def __getstate__(self):
554
+ state = self.__dict__.copy()
555
+ # Include any additional state that needs to be serialized
556
+ return state
557
+
558
+ def __setstate__(self, state):
559
+ self.__dict__.update(state)
560
+ # Ensure _traits is initialized if it's missing
561
+ if "_traits" not in self.__dict__:
562
+ self._traits = {}
563
+
564
+ def print(self) -> None:
565
+ from rich import print_json
566
+ import json
567
+
568
+ print_json(json.dumps(self.to_dict()))
569
+
570
+ def __repr__(self) -> str:
571
+ """Return representation of Agent."""
572
+ class_name = self.__class__.__name__
573
+ items = [
574
+ f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
575
+ for k, v in self.data.items()
576
+ if k != "question_type"
577
+ ]
578
+ return f"{class_name}({', '.join(items)})"
579
+
580
+ def _repr_html_(self):
581
+ from edsl.utilities.utilities import data_to_html
582
+
583
+ return data_to_html(self.to_dict())
584
+
585
+ #######################
586
+ # SERIALIZATION METHODS
587
+ #######################
588
+ @property
589
+ def data(self) -> dict:
590
+ """Format the data for serialization.
591
+
592
+ TODO: Warn if has dynamic traits function or direct answer function that cannot be serialized.
593
+ TODO: Add ability to have coop-hosted functions that are serializable.
594
+ """
595
+
596
+ raw_data = {
597
+ k.replace("_", "", 1): v
598
+ for k, v in self.__dict__.items()
599
+ if k.startswith("_")
600
+ }
601
+ if hasattr(self, "set_instructions"):
602
+ if not self.set_instructions:
603
+ raw_data.pop("instruction")
604
+ if self.codebook == {}:
605
+ raw_data.pop("codebook")
606
+ if self.name == None:
607
+ raw_data.pop("name")
608
+
609
+ if hasattr(self, "dynamic_traits_function"):
610
+ raw_data.pop(
611
+ "dynamic_traits_function", None
612
+ ) # in case dynamic_traits_function will appear with _ in self.__dict__
613
+ dynamic_traits_func = self.dynamic_traits_function
614
+ if dynamic_traits_func:
615
+ func = inspect.getsource(dynamic_traits_func)
616
+ raw_data["dynamic_traits_function_source_code"] = func
617
+ raw_data[
618
+ "dynamic_traits_function_name"
619
+ ] = self.dynamic_traits_function_name
620
+ if hasattr(self, "answer_question_directly"):
621
+ raw_data.pop(
622
+ "answer_question_directly", None
623
+ ) # in case answer_question_directly will appear with _ in self.__dict__
624
+ answer_question_directly_func = self.answer_question_directly
625
+ # print(answer_question_directly_func)
626
+ # print(type(answer_question_directly_func), flush=True)
627
+
628
+ if (
629
+ answer_question_directly_func
630
+ and raw_data.get("answer_question_directly_source_code", None) != None
631
+ ):
632
+ raw_data["answer_question_directly_source_code"] = inspect.getsource(
633
+ answer_question_directly_func
634
+ )
635
+ raw_data[
636
+ "answer_question_directly_function_name"
637
+ ] = self.answer_question_directly_function_name
638
+
639
+ return raw_data
640
+
641
+ def __hash__(self) -> int:
642
+ from edsl.utilities.utilities import dict_hash
643
+
644
+ return dict_hash(self._to_dict())
645
+
646
+ def _to_dict(self) -> dict[str, Union[dict, bool]]:
647
+ """Serialize to a dictionary."""
648
+ return self.data
649
+
650
+ @add_edsl_version
651
+ def to_dict(self) -> dict[str, Union[dict, bool]]:
652
+ """Serialize to a dictionary.
653
+
654
+ Example usage:
655
+
656
+ >>> a = Agent(name = "Steve", traits = {"age": 10, "hair": "brown", "height": 5.5})
657
+ >>> a.to_dict()
658
+ {'name': 'Steve', 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}, 'edsl_version': '...', 'edsl_class_name': 'Agent'}
659
+ """
660
+ return self._to_dict()
661
+
662
+ @classmethod
663
+ @remove_edsl_version
664
+ def from_dict(cls, agent_dict: dict[str, Union[dict, bool]]) -> Agent:
665
+ """Deserialize from a dictionary.
666
+
667
+ Example usage:
668
+
669
+ >>> Agent.from_dict({'name': "Steve", 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}})
670
+ Agent(name = \"""Steve\""", traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
671
+
672
+ """
673
+ return cls(**agent_dict)
674
+
675
+ ################
676
+ # DISPLAY Methods
677
+ ################
678
+
679
+ def _table(self) -> tuple[dict, list]:
680
+ """Prepare generic table data."""
681
+ table_data = []
682
+ for attr_name, attr_value in self.__dict__.items():
683
+ table_data.append({"Attribute": attr_name, "Value": repr(attr_value)})
684
+ column_names = ["Attribute", "Value"]
685
+ return table_data, column_names
686
+
687
+ def add_trait(self, trait_name_or_dict: str, value: Optional[Any] = None) -> Agent:
688
+ """Adds a trait to an agent and returns that agent"""
689
+ if isinstance(trait_name_or_dict, dict) and value is None:
690
+ self.traits.update(trait_name_or_dict)
691
+ return self
692
+
693
+ if isinstance(trait_name_or_dict, dict) and value:
694
+ raise ValueError(f"You passed a dict: {trait_name_or_dict}")
695
+
696
+ if isinstance(trait_name_or_dict, str):
697
+ trait = trait_name_or_dict
698
+ self.traits[trait] = value
699
+ return self
700
+
701
+ raise Exception("Something is not right with adding")
702
+
703
+ def remove_trait(self, trait: str) -> Agent:
704
+ """Remove a trait from the agent.
705
+
706
+ Example usage:
707
+
708
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
709
+ >>> a.remove_trait("age")
710
+ Agent(traits = {'hair': 'brown', 'height': 5.5})
711
+ """
712
+ _ = self.traits.pop(trait)
713
+ return self
714
+
715
+ def translate_traits(self, values_codebook: dict) -> Agent:
716
+ """Translate traits to a new codebook.
717
+
718
+ >>> a = Agent(traits = {"age": 10, "hair": 1, "height": 5.5})
719
+ >>> a.translate_traits({"hair": {1:"brown"}})
720
+ Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
721
+
722
+ :param values_codebook: The new codebook.
723
+ """
724
+ for key, value in self.traits.items():
725
+ if key in values_codebook:
726
+ self.traits[key] = values_codebook[key][value]
727
+ return self
728
+
729
+ def rich_print(self):
730
+ """Display an object as a rich table.
731
+
732
+ Example usage:
733
+
734
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
735
+ >>> a.rich_print()
736
+ <rich.table.Table object at ...>
737
+ """
738
+ from rich.table import Table
739
+
740
+ table_data, column_names = self._table()
741
+ table = Table(title=f"{self.__class__.__name__} Attributes")
742
+ for column in column_names:
743
+ table.add_column(column, style="bold")
744
+
745
+ for row in table_data:
746
+ row_data = [row[column] for column in column_names]
747
+ table.add_row(*row_data)
748
+
749
+ return table
750
+
751
+ @classmethod
752
+ def example(cls, randomize: bool = False) -> Agent:
753
+ """
754
+ Returns an example Agent instance.
755
+
756
+ :param randomize: If True, adds a random string to the value of an example key.
757
+ """
758
+ addition = "" if not randomize else str(uuid4())
759
+ return cls(traits={"age": 22, "hair": f"brown{addition}", "height": 5.5})
760
+
761
+ def code(self) -> str:
762
+ """Return the code for the agent.
763
+
764
+ Example usage:
765
+
766
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
767
+ >>> print(a.code())
768
+ from edsl import Agent
769
+ agent = Agent(traits={'age': 10, 'hair': 'brown', 'height': 5.5})
770
+ """
771
+ return f"from edsl import Agent\nagent = Agent(traits={self.traits})"
772
+
773
+
774
+ def main():
775
+ """
776
+ Give an example of usage.
777
+
778
+ WARNING: Consume API credits
779
+ """
780
+ from edsl.agents import Agent
781
+ from edsl.questions import QuestionMultipleChoice
782
+
783
+ # a simple agent
784
+ agent = Agent(traits={"age": 10, "hair": "brown", "height": 5.5})
785
+ agent.traits
786
+ agent.print()
787
+ # combining two agents
788
+ agent = Agent(traits={"age": 10}) + Agent(traits={"height": 5.5})
789
+ agent.traits
790
+ # Agent -> Job using the to() method
791
+ agent = Agent(traits={"allergies": "peanut"})
792
+ question = QuestionMultipleChoice(
793
+ question_text="Would you enjoy a PB&J?",
794
+ question_options=["Yes", "No"],
795
+ question_name="food_preference",
796
+ )
797
+ job = question.by(agent)
798
+ results = job.run()
799
+
800
+
801
+ if __name__ == "__main__":
802
+ import doctest
803
+
804
+ doctest.testmod(optionflags=doctest.ELLIPSIS)