llumo 0.2.14b7__py3-none-any.whl → 0.2.15b2__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 +337 -140
- llumo/exceptions.py +4 -0
- llumo/execution.py +4 -5
- llumo/helpingFuntions.py +67 -22
- llumo/models.py +63 -26
- {llumo-0.2.14b7.dist-info → llumo-0.2.15b2.dist-info}/METADATA +1 -1
- llumo-0.2.15b2.dist-info/RECORD +13 -0
- llumo-0.2.14b7.dist-info/RECORD +0 -13
- {llumo-0.2.14b7.dist-info → llumo-0.2.15b2.dist-info}/WHEEL +0 -0
- {llumo-0.2.14b7.dist-info → llumo-0.2.15b2.dist-info}/licenses/LICENSE +0 -0
- {llumo-0.2.14b7.dist-info → llumo-0.2.15b2.dist-info}/top_level.txt +0 -0
llumo/client.py
CHANGED
@@ -10,7 +10,7 @@ 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 *
|
@@ -38,7 +38,7 @@ class LlumoClient:
|
|
38
38
|
|
39
39
|
def __init__(self, api_key):
|
40
40
|
self.apiKey = api_key
|
41
|
-
|
41
|
+
|
42
42
|
self.processMapping = {}
|
43
43
|
self.definationMapping = {}
|
44
44
|
|
@@ -50,6 +50,7 @@ class LlumoClient:
|
|
50
50
|
reqBody = {"analytics": [evalName]}
|
51
51
|
|
52
52
|
try:
|
53
|
+
|
53
54
|
response = requests.post(url=validateUrl, json=reqBody, headers=headers)
|
54
55
|
|
55
56
|
except requests.exceptions.RequestException as e:
|
@@ -581,7 +582,8 @@ class LlumoClient:
|
|
581
582
|
createExperiment: bool = False,
|
582
583
|
_tocheck=True,
|
583
584
|
):
|
584
|
-
|
585
|
+
self.socket = LlumoSocketClient(socketUrl)
|
586
|
+
dataframe = pd.DataFrame(data).astype(str)
|
585
587
|
workspaceID = None
|
586
588
|
email = None
|
587
589
|
socketID = self.socket.connect(timeout=250)
|
@@ -812,7 +814,7 @@ class LlumoClient:
|
|
812
814
|
else:
|
813
815
|
return dataframe
|
814
816
|
|
815
|
-
def
|
817
|
+
def promptSweep(
|
816
818
|
self,
|
817
819
|
templates: List[str],
|
818
820
|
dataset: Dict[str, List[str]],
|
@@ -821,9 +823,15 @@ class LlumoClient:
|
|
821
823
|
evals=["Response Correctness"],
|
822
824
|
toEvaluate: bool = False,
|
823
825
|
createExperiment: bool = False,
|
826
|
+
|
827
|
+
|
824
828
|
) -> pd.DataFrame:
|
825
829
|
|
826
|
-
|
830
|
+
modelStatus = validateModels(model_aliases=model_aliases)
|
831
|
+
if modelStatus["status"]== False:
|
832
|
+
raise LlumoAIError.providerError(modelStatus["message"])
|
833
|
+
|
834
|
+
self.validateApiKey()
|
827
835
|
workspaceID = self.workspaceID
|
828
836
|
email = self.email
|
829
837
|
executor = ModelExecutor(apiKey)
|
@@ -928,6 +936,7 @@ class LlumoClient:
|
|
928
936
|
evals=["Final Task Alignment"],
|
929
937
|
prompt_template="Give answer for the given query: {{query}}",
|
930
938
|
createExperiment: bool = False,
|
939
|
+
|
931
940
|
):
|
932
941
|
if model.lower() not in ["openai", "google"]:
|
933
942
|
raise ValueError("Model must be 'openai' or 'google'")
|
@@ -1002,174 +1011,362 @@ class LlumoClient:
|
|
1002
1011
|
except Exception as e:
|
1003
1012
|
raise e
|
1004
1013
|
|
1005
|
-
def
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1014
|
+
def ragSweep(
|
1015
|
+
self,
|
1016
|
+
data,
|
1017
|
+
streamName: str,
|
1018
|
+
queryColName: str = "query",
|
1019
|
+
createExperiment: bool = False,
|
1020
|
+
modelAliases=[],
|
1021
|
+
apiKey="",
|
1022
|
+
prompt_template="Give answer to the given: {{query}} using the context:{{context}}",
|
1023
|
+
evals=["Context Utilization"],
|
1024
|
+
toEvaluate=False,
|
1025
|
+
generateOutput=True
|
1011
1026
|
):
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
)
|
1026
|
-
# print(f"Connected with socket ID: {socketID}")
|
1027
|
-
rowIdMapping = {}
|
1027
|
+
# Validate required parameters
|
1028
|
+
if generateOutput:
|
1029
|
+
if not modelAliases:
|
1030
|
+
raise ValueError("Model aliases must be provided when generateOutput is True.")
|
1031
|
+
if not apiKey or not isinstance(apiKey, str) or apiKey.strip() == "":
|
1032
|
+
raise ValueError("Valid API key must be provided when generateOutput is True.")
|
1033
|
+
|
1034
|
+
modelStatus = validateModels(model_aliases=modelAliases)
|
1035
|
+
if modelStatus["status"]== False:
|
1036
|
+
if len(modelAliases) == 0:
|
1037
|
+
raise LlumoAIError.providerError("No model selected.")
|
1038
|
+
else:
|
1039
|
+
raise LlumoAIError.providerError(modelStatus["message"])
|
1028
1040
|
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1041
|
+
# Copy the original dataframe
|
1042
|
+
original_df = pd.DataFrame(data)
|
1043
|
+
working_df = original_df.copy()
|
1032
1044
|
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1045
|
+
# Connect to socket
|
1046
|
+
self.socket = LlumoSocketClient(socketUrl)
|
1047
|
+
socketID = self.socket.connect(timeout=150)
|
1048
|
+
waited_secs = 0
|
1049
|
+
while not self.socket._connection_established.is_set():
|
1050
|
+
time.sleep(0.1)
|
1051
|
+
waited_secs += 0.1
|
1052
|
+
if waited_secs >= 20:
|
1053
|
+
raise RuntimeError("Timeout waiting for server 'connection-established' event.")
|
1042
1054
|
|
1043
|
-
|
1044
|
-
if not userHits["success"]:
|
1045
|
-
raise LlumoAIError.InsufficientCredits(userHits["message"])
|
1055
|
+
self.validateApiKey()
|
1046
1056
|
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
currentBatch = []
|
1057
|
+
# Check user credits
|
1058
|
+
userHits = checkUserHits(
|
1059
|
+
self.workspaceID, self.hasSubscribed, self.trialEndDate,
|
1060
|
+
self.subscriptionEndDate, self.hitsAvailable, len(working_df)
|
1061
|
+
)
|
1062
|
+
if not userHits["success"]:
|
1063
|
+
raise LlumoAIError.InsufficientCredits(userHits["message"])
|
1055
1064
|
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
1061
|
-
columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
1065
|
+
print("====🚀Sit back while we fetch data from the stream 🚀====")
|
1066
|
+
workspaceID, email = self.workspaceID, self.email
|
1067
|
+
activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
1068
|
+
streamId = getStreamId(workspaceID, self.apiKey, streamName)
|
1062
1069
|
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
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
|
-
}
|
1070
|
+
# Prepare batches
|
1071
|
+
rowIdMapping = {}
|
1072
|
+
self.allBatches = []
|
1073
|
+
currentBatch = []
|
1081
1074
|
|
1082
|
-
|
1075
|
+
expectedResults = len(working_df)
|
1076
|
+
timeout = max(100, min(150, expectedResults * 10))
|
1083
1077
|
|
1084
|
-
|
1078
|
+
listener_thread = threading.Thread(
|
1079
|
+
target=self.socket.listenForResults,
|
1080
|
+
kwargs={
|
1081
|
+
"min_wait": 40,
|
1082
|
+
"max_wait": timeout,
|
1083
|
+
"inactivity_timeout": 10,
|
1084
|
+
"expected_results": expectedResults,
|
1085
|
+
},
|
1086
|
+
daemon=True
|
1087
|
+
)
|
1088
|
+
listener_thread.start()
|
1089
|
+
|
1090
|
+
for index, row in working_df.iterrows():
|
1091
|
+
rowID, columnID = uuid.uuid4().hex, uuid.uuid4().hex
|
1092
|
+
compoundKey = f"{rowID}-{columnID}-{columnID}"
|
1093
|
+
rowIdMapping[compoundKey] = {"index": index}
|
1094
|
+
templateData = {
|
1095
|
+
"processID": getProcessID(),
|
1096
|
+
"socketID": socketID,
|
1097
|
+
"processData": {
|
1098
|
+
"executionDependency": {"query": row[queryColName]},
|
1099
|
+
"dataStreamID": streamId,
|
1100
|
+
},
|
1101
|
+
"workspaceID": workspaceID,
|
1102
|
+
"email": email,
|
1103
|
+
"type": "DATA_STREAM",
|
1104
|
+
"playgroundID": activePlayground,
|
1105
|
+
"processType": "DATA_STREAM",
|
1106
|
+
"rowID": rowID,
|
1107
|
+
"columnID": columnID,
|
1108
|
+
"source": "SDK",
|
1109
|
+
}
|
1110
|
+
currentBatch.append(templateData)
|
1111
|
+
if len(currentBatch) == 10 or index == len(working_df) - 1:
|
1085
1112
|
self.allBatches.append(currentBatch)
|
1086
1113
|
currentBatch = []
|
1087
1114
|
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1115
|
+
for batch in tqdm(self.allBatches, desc="Processing Batches", unit="batch", colour="magenta", ncols=80):
|
1116
|
+
try:
|
1117
|
+
self.postDataStream(batch=batch, workspaceID=workspaceID)
|
1118
|
+
time.sleep(3)
|
1119
|
+
except Exception as e:
|
1120
|
+
print(f"Error posting batch: {e}")
|
1121
|
+
raise
|
1091
1122
|
|
1092
|
-
|
1093
|
-
|
1123
|
+
time.sleep(3)
|
1124
|
+
listener_thread.join()
|
1125
|
+
|
1126
|
+
rawResults = self.socket.getReceivedData()
|
1127
|
+
expectedRowIDs = set(rowIdMapping.keys())
|
1128
|
+
receivedRowIDs = {key for item in rawResults for key in item.keys()}
|
1129
|
+
missingRowIDs = list(expectedRowIDs - receivedRowIDs)
|
1130
|
+
|
1131
|
+
if missingRowIDs:
|
1132
|
+
dataFromDb = fetchData(workspaceID, activePlayground, missingRowIDs)
|
1133
|
+
rawResults.extend(dataFromDb)
|
1134
|
+
|
1135
|
+
working_df["context"] = None
|
1136
|
+
for item in rawResults:
|
1137
|
+
for compound_key, value in item.items():
|
1138
|
+
if compound_key in rowIdMapping:
|
1139
|
+
idx = rowIdMapping[compound_key]["index"]
|
1140
|
+
working_df.at[idx, "context"] = value.get("value")
|
1141
|
+
|
1142
|
+
# Output generation
|
1143
|
+
if generateOutput == True:
|
1144
|
+
working_df = self._outputForStream(working_df, modelAliases, prompt_template, apiKey)
|
1145
|
+
|
1146
|
+
# Optional evaluation
|
1147
|
+
outputEvalMapping = None
|
1148
|
+
if toEvaluate:
|
1149
|
+
for evalName in evals:
|
1150
|
+
|
1151
|
+
# Validate API and dependencies
|
1152
|
+
self.validateApiKey(evalName=evalName)
|
1153
|
+
metricDependencies = checkDependency(
|
1154
|
+
evalName, list(working_df.columns), tocheck=False
|
1155
|
+
)
|
1156
|
+
if not metricDependencies["status"]:
|
1157
|
+
raise LlumoAIError.dependencyError(metricDependencies["message"])
|
1158
|
+
|
1159
|
+
working_df, outputEvalMapping = self._evaluateForStream(working_df, evals, modelAliases, prompt_template)
|
1160
|
+
|
1161
|
+
|
1162
|
+
self.socket.disconnect()
|
1163
|
+
|
1164
|
+
# Create experiment if required
|
1165
|
+
if createExperiment:
|
1166
|
+
df = working_df.fillna("Some error occured").astype(object)
|
1167
|
+
if createPlayground(
|
1168
|
+
email, workspaceID, df,
|
1169
|
+
queryColName=queryColName,
|
1170
|
+
dataStreamName=streamId,
|
1171
|
+
promptText=prompt_template,
|
1172
|
+
definationMapping=self.definationMapping,
|
1173
|
+
evalOutputMap=outputEvalMapping
|
1174
|
+
):
|
1175
|
+
print(
|
1176
|
+
"Your data has been saved in the Llumo Experiment. Visit https://app.llumo.ai/evallm to see the results.")
|
1177
|
+
else:
|
1178
|
+
self.latestDataframe = working_df
|
1179
|
+
return working_df
|
1180
|
+
|
1181
|
+
def _outputForStream(self, df, modelAliases, prompt_template, apiKey):
|
1182
|
+
executor = ModelExecutor(apiKey)
|
1183
|
+
|
1184
|
+
for indx, row in df.iterrows():
|
1185
|
+
inputVariables = re.findall(r"{{(.*?)}}", prompt_template)
|
1186
|
+
if not all([k in df.columns for k in inputVariables]):
|
1187
|
+
raise LlumoAIError.InvalidPromptTemplate()
|
1188
|
+
|
1189
|
+
inputDict = {key: row[key] for key in inputVariables}
|
1190
|
+
for i, model in enumerate(modelAliases, 1):
|
1094
1191
|
try:
|
1095
|
-
|
1096
|
-
|
1192
|
+
|
1193
|
+
provider = getProviderFromModel(model)
|
1194
|
+
if provider == Provider.OPENAI:
|
1195
|
+
print(validateOpenaiKey(apiKey))
|
1196
|
+
elif provider == Provider.GOOGLE:
|
1197
|
+
validateGoogleKey(apiKey)
|
1198
|
+
|
1199
|
+
filled_template = getInputPopulatedPrompt(prompt_template, inputDict)
|
1200
|
+
response = executor.execute(provider, model.value, filled_template, apiKey)
|
1201
|
+
df.at[indx, f"output_{i}"] = response
|
1097
1202
|
except Exception as e:
|
1098
|
-
|
1099
|
-
|
1203
|
+
# df.at[indx, f"output_{i}"] = str(e)
|
1204
|
+
raise e
|
1100
1205
|
|
1101
|
-
|
1102
|
-
time.sleep(1)
|
1206
|
+
return df
|
1103
1207
|
|
1104
|
-
|
1105
|
-
|
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...")
|
1208
|
+
def _evaluateForStream(self, df, evals, modelAliases, prompt_template):
|
1209
|
+
dfWithEvals = df.copy()
|
1109
1210
|
|
1110
|
-
|
1111
|
-
self.socket.listenForResults(
|
1112
|
-
min_wait=20,
|
1113
|
-
max_wait=timeout,
|
1114
|
-
inactivity_timeout=30,
|
1115
|
-
expected_results=None,
|
1116
|
-
)
|
1211
|
+
outputColMapping = {}
|
1117
1212
|
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1213
|
+
for i, model in enumerate(modelAliases, 1):
|
1214
|
+
outputColName = f"output_{i}"
|
1215
|
+
try:
|
1216
|
+
|
1217
|
+
res = self.evaluateMultiple(
|
1218
|
+
dfWithEvals.to_dict("records"),
|
1219
|
+
evals=evals,
|
1220
|
+
prompt_template=prompt_template,
|
1221
|
+
outputColName=outputColName,
|
1222
|
+
_tocheck=False,
|
1223
|
+
)
|
1224
|
+
for evalMetric in evals:
|
1225
|
+
scoreCol = f"{evalMetric}"
|
1226
|
+
reasonCol = f"{evalMetric} Reason"
|
1121
1227
|
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1228
|
+
if scoreCol in res.columns:
|
1229
|
+
res = res.rename(columns={scoreCol: f"{scoreCol}_{i}"})
|
1230
|
+
if reasonCol in res.columns:
|
1231
|
+
res = res.rename(columns={reasonCol: f"{evalMetric}_{i} Reason"})
|
1125
1232
|
|
1126
|
-
|
1233
|
+
outputColMapping[f"{scoreCol}_{i}"] = outputColName
|
1127
1234
|
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1235
|
+
newCols = [col for col in res.columns if col not in dfWithEvals.columns]
|
1236
|
+
dfWithEvals = pd.concat([dfWithEvals, res[newCols]], axis=1)
|
1237
|
+
except Exception as e:
|
1238
|
+
print(f"Evaluation failed for model {model.value}: {str(e)}")
|
1239
|
+
return dfWithEvals, outputColMapping
|
1240
|
+
|
1241
|
+
def runDataStream(
|
1242
|
+
self,
|
1243
|
+
data,
|
1244
|
+
streamName: str,
|
1245
|
+
queryColName: str = "query",
|
1246
|
+
createExperiment: bool = False,
|
1247
|
+
):
|
1248
|
+
|
1249
|
+
|
1250
|
+
# Copy the original dataframe
|
1251
|
+
original_df = pd.DataFrame(data)
|
1252
|
+
working_df = original_df.copy()
|
1253
|
+
|
1254
|
+
# Connect to socket
|
1255
|
+
self.socket = LlumoSocketClient(socketUrl)
|
1256
|
+
socketID = self.socket.connect(timeout=150)
|
1257
|
+
waited_secs = 0
|
1258
|
+
while not self.socket._connection_established.is_set():
|
1259
|
+
time.sleep(0.1)
|
1260
|
+
waited_secs += 0.1
|
1261
|
+
if waited_secs >= 20:
|
1262
|
+
raise RuntimeError("Timeout waiting for server 'connection-established' event.")
|
1263
|
+
|
1264
|
+
self.validateApiKey()
|
1265
|
+
|
1266
|
+
# Check user credits
|
1267
|
+
userHits = checkUserHits(
|
1268
|
+
self.workspaceID, self.hasSubscribed, self.trialEndDate,
|
1269
|
+
self.subscriptionEndDate, self.hitsAvailable, len(working_df)
|
1270
|
+
)
|
1271
|
+
if not userHits["success"]:
|
1272
|
+
raise LlumoAIError.InsufficientCredits(userHits["message"])
|
1273
|
+
|
1274
|
+
print("====🚀Sit back while we fetch data from the stream 🚀====")
|
1275
|
+
workspaceID, email = self.workspaceID, self.email
|
1276
|
+
activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
|
1277
|
+
streamId = getStreamId(workspaceID, self.apiKey, streamName)
|
1278
|
+
|
1279
|
+
# Prepare batches
|
1280
|
+
rowIdMapping = {}
|
1281
|
+
self.allBatches = []
|
1282
|
+
currentBatch = []
|
1283
|
+
|
1284
|
+
expectedResults = len(working_df)
|
1285
|
+
timeout = max(100, min(150, expectedResults * 10))
|
1286
|
+
|
1287
|
+
listener_thread = threading.Thread(
|
1288
|
+
target=self.socket.listenForResults,
|
1289
|
+
kwargs={
|
1290
|
+
"min_wait": 40,
|
1291
|
+
"max_wait": timeout,
|
1292
|
+
"inactivity_timeout": 10,
|
1293
|
+
"expected_results": expectedResults,
|
1294
|
+
},
|
1295
|
+
daemon=True
|
1296
|
+
)
|
1297
|
+
listener_thread.start()
|
1298
|
+
|
1299
|
+
for index, row in working_df.iterrows():
|
1300
|
+
rowID, columnID = uuid.uuid4().hex, uuid.uuid4().hex
|
1301
|
+
compoundKey = f"{rowID}-{columnID}-{columnID}"
|
1302
|
+
rowIdMapping[compoundKey] = {"index": index}
|
1303
|
+
templateData = {
|
1304
|
+
"processID": getProcessID(),
|
1305
|
+
"socketID": socketID,
|
1306
|
+
"processData": {
|
1307
|
+
"executionDependency": {"query": row[queryColName]},
|
1308
|
+
"dataStreamID": streamId,
|
1309
|
+
},
|
1310
|
+
"workspaceID": workspaceID,
|
1311
|
+
"email": email,
|
1312
|
+
"type": "DATA_STREAM",
|
1313
|
+
"playgroundID": activePlayground,
|
1314
|
+
"processType": "DATA_STREAM",
|
1315
|
+
"rowID": rowID,
|
1316
|
+
"columnID": columnID,
|
1317
|
+
"source": "SDK",
|
1318
|
+
}
|
1319
|
+
currentBatch.append(templateData)
|
1320
|
+
if len(currentBatch) == 10 or index == len(working_df) - 1:
|
1321
|
+
self.allBatches.append(currentBatch)
|
1322
|
+
currentBatch = []
|
1323
|
+
|
1324
|
+
for batch in tqdm(self.allBatches, desc="Processing Batches", unit="batch", colour="magenta", ncols=80):
|
1133
1325
|
try:
|
1134
|
-
self.
|
1135
|
-
|
1326
|
+
self.postDataStream(batch=batch, workspaceID=workspaceID)
|
1327
|
+
time.sleep(3)
|
1136
1328
|
except Exception as e:
|
1137
|
-
print(f"Error
|
1329
|
+
print(f"Error posting batch: {e}")
|
1330
|
+
raise
|
1138
1331
|
|
1139
|
-
|
1140
|
-
|
1141
|
-
for item in records:
|
1142
|
-
for compound_key, value in item.items():
|
1143
|
-
# for compound_key, value in item['data'].items():
|
1332
|
+
time.sleep(3)
|
1333
|
+
listener_thread.join()
|
1144
1334
|
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
# dataframe.at[index, evalName] = value
|
1150
|
-
dataframe.at[index, streamName] = value["value"]
|
1335
|
+
rawResults = self.socket.getReceivedData()
|
1336
|
+
expectedRowIDs = set(rowIdMapping.keys())
|
1337
|
+
receivedRowIDs = {key for item in rawResults for key in item.keys()}
|
1338
|
+
missingRowIDs = list(expectedRowIDs - receivedRowIDs)
|
1151
1339
|
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1340
|
+
if missingRowIDs:
|
1341
|
+
dataFromDb = fetchData(workspaceID, activePlayground, missingRowIDs)
|
1342
|
+
rawResults.extend(dataFromDb)
|
1155
1343
|
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1344
|
+
working_df["context"] = None
|
1345
|
+
for item in rawResults:
|
1346
|
+
for compound_key, value in item.items():
|
1347
|
+
if compound_key in rowIdMapping:
|
1348
|
+
idx = rowIdMapping[compound_key]["index"]
|
1349
|
+
working_df.at[idx, "context"] = value.get("value")
|
1159
1350
|
|
1351
|
+
|
1352
|
+
|
1353
|
+
self.socket.disconnect()
|
1354
|
+
|
1355
|
+
# Create experiment if required
|
1356
|
+
if createExperiment:
|
1357
|
+
df = working_df.fillna("Some error occured").astype(object)
|
1160
1358
|
if createPlayground(
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
dataStreamName=streamId,
|
1359
|
+
email, workspaceID, df,
|
1360
|
+
queryColName=queryColName,
|
1361
|
+
dataStreamName=streamId,
|
1362
|
+
definationMapping=self.definationMapping,
|
1166
1363
|
):
|
1167
1364
|
print(
|
1168
|
-
"Your data has been saved in the Llumo Experiment. Visit https://app.llumo.ai/evallm to see the results."
|
1169
|
-
)
|
1365
|
+
"Your data has been saved in the Llumo Experiment. Visit https://app.llumo.ai/evallm to see the results.")
|
1170
1366
|
else:
|
1171
|
-
self.latestDataframe =
|
1172
|
-
return
|
1367
|
+
self.latestDataframe = working_df
|
1368
|
+
return working_df
|
1369
|
+
|
1173
1370
|
|
1174
1371
|
def createExperiment(self, dataframe):
|
1175
1372
|
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,
|
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('
|
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": "
|
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").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
|
-
"
|
10
|
-
"
|
11
|
-
"
|
12
|
-
"
|
13
|
-
"
|
14
|
-
"
|
15
|
-
|
16
|
-
"
|
17
|
-
"
|
18
|
-
"
|
19
|
-
"
|
20
|
-
"
|
21
|
-
"
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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():
|
@@ -0,0 +1,13 @@
|
|
1
|
+
llumo/__init__.py,sha256=O04b4yW1BnOvcHzxWFddAKhtdBEhBNhLdb6xgnpHH_Q,205
|
2
|
+
llumo/client.py,sha256=60RSxhk-9wzK9KgBz8dfbUd3-AaKiljxqbHI5UL8GIw,54021
|
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=BZfUIgTO0PJchppHn0wDRF1wcYSuMST5ry95HBPN5SQ,23534
|
7
|
+
llumo/models.py,sha256=aVEZsOOoQx5LeNtwSyBxqvrINq0izH3QWu_YjsMPE6o,2910
|
8
|
+
llumo/sockets.py,sha256=I2JO_eNEctRo_ikgvFVp5zDd-m0VDu04IEUhhsa1Tic,5950
|
9
|
+
llumo-0.2.15b2.dist-info/licenses/LICENSE,sha256=tF9yAcfPV9xGT3ViWmC8hPvOo8BEk4ZICbUfcEo8Dlk,182
|
10
|
+
llumo-0.2.15b2.dist-info/METADATA,sha256=vbXwSwhuxnO0CSMz4uJ45AepuwVMl7irZlHmYkqRYbY,1521
|
11
|
+
llumo-0.2.15b2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
12
|
+
llumo-0.2.15b2.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
|
13
|
+
llumo-0.2.15b2.dist-info/RECORD,,
|
llumo-0.2.14b7.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|