SimplerLLM 0.3.4__tar.gz → 0.3.6__tar.gz

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 (152) hide show
  1. simplerllm-0.3.6/MANIFEST.in +3 -0
  2. {simplerllm-0.3.4 → simplerllm-0.3.6}/PKG-INFO +5 -2
  3. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/anthropic_wrapper.py +2 -0
  4. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/openai_wrapper.py +2 -0
  5. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_addons/json_generation.py +13 -0
  6. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/anthropic_llm.py +16 -2
  7. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/openai_llm.py +26 -10
  8. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/json_helpers.py +180 -17
  9. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM.egg-info/PKG-INFO +5 -2
  10. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM.egg-info/SOURCES.txt +3 -0
  11. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM.egg-info/requires.txt +3 -1
  12. simplerllm-0.3.6/requirements.txt +30 -0
  13. {simplerllm-0.3.4 → simplerllm-0.3.6}/setup.py +6 -1
  14. simplerllm-0.3.6/tests/test_aspect_ratio_debug.py +114 -0
  15. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_pydantic_json.py +1 -1
  16. {simplerllm-0.3.4 → simplerllm-0.3.6}/LICENSE +0 -0
  17. {simplerllm-0.3.4 → simplerllm-0.3.6}/README.md +0 -0
  18. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/__init__.py +0 -0
  19. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/__init__.py +0 -0
  20. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/__init__.py +0 -0
  21. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/base.py +0 -0
  22. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/providers/__init__.py +0 -0
  23. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/providers/google_image.py +0 -0
  24. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/providers/image_response_models.py +0 -0
  25. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/providers/openai_image.py +0 -0
  26. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/providers/seedream_image.py +0 -0
  27. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/providers/stability_image.py +0 -0
  28. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/wrappers/__init__.py +0 -0
  29. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/wrappers/google_wrapper.py +0 -0
  30. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/wrappers/openai_wrapper.py +0 -0
  31. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/wrappers/seedream_wrapper.py +0 -0
  32. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/image/generation/wrappers/stability_wrapper.py +0 -0
  33. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/__init__.py +0 -0
  34. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/embeddings/__init__.py +0 -0
  35. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/embeddings/base.py +0 -0
  36. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/embeddings/models.py +0 -0
  37. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/embeddings/providers.py +0 -0
  38. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/__init__.py +0 -0
  39. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/base.py +0 -0
  40. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/reliable.py +0 -0
  41. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/__init__.py +0 -0
  42. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/cohere_wrapper.py +0 -0
  43. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/deepseek_wrapper.py +0 -0
  44. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/gemini_wrapper.py +0 -0
  45. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/hf_local_wrapper.py +0 -0
  46. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/moonshot_wrapper.py +0 -0
  47. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/ollama_wrapper.py +0 -0
  48. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/openrouter_wrapper.py +0 -0
  49. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm/wrappers/perplexity_wrapper.py +0 -0
  50. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_addons/__init__.py +0 -0
  51. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_addons/cost_utils.py +0 -0
  52. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_addons/pattern_extraction.py +0 -0
  53. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_brainstorm/__init__.py +0 -0
  54. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_brainstorm/models.py +0 -0
  55. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_brainstorm/recursive_brainstorm.py +0 -0
  56. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_feedback/__init__.py +0 -0
  57. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_feedback/feedback_loop.py +0 -0
  58. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_feedback/models.py +0 -0
  59. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_judge/__init__.py +0 -0
  60. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_judge/judge.py +0 -0
  61. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_judge/models.py +0 -0
  62. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/__init__.py +0 -0
  63. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/cohere_llm.py +0 -0
  64. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/deepseek_llm.py +0 -0
  65. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/gemini_llm.py +0 -0
  66. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/hf_local_llm.py +0 -0
  67. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/llm_response_models.py +0 -0
  68. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/moonshot_llm.py +0 -0
  69. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/ollama_llm.py +0 -0
  70. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/openrouter_llm.py +0 -0
  71. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/perplexity_llm.py +0 -0
  72. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_providers/voyage_llm.py +0 -0
  73. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_router/__init__.py +0 -0
  74. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_router/models.py +0 -0
  75. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/language/llm_router/router.py +0 -0
  76. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/prompts/__init__.py +0 -0
  77. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/prompts/hub/__init__.py +0 -0
  78. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/prompts/hub/agentic_prompts.py +0 -0
  79. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/prompts/hub/prompt_manager.py +0 -0
  80. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/prompts/messages_template.py +0 -0
  81. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/prompts/prompt_builder.py +0 -0
  82. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/__init__.py +0 -0
  83. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/apify_api.py +0 -0
  84. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/email_functions.py +0 -0
  85. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/file_functions.py +0 -0
  86. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/file_loader.py +0 -0
  87. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/generic_loader.py +0 -0
  88. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/image_helpers.py +0 -0
  89. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/pandas_func.py +0 -0
  90. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/pattern_helpers.py +0 -0
  91. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/python_func.py +0 -0
  92. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/rapid_api.py +0 -0
  93. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/serp.py +0 -0
  94. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/text_chunker.py +0 -0
  95. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/tools/youtube.py +0 -0
  96. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/utils/__init__.py +0 -0
  97. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/utils/custom_verbose.py +0 -0
  98. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/__init__.py +0 -0
  99. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/base.py +0 -0
  100. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/exceptions.py +0 -0
  101. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/models.py +0 -0
  102. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/providers/__init__.py +0 -0
  103. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/providers/local_provider.py +0 -0
  104. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/providers/qdrant_provider.py +0 -0
  105. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/wrappers/__init__.py +0 -0
  106. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/wrappers/local_wrapper.py +0 -0
  107. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/vectors/wrappers/qdrant_wrapper.py +0 -0
  108. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/__init__.py +0 -0
  109. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/stt/__init__.py +0 -0
  110. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/stt/base.py +0 -0
  111. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/stt/providers/__init__.py +0 -0
  112. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/stt/providers/openai_stt.py +0 -0
  113. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/stt/providers/stt_response_models.py +0 -0
  114. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/stt/wrappers/__init__.py +0 -0
  115. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/stt/wrappers/openai_wrapper.py +0 -0
  116. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/__init__.py +0 -0
  117. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/base.py +0 -0
  118. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/factory.py +0 -0
  119. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/models.py +0 -0
  120. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/providers/__init__.py +0 -0
  121. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/providers/elevenlabs_tts.py +0 -0
  122. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/providers/lahajati_tts.py +0 -0
  123. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM/voice/tts/providers/openai_tts.py +0 -0
  124. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM.egg-info/dependency_links.txt +0 -0
  125. {simplerllm-0.3.4 → simplerllm-0.3.6}/SimplerLLM.egg-info/top_level.txt +0 -0
  126. {simplerllm-0.3.4 → simplerllm-0.3.6}/setup.cfg +0 -0
  127. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/__init__.py +0 -0
  128. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_anthropic.py +0 -0
  129. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_brainstorm.py +0 -0
  130. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_cohere.py +0 -0
  131. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_deepseek.py +0 -0
  132. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_embeddings.py +0 -0
  133. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_gemini.py +0 -0
  134. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_google_image.py +0 -0
  135. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_hf_local.py +0 -0
  136. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_hf_mini.py +0 -0
  137. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_judge.py +0 -0
  138. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_lahajati_tts.py +0 -0
  139. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_llm_addons.py +0 -0
  140. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_moonshot.py +0 -0
  141. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_ollama.py +0 -0
  142. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_openai.py +0 -0
  143. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_openai_image.py +0 -0
  144. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_openrouter.py +0 -0
  145. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_perplexity.py +0 -0
  146. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_pydantic_advanced.py +0 -0
  147. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_reliable.py +0 -0
  148. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_reliable_pydantic.py +0 -0
  149. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_seedream_image.py +0 -0
  150. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_stability_image.py +0 -0
  151. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_tts.py +0 -0
  152. {simplerllm-0.3.4 → simplerllm-0.3.6}/tests/test_vectors.py +0 -0
