sunholo 0.76.9__tar.gz → 0.77.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. {sunholo-0.76.9 → sunholo-0.77.0}/PKG-INFO +2 -2
  2. {sunholo-0.76.9 → sunholo-0.77.0}/setup.py +1 -1
  3. sunholo-0.77.0/sunholo/gcs/download_folder.py +50 -0
  4. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/vertex/__init__.py +1 -1
  5. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/vertex/extensions_call.py +112 -3
  6. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/vertex/extensions_class.py +78 -17
  7. sunholo-0.77.0/sunholo/vertex/genai_functions.py +61 -0
  8. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/vertex/init.py +1 -1
  9. sunholo-0.77.0/sunholo/vertex/type_dict_to_json.py +127 -0
  10. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo.egg-info/PKG-INFO +2 -2
  11. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo.egg-info/SOURCES.txt +3 -0
  12. {sunholo-0.76.9 → sunholo-0.77.0}/LICENSE.txt +0 -0
  13. {sunholo-0.76.9 → sunholo-0.77.0}/MANIFEST.in +0 -0
  14. {sunholo-0.76.9 → sunholo-0.77.0}/README.md +0 -0
  15. {sunholo-0.76.9 → sunholo-0.77.0}/setup.cfg +0 -0
  16. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/__init__.py +0 -0
  17. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/__init__.py +0 -0
  18. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/chat_history.py +0 -0
  19. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/dispatch_to_qa.py +0 -0
  20. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/fastapi/__init__.py +0 -0
  21. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/fastapi/base.py +0 -0
  22. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/fastapi/qna_routes.py +0 -0
  23. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/flask/__init__.py +0 -0
  24. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/flask/base.py +0 -0
  25. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/flask/qna_routes.py +0 -0
  26. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/flask/vac_routes.py +0 -0
  27. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/langserve.py +0 -0
  28. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/pubsub.py +0 -0
  29. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/route.py +0 -0
  30. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/special_commands.py +0 -0
  31. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/agents/swagger.py +0 -0
  32. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/archive/__init__.py +0 -0
  33. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/archive/archive.py +0 -0
  34. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/auth/__init__.py +0 -0
  35. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/auth/gcloud.py +0 -0
  36. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/auth/refresh.py +0 -0
  37. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/auth/run.py +0 -0
  38. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/azure/__init__.py +0 -0
  39. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/azure/event_grid.py +0 -0
  40. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/bots/__init__.py +0 -0
  41. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/bots/discord.py +0 -0
  42. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/bots/github_webhook.py +0 -0
  43. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/bots/webapp.py +0 -0
  44. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/__init__.py +0 -0
  45. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/azure.py +0 -0
  46. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/doc_handling.py +0 -0
  47. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/images.py +0 -0
  48. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/loaders.py +0 -0
  49. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/message_data.py +0 -0
  50. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/pdfs.py +0 -0
  51. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/process_chunker_data.py +0 -0
  52. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/publish.py +0 -0
  53. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/pubsub.py +0 -0
  54. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/chunker/splitter.py +0 -0
  55. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/__init__.py +0 -0
  56. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/chat_vac.py +0 -0
  57. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/cli.py +0 -0
  58. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/cli_init.py +0 -0
  59. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/configs.py +0 -0
  60. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/deploy.py +0 -0
  61. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/embedder.py +0 -0
  62. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/merge_texts.py +0 -0
  63. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/run_proxy.py +0 -0
  64. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/sun_rich.py +0 -0
  65. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/swagger.py +0 -0
  66. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/cli/vertex.py +0 -0
  67. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/components/__init__.py +0 -0
  68. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/components/llm.py +0 -0
  69. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/components/retriever.py +0 -0
  70. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/components/vectorstore.py +0 -0
  71. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/__init__.py +0 -0
  72. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/alloydb.py +0 -0
  73. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/alloydb_client.py +0 -0
  74. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/database.py +0 -0
  75. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/lancedb.py +0 -0
  76. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/sql/sb/create_function.sql +0 -0
  77. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/sql/sb/create_function_time.sql +0 -0
  78. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/sql/sb/create_table.sql +0 -0
  79. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  80. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/sql/sb/return_sources.sql +0 -0
  81. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/sql/sb/setup.sql +0 -0
  82. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/static_dbs.py +0 -0
  83. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/database/uuid.py +0 -0
  84. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/discovery_engine/__init__.py +0 -0
  85. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/discovery_engine/chunker_handler.py +0 -0
  86. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/discovery_engine/create_new.py +0 -0
  87. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  88. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/embedder/__init__.py +0 -0
  89. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/embedder/embed_chunk.py +0 -0
  90. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/gcs/__init__.py +0 -0
  91. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/gcs/add_file.py +0 -0
  92. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/gcs/download_url.py +0 -0
  93. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/gcs/metadata.py +0 -0
  94. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/invoke/__init__.py +0 -0
  95. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/invoke/invoke_vac_utils.py +0 -0
  96. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/langfuse/__init__.py +0 -0
  97. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/langfuse/callback.py +0 -0
  98. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/langfuse/prompts.py +0 -0
  99. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/llamaindex/__init__.py +0 -0
  100. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/llamaindex/generate.py +0 -0
  101. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/llamaindex/get_files.py +0 -0
  102. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/llamaindex/import_files.py +0 -0
  103. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/logging.py +0 -0
  104. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/lookup/__init__.py +0 -0
  105. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/lookup/model_lookup.yaml +0 -0
  106. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/patches/__init__.py +0 -0
  107. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/patches/langchain/__init__.py +0 -0
  108. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/patches/langchain/lancedb.py +0 -0
  109. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/patches/langchain/vertexai.py +0 -0
  110. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/pubsub/__init__.py +0 -0
  111. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/pubsub/process_pubsub.py +0 -0
  112. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/pubsub/pubsub_manager.py +0 -0
  113. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/qna/__init__.py +0 -0
  114. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/qna/parsers.py +0 -0
  115. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/qna/retry.py +0 -0
  116. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/streaming/__init__.py +0 -0
  117. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/streaming/content_buffer.py +0 -0
  118. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/streaming/langserve.py +0 -0
  119. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/streaming/stream_lookup.py +0 -0
  120. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/streaming/streaming.py +0 -0
  121. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/summarise/__init__.py +0 -0
  122. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/summarise/summarise.py +0 -0
  123. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/tools/__init__.py +0 -0
  124. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/tools/web_browser.py +0 -0
  125. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/__init__.py +0 -0
  126. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/api_key.py +0 -0
  127. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/big_context.py +0 -0
  128. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/config.py +0 -0
  129. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/config_class.py +0 -0
  130. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/config_schema.py +0 -0
  131. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/gcp.py +0 -0
  132. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/gcp_project.py +0 -0
  133. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/parsers.py +0 -0
  134. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/timedelta.py +0 -0
  135. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/user_ids.py +0 -0
  136. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/utils/version.py +0 -0
  137. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/vertex/memory_tools.py +0 -0
  138. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo/vertex/safety.py +0 -0
  139. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo.egg-info/dependency_links.txt +0 -0
  140. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo.egg-info/entry_points.txt +0 -0
  141. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo.egg-info/requires.txt +0 -0
  142. {sunholo-0.76.9 → sunholo-0.77.0}/sunholo.egg-info/top_level.txt +0 -0
  143. {sunholo-0.76.9 → sunholo-0.77.0}/tests/test_chat_history.py +0 -0
  144. {sunholo-0.76.9 → sunholo-0.77.0}/tests/test_config.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.76.9
