edsl 0.1.38.dev2__py3-none-any.whl → 0.1.38.dev3__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 (248) hide show
  1. edsl/Base.py +303 -303
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +49 -49
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +858 -858
  7. edsl/agents/AgentList.py +362 -362
  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 +279 -279
  26. edsl/config.py +149 -149
  27. edsl/conversation/Conversation.py +290 -290
  28. edsl/conversation/car_buying.py +58 -58
  29. edsl/conversation/chips.py +95 -95
  30. edsl/conversation/mug_negotiation.py +81 -81
  31. edsl/conversation/next_speaker_utilities.py +93 -93
  32. edsl/coop/PriceFetcher.py +54 -54
  33. edsl/coop/__init__.py +2 -2
  34. edsl/coop/coop.py +961 -961
  35. edsl/coop/utils.py +131 -131
  36. edsl/data/Cache.py +530 -530
  37. edsl/data/CacheEntry.py +228 -228
  38. edsl/data/CacheHandler.py +149 -149
  39. edsl/data/RemoteCacheSync.py +97 -97
  40. edsl/data/SQLiteDict.py +292 -292
  41. edsl/data/__init__.py +4 -4
  42. edsl/data/orm.py +10 -10
  43. edsl/data_transfer_models.py +73 -73
  44. edsl/enums.py +173 -173
  45. edsl/exceptions/BaseException.py +21 -21
  46. edsl/exceptions/__init__.py +54 -54
  47. edsl/exceptions/agents.py +42 -42
  48. edsl/exceptions/cache.py +5 -5
  49. edsl/exceptions/configuration.py +16 -16
  50. edsl/exceptions/coop.py +10 -10
  51. edsl/exceptions/data.py +14 -14
  52. edsl/exceptions/general.py +34 -34
  53. edsl/exceptions/jobs.py +33 -33
  54. edsl/exceptions/language_models.py +63 -63
  55. edsl/exceptions/prompts.py +15 -15
  56. edsl/exceptions/questions.py +91 -91
  57. edsl/exceptions/results.py +29 -29
  58. edsl/exceptions/scenarios.py +22 -22
  59. edsl/exceptions/surveys.py +37 -37
  60. edsl/inference_services/AnthropicService.py +87 -87
  61. edsl/inference_services/AwsBedrock.py +120 -120
  62. edsl/inference_services/AzureAI.py +217 -217
  63. edsl/inference_services/DeepInfraService.py +18 -18
  64. edsl/inference_services/GoogleService.py +156 -156
  65. edsl/inference_services/GroqService.py +20 -20
  66. edsl/inference_services/InferenceServiceABC.py +147 -147
  67. edsl/inference_services/InferenceServicesCollection.py +97 -97
  68. edsl/inference_services/MistralAIService.py +123 -123
  69. edsl/inference_services/OllamaService.py +18 -18
  70. edsl/inference_services/OpenAIService.py +224 -224
  71. edsl/inference_services/TestService.py +89 -89
  72. edsl/inference_services/TogetherAIService.py +170 -170
  73. edsl/inference_services/models_available_cache.py +118 -118
  74. edsl/inference_services/rate_limits_cache.py +25 -25
  75. edsl/inference_services/registry.py +39 -39
  76. edsl/inference_services/write_available.py +10 -10
  77. edsl/jobs/Answers.py +56 -56
  78. edsl/jobs/Jobs.py +1358 -1358
  79. edsl/jobs/__init__.py +1 -1
  80. edsl/jobs/buckets/BucketCollection.py +63 -63
  81. edsl/jobs/buckets/ModelBuckets.py +65 -65
  82. edsl/jobs/buckets/TokenBucket.py +251 -251
  83. edsl/jobs/interviews/Interview.py +661 -661
  84. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  85. edsl/jobs/interviews/InterviewExceptionEntry.py +186 -186
  86. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  87. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  88. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  89. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  90. edsl/jobs/interviews/ReportErrors.py +66 -66
  91. edsl/jobs/interviews/interview_status_enum.py +9 -9
  92. edsl/jobs/runners/JobsRunnerAsyncio.py +361 -361
  93. edsl/jobs/runners/JobsRunnerStatus.py +332 -332
  94. edsl/jobs/tasks/QuestionTaskCreator.py +242 -242
  95. edsl/jobs/tasks/TaskCreators.py +64 -64
  96. edsl/jobs/tasks/TaskHistory.py +451 -451
  97. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  98. edsl/jobs/tasks/task_status_enum.py +163 -163
  99. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  100. edsl/jobs/tokens/TokenUsage.py +34 -34
  101. edsl/language_models/KeyLookup.py +30 -30
  102. edsl/language_models/LanguageModel.py +708 -708
  103. edsl/language_models/ModelList.py +109 -109
  104. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  105. edsl/language_models/__init__.py +3 -3
  106. edsl/language_models/fake_openai_call.py +15 -15
  107. edsl/language_models/fake_openai_service.py +61 -61
  108. edsl/language_models/registry.py +137 -137
  109. edsl/language_models/repair.py +156 -156
  110. edsl/language_models/unused/ReplicateBase.py +83 -83
  111. edsl/language_models/utilities.py +64 -64
  112. edsl/notebooks/Notebook.py +258 -258
  113. edsl/notebooks/__init__.py +1 -1
  114. edsl/prompts/Prompt.py +357 -357
  115. edsl/prompts/__init__.py +2 -2
  116. edsl/questions/AnswerValidatorMixin.py +289 -289
  117. edsl/questions/QuestionBase.py +660 -660
  118. edsl/questions/QuestionBaseGenMixin.py +161 -161
  119. edsl/questions/QuestionBasePromptsMixin.py +217 -217
  120. edsl/questions/QuestionBudget.py +227 -227
  121. edsl/questions/QuestionCheckBox.py +359 -359
  122. edsl/questions/QuestionExtract.py +183 -183
  123. edsl/questions/QuestionFreeText.py +114 -114
  124. edsl/questions/QuestionFunctional.py +166 -166
  125. edsl/questions/QuestionList.py +231 -231
  126. edsl/questions/QuestionMultipleChoice.py +286 -286
  127. edsl/questions/QuestionNumerical.py +153 -153
  128. edsl/questions/QuestionRank.py +324 -324
  129. edsl/questions/Quick.py +41 -41
  130. edsl/questions/RegisterQuestionsMeta.py +71 -71
  131. edsl/questions/ResponseValidatorABC.py +174 -174
  132. edsl/questions/SimpleAskMixin.py +73 -73
  133. edsl/questions/__init__.py +26 -26
  134. edsl/questions/compose_questions.py +98 -98
  135. edsl/questions/decorators.py +21 -21
  136. edsl/questions/derived/QuestionLikertFive.py +76 -76
  137. edsl/questions/derived/QuestionLinearScale.py +87 -87
  138. edsl/questions/derived/QuestionTopK.py +93 -93
  139. edsl/questions/derived/QuestionYesNo.py +82 -82
  140. edsl/questions/descriptors.py +413 -413
  141. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  142. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  143. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  144. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  145. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  146. edsl/questions/prompt_templates/question_list.jinja +17 -17
  147. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  148. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  149. edsl/questions/question_registry.py +147 -147
  150. edsl/questions/settings.py +12 -12
  151. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  152. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  153. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  154. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  155. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  156. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  157. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  158. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  159. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  160. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  161. edsl/questions/templates/list/question_presentation.jinja +5 -5
  162. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  163. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  164. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  165. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  166. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  167. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  168. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  169. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  170. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  171. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  172. edsl/results/Dataset.py +293 -293
  173. edsl/results/DatasetExportMixin.py +717 -717
  174. edsl/results/DatasetTree.py +145 -145
  175. edsl/results/Result.py +456 -456
  176. edsl/results/Results.py +1071 -1071
  177. edsl/results/ResultsDBMixin.py +238 -238
  178. edsl/results/ResultsExportMixin.py +43 -43
  179. edsl/results/ResultsFetchMixin.py +33 -33
  180. edsl/results/ResultsGGMixin.py +121 -121
  181. edsl/results/ResultsToolsMixin.py +98 -98
  182. edsl/results/Selector.py +135 -135
  183. edsl/results/__init__.py +2 -2
  184. edsl/results/tree_explore.py +115 -115
  185. edsl/scenarios/FileStore.py +458 -458
  186. edsl/scenarios/Scenario.py +544 -544
  187. edsl/scenarios/ScenarioHtmlMixin.py +64 -64
  188. edsl/scenarios/ScenarioList.py +1112 -1112
  189. edsl/scenarios/ScenarioListExportMixin.py +52 -52
  190. edsl/scenarios/ScenarioListPdfMixin.py +261 -261
  191. edsl/scenarios/__init__.py +4 -4
  192. edsl/shared.py +1 -1
  193. edsl/study/ObjectEntry.py +173 -173
  194. edsl/study/ProofOfWork.py +113 -113
  195. edsl/study/SnapShot.py +80 -80
  196. edsl/study/Study.py +528 -528
  197. edsl/study/__init__.py +4 -4
  198. edsl/surveys/DAG.py +148 -148
  199. edsl/surveys/Memory.py +31 -31
  200. edsl/surveys/MemoryPlan.py +244 -244
  201. edsl/surveys/Rule.py +326 -326
  202. edsl/surveys/RuleCollection.py +387 -387
  203. edsl/surveys/Survey.py +1787 -1787
  204. edsl/surveys/SurveyCSS.py +261 -261
  205. edsl/surveys/SurveyExportMixin.py +259 -259
  206. edsl/surveys/SurveyFlowVisualizationMixin.py +121 -121
  207. edsl/surveys/SurveyQualtricsImport.py +284 -284
  208. edsl/surveys/__init__.py +3 -3
  209. edsl/surveys/base.py +53 -53
  210. edsl/surveys/descriptors.py +56 -56
  211. edsl/surveys/instructions/ChangeInstruction.py +49 -49
  212. edsl/surveys/instructions/Instruction.py +53 -53
  213. edsl/surveys/instructions/InstructionCollection.py +77 -77
  214. edsl/templates/error_reporting/base.html +23 -23
  215. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  216. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  217. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  218. edsl/templates/error_reporting/interview_details.html +115 -115
  219. edsl/templates/error_reporting/interviews.html +9 -9
  220. edsl/templates/error_reporting/overview.html +4 -4
  221. edsl/templates/error_reporting/performance_plot.html +1 -1
  222. edsl/templates/error_reporting/report.css +73 -73
  223. edsl/templates/error_reporting/report.html +117 -117
  224. edsl/templates/error_reporting/report.js +25 -25
  225. edsl/tools/__init__.py +1 -1
  226. edsl/tools/clusters.py +192 -192
  227. edsl/tools/embeddings.py +27 -27
  228. edsl/tools/embeddings_plotting.py +118 -118
  229. edsl/tools/plotting.py +112 -112
  230. edsl/tools/summarize.py +18 -18
  231. edsl/utilities/SystemInfo.py +28 -28
  232. edsl/utilities/__init__.py +22 -22
  233. edsl/utilities/ast_utilities.py +25 -25
  234. edsl/utilities/data/Registry.py +6 -6
  235. edsl/utilities/data/__init__.py +1 -1
  236. edsl/utilities/data/scooter_results.json +1 -1
  237. edsl/utilities/decorators.py +77 -77
  238. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  239. edsl/utilities/interface.py +627 -627
  240. edsl/utilities/naming_utilities.py +263 -263
  241. edsl/utilities/repair_functions.py +28 -28
  242. edsl/utilities/restricted_python.py +70 -70
  243. edsl/utilities/utilities.py +409 -409
  244. {edsl-0.1.38.dev2.dist-info → edsl-0.1.38.dev3.dist-info}/LICENSE +21 -21
  245. {edsl-0.1.38.dev2.dist-info → edsl-0.1.38.dev3.dist-info}/METADATA +1 -1
  246. edsl-0.1.38.dev3.dist-info/RECORD +269 -0
  247. edsl-0.1.38.dev2.dist-info/RECORD +0 -269
  248. {edsl-0.1.38.dev2.dist-info → edsl-0.1.38.dev3.dist-info}/WHEEL +0 -0
