llumo 0.1.1__py3-none-any.whl → 0.1.4__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.
- llumo/.env +6 -0
- llumo/__init__.py +7 -2
- llumo/client.py +554 -77
- llumo/exceptions.py +31 -30
- llumo/execution.py +39 -0
- llumo/helpingFuntions.py +60 -0
- llumo/models.py +43 -0
- llumo/sockets.py +154 -0
- {llumo-0.1.1.dist-info → llumo-0.1.4.dist-info}/METADATA +26 -23
- llumo-0.1.4.dist-info/RECORD +13 -0
- {llumo-0.1.1.dist-info → llumo-0.1.4.dist-info}/WHEEL +1 -1
- {llumo-0.1.1.dist-info → llumo-0.1.4.dist-info}/licenses/LICENSE +4 -4
- llumo-0.1.1.dist-info/RECORD +0 -8
- {llumo-0.1.1.dist-info → llumo-0.1.4.dist-info}/top_level.txt +0 -0
llumo/.env
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
|
2
|
+
BASE_URL="https://app.llumo.ai/api"
|
3
|
+
postUrl = "https://red-skull-service-392377961931.us-central1.run.app/api/process-playground"
|
4
|
+
fetchUrl = "https://red-skull-service-392377961931.us-central1.run.app/api/get-cells-data"
|
5
|
+
validateUrl = "https://backend-api.llumo.ai/api/v1/workspace-key-details"
|
6
|
+
SOCKET_URL="https://red-skull-service-392377961931.us-central1.run.app/"
|
llumo/__init__.py
CHANGED
llumo/client.py
CHANGED
@@ -1,77 +1,554 @@
|
|
1
|
-
import requests
|
2
|
-
from .exceptions import LlumoAPIError
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
}
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
raise LlumoAPIError.
|
76
|
-
|
77
|
-
|
1
|
+
import requests
|
2
|
+
from .exceptions import LlumoAPIError
|
3
|
+
import time
|
4
|
+
import re
|
5
|
+
import json
|
6
|
+
import uuid
|
7
|
+
import threading
|
8
|
+
from .helpingFuntions import *
|
9
|
+
from dotenv import load_dotenv
|
10
|
+
import os
|
11
|
+
import itertools
|
12
|
+
import pandas as pd
|
13
|
+
from typing import List, Dict
|
14
|
+
from .models import AVAILABLEMODELS,getProviderFromModel
|
15
|
+
from .execution import ModelExecutor
|
16
|
+
from .sockets import LlumoSocketClient
|
17
|
+
|
18
|
+
|
19
|
+
# 👇 NEW: Explicitly load .env from the package folder
|
20
|
+
envPath = os.path.join(os.path.dirname(__file__), '.env')
|
21
|
+
load_dotenv(dotenv_path=envPath, override=False)# Automatically looks for .env in current directory
|
22
|
+
|
23
|
+
postUrl = os.getenv("postUrl")
|
24
|
+
fetchUrl = os.getenv("fetchUrl")
|
25
|
+
validateUrl = os.getenv("validateUrl")
|
26
|
+
socketUrl = os.getenv("SOCKET_URL")
|
27
|
+
|
28
|
+
|
29
|
+
class LlumoClient:
|
30
|
+
|
31
|
+
def __init__(self, api_key):
|
32
|
+
self.apiKey = api_key
|
33
|
+
self.socket = LlumoSocketClient(socketUrl)
|
34
|
+
self.processMapping = {}
|
35
|
+
|
36
|
+
|
37
|
+
def validateApiKey(self, evalName = ""):
|
38
|
+
headers = {
|
39
|
+
"Authorization": f"Bearer {self.apiKey}",
|
40
|
+
"Content-Type": "application/json",
|
41
|
+
}
|
42
|
+
reqBody = {"analytics": [evalName]}
|
43
|
+
|
44
|
+
print(f"Making API key validation request to: {validateUrl}")
|
45
|
+
print(f"Request body: {reqBody}")
|
46
|
+
|
47
|
+
try:
|
48
|
+
response = requests.post(url=validateUrl, json=reqBody, headers=headers)
|
49
|
+
print(response.text)
|
50
|
+
# Print response info for debugging
|
51
|
+
print(f"Response status code: {response.status_code}")
|
52
|
+
print(f"Response headers: {response.headers}")
|
53
|
+
|
54
|
+
# Try to get at least some of the response content
|
55
|
+
try:
|
56
|
+
response_preview = response.text[:500] # First 500 chars
|
57
|
+
print(f"Response preview: {response_preview}")
|
58
|
+
except Exception as e:
|
59
|
+
print(f"Could not get response preview: {e}")
|
60
|
+
|
61
|
+
except requests.exceptions.RequestException as e:
|
62
|
+
print(f"Request exception: {str(e)}")
|
63
|
+
raise LlumoAPIError.RequestFailed(detail=str(e))
|
64
|
+
|
65
|
+
if response.status_code == 401:
|
66
|
+
raise LlumoAPIError.InvalidApiKey()
|
67
|
+
|
68
|
+
# Handle other common status codes
|
69
|
+
if response.status_code == 404:
|
70
|
+
raise LlumoAPIError.RequestFailed(
|
71
|
+
detail=f"Endpoint not found (404): {validateUrl}"
|
72
|
+
)
|
73
|
+
|
74
|
+
if response.status_code >= 500:
|
75
|
+
raise LlumoAPIError.ServerError(
|
76
|
+
detail=f"Server error ({response.status_code})"
|
77
|
+
)
|
78
|
+
|
79
|
+
if response.status_code != 200:
|
80
|
+
raise LlumoAPIError.RequestFailed(
|
81
|
+
detail=f"Unexpected status code: {response.status_code}"
|
82
|
+
)
|
83
|
+
|
84
|
+
# Try to parse JSON
|
85
|
+
try:
|
86
|
+
data = response.json()
|
87
|
+
except ValueError as e:
|
88
|
+
print(f"JSON parsing error: {str(e)}")
|
89
|
+
print(
|
90
|
+
f"Response content that could not be parsed: {response.text[:1000]}..."
|
91
|
+
)
|
92
|
+
raise LlumoAPIError.InvalidJsonResponse()
|
93
|
+
|
94
|
+
if "data" not in data or not data["data"]:
|
95
|
+
print(f"Invalid API response structure: {data}")
|
96
|
+
raise LlumoAPIError.InvalidApiResponse()
|
97
|
+
|
98
|
+
try:
|
99
|
+
self.hitsAvailable = data["data"].get("remainingHits", 0)
|
100
|
+
self.workspaceID = data["data"].get("workspaceID")
|
101
|
+
self.evalDefinition = data["data"].get("analyticsMapping")
|
102
|
+
self.token = data["data"].get("token")
|
103
|
+
|
104
|
+
print(f"API key validation successful:")
|
105
|
+
# print(f"- Remaining hits: {self.hitsAvailable}")
|
106
|
+
# print(f"- Workspace ID: {self.workspaceID}")
|
107
|
+
# print(f"- Token received: {'Yes' if self.token else 'No'}")
|
108
|
+
|
109
|
+
except Exception as e:
|
110
|
+
print(f"Error extracting data from response: {str(e)}")
|
111
|
+
raise LlumoAPIError.UnexpectedError(detail=str(e))
|
112
|
+
|
113
|
+
def postBatch(self, batch, workspaceID):
|
114
|
+
payload = {
|
115
|
+
"batch": json.dumps(batch),
|
116
|
+
"runType": "EVAL",
|
117
|
+
"workspaceID": workspaceID,
|
118
|
+
}
|
119
|
+
headers = {
|
120
|
+
"Authorization": f"Bearer {self.token}",
|
121
|
+
"Content-Type": "application/json",
|
122
|
+
}
|
123
|
+
try:
|
124
|
+
print(postUrl)
|
125
|
+
response = requests.post(postUrl, json=payload, headers=headers)
|
126
|
+
# print(f"Post API Status Code: {response.status_code}")
|
127
|
+
# print(response.text)
|
128
|
+
|
129
|
+
except Exception as e:
|
130
|
+
print(f"Error in posting batch: {e}")
|
131
|
+
|
132
|
+
def AllProcessMapping(self):
|
133
|
+
for batch in self.allBatches:
|
134
|
+
for record in batch:
|
135
|
+
rowId = record['rowID']
|
136
|
+
colId = record['columnID']
|
137
|
+
pid = f'{rowId}-{colId}-{colId}'
|
138
|
+
self.processMapping[pid] = record
|
139
|
+
|
140
|
+
|
141
|
+
def finalResp(self,results):
|
142
|
+
seen = set()
|
143
|
+
uniqueResults = []
|
144
|
+
|
145
|
+
for item in results:
|
146
|
+
for rowID in item: # Each item has only one key
|
147
|
+
if rowID not in seen:
|
148
|
+
seen.add(rowID)
|
149
|
+
uniqueResults.append(item)
|
150
|
+
|
151
|
+
return uniqueResults
|
152
|
+
|
153
|
+
def evaluate(self, dataframe, evals=["Response Completeness"],prompt_template = ""):
|
154
|
+
results = {}
|
155
|
+
try:
|
156
|
+
# Connect to socket first
|
157
|
+
print("Connecting to socket server...")
|
158
|
+
socketID = self.socket.connect(timeout=20)
|
159
|
+
print(f"Connected with socket ID: {socketID}")
|
160
|
+
|
161
|
+
# Process each evaluation
|
162
|
+
for eval in evals:
|
163
|
+
print(f"\n======= Running evaluation for: {eval} =======")
|
164
|
+
|
165
|
+
try:
|
166
|
+
print(f"Validating API key for {eval}...")
|
167
|
+
self.validateApiKey(evalName=eval)
|
168
|
+
print(
|
169
|
+
f"API key validation successful. Hits available: {self.hitsAvailable}"
|
170
|
+
)
|
171
|
+
except Exception as e:
|
172
|
+
print(f"Error during API key validation: {str(e)}")
|
173
|
+
if (
|
174
|
+
hasattr(e, "response")
|
175
|
+
and getattr(e, "response", None) is not None
|
176
|
+
):
|
177
|
+
print(f"Status code: {e.response.status_code}")
|
178
|
+
print(f"Response content: {e.response.text[:500]}...")
|
179
|
+
raise
|
180
|
+
|
181
|
+
if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
|
182
|
+
raise LlumoAPIError.InsufficientCredits()
|
183
|
+
|
184
|
+
evalDefinition = self.evalDefinition[eval]
|
185
|
+
model = "GPT_4"
|
186
|
+
provider = "OPENAI"
|
187
|
+
evalType = "LLM"
|
188
|
+
workspaceID = self.workspaceID
|
189
|
+
|
190
|
+
# Prepare all batches before sending
|
191
|
+
print("Preparing batches...")
|
192
|
+
self.allBatches = []
|
193
|
+
currentBatch = []
|
194
|
+
|
195
|
+
for index, row in dataframe.iterrows():
|
196
|
+
|
197
|
+
tools = row["tools"] if "tools" in dataframe.columns else []
|
198
|
+
groundTruth = row["groundTruth"] if "groundTruth" in dataframe.columns else ""
|
199
|
+
messageHistory = row["messageHistory"] if "messageHistory" in dataframe.columns else []
|
200
|
+
promptTemplate = prompt_template
|
201
|
+
|
202
|
+
keys = re.findall(r"{{(.*?)}}", promptTemplate)
|
203
|
+
|
204
|
+
# extracting the required values for the the columns based on the prompt template
|
205
|
+
inputDict = {key: row[key] for key in keys if key in row}
|
206
|
+
output = row["output"]
|
207
|
+
|
208
|
+
activePlayground = (
|
209
|
+
f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
210
|
+
)
|
211
|
+
rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
212
|
+
columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace(
|
213
|
+
"-", ""
|
214
|
+
)
|
215
|
+
|
216
|
+
# Use the server-provided socket ID here
|
217
|
+
templateData = {
|
218
|
+
"processID": getProcessID(),
|
219
|
+
"socketID": socketID, # Using the server-assigned socket ID
|
220
|
+
"processData": {
|
221
|
+
"executionDependency": {
|
222
|
+
"query": "",
|
223
|
+
"context": "",
|
224
|
+
"output": output,
|
225
|
+
"tools": tools,
|
226
|
+
"groundTruth": groundTruth,
|
227
|
+
"messageHistory": messageHistory,
|
228
|
+
},
|
229
|
+
"definition": evalDefinition,
|
230
|
+
"model": model,
|
231
|
+
"provider": provider,
|
232
|
+
"analytics": eval,
|
233
|
+
},
|
234
|
+
"workspaceID": workspaceID,
|
235
|
+
"type": "EVAL",
|
236
|
+
"evalType": evalType,
|
237
|
+
"kpi": eval,
|
238
|
+
"columnID": columnID,
|
239
|
+
"rowID": rowID,
|
240
|
+
"playgroundID": activePlayground,
|
241
|
+
"processType": "EVAL",
|
242
|
+
}
|
243
|
+
|
244
|
+
# Build query/context from input
|
245
|
+
query = ""
|
246
|
+
context = ""
|
247
|
+
for key, value in inputDict.items():
|
248
|
+
if isinstance(value, str):
|
249
|
+
length = len(value.split()) * 1.5
|
250
|
+
if length > 50:
|
251
|
+
context += f" {key}: {value}, "
|
252
|
+
else:
|
253
|
+
if promptTemplate:
|
254
|
+
tempObj = {key: value}
|
255
|
+
promptTemplate = getInputPopulatedPrompt(promptTemplate, tempObj)
|
256
|
+
else:
|
257
|
+
query += f" {key}: {value}, "
|
258
|
+
|
259
|
+
if not context.strip():
|
260
|
+
for key, value in inputDict.items():
|
261
|
+
context += f" {key}: {value}, "
|
262
|
+
|
263
|
+
templateData["processData"]["executionDependency"]["context"] = context.strip()
|
264
|
+
templateData["processData"]["executionDependency"]["query"] = query.strip()
|
265
|
+
|
266
|
+
if promptTemplate and not query.strip():
|
267
|
+
templateData["processData"]["executionDependency"]["query"] = promptTemplate
|
268
|
+
|
269
|
+
currentBatch.append(templateData)
|
270
|
+
|
271
|
+
if len(currentBatch) == 10 or index == len(dataframe) - 1:
|
272
|
+
self.allBatches.append(currentBatch)
|
273
|
+
currentBatch = []
|
274
|
+
|
275
|
+
# Post all batches
|
276
|
+
total_items = sum(len(batch) for batch in self.allBatches)
|
277
|
+
print(f"Posting {len(self.allBatches)} batches ({total_items} items total)")
|
278
|
+
|
279
|
+
for cnt, batch in enumerate(self.allBatches):
|
280
|
+
print(
|
281
|
+
f"Posting batch {cnt + 1}/{len(self.allBatches)} for eval '{eval}'"
|
282
|
+
)
|
283
|
+
try:
|
284
|
+
self.postBatch(batch=batch, workspaceID=workspaceID)
|
285
|
+
print(f"Batch {cnt + 1} posted successfully")
|
286
|
+
except Exception as e:
|
287
|
+
print(f"Error posting batch {cnt + 1}: {str(e)}")
|
288
|
+
continue
|
289
|
+
|
290
|
+
|
291
|
+
|
292
|
+
# Small delay between batches to prevent overwhelming the server
|
293
|
+
time.sleep(1)
|
294
|
+
|
295
|
+
# updating the dict for row column mapping
|
296
|
+
self.AllProcessMapping()
|
297
|
+
# Calculate a reasonable timeout based on the data size
|
298
|
+
timeout = max(60, min(600, total_items * 10))
|
299
|
+
print(
|
300
|
+
f"All batches posted. Waiting up to {timeout} seconds for results..."
|
301
|
+
)
|
302
|
+
|
303
|
+
# Listen for results
|
304
|
+
self.socket.listen_for_results(
|
305
|
+
min_wait=10, max_wait=timeout, inactivity_timeout=30
|
306
|
+
)
|
307
|
+
|
308
|
+
# Get results for this evaluation
|
309
|
+
eval_results = self.socket.get_received_data()
|
310
|
+
print(f"Received {len(eval_results)} results for evaluation '{eval}'")
|
311
|
+
|
312
|
+
# Add these results to our overall results
|
313
|
+
results[eval] = self.finalResp(eval_results)
|
314
|
+
print(f"======= Completed evaluation: {eval} =======\n")
|
315
|
+
|
316
|
+
print("All evaluations completed successfully")
|
317
|
+
|
318
|
+
except Exception as e:
|
319
|
+
print(f"Error during evaluation: {e}")
|
320
|
+
raise
|
321
|
+
finally:
|
322
|
+
# Always disconnect the socket when done
|
323
|
+
try:
|
324
|
+
self.socket.disconnect()
|
325
|
+
print("Socket disconnected")
|
326
|
+
except Exception as e:
|
327
|
+
print(f"Error disconnecting socket: {e}")
|
328
|
+
|
329
|
+
for evalName, records in results.items():
|
330
|
+
for item in records:
|
331
|
+
self.processMapping[list(item.keys())[0]] = list(item.values())[0]
|
332
|
+
|
333
|
+
|
334
|
+
|
335
|
+
dataframe[evalName] = self.processMapping.values()
|
336
|
+
|
337
|
+
return dataframe
|
338
|
+
|
339
|
+
def evaluateCompressor(self, dataframe, prompt_template):
|
340
|
+
results = []
|
341
|
+
try:
|
342
|
+
# Connect to socket first
|
343
|
+
print("Connecting to socket server...")
|
344
|
+
socketID = self.socket.connect(timeout=20)
|
345
|
+
print(f"Connected with socket ID: {socketID}")
|
346
|
+
|
347
|
+
try:
|
348
|
+
print(f"Validating API key...")
|
349
|
+
self.validateApiKey()
|
350
|
+
print(f"API key validation successful. Hits available: {self.hitsAvailable}")
|
351
|
+
except Exception as e:
|
352
|
+
print(f"Error during API key validation: {str(e)}")
|
353
|
+
if hasattr(e, "response") and getattr(e, "response", None) is not None:
|
354
|
+
print(f"Status code: {e.response.status_code}")
|
355
|
+
print(f"Response content: {e.response.text[:500]}...")
|
356
|
+
raise
|
357
|
+
|
358
|
+
if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
|
359
|
+
raise LlumoAPIError.InsufficientCredits()
|
360
|
+
|
361
|
+
model = "GPT_4"
|
362
|
+
provider = "OPENAI"
|
363
|
+
evalType = "LLUMO"
|
364
|
+
workspaceID = self.workspaceID
|
365
|
+
|
366
|
+
# Prepare all batches before sending
|
367
|
+
print("Preparing batches...")
|
368
|
+
self.allBatches = []
|
369
|
+
currentBatch = []
|
370
|
+
|
371
|
+
for index, row in dataframe.iterrows():
|
372
|
+
promptTemplate = prompt_template
|
373
|
+
|
374
|
+
# extracting the placeholders from the prompt template
|
375
|
+
keys = re.findall(r"{{(.*?)}}", promptTemplate)
|
376
|
+
inputDict = {key: row[key] for key in keys if key in row}
|
377
|
+
|
378
|
+
activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
379
|
+
rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
380
|
+
columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
381
|
+
|
382
|
+
compressed_prompt_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
383
|
+
compressed_prompt_output_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
384
|
+
cost_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
385
|
+
cost_saving_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
386
|
+
|
387
|
+
# Use the server-provided socket ID here
|
388
|
+
templateData = {
|
389
|
+
"processID": getProcessID(),
|
390
|
+
"socketID": socketID,
|
391
|
+
"rowID": rowID,
|
392
|
+
"columnID": columnID,
|
393
|
+
"processType": "COST_SAVING",
|
394
|
+
"evalType": evalType,
|
395
|
+
"dependency": list(inputDict.keys()),
|
396
|
+
"costColumnMapping": {
|
397
|
+
"compressed_prompt": compressed_prompt_id,
|
398
|
+
"compressed_prompt_output": compressed_prompt_output_id,
|
399
|
+
"cost": cost_id,
|
400
|
+
"cost_saving": cost_saving_id
|
401
|
+
},
|
402
|
+
"processData": {
|
403
|
+
"rowData": {
|
404
|
+
"query": {"type": "VARIABLE", "value": ""},
|
405
|
+
"context": {"type": "VARIABLE", "value": ""},
|
406
|
+
},
|
407
|
+
"dependency": list(inputDict.keys()),
|
408
|
+
"dependencyMapping": {ky: ky for ky in list(inputDict.keys())},
|
409
|
+
"provider": provider,
|
410
|
+
"model": model,
|
411
|
+
"promptText": promptTemplate,
|
412
|
+
"costColumnMapping": {
|
413
|
+
"compressed_prompt": compressed_prompt_id,
|
414
|
+
"compressed_prompt_output": compressed_prompt_output_id,
|
415
|
+
"cost": cost_id,
|
416
|
+
"cost_saving": cost_saving_id
|
417
|
+
}
|
418
|
+
},
|
419
|
+
"workspaceID": workspaceID,
|
420
|
+
"email": "",
|
421
|
+
"playgroundID": activePlayground
|
422
|
+
}
|
423
|
+
|
424
|
+
|
425
|
+
# Build query/context from input
|
426
|
+
query = ""
|
427
|
+
context = ""
|
428
|
+
|
429
|
+
for key, value in inputDict.items():
|
430
|
+
if isinstance(value, str):
|
431
|
+
length = len(value.split()) * 1.5
|
432
|
+
if length > 50:
|
433
|
+
context += f" {key}: {value}, "
|
434
|
+
else:
|
435
|
+
if promptTemplate:
|
436
|
+
populatedPrompt = getInputPopulatedPrompt(promptTemplate, {key: value})
|
437
|
+
query += f"{populatedPrompt} "
|
438
|
+
else:
|
439
|
+
query += f" {key}: {value}, "
|
440
|
+
|
441
|
+
if not context.strip():
|
442
|
+
for key, value in inputDict.items():
|
443
|
+
context += f" {key}: {value}, "
|
444
|
+
|
445
|
+
templateData["processData"]["rowData"]["context"]["value"] = context.strip()
|
446
|
+
templateData["processData"]["rowData"]["query"]["value"] = query.strip()
|
447
|
+
|
448
|
+
if promptTemplate and not query.strip():
|
449
|
+
templateData["processData"]["rowData"]["query"]["value"] = promptTemplate
|
450
|
+
|
451
|
+
print(templateData)
|
452
|
+
currentBatch.append(templateData)
|
453
|
+
|
454
|
+
if len(currentBatch) == 10 or index == len(dataframe) - 1:
|
455
|
+
self.allBatches.append(currentBatch)
|
456
|
+
currentBatch = []
|
457
|
+
|
458
|
+
# Post all batches
|
459
|
+
total_items = sum(len(batch) for batch in self.allBatches)
|
460
|
+
print(f"Posting {len(self.allBatches)} batches ({total_items} items total)")
|
461
|
+
|
462
|
+
for cnt, batch in enumerate(self.allBatches):
|
463
|
+
print(f"Posting batch {cnt + 1}/{len(self.allBatches)} for eval '{eval}'")
|
464
|
+
try:
|
465
|
+
self.postBatch(batch=batch, workspaceID=workspaceID)
|
466
|
+
print(f"Batch {cnt + 1} posted successfully")
|
467
|
+
except Exception as e:
|
468
|
+
print(f"Error posting batch {cnt + 1}: {str(e)}")
|
469
|
+
continue
|
470
|
+
|
471
|
+
# Small delay between batches to prevent overwhelming the server
|
472
|
+
time.sleep(1)
|
473
|
+
|
474
|
+
# updating the dict for row column mapping
|
475
|
+
self.AllProcessMapping()
|
476
|
+
# Calculate a reasonable timeout based on the data size
|
477
|
+
timeout = max(60, min(600, total_items * 10))
|
478
|
+
print(f"All batches posted. Waiting up to {timeout} seconds for results...")
|
479
|
+
|
480
|
+
# Listen for results
|
481
|
+
self.socket.listen_for_results(min_wait=10, max_wait=timeout, inactivity_timeout=30)
|
482
|
+
|
483
|
+
# Get results for this evaluation
|
484
|
+
eval_results = self.socket.get_received_data()
|
485
|
+
print(f"Received {len(eval_results)} results for evaluation '{eval}'")
|
486
|
+
|
487
|
+
# Add these results to our overall results
|
488
|
+
results = self.finalResp(eval_results)
|
489
|
+
print(f"======= Completed evaluation: {eval} =======\n")
|
490
|
+
|
491
|
+
print("All evaluations completed successfully")
|
492
|
+
|
493
|
+
except Exception as e:
|
494
|
+
print(f"Error during evaluation: {e}")
|
495
|
+
raise
|
496
|
+
finally:
|
497
|
+
# Always disconnect the socket when done
|
498
|
+
try:
|
499
|
+
self.socket.disconnect()
|
500
|
+
print("Socket disconnected")
|
501
|
+
except Exception as e:
|
502
|
+
print(f"Error disconnecting socket: {e}")
|
503
|
+
|
504
|
+
compressed_prompt , compressed_prompt_output , cost , cost_saving = costColumnMapping(results,self.processMapping)
|
505
|
+
dataframe["compressed_prompt"] = compressed_prompt
|
506
|
+
dataframe["compressed_prompt_output"] = compressed_prompt_output
|
507
|
+
dataframe["cost"] = cost
|
508
|
+
dataframe["cost_saving"] = cost_saving
|
509
|
+
return dataframe
|
510
|
+
|
511
|
+
|
512
|
+
def run_sweep(self,templates: List[str], dataset: Dict[str, List[str]], model_aliases: List[AVAILABLEMODELS], apiKey: str, evals = ["Response Correctness"]) -> pd.DataFrame:
|
513
|
+
executor = ModelExecutor(apiKey)
|
514
|
+
|
515
|
+
keys = list(dataset.keys())
|
516
|
+
value_combinations = list(itertools.product(*dataset.values()))
|
517
|
+
combinations = [dict(zip(keys, values)) for values in value_combinations]
|
518
|
+
|
519
|
+
results = []
|
520
|
+
|
521
|
+
# Iterate through combinations
|
522
|
+
for combo in combinations:
|
523
|
+
for template in templates:
|
524
|
+
prompt = template
|
525
|
+
for k, v in combo.items():
|
526
|
+
prompt = prompt.replace(f"{{{{{k}}}}}", v)
|
527
|
+
# Add a row for each model
|
528
|
+
for model in model_aliases:
|
529
|
+
row = {
|
530
|
+
"template": template,
|
531
|
+
"prompt": prompt,
|
532
|
+
**combo,
|
533
|
+
"model": model.value
|
534
|
+
}
|
535
|
+
|
536
|
+
|
537
|
+
try:
|
538
|
+
provider = getProviderFromModel(model)
|
539
|
+
response = executor.execute(provider, model.value, prompt, apiKey)
|
540
|
+
row["output"] = response
|
541
|
+
except Exception as e:
|
542
|
+
row["output"] = f"Error: {str(e)}"
|
543
|
+
|
544
|
+
results.append(row)
|
545
|
+
df=pd.DataFrame(results)
|
546
|
+
df.to_csv("sweep_results.csv", index=False)
|
547
|
+
print(str(templates[0]))
|
548
|
+
res = self.evaluate(df,evals =evals,prompt_template=str(templates[0]))
|
549
|
+
return res
|
550
|
+
|
551
|
+
|
552
|
+
class SafeDict(dict):
|
553
|
+
def __missing__(self, key):
|
554
|
+
return ""
|
llumo/exceptions.py
CHANGED
@@ -1,30 +1,31 @@
|
|
1
|
-
class LlumoAPIError(Exception):
|
2
|
-
"""Base class for all Llumo SDK-related errors."""
|
3
|
-
|
4
|
-
def __init__(self, message):
|
5
|
-
self.message = message
|
6
|
-
super().__init__(self.message)
|
7
|
-
|
8
|
-
@staticmethod
|
9
|
-
def InvalidApiKey():
|
10
|
-
return LlumoAPIError("The provided API key is invalid or unauthorized")
|
11
|
-
|
12
|
-
@staticmethod
|
13
|
-
def InvalidApiResponse():
|
14
|
-
return LlumoAPIError("Invalid or UnexpectedError response from the API")
|
15
|
-
|
16
|
-
@staticmethod
|
17
|
-
def RequestFailed(detail="The request to the API failed"):
|
18
|
-
return LlumoAPIError(f"Request to the API failed: {detail}")
|
19
|
-
|
20
|
-
@staticmethod
|
21
|
-
def InvalidJsonResponse():
|
22
|
-
return LlumoAPIError("The API response is not in valid JSON format")
|
23
|
-
|
24
|
-
@staticmethod
|
25
|
-
def UnexpectedError(detail="An UnexpectedError error occurred"):
|
26
|
-
return LlumoAPIError(f"UnexpectedError error: {detail}")
|
27
|
-
|
28
|
-
@staticmethod
|
29
|
-
def EvalError(detail="Some error occured while processing"):
|
30
|
-
return LlumoAPIError(f"error: {detail}")
|
1
|
+
class LlumoAPIError(Exception):
|
2
|
+
"""Base class for all Llumo SDK-related errors."""
|
3
|
+
|
4
|
+
def __init__(self, message):
|
5
|
+
self.message = message
|
6
|
+
super().__init__(self.message)
|
7
|
+
|
8
|
+
@staticmethod
|
9
|
+
def InvalidApiKey():
|
10
|
+
return LlumoAPIError("The provided API key is invalid or unauthorized")
|
11
|
+
|
12
|
+
@staticmethod
|
13
|
+
def InvalidApiResponse():
|
14
|
+
return LlumoAPIError("Invalid or UnexpectedError response from the API")
|
15
|
+
|
16
|
+
@staticmethod
|
17
|
+
def RequestFailed(detail="The request to the API failed"):
|
18
|
+
return LlumoAPIError(f"Request to the API failed: {detail}")
|
19
|
+
|
20
|
+
@staticmethod
|
21
|
+
def InvalidJsonResponse():
|
22
|
+
return LlumoAPIError("The API response is not in valid JSON format")
|
23
|
+
|
24
|
+
@staticmethod
|
25
|
+
def UnexpectedError(detail="An UnexpectedError error occurred"):
|
26
|
+
return LlumoAPIError(f"UnexpectedError error: {detail}")
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def EvalError(detail="Some error occured while processing"):
|
30
|
+
return LlumoAPIError(f"error: {detail}")
|
31
|
+
|
llumo/execution.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
import openai
|
2
|
+
import google.generativeai as genai
|
3
|
+
from .models import Provider
|
4
|
+
|
5
|
+
class ModelExecutor:
|
6
|
+
def __init__(self, apiKey: str):
|
7
|
+
self.apiKey = apiKey
|
8
|
+
|
9
|
+
def execute(self, provider: Provider, modelName: str, prompt: str,api_key) -> str:
|
10
|
+
if provider == Provider.OPENAI:
|
11
|
+
return self._executeOpenAI(modelName, prompt,api_key)
|
12
|
+
elif provider == Provider.GOOGLE:
|
13
|
+
return self._executeGoogle(modelName, prompt,api_key)
|
14
|
+
else:
|
15
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
16
|
+
|
17
|
+
def _executeOpenAI(self, modelName: str, prompt: str,api_key) -> str:
|
18
|
+
client = openai.OpenAI(api_key=api_key)
|
19
|
+
response = client.chat.completions.create(model="gpt-4", # Replace with the desired model
|
20
|
+
messages=[
|
21
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
22
|
+
{"role": "user", "content": prompt} # User's prompt
|
23
|
+
]
|
24
|
+
)
|
25
|
+
return response.choices[0].message.content
|
26
|
+
|
27
|
+
def _executeGoogle(self, modelName: str, prompt: str,api_key) -> str:
|
28
|
+
|
29
|
+
# Configure GenAI with API Key
|
30
|
+
genai.configure(api_key=api_key)
|
31
|
+
|
32
|
+
# Select Generative Model
|
33
|
+
model = genai.GenerativeModel("gemini-1.5-flash-latest")
|
34
|
+
# Generate Response
|
35
|
+
response = model.generate_content(prompt)
|
36
|
+
return response.text
|
37
|
+
|
38
|
+
|
39
|
+
|
llumo/helpingFuntions.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
import time
|
2
|
+
import uuid
|
3
|
+
|
4
|
+
def getProcessID():
|
5
|
+
return f"{int(time.time() * 1000)}{uuid.uuid4()}"
|
6
|
+
|
7
|
+
|
8
|
+
def getInputPopulatedPrompt(promptTemplate, tempObj):
|
9
|
+
for key, value in tempObj.items():
|
10
|
+
promptTemplate = promptTemplate.replace(f"{{{{{key}}}}}", value)
|
11
|
+
return promptTemplate
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
import time
|
16
|
+
import uuid
|
17
|
+
|
18
|
+
def getProcessID():
|
19
|
+
return f"{int(time.time() * 1000)}{uuid.uuid4()}"
|
20
|
+
|
21
|
+
|
22
|
+
def getInputPopulatedPrompt(promptTemplate, tempObj):
|
23
|
+
for key, value in tempObj.items():
|
24
|
+
promptTemplate = promptTemplate.replace(f"{{{{{key}}}}}", value)
|
25
|
+
return promptTemplate
|
26
|
+
|
27
|
+
def costColumnMapping(costResults,allProcess):
|
28
|
+
# this dict will store cost column data for each row
|
29
|
+
cost_cols = {}
|
30
|
+
compressed_prompt = []
|
31
|
+
compressed_prompt_output = []
|
32
|
+
cost = []
|
33
|
+
cost_saving = []
|
34
|
+
print("BATCHES: ",allProcess)
|
35
|
+
print("COST RESULTS :", costResults)
|
36
|
+
# iterate through each batch
|
37
|
+
for record in allProcess:
|
38
|
+
cost_cols[record] = []
|
39
|
+
# iterate through each record of cost saving results received from the api
|
40
|
+
for item in costResults:
|
41
|
+
# fetching all cost column data for a specific row. i.e each row will have 4 columns
|
42
|
+
if list(item.keys())[0].split("-")[0] == record.split("-")[0]:
|
43
|
+
cost_cols[record].append(list(item.values())[0])
|
44
|
+
|
45
|
+
for ky, val in cost_cols.items():
|
46
|
+
# compressed prompt column
|
47
|
+
compressed_prompt.append(val[0])
|
48
|
+
# compressed output
|
49
|
+
compressed_prompt_output.append(val[1])
|
50
|
+
# cost
|
51
|
+
cost.append(val[2])
|
52
|
+
# cost saved
|
53
|
+
cost_saving.append(val[3])
|
54
|
+
|
55
|
+
return compressed_prompt , compressed_prompt_output , cost , cost_saving
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
|
llumo/models.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
class Provider(str, Enum):
|
4
|
+
OPENAI = "OPENAI"
|
5
|
+
GOOGLE = "GOOGLE"
|
6
|
+
|
7
|
+
# Maps model aliases → (provider, actual model name for API)
|
8
|
+
_MODEL_METADATA = {
|
9
|
+
"GPT_4": (Provider.OPENAI, "gpt-4"),
|
10
|
+
"GPT_4_32K": (Provider.OPENAI, "gpt-4-32k"),
|
11
|
+
"GPT_35T": (Provider.OPENAI, "gpt-3.5-turbo"),
|
12
|
+
"GPT_35T_INS": (Provider.OPENAI, "gpt-3.5-turbo-instruct"),
|
13
|
+
"GPT_35T_16K": (Provider.OPENAI, "gpt-3.5-turbo-16k"),
|
14
|
+
"GPT_35_TURBO": (Provider.OPENAI, "gpt-3.5-turbo"),
|
15
|
+
|
16
|
+
"GOOGLE_15_FLASH": (Provider.GOOGLE, "gemini-1.5-flash-latest"),
|
17
|
+
"GEMINI_PRO": (Provider.GOOGLE, "gemini-pro"),
|
18
|
+
"TEXT_BISON": (Provider.GOOGLE, "text-bison-001"),
|
19
|
+
"CHAT_BISON": (Provider.GOOGLE, "chat-bison-001"),
|
20
|
+
"TEXT_BISON_32K": (Provider.GOOGLE, "text-bison-32k"),
|
21
|
+
"TEXT_UNICORN": (Provider.GOOGLE, "text-unicorn-experimental"),
|
22
|
+
}
|
23
|
+
|
24
|
+
class AVAILABLEMODELS(str, Enum):
|
25
|
+
GPT_4 = "gpt-4"
|
26
|
+
GPT_4_32K = "gpt-4-32k"
|
27
|
+
GPT_35T = "gpt-3.5-turbo"
|
28
|
+
GPT_35T_INS = "gpt-3.5-turbo-instruct"
|
29
|
+
GPT_35T_16K = "gpt-3.5-turbo-16k"
|
30
|
+
GPT_35_TURBO = "gpt-3.5-turbo"
|
31
|
+
|
32
|
+
GOOGLE_15_FLASH = "gemini-1.5-flash-latest"
|
33
|
+
GEMINI_PRO = ""
|
34
|
+
TEXT_BISON = "text-bison-001"
|
35
|
+
CHAT_BISON = "chat-bison-001"
|
36
|
+
TEXT_BISON_32K = "text-bison-32k"
|
37
|
+
TEXT_UNICORN = "text-unicorn-experimental"
|
38
|
+
|
39
|
+
def getProviderFromModel(model: AVAILABLEMODELS) -> Provider:
|
40
|
+
for alias, (provider, apiName) in _MODEL_METADATA.items():
|
41
|
+
if model.value == apiName:
|
42
|
+
return provider
|
43
|
+
raise ValueError(f"Provider not found for model: {model}")
|
llumo/sockets.py
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
import socketio
|
2
|
+
import threading
|
3
|
+
import time
|
4
|
+
|
5
|
+
|
6
|
+
class LlumoSocketClient:
|
7
|
+
def __init__(self, socket_url):
|
8
|
+
self.socket_url = socket_url
|
9
|
+
self._received_data = []
|
10
|
+
self._last_update_time = None
|
11
|
+
self._listening_done = threading.Event()
|
12
|
+
self._connection_established = threading.Event()
|
13
|
+
self._lock = threading.Lock()
|
14
|
+
self._connected = False
|
15
|
+
self.server_socket_id = None # Store the server-assigned socket ID
|
16
|
+
|
17
|
+
# Initialize client
|
18
|
+
self.sio = socketio.Client(
|
19
|
+
# logger=True,
|
20
|
+
# engineio_logger=True,
|
21
|
+
reconnection=True,
|
22
|
+
reconnection_attempts=5,
|
23
|
+
reconnection_delay=1,
|
24
|
+
)
|
25
|
+
|
26
|
+
@self.sio.on("connect")
|
27
|
+
def on_connect():
|
28
|
+
print("Socket connection established")
|
29
|
+
self._connected = True
|
30
|
+
# Don't set connection_established yet - wait for server confirmation
|
31
|
+
|
32
|
+
# Listen for the connection-established event from the server
|
33
|
+
@self.sio.on("connection-established")
|
34
|
+
def on_connection_established(data):
|
35
|
+
print(
|
36
|
+
f"Server acknowledged connection with 'connection-established' event: {data}"
|
37
|
+
)
|
38
|
+
if isinstance(data, dict) and "socketId" in data:
|
39
|
+
self.server_socket_id = data["socketId"]
|
40
|
+
print(f"Received server socket ID: {self.server_socket_id}")
|
41
|
+
self._connection_established.set()
|
42
|
+
|
43
|
+
@self.sio.on("result-update")
|
44
|
+
def on_result_update(data):
|
45
|
+
with self._lock:
|
46
|
+
print(f"Received result-update event: {data}")
|
47
|
+
self._received_data.append(data)
|
48
|
+
self._last_update_time = time.time()
|
49
|
+
|
50
|
+
@self.sio.on("disconnect")
|
51
|
+
def on_disconnect():
|
52
|
+
print("Socket disconnected")
|
53
|
+
self._connected = False
|
54
|
+
|
55
|
+
@self.sio.on("connect_error")
|
56
|
+
def on_connect_error(error):
|
57
|
+
print(f"Socket connection error: {error}")
|
58
|
+
|
59
|
+
@self.sio.on("error")
|
60
|
+
def on_error(error):
|
61
|
+
print(f"Socket error event: {error}")
|
62
|
+
|
63
|
+
def connect(self, timeout=20):
|
64
|
+
self._received_data = []
|
65
|
+
self._connection_established.clear()
|
66
|
+
self._listening_done.clear()
|
67
|
+
self.server_socket_id = None
|
68
|
+
|
69
|
+
try:
|
70
|
+
print("Attempting direct WebSocket connection...")
|
71
|
+
# Connect with websocket transport
|
72
|
+
self.sio.connect(self.socket_url, transports=["websocket"], wait=True)
|
73
|
+
|
74
|
+
print(f"Engine.IO connection established with SID: {self.sio.sid}")
|
75
|
+
print(
|
76
|
+
"Waiting for server to acknowledge connection with connection-established event..."
|
77
|
+
)
|
78
|
+
|
79
|
+
# Wait for the connection-established event
|
80
|
+
if not self._connection_established.wait(timeout):
|
81
|
+
raise RuntimeError("Timed out waiting for connection-established event")
|
82
|
+
|
83
|
+
self._last_update_time = time.time()
|
84
|
+
print(
|
85
|
+
f"Connection fully established. Server socket ID: {self.server_socket_id}"
|
86
|
+
)
|
87
|
+
|
88
|
+
# Return the server-assigned socket ID if available, otherwise fall back to the client's SID
|
89
|
+
return self.server_socket_id or self.sio.sid
|
90
|
+
except Exception as e:
|
91
|
+
self._connected = False
|
92
|
+
raise RuntimeError(f"WebSocket connection failed: {e}")
|
93
|
+
|
94
|
+
def listen_for_results(self, min_wait=5, max_wait=300, inactivity_timeout=30):
|
95
|
+
"""
|
96
|
+
Listen for results with improved timeout handling:
|
97
|
+
- min_wait: Minimum time to wait even if no data is received
|
98
|
+
- max_wait: Maximum total time to wait for results
|
99
|
+
- inactivity_timeout: Time to wait after last data received
|
100
|
+
"""
|
101
|
+
if not self._connected:
|
102
|
+
raise RuntimeError("WebSocket is not connected. Call connect() first.")
|
103
|
+
|
104
|
+
start_time = time.time()
|
105
|
+
self._last_update_time = time.time()
|
106
|
+
|
107
|
+
def timeout_watcher():
|
108
|
+
while not self._listening_done.is_set():
|
109
|
+
current_time = time.time()
|
110
|
+
time_since_last_update = current_time - self._last_update_time
|
111
|
+
total_elapsed = current_time - start_time
|
112
|
+
|
113
|
+
# Always wait for minimum time
|
114
|
+
if total_elapsed < min_wait:
|
115
|
+
time.sleep(0.5)
|
116
|
+
continue
|
117
|
+
|
118
|
+
# Stop if maximum time exceeded
|
119
|
+
if total_elapsed > max_wait:
|
120
|
+
print(
|
121
|
+
f"⚠️ Maximum wait time of {max_wait}s reached, stopping listener."
|
122
|
+
)
|
123
|
+
self._listening_done.set()
|
124
|
+
break
|
125
|
+
|
126
|
+
# Stop if no activity for inactivity_timeout
|
127
|
+
if time_since_last_update > inactivity_timeout:
|
128
|
+
print(
|
129
|
+
f"⚠️ No data received for {inactivity_timeout}s, stopping listener."
|
130
|
+
)
|
131
|
+
self._listening_done.set()
|
132
|
+
break
|
133
|
+
|
134
|
+
# Check every second
|
135
|
+
time.sleep(1)
|
136
|
+
|
137
|
+
timeout_thread = threading.Thread(target=timeout_watcher, daemon=True)
|
138
|
+
timeout_thread.start()
|
139
|
+
print("Started listening for WebSocket events...")
|
140
|
+
self._listening_done.wait()
|
141
|
+
print(f"Finished listening. Received {len(self._received_data)} data updates.")
|
142
|
+
|
143
|
+
def get_received_data(self):
|
144
|
+
with self._lock:
|
145
|
+
return self._received_data.copy()
|
146
|
+
|
147
|
+
def disconnect(self):
|
148
|
+
try:
|
149
|
+
if self._connected:
|
150
|
+
self.sio.disconnect()
|
151
|
+
self._connected = False
|
152
|
+
print("WebSocket client disconnected")
|
153
|
+
except Exception as e:
|
154
|
+
print(f"Error during WebSocket disconnect: {e}")
|
@@ -1,23 +1,26 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: llumo
|
3
|
-
Version: 0.1.
|
4
|
-
Summary: Python SDK for interacting with the Llumo ai API.
|
5
|
-
Home-page: https://www.llumo.ai/
|
6
|
-
Author: Llumo
|
7
|
-
Author-email: product@llumo.ai
|
8
|
-
License: Proprietary
|
9
|
-
Requires-Python: >=3.7
|
10
|
-
License-File: LICENSE
|
11
|
-
Requires-Dist: requests>=2.
|
12
|
-
Requires-Dist:
|
13
|
-
Requires-Dist:
|
14
|
-
Requires-Dist:
|
15
|
-
Requires-Dist:
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
Dynamic:
|
20
|
-
Dynamic:
|
21
|
-
Dynamic:
|
22
|
-
Dynamic:
|
23
|
-
Dynamic:
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: llumo
|
3
|
+
Version: 0.1.4
|
4
|
+
Summary: Python SDK for interacting with the Llumo ai API.
|
5
|
+
Home-page: https://www.llumo.ai/
|
6
|
+
Author: Llumo
|
7
|
+
Author-email: product@llumo.ai
|
8
|
+
License: Proprietary
|
9
|
+
Requires-Python: >=3.7
|
10
|
+
License-File: LICENSE
|
11
|
+
Requires-Dist: requests>=2.0.0
|
12
|
+
Requires-Dist: websocket-client>=1.0.0
|
13
|
+
Requires-Dist: pandas>=1.0.0
|
14
|
+
Requires-Dist: numpy>=1.0.0
|
15
|
+
Requires-Dist: python-socketio[client]==5.13.0
|
16
|
+
Requires-Dist: python-dotenv==1.1.0
|
17
|
+
Requires-Dist: openai==1.75.0
|
18
|
+
Requires-Dist: google-generativeai==0.8.5
|
19
|
+
Dynamic: author
|
20
|
+
Dynamic: author-email
|
21
|
+
Dynamic: home-page
|
22
|
+
Dynamic: license
|
23
|
+
Dynamic: license-file
|
24
|
+
Dynamic: requires-dist
|
25
|
+
Dynamic: requires-python
|
26
|
+
Dynamic: summary
|
@@ -0,0 +1,13 @@
|
|
1
|
+
llumo/.env,sha256=Vx5FkuywpYHXH2N8epJ7PlNOPiwx9UP9DUz4vWd0urs,373
|
2
|
+
llumo/__init__.py,sha256=8ZgAtxJNNgHorEXoxaLQ2YWrVXGgamoayyLMD1L4FbE,183
|
3
|
+
llumo/client.py,sha256=vu4xpjKOCK9Lb6dttZJ28PnxO8Wf5OR_YRGoDVLXG7o,23238
|
4
|
+
llumo/exceptions.py,sha256=l3_5d9cBMm-hwpuFrg3nvI9cEP2GTKXcCyWiWHwnYDM,1041
|
5
|
+
llumo/execution.py,sha256=ZvbZDSAvwj1XwSlgPNiy4r9fZG_vtfSlaWGwNI9xCa8,1453
|
6
|
+
llumo/helpingFuntions.py,sha256=HPy2w3IaYfH_hDBgXdoAmNZmAbDUO01bgW7gHBGNw8A,1765
|
7
|
+
llumo/models.py,sha256=WBtnu7ckOy9TGRiwswz04xOGYF6EslTUOxHUz4QWzUA,1602
|
8
|
+
llumo/sockets.py,sha256=M6piy6bNt342GmTQCdUJJDUgMYGxk0Acjgj11uI4Vdg,5965
|
9
|
+
llumo-0.1.4.dist-info/licenses/LICENSE,sha256=vMiqSi3KpDHq3RFxKiqdh10ZUF3PjE3nnntANU-HEu4,186
|
10
|
+
llumo-0.1.4.dist-info/METADATA,sha256=eheCu7zcfVenaaywY-2m0X2wZHWc_M13pI7oNV57m6U,721
|
11
|
+
llumo-0.1.4.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
12
|
+
llumo-0.1.4.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
|
13
|
+
llumo-0.1.4.dist-info/RECORD,,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2025 Llumo AI
|
2
|
-
|
3
|
-
All rights reserved. This software is proprietary and confidential.
|
4
|
-
Unauthorized copying, distribution, or use of this software is strictly prohibited.
|
1
|
+
Copyright (c) 2025 Llumo AI
|
2
|
+
|
3
|
+
All rights reserved. This software is proprietary and confidential.
|
4
|
+
Unauthorized copying, distribution, or use of this software is strictly prohibited.
|
llumo-0.1.1.dist-info/RECORD
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
llumo/__init__.py,sha256=W3XFr7TrJsFYI6a5kXUzWqMP-VA1IXUYELqc--mjBaM,70
|
2
|
-
llumo/client.py,sha256=S7IPdVZZbEqwFpIeMC48OFNAXUJWsFSvtAWDphNTZUg,2411
|
3
|
-
llumo/exceptions.py,sha256=RQMcL7FwWLvkT7P5hn4idO60xoEIPLy_9pq7LPQ9slI,1009
|
4
|
-
llumo-0.1.1.dist-info/licenses/LICENSE,sha256=tF9yAcfPV9xGT3ViWmC8hPvOo8BEk4ZICbUfcEo8Dlk,182
|
5
|
-
llumo-0.1.1.dist-info/METADATA,sha256=apy-jkN-8D8ELDh8kFWpcsBj_WFJxAPr-IfkSG60KFs,570
|
6
|
-
llumo-0.1.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
7
|
-
llumo-0.1.1.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
|
8
|
-
llumo-0.1.1.dist-info/RECORD,,
|
File without changes
|