edsl 0.1.39.dev3__py3-none-any.whl → 0.1.39.dev5__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 (341) hide show
  1. edsl/Base.py +413 -332
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +57 -49
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +1071 -867
  7. edsl/agents/AgentList.py +551 -413
  8. edsl/agents/Invigilator.py +284 -233
  9. edsl/agents/InvigilatorBase.py +257 -270
  10. edsl/agents/PromptConstructor.py +272 -354
  11. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  12. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  13. edsl/agents/__init__.py +2 -3
  14. edsl/agents/descriptors.py +99 -99
  15. edsl/agents/prompt_helpers.py +129 -129
  16. edsl/agents/question_option_processor.py +172 -0
  17. edsl/auto/AutoStudy.py +130 -117
  18. edsl/auto/StageBase.py +243 -230
  19. edsl/auto/StageGenerateSurvey.py +178 -178
  20. edsl/auto/StageLabelQuestions.py +125 -125
  21. edsl/auto/StagePersona.py +61 -61
  22. edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
  23. edsl/auto/StagePersonaDimensionValues.py +74 -74
  24. edsl/auto/StagePersonaDimensions.py +69 -69
  25. edsl/auto/StageQuestions.py +74 -73
  26. edsl/auto/SurveyCreatorPipeline.py +21 -21
  27. edsl/auto/utilities.py +218 -224
  28. edsl/base/Base.py +279 -279
  29. edsl/config.py +177 -157
  30. edsl/conversation/Conversation.py +290 -290
  31. edsl/conversation/car_buying.py +59 -58
  32. edsl/conversation/chips.py +95 -95
  33. edsl/conversation/mug_negotiation.py +81 -81
  34. edsl/conversation/next_speaker_utilities.py +93 -93
  35. edsl/coop/CoopFunctionsMixin.py +15 -0
  36. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  37. edsl/coop/PriceFetcher.py +54 -54
  38. edsl/coop/__init__.py +2 -2
  39. edsl/coop/coop.py +1106 -1028
  40. edsl/coop/utils.py +131 -131
  41. edsl/data/Cache.py +573 -555
  42. edsl/data/CacheEntry.py +230 -233
  43. edsl/data/CacheHandler.py +168 -149
  44. edsl/data/RemoteCacheSync.py +186 -78
  45. edsl/data/SQLiteDict.py +292 -292
  46. edsl/data/__init__.py +5 -4
  47. edsl/data/orm.py +10 -10
  48. edsl/data_transfer_models.py +74 -73
  49. edsl/enums.py +202 -175
  50. edsl/exceptions/BaseException.py +21 -21
  51. edsl/exceptions/__init__.py +54 -54
  52. edsl/exceptions/agents.py +54 -42
  53. edsl/exceptions/cache.py +5 -5
  54. edsl/exceptions/configuration.py +16 -16
  55. edsl/exceptions/coop.py +10 -10
  56. edsl/exceptions/data.py +14 -14
  57. edsl/exceptions/general.py +34 -34
  58. edsl/exceptions/inference_services.py +5 -0
  59. edsl/exceptions/jobs.py +33 -33
  60. edsl/exceptions/language_models.py +63 -63
  61. edsl/exceptions/prompts.py +15 -15
  62. edsl/exceptions/questions.py +109 -91
  63. edsl/exceptions/results.py +29 -29
  64. edsl/exceptions/scenarios.py +29 -22
  65. edsl/exceptions/surveys.py +37 -37
  66. edsl/inference_services/AnthropicService.py +106 -87
  67. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  68. edsl/inference_services/AvailableModelFetcher.py +215 -0
  69. edsl/inference_services/AwsBedrock.py +118 -120
  70. edsl/inference_services/AzureAI.py +215 -217
  71. edsl/inference_services/DeepInfraService.py +18 -18
  72. edsl/inference_services/GoogleService.py +143 -148
  73. edsl/inference_services/GroqService.py +20 -20
  74. edsl/inference_services/InferenceServiceABC.py +80 -147
  75. edsl/inference_services/InferenceServicesCollection.py +138 -97
  76. edsl/inference_services/MistralAIService.py +120 -123
  77. edsl/inference_services/OllamaService.py +18 -18
  78. edsl/inference_services/OpenAIService.py +236 -224
  79. edsl/inference_services/PerplexityService.py +160 -163
  80. edsl/inference_services/ServiceAvailability.py +135 -0
  81. edsl/inference_services/TestService.py +90 -89
  82. edsl/inference_services/TogetherAIService.py +172 -170
  83. edsl/inference_services/data_structures.py +134 -0
  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 +41 -41
  87. edsl/inference_services/write_available.py +10 -10
  88. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  89. edsl/jobs/Answers.py +43 -56
  90. edsl/jobs/FetchInvigilator.py +47 -0
  91. edsl/jobs/InterviewTaskManager.py +98 -0
  92. edsl/jobs/InterviewsConstructor.py +50 -0
  93. edsl/jobs/Jobs.py +823 -898
  94. edsl/jobs/JobsChecks.py +172 -147
  95. edsl/jobs/JobsComponentConstructor.py +189 -0
  96. edsl/jobs/JobsPrompts.py +270 -268
  97. edsl/jobs/JobsRemoteInferenceHandler.py +311 -239
  98. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  99. edsl/jobs/RequestTokenEstimator.py +30 -0
  100. edsl/jobs/__init__.py +1 -1
  101. edsl/jobs/async_interview_runner.py +138 -0
  102. edsl/jobs/buckets/BucketCollection.py +104 -63
  103. edsl/jobs/buckets/ModelBuckets.py +65 -65
  104. edsl/jobs/buckets/TokenBucket.py +283 -251
  105. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  106. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  107. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  108. edsl/jobs/data_structures.py +120 -0
  109. edsl/jobs/decorators.py +35 -0
  110. edsl/jobs/interviews/Interview.py +396 -661
  111. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  112. edsl/jobs/interviews/InterviewExceptionEntry.py +186 -186
  113. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  114. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  115. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  116. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  117. edsl/jobs/interviews/ReportErrors.py +66 -66
  118. edsl/jobs/interviews/interview_status_enum.py +9 -9
  119. edsl/jobs/jobs_status_enums.py +9 -0
  120. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  121. edsl/jobs/results_exceptions_handler.py +98 -0
  122. edsl/jobs/runners/JobsRunnerAsyncio.py +151 -466
  123. edsl/jobs/runners/JobsRunnerStatus.py +297 -330
  124. edsl/jobs/tasks/QuestionTaskCreator.py +244 -242
  125. edsl/jobs/tasks/TaskCreators.py +64 -64
  126. edsl/jobs/tasks/TaskHistory.py +470 -450
  127. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  128. edsl/jobs/tasks/task_status_enum.py +161 -163
  129. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  130. edsl/jobs/tokens/TokenUsage.py +34 -34
  131. edsl/language_models/ComputeCost.py +63 -0
  132. edsl/language_models/LanguageModel.py +626 -668
  133. edsl/language_models/ModelList.py +164 -155
  134. edsl/language_models/PriceManager.py +127 -0
  135. edsl/language_models/RawResponseHandler.py +106 -0
  136. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  137. edsl/language_models/ServiceDataSources.py +0 -0
  138. edsl/language_models/__init__.py +2 -3
  139. edsl/language_models/fake_openai_call.py +15 -15
  140. edsl/language_models/fake_openai_service.py +61 -61
  141. edsl/language_models/key_management/KeyLookup.py +63 -0
  142. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  143. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  144. edsl/language_models/key_management/__init__.py +0 -0
  145. edsl/language_models/key_management/models.py +131 -0
  146. edsl/language_models/model.py +256 -0
  147. edsl/language_models/repair.py +156 -156
  148. edsl/language_models/utilities.py +65 -64
  149. edsl/notebooks/Notebook.py +263 -258
  150. edsl/notebooks/NotebookToLaTeX.py +142 -0
  151. edsl/notebooks/__init__.py +1 -1
  152. edsl/prompts/Prompt.py +352 -362
  153. edsl/prompts/__init__.py +2 -2
  154. edsl/questions/ExceptionExplainer.py +77 -0
  155. edsl/questions/HTMLQuestion.py +103 -0
  156. edsl/questions/QuestionBase.py +518 -664
  157. edsl/questions/QuestionBasePromptsMixin.py +221 -217
  158. edsl/questions/QuestionBudget.py +227 -227
  159. edsl/questions/QuestionCheckBox.py +359 -359
  160. edsl/questions/QuestionExtract.py +180 -182
  161. edsl/questions/QuestionFreeText.py +113 -114
  162. edsl/questions/QuestionFunctional.py +166 -166
  163. edsl/questions/QuestionList.py +223 -231
  164. edsl/questions/QuestionMatrix.py +265 -0
  165. edsl/questions/QuestionMultipleChoice.py +330 -286
  166. edsl/questions/QuestionNumerical.py +151 -153
  167. edsl/questions/QuestionRank.py +314 -324
  168. edsl/questions/Quick.py +41 -41
  169. edsl/questions/SimpleAskMixin.py +74 -73
  170. edsl/questions/__init__.py +27 -26
  171. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +334 -289
  172. edsl/questions/compose_questions.py +98 -98
  173. edsl/questions/data_structures.py +20 -0
  174. edsl/questions/decorators.py +21 -21
  175. edsl/questions/derived/QuestionLikertFive.py +76 -76
  176. edsl/questions/derived/QuestionLinearScale.py +90 -87
  177. edsl/questions/derived/QuestionTopK.py +93 -93
  178. edsl/questions/derived/QuestionYesNo.py +82 -82
  179. edsl/questions/descriptors.py +427 -413
  180. edsl/questions/loop_processor.py +149 -0
  181. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  182. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  183. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  184. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  185. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  186. edsl/questions/prompt_templates/question_list.jinja +17 -17
  187. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  188. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  189. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +168 -161
  190. edsl/questions/question_registry.py +177 -177
  191. edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +71 -71
  192. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +188 -174
  193. edsl/questions/response_validator_factory.py +34 -0
  194. edsl/questions/settings.py +12 -12
  195. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  196. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  197. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  198. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  199. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  200. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  201. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  202. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  203. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  204. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  205. edsl/questions/templates/list/question_presentation.jinja +5 -5
  206. edsl/questions/templates/matrix/__init__.py +1 -0
  207. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  208. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  209. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  210. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  211. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  212. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  213. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  214. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  215. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  216. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  217. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  218. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  219. edsl/results/CSSParameterizer.py +108 -108
  220. edsl/results/Dataset.py +587 -424
  221. edsl/results/DatasetExportMixin.py +594 -731
  222. edsl/results/DatasetTree.py +295 -275
  223. edsl/results/MarkdownToDocx.py +122 -0
  224. edsl/results/MarkdownToPDF.py +111 -0
  225. edsl/results/Result.py +557 -465
  226. edsl/results/Results.py +1183 -1165
  227. edsl/results/ResultsExportMixin.py +45 -43
  228. edsl/results/ResultsGGMixin.py +121 -121
  229. edsl/results/TableDisplay.py +125 -198
  230. edsl/results/TextEditor.py +50 -0
  231. edsl/results/__init__.py +2 -2
  232. edsl/results/file_exports.py +252 -0
  233. edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +33 -33
  234. edsl/results/{Selector.py → results_selector.py} +145 -135
  235. edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +98 -98
  236. edsl/results/smart_objects.py +96 -0
  237. edsl/results/table_data_class.py +12 -0
  238. edsl/results/table_display.css +77 -77
  239. edsl/results/table_renderers.py +118 -0
  240. edsl/results/tree_explore.py +115 -115
  241. edsl/scenarios/ConstructDownloadLink.py +109 -0
  242. edsl/scenarios/DocumentChunker.py +102 -0
  243. edsl/scenarios/DocxScenario.py +16 -0
  244. edsl/scenarios/FileStore.py +511 -632
  245. edsl/scenarios/PdfExtractor.py +40 -0
  246. edsl/scenarios/Scenario.py +498 -601
  247. edsl/scenarios/ScenarioHtmlMixin.py +65 -64
  248. edsl/scenarios/ScenarioList.py +1458 -1287
  249. edsl/scenarios/ScenarioListExportMixin.py +45 -52
  250. edsl/scenarios/ScenarioListPdfMixin.py +239 -261
  251. edsl/scenarios/__init__.py +3 -4
  252. edsl/scenarios/directory_scanner.py +96 -0
  253. edsl/scenarios/file_methods.py +85 -0
  254. edsl/scenarios/handlers/__init__.py +13 -0
  255. edsl/scenarios/handlers/csv.py +38 -0
  256. edsl/scenarios/handlers/docx.py +76 -0
  257. edsl/scenarios/handlers/html.py +37 -0
  258. edsl/scenarios/handlers/json.py +111 -0
  259. edsl/scenarios/handlers/latex.py +5 -0
  260. edsl/scenarios/handlers/md.py +51 -0
  261. edsl/scenarios/handlers/pdf.py +68 -0
  262. edsl/scenarios/handlers/png.py +39 -0
  263. edsl/scenarios/handlers/pptx.py +105 -0
  264. edsl/scenarios/handlers/py.py +294 -0
  265. edsl/scenarios/handlers/sql.py +313 -0
  266. edsl/scenarios/handlers/sqlite.py +149 -0
  267. edsl/scenarios/handlers/txt.py +33 -0
  268. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +131 -127
  269. edsl/scenarios/scenario_selector.py +156 -0
  270. edsl/shared.py +1 -1
  271. edsl/study/ObjectEntry.py +173 -173
  272. edsl/study/ProofOfWork.py +113 -113
  273. edsl/study/SnapShot.py +80 -80
  274. edsl/study/Study.py +521 -528
  275. edsl/study/__init__.py +4 -4
  276. edsl/surveys/ConstructDAG.py +92 -0
  277. edsl/surveys/DAG.py +148 -148
  278. edsl/surveys/EditSurvey.py +221 -0
  279. edsl/surveys/InstructionHandler.py +100 -0
  280. edsl/surveys/Memory.py +31 -31
  281. edsl/surveys/MemoryManagement.py +72 -0
  282. edsl/surveys/MemoryPlan.py +244 -244
  283. edsl/surveys/Rule.py +327 -326
  284. edsl/surveys/RuleCollection.py +385 -387
  285. edsl/surveys/RuleManager.py +172 -0
  286. edsl/surveys/Simulator.py +75 -0
  287. edsl/surveys/Survey.py +1280 -1801
  288. edsl/surveys/SurveyCSS.py +273 -261
  289. edsl/surveys/SurveyExportMixin.py +259 -259
  290. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +181 -179
  291. edsl/surveys/SurveyQualtricsImport.py +284 -284
  292. edsl/surveys/SurveyToApp.py +141 -0
  293. edsl/surveys/__init__.py +5 -3
  294. edsl/surveys/base.py +53 -53
  295. edsl/surveys/descriptors.py +60 -56
  296. edsl/surveys/instructions/ChangeInstruction.py +48 -49
  297. edsl/surveys/instructions/Instruction.py +56 -65
  298. edsl/surveys/instructions/InstructionCollection.py +82 -77
  299. edsl/templates/error_reporting/base.html +23 -23
  300. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  301. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  302. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  303. edsl/templates/error_reporting/interview_details.html +115 -115
  304. edsl/templates/error_reporting/interviews.html +19 -19
  305. edsl/templates/error_reporting/overview.html +4 -4
  306. edsl/templates/error_reporting/performance_plot.html +1 -1
  307. edsl/templates/error_reporting/report.css +73 -73
  308. edsl/templates/error_reporting/report.html +117 -117
  309. edsl/templates/error_reporting/report.js +25 -25
  310. edsl/tools/__init__.py +1 -1
  311. edsl/tools/clusters.py +192 -192
  312. edsl/tools/embeddings.py +27 -27
  313. edsl/tools/embeddings_plotting.py +118 -118
  314. edsl/tools/plotting.py +112 -112
  315. edsl/tools/summarize.py +18 -18
  316. edsl/utilities/PrettyList.py +56 -0
  317. edsl/utilities/SystemInfo.py +28 -28
  318. edsl/utilities/__init__.py +22 -22
  319. edsl/utilities/ast_utilities.py +25 -25
  320. edsl/utilities/data/Registry.py +6 -6
  321. edsl/utilities/data/__init__.py +1 -1
  322. edsl/utilities/data/scooter_results.json +1 -1
  323. edsl/utilities/decorators.py +77 -77
  324. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  325. edsl/utilities/interface.py +627 -627
  326. edsl/utilities/is_notebook.py +18 -0
  327. edsl/utilities/is_valid_variable_name.py +11 -0
  328. edsl/utilities/naming_utilities.py +263 -263
  329. edsl/utilities/remove_edsl_version.py +24 -0
  330. edsl/utilities/repair_functions.py +28 -28
  331. edsl/utilities/restricted_python.py +70 -70
  332. edsl/utilities/utilities.py +436 -424
  333. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/LICENSE +21 -21
  334. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/METADATA +13 -11
  335. edsl-0.1.39.dev5.dist-info/RECORD +358 -0
  336. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/WHEEL +1 -1
  337. edsl/language_models/KeyLookup.py +0 -30
  338. edsl/language_models/registry.py +0 -190
  339. edsl/language_models/unused/ReplicateBase.py +0 -83
  340. edsl/results/ResultsDBMixin.py +0 -238
  341. edsl-0.1.39.dev3.dist-info/RECORD +0 -277
