sunholo 0.116.2__py3-none-any.whl → 0.118.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.
@@ -0,0 +1,157 @@
1
+
2
+ from sunholo.utils import ConfigManager
3
+ from sunholo.vertex import (
4
+ init_genai,
5
+ )
6
+
7
+ from tools.your_agent import get_quarto, quarto_content, QuartoProcessor
8
+
9
+ from my_log import log
10
+
11
+ init_genai()
12
+
13
+ # kwargs supports - image_uri, mime
14
+ def vac_stream(question: str, vector_name:str, chat_history=[], callback=None, **kwargs):
15
+
16
+ config=ConfigManager(vector_name)
17
+ processor = QuartoProcessor(config)
18
+
19
+ orchestrator = get_quarto(config, processor)
20
+ if not orchestrator:
21
+ msg = f"No quarto model could be configured for {vector_name}"
22
+ log.error(msg)
23
+ callback.on_llm_end(response=msg)
24
+ return {"answer": msg}
25
+
26
+ chat = orchestrator.start_chat()
27
+
28
+ guardrail = 0
29
+ guardrail_max = kwargs.get('max_steps', 10)
30
+ big_text = ""
31
+ usage_metadata = None
32
+ functions_called = []
33
+ result=None
34
+ last_responses=None
35
+ while guardrail < guardrail_max:
36
+
37
+ content = quarto_content(question, chat_history)
38
+ log.info(f"# Loop [{guardrail}] - {content=}")
39
+ response = chat.send_message(content, stream=True)
40
+ this_text = "" # reset for this loop
41
+ log.debug(f"[{guardrail}] {response}")
42
+
43
+ for chunk in response:
44
+ try:
45
+ log.debug(f"[{guardrail}] {chunk=}")
46
+ # Check if 'text' is an attribute of chunk and if it's a string
47
+ if hasattr(chunk, 'text') and isinstance(chunk.text, str):
48
+ token = chunk.text
49
+ else:
50
+ function_names = []
51
+ try:
52
+ for part in chunk.candidates[0].content.parts:
53
+ if fn := part.function_call:
54
+ params = {key: val for key, val in fn.args.items()}
55
+ func_args = ",".join(f"{key}={value}" for key, value in params.items())
56
+ log.info(f"Found function call: {fn.name}({func_args})")
57
+ function_names.append(f"{fn.name}({func_args})")
58
+ functions_called.append(f"{fn.name}({func_args})")
59
+ except Exception as err:
60
+ log.warning(f"{str(err)}")
61
+
62
+ token = "" # Handle the case where 'text' is not available
63
+
64
+ if processor.last_api_requests_and_responses:
65
+ if processor.last_api_requests_and_responses != last_responses:
66
+ last_responses = processor.last_api_requests_and_responses
67
+ for last_response in last_responses:
68
+ result=None # reset for this function response
69
+ if last_response:
70
+ log.info(f"[{guardrail}] {last_response=}")
71
+
72
+ # Convert the last_response to a string by extracting relevant information
73
+ function_name = last_response[0]
74
+ arguments = last_response[1]
75
+ result = last_response[2]
76
+ func_args = ",".join(f"{key}={value}" for key, value in arguments.items())
77
+
78
+ if f"{function_name}({func_args})" not in function_names:
79
+ log.warning(f"skipping {function_name}({func_args}) as not in execution list")
80
+ continue
81
+
82
+ token = f"\n## Loop [{guardrail}] Function call: {function_name}({func_args}):\n"
83
+
84
+ if function_name == "decide_to_go_on":
85
+ token += f"# go_on={result}\n"
86
+ else:
87
+ log.info("Adding result for: {function_name}")
88
+ token += result
89
+
90
+ callback.on_llm_new_token(token=token)
91
+ big_text += token
92
+ this_text += token
93
+
94
+ if not usage_metadata:
95
+ chunk_metadata = chunk.usage_metadata
96
+ usage_metadata = {
97
+ "prompt_token_count": chunk_metadata.prompt_token_count,
98
+ "candidates_token_count": chunk_metadata.candidates_token_count,
99
+ "total_token_count": chunk_metadata.total_token_count,
100
+ }
101
+
102
+ except ValueError as err:
103
+ callback.on_llm_new_token(token=str(err))
104
+
105
+ # change response to one with executed functions
106
+ response = processor.process_funcs(response)
107
+
108
+ if this_text:
109
+ chat_history.append(("<waiting for ai>", this_text))
110
+ log.info(f"[{guardrail}] Updated chat_history: {chat_history}")
111
+
112
+ go_on_check = processor.check_function_result("decide_to_go_on", False)
113
+ if go_on_check:
114
+ log.info("Breaking agent loop")
115
+ break
116
+
117
+ guardrail += 1
118
+ if guardrail > guardrail_max:
119
+ log.warning("Guardrail kicked in, more than 10 loops")
120
+ break
121
+
122
+ callback.on_llm_end(response=big_text)
123
+ log.info(f"orchestrator.response: {big_text}")
124
+
125
+ metadata = {
126
+ "question:": question,
127
+ "chat_history": chat_history,
128
+ "usage_metadata": usage_metadata,
129
+ "functions_called": functions_called
130
+ }
131
+
132
+ return {"answer": big_text or "No answer was given", "metadata": metadata}
133
+
134
+
135
+ def vac(question: str, vector_name: str, chat_history=[], **kwargs):
136
+ # Create a callback that does nothing for streaming if you don't want intermediate outputs
137
+ class NoOpCallback:
138
+ def on_llm_new_token(self, token):
139
+ pass
140
+ def on_llm_end(self, response):
141
+ pass
142
+
143
+ # Use the NoOpCallback for non-streaming behavior
144
+ callback = NoOpCallback()
145
+
146
+ # Pass all arguments to vac_stream and use the final return
147
+ result = vac_stream(
148
+ question=question,
149
+ vector_name=vector_name,
150
+ chat_history=chat_history,
151
+ callback=callback,
152
+ **kwargs
153
+ )
154
+
155
+ return result
156
+
157
+
@@ -0,0 +1,16 @@
1
+ import os
2
+
3
+ from sunholo.agents import VACRoutes, create_app
4
+
5
+ from vac_service import vac_stream, vac
6
+
7
+ app = create_app(__name__)
8
+
9
+ # Register the Q&A routes with the specific interpreter functions
10
+ # creates /vac/<vector_name> and /vac/streaming/<vector_name>
11
+ VACRoutes(app, vac_stream, vac)
12
+
13
+ if __name__ == "__main__":
14
+ import os
15
+ app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)), debug=True)
16
+
@@ -0,0 +1,3 @@
1
+ from sunholo.custom_logging import setup_logging
2
+
3
+ log = setup_logging("sunholo")
File without changes
@@ -0,0 +1,78 @@
1
+ from sunholo.genai import GenAIFunctionProcessor
2
+ from sunholo.utils import ConfigManager
3
+
4
+ from my_log import log
5
+
6
+
7
+ class QuartoProcessor(GenAIFunctionProcessor):
8
+ def construct_tools(self) -> dict:
9
+ tools = self.config.vacConfig("tools")
10
+ quarto_config = tools.get("quarto")
11
+
12
+ def decide_to_go_on(go_on: bool):
13
+ """
14
+ Examine the chat history. If the answer to the user's question has been answered, then go_on=False.
15
+ If the chat history indicates the answer is still being looked for, then go_on=True.
16
+ If there is no chat history, then go_on=True.
17
+ If there is an error that can't be corrected or solved by you, then go_on=False.
18
+ If there is an error but you think you can solve it by correcting your function arguments (such as an incorrect source), then go_on=True
19
+ If you want to ask the user a question or for some more feedback, then go_on=False.
20
+
21
+ Args:
22
+ go_on: boolean Whether to continue searching or fetching from the AlloyDB database
23
+
24
+ Returns:
25
+ boolean: True to carry on, False to continue
26
+ """
27
+ return go_on
28
+
29
+ def quarto_render() -> dict:
30
+ """
31
+ ...
32
+
33
+ Args:
34
+
35
+
36
+ Returns:
37
+
38
+ """
39
+ pass
40
+
41
+ return {
42
+ "quarto_render": quarto_render,
43
+ "decide_to_go_on": decide_to_go_on
44
+ }
45
+
46
+ def quarto_content(question: str, chat_history=[]) -> str:
47
+ prompt_config = ConfigManager("quarto")
48
+ alloydb_template = prompt_config.promptConfig("quarto_template")
49
+
50
+ conversation_text = ""
51
+ for human, ai in chat_history:
52
+ conversation_text += f"Human: {human}\nAI: {ai}\n"
53
+
54
+ return alloydb_template.format(the_question=question, chat_history=conversation_text[-10000:])
55
+
56
+
57
+ def get_quarto(config:ConfigManager, processor:QuartoProcessor):
58
+
59
+ tools = config.vacConfig('tools')
60
+
61
+ if tools and tools.get('quarto'):
62
+ model_name = None
63
+ if config.vacConfig('llm') != "vertex":
64
+ model_name = 'gemini-1.5-flash'
65
+ alloydb_model = processor.get_model(
66
+ system_instruction=(
67
+ "You are a helpful Quarto agent that helps users create and render Quarto documents. "
68
+ "When you think the answer has been given to the satisfaction of the user, or you think no answer is possible, or you need user confirmation or input, you MUST use the decide_to_go_on(go_on=False) function"
69
+ "When you want to ask the question to the user, mark the go_on=False in the function"
70
+ ),
71
+ model_name=model_name
72
+ )
73
+
74
+ if alloydb_model:
75
+ return alloydb_model
76
+
77
+ log.error("Error initializing quarto model")
78
+ return None
@@ -0,0 +1,73 @@
1
+ from my_log import log
2
+ from sunholo.utils import ConfigManager
3
+
4
+ # VAC specific imports
5
+
6
+ #TODO: Developer to update to their own implementation
7
+ from sunholo.vertex import init_vertex, get_vertex_memories
8
+ from vertexai.preview.generative_models import GenerativeModel
9
+
10
+ #TODO: change this to a streaming VAC function
11
+ def vac_stream(question: str, vector_name, chat_history=[], callback=None, **kwargs):
12
+
13
+ rag_model = create_model(vector_name)
14
+
15
+ # streaming model calls
16
+ response = rag_model.generate_content(question, stream=True)
17
+ for chunk in response:
18
+ try:
19
+ callback.on_llm_new_token(token=chunk.text)
20
+ except ValueError as err:
21
+ callback.on_llm_new_token(token=str(err))
22
+
23
+ callback.on_llm_end(response=response)
24
+ log.info(f"rag_model.response: {response}")
25
+
26
+ metadata = {
27
+ "chat_history": chat_history
28
+ }
29
+
30
+ return {"answer": response.text, "metadata": metadata}
31
+
32
+
33
+
34
+ #TODO: change this to a batch VAC function
35
+ def vac(question: str, vector_name: str, chat_history=[], **kwargs):
36
+ # Create a callback that does nothing for streaming if you don't want intermediate outputs
37
+ class NoOpCallback:
38
+ def on_llm_new_token(self, token):
39
+ pass
40
+ def on_llm_end(self, response):
41
+ pass
42
+
43
+ # Use the NoOpCallback for non-streaming behavior
44
+ callback = NoOpCallback()
45
+
46
+ # Pass all arguments to vac_stream and use the final return
47
+ result = vac_stream(
48
+ question=question,
49
+ vector_name=vector_name,
50
+ chat_history=chat_history,
51
+ callback=callback,
52
+ **kwargs
53
+ )
54
+
55
+ return result
56
+
57
+
58
+ # TODO: common model setup to both batching and streaming
59
+ def create_model(vac):
60
+ config = ConfigManager(vac)
61
+
62
+ init_vertex()
63
+ corpus_tools = get_vertex_memories(config)
64
+
65
+ model = config.vacConfig("model")
66
+
67
+ # Create a gemini-pro model instance
68
+ # https://ai.google.dev/api/python/google/generativeai/GenerativeModel#streaming
69
+ rag_model = GenerativeModel(
70
+ model_name=model or "gemini-1.5-flash", tools=[corpus_tools]
71
+ )
72
+
73
+ return rag_model
File without changes
@@ -0,0 +1,17 @@
1
+ import os
2
+
3
+ from sunholo.agents import VACRoutes, create_app
4
+
5
+ from vac_service import vac_stream
6
+
7
+ app = create_app(__name__)
8
+
9
+ # Register the Q&A routes with the specific interpreter functions
10
+ # creates endpoints /vac/streaming/<vector_name> and /vac/<vector_name> etc.
11
+ VACRoutes(app, vac_stream)
12
+
13
+ # start via `python app.py`
14
+ if __name__ == "__main__":
15
+ import os
16
+ app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)), debug=True)
17
+
@@ -0,0 +1,3 @@
1
+ from sunholo.custom_logging import setup_logging
2
+
3
+ log = setup_logging("sunholo")
@@ -0,0 +1,71 @@
1
+ from my_log import log
2
+ from sunholo.utils import ConfigManager
3
+
4
+ # VAC specific imports
5
+
6
+ #TODO: Developer to update to their own implementation
7
+ from sunholo.genai import init_genai, genai_safety
8
+ import google.generativeai as genai
9
+
10
+ #TODO: change this to a streaming VAC function for your use case
11
+ def vac_stream(question: str, vector_name:str, chat_history=[], callback=None, **kwargs):
12
+
13
+ model = create_model(vector_name)
14
+
15
+ # create chat history for genai model
16
+ # https://ai.google.dev/api/generate-content
17
+ contents = []
18
+ for human, ai in chat_history:
19
+ if human:
20
+ contents.append({"role":"user", "parts":[{"text": human}]})
21
+
22
+ if ai:
23
+ contents.append({"role":"model", "parts":[{"text": ai}]})
24
+
25
+
26
+ # the user question at the end of contents list
27
+ contents.append({"role":"user", "parts":[{"text": question}]})
28
+
29
+ log.info(contents)
30
+ # streaming model calls
31
+ response = model.generate_content(contents, stream=True)
32
+ chunks=""
33
+ for chunk in response:
34
+ if chunk and chunk.text:
35
+ try:
36
+ callback.on_llm_new_token(token=chunk.text)
37
+ chunks += chunk.text
38
+ except ValueError as err:
39
+ callback.on_llm_new_token(token=str(err))
40
+
41
+ # stream has finished, full response is also returned
42
+ callback.on_llm_end(response=response)
43
+ log.info(f"model.response: {response}")
44
+
45
+ metadata = {
46
+ "question": question,
47
+ "vector_name": vector_name,
48
+ "chat_history": chat_history
49
+ }
50
+
51
+ # to not return this dict at the end of the stream, pass stream_only: true in request
52
+ return {"answer": chunks, "metadata": metadata}
53
+
54
+
55
+ # TODO: example model setup function
56
+ def create_model(vac):
57
+ config = ConfigManager(vac)
58
+
59
+ init_genai()
60
+
61
+ # get a setting from the config vacConfig object (returns None if not found)
62
+ model = config.vacConfig("model")
63
+
64
+ # Create a gemini-flash model instance
65
+ # https://ai.google.dev/api/python/google/generativeai/GenerativeModel#streaming
66
+ genai_model = genai.GenerativeModel(
67
+ model_name=model or "gemini-1.5-flash",
68
+ safety_settings=genai_safety()
69
+ )
70
+
71
+ return genai_model
File without changes
@@ -0,0 +1,49 @@
1
+ import os
2
+ import traceback
3
+
4
+ # app.py
5
+ from fastapi import FastAPI, Request
6
+ from fastapi.responses import JSONResponse
7
+
8
+ from my_log import log
9
+
10
+ app = FastAPI()
11
+
12
+ @app.get("/")
13
+ def home():
14
+ """Simple endpoint to indicate that the app is running."""
15
+ return {"message": "Hello, service!"}
16
+
17
+ @app.post("/system_service/<param>")
18
+ async def system_service(request: Request):
19
+ """
20
+ Pubsub message parsed and sent to Langfuse ID server
21
+ """
22
+ data = await request.json()
23
+
24
+ try:
25
+ #TODO: add stuff here
26
+ meta = ""
27
+ return {"status": "success", "message": meta}
28
+ except Exception as err:
29
+ log.error(f'EVAL_ERROR: Error when sending {data} to /pubsub_to_langfuse: {str(err)} traceback: {traceback.format_exc()}')
30
+ return JSONResponse(status_code=200, content={"status": "error", "message": f'{str(err)} traceback: {traceback.format_exc()}'})
31
+
32
+ @app.post("/test_endpoint")
33
+ async def test_me(request: Request):
34
+ """
35
+ Endpoint to send trace_ids directly for evals then sent to Langfuse ID server
36
+ """
37
+ data = await request.json()
38
+
39
+ try:
40
+ #TODO: do something here
41
+ meta = ""
42
+ return {"status": "success", "message": meta}
43
+ except Exception as err:
44
+ log.error(f'EVAL_ERROR: Error when sending {data} to /direct_evals: {str(err)} traceback: {traceback.format_exc()}')
45
+ return JSONResponse(status_code=500, content={"status": "error", "message": f'{str(err)} traceback: {traceback.format_exc()}'})
46
+
47
+ if __name__ == "__main__":
48
+ import uvicorn
49
+ uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)), debug=True)
@@ -0,0 +1,3 @@
1
+ from sunholo.custom_logging import setup_logging
2
+
3
+ log = setup_logging("system")
@@ -43,7 +43,7 @@ def load_gitignore_patterns(gitignore_path):
43
43
  """
44
44
  with open(gitignore_path, 'r') as f:
45
45
  patterns = [line.strip() for line in f if line.strip() and not line.startswith('#')]
46
- patterns.extend(["*.git/*", "*.terraform/*"])
46
+ patterns.extend([".git/", ".terraform/"]) # More precise pattern matching
47
47
  return patterns
48
48
 
49
49
  def should_ignore(file_path, patterns):
@@ -62,11 +62,18 @@ def should_ignore(file_path, patterns):
62
62
  True
63
63
  """
64
64
  rel_path = os.path.relpath(file_path)
65
-
65
+
66
66
  for pattern in patterns:
67
- if fnmatch(rel_path, pattern) or fnmatch(os.path.basename(rel_path), pattern):
67
+ # Handle directory patterns ending with /
68
+ if pattern.endswith('/'):
69
+ if any(part == pattern[:-1] for part in rel_path.split(os.sep)):
70
+ print(f"Ignoring {rel_path}")
71
+ return True
72
+ # Handle file patterns
73
+ elif fnmatch(rel_path, pattern):
74
+ print(f"Ignoring {rel_path}")
68
75
  return True
69
-
76
+
70
77
  return False
71
78
 
72
79
 
@@ -1,12 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: sunholo
3
- Version: 0.116.2
3
+ Version: 0.118.1
4
4
  Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
5
- Home-page: https://github.com/sunholo-data/sunholo-py
6
- Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.116.2.tar.gz
7
- Author: Holosun ApS
8
- Author-email: multivac@sunholo.com
5
+ Author-email: Holosun ApS <multivac@sunholo.com>
9
6
  License: Apache License, Version 2.0
7
+ Project-URL: Homepage, https://github.com/sunholo-data/sunholo-py
8
+ Project-URL: Download, https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.118.0.tar.gz
10
9
  Keywords: llms,devops,google_cloud_platform
