edsl 0.1.46__py3-none-any.whl → 0.1.48__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 (328) hide show
  1. edsl/__init__.py +44 -39
  2. edsl/__version__.py +1 -1
  3. edsl/agents/__init__.py +4 -2
  4. edsl/agents/{Agent.py → agent.py} +442 -152
  5. edsl/agents/{AgentList.py → agent_list.py} +220 -162
  6. edsl/agents/descriptors.py +46 -7
  7. edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
  8. edsl/base/__init__.py +75 -0
  9. edsl/base/base_class.py +1303 -0
  10. edsl/base/data_transfer_models.py +114 -0
  11. edsl/base/enums.py +215 -0
  12. edsl/base.py +8 -0
  13. edsl/buckets/__init__.py +25 -0
  14. edsl/buckets/bucket_collection.py +324 -0
  15. edsl/buckets/model_buckets.py +206 -0
  16. edsl/buckets/token_bucket.py +502 -0
  17. edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
  18. edsl/buckets/token_bucket_client.py +509 -0
  19. edsl/caching/__init__.py +20 -0
  20. edsl/caching/cache.py +814 -0
  21. edsl/caching/cache_entry.py +427 -0
  22. edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
  23. edsl/caching/exceptions.py +24 -0
  24. edsl/caching/orm.py +30 -0
  25. edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
  26. edsl/caching/sql_dict.py +441 -0
  27. edsl/config/__init__.py +8 -0
  28. edsl/config/config_class.py +177 -0
  29. edsl/config.py +4 -176
  30. edsl/conversation/Conversation.py +7 -7
  31. edsl/conversation/car_buying.py +4 -4
  32. edsl/conversation/chips.py +6 -6
  33. edsl/coop/__init__.py +25 -2
  34. edsl/coop/coop.py +430 -113
  35. edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
  36. edsl/coop/exceptions.py +62 -0
  37. edsl/coop/price_fetcher.py +126 -0
  38. edsl/coop/utils.py +89 -24
  39. edsl/data_transfer_models.py +5 -72
  40. edsl/dataset/__init__.py +10 -0
  41. edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
  42. edsl/dataset/dataset_operations_mixin.py +1492 -0
  43. edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
  44. edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
  45. edsl/{results → dataset/display}/table_renderers.py +58 -2
  46. edsl/{results → dataset}/file_exports.py +4 -5
  47. edsl/{results → dataset}/smart_objects.py +2 -2
  48. edsl/enums.py +5 -205
  49. edsl/inference_services/__init__.py +5 -0
  50. edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
  51. edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
  52. edsl/inference_services/data_structures.py +3 -2
  53. edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
  54. edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
  55. edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
  56. edsl/inference_services/registry.py +4 -41
  57. edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
  58. edsl/inference_services/services/__init__.py +31 -0
  59. edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
  60. edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
  61. edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
  62. edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
  63. edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
  64. edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
  65. edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
  66. edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
  67. edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
  68. edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
  69. edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +12 -12
  70. edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
  71. edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
  72. edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
  73. edsl/inference_services/write_available.py +1 -2
  74. edsl/instructions/__init__.py +6 -0
  75. edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
  76. edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
  77. edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
  78. edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
  79. edsl/interviews/__init__.py +4 -0
  80. edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
  81. edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
  82. edsl/interviews/interview.py +638 -0
  83. edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
  84. edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
  85. edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
  86. edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
  87. edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
  88. edsl/invigilators/__init__.py +38 -0
  89. edsl/invigilators/invigilator_base.py +477 -0
  90. edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
  91. edsl/invigilators/prompt_constructor.py +476 -0
  92. edsl/{agents → invigilators}/prompt_helpers.py +2 -1
  93. edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
  94. edsl/{agents → invigilators}/question_option_processor.py +96 -21
  95. edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
  96. edsl/jobs/__init__.py +7 -1
  97. edsl/jobs/async_interview_runner.py +99 -35
  98. edsl/jobs/check_survey_scenario_compatibility.py +7 -5
  99. edsl/jobs/data_structures.py +153 -22
  100. edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
  101. edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
  102. edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
  103. edsl/jobs/{Jobs.py → jobs.py} +321 -155
  104. edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
  105. edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +20 -17
  106. edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
  107. edsl/jobs/jobs_pricing_estimation.py +347 -0
  108. edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
  109. edsl/jobs/jobs_runner_asyncio.py +282 -0
  110. edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
  111. edsl/jobs/results_exceptions_handler.py +2 -2
  112. edsl/key_management/__init__.py +28 -0
  113. edsl/key_management/key_lookup.py +161 -0
  114. edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
  115. edsl/key_management/key_lookup_collection.py +82 -0
  116. edsl/key_management/models.py +218 -0
  117. edsl/language_models/__init__.py +7 -2
  118. edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
  119. edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
  120. edsl/language_models/language_model.py +1080 -0
  121. edsl/language_models/model.py +10 -25
  122. edsl/language_models/{ModelList.py → model_list.py} +9 -14
  123. edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
  124. edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
  125. edsl/language_models/repair.py +4 -4
  126. edsl/language_models/utilities.py +4 -4
  127. edsl/notebooks/__init__.py +3 -1
  128. edsl/notebooks/{Notebook.py → notebook.py} +7 -8
  129. edsl/prompts/__init__.py +1 -1
  130. edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
  131. edsl/prompts/{Prompt.py → prompt.py} +101 -95
  132. edsl/questions/HTMLQuestion.py +1 -1
  133. edsl/questions/__init__.py +154 -25
  134. edsl/questions/answer_validator_mixin.py +1 -1
  135. edsl/questions/compose_questions.py +4 -3
  136. edsl/questions/derived/question_likert_five.py +166 -0
  137. edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
  138. edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
  139. edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
  140. edsl/questions/descriptors.py +24 -30
  141. edsl/questions/loop_processor.py +65 -19
  142. edsl/questions/question_base.py +881 -0
  143. edsl/questions/question_base_gen_mixin.py +15 -16
  144. edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
  145. edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
  146. edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
  147. edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
  148. edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
  149. edsl/questions/question_free_text.py +282 -0
  150. edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
  151. edsl/questions/{QuestionList.py → question_list.py} +6 -7
  152. edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
  153. edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
  154. edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
  155. edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
  156. edsl/questions/question_registry.py +10 -16
  157. edsl/questions/register_questions_meta.py +8 -4
  158. edsl/questions/response_validator_abc.py +17 -16
  159. edsl/results/__init__.py +4 -1
  160. edsl/{exceptions/results.py → results/exceptions.py} +1 -1
  161. edsl/results/report.py +197 -0
  162. edsl/results/{Result.py → result.py} +131 -45
  163. edsl/results/{Results.py → results.py} +420 -216
  164. edsl/results/results_selector.py +344 -25
  165. edsl/scenarios/__init__.py +30 -3
  166. edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
  167. edsl/scenarios/directory_scanner.py +156 -13
  168. edsl/scenarios/document_chunker.py +186 -0
  169. edsl/scenarios/exceptions.py +101 -0
  170. edsl/scenarios/file_methods.py +2 -3
  171. edsl/scenarios/file_store.py +755 -0
  172. edsl/scenarios/handlers/__init__.py +14 -14
  173. edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
  174. edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
  175. edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
  176. edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
  177. edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
  178. edsl/scenarios/handlers/latex_file_store.py +5 -0
  179. edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
  180. edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
  181. edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
  182. edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
  183. edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
  184. edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
  185. edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
  186. edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
  187. edsl/scenarios/scenario.py +928 -0
  188. edsl/scenarios/scenario_join.py +18 -5
  189. edsl/scenarios/{ScenarioList.py → scenario_list.py} +424 -106
  190. edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
  191. edsl/scenarios/scenario_selector.py +5 -1
  192. edsl/study/ObjectEntry.py +2 -2
  193. edsl/study/SnapShot.py +5 -5
  194. edsl/study/Study.py +20 -21
  195. edsl/study/__init__.py +6 -4
  196. edsl/surveys/__init__.py +7 -4
  197. edsl/surveys/dag/__init__.py +2 -0
  198. edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
  199. edsl/surveys/{DAG.py → dag/dag.py} +13 -10
  200. edsl/surveys/descriptors.py +1 -1
  201. edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
  202. edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
  203. edsl/surveys/memory/__init__.py +3 -0
  204. edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
  205. edsl/surveys/rules/__init__.py +3 -0
  206. edsl/surveys/{Rule.py → rules/rule.py} +103 -43
  207. edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
  208. edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
  209. edsl/surveys/survey.py +1743 -0
  210. edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
  211. edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
  212. edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
  213. edsl/tasks/__init__.py +32 -0
  214. edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
  215. edsl/tasks/task_creators.py +135 -0
  216. edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
  217. edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
  218. edsl/tasks/task_status_log.py +85 -0
  219. edsl/tokens/__init__.py +2 -0
  220. edsl/tokens/interview_token_usage.py +53 -0
  221. edsl/utilities/PrettyList.py +1 -1
  222. edsl/utilities/SystemInfo.py +25 -22
  223. edsl/utilities/__init__.py +29 -21
  224. edsl/utilities/gcp_bucket/__init__.py +2 -0
  225. edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
  226. edsl/utilities/interface.py +44 -536
  227. edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
  228. edsl/utilities/repair_functions.py +1 -1
  229. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/METADATA +3 -2
  230. edsl-0.1.48.dist-info/RECORD +347 -0
  231. edsl/Base.py +0 -426
  232. edsl/BaseDiff.py +0 -260
  233. edsl/agents/InvigilatorBase.py +0 -260
  234. edsl/agents/PromptConstructor.py +0 -318
  235. edsl/auto/AutoStudy.py +0 -130
  236. edsl/auto/StageBase.py +0 -243
  237. edsl/auto/StageGenerateSurvey.py +0 -178
  238. edsl/auto/StageLabelQuestions.py +0 -125
  239. edsl/auto/StagePersona.py +0 -61
  240. edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
  241. edsl/auto/StagePersonaDimensionValues.py +0 -74
  242. edsl/auto/StagePersonaDimensions.py +0 -69
  243. edsl/auto/StageQuestions.py +0 -74
  244. edsl/auto/SurveyCreatorPipeline.py +0 -21
  245. edsl/auto/utilities.py +0 -218
  246. edsl/base/Base.py +0 -279
  247. edsl/coop/PriceFetcher.py +0 -54
  248. edsl/data/Cache.py +0 -580
  249. edsl/data/CacheEntry.py +0 -230
  250. edsl/data/SQLiteDict.py +0 -292
  251. edsl/data/__init__.py +0 -5
  252. edsl/data/orm.py +0 -10
  253. edsl/exceptions/cache.py +0 -5
  254. edsl/exceptions/coop.py +0 -14
  255. edsl/exceptions/data.py +0 -14
  256. edsl/exceptions/scenarios.py +0 -29
  257. edsl/jobs/Answers.py +0 -43
  258. edsl/jobs/JobsPrompts.py +0 -354
  259. edsl/jobs/buckets/BucketCollection.py +0 -134
  260. edsl/jobs/buckets/ModelBuckets.py +0 -65
  261. edsl/jobs/buckets/TokenBucket.py +0 -283
  262. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  263. edsl/jobs/interviews/Interview.py +0 -395
  264. edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
  265. edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
  266. edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
  267. edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
  268. edsl/jobs/tasks/TaskCreators.py +0 -64
  269. edsl/jobs/tasks/TaskStatusLog.py +0 -23
  270. edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
  271. edsl/language_models/LanguageModel.py +0 -635
  272. edsl/language_models/ServiceDataSources.py +0 -0
  273. edsl/language_models/key_management/KeyLookup.py +0 -63
  274. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  275. edsl/language_models/key_management/models.py +0 -137
  276. edsl/questions/QuestionBase.py +0 -539
  277. edsl/questions/QuestionFreeText.py +0 -130
  278. edsl/questions/derived/QuestionLikertFive.py +0 -76
  279. edsl/results/DatasetExportMixin.py +0 -911
  280. edsl/results/ResultsExportMixin.py +0 -45
  281. edsl/results/TextEditor.py +0 -50
  282. edsl/results/results_fetch_mixin.py +0 -33
  283. edsl/results/results_tools_mixin.py +0 -98
  284. edsl/scenarios/DocumentChunker.py +0 -104
  285. edsl/scenarios/FileStore.py +0 -564
  286. edsl/scenarios/Scenario.py +0 -548
  287. edsl/scenarios/ScenarioHtmlMixin.py +0 -65
  288. edsl/scenarios/ScenarioListExportMixin.py +0 -45
  289. edsl/scenarios/handlers/latex.py +0 -5
  290. edsl/shared.py +0 -1
  291. edsl/surveys/Survey.py +0 -1306
  292. edsl/surveys/SurveyQualtricsImport.py +0 -284
  293. edsl/surveys/SurveyToApp.py +0 -141
  294. edsl/surveys/instructions/__init__.py +0 -0
  295. edsl/tools/__init__.py +0 -1
  296. edsl/tools/clusters.py +0 -192
  297. edsl/tools/embeddings.py +0 -27
  298. edsl/tools/embeddings_plotting.py +0 -118
  299. edsl/tools/plotting.py +0 -112
  300. edsl/tools/summarize.py +0 -18
  301. edsl/utilities/data/Registry.py +0 -6
  302. edsl/utilities/data/__init__.py +0 -1
  303. edsl/utilities/data/scooter_results.json +0 -1
  304. edsl-0.1.46.dist-info/RECORD +0 -366
  305. /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
  306. /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
  307. /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
  308. /edsl/{results → dataset/display}/table_data_class.py +0 -0
  309. /edsl/{results → dataset/display}/table_display.css +0 -0
  310. /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
  311. /edsl/{results → dataset}/tree_explore.py +0 -0
  312. /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
  313. /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
  314. /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
  315. /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
  316. /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
  317. /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
  318. /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
  319. /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
  320. /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
  321. /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
  322. /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
  323. /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
  324. /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
  325. /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
  326. /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
  327. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
  328. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
@@ -0,0 +1,509 @@
1
+ """
2
+ Token Bucket Client for distributed rate limiting in EDSL.
3
+
4
+ This module provides a client implementation for interacting with a remote token
5
+ bucket server. It implements the same interface as TokenBucket, but delegates
6
+ operations to a remote server, enabling distributed rate limiting across
7
+ multiple processes or machines.
8
+ """
9
+
10
+ from typing import Union, Optional, Dict, List, Any, Tuple
11
+ import asyncio
12
+ import time
13
+ import aiohttp
14
+ from matplotlib import pyplot as plt
15
+ from matplotlib.figure import Figure
16
+
17
+
18
+ class TokenBucketClient:
19
+ """
20
+ Client implementation for interacting with a remote token bucket server.
21
+
22
+ TokenBucketClient implements the same interface as TokenBucket, but
23
+ delegates operations to a remote server via REST API calls. This enables
24
+ distributed rate limiting across multiple processes or machines, ensuring
25
+ that rate limits are properly enforced in distributed environments.
26
+
27
+ The client maintains minimal local state and fetches most information from
28
+ the server when needed. It creates the bucket on the server during
29
+ initialization if it doesn't already exist.
30
+
31
+ Attributes:
32
+ bucket_name (str): Name identifier for the bucket (usually service name)
33
+ bucket_type (str): Type of bucket ("requests" or "tokens")
34
+ capacity (float): Maximum tokens the bucket can hold
35
+ refill_rate (float): Rate at which tokens are refilled (tokens per second)
36
+ api_base_url (str): Base URL for the token bucket server API
37
+ bucket_id (str): Unique identifier for this bucket on the server
38
+ creation_time (float): Local timestamp when this client was created
39
+ turbo_mode (bool): Flag indicating if turbo mode is active
40
+
41
+ Example:
42
+ >>> # Create a client connected to a running token bucket server
43
+ >>> client = TokenBucketClient(
44
+ ... bucket_name="openai",
45
+ ... bucket_type="requests",
46
+ ... capacity=100,
47
+ ... refill_rate=10,
48
+ ... api_base_url="http://localhost:8000"
49
+ ... )
50
+ >>> # Now use this client just like a regular TokenBucket
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ bucket_name: str,
57
+ bucket_type: str,
58
+ capacity: Union[int, float],
59
+ refill_rate: Union[int, float],
60
+ api_base_url: str = "http://localhost:8000",
61
+ ):
62
+ """
63
+ Initialize a new TokenBucketClient connected to a remote token bucket server.
64
+
65
+ Creates a new TokenBucketClient instance that connects to a remote token
66
+ bucket server. During initialization, it attempts to create the bucket
67
+ on the server if it doesn't already exist.
68
+
69
+ Args:
70
+ bucket_name: Name identifier for the bucket (usually service name)
71
+ bucket_type: Type of bucket, either "requests" or "tokens"
72
+ capacity: Maximum tokens the bucket can hold
73
+ refill_rate: Rate at which tokens are added (tokens per second)
74
+ api_base_url: Base URL for the token bucket server API
75
+ (default: "http://localhost:8000")
76
+
77
+ Raises:
78
+ ValueError: If bucket creation on the server fails
79
+
80
+ Example:
81
+ >>> client = TokenBucketClient(
82
+ ... bucket_name="openai",
83
+ ... bucket_type="requests",
84
+ ... capacity=100,
85
+ ... refill_rate=10
86
+ ... )
87
+ """
88
+ self.bucket_name = bucket_name
89
+ self.bucket_type = bucket_type
90
+ self.capacity = capacity
91
+ self.refill_rate = refill_rate
92
+ self.api_base_url = api_base_url
93
+ self.bucket_id = f"{bucket_name}_{bucket_type}"
94
+
95
+ # Initialize the bucket on the server
96
+ asyncio.run(self._create_bucket())
97
+
98
+ # Cache some values locally
99
+ self.creation_time = time.monotonic()
100
+ self.turbo_mode = False
101
+
102
+ async def _create_bucket(self) -> None:
103
+ """
104
+ Create or retrieve the bucket on the remote server.
105
+
106
+ This private async method sends a request to the server to create a new
107
+ bucket with the specified parameters. If the bucket already exists on
108
+ the server, it updates the local parameters to match the server's values.
109
+
110
+ Raises:
111
+ ValueError: If the server returns an error
112
+ """
113
+ async with aiohttp.ClientSession() as session:
114
+ # Prepare payload with bucket parameters
115
+ payload = {
116
+ "bucket_name": self.bucket_name,
117
+ "bucket_type": self.bucket_type,
118
+ "capacity": self.capacity,
119
+ "refill_rate": self.refill_rate,
120
+ }
121
+
122
+ # Send request to create/retrieve bucket
123
+ async with session.post(
124
+ f"{self.api_base_url}/bucket",
125
+ json=payload,
126
+ ) as response:
127
+ if response.status != 200:
128
+ raise ValueError(f"Unexpected error: {await response.text()}")
129
+
130
+ # Process server response
131
+ result = await response.json()
132
+ if result["status"] == "existing":
133
+ # Update our local values to match the existing bucket
134
+ self.capacity = float(result["bucket"]["capacity"])
135
+ self.refill_rate = float(result["bucket"]["refill_rate"])
136
+
137
+ def turbo_mode_on(self) -> None:
138
+ """
139
+ Enable turbo mode to bypass rate limits.
140
+
141
+ Turbo mode sets the refill rate to infinity on the server,
142
+ effectively bypassing rate limits. This is useful for testing
143
+ or when rate limits are not needed.
144
+
145
+ Raises:
146
+ ValueError: If the server returns an error
147
+
148
+ Example:
149
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
150
+ ... capacity=100, refill_rate=10)
151
+ >>> client.turbo_mode_on() # Now rate limits are bypassed
152
+ """
153
+ asyncio.run(self._set_turbo_mode(True))
154
+ self.turbo_mode = True
155
+
156
+ def turbo_mode_off(self) -> None:
157
+ """
158
+ Disable turbo mode and restore original rate limits.
159
+
160
+ This method restores the original refill rates on the server,
161
+ re-enabling rate limiting after it was bypassed with turbo_mode_on().
162
+
163
+ Raises:
164
+ ValueError: If the server returns an error
165
+
166
+ Example:
167
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
168
+ ... capacity=100, refill_rate=10)
169
+ >>> client.turbo_mode_on() # Bypass rate limits
170
+ >>> # Do some work without rate limiting
171
+ >>> client.turbo_mode_off() # Restore rate limits
172
+ """
173
+ asyncio.run(self._set_turbo_mode(False))
174
+ self.turbo_mode = False
175
+
176
+ async def add_tokens(self, amount: Union[int, float]) -> None:
177
+ """
178
+ Add tokens to the bucket on the server.
179
+
180
+ This async method adds tokens to the bucket on the server.
181
+ It's useful for manually restoring tokens or increasing the
182
+ available tokens beyond the normal refill rate.
183
+
184
+ Args:
185
+ amount: Number of tokens to add to the bucket
186
+
187
+ Raises:
188
+ ValueError: If the server returns an error
189
+
190
+ Example:
191
+ >>> import asyncio
192
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
193
+ ... capacity=100, refill_rate=10)
194
+ >>> # Add 50 tokens to the bucket
195
+ >>> asyncio.run(client.add_tokens(50))
196
+ """
197
+ async with aiohttp.ClientSession() as session:
198
+ async with session.post(
199
+ f"{self.api_base_url}/bucket/{self.bucket_id}/add_tokens",
200
+ params={"amount": amount},
201
+ ) as response:
202
+ if response.status != 200:
203
+ raise ValueError(f"Failed to add tokens: {await response.text()}")
204
+
205
+ async def _set_turbo_mode(self, state: bool) -> None:
206
+ """
207
+ Set the turbo mode state on the server.
208
+
209
+ This private async method sends a request to the server to
210
+ enable or disable turbo mode.
211
+
212
+ Args:
213
+ state: True to enable turbo mode, False to disable
214
+
215
+ Raises:
216
+ ValueError: If the server returns an error
217
+ """
218
+ async with aiohttp.ClientSession() as session:
219
+ async with session.post(
220
+ f"{self.api_base_url}/bucket/{self.bucket_id}/turbo_mode/{str(state).lower()}"
221
+ ) as response:
222
+ if response.status != 200:
223
+ raise ValueError(
224
+ f"Failed to set turbo mode: {await response.text()}"
225
+ )
226
+
227
+ async def get_tokens(
228
+ self, amount: Union[int, float] = 1, cheat_bucket_capacity: bool = True
229
+ ) -> None:
230
+ """
231
+ Request tokens from the token bucket on the server.
232
+
233
+ This async method requests tokens from the token bucket on the server.
234
+ It will either return immediately if tokens are available or raise an
235
+ exception if tokens are not available.
236
+
237
+ Args:
238
+ amount: Number of tokens to request (default: 1)
239
+ cheat_bucket_capacity: If True, allow exceeding capacity temporarily
240
+ (default: True)
241
+
242
+ Raises:
243
+ ValueError: If the server returns an error, which may indicate
244
+ insufficient tokens are available
245
+
246
+ Example:
247
+ >>> import asyncio
248
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
249
+ ... capacity=100, refill_rate=10)
250
+ >>> # Request 20 tokens
251
+ >>> asyncio.run(client.get_tokens(20))
252
+ """
253
+ async with aiohttp.ClientSession() as session:
254
+ async with session.post(
255
+ f"{self.api_base_url}/bucket/{self.bucket_id}/get_tokens",
256
+ params={
257
+ "amount": amount,
258
+ "cheat_bucket_capacity": int(cheat_bucket_capacity),
259
+ },
260
+ ) as response:
261
+ if response.status != 200:
262
+ raise ValueError(f"Failed to get tokens: {await response.text()}")
263
+
264
+ def get_throughput(self, time_window: Optional[float] = None) -> float:
265
+ """
266
+ Calculate the token throughput over a specified time window.
267
+
268
+ This method calculates the average token throughput (tokens per minute)
269
+ over the specified time window by requesting the bucket status from
270
+ the server and analyzing token usage.
271
+
272
+ Args:
273
+ time_window: Time window in seconds to calculate throughput over
274
+ (default: entire bucket lifetime)
275
+
276
+ Returns:
277
+ Average throughput in tokens per minute
278
+
279
+ Example:
280
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
281
+ ... capacity=100, refill_rate=10)
282
+ >>> # Calculate throughput over the last 60 seconds
283
+ >>> throughput = client.get_throughput(60)
284
+ >>> print(f"Average throughput: {throughput:.1f} tokens/minute")
285
+ """
286
+ # Get current bucket status from server
287
+ status = asyncio.run(self._get_status())
288
+ now = time.monotonic()
289
+
290
+ # Determine start time based on time_window parameter
291
+ if time_window is None:
292
+ start_time = self.creation_time
293
+ else:
294
+ start_time = now - time_window
295
+
296
+ # Ensure start_time isn't before bucket creation
297
+ if start_time < self.creation_time:
298
+ start_time = self.creation_time
299
+
300
+ # Calculate elapsed time and avoid division by zero
301
+ elapsed_time = now - start_time
302
+ if elapsed_time == 0:
303
+ return status["num_released"] / 0.001 # Avoid division by zero
304
+
305
+ # Convert to tokens per minute
306
+ return (status["num_released"] / elapsed_time) * 60
307
+
308
+ async def _get_status(self) -> Dict[str, Any]:
309
+ """
310
+ Get the current status of the bucket from the server.
311
+
312
+ This private async method retrieves the current status of the bucket
313
+ from the server, including the current token count, history log,
314
+ and various statistics.
315
+
316
+ Returns:
317
+ Dictionary containing the bucket status information
318
+
319
+ Raises:
320
+ ValueError: If the server returns an error
321
+ """
322
+ async with aiohttp.ClientSession() as session:
323
+ async with session.get(
324
+ f"{self.api_base_url}/bucket/{self.bucket_id}/status"
325
+ ) as response:
326
+ if response.status != 200:
327
+ raise ValueError(
328
+ f"Failed to get bucket status: {await response.text()}"
329
+ )
330
+ return await response.json()
331
+
332
+ def __add__(self, other: "TokenBucketClient") -> "TokenBucketClient":
333
+ """
334
+ Combine two token bucket clients to create a new one with merged limits.
335
+
336
+ This method combines two token bucket clients by creating a new client
337
+ with the minimum capacity and refill rate of both inputs. This is useful
338
+ for creating a client that respects both sets of rate limits.
339
+
340
+ Args:
341
+ other: Another TokenBucketClient to combine with this one
342
+
343
+ Returns:
344
+ A new TokenBucketClient with the combined (minimum) limits
345
+
346
+ Example:
347
+ >>> client1 = TokenBucketClient(bucket_name="service1", bucket_type="requests",
348
+ ... capacity=100, refill_rate=10)
349
+ >>> client2 = TokenBucketClient(bucket_name="service2", bucket_type="requests",
350
+ ... capacity=50, refill_rate=5)
351
+ >>> combined = client1 + client2 # Takes the minimum of both limits
352
+ """
353
+ return TokenBucketClient(
354
+ bucket_name=self.bucket_name,
355
+ bucket_type=self.bucket_type,
356
+ capacity=min(self.capacity, other.capacity),
357
+ refill_rate=min(self.refill_rate, other.refill_rate),
358
+ api_base_url=self.api_base_url,
359
+ )
360
+
361
+ @property
362
+ def tokens(self) -> float:
363
+ """
364
+ Get the current number of tokens available in the bucket.
365
+
366
+ This property retrieves the current token count from the server.
367
+
368
+ Returns:
369
+ Current number of tokens available in the bucket
370
+
371
+ Example:
372
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
373
+ ... capacity=100, refill_rate=10)
374
+ >>> available = client.tokens
375
+ >>> print(f"Available tokens: {available}")
376
+ """
377
+ status = asyncio.run(self._get_status())
378
+ return float(status["tokens"])
379
+
380
+ def wait_time(self, requested_tokens: Union[float, int]) -> float:
381
+ """
382
+ Calculate the time to wait for the requested number of tokens.
383
+
384
+ This method calculates how long to wait (in seconds) for the requested
385
+ number of tokens to become available, based on the current token count
386
+ and refill rate.
387
+
388
+ Args:
389
+ requested_tokens: Number of tokens needed
390
+
391
+ Returns:
392
+ Time to wait in seconds (0.0 if tokens are already available)
393
+
394
+ Raises:
395
+ ValueError: If an error occurs while calculating wait time
396
+
397
+ Example:
398
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
399
+ ... capacity=100, refill_rate=10)
400
+ >>> wait_seconds = client.wait_time(50)
401
+ >>> print(f"Need to wait {wait_seconds:.2f} seconds")
402
+ """
403
+ # If we have enough tokens, no need to wait
404
+ if self.tokens >= float(requested_tokens):
405
+ return 0.0
406
+
407
+ try:
408
+ # Calculate time needed to accumulate the required tokens
409
+ return (requested_tokens - self.tokens) / self.refill_rate
410
+ except Exception as e:
411
+ raise ValueError(f"Error calculating wait time: {e}")
412
+
413
+ # Note: The commented out method below is a reminder for future implementation
414
+ # def wait_time(self, num_tokens: Union[int, float]) -> float:
415
+ # """Server-side wait time calculation (future implementation)"""
416
+ # return 0 # TODO - Need to implement this on the server side
417
+
418
+ def visualize(self) -> Figure:
419
+ """
420
+ Visualize the token bucket usage over time as a matplotlib figure.
421
+
422
+ This method generates a plot showing the available tokens over time,
423
+ which can be useful for monitoring and debugging rate limit issues.
424
+
425
+ Returns:
426
+ A matplotlib Figure object that can be displayed or saved
427
+
428
+ Example:
429
+ >>> client = TokenBucketClient(bucket_name="test", bucket_type="test",
430
+ ... capacity=100, refill_rate=10)
431
+ >>> # Do some operations with the bucket
432
+ >>> plot = client.visualize()
433
+ >>> # Now you can display or save the plot
434
+ """
435
+ # Get the bucket history from the server
436
+ status = asyncio.run(self._get_status())
437
+ times, tokens = zip(*status["log"])
438
+
439
+ # Normalize times to start at 0
440
+ start_time = times[0]
441
+ times = [t - start_time for t in times]
442
+
443
+ # Create the plot
444
+ fig = plt.figure(figsize=(10, 6))
445
+ plt.plot(times, tokens, label="Tokens Available")
446
+ plt.xlabel("Time (seconds)", fontsize=12)
447
+ plt.ylabel("Number of Tokens", fontsize=12)
448
+ details = f"{self.bucket_name} ({self.bucket_type}) Bucket Usage Over Time\nCapacity: {self.capacity:.1f}, Refill Rate: {self.refill_rate:.1f}/second"
449
+ plt.title(details, fontsize=14)
450
+ plt.legend()
451
+ plt.grid(True)
452
+ plt.tight_layout()
453
+
454
+ return fig
455
+
456
+
457
+ # Examples and doctests
458
+ if __name__ == "__main__":
459
+ #import doctest
460
+
461
+ # Example showing how to use TokenBucketClient
462
+ def example_usage():
463
+ """
464
+ Example demonstrating how to use TokenBucketClient:
465
+
466
+ ```python
467
+ import asyncio
468
+ import time
469
+ from edsl.buckets.token_bucket_client import TokenBucketClient
470
+
471
+ # Create a client connected to a running token bucket server
472
+ bucket = TokenBucketClient(
473
+ bucket_name="openai",
474
+ bucket_type="requests",
475
+ capacity=100,
476
+ refill_rate=10,
477
+ api_base_url="http://localhost:8000"
478
+ )
479
+
480
+ # Get tokens from the bucket
481
+ asyncio.run(bucket.get_tokens(50))
482
+
483
+ # Wait for a second
484
+ time.sleep(1)
485
+
486
+ # Get more tokens
487
+ asyncio.run(bucket.get_tokens(30))
488
+
489
+ # Check throughput
490
+ throughput = bucket.get_throughput(1)
491
+ print(f"Throughput: {throughput:.1f} tokens/minute")
492
+
493
+ # Enable turbo mode to bypass rate limits
494
+ bucket.turbo_mode_on()
495
+
496
+ # Do some operations without rate limiting
497
+ asyncio.run(bucket.get_tokens(1000)) # Would normally exceed limits
498
+
499
+ # Disable turbo mode
500
+ bucket.turbo_mode_off()
501
+
502
+ # Visualize bucket usage
503
+ plot = bucket.visualize()
504
+ ```
505
+ """
506
+ pass
507
+
508
+ # Run doctests
509
+ #doctest.testmod()
@@ -0,0 +1,20 @@
1
+ """
2
+ Caching module for EDSL framework.
3
+
4
+ This module provides caching functionality for language model responses,
5
+ with support for both in-memory and persistent storage through SQLite.
6
+ It includes components for managing cache entries, handling cache initialization
7
+ and migration, and synchronizing with remote caches.
8
+
9
+ Key components:
10
+ - Cache: Central class for storing and retrieving language model responses
11
+ - CacheEntry: Represents individual cached responses with metadata
12
+ - CacheHandler: Manages cache initialization and migration
13
+ - SQLiteDict: Dictionary-like interface to SQLite database
14
+ """
15
+
16
+ from .cache import Cache
17
+ from .cache_entry import CacheEntry
18
+ from .cache_handler import CacheHandler
19
+
20
+ __all__ = ["Cache", "CacheEntry", "CacheHandler"]