sunholo 0.100.2__py3-none-any.whl → 0.101.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sunholo/gcs/add_file.py CHANGED
@@ -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
@@ -17,9 +17,13 @@ try:
17
17
  except ImportError:
18
18
  genai = None
19
19
 
20
+ from .images import extract_gs_images_and_genai_upload
21
+
20
22
  if TYPE_CHECKING:
21
23
  from google.generativeai.protos import Part
22
24
 
25
+
26
+
23
27
  class GenAIFunctionProcessor:
24
28
  """
25
29
  A generic class for processing function calls from google.generativeai function calling models.
@@ -366,20 +370,22 @@ class GenAIFunctionProcessor:
366
370
 
367
371
  return clean
368
372
 
369
- def run_agent_loop(self, chat, content, callback, guardrail_max=10, loop_return=3):
373
+ def run_agent_loop(self, chat, content, callback=None, guardrail_max=10, loop_return=3):
370
374
  """
371
375
  Runs the agent loop, sending messages to the orchestrator, processing responses, and executing functions.
372
376
 
373
377
  Args:
374
378
  chat: The chat object for interaction with the orchestrator.
375
379
  content: The initial content to send to the agent.
376
- callback: The callback object for handling intermediate responses.
380
+ callback: The callback object for handling intermediate responses. If not supplied will use self.IOCallback()
377
381
  guardrail_max (int): The maximum number of iterations for the loop.
378
382
  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
383
 
380
384
  Returns:
381
385
  tuple: (big_text, usage_metadata) from the loop execution.
382
386
  """
387
+ if not callback:
388
+ callback = self.IOCallback()
383
389
  guardrail = 0
384
390
  big_result = []
385
391
  usage_metadata = {
@@ -535,6 +541,13 @@ class GenAIFunctionProcessor:
535
541
 
536
542
  if this_text:
537
543
  content.append(f"Agent: {this_text}")
544
+ # if text includes gs:// try to download it
545
+ image_uploads = extract_gs_images_and_genai_upload(this_text)
546
+ if image_uploads:
547
+ for img in image_uploads:
548
+ log.info(f"Adding {img=}")
549
+ content.append(img)
550
+ content.append(f"{img.name} was created by agent and added")
538
551
  log.info(f"[{guardrail}] Updated content:\n{this_text}")
539
552
  big_result.append(this_text)
540
553
  else:
@@ -567,6 +580,15 @@ class GenAIFunctionProcessor:
567
580
 
568
581
  return big_text, usage_metadata
569
582
 
583
+ class IOCallback:
584
+ """
585
+ This is a default callback that will print to console any tokens it recieves.
586
+ """
587
+ def on_llm_new_token(self, token:str):
588
+ print(token)
589
+ def on_llm_end(self, response):
590
+ print(f"\nFull response: \n{response}")
591
+
570
592
  @staticmethod
571
593
  def decide_to_go_on(go_on: bool, chat_summary: str) -> dict:
572
594
  """
@@ -1,63 +1,107 @@
1
1
  import asyncio
2
- from ..custom_logging import log
3
- import traceback
4
2
  from typing import Callable, Any, AsyncGenerator, Dict
3
+ import time
4
+ import traceback
5
+ from ..custom_logging import setup_logging
5
6
  from tenacity import AsyncRetrying, retry_if_exception_type, wait_random_exponential, stop_after_attempt
6
7
 
8
+ log = setup_logging("sunholo_AsyncTaskRunner")
9
+
7
10
  class AsyncTaskRunner:
8
11
  def __init__(self, retry_enabled=False, retry_kwargs=None):
9
12
  self.tasks = []
10
13
  self.retry_enabled = retry_enabled
11
14
  self.retry_kwargs = retry_kwargs or {}
12
-
15
+
13
16
  def add_task(self, func: Callable[..., Any], *args: Any):
14
- """Adds a task to the list of tasks to be executed."""
17
+ """
18
+ Adds a task to the list of tasks to be executed.
19
+ """
15
20
  log.info(f"Adding task: {func.__name__} with args: {args}")
16
21
  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
 
22
- Args:
23
- callback: The callback object that will receive heartbeat messages.
23
+ async def run_async_as_completed(self) -> AsyncGenerator[Dict[str, Any], None]:
24
+ """
25
+ Runs all tasks concurrently and yields results as they complete,
26
+ while periodically sending heartbeat messages.
24
27
  """