3
+ Version: 0.77.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.76.9.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.77.0.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -1,7 +1,7 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
3
  # Define your base version
4
- version = '0.76.9'
4
+ version = '0.77.0'
5
5
 
6
6
  setup(
7
7
  name='sunholo',
@@ -0,0 +1,50 @@
1
+ import os
2
+
3
+ try:
4
+ from google.cloud import storage
5
+ except ImportError:
6
+ storage = None
7
+
8
+ from ..logging import log
9
+
10
+ def download_files_from_gcs(bucket_name: str, source_folder: str, destination_folder: str=None):
11
+ """
12
+ Download all files from a specified folder in a Google Cloud Storage bucket to a local directory.
13
+
14
+ Parameters:
15
+ - bucket_name: The name of the GCS bucket.
16
+ - source_folder: The folder (prefix) in the GCS bucket to download files from.
17
+ - destination_folder: The local directory to save the downloaded files, or os.getcwd() if None
18
+ """
19
+ try:
20
+ storage_client = storage.Client()
21
+ except Exception as err:
22
+ log.error(f"Error creating storage client: {str(err)}")
23
+ return None
24
+
25
+ # Get the bucket
26
+ bucket = storage_client.bucket(bucket_name)
27
+
28
+ # List blobs in the specified folder
29
+ blobs = bucket.list_blobs(prefix=source_folder)
30
+
31
+ if not destination_folder:
32
+ destination_folder = os.getcwd()
33
+
34
+ # Ensure the destination folder exists
35
+ os.makedirs(destination_folder, exist_ok=True)
36
+
37
+ for blob in blobs:
38
+ # Skip if the blob is a directory
39
+ if blob.name.endswith('/'):
40
+ continue
41
+
42
+ # Define the local path
43
+ local_path = os.path.join(destination_folder, os.path.relpath(blob.name, source_folder))
44
+
45
+ # Ensure the local folder exists
46
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
47
+
48
+ # Download the blob to a local file
49
+ blob.download_to_filename(local_path)
50
+ log.info(f"Downloaded {blob.name} to {local_path}")
@@ -2,5 +2,5 @@ from .init import init_vertex, init_genai
2
2
  from .memory_tools import get_vertex_memories, print_grounding_response, get_google_search_grounding
3
3
  from .safety import vertex_safety, genai_safety
4
4
  from .extensions_class import VertexAIExtensions
5
- from .extensions_call import get_extension_content
5
+ from .extensions_call import get_extension_content, parse_extension_input, dynamic_extension_call
6
6
 
@@ -2,6 +2,106 @@ from .extensions_class import VertexAIExtensions
2
2
  from ..utils import ConfigManager
3
3
  from ..logging import log
4
4
  import collections.abc
5
+ import json
6
+
7
+ from .genai_functions import genai_structured_output
8
+
9
+ def dynamic_extension_call(question, vac, project_id:str=None, model_name:str="models/gemini-1.5-pro", **kwargs):
10
+ config = ConfigManager(vac)
11
+
12
+ extensions = config.vacConfig('extensions')
13
+ if not extensions:
14
+ log.warning("No extensions founded for vac: {vac}")
15
+
16
+ return None
17
+
18
+ responses = []
19
+ for tool in extensions:
20
+
21
+ call_json = parse_extension_input(question,
22
+ extension_id=tool.get('extension_id'),
23
+ extension_display_name=tool.get('extension_display_name'),
24
+ config=config,
25
+ project_id=project_id,
26
+ model_name=model_name,
27
+ **kwargs)
28
+ if call_json:
29
+ question = call_json.pop('question')
30
+ extension_output = get_extension_content(question=question,
31
+ config=config,
32
+ project_id=project_id,
33
+ **call_json)
34
+ responses.append(extension_output)
35
+ else:
36
+ log.warning(f"No json found for extension {tool}")
37
+
38
+ return responses
39
+
40
+ def parse_extension_input(
41
+ question: str,
42
+ extension_id: str=None,
43
+ extension_display_name:str=None,
44
+ config: ConfigManager=None,
45
+ project_id:str=None,
46
+ model_name:str="models/gemini-1.5-pro",
47
+ **kwargs):
48
+ """
49
+ Takes a question and kwargs and makes an LLM call to extract parameters for an extension call.
50
+ If no parameters are found, returns None
51
+ Once parameters are extracted, makes the call to the extension via get_extenstion_content()
52
+
53
+ Example:
54
+ Assuming an OpenAPI configuration file as follows:
55
+
56
+ """
57
+
58
+ extensions = config.vacConfig('extensions')
59
+ ve = VertexAIExtensions(project_id)
60
+
61
+ for ext in extensions:
62
+ if extension_id == ext.get("extension_id") or extension_display_name == ext.get("extension_display_name"):
63
+ extension = ve.get_extension(extension_id=extension_id, extension_display_name=extension_display_name)
64
+ break
65
+
66
+ if not extension:
67
+ raise ValueError(f"No extension found matching {extension_id=} or {extension_display_name=}")
68
+
69
+ openapi_spec = ve.get_openapi_spec()
70
+ log.info(f"OpenAPI Spec: {openapi_spec}")
71
+
72
+ if not openapi_spec:
73
+ raise ValueError(f"No input schema detected for {extension=}")
74
+
75
+ model = genai_structured_output(
76
+ openapi_spec,
77
+ system_prompt="You are an assistant that must only parse your input into the provided json schema output. Do not attempt to answer any questions or do anything else other than extracting data into the output schema",
78
+ model_name=model_name,
79
+ **kwargs)
80
+
81
+ contents = [
82
+ "The user question may contain information that can be used to populate the output schema",
83
+ "As a minimum the question key should container the user question in 'question', but also examine the content and see if you can fill in the rest of the output schema fields",
84
+ f"User Question to parse: {question}"
85
+ ]
86
+
87
+ tokens = model.count_tokens(contents)
88
+ log.info(f"Used [{tokens}] in prompt")
89
+
90
+ json_response = model.generate_content(contents)
91
+
92
+ log.debug(f"parsed_extension_input returns: {json_response=}")
93
+
94
+ try:
95
+ json_object = json.loads(json_response.text)
96
+ log.info(f"Got valid json: {json_object}")
97
+
98
+ return json_object
99
+
100
+ except Exception as err:
101
+ log.error(f"Failed to parse GenAI output to JSON: {json_response=} - {str(err)}")
102
+
103
+ return None
104
+
5
105
 
6
106
  def get_extension_content(question: str, config: ConfigManager, project_id:str=None, **kwargs):
7
107
  """
@@ -23,8 +123,8 @@ def get_extension_content(question: str, config: ConfigManager, project_id:str=N
23
123
  vac:
24
124
  my_vac:
25
125
  extensions:
26
- - extension_id: 8524997435263549440
27
- operation_id: post_our_new_energy_invoke_one_generic
126
+ - extension_id: 8524997435263549440 # or extension_display_name:
127
+ operation_id: post_extension_invoke_one_generic
28
128
  vac: our_generic
29
129
  operation_params:
30
130
  input:
@@ -281,4 +381,13 @@ def extract_nested_value(data, key):
281
381
 
282
382
  if __name__ == "__main__":
283
383
  config = ConfigManager("one_ai")
284
- get_extension_content("What are PPAs in france like?", config=config)
384
+ #get_extension_content("What are PPAs in france like?", config=config)
385
+ parse_extension_input("What are PPAs in france like?",
386
+ extension_display_name="Our New Energy Database2",
387
+ config=config)
388
+ parse_extension_input("What are PPas in france like? Look in files within the PPA/ or /PPA2 folder, returning the whole documents",
389
+ extension_display_name="Our New Energy Database2",
390
+ config=config, model_name="models/gemini-1.5-pro")
391
+ # {'question': 'What are PPas in france like? Look in files within the PPA/ or /PPA2 folder, returning the whole documents',
392
+ # 'chat_history': [], 'source_filters': ['PPA/', '/PPA2'],
393
+ # 'source_filters_and_or': False, 'search_kwargs': {}, 'private_docs': [], 'whole_document': True}
@@ -6,13 +6,13 @@ except ImportError:
6
6
  from .init import init_vertex
7
7
  from ..logging import log
8
8
  from ..utils.gcp_project import get_gcp_project
9
- from ..utils.parsers import validate_extension_id
10
9
  from ..utils.gcp import is_running_on_cloudrun
11
10
  from ..auth import get_local_gcloud_token, get_cloud_run_token
12
11
  import base64
13
12
  import json
14
13
  from io import StringIO
15
14
  import os
15
+ import re
16
16
 
17
17
  class VertexAIExtensions:
18
18
  """
@@ -74,6 +74,7 @@ class VertexAIExtensions:
74
74
  self.bucket_name = os.getenv('EXTENSIONS_BUCKET')
75
75
  self.project_id = project_id or get_gcp_project()
76
76
  self.access_token = None
77
+ self.current_extension = None
77
78
  init_vertex(location=self.location, project_id=self.project_id)
78
79
 
79
80
  def list_extensions(self):
@@ -111,7 +112,7 @@ class VertexAIExtensions:
111
112
 
112
113
  return self_uri
113
114
 
114
- def upload_openapi_file(self, filename: str, vac:str=None):
115
+ def upload_openapi_file(self, filename: str, extension_name:str, vac:str=None):
115
116
  if vac:
116
117
  from ..agents.route import route_vac
117
118
  import yaml
@@ -130,8 +131,19 @@ class VertexAIExtensions:
130
131
  if not self.bucket_name:
131
132
  raise ValueError('Please specify env var EXTENSIONS_BUCKET for location to upload openapi spec')
132
133
 
133
- self.openapi_file_gcs = self.upload_to_gcs(filename)
134
-
134
+ upload_name = f"{extension_name}/{filename}"
135
+
136
+ self.openapi_file_gcs = self.upload_to_gcs(upload_name)
137
+
138
+ def get_openapi_spec(self, extension_id: str=None, extension_display_name:str=None):
139
+ """
140
+ Gets the openapi spec file for an extension
141
+ """
142
+ if not self.current_extension:
143
+ self.current_extension = self.get_extension(extension_id=extension_id, extension_display_name=extension_display_name)
144
+
145
+ return self.current_extension.api_spec()
146
+
135
147
  def load_tool_use_examples(self, filename: str):
136
148
  import yaml
137
149
 
@@ -155,7 +167,7 @@ class VertexAIExtensions:
155
167
  import requests
156
168
  import json
157
169
 
158
- extension = self.created_extension
170
+ extension = self.created_extension or self.current_extension
159
171
  if extension is None:
160
172
  raise ValueError("Need to create the extension first")
161
173
 
@@ -214,6 +226,38 @@ class VertexAIExtensions:
214
226
 
215
227
  return self.manifest
216
228
 
229
+ def validate_extension_id(self, ext_id: str):
230
+ """
231
+ Ensures the passed string fits the criteria for an extension ID.
232
+ If not, changes it so it will be.
233
+
234
+ Criteria:
235
+ - Length should be 4-63 characters.
236
+ - Valid characters are lowercase letters, numbers, and hyphens ("-").
237
+ - Should start with a number or a lowercase letter.
238
+
239
+ Args:
240
+ ext_id (str): The extension ID to validate and correct.
241
+
242
+ Returns:
243
+ str: The validated and corrected extension ID.
244
+ """
245
+ # Replace invalid characters
246
+ ext_id = re.sub(r'[^a-z0-9-]', '-', ext_id.lower())
247
+
248
+ # Ensure it starts with a number or a lowercase letter
249
+ if not re.match(r'^[a-z0-9]', ext_id):
250
+ ext_id = 'a' + ext_id
251
+
252
+ # Trim to 63 characters
253
+ ext_id = ext_id[:63]
254
+
255
+ # Pad to at least 4 characters
256
+ while len(ext_id) < 4:
257
+ ext_id += 'a'
258
+
259
+ return ext_id
260
+
217
261
  def create_extension(self,
218
262
  display_name: str,
219
263
  description: str,
@@ -225,7 +269,7 @@ class VertexAIExtensions:
225
269
  vac: str = None):
226
270
 
227
271
  log.info(f"Creating extension within {self.project_id=}")
228
- extension_name = f"projects/{self.project_id}/locations/us-central1/extensions/{validate_extension_id(display_name)}"
272
+ extension_name = f"projects/{self.project_id}/locations/us-central1/extensions/{self.validate_extension_id(display_name)}"
229
273
 
230
274
  if bucket_name:
231
275
  log.info(f"Setting extension bucket name to {bucket_name}")
@@ -238,7 +282,7 @@ class VertexAIExtensions:
238
282
  raise NameError(f"display_name {display_name} already exists. Delete it or rename your new extension")
239
283
 
240
284
  if open_api_file:
241
- self.upload_openapi_file(open_api_file, vac)
285
+ self.upload_openapi_file(open_api_file, self.validate_extension_id(display_name), vac)
242
286
 
243
287
  manifest = self.create_extension_manifest(
244
288
  display_name,
@@ -261,20 +305,25 @@ class VertexAIExtensions:
261
305
  log.info(f"Created Vertex Extension: {extension_name}")
262
306
 
263
307
  self.created_extension = extension
308
+ self.current_extension = extension
264
309
 
265
310
  if tool_example_file:
266
311
  self.update_tool_use_examples_via_patch()
267
312
 
268
313
  return extension.resource_name
269
-
270
- def execute_extension(
271
- self,
272
- operation_id: str,
273
- operation_params: dict,
274
- extension_id: str=None,
314
+
315
+ def get_extension(
316
+ self,
317
+ extension_id: str=None,
275
318
  extension_display_name: str=None,
276
- vac: str=None):
319
+ ):
320
+ """
321
+ Resolves the extension_id from the Display Name if not given.
322
+
323
+ Returns:
324
+ Extension object
277
325
 
326
+ """
278
327
  if extension_display_name:
279
328
  exts = self.list_extensions()
280
329
  for ext in exts:
@@ -293,8 +342,20 @@ class VertexAIExtensions:
293
342
  extension_name = self.created_extension.resource_name
294
343
  if not extension_name:
295
344
  raise ValueError("Must specify extension_id or extension_name - both were None")
345
+
346
+ self.current_extension = extensions.Extension(extension_name)
347
+
348
+ return self.current_extension
349
+
350
+ def execute_extension(
351
+ self,
352
+ operation_id: str,
353
+ operation_params: dict,
354
+ extension_id: str=None,
355
+ extension_display_name: str=None,
356
+ vac: str=None):
296
357
 
297
- extension = extensions.Extension(extension_name)
358
+ extension = self.get_extension(extension_id=extension_id, extension_display_name=extension_display_name)
298
359
 
299
360
  auth_config=None
300
361
  if not is_running_on_cloudrun():
@@ -314,14 +375,14 @@ class VertexAIExtensions:
314
375
  else:
315
376
  log.warning("No vac configuration and not running locally so no authentication being set for this extension API call")
316
377
 
317
- log.info(f"Executing extension {extension_name=} with {operation_id=} and {operation_params=}")
378
+ log.info(f"Executing extension {extension.display_name=} with {operation_id=} and {operation_params=}")
318
379
  response = extension.execute(
319
380
  operation_id=operation_id,
320
381
  operation_params=operation_params,
321
382
  runtime_auth_config=auth_config
322
383
  )
323
384
 
324
- log.info(f"Extension {extension_name=} {response=}")
385
+ log.info(f"Extension {extension.display_name=} {response=}")
325
386
 
326
387
  return response
327
388
 
@@ -0,0 +1,61 @@
1
+ try:
2
+ import google.generativeai as genai
3
+ except ImportError:
4
+ genai = None
5
+
6
+ from .init import init_genai
7
+ from .safety import genai_safety
8
+ from ..logging import log
9
+ import json
10
+ from .type_dict_to_json import describe_typed_dict, openapi_to_typed_dict, is_typed_dict
11
+
12
+ def genai_structured_output(
13
+ openapi_spec,
14
+ system_prompt: str = "",
15
+ model_name: str = "models/gemini-1.5-pro",
16
+ **kwargs):
17
+ """
18
+ Generate AI function output with the specified configuration.
19
+
20
+ Parameters:
21
+ - output_schema: The schema for the response output.
22
+ - system_prompt: Optional system prompt to guide the generation.
23
+ - model_name: The name of the model to use (default is 'models/gemini-1.5-flash').
24
+ - output_schema_json: The JSON schema with descriptions.
25
+ - **kwargs: Additional keyword arguments to customize the generation config.
26
+
27
+ Returns:
28
+ - model: The configured generative model.
29
+ """
30
+
31
+ init_genai()
32
+
33
+ # Generate the JSON schema with descriptions
34
+ output_schema, descriptions = openapi_to_typed_dict(openapi_spec, 'Input')
35
+
36
+ # Convert TypedDict to JSON schema if necessary
37
+ if is_typed_dict(output_schema):
38
+ output_schema_json = describe_typed_dict(output_schema, descriptions)
39
+
40
+ # Base generation configuration
41
+ generation_config = {
42
+ "response_mime_type": "application/json",
43
+ #"response_schema": output_schema, # didn't work yet as couldn't deal with Optional values
44
+ "temperature": 0.5
45
+ }
46
+
47
+ # Merge additional kwargs into generation_config
48
+ generation_config.update(kwargs)
49
+
50
+ if output_schema_json:
51
+ system_prompt = f"{system_prompt}\n##OUTPUT JSON SCHEMA:\n{json.dumps(output_schema_json, indent=2)}\n"
52
+
53
+ model = genai.GenerativeModel(
54
+ model_name=model_name,
55
+ safety_settings=genai_safety(),
56
+ generation_config=generation_config,
57
+ system_instruction=system_prompt,
58
+ tool_config={'function_calling_config': 'ANY'} # pro models only
59
+ )
60
+
61
+ return model
@@ -12,7 +12,7 @@ def init_genai():
12
12
  try:
13
13
  import google.generativeai as genai
14
14
  except ImportError:
15
- raise ImportError("google.generativeai not installed, please install via 'pip install sunholo[gcp]")
15
+ raise ImportError("google.generativeai not installed, please install via 'pip install sunholo'[gcp]'")
16
16
 
17
17
  GOOGLE_API_KEY=os.getenv('GOOGLE_API_KEY')
18
18
  if not GOOGLE_API_KEY:
@@ -0,0 +1,127 @@
1
+ import typing
2
+ from typing_extensions import TypedDict, Any, Dict
3
+
4
+ # Type mapping for OpenAPI types to Python types
5
+ type_mapping = {
6
+ 'string': str,
7
+ 'integer': int,
8
+ 'number': float,
9
+ 'boolean': bool,
10
+ 'array': list,
11
+ 'object': dict
12
+ }
13
+
14
+ def resolve_ref(openapi_spec: Dict[str, Any], ref: str) -> Dict[str, Any]:
15
+ """
16
+ Resolve a $ref in the OpenAPI spec.
17
+
18
+ Parameters:
19
+ - openapi_spec: The OpenAPI specification.
20
+ - ref: The reference string.
21
+
22
+ Returns:
23
+ - The resolved schema.
24
+ """
25
+ ref_path = ref.lstrip('#/').split('/')
26
+ resolved = openapi_spec
27
+ for part in ref_path:
28
+ resolved = resolved.get(part)
29
+ if resolved is None:
30
+ raise ValueError(f"Reference {ref} could not be resolved at part {part}")
31
+ return resolved
32
+
33
+ def openapi_to_typed_dict(openapi_spec: Dict[str, Any], schema_name: str) -> Any:
34
+ """
35
+ Convert an OpenAPI schema to a TypedDict dynamically.
36
+
37
+ Parameters:
38
+ - openapi_spec: The OpenAPI specification.
39
+ - schema_name: The name of the schema to convert.
40
+
41
+ Returns:
42
+ - A dynamically created TypedDict class and field descriptions.
43
+ """
44
+ schema = openapi_spec['components']['schemas'][schema_name]
45
+ properties = schema['properties']
46
+ required = schema.get('required', [])
47
+ annotations = {}
48
+ descriptions = {}
49
+
50
+ def process_property(key: str, value: Dict[str, Any]) -> Any:
51
+ """Process an individual property and update annotations and descriptions."""
52
+ if '$ref' in value:
53
+ value = resolve_ref(openapi_spec, value['$ref'])
54
+
55
+ field_type = value.get('type')
56
+ if field_type in type_mapping:
57
+ annotations[key] = type_mapping[field_type]
58
+ elif field_type == 'array':
59
+ item_ref = value['items'].get('$ref')
60
+ item_type = value['items'].get('type')
61
+ if item_ref:
62
+ item_schema = resolve_ref(openapi_spec, item_ref)
63
+ item_typed_dict, _ = openapi_to_typed_dict(openapi_spec, item_schema['title'])
64
+ annotations[key] = typing.List[item_typed_dict]
65
+ elif item_type in type_mapping:
66
+ annotations[key] = typing.List[type_mapping[item_type]]
67
+ else:
68
+ annotations[key] = typing.List[dict]
69
+ elif field_type == 'object':
70
+ annotations[key] = dict
71
+ nested_properties = value.get('properties', {})
72
+ nested_required = value.get('required', [])
73
+ nested_annotations = {}
74
+ for nested_key, nested_value in nested_properties.items():
75
+ nested_annotations[nested_key] = process_property(nested_key, nested_value)
76
+ annotations[key] = TypedDict(f"{key.capitalize()}Nested", nested_annotations, total=False)
77
+ for nested_key in nested_required:
78
+ annotations[key].__annotations__[nested_key] = nested_annotations[nested_key]
79
+
80
+ descriptions[key] = value.get('description', '')
81
+ return annotations[key]
82
+
83
+ for key, value in properties.items():
84
+ process_property(key, value)
85
+
86
+ typed_dict_cls = TypedDict(schema_name, annotations, total=False)
87
+ for key in required:
88
+ typed_dict_cls.__annotations__[key] = annotations[key]
89
+
90
+ return typed_dict_cls, descriptions
91
+
92
+ def describe_typed_dict(typed_dict_cls: Any, descriptions: Dict[str, str]) -> Dict[str, Any]:
93
+ """
94
+ Generate a dictionary with field descriptions.
95
+
96
+ Parameters:
97
+ - typed_dict_cls: The TypedDict class.
98
+ - descriptions: The dictionary containing field descriptions.
99
+
100
+ Returns:
101
+ - A dictionary with field descriptions.
102
+ """
103
+ schema = {
104
+ "type": "object",
105
+ "properties": {},
106
+ "required": []
107
+ }
108
+ for key, value in typed_dict_cls.__annotations__.items():
109
+ description = descriptions.get(key, "")
110
+ property_schema = {
111
+ "type": value.__name__.lower() if isinstance(value, type) else "object",
112
+ "description": description
113
+ }
114
+ if typing.get_origin(value) is list:
115
+ item_type = typing.get_args(value)[0]
116
+ property_schema["items"] = {"type": item_type.__name__.lower()}
117
+ elif typing.get_origin(value) is dict:
118
+ property_schema["properties"] = describe_typed_dict(value, descriptions)["properties"]
119
+ schema["properties"][key] = property_schema
120
+ if key in typed_dict_cls.__required_keys__:
121
+ schema["required"].append(key)
122
+ return schema
123
+
124
+
125
+ def is_typed_dict(cls):
126
+ """Check if a class is a TypedDict."""
127
+ return isinstance(cls, type) and issubclass(cls, dict) and hasattr(cls, '__annotations__')
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.76.9
3
+ Version: 0.77.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.76.9.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.77.0.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -86,6 +86,7 @@ sunholo/embedder/__init__.py
86
86
  sunholo/embedder/embed_chunk.py
87
87
  sunholo/gcs/__init__.py
88
88
  sunholo/gcs/add_file.py
89
+ sunholo/gcs/download_folder.py
89
90
  sunholo/gcs/download_url.py
90
91
  sunholo/gcs/metadata.py
91
92
  sunholo/invoke/__init__.py
@@ -133,8 +134,10 @@ sunholo/utils/version.py
133
134
  sunholo/vertex/__init__.py
134
135
  sunholo/vertex/extensions_call.py
135
136
  sunholo/vertex/extensions_class.py
137
+ sunholo/vertex/genai_functions.py
136
138
  sunholo/vertex/init.py
137
139
  sunholo/vertex/memory_tools.py
138
140
  sunholo/vertex/safety.py
141
+ sunholo/vertex/type_dict_to_json.py
139
142
  tests/test_chat_history.py
140
143
  tests/test_config.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes