llumo 0.2.14b7__py3-none-any.whl → 0.2.15__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/client.py CHANGED
@@ -5,12 +5,12 @@ import time
5
5
  import re
6
6
  import json
7
7
  import uuid
8
-
8
+ import warnings
9
9
  import os
10
10
  import itertools
11
11
  import pandas as pd
12
12
  from typing import List, Dict
13
- from .models import AVAILABLEMODELS, getProviderFromModel
13
+ from .models import AVAILABLEMODELS, getProviderFromModel, Provider
14
14
  from .execution import ModelExecutor
15
15
  from .exceptions import LlumoAIError
16
16
  from .helpingFuntions import *
@@ -19,6 +19,8 @@ from .functionCalling import LlumoAgentExecutor
19
19
  import threading
20
20
  from tqdm import tqdm
21
21
 
22
+ pd.set_option('future.no_silent_downcasting', True)
23
+
22
24
  postUrl = (
23
25
  "https://red-skull-service-392377961931.us-central1.run.app/api/process-playground"
24
26
  )
@@ -38,7 +40,7 @@ class LlumoClient:
38
40
 
39
41
  def __init__(self, api_key):
40
42
  self.apiKey = api_key
41
- self.socket = LlumoSocketClient(socketUrl)
43
+
42
44
  self.processMapping = {}
43
45
  self.definationMapping = {}
44
46
 
@@ -50,6 +52,7 @@ class LlumoClient:
50
52
  reqBody = {"analytics": [evalName]}
51
53
 
52
54
  try:
55
+
53
56
  response = requests.post(url=validateUrl, json=reqBody, headers=headers)
54
57
 
55
58
  except requests.exceptions.RequestException as e:
@@ -581,7 +584,8 @@ class LlumoClient:
581
584
  createExperiment: bool = False,
582
585
  _tocheck=True,
583
586
  ):
584
- dataframe = pd.DataFrame(data)
587
+ self.socket = LlumoSocketClient(socketUrl)
588
+ dataframe = pd.DataFrame(data).astype(str)
585
589
  workspaceID = None
586
590
  email = None
587
591
  socketID = self.socket.connect(timeout=250)
@@ -774,9 +778,6 @@ class LlumoClient:
774
778
  rawResults.extend(dataFromDb)
775
779
 
776
780
 
777
-
778
-
779
-
780
781
 
781
782
  # Initialize dataframe columns for each eval
782
783
  for eval in evals:
@@ -797,7 +798,12 @@ class LlumoClient:
797
798
 
798
799
  if createExperiment:
799
800
  pd.set_option("future.no_silent_downcasting", True)
