llumo 0.1.0__py3-none-any.whl → 0.1.3__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
1
  from .client import LlumoClient
2
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,76 +1,551 @@
1
1
  import requests
2
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
3
17
 
4
- class LlumoClient:
5
- """
6
- A client to interact with Llumo API for evaluating AI-generated responses
7
- """
18
+ load_dotenv() # Automatically looks for .env in current directory
19
+
20
+ postUrl = os.getenv("postUrl")
21
+ fetchUrl = os.getenv("fetchUrl")
22
+ validateUrl = os.getenv("validateUrl")
23
+ socketUrl = os.getenv("SOCKET_URL")
8
24
 
9
- base_url = "https://app.llumo.ai/api"
25
+
26
+ class LlumoClient:
10
27
 
11
28
  def __init__(self, api_key):
12
- """
13
- Initializes the LlumoClient with the given API key.
14
-
15
- Parameters:
16
- - api_key (str): The Llumo API key for authentication.
17
- """
18
- self.api_key = api_key
19
-
20
-
21
- def EvaluateGrounded(self, outputText, groundTruthText, embeddingModelName="Google", metricsName="Cosine"):
22
- """
23
- Evaluates the groundedness of a response using a similarity metric.
24
-
25
- Parameters:
26
- - outputText (str): The generated output text to evaluate.
27
- - groundTruthText (str): The reference ground truth text.
28
- - embeddingModelName (str): Name of the embedding model to use. Default is "Google".
29
- - metricsName (str): Similarity metric to apply (e.g., "Bleu"). Default is "Cosine".
30
-
31
- Returns:
32
- - dict: Contains statusCode, message, and evaluation data if successful.
33
-
34
- Raises:
35
- - LlumoAPIError for all specific error types.
36
- """
37
- url = f"{self.base_url}/external/grounded-external"
38
-
39
- requestBody = {
40
- "prompt": outputText,
41
- "groundTruth": groundTruthText,
42
- "embeddingModel": embeddingModelName,
43
- "similarityMetric": metricsName,
44
- }
29
+ self.apiKey = api_key
30
+ self.socket = LlumoSocketClient(socketUrl)
31
+ self.processMapping = {}
32
+
45
33
 
34
+ def validateApiKey(self, evalName = ""):
46
35
  headers = {
47
- "Authorization": f"Bearer {self.api_key}",
48
- "Content-Type": "application/json"
36
+ "Authorization": f"Bearer {self.apiKey}",
37
+ "Content-Type": "application/json",
49
38
  }
39
+ reqBody = {"analytics": [evalName]}
40
+
41
+ print(f"Making API key validation request to: {validateUrl}")
42
+ print(f"Request body: {reqBody}")
50
43
 
51
44
  try:
52
- res = requests.post(url=url, json=requestBody, headers=headers)
45
+ response = requests.post(url=validateUrl, json=reqBody, headers=headers)
46
+ print(response.text)
47
+ # Print response info for debugging
48
+ print(f"Response status code: {response.status_code}")
49
+ print(f"Response headers: {response.headers}")
53
50
 
54
- if res.status_code == 401:
55
- raise LlumoAPIError.InvalidApiKey()
51
+ # Try to get at least some of the response content
52
+ try:
53
+ response_preview = response.text[:500] # First 500 chars
54
+ print(f"Response preview: {response_preview}")
55
+ except Exception as e:
56
+ print(f"Could not get response preview: {e}")
56
57
 
57
- res.raise_for_status()
58
- result = res.json()
58
+ except requests.exceptions.RequestException as e:
59
+ print(f"Request exception: {str(e)}")
60
+ raise LlumoAPIError.RequestFailed(detail=str(e))
59
61
 
60
- if 'data' not in result:
61
- raise LlumoAPIError.InvalidApiResponse()
62
+ if response.status_code == 401:
63
+ raise LlumoAPIError.InvalidApiKey()
62
64
 
63
- return {
64
- "statusCode": result['data'].get('statusCode'),
65
- "message": result['data'].get('message'),
66
- "analytics": result['data']
67
- }
65
+ # Handle other common status codes
66
+ if response.status_code == 404:
67
+ raise LlumoAPIError.RequestFailed(
68
+ detail=f"Endpoint not found (404): {validateUrl}"
69
+ )
68
70
 
