sunholo 0.101.0__py3-none-any.whl → 0.101.3__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.
@@ -14,13 +14,16 @@ 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
19
21
 
20
22
  from .images import extract_gs_images_and_genai_upload
21
23
 
22
24
  if TYPE_CHECKING:
23
25
  from google.generativeai.protos import Part
26
+ from google.generativeai import ChatSession
24
27
 
25
28
 
26
29
 
@@ -369,8 +372,28 @@ class GenAIFunctionProcessor:
369
372
  log.info(f"Cleaning:\n{string}\n > to >\n{clean}")
370
373
 
371
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.
382
+
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
372
395
 
373
- def run_agent_loop(self, chat, content, callback=None, guardrail_max=10, loop_return=3):
396
+ def run_agent_loop(self, chat:ChatSession, content:list, callback=None, guardrail_max=10, loop_return=3):
374
397
  """
375
398
  Runs the agent loop, sending messages to the orchestrator, processing responses, and executing functions.
376
399
 
@@ -488,7 +511,9 @@ class GenAIFunctionProcessor:
488
511
  fn_result_json = None
489
512
  # Convert MapComposite to a standard Python dictionary
490
513
  if isinstance(fn_result, proto.marshal.collections.maps.MapComposite):
491
- 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)
492
517
  elif isinstance(fn_result, dict):
493
518
  fn_result_json = fn_result
494
519
  elif isinstance(fn_result, str):
@@ -13,12 +13,18 @@ class AsyncTaskRunner:
13
13
  self.retry_enabled = retry_enabled
14
14
  self.retry_kwargs = retry_kwargs or {}
15
15
 
16
- def add_task(self, func: Callable[..., Any], *args: Any):
16
+ def add_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any):
17
17
  """
18
- Adds a task to the list of tasks to be executed.
18
+ Adds a task to the list of tasks to be executed, supporting both positional and keyword arguments.
19
+
20
+ Args:
21
+ func: The function to be executed.
22
+ *args: Positional arguments for the function.
23
+ **kwargs: Keyword arguments for the function.
19
24
  """
20
- log.info(f"Adding task: {func.__name__} with args: {args}")
21
- self.tasks.append((func.__name__, func, args))
25
+ log.info(f"Adding task: {func.__name__} with args: {args}, kwargs: {kwargs}")
26
+ # Store the function name, function itself, args, and kwargs
27
+ self.tasks.append((func.__name__, func, args, kwargs))
22
28
 
23
29
  async def run_async_as_completed(self) -> AsyncGenerator[Dict[str, Any], None]:
24
30
  """
@@ -34,12 +40,13 @@ class AsyncTaskRunner:
34
40
  task_infos = []
35
41
 
36
42
  # Start all tasks and their corresponding heartbeats
37
- for name, func, args in self.tasks:
43
+ for name, func, args, kwargs in self.tasks:
38
44
  # Create an event to signal task completion to the heartbeat