@@ -0,0 +1,3 @@
1
+ include requirements.txt
2
+ include README.md
3
+ include LICENSE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SimplerLLM
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: An easy-to-use Library for interacting with language models.
5
5
  Home-page: https://github.com/hassancs91/SimplerLLM
6
6
  Author: Hasan Aboul Hasan
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
21
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
24
  Requires-Python: >=3.6
23
25
  Description-Content-Type: text/markdown
@@ -43,7 +45,6 @@ Requires-Dist: colorama>=0.4
43
45
  Requires-Dist: scipy>=1.15
44
46
  Requires-Dist: tiktoken>=0.9
45
47
  Requires-Dist: qdrant-client==1.14.3
46
- Requires-Dist: voyageai==0.3.3
47
48
  Requires-Dist: pydub>=0.25.1
48
49
  Requires-Dist: elevenlabs>=2.22.0
49
50
  Requires-Dist: moviepy>=1.0.3
@@ -52,6 +53,8 @@ Provides-Extra: transformers
52
53
  Requires-Dist: transformers>=4.36.0; extra == "transformers"
53
54
  Requires-Dist: torch>=2.0.0; extra == "transformers"
54
55
  Requires-Dist: accelerate>=0.25.0; extra == "transformers"
56
+ Provides-Extra: voyage
57
+ Requires-Dist: voyageai>=0.3.3; extra == "voyage"
55
58
  Dynamic: author