800
- df = dataframe.fillna("Some error occured").astype(object)
801
+ # df = dataframe.fillna("Some error occured").astype(object)
802
+ with warnings.catch_warnings():
803
+ warnings.simplefilter(action='ignore', category=FutureWarning)
804
+ df = dataframe.fillna("Some error occurred").astype(str)
805
+
806
+ df = dataframe.fillna("Some error occured").infer_objects(copy=False)
801
807
  if createPlayground(
802
808
  email,
803
809
  workspaceID,
@@ -812,7 +818,7 @@ class LlumoClient:
812
818
  else:
813
819
  return dataframe
814
820
 
815
- def run_sweep(
821
+ def promptSweep(
816
822
  self,
817
823
  templates: List[str],
818
824
  dataset: Dict[str, List[str]],
@@ -821,9 +827,15 @@ class LlumoClient:
821
827
  evals=["Response Correctness"],
822
828
  toEvaluate: bool = False,
823
829
  createExperiment: bool = False,
830
+
831
+
824
832
  ) -> pd.DataFrame:
825
833
 
826
- self.validateApiKey(evalName=" ")
834
+ modelStatus = validateModels(model_aliases=model_aliases)
835
+ if modelStatus["status"]== False:
836
+ raise LlumoAIError.providerError(modelStatus["message"])
837
+
838
+ self.validateApiKey()
827
839
  workspaceID = self.workspaceID
828
840
  email = self.email
829
841
  executor = ModelExecutor(apiKey)
@@ -928,6 +940,7 @@ class LlumoClient:
928
940
  evals=["Final Task Alignment"],
929
941
  prompt_template="Give answer for the given query: {{query}}",
930
942
  createExperiment: bool = False,
943
+
931
944
  ):
932
945
  if model.lower() not in ["openai", "google"]:
933
946
  raise ValueError("Model must be 'openai' or 'google'")
@@ -1002,174 +1015,367 @@ class LlumoClient:
1002
1015
  except Exception as e:
1003
1016
  raise e
1004
1017
 
1005
- def runDataStream(
1006
- self,
1007
- data,
1008
- streamName: str,
1009
- queryColName: str = "query",
1010
- createExperiment: bool = False,
1018
+ def ragSweep(
1019
+ self,
1020
+ data,
1021
+ streamName: str,
1022
+ queryColName: str = "query",
1023
+ createExperiment: bool = False,
1024
+ modelAliases=[],
1025
+ apiKey="",
1026
+ prompt_template="Give answer to the given: {{query}} using the context:{{context}}",
1027
+ evals=["Context Utilization"],
1028
+ toEvaluate=False,
1029
+ generateOutput=True
1011
1030
  ):
1012
- results = {}
1013
- dataframe = pd.DataFrame(data)
1014
- try:
1015
- socketID = self.socket.connect(timeout=150)
1016
- # Ensure full connection before proceeding
1017
- max_wait_secs = 20
1018
- waited_secs = 0
1019
- while not self.socket._connection_established.is_set():
1020
- time.sleep(0.1)
1021
- waited_secs += 0.1
1022
- if waited_secs >= max_wait_secs:
1023
- raise RuntimeError(
1024
- "Timeout waiting for server 'connection-established' event."
1025
- )
1026
- # print(f"Connected with socket ID: {socketID}")
1027
- rowIdMapping = {}
1031
+ # Validate required parameters
1032
+ if generateOutput:
1033
+ if not modelAliases:
1034
+ raise ValueError("Model aliases must be provided when generateOutput is True.")
1035
+ if not apiKey or not isinstance(apiKey, str) or apiKey.strip() == "":
1036
+ raise ValueError("Valid API key must be provided when generateOutput is True.")
1037
+
1038
+ modelStatus = validateModels(model_aliases=modelAliases)
1039
+ if modelStatus["status"]== False:
1040
+ if len(modelAliases) == 0:
1041
+ raise LlumoAIError.providerError("No model selected.")
1042
+ else:
1043
+ raise LlumoAIError.providerError(modelStatus["message"])
1028
1044
 
1029
- # print(f"Validating API key...")
1030
- self.validateApiKey()
1031
- # print(f"API key validation successful. Hits available: {self.hitsAvailable}")
1045
+ # Copy the original dataframe
1046
+ original_df = pd.DataFrame(data)
1047
+ working_df = original_df.copy()
1032
1048
 
1033
- # check for available hits and trial limit
1034
- userHits = checkUserHits(
1035
- self.workspaceID,
1036
- self.hasSubscribed,
1037
- self.trialEndDate,
1038
- self.subscriptionEndDate,
1039
- self.hitsAvailable,
1040
- len(dataframe),
1041
- )
1049
+ # Connect to socket
1050
+ self.socket = LlumoSocketClient(socketUrl)
1051
+ socketID = self.socket.connect(timeout=150)
1052
+ waited_secs = 0
1053
+ while not self.socket._connection_established.is_set():
1054
+ time.sleep(0.1)
1055
+ waited_secs += 0.1
1056
+ if waited_secs >= 20:
1057
+ raise RuntimeError("Timeout waiting for server 'connection-established' event.")
1042
1058
 
1043
- # do not proceed if subscription or trial limit has exhausted
1044
- if not userHits["success"]:
1045
- raise LlumoAIError.InsufficientCredits(userHits["message"])
1059
+ self.validateApiKey()
1046
1060
 
1047
- print("====🚀Sit back while we fetch data from the stream 🚀====")
1048
- workspaceID = self.workspaceID
1049
- email = self.email
1050
- streamId = getStreamId(workspaceID, self.apiKey, streamName)
1051
- # Prepare all batches before sending
1052
- # print("Preparing batches...")
1053
- self.allBatches = []
1054
- currentBatch = []
1061
+ # Check user credits
1062
+ userHits = checkUserHits(
1063
+ self.workspaceID, self.hasSubscribed, self.trialEndDate,
1064
+ self.subscriptionEndDate, self.hitsAvailable, len(working_df)
1065
+ )
1066
+ if not userHits["success"]:
1067
+ raise LlumoAIError.InsufficientCredits(userHits["message"])
1055
1068
 
1056
- for index, row in dataframe.iterrows():
1057
- activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace(
1058
- "-", ""
1059
- )
1060
- rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
1061
- columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
1069
+ print("====🚀Sit back while we fetch data from the stream 🚀====")
1070
+ workspaceID, email = self.workspaceID, self.email
1071
+ activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
1072
+ streamId = getStreamId(workspaceID, self.apiKey, streamName)
1062
1073
 
1063
- rowIdMapping[rowID] = index
1064
- # Use the server-provided socket ID here
1065
- templateData = {
1066
- "processID": getProcessID(),
1067
- "socketID": socketID,
1068
- "processData": {
1069
- "executionDependency": {"query": row[queryColName]},
1070
- "dataStreamID": streamId,
1071
- },
1072
- "workspaceID": workspaceID,
1073
- "email": email,
1074
- "type": "DATA_STREAM",
1075
- "playgroundID": activePlayground,
1076
- "processType": "DATA_STREAM",
1077
- "rowID": rowID,
1078
- "columnID": columnID,
1079
- "source": "SDK",
1080
- }
1074
+ # Prepare batches
1075
+ rowIdMapping = {}
1076
+ self.allBatches = []
1077
+ currentBatch = []
1081
1078
 
1082
- currentBatch.append(templateData)
1079
+ expectedResults = len(working_df)
1080
+ timeout = max(100, min(150, expectedResults * 10))
1083
1081
 
1084
- if len(currentBatch) == 10 or index == len(dataframe) - 1:
1082
+ listener_thread = threading.Thread(
1083
+ target=self.socket.listenForResults,
1084
+ kwargs={
1085
+ "min_wait": 40,
1086
+ "max_wait": timeout,
1087
+ "inactivity_timeout": 10,
1088
+ "expected_results": expectedResults,
1089
+ },
1090
+ daemon=True
1091
+ )
1092
+ listener_thread.start()
1093
+
1094
+ for index, row in working_df.iterrows():
1095
+ rowID, columnID = uuid.uuid4().hex, uuid.uuid4().hex
1096
+ compoundKey = f"{rowID}-{columnID}-{columnID}"
1097
+ rowIdMapping[compoundKey] = {"index": index}
1098
+ templateData = {
1099
+ "processID": getProcessID(),
1100
+ "socketID": socketID,
1101
+ "processData": {
1102
+ "executionDependency": {"query": row[queryColName]},
1103
+ "dataStreamID": streamId,
1104
+ },
1105
+ "workspaceID": workspaceID,
1106
+ "email": email,
1107
+ "type": "DATA_STREAM",
1108
+ "playgroundID": activePlayground,
1109
+ "processType": "DATA_STREAM",
1110
+ "rowID": rowID,
1111
+ "columnID": columnID,
1112
+ "source": "SDK",
1113
+ }
1114
+ currentBatch.append(templateData)
1115
+ if len(currentBatch) == 10 or index == len(working_df) - 1:
1085
1116
  self.allBatches.append(currentBatch)
1086
1117
  currentBatch = []
1087
1118
 
1088
- # Post all batches
1089
- total_items = sum(len(batch) for batch in self.allBatches)
1090
- # print(f"Posting {len(self.allBatches)} batches ({total_items} items total)")
1119
+ for batch in tqdm(self.allBatches, desc="Processing Batches", unit="batch", colour="magenta", ncols=80):
1120
+ try:
1121
+ self.postDataStream(batch=batch, workspaceID=workspaceID)
1122
+ time.sleep(3)
1123
+ except Exception as e:
1124
+ print(f"Error posting batch: {e}")
1125
+ raise
1091
1126
 
1092
- for cnt, batch in enumerate(self.allBatches):
1093
- # print(f"Posting batch {cnt + 1}/{len(self.allBatches)} for eval '{eval}'")
1127
+ time.sleep(3)
1128
+ listener_thread.join()
1129
+
1130
+ rawResults = self.socket.getReceivedData()
1131
+ expectedRowIDs = set(rowIdMapping.keys())
1132
+ receivedRowIDs = {key for item in rawResults for key in item.keys()}
1133
+ missingRowIDs = list(expectedRowIDs - receivedRowIDs)
1134
+
1135
+ if missingRowIDs:
1136
+ dataFromDb = fetchData(workspaceID, activePlayground, missingRowIDs)
1137
+ rawResults.extend(dataFromDb)
1138
+
1139
+ working_df["context"] = None
1140
+ for item in rawResults:
1141
+ for compound_key, value in item.items():
1142
+ if compound_key in rowIdMapping:
1143
+ idx = rowIdMapping[compound_key]["index"]
1144
+ working_df.at[idx, "context"] = value.get("value")
1145
+
1146
+ # Output generation
1147
+ if generateOutput == True:
1148
+ working_df = self._outputForStream(working_df, modelAliases, prompt_template, apiKey)
1149
+
1150
+ # Optional evaluation
1151
+ outputEvalMapping = None
1152
+ if toEvaluate:
1153
+ for evalName in evals:
1154
+ # Validate API and dependencies
1155
+ self.validateApiKey(evalName=evalName)
1156
+ metricDependencies = checkDependency(
1157
+ evalName, list(working_df.columns), tocheck=False
1158
+ )
1159
+ if not metricDependencies["status"]:
1160
+ raise LlumoAIError.dependencyError(metricDependencies["message"])
1161
+
1162
+ working_df, outputEvalMapping = self._evaluateForStream(working_df, evals, modelAliases, prompt_template,generateOutput)
1163
+
1164
+
1165
+ self.socket.disconnect()
1166
+
1167
+ # Create experiment if required
1168
+ if createExperiment:
1169
+ # df = working_df.fillna("Some error occured").astype(object)
1170
+ with warnings.catch_warnings():
1171
+ warnings.simplefilter(action='ignore', category=FutureWarning)
1172
+ df = working_df.fillna("Some error occurred").astype(str)
1173
+ if createPlayground(
1174
+ email, workspaceID, df,
1175
+ queryColName=queryColName,
1176
+ dataStreamName=streamId,
1177
+ promptText=prompt_template,
1178
+ definationMapping=self.definationMapping,
1179
+ evalOutputMap=outputEvalMapping
1180
+ ):
1181
+ print(
1182
+ "Your data has been saved in the Llumo Experiment. Visit https://app.llumo.ai/evallm to see the results.")
1183
+ else:
1184
+ self.latestDataframe = working_df
1185
+ return working_df
1186
+
1187
+ def _outputForStream(self, df, modelAliases, prompt_template, apiKey):
1188
+ executor = ModelExecutor(apiKey)
1189
+
1190
+ for indx, row in df.iterrows():
1191
+ inputVariables = re.findall(r"{{(.*?)}}", prompt_template)
1192
+ if not all([k in df.columns for k in inputVariables]):
1193
+ raise LlumoAIError.InvalidPromptTemplate()
1194
+
1195
+ inputDict = {key: row[key] for key in inputVariables}
1196
+ for i, model in enumerate(modelAliases, 1):
1094
1197
  try:
1095
- self.postDataStream(batch=batch, workspaceID=workspaceID)
1096
- # print(f"Batch {cnt + 1} posted successfully")
1198
+
1199
+ provider = getProviderFromModel(model)
1200
+ if provider == Provider.OPENAI:
1201
+ validateOpenaiKey(apiKey)
1202
+ elif provider == Provider.GOOGLE:
1203
+ validateGoogleKey(apiKey)
1204
+
1205
+ filled_template = getInputPopulatedPrompt(prompt_template, inputDict)
1206
+ response = executor.execute(provider, model.value, filled_template, apiKey)
1207
+ df.at[indx, f"output_{i}"] = response
1208
+
1097
1209
  except Exception as e:
1098
- print(f"Error posting batch {cnt + 1}: {str(e)}")
1099
- continue
1210
+ # df.at[indx, f"output_{i}"] = str(e)
1211
+ raise e
1100
1212
 
1101
- # Small delay between batches to prevent overwhelming the server
1102
- time.sleep(1)
1213
+ return df
1103
1214
 
1104
- # updating the dict for row column mapping
1105
- self.AllProcessMapping()
1106
- # Calculate a reasonable timeout based on the data size
1107
- timeout = max(60, min(600, total_items * 10))
1108
- # print(f"All batches posted. Waiting up to {timeout} seconds for results...")
1215
+ def _evaluateForStream(self, df, evals, modelAliases, prompt_template,generateOutput):
1216
+ dfWithEvals = df.copy()
1109
1217
 
1110
- # Listen for results
1111
- self.socket.listenForResults(
1112
- min_wait=20,
1113
- max_wait=timeout,
1114
- inactivity_timeout=30,
1115
- expected_results=None,
1116
- )
1218
+ outputColMapping = {}
1219
+ for i, model in enumerate(modelAliases, 1):
1220
+ if generateOutput:
1221
+ outputColName = f"output_{i}"
1222
+ else:
1223
+ outputColName = "output"
1224
+ try:
1117
1225
 
1118
- # Get results for this evaluation
1119
- eval_results = self.socket.getReceivedData()
1120
- # print(f"Received {len(eval_results)} results for evaluation '{eval}'")
1226
+ res = self.evaluateMultiple(
1227
+ dfWithEvals.to_dict("records"),
1228
+ evals=evals,
1229
+ prompt_template=prompt_template,
1230
+ outputColName=outputColName,
1231
+ _tocheck=False,
1232
+ )
1233
+ for evalMetric in evals:
1234
+ scoreCol = f"{evalMetric}"
1235
+ reasonCol = f"{evalMetric} Reason"
1121
1236
 
1122
- # Add these results to our overall results
1123
- results["Data Stream"] = self.finalResp(eval_results)
1124
- print(f"=======You are all set! continue your expectations 🚀======\n")
1237
+ if scoreCol in res.columns:
1238
+ res = res.rename(columns={scoreCol: f"{scoreCol}_{i}"})
1239
+ if reasonCol in res.columns:
1240
+ res = res.rename(columns={reasonCol: f"{evalMetric}_{i} Reason"})
1125
1241
 
1126
- # print("All evaluations completed successfully")
1242
+ outputColMapping[f"{scoreCol}_{i}"] = outputColName
1127
1243
 
1128
- except Exception as e:
1129
- print(f"Error during evaluation: {e}")
1130
- raise
1131
- finally:
1132
- # Always disconnect the socket when done
1244
+ newCols = [col for col in res.columns if col not in dfWithEvals.columns]
1245
+ dfWithEvals = pd.concat([dfWithEvals, res[newCols]], axis=1)
1246
+ except Exception as e:
1247
+ print(f"Evaluation failed for model {model.value}: {str(e)}")
1248
+ return dfWithEvals, outputColMapping
1249
+
1250
+ def runDataStream(
1251
+ self,
1252
+ data,
1253
+ streamName: str,
1254
+ queryColName: str = "query",
1255
+ createExperiment: bool = False,
1256
+ ):
1257
+
1258
+
1259
+ # Copy the original dataframe
1260
+ original_df = pd.DataFrame(data)
1261
+ working_df = original_df.copy()
1262
+
1263
+ # Connect to socket
1264
+ self.socket = LlumoSocketClient(socketUrl)
1265
+ socketID = self.socket.connect(timeout=150)
1266
+ waited_secs = 0
1267
+ while not self.socket._connection_established.is_set():
1268
+ time.sleep(0.1)
1269
+ waited_secs += 0.1
1270
+ if waited_secs >= 20:
1271
+ raise RuntimeError("Timeout waiting for server 'connection-established' event.")
1272
+
1273
+ self.validateApiKey()
1274
+
1275
+ # Check user credits
1276
+ userHits = checkUserHits(
1277
+ self.workspaceID, self.hasSubscribed, self.trialEndDate,
1278
+ self.subscriptionEndDate, self.hitsAvailable, len(working_df)
1279
+ )
1280
+ if not userHits["success"]:
1281
+ raise LlumoAIError.InsufficientCredits(userHits["message"])
1282
+
1283
+ print("====🚀Sit back while we fetch data from the stream 🚀====")
1284
+ workspaceID, email = self.workspaceID, self.email
1285
+ activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
1286
+ streamId = getStreamId(workspaceID, self.apiKey, streamName)
1287
+
1288
+ # Prepare batches
1289
+ rowIdMapping = {}
1290
+ self.allBatches = []
1291
+ currentBatch = []
1292
+
1293
+ expectedResults = len(working_df)
1294
+ timeout = max(100, min(150, expectedResults * 10))
1295
+
1296
+ listener_thread = threading.Thread(
1297
+ target=self.socket.listenForResults,
1298
+ kwargs={
1299
+ "min_wait": 40,
1300
+ "max_wait": timeout,
1301
+ "inactivity_timeout": 10,
1302
+ "expected_results": expectedResults,
1303
+ },
1304
+ daemon=True
1305
+ )
1306
+ listener_thread.start()
1307
+
1308
+ for index, row in working_df.iterrows():
1309
+ rowID, columnID = uuid.uuid4().hex, uuid.uuid4().hex
1310
+ compoundKey = f"{rowID}-{columnID}-{columnID}"
1311
+ rowIdMapping[compoundKey] = {"index": index}
1312
+ templateData = {
1313
+ "processID": getProcessID(),
1314
+ "socketID": socketID,
1315
+ "processData": {
1316
+ "executionDependency": {"query": row[queryColName]},
1317
+ "dataStreamID": streamId,
1318
+ },
1319
+ "workspaceID": workspaceID,
1320
+ "email": email,
1321
+ "type": "DATA_STREAM",
1322
+ "playgroundID": activePlayground,
1323
+ "processType": "DATA_STREAM",
1324
+ "rowID": rowID,
1325
+ "columnID": columnID,
1326
+ "source": "SDK",
1327
+ }
1328
+ currentBatch.append(templateData)
1329
+ if len(currentBatch) == 10 or index == len(working_df) - 1:
1330
+ self.allBatches.append(currentBatch)
1331
+ currentBatch = []
1332
+
1333
+ for batch in tqdm(self.allBatches, desc="Processing Batches", unit="batch", colour="magenta", ncols=80):
1133
1334
  try:
1134
- self.socket.disconnect()
1135
- # print("Socket disconnected")
1335
+ self.postDataStream(batch=batch, workspaceID=workspaceID)
1336
+ time.sleep(3)
1136
1337
  except Exception as e:
1137
- print(f"Error disconnecting socket: {e}")
1338
+ print(f"Error posting batch: {e}")
1339
+ raise
1138
1340
 
1139
- for streamName, records in results.items():
1140
- dataframe[streamName] = None
1141
- for item in records:
1142
- for compound_key, value in item.items():
1143
- # for compound_key, value in item['data'].items():
1341
+ time.sleep(3)
1342
+ listener_thread.join()
1144
1343
 
1145
- rowID = compound_key.split("-")[0]
1146
- # looking for the index of each rowID , in the original dataframe
1147
- if rowID in rowIdMapping:
1148
- index = rowIdMapping[rowID]
1149
- # dataframe.at[index, evalName] = value
1150
- dataframe.at[index, streamName] = value["value"]
1344
+ rawResults = self.socket.getReceivedData()
1345
+ expectedRowIDs = set(rowIdMapping.keys())
1346
+ receivedRowIDs = {key for item in rawResults for key in item.keys()}
1347
+ missingRowIDs = list(expectedRowIDs - receivedRowIDs)
1151
1348
 
1152
- else:
1153
- pass
1154
- # print(f"⚠️ Warning: Could not find rowID {rowID} in mapping")
1349
+ if missingRowIDs:
1350
+ dataFromDb = fetchData(workspaceID, activePlayground, missingRowIDs)
1351
+ rawResults.extend(dataFromDb)
1155
1352
 
1156
- if createExperiment:
1157
- pd.set_option("future.no_silent_downcasting", True)
1158
- df = dataframe.fillna("Some error occured").astype(object)
1353
+ working_df["context"] = None
1354
+ for item in rawResults:
1355
+ for compound_key, value in item.items():
1356
+ if compound_key in rowIdMapping:
1357
+ idx = rowIdMapping[compound_key]["index"]
1358
+ working_df.at[idx, "context"] = value.get("value")
1159
1359
 
1360
+
1361
+
1362
+ self.socket.disconnect()
1363
+
1364
+ # Create experiment if required
1365
+ if createExperiment:
1366
+ df = working_df.fillna("Some error occured").astype(object)
1160
1367
  if createPlayground(
1161
- email,
1162
- workspaceID,
1163
- df,
1164
- queryColName=queryColName,
1165
- dataStreamName=streamId,
1368
+ email, workspaceID, df,
1369
+ queryColName=queryColName,
1370
+ dataStreamName=streamId,
1371
+ definationMapping=self.definationMapping,
1166
1372
  ):
1167
1373
  print(
1168
- "Your data has been saved in the Llumo Experiment. Visit https://app.llumo.ai/evallm to see the results."
1169
- )
1374
+ "Your data has been saved in the Llumo Experiment. Visit https://app.llumo.ai/evallm to see the results.")
1170
1375
  else:
1171
- self.latestDataframe = dataframe
1172
- return dataframe
1376
+ self.latestDataframe = working_df
1377
+ return working_df
1378
+
1173
1379
 
1174
1380
  def createExperiment(self, dataframe):
1175
1381
  try:
llumo/exceptions.py CHANGED
@@ -50,6 +50,10 @@ class LlumoAIError(Exception):
50
50
  def dependencyError(details):
51
51
  return LlumoAIError(details)
52
52
 
53
+ @staticmethod
54
+ def providerError(details):
55
+ return LlumoAIError(details)
56
+
53
57
  # @staticmethod
54
58
  # def dateNotFound():
55
59
  # return LlumoAIError("Trial end date or subscription end date not found for the given user.")
llumo/execution.py CHANGED
@@ -25,15 +25,14 @@ class ModelExecutor:
25
25
  return response.choices[0].message.content
26
26
 
27
27
  def _executeGoogle(self, modelName: str, prompt: str,api_key) -> str:
28
-
28
+
29
29
  # Configure GenAI with API Key
30
30
  genai.configure(api_key=api_key)
31
-
31
+
32
32
  # Select Generative Model
33
33
  model = genai.GenerativeModel("gemini-2.0-flash-lite")
34
34
  # Generate Response
35
35
  response = model.generate_content(prompt)
36
36
  return response.text
37
-
38
-
39
-
37
+
38
+
llumo/helpingFuntions.py CHANGED
@@ -8,7 +8,11 @@ import json
8
8
  import base64
9
9
  import os
10
10
  import re
11
+ import openai
12
+ import google.generativeai as genai
11
13
 
14
+
15
+ from .models import _MODEL_METADATA, AVAILABLEMODELS
12
16
  subscriptionUrl = "https://app.llumo.ai/api/workspace/record-extra-usage"
13
17
  getStreamdataUrl = "https://app.llumo.ai/api/data-stream/all"
14
18
  createPlayUrl = "https://app.llumo.ai/api/New-Eval-API/create-new-eval-playground"
@@ -212,7 +216,8 @@ def deleteColumnListInPlayground(workspaceID: str, playgroundID: str):
212
216
  print("❌ Error:", response.status_code, response.text)
213
217
  return None
214
218
 
215
- def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColName=None,outputColName= "output",dataStreamName=None,definationMapping=None):
219
+ def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColName=None,
220
+ outputColName= "output",dataStreamName=None,definationMapping=None,evalOutputMap = None):
216
221
  if len(dataframe) > 100:
