sunholo 0.100.0__tar.gz → 0.100.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 (161) hide show
  1. {sunholo-0.100.0 → sunholo-0.100.2}/PKG-INFO +2 -2
  2. {sunholo-0.100.0 → sunholo-0.100.2}/setup.py +1 -1
  3. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/auth/refresh.py +2 -2
  4. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/gcs/add_file.py +0 -2
  5. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/gcs/download_url.py +8 -1
  6. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/genai/process_funcs_cls.py +1 -2
  7. sunholo-0.100.2/sunholo/invoke/async_class.py +117 -0
  8. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/streaming/content_buffer.py +4 -4
  9. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/streaming/streaming.py +1 -1
  10. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo.egg-info/PKG-INFO +2 -2
  11. sunholo-0.100.0/sunholo/invoke/async_class.py +0 -134
  12. {sunholo-0.100.0 → sunholo-0.100.2}/LICENSE.txt +0 -0
  13. {sunholo-0.100.0 → sunholo-0.100.2}/MANIFEST.in +0 -0
  14. {sunholo-0.100.0 → sunholo-0.100.2}/README.md +0 -0
  15. {sunholo-0.100.0 → sunholo-0.100.2}/setup.cfg +0 -0
  16. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/__init__.py +0 -0
  17. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/__init__.py +0 -0
  18. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/chat_history.py +0 -0
  19. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/dispatch_to_qa.py +0 -0
  20. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/fastapi/__init__.py +0 -0
  21. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/fastapi/base.py +0 -0
  22. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/fastapi/qna_routes.py +0 -0
  23. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/flask/__init__.py +0 -0
  24. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/flask/base.py +0 -0
  25. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/flask/qna_routes.py +0 -0
  26. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/flask/vac_routes.py +0 -0
  27. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/langserve.py +0 -0
  28. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/pubsub.py +0 -0
  29. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/route.py +0 -0
  30. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/special_commands.py +0 -0
  31. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/agents/swagger.py +0 -0
  32. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/archive/__init__.py +0 -0
  33. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/archive/archive.py +0 -0
  34. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/auth/__init__.py +0 -0
  35. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/auth/gcloud.py +0 -0
  36. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/auth/run.py +0 -0
  37. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/azure/__init__.py +0 -0
  38. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/azure/auth.py +0 -0
  39. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/azure/blobs.py +0 -0
  40. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/azure/event_grid.py +0 -0
  41. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/bots/__init__.py +0 -0
  42. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/bots/discord.py +0 -0
  43. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/bots/github_webhook.py +0 -0
  44. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/bots/webapp.py +0 -0
  45. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/__init__.py +0 -0
  46. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/azure.py +0 -0
  47. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/doc_handling.py +0 -0
  48. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/encode_metadata.py +0 -0
  49. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/images.py +0 -0
  50. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/loaders.py +0 -0
  51. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/message_data.py +0 -0
  52. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/pdfs.py +0 -0
  53. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/process_chunker_data.py +0 -0
  54. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/publish.py +0 -0
  55. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/pubsub.py +0 -0
  56. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/chunker/splitter.py +0 -0
  57. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/__init__.py +0 -0
  58. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/chat_vac.py +0 -0
  59. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/cli.py +0 -0
  60. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/cli_init.py +0 -0
  61. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/configs.py +0 -0
  62. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/deploy.py +0 -0
  63. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/embedder.py +0 -0
  64. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/merge_texts.py +0 -0
  65. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/run_proxy.py +0 -0
  66. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/sun_rich.py +0 -0
  67. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/swagger.py +0 -0
  68. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/cli/vertex.py +0 -0
  69. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/components/__init__.py +0 -0
  70. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/components/llm.py +0 -0
  71. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/components/retriever.py +0 -0
  72. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/components/vectorstore.py +0 -0
  73. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/custom_logging.py +0 -0
  74. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/__init__.py +0 -0
  75. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/alloydb.py +0 -0
  76. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/alloydb_client.py +0 -0
  77. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/database.py +0 -0
  78. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/lancedb.py +0 -0
  79. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/sql/sb/create_function.sql +0 -0
  80. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/sql/sb/create_function_time.sql +0 -0
  81. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/sql/sb/create_table.sql +0 -0
  82. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  83. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/sql/sb/return_sources.sql +0 -0
  84. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/sql/sb/setup.sql +0 -0
  85. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/static_dbs.py +0 -0
  86. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/database/uuid.py +0 -0
  87. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/discovery_engine/__init__.py +0 -0
  88. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/discovery_engine/chunker_handler.py +0 -0
  89. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/discovery_engine/create_new.py +0 -0
  90. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  91. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/discovery_engine/get_ai_search_chunks.py +0 -0
  92. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/embedder/__init__.py +0 -0
  93. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/embedder/embed_chunk.py +0 -0
  94. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/excel/__init__.py +0 -0
  95. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/excel/plugin.py +0 -0
  96. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/gcs/__init__.py +0 -0
  97. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/gcs/download_folder.py +0 -0
  98. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/gcs/metadata.py +0 -0
  99. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/genai/__init__.py +0 -0
  100. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/genai/init.py +0 -0
  101. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/genai/safety.py +0 -0
  102. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/invoke/__init__.py +0 -0
  103. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/invoke/direct_vac_func.py +0 -0
  104. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/invoke/invoke_vac_utils.py +0 -0
  105. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/langfuse/__init__.py +0 -0
  106. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/langfuse/callback.py +0 -0
  107. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/langfuse/evals.py +0 -0
  108. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/langfuse/prompts.py +0 -0
  109. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/llamaindex/__init__.py +0 -0
  110. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/llamaindex/get_files.py +0 -0
  111. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/llamaindex/import_files.py +0 -0
  112. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/llamaindex/llamaindex_class.py +0 -0
  113. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/llamaindex/user_history.py +0 -0
  114. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/lookup/__init__.py +0 -0
  115. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/lookup/model_lookup.yaml +0 -0
  116. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/patches/__init__.py +0 -0
  117. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/patches/langchain/__init__.py +0 -0
  118. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/patches/langchain/lancedb.py +0 -0
  119. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/patches/langchain/vertexai.py +0 -0
  120. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/pubsub/__init__.py +0 -0
  121. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/pubsub/process_pubsub.py +0 -0
  122. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/pubsub/pubsub_manager.py +0 -0
  123. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/qna/__init__.py +0 -0
  124. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/qna/parsers.py +0 -0
  125. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/qna/retry.py +0 -0
  126. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/streaming/__init__.py +0 -0
  127. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/streaming/langserve.py +0 -0
  128. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/streaming/stream_lookup.py +0 -0
  129. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/summarise/__init__.py +0 -0
  130. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/summarise/summarise.py +0 -0
  131. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/terraform/__init__.py +0 -0
  132. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/terraform/tfvars_editor.py +0 -0
  133. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/tools/__init__.py +0 -0
  134. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/tools/web_browser.py +0 -0
  135. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/__init__.py +0 -0
  136. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/api_key.py +0 -0
  137. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/big_context.py +0 -0
  138. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/config.py +0 -0
  139. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/config_class.py +0 -0
  140. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/config_schema.py +0 -0
  141. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/gcp.py +0 -0
  142. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/gcp_project.py +0 -0
  143. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/parsers.py +0 -0
  144. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/timedelta.py +0 -0
  145. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/user_ids.py +0 -0
  146. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/utils/version.py +0 -0
  147. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/__init__.py +0 -0
  148. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/extensions_call.py +0 -0
  149. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/extensions_class.py +0 -0
  150. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/genai_functions.py +0 -0
  151. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/init.py +0 -0
  152. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/memory_tools.py +0 -0
  153. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/safety.py +0 -0
  154. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo/vertex/type_dict_to_json.py +0 -0
  155. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo.egg-info/SOURCES.txt +0 -0
  156. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo.egg-info/dependency_links.txt +0 -0
  157. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo.egg-info/entry_points.txt +0 -0
  158. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo.egg-info/requires.txt +0 -0
  159. {sunholo-0.100.0 → sunholo-0.100.2}/sunholo.egg-info/top_level.txt +0 -0
  160. {sunholo-0.100.0 → sunholo-0.100.2}/tests/test_chat_history.py +0 -0
  161. {sunholo-0.100.0 → sunholo-0.100.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.0
3
+ Version: 0.100.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.0.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.100.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.0'
3
+ version = '0.100.2'
4
4
 
5
5
  setup(
6
6
  name='sunholo',
@@ -14,7 +14,7 @@ def get_default_email():
14
14
 
15
15
  if gcs_credentials is None:
16
16
  log.error("Could not refresh the credentials properly.")
17
- return None
17
+ return None, None
18
18
 
19
19
  service_account_email = getattr(gcs_credentials, 'service_account_email', None)
20
20
  # If you use a service account credential, you can use the embedded email
@@ -23,7 +23,7 @@ def get_default_email():
23
23
  if not service_account_email:
24
24
  log.error("Could not create the credentials for signed requests - no credentials.service_account_email or GCS_MAIL_USER with roles/iam.serviceAccountTokenCreator")
25
25
 
26
- return None
26
+ return None, None
27
27
 
28
28
  log.info(f"Found default email: {service_account_email=} for {project_id=}")
29
29
  return service_account_email, gcs_credentials
@@ -182,8 +182,6 @@ def add_file_to_gcs(filename: str,
182
182
 
183
183
  blob.metadata = the_metadata
184
184
 
185
-
186
-
187
185
  max_retries = 5
188
186
  base_delay = 1 # 1 second
189
187
  for attempt in range(max_retries):
@@ -106,7 +106,14 @@ def sign_gcs_url(bucket_name:str, object_name:str, expiry_secs:int = 86400) -> O
106
106
  Returns:
107
107
  str: The signed URL or None if not avialable
108
108
  """
109
- service_account_email, gcs_credentials = get_default_email()
109
+ result = get_default_email()
110
+
111
+ # Check if the result is None
112
+ if result is None or any(item is None for item in result):
113
+ log.error("Failed to retrieve the service account email and credentials.")
114
+ return None
115
+
116
+ service_account_email, gcs_credentials = result
110
117
 
111
118
  expires = datetime.now() + timedelta(seconds=expiry_secs)
112
119
 
@@ -262,8 +262,7 @@ class GenAIFunctionProcessor:
262
262
  # Execute the function with the provided parameters
263
263
  result = fn_exec(**params_obj)
264
264
  log.info(f"Got result from {function_name}: {result} of type: {type(result)}")
265
- if not isinstance(result, str):
266
- log.warning(f"Tool functions should return strings: {function_name} returned type: {type(result)}")
265
+ #TODO: return images
267
266
  else:
268
267
  fn_result = type(fn).to_dict(fn)
269
268
  result = fn_result.get("result")
@@ -0,0 +1,117 @@
1
+ import asyncio
2
+ from ..custom_logging import log
3
+ import traceback
4
+ from typing import Callable, Any, AsyncGenerator, Dict
5
+ from tenacity import AsyncRetrying, retry_if_exception_type, wait_random_exponential, stop_after_attempt
6
+
7
+ class AsyncTaskRunner:
8
+ def __init__(self, retry_enabled=False, retry_kwargs=None):
9
+ self.tasks = []
10
+ self.retry_enabled = retry_enabled
11
+ self.retry_kwargs = retry_kwargs or {}
12
+
13
+ def add_task(self, func: Callable[..., Any], *args: Any):
14
+ """Adds a task to the list of tasks to be executed."""
15
+ log.info(f"Adding task: {func.__name__} with args: {args}")
16
+ self.tasks.append((func.__name__, func, args))
17
+
18
+ async def run_async_as_completed(self, callback=None) -> AsyncGenerator[Dict[str, Any], None]:
19
+ """
20
+ Runs all tasks concurrently and yields results as they complete, while periodically sending heartbeat messages.
21
+
22
+ Args:
23
+ callback: The callback object that will receive heartbeat messages.
24
+ """
25
+ log.info("Running tasks asynchronously and yielding results as they complete")
26
+ tasks = {}
27
+ for name, func, args in self.tasks:
28
+ # Pass the callback down to _task_wrapper
29
+ coro = self._task_wrapper(name, func, args, callback)
30
+ task = asyncio.create_task(coro)
31
+ tasks[task] = name
32
+
33
+ log.info(f"Start async run with {len(self.tasks)} runners")
34
+ while tasks:
35
+ done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED)
36
+ for task in done:
37
+ name = tasks.pop(task)
38
+ try:
39
+ # func_name = message['func_name']; result = message['result']
40
+ result = await task
41
+ yield {'type':'task_complete', 'func_name': name, 'result': result}
42
+ except Exception as e:
43
+ log.error(f"Task {name} resulted in an error: {e}\n{traceback.format_exc()}")
44
+ yield {'type':'task_error', 'func_name': name, 'error': f'{e}\n{traceback.format_exc()}'}
45
+
46
+ async def _task_wrapper(self, name: str, func: Callable[..., Any], args: Any, callback=None) -> Any:
47
+ """Wraps the task function to process its output and handle retries, while managing heartbeat updates."""
48
+ async def run_func():
49
+ if asyncio.iscoroutinefunction(func):
50
+ # If the function is async, await it
51
+ return await func(*args)
52
+ else:
53
+ # If the function is sync, run it in a thread to prevent blocking
54
+ return await asyncio.to_thread(func, *args)
55
+
56
+ # Start the heartbeat task if a callback is provided
57
+ heartbeat_task = None
58
+ if callback:
59
+ heartbeat_task = asyncio.create_task(self._send_heartbeat(callback, name))
60
+
61
+ try:
62
+ if self.retry_enabled:
63
+ retry_kwargs = {
64
+ 'wait': wait_random_exponential(multiplier=1, max=60),
65
+ 'stop': stop_after_attempt(5),
66
+ 'retry': retry_if_exception_type(Exception),
67
+ **self.retry_kwargs
68
+ }
69
+ async for attempt in AsyncRetrying(**retry_kwargs):
70
+ with attempt:
71
+ return await run_func()
72
+ else:
73
+ try:
74
+ return await run_func()
75
+ except Exception as e:
76
+ log.error(f"Error in task {name}: {e}\n{traceback.format_exc()}")
77
+ raise
78
+ finally:
79
+ # Stop the heartbeat task
80
+ if heartbeat_task:
81
+ heartbeat_task.cancel()
82
+ try:
83
+ await heartbeat_task # Ensure the heartbeat task is properly canceled
84
+ except asyncio.CancelledError:
85
+ pass
86
+
87
+ # Send a message indicating task completion and remove spinner
88
+ if callback:
89
+ completion_html = (
90
+ f"<script>"
91
+ f"document.getElementById('{name}-spinner').innerHTML = '✔️ Task {name} completed!';"
92
+ f"</script>"
93
+ )
94
+ await callback.async_on_llm_new_token(token=completion_html)
95
+
96
+ async def _send_heartbeat(self, callback, func_name, interval=2):
97
+ """
98
+ Sends periodic heartbeat updates to indicate the task is still in progress.
99
+
100
+ Args:
101
+ callback: The callback to notify that the task is still working.
102
+ func_name: The name of the task function.
103
+ interval: How frequently to send heartbeat messages (in seconds).
104
+ """
105
+ # Send the initial spinner HTML
106
+ spinner_html = (
107
+ f'<div id="{func_name}-spinner" style="display:inline-block; margin: 5px;">'
108
+ f'<div class="spinner" style="width:16px; height:16px; border: 2px solid #f3f3f3; '
109
+ f'border-radius: 50%; border-top: 2px solid #3498db; animation: spin 1s linear infinite;"></div>'
110
+ f'<style>@keyframes spin {{0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }}}}</style>'
111
+ f' <span>Task {func_name} is in progress...</span></div>'
112
+ )
113
+ await callback.async_on_llm_new_token(token=spinner_html)
114
+
115
+ # Keep sending heartbeats until task completes
116
+ while True:
117
+ await asyncio.sleep(interval) # Sleep for the interval but do not send multiple messages
@@ -173,13 +173,13 @@ class BufferStreamingStdOutCallbackHandler(StreamingStdOutCallbackHandler):
173
173
  The buffer content is written to the content buffer when appropriate tokens or
174
174
  patterns are detected.
175
175
  """
176
- log.debug(f"on_llm_new_token: {token}")
176
+ #log.debug(f"on_llm_new_token: {token}")
177
177
 
178
178
  # Check if the token is a heartbeat message
179
179
  if self._is_heartbeat_token(token):
180
180
  # Strip the [[HEARTBEAT]] markers and write immediately
181
181
  heartbeat_content = self._strip_heartbeat_markers(token)
182
- log.info(f"Heartbeat token detected, writing immediately: {heartbeat_content}")
182
+ #log.info(f"Heartbeat token detected, writing immediately: {heartbeat_content}")
183
183
  self.content_buffer.write(heartbeat_content)
184
184
  else:
185
185
  self.buffer += token
@@ -268,13 +268,13 @@ class BufferStreamingStdOutCallbackHandlerAsync(StreamingStdOutCallbackHandler):
268
268
  log.info("Starting to stream LLM")
269
269
 
270
270
  async def async_on_llm_new_token(self, token: str, **kwargs: Any) -> None:
271
- log.debug(f"async_on_llm_new_token: {token}")
271
+ #log.debug(f"async_on_llm_new_token: {token}")
272
272
 
273
273
  # Check if the token is a heartbeat message
274
274
  if self._is_heartbeat_token(token):
275
275
  # Strip the [[HEARTBEAT]] markers and write immediately
276
276
  heartbeat_content = self._strip_heartbeat_markers(token)
277
- log.info(f"Heartbeat token detected, writing immediately: {heartbeat_content}")
277
+ #log.info(f"Heartbeat token detected, writing immediately: {heartbeat_content}")
278
278
  await self.content_buffer.async_write(heartbeat_content)
279
279
  else:
280
280
  self.buffer += token
@@ -194,7 +194,7 @@ async def start_streaming_chat_async(question, vector_name, qna_func_async, chat
194
194
 
195
195
  content_to_send = await content_buffer.async_read()
196
196
  if content_to_send:
197
- log.info(f"Content to send: {content_to_send}")
197
+ log.info(f"==Async\n{content_to_send}")
198
198
  yield content_to_send
199
199
  await content_buffer.async_clear()
200
200
  else:
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.100.0
3
+ Version: 0.100.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.0.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.100.2.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -1,134 +0,0 @@
1
- import asyncio
2
- from ..custom_logging import log
3
- import traceback
4
- from typing import Callable, Any, AsyncGenerator, Dict
5
- from tenacity import AsyncRetrying, retry_if_exception_type, wait_random_exponential, stop_after_attempt
6
-
7
- class AsyncTaskRunner:
8
- def __init__(self, retry_enabled=False, retry_kwargs=None):
9
- self.tasks = []
10
- self.retry_enabled = retry_enabled
11
- self.retry_kwargs = retry_kwargs or {}
12
-
13
- def add_task(self, func: Callable[..., Any], *args: Any):
14
- """Adds a task to the list of tasks to be executed."""
15
- log.info(f"Adding task: {func.__name__} with args: {args}")
16
- self.tasks.append((func.__name__, func, args))
17
-
18
- async def run_async_as_completed(self) -> AsyncGenerator[Dict[str, Any], None]:
19
- """
20
- Runs all tasks concurrently and yields results and heartbeats as they are produced.
21
- """
22
- log.info("Running tasks asynchronously and yielding results and heartbeats as they occur")
23
- queue = asyncio.Queue()
24
-
25
- tasks = {}
26
- for name, func, args in self.tasks:
27
- coro = self._task_wrapper(name, func, args, queue)
28
- task = asyncio.create_task(coro)
29
- tasks[task] = name
30
-
31
- while tasks or not queue.empty():
32
- if not queue.empty():
33
- message = await queue.get()
34
- log.info(f"Found queue message: {message}")
35
- yield message
36
- else:
37
- # Wait for either a message in the queue or a task to complete
38
- done, _ = await asyncio.wait(
39
- list(tasks.keys()),
40
- timeout=0.1,
41
- return_when=asyncio.FIRST_COMPLETED
42
- )
43
- for task in done:
44
- name = tasks.pop(task)
45
- try:
46
- result = await task
47
- await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
48
- except Exception as e:
49
- log.error(f"Task {name} resulted in an error: {e}\n{traceback.format_exc()}")
50
- await queue.put({'type': 'task_error', 'func_name': name, 'error': e})
51
-
52
- # After all tasks have completed, process any remaining messages in the queue
53
- while not queue.empty():
54
- message = await queue.get()
55
- yield message
56
-
57
- async def _task_wrapper(self, name: str, func: Callable[..., Any], args: Any, queue: asyncio.Queue) -> Any:
58
- """Wraps the task function to process its output and handle retries, while managing heartbeat updates."""
59
- async def run_func():
60
- if asyncio.iscoroutinefunction(func):
61
- # If the function is async, await it
62
- return await func(*args)
63
- else:
64
- # If the function is sync, run it in a thread to prevent blocking
65
- return await asyncio.to_thread(func, *args)
66
-
67
- # Start the heartbeat task
68
- heartbeat_task = asyncio.create_task(self._send_heartbeat(queue, name))
69
-
70
- try:
71
- if self.retry_enabled:
72
- retry_kwargs = {
73
- 'wait': wait_random_exponential(multiplier=1, max=60),
74
- 'stop': stop_after_attempt(5),
75
- 'retry': retry_if_exception_type(Exception),
76
- **self.retry_kwargs
77
- }
78
- async for attempt in AsyncRetrying(**retry_kwargs):
79
- with attempt:
80
- return await run_func()
81
- else:
82
- try:
83
- return await run_func()
84
- except Exception as e:
85
- log.error(f"Error in task {name}: {e}\n{traceback.format_exc()}")
86
- raise
87
- finally:
88
- # Stop the heartbeat task
89
- heartbeat_task.cancel()
90
- # Let the heartbeat_task finish in the background without awaiting it
91
- try:
92
- await asyncio.shield(heartbeat_task) # Ensure cancellation is handled cleanly
93
- except asyncio.CancelledError:
94
- pass
95
-
96
- # Send a message indicating task completion to update the spinner's state
97
- completion_html = (
98
- f'<div style="display: none;" data-complete-id="{name}-spinner"></div>'
99
- )
100
- await queue.put({'type': 'heartbeat', 'func_name': name, 'token': completion_html})
101
-
102
- async def _send_heartbeat(self, queue: asyncio.Queue, func_name: str, interval=2):
103
- """
104
- Sends a periodic heartbeat to keep the task alive and update the spinner with elapsed time.
105
- """
106
- # Send the initial spinner HTML
107
- spinner_html = (
108
- f'<div id="{func_name}-spinner" class="spinner-container">'
109
- f' <div class="spinner"></div>'
110
- f' <span class="elapsed-time">Task {func_name} is still running... 0s elapsed</span>'
111
- f'</div>'
112
- )
113
- log.info(f"Heartbeat started for task {func_name}")
114
-
115
- await queue.put({'type': 'heartbeat', 'func_name': func_name, 'token': spinner_html})
116
-
117
- # Keep track of elapsed time
118
- elapsed_time = 0
119
-
120
- # Keep sending heartbeats until task completes
121
- while True:
122
- try:
123
- await asyncio.sleep(interval) # Sleep for the interval
124
- elapsed_time += interval # Increment elapsed time
125
- log.info(f"Sending heartbeat for {func_name}: {elapsed_time}s elapsed")
126
- # Update spinner with the elapsed time
127
- update_html = (
128
- f'<div style="display: none;" data-update-id="{func_name}-spinner">'
129
- f'<span class="elapsed-time">Task {func_name} is still running... {elapsed_time}s elapsed</span>'
130
- f'</div>'
131
- )
132
- await queue.put({'type': 'heartbeat', 'func_name': func_name, 'token': update_html})
133
- except asyncio.CancelledError:
134
- break # Exit the loop if the heartbeat task is canceled
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes