osbot-utils 2.32.0__py3-none-any.whl → 2.34.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.
Files changed (64) hide show
  1. osbot_utils/helpers/Local_Cache.py +5 -7
  2. osbot_utils/helpers/Local_Caches.py +5 -5
  3. osbot_utils/helpers/flows/Flow.py +4 -0
  4. osbot_utils/helpers/llms/__init__.py +0 -0
  5. osbot_utils/helpers/llms/actions/LLM_Request__Execute.py +31 -0
  6. osbot_utils/helpers/llms/actions/Type_Safe__Schema_For__LLMs.py +213 -0
  7. osbot_utils/helpers/llms/actions/__init__.py +0 -0
  8. osbot_utils/helpers/llms/builders/LLM_Request__Builder.py +41 -0
  9. osbot_utils/helpers/llms/builders/LLM_Request__Builder__Open_AI.py +54 -0
  10. osbot_utils/helpers/llms/builders/LLM_Request__Factory.py +95 -0
  11. osbot_utils/helpers/llms/builders/__init__.py +0 -0
  12. osbot_utils/helpers/llms/cache/LLM_Cache__Path_Generator.py +83 -0
  13. osbot_utils/helpers/llms/cache/LLM_Request__Cache.py +112 -0
  14. osbot_utils/helpers/llms/cache/LLM_Request__Cache__File_System.py +237 -0
  15. osbot_utils/helpers/llms/cache/LLM_Request__Cache__Storage.py +85 -0
  16. osbot_utils/helpers/llms/cache/Virtual_Storage__Local__Folder.py +64 -0
  17. osbot_utils/helpers/llms/cache/Virtual_Storage__Sqlite.py +72 -0
  18. osbot_utils/helpers/llms/cache/__init__.py +0 -0
  19. osbot_utils/helpers/llms/platforms/__init__.py +0 -0
  20. osbot_utils/helpers/llms/platforms/open_ai/API__LLM__Open_AI.py +55 -0
  21. osbot_utils/helpers/llms/platforms/open_ai/__init__.py +0 -0
  22. osbot_utils/helpers/llms/schemas/Schema__LLM_Cache__Index.py +9 -0
  23. osbot_utils/helpers/llms/schemas/Schema__LLM_Request.py +7 -0
  24. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Data.py +14 -0
  25. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Function_Call.py +8 -0
  26. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Content.py +6 -0
  27. osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Role.py +9 -0
  28. osbot_utils/helpers/llms/schemas/Schema__LLM_Response.py +9 -0
  29. osbot_utils/helpers/llms/schemas/Schema__LLM_Response__Cache.py +13 -0
  30. osbot_utils/helpers/llms/schemas/__init__.py +0 -0
  31. osbot_utils/helpers/safe_str/Safe_Str.py +50 -0
  32. osbot_utils/helpers/safe_str/Safe_Str__File__Name.py +8 -0
  33. osbot_utils/helpers/safe_str/Safe_Str__File__Path.py +12 -0
  34. osbot_utils/helpers/safe_str/Safe_Str__Hash.py +14 -0
  35. osbot_utils/helpers/safe_str/Safe_Str__Text.py +9 -0
  36. osbot_utils/helpers/safe_str/Safe_Str__Text__Dangerous.py +9 -0
  37. osbot_utils/helpers/safe_str/__init__.py +0 -0
  38. osbot_utils/helpers/sqlite/Sqlite__Cursor.py +4 -6
  39. osbot_utils/helpers/sqlite/Sqlite__Database.py +1 -1
  40. osbot_utils/helpers/sqlite/Sqlite__Field.py +3 -8
  41. osbot_utils/helpers/sqlite/Sqlite__Table.py +1 -3
  42. osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py +6 -2
  43. osbot_utils/helpers/sqlite/models/Sqlite__Field__Type.py +2 -2
  44. osbot_utils/helpers/sqlite/tables/Sqlite__Table__Files.py +5 -5
  45. osbot_utils/helpers/ssh/SSH__Execute.py +0 -1
  46. osbot_utils/helpers/ssh/SSH__Health_Check.py +4 -5
  47. osbot_utils/type_safe/Type_Safe__Base.py +12 -0
  48. osbot_utils/type_safe/Type_Safe__Dict.py +2 -8
  49. osbot_utils/type_safe/Type_Safe__List.py +8 -4
  50. osbot_utils/type_safe/Type_Safe__Method.py +46 -5
  51. osbot_utils/type_safe/Type_Safe__Tuple.py +1 -1
  52. osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py +3 -2
  53. osbot_utils/type_safe/shared/Type_Safe__Validation.py +6 -4
  54. osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py +0 -2
  55. osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py +8 -0
  56. osbot_utils/type_safe/steps/Type_Safe__Step__Init.py +57 -1
  57. osbot_utils/utils/Files.py +8 -8
  58. osbot_utils/utils/Objects.py +0 -1
  59. osbot_utils/version +1 -1
  60. {osbot_utils-2.32.0.dist-info → osbot_utils-2.34.0.dist-info}/METADATA +2 -2
  61. {osbot_utils-2.32.0.dist-info → osbot_utils-2.34.0.dist-info}/RECORD +63 -30
  62. osbot_utils/helpers/cache_requests/flows/flow__Cache__Requests.py +0 -11
  63. {osbot_utils-2.32.0.dist-info → osbot_utils-2.34.0.dist-info}/LICENSE +0 -0
  64. {osbot_utils-2.32.0.dist-info → osbot_utils-2.34.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,112 @@
1
+ from typing import Dict, Optional, List
2
+ from osbot_utils.helpers.Obj_Id import Obj_Id
3
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Cache__Index import Schema__LLM_Cache__Index
4
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Request import Schema__LLM_Request
5
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Response import Schema__LLM_Response
6
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Response__Cache import Schema__LLM_Response__Cache
7
+ from osbot_utils.helpers.safe_str.Safe_Str__Hash import Safe_Str__Hash
8
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
9
+ from osbot_utils.type_safe.decorators.type_safe import type_safe
10
+ from osbot_utils.utils.Json import json_md5
11
+
12
+ SIZE__VALUE_HASH = 10
13
+
14
+ class LLM_Request__Cache(Type_Safe):
15
+ cache_index : Schema__LLM_Cache__Index # Index mapping request hashes to cache entries
16
+ cache_entries : Dict[Obj_Id, Schema__LLM_Response__Cache] # In-memory storage of cache entries
17
+
18
+ def save(self) -> bool: # For overriding in subclasses
19
+ return True
20
+
21
+ @type_safe
22
+ def compute_request_hash(self, request: Schema__LLM_Request) -> Safe_Str__Hash: # Computes hash for full request
23
+ request_json = request.request_data.json()
24
+ hash_value = json_md5(request_json)[:SIZE__VALUE_HASH]
25
+ return Safe_Str__Hash(hash_value)
26
+
27
+ @type_safe
28
+ def add(self, request : Schema__LLM_Request , # Request to cache
29
+ response : Schema__LLM_Response # Response to store
30
+ ) -> Obj_Id: # returns cache_id
31
+
32
+ hash_request = self.compute_request_hash (request) # calculate request hash
33
+ cache_entry = Schema__LLM_Response__Cache(cache_id = Obj_Id() , # Create a cache entry
34
+ llm_request = request ,
35
+ llm_response = response ,
36
+ hash__request = hash_request )
37
+ cache_id = cache_entry.cache_id
38
+
39
+ self.cache_index.cache_id__from__hash__request [ hash_request ] = cache_id # Update the cache index
40
+ self.cache_entries [cache_id ] = cache_entry # Store in memory
41
+
42
+ return cache_id
43
+
44
+ def get(self, request: Schema__LLM_Request) -> Optional[Schema__LLM_Response]: # Cached response or None
45
+ request_hash = self.compute_request_hash(request)
46
+
47
+ if request_hash in self.cache_index.cache_id__from__hash__request: # Check if we have an exact match
48
+ cache_id = self.cache_index.cache_id__from__hash__request[request_hash]
49
+ cache_entry = self.get_cache_entry(cache_id)
50
+ if cache_entry:
51
+ return cache_entry.llm_response
52
+
53
+ return None
54
+
55
+ @type_safe
56
+ def get_cache_entry(self, cache_id: Obj_Id) -> Optional[Schema__LLM_Response__Cache]: # Get cache entry by ID
57
+ return self.cache_entries.get(cache_id)
58
+
59
+ def exists(self, request: Schema__LLM_Request) -> bool: # True if in cache
60
+ request_hash = self.compute_request_hash(request)
61
+ return request_hash in self.cache_index.cache_id__from__hash__request
62
+
63
+ def delete(self, request : Schema__LLM_Request) -> bool: # Success status
64
+ request_hash = self.compute_request_hash(request)
65
+
66
+ if request_hash not in self.cache_index.cache_id__from__hash__request:
67
+ return False
68
+
69
+ cache_id = self.cache_index.cache_id__from__hash__request[request_hash]
70
+
71
+ del self.cache_index.cache_id__from__hash__request[request_hash] # Remove from hashes
72
+
73
+ if cache_id in self.cache_entries: # Remove from memory
74
+ del self.cache_entries[cache_id]
75
+
76
+ return self.save()
77
+
78
+ def get_by_id(self, cache_id : Obj_Id)-> Optional[Schema__LLM_Response]: # Cached response or None
79
+ cache_entry = self.get_cache_entry(cache_id)
80
+ if cache_entry:
81
+ return cache_entry.llm_response
82
+ return None
83
+
84
+ def clear(self) -> bool: # Clear all cache entries
85
+ self.cache_index = Schema__LLM_Cache__Index()
86
+ self.cache_entries = {}
87
+ return self.save()
88
+
89
+ def stats(self) -> Dict: # Cache statistics
90
+
91
+ total_entries = len(self.cache_index.cache_id__from__hash__request)
92
+ models = {}
93
+ oldest_timestamp = None
94
+ newest_timestamp = None
95
+
96
+ for cache_id, entry in self.cache_entries.items(): # Track models
97
+ model = entry.llm_request.request_data.model
98
+ if model in models:
99
+ models[model] += 1
100
+ else:
101
+ models[model] = 1
102
+
103
+ timestamp = entry.llm_response.timestamp # Track timestamps
104
+ if oldest_timestamp is None or timestamp < oldest_timestamp:
105
+ oldest_timestamp = timestamp
106
+ if newest_timestamp is None or timestamp > newest_timestamp:
107
+ newest_timestamp = timestamp
108
+
109
+ return { "total_entries" : total_entries ,
110
+ "models" : models ,
111
+ "oldest_entry" : str(oldest_timestamp) if oldest_timestamp else None ,
112
+ "newest_entry" : str(newest_timestamp) if newest_timestamp else None }
@@ -0,0 +1,237 @@
1
+ from datetime import datetime, UTC
2
+ from typing import Optional, List
3
+ from osbot_utils.decorators.methods.cache_on_self import cache_on_self
4
+ from osbot_utils.helpers.Obj_Id import Obj_Id
5
+ from osbot_utils.helpers.Safe_Id import Safe_Id
6
+ from osbot_utils.helpers.llms.cache.LLM_Cache__Path_Generator import LLM_Cache__Path_Generator
7
+ from osbot_utils.helpers.llms.cache.LLM_Request__Cache import LLM_Request__Cache
8
+ from osbot_utils.helpers.llms.cache.LLM_Request__Cache__Storage import LLM_Request__Cache__Storage
9
+ from osbot_utils.helpers.llms.cache.Virtual_Storage__Local__Folder import Virtual_Storage__Local__Folder
10
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Cache__Index import Schema__LLM_Cache__Index
11
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Request import Schema__LLM_Request
12
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Response import Schema__LLM_Response
13
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Response__Cache import Schema__LLM_Response__Cache
14
+ from osbot_utils.helpers.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
15
+ from osbot_utils.type_safe.decorators.type_safe import type_safe
16
+
17
+ class LLM_Request__Cache__File_System(LLM_Request__Cache):
18
+ virtual_storage: Virtual_Storage__Local__Folder
19
+ path_generator : LLM_Cache__Path_Generator
20
+ shared_domains : List[Safe_Id]
21
+ shared_areas : List[Safe_Id]
22
+
23
+ @cache_on_self
24
+ def storage(self):
25
+ return LLM_Request__Cache__Storage(virtual_storage=self.virtual_storage)
26
+
27
+ def save(self) -> bool: # Save cache index to disk
28
+ self.storage().save__cache_index(cache_index=self.cache_index)
29
+ return True
30
+
31
+ def setup(self) -> 'LLM_Request__Cache__File_System': # Load cache from disk
32
+ self.load_or_create()
33
+ return self
34
+
35
+ def get_all_cache_ids(self) -> List[Obj_Id]: # Get all cache IDs from disk
36
+ return sorted(self.cache_index.cache_id__to__file_path.keys())
37
+
38
+ def load_cache_entry(self, cache_id: Obj_Id) -> Optional[Schema__LLM_Response__Cache]: # Load cache entry from disk
39
+ cache_path = self.path_file__cache_entry(cache_id)
40
+ cache_entry = self.storage().load__cache_entry(cache_path)
41
+ if cache_entry:
42
+ self.cache_entries[cache_id] = cache_entry
43
+ return cache_entry
44
+ return None
45
+
46
+ def get_cache_entry(self, cache_id: Obj_Id) -> Optional[Schema__LLM_Response__Cache]: # Get cache entry by ID (overridden)
47
+ if cache_id in self.cache_entries: # Check memory first
48
+ return self.cache_entries[cache_id]
49
+ return self.load_cache_entry(cache_id) # Load from disk if not in memory
50
+
51
+ @type_safe
52
+ def delete(self, request: Schema__LLM_Request)-> bool: # Delete from cache (overridden) , returns Success status
53
+ request_hash = self.compute_request_hash(request)
54
+
55
+ if request_hash not in self.cache_index.cache_id__from__hash__request:
56
+ return False
57
+
58
+ cache_id = self.cache_index.cache_id__from__hash__request[request_hash]
59
+ cache_path = self.path_file__cache_entry(cache_id)
60
+
61
+ self.storage().delete__cache_entry(cache_path) # Delete the file
62
+
63
+ return super().delete(request) # Remove from memory and index
64
+
65
+ def clear(self) -> bool: # Clear all cache entries (overridden)
66
+ for cache_id in self.get_all_cache_ids(): # Delete all files
67
+ cache_path = self.path_file__cache_entry(cache_id)
68
+ self.storage().delete__cache_entry(cache_path)
69
+
70
+ self.storage().delete__cache_index()
71
+
72
+ return super().clear() # Clear memory cache
73
+
74
+ def load_or_create(self):
75
+ if self.storage().exists__cache_index(): # if cache file exists
76
+ self.cache_index = self.storage().load__cache_index() # load it
77
+ else:
78
+ self.save() # if not save the current cache_index (which should be empty)
79
+
80
+ def rebuild_cache_id_to_file_path(self) -> List[Obj_Id]: # todo: check the performance impact of this (and if we really need this method) # Get all cache IDs from disk
81
+ self.cache_index.cache_id__to__file_path = self.storage().reload__cache_id_to_file_path() # assign the new cache_id__to__file_path
82
+ return self
83
+
84
+
85
+ def rebuild_index(self) -> bool: # Rebuild index from disk files
86
+ self.cache_index = Schema__LLM_Cache__Index() # Create new empty index
87
+ self.cache_entries = {}
88
+ self.rebuild_cache_id_to_file_path() # rebuild the cache_id_to_file_path (needed so that we can find the files from its cache_ids)
89
+ for cache_id in self.get_all_cache_ids(): # Load all cache entries
90
+ cache_entry = self.load_cache_entry(cache_id)
91
+ if cache_entry:
92
+ request = cache_entry.llm_request
93
+ hash_request = cache_entry.hash__request
94
+ if hash_request is None: # if hash_request doesn't exist # todo: see if this a valid scenario
95
+ hash_request = self.compute_request_hash(request) # recompute hash_request
96
+ self.cache_index.cache_id__from__hash__request[hash_request] = cache_id # Update the index
97
+
98
+ return self.save()
99
+
100
+ # def stats(self) -> Dict: # Cache statistics (overridden)
101
+ # stats = super().stats()
102
+ # total_size = 0 # Add disk-specific stats
103
+ #
104
+ # # Add index file size
105
+ # index_path = self.path_file__cache_index()
106
+ # if file_exists(index_path):
107
+ # total_size += os.path.getsize(index_path)
108
+ #
109
+ # # Add cache entry files size
110
+ # for cache_id in self.get_all_cache_ids():
111
+ # cache_path = self.path_file__cache_entry(cache_id)
112
+ # if file_exists(cache_path):
113
+ # total_size += os.path.getsize(cache_path)
114
+ #
115
+ # stats["total_size_bytes"] = total_size
116
+ # #stats["root_folder" ] = self.path_folder__root_cache()
117
+ # stats["cache_files" ] = len(self.get_all_cache_ids())
118
+ #
119
+ # return stats
120
+
121
+ # @cache_on_self
122
+ # def path_folder__root_cache(self): # Get root cache folder path
123
+ # if folder_exists(self.root_folder): # If cache_folder is a folder that exists
124
+ # path_cache_folder = self.root_folder # Then use it
125
+ # else: # If not
126
+ # path_cache_folder = path_combine_safe(current_temp_folder(), self.root_folder) # Combine with temp folder
127
+ # folder_create(path_cache_folder) # Make sure it exists
128
+ # return path_cache_folder
129
+ #
130
+ # def path_file__cache_index(self): # Get path to cache index file
131
+ # return path_combine_safe(self.path_folder__root_cache(), FILE_NAME__CACHE_INDEX)
132
+ #
133
+ def path_file__cache_entry(self, cache_id: Obj_Id) -> str: # Get path to cache entry file
134
+ file_path = self.cache_id__to__file_path(cache_id)
135
+ return file_path
136
+ # if file_path:
137
+ # full_file_path = path_combine_safe(self.path_folder__root_cache(), file_path)
138
+ # return full_file_path
139
+
140
+ @type_safe
141
+ def cache_id__to__file_path(self, cache_id: Obj_Id) -> str:
142
+ return self.cache_index.cache_id__to__file_path.get(cache_id)
143
+
144
+ @type_safe
145
+ def extract_domains_from_request(self, request: Schema__LLM_Request) -> List[Safe_Id]: # Extract organizational information from a request.
146
+ domains = []
147
+ if request and request.request_data:
148
+ if request.request_data.model: # first add the model (if exists)
149
+ domains.append(Safe_Id(request.request_data.model))
150
+ if request.request_data.provider: # then add the provider (if exists)
151
+ domains.append(Safe_Id(request.request_data.provider))
152
+ if request.request_data.platform: # finally add the platform (if exists)
153
+ domains.append(Safe_Id(request.request_data.platform))
154
+
155
+ return domains
156
+
157
+ @type_safe
158
+ def path_for_temporal_entry(self, cache_id : Obj_Id ,
159
+ date_time : datetime = None,
160
+ domains : List[Safe_Id] = None,
161
+ areas : List[Safe_Id] = None
162
+ ) -> Safe_Str__File__Path: # Generate a time-based path for a cache entry
163
+ date_time = date_time or datetime.now()
164
+ path = self.path_generator.from_date_time(date_time = date_time,
165
+ domains = domains,
166
+ areas = areas,
167
+ file_id = Safe_Id(cache_id),
168
+ extension = "json")
169
+ return path
170
+
171
+ @type_safe
172
+ def add(self, request : Schema__LLM_Request,
173
+ response : Schema__LLM_Response,
174
+ now : datetime = None
175
+ ) -> Obj_Id: # Save an LLM request/response pair using temporal organization.
176
+
177
+ cache_id = super().add(request, response) # First use standard add() to handle in-memory caching
178
+ cache_entry = self.cache_entries[cache_id] # get the cache entry (which will exist since it was added on super().add(request, response) )
179
+ request_domains = self.extract_domains_from_request(request) # Extract domains and areas for organization
180
+ domains = self.shared_domains + request_domains
181
+ areas = self.shared_areas
182
+ date_time = now or datetime.now(UTC)
183
+
184
+ file_path = self.path_for_temporal_entry(cache_id = cache_id , # Generate file path and save
185
+ date_time = date_time,
186
+ domains = domains ,
187
+ areas = areas )
188
+ self.cache_index.cache_id__to__file_path[cache_id] = file_path
189
+
190
+ self.storage().save__cache_entry(file_path, cache_entry)
191
+
192
+ self.save() # save the cache to disk
193
+ return cache_id
194
+
195
+ # todo: see if we need this, since we should create an MGraph with this data (also self.cache_index.cache_id__to__file_path kinda have this data)
196
+ # @type_safe
197
+ # def get_from__date_time(self,date_time: datetime,
198
+ # domains : List[Safe_Id] = None,
199
+ # areas : List[Safe_Id] = None) -> List[Schema__LLM_Response__Cache]: # Get all cache entries from a specific date/time.
200
+ #
201
+ # folder_path = self.path_generator.from_date_time(date_time = date_time, # Generate the folder path pattern for the date/time
202
+ # domains = domains ,
203
+ # areas = areas )
204
+ # full_folder_path = path_combine_safe(self.path_folder__root_cache(), folder_path)
205
+ #
206
+ #
207
+ # if not folder_exists(full_folder_path): # Check if folder exists
208
+ # return []
209
+ #
210
+ #
211
+ # results = [] # Find all cache files in this folder and subfolders
212
+ # # todo: refactor using osbot_utils files methods
213
+ # def collect_entries(directory): # Function to collect entries recursively
214
+ # for item in os.listdir(directory):
215
+ # item_path = os.path.join(directory, item)
216
+ # if os.path.isdir(item_path):
217
+ # collect_entries(item_path)
218
+ # elif item.endswith('.json') and item != FILE_NAME__CACHE_INDEX:
219
+ # cache_id_str = os.path.splitext(os.path.basename(item_path))[0]
220
+ # if is_obj_id(cache_id_str):
221
+ # cache_id = Obj_Id(cache_id_str)
222
+ # cache_entry = self.get_cache_entry(cache_id)
223
+ # if cache_entry:
224
+ # results.append(cache_entry)
225
+ #
226
+ # collect_entries(full_folder_path)
227
+ # return results
228
+
229
+ # @type_safe
230
+ # def get_from__now(self,domains: List[Safe_Id] = None,
231
+ # areas : List[Safe_Id] = None,
232
+ # now : datetime = None
233
+ # ) -> List[Schema__LLM_Response__Cache]: # Get all cache entries from current time or specified time.
234
+ # timestamp = now or datetime.now()
235
+ # return self.get_from__date_time(date_time = timestamp,
236
+ # domains = domains ,
237
+ # areas = areas )
@@ -0,0 +1,85 @@
1
+ import os
2
+ from typing import List, Optional
3
+ from osbot_utils.decorators.methods.cache_on_self import cache_on_self
4
+ from osbot_utils.helpers.llms.cache.Virtual_Storage__Local__Folder import Virtual_Storage__Local__Folder
5
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Cache__Index import Schema__LLM_Cache__Index
6
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Response__Cache import Schema__LLM_Response__Cache
7
+ from osbot_utils.helpers.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
8
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
9
+ from osbot_utils.type_safe.decorators.type_safe import type_safe
10
+ from osbot_utils.helpers.Obj_Id import Obj_Id, is_obj_id
11
+ from osbot_utils.utils.Files import path_combine_safe, file_name_without_extension, parent_folder
12
+
13
+ FILE_NAME__CACHE_INDEX = "cache_index.json"
14
+
15
+ class LLM_Request__Cache__Storage(Type_Safe):
16
+ virtual_storage : Virtual_Storage__Local__Folder
17
+ index_file_name : str = FILE_NAME__CACHE_INDEX
18
+
19
+ @type_safe
20
+ def delete__cache_entry(self, file_path : Safe_Str__File__Path) -> bool: # Delete cache entry from storage
21
+ return self.virtual_storage.file__delete(self.path_file__cache_entry(file_path))
22
+
23
+ def delete__cache_index(self):
24
+ index_path = self.path_file__cache_index()
25
+ return self.virtual_storage.file__delete(index_path)
26
+
27
+ @type_safe
28
+ def exists__cache_entry(self,file_path : Safe_Str__File__Path) -> bool: # Check if cache entry exists
29
+ return self.virtual_storage.file__exists(self.path_file__cache_entry(file_path))
30
+
31
+ def exists__cache_index(self):
32
+ path_cache_index = self.path_file__cache_index()
33
+ return self.virtual_storage.file__exists(path_cache_index)
34
+
35
+ @type_safe
36
+ def load__cache_entry(self, file_path : Safe_Str__File__Path) -> Optional[Schema__LLM_Response__Cache]: # Load cache entry from storage
37
+ path_entry = self.path_file__cache_entry(file_path)
38
+ if self.virtual_storage.file__exists(path=path_entry):
39
+ json_data = self.virtual_storage.json__load(path=path_entry)
40
+ cache_entry = Schema__LLM_Response__Cache.from_json(json_data)
41
+ return cache_entry
42
+ return None
43
+
44
+ def load__cache_index(self) -> Optional[Schema__LLM_Cache__Index]: # Load cache index data
45
+ path_cache_index = self.path_file__cache_index()
46
+ if self.virtual_storage.file__exists(path_cache_index):
47
+ json_data = self.virtual_storage.json__load(path=path_cache_index) # get the data
48
+ return Schema__LLM_Cache__Index.from_json(json_data) # and load it as cache_index
49
+ return None
50
+
51
+ @cache_on_self
52
+ def path_file__cache_index(self) -> Safe_Str__File__Path: # Get path to cache index file
53
+ path = path_combine_safe(self.virtual_storage.path_folder__root_cache(), self.index_file_name)
54
+ return Safe_Str__File__Path(path)
55
+
56
+ @type_safe
57
+ def path_file__cache_entry(self, file_path : Safe_Str__File__Path) -> Safe_Str__File__Path: # Get full path to cache entry file
58
+ path = path_combine_safe(self.virtual_storage.path_folder__root_cache(), file_path)
59
+ return Safe_Str__File__Path(path)
60
+
61
+ def reload__cache_id_to_file_path(self) -> List[Obj_Id]: # todo: check the performance impact of this (and if we really need this method) # Get all cache IDs from disk
62
+ all_files_paths = self.virtual_storage.files__all()
63
+ path_root = self.virtual_storage.path_folder__root_cache()
64
+ cache_id__to__file_path = {}
65
+ for full_file_path in all_files_paths:
66
+ file_path = os.path.relpath(full_file_path, path_root)
67
+ cache_id = file_name_without_extension(full_file_path)
68
+ if is_obj_id(cache_id):
69
+ cache_id__to__file_path[cache_id] = file_path
70
+ return cache_id__to__file_path
71
+
72
+ @type_safe
73
+ def save__cache_index(self, cache_index : Schema__LLM_Cache__Index) -> bool: # Save cache index data
74
+ json_data = cache_index.json()
75
+ return self.virtual_storage.json__save(data=json_data, path=self.path_file__cache_index())
76
+
77
+ @type_safe
78
+ def save__cache_entry(self,file_path : Safe_Str__File__Path,
79
+ cache_entry : Schema__LLM_Response__Cache
80
+ ) -> bool: # Save cache entry to storage
81
+ full_file_path = Safe_Str__File__Path(path_combine_safe(self.virtual_storage.path_folder__root_cache(), file_path))
82
+ folder_full_file_path = parent_folder(full_file_path)
83
+ json_data = cache_entry.json()
84
+ self.virtual_storage.folder__create(folder_full_file_path) # Ensure parent folder exists
85
+ return self.virtual_storage.json__save(data=json_data, path=full_file_path)
@@ -0,0 +1,64 @@
1
+ from typing import List, Optional, Dict, Any
2
+ from osbot_utils.decorators.methods.cache_on_self import cache_on_self
3
+ from osbot_utils.helpers.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
4
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
5
+ from osbot_utils.type_safe.decorators.type_safe import type_safe
6
+ from osbot_utils.utils.Files import current_temp_folder, path_combine_safe, folder_create, file_exists, folder_exists, file_delete, file_name_without_extension, parent_folder, create_folder, files_recursive
7
+ from osbot_utils.utils.Json import json_save_file, json_load_file, json_file_load
8
+
9
+ FOLDER_NAME__CACHE_IN_TEMP_FOLDER = '_llm_requests_cache'
10
+ FILE_NAME__CACHE_INDEX = "cache_index.json"
11
+
12
+ class Virtual_Storage__Local__Folder(Type_Safe):
13
+
14
+ root_folder : Safe_Str__File__Path = None # Base directory for file operations
15
+
16
+ def folder__create(self, path_folder) -> None: # Make sure root folder exists
17
+ folder_create(path_folder)
18
+
19
+ @type_safe
20
+ def json__load(self, path: Safe_Str__File__Path) -> Optional[Dict[str, Any]]: # Read JSON from file
21
+ full_path = self.get_full_path(path)
22
+ if file_exists(full_path):
23
+ return json_load_file(path=full_path)
24
+ return None
25
+
26
+ @type_safe
27
+ def json__save(self, path: Safe_Str__File__Path,
28
+ data: dict
29
+ ) -> bool: # Write JSON to file
30
+ full_path = self.get_full_path(path)
31
+ folder = parent_folder(full_path)
32
+ create_folder(folder) # Ensure parent folder exists
33
+ return json_save_file(data, path=full_path)
34
+
35
+ @cache_on_self
36
+ def path_folder__root_cache(self) -> str: # Get root cache folder path
37
+ if folder_exists(self.root_folder):
38
+ path_cache_folder = self.root_folder
39
+ else:
40
+ path_cache_folder = path_combine_safe(current_temp_folder(), self.root_folder)
41
+ folder_create(path_cache_folder)
42
+ return path_cache_folder
43
+
44
+ @type_safe
45
+ def get_full_path(self, path: Safe_Str__File__Path) -> Safe_Str__File__Path: # Convert relative path to absolute
46
+ base_path = self.path_folder__root_cache()
47
+ full_path = path_combine_safe(base_path, path)
48
+ return Safe_Str__File__Path(full_path)
49
+
50
+ @type_safe
51
+ def file__delete(self, path: Safe_Str__File__Path) -> bool: # Delete a file
52
+ full_path = self.get_full_path(path)
53
+ return file_delete(full_path)
54
+
55
+ @type_safe
56
+ def file__exists(self, path: Safe_Str__File__Path) -> bool: # Check if file exists
57
+ full_path = self.get_full_path(path)
58
+ return file_exists(full_path)
59
+
60
+ @type_safe
61
+ def files__all(self) -> List[str]: # List all files recursively
62
+ base_path = self.path_folder__root_cache()
63
+ return files_recursive(base_path)
64
+
@@ -0,0 +1,72 @@
1
+ from typing import List, Optional, Dict, Any
2
+ from osbot_utils.decorators.methods.cache_on_self import cache_on_self
3
+ from osbot_utils.helpers.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
4
+ from osbot_utils.helpers.sqlite.domains.Sqlite__DB__Files import Sqlite__DB__Files
5
+ from osbot_utils.helpers.llms.cache.Virtual_Storage__Local__Folder import Virtual_Storage__Local__Folder
6
+ from osbot_utils.utils.Files import path_combine_safe
7
+ from osbot_utils.utils.Json import json_parse, json_dumps
8
+
9
+ class Virtual_Storage__Sqlite(Virtual_Storage__Local__Folder):
10
+ db : Sqlite__DB__Files # SQLite database for file storage (defaults to an in memory db)
11
+ root_folder : Safe_Str__File__Path = Safe_Str__File__Path("llm-cache/" ) # Prefix for all stored files
12
+
13
+ def folder__create(self, path_folder) -> None: # Folders don't need to be explicitly created in SQLite storage
14
+ pass # They're implicitly created when files are added with path prefixes
15
+
16
+ def json__load(self, path: Safe_Str__File__Path) -> Optional[Dict[str, Any]]: # Load JSON data from SQLite
17
+ virtual_path = self.get_virtual_path(path)
18
+ if self.file__exists(path):
19
+ content = self.db.file_contents(virtual_path)
20
+ if content:
21
+ return json_parse(content)
22
+ return None
23
+
24
+ def json__save(self, path: Safe_Str__File__Path, data: dict) -> bool: # Save JSON data to SQLite
25
+ self.db.delete_file(path) # todo: figure out a better way to do this, since at the moment we need to delete an existing file, in order to make sure it is updated
26
+ virtual_path = self.get_virtual_path(path)
27
+ content = json_dumps(data)
28
+ return self.db.add_file(virtual_path, content) is not None
29
+
30
+ def get_full_path(self, path: Safe_Str__File__Path) -> Safe_Str__File__Path: # For SQLite, we don't need physical paths, but we maintain
31
+ return path # the same interface for compatibility
32
+
33
+ def file__delete(self, path: Safe_Str__File__Path) -> bool: # Delete a file from SQLite
34
+ virtual_path = self.get_virtual_path(path)
35
+ return self.db.delete_file(virtual_path)
36
+
37
+ def file__exists(self, path: Safe_Str__File__Path) -> bool: # Check if file exists in SQLite
38
+ virtual_path = self.get_virtual_path(path)
39
+ return self.db.file_exists(virtual_path)
40
+
41
+ # todo: see if need the filter below
42
+ def files__all(self) -> List[str]: # List all files in SQLite
43
+ all_files = self.db.file_names()
44
+ return all_files
45
+ #return [f for f in all_files if f.startswith(self.root_folder)] # Filter to only include files that start with our root_prefix
46
+
47
+ def get_virtual_path(self, path: Safe_Str__File__Path) -> str: # Create a virtual path that incorporates the root_folder concept
48
+ if path.startswith(self.root_folder):
49
+ return path
50
+ return path_combine_safe(self.root_folder, path)
51
+
52
+ @cache_on_self
53
+ def path_folder__root_cache(self) -> str: # In SQLite storage, this is a virtual concept
54
+ return self.root_folder # We use the root_folder as the base path for all files
55
+
56
+ def clear_all(self) -> bool: # Clear all stored files in this virtual storage
57
+ for file_path in self.files__all():
58
+ self.db.delete_file(file_path)
59
+ return True
60
+
61
+ def stats(self) -> Dict[str, Any]: # Get storage statistics
62
+ total_size = 0
63
+ files = self.files__all()
64
+ for file_path in files:
65
+ file_info = self.db.file(file_path)
66
+ if file_info and 'size' in file_info:
67
+ total_size += file_info['size']
68
+
69
+ return { "storage_type" : "sqlite" ,
70
+ "db_path" : self.db.db_path,
71
+ "file_count" : len(files) ,
72
+ "total_size_bytes": total_size }
File without changes
File without changes
@@ -0,0 +1,55 @@
1
+ from typing import Dict, Any
2
+ from urllib.error import HTTPError
3
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
4
+ from osbot_utils.utils.Env import get_env
5
+ from osbot_utils.utils.Http import POST_json
6
+ from osbot_utils.utils.Json import json_parse, str_to_json
7
+
8
+ DEFAULT__LLM__SELECTED_PLATFORM = "OpenAI (Paid)"
9
+ DEFAULT__LLM__SELECTED_PROVIDER = "OpenAI"
10
+ DEFAULT__LLM__SELECTED_MODEL = "gpt-4o"
11
+
12
+ ENV_NAME_OPEN_AI__API_KEY = "OPEN_AI__API_KEY"
13
+
14
+ class API__LLM__Open_AI(Type_Safe):
15
+ api_url : str = "https://api.openai.com/v1/chat/completions"
16
+ api_key_name: str = ENV_NAME_OPEN_AI__API_KEY
17
+
18
+ def execute(self, llm_payload : Dict[str, Any]):
19
+ url = self.api_url
20
+
21
+ headers = { "Authorization": f"Bearer {self.api_key()}",
22
+ "Content-Type" : "application/json" ,
23
+ 'User-Agent' : "myfeeds.ai" }
24
+ try:
25
+ response = POST_json(url, headers=headers, data=llm_payload)
26
+ return response
27
+ except HTTPError as error:
28
+
29
+ error_message = str_to_json(error.file.read().decode('utf-8'))
30
+ raise ValueError(error_message)
31
+
32
+
33
+ # todo: refactor this into a separate class with better error detection and context specific methods
34
+ def get_json(self, llm_response):
35
+ choices = llm_response.get('choices')
36
+ if len(choices) == 1:
37
+ message = choices[0].get('message')
38
+ if 'function_call' in message:
39
+ arguments = message.get('function_call').get('arguments')
40
+ else:
41
+ arguments = message.get('tool_calls')[0].get('function').get('arguments')
42
+ else:
43
+ return choices
44
+ return json_parse(arguments)
45
+
46
+ def api_key(self):
47
+ api_key = get_env(self.api_key_name)
48
+ if not api_key:
49
+ raise ValueError("{self.api_key_name} key not set")
50
+ return api_key
51
+
52
+
53
+ def get_json__entities(self, llm_response):
54
+ function_arguments = self.get_json(llm_response)
55
+ return function_arguments.get('entities')
File without changes
@@ -0,0 +1,9 @@
1
+ from typing import Dict
2
+ from osbot_utils.helpers.Obj_Id import Obj_Id
3
+ from osbot_utils.helpers.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
4
+ from osbot_utils.helpers.safe_str.Safe_Str__Hash import Safe_Str__Hash
5
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
6
+
7
+ class Schema__LLM_Cache__Index(Type_Safe):
8
+ cache_id__from__hash__request : Dict[Safe_Str__Hash, Obj_Id] # map hash of the full request to a Schema__LLM_Response__Cache to the cache_id
9
+ cache_id__to__file_path : Dict[Obj_Id , Safe_Str__File__Path] # map the cache_id to the file that holds the data
@@ -0,0 +1,7 @@
1
+ from osbot_utils.helpers.llms.schemas.Schema__LLM_Request__Data import Schema__LLM_Request__Data
2
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
3
+
4
+
5
+ class Schema__LLM_Request(Type_Safe):
6
+ #request_id : Obj_Id # can't put this id here or we won't be able to cache this data
7
+ request_data : Schema__LLM_Request__Data