217
222
  dataframe = dataframe.head(100)
218
223
  print("⚠️ Dataframe truncated to 100 rows for upload.")
@@ -232,11 +237,11 @@ def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColN
232
237
  # Iterate over each column in the dataframe
233
238
  for indx, col in enumerate(dataframe.columns):
234
239
  # Generate a unique column ID using uuid
235
- columnID = str(uuid.uuid4().hex[:8])
240
+ columnID = str(uuid.uuid4().hex[:8])
236
241
 
237
242
  columnIDMapping[col] = columnID
238
243
 
239
-
244
+
240
245
  if col.startswith('output') and promptText!=None:
241
246
  # For output columns, create the prompt template with promptText
242
247
  if promptText:
@@ -248,12 +253,12 @@ def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColN
248
253
 
249
254
  # Loop through each variable and check if it exists as a column name
250
255
  for var in variables:
251
- varName = var.strip()
256
+ varName = var.strip()
252
257
  if varName in columnIDMapping: # Check if the variable is a column name
253
258
  dependencies.append(columnIDMapping[varName]) # Add its columnID
254
259
 
255
260
  # Now update the template for the output column
256
-
261
+
257
262
  template={
258
263
  "provider": "OPENAI",
259
264
  "model": "GPT_4o",
@@ -275,8 +280,8 @@ def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColN
275
280
  "type": "PROMPT",
276
281
  "order": indx,
277
282
  }
278
-
279
- elif col.startswith('Data ') :
283
+
284
+ elif col.startswith('context') and dataStreamName != None :
280
285
  if queryColName and dataStreamName:
281
286
  dependencies = []
282
287
  dependencies.append(columnIDMapping[queryColName])
@@ -286,22 +291,27 @@ def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColN
286
291
  "dataStreamName": dataStreamName,
287
292
  "query": columnIDMapping[queryColName],
288
293
  "columnID": columnID, # Use the generated column ID
289
- "label": "Data stream",
294
+ "label": "context",
290
295
  "type": "DATA_STREAM",
291
296
  "order": indx}
292
297
 
293
- elif col in allEvals and promptText!=None:
294
298
 
299
+ elif any(col.startswith(eval + "_") or col == eval for eval in allEvals) and not " Reason" in col and promptText is not None:
300
+ if evalOutputMap != None:
301
+ outputColName = evalOutputMap[col]
302
+ else:
303
+ outputColName = outputColName
295
304
  dependencies = []
296
305
  variables = re.findall(r'{{(.*?)}}', promptText)
297
306
 
298
307
  # Loop through each variable and check if it exists as a column name
299
308
  for var in variables:
300
- varName = var.strip()
309
+ varName = var.strip()
301
310
  if varName in columnIDMapping: # Check if the variable is a column name
302
311
  dependencies.append(columnIDMapping[varName])
303
-
312
+
304
313
  dependencies.append(columnIDMapping[outputColName]) # Add the output column ID
314
+
305
315
  longDef = definationMapping.get(col, {}).get('definition', "")
306
316
  shortDef =definationMapping.get(col, {}).get('briefDefinition', "")
307
317
  enum = col.upper().replace(" ","_")
@@ -341,11 +351,11 @@ def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColN
341
351
  }
342
352
 
343
353
  elif col.endswith(' Reason') and promptText!=None:
344
- continue
354
+ continue
355
+
345
356
 
346
-
347
357
  else:
348
-
358
+
349
359
  template = {
350
360
  "label": col, # Label is the column name
351
361
  "type": "VARIABLE", # Default type for non-output columns
@@ -370,25 +380,27 @@ def createColumn(workspaceID, dataframe, playgroundID, promptText=None,queryColN
370
380
  row_dict = {}
371
381
 
372
382
  # For each column, we need to map the column ID to the corresponding value in the row
383
+
373
384
  for col in dataframe.columns:
374
385
  columnID = columnIDMapping[col]
375
-
376
- if col in allEvals and promptText!=None:
386
+
387
+ if any(col.startswith(eval + "_") or col == eval for eval in allEvals) and not " Reason" in col and promptText!=None:
388
+
377
389
  row_dict[columnID] = {
378
-
390
+
379
391
  "value": row[col],
380
392
  "type": "EVAL",
381
393
  "isValid": True,
382
394
  "reasoning": row[col+" Reason"],
383
395
  "edgeCase": "minorHallucinationDetailNotInContext",
384
396
  "kpi": col
385
-
386
- }
397
+
398
+ }
387
399
  elif col.endswith(' Reason') and promptText!=None:
388
400
  continue
389
401
  else:# Get the columnID from the mapping
390
402
  row_dict[columnID] = row[col]
391
-
403
+
392
404
  # row_dict[columnID] = row[col] # Directly map the column ID to the row value
393
405
  # Add the row index (if necessary)
394
406
  row_dict["pIndex"] = indx
@@ -440,11 +452,11 @@ def uploadRowsInDBPlayground(payload):
440
452
  return None
441
453
 
442
454
 
443
- def createPlayground(email, workspaceID, df, promptText=None,queryColName=None,dataStreamName=None,definationMapping=None,outputColName="output"):
455
+ def createPlayground(email, workspaceID, df, promptText=None,queryColName=None,dataStreamName=None,definationMapping=None,outputColName="output",evalOutputMap = None):
444
456
 
445
457
  playgroundId = str(createEvalPlayground(email=email, workspaceID=workspaceID))
446
458
  payload1, payload2 = createColumn(
447
- workspaceID=workspaceID, dataframe=df, playgroundID=playgroundId, promptText=promptText,queryColName=queryColName,dataStreamName=dataStreamName,definationMapping=definationMapping,outputColName=outputColName
459
+ workspaceID=workspaceID, dataframe=df, playgroundID=playgroundId, promptText=promptText,queryColName=queryColName,dataStreamName=dataStreamName,definationMapping=definationMapping,outputColName=outputColName,evalOutputMap=evalOutputMap
448
460
  )
449
461
 
450
462
  # Debugging line to check the payload2 structure
@@ -606,3 +618,36 @@ def fetchData(workspaceID, playgroundID, missingList: list):
606
618
  except Exception as e:
607
619
  print(f"An error occurred: {e}")
608
620
  return []
621
+
622
+ def validateModels(model_aliases):
623
+
624
+ selectedProviders = []
625
+ for name in model_aliases:
626
+ for alias ,(provider , modelName ) in _MODEL_METADATA.items():
627
+ if modelName == name:
628
+ selectedProviders.append(provider)
629
+
630
+ if len(set(selectedProviders)) > 1:
631
+ return {"status": False,"message":"All selected models should be of same provider."}
632
+ else:
633
+ return {"status": True,"message":"All selected models are of same provider."}
634
+
635
+
636
+
637
+ def validateOpenaiKey(api_key):
638
+ try:
639
+ client = openai.OpenAI(api_key=api_key)
640
+ _ = client.models.list() # Light call to list models
641
+ except openai.AuthenticationError:
642
+ raise ValueError("❌ Invalid OpenAI API key.")
643
+ except Exception as e:
644
+ raise RuntimeError(f"⚠️ Error validating OpenAI key: {e}")
645
+
646
+ def validateGoogleKey(api_key):
647
+ try:
648
+ genai.configure(api_key=api_key)
649
+ _ = genai.GenerativeModel("gemini-2.0-flash-lite").generate_content("test")
650
+ except Exception as e:
651
+ if "PERMISSION_DENIED" in str(e) or "API key not valid" in str(e):
652
+ raise ValueError("❌ Invalid Google API key.")
653
+ raise RuntimeError(f"⚠️ Error validating Gemini key: {e}")
llumo/models.py CHANGED
@@ -6,35 +6,72 @@ class Provider(str, Enum):
6
6
 
7
7
  # Maps model aliases → (provider, actual model name for API)
8
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"),
9
+ "GPT_4O": (Provider.OPENAI, "GPT_4O"),
10
+ "GPT_4_5": (Provider.OPENAI, "GPT_4_5"),
11
+ "GPT_4": (Provider.OPENAI, "GPT_4"),
12
+ "GPT_4_32K": (Provider.OPENAI, "GPT_4_32K"),
13
+ "GPT_3_5_Turbo": (Provider.OPENAI, "GPT_35T"),
14
+ "GPT_3_5_Turbo_Instruct": (Provider.OPENAI, "GPT_35T_INS"),
15
+ "GPT_3_5_Turbo_16K": (Provider.OPENAI, "GPT_35T_16K"),
16
+ "GPT_4_o_Mini": (Provider.OPENAI, "GPT_4O_MINI"),
17
+ "o4_MINI": (Provider.OPENAI, "O4_MINI"),
18
+ "o4_MINI_HIGH": (Provider.OPENAI, "O4_MINI_HIGH"),
19
+ "GPT_4_1": (Provider.OPENAI, "GPT_4_1"),
20
+ "GPT_4_1_Mini": (Provider.OPENAI, "GPT_4_1_MINI"),
21
+ "GPT_4_1_nano": (Provider.OPENAI, "GPT_4_1_NANO"),
22
+ "o3": (Provider.OPENAI, "O3"),
23
+ "o3_MINI": (Provider.OPENAI, "O3_MINI"),
24
+ "o1": (Provider.OPENAI, "O1"),
25
+ "o1_MINI": (Provider.OPENAI, "O1_MINI"),
26
+
27
+
28
+ "Gemini_2_5_Pro": (Provider.GOOGLE, "GEMINI_2_5_PRO"),
29
+ "Gemini_2_5_Flash": (Provider.GOOGLE, "GEMINI_2_5_FLASH"),
30
+ "Gemini_2_0": (Provider.GOOGLE, "GEMINI_2_0"),
31
+ "Gemini_2_0_Flash": (Provider.GOOGLE, "GEMINI_2_0_FLASH"),
32
+ "Gemini_Pro": (Provider.GOOGLE, "GEMINI_PRO"),
33
+ "Text_Bison": (Provider.GOOGLE, "TEXT_BISON"),
34
+ "Chat_Bison": (Provider.GOOGLE, "CHAT_BISON"),
35
+ "Text_Bison_32k": (Provider.GOOGLE, "TEXT_BISON_32K"),
36
+ "Text_Unicorn": (Provider.GOOGLE, "TEXT_UNICORN"),
37
+ "Google_1_5_Flash": (Provider.GOOGLE, "GOOGLE_15_FLASH"),
38
+ "Gemma_3_9B": (Provider.GOOGLE, "GEMMA_3_9B"),
39
+ "Gemma_3_27B": (Provider.GOOGLE, "GEMMA_3_27B"),
22
40
  }
23
41
 
24
42
  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"
43
+ GPT_4o= "GPT_4O",
44
+ GPT_4o_Mini= "GPT_4O_MINI",
45
+ GPT_4_5= "GPT_4_5",
46
+ GPT_4= "GPT_4",
47
+ GPT_4_32K= "GPT_4_32K",
48
+ GPT_3_5_Turbo= "GPT_35T",
49
+ GPT_3_5_Turbo_Instruct= "GPT_35T_INS",
50
+ GPT_3_5_Turbo_16K= "GPT_35T_16K",
51
+ GPT_4_o_Mini= "GPT_4O_MINI",
52
+ o4_MINI = "O4_MINI",
53
+ o4_MINI_HIGH = "O4_MINI_HIGH",
54
+ GPT_4_1 = "GPT_4_1",
55
+ GPT_4_1_Mini = "GPT_4_1_MINI",
56
+ GPT_4_1_nano = "GPT_4_1_NANO",
57
+ o3 = "O3",
58
+ o3_MINI = "O3_MINI",
59
+ o1 = "O1",
60
+ o1_MINI = "O1_MINI",
61
+
62
+ Gemini_2_5_Pro = "GEMINI_2_5_PRO",
63
+ Gemini_2_5_Flash = "GEMINI_2_5_FLASH",
64
+ Gemini_2_0 = "GEMINI_2_0",
65
+ Gemini_2_0_Flash = "GEMINI_2_0_FLASH",
66
+ Gemini_Pro = "GEMINI_PRO",
67
+ Text_Bison = "TEXT_BISON",
68
+ Chat_Bison = "CHAT_BISON",
69
+ Text_Bison_32k = "TEXT_BISON_32K",
70
+ Text_Unicorn = "TEXT_UNICORN",
71
+ Google_1_5_Flash = "GOOGLE_15_FLASH",
72
+ Gemma_3_9B = "GEMMA_3_9B",
73
+ Gemma_3_27B = "GEMMA_3_27B",
74
+
38
75
 
