sunholo 0.94.0__py3-none-any.whl → 0.95.1__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.
@@ -4,10 +4,12 @@ import datetime
4
4
  import uuid
5
5
  import random
6
6
  from functools import partial
7
+ import inspect
8
+ import asyncio
7
9
 
8
10
  from ...agents import extract_chat_history, handle_special_commands
9
11
  from ...qna.parsers import parse_output
10
- from ...streaming import start_streaming_chat
12
+ from ...streaming import start_streaming_chat, start_streaming_chat_async
11
13
  from ...archive import archive_qa
12
14
  from ...custom_logging import log
13
15
  from ...utils import ConfigManager
@@ -183,10 +185,16 @@ if __name__ == "__main__":
183
185
 
184
186
  log.info(f"OpenAI response: {openai_response}")
185
187
  return jsonify(openai_response)
186
-
187
-
188
+
188
189
  def handle_stream_vac(self, vector_name):
189
190
  observed_stream_interpreter = self.stream_interpreter
191
+
192
+ # Determine if the stream interpreter is async or sync
193
+ is_async = inspect.iscoroutinefunction(self.stream_interpreter)
194
+
195
+ if is_async:
196
+ log.info(f"Stream interpreter is async: {observed_stream_interpreter}")
197
+
190
198
  prep = self.prep_vac(request, vector_name)
191
199
  log.info(f"Processing prep: {prep}")
192
200
  trace = prep["trace"]
@@ -203,41 +211,88 @@ if __name__ == "__main__":
203
211
  generation = span.generation(
204
212
  name="start_streaming_chat",
205
213
  metadata=vac_config.configs_by_kind,
206
- input = all_input,
214
+ input=all_input,
207
215
  completion_start_time=str(int(datetime.datetime.now().timestamp())),
208
216
  model=vac_config.vacConfig("model") or vac_config.vacConfig("llm")
209
217
  )
210
218
 
211
219
  def generate_response_content():
220
+ try:
221
+ if is_async:
222
+ # Run the async version
223
+ async def process_async():
224
+ async_gen = start_streaming_chat_async(
225
+ question=all_input["user_input"],
226
+ vector_name=vector_name,
227
+ qna_func_async=observed_stream_interpreter,
228
+ chat_history=all_input["chat_history"],
229
+ wait_time=all_input["stream_wait_time"],
230
+ timeout=all_input["stream_timeout"],
231
+ trace_id=trace.id if trace else None,
232
+ **all_input["kwargs"]
233
+ )
234
+ log.info(f"{async_gen=}")
235
+ # Yield intermediate results as they become available
236
+ async for chunk in async_gen:
237
+ if isinstance(chunk, dict) and 'answer' in chunk:
238
+ if trace:
239
+ chunk["trace_id"] = trace.id
240
+ chunk["trace_url"] = trace.get_trace_url()
241
+ generation.end(output=json.dumps(chunk))
242
+ span.end(output=json.dumps(chunk))
243
+ trace.update(output=json.dumps(chunk))
244
+
245
+ archive_qa(chunk, vector_name)
246
+
247
+ yield json.dumps(chunk)
248
+ else:
249
+ yield chunk
250
+
251
+ try:
252
+ loop = asyncio.get_running_loop()
253
+ # Use the event loop to consume the async generator and yield results
254
+ return [chunk for chunk in loop.run_until_complete(self._async_generator_to_list(process_async()))]
255
+ except RuntimeError:
256
+ # No running event loop, create a new one
257
+ log.info("Creating new event loop")
258
+ loop = asyncio.new_event_loop()
259
+ asyncio.set_event_loop(loop)
260
+ try:
261
+ log.info(f"Run until complete loop for {process_async}")
262
+ chunks = loop.run_until_complete(self._async_generator_to_list(process_async()))
263
+ for chunk in chunks:
264
+ yield chunk
265
+ finally:
266
+ loop.close()
212
267
 
213
- for chunk in start_streaming_chat(question=all_input["user_input"],
214
- vector_name=vector_name,
215
- qna_func=observed_stream_interpreter,
216
- chat_history=all_input["chat_history"],
217
- wait_time=all_input["stream_wait_time"],
218
- timeout=all_input["stream_timeout"],
219
- trace_id=trace.id if trace else None,
220
- #kwargs
221
- **all_input["kwargs"]
222
- ):
223
- if isinstance(chunk, dict) and 'answer' in chunk:
224
- # When we encounter the dictionary, we yield it as a JSON string
225
- # and stop the generator.
226
- if trace:
227
- chunk["trace_id"] = trace.id
228
- chunk["trace_url"] = trace.get_trace_url()
229
- archive_qa(chunk, vector_name)
230
- if trace:
231
- generation.end(output=json.dumps(chunk))
232
- span.end(output=json.dumps(chunk))
233
- trace.update(output=json.dumps(chunk))
234
-
235
- return json.dumps(chunk)
236
-
237
268
  else:
238
- # Otherwise, we yield the plain text chunks as they come in.
239
- yield chunk
240
-
269
+ log.info("sync streaming response")
270
+ for chunk in start_streaming_chat(
271
+ question=all_input["user_input"],
272
+ vector_name=vector_name,
273
+ qna_func=observed_stream_interpreter,
274
+ chat_history=all_input["chat_history"],
275
+ wait_time=all_input["stream_wait_time"],
276
+ timeout=all_input["stream_timeout"],
277
+ trace_id=trace.id if trace else None,
278
+ **all_input["kwargs"]
279
+ ):
280
+ if isinstance(chunk, dict) and 'answer' in chunk:
281
+ if trace:
282
+ chunk["trace_id"] = trace.id
283
+ chunk["trace_url"] = trace.get_trace_url()
284
+ archive_qa(chunk, vector_name)
285
+ if trace:
286
+ generation.end(output=json.dumps(chunk))
287
+ span.end(output=json.dumps(chunk))
288
+ trace.update(output=json.dumps(chunk))
289
+ return json.dumps(chunk)
290
+ else:
291
+ yield chunk
292
+
293
+ except Exception as e:
294
+ yield f"Streaming Error: {str(e)} {traceback.format_exc()}"
295
+
241
296
  # Here, the generator function will handle streaming the content to the client.
242
297
  response = Response(generate_response_content(), content_type='text/plain; charset=utf-8')
243
298
  response.headers['Transfer-Encoding'] = 'chunked'
@@ -250,6 +305,14 @@ if __name__ == "__main__":
250
305
  self.langfuse_eval_response(trace_id=trace.id, eval_percent=all_input.get('eval_percent'))
251
306
 
252
307
  return response
308
+
309
+ @staticmethod
310
+ async def _async_generator_to_list(async_gen):
311
+ """Helper function to collect an async generator's values into a list."""
312
+ result = []
313
+ async for item in async_gen:
314
+ result.append(item)
315
+ return result
253
316
 
254
317
  def langfuse_eval_response(self, trace_id, eval_percent=0.01):