edsl/agents/Agent.py CHANGED
@@ -1,867 +1,1071 @@
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 _repr_html_(self):
247
- # d = self.to_dict(add_edsl_version=False)
248
- d = self.traits
249
- data = [[k, v] for k, v in d.items()]
250
- from tabulate import tabulate
251
-
252
- table = str(tabulate(data, headers=["keys", "values"], tablefmt="html"))
253
- return f"<pre>{table}</pre>"
254
-
255
- def rename(
256
- self, old_name_or_dict: Union[str, dict], new_name: Optional[str] = None
257
- ) -> Agent:
258
- """Rename a trait.
259
-
260
- Example usage:
261
-
262
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
263
- >>> a.rename("age", "years") == Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5})
264
- True
265
-
266
- >>> a.rename({'years': 'smage'})
267
- Agent(traits = {'hair': 'brown', 'height': 5.5, 'smage': 10})
268
-
269
- """
270
- if isinstance(old_name_or_dict, dict) and new_name is None:
271
- for old_name, new_name in old_name_or_dict.items():
272
- self = self._rename(old_name, new_name)
273
- return self
274
-
275
- if isinstance(old_name_or_dict, dict) and new_name:
276
- raise AgentErrors(
277
- f"You passed a dict: {old_name_or_dict} and a new name: {new_name}. You should pass only a dict."
278
- )
279
-
280
- if isinstance(old_name_or_dict, str):
281
- self._rename(old_name_or_dict, new_name)
282
- return self
283
-
284
- raise AgentErrors("Something is not right with Agent renaming")
285
-
286
- def _rename(self, old_name: str, new_name: str) -> Agent:
287
- """Rename a trait.
288
-
289
- Example usage:
290
-
291
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
292
- >>> a.rename("age", "years") == Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5})
293
- True
294
- """
295
- self.traits[new_name] = self.traits.pop(old_name)
296
- return self
297
-
298
- def __getitem__(self, key):
299
- """Allow for accessing traits using the bracket notation.
300
-
301
- Example:
302
-
303
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
304
- >>> a['traits']['age']
305
- 10
306
-
307
- """
308
- return getattr(self, key)
309
-
310
- def remove_direct_question_answering_method(self) -> None:
311
- """Remove the direct question answering method.
312
-
313
- Example usage:
314
-
315
- >>> a = Agent()
316
- >>> def f(self, question, scenario): return "I am a direct answer."
317
- >>> a.add_direct_question_answering_method(f)
318
- >>> a.remove_direct_question_answering_method()
319
- >>> hasattr(a, "answer_question_directly")
320
- False
321
- """
322
- if hasattr(self, "answer_question_directly"):
323
- delattr(self, "answer_question_directly")
324
-
325
- def add_direct_question_answering_method(
326
- self,
327
- method: Callable,
328
- validate_response: bool = False,
329
- translate_response: bool = False,
330
- ) -> None:
331
- """Add a method to the agent that can answer a particular question type.
332
- https://docs.expectedparrot.com/en/latest/agents.html#agent-direct-answering-methods
333
-
334
- :param method: A method that can answer a question directly.
335
- :param validate_response: Whether to validate the response.
336
- :param translate_response: Whether to translate the response.
337
-
338
- Example usage:
339
-
340
- >>> a = Agent()
341
- >>> def f(self, question, scenario): return "I am a direct answer."
342
- >>> a.add_direct_question_answering_method(f)
343
- >>> a.answer_question_directly(question = None, scenario = None)
344
- 'I am a direct answer.'
345
- """
346
- if hasattr(self, "answer_question_directly"):
347
- import warnings
348
-
349
- warnings.warn(
350
- "Warning: overwriting existing answer_question_directly method"
351
- )
352
-
353
- self.validate_response = validate_response
354
- self.translate_response = translate_response
355
-
356
- signature = inspect.signature(method)
357
- for argument in ["question", "scenario", "self"]:
358
- if argument not in signature.parameters:
359
- raise AgentDirectAnswerFunctionError(
360
- f"The method {method} does not have a '{argument}' parameter."
361
- )
362
- bound_method = types.MethodType(method, self)
363
- setattr(self, "answer_question_directly", bound_method)
364
- self.answer_question_directly_function_name = bound_method.__name__
365
-
366
- def create_invigilator(
367
- self,
368
- *,
369
- question: "QuestionBase",
370
- cache: "Cache",
371
- survey: Optional["Survey"] = None,
372
- scenario: Optional["Scenario"] = None,
373
- model: Optional["LanguageModel"] = None,
374
- debug: bool = False,
375
- memory_plan: Optional["MemoryPlan"] = None,
376
- current_answers: Optional[dict] = None,
377
- iteration: int = 1,
378
- sidecar_model=None,
379
- raise_validation_errors: bool = True,
380
- ) -> "InvigilatorBase":
381
- """Create an Invigilator.
382
-
383
- An invigilator is an object that is responsible for administering a question to an agent.
384
- There are several different types of invigilators, depending on the type of question and the agent.
385
- 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.
386
-
387
- >>> a = Agent(traits = {})
388
- >>> a.create_invigilator(question = None, cache = False)
389
- InvigilatorAI(...)
390
-
391
- An invigator is an object that is responsible for administering a question to an agent and
392
- recording the responses.
393
- """
394
- from edsl import Model, Scenario
395
-
396
- cache = cache
397
- self.current_question = question
398
- model = model or Model()
399
- scenario = scenario or Scenario()
400
- invigilator = self._create_invigilator(
401
- question=question,
402
- scenario=scenario,
403
- survey=survey,
404
- model=model,
405
- debug=debug,
406
- memory_plan=memory_plan,
407
- current_answers=current_answers,
408
- iteration=iteration,
409
- cache=cache,
410
- sidecar_model=sidecar_model,
411
- raise_validation_errors=raise_validation_errors,
412
- )
413
- if hasattr(self, "validate_response"):
414
- invigilator.validate_response = self.validate_response
415
- if hasattr(self, "translate_response"):
416
- invigilator.translate_response = self.translate_response
417
- return invigilator
418
-
419
- async def async_answer_question(
420
- self,
421
- *,
422
- question: QuestionBase,
423
- cache: Cache,
424
- scenario: Optional[Scenario] = None,
425
- survey: Optional[Survey] = None,
426
- model: Optional[LanguageModel] = None,
427
- debug: bool = False,
428
- memory_plan: Optional[MemoryPlan] = None,
429
- current_answers: Optional[dict] = None,
430
- iteration: int = 0,
431
- ) -> AgentResponseDict:
432
- """
433
- Answer a posed question.
434
-
435
- :param question: The question to answer.
436
- :param scenario: The scenario in which the question is asked.
437
- :param model: The language model to use.
438
- :param debug: Whether to run in debug mode.
439
- :param memory_plan: The memory plan to use.
440
- :param current_answers: The current answers.
441
- :param iteration: The iteration number.
442
-
443
- >>> a = Agent(traits = {})
444
- >>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.")
445
- >>> from edsl import QuestionFreeText
446
- >>> q = QuestionFreeText.example()
447
- >>> a.answer_question(question = q, cache = False).answer
448
- 'I am a direct answer.'
449
-
450
- This is a function where an agent returns an answer to a particular question.
451
- However, there are several different ways an agent can answer a question, so the
452
- actual functionality is delegated to an :class:`edsl.agents.InvigilatorBase`: object.
453
- """
454
- invigilator = self.create_invigilator(
455
- question=question,
456
- cache=cache,
457
- scenario=scenario,
458
- survey=survey,
459
- model=model,
460
- debug=debug,
461
- memory_plan=memory_plan,
462
- current_answers=current_answers,
463
- iteration=iteration,
464
- )
465
- response: AgentResponseDict = await invigilator.async_answer_question()
466
- return response
467
-
468
- answer_question = sync_wrapper(async_answer_question)
469
-
470
- def _create_invigilator(
471
- self,
472
- question: QuestionBase,
473
- cache: Optional[Cache] = None,
474
- scenario: Optional[Scenario] = None,
475
- model: Optional[LanguageModel] = None,
476
- survey: Optional[Survey] = None,
477
- debug: bool = False,
478
- memory_plan: Optional[MemoryPlan] = None,
479
- current_answers: Optional[dict] = None,
480
- iteration: int = 0,
481
- sidecar_model=None,
482
- raise_validation_errors: bool = True,
483
- ) -> "InvigilatorBase":
484
- """Create an Invigilator."""
485
- from edsl import Model
486
- from edsl import Scenario
487
-
488
- model = model or Model()
489
- scenario = scenario or Scenario()
490
-
491
- from edsl.agents.Invigilator import (
492
- InvigilatorHuman,
493
- InvigilatorFunctional,
494
- InvigilatorAI,
495
- InvigilatorBase,
496
- )
497
-
498
- if cache is None:
499
- from edsl.data.Cache import Cache
500
-
501
- cache = Cache()
502
-
503
- if debug:
504
- raise NotImplementedError("Debug mode is not yet implemented.")
505
- # use the question's _simulate_answer method
506
- # invigilator_class = InvigilatorDebug
507
- elif hasattr(question, "answer_question_directly"):
508
- # It's a functional question and the answer only depends on the agent's traits & the scenario
509
- invigilator_class = InvigilatorFunctional
510
- elif hasattr(self, "answer_question_directly"):
511
- # this of the case where the agent has a method that can answer the question directly
512
- # this occurrs when 'answer_question_directly' has been given to the
513
- # which happens when the agent is created from an existing survey
514
- invigilator_class = InvigilatorHuman
515
- else:
516
- # this means an LLM agent will be used. This is the standard case.
517
- invigilator_class = InvigilatorAI
518
-
519
- if sidecar_model is not None:
520
- # this is the case when a 'simple' model is being used
521
- from edsl.agents.Invigilator import InvigilatorSidecar
522
-
523
- invigilator_class = InvigilatorSidecar
524
-
525
- invigilator = invigilator_class(
526
- self,
527
- question=question,
528
- scenario=scenario,
529
- survey=survey,
530
- model=model,
531
- memory_plan=memory_plan,
532
- current_answers=current_answers,
533
- iteration=iteration,
534
- cache=cache,
535
- sidecar_model=sidecar_model,
536
- raise_validation_errors=raise_validation_errors,
537
- )
538
- return invigilator
539
-
540
- def select(self, *traits: str) -> Agent:
541
- """Selects agents with only the references traits
542
-
543
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
544
-
545
-
546
- >>> a.select("age", "height")
547
- Agent(traits = {'age': 10, 'height': 5.5})
548
-
549
- >>> a.select("age")
550
- Agent(traits = {'age': 10})
551
-
552
- """
553
-
554
- if len(traits) == 1:
555
- traits_to_select = [list(traits)[0]]
556
- else:
557
- traits_to_select = list(traits)
558
-
559
- return Agent(traits={trait: self.traits[trait] for trait in traits_to_select})
560
-
561
- def __add__(self, other_agent: Optional[Agent] = None) -> Agent:
562
- """
563
- Combine two agents by joining their traits.
564
-
565
- The agents must not have overlapping traits.
566
-
567
- Example usage:
568
-
569
- >>> a1 = Agent(traits = {"age": 10})
570
- >>> a2 = Agent(traits = {"height": 5.5})
571
- >>> a1 + a2
572
- Agent(traits = {'age': 10, 'height': 5.5})
573
- >>> a1 + a1
574
- Traceback (most recent call last):
575
- ...
576
- edsl.exceptions.agents.AgentCombinationError: The agents have overlapping traits: {'age'}.
577
- ...
578
- """
579
- if other_agent is None:
580
- return self
581
- elif common_traits := set(self.traits.keys()) & set(other_agent.traits.keys()):
582
- raise AgentCombinationError(
583
- f"The agents have overlapping traits: {common_traits}."
584
- )
585
- else:
586
- new_agent = Agent(traits=copy.deepcopy(self.traits))
587
- new_agent.traits.update(other_agent.traits)
588
- return new_agent
589
-
590
- def __eq__(self, other: Agent) -> bool:
591
- """Check if two agents are equal.
592
-
593
- This only checks the traits.
594
- >>> a1 = Agent(traits = {"age": 10})
595
- >>> a2 = Agent(traits = {"age": 10})
596
- >>> a1 == a2
597
- True
598
- >>> a3 = Agent(traits = {"age": 11})
599
- >>> a1 == a3
600
- False
601
- """
602
- return self.data == other.data
603
-
604
- def __getattr__(self, name):
605
- # This will be called only if 'name' is not found in the usual places
606
- if name == "has_dynamic_traits_function":
607
- return self.has_dynamic_traits_function
608
-
609
- if name in self._traits:
610
- return self._traits[name]
611
-
612
- raise AttributeError(
613
- f"'{type(self).__name__}' object has no attribute '{name}'"
614
- )
615
-
616
- def __getstate__(self):
617
- state = self.__dict__.copy()
618
- # Include any additional state that needs to be serialized
619
- return state
620
-
621
- def __setstate__(self, state):
622
- self.__dict__.update(state)
623
- # Ensure _traits is initialized if it's missing
624
- if "_traits" not in self.__dict__:
625
- self._traits = {}
626
-
627
- def print(self) -> None:
628
- from rich import print_json
629
- import json
630
-
631
- print_json(json.dumps(self.to_dict()))
632
-
633
- def __repr__(self) -> str:
634
- """Return representation of Agent."""
635
- class_name = self.__class__.__name__
636
- items = [
637
- f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
638
- for k, v in self.data.items()
639
- if k != "question_type"
640
- ]
641
- return f"{class_name}({', '.join(items)})"
642
-
643
- # def _repr_html_(self):
644
- # from edsl.utilities.utilities import data_to_html
645
-
646
- # return data_to_html(self.to_dict())
647
-
648
- #######################
649
- # SERIALIZATION METHODS
650
- #######################
651
- @property
652
- def data(self) -> dict:
653
- """Format the data for serialization.
654
-
655
- TODO: Warn if has dynamic traits function or direct answer function that cannot be serialized.
656
- TODO: Add ability to have coop-hosted functions that are serializable.
657
- """
658
-
659
- raw_data = {
660
- k.replace("_", "", 1): v
661
- for k, v in self.__dict__.items()
662
- if k.startswith("_")
663
- }
664
-
665
- if hasattr(self, "set_instructions"):
666
- if not self.set_instructions:
667
- raw_data.pop("instruction")
668
- if self.codebook == {}:
669
- raw_data.pop("codebook")
670
- if self.name == None:
671
- raw_data.pop("name")
672
-
673
- if hasattr(self, "dynamic_traits_function"):
674
- raw_data.pop(
675
- "dynamic_traits_function", None
676
- ) # in case dynamic_traits_function will appear with _ in self.__dict__
677
- dynamic_traits_func = self.dynamic_traits_function
678
- if dynamic_traits_func:
679
- func = inspect.getsource(dynamic_traits_func)
680
- raw_data["dynamic_traits_function_source_code"] = func
681
- raw_data[
682
- "dynamic_traits_function_name"
683
- ] = self.dynamic_traits_function_name
684
- if hasattr(self, "answer_question_directly"):
685
- raw_data.pop(
686
- "answer_question_directly", None
687
- ) # in case answer_question_directly will appear with _ in self.__dict__
688
- answer_question_directly_func = self.answer_question_directly
689
-
690
- if (
691
- answer_question_directly_func
692
- and raw_data.get("answer_question_directly_source_code", None) != None
693
- ):
694
- raw_data["answer_question_directly_source_code"] = inspect.getsource(
695
- answer_question_directly_func
696
- )
697
- raw_data[
698
- "answer_question_directly_function_name"
699
- ] = self.answer_question_directly_function_name
700
-
701
- return raw_data
702
-
703
- def __hash__(self) -> int:
704
- from edsl.utilities.utilities import dict_hash
705
-
706
- return dict_hash(self.to_dict(add_edsl_version=False))
707
-
708
- # @add_edsl_version
709
- def to_dict(self, add_edsl_version=True) -> dict[str, Union[dict, bool]]:
710
- """Serialize to a dictionary with EDSL info.
711
-
712
- Example usage:
713
-
714
- >>> a = Agent(name = "Steve", traits = {"age": 10, "hair": "brown", "height": 5.5})
715
- >>> a.to_dict()
716
- {'name': 'Steve', 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}, 'edsl_version': '...', 'edsl_class_name': 'Agent'}
717
- """
718
- d = copy.deepcopy(self.data)
719
- if add_edsl_version:
720
- from edsl import __version__
721
-
722
- d["edsl_version"] = __version__
723
- d["edsl_class_name"] = self.__class__.__name__
724
-
725
- return d
726
-
727
- @classmethod
728
- @remove_edsl_version
729
- def from_dict(cls, agent_dict: dict[str, Union[dict, bool]]) -> Agent:
730
- """Deserialize from a dictionary.
731
-
732
- Example usage:
733
-
734
- >>> Agent.from_dict({'name': "Steve", 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}})
735
- Agent(name = \"""Steve\""", traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
736
-
737
- """
738
- return cls(**agent_dict)
739
-
740
- def _table(self) -> tuple[dict, list]:
741
- """Prepare generic table data."""
742
- table_data = []
743
- for attr_name, attr_value in self.__dict__.items():
744
- table_data.append({"Attribute": attr_name, "Value": repr(attr_value)})
745
- column_names = ["Attribute", "Value"]
746
- return table_data, column_names
747
-
748
- def add_trait(self, trait_name_or_dict: str, value: Optional[Any] = None) -> Agent:
749
- """Adds a trait to an agent and returns that agent"""
750
- if isinstance(trait_name_or_dict, dict) and value is None:
751
- self.traits.update(trait_name_or_dict)
752
- return self
753
-
754
- if isinstance(trait_name_or_dict, dict) and value:
755
- raise AgentErrors(
756
- f"You passed a dict: {trait_name_or_dict} and a value: {value}. You should pass only a dict."
757
- )
758
-
759
- if isinstance(trait_name_or_dict, str):
760
- trait = trait_name_or_dict
761
- self.traits[trait] = value
762
- return self
763
-
764
- raise AgentErrors("Something is not right with adding a trait to an Agent")
765
-
766
- def remove_trait(self, trait: str) -> Agent:
767
- """Remove a trait from the agent.
768
-
769
- Example usage:
770
-
771
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
772
- >>> a.remove_trait("age")
773
- Agent(traits = {'hair': 'brown', 'height': 5.5})
774
- """
775
- _ = self.traits.pop(trait)
776
- return self
777
-
778
- def translate_traits(self, values_codebook: dict) -> Agent:
779
- """Translate traits to a new codebook.
780
-
781
- >>> a = Agent(traits = {"age": 10, "hair": 1, "height": 5.5})
782
- >>> a.translate_traits({"hair": {1:"brown"}})
783
- Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
784
-
785
- :param values_codebook: The new codebook.
786
- """
787
- for key, value in self.traits.items():
788
- if key in values_codebook:
789
- self.traits[key] = values_codebook[key][value]
790
- return self
791
-
792
- def rich_print(self):
793
- """Display an object as a rich table.
794
-
795
- Example usage:
796
-
797
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
798
- >>> a.rich_print()
799
- <rich.table.Table object at ...>
800
- """
801
- from rich.table import Table
802
-
803
- table_data, column_names = self._table()
804
- table = Table(title=f"{self.__class__.__name__} Attributes")
805
- for column in column_names:
806
- table.add_column(column, style="bold")
807
-
808
- for row in table_data:
809
- row_data = [row[column] for column in column_names]
810
- table.add_row(*row_data)
811
-
812
- return table
813
-
814
- @classmethod
815
- def example(cls, randomize: bool = False) -> Agent:
816
- """
817
- Returns an example Agent instance.
818
-
819
- :param randomize: If True, adds a random string to the value of an example key.
820
- """
821
- addition = "" if not randomize else str(uuid4())
822
- return cls(traits={"age": 22, "hair": f"brown{addition}", "height": 5.5})
823
-
824
- def code(self) -> str:
825
- """Return the code for the agent.
826
-
827
- Example usage:
828
-
829
- >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
830
- >>> print(a.code())
831
- from edsl import Agent
832
- agent = Agent(traits={'age': 10, 'hair': 'brown', 'height': 5.5})
833
- """
834
- return f"from edsl import Agent\nagent = Agent(traits={self.traits})"
835
-
836
-
837
- def main():
838
- """
839
- Give an example of usage.
840
-
841
- WARNING: Consume API credits
842
- """
843
- from edsl.agents import Agent
844
- from edsl.questions import QuestionMultipleChoice
845
-
846
- # a simple agent
847
- agent = Agent(traits={"age": 10, "hair": "brown", "height": 5.5})
848
- agent.traits
849
- agent.print()
850
- # combining two agents
851
- agent = Agent(traits={"age": 10}) + Agent(traits={"height": 5.5})
852
- agent.traits
853
- # Agent -> Job using the to() method
854
- agent = Agent(traits={"allergies": "peanut"})
855
- question = QuestionMultipleChoice(
856
- question_text="Would you enjoy a PB&J?",
857
- question_options=["Yes", "No"],
858
- question_name="food_preference",
859
- )
860
- job = question.by(agent)
861
- results = job.run()
862
-
863
-
864
- if __name__ == "__main__":
865
- import doctest
866
-
867
- 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 (
8
+ Callable,
9
+ Optional,
10
+ Union,
11
+ Any,
12
+ TYPE_CHECKING,
13
+ Protocol,
14
+ runtime_checkable,
15
+ TypeVar,
16
+ )
17
+ from contextlib import contextmanager
18
+ from dataclasses import dataclass
19
+
20
+ # Type variable for the Agent class
21
+ A = TypeVar("A", bound="Agent")
22
+
23
+ if TYPE_CHECKING:
24
+ from edsl.data.Cache import Cache
25
+ from edsl.surveys.Survey import Survey
26
+ from edsl.scenarios.Scenario import Scenario
27
+ from edsl.language_models import LanguageModel
28
+ from edsl.surveys.MemoryPlan import MemoryPlan
29
+ from edsl.questions import QuestionBase
30
+ from edsl.agents.Invigilator import InvigilatorBase
31
+ from edsl.prompts import Prompt
32
+ from edsl.questions.QuestionBase import QuestionBase
33
+ from edsl.scenarios.Scenario import Scenario
34
+
35
+
36
+ @runtime_checkable
37
+ class DirectAnswerMethod(Protocol):
38
+ """Protocol defining the required signature for direct answer methods."""
39
+
40
+ def __call__(self, self_: A, question: QuestionBase, scenario: Scenario) -> Any: ...
41
+
42
+
43
+ from uuid import uuid4
44
+
45
+ from edsl.Base import Base
46
+ from edsl.exceptions.questions import QuestionScenarioRenderError
47
+
48
+ from edsl.exceptions.agents import (
49
+ AgentErrors,
50
+ AgentCombinationError,
51
+ AgentDirectAnswerFunctionError,
52
+ AgentDynamicTraitsFunctionError,
53
+ )
54
+
55
+ from edsl.agents.descriptors import (
56
+ TraitsDescriptor,
57
+ CodebookDescriptor,
58
+ InstructionDescriptor,
59
+ NameDescriptor,
60
+ )
61
+ from edsl.utilities.decorators import (
62
+ sync_wrapper,
63
+ )
64
+ from edsl.utilities.remove_edsl_version import remove_edsl_version
65
+ from edsl.data_transfer_models import AgentResponseDict
66
+ from edsl.utilities.restricted_python import create_restricted_function
67
+
68
+ from edsl.scenarios.Scenario import Scenario
69
+
70
+
71
+ class AgentTraits(Scenario):
72
+ """A class representing the traits of an agent."""
73
+
74
+ def __repr__(self):
75
+ return f"{self.data}"
76
+
77
+
78
+ class Agent(Base):
79
+ """An class representing an agent that can answer questions."""
80
+
81
+ __documentation__ = "https://docs.expectedparrot.com/en/latest/agents.html"
82
+
83
+ default_instruction = """You are answering questions as if you were a human. Do not break character."""
84
+
85
+ _traits = TraitsDescriptor()
86
+ codebook = CodebookDescriptor()
87
+ instruction = InstructionDescriptor()
88
+ name = NameDescriptor()
89
+ dynamic_traits_function_name = ""
90
+ answer_question_directly_function_name = ""
91
+ has_dynamic_traits_function = False
92
+
93
+ def __init__(
94
+ self,
95
+ traits: Optional[dict] = None,
96
+ name: Optional[str] = None,
97
+ codebook: Optional[dict] = None,
98
+ instruction: Optional[str] = None,
99
+ traits_presentation_template: Optional[str] = None,
100
+ dynamic_traits_function: Optional[Callable] = None,
101
+ dynamic_traits_function_source_code: Optional[str] = None,
102
+ dynamic_traits_function_name: Optional[str] = None,
103
+ answer_question_directly_source_code: Optional[str] = None,
104
+ answer_question_directly_function_name: Optional[str] = None,
105
+ ):
106
+ """Initialize a new instance of Agent.
107
+
108
+ :param traits: A dictionary of traits that the agent has. The keys need to be valid identifiers.
109
+ :param name: A name for the agent
110
+ :param codebook: A codebook mapping trait keys to trait descriptions.
111
+ :param instruction: Instructions for the agent in how to answer questions.
112
+ :param trait_presentation_template: A template for how to present the agent's traits.
113
+ :param dynamic_traits_function: A function that returns a dictionary of traits.
114
+ :param dynamic_traits_function_source_code: The source code for the dynamic traits function.
115
+ :param dynamic_traits_function_name: The name of the dynamic traits function.
116
+
117
+ The `traits` parameter is a dictionary of traits that the agent has.
118
+ These traits are used to construct a prompt that is presented to the LLM.
119
+ In the absence of a `traits_presentation_template`, the default is used.
120
+ This is a template that is used to present the agent's traits to the LLM.
121
+ See :py:class:`edsl.prompts.library.agent_persona.AgentPersona` for more information.
122
+
123
+ Example usage:
124
+
125
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
126
+ >>> a.traits
127
+ {'age': 10, 'hair': 'brown', 'height': 5.5}
128
+
129
+ These traits are used to construct a prompt that is presented to the LLM.
130
+
131
+ In the absence of a `traits_presentation_template`, the default is used.
132
+
133
+ >>> a = Agent(traits = {"age": 10}, traits_presentation_template = "I am a {{age}} year old.")
134
+ >>> repr(a.agent_persona)
135
+ 'Prompt(text=\"""I am a {{age}} year old.\""")'
136
+
137
+ When this is rendered for presentation to the LLM, it will replace the `{{age}}` with the actual age.
138
+ it is also possible to use the `codebook` to provide a more human-readable description of the trait.
139
+ Here is an example where we give a prefix to the age trait (namely the age):
140
+
141
+ >>> traits = {"age": 10, "hair": "brown", "height": 5.5}
142
+ >>> codebook = {'age': 'Their age is'}
143
+ >>> a = Agent(traits = traits, codebook = codebook, traits_presentation_template = "This agent is Dave. {{codebook['age']}} {{age}}")
144
+ >>> d = a.traits | {'codebook': a.codebook}
145
+ >>> a.agent_persona.render(d)
146
+ Prompt(text=\"""This agent is Dave. Their age is 10\""")
147
+
148
+ Instructions
149
+ ------------
150
+ The agent can also have instructions. These are instructions that are given to the agent when answering questions.
151
+
152
+ >>> Agent.default_instruction
153
+ 'You are answering questions as if you were a human. Do not break character.'
154
+
155
+ See see how these are used to actually construct the prompt that is presented to the LLM, see :py:class:`edsl.agents.Invigilator.InvigilatorBase`.
156
+
157
+ """
158
+ self._initialize_basic_attributes(traits, name, codebook)
159
+ self._initialize_instruction(instruction)
160
+ self._initialize_dynamic_traits_function(
161
+ dynamic_traits_function,
162
+ dynamic_traits_function_source_code,
163
+ dynamic_traits_function_name,
164
+ )
165
+ self._initialize_answer_question_directly(
166
+ answer_question_directly_source_code, answer_question_directly_function_name
167
+ )
168
+ self._check_dynamic_traits_function()
169
+ self._initialize_traits_presentation_template(traits_presentation_template)
170
+ self.current_question = None
171
+
172
+ def _initialize_basic_attributes(self, traits, name, codebook) -> None:
173
+ """Initialize the basic attributes of the agent."""
174
+ self.name = name
175
+ self._traits = AgentTraits(traits or dict())
176
+ self.codebook = codebook or dict()
177
+
178
+ def _initialize_instruction(self, instruction) -> None:
179
+ """Initialize the instruction for the agent i.e., how the agent should answer questions."""
180
+ if instruction is None:
181
+ self.instruction = self.default_instruction
182
+ self._instruction = self.default_instruction
183
+ self.set_instructions = False
184
+ else:
185
+ self.instruction = instruction
186
+ self._instruction = instruction
187
+ self.set_instructions = True
188
+
189
+ def _initialize_traits_presentation_template(
190
+ self, traits_presentation_template
191
+ ) -> None:
192
+ """Initialize the traits presentation template. How the agent's traits are presented to the LLM."""
193
+ if traits_presentation_template is not None:
194
+ self._traits_presentation_template = traits_presentation_template
195
+ self.traits_presentation_template = traits_presentation_template
196
+ self.set_traits_presentation_template = True
197
+ else:
198
+ self.traits_presentation_template = "Your traits: {{traits}}"
199
+ self.set_traits_presentation_template = False
200
+
201
+ def _initialize_dynamic_traits_function(
202
+ self,
203
+ dynamic_traits_function,
204
+ dynamic_traits_function_source_code,
205
+ dynamic_traits_function_name,
206
+ ) -> None:
207
+ """Initialize the dynamic traits function i.e., a function that returns a dictionary of traits based on the question."""
208
+ # Deal with dynamic traits function
209
+ self.dynamic_traits_function = dynamic_traits_function
210
+
211
+ if self.dynamic_traits_function:
212
+ self.dynamic_traits_function_name = self.dynamic_traits_function.__name__
213
+ self.has_dynamic_traits_function = True
214
+ else:
215
+ self.has_dynamic_traits_function = False
216
+
217
+ if dynamic_traits_function_source_code:
218
+ self.dynamic_traits_function_name = dynamic_traits_function_name
219
+ self.dynamic_traits_function = create_restricted_function(
220
+ dynamic_traits_function_name, dynamic_traits_function
221
+ )
222
+
223
+ def _initialize_answer_question_directly(
224
+ self,
225
+ answer_question_directly_source_code,
226
+ answer_question_directly_function_name,
227
+ ) -> None:
228
+ if answer_question_directly_source_code:
229
+ self.answer_question_directly_function_name = (
230
+ answer_question_directly_function_name
231
+ )
232
+ protected_method = create_restricted_function(
233
+ answer_question_directly_function_name,
234
+ answer_question_directly_source_code,
235
+ )
236
+ bound_method = types.MethodType(protected_method, self)
237
+ setattr(self, "answer_question_directly", bound_method)
238
+
239
+ def _initialize_traits_presentation_template(
240
+ self, traits_presentation_template
241
+ ) -> None:
242
+ if traits_presentation_template is not None:
243
+ self._traits_presentation_template = traits_presentation_template
244
+ self.traits_presentation_template = traits_presentation_template
245
+ self.set_traits_presentation_template = True
246
+ else:
247
+ self.traits_presentation_template = "Your traits: {{traits}}"
248
+ self.set_traits_presentation_template = False
249
+
250
+ def duplicate(self) -> Agent:
251
+ """Return a duplicate of the agent.
252
+
253
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}, codebook = {'age': 'Their age is'})
254
+ >>> a2 = a.duplicate()
255
+ >>> a2 == a
256
+ True
257
+ >>> id(a) == id(a2)
258
+ False
259
+ >>> def f(self, question, scenario): return "I am a direct answer."
260
+ >>> a.add_direct_question_answering_method(f)
261
+ >>> hasattr(a, "answer_question_directly")
262
+ True
263
+ >>> a2 = a.duplicate()
264
+ >>> a2.answer_question_directly(None, None)
265
+ 'I am a direct answer.'
266
+
267
+ >>> a = Agent(traits = {'age': 10}, instruction = "Have fun!")
268
+ >>> a2 = a.duplicate()
269
+ >>> a2.instruction
270
+ 'Have fun!'
271
+ """
272
+ new_agent = Agent.from_dict(self.to_dict())
273
+ if hasattr(self, "answer_question_directly"):
274
+ answer_question_directly = self.answer_question_directly
275
+ newf = lambda self, question, scenario: answer_question_directly(
276
+ question, scenario
277
+ )
278
+ new_agent.add_direct_question_answering_method(newf)
279
+ if hasattr(self, "dynamic_traits_function"):
280
+ dynamic_traits_function = self.dynamic_traits_function
281
+ new_agent.dynamic_traits_function = dynamic_traits_function
282
+ return new_agent
283
+
284
+ @property
285
+ def agent_persona(self) -> Prompt:
286
+ """Return the agent persona template."""
287
+ from edsl.prompts.Prompt import Prompt
288
+
289
+ return Prompt(text=self.traits_presentation_template)
290
+
291
+ def prompt(self) -> str:
292
+ """Return the prompt for the agent.
293
+
294
+ Example usage:
295
+
296
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
297
+ >>> a.prompt()
298
+ Prompt(text=\"""Your traits: {'age': 10, 'hair': 'brown', 'height': 5.5}\""")
299
+ """
300
+ replacement_dict = (
301
+ self.traits | {"traits": self.traits} | {"codebook": self.codebook}
302
+ )
303
+ if undefined := self.agent_persona.undefined_template_variables(
304
+ replacement_dict
305
+ ):
306
+ raise QuestionScenarioRenderError(
307
+ f"Agent persona still has variables that were not rendered: {undefined}"
308
+ )
309
+ else:
310
+ return self.agent_persona.render(replacement_dict)
311
+
312
+ def _check_dynamic_traits_function(self) -> None:
313
+ """Check whether dynamic trait function is valid.
314
+
315
+ This checks whether the dynamic traits function is valid.
316
+
317
+ >>> def f(question): return {"age": 10, "hair": "brown", "height": 5.5}
318
+ >>> a = Agent(dynamic_traits_function = f)
319
+ >>> a._check_dynamic_traits_function()
320
+
321
+ >>> def g(question, poo): return {"age": 10, "hair": "brown", "height": 5.5}
322
+ >>> a = Agent(dynamic_traits_function = g)
323
+ Traceback (most recent call last):
324
+ ...
325
+ edsl.exceptions.agents.AgentDynamicTraitsFunctionError: ...
326
+ """
327
+ if self.has_dynamic_traits_function:
328
+ sig = inspect.signature(self.dynamic_traits_function)
329
+ if "question" in sig.parameters:
330
+ if len(sig.parameters) > 1:
331
+ raise AgentDynamicTraitsFunctionError(
332
+ message=f"The dynamic traits function {self.dynamic_traits_function} has too many parameters. It should only have one parameter: 'question'."
333
+ )
334
+ else:
335
+ if len(sig.parameters) > 0:
336
+ raise AgentDynamicTraitsFunctionError(
337
+ f"""The dynamic traits function {self.dynamic_traits_function} has too many parameters. It should have no parameters or
338
+ just a single parameter: 'question'."""
339
+ )
340
+
341
+ @property
342
+ def traits(self) -> dict[str, str]:
343
+ """An agent's traits, which is a dictionary.
344
+
345
+ The agent could have a a dynamic traits function (`dynamic_traits_function`) that returns a dictionary of traits
346
+ when called. This function can also take a `question` as an argument.
347
+ If so, the dynamic traits function is called and the result is returned.
348
+ Otherwise, the traits are returned.
349
+
350
+ Example:
351
+
352
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
353
+ >>> a.traits
354
+ {'age': 10, 'hair': 'brown', 'height': 5.5}
355
+
356
+ """
357
+ if self.has_dynamic_traits_function:
358
+ sig = inspect.signature(self.dynamic_traits_function)
359
+ if "question" in sig.parameters:
360
+ return self.dynamic_traits_function(question=self.current_question)
361
+ else:
362
+ return self.dynamic_traits_function()
363
+ else:
364
+ return dict(self._traits)
365
+
366
+ @contextmanager
367
+ def modify_traits_context(self):
368
+ self._check_before_modifying_traits()
369
+ try:
370
+ yield
371
+ finally:
372
+ self._traits = AgentTraits(self._traits)
373
+
374
+ def _check_before_modifying_traits(self):
375
+ """Check before modifying traits."""
376
+ if self.has_dynamic_traits_function:
377
+ raise AgentErrors(
378
+ "You cannot modify the traits of an agent that has a dynamic traits function.",
379
+ "If you want to modify the traits, you should remove the dynamic traits function.",
380
+ )
381
+
382
+ @traits.setter
383
+ def traits(self, traits: dict[str, str]):
384
+ with self.modify_traits_context():
385
+ self._traits = traits
386
+ # self._check_before_modifying_traits()
387
+ # self._traits = AgentTraits(traits)
388
+
389
+ def rename(
390
+ self,
391
+ old_name_or_dict: Union[str, dict[str, str]],
392
+ new_name: Optional[str] = None,
393
+ ) -> Agent:
394
+ """Rename a trait.
395
+
396
+ :param old_name_or_dict: The old name of the trait or a dictionary of old names and new names.
397
+ :param new_name: The new name of the trait.
398
+
399
+ Example usage:
400
+
401
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
402
+ >>> newa = a.rename("age", "years")
403
+ >>> newa == Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5})
404
+ True
405
+
406
+ >>> newa.rename({'years': 'smage'}) == Agent(traits = {'smage': 10, 'hair': 'brown', 'height': 5.5})
407
+ True
408
+
409
+ """
410
+ self._check_before_modifying_traits()
411
+ if isinstance(old_name_or_dict, dict) and new_name:
412
+ raise AgentErrors(
413
+ f"You passed a dict: {old_name_or_dict} and a new name: {new_name}. You should pass only a dict."
414
+ )
415
+
416
+ if isinstance(old_name_or_dict, dict) and new_name is None:
417
+ return self._rename_dict(old_name_or_dict)
418
+
419
+ if isinstance(old_name_or_dict, str):
420
+ return self._rename(old_name_or_dict, new_name)
421
+
422
+ raise AgentErrors("Something is not right with Agent renaming")
423
+
424
+ def _rename_dict(self, renaming_dict: dict[str, str]) -> Agent:
425
+ """
426
+ Internal method to rename traits using a dictionary.
427
+ The keys should all be old names and the values should all be new names.
428
+
429
+ Example usage:
430
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
431
+ >>> a._rename_dict({"age": "years", "height": "feet"})
432
+ Agent(traits = {'years': 10, 'hair': 'brown', 'feet': 5.5})
433
+
434
+ """
435
+ try:
436
+ assert all(k in self.traits for k in renaming_dict.keys())
437
+ except AssertionError:
438
+ raise AgentErrors(
439
+ f"The trait(s) {set(renaming_dict.keys()) - set(self.traits.keys())} do not exist in the agent's traits, which are {self.traits}."
440
+ )
441
+ new_agent = self.duplicate()
442
+ new_agent.traits = {renaming_dict.get(k, k): v for k, v in self.traits.items()}
443
+ return new_agent
444
+
445
+ def _rename(self, old_name: str, new_name: str) -> Agent:
446
+ """Rename a trait.
447
+
448
+ Example usage:
449
+
450
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
451
+ >>> a._rename(old_name="age", new_name="years")
452
+ Agent(traits = {'years': 10, 'hair': 'brown', 'height': 5.5})
453
+
454
+ """
455
+ try:
456
+ assert old_name in self.traits
457
+ except AssertionError:
458
+ raise AgentErrors(
459
+ f"The trait '{old_name}' does not exist in the agent's traits, which are {self.traits}."
460
+ )
461
+ newagent = self.duplicate()
462
+ newagent.traits = {
463
+ new_name if k == old_name else k: v for k, v in self.traits.items()
464
+ }
465
+ newagent.codebook = {
466
+ new_name if k == old_name else k: v for k, v in self.codebook.items()
467
+ }
468
+ return newagent
469
+
470
+ def __getitem__(self, key):
471
+ """Allow for accessing traits using the bracket notation.
472
+
473
+ Example:
474
+
475
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
476
+ >>> a['traits']['age']
477
+ 10
478
+
479
+ """
480
+ return getattr(self, key)
481
+
482
+ def remove_direct_question_answering_method(self) -> None:
483
+ """Remove the direct question answering method.
484
+
485
+ Example usage:
486
+
487
+ >>> a = Agent()
488
+ >>> def f(self, question, scenario): return "I am a direct answer."
489
+ >>> a.add_direct_question_answering_method(f)
490
+ >>> a.remove_direct_question_answering_method()
491
+ >>> hasattr(a, "answer_question_directly")
492
+ False
493
+ """
494
+ if hasattr(self, "answer_question_directly"):
495
+ delattr(self, "answer_question_directly")
496
+
497
+ def add_direct_question_answering_method(
498
+ self,
499
+ method: DirectAnswerMethod,
500
+ validate_response: bool = False,
501
+ translate_response: bool = False,
502
+ ) -> None:
503
+ """Add a method to the agent that can answer a particular question type.
504
+ https://docs.expectedparrot.com/en/latest/agents.html#agent-direct-answering-methods
505
+
506
+ :param method: A method that can answer a question directly.
507
+ :param validate_response: Whether to validate the response.
508
+ :param translate_response: Whether to translate the response.
509
+
510
+ Example usage:
511
+
512
+ >>> a = Agent()
513
+ >>> def f(self, question, scenario): return "I am a direct answer."
514
+ >>> a.add_direct_question_answering_method(f)
515
+ >>> a.answer_question_directly(question = None, scenario = None)
516
+ 'I am a direct answer.'
517
+ """
518
+ if hasattr(self, "answer_question_directly"):
519
+ import warnings
520
+
521
+ warnings.warn(
522
+ "Warning: overwriting existing answer_question_directly method"
523
+ )
524
+
525
+ self.validate_response = validate_response
526
+ self.translate_response = translate_response
527
+
528
+ # if not isinstance(method, DirectAnswerMethod):
529
+ # raise AgentDirectAnswerFunctionError(
530
+ # f"Method {method} does not match required signature. "
531
+ # "Must take (self, question, scenario) parameters."
532
+ # )
533
+
534
+ signature = inspect.signature(method)
535
+ for argument in ["question", "scenario", "self"]:
536
+ if argument not in signature.parameters:
537
+ raise AgentDirectAnswerFunctionError(
538
+ f"The method {method} does not have a '{argument}' parameter."
539
+ )
540
+ bound_method = types.MethodType(method, self)
541
+ setattr(self, "answer_question_directly", bound_method)
542
+ self.answer_question_directly_function_name = bound_method.__name__
543
+
544
+ def create_invigilator(
545
+ self,
546
+ *,
547
+ question: "QuestionBase",
548
+ cache: "Cache",
549
+ survey: Optional["Survey"] = None,
550
+ scenario: Optional["Scenario"] = None,
551
+ model: Optional["LanguageModel"] = None,
552
+ memory_plan: Optional["MemoryPlan"] = None,
553
+ current_answers: Optional[dict] = None,
554
+ iteration: int = 1,
555
+ raise_validation_errors: bool = True,
556
+ key_lookup: Optional["KeyLookup"] = None,
557
+ ) -> "InvigilatorBase":
558
+ """Create an Invigilator.
559
+
560
+ An invigilator is an object that is responsible for administering a question to an agent.
561
+ There are several different types of invigilators, depending on the type of question and the agent.
562
+ 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.
563
+
564
+ >>> a = Agent(traits = {})
565
+ >>> a.create_invigilator(question = None, cache = False)
566
+ InvigilatorAI(...)
567
+
568
+ An invigator is an object that is responsible for administering a question to an agent and
569
+ recording the responses.
570
+ """
571
+ from edsl.language_models.model import Model
572
+
573
+ from edsl.scenarios.Scenario import Scenario
574
+
575
+ cache = cache
576
+ self.current_question = question
577
+ model = model or Model()
578
+ scenario = scenario or Scenario()
579
+ invigilator = self._create_invigilator(
580
+ question=question,
581
+ scenario=scenario,
582
+ survey=survey,
583
+ model=model,
584
+ memory_plan=memory_plan,
585
+ current_answers=current_answers,
586
+ iteration=iteration,
587
+ cache=cache,
588
+ raise_validation_errors=raise_validation_errors,
589
+ key_lookup=key_lookup,
590
+ )
591
+ if hasattr(self, "validate_response"):
592
+ invigilator.validate_response = self.validate_response
593
+ if hasattr(self, "translate_response"):
594
+ invigilator.translate_response = self.translate_response
595
+ return invigilator
596
+
597
+ async def async_answer_question(
598
+ self,
599
+ *,
600
+ question: QuestionBase,
601
+ cache: Cache,
602
+ scenario: Optional[Scenario] = None,
603
+ survey: Optional[Survey] = None,
604
+ model: Optional[LanguageModel] = None,
605
+ debug: bool = False,
606
+ memory_plan: Optional[MemoryPlan] = None,
607
+ current_answers: Optional[dict] = None,
608
+ iteration: int = 0,
609
+ key_lookup: Optional["KeyLookup"] = None,
610
+ ) -> AgentResponseDict:
611
+ """
612
+ Answer a posed question.
613
+
614
+ :param question: The question to answer.
615
+ :param scenario: The scenario in which the question is asked.
616
+ :param model: The language model to use.
617
+ :param debug: Whether to run in debug mode.
618
+ :param memory_plan: The memory plan to use.
619
+ :param current_answers: The current answers.
620
+ :param iteration: The iteration number.
621
+
622
+ >>> a = Agent(traits = {})
623
+ >>> a.add_direct_question_answering_method(lambda self, question, scenario: "I am a direct answer.")
624
+ >>> from edsl.questions.QuestionFreeText import QuestionFreeText
625
+ >>> q = QuestionFreeText.example()
626
+ >>> a.answer_question(question = q, cache = False).answer
627
+ 'I am a direct answer.'
628
+
629
+ This is a function where an agent returns an answer to a particular question.
630
+ However, there are several different ways an agent can answer a question, so the
631
+ actual functionality is delegated to an :class:`edsl.agents.InvigilatorBase`: object.
632
+ """
633
+ invigilator = self.create_invigilator(
634
+ question=question,
635
+ cache=cache,
636
+ scenario=scenario,
637
+ survey=survey,
638
+ model=model,
639
+ memory_plan=memory_plan,
640
+ current_answers=current_answers,
641
+ iteration=iteration,
642
+ key_lookup=key_lookup,
643
+ )
644
+ response: AgentResponseDict = await invigilator.async_answer_question()
645
+ return response
646
+
647
+ answer_question = sync_wrapper(async_answer_question)
648
+
649
+ def _get_invigilator_class(self, question: QuestionBase) -> Type[InvigilatorBase]:
650
+ """Get the invigilator class for a question.
651
+
652
+ This method returns the invigilator class that should be used to answer a question.
653
+ The invigilator class is determined by the type of question and the type of agent.
654
+ """
655
+ from edsl.agents.Invigilator import (
656
+ InvigilatorHuman,
657
+ InvigilatorFunctional,
658
+ InvigilatorAI,
659
+ )
660
+
661
+ if hasattr(question, "answer_question_directly"):
662
+ return InvigilatorFunctional
663
+ elif hasattr(self, "answer_question_directly"):
664
+ return InvigilatorHuman
665
+ else:
666
+ return InvigilatorAI
667
+
668
+ def _create_invigilator(
669
+ self,
670
+ question: QuestionBase,
671
+ cache: Optional[Cache] = None,
672
+ scenario: Optional[Scenario] = None,
673
+ model: Optional[LanguageModel] = None,
674
+ survey: Optional[Survey] = None,
675
+ memory_plan: Optional[MemoryPlan] = None,
676
+ current_answers: Optional[dict] = None,
677
+ iteration: int = 0,
678
+ raise_validation_errors: bool = True,
679
+ key_lookup: Optional["KeyLookup"] = None,
680
+ ) -> "InvigilatorBase":
681
+ """Create an Invigilator."""
682
+ from edsl.language_models.model import Model
683
+ from edsl.scenarios.Scenario import Scenario
684
+
685
+ model = model or Model()
686
+ scenario = scenario or Scenario()
687
+
688
+ if cache is None:
689
+ from edsl.data.Cache import Cache
690
+
691
+ cache = Cache()
692
+
693
+ invigilator_class = self._get_invigilator_class(question)
694
+
695
+ invigilator = invigilator_class(
696
+ self,
697
+ question=question,
698
+ scenario=scenario,
699
+ survey=survey,
700
+ model=model,
701
+ memory_plan=memory_plan,
702
+ current_answers=current_answers,
703
+ iteration=iteration,
704
+ cache=cache,
705
+ raise_validation_errors=raise_validation_errors,
706
+ key_lookup=key_lookup,
707
+ )
708
+ return invigilator
709
+
710
+ def select(self, *traits: str) -> Agent:
711
+ """Selects agents with only the references traits
712
+
713
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}, codebook = {'age': 'Their age is'})
714
+ >>> a
715
+ Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5}, codebook = {'age': 'Their age is'})
716
+
717
+
718
+ >>> a.select("age", "height")
719
+ Agent(traits = {'age': 10, 'height': 5.5}, codebook = {'age': 'Their age is'})
720
+
721
+ >>> a.select("height")
722
+ Agent(traits = {'height': 5.5})
723
+
724
+ """
725
+
726
+ if len(traits) == 1:
727
+ traits_to_select = [list(traits)[0]]
728
+ else:
729
+ traits_to_select = list(traits)
730
+
731
+ def _remove_none(d):
732
+ return {k: v for k, v in d.items() if v is not None}
733
+
734
+ newagent = self.duplicate()
735
+ newagent.traits = {
736
+ trait: self.traits.get(trait, None) for trait in traits_to_select
737
+ }
738
+ newagent.codebook = _remove_none(
739
+ {trait: self.codebook.get(trait, None) for trait in traits_to_select}
740
+ )
741
+ return newagent
742
+
743
+ def __add__(self, other_agent: Optional[Agent] = None) -> Agent:
744
+ """
745
+ Combine two agents by joining their traits.
746
+
747
+ The agents must not have overlapping traits.
748
+
749
+ Example usage:
750
+
751
+ >>> a1 = Agent(traits = {"age": 10})
752
+ >>> a2 = Agent(traits = {"height": 5.5})
753
+ >>> a1 + a2
754
+ Agent(traits = {'age': 10, 'height': 5.5})
755
+ >>> a1 + a1
756
+ Traceback (most recent call last):
757
+ ...
758
+ edsl.exceptions.agents.AgentCombinationError: The agents have overlapping traits: {'age'}.
759
+ ...
760
+ >>> a1 = Agent(traits = {"age": 10}, codebook = {"age": "Their age is"})
761
+ >>> a2 = Agent(traits = {"height": 5.5}, codebook = {"height": "Their height is"})
762
+ >>> a1 + a2
763
+ Agent(traits = {'age': 10, 'height': 5.5}, codebook = {'age': 'Their age is', 'height': 'Their height is'})
764
+ """
765
+ if other_agent is None:
766
+ return self
767
+ elif common_traits := set(self.traits.keys()) & set(other_agent.traits.keys()):
768
+ raise AgentCombinationError(
769
+ f"The agents have overlapping traits: {common_traits}."
770
+ )
771
+ else:
772
+ new_codebook = copy.deepcopy(self.codebook) | copy.deepcopy(
773
+ other_agent.codebook
774
+ )
775
+ d = self.traits | other_agent.traits
776
+ newagent = self.duplicate()
777
+ newagent.traits = d
778
+ newagent.codebook = new_codebook
779
+ return newagent
780
+
781
+ def __eq__(self, other: Agent) -> bool:
782
+ """Check if two agents are equal.
783
+
784
+ This only checks the traits.
785
+ >>> a1 = Agent(traits = {"age": 10})
786
+ >>> a2 = Agent(traits = {"age": 10})
787
+ >>> a1 == a2
788
+ True
789
+ >>> a3 = Agent(traits = {"age": 11})
790
+ >>> a1 == a3
791
+ False
792
+ """
793
+ return self.data == other.data
794
+
795
+ def __getattr__(self, name):
796
+ """
797
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
798
+ >>> a.age
799
+ 10
800
+ """
801
+ if name == "has_dynamic_traits_function":
802
+ return self.has_dynamic_traits_function
803
+
804
+ if name in self._traits:
805
+ return self._traits[name]
806
+
807
+ raise AttributeError(
808
+ f"'{type(self).__name__}' object has no attribute '{name}'"
809
+ )
810
+
811
+ def __getstate__(self):
812
+ state = self.__dict__.copy()
813
+ # Include any additional state that needs to be serialized
814
+ return state
815
+
816
+ def __setstate__(self, state):
817
+ self.__dict__.update(state)
818
+ # Ensure _traits is initialized if it's missing
819
+ if "_traits" not in self.__dict__:
820
+ self._traits = {}
821
+
822
+ def __repr__(self) -> str:
823
+ """Return representation of Agent."""
824
+ class_name = self.__class__.__name__
825
+ items = [
826
+ f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
827
+ for k, v in self.data.items()
828
+ if k != "question_type"
829
+ ]
830
+ return f"{class_name}({', '.join(items)})"
831
+
832
+ @property
833
+ def data(self) -> dict:
834
+ """Format the data for serialization.
835
+
836
+ TODO: Warn if has dynamic traits function or direct answer function that cannot be serialized.
837
+ TODO: Add ability to have coop-hosted functions that are serializable.
838
+ """
839
+
840
+ raw_data = {
841
+ k.replace("_", "", 1): v
842
+ for k, v in self.__dict__.items()
843
+ if k.startswith("_")
844
+ }
845
+
846
+ if hasattr(self, "set_instructions"):
847
+ if not self.set_instructions:
848
+ raw_data.pop("instruction")
849
+ if self.codebook == {}:
850
+ raw_data.pop("codebook")
851
+ if self.name == None:
852
+ raw_data.pop("name")
853
+
854
+ if hasattr(self, "dynamic_traits_function"):
855
+ raw_data.pop(
856
+ "dynamic_traits_function", None
857
+ ) # in case dynamic_traits_function will appear with _ in self.__dict__
858
+ dynamic_traits_func = self.dynamic_traits_function
859
+ if dynamic_traits_func:
860
+ func = inspect.getsource(dynamic_traits_func)
861
+ raw_data["dynamic_traits_function_source_code"] = func
862
+ raw_data["dynamic_traits_function_name"] = (
863
+ self.dynamic_traits_function_name
864
+ )
865
+ if hasattr(self, "answer_question_directly"):
866
+ raw_data.pop(
867
+ "answer_question_directly", None
868
+ ) # in case answer_question_directly will appear with _ in self.__dict__
869
+ answer_question_directly_func = self.answer_question_directly
870
+
871
+ if (
872
+ answer_question_directly_func
873
+ and raw_data.get("answer_question_directly_source_code", None) != None
874
+ ):
875
+ raw_data["answer_question_directly_source_code"] = inspect.getsource(
876
+ answer_question_directly_func
877
+ )
878
+ raw_data["answer_question_directly_function_name"] = (
879
+ self.answer_question_directly_function_name
880
+ )
881
+ raw_data["traits"] = dict(raw_data["traits"])
882
+
883
+ return raw_data
884
+
885
+ def __hash__(self) -> int:
886
+ """Return a hash of the agent.
887
+
888
+ >>> hash(Agent.example())
889
+ 2067581884874391607
890
+ """
891
+ from edsl.utilities.utilities import dict_hash
892
+
893
+ return dict_hash(self.to_dict(add_edsl_version=False))
894
+
895
+ def to_dict(self, add_edsl_version=True) -> dict[str, Union[dict, bool]]:
896
+ """Serialize to a dictionary with EDSL info.
897
+
898
+ Example usage:
899
+
900
+ >>> a = Agent(name = "Steve", traits = {"age": 10, "hair": "brown", "height": 5.5})
901
+ >>> a.to_dict()
902
+ {'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}, 'name': 'Steve', 'edsl_version': '...', 'edsl_class_name': 'Agent'}
903
+
904
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5}, instruction = "Have fun.")
905
+ >>> a.to_dict()
906
+ {'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}, 'instruction': 'Have fun.', 'edsl_version': '...', 'edsl_class_name': 'Agent'}
907
+ """
908
+ d = {}
909
+ d["traits"] = copy.deepcopy(self.traits)
910
+ if self.name:
911
+ d["name"] = self.name
912
+ if self.set_instructions:
913
+ d["instruction"] = self.instruction
914
+ if self.set_traits_presentation_template:
915
+ d["traits_presentation_template"] = self.traits_presentation_template
916
+ if self.codebook:
917
+ d["codebook"] = self.codebook
918
+ if add_edsl_version:
919
+ from edsl import __version__
920
+
921
+ d["edsl_version"] = __version__
922
+ d["edsl_class_name"] = self.__class__.__name__
923
+
924
+ return d
925
+
926
+ @classmethod
927
+ @remove_edsl_version
928
+ def from_dict(cls, agent_dict: dict[str, Union[dict, bool]]) -> Agent:
929
+ """Deserialize from a dictionary.
930
+
931
+ Example usage:
932
+
933
+ >>> Agent.from_dict({'name': "Steve", 'traits': {'age': 10, 'hair': 'brown', 'height': 5.5}})
934
+ Agent(name = \"""Steve\""", traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
935
+
936
+ """
937
+ if "traits" in agent_dict:
938
+ return cls(
939
+ traits=agent_dict["traits"],
940
+ name=agent_dict.get("name", None),
941
+ instruction=agent_dict.get("instruction", None),
942
+ traits_presentation_template=agent_dict.get(
943
+ "traits_presentation_template", None
944
+ ),
945
+ codebook=agent_dict.get("codebook", None),
946
+ )
947
+ else: # old-style agent - we used to only store the traits
948
+ return cls(**agent_dict)
949
+
950
+ def _table(self) -> tuple[dict, list]:
951
+ """Prepare generic table data."""
952
+ table_data = []
953
+ for attr_name, attr_value in self.__dict__.items():
954
+ table_data.append({"Attribute": attr_name, "Value": repr(attr_value)})
955
+ column_names = ["Attribute", "Value"]
956
+ return table_data, column_names
957
+
958
+ def add_trait(self, trait_name_or_dict: str, value: Optional[Any] = None) -> Agent:
959
+ """Adds a trait to an agent and returns that agent
960
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
961
+ >>> a.add_trait("weight", 150)
962
+ Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5, 'weight': 150})
963
+ """
964
+ if isinstance(trait_name_or_dict, dict) and value is None:
965
+ newagent = self.duplicate()
966
+ newagent.traits = {**self.traits, **trait_name_or_dict}
967
+ return newagent
968
+
969
+ if isinstance(trait_name_or_dict, dict) and value:
970
+ raise AgentErrors(
971
+ f"You passed a dict: {trait_name_or_dict} and a value: {value}. You should pass only a dict."
972
+ )
973
+
974
+ if isinstance(trait_name_or_dict, str):
975
+ newagent = self.duplicate()
976
+ newagent.traits = {**self.traits, **{trait_name_or_dict: value}}
977
+ return newagent
978
+
979
+ raise AgentErrors("Something is not right with adding a trait to an Agent")
980
+
981
+ def remove_trait(self, trait: str) -> Agent:
982
+ """Remove a trait from the agent.
983
+
984
+ Example usage:
985
+
986
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
987
+ >>> a.remove_trait("age")
988
+ Agent(traits = {'hair': 'brown', 'height': 5.5})
989
+ """
990
+ newagent = self.duplicate()
991
+ newagent.traits = {k: v for k, v in self.traits.items() if k != trait}
992
+ return newagent
993
+
994
+ def translate_traits(self, values_codebook: dict) -> Agent:
995
+ """Translate traits to a new codebook.
996
+
997
+ >>> a = Agent(traits = {"age": 10, "hair": 1, "height": 5.5})
998
+ >>> a.translate_traits({"hair": {1:"brown"}})
999
+ Agent(traits = {'age': 10, 'hair': 'brown', 'height': 5.5})
1000
+
1001
+ :param values_codebook: The new codebook.
1002
+ """
1003
+ new_traits = {}
1004
+ for key, value in self.traits.items():
1005
+ if key in values_codebook:
1006
+ new_traits[key] = values_codebook[key].get(value, value)
1007
+ else:
1008
+ new_traits[key] = value
1009
+ newagent = self.duplicate()
1010
+ newagent.traits = new_traits
1011
+ return newagent
1012
+
1013
+ @classmethod
1014
+ def example(cls, randomize: bool = False) -> Agent:
1015
+ """
1016
+ Returns an example Agent instance.
1017
+
1018
+ :param randomize: If True, adds a random string to the value of an example key.
1019
+
1020
+ >>> Agent.example()
1021
+ Agent(traits = {'age': 22, 'hair': 'brown', 'height': 5.5})
1022
+ """
1023
+ addition = "" if not randomize else str(uuid4())
1024
+ return cls(traits={"age": 22, "hair": f"brown{addition}", "height": 5.5})
1025
+
1026
+ def code(self) -> str:
1027
+ """Return the code for the agent.
1028
+
1029
+ Example usage:
1030
+
1031
+ >>> a = Agent(traits = {"age": 10, "hair": "brown", "height": 5.5})
1032
+ >>> print(a.code())
1033
+ from edsl.agents.Agent import Agent
1034
+ agent = Agent(traits={'age': 10, 'hair': 'brown', 'height': 5.5})
1035
+ """
1036
+ return (
1037
+ f"from edsl.agents.Agent import Agent\nagent = Agent(traits={self.traits})"
1038
+ )
1039
+
1040
+
1041
+ def main():
1042
+ """
1043
+ Give an example of usage.
1044
+
1045
+ WARNING: Consume API credits
1046
+ """
1047
+ from edsl.agents import Agent
1048
+ from edsl.questions import QuestionMultipleChoice
1049
+
1050
+ # a simple agent
1051
+ agent = Agent(traits={"age": 10, "hair": "brown", "height": 5.5})
1052
+ agent.traits
1053
+ agent.print()
1054
+ # combining two agents
1055
+ agent = Agent(traits={"age": 10}) + Agent(traits={"height": 5.5})
1056
+ agent.traits
1057
+ # Agent -> Job using the to() method
1058
+ agent = Agent(traits={"allergies": "peanut"})
1059
+ question = QuestionMultipleChoice(
1060
+ question_text="Would you enjoy a PB&J?",
1061
+ question_options=["Yes", "No"],
1062
+ question_name="food_preference",
1063
+ )
1064
+ job = question.by(agent)
1065
+ results = job.run()
1066
+
1067
+
1068
+ if __name__ == "__main__":
1069
+ import doctest
1070
+
1071
+ doctest.testmod(optionflags=doctest.ELLIPSIS)