sunholo 0.100.3__tar.gz → 0.101.2__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 (165) hide show
  1. {sunholo-0.100.3 → sunholo-0.101.2}/PKG-INFO +2 -2
  2. {sunholo-0.100.3 → sunholo-0.101.2}/setup.py +1 -1
  3. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/gcs/add_file.py +35 -8
  4. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/gcs/download_url.py +1 -0
  5. sunholo-0.101.2/sunholo/gcs/extract_and_sign.py +41 -0
  6. sunholo-0.101.2/sunholo/genai/images.py +38 -0
  7. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/genai/process_funcs_cls.py +50 -3
  8. sunholo-0.101.2/sunholo/langfuse/prompts.py +80 -0
  9. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/streaming/content_buffer.py +0 -1
  10. sunholo-0.101.2/sunholo/utils/mime.py +63 -0
  11. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo.egg-info/PKG-INFO +2 -2
  12. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo.egg-info/SOURCES.txt +4 -0
  13. sunholo-0.101.2/tests/test_async.py +93 -0
  14. sunholo-0.100.3/sunholo/langfuse/prompts.py +0 -44
  15. {sunholo-0.100.3 → sunholo-0.101.2}/LICENSE.txt +0 -0
  16. {sunholo-0.100.3 → sunholo-0.101.2}/MANIFEST.in +0 -0
  17. {sunholo-0.100.3 → sunholo-0.101.2}/README.md +0 -0
  18. {sunholo-0.100.3 → sunholo-0.101.2}/setup.cfg +0 -0
  19. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/__init__.py +0 -0
  20. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/__init__.py +0 -0
  21. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/chat_history.py +0 -0
  22. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/dispatch_to_qa.py +0 -0
  23. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/fastapi/__init__.py +0 -0
  24. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/fastapi/base.py +0 -0
  25. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/fastapi/qna_routes.py +0 -0
  26. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/flask/__init__.py +0 -0
  27. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/flask/base.py +0 -0
  28. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/flask/qna_routes.py +0 -0
  29. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/flask/vac_routes.py +0 -0
  30. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/langserve.py +0 -0
  31. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/pubsub.py +0 -0
  32. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/route.py +0 -0
  33. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/special_commands.py +0 -0
  34. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/agents/swagger.py +0 -0
  35. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/archive/__init__.py +0 -0
  36. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/archive/archive.py +0 -0
  37. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/auth/__init__.py +0 -0
  38. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/auth/gcloud.py +0 -0
  39. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/auth/refresh.py +0 -0
  40. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/auth/run.py +0 -0
  41. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/azure/__init__.py +0 -0
  42. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/azure/auth.py +0 -0
  43. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/azure/blobs.py +0 -0
  44. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/azure/event_grid.py +0 -0
  45. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/bots/__init__.py +0 -0
  46. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/bots/discord.py +0 -0
  47. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/bots/github_webhook.py +0 -0
  48. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/bots/webapp.py +0 -0
  49. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/__init__.py +0 -0
  50. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/azure.py +0 -0
  51. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/doc_handling.py +0 -0
  52. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/encode_metadata.py +0 -0
  53. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/images.py +0 -0
  54. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/loaders.py +0 -0
  55. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/message_data.py +0 -0
  56. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/pdfs.py +0 -0
  57. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/process_chunker_data.py +0 -0
  58. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/publish.py +0 -0
  59. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/pubsub.py +0 -0
  60. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/chunker/splitter.py +0 -0
  61. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/__init__.py +0 -0
  62. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/chat_vac.py +0 -0
  63. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/cli.py +0 -0
  64. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/cli_init.py +0 -0
  65. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/configs.py +0 -0
  66. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/deploy.py +0 -0
  67. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/embedder.py +0 -0
  68. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/merge_texts.py +0 -0
  69. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/run_proxy.py +0 -0
  70. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/sun_rich.py +0 -0
  71. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/swagger.py +0 -0
  72. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/cli/vertex.py +0 -0
  73. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/components/__init__.py +0 -0
  74. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/components/llm.py +0 -0
  75. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/components/retriever.py +0 -0
  76. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/components/vectorstore.py +0 -0
  77. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/custom_logging.py +0 -0
  78. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/__init__.py +0 -0
  79. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/alloydb.py +0 -0
  80. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/alloydb_client.py +0 -0
  81. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/database.py +0 -0
  82. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/lancedb.py +0 -0
  83. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/sql/sb/create_function.sql +0 -0
  84. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/sql/sb/create_function_time.sql +0 -0
  85. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/sql/sb/create_table.sql +0 -0
  86. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  87. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/sql/sb/return_sources.sql +0 -0
  88. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/sql/sb/setup.sql +0 -0
  89. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/static_dbs.py +0 -0
  90. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/database/uuid.py +0 -0
  91. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/discovery_engine/__init__.py +0 -0
  92. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/discovery_engine/chunker_handler.py +0 -0
  93. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/discovery_engine/create_new.py +0 -0
  94. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  95. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/discovery_engine/get_ai_search_chunks.py +0 -0
  96. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/embedder/__init__.py +0 -0
  97. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/embedder/embed_chunk.py +0 -0
  98. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/excel/__init__.py +0 -0
  99. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/excel/plugin.py +0 -0
  100. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/gcs/__init__.py +0 -0
  101. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/gcs/download_folder.py +0 -0
  102. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/gcs/metadata.py +0 -0
  103. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/genai/__init__.py +0 -0
  104. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/genai/init.py +0 -0
  105. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/genai/safety.py +0 -0
  106. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/invoke/__init__.py +0 -0
  107. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/invoke/async_class.py +0 -0
  108. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/invoke/direct_vac_func.py +0 -0
  109. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/invoke/invoke_vac_utils.py +0 -0
  110. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/langfuse/__init__.py +0 -0
  111. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/langfuse/callback.py +0 -0
  112. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/langfuse/evals.py +0 -0
  113. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/llamaindex/__init__.py +0 -0
  114. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/llamaindex/get_files.py +0 -0
  115. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/llamaindex/import_files.py +0 -0
  116. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/llamaindex/llamaindex_class.py +0 -0
  117. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/llamaindex/user_history.py +0 -0
  118. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/lookup/__init__.py +0 -0
  119. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/lookup/model_lookup.yaml +0 -0
  120. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/patches/__init__.py +0 -0
  121. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/patches/langchain/__init__.py +0 -0
  122. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/patches/langchain/lancedb.py +0 -0
  123. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/patches/langchain/vertexai.py +0 -0
  124. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/pubsub/__init__.py +0 -0
  125. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/pubsub/process_pubsub.py +0 -0
  126. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/pubsub/pubsub_manager.py +0 -0
  127. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/qna/__init__.py +0 -0
  128. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/qna/parsers.py +0 -0
  129. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/qna/retry.py +0 -0
  130. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/streaming/__init__.py +0 -0
  131. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/streaming/langserve.py +0 -0
  132. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/streaming/stream_lookup.py +0 -0
  133. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/streaming/streaming.py +0 -0
  134. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/summarise/__init__.py +0 -0
  135. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/summarise/summarise.py +0 -0
  136. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/terraform/__init__.py +0 -0
  137. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/terraform/tfvars_editor.py +0 -0
  138. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/tools/__init__.py +0 -0
  139. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/tools/web_browser.py +0 -0
  140. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/__init__.py +0 -0
  141. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/api_key.py +0 -0
  142. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/big_context.py +0 -0
  143. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/config.py +0 -0
  144. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/config_class.py +0 -0
  145. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/config_schema.py +0 -0
  146. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/gcp.py +0 -0
  147. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/gcp_project.py +0 -0
  148. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/parsers.py +0 -0
  149. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/timedelta.py +0 -0
  150. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/user_ids.py +0 -0
  151. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/utils/version.py +0 -0
  152. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/__init__.py +0 -0
  153. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/extensions_call.py +0 -0
  154. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/extensions_class.py +0 -0
  155. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/genai_functions.py +0 -0
  156. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/init.py +0 -0
  157. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/memory_tools.py +0 -0
  158. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/safety.py +0 -0
  159. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo/vertex/type_dict_to_json.py +0 -0
  160. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo.egg-info/dependency_links.txt +0 -0
  161. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo.egg-info/entry_points.txt +0 -0
  162. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo.egg-info/requires.txt +0 -0
  163. {sunholo-0.100.3 → sunholo-0.101.2}/sunholo.egg-info/top_level.txt +0 -0
  164. {sunholo-0.100.3 → sunholo-0.101.2}/tests/test_chat_history.py +0 -0
  165. {sunholo-0.100.3 → sunholo-0.101.2}/tests/test_config.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.100.3