255
318
  """
@@ -282,6 +345,7 @@ if __name__ == "__main__":
282
345
  log.info(f"Did not do Langfuse eval due to random sampling not passed: {eval_percent=}")
283
346
 
284
347
  def handle_process_vac(self, vector_name):
348
+ #TODO: handle async
285
349
  observed_vac_interpreter = self.vac_interpreter
286
350
  prep = self.prep_vac(request, vector_name)
287
351
  log.debug(f"Processing prep: {prep}")
@@ -1,2 +1,3 @@
1
1
  from .invoke_vac_utils import invoke_vac
2
- from .direct_vac_func import direct_vac, direct_vac_stream
2
+ from .direct_vac_func import direct_vac, direct_vac_stream
3
+ from .async_class import AsyncTaskRunner
@@ -1,142 +1,72 @@
1
1
  import asyncio
2
2
  import logging
3
- from typing import Callable, List, Any, Coroutine, Tuple
3
+ import traceback
4
+ from typing import Callable, List, Any, AsyncGenerator, Dict
5
+ from tenacity import AsyncRetrying, retry_if_exception_type, wait_random_exponential, stop_after_attempt
4
6
 
5
- # Setup basic logging configuration
6
- logging.basicConfig(level=logging.INFO)
7
7
  logger = logging.getLogger(__name__)
8
8
 
9
9
  class AsyncTaskRunner:
10
- """
11
- Example async functions for testing
12
- ```python
13
- async def api_call_1(url, params):
14
- await asyncio.sleep(1)
15
- if "fail" in params:
16
- raise ValueError(f"Error in api_call_1 with params: {params}")
17
- return f"api_call_1 response from {url} with params {params}"
18
-
19
- async def api_call_2(url, params):
20
- await asyncio.sleep(2)
21
- if "fail" in params:
22
- raise ValueError(f"Error in api_call_2 with params: {params}")
23
- return f"api_call_2 response from {url} with params {params}"
24
-
25
- # Example usage in an existing async function
26
- async def example_usage():
27
- runner = AsyncTaskRunner()
28
-
29
- runner.add_task(api_call_1, "http://example.com", {"key": "value"})
30
- runner.add_task(api_call_2, "http://example.org", {"key": "fail"})
31
-
32
- # Run all tasks within the existing event loop
33
- results = await runner.run_async()
34
- for result in results:
35
- print(result)
36
-
37
- # Example of calling run_sync() in a synchronous context
38
- if __name__ == "__main__":
39
- runner = AsyncTaskRunner()
40
-
41
- runner.add_task(api_call_1, "http://example.com", {"key": "value"})
42
- runner.add_task(api_call_2, "http://example.org", {"key": "fail"})
43
-
44
- # Running in a synchronous context
45
- results = runner.run_sync()
46
- for result in results:
47
- print(result)
48
- ```
49
-
50
- Example streaming fast and slow
51
-
52
- ```python
53
- import asyncio
54
- from typing import AsyncGenerator
55
-
56
- # Example streaming function that simulates yielding chunks of data
57
- async def stream_chunks(url: str, params: dict) -> AsyncGenerator[str, None]:
58
- # Simulate streaming with a series of chunks
59
- for i in range(5):
60
- await asyncio.sleep(1) # Simulate delay between chunks
61
- yield f"Chunk {i+1} from {url} with params {params}"
62
-
63
- # Example slow API call function
64
- async def slow_api_call(url: str, params: dict) -> str:
65
- await asyncio.sleep(5) # Simulate a slow API response
66
- return f"Slow API response from {url} with params {params}"
67
-
68
- # Function to manage streaming and slow API call
69
- async def process_api_calls(stream_url: str, stream_params: dict, slow_url: str, slow_params: dict) -> AsyncGenerator[str, None]:
70
- # Create the AsyncTaskRunner instance
71
- runner = AsyncTaskRunner()
72
-
73
- # Add the slow API call as a task
74
- runner.add_task(slow_api_call, slow_url, slow_params)
75
-
76
- # Run the slow API call concurrently with the streaming
77
- slow_api_result_task = asyncio.create_task(runner.run_async())
78
-
79
- # Process the streaming chunks and yield them
80
- async for chunk in stream_chunks(stream_url, stream_params):
81
- yield chunk
82
-
83
- # Wait for the slow API call to complete and get the result
84
- slow_api_results = await slow_api_result_task
85
-
86
- # Yield the slow API response after streaming is finished
87
- for result in slow_api_results:
88
- yield result
89
-
90
- # Example usage in an existing async function
91
- async def example_usage():
92
- # Define the URLs and parameters for the calls
93
- stream_url = "http://streaming.example.com"
94
- stream_params = {"key": "stream_value"}
95
- slow_url = "http://slowapi.example.com"
96
- slow_params = {"key": "slow_value"}
97
-
98
- # Process the API calls and stream the results
99
- async for output in process_api_calls(stream_url, stream_params, slow_url, slow_params):
100
- print(output)
101
-
102
- # Running the example usage
103
- if __name__ == "__main__":
104
- asyncio.run(example_usage())
105
- ```
106
- """
107
- def __init__(self):
10
+ def __init__(self, retry_enabled=False, retry_kwargs=None):
108
11
  self.tasks = []
109
-
110
- def add_task(self, func: Callable[..., Coroutine], *args: Any):
111
- """Adds a task (function and its arguments) to the list of tasks to be executed."""
12
+ self.retry_enabled = retry_enabled
13
+ self.retry_kwargs = retry_kwargs or {}
14
+
15
+ def add_task(self, func: Callable[..., Any], *args: Any):
16
+ """Adds a task to the list of tasks to be executed."""
112
17
  logger.info(f"Adding task: {func.__name__} with args: {args}")
113
- self.tasks.append(func(*args))
114
-
115
- def add_group(self, functions: List[Callable[..., Coroutine]], *args: Any):
116
- """Adds a group of functions that should run in parallel with the same arguments."""
117
- group_tasks = [func(*args) for func in functions]
118
- logger.info(f"Adding group of tasks with args: {args}")
119
- self.tasks.append(asyncio.gather(*group_tasks, return_exceptions=True))
120
-
121
- async def run_async(self) -> List[Any]:
122
- """Runs all tasks using the current event loop and returns the results."""
123
- logger.info("Running tasks asynchronously")
124
- results = await asyncio.gather(*self.tasks, return_exceptions=True)
125
- self._log_results(results)
126
- return results
127
-
128
- def run_sync(self) -> List[Any]:
129
- """Runs all tasks synchronously (blocking) using a new event loop and returns the results."""
130
- try:
131
- logger.info("Running tasks synchronously")
132
- loop = asyncio.get_running_loop()
133
- return loop.run_until_complete(self.run_async())
134
- except RuntimeError:
135
- loop = asyncio.new_event_loop()
136
- asyncio.set_event_loop(loop)
137
- results = loop.run_until_complete(self.run_async())
138
- loop.close()
139
- return results
18
+ self.tasks.append((func.__name__, func, args))
19
+
20
+ async def run_async_as_completed(self) -> AsyncGenerator[Dict[str, Any], None]:
21
+ """Runs all tasks concurrently and yields results as they complete."""
22
+ logger.info("Running tasks asynchronously and yielding results as they complete")
23
+ tasks = {}
24
+ for name, func, args in self.tasks:
25
+ coro = self._task_wrapper(name, func, args)
26
+ task = asyncio.create_task(coro)
27
+ tasks[task] = name
28
+
29
+ while tasks:
30
+ done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED)
31
+ for task in done:
32
+ name = tasks.pop(task)
33
+ try:
34
+ result = await task
35
+ yield {name: result}
36
+ except Exception as e:
37
+ logger.error(f"Task {name} resulted in an error: {e}\n{traceback.format_exc()}")
38
+ yield {name: e}
39
+
40
+ async def _task_wrapper(self, name: str, func: Callable[..., Any], args: Any) -> Any:
41
+ """Wraps the task function to process its output and handle retries."""
42
+ async def run_func():
43
+ gen = func(*args)
44
+ if hasattr(gen, '__aiter__'):
45
+ # If it's an async generator, consume it completely
46
+ output = ''
47
+ async for chunk in gen:
48
+ output += chunk
49
+ return output
50
+ else:
51
+ # If it's a coroutine, await the result
52
+ return await gen
53
+
54
+ if self.retry_enabled:
55
+ retry_kwargs = {
56
+ 'wait': wait_random_exponential(multiplier=1, max=60),
57
+ 'stop': stop_after_attempt(5),
58
+ 'retry': retry_if_exception_type(Exception),
59
+ **self.retry_kwargs
60
+ }
61
+ async for attempt in AsyncRetrying(**retry_kwargs):
62
+ with attempt:
63
+ return await run_func()
64
+ else:
65
+ try:
66
+ return await run_func()
67
+ except Exception as e:
68
+ logger.error(f"Error in task {name}: {e}\n{traceback.format_exc()}")
69
+ raise
140
70
 
141
71
  def _log_results(self, results: List[Any]):
142
72
  """Logs the results of the task executions."""
@@ -144,5 +74,4 @@ class AsyncTaskRunner:
144
74
  if isinstance(result, Exception):
145
75
  logger.error(f"Task resulted in an error: {result}")
146
76
  else:
147
- logger.info(f"Task completed successfully: {result}")
148
-
77
+ logger.info(f"Task completed successfully: {result}")
sunholo/qna/parsers.py CHANGED
@@ -54,7 +54,7 @@ def parse_output(bot_output):
54
54
 
55
55
  elif isinstance(bot_output, dict):
56
56
  if not bot_output.get("answer"):
57
- raise ValueError(f"VAC output was not a string or a dict with the key 'answer' - got: {bot_output}")
57
+ raise ValueError(f"VAC output was not a string or a dict with the key 'answer' - got: {bot_output} {type(bot_output)}")
58
58
  else:
59
59
 
60
60
  return bot_output
@@ -107,7 +107,7 @@ def start_streaming_chat(question,
107
107
  # parses out source_documents if not present etc.
108
108
  yield parse_output(final_result)
109
109
 
110
- async def start_streaming_chat_async(question, vector_name, qna_func, chat_history=[], wait_time=2, timeout=120, **kwargs):
110
+ async def start_streaming_chat_async(question, vector_name, qna_func_async, chat_history=[], wait_time=2, timeout=120, **kwargs):
111
111
  """