56
59
  Dynamic: author-email
57
60
  Dynamic: classifier
@@ -89,6 +89,7 @@ class AnthropicLLM(LLM):
89
89
  max_tokens=max_tokens,
90
90
  full_response=full_response,
91
91
  api_key=self.api_key,
92
+ json_mode=json_mode,
92
93
  )
93
94
  if self.verbose:
94
95
  verbose_print("Web search response received successfully", "info")
@@ -241,6 +242,7 @@ class AnthropicLLM(LLM):
241
242
  max_tokens=max_tokens,
242
243
  full_response=full_response,
243
244
  api_key=self.api_key,
245
+ json_mode=json_mode,
244
246
  )
245
247
  if self.verbose:
246
248
  verbose_print("Web search response received successfully", "info")
@@ -245,6 +245,7 @@ class OpenAILLM(LLM):
245
245
  max_tokens=max_tokens,
246
246
  full_response=full_response,
247
247
  api_key=self.api_key,
248
+ json_mode=json_mode,
248
249
  )
249
250
  if self.verbose:
250
251
  verbose_print("Web search response received successfully", "info")
@@ -407,6 +408,7 @@ class OpenAILLM(LLM):
407
408
  max_tokens=max_tokens,
408
409
  full_response=full_response,
409
410
  api_key=self.api_key,
411
+ json_mode=json_mode,
410
412
  )
411
413
  if self.verbose:
412
414
  verbose_print("Web search response received successfully", "info")
@@ -47,6 +47,7 @@ from SimplerLLM.tools.json_helpers import (
47
47
  validate_json_with_pydantic_model,
48
48
  generate_json_example_from_pydantic,
49
49
  extract_schema_constraints,
50
+ try_auto_wrap_for_nested_model,
50
51
  HAS_ROOT_MODEL,
51
52
  RootModel,
52
53
  )
@@ -279,6 +280,9 @@ def generate_pydantic_json_model(
279
280
  # Handle RootModel list unwrapping (when LLM wraps list in object)
280
281
  json_object = _unwrap_rootmodel_list(json_object, model_class)
281
282
 
283
+ # Handle auto-wrapping for nested models (when LLM returns unwrapped objects)
284
+ json_object = try_auto_wrap_for_nested_model(model_class, json_object)
285
+
282
286
  validated, errors = validate_json_with_pydantic_model(
283
287
  model_class, json_object
284
288
  )
@@ -411,6 +415,9 @@ def generate_pydantic_json_model_reliable(
411
415
  # Handle RootModel list unwrapping (when LLM wraps list in object)
412
416
  json_object = _unwrap_rootmodel_list(json_object, model_class)
413
417
 
418
+ # Handle auto-wrapping for nested models (when LLM returns unwrapped objects)
419
+ json_object = try_auto_wrap_for_nested_model(model_class, json_object)
420
+
414
421
  validated, errors = validate_json_with_pydantic_model(
415
422
  model_class, json_object
416
423
  )
@@ -541,6 +548,9 @@ async def generate_pydantic_json_model_async(
541
548
  # Handle RootModel list unwrapping (when LLM wraps list in object)
542
549
  json_object = _unwrap_rootmodel_list(json_object, model_class)
543
550
 
551
+ # Handle auto-wrapping for nested models (when LLM returns unwrapped objects)
552
+ json_object = try_auto_wrap_for_nested_model(model_class, json_object)
553
+
544
554
  validated, errors = validate_json_with_pydantic_model(
545
555
  model_class, json_object
546
556
  )
@@ -674,6 +684,9 @@ async def generate_pydantic_json_model_reliable_async(
674
684
  # Handle RootModel list unwrapping (when LLM wraps list in object)
675
685
  json_object = _unwrap_rootmodel_list(json_object, model_class)
676
686
 
687
+ # Handle auto-wrapping for nested models (when LLM returns unwrapped objects)
688
+ json_object = try_auto_wrap_for_nested_model(model_class, json_object)
689
+
677
690
  validated, errors = validate_json_with_pydantic_model(
678
691
  model_class, json_object
679
692
  )
@@ -443,6 +443,7 @@ def generate_response_with_web_search(
443
443
  max_tokens: int = 300,
444
444
  full_response: bool = False,
445
445
  api_key: Optional[str] = None,
446
+ json_mode: bool = False,
446
447
  ) -> Optional[Dict]:
447
448
  """
448
449
  Generate a response using Anthropic's Messages API with web search enabled.
@@ -454,6 +455,7 @@ def generate_response_with_web_search(
454
455
  max_tokens: Maximum tokens for the response
455
456
  full_response: If True, returns LLMFullResponse with web_sources
456
457
  api_key: Anthropic API key
458
+ json_mode: If True, enhances system prompt to request JSON output
457
459
 
458
460
  Returns:
459
461
  str or LLMFullResponse: Generated text or full response with web sources
@@ -475,6 +477,11 @@ def generate_response_with_web_search(
475
477
 
476
478
  client = Anthropic(api_key=api_key)
477
479
 
480
+ # Enhance system prompt for JSON mode
481
+ effective_system_prompt = system_prompt
482
+ if json_mode:
483
+ effective_system_prompt = f"{system_prompt}\n\nIMPORTANT: You MUST respond with valid JSON only. No additional text or explanation outside the JSON structure."
484
+
478
485
  # Execute with retry logic
479
486
  response = None
480
487
  last_error = None
@@ -484,7 +491,7 @@ def generate_response_with_web_search(
484
491
  response = client.messages.create(
485
492
  model=model_name,
486
493
  max_tokens=max_tokens,
487
- system=system_prompt,
494
+ system=effective_system_prompt,
488
495
  messages=messages,
489
496
  tools=[{
490
497
  "type": "web_search_20250305",
@@ -550,6 +557,7 @@ async def generate_response_with_web_search_async(
550
557
  max_tokens: int = 300,
551
558
  full_response: bool = False,
552
559
  api_key: Optional[str] = None,
560
+ json_mode: bool = False,
553
561
  ) -> Optional[Dict]:
554
562
  """
555
563
  Asynchronously generate a response using Anthropic's Messages API with web search enabled.
@@ -561,6 +569,7 @@ async def generate_response_with_web_search_async(
561
569
  max_tokens: Maximum tokens for the response
562
570
  full_response: If True, returns LLMFullResponse with web_sources
563
571
  api_key: Anthropic API key
572
+ json_mode: If True, enhances system prompt to request JSON output
564
573
 
565
574
  Returns:
566
575
  str or LLMFullResponse: Generated text or full response with web sources
@@ -584,6 +593,11 @@ async def generate_response_with_web_search_async(
584
593
 
585
594
  client = AsyncAnthropic(api_key=api_key)
586
595
 
596
+ # Enhance system prompt for JSON mode
597
+ effective_system_prompt = system_prompt
598
+ if json_mode:
599
+ effective_system_prompt = f"{system_prompt}\n\nIMPORTANT: You MUST respond with valid JSON only. No additional text or explanation outside the JSON structure."
600
+
587
601
  # Execute with retry logic
588
602
  response = None
589
603
  last_error = None
@@ -593,7 +607,7 @@ async def generate_response_with_web_search_async(
593
607
  response = await client.messages.create(
594
608
  model=model_name,
595
609
  max_tokens=max_tokens,
596
- system=system_prompt,
610
+ system=effective_system_prompt,
597
611
  messages=messages,
598
612
  tools=[{
599
613
  "type": "web_search_20250305",
@@ -713,6 +713,7 @@ def generate_response_with_web_search(
713
713
  max_tokens=300,
714
714
  full_response=False,
715
715
  api_key=None,
716
+ json_mode=False,
716
717
  ):
717
718
  """
718
719
  Generate a response using OpenAI's Responses API with web search enabled.
@@ -723,6 +724,7 @@ def generate_response_with_web_search(
723
724
  max_tokens: Maximum tokens for the response
724
725
  full_response: If True, returns LLMFullResponse with web_sources
725
726
  api_key: OpenAI API key
727
+ json_mode: If True, enables JSON output format
726
728
 
727
729
  Returns:
728
730
  str or LLMFullResponse: Generated text or full response with web sources
@@ -732,11 +734,17 @@ def generate_response_with_web_search(
732
734
 
733
735
  for attempt in range(MAX_RETRIES):
734
736
  try:
735
- response = openai_client.responses.create(
736
- model=model_name,
737
- input=input_text,
738
- tools=[{"type": "web_search"}],
739
- )
737
+ # Build request parameters
738
+ # Note: JSON mode is NOT supported with web search in OpenAI's Responses API
739
+ # The json_mode parameter is accepted but ignored - JSON extraction happens
740
+ # post-response via extract_json_from_text() in json_generation.py
741
+ request_params = {
742
+ "model": model_name,
743
+ "input": input_text,
744
+ "tools": [{"type": "web_search"}],
745
+ }
746
+
747
+ response = openai_client.responses.create(**request_params)
740
748
 
741
749
  # Extract text and citations from the response
742
750
  generated_text = ""
@@ -784,6 +792,7 @@ async def generate_response_with_web_search_async(
784
792
  max_tokens=300,
785
793
  full_response=False,
786
794
  api_key=None,
795
+ json_mode=False,
787
796
  ):
788
797
  """
789
798
  Asynchronously generate a response using OpenAI's Responses API with web search enabled.
@@ -794,6 +803,7 @@ async def generate_response_with_web_search_async(
794
803
  max_tokens: Maximum tokens for the response
795
804
  full_response: If True, returns LLMFullResponse with web_sources
796
805
  api_key: OpenAI API key
806
+ json_mode: If True, enables JSON output format
797
807
 
798
808
  Returns:
799
809
  str or LLMFullResponse: Generated text or full response with web sources
@@ -803,11 +813,17 @@ async def generate_response_with_web_search_async(
803
813
 
804
814
  for attempt in range(MAX_RETRIES):
805
815
  try:
806
- response = await async_openai_client.responses.create(
807
- model=model_name,
808
- input=input_text,
809
- tools=[{"type": "web_search"}],
810
- )
816
+ # Build request parameters
817
+ # Note: JSON mode is NOT supported with web search in OpenAI's Responses API
818
+ # The json_mode parameter is accepted but ignored - JSON extraction happens
819
+ # post-response via extract_json_from_text() in json_generation.py
820
+ request_params = {
821
+ "model": model_name,
822
+ "input": input_text,
823
+ "tools": [{"type": "web_search"}],
824
+ }
825
+
826
+ response = await async_openai_client.responses.create(**request_params)
811
827
 
812
828
  # Extract text and citations from the response
813
829
  generated_text = ""
@@ -15,6 +15,38 @@ except ImportError:
15
15
  RootModel = None
16
16
 
17
17
 
18
+ def _is_decimal_type(field_type) -> bool:
19
+ """Check if a type is Decimal, handling various import scenarios."""
20
+ if field_type is Decimal:
21
+ return True
22
+ if field_type == Decimal:
23
+ return True
24
+ # Fallback: check by name for cases where Decimal is imported differently
25
+ type_name = getattr(field_type, '__name__', None)
26
+ if type_name == 'Decimal':
27
+ return True
28
+ return False
29
+
30
+
31
+ def _is_origin_type(origin, target_types: tuple) -> bool:
32
+ """
33
+ Check if origin matches any of the target types.
34
+ Handles differences between Python versions where get_origin() may return
35
+ different values (e.g., set vs typing.Set).
36
+ """
37
+ if origin is None:
38
+ return False
39
+ # Direct comparison
40
+ if origin in target_types:
41
+ return True
42
+ # Check by name for cross-version compatibility
43
+ origin_name = getattr(origin, '__name__', str(origin))
44
+ for target in target_types:
45
+ target_name = getattr(target, '__name__', str(target))
46
+ if origin_name == target_name:
47
+ return True
48
+ return False
49
+
18
50
 
19
51
  def convert_pydantic_to_json(model_instance):
20
52
  """
@@ -126,7 +158,14 @@ def validate_json_with_pydantic_model(model_class, json_data):
126
158
  try:
127
159
  # RootModel takes value directly, not as kwargs
128
160
  if is_root_model:
129
- model_instance = model_class(item)
161
+ # Handle case where LLM wrapped the list in an object
162
+ # e.g., {"items": ["a", "b"]} should become ["a", "b"]
163
+ actual_item = item
164
+ if isinstance(item, dict) and len(item) == 1:
165
+ single_value = list(item.values())[0]
166
+ if isinstance(single_value, list):
167
+ actual_item = single_value
168
+ model_instance = model_class(actual_item)
130
169
  else:
131
170
  model_instance = model_class(**item)
132
171
  validated_data.append(model_instance.model_dump())
@@ -144,6 +183,102 @@ def validate_json_with_pydantic_model(model_class, json_data):
144
183
  return validated_data, validation_errors
145
184
 
146
185
 
186
+ def try_auto_wrap_for_nested_model(model_class: Type[BaseModel], json_data: list) -> list:
187
+ """
188
+ Auto-wrap unwrapped JSON objects if the model expects them in a single List field.
189
+
190
+ When an LLM returns separate objects instead of wrapping them in the expected
191
+ container field, this function detects and fixes the structure.
192
+
193
+ Example:
194
+ Input (unwrapped): [{"name": "A", ...}, {"name": "B", ...}]
195
+ Output (wrapped): [{"items": [{"name": "A", ...}, {"name": "B", ...}]}]
196
+
197
+ Args:
198
+ model_class: The target Pydantic model class
199
+ json_data: List of extracted JSON objects
200
+
201
+ Returns:
202
+ Modified json_data with auto-wrapping applied if applicable
203
+ """
204
+ if not json_data or len(json_data) == 0:
205
+ return json_data
206
+
207
+ # Skip RootModel (handled separately by _unwrap_rootmodel_list)
208
+ if HAS_ROOT_MODEL and RootModel is not None:
209
+ try:
210
+ if issubclass(model_class, RootModel):
211
+ return json_data
212
+ except TypeError:
213
+ pass
214
+
215
+ # Handle case where LLM returned a JSON array that got wrapped as [[{}, {}]]
216
+ # This happens when extract_json_from_text parses "[{...}, {...}]" and wraps it
217
+ working_data = json_data
218
+ if (len(json_data) == 1 and
219
+ isinstance(json_data[0], list) and
220
+ len(json_data[0]) > 0 and
221
+ all(isinstance(item, dict) for item in json_data[0])):
222
+ # json_data is [[{}, {}]] - use the inner list for processing
223
+ working_data = json_data[0]
224
+
225
+ # First try validation - if at least one item validates, use it
226
+ # This handles the case where extract_json_from_text extracts both
227
+ # the outer wrapper AND inner objects from pretty-printed JSON
228
+ validated, errors = validate_json_with_pydantic_model(model_class, working_data)
229
+ if validated:
230
+ # At least one item validated - return only the validated items
231
+ # Re-extract just the validated data to return
232
+ valid_items = []
233
+ for item in working_data:
234
+ try:
235
+ model_class(**item)
236
+ valid_items.append(item)
237
+ except:
238
+ pass
239
+ if valid_items:
240
+ return valid_items
241
+
242
+ # Find List[SomeModel] fields in the model
243
+ type_hints = get_type_hints(model_class)
244
+ list_fields = []
245
+
246
+ for field_name, field_type in type_hints.items():
247
+ origin = get_origin(field_type)
248
+ if origin is list:
249
+ args = get_args(field_type)
250
+ if args and hasattr(args[0], '__annotations__'):
251
+ # This is a List[SomeModel] field
252
+ list_fields.append((field_name, args[0]))
253
+
254
+ # Only auto-wrap if model has exactly ONE List[Model] field
255
+ if len(list_fields) != 1:
256
+ return json_data
257
+
258
+ field_name, inner_model = list_fields[0]
259
+ inner_fields = set(get_type_hints(inner_model).keys())
260
+
261
+ # Check if ALL extracted objects contain the inner model's required fields
262
+ # Allow extra keys (they'll be ignored during Pydantic validation)
263
+ # This makes auto-wrap more robust with web search where LLM output is less constrained
264
+ all_match = all(
265
+ isinstance(obj, dict) and inner_fields.issubset(set(obj.keys()))
266
+ for obj in working_data
267
+ )
268
+
269
+ if all_match:
270
+ # Wrap the objects in the expected structure
271
+ wrapped = [{field_name: working_data}]
272
+
273
+ # Validate the wrapped structure
274
+ validated, errors = validate_json_with_pydantic_model(model_class, wrapped)
275
+ if not errors:
276
+ return wrapped
277
+
278
+ # Return original if wrapping didn't help
279
+ return working_data
280
+
281
+
147
282
  def convert_json_to_pydantic_model(model_class, json_data):
148
283
  try:
149
284
  # Handle RootModel - it takes the value directly, not as kwargs
@@ -173,6 +308,10 @@ def get_field_constraints(field_info):
173
308
  """
174
309
  constraints = {}
175
310
 
311
+ # Check direct pattern attribute on FieldInfo (Pydantic v2 Field(pattern="..."))
312
+ if hasattr(field_info, 'pattern') and field_info.pattern:
313
+ constraints['pattern'] = field_info.pattern
314
+
176
315
  # Check if field_info has constraints attribute (Pydantic v2)
177
316
  if hasattr(field_info, 'constraints'):
178
317
  for constraint in field_info.constraints:
@@ -362,7 +501,7 @@ def example_value_for_type(field_type: Type, constraints: dict = None, _recursio
362
501
  return "12345678-1234-5678-1234-567812345678"
363
502
 
364
503
  # Handle Decimal type
365
- if field_type == Decimal:
504
+ if _is_decimal_type(field_type):
366
505
  if 'ge' in constraints:
367
506
  return str(constraints['ge'])
368
507
  elif 'gt' in constraints:
@@ -406,21 +545,44 @@ def example_value_for_type(field_type: Type, constraints: dict = None, _recursio
406
545
  for field_name, field_info in field_type.model_fields.items():
407
546
  field_constraints = get_field_constraints(field_info)
408
547
  field_type_hint = get_type_hints(field_type).get(field_name)
409
- example_data[field_name] = example_value_for_type(
410
- field_type_hint,
411
- field_constraints,
412
- _recursion_depth + 1,
413
- _seen_types
414
- )
548
+ # Try smart example first (handles field validators like username, password, email)
549
+ smart = get_smart_example_for_field(field_name, field_type_hint, field_constraints)
550
+ if smart is not None:
551
+ example_data[field_name] = smart
552
+ else:
553
+ value = example_value_for_type(
554
+ field_type_hint,
555
+ field_constraints,
556
+ _recursion_depth + 1,
557
+ _seen_types
558
+ )
559
+ # For list-type fields, ensure we return [] instead of None
560
+ # This handles circular references in List[Model] fields
561
+ if value is None and field_type_hint is not None:
562
+ hint_origin = get_origin(field_type_hint)
563
+ if hint_origin is list:
564
+ value = []
565
+ example_data[field_name] = value
415
566
  else:
416
567
  # Fallback for older Pydantic versions
417
568
  for field_name, field_type_hint in get_type_hints(field_type).items():
418
- example_data[field_name] = example_value_for_type(
419
- field_type_hint,
420
- None,
421
- _recursion_depth + 1,
422
- _seen_types
423
- )
569
+ # Try smart example first
570
+ smart = get_smart_example_for_field(field_name, field_type_hint, {})
571
+ if smart is not None:
572
+ example_data[field_name] = smart
573
+ else:
574
+ value = example_value_for_type(
575
+ field_type_hint,
576
+ {},
577
+ _recursion_depth + 1,
578
+ _seen_types
579
+ )
580
+ # For list-type fields, ensure we return [] instead of None
581
+ if value is None and field_type_hint is not None:
582
+ hint_origin = get_origin(field_type_hint)
583
+ if hint_origin is list:
584
+ value = []
585
+ example_data[field_name] = value
424
586
  return field_type(**example_data)
425
587
  elif field_type == str:
426
588
  # Check for pattern constraint first
@@ -447,8 +609,9 @@ def example_value_for_type(field_type: Type, constraints: dict = None, _recursio
447
609
  return base_string[:target_length]
448
610
  else:
449
611
  # Need to pad or repeat to meet minimum length
612
+ # Use underscores instead of spaces to avoid breaking alphanumeric validators
450
613
  repeats = (target_length // len(base_string)) + 1
451
- repeated = (base_string + " ") * repeats
614
+ repeated = (base_string + "_") * repeats
452
615
  return repeated[:target_length]
453
616
  elif field_type == int:
454
617
  # Respect numeric constraints
@@ -532,7 +695,7 @@ def example_value_for_type(field_type: Type, constraints: dict = None, _recursio
532
695
  for i in range(1, 3): # Create 2 example key-value pairs
533
696
  example_dict[f"example_key_{i}"] = example_value_for_type(value_type, {}, _recursion_depth + 1, _seen_types)
534
697
  return example_dict
535
- elif origin == set or origin == frozenset: # Handle Set and FrozenSet
698
+ elif _is_origin_type(origin, (set, frozenset)): # Handle Set and FrozenSet
536
699
  args = get_args(field_type)
537
700
  if not args:
538
701
  return [] # No type specified, return empty list
@@ -548,7 +711,7 @@ def example_value_for_type(field_type: Type, constraints: dict = None, _recursio
548
711
  value = f"{value}_{i}"
549
712
  items.append(value)
550
713
  return items # Return as list since JSON doesn't support sets
551
- elif origin == tuple: # Handle Tuple types
714
+ elif _is_origin_type(origin, (tuple,)): # Handle Tuple types
552
715
  args = get_args(field_type)
553
716
  if not args:
554
717
  return [] # No type args, return empty list
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SimplerLLM
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: An easy-to-use Library for interacting with language models.
5
5
  Home-page: https://github.com/hassancs91/SimplerLLM
6
6
  Author: Hasan Aboul Hasan
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
21
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
24
  Requires-Python: >=3.6
23
25
  Description-Content-Type: text/markdown
@@ -43,7 +45,6 @@ Requires-Dist: colorama>=0.4
43
45
  Requires-Dist: scipy>=1.15
44
46
  Requires-Dist: tiktoken>=0.9
45
47
  Requires-Dist: qdrant-client==1.14.3
46
- Requires-Dist: voyageai==0.3.3
47
48
  Requires-Dist: pydub>=0.25.1
48
49
  Requires-Dist: elevenlabs>=2.22.0
49
50
  Requires-Dist: moviepy>=1.0.3
@@ -52,6 +53,8 @@ Provides-Extra: transformers
52
53
  Requires-Dist: transformers>=4.36.0; extra == "transformers"
53
54
  Requires-Dist: torch>=2.0.0; extra == "transformers"
54
55
  Requires-Dist: accelerate>=0.25.0; extra == "transformers"
56
+ Provides-Extra: voyage
57
+ Requires-Dist: voyageai>=0.3.3; extra == "voyage"
55
58
  Dynamic: author
56
59
  Dynamic: author-email
57
60
  Dynamic: classifier
@@ -1,5 +1,7 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
4
+ requirements.txt
3
5
  setup.py
4
6
  SimplerLLM/__init__.py
5
7
  SimplerLLM.egg-info/PKG-INFO
@@ -120,6 +122,7 @@ SimplerLLM/voice/tts/providers/lahajati_tts.py
120
122
  SimplerLLM/voice/tts/providers/openai_tts.py
121
123
  tests/__init__.py
122
124
  tests/test_anthropic.py
125
+ tests/test_aspect_ratio_debug.py
123
126
  tests/test_brainstorm.py
124
127
  tests/test_cohere.py
125
128
  tests/test_deepseek.py
@@ -19,7 +19,6 @@ colorama>=0.4
19
19
  scipy>=1.15
20
20
  tiktoken>=0.9
21
21
  qdrant-client==1.14.3
22
- voyageai==0.3.3
23
22
  pydub>=0.25.1
24
23
  elevenlabs>=2.22.0
25
24
  moviepy>=1.0.3
@@ -29,3 +28,6 @@ yt-dlp>=2023.3.4
29
28
  transformers>=4.36.0
30
29
  torch>=2.0.0
31
30
  accelerate>=0.25.0
31
+
32
+ [voyage]
33
+ voyageai>=0.3.3
@@ -0,0 +1,30 @@
1
+ aiohttp>=3.9
2
+ anthropic>=0.40.0
3
+ cohere>=5.0
4
+ duckduckgo_search>=5.3
5
+ elevenlabs>=1.0
6
+ lxml_html_clean>=0.1
7
+ newspaper3k>=0.2
8
+ numpy>=1.26
9
+ openai>=1.59
10
+ google-genai>=1.52.0
11
+ Pillow>=10.0
12
+ pydantic>=2.10
13
+ PyPDF2>=3.0
14
+ python-dotenv>=1.0
15
+ python_docx>=1.1
16
+ requests>=2.31
17
+ youtube_transcript_api>=0.6
18
+ colorama>=0.4
19
+ scipy>=1.15
20
+ tiktoken>=0.9
21
+ qdrant-client==1.14.3
22
+ # voyageai is optional - install via: pip install SimplerLLM[voyage]
23
+ # (Not in default install because upstream voyageai currently requires-python <3.14)
24
+ pydub>=0.25.1
25
+ elevenlabs>=2.22.0
26
+ # sounddevice>=0.4.6 # Optional - for live voice features. Requires PortAudio. Install with: pip install SimplerLLM[voice]
27
+ # pynput>=1.7.6 # Optional - for push-to-talk. Install with: pip install SimplerLLM[voice]
28
+ # pygame>=2.5.0 # Optional - only needed for AudioPlayer file playback. Install with: pip install pygame>=2.5.0
29
+ moviepy>=1.0.3
30
+ yt-dlp>=2023.3.4
@@ -15,6 +15,9 @@ extras_require = {
15
15
  "torch>=2.0.0",
16
16
  "accelerate>=0.25.0",
17
17
  ],
18
+ "voyage": [
19
+ "voyageai>=0.3.3",
20
+ ],
18
21
  }
19
22
 
20
23
  # Read the long description from the README file
@@ -23,7 +26,7 @@ with open("README.md", encoding="utf-8") as f:
23
26
 
24
27
  setup(
25
28
  name="SimplerLLM",
26
- version="0.3.4",
29
+ version="0.3.6",
27
30
  author="Hasan Aboul Hasan",
28
31
  author_email="hasan@learnwithhasan.com",
29
32
  description="An easy-to-use Library for interacting with language models.",
@@ -48,6 +51,8 @@ setup(
48
51
  "Programming Language :: Python :: 3.10",
49
52
  "Programming Language :: Python :: 3.11",
50
53
  "Programming Language :: Python :: 3.12",
54
+ "Programming Language :: Python :: 3.13",
55
+ "Programming Language :: Python :: 3.14",
51
56
  "Topic :: Software Development :: Libraries :: Python Modules",
52
57
  ],
53
58