69
- except requests.exceptions.HTTPError as e:
70
- raise LlumoAPIError.RequestFailed(str(e))
71
- except ValueError:
71
+ if response.status_code >= 500:
72
+ raise LlumoAPIError.ServerError(
73
+ detail=f"Server error ({response.status_code})"
74
+ )
75
+
76
+ if response.status_code != 200:
77
+ raise LlumoAPIError.RequestFailed(
78
+ detail=f"Unexpected status code: {response.status_code}"
79
+ )
80
+
81
+ # Try to parse JSON
82
+ try:
83
+ data = response.json()
84
+ except ValueError as e:
85
+ print(f"JSON parsing error: {str(e)}")
86
+ print(
87
+ f"Response content that could not be parsed: {response.text[:1000]}..."
88
+ )
72
89
  raise LlumoAPIError.InvalidJsonResponse()
90
+
91
+ if "data" not in data or not data["data"]:
92
+ print(f"Invalid API response structure: {data}")
93
+ raise LlumoAPIError.InvalidApiResponse()
94
+
95
+ try:
96
+ self.hitsAvailable = data["data"].get("remainingHits", 0)
97
+ self.workspaceID = data["data"].get("workspaceID")
98
+ self.evalDefinition = data["data"].get("analyticsMapping")
99
+ self.token = data["data"].get("token")
100
+
101
+ print(f"API key validation successful:")
102
+ # print(f"- Remaining hits: {self.hitsAvailable}")
103
+ # print(f"- Workspace ID: {self.workspaceID}")
104
+ # print(f"- Token received: {'Yes' if self.token else 'No'}")
105
+
106
+ except Exception as e:
107
+ print(f"Error extracting data from response: {str(e)}")
108
+ raise LlumoAPIError.UnexpectedError(detail=str(e))
109
+
110
+ def postBatch(self, batch, workspaceID):
111
+ payload = {
112
+ "batch": json.dumps(batch),
113
+ "runType": "EVAL",
114
+ "workspaceID": workspaceID,
115
+ }
116
+ headers = {
117
+ "Authorization": f"Bearer {self.token}",
118
+ "Content-Type": "application/json",
119
+ }
120
+ try:
121
+ print(postUrl)
122
+ response = requests.post(postUrl, json=payload, headers=headers)
123
+ # print(f"Post API Status Code: {response.status_code}")
124
+ # print(response.text)
125
+
73
126
  except Exception as e:
74
- raise LlumoAPIError.UnexpectedError(str(e))
127
+ print(f"Error in posting batch: {e}")
128
+
129
+ def AllProcessMapping(self):
130
+ for batch in self.allBatches:
131
+ for record in batch:
132
+ rowId = record['rowID']
133
+ colId = record['columnID']
134
+ pid = f'{rowId}-{colId}-{colId}'
135
+ self.processMapping[pid] = record
136
+
137
+
138
+ def finalResp(self,results):
139
+ seen = set()
140
+ uniqueResults = []
141
+
142
+ for item in results:
143
+ for rowID in item: # Each item has only one key
144
+ if rowID not in seen:
145
+ seen.add(rowID)
146
+ uniqueResults.append(item)
147
+
148
+ return uniqueResults
149
+
150
+ def evaluate(self, dataframe, evals=["Response Completeness"],prompt_template = ""):
151
+ results = {}
152
+ try:
153
+ # Connect to socket first
154
+ print("Connecting to socket server...")
155
+ socketID = self.socket.connect(timeout=20)
156
+ print(f"Connected with socket ID: {socketID}")
157
+
158
+ # Process each evaluation
159
+ for eval in evals:
160
+ print(f"\n======= Running evaluation for: {eval} =======")
161
+
162
+ try:
163
+ print(f"Validating API key for {eval}...")
164
+ self.validateApiKey(evalName=eval)
165
+ print(
166
+ f"API key validation successful. Hits available: {self.hitsAvailable}"
167
+ )
168
+ except Exception as e:
169
+ print(f"Error during API key validation: {str(e)}")
170
+ if (
171
+ hasattr(e, "response")
172
+ and getattr(e, "response", None) is not None
173
+ ):
174
+ print(f"Status code: {e.response.status_code}")
175
+ print(f"Response content: {e.response.text[:500]}...")
176
+ raise
177
+
178
+ if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
179
+ raise LlumoAPIError.InsufficientCredits()
180
+
181
+ evalDefinition = self.evalDefinition[eval]
182
+ model = "GPT_4"
183
+ provider = "OPENAI"
184
+ evalType = "LLM"
185
+ workspaceID = self.workspaceID
186
+
187
+ # Prepare all batches before sending
188
+ print("Preparing batches...")
189
+ self.allBatches = []
190
+ currentBatch = []
191
+
192
+ for index, row in dataframe.iterrows():
193
+
194
+ tools = row["tools"] if "tools" in dataframe.columns else []
195
+ groundTruth = row["groundTruth"] if "groundTruth" in dataframe.columns else ""
196
+ messageHistory = row["messageHistory"] if "messageHistory" in dataframe.columns else []
197
+ promptTemplate = prompt_template
198
+
199
+ keys = re.findall(r"{{(.*?)}}", promptTemplate)
200
+
201
+ # extracting the required values for the the columns based on the prompt template
202
+ inputDict = {key: row[key] for key in keys if key in row}
203
+ output = row["output"]
204
+
205
+ activePlayground = (
206
+ f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
207
+ )
208
+ rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
209
+ columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace(
210
+ "-", ""
211
+ )
212
+
213
+ # Use the server-provided socket ID here
214
+ templateData = {
215
+ "processID": getProcessID(),
216
+ "socketID": socketID, # Using the server-assigned socket ID
217
+ "processData": {
218
+ "executionDependency": {
219
+ "query": "",
220
+ "context": "",
221
+ "output": output,
222
+ "tools": tools,
223
+ "groundTruth": groundTruth,
224
+ "messageHistory": messageHistory,
225
+ },
226
+ "definition": evalDefinition,
227
+ "model": model,
228
+ "provider": provider,
229
+ "analytics": eval,
230
+ },
231
+ "workspaceID": workspaceID,
232
+ "type": "EVAL",
233
+ "evalType": evalType,
234
+ "kpi": eval,
235
+ "columnID": columnID,
236
+ "rowID": rowID,
237
+ "playgroundID": activePlayground,
238
+ "processType": "EVAL",
239
+ }
240
+
241
+ # Build query/context from input
242
+ query = ""
243
+ context = ""
244
+ for key, value in inputDict.items():
245
+ if isinstance(value, str):
246
+ length = len(value.split()) * 1.5
247
+ if length > 50:
248
+ context += f" {key}: {value}, "
249
+ else:
250
+ if promptTemplate:
251
+ tempObj = {key: value}
252
+ promptTemplate = getInputPopulatedPrompt(promptTemplate, tempObj)
253
+ else:
254
+ query += f" {key}: {value}, "
255
+
256
+ if not context.strip():
257
+ for key, value in inputDict.items():
258
+ context += f" {key}: {value}, "
259
+
260
+ templateData["processData"]["executionDependency"]["context"] = context.strip()
261
+ templateData["processData"]["executionDependency"]["query"] = query.strip()
262
+
263
+ if promptTemplate and not query.strip():
264
+ templateData["processData"]["executionDependency"]["query"] = promptTemplate
265
+
266
+ currentBatch.append(templateData)
267
+
268
+ if len(currentBatch) == 10 or index == len(dataframe) - 1:
269
+ self.allBatches.append(currentBatch)
270
+ currentBatch = []
271
+
272
+ # Post all batches
273
+ total_items = sum(len(batch) for batch in self.allBatches)
274
+ print(f"Posting {len(self.allBatches)} batches ({total_items} items total)")
275
+
276
+ for cnt, batch in enumerate(self.allBatches):
277
+ print(
278
+ f"Posting batch {cnt + 1}/{len(self.allBatches)} for eval '{eval}'"
279
+ )
280
+ try:
281
+ self.postBatch(batch=batch, workspaceID=workspaceID)
282
+ print(f"Batch {cnt + 1} posted successfully")
283
+ except Exception as e:
284
+ print(f"Error posting batch {cnt + 1}: {str(e)}")
285
+ continue
286
+
287
+
288
+
289
+ # Small delay between batches to prevent overwhelming the server
290
+ time.sleep(1)
291
+
292
+ # updating the dict for row column mapping
293
+ self.AllProcessMapping()
294
+ # Calculate a reasonable timeout based on the data size
295
+ timeout = max(60, min(600, total_items * 10))
296
+ print(
297
+ f"All batches posted. Waiting up to {timeout} seconds for results..."
298
+ )
299
+
300
+ # Listen for results
301
+ self.socket.listen_for_results(
302
+ min_wait=10, max_wait=timeout, inactivity_timeout=30
303
+ )
304
+
305
+ # Get results for this evaluation
306
+ eval_results = self.socket.get_received_data()
307
+ print(f"Received {len(eval_results)} results for evaluation '{eval}'")
308
+
309
+ # Add these results to our overall results
310
+ results[eval] = self.finalResp(eval_results)
311
+ print(f"======= Completed evaluation: {eval} =======\n")
312
+
313
+ print("All evaluations completed successfully")
314
+
315
+ except Exception as e:
316
+ print(f"Error during evaluation: {e}")
317
+ raise
318
+ finally:
319
+ # Always disconnect the socket when done
320
+ try:
321
+ self.socket.disconnect()
322
+ print("Socket disconnected")
323
+ except Exception as e:
324
+ print(f"Error disconnecting socket: {e}")
325
+
326
+ for evalName, records in results.items():
327
+ for item in records:
328
+ self.processMapping[list(item.keys())[0]] = list(item.values())[0]
329
+
330
+
331
+
332
+ dataframe[evalName] = self.processMapping.values()
333
+
334
+ return dataframe
335
+
336
+ def evaluateCompressor(self, dataframe, prompt_template):
337
+ results = []
338
+ try:
339
+ # Connect to socket first
340
+ print("Connecting to socket server...")
341
+ socketID = self.socket.connect(timeout=20)
342
+ print(f"Connected with socket ID: {socketID}")
343
+
344
+ try:
345
+ print(f"Validating API key...")
346
+ self.validateApiKey()
347
+ print(f"API key validation successful. Hits available: {self.hitsAvailable}")
348
+ except Exception as e:
349
+ print(f"Error during API key validation: {str(e)}")
350
+ if hasattr(e, "response") and getattr(e, "response", None) is not None:
351
+ print(f"Status code: {e.response.status_code}")
352
+ print(f"Response content: {e.response.text[:500]}...")
353
+ raise
354
+
355
+ if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
356
+ raise LlumoAPIError.InsufficientCredits()
357
+
358
+ model = "GPT_4"
359
+ provider = "OPENAI"
360
+ evalType = "LLUMO"
361
+ workspaceID = self.workspaceID
362
+
363
+ # Prepare all batches before sending
364
+ print("Preparing batches...")
365
+ self.allBatches = []
366
+ currentBatch = []
367
+
368
+ for index, row in dataframe.iterrows():
369
+ promptTemplate = prompt_template
370
+
371
+ # extracting the placeholders from the prompt template
372
+ keys = re.findall(r"{{(.*?)}}", promptTemplate)
373
+ inputDict = {key: row[key] for key in keys if key in row}
374
+
375
+ activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
376
+ rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
377
+ columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
378
+
379
+ compressed_prompt_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
380
+ compressed_prompt_output_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
381
+ cost_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
382
+ cost_saving_id = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
383
+
384
+ # Use the server-provided socket ID here
385
+ templateData = {
386
+ "processID": getProcessID(),
387
+ "socketID": socketID,
388
+ "rowID": rowID,
389
+ "columnID": columnID,
390
+ "processType": "COST_SAVING",
391
+ "evalType": evalType,
392
+ "dependency": list(inputDict.keys()),
393
+ "costColumnMapping": {
394
+ "compressed_prompt": compressed_prompt_id,
395
+ "compressed_prompt_output": compressed_prompt_output_id,
396
+ "cost": cost_id,
397
+ "cost_saving": cost_saving_id
398
+ },
399
+ "processData": {
400
+ "rowData": {
401
+ "query": {"type": "VARIABLE", "value": ""},
402
+ "context": {"type": "VARIABLE", "value": ""},
403
+ },
404
+ "dependency": list(inputDict.keys()),
405
+ "dependencyMapping": {ky: ky for ky in list(inputDict.keys())},
406
+ "provider": provider,
407
+ "model": model,
408
+ "promptText": promptTemplate,
409
+ "costColumnMapping": {
410
+ "compressed_prompt": compressed_prompt_id,
411
+ "compressed_prompt_output": compressed_prompt_output_id,
412
+ "cost": cost_id,
413
+ "cost_saving": cost_saving_id
414
+ }
415
+ },
416
+ "workspaceID": workspaceID,
417
+ "email": "",
418
+ "playgroundID": activePlayground
419
+ }
420
+
421
+
422
+ # Build query/context from input
423
+ query = ""
424
+ context = ""
425
+
426
+ for key, value in inputDict.items():
427
+ if isinstance(value, str):
428
+ length = len(value.split()) * 1.5
429
+ if length > 50:
430
+ context += f" {key}: {value}, "
431
+ else:
432
+ if promptTemplate:
433
+ populatedPrompt = getInputPopulatedPrompt(promptTemplate, {key: value})
434
+ query += f"{populatedPrompt} "
435
+ else:
436
+ query += f" {key}: {value}, "
437
+
438
+ if not context.strip():
439
+ for key, value in inputDict.items():
440
+ context += f" {key}: {value}, "
441
+
442
+ templateData["processData"]["rowData"]["context"]["value"] = context.strip()
443
+ templateData["processData"]["rowData"]["query"]["value"] = query.strip()
444
+
445
+ if promptTemplate and not query.strip():
446
+ templateData["processData"]["rowData"]["query"]["value"] = promptTemplate
447
+
448
+ print(templateData)
449
+ currentBatch.append(templateData)
450
+
451
+ if len(currentBatch) == 10 or index == len(dataframe) - 1:
452
+ self.allBatches.append(currentBatch)
453
+ currentBatch = []
454
+
455
+ # Post all batches
456
+ total_items = sum(len(batch) for batch in self.allBatches)
457
+ print(f"Posting {len(self.allBatches)} batches ({total_items} items total)")
458
+
459
+ for cnt, batch in enumerate(self.allBatches):
460
+ print(f"Posting batch {cnt + 1}/{len(self.allBatches)} for eval '{eval}'")
461
+ try:
462
+ self.postBatch(batch=batch, workspaceID=workspaceID)
463
+ print(f"Batch {cnt + 1} posted successfully")
464
+ except Exception as e:
465
+ print(f"Error posting batch {cnt + 1}: {str(e)}")
466
+ continue
467
+
468
+ # Small delay between batches to prevent overwhelming the server
469
+ time.sleep(1)
470
+
471
+ # updating the dict for row column mapping
472
+ self.AllProcessMapping()
473
+ # Calculate a reasonable timeout based on the data size
474
+ timeout = max(60, min(600, total_items * 10))
475
+ print(f"All batches posted. Waiting up to {timeout} seconds for results...")
476
+
477
+ # Listen for results
478
+ self.socket.listen_for_results(min_wait=10, max_wait=timeout, inactivity_timeout=30)
479
+
480
+ # Get results for this evaluation
481
+ eval_results = self.socket.get_received_data()
482
+ print(f"Received {len(eval_results)} results for evaluation '{eval}'")
483
+
484
+ # Add these results to our overall results
485
+ results = self.finalResp(eval_results)
486
+ print(f"======= Completed evaluation: {eval} =======\n")
487
+
488
+ print("All evaluations completed successfully")
489
+
490
+ except Exception as e:
491
+ print(f"Error during evaluation: {e}")
492
+ raise
493
+ finally:
494
+ # Always disconnect the socket when done
495
+ try:
496
+ self.socket.disconnect()
497
+ print("Socket disconnected")
498
+ except Exception as e:
499
+ print(f"Error disconnecting socket: {e}")
500
+
501
+ compressed_prompt , compressed_prompt_output , cost , cost_saving = costColumnMapping(results,self.processMapping)
502
+ dataframe["compressed_prompt"] = compressed_prompt
503
+ dataframe["compressed_prompt_output"] = compressed_prompt_output
504
+ dataframe["cost"] = cost
505
+ dataframe["cost_saving"] = cost_saving
506
+ return dataframe
507
+
508
+
509
+ def run_sweep(self,templates: List[str], dataset: Dict[str, List[str]], model_aliases: List[AVAILABLEMODELS], apiKey: str, evals = ["Response Correctness"]) -> pd.DataFrame:
510
+ executor = ModelExecutor(apiKey)
511
+
512
+ keys = list(dataset.keys())
513
+ value_combinations = list(itertools.product(*dataset.values()))
514
+ combinations = [dict(zip(keys, values)) for values in value_combinations]
515
+
516
+ results = []
75
517
 
518
+ # Iterate through combinations
519
+ for combo in combinations:
520
+ for template in templates:
521
+ prompt = template
522
+ for k, v in combo.items():
523
+ prompt = prompt.replace(f"{{{{{k}}}}}", v)
524
+ # Add a row for each model
525
+ for model in model_aliases:
526
+ row = {
527
+ "template": template,
528
+ "prompt": prompt,
529
+ **combo,
530
+ "model": model.value
531
+ }
76
532
 
533
+
534
+ try:
535
+ provider = getProviderFromModel(model)
536
+ response = executor.execute(provider, model.value, prompt, apiKey)
537
+ row["output"] = response
538
+ except Exception as e:
539
+ row["output"] = f"Error: {str(e)}"
540
+
541
+ results.append(row)
542
+ df=pd.DataFrame(results)
543
+ df.to_csv("sweep_results.csv", index=False)
544
+ print(str(templates[0]))
545
+ res = self.evaluate(df,evals =evals,prompt_template=str(templates[0]))
546
+ return res
547
+
548
+
549
+ class SafeDict(dict):
550
+ def __missing__(self, key):
551
+ return ""
llumo/exceptions.py CHANGED
@@ -28,3 +28,4 @@ class LlumoAPIError(Exception):
28
28
  @staticmethod
29
29
  def EvalError(detail="Some error occured while processing"):
30
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llumo
3
- Version: 0.1.0
3
+ Version: 0.1.3
4
4
  Summary: Python SDK for interacting with the Llumo ai API.
5
5
  Home-page: https://www.llumo.ai/
6
6
  Author: Llumo
@@ -8,11 +8,14 @@ Author-email: product@llumo.ai
8
8
  License: Proprietary
9
9
  Requires-Python: >=3.7
10
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
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
16
19
  Dynamic: author
17
20
  Dynamic: author-email
18
21
  Dynamic: home-page
@@ -0,0 +1,13 @@
1
+ llumo/.env,sha256=Vx5FkuywpYHXH2N8epJ7PlNOPiwx9UP9DUz4vWd0urs,373
2
+ llumo/__init__.py,sha256=8ZgAtxJNNgHorEXoxaLQ2YWrVXGgamoayyLMD1L4FbE,183
3
+ llumo/client.py,sha256=DggiOLmBG21lEpg1vqjV5SC-PfR2LuVnpsY6HMTyF9I,23086
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.3.dist-info/licenses/LICENSE,sha256=vMiqSi3KpDHq3RFxKiqdh10ZUF3PjE3nnntANU-HEu4,186
10
+ llumo-0.1.3.dist-info/METADATA,sha256=DyqkkQAIg95hiKyFdYrHj_CazvTb8ocSUZLi13cslLc,721
11
+ llumo-0.1.3.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
12
+ llumo-0.1.3.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
13
+ llumo-0.1.3.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,8 +0,0 @@
1
- llumo/__init__.py,sha256=WT0Y6g-MtJqIIMpH1AoWQfL-XraaQDtMbjApjBHRPqk,72
2
- llumo/client.py,sha256=w1rd6uT4F-iwHty8xDqXy3Q5BM_9u1jgdSZvy032LQE,2468
3
- llumo/exceptions.py,sha256=BQcLqfViMxPklGIPJnH1tfajdytvuGpd5Sidv4ta6h0,1039
4
- llumo-0.1.0.dist-info/licenses/LICENSE,sha256=vMiqSi3KpDHq3RFxKiqdh10ZUF3PjE3nnntANU-HEu4,186
5
- llumo-0.1.0.dist-info/METADATA,sha256=OcJtsjap9U3XwzxjR03V4VzITQbPkOO_JgCcSgg0qo0,593
6
- llumo-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
7
- llumo-0.1.0.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
8
- llumo-0.1.0.dist-info/RECORD,,