3
+ Version: 0.101.2
4
4
  Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
5
5
  Home-page: https://github.com/sunholo-data/sunholo-py
6
- Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.100.3.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.101.2.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -1,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- version = '0.100.3'
3
+ version = '0.101.2'
4
4
 
5
5
  setup(
6
6
  name='sunholo',
@@ -25,6 +25,40 @@ except ImportError:
25
25
  from ..custom_logging import log
26
26
  from ..utils import ConfigManager
27
27
 
28
+ def guess_image_type(file_path: str) -> str:
29
+ """
30
+ Guess the image type based on the file extension.
31
+
32
+ Args:
33
+ file_path (str): The path or URL of the image file.
34
+
35
+ Returns:
36
+ str: The guessed image type (e.g., "jpeg", "png", "gif", etc.)
37
+ or None if the extension is not recognized.
38
+ """
39
+ # Extract the file extension
40
+ _, ext = os.path.splitext(file_path)
41
+
42
+ # Normalize and remove the leading dot
43
+ ext = ext.lower().strip('.')
44
+
45
+ # Mapping of common file extensions to image types
46
+ extension_to_type = {
47
+ "jpg": "image/jpeg",
48
+ "jpeg": "image/jpeg",
49
+ "png": "image/png",
50
+ "gif": "image/gif",
51
+ "bmp": "image/bmp",
52
+ "tiff": "image/tiff",
53
+ "tif": "image/tiff",
54
+ "webp": "image/webp",
55
+ "ico": "image/ico",
56
+ "svg": "image/svg",
57
+ "pdf": "application/pdf",
58
+ }
59
+
60
+ return extension_to_type.get(ext.lower(), None)
61
+
28
62
 
29
63
  def handle_base64_image(base64_data: str, vector_name: str, extension: str):
30
64
  """
@@ -58,14 +92,7 @@ def handle_base64_image(base64_data: str, vector_name: str, extension: str):
58
92
  os.remove(filename) # Clean up the saved file
59
93
 
60
94
  # Determine MIME type based on extension
61
- mime_type = {
62
- ".jpg": "image/jpeg",
63
- ".jpeg": "image/jpeg",
64
- ".png": "image/png",
65
- ".gif": "image/gif",
66
- ".bmp": "image/bmp",
67
- ".tiff": "image/tiff"
68
- }.get(extension.lower(), "application/octet-stream") # Default MIME type if unknown
95
+ mime_type = guess_image_type(extension) or "application/octet-stream"
69
96
 
70
97
  return image_uri, mime_type
71
98
  except Exception as e:
@@ -176,6 +176,7 @@ def construct_download_link_simple(bucket_name:str, object_name:str) -> Tuple[st
176
176
  else:
177
177
  public_url = f"https://storage.cloud.google.com/{bucket_name}/{quote(object_name)}"
178
178
  filename = os.path.basename(object_name)
179
+ log.info(f"Created simple download link: {public_url}")
179
180
  return public_url, filename, False
180
181
 
181
182
  def parse_gs_uri(gs_uri: str) -> Tuple[str, str]:
@@ -0,0 +1,41 @@
1
+ import re
2
+ import asyncio
3
+
4
+ from .download_url import construct_download_link
5
+ from ..utils.mime import guess_mime_type
6
+ from ..custom_logging import log
7
+
8
+ async def extract_gs_uris_and_sign(content, pattern=r'gs://[^\n]+\.(?:png|jpg|jpeg|pdf|txt|md)'):
9
+
10
+ gs_matches = re.findall(pattern, content)
11
+ unique_gs_matches = set(gs_matches)
12
+ image_signed_urls = []
13
+ if unique_gs_matches:
14
+ log.info(f"Got gs matches: {unique_gs_matches}")
15
+
16
+ async def process_link(gs_url):
17
+ log.info(f"Processing {gs_url}")
18
+ link, encoded_filename, signed = await asyncio.to_thread(construct_download_link, gs_url)
19
+ if signed:
20
+ try:
21
+ mime_type = guess_mime_type(gs_url)
22
+ except Exception as err:
23
+ log.error(f"Could not find mime_type for {link} - {str(err)}")
24
+ mime_type = "application/octet-stream"
25
+
26
+ return {
27
+ "original": gs_url,
28
+ "link": link,
29
+ "name": encoded_filename,
30
+ "mime": mime_type,
31
+ "signed": signed
32
+ }
33
+ else:
34
+ log.info(f"Could not sign this GS_URI: {gs_url} - skipping")
35
+ return None
36
+
37
+ # Gather all tasks and run them concurrently
38
+ image_signed_urls = await asyncio.gather(*(process_link(gs_url) for gs_url in unique_gs_matches))
39
+
40
+ log.info(f"found files to msg: {image_signed_urls}")
41
+ return image_signed_urls
@@ -0,0 +1,38 @@
1
+ import re
2
+ from ..utils.mime import guess_mime_type
3
+ from ..gcs import get_bytes_from_gcs
4
+ from ..custom_logging import log
5
+ import io
6
+ import os
7
+ try:
8
+ import google.generativeai as genai
9
+ except ImportError:
10
+ genai = None
11
+
12
+ def extract_gs_images_and_genai_upload(content:str):
13
+ # Regular expression to find gs:// URLs
14
+ pattern = r'gs://[^ ]+\.(?:png|jpg|jpeg|pdf)'
15
+
16
+ gs_matches = re.findall(pattern, content)
17
+ unique_gs_matches = set(gs_matches)
18
+ output_gs_images = []
19
+ for gs_uri in unique_gs_matches:
20
+ mime_type = guess_mime_type(gs_uri)
21
+ if mime_type is None:
22
+ continue
23
+
24
+ log.info(f"Getting bytes from GCS: {gs_uri}")
25
+ image_bytes = get_bytes_from_gcs(gs_uri)
26
+ if image_bytes is None:
27
+ continue
28
+ image_file = io.BytesIO(image_bytes)
29
+ image_file.name = os.path.basename(gs_uri) # Assign a name, as some APIs require it
30
+
31
+ try:
32
+ uploaded_file = genai.upload_file(image_file)
33
+ output_gs_images.append(uploaded_file)
34
+
35
+ except Exception as e:
36
+ log.error(f"Error adding {gs_uri} to base64: {str(e)}")
37
+
38
+ return output_gs_images
@@ -14,11 +14,18 @@ try:
14
14
  import proto
15
15
  from google.generativeai.types import RequestOptions
16
16
  from google.api_core import retry
17
+ from google.generativeai import ChatSession
17
18
  except ImportError:
18
19
  genai = None
20
+ ChatSession = None
21
+
22
+ from .images import extract_gs_images_and_genai_upload
19
23
 
20
24
  if TYPE_CHECKING:
21
25
  from google.generativeai.protos import Part
26
+ from google.generativeai import ChatSession
27
+
28
+
22
29
 
23
30
  class GenAIFunctionProcessor:
24
31
  """
@@ -365,21 +372,43 @@ class GenAIFunctionProcessor:
365
372
  log.info(f"Cleaning:\n{string}\n > to >\n{clean}")
366
373
 
367
374
  return clean
375
+
376
+ def convert_composite_to_native(self, value):
377
+ """
378
+ Recursively converts a proto MapComposite or RepeatedComposite object to native Python types.
379
+
380
+ Args:
381
+ value: The proto object, which could be a MapComposite, RepeatedComposite, or a primitive.
368
382
 
369
- def run_agent_loop(self, chat, content, callback, guardrail_max=10, loop_return=3):
383
+ Returns:
384
+ The equivalent Python dictionary, list, or primitive type.
385
+ """
386
+ if isinstance(value, proto.marshal.collections.maps.MapComposite):
387
+ # Convert MapComposite to a dictionary, recursively processing its values
388
+ return {key: self.convert_composite_to_native(val) for key, val in value.items()}
389
+ elif isinstance(value, proto.marshal.collections.repeated.RepeatedComposite):
390
+ # Convert RepeatedComposite to a list, recursively processing its elements
391
+ return [self.convert_composite_to_native(item) for item in value]
392
+ else:
393
+ # If it's a primitive value, return it as is
394
+ return value
395
+
396
+ def run_agent_loop(self, chat:ChatSession, content:list, callback=None, guardrail_max=10, loop_return=3):
370
397
  """
371
398
  Runs the agent loop, sending messages to the orchestrator, processing responses, and executing functions.
372
399
 
373
400
  Args:
374
401
  chat: The chat object for interaction with the orchestrator.
375
402
  content: The initial content to send to the agent.
376
- callback: The callback object for handling intermediate responses.
403
+ callback: The callback object for handling intermediate responses. If not supplied will use self.IOCallback()
377
404
  guardrail_max (int): The maximum number of iterations for the loop.
378
405
  loop_return (int): The number of last loop iterations to return. Default 3 will return last 3 iterations. If loop_return > guardrail_max then all iterations are returned.
379
406
 
380
407
  Returns:
381
408
  tuple: (big_text, usage_metadata) from the loop execution.
382
409
  """
410
+ if not callback:
411
+ callback = self.IOCallback()
383
412
  guardrail = 0
384
413
  big_result = []
385
414
  usage_metadata = {
@@ -482,7 +511,9 @@ class GenAIFunctionProcessor:
482
511
  fn_result_json = None
483
512
  # Convert MapComposite to a standard Python dictionary
484
513
  if isinstance(fn_result, proto.marshal.collections.maps.MapComposite):
485
- fn_result_json = dict(fn_result)
514
+ fn_result_json = self.convert_composite_to_native(fn_result)
515
+ elif isinstance(fn_result, proto.marshal.collections.repeated.RepeatedComposite):
516
+ fn_result = self.convert_composite_to_native(fn_result)
486
517
  elif isinstance(fn_result, dict):
487
518
  fn_result_json = fn_result
488
519
  elif isinstance(fn_result, str):
@@ -535,6 +566,13 @@ class GenAIFunctionProcessor:
535
566
 
536
567
  if this_text:
537
568
  content.append(f"Agent: {this_text}")
569
+ # if text includes gs:// try to download it
570
+ image_uploads = extract_gs_images_and_genai_upload(this_text)
571
+ if image_uploads:
572
+ for img in image_uploads:
573
+ log.info(f"Adding {img=}")
574
+ content.append(img)
575
+ content.append(f"{img.name} was created by agent and added")
538
576
  log.info(f"[{guardrail}] Updated content:\n{this_text}")
539
577
  big_result.append(this_text)
540
578
  else:
@@ -567,6 +605,15 @@ class GenAIFunctionProcessor:
567
605
 
568
606
  return big_text, usage_metadata
569
607
 
608
+ class IOCallback:
609
+ """
610
+ This is a default callback that will print to console any tokens it recieves.
611
+ """
612
+ def on_llm_new_token(self, token:str):
613
+ print(token)
614
+ def on_llm_end(self, response):
615
+ print(f"\nFull response: \n{response}")
616
+
570
617
  @staticmethod
571
618
  def decide_to_go_on(go_on: bool, chat_summary: str) -> dict:
572
619
  """
@@ -0,0 +1,80 @@
1
+ from ..custom_logging import log
2
+ from ..utils import ConfigManager
3
+
4
+ # Load the YAML file
5
+ def load_prompt_from_yaml(key, prefix="sunholo", load_from_file=False, f_string=True):
6
+ """
7
+ Returns a string you can use with prompts.
8
+
9
+ If load_from_file=False, by default it will try to load from Langfuse, if fails (which is laggy so not ideal) then load from file.
10
+
11
+ Prompts on Langfuse should be specified with a name with {prefix}-{key} e.g. "sunholo-hello"
12
+
13
+ Prompts in files will use yaml:
14
+
15
+ ```yaml
16
+ kind: promptConfig
17
+ apiVersion: v1
18
+ prompts:
19
+ sunholo:
20
+ hello: |
21
+ Say hello to {name}
22
+ ```
23
+
24
+ And load via utils.ConfigManager:
25
+
26
+ ```python
27
+ # equivalent to load_prompt_from_yaml("hello", load_from_file=True)
28
+ config = ConfigManager("sunholo")
29
+ config.promptConfig("hello")
30
+ ```
31
+
32
+ If f_string is True will be in a Langchain style prompt e.g. { one brace }
33
+ If f_string is False will be Langfuse style prompt e.g. {{ two braces }} - see https://langfuse.com/docs/prompts/get-started
34
+
35
+ Example:
36
+
37
+ ```python
38
+ from sunholo.langfuse.prompts import load_prompt_from_yaml
39
+ # f_string
40
+ hello_template = load_prompt_from_yaml("hello")
41
+ hello_template.format(name="Bob")
42
+
43
+ #langfuse style
44
+ hello_template = load_prompt_from_yaml("hello", f_string=False)
45
+ hello_template.compile(name="Bob")
46
+
47
+ # if prompt not available on langfuse, will attempt to load from local promptConfig file
48
+ hello_template = load_prompt_from_yaml("hello", load_from_file=True)
49
+
50
+ ```
51
+
52
+ """
53
+ config = ConfigManager(prefix)
54
+ if load_from_file:
55
+
56
+ return config.promptConfig(key)
57
+
58
+
59
+ from langfuse import Langfuse
60
+
61
+ # Initialize Langfuse client
62
+ langfuse = Langfuse()
63
+
64
+ try:
65
+ if prefix is None:
66
+ langfuse_template = key
67
+ else:
68
+ langfuse_template = f"{prefix}-{key}"
69
+
70
+ langfuse_prompt = langfuse.get_prompt(langfuse_template, cache_ttl_seconds=300)
71
+
72
+ if f_string:
73
+ return langfuse_prompt.get_langchain_prompt()
74
+
75
+ return langfuse_prompt
76
+
77
+ except Exception as err:
78
+ log.warning(f"Could not find langfuse template: {langfuse_template} - {str(err)} - attempting to load from promptConfig")
79
+
80
+ return config.promptConfig(key)
@@ -106,7 +106,6 @@ class ContentBuffer:
106
106
  """
107
107
  async with self.lock:
108
108
  self.content = ""
109
- log.debug("Content buffer cleared")
110
109
  self.content_available.clear()
111
110
 
112
111
 
@@ -0,0 +1,63 @@
1
+ import os
2
+
3
+ def guess_mime_type(file_path: str) -> str:
4
+ """
5
+ Guess the mime type based on the file extension.
6
+
7
+ Args:
8
+ file_path (str): The path or URL of the image file.
9
+
10
+ Returns:
11
+ str: The guessed image type (e.g., "jpeg", "png", "gif", etc.)
12
+ or None if the extension is not recognized.
13
+ """
14
+ # Extract the file extension
15
+ _, ext = os.path.splitext(file_path)
16
+
17
+ # Normalize and remove the leading dot
18
+ ext = ext.lower().strip('.')
19
+
20
+ # Mapping of common file extensions to file types
21
+ extension_to_type = {
22
+ "jpg": "image/jpeg",
23
+ "jpeg": "image/jpeg",
24
+ "png": "image/png",
25
+ "gif": "image/gif",
26
+ "bmp": "image/bmp",
27
+ "tiff": "image/tiff",
28
+ "tif": "image/tiff",
29
+ "webp": "image/webp",
30
+ "ico": "image/vnd.microsoft.icon",
31
+ "svg": "image/svg+xml",
32
+ "pdf": "application/pdf",
33
+ "txt": "text/plain",
34
+ "md": "text/markdown",
35
+ "html": "text/html",
36
+ "css": "text/css",
37
+ "js": "application/javascript",
38
+ "json": "application/json",
39
+ "xml": "application/xml",
40
+ "csv": "text/csv",
41
+ "py": "text/x-python",
42
+ "java": "text/x-java-source",
43
+ "c": "text/x-c",
44
+ "cpp": "text/x-c++",
45
+ "h": "text/x-c",
46
+ "hpp": "text/x-c++",
47
+ "sh": "application/x-sh",
48
+ "bat": "application/x-msdos-program",
49
+ "php": "application/x-httpd-php",
50
+ "rb": "application/x-ruby",
51
+ "pl": "application/x-perl",
52
+ "swift": "application/x-swift",
53
+ "r": "text/x-r",
54
+ "go": "text/x-go",
55
+ "sql": "application/sql",
56
+ "yaml": "text/yaml",
57
+ "yml": "text/yaml",
58
+ "ts": "application/typescript",
59
+ "tsx": "text/tsx",
60
+ "jsx": "text/jsx",
61
+ }
62
+
63
+ return extension_to_type.get(ext, "")
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.100.3
3
+ Version: 0.101.2
4
4
  Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
5
5
  Home-page: https://github.com/sunholo-data/sunholo-py
6
- Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.100.3.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.101.2.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -94,8 +94,10 @@ sunholo/gcs/__init__.py
94
94
  sunholo/gcs/add_file.py
95
95
  sunholo/gcs/download_folder.py
96
96
  sunholo/gcs/download_url.py
97
+ sunholo/gcs/extract_and_sign.py
97
98
  sunholo/gcs/metadata.py
98
99
  sunholo/genai/__init__.py
100
+ sunholo/genai/images.py
99
101
  sunholo/genai/init.py
100
102
  sunholo/genai/process_funcs_cls.py
101
103
  sunholo/genai/safety.py
@@ -143,6 +145,7 @@ sunholo/utils/config_class.py
143
145
  sunholo/utils/config_schema.py
144
146
  sunholo/utils/gcp.py
145
147
  sunholo/utils/gcp_project.py
148
+ sunholo/utils/mime.py
146
149
  sunholo/utils/parsers.py
147
150
  sunholo/utils/timedelta.py
148
151
  sunholo/utils/user_ids.py
@@ -155,5 +158,6 @@ sunholo/vertex/init.py
155
158
  sunholo/vertex/memory_tools.py
156
159
  sunholo/vertex/safety.py
157
160
  sunholo/vertex/type_dict_to_json.py
161
+ tests/test_async.py
158
162
  tests/test_chat_history.py
159
163
  tests/test_config.py
@@ -0,0 +1,93 @@
1
+ import asyncio
2
+ from sunholo.invoke import AsyncTaskRunner
3
+ # Mock logger for demonstration
4
+ import logging
5
+ logging.basicConfig(level=logging.INFO)
6
+ log = logging.getLogger(__name__)
7
+
8
+
9
+ # Dummy task functions for testing
10
+ async def google_search(query, config_manager):
11
+ await asyncio.sleep(5) # Simulate a long-running task
12
+ return f"Results for '{query}'"
13
+
14
+ async def use_pdfs(query, arg2, arg3):
15
+ await asyncio.sleep(2) # Simulate a medium-running task
16
+ return "No image_uri specified\n"
17
+
18
+ async def process_urls(url):
19
+ await asyncio.sleep(1) # Simulate a short-running task
20
+ return "No URLs were found"
21
+
22
+ # Mock callback for demonstration
23
+ class MockCallback:
24
+ async def async_on_llm_new_token(self, token):
25
+ log.info(f"Callback received token: {token}")
26
+
27
+ # Mock functions for demonstration
28
+ def format_token_output(func_name, tokens, result=None, error=None):
29
+ if error:
30
+ return f"Error in {func_name}: {error}"
31
+ return f"{func_name} completed with {tokens} tokens. Result: {result}"
32
+
33
+ async def count_tokens(result):
34
+ return len(result.split())
35
+
36
+ # Example usage
37
+ async def main():
38
+ runner = AsyncTaskRunner(retry_enabled=True)
39
+
40
+ # Add your tasks
41
+ runner.add_task(google_search, "<original user question>...", "<config>")
42
+ runner.add_task(use_pdfs, "<original user question>...", None, None)
43
+ runner.add_task(process_urls, "please give me a forecast of ppas for spain using wind energy")
44
+
45
+ callback = MockCallback()
46
+ answers = {}
47
+ total_context_tokens = 0
48
+
49
+ async for message in runner.run_async_as_completed():
50
+ log.info(f"Runner message={message}")
51
+ if message['type'] == 'heartbeat':
52
+ func_name = message['name']
53
+ elapsed_time = message['elapsed_time']
54
+ log.info(f"Runner Heartbeat for {func_name}, elapsed_time={elapsed_time}")
55
+ # Send heartbeat to callback
56
+ update_html = (
57
+ f'<div style="display: none;" data-update-id="{func_name}-spinner">'
58
+ f'<span class="elapsed-time">{elapsed_time} seconds elapsed</span>'
59
+ f'</div>'
60
+ )
61
+ await callback.async_on_llm_new_token(token=f"[[HEARTBEAT]]{update_html}[[/HEARTBEAT]]")
62
+ elif message['type'] == 'task_complete':
63
+ func_name = message['func_name']
64
+ result = message['result']
65
+ log.info(f"Runner completed task: {func_name}")
66
+ # Process result
67
+ if isinstance(result, Exception):
68
+ log.info(f"Error Exception for {func_name}: {str(result)}")
69
+ formatted_output = format_token_output(func_name, 0, error="No results")
70
+ await callback.async_on_llm_new_token(token=formatted_output)
71
+ else:
72
+ # Stream the result to the callback
73
+ tokens = await count_tokens(result)
74
+ total_context_tokens += tokens
75
+ log.info(f"Got task {func_name} to stream result length [{len(result)}] [{tokens} tokens]")
76
+ formatted_output = format_token_output(func_name, tokens, result=result)
77
+ await callback.async_on_llm_new_token(token=formatted_output)
78
+ answers[func_name] = result
79
+ elif message['type'] == 'task_error':
80
+ func_name = message['func_name']
81
+ error = message['error']
82
+ log.info(f"Error Exception for {func_name}: {str(error)}")
83
+ formatted_output = format_token_output(func_name, 0, error="No results")
84
+ await callback.async_on_llm_new_token(token=formatted_output)
85
+
86
+ await callback.async_on_llm_new_token(
87
+ token=f'<div style="margin-top: 20px; color: #333;"><strong>-- Using [{total_context_tokens}] tokens in context for final answer --</strong></div>'
88
+ )
89
+ log.info("All tasks have been processed.")
90
+
91
+ # Run the main coroutine
92
+ if __name__ == "__main__":
93
+ asyncio.run(main())
@@ -1,44 +0,0 @@
1
- from ..custom_logging import log
2
- from ..utils import ConfigManager
3
-
4
- # Load the YAML file
5
- def load_prompt_from_yaml(key, prefix="sunholo", load_from_file=False):
6
- """
7
- Returns a string you can use with Langfuse PromptTemplate.from_template()
8
-
9
- Will first try to load from the Langfuse prompt library, if unavailable will look in promptConfig type file.
10
-
11
- Langfuse prompts have {{ two braces }}, Langchain prompts have { one brace }.
12
-
13
- Example:
14
-
15
- ```python
16
- from sunholo.langfuse.prompts import load_prompt_from_yaml
17
- from langchain_core.prompts import PromptTemplate
18
-
19
- """
20
- config = ConfigManager(prefix)
21
- if load_from_file:
22
-
23
- return config.promptConfig(key)
24
-
25
-
26
- from langfuse import Langfuse
27
-
28
- # Initialize Langfuse client
29
- langfuse = Langfuse()
30
-
31
- try:
32
- if prefix is None:
33
- langfuse_template = key
34
- else:
35
- langfuse_template = f"{prefix}-{key}"
36
-
37
- langfuse_prompt = langfuse.get_prompt(langfuse_template, cache_ttl_seconds=300)
38
-
39
- return langfuse_prompt.get_langchain_prompt()
40
-
41
- except Exception as err:
42
- log.warning(f"Could not find langfuse template: {langfuse_template} - {str(err)} - attempting to load from promptConfig")
43
-
44
- return config.promptConfig(key)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes