sunholo 0.121.0__py3-none-any.whl → 0.123.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.
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,220 @@ 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 check_connection(self):
535
+ """
536
+ Checks if the database connection is still valid.
537
+
538
+ Returns:
539
+ bool: True if connection is valid, False otherwise
540
+ """
541
+ try:
542
+ # Simple query to check connection
543
+ result = await self.execute_sql_async("SELECT 1")
544
+ return True
545
+ except Exception as e:
546
+ log.warning(f"Database connection check failed: {e}")
547
+ return False
548
+
549
+ async def ensure_connected(self):
550
+ """
551
+ Ensures the database connection is valid, attempting to reconnect if necessary.
552
+
553
+ Returns:
554
+ bool: True if connection is valid or reconnection successful, False otherwise
555
+ """
556
+ if await self.check_connection():
557
+ return True
558
+
559
+ try:
560
+ # Attempt to reconnect - implementation depends on your database driver
561
+ if self.engine_type == "pg8000":
562
+ # Re-create the engine
563
+ self.engine = self._create_engine_from_pg8000(self.user, self.password, self.database)
564
+ elif self.engine_type == "langchain":
565
+ # Re-create the engine
566
+ self.engine = self._create_engine()
567
+
568
+ log.info(f"Successfully reconnected to AlloyDB")
569
+ return True
570
+ except Exception as e:
571
+ log.error(f"Failed to reconnect to AlloyDB: {e}")
572
+ return False
573
+
574
+ async def close(self):
575
+ """
576
+ Properly close the database connection.
577
+ """
578
+ try:
579
+ if self.engine_type == "pg8000":
580
+ # Close engine or connector
581
+ if hasattr(self, 'connector'):
582
+ await self.connector.close()
583
+ # For langchain engine, additional cleanup might be needed
584
+ log.info("Closed AlloyDB connection")
585
+ except Exception as e:
586
+ log.warning(f"Error closing AlloyDB connection: {e}")
587
+
588
+ async def create_table_from_schema(self, table_name: str, schema_data: dict, users: list = None):
589
+ """
590
+ Creates or ensures a table exists based on the structure of the provided schema data.
591
+
592
+ Args:
593
+ table_name (str): Name of the table to create
594
+ schema_data (dict): Data structure that matches the expected schema
595
+ users (list, optional): List of users to grant permissions to
596
+
597
+ Returns:
598
+ Result of SQL execution
599
+ """
600
+ # Generate column definitions from schema data
601
+ columns = []
602
+ for key, value in schema_data.items():
603
+ if isinstance(value, dict):
604
+ # For nested objects, store as JSONB
605
+ columns.append(f'"{key}" JSONB')
606
+ elif isinstance(value, list):
607
+ # For arrays, store as JSONB
608
+ columns.append(f'"{key}" JSONB')
609
+ elif isinstance(value, int):
610
+ columns.append(f'"{key}" INTEGER')
611
+ elif isinstance(value, float):
612
+ columns.append(f'"{key}" NUMERIC')
613
+ elif isinstance(value, bool):
614
+ columns.append(f'"{key}" BOOLEAN')
615
+ else:
616
+ # Default to TEXT for strings and other types
617
+ columns.append(f'"{key}" TEXT')
618
+
619
+ # Add metadata columns
620
+ columns.extend([
621
+ '"source" TEXT',
622
+ '"extraction_date" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP',
623
+ '"extraction_backend" TEXT',
624
+ '"extraction_model" TEXT'
625
+ ])
626
+
627
+ # Create SQL statement for table creation
628
+ columns_sql = ", ".join(columns)
629
+ sql = f'''
630
+ CREATE TABLE IF NOT EXISTS "{table_name}" (
631
+ id SERIAL PRIMARY KEY,
632
+ {columns_sql}
633
+ )
634
+ '''
635
+
636
+ # Execute SQL to create table
637
+ result = await self.execute_sql_async(sql)
638
+ log.info(f"Created or ensured table {table_name} exists")
639
+
640
+ # Grant permissions if users are provided
641
+ if users:
642
+ for user in users:
643
+ await self.execute_sql_async(f'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE "{table_name}" TO "{user}";')
644
+
645
+ return result
646
+
647
+ async def write_data_to_table(self, table_name: str, data: dict, metadata: dict = None):
648
+ """
649
+ Writes data to the specified table.
650
+
651
+ Args:
652
+ table_name (str): Name of the table
653
+ data (dict): Data to write to the table
654
+ metadata (dict, optional): Additional metadata to include
655
+
656
+ Returns:
657
+ Result of SQL execution
658
+ """
659
+ # Create copies to avoid modifying the original data
660
+ insert_data = dict(data)
661
+
662
+ # Add metadata if provided
663
+ if metadata:
664
+ insert_data["source"] = metadata.get("objectId", metadata.get("source", "unknown"))
665
+ insert_data["extraction_backend"] = metadata.get("extraction_backend", "unknown")
666
+ insert_data["extraction_model"] = metadata.get("extraction_model", "unknown")
667
+
668
+ # Prepare column names and placeholders for values
669
+ columns = [f'"{key}"' for key in insert_data.keys()]
670
+ placeholders = []
671
+ values = []
672
+
673
+ # Process values and create properly formatted placeholders
674
+ for key, value in insert_data.items():
675
+ values.append(json.dumps(value) if isinstance(value, (dict, list)) else value)
676
+ placeholders.append("%s")
677
+
678
+ # Create SQL statement for insertion
679
+ columns_str = ", ".join(columns)
680
+ placeholders_str = ", ".join(placeholders)
681
+
682
+ sql = f'''
683
+ INSERT INTO "{table_name}" ({columns_str})
684
+ VALUES ({placeholders_str})
685
+ RETURNING id
686
+ '''
687
+
688
+ # Execute SQL to insert data
689
+ result = await self.execute_sql_async(sql, values)
690
+ log.info(f"Inserted data into table {table_name}")
691
+
692
+ return result
693
+
694
+ async def execute_sql_async(self, sql_statement, values=None):
695
+ """
696
+ Executes a given SQL statement asynchronously with optional parameter values.
697
+
698
+ Args:
699
+ sql_statement (str): The SQL statement to execute
700
+ values (list, optional): Values for parameterized query
701
+
702
+ Returns:
703
+ Result of SQL execution
704
+ """
705
+ log.info(f"Executing async SQL statement: {sql_statement}")
706
+ if self.engine_type == "pg8000":
707
+ result = await self._execute_sql_async_pg8000(sql_statement, values)
708
+ elif self.engine_type == "langchain":
709
+ result = await self._execute_sql_async_langchain(sql_statement, values)
710
+
711
+ return result
712
+
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.2
1
+ Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.121.0
3
+ Version: 0.123.1
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
  [![PyPi Version](https://img.shields.io/pypi/v/sunholo.svg)](https://pypi.python.org/pypi/sunholo/)
180
186
 
@@ -1,4 +1,4 @@
1
- sunholo/__init__.py,sha256=Ap2yX2ITBVt_vkloYipUM8OwW14g6aor2NX7LWp0-mI,1133
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=q732tmRdSDutnUk7vRUPUPpi-yU5FK5rQko8co6yke0,19132
63
+ sunholo/database/alloydb_client.py,sha256=WpkrQmy2hK4148df-6Ys8XRjCGpObZa9Dc9TXLSX_sE,27108
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.121.0.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
170
- sunholo-0.121.0.dist-info/METADATA,sha256=xfdz083WUmJThGmmnsBzJl1kN_l5xw9L8ogD6L0i0qw,9808
171
- sunholo-0.121.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
172
- sunholo-0.121.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
173
- sunholo-0.121.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
174
- sunholo-0.121.0.dist-info/RECORD,,
171
+ sunholo-0.123.1.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
172
+ sunholo-0.123.1.dist-info/METADATA,sha256=eMkSTIyICC03RID4bD6yN7ryL8Faglbjjj2EKsnvVf4,10001
173
+ sunholo-0.123.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
174
+ sunholo-0.123.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
175
+ sunholo-0.123.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
176
+ sunholo-0.123.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5