45
+ log.info(f"Executing task: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
39
46
  completion_event = asyncio.Event()
40
47
 
41
48
  # Start the main task with retries
42
- task_coro = self._run_with_retries(name, func, *args, queue=queue, completion_event=completion_event)
49
+ task_coro = self._run_with_retries(name, func, args, kwargs, queue, completion_event)
43
50
  task = asyncio.create_task(task_coro)
44
51
 
45
52
  # Start the heartbeat coroutine
@@ -98,11 +105,18 @@ class AsyncTaskRunner:
98
105
  await queue.put(None)
99
106
  log.info("Monitor: Sent sentinel to queue")
100
107
 
101
- async def _run_with_retries(self, name: str, func: Callable[..., Any], *args: Any, queue: asyncio.Queue, completion_event: asyncio.Event) -> None:
108
+ async def _run_with_retries(self,
109
+ name: str,
110
+ func: Callable[..., Any],
111
+ args: tuple,
112
+ kwargs: dict,
113
+ queue: asyncio.Queue,
114
+ completion_event: asyncio.Event) -> None:
102
115
  """
103
116
  Executes a task with optional retries and sends completion or error messages to the queue.
104
117
  """
105
118
  try:
119
+ log.info(f"run_with_retries: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
106
120
  if self.retry_enabled:
107
121
  retry_kwargs = {
108
122
  'wait': wait_random_exponential(multiplier=1, max=60),
@@ -113,13 +127,13 @@ class AsyncTaskRunner:
113
127
  async for attempt in AsyncRetrying(**retry_kwargs):
114
128
  with attempt:
115
129
  log.info(f"Starting task '{name}' with retry")
116
- result = await self._execute_task(func, *args)
130
+ result = await self._execute_task(func, *args, **kwargs)
117
131
  await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
118
132
  log.info(f"Sent 'task_complete' message for task '{name}'")
119
133
  return
120
134
  else:
121
135
  log.info(f"Starting task '{name}' with no retry")
122
- result = await self._execute_task(func, *args)
136
+ result = await self._execute_task(func, *args, **kwargs)
123
137
  await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
124
138
  log.info(f"Sent 'task_complete' message for task '{name}'")
125
139
  except Exception as e:
@@ -128,24 +142,24 @@ class AsyncTaskRunner:
128
142
  log.info(f"Sent 'task_error' message for task '{name}'")
129
143
  finally:
130
144
  log.info(f"Task '{name}' completed.")
131
- # Set the completion event after sending the message
132
145
  completion_event.set()
133
146
 
134
- async def _execute_task(self, func: Callable[..., Any], *args: Any) -> Any:
147
+ async def _execute_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
135
148
  """
136
149
  Executes the given task function and returns its result.
137
150
 
138
151
  Args:
139
152
  func (Callable): The callable to execute.
140
- *args: Arguments to pass to the callable.
153
+ *args: Positional arguments to pass to the callable.
154
+ **kwargs: Keyword arguments to pass to the callable.
141
155
 
142
156
  Returns:
143
157
  Any: The result of the task.
144
158
  """
145
159
  if asyncio.iscoroutinefunction(func):
146
- return await func(*args)
160
+ return await func(*args, **kwargs)
147
161
  else:
148
- return await asyncio.to_thread(func, *args)
162
+ return await asyncio.to_thread(func, *args, **kwargs)
149
163
 
150
164
  async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, interval: int = 2):
151
165
  """
@@ -174,5 +188,4 @@ class AsyncTaskRunner:
174
188
  except asyncio.CancelledError:
175
189
  log.info(f"Heartbeat for task '{func_name}' has been canceled")
176
190
  finally:
177
- log.info(f"Heartbeat for task '{func_name}' stopped")
178
-
191
+ log.info(f"Heartbeat for task '{func_name}' stopped")
@@ -6,17 +6,49 @@ def load_prompt_from_yaml(key, prefix="sunholo", load_from_file=False, f_string=
6
6
  """
7
7
  Returns a string you can use with prompts.
8
8
 
9
- Will first try to load from the Langfuse prompt library, if unavailable will look in promptConfig type file.
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
+ ```
10
31
 
11
32
  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 }}
33
+ If f_string is False will be Langfuse style prompt e.g. {{ two braces }} - see https://langfuse.com/docs/prompts/get-started
13
34
 
14
35
  Example:
15
36
 
16
37
  ```python
17
38
  from sunholo.langfuse.prompts import load_prompt_from_yaml
18
- from langchain_core.prompts import PromptTemplate
19
-
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
+
20
52
  """
21
53
  config = ConfigManager(prefix)
22
54
  if load_from_file:
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.101.0
3
+ Version: 0.101.3
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.101.0.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.101.3.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -88,16 +88,16 @@ sunholo/gcs/metadata.py,sha256=oQLcXi4brsZ74aegWyC1JZmhlaEV270HS5_UWtAYYWE,898
88
88
  sunholo/genai/__init__.py,sha256=dBl6IA3-Fx6-Vx81r0XqxHlUq6WeW1iDX188dpChu8s,115
89
89
  sunholo/genai/images.py,sha256=WaeO705mK0BOz1k7OwsGslw7qF1WiyE_zsueYt07vV8,1191
90
90
  sunholo/genai/init.py,sha256=yG8E67TduFCTQPELo83OJuWfjwTnGZsyACospahyEaY,687
91
- sunholo/genai/process_funcs_cls.py,sha256=VvbTjyGQ_dq0xiEUx-Zn0ZomKbKTZwuoWdiWeestRfM,27419
91
+ sunholo/genai/process_funcs_cls.py,sha256=krjhwruW-7OUR0gEOPC5Ew9KOoAoTWDMW5HfvpvuPNM,28755
92
92
  sunholo/genai/safety.py,sha256=mkFDO_BeEgiKjQd9o2I4UxB6XI7a9U-oOFjZ8LGRUC4,1238
93
93
  sunholo/invoke/__init__.py,sha256=o1RhwBGOtVK0MIdD55fAIMCkJsxTksi8GD5uoqVKI-8,184
94
- sunholo/invoke/async_class.py,sha256=Wkrv3exjN3aDQdI1Q4tv8I8xSloKgdz_LzGomayfLhw,7734
94
+ sunholo/invoke/async_class.py,sha256=DViVOHsuir7Zrfa3tVKgvz5wLCfXba15XryWGuMjKYI,8540
95
95
  sunholo/invoke/direct_vac_func.py,sha256=GXSCMkC6vOWGUtQjxy-ZpTrMvJa3CgcW-y9mDpJwWC8,9533
96
96
  sunholo/invoke/invoke_vac_utils.py,sha256=sJc1edHTHMzMGXjji1N67c3iUaP7BmAL5nj82Qof63M,2053
97
97
  sunholo/langfuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  sunholo/langfuse/callback.py,sha256=jl0SZsFS53uMW9DGeM9SOL_EsRZsba0wwFGLqKzu9_U,1684
99
99
  sunholo/langfuse/evals.py,sha256=fQBaC0dBTYfgCzyfv9QBRvUfc9f42lbwQAeZmynaHO8,3841
100
- sunholo/langfuse/prompts.py,sha256=3Zo-YX2YpZdKNAGKjlGePXb8t8FisA9BrraA6tO7RHM,1422
100
+ sunholo/langfuse/prompts.py,sha256=9TkmT2A-P4YRlk7sWz_EYk6yASxPKU8jhM7jAg9ND_Q,2330
101
101
  sunholo/llamaindex/__init__.py,sha256=DlY_cHWCsVEV1C5WBgDdHRgOMlJc8pDoCRukUJ8PT9w,88
102
102
  sunholo/llamaindex/get_files.py,sha256=6rhXCDqQ_lrIapISQ_OYQDjiSATXvS_9m3qq53-oIl0,781
103
103
  sunholo/llamaindex/import_files.py,sha256=Bnic5wz8c61af9Kwq8KSrNBbc4imYnzMtBCb2jzSImI,6224
@@ -147,9 +147,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
147
147
  sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
148
148
  sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
149
149
  sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
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,,
150
+ sunholo-0.101.3.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
151
+ sunholo-0.101.3.dist-info/METADATA,sha256=X_RjEUyzY8ejvVlwkm7Pf1GIkAxybLMR0v2HpKentr4,8312
152
+ sunholo-0.101.3.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
153
+ sunholo-0.101.3.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
154
+ sunholo-0.101.3.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
155
+ sunholo-0.101.3.dist-info/RECORD,,