edsl/data/Cache.py CHANGED
@@ -1,530 +1,530 @@
1
- """
2
- The `Cache` class is used to store responses from a language model.
3
- """
4
-
5
- from __future__ import annotations
6
- import json
7
- import os
8
- import warnings
9
- import copy
10
- from typing import Optional, Union
11
- from edsl.Base import Base
12
- from edsl.data.CacheEntry import CacheEntry
13
- from edsl.utilities.utilities import dict_hash
14
- from edsl.utilities.decorators import remove_edsl_version
15
- from edsl.exceptions.cache import CacheError
16
-
17
-
18
- class Cache(Base):
19
- """
20
- A class that represents a cache of responses from a language model.
21
-
22
- :param data: The data to initialize the cache with.
23
- :param immediate_write: Whether to write to the cache immediately after storing a new entry.
24
-
25
- Deprecated:
26
-
27
- :param method: The method of storage to use for the cache.
28
- """
29
-
30
- data = {}
31
-
32
- def __init__(
33
- self,
34
- *,
35
- filename: Optional[str] = None,
36
- data: Optional[Union["SQLiteDict", dict]] = None,
37
- immediate_write: bool = True,
38
- method=None,
39
- verbose=False,
40
- ):
41
- """
42
- Create two dictionaries to store the cache data.
43
-
44
- :param filename: The name of the file to read/write the cache from/to.
45
- :param data: The data to initialize the cache with.
46
- :param immediate_write: Whether to write to the cache immediately after storing a new entry.
47
- :param method: The method of storage to use for the cache.
48
-
49
- """
50
-
51
- # self.data_at_init = data or {}
52
- self.fetched_data = {}
53
- self.immediate_write = immediate_write
54
- self.method = method
55
- self.new_entries = {}
56
- self.new_entries_to_write_later = {}
57
- self.coop = None
58
- self.verbose = verbose
59
-
60
- self.filename = filename
61
- if filename and data:
62
- raise CacheError("Cannot provide both filename and data")
63
- if filename is None and data is None:
64
- data = {}
65
- if data is not None:
66
- self.data = data
67
- if filename is not None:
68
- self.data = {}
69
- if filename.endswith(".jsonl"):
70
- if os.path.exists(filename):
71
- self.add_from_jsonl(filename)
72
- else:
73
- print(
74
- f"File {filename} not found, but will write to this location."
75
- )
76
- elif filename.endswith(".db"):
77
- if os.path.exists(filename):
78
- self.add_from_sqlite(filename)
79
- else:
80
- raise CacheError("Invalid file extension. Must be .jsonl or .db")
81
-
82
- self._perform_checks()
83
-
84
- def rich_print(sefl):
85
- pass
86
- # raise NotImplementedError("This method is not implemented yet.")
87
-
88
- def code(sefl):
89
- pass
90
- # raise NotImplementedError("This method is not implemented yet.")
91
-
92
- def keys(self):
93
- """
94
- >>> from edsl import Cache
95
- >>> Cache.example().keys()
96
- ['5513286eb6967abc0511211f0402587d']
97
- """
98
- return list(self.data.keys())
99
-
100
- def values(self):
101
- """
102
- >>> from edsl import Cache
103
- >>> Cache.example().values()
104
- [CacheEntry(...)]
105
- """
106
- return list(self.data.values())
107
-
108
- def items(self):
109
- return zip(self.keys(), self.values())
110
-
111
- def new_entries_cache(self) -> Cache:
112
- """Return a new Cache object with the new entries."""
113
- return Cache(data={**self.new_entries, **self.fetched_data})
114
-
115
- def _perform_checks(self):
116
- """Perform checks on the cache."""
117
- from edsl.data.CacheEntry import CacheEntry
118
-
119
- if any(not isinstance(value, CacheEntry) for value in self.data.values()):
120
- raise CacheError("Not all values are CacheEntry instances")
121
- if self.method is not None:
122
- warnings.warn("Argument `method` is deprecated", DeprecationWarning)
123
-
124
- ####################
125
- # READ/WRITE
126
- ####################
127
- def fetch(
128
- self,
129
- *,
130
- model: str,
131
- parameters: dict,
132
- system_prompt: str,
133
- user_prompt: str,
134
- iteration: int,
135
- ) -> tuple(Union[None, str], str):
136
- """
137
- Fetch a value (LLM output) from the cache.
138
-
139
- :param model: The name of the language model.
140
- :param parameters: The model parameters.
141
- :param system_prompt: The system prompt.
142
- :param user_prompt: The user prompt.
143
- :param iteration: The iteration number.
144
-
145
- Return None if the response is not found.
146
-
147
- >>> c = Cache()
148
- >>> c.fetch(model="gpt-3", parameters="default", system_prompt="Hello", user_prompt="Hi", iteration=1)[0] is None
149
- True
150
-
151
-
152
- """
153
- from edsl.data.CacheEntry import CacheEntry
154
-
155
- key = CacheEntry.gen_key(
156
- model=model,
157
- parameters=parameters,
158
- system_prompt=system_prompt,
159
- user_prompt=user_prompt,
160
- iteration=iteration,
161
- )
162
- entry = self.data.get(key, None)
163
- if entry is not None:
164
- if self.verbose:
165
- print(f"Cache hit for key: {key}")
166
- self.fetched_data[key] = entry
167
- else:
168
- if self.verbose:
169
- print(f"Cache miss for key: {key}")
170
- return None if entry is None else entry.output, key
171
-
172
- def store(
173
- self,
174
- model: str,
175
- parameters: str,
176
- system_prompt: str,
177
- user_prompt: str,
178
- response: dict,
179
- iteration: int,
180
- ) -> str:
181
- """
182
- Add a new key-value pair to the cache.
183
-
184
- * Key is a hash of the input parameters.
185
- * Output is the response from the language model.
186
-
187
- How it works:
188
-
189
- * The key-value pair is added to `self.new_entries`
190
- * If `immediate_write` is True , the key-value pair is added to `self.data`
191
- * If `immediate_write` is False, the key-value pair is added to `self.new_entries_to_write_later`
192
-
193
- >>> from edsl import Cache, Model, Question
194
- >>> m = Model("test")
195
- >>> c = Cache()
196
- >>> len(c)
197
- 0
198
- >>> results = Question.example("free_text").by(m).run(cache = c, disable_remote_cache = True, disable_remote_inference = True)
199
- >>> len(c)
200
- 1
201
- """
202
-
203
- entry = CacheEntry(
204
- model=model,
205
- parameters=parameters,
206
- system_prompt=system_prompt,
207
- user_prompt=user_prompt,
208
- output=json.dumps(response),
209
- iteration=iteration,
210
- )
211
- key = entry.key
212
- self.new_entries[key] = entry
213
- if self.immediate_write:
214
- self.data[key] = entry
215
- else:
216
- self.new_entries_to_write_later[key] = entry
217
- return key
218
-
219
- def add_from_dict(
220
- self, new_data: dict[str, "CacheEntry"], write_now: Optional[bool] = True
221
- ) -> None:
222
- """
223
- Add entries to the cache from a dictionary.
224
-
225
- :param write_now: Whether to write to the cache immediately (similar to `immediate_write`).
226
- """
227
-
228
- for key, value in new_data.items():
229
- if key in self.data:
230
- if value != self.data[key]:
231
- raise CacheError("Mismatch in values")
232
- if not isinstance(value, CacheEntry):
233
- raise CacheError(f"Wrong type - the observed type is {type(value)}")
234
-
235
- self.new_entries.update(new_data)
236
- if write_now:
237
- self.data.update(new_data)
238
- else:
239
- self.new_entries_to_write_later.update(new_data)
240
-
241
- def add_from_jsonl(self, filename: str, write_now: Optional[bool] = True) -> None:
242
- """
243
- Add entries to the cache from a JSONL.
244
-
245
- :param write_now: Whether to write to the cache immediately (similar to `immediate_write`).
246
- """
247
- with open(filename, "a+") as f:
248
- f.seek(0)
249
- lines = f.readlines()
250
- new_data = {}
251
- for line in lines:
252
- d = json.loads(line)
253
- key = list(d.keys())[0]
254
- value = list(d.values())[0]
255
- new_data[key] = CacheEntry(**value)
256
- self.add_from_dict(new_data=new_data, write_now=write_now)
257
-
258
- def add_from_sqlite(self, db_path: str, write_now: Optional[bool] = True):
259
- """
260
- Add entries to the cache from an SQLite database.
261
-
262
- :param write_now: Whether to write to the cache immediately (similar to `immediate_write`).
263
- """
264
- from edsl.data.SQLiteDict import SQLiteDict
265
-
266
- db = SQLiteDict(db_path)
267
- new_data = {}
268
- for key, value in db.items():
269
- new_data[key] = CacheEntry(**value)
270
- self.add_from_dict(new_data=new_data, write_now=write_now)
271
-
272
- @classmethod
273
- def from_sqlite_db(cls, db_path: str) -> Cache:
274
- """
275
- Construct a Cache from a SQLite database.
276
- """
277
- from edsl.data.SQLiteDict import SQLiteDict
278
-
279
- return cls(data=SQLiteDict(db_path))
280
-
281
- @classmethod
282
- def from_local_cache(cls) -> Cache:
283
- """
284
- Construct a Cache from a local cache file.
285
- """
286
- from edsl.config import CONFIG
287
-
288
- CACHE_PATH = CONFIG.get("EDSL_DATABASE_PATH")
289
- path = CACHE_PATH.replace("sqlite:///", "")
290
- db_path = os.path.join(os.path.dirname(path), "data.db")
291
- return cls.from_sqlite_db(db_path=db_path)
292
-
293
- @classmethod
294
- def from_jsonl(cls, jsonlfile: str, db_path: Optional[str] = None) -> Cache:
295
- """
296
- Construct a Cache from a JSONL file.
297
-
298
- :param jsonlfile: The path to the JSONL file of cache entries.
299
- :param db_path: The path to the SQLite database used to store the cache.
300
-
301
- * If `db_path` is None, the cache will be stored in memory, as a dictionary.
302
- * If `db_path` is provided, the cache will be stored in an SQLite database.
303
- """
304
- # if a file doesn't exist at jsonfile, throw an error
305
- from edsl.data.SQLiteDict import SQLiteDict
306
-
307
- if not os.path.exists(jsonlfile):
308
- raise FileNotFoundError(f"File {jsonlfile} not found")
309
-
310
- if db_path is None:
311
- data = {}
312
- else:
313
- data = SQLiteDict(db_path)
314
-
315
- cache = Cache(data=data)
316
- cache.add_from_jsonl(jsonlfile)
317
- return cache
318
-
319
- def write_sqlite_db(self, db_path: str) -> None:
320
- """
321
- Write the cache to an SQLite database.
322
- """
323
- ## TODO: Check to make sure not over-writing (?)
324
- ## Should be added to SQLiteDict constructor (?)
325
- from edsl.data.SQLiteDict import SQLiteDict
326
-
327
- new_data = SQLiteDict(db_path)
328
- for key, value in self.data.items():
329
- new_data[key] = value
330
-
331
- def write(self, filename: Optional[str] = None) -> None:
332
- """
333
- Write the cache to a file at the specified location.
334
- """
335
- if filename is None:
336
- filename = self.filename
337
- if filename.endswith(".jsonl"):
338
- self.write_jsonl(filename)
339
- elif filename.endswith(".db"):
340
- self.write_sqlite_db(filename)
341
- else:
342
- raise CacheError("Invalid file extension. Must be .jsonl or .db")
343
-
344
- def write_jsonl(self, filename: str) -> None:
345
- """
346
- Write the cache to a JSONL file.
347
- """
348
- path = os.path.join(os.getcwd(), filename)
349
- with open(path, "w") as f:
350
- for key, value in self.data.items():
351
- f.write(json.dumps({key: value.to_dict()}) + "\n")
352
-
353
- def to_scenario_list(self):
354
- from edsl import ScenarioList, Scenario
355
-
356
- scenarios = []
357
- for key, value in self.data.items():
358
- new_d = value.to_dict()
359
- new_d["cache_key"] = key
360
- s = Scenario(new_d)
361
- scenarios.append(s)
362
- return ScenarioList(scenarios)
363
-
364
- ####################
365
- # REMOTE
366
- ####################
367
- # TODO: Make this work
368
- # - Need to decide whether the cache belongs to a user and what can be shared
369
- # - I.e., some cache entries? all or nothing?
370
- @classmethod
371
- def from_url(cls, db_path=None) -> Cache:
372
- """
373
- Construct a Cache object from a remote.
374
- """
375
- # ...do something here
376
- # return Cache(data=db)
377
- pass
378
-
379
- def __enter__(self):
380
- """
381
- Run when a context is entered.
382
- """
383
- return self
384
-
385
- def __exit__(self, exc_type, exc_value, traceback):
386
- """
387
- Run when a context is exited.
388
- """
389
- for key, entry in self.new_entries_to_write_later.items():
390
- self.data[key] = entry
391
-
392
- if self.filename:
393
- self.write(self.filename)
394
-
395
- ####################
396
- # DUNDER / USEFUL
397
- ####################
398
- def __hash__(self):
399
- """Return the hash of the Cache."""
400
- return dict_hash(self.to_dict(add_edsl_version=False))
401
-
402
- def to_dict(self, add_edsl_version=True) -> dict:
403
- d = {k: v.to_dict() for k, v in self.data.items()}
404
- if add_edsl_version:
405
- from edsl import __version__
406
-
407
- d["edsl_version"] = __version__
408
- d["edsl_class_name"] = "Cache"
409
-
410
- return d
411
-
412
- def _repr_html_(self):
413
- from edsl.utilities.utilities import data_to_html
414
-
415
- return data_to_html(self.to_dict())
416
-
417
- @classmethod
418
- @remove_edsl_version
419
- def from_dict(cls, data) -> Cache:
420
- """Construct a Cache from a dictionary."""
421
- newdata = {k: CacheEntry.from_dict(v) for k, v in data.items()}
422
- return cls(data=newdata)
423
-
424
- def __len__(self):
425
- """Return the number of CacheEntry objects in the Cache."""
426
- return len(self.data)
427
-
428
- # TODO: Same inputs could give different results and this could be useful
429
- # can't distinguish unless we do the ε trick or vary iterations
430
- def __eq__(self, other_cache: "Cache") -> bool:
431
- """
432
- Check if two Cache objects are equal.
433
- Does not verify their values are equal, only that they have the same keys.
434
- """
435
- if not isinstance(other_cache, Cache):
436
- return False
437
- return set(self.data.keys()) == set(other_cache.data.keys())
438
-
439
- def __add__(self, other: "Cache"):
440
- """
441
- Combine two caches.
442
- """
443
- if not isinstance(other, Cache):
444
- raise CacheError("Can only add two caches together")
445
- self.data.update(other.data)
446
- return self
447
-
448
- def __repr__(self):
449
- """
450
- Return a string representation of the Cache object.
451
- """
452
- return (
453
- f"Cache(data = {repr(self.data)}, immediate_write={self.immediate_write})"
454
- )
455
-
456
- ####################
457
- # EXAMPLES
458
- ####################
459
- def fetch_input_example(self) -> dict:
460
- """
461
- Create an example input for a 'fetch' operation.
462
- """
463
- return CacheEntry.fetch_input_example()
464
-
465
- def to_html(self):
466
- # json_str = json.dumps(self.data, indent=4)
467
- d = {k: v.to_dict() for k, v in self.data.items()}
468
- for key, value in d.items():
469
- for k, v in value.items():
470
- if isinstance(v, dict):
471
- d[key][k] = {kk: str(vv) for kk, vv in v.items()}
472
- else:
473
- d[key][k] = str(v)
474
-
475
- json_str = json.dumps(d, indent=4)
476
-
477
- # HTML template with the JSON string embedded
478
- html = f"""
479
- <!DOCTYPE html>
480
- <html>
481
- <head>
482
- <title>Display JSON</title>
483
- </head>
484
- <body>
485
- <pre id="jsonData"></pre>
486
- <script>
487
- var json = {json_str};
488
-
489
- // JSON.stringify with spacing to format
490
- document.getElementById('jsonData').textContent = JSON.stringify(json, null, 4);
491
- </script>
492
- </body>
493
- </html>
494
- """
495
- return html
496
-
497
- def view(self) -> None:
498
- """View the Cache in a new browser tab."""
499
- import tempfile
500
- import webbrowser
501
-
502
- html_content = self.to_html()
503
- # Create a temporary file to hold the HTML
504
- with tempfile.NamedTemporaryFile("w", delete=False, suffix=".html") as tmpfile:
505
- tmpfile.write(html_content)
506
- # Get the path to the temporary file
507
- filepath = tmpfile.name
508
-
509
- # Open the HTML file in a new browser tab
510
- webbrowser.open("file://" + filepath)
511
-
512
- @classmethod
513
- def example(cls, randomize: bool = False) -> Cache:
514
- """
515
- Returns an example Cache instance.
516
-
517
- :param randomize: If True, uses CacheEntry's randomize method.
518
- """
519
- return cls(
520
- data={
521
- CacheEntry.example(randomize).key: CacheEntry.example(),
522
- CacheEntry.example(randomize).key: CacheEntry.example(),
523
- }
524
- )
525
-
526
-
527
- if __name__ == "__main__":
528
- import doctest
529
-
530
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ """
2
+ The `Cache` class is used to store responses from a language model.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import json
7
+ import os
8
+ import warnings
9
+ import copy
10
+ from typing import Optional, Union
11
+ from edsl.Base import Base
12
+ from edsl.data.CacheEntry import CacheEntry
13
+ from edsl.utilities.utilities import dict_hash
14
+ from edsl.utilities.decorators import remove_edsl_version
15
+ from edsl.exceptions.cache import CacheError
16
+
17
+
18
+ class Cache(Base):
19
+ """
20
+ A class that represents a cache of responses from a language model.
21
+
22
+ :param data: The data to initialize the cache with.
23
+ :param immediate_write: Whether to write to the cache immediately after storing a new entry.
24
+
25
+ Deprecated:
26
+
27
+ :param method: The method of storage to use for the cache.
28
+ """
29
+
30
+ data = {}
31
+
32
+ def __init__(
33
+ self,
34
+ *,
35
+ filename: Optional[str] = None,
36
+ data: Optional[Union["SQLiteDict", dict]] = None,
37
+ immediate_write: bool = True,
38
+ method=None,
39
+ verbose=False,
40
+ ):
41
+ """
42
+ Create two dictionaries to store the cache data.
43
+
44
+ :param filename: The name of the file to read/write the cache from/to.
45
+ :param data: The data to initialize the cache with.
46
+ :param immediate_write: Whether to write to the cache immediately after storing a new entry.
47
+ :param method: The method of storage to use for the cache.
48
+
49
+ """
50
+
51
+ # self.data_at_init = data or {}
52
+ self.fetched_data = {}
53
+ self.immediate_write = immediate_write
54
+ self.method = method
55
+ self.new_entries = {}
56
+ self.new_entries_to_write_later = {}
57
+ self.coop = None
58
+ self.verbose = verbose
59
+
60
+ self.filename = filename
61
+ if filename and data:
62
+ raise CacheError("Cannot provide both filename and data")
63
+ if filename is None and data is None:
64
+ data = {}
65
+ if data is not None:
66
+ self.data = data
67
+ if filename is not None:
68
+ self.data = {}
69
+ if filename.endswith(".jsonl"):
70
+ if os.path.exists(filename):
71
+ self.add_from_jsonl(filename)
72
+ else:
73
+ print(
74
+ f"File {filename} not found, but will write to this location."
75
+ )
76
+ elif filename.endswith(".db"):
77
+ if os.path.exists(filename):
78
+ self.add_from_sqlite(filename)
79
+ else:
80
+ raise CacheError("Invalid file extension. Must be .jsonl or .db")
81
+
82
+ self._perform_checks()
83
+
84
+ def rich_print(sefl):
85
+ pass
86
+ # raise NotImplementedError("This method is not implemented yet.")
87
+
88
+ def code(sefl):
89
+ pass
90
+ # raise NotImplementedError("This method is not implemented yet.")
91
+
92
+ def keys(self):
93
+ """
94
+ >>> from edsl import Cache
95
+ >>> Cache.example().keys()
96
+ ['5513286eb6967abc0511211f0402587d']
97
+ """
98
+ return list(self.data.keys())
99
+
100
+ def values(self):
101
+ """
102
+ >>> from edsl import Cache
103
+ >>> Cache.example().values()
104
+ [CacheEntry(...)]
105
+ """
106
+ return list(self.data.values())
107
+
108
+ def items(self):
109
+ return zip(self.keys(), self.values())
110
+
111
+ def new_entries_cache(self) -> Cache:
112
+ """Return a new Cache object with the new entries."""
113
+ return Cache(data={**self.new_entries, **self.fetched_data})
114
+
115
+ def _perform_checks(self):
116
+ """Perform checks on the cache."""
117
+ from edsl.data.CacheEntry import CacheEntry
118
+
119
+ if any(not isinstance(value, CacheEntry) for value in self.data.values()):
120
+ raise CacheError("Not all values are CacheEntry instances")
121
+ if self.method is not None:
122
+ warnings.warn("Argument `method` is deprecated", DeprecationWarning)
123
+
124
+ ####################
125
+ # READ/WRITE
126
+ ####################
127
+ def fetch(
128
+ self,
129
+ *,
130
+ model: str,
131
+ parameters: dict,
132
+ system_prompt: str,
133
+ user_prompt: str,
134
+ iteration: int,
135
+ ) -> tuple(Union[None, str], str):
136
+ """
137
+ Fetch a value (LLM output) from the cache.
138
+
139
+ :param model: The name of the language model.
140
+ :param parameters: The model parameters.
141
+ :param system_prompt: The system prompt.
142
+ :param user_prompt: The user prompt.
143
+ :param iteration: The iteration number.
144
+
145
+ Return None if the response is not found.
146
+
147
+ >>> c = Cache()
148
+ >>> c.fetch(model="gpt-3", parameters="default", system_prompt="Hello", user_prompt="Hi", iteration=1)[0] is None
149
+ True
150
+
151
+
152
+ """
153
+ from edsl.data.CacheEntry import CacheEntry
154
+
155
+ key = CacheEntry.gen_key(
156
+ model=model,
157
+ parameters=parameters,
158
+ system_prompt=system_prompt,
159
+ user_prompt=user_prompt,
160
+ iteration=iteration,
161
+ )
162
+ entry = self.data.get(key, None)
163
+ if entry is not None:
164
+ if self.verbose:
165
+ print(f"Cache hit for key: {key}")
166
+ self.fetched_data[key] = entry
167
+ else:
168
+ if self.verbose:
169
+ print(f"Cache miss for key: {key}")
170
+ return None if entry is None else entry.output, key
171
+
172
+ def store(
173
+ self,
174
+ model: str,
175
+ parameters: str,
176
+ system_prompt: str,
177
+ user_prompt: str,
178
+ response: dict,
179
+ iteration: int,
180
+ ) -> str:
181
+ """
182
+ Add a new key-value pair to the cache.
183
+
184
+ * Key is a hash of the input parameters.
185
+ * Output is the response from the language model.
186
+
187
+ How it works:
188
+
189
+ * The key-value pair is added to `self.new_entries`
190
+ * If `immediate_write` is True , the key-value pair is added to `self.data`
191
+ * If `immediate_write` is False, the key-value pair is added to `self.new_entries_to_write_later`
192
+
193
+ >>> from edsl import Cache, Model, Question
194
+ >>> m = Model("test")
195
+ >>> c = Cache()
196
+ >>> len(c)
197
+ 0
198
+ >>> results = Question.example("free_text").by(m).run(cache = c, disable_remote_cache = True, disable_remote_inference = True)
199
+ >>> len(c)
200
+ 1
201
+ """
202
+
203
+ entry = CacheEntry(
204
+ model=model,
205
+ parameters=parameters,
206
+ system_prompt=system_prompt,
207
+ user_prompt=user_prompt,
208
+ output=json.dumps(response),
209
+ iteration=iteration,
210
+ )
211
+ key = entry.key
212
+ self.new_entries[key] = entry
213
+ if self.immediate_write:
214
+ self.data[key] = entry
215
+ else:
216
+ self.new_entries_to_write_later[key] = entry
217
+ return key
218
+
219
+ def add_from_dict(
220
+ self, new_data: dict[str, "CacheEntry"], write_now: Optional[bool] = True
221
+ ) -> None:
222
+ """
223
+ Add entries to the cache from a dictionary.
224
+
225
+ :param write_now: Whether to write to the cache immediately (similar to `immediate_write`).
226
+ """
227
+
228
+ for key, value in new_data.items():
229
+ if key in self.data:
230
+ if value != self.data[key]:
231
+ raise CacheError("Mismatch in values")
232
+ if not isinstance(value, CacheEntry):
233
+ raise CacheError(f"Wrong type - the observed type is {type(value)}")
234
+
235
+ self.new_entries.update(new_data)
236
+ if write_now:
237
+ self.data.update(new_data)
238
+ else:
239
+ self.new_entries_to_write_later.update(new_data)
240
+
241
+ def add_from_jsonl(self, filename: str, write_now: Optional[bool] = True) -> None:
242
+ """
243
+ Add entries to the cache from a JSONL.
244
+
245
+ :param write_now: Whether to write to the cache immediately (similar to `immediate_write`).
246
+ """
247
+ with open(filename, "a+") as f:
248
+ f.seek(0)
249
+ lines = f.readlines()
250
+ new_data = {}
251
+ for line in lines:
252
+ d = json.loads(line)
253
+ key = list(d.keys())[0]
254
+ value = list(d.values())[0]
255
+ new_data[key] = CacheEntry(**value)
256
+ self.add_from_dict(new_data=new_data, write_now=write_now)
257
+
258
+ def add_from_sqlite(self, db_path: str, write_now: Optional[bool] = True):
259
+ """
260
+ Add entries to the cache from an SQLite database.
261
+
262
+ :param write_now: Whether to write to the cache immediately (similar to `immediate_write`).
263
+ """
264
+ from edsl.data.SQLiteDict import SQLiteDict
265
+
266
+ db = SQLiteDict(db_path)
267
+ new_data = {}
268
+ for key, value in db.items():
269
+ new_data[key] = CacheEntry(**value)
270
+ self.add_from_dict(new_data=new_data, write_now=write_now)
271
+
272
+ @classmethod
273
+ def from_sqlite_db(cls, db_path: str) -> Cache:
274
+ """
275
+ Construct a Cache from a SQLite database.
276
+ """
277
+ from edsl.data.SQLiteDict import SQLiteDict
278
+
279
+ return cls(data=SQLiteDict(db_path))
280
+
281
+ @classmethod
282
+ def from_local_cache(cls) -> Cache:
283
+ """
284
+ Construct a Cache from a local cache file.
285
+ """
286
+ from edsl.config import CONFIG
287
+
288
+ CACHE_PATH = CONFIG.get("EDSL_DATABASE_PATH")
289
+ path = CACHE_PATH.replace("sqlite:///", "")
290
+ db_path = os.path.join(os.path.dirname(path), "data.db")
291
+ return cls.from_sqlite_db(db_path=db_path)
292
+
293
+ @classmethod
294
+ def from_jsonl(cls, jsonlfile: str, db_path: Optional[str] = None) -> Cache:
295
+ """
296
+ Construct a Cache from a JSONL file.
297
+
298
+ :param jsonlfile: The path to the JSONL file of cache entries.
299
+ :param db_path: The path to the SQLite database used to store the cache.
300
+
301
+ * If `db_path` is None, the cache will be stored in memory, as a dictionary.
302
+ * If `db_path` is provided, the cache will be stored in an SQLite database.
303
+ """
304
+ # if a file doesn't exist at jsonfile, throw an error
305
+ from edsl.data.SQLiteDict import SQLiteDict
306
+
307
+ if not os.path.exists(jsonlfile):
308
+ raise FileNotFoundError(f"File {jsonlfile} not found")
309
+
310
+ if db_path is None:
311
+ data = {}
312
+ else:
313
+ data = SQLiteDict(db_path)
314
+
315
+ cache = Cache(data=data)
316
+ cache.add_from_jsonl(jsonlfile)
317
+ return cache
318
+
319
+ def write_sqlite_db(self, db_path: str) -> None:
320
+ """
321
+ Write the cache to an SQLite database.
322
+ """
323
+ ## TODO: Check to make sure not over-writing (?)
324
+ ## Should be added to SQLiteDict constructor (?)
325
+ from edsl.data.SQLiteDict import SQLiteDict
326
+
327
+ new_data = SQLiteDict(db_path)
328
+ for key, value in self.data.items():
329
+ new_data[key] = value
330
+
331
+ def write(self, filename: Optional[str] = None) -> None:
332
+ """
333
+ Write the cache to a file at the specified location.
334
+ """
335
+ if filename is None:
336
+ filename = self.filename
337
+ if filename.endswith(".jsonl"):
338
+ self.write_jsonl(filename)
339
+ elif filename.endswith(".db"):
340
+ self.write_sqlite_db(filename)
341
+ else:
342
+ raise CacheError("Invalid file extension. Must be .jsonl or .db")
343
+
344
+ def write_jsonl(self, filename: str) -> None:
345
+ """
346
+ Write the cache to a JSONL file.
347
+ """
348
+ path = os.path.join(os.getcwd(), filename)
349
+ with open(path, "w") as f:
350
+ for key, value in self.data.items():
351
+ f.write(json.dumps({key: value.to_dict()}) + "\n")
352
+
353
+ def to_scenario_list(self):
354
+ from edsl import ScenarioList, Scenario
355
+
356
+ scenarios = []
357
+ for key, value in self.data.items():
358
+ new_d = value.to_dict()
359
+ new_d["cache_key"] = key
360
+ s = Scenario(new_d)
361
+ scenarios.append(s)
362
+ return ScenarioList(scenarios)
363
+
364
+ ####################
365
+ # REMOTE
366
+ ####################
367
+ # TODO: Make this work
368
+ # - Need to decide whether the cache belongs to a user and what can be shared
369
+ # - I.e., some cache entries? all or nothing?
370
+ @classmethod
371
+ def from_url(cls, db_path=None) -> Cache:
372
+ """
373
+ Construct a Cache object from a remote.
374
+ """
375
+ # ...do something here
376
+ # return Cache(data=db)
377
+ pass
378
+
379
+ def __enter__(self):
380
+ """
381
+ Run when a context is entered.
382
+ """
383
+ return self
384
+
385
+ def __exit__(self, exc_type, exc_value, traceback):
386
+ """
387
+ Run when a context is exited.
388
+ """
389
+ for key, entry in self.new_entries_to_write_later.items():
390
+ self.data[key] = entry
391
+
392
+ if self.filename:
393
+ self.write(self.filename)
394
+
395
+ ####################
396
+ # DUNDER / USEFUL
397
+ ####################
398
+ def __hash__(self):
399
+ """Return the hash of the Cache."""
400
+ return dict_hash(self.to_dict(add_edsl_version=False))
401
+
402
+ def to_dict(self, add_edsl_version=True) -> dict:
403
+ d = {k: v.to_dict() for k, v in self.data.items()}
404
+ if add_edsl_version:
405
+ from edsl import __version__
406
+
407
+ d["edsl_version"] = __version__
408
+ d["edsl_class_name"] = "Cache"
409
+
410
+ return d
411
+
412
+ def _repr_html_(self):
413
+ from edsl.utilities.utilities import data_to_html
414
+
415
+ return data_to_html(self.to_dict())
416
+
417
+ @classmethod
418
+ @remove_edsl_version
419
+ def from_dict(cls, data) -> Cache:
420
+ """Construct a Cache from a dictionary."""
421
+ newdata = {k: CacheEntry.from_dict(v) for k, v in data.items()}
422
+ return cls(data=newdata)
423
+
424
+ def __len__(self):
425
+ """Return the number of CacheEntry objects in the Cache."""
426
+ return len(self.data)
427
+
428
+ # TODO: Same inputs could give different results and this could be useful
429
+ # can't distinguish unless we do the ε trick or vary iterations
430
+ def __eq__(self, other_cache: "Cache") -> bool:
431
+ """
432
+ Check if two Cache objects are equal.
433
+ Does not verify their values are equal, only that they have the same keys.
434
+ """
435
+ if not isinstance(other_cache, Cache):
436
+ return False
437
+ return set(self.data.keys()) == set(other_cache.data.keys())
438
+
439
+ def __add__(self, other: "Cache"):
440
+ """
441
+ Combine two caches.
442
+ """
443
+ if not isinstance(other, Cache):
444
+ raise CacheError("Can only add two caches together")
445
+ self.data.update(other.data)
446
+ return self
447
+
448
+ def __repr__(self):
449
+ """
450
+ Return a string representation of the Cache object.
451
+ """
452
+ return (
453
+ f"Cache(data = {repr(self.data)}, immediate_write={self.immediate_write})"
454
+ )
455
+
456
+ ####################
457
+ # EXAMPLES
458
+ ####################
459
+ def fetch_input_example(self) -> dict:
460
+ """
461
+ Create an example input for a 'fetch' operation.
462
+ """
463
+ return CacheEntry.fetch_input_example()
464
+
465
+ def to_html(self):
466
+ # json_str = json.dumps(self.data, indent=4)
467
+ d = {k: v.to_dict() for k, v in self.data.items()}
468
+ for key, value in d.items():
469
+ for k, v in value.items():
470
+ if isinstance(v, dict):
471
+ d[key][k] = {kk: str(vv) for kk, vv in v.items()}
472
+ else:
473
+ d[key][k] = str(v)
474
+
475
+ json_str = json.dumps(d, indent=4)
476
+
477
+ # HTML template with the JSON string embedded
478
+ html = f"""
479
+ <!DOCTYPE html>
480
+ <html>
481
+ <head>
482
+ <title>Display JSON</title>
483
+ </head>
484
+ <body>
485
+ <pre id="jsonData"></pre>
486
+ <script>
487
+ var json = {json_str};
488
+
489
+ // JSON.stringify with spacing to format
490
+ document.getElementById('jsonData').textContent = JSON.stringify(json, null, 4);
491
+ </script>
492
+ </body>
493
+ </html>
494
+ """
495
+ return html
496
+
497
+ def view(self) -> None:
498
+ """View the Cache in a new browser tab."""
499
+ import tempfile
500
+ import webbrowser
501
+
502
+ html_content = self.to_html()
503
+ # Create a temporary file to hold the HTML
504
+ with tempfile.NamedTemporaryFile("w", delete=False, suffix=".html") as tmpfile:
505
+ tmpfile.write(html_content)
506
+ # Get the path to the temporary file
507
+ filepath = tmpfile.name
508
+
509
+ # Open the HTML file in a new browser tab
510
+ webbrowser.open("file://" + filepath)
511
+
512
+ @classmethod
513
+ def example(cls, randomize: bool = False) -> Cache:
514
+ """
515
+ Returns an example Cache instance.
516
+
517
+ :param randomize: If True, uses CacheEntry's randomize method.
518
+ """
519
+ return cls(
520
+ data={
521
+ CacheEntry.example(randomize).key: CacheEntry.example(),
522
+ CacheEntry.example(randomize).key: CacheEntry.example(),
523
+ }
524
+ )
525
+
526
+
527
+ if __name__ == "__main__":
528
+ import doctest
529
+
530
+ doctest.testmod(optionflags=doctest.ELLIPSIS)