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.
- osbot_utils/helpers/Local_Cache.py +5 -7
- osbot_utils/helpers/Local_Caches.py +5 -5
- osbot_utils/helpers/flows/Flow.py +4 -0
- osbot_utils/helpers/llms/__init__.py +0 -0
- osbot_utils/helpers/llms/actions/LLM_Request__Execute.py +31 -0
- osbot_utils/helpers/llms/actions/Type_Safe__Schema_For__LLMs.py +213 -0
- osbot_utils/helpers/llms/actions/__init__.py +0 -0
- osbot_utils/helpers/llms/builders/LLM_Request__Builder.py +41 -0
- osbot_utils/helpers/llms/builders/LLM_Request__Builder__Open_AI.py +54 -0
- osbot_utils/helpers/llms/builders/LLM_Request__Factory.py +95 -0
- osbot_utils/helpers/llms/builders/__init__.py +0 -0
- osbot_utils/helpers/llms/cache/LLM_Cache__Path_Generator.py +83 -0
- osbot_utils/helpers/llms/cache/LLM_Request__Cache.py +112 -0
- osbot_utils/helpers/llms/cache/LLM_Request__Cache__File_System.py +237 -0
- osbot_utils/helpers/llms/cache/LLM_Request__Cache__Storage.py +85 -0
- osbot_utils/helpers/llms/cache/Virtual_Storage__Local__Folder.py +64 -0
- osbot_utils/helpers/llms/cache/Virtual_Storage__Sqlite.py +72 -0
- osbot_utils/helpers/llms/cache/__init__.py +0 -0
- osbot_utils/helpers/llms/platforms/__init__.py +0 -0
- osbot_utils/helpers/llms/platforms/open_ai/API__LLM__Open_AI.py +55 -0
- osbot_utils/helpers/llms/platforms/open_ai/__init__.py +0 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Cache__Index.py +9 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request.py +7 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Data.py +14 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Function_Call.py +8 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Content.py +6 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Request__Message__Role.py +9 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Response.py +9 -0
- osbot_utils/helpers/llms/schemas/Schema__LLM_Response__Cache.py +13 -0
- osbot_utils/helpers/llms/schemas/__init__.py +0 -0
- osbot_utils/helpers/safe_str/Safe_Str.py +50 -0
- osbot_utils/helpers/safe_str/Safe_Str__File__Name.py +8 -0
- osbot_utils/helpers/safe_str/Safe_Str__File__Path.py +12 -0
- osbot_utils/helpers/safe_str/Safe_Str__Hash.py +14 -0
- osbot_utils/helpers/safe_str/Safe_Str__Text.py +9 -0
- osbot_utils/helpers/safe_str/Safe_Str__Text__Dangerous.py +9 -0
- osbot_utils/helpers/safe_str/__init__.py +0 -0
- osbot_utils/helpers/sqlite/Sqlite__Cursor.py +4 -6
- osbot_utils/helpers/sqlite/Sqlite__Database.py +1 -1
- osbot_utils/helpers/sqlite/Sqlite__Field.py +3 -8
- osbot_utils/helpers/sqlite/Sqlite__Table.py +1 -3
- osbot_utils/helpers/sqlite/domains/Sqlite__DB__Files.py +6 -2
- osbot_utils/helpers/sqlite/models/Sqlite__Field__Type.py +2 -2
- osbot_utils/helpers/sqlite/tables/Sqlite__Table__Files.py +5 -5
- osbot_utils/helpers/ssh/SSH__Execute.py +0 -1
- osbot_utils/helpers/ssh/SSH__Health_Check.py +4 -5
- osbot_utils/type_safe/Type_Safe__Base.py +12 -0
- osbot_utils/type_safe/Type_Safe__Dict.py +2 -8
- osbot_utils/type_safe/Type_Safe__List.py +8 -4
- osbot_utils/type_safe/Type_Safe__Method.py +46 -5
- osbot_utils/type_safe/Type_Safe__Tuple.py +1 -1
- osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py +3 -2
- osbot_utils/type_safe/shared/Type_Safe__Validation.py +6 -4
- osbot_utils/type_safe/steps/Type_Safe__Step__Default_Value.py +0 -2
- osbot_utils/type_safe/steps/Type_Safe__Step__From_Json.py +8 -0
- osbot_utils/type_safe/steps/Type_Safe__Step__Init.py +57 -1
- osbot_utils/utils/Files.py +8 -8
- osbot_utils/utils/Objects.py +0 -1
- osbot_utils/version +1 -1
- {osbot_utils-2.32.0.dist-info → osbot_utils-2.34.0.dist-info}/METADATA +2 -2
- {osbot_utils-2.32.0.dist-info → osbot_utils-2.34.0.dist-info}/RECORD +63 -30
- osbot_utils/helpers/cache_requests/flows/flow__Cache__Requests.py +0 -11
- {osbot_utils-2.32.0.dist-info → osbot_utils-2.34.0.dist-info}/LICENSE +0 -0
- {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
|