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 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
@@ -1,2 +1,7 @@
1
- from .client import LlumoClient
2
- from .exceptions import LlumoAPIError
1
+ from .client import LlumoClient
2
+ from .exceptions import LlumoAPIError
3
+ from .helpingFuntions import *
4
+ from .models import AVAILABLEMODELS
5
+ from .execution import ModelExecutor
6
+
7
+
llumo/client.py CHANGED
@@ -1,77 +1,554 @@
1
- import requests
2
- from .exceptions import LlumoAPIError
3
-
4
- class LlumoClient:
5
- """
6
- A client to interact with Llumo API for evaluating AI-generated responses
7
-
8
- """
9
-
10
- base_url = "https://app.llumo.ai/api"
11
-
12
- def __init__(self, api_key):
13
- """
14
- Initializes the LlumoClient with the given API key.
15
-
16
- Parameters:
17
- - api_key (str): The Llumo API key for authentication.
18
- """
19
- self.api_key = api_key
20
-
21
-
22
- def EvaluateGrounded(self, outputText, groundTruthText, embeddingModelName="Google", metricsName="Cosine"):
23
- """
24
- Evaluates the groundedness of a response using a similarity metric.
25
-
26
- Parameters:
27
- - outputText (str): The generated output text to evaluate.
28
- - groundTruthText (str): The reference ground truth text.
29
- - embeddingModelName (str): Name of the embedding model to use. Default is "Google".
30
- - metricsName (str): Similarity metric to apply (e.g., "Bleu"). Default is "Cosine".
31
- - test
32
- Returns:
33
- - dict: Contains statusCode, message, and evaluation data if successful.
34
-
35
- Raises:
36
- - LlumoAPIError for all specific error types.
37
- """
38
- url = f"{self.base_url}/external/grounded-external"
39
-
40
- requestBody = {
41
- "prompt": outputText,
42
- "groundTruth": groundTruthText,
43
- "embeddingModel": embeddingModelName,
44
- "similarityMetric": metricsName,
45
- }
46
-
47
- headers = {
48
- "Authorization": f"Bearer {self.api_key}",
49
- "Content-Type": "application/json"
50
- }
51
-
52
- try:
53
- res = requests.post(url=url, json=requestBody, headers=headers)
54
-
55
- if res.status_code == 401:
56
- raise LlumoAPIError.InvalidApiKey()
57
-
58
- res.raise_for_status()
59
- result = res.json()
60
-
61
- if 'data' not in result:
62
- raise LlumoAPIError.InvalidApiResponse()
63
-
64
- return {
65
- "statusCode": result['data'].get('statusCode'),
66
- "message": result['data'].get('message'),
67
- "analytics": result['data']
68
- }
69
-
70
- except requests.exceptions.HTTPError as e:
71
- raise LlumoAPIError.RequestFailed(str(e))
72
- except ValueError:
73
- raise LlumoAPIError.InvalidJsonResponse()
74
- except Exception as e:
75
- raise LlumoAPIError.UnexpectedError(str(e))
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
+
@@ -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.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.25.1
12
- Requires-Dist: setuptools>=58.1.0
13
- Requires-Dist: twine>=6.1.0
14
- Requires-Dist: wheel>=0.45.1
15
- Requires-Dist: build>=1.2.2.post1
16
- Dynamic: author
17
- Dynamic: author-email
18
- Dynamic: home-page
19
- Dynamic: license
20
- Dynamic: license-file
21
- Dynamic: requires-dist
22
- Dynamic: requires-python
23
- Dynamic: summary
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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.
@@ -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,,