25
28
  log.info("Running tasks asynchronously and yielding results as they complete")
26
- tasks = {}
29
+
30
+ # Create a queue for inter-coroutine communication
31
+ queue = asyncio.Queue()
32
+
33
+ # List to keep track of all running tasks and their heartbeats
34
+ task_infos = []
35
+
36
+ # Start all tasks and their corresponding heartbeats
27
37
  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)
38
+ # Create an event to signal task completion to the heartbeat
39
+ completion_event = asyncio.Event()
40
+
41
+ # Start the main task with retries
42
+ task_coro = self._run_with_retries(name, func, *args, queue=queue, completion_event=completion_event)
43
+ task = asyncio.create_task(task_coro)
44
+
45
+ # Start the heartbeat coroutine
46
+ heartbeat_coro = self._send_heartbeat(name, completion_event, queue)
47
+ heartbeat_task = asyncio.create_task(heartbeat_coro)
48
+
49
+ # Store task information for management
50
+ task_infos.append({
51
+ 'name': name,
52
+ 'task': task,
53
+ 'heartbeat_task': heartbeat_task,
54
+ 'completion_event': completion_event
55
+ })
56
+
57
+ log.info(f"Started task '{name}' and its heartbeat")
58
+
59
+ log.info(f"Started async run with {len(self.tasks)} tasks and heartbeats")
60
+
61
+ # Create a monitor task to detect when all tasks and heartbeats are done
62
+ monitor = asyncio.create_task(self._monitor_tasks(task_infos, queue))
63
+
64
+ # Continuously yield messages from the queue until sentinel is received
65
+ while True:
66
+ message = await queue.get()
67
+ if message is None:
68
+ log.info("Received sentinel. Exiting message loop.")
69
+ break # Sentinel received, all tasks and heartbeats are done
70
+ log.info(f"Received message from queue: {message}")
71
+ yield message
55
72
 
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))
73
+ # Wait for the monitor to finish
74
+ await monitor
60
75
 
76
+ log.info("All tasks and heartbeats have completed")
77
+
78
+ async def _monitor_tasks(self, task_infos, queue):
79
+ """
80
+ Monitors the tasks and heartbeats, and sends a sentinel to the queue when done.
81
+ """
82
+ # Wait for all main tasks to complete
83
+ main_tasks = [info['task'] for info in task_infos]
84
+ log.info("Monitor: Waiting for all main tasks to complete")
85
+ await asyncio.gather(*main_tasks, return_exceptions=True)
86
+ log.info("Monitor: All main tasks have completed")
87
+
88
+ # Cancel all heartbeat tasks
89
+ for info in task_infos:
90
+ info['heartbeat_task'].cancel()
91
+ try:
92
+ await info['heartbeat_task']
93
+ except asyncio.CancelledError:
94
+ pass
95
+ log.info(f"Monitor: Heartbeat for task '{info['name']}' has been canceled")
96
+
97
+ # Send a sentinel to indicate completion
98
+ await queue.put(None)
99
+ log.info("Monitor: Sent sentinel to queue")
100
+
101
+ async def _run_with_retries(self, name: str, func: Callable[..., Any], *args: Any, queue: asyncio.Queue, completion_event: asyncio.Event) -> None:
102
+ """
103
+ Executes a task with optional retries and sends completion or error messages to the queue.
104
+ """
61
105
  try:
62
106
  if self.retry_enabled:
63
107
  retry_kwargs = {
@@ -68,50 +112,67 @@ class AsyncTaskRunner:
68
112
  }
69
113
  async for attempt in AsyncRetrying(**retry_kwargs):
70
114
  with attempt:
71
- return await run_func()
115
+ log.info(f"Starting task '{name}' with retry")
116
+ result = await self._execute_task(func, *args)
117
+ await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
118
+ log.info(f"Sent 'task_complete' message for task '{name}'")
119
+ return
72
120
  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
121
+ log.info(f"Starting task '{name}' with no retry")
122
+ result = await self._execute_task(func, *args)
123
+ await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
124
+ log.info(f"Sent 'task_complete' message for task '{name}'")
125
+ except Exception as e:
126
+ log.error(f"Error in task '{name}': {e}\n{traceback.format_exc()}")
127
+ await queue.put({'type': 'task_error', 'func_name': name, 'error': f'{e}\n{traceback.format_exc()}'})
128
+ log.info(f"Sent 'task_error' message for task '{name}'")
78
129
  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):
130
+ log.info(f"Task '{name}' completed.")
131
+ # Set the completion event after sending the message
132
+ completion_event.set()
133
+
134
+ async def _execute_task(self, func: Callable[..., Any], *args: Any) -> Any:
135
+ """
136
+ Executes the given task function and returns its result.
137
+
138
+ Args:
139
+ func (Callable): The callable to execute.
140
+ *args: Arguments to pass to the callable.
141
+
142
+ Returns:
143
+ Any: The result of the task.
144
+ """
145
+ if asyncio.iscoroutinefunction(func):
146
+ return await func(*args)
147
+ else:
148
+ return await asyncio.to_thread(func, *args)
149
+
150
+ async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, interval: int = 2):
97
151
  """
98
152
  Sends periodic heartbeat updates to indicate the task is still in progress.
99
153
 
100
154
  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).
155
+ func_name (str): The name of the task function.
156
+ completion_event (asyncio.Event): Event to signal when the task is completed.
157
+ queue (asyncio.Queue): The queue to send heartbeat messages to.
158
+ interval (int): How frequently to send heartbeat messages (in seconds).
104
159
  """
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
160
+ start_time = time.time()
161
+ log.info(f"Starting heartbeat for task '{func_name}' with interval {interval} seconds")
162
+ try:
163
+ while not completion_event.is_set():
164
+ await asyncio.sleep(interval)
165
+ elapsed_time = int(time.time() - start_time)
166
+ heartbeat_message = {
167
+ 'type': 'heartbeat',
168
+ 'name': func_name,
169
+ 'interval': interval,
170
+ 'elapsed_time': elapsed_time
171
+ }
172
+ log.info(f"Sending heartbeat for task '{func_name}', running for {elapsed_time} seconds")
173
+ await queue.put(heartbeat_message)
174
+ except asyncio.CancelledError:
175
+ log.info(f"Heartbeat for task '{func_name}' has been canceled")
176
+ finally:
177
+ log.info(f"Heartbeat for task '{func_name}' stopped")
178
+
@@ -2,13 +2,14 @@ from ..custom_logging import log
2
2
  from ..utils import ConfigManager
3
3
 
4
4
  # Load the YAML file
5
- def load_prompt_from_yaml(key, prefix="sunholo", load_from_file=False):
5
+ def load_prompt_from_yaml(key, prefix="sunholo", load_from_file=False, f_string=True):
6
6
  """
7
- Returns a string you can use with Langfuse PromptTemplate.from_template()
7
+ Returns a string you can use with prompts.
8
8
 
9
9
  Will first try to load from the Langfuse prompt library, if unavailable will look in promptConfig type file.
10
10
 
11
- Langfuse prompts have {{ two braces }}, Langchain prompts have { one brace }.
11
+ If f_string is True will be in a Langchain style prompt e.g. { one brace }
12
+ If f_string is False will be Langfuse style prompt e.g. {{ two braces }}
12
13
 
13
14
  Example:
14
15
 
@@ -36,7 +37,10 @@ def load_prompt_from_yaml(key, prefix="sunholo", load_from_file=False):
36
37
 
37
38
  langfuse_prompt = langfuse.get_prompt(langfuse_template, cache_ttl_seconds=300)
38
39
 
39
- return langfuse_prompt.get_langchain_prompt()
40
+ if f_string:
41
+ return langfuse_prompt.get_langchain_prompt()
42
+
43
+ return langfuse_prompt
40
44
 
41
45
  except Exception as err:
42
46
  log.warning(f"Could not find langfuse template: {langfuse_template} - {str(err)} - attempting to load from promptConfig")
@@ -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
 
sunholo/utils/mime.py ADDED
@@ -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.2
3
+ Version: 0.101.0
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.2.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.101.0.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -80,22 +80,24 @@ sunholo/embedder/embed_chunk.py,sha256=_FdO4-9frUJlDPqXv2Ai49ejUrrMTMGti3D7bfJGf
80
80
  sunholo/excel/__init__.py,sha256=AqTMN9K4qJYi4maEgoORc5oxDVGO_eqmwzDaVP37JgY,56
81
81
  sunholo/excel/plugin.py,sha256=rl3FoECZ6Ts8KKExPrbPwr3u3CegZfsevmcjgUXAlhE,4033
82
82
  sunholo/gcs/__init__.py,sha256=SZvbsMFDko40sIRHTHppA37IijvJTae54vrhooEF5-4,90
83
- sunholo/gcs/add_file.py,sha256=NxUaWTP-h9l5fbjnrEYpipo5rjS3uPkjMWeDhpK113g,8369
83
+ sunholo/gcs/add_file.py,sha256=vqgt71BodjVWKb-slr24lE0b12_ipFIbFyiSuPCVHGA,9081
84
84
  sunholo/gcs/download_folder.py,sha256=ijJTnS595JqZhBH8iHFErQilMbkuKgL-bnTCMLGuvlA,1614
85
- sunholo/gcs/download_url.py,sha256=oPC8phesRcpXB4zB_p0nWPKPBMs7JBhhVJ7tnVYhzD8,6356
85
+ sunholo/gcs/download_url.py,sha256=Ul81n1rklr8WogPsuxWWD1Nr8RHU451LzHPMJNhAKzw,6416
86
+ sunholo/gcs/extract_and_sign.py,sha256=paRrTCvCN5vkQwCB7OSkxWi-pfOgOtZ0bwdXE08c3Ps,1546
86
87
  sunholo/gcs/metadata.py,sha256=oQLcXi4brsZ74aegWyC1JZmhlaEV270HS5_UWtAYYWE,898
87
88
  sunholo/genai/__init__.py,sha256=dBl6IA3-Fx6-Vx81r0XqxHlUq6WeW1iDX188dpChu8s,115
89
+ sunholo/genai/images.py,sha256=WaeO705mK0BOz1k7OwsGslw7qF1WiyE_zsueYt07vV8,1191
88
90
  sunholo/genai/init.py,sha256=yG8E67TduFCTQPELo83OJuWfjwTnGZsyACospahyEaY,687
89
- sunholo/genai/process_funcs_cls.py,sha256=DPe70E71pofInLMFBFdudFZr0ZCFHN1LFCQjpxtG8xU,26552
91
+ sunholo/genai/process_funcs_cls.py,sha256=VvbTjyGQ_dq0xiEUx-Zn0ZomKbKTZwuoWdiWeestRfM,27419
90
92
  sunholo/genai/safety.py,sha256=mkFDO_BeEgiKjQd9o2I4UxB6XI7a9U-oOFjZ8LGRUC4,1238
91
93
  sunholo/invoke/__init__.py,sha256=o1RhwBGOtVK0MIdD55fAIMCkJsxTksi8GD5uoqVKI-8,184
92
- sunholo/invoke/async_class.py,sha256=LNe57uxwGwLaN44MCOVOOSMNX8FZ5buqCxevYTHUTPg,5501
94
+ sunholo/invoke/async_class.py,sha256=Wkrv3exjN3aDQdI1Q4tv8I8xSloKgdz_LzGomayfLhw,7734
93
95
  sunholo/invoke/direct_vac_func.py,sha256=GXSCMkC6vOWGUtQjxy-ZpTrMvJa3CgcW-y9mDpJwWC8,9533
94
96
  sunholo/invoke/invoke_vac_utils.py,sha256=sJc1edHTHMzMGXjji1N67c3iUaP7BmAL5nj82Qof63M,2053
95
97
  sunholo/langfuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
98
  sunholo/langfuse/callback.py,sha256=jl0SZsFS53uMW9DGeM9SOL_EsRZsba0wwFGLqKzu9_U,1684
97
99
  sunholo/langfuse/evals.py,sha256=fQBaC0dBTYfgCzyfv9QBRvUfc9f42lbwQAeZmynaHO8,3841
98
- sunholo/langfuse/prompts.py,sha256=27BsVfihM6-h1jscbkGSO4HsATl-d4ZN6tcNCVztWoY,1300
100
+ sunholo/langfuse/prompts.py,sha256=3Zo-YX2YpZdKNAGKjlGePXb8t8FisA9BrraA6tO7RHM,1422
99
101
  sunholo/llamaindex/__init__.py,sha256=DlY_cHWCsVEV1C5WBgDdHRgOMlJc8pDoCRukUJ8PT9w,88
100
102
  sunholo/llamaindex/get_files.py,sha256=6rhXCDqQ_lrIapISQ_OYQDjiSATXvS_9m3qq53-oIl0,781
101
103
  sunholo/llamaindex/import_files.py,sha256=Bnic5wz8c61af9Kwq8KSrNBbc4imYnzMtBCb2jzSImI,6224
@@ -114,7 +116,7 @@ sunholo/qna/__init__.py,sha256=F8q1uR_HreoSX0IfmKY1qoSwIgXhO2Q8kuDSxh9_-EE,28
114
116
  sunholo/qna/parsers.py,sha256=YpOaK5S_LxJ6FbliSYDc3AVOJ62RVduayoNnzi_p8CM,2494
115
117
  sunholo/qna/retry.py,sha256=yMw7RTkw-RXCzfENPJOt8c32mXlpvOR589EGkvK-6yI,2028
116
118
  sunholo/streaming/__init__.py,sha256=MpbydI2UYo_adttPQFkxNM33b-QRyNEbrKJx0C2AGPc,241
117
- sunholo/streaming/content_buffer.py,sha256=dGzzajY_DEnIxMERiApCKUSzDI5mXKByW9L4dmYRqSw,12814
119
+ sunholo/streaming/content_buffer.py,sha256=0LHMwH4ctq5kjhIgMFNH0bA1RL0jMISlLVzzLcFrvv4,12766
118
120
  sunholo/streaming/langserve.py,sha256=hi7q8WY8DPKrALl9m_dOMxWOdE-iEuk7YW05SVDFIX8,6514
119
121
  sunholo/streaming/stream_lookup.py,sha256=hYg1DbdSE_QNJ8ZB-ynXJlWgvFjrGvwoUsGJu_E0pRQ,360
120
122
  sunholo/streaming/streaming.py,sha256=S4GzVNol3J_8tkzXodvkcR-THiTGnIKYj40NtrBhtL4,16459
@@ -132,6 +134,7 @@ sunholo/utils/config_class.py,sha256=aNhxhEHIxRvqQNq_9wFJt1yM9U1ypahCVZ3NzPWuay4
132
134
  sunholo/utils/config_schema.py,sha256=Wv-ncitzljOhgbDaq9qnFqH5LCuxNv59dTGDWgd1qdk,4189
133
135
  sunholo/utils/gcp.py,sha256=uueODEpA-P6O15-t0hmcGC9dONLO_hLfzSsSoQnkUss,4854
134
136
  sunholo/utils/gcp_project.py,sha256=Fa0IhCX12bZ1ctF_PKN8PNYd7hihEUfb90kilBfUDjg,1411
137
+ sunholo/utils/mime.py,sha256=hTtSilPDK9oJe8yJqTcPGNV4fIUyaL43Ql8ZncB1DlU,1867
135
138
  sunholo/utils/parsers.py,sha256=wES0fRn3GONoymRXOXt-z62HCoOiUvvFXa-MfKfjCls,6421
136
139
  sunholo/utils/timedelta.py,sha256=BbLabEx7_rbErj_YbNM0MBcaFN76DC4PTe4zD2ucezg,493
137
140
  sunholo/utils/user_ids.py,sha256=SQd5_H7FE7vcTZp9AQuQDWBXd4FEEd7TeVMQe1H4Ny8,292
@@ -144,9 +147,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
144
147
  sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
145
148
  sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
146
149
  sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
147
- sunholo-0.100.2.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
148
- sunholo-0.100.2.dist-info/METADATA,sha256=hkHwKSigiDOe9t6fcubbDM0Osr542-T3R8guKamW2YY,8312
149
- sunholo-0.100.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
150
- sunholo-0.100.2.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
151
- sunholo-0.100.2.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
152
- sunholo-0.100.2.dist-info/RECORD,,
150
+ sunholo-0.101.0.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
151
+ sunholo-0.101.0.dist-info/METADATA,sha256=f5ACnI-b0miaBTKmVuiOB6xgtIcwb9QXDq3s__-_3Aw,8312
152
+ sunholo-0.101.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
153
+ sunholo-0.101.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
154
+ sunholo-0.101.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
155
+ sunholo-0.101.0.dist-info/RECORD,,