sunholo 0.121.0__py3-none-any.whl → 0.123.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sunholo/__init__.py +4 -0
- sunholo/database/alloydb_client.py +164 -1
- sunholo/ollama/__init__.py +0 -0
- sunholo/ollama/ollama_images.py +71 -0
- {sunholo-0.121.0.dist-info → sunholo-0.123.0.dist-info}/METADATA +8 -2
- {sunholo-0.121.0.dist-info → sunholo-0.123.0.dist-info}/RECORD +10 -8
- {sunholo-0.121.0.dist-info → sunholo-0.123.0.dist-info}/WHEEL +1 -1
- {sunholo-0.121.0.dist-info → sunholo-0.123.0.dist-info}/entry_points.txt +0 -0
- {sunholo-0.121.0.dist-info → sunholo-0.123.0.dist-info/licenses}/LICENSE.txt +0 -0
- {sunholo-0.121.0.dist-info → sunholo-0.123.0.dist-info}/top_level.txt +0 -0
sunholo/__init__.py
CHANGED
@@ -15,6 +15,8 @@ from . import invoke
|
|
15
15
|
from . import langfuse
|
16
16
|
from . import llamaindex
|
17
17
|
from . import lookup
|
18
|
+
from . import mcp
|
19
|
+
from . import ollama
|
18
20
|
from . import pubsub
|
19
21
|
from . import qna
|
20
22
|
from . import senses
|
@@ -43,6 +45,8 @@ __all__ = ['agents',
|
|
43
45
|
'langfuse',
|
44
46
|
'llamaindex',
|
45
47
|
'lookup',
|
48
|
+
'mcp',
|
49
|
+
'ollama',
|
46
50
|
'pubsub',
|
47
51
|
'qna',
|
48
52
|
'senses',
|
@@ -8,6 +8,7 @@ except ImportError:
|
|
8
8
|
AlloyDBEngine = None
|
9
9
|
pass
|
10
10
|
|
11
|
+
import json
|
11
12
|
from .database import get_vector_size
|
12
13
|
from .uuid import generate_uuid_from_object_id
|
13
14
|
from ..custom_logging import log
|
@@ -492,4 +493,166 @@ class AlloyDBClient:
|
|
492
493
|
'''
|
493
494
|
self.execute_sql(sql)
|
494
495
|
|
495
|
-
self.grant_table_permissions(vectorstore_id, users)
|
496
|
+
self.grant_table_permissions(vectorstore_id, users)
|
497
|
+
|
498
|
+
async def _execute_sql_async_pg8000(self, sql_statement, values=None):
|
499
|
+
"""Executes a given SQL statement asynchronously with error handling."""
|
500
|
+
sql_ = sqlalchemy.text(sql_statement)
|
501
|
+
result = None
|
502
|
+
async with self.engine.connect() as conn:
|
503
|
+
try:
|
504
|
+
log.info(f"Executing SQL statement asynchronously: {sql_}")
|
505
|
+
if values:
|
506
|
+
result = await conn.execute(sql_, values)
|
507
|
+
else:
|
508
|
+
result = await conn.execute(sql_)
|
509
|
+
except DatabaseError as e:
|
510
|
+
if "already exists" in str(e):
|
511
|
+
log.warning(f"Error ignored: {str(e)}. Assuming object already exists.")
|
512
|
+
else:
|
513
|
+
raise
|
514
|
+
finally:
|
515
|
+
await conn.close()
|
516
|
+
|
517
|
+
return result
|
518
|
+
|
519
|
+
async def _execute_sql_async_langchain(self, sql_statement, values=None):
|
520
|
+
"""Execute SQL asynchronously using langchain engine"""
|
521
|
+
if values:
|
522
|
+
# Implement parameterized queries for langchain engine
|
523
|
+
# This would need to be adjusted based on how langchain engine handles parameters
|
524
|
+
log.warning("Parameterized queries may not be fully supported with langchain engine")
|
525
|
+
# For now, attempt a basic string substitution (not ideal for production)
|
526
|
+
for value in values:
|
527
|
+
if isinstance(value, str):
|
528
|
+
sql_statement = sql_statement.replace("%s", f"'{value}'", 1)
|
529
|
+
else:
|
530
|
+
sql_statement = sql_statement.replace("%s", str(value), 1)
|
531
|
+
|
532
|
+
return await self.engine._afetch(query=sql_statement)
|
533
|
+
|
534
|
+
async def create_table_from_schema(self, table_name: str, schema_data: dict, users: list = None):
|
535
|
+
"""
|
536
|
+
Creates or ensures a table exists based on the structure of the provided schema data.
|
537
|
+
|
538
|
+
Args:
|
539
|
+
table_name (str): Name of the table to create
|
540
|
+
schema_data (dict): Data structure that matches the expected schema
|
541
|
+
users (list, optional): List of users to grant permissions to
|
542
|
+
|
543
|
+
Returns:
|
544
|
+
Result of SQL execution
|
545
|
+
"""
|
546
|
+
# Generate column definitions from schema data
|
547
|
+
columns = []
|
548
|
+
for key, value in schema_data.items():
|
549
|
+
if isinstance(value, dict):
|
550
|
+
# For nested objects, store as JSONB
|
551
|
+
columns.append(f'"{key}" JSONB')
|
552
|
+
elif isinstance(value, list):
|
553
|
+
# For arrays, store as JSONB
|
554
|
+
columns.append(f'"{key}" JSONB')
|
555
|
+
elif isinstance(value, int):
|
556
|
+
columns.append(f'"{key}" INTEGER')
|
557
|
+
elif isinstance(value, float):
|
558
|
+
columns.append(f'"{key}" NUMERIC')
|
559
|
+
elif isinstance(value, bool):
|
560
|
+
columns.append(f'"{key}" BOOLEAN')
|
561
|
+
else:
|
562
|
+
# Default to TEXT for strings and other types
|
563
|
+
columns.append(f'"{key}" TEXT')
|
564
|
+
|
565
|
+
# Add metadata columns
|
566
|
+
columns.extend([
|
567
|
+
'"source" TEXT',
|
568
|
+
'"extraction_date" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP',
|
569
|
+
'"extraction_backend" TEXT',
|
570
|
+
'"extraction_model" TEXT'
|
571
|
+
])
|
572
|
+
|
573
|
+
# Create SQL statement for table creation
|
574
|
+
columns_sql = ", ".join(columns)
|
575
|
+
sql = f'''
|
576
|
+
CREATE TABLE IF NOT EXISTS "{table_name}" (
|
577
|
+
id SERIAL PRIMARY KEY,
|
578
|
+
{columns_sql}
|
579
|
+
)
|
580
|
+
'''
|
581
|
+
|
582
|
+
# Execute SQL to create table
|
583
|
+
result = await self.execute_sql_async(sql)
|
584
|
+
log.info(f"Created or ensured table {table_name} exists")
|
585
|
+
|
586
|
+
# Grant permissions if users are provided
|
587
|
+
if users:
|
588
|
+
for user in users:
|
589
|
+
await self.execute_sql_async(f'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE "{table_name}" TO "{user}";')
|
590
|
+
|
591
|
+
return result
|
592
|
+
|
593
|
+
async def write_data_to_table(self, table_name: str, data: dict, metadata: dict = None):
|
594
|
+
"""
|
595
|
+
Writes data to the specified table.
|
596
|
+
|
597
|
+
Args:
|
598
|
+
table_name (str): Name of the table
|
599
|
+
data (dict): Data to write to the table
|
600
|
+
metadata (dict, optional): Additional metadata to include
|
601
|
+
|
602
|
+
Returns:
|
603
|
+
Result of SQL execution
|
604
|
+
"""
|
605
|
+
# Create copies to avoid modifying the original data
|
606
|
+
insert_data = dict(data)
|
607
|
+
|
608
|
+
# Add metadata if provided
|
609
|
+
if metadata:
|
610
|
+
insert_data["source"] = metadata.get("objectId", metadata.get("source", "unknown"))
|
611
|
+
insert_data["extraction_backend"] = metadata.get("extraction_backend", "unknown")
|
612
|
+
insert_data["extraction_model"] = metadata.get("extraction_model", "unknown")
|
613
|
+
|
614
|
+
# Prepare column names and placeholders for values
|
615
|
+
columns = [f'"{key}"' for key in insert_data.keys()]
|
616
|
+
placeholders = []
|
617
|
+
values = []
|
618
|
+
|
619
|
+
# Process values and create properly formatted placeholders
|
620
|
+
for key, value in insert_data.items():
|
621
|
+
values.append(json.dumps(value) if isinstance(value, (dict, list)) else value)
|
622
|
+
placeholders.append("%s")
|
623
|
+
|
624
|
+
# Create SQL statement for insertion
|
625
|
+
columns_str = ", ".join(columns)
|
626
|
+
placeholders_str = ", ".join(placeholders)
|
627
|
+
|
628
|
+
sql = f'''
|
629
|
+
INSERT INTO "{table_name}" ({columns_str})
|
630
|
+
VALUES ({placeholders_str})
|
631
|
+
RETURNING id
|
632
|
+
'''
|
633
|
+
|
634
|
+
# Execute SQL to insert data
|
635
|
+
result = await self.execute_sql_async(sql, values)
|
636
|
+
log.info(f"Inserted data into table {table_name}")
|
637
|
+
|
638
|
+
return result
|
639
|
+
|
640
|
+
async def execute_sql_async(self, sql_statement, values=None):
|
641
|
+
"""
|
642
|
+
Executes a given SQL statement asynchronously with optional parameter values.
|
643
|
+
|
644
|
+
Args:
|
645
|
+
sql_statement (str): The SQL statement to execute
|
646
|
+
values (list, optional): Values for parameterized query
|
647
|
+
|
648
|
+
Returns:
|
649
|
+
Result of SQL execution
|
650
|
+
"""
|
651
|
+
log.info(f"Executing async SQL statement: {sql_statement}")
|
652
|
+
if self.engine_type == "pg8000":
|
653
|
+
result = await self._execute_sql_async_pg8000(sql_statement, values)
|
654
|
+
elif self.engine_type == "langchain":
|
655
|
+
result = await self._execute_sql_async_langchain(sql_statement, values)
|
656
|
+
|
657
|
+
return result
|
658
|
+
|
File without changes
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import os.path
|
2
|
+
import argparse
|
3
|
+
from typing import List, Optional
|
4
|
+
import sys
|
5
|
+
from ..custom_logging import log
|
6
|
+
|
7
|
+
try:
|
8
|
+
import PIL.Image
|
9
|
+
from ollama import generate
|
10
|
+
except ImportError:
|
11
|
+
generate = None
|
12
|
+
|
13
|
+
CHAT_MODEL_NAME = os.getenv("MODEL_NAME_LATEST")
|
14
|
+
|
15
|
+
def chat_ollama(msg, model_name, the_images=None):
|
16
|
+
|
17
|
+
if not generate:
|
18
|
+
raise ImportError("Import ollama via `pip install ollama`")
|
19
|
+
|
20
|
+
chat_images = []
|
21
|
+
if the_images:
|
22
|
+
for the_image in the_images:
|
23
|
+
chat_image = PIL.Image.open(the_image)
|
24
|
+
chat_images.append(chat_image)
|
25
|
+
|
26
|
+
log.info(f"Ollama [{model_name}]: Chatting...{msg=}")
|
27
|
+
for response in generate(model_name, msg, images=chat_images, stream=True):
|
28
|
+
print(response['response'], end='', flush=True)
|
29
|
+
|
30
|
+
def main():
|
31
|
+
parser = argparse.ArgumentParser(description='Chat with Ollama models from the command line')
|
32
|
+
parser.add_argument('--model', '-m', type=str, default=CHAT_MODEL_NAME,
|
33
|
+
help='Model name to use (defaults to MODEL_NAME_LATEST env var)')
|
34
|
+
parser.add_argument('--images', '-i', type=str, nargs='+',
|
35
|
+
help='Image file paths to include in the prompt')
|
36
|
+
parser.add_argument('--message', '-p', type=str,
|
37
|
+
help='Message to send')
|
38
|
+
|
39
|
+
args = parser.parse_args()
|
40
|
+
|
41
|
+
if not args.model:
|
42
|
+
print("Error: No model specified. Either set MODEL_NAME_LATEST environment variable or use --model flag.")
|
43
|
+
sys.exit(1)
|
44
|
+
|
45
|
+
# If no message provided via args, read from stdin
|
46
|
+
if not args.message:
|
47
|
+
print(f"Enter your message to {args.model} (Ctrl+D to send):")
|
48
|
+
user_input = sys.stdin.read().strip()
|
49
|
+
else:
|
50
|
+
user_input = args.message
|
51
|
+
|
52
|
+
if not user_input:
|
53
|
+
print("Error: Empty message. Exiting.")
|
54
|
+
sys.exit(1)
|
55
|
+
|
56
|
+
try:
|
57
|
+
chat_ollama(user_input, args.model, args.images)
|
58
|
+
print() # Add a newline after the response
|
59
|
+
except ImportError as e:
|
60
|
+
print(f"Error: {e}")
|
61
|
+
sys.exit(1)
|
62
|
+
except Exception as e:
|
63
|
+
print(f"Error: {e}")
|
64
|
+
sys.exit(1)
|
65
|
+
|
66
|
+
if __name__ == "__main__":
|
67
|
+
# uv run src/sunholo/ollama/ollama_images.py --model=gemma3:12b - chat and then CTRL+D
|
68
|
+
# uv run src/sunholo/ollama/ollama_images.py --model gemma3:12b --message "Tell me about quantum computing"
|
69
|
+
|
70
|
+
|
71
|
+
main()
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: sunholo
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.123.0
|
4
4
|
Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
|
5
5
|
Author-email: Holosun ApS <multivac@sunholo.com>
|
6
6
|
License: Apache License, Version 2.0
|
@@ -20,6 +20,8 @@ Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE.txt
|
21
21
|
Requires-Dist: aiohttp
|
22
22
|
Requires-Dist: google-auth
|
23
|
+
Requires-Dist: ollama>=0.4.7
|
24
|
+
Requires-Dist: pillow>=11.0.0
|
23
25
|
Requires-Dist: pydantic
|
24
26
|
Requires-Dist: requests
|
25
27
|
Requires-Dist: ruamel.yaml
|
@@ -144,6 +146,9 @@ Requires-Dist: langchain-google-genai>=2.0.0; extra == "gcp"
|
|
144
146
|
Requires-Dist: langchain_google_alloydb_pg>=0.2.2; extra == "gcp"
|
145
147
|
Requires-Dist: langchain-google-vertexai; extra == "gcp"
|
146
148
|
Requires-Dist: pillow; extra == "gcp"
|
149
|
+
Provides-Extra: ollama
|
150
|
+
Requires-Dist: pillow; extra == "ollama"
|
151
|
+
Requires-Dist: ollama>=0.4.7; extra == "ollama"
|
147
152
|
Provides-Extra: openai
|
148
153
|
Requires-Dist: langchain-openai>=0.3.2; extra == "openai"
|
149
154
|
Requires-Dist: tiktoken; extra == "openai"
|
@@ -175,6 +180,7 @@ Requires-Dist: numpy; extra == "tts"
|
|
175
180
|
Requires-Dist: sounddevice; extra == "tts"
|
176
181
|
Provides-Extra: video
|
177
182
|
Requires-Dist: opencv-python; extra == "video"
|
183
|
+
Dynamic: license-file
|
178
184
|
|
179
185
|
[](https://pypi.python.org/pypi/sunholo/)
|
180
186
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
sunholo/__init__.py,sha256=
|
1
|
+
sunholo/__init__.py,sha256=InRbX4V0-qdNHo9zYH3GEye7ASLR6LX8-SMvPV4Jsaw,1212
|
2
2
|
sunholo/custom_logging.py,sha256=YfIN1oP3dOEkkYkyRBU8BGS3uJFGwUDsFCl8mIVbwvE,12225
|
3
3
|
sunholo/langchain_types.py,sha256=uZ4zvgej_f7pLqjtu4YP7qMC_eZD5ym_5x4pyvA1Ih4,1834
|
4
4
|
sunholo/agents/__init__.py,sha256=X2I3pPkGeKWjc3d0QgSpkTyqD8J8JtrEWqwrumf1MMc,391
|
@@ -60,7 +60,7 @@ sunholo/components/retriever.py,sha256=Wmchv3huAM4w7DIS-a5Lp9Hi7M8pE6vZdxgseiT9S
|
|
60
60
|
sunholo/components/vectorstore.py,sha256=k7GS1Y5c6ZGXSDAJvyCes6dTjhDAi0fjGbVLqpyfzBc,5918
|
61
61
|
sunholo/database/__init__.py,sha256=bpB5Nk21kwqYj-qdVnvNgXjLsbflnH4g-San7OHMqR4,283
|
62
62
|
sunholo/database/alloydb.py,sha256=x1zUMB-EVWbE2Zvp4nAs2Z-tB_kOZmS45H2lwVHdYnk,11678
|
63
|
-
sunholo/database/alloydb_client.py,sha256=
|
63
|
+
sunholo/database/alloydb_client.py,sha256=uUJN-FYPmGAb9rzGVdg7ds38YhwKBQCXPqUtdOzBQCg,25123
|
64
64
|
sunholo/database/database.py,sha256=VqhZdkXUNdvWn8sUcUV3YNby1JDVf7IykPVXWBtxo9U,7361
|
65
65
|
sunholo/database/lancedb.py,sha256=DyfZntiFKBlVPaFooNN1Z6Pl-LAs4nxWKKuq8GBqN58,715
|
66
66
|
sunholo/database/static_dbs.py,sha256=8cvcMwUK6c32AS2e_WguKXWMkFf5iN3g9WHzsh0C07Q,442
|
@@ -112,6 +112,8 @@ sunholo/lookup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
112
|
sunholo/lookup/model_lookup.yaml,sha256=O7o-jP53MLA06C8pI-ILwERShO-xf6z_258wtpZBv6A,739
|
113
113
|
sunholo/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
114
114
|
sunholo/mcp/cli.py,sha256=d24nnVzhZYz4AWgTqmN-qjKG4rPbf8RhdmEOHZkBHy8,10570
|
115
|
+
sunholo/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
116
|
+
sunholo/ollama/ollama_images.py,sha256=H2cpcNu88R4TwyfL_nnqkQhdvBQ2FPCAy4Ok__0yQmo,2351
|
115
117
|
sunholo/pubsub/__init__.py,sha256=DfTEk4zmCfqn6gFxRrqDO0pOrvXTDqH-medpgYO4PGw,117
|
116
118
|
sunholo/pubsub/process_pubsub.py,sha256=rN2N4WM6PZkMKDrdT8pnEfTvsXACRyJFqIHJQCbuxLs,3088
|
117
119
|
sunholo/pubsub/pubsub_manager.py,sha256=19w_N0LiG-wgVWvgJ13b8BUeN8ZzgSPXAhPmL1HRRSI,6966
|
@@ -166,9 +168,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
|
|
166
168
|
sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
|
167
169
|
sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
|
168
170
|
sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
|
169
|
-
sunholo-0.
|
170
|
-
sunholo-0.
|
171
|
-
sunholo-0.
|
172
|
-
sunholo-0.
|
173
|
-
sunholo-0.
|
174
|
-
sunholo-0.
|
171
|
+
sunholo-0.123.0.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
|
172
|
+
sunholo-0.123.0.dist-info/METADATA,sha256=ue-wXoQBj7xkEm3nzOg0to1O3counnyi7K3RJfzcv0M,10001
|
173
|
+
sunholo-0.123.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
174
|
+
sunholo-0.123.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
|
175
|
+
sunholo-0.123.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
|
176
|
+
sunholo-0.123.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|