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