112
112
  Asynchronously initiates a chat session that streams responses back to the caller in real-time.
113
113
  This function manages the chat by starting a separate thread to handle the chat interaction,
@@ -116,7 +116,7 @@ async def start_streaming_chat_async(question, vector_name, qna_func, chat_histo
116
116
  Parameters:
117
117
  - question (str): The initial question to start the chat with.
118
118
  - vector_name (str): The vector configuration that determines the behavior of the AI in the chat.
119
- - qna_func (callable): The function that processes the chat and generates responses.
119
+ - qna_func_async (callable): The async function that processes the chat and generates responses.
120
120
  - chat_history (list, optional): A list of previous messages in the chat session to maintain context.
121
121
  - wait_time (int, optional): The interval in seconds between checking for new chat responses.
122
122
  - timeout (int, optional): The maximum time in seconds to wait for a new response before timing out.
@@ -154,9 +154,9 @@ async def start_streaming_chat_async(question, vector_name, qna_func, chat_histo
154
154
  The `start_streaming_chat_async` function is used in a coroutine that prints messages as they are generated.
155
155
  """
156
156
 
157
- log.info(f"Streaming chat with wait time {wait_time} seconds and timeout {timeout} seconds and kwargs {kwargs}")
158
- if not check_kwargs_support(qna_func):
159
- yield "No **kwargs in qna_func - please add it"
157
+ log.info(f"Async Streaming chat with wait time {wait_time} seconds and timeout {timeout} seconds and kwargs {kwargs}")
158
+ if not check_kwargs_support(qna_func_async):
159
+ yield "No **kwargs in qna_func_async - please add it"
160
160
 
161
161
  content_buffer = ContentBuffer()
162
162
  chat_callback_handler = BufferStreamingStdOutCallbackHandler(content_buffer=content_buffer, tokens=".!?\n")
@@ -165,21 +165,21 @@ async def start_streaming_chat_async(question, vector_name, qna_func, chat_histo
165
165
  exception_queue = Queue()
166
166
  stop_event = Event()
167
167
 
168
- def start_chat():
168
+ async def start_chat():
169
169
  try:
170
- final_result = qna_func(question, vector_name, chat_history, callback=chat_callback_handler, **kwargs)
170
+ final_result = await qna_func_async(question, vector_name, chat_history, callback=chat_callback_handler, **kwargs)
171
171
  result_queue.put(final_result)
172
172
  except Exception as e:
173
173
  exception_queue.put(e)
174
174
 
175
- chat_thread = Thread(target=start_chat)
176
- chat_thread.start()
175
+ # Run start_chat asynchronously
176
+ chat_task = asyncio.create_task(start_chat())
177
177
 
178
178
  start = time.time()
179
179
 
180
180
  while not chat_callback_handler.stream_finished.is_set() and not stop_event.is_set():
181
181
  await asyncio.sleep(wait_time) # Use asyncio.sleep for async compatibility
182
- log.info(f"heartbeat - {round(time.time() - start, 2)} seconds")
182
+ log.info(f"async heartbeat - {round(time.time() - start, 2)} seconds")
183
183
 
184
184
  while not exception_queue.empty():
185
185
  exception = exception_queue.get_nowait()
@@ -197,15 +197,14 @@ async def start_streaming_chat_async(question, vector_name, qna_func, chat_histo
197
197
  break
198
198
 
199
199
  stop_event.set()
200
- chat_thread.join()
200
+ await chat_task # Ensure the async task is awaited
201
201
 
202
202
  # Handle final result
203
203
  if not result_queue.empty():
204
204
  final_result = result_queue.get()
205
- # Ensure parse_output is called outside of the async generator context if needed
206
- parsed_final_result = parse_output(final_result) # Assuming parse_output can handle final_result structure
207
- if 'answer' in parsed_final_result: # Yield final structured result if needed
208
- yield f"###JSON_START###{json.dumps(parsed_final_result)}###JSON_END###"
205
+ parsed_final_result = parse_output(final_result)
206
+ if 'answer' in parsed_final_result:
207
+ yield json.dumps(parsed_final_result)
209
208
 
210
209
 
211
210
 
@@ -153,42 +153,44 @@ def print_grounding_response(response):
153
153
  prev_index = 0
154
154
  markdown_text = ""
155
155
 
156
- sources: dict[str, str] = {}
157
- footnote = 1
158
- for attribution in grounding_metadata.grounding_attributions:
159
- context = attribution.web or attribution.retrieved_context
160
- if not context:
161
- log.info(f"Skipping Grounding Attribution {attribution}")
162
- continue
163
-
164
- title = context.title
165
- uri = context.uri
166
- end_index = int(attribution.segment.end_index)
156
+ for grounding_support in grounding_metadata.grounding_supports:
157
+ text_segment = text_bytes[
158
+ prev_index : grounding_support.segment.end_index
159
+ ].decode(ENCODING)
167
160
 
168
- if uri not in sources:
169
- sources[uri] = {"title": title, "footnote": footnote}
170
- footnote += 1
161
+ footnotes_text = ""
162
+ for grounding_chunk_index in grounding_support.grounding_chunk_indices:
163
+ footnotes_text += f"[{grounding_chunk_index + 1}]"
171
164
 
172
- text_segment = text_bytes[prev_index:end_index].decode(ENCODING)
173
- markdown_text += f"{text_segment} [[{sources[uri]['footnote']}]]({uri})"
174
- prev_index = end_index
165
+ markdown_text += f"{text_segment} {footnotes_text}\n"
166
+ prev_index = grounding_support.segment.end_index
175
167
 
176
168
  if prev_index < len(text_bytes):
177
169
  markdown_text += str(text_bytes[prev_index:], encoding=ENCODING)
178
170
 
179
- markdown_text += "\n## Grounding Sources\n"
171
+ markdown_text += "\n----\n## Grounding Sources\n"
180
172
 
181
173
  if grounding_metadata.web_search_queries:
182
174
  markdown_text += (
183
175
  f"\n**Web Search Queries:** {grounding_metadata.web_search_queries}\n"
184
176
  )
177
+ if grounding_metadata.search_entry_point:
178
+ markdown_text += f"\n**Search Entry Point:**\n {grounding_metadata.search_entry_point.rendered_content}\n"
185
179
  elif grounding_metadata.retrieval_queries:
186
180
  markdown_text += (
187
181
  f"\n**Retrieval Queries:** {grounding_metadata.retrieval_queries}\n"
188
182
  )
189
183
 
190
- for uri, source in sources.items():
191
- markdown_text += f"{source['footnote']}. [{source['title']}]({uri})\n"
192
-
193
- log.info(markdown_text)
194
- return markdown_text
184
+ markdown_text += "### Grounding Chunks\n"
185
+
186
+ for index, grounding_chunk in enumerate(
187
+ grounding_metadata.grounding_chunks, start=1
188
+ ):
189
+ context = grounding_chunk.web or grounding_chunk.retrieved_context
190
+ if not context:
191
+ print(f"Skipping Grounding Chunk {grounding_chunk}")
192
+ continue
193
+
194
+ markdown_text += f"{index}. [{context.title}]({context.uri})\n"
195
+
196
+ return markdown_text
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.94.0
3
+ Version: 0.95.1
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.94.0.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.95.1.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -72,6 +72,7 @@ Requires-Dist: rich ; extra == 'all'
72
72
  Requires-Dist: supabase ; extra == 'all'
73
73
  Requires-Dist: tabulate ; extra == 'all'
74
74
  Requires-Dist: tantivy ; extra == 'all'
75
+ Requires-Dist: tenacity ; extra == 'all'
75
76
  Requires-Dist: tiktoken ; extra == 'all'
76
77
  Requires-Dist: unstructured[local-inference] ==0.14.9 ; extra == 'all'
77
78
  Requires-Dist: xlwings ; extra == 'all'
@@ -124,6 +125,7 @@ Requires-Dist: httpx ; extra == 'http'
124
125
  Requires-Dist: langfuse ; extra == 'http'
125
126
  Requires-Dist: python-socketio ; extra == 'http'
126
127
  Requires-Dist: requests ; extra == 'http'
128
+ Requires-Dist: tenacity ; extra == 'http'
127
129
  Provides-Extra: iac
128
130
  Requires-Dist: python-hcl2 ; extra == 'iac'
129
131
  Provides-Extra: openai
@@ -14,7 +14,7 @@ sunholo/agents/fastapi/qna_routes.py,sha256=lKHkXPmwltu9EH3RMwmD153-J6pE7kWQ4BhB
14
14
  sunholo/agents/flask/__init__.py,sha256=poJDKMr2qj8qMb99JqCvCPSiEt1tj2tLQ3hKW3f2aVw,107
15
15
  sunholo/agents/flask/base.py,sha256=FgSaCODyoTtlstJtsqlLPScdgRUtv9_plxftdzHdVFo,809
16
16
  sunholo/agents/flask/qna_routes.py,sha256=uwUD1yrzOPH27m2AXpiQrPk_2VfJOQOM6dAynOWQtoQ,22532
17
- sunholo/agents/flask/vac_routes.py,sha256=wMH0a53BFrBnksi4F34pEGJIJgRImxndPeMWGAjUQZw,24179
17
+ sunholo/agents/flask/vac_routes.py,sha256=6Z8Eqt1SgfcmUf5V-7O3vYSXnGCBWQKWBUPj-NwX1RU,27269
18
18
  sunholo/archive/__init__.py,sha256=qNHWm5rGPVOlxZBZCpA1wTYPbalizRT7f8X4rs2t290,31
19
19
  sunholo/archive/archive.py,sha256=PxVfDtO2_2ZEEbnhXSCbXLdeoHoQVImo4y3Jr2XkCFY,1204
20
20
  sunholo/auth/__init__.py,sha256=TeP-OY0XGxYV_8AQcVGoh35bvyWhNUcMRfhuD5l44Sk,91
@@ -88,8 +88,8 @@ sunholo/genai/__init__.py,sha256=dBl6IA3-Fx6-Vx81r0XqxHlUq6WeW1iDX188dpChu8s,115
88
88
  sunholo/genai/init.py,sha256=yG8E67TduFCTQPELo83OJuWfjwTnGZsyACospahyEaY,687
89
89
  sunholo/genai/process_funcs_cls.py,sha256=1yDVh7Gi5qX0FMzV__8k3vn3BJ7AreX9CvPxM_QPFkU,24235
90
90
  sunholo/genai/safety.py,sha256=mkFDO_BeEgiKjQd9o2I4UxB6XI7a9U-oOFjZ8LGRUC4,1238
91
- sunholo/invoke/__init__.py,sha256=bELcqIjzKvaupcIN5OQmDgGx_8jARtH9T6PCe8UgcvE,99
92
- sunholo/invoke/async_class.py,sha256=vmLT6DqE1YaPd4W88_QzPQvSzsjwLUAwt23vGZm-BEs,5767
91
+ sunholo/invoke/__init__.py,sha256=VOpsfhNf98az3at7bMaY1Fiw4UIZAS6zMmSqWd6_ksI,141
92
+ sunholo/invoke/async_class.py,sha256=3QR_hFxyEEkksbXmXziDuC__dg4XWNghuw2ZUAPpQ3Q,3195
93
93
  sunholo/invoke/direct_vac_func.py,sha256=fuTJlH5PsqWhN_yVMaWisHCTZU1JEUz8I8yVbWsNUFE,4268
94
94
  sunholo/invoke/invoke_vac_utils.py,sha256=sJc1edHTHMzMGXjji1N67c3iUaP7BmAL5nj82Qof63M,2053
95
95
  sunholo/langfuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -111,13 +111,13 @@ sunholo/pubsub/__init__.py,sha256=DfTEk4zmCfqn6gFxRrqDO0pOrvXTDqH-medpgYO4PGw,11
111
111
  sunholo/pubsub/process_pubsub.py,sha256=rN2N4WM6PZkMKDrdT8pnEfTvsXACRyJFqIHJQCbuxLs,3088
112
112
  sunholo/pubsub/pubsub_manager.py,sha256=19w_N0LiG-wgVWvgJ13b8BUeN8ZzgSPXAhPmL1HRRSI,6966
113
113
  sunholo/qna/__init__.py,sha256=F8q1uR_HreoSX0IfmKY1qoSwIgXhO2Q8kuDSxh9_-EE,28
114
- sunholo/qna/parsers.py,sha256=or5BQ0M-bvYvozJ_ui_4OlRqDaihuLHr2AC_QxUvvPw,2475
114
+ sunholo/qna/parsers.py,sha256=YpOaK5S_LxJ6FbliSYDc3AVOJ62RVduayoNnzi_p8CM,2494
115
115
  sunholo/qna/retry.py,sha256=yMw7RTkw-RXCzfENPJOt8c32mXlpvOR589EGkvK-6yI,2028
116
116
  sunholo/streaming/__init__.py,sha256=MpbydI2UYo_adttPQFkxNM33b-QRyNEbrKJx0C2AGPc,241
117
117
  sunholo/streaming/content_buffer.py,sha256=A9rVhlmLZxaFPvGYjVqCs5GfIsA1EMl_tyk-0VCDvo4,6462
118
118
  sunholo/streaming/langserve.py,sha256=hi7q8WY8DPKrALl9m_dOMxWOdE-iEuk7YW05SVDFIX8,6514
119
119
  sunholo/streaming/stream_lookup.py,sha256=hYg1DbdSE_QNJ8ZB-ynXJlWgvFjrGvwoUsGJu_E0pRQ,360
120
- sunholo/streaming/streaming.py,sha256=TNnjlMH4tC4vDqLICyeLCj2tj9EcTLLvyH6zDSpsxh4,16326
120
+ sunholo/streaming/streaming.py,sha256=tMCAdB_piQXiiQLPkkYJwDiDxfg6UOBeto0d9C8m_Jk,16211
121
121
  sunholo/summarise/__init__.py,sha256=MZk3dblUMODcPb1crq4v-Z508NrFIpkSWNf9FIO8BcU,38
122
122
  sunholo/summarise/summarise.py,sha256=95A-6PXFGanjona8DvZPnnIHLbzZ2ip5hO0wOAJQhfw,3791
123
123
  sunholo/terraform/__init__.py,sha256=yixxEltc3n9UpZaVi05GlgS-YRq_DVGjUc37I9ajeP4,76
@@ -141,12 +141,12 @@ sunholo/vertex/extensions_call.py,sha256=QeQbL3aAHlc4_-SynOzooZ_3xkQWAlcgNmFBSwL
141
141
  sunholo/vertex/extensions_class.py,sha256=2QGW28lNjoMEnaoVb3QcqEDwphclIsZthnpLUi5_Ivo,21033
142
142
  sunholo/vertex/genai_functions.py,sha256=2z6grM9H0Z79Yzx88l8mE1wXck3bRa0TWvnqZZ9ifDc,2051
143
143
  sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
144
- sunholo/vertex/memory_tools.py,sha256=q_phxgGX2TG2j2MXNULF2xGzQnQPENwjPN9nZ_A9Gh0,7526
144
+ sunholo/vertex/memory_tools.py,sha256=x3_ESOhgMpf-gNpiOzmP7YQI0l0FAoLtbAVP2K2N0OA,7724
145
145
  sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
146
146
  sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
147
- sunholo-0.94.0.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
148
- sunholo-0.94.0.dist-info/METADATA,sha256=WzxVoYlzQITMFyMSG8aGXvkAaIoE2XVwOzXgkLZDZPM,7806
149
- sunholo-0.94.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
150
- sunholo-0.94.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
151
- sunholo-0.94.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
152
- sunholo-0.94.0.dist-info/RECORD,,
147
+ sunholo-0.95.1.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
148
+ sunholo-0.95.1.dist-info/METADATA,sha256=eGcN3ki323hTYiSMUNGlgJCDQI_r1hxVLHbwksnSdXg,7889
149
+ sunholo-0.95.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
150
+ sunholo-0.95.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
151
+ sunholo-0.95.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
152
+ sunholo-0.95.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5