39
76
  def getProviderFromModel(model: AVAILABLEMODELS) -> Provider:
40
77
  for alias, (provider, apiName) in _MODEL_METADATA.items():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llumo
3
- Version: 0.2.14b7
3
+ Version: 0.2.15
4
4
  Summary: Python SDK for interacting with the Llumo ai API.
5
5
  Home-page: https://www.llumo.ai/
6
6
  Author: Llumo
@@ -0,0 +1,13 @@
1
+ llumo/__init__.py,sha256=O04b4yW1BnOvcHzxWFddAKhtdBEhBNhLdb6xgnpHH_Q,205
2
+ llumo/client.py,sha256=zh6fpKpjlYcvzrPZkPviF1hDRzfnA1K0U1gweoKfkwc,54675
3
+ llumo/exceptions.py,sha256=Vp_MnanHbnd1Yjuoi6WLrKiwwZbJL3znCox2URMmGU4,2032
4
+ llumo/execution.py,sha256=nWbJ7AvWuUPcOb6i-JzKRna_PvF-ewZTiK8skS-5n3w,1380
5
+ llumo/functionCalling.py,sha256=D5jYapu1rIvdIJNUYPYMTyhQ1H-6nkwoOLMi6eekfUE,7241
6
+ llumo/helpingFuntions.py,sha256=-9GA9X0KBUVZb3_25D8AlninWnVc9ajFp4QkR_mDePY,23545
7
+ llumo/models.py,sha256=aVEZsOOoQx5LeNtwSyBxqvrINq0izH3QWu_YjsMPE6o,2910
8
+ llumo/sockets.py,sha256=I2JO_eNEctRo_ikgvFVp5zDd-m0VDu04IEUhhsa1Tic,5950
9
+ llumo-0.2.15.dist-info/licenses/LICENSE,sha256=tF9yAcfPV9xGT3ViWmC8hPvOo8BEk4ZICbUfcEo8Dlk,182
10
+ llumo-0.2.15.dist-info/METADATA,sha256=OQApH-0Gj918OaMbyQasOtE6lAhU5__No3SK9xge-NM,1519
11
+ llumo-0.2.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ llumo-0.2.15.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
13
+ llumo-0.2.15.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- llumo/__init__.py,sha256=O04b4yW1BnOvcHzxWFddAKhtdBEhBNhLdb6xgnpHH_Q,205
2
- llumo/client.py,sha256=HpvUyucrGPbcPQMz_cTRDcEsBFpmNt8jfW1zJU4Nyss,46781
3
- llumo/exceptions.py,sha256=i3Qv4_g7XjRuho7-b7ybjw2bwSh_NhvICR6ZAgiLQX8,1944
4
- llumo/execution.py,sha256=x88wQV8eL99wNN5YtjFaAMCIfN1PdfQVlAZQb4vzgQ0,1413
5
- llumo/functionCalling.py,sha256=D5jYapu1rIvdIJNUYPYMTyhQ1H-6nkwoOLMi6eekfUE,7241
6
- llumo/helpingFuntions.py,sha256=RgWok8DoE1R-Tc0kJ9B5En6LEUEk5EvQU8iJiGPbUsw,21911
7
- llumo/models.py,sha256=YH-qAMnShmUpmKE2LQAzQdpRsaXkFSlOqMxHwU4zBUI,1560
8
- llumo/sockets.py,sha256=I2JO_eNEctRo_ikgvFVp5zDd-m0VDu04IEUhhsa1Tic,5950
9
- llumo-0.2.14b7.dist-info/licenses/LICENSE,sha256=tF9yAcfPV9xGT3ViWmC8hPvOo8BEk4ZICbUfcEo8Dlk,182
10
- llumo-0.2.14b7.dist-info/METADATA,sha256=kdeDmcNgV8uRyH7gXhhAqeb3se5U_Gqo3bA3Cf4SLlM,1521
11
- llumo-0.2.14b7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- llumo-0.2.14b7.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
13
- llumo-0.2.14b7.dist-info/RECORD,,