11
10
  Classifier: Development Status :: 3 - Alpha
12
11
  Classifier: Intended Audience :: Developers
@@ -16,6 +15,7 @@ Classifier: Programming Language :: Python :: 3
16
15
  Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE.txt
21
21
  Requires-Dist: aiohttp
@@ -24,6 +24,9 @@ Requires-Dist: pydantic
24
24
  Requires-Dist: requests
25
25
  Requires-Dist: ruamel.yaml
26
26
  Requires-Dist: tenacity
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest; extra == "test"
29
+ Requires-Dist: pytest-cov; extra == "test"
27
30
  Provides-Extra: all
28
31
  Requires-Dist: aiohttp; extra == "all"
29
32
  Requires-Dist: anthropic[vertex]; extra == "all"
@@ -47,6 +50,7 @@ Requires-Dist: google-cloud-pubsub; extra == "all"
47
50
  Requires-Dist: google-cloud-discoveryengine; extra == "all"
48
51
  Requires-Dist: google-cloud-texttospeech; extra == "all"
49
52
  Requires-Dist: google-generativeai>=0.7.1; extra == "all"
53
+ Requires-Dist: google-genai; extra == "all"
50
54
  Requires-Dist: gunicorn; extra == "all"
51
55
  Requires-Dist: httpcore; extra == "all"
52
56
  Requires-Dist: httpx; extra == "all"
@@ -64,6 +68,7 @@ Requires-Dist: langchain-unstructured; extra == "all"
64
68
  Requires-Dist: langfuse; extra == "all"
65
69
  Requires-Dist: mcp; extra == "all"
66
70
  Requires-Dist: numpy; extra == "all"
71
+ Requires-Dist: opencv-python; extra == "all"
67
72
  Requires-Dist: pg8000; extra == "all"
68
73
  Requires-Dist: pgvector; extra == "all"
69
74
  Requires-Dist: pillow; extra == "all"
@@ -118,9 +123,9 @@ Requires-Dist: unstructured[all-docs,local-inference]; extra == "pipeline"
118
123
  Provides-Extra: gcp
119
124
  Requires-Dist: anthropic[vertex]; extra == "gcp"
120
125
  Requires-Dist: google-api-python-client; extra == "gcp"
121
- Requires-Dist: google-cloud-alloydb-connector[pg8000]; extra == "gcp"
122
126
  Requires-Dist: google-auth-httplib2; extra == "gcp"
123
127
  Requires-Dist: google-auth-oauthlib; extra == "gcp"
128
+ Requires-Dist: google-cloud-alloydb-connector[pg8000]; extra == "gcp"
124
129
  Requires-Dist: google-cloud-aiplatform>=1.58.0; extra == "gcp"
125
130
  Requires-Dist: google-cloud-bigquery; extra == "gcp"
126
131
  Requires-Dist: google-cloud-build; extra == "gcp"
@@ -130,6 +135,7 @@ Requires-Dist: google-cloud-logging; extra == "gcp"
130
135
  Requires-Dist: google-cloud-pubsub; extra == "gcp"
131
136
  Requires-Dist: google-cloud-discoveryengine; extra == "gcp"
132
137
  Requires-Dist: google-cloud-texttospeech; extra == "gcp"
138
+ Requires-Dist: google-genai; extra == "gcp"
133
139
  Requires-Dist: google-generativeai>=0.8.3; extra == "gcp"
134
140
  Requires-Dist: langchain-google-genai>=2.0.0; extra == "gcp"
135
141
  Requires-Dist: langchain_google_alloydb_pg>=0.2.2; extra == "gcp"
@@ -164,6 +170,8 @@ Provides-Extra: tts
164
170
  Requires-Dist: google-cloud-texttospeech; extra == "tts"
165
171
  Requires-Dist: numpy; extra == "tts"
166
172
  Requires-Dist: sounddevice; extra == "tts"
173
+ Provides-Extra: video
174
+ Requires-Dist: opencv-python; extra == "video"
167
175
 
168
176
  [![PyPi Version](https://img.shields.io/pypi/v/sunholo.svg)](https://pypi.python.org/pypi/sunholo/)
169
177