llumo 0.2.35__tar.gz → 0.2.37__tar.gz

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.
Files changed (26) hide show
  1. {llumo-0.2.35/llumo.egg-info → llumo-0.2.37}/PKG-INFO +1 -1
  2. {llumo-0.2.35 → llumo-0.2.37}/llumo/callback.py +13 -7
  3. {llumo-0.2.35 → llumo-0.2.37}/llumo/client.py +383 -220
  4. {llumo-0.2.35 → llumo-0.2.37}/llumo/helpingFuntions.py +28 -1
  5. {llumo-0.2.35 → llumo-0.2.37}/llumo/llumoLogger.py +15 -9
  6. {llumo-0.2.35 → llumo-0.2.37}/llumo/llumoSessionContext.py +64 -8
  7. {llumo-0.2.35 → llumo-0.2.37}/llumo/sockets.py +2 -1
  8. {llumo-0.2.35 → llumo-0.2.37/llumo.egg-info}/PKG-INFO +1 -1
  9. {llumo-0.2.35 → llumo-0.2.37}/setup.py +4 -0
  10. {llumo-0.2.35 → llumo-0.2.37}/LICENSE +0 -0
  11. {llumo-0.2.35 → llumo-0.2.37}/MANIFEST.in +0 -0
  12. {llumo-0.2.35 → llumo-0.2.37}/README.md +0 -0
  13. {llumo-0.2.35 → llumo-0.2.37}/llumo/__init__.py +0 -0
  14. {llumo-0.2.35 → llumo-0.2.37}/llumo/callbacks-0.py +0 -0
  15. {llumo-0.2.35 → llumo-0.2.37}/llumo/chains.py +0 -0
  16. {llumo-0.2.35 → llumo-0.2.37}/llumo/exceptions.py +0 -0
  17. {llumo-0.2.35 → llumo-0.2.37}/llumo/execution.py +0 -0
  18. {llumo-0.2.35 → llumo-0.2.37}/llumo/functionCalling.py +0 -0
  19. {llumo-0.2.35 → llumo-0.2.37}/llumo/google.py +0 -0
  20. {llumo-0.2.35 → llumo-0.2.37}/llumo/models.py +0 -0
  21. {llumo-0.2.35 → llumo-0.2.37}/llumo/openai.py +0 -0
  22. {llumo-0.2.35 → llumo-0.2.37}/llumo.egg-info/SOURCES.txt +0 -0
  23. {llumo-0.2.35 → llumo-0.2.37}/llumo.egg-info/dependency_links.txt +0 -0
  24. {llumo-0.2.35 → llumo-0.2.37}/llumo.egg-info/requires.txt +0 -0
  25. {llumo-0.2.35 → llumo-0.2.37}/llumo.egg-info/top_level.txt +0 -0
  26. {llumo-0.2.35 → llumo-0.2.37}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llumo
3
- Version: 0.2.35
3
+ Version: 0.2.37
4
4
  Summary: Python SDK for interacting with the Llumo ai API.
5
5
  Home-page: https://www.llumo.ai/
6
6
  Author: Llumo
@@ -4,6 +4,7 @@ from langchain_core.messages import BaseMessage
4
4
  from langchain_core.outputs import LLMResult
5
5
  from langchain_core.agents import AgentAction, AgentFinish
6
6
  import json
7
+
7
8
  from llumo.llumoLogger import LlumoLogger
8
9
  from llumo.llumoSessionContext import LlumoSessionContext
9
10
  import time
@@ -16,6 +17,7 @@ class LlumoCallbackHandler(BaseCallbackHandler):
16
17
  raise ValueError("LlumoSessionContext is required")
17
18
 
18
19
  self.sessionLogger = session
20
+ self.sessionLogger.isLangchain = True
19
21
  self.agentType = agentType
20
22
 
21
23
  # Initialize timing and state variables
@@ -93,7 +95,7 @@ class LlumoCallbackHandler(BaseCallbackHandler):
93
95
 
94
96
  self.agentStartTime = time.time()
95
97
  self.isAgentExecution = True
96
- print(f"[DEBUG] Agent execution started: {self.currentAgentName} - Reset counters for new query")
98
+ # print(f"[DEBUG] Agent execution started: {self.currentAgentName} - Reset counters for new query")
97
99
  else:
98
100
  self.isAgentExecution = False
99
101
 
@@ -168,6 +170,10 @@ class LlumoCallbackHandler(BaseCallbackHandler):
168
170
 
169
171
  def on_llm_end(self, response: Any, **kwargs: Any) -> None:
170
172
  """Called when LLM completes"""
173
+ # print("ON LLM END kwargs: ",kwargs)
174
+ # print("ON LLM END response: ",response)
175
+
176
+
171
177
  duration_ms = int((time.time() - self.llmStartTime) * 1000) if self.llmStartTime else 0
172
178
 
173
179
  # Initialize default values
@@ -347,8 +353,8 @@ class LlumoCallbackHandler(BaseCallbackHandler):
347
353
 
348
354
  def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> None:
349
355
  """Called when a tool starts executing"""
350
- # print("ON TOOL START: ",serialized)
351
- # print("ON TOOL START: ",kwargs)
356
+ # print("ON TOOL START serialized: ",serialized)
357
+ # print("ON TOOL START kwargs: ",kwargs)
352
358
 
353
359
  self.toolStartTime = time.time()
354
360
  self.stepTime = time.time()
@@ -376,7 +382,7 @@ class LlumoCallbackHandler(BaseCallbackHandler):
376
382
  if self.currentToolName not in self.toolsUsed:
377
383
  self.toolsUsed.append(self.currentToolName)
378
384
 
379
- print(f"[DEBUG] Tool started: {self.currentToolName} with input: {input_str}")
385
+ # print(f"[DEBUG] Tool started: {self.currentToolName} with input: {input_str}")
380
386
 
381
387
  def on_tool_end(self, output: Any, **kwargs: Any) -> None:
382
388
  """Called when a tool completes execution"""
@@ -409,7 +415,7 @@ class LlumoCallbackHandler(BaseCallbackHandler):
409
415
  status="SUCCESS",
410
416
  # message="",
411
417
  )
412
- print(f"[DEBUG] Tool completed: {self.currentToolName} -> {output_str}")
418
+ # print(f"[DEBUG] Tool completed: {self.currentToolName} -> {output_str}")
413
419
 
414
420
  except Exception as e:
415
421
  print(f"[ERROR] Failed to log tool end: {e}")
@@ -500,7 +506,7 @@ class LlumoCallbackHandler(BaseCallbackHandler):
500
506
  toolName=self.currentToolName or "unknown",
501
507
  description=self.currentToolDescription,
502
508
  input=self.currentToolInput or {"input": ""},
503
- output="",
509
+ output=f'{error}' if error else "",
504
510
  latencyMs=0,
505
511
  status="FAILURE",
506
512
  # message=str(error),
@@ -557,7 +563,7 @@ class LlumoCallbackHandler(BaseCallbackHandler):
557
563
  """Called when arbitrary text is logged"""
558
564
  # Only log significant text events during agent execution
559
565
  if self.isAgentExecution and text.strip():
560
- print(f"[DEBUG] Additional text: {text}")
566
+ # print(f"[DEBUG] Additional text: {text}")
561
567
 
562
568
  # Check if this text contains important ReAct information like "Observation:"
563
569
  if any(keyword in text.lower() for keyword in ['observation:']):
@@ -1,12 +1,14 @@
1
1
  import requests
2
2
 
3
-
3
+ import math
4
+ import random
4
5
  import time
5
6
  import re
6
7
  import json
7
8
  import uuid
8
9
  import warnings
9
10
  import os
11
+
10
12
  import itertools
11
13
  import pandas as pd
12
14
  from typing import List, Dict
@@ -19,6 +21,7 @@ from .functionCalling import LlumoAgentExecutor
19
21
  from .chains import LlumoDataFrameResults, LlumoDictResults
20
22
  import threading
21
23
  from tqdm import tqdm
24
+ from datetime import datetime, timezone
22
25
 
23
26
  pd.set_option("future.no_silent_downcasting", True)
24
27
 
@@ -773,99 +776,30 @@ class LlumoClient:
773
776
 
774
777
  return dataframe
775
778
 
776
- def evaluateMultiple(
777
- self,
778
- data,
779
- evals: list = [],
780
- # prompt_template="Give answer to the given query: {{query}} using the given context: {{context}}.",
781
- prompt_template="",
782
- getDataFrame: bool = False,
783
- _tocheck=True,
779
+ def debugLogs(
780
+ self,
781
+ data,
782
+ prompt_template="",
783
+
784
784
  ):
785
- # if hasattr(self, "startLlumoRun"):
786
- # self.startLlumoRun(runName="evaluateMultiple")
787
785
  if isinstance(data, dict):
788
786
  data = [data]
789
787
  elif not isinstance(data, list):
790
788
  raise ValueError("Data should be a dict or a list of dicts.")
791
789
 
792
- self.socket = LlumoSocketClient(socketUrl)
793
790
  dataframe = pd.DataFrame(data).astype(str)
794
791
  workspaceID = None
795
792
  email = None
796
- try:
797
- socketID = self.socket.connect(timeout=250)
798
- print("Socket connected")
799
- # print("Socket connected with ID:", socketID)
800
- except Exception as e:
801
- socketID = "DummySocketID"
802
- print(f"Socket connection failed, using dummy ID. Error: {str(e)}")
803
793
 
804
- self.evalData = []
805
- self.evals = evals
806
- self.allBatches = []
807
- rowIdMapping = {} # (rowID-columnID-columnID -> (index, evalName))
808
794
 
809
- # Wait for socket connection
810
- # max_wait_secs = 20
811
- # waited_secs = 0
812
- # while not self.socket._connection_established.is_set():
813
- # time.sleep(0.1)
814
- # waited_secs += 0.1
815
- # if waited_secs >= max_wait_secs:
816
- # raise RuntimeError("Timeout waiting for server connection")
817
-
818
- # Start listener thread
819
- # expectedResults = len(dataframe) * len(evals)
820
- expectedResults = len(dataframe)
821
- # print("expected result" ,expectedResults)
822
- timeout = max(100, min(250, expectedResults * 60))
823
- listener_thread = threading.Thread(
824
- target=self.socket.listenForResults,
825
- kwargs={
826
- "min_wait": 20,
827
- "max_wait": timeout,
828
- "inactivity_timeout": timeout,
829
- "expected_results": expectedResults,
830
- },
831
- daemon=True,
832
- )
833
- listener_thread.start()
834
795
  # commenting validate api key as we don't need it logger does it for us. uncommented but we need different
835
796
  # api for this which don't spend time on eval defintiion fetches and just bring hits
836
797
  self.validateApiKey()
837
798
  activePlayground = self.playgroundID
838
- # print(f"\n======= Running evaluation for: {evalName} =======")
839
799
 
840
- # Validate API and dependencies
841
- # self.validateApiKey(evalName=evals[0])
842
-
843
- # why we need custom analytics here? there is no such usage below
844
- # customAnalytics = getCustomAnalytics(self.workspaceID)
845
-
846
- # metricDependencies = checkDependency(
847
- # evalName,
848
- # list(dataframe.columns),
849
- # tocheck=_tocheck,
850
- # customevals=customAnalytics,
851
- # )
852
- # if not metricDependencies["status"]:
853
- # raise LlumoAIError.dependencyError(metricDependencies["message"])
854
800
 
855
- # evalDefinition = self.evalDefinition[evalName]["definition"]
856
- model = "GPT_4"
857
- provider = "OPENAI"
858
- evalType = "LLM"
859
801
  workspaceID = self.workspaceID
860
802
  email = self.email
861
- # categories = self.categories
862
- # evaluationStrictness = self.evaluationStrictness
863
- # grammarCheckOutput = self.grammarCheckOutput
864
- # insightLength = self.insightsLength
865
- # numJudges = self.numJudges
866
- # penaltyBonusInstructions = self.penaltyBonusInstructions
867
- # probableEdgeCases = self.probableEdgeCases
868
- # fieldMapping = self.fieldMapping
869
803
 
870
804
  userHits = checkUserHits(
871
805
  self.workspaceID,
@@ -876,15 +810,13 @@ class LlumoClient:
876
810
  len(dataframe),
877
811
  )
878
812
 
879
- #where does this remaining hit comes from?
813
+ # where does this remaining hit comes from?
880
814
 
881
-
882
815
  if not userHits["success"]:
883
816
  raise LlumoAIError.InsufficientCredits(userHits["message"])
884
817
 
885
- currentBatch = []
886
-
887
-
818
+ sessionID = str(uuid.uuid4().hex[:16])
819
+ allBatches = []
888
820
  for index, row in dataframe.iterrows():
889
821
  # Extract required fields
890
822
  tools = row.get("tools", "")
@@ -892,19 +824,19 @@ class LlumoClient:
892
824
  messageHistory = row.get("messageHistory", "")
893
825
  intermediateSteps = row.get("intermediateSteps", "")
894
826
  output = row.get("output", "")
895
-
827
+
896
828
  # Initialize query and context
897
829
  query = ""
898
830
  context = ""
899
-
831
+
900
832
  # Process prompt template if provided
901
833
  if prompt_template:
902
834
  # Extract template variables
903
835
  keys = re.findall(r"{{(.*?)}}", prompt_template)
904
-
836
+
905
837
  if not all([key in dataframe.columns for key in keys]):
906
838
  raise LlumoAIError.InvalidPromptTemplate()
907
-
839
+
908
840
  # Populate template and separate query/context
909
841
  populated_template = prompt_template
910
842
  for key in keys:
@@ -918,9 +850,9 @@ class LlumoClient:
918
850
  else:
919
851
  # Long value - add to context
920
852
  context += f" {key}: {value}, "
921
-
853
+
922
854
  query = populated_template.strip()
923
-
855
+
924
856
  # Add any remaining context from other fields
925
857
  if not context.strip():
926
858
  for key, value in row.items():
@@ -930,159 +862,390 @@ class LlumoClient:
930
862
  # No prompt template - use direct query and context fields
931
863
  query = row.get("query", "")
932
864
  context = row.get("context", "")
933
-
934
- # Generate unique IDs
865
+
866
+ INPUT_TOKEN_PRICE = 0.0000025
867
+ OUTPUT_TOKEN_PRICE = 0.00001
868
+ inputTokens = math.ceil(len(query)/ 4)
869
+ outputTokens = math.ceil(len(output) / 4)
870
+ totalTokens = inputTokens + outputTokens
871
+ cost = (inputTokens * INPUT_TOKEN_PRICE) + (outputTokens * OUTPUT_TOKEN_PRICE)
872
+
873
+ # compoundKey = f"{rowID}-{columnID}-{columnID}"
874
+ inputDict = {
875
+ "query": query,
876
+ "context": context.strip(),
877
+ "output": output,
878
+ "tools": tools,
879
+ "groundTruth": groundTruth,
880
+ "messageHistory": messageHistory,
881
+ "intermediateSteps": intermediateSteps,
882
+ "inputTokens": inputTokens,
883
+ "outputTokens": outputTokens,
884
+ "totalTokens": totalTokens,
885
+ "cost": round(cost, 8),
886
+ "modelsUsed": "gpt-4o",
887
+ "latency":round(random.uniform(1,1.6),2)
888
+
889
+ }
890
+ currentTime = datetime(2025, 8, 2, 10, 20, 15, tzinfo=timezone.utc)
891
+ createdAt = currentTime.strftime("%Y-%m-%dT%H:%M:%S.000Z")
935
892
  rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
936
893
  columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
937
-
938
- compoundKey = f"{rowID}-{columnID}-{columnID}"
939
- rowIdMapping[compoundKey] = {"index": index}
940
- print("rowIdMapping:", rowIdMapping)
894
+ runID = str(uuid.uuid4().hex[:16])
941
895
 
942
- # Create evaluation payload
943
- # print("socketID in before templateData: ", socketID)
944
- templateData = {
945
- "processID": getProcessID(),
946
- "socketID": socketID,
947
- "rowID": rowID,
948
- "columnID": columnID,
949
- "processType": "FULL_EVAL_RUN",
950
- "evalType": "LLM",
896
+
897
+ batch = {
898
+ "sessionID":sessionID,
951
899
  "workspaceID": workspaceID,
952
- "email": email,
953
900
  "playgroundID": activePlayground,
954
- "source": "SDK",
955
- "processData": {
956
- "executionDependency": {
957
- "query": query,
958
- "context": context.strip(),
959
- "output": output,
960
- "tools": tools,
961
- "groundTruth": groundTruth,
962
- "messageHistory": messageHistory,
963
- "intermediateSteps": intermediateSteps,
964
- },
965
- "evallist": evals,
966
- "sessionID": self.sessionID
967
- },
968
- "type": "FULL_EVAL_RUN",
901
+ "logID": runID,
902
+ "format": "UPLOAD",
903
+ "logData": inputDict,
904
+ "userAim":[],
905
+ "source": "SDK_DEBUG_UPLOAD",
906
+ "email":email,
907
+ "createdBy": email,
908
+ "createdAt":createdAt,
909
+ "columnID":rowID,
910
+ "rowID":columnID,
911
+ "latency": random.randint(1000, 1500)
969
912
  }
970
913
 
971
- # Add to batch
972
- currentBatch.append(templateData)
973
- if len(currentBatch) == 10:
974
- self.allBatches.append(currentBatch)
975
- currentBatch = []
914
+ allBatches.append(batch)
976
915
 
977
- if currentBatch:
978
- self.allBatches.append(currentBatch)
916
+ print(f"\nProcessing {len(allBatches)} records...")
917
+ for i, batch in enumerate(allBatches, start=1):
979
918
 
980
- for batch in tqdm(
981
- self.allBatches,
982
- desc="Processing Batches",
983
- unit="batch",
984
- colour="magenta",
985
- ascii=False,
986
- ):
987
919
  try:
988
- self.postBatch(batch=batch, workspaceID=workspaceID)
989
- time.sleep(2)
990
920
  # print(batch)
991
- except Exception as e:
992
- print(f"Error posting batch: {e}")
993
- raise
921
+ response = postForListOfSteps(record=batch,workspaceID=workspaceID)
994
922
 
995
- # Wait for results
996
- time.sleep(3)
997
- listener_thread.join()
923
+ # failure case inside response
924
+ if isinstance(response, dict) and str(response.get("status", "")).lower() == "false":
925
+ error_msg = response.get("exception") or response.get("error") or "Unknown error"
926
+ print(f"❌ Record {i} failed: {error_msg}")
998
927
 
999
- rawResults = self.socket.getReceivedData()
1000
-
1001
- # print(f"Total results received: {len(rawResults)}")
1002
- # print("Raw results:", rawResults)
1003
-
1004
- # print("data from db #####################",dataFromDb)
1005
- # Fix here: keep full keys, do not split keys
1006
- receivedRowIDs = {key for item in rawResults for key in item.keys()}
1007
- # print("Received Row IDs:", receivedRowIDs)
1008
- expectedRowIDs = set(rowIdMapping.keys())
1009
- missingRowIDs = expectedRowIDs - receivedRowIDs
1010
- # print("All expected keys:", expectedRowIDs)
1011
- # print("All received keys:", receivedRowIDs)
1012
- # print("Missing keys:", len(missingRowIDs))
1013
- missingRowIDs = list(missingRowIDs)
1014
-
1015
- # print("Missing Row IDs:", missingRowIDs)
1016
- # print(f"Total results before fetching missing data: {len(rawResults)}")
1017
- if len(missingRowIDs) > 0:
1018
- print('''It's taking longer than expected to get results for some rows. You can close this now.
1019
- Please wait for 15 mins while we create the flow graph for you. You can check the graph at app.llumo.ai/debugger''')
1020
- else:
1021
- print('''All results received successfully. You can check flowgraph in 5 mins at app.llumo.ai/debugger''')
1022
- # if len(missingRowIDs) > 0:
1023
- # dataFromDb = self.fetchDataForMissingKeys(workspaceID, missingRowIDs)
1024
- # # print("Fetched missing data from DB:", dataFromDb)
1025
- # rawResults.extend(dataFromDb)
1026
- # print(f"Total results after fetching missing data: {len(rawResults)}")
1027
-
1028
- self.evalData = rawResults
1029
- # print("RAW RESULTS: ", self.evalData)
1030
-
1031
- # Initialize dataframe columns for each eval
1032
- for ev_name in evals:
1033
- dataframe[ev_name] = ""
1034
- dataframe[f"{ev_name} Reason"] = ""
1035
- # dataframe[f"{ev_name} EdgeCase"] = None
1036
-
1037
- # Map results to dataframe rows
1038
- for item in rawResults:
1039
- for compound_key, value in item.items():
1040
- if compound_key not in rowIdMapping:
1041
- continue
1042
- index = rowIdMapping[compound_key]["index"]
1043
- rowID, columnID, _ = compound_key.split("-", 2)
928
+ else:
929
+ print(f"✅ Record {i} uploaded successfully.")
1044
930
 
1045
- # get the dataframe row at this index
1046
- row = dataframe.iloc[index].to_dict()
931
+ except Exception as e:
932
+ print(f"❌ Record {i} failed: {e}")
1047
933
 
1048
- if not value:
1049
- continue
1050
934
 
935
+ print("Records Uploaded successfully. You may now review the flow graph at: https://app.llumo.ai/all-debug")
1051
936
 
1052
- # ️ Handle fullEval block
1053
- fullEval = value.get("fullEval") if isinstance(value, dict) else None
1054
- if fullEval:
1055
- if "evalMetrics" in fullEval and isinstance(fullEval["evalMetrics"], list):
1056
- for evalItem in fullEval["evalMetrics"]:
1057
- evalName = evalItem.get("evalName") or evalItem.get("kpiName")
1058
- score = str(evalItem.get("score")) or evalItem.get("value")
1059
- reasoning = evalItem.get("reasoning")
1060
- # edgeCase = eval_item.get("edgeCase")
1061
-
1062
- if evalName:
1063
- dataframe.at[index, evalName] = score
1064
- dataframe.at[index, f"{evalName} Reason"] = reasoning
1065
- # dataframe.at[index, f"{evalName} EdgeCase"] = edgeCase
1066
-
1067
-
1068
- # runLog = value.get("runLog") if isinstance(value, dict) else None
1069
- # if runLog:
1070
- # try:
1071
- # self.createRunForEvalMultiple(smartLog=runLog)
1072
- # except Exception as e:
1073
- # print(f"Error posting smartlog: {e}")
1074
-
1075
937
 
1076
-
1077
- try:
1078
- self.socket.disconnect()
1079
- except Exception:
1080
- pass
938
+ # Wait for results
1081
939
 
1082
- # if hasattr(self, "endLlumoRun"):
1083
- # self.endEvalRun()
1084
- #
1085
- return dataframe
940
+ # def evaluateMultiple(
941
+ # self,
942
+ # data,
943
+ # evals: list = [],
944
+ # # prompt_template="Give answer to the given query: {{query}} using the given context: {{context}}.",
945
+ # prompt_template="",
946
+ # getDataFrame: bool = False,
947
+ # _tocheck=True,
948
+ # ):
949
+ # # if hasattr(self, "startLlumoRun"):
950
+ # # self.startLlumoRun(runName="evaluateMultiple")
951
+ # if isinstance(data, dict):
952
+ # data = [data]
953
+ # elif not isinstance(data, list):
954
+ # raise ValueError("Data should be a dict or a list of dicts.")
955
+ #
956
+ # self.socket = LlumoSocketClient(socketUrl)
957
+ # dataframe = pd.DataFrame(data).astype(str)
958
+ # workspaceID = None
959
+ # email = None
960
+ # try:
961
+ # socketID = self.socket.connect(timeout=250)
962
+ # # print("Socket connected with ID:", socketID)
963
+ # except Exception as e:
964
+ # socketID = "DummySocketID"
965
+ # # print(f"Socket connection failed, using dummy ID. Error: {str(e)}")
966
+ #
967
+ # self.evalData = []
968
+ # self.evals = evals
969
+ # self.allBatches = []
970
+ # rowIdMapping = {} # (rowID-columnID-columnID -> (index, evalName))
971
+ #
972
+ # # Wait for socket connection
973
+ # # max_wait_secs = 20
974
+ # # waited_secs = 0
975
+ # # while not self.socket._connection_established.is_set():
976
+ # # time.sleep(0.1)
977
+ # # waited_secs += 0.1
978
+ # # if waited_secs >= max_wait_secs:
979
+ # # raise RuntimeError("Timeout waiting for server connection")
980
+ #
981
+ # # Start listener thread
982
+ # # expectedResults = len(dataframe) * len(evals)
983
+ # expectedResults = len(dataframe)
984
+ # # print("expected result" ,expectedResults)
985
+ # timeout = max(100, min(250, expectedResults * 60))
986
+ # listener_thread = threading.Thread(
987
+ # target=self.socket.listenForResults,
988
+ # kwargs={
989
+ # "min_wait": 20,
990
+ # "max_wait": timeout,
991
+ # "inactivity_timeout": timeout,
992
+ # "expected_results": expectedResults,
993
+ # },
994
+ # daemon=True,
995
+ # )
996
+ # listener_thread.start()
997
+ # # commenting validate api key as we don't need it logger does it for us. uncommented but we need different
998
+ # # api for this which don't spend time on eval defintiion fetches and just bring hits
999
+ # self.validateApiKey()
1000
+ # activePlayground = self.playgroundID
1001
+ # # print(f"\n======= Running evaluation for: {evalName} =======")
1002
+ #
1003
+ # # Validate API and dependencies
1004
+ # # self.validateApiKey(evalName=evals[0])
1005
+ #
1006
+ # # why we need custom analytics here? there is no such usage below
1007
+ # # customAnalytics = getCustomAnalytics(self.workspaceID)
1008
+ #
1009
+ # # metricDependencies = checkDependency(
1010
+ # # evalName,
1011
+ # # list(dataframe.columns),
1012
+ # # tocheck=_tocheck,
1013
+ # # customevals=customAnalytics,
1014
+ # # )
1015
+ # # if not metricDependencies["status"]:
1016
+ # # raise LlumoAIError.dependencyError(metricDependencies["message"])
1017
+ #
1018
+ # # evalDefinition = self.evalDefinition[evalName]["definition"]
1019
+ # model = "GPT_4"
1020
+ # provider = "OPENAI"
1021
+ # evalType = "LLM"
1022
+ # workspaceID = self.workspaceID
1023
+ # email = self.email
1024
+ # # categories = self.categories
1025
+ # # evaluationStrictness = self.evaluationStrictness
1026
+ # # grammarCheckOutput = self.grammarCheckOutput
1027
+ # # insightLength = self.insightsLength
1028
+ # # numJudges = self.numJudges
1029
+ # # penaltyBonusInstructions = self.penaltyBonusInstructions
1030
+ # # probableEdgeCases = self.probableEdgeCases
1031
+ # # fieldMapping = self.fieldMapping
1032
+ #
1033
+ # userHits = checkUserHits(
1034
+ # self.workspaceID,
1035
+ # self.hasSubscribed,
1036
+ # self.trialEndDate,
1037
+ # self.subscriptionEndDate,
1038
+ # self.hitsAvailable,
1039
+ # len(dataframe),
1040
+ # )
1041
+ #
1042
+ # #where does this remaining hit comes from?
1043
+ #
1044
+ #
1045
+ # if not userHits["success"]:
1046
+ # raise LlumoAIError.InsufficientCredits(userHits["message"])
1047
+ #
1048
+ # currentBatch = []
1049
+ #
1050
+ #
1051
+ # for index, row in dataframe.iterrows():
1052
+ # # Extract required fields
1053
+ # tools = row.get("tools", "")
1054
+ # groundTruth = row.get("groundTruth", "")
1055
+ # messageHistory = row.get("messageHistory", "")
1056
+ # intermediateSteps = row.get("intermediateSteps", "")
1057
+ # output = row.get("output", "")
1058
+ #
1059
+ # # Initialize query and context
1060
+ # query = ""
1061
+ # context = ""
1062
+ #
1063
+ # # Process prompt template if provided
1064
+ # if prompt_template:
1065
+ # # Extract template variables
1066
+ # keys = re.findall(r"{{(.*?)}}", prompt_template)
1067
+ #
1068
+ # if not all([key in dataframe.columns for key in keys]):
1069
+ # raise LlumoAIError.InvalidPromptTemplate()
1070
+ #
1071
+ # # Populate template and separate query/context
1072
+ # populated_template = prompt_template
1073
+ # for key in keys:
1074
+ # value = row.get(key, "")
1075
+ # if isinstance(value, str):
1076
+ # length = len(value.split()) * 1.5
1077
+ # if length <= 50:
1078
+ # # Short value - include in query via template
1079
+ # temp_obj = {key: value}
1080
+ # populated_template = getInputPopulatedPrompt(populated_template, temp_obj)
1081
+ # else:
1082
+ # # Long value - add to context
1083
+ # context += f" {key}: {value}, "
1084
+ #
1085
+ # query = populated_template.strip()
1086
+ #
1087
+ # # Add any remaining context from other fields
1088
+ # if not context.strip():
1089
+ # for key, value in row.items():
1090
+ # if key not in keys and isinstance(value, str) and value.strip():
1091
+ # context += f" {key}: {value}, "
1092
+ # else:
1093
+ # # No prompt template - use direct query and context fields
1094
+ # query = row.get("query", "")
1095
+ # context = row.get("context", "")
1096
+ #
1097
+ # # Generate unique IDs
1098
+ # rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
1099
+ # columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
1100
+ #
1101
+ # compoundKey = f"{rowID}-{columnID}-{columnID}"
1102
+ # rowIdMapping[compoundKey] = {"index": index}
1103
+ # # print("rowIdMapping:", rowIdMapping)
1104
+ #
1105
+ # # Create evaluation payload
1106
+ # # print("socketID in before templateData: ", socketID)
1107
+ # templateData = {
1108
+ # "processID": getProcessID(),
1109
+ # "socketID": socketID,
1110
+ # "rowID": rowID,
1111
+ # "columnID": columnID,
1112
+ # "processType": "FULL_EVAL_RUN",
1113
+ # "evalType": "LLM",
1114
+ # "workspaceID": workspaceID,
1115
+ # "email": email,
1116
+ # "playgroundID": activePlayground,
1117
+ # "source": "SDK",
1118
+ # "processData": {
1119
+ # "executionDependency": {
1120
+ # "query": query,
1121
+ # "context": context.strip(),
1122
+ # "output": output,
1123
+ # "tools": tools,
1124
+ # "groundTruth": groundTruth,
1125
+ # "messageHistory": messageHistory,
1126
+ # "intermediateSteps": intermediateSteps,
1127
+ # },
1128
+ # "evallist": evals,
1129
+ # "sessionID": self.sessionID
1130
+ # },
1131
+ # "type": "FULL_EVAL_RUN",
1132
+ # }
1133
+ #
1134
+ # # Add to batch
1135
+ # currentBatch.append(templateData)
1136
+ # if len(currentBatch) == 10:
1137
+ # self.allBatches.append(currentBatch)
1138
+ # currentBatch = []
1139
+ #
1140
+ # if currentBatch:
1141
+ # self.allBatches.append(currentBatch)
1142
+ #
1143
+ # for batch in tqdm(
1144
+ # self.allBatches,
1145
+ # desc="Processing Batches",
1146
+ # unit="batch",
1147
+ # colour="magenta",
1148
+ # ascii=False,
1149
+ # ):
1150
+ # try:
1151
+ # self.postBatch(batch=batch, workspaceID=workspaceID)
1152
+ # time.sleep(2)
1153
+ # # print(batch)
1154
+ # except Exception as e:
1155
+ # print(f"Error posting batch: {e}")
1156
+ # raise
1157
+ #
1158
+ # # Wait for results
1159
+ # time.sleep(3)
1160
+ # listener_thread.join()
1161
+ #
1162
+ # rawResults = self.socket.getReceivedData()
1163
+ #
1164
+ # # print(f"Total results received: {len(rawResults)}")
1165
+ # # print("Raw results:", rawResults)
1166
+ #
1167
+ # # print("data from db #####################",dataFromDb)
1168
+ # # Fix here: keep full keys, do not split keys
1169
+ # receivedRowIDs = {key for item in rawResults for key in item.keys()}
1170
+ # # print("Received Row IDs:", receivedRowIDs)
1171
+ # expectedRowIDs = set(rowIdMapping.keys())
1172
+ # missingRowIDs = expectedRowIDs - receivedRowIDs
1173
+ # # print("All expected keys:", expectedRowIDs)
1174
+ # # print("All received keys:", receivedRowIDs)
1175
+ # # print("Missing keys:", len(missingRowIDs))
1176
+ # missingRowIDs = list(missingRowIDs)
1177
+ #
1178
+ # # print("Missing Row IDs:", missingRowIDs)
1179
+ # # print(f"Total results before fetching missing data: {len(rawResults)}")
1180
+ # if len(missingRowIDs) > 0:
1181
+ # print('''It's taking longer than expected to get results for some rows. You can close this now.
1182
+ # Please wait for 15 mins while we create the flow graph for you. You can check the graph at app.llumo.ai/debugging''')
1183
+ # else:
1184
+ # print('''All results received successfully. You can check flowgraph in 5 mins at app.llumo.ai/debugging''')
1185
+ # # if len(missingRowIDs) > 0:
1186
+ # # dataFromDb = self.fetchDataForMissingKeys(workspaceID, missingRowIDs)
1187
+ # # # print("Fetched missing data from DB:", dataFromDb)
1188
+ # # rawResults.extend(dataFromDb)
1189
+ # # print(f"Total results after fetching missing data: {len(rawResults)}")
1190
+ #
1191
+ # self.evalData = rawResults
1192
+ # # print("RAW RESULTS: ", self.evalData)
1193
+ #
1194
+ # # Initialize dataframe columns for each eval
1195
+ # for ev_name in evals:
1196
+ # dataframe[ev_name] = ""
1197
+ # dataframe[f"{ev_name} Reason"] = ""
1198
+ # # dataframe[f"{ev_name} EdgeCase"] = None
1199
+ #
1200
+ # # Map results to dataframe rows
1201
+ # for item in rawResults:
1202
+ # for compound_key, value in item.items():
1203
+ # if compound_key not in rowIdMapping:
1204
+ # continue
1205
+ # index = rowIdMapping[compound_key]["index"]
1206
+ # rowID, columnID, _ = compound_key.split("-", 2)
1207
+ #
1208
+ # # get the dataframe row at this index
1209
+ # row = dataframe.iloc[index].to_dict()
1210
+ #
1211
+ # if not value:
1212
+ # continue
1213
+ #
1214
+ #
1215
+ # # ️ Handle fullEval block
1216
+ # fullEval = value.get("fullEval") if isinstance(value, dict) else None
1217
+ # if fullEval:
1218
+ # if "evalMetrics" in fullEval and isinstance(fullEval["evalMetrics"], list):
1219
+ # for evalItem in fullEval["evalMetrics"]:
1220
+ # evalName = evalItem.get("evalName") or evalItem.get("kpiName")
1221
+ # score = str(evalItem.get("score")) or evalItem.get("value")
1222
+ # reasoning = evalItem.get("reasoning")
1223
+ # # edgeCase = eval_item.get("edgeCase")
1224
+ #
1225
+ # if evalName:
1226
+ # dataframe.at[index, evalName] = score
1227
+ # dataframe.at[index, f"{evalName} Reason"] = reasoning
1228
+ # # dataframe.at[index, f"{evalName} EdgeCase"] = edgeCase
1229
+ #
1230
+ #
1231
+ # # runLog = value.get("runLog") if isinstance(value, dict) else None
1232
+ # # if runLog:
1233
+ # # try:
1234
+ # # self.createRunForEvalMultiple(smartLog=runLog)
1235
+ # # except Exception as e:
1236
+ # # print(f"Error posting smartlog: {e}")
1237
+ #
1238
+ #
1239
+ #
1240
+ # try:
1241
+ # self.socket.disconnect()
1242
+ # except Exception:
1243
+ # pass
1244
+ #
1245
+ # # if hasattr(self, "endLlumoRun"):
1246
+ # # self.endEvalRun()
1247
+ # #
1248
+ # return dataframe
1086
1249
 
1087
1250
  def promptSweep(
1088
1251
  self,
@@ -11,6 +11,7 @@ import re
11
11
  import openai
12
12
  import google.generativeai as genai
13
13
  from collections import defaultdict
14
+ import requests
14
15
 
15
16
 
16
17
  from .models import _MODEL_METADATA, AVAILABLEMODELS
@@ -735,4 +736,30 @@ def getCustomAnalytics(workspaceID):
735
736
  return metricDependencies
736
737
 
737
738
  except Exception as e:
738
- return {}
739
+ return {}
740
+
741
+
742
+
743
+ def postForListOfSteps(record: {},workspaceID):
744
+ url = "https://backend-api.llumo.ai/api/v1/get-debug-log-for-upload"
745
+ payload = record
746
+ workspaceID = workspaceID
747
+
748
+ # Encode to Base64
749
+ workspaceIDEncoded = base64.b64encode(workspaceID.encode()).decode()
750
+
751
+ headers = {
752
+ "Authorization": f"Bearer {workspaceIDEncoded}",
753
+ "Content-Type": "application/json",
754
+ }
755
+
756
+ authorization = {}
757
+ # print("[PAYLOAD]: ",payload)
758
+ try:
759
+ response = requests.post(url=url, json=payload,headers = headers)
760
+ # print("[RESPONSE]: ",response.json())
761
+ # print()
762
+ return {"status":"True","data":response.json()}
763
+
764
+ except Exception as e:
765
+ return {"status":"False","exception": str(e)}
@@ -23,6 +23,12 @@ class LlumoLogger:
23
23
  timeout=10,
24
24
  )
25
25
 
26
+ if response.status_code == 401:
27
+ # Wrong API key
28
+ print("❌ SDK integration failed! ")
29
+ raise Exception("Your Llumo API key is Invalid. Try again.")
30
+
31
+
26
32
  response.raise_for_status()
27
33
  res_json = response.json()
28
34
 
@@ -33,19 +39,19 @@ class LlumoLogger:
33
39
  self.playgroundID = inner_data.get("playgroundID")
34
40
  self.userEmailID = inner_data.get("createdBy")
35
41
 
36
- if not self.workspaceID or not self.playgroundID:
37
- raise RuntimeError(
38
- f"Invalid response: workspaceID or playgroundID missing. Full response: {res_json}"
39
- )
40
-
42
+ # if not self.workspaceID or not self.playgroundID:
43
+ # raise RuntimeError(
44
+ # f"Invalid response: workspaceID or playgroundID missing. Full response: {res_json}"
45
+ # )
46
+ print("✅ SDK integration successful! ")
41
47
  except requests.exceptions.RequestException as req_err:
42
48
  raise RuntimeError(
43
49
  f"Network or HTTP error during authentication: {req_err}"
44
50
  )
45
- except ValueError as json_err:
46
- raise RuntimeError(f"Invalid JSON in authentication response: {json_err}")
47
- except Exception as e:
48
- raise RuntimeError(f"Authentication failed: {e}")
51
+ # except ValueError as json_err:
52
+ # raise RuntimeError(f"Invalid JSON in authentication response: {json_err}")
53
+ # except Exception as e:
54
+ # raise RuntimeError(f"Authentication failed: {e}")
49
55
 
50
56
  def getWorkspaceID(self):
51
57
  return self.workspaceID
@@ -4,6 +4,10 @@ from typing import Optional, List, Dict, Any
4
4
  from datetime import datetime, timezone
5
5
  import requests
6
6
  from .client import LlumoClient
7
+ import math
8
+ import base64
9
+
10
+ import random
7
11
 
8
12
  _ctxLogger = contextvars.ContextVar("ctxLogger")
9
13
  _ctxSessionID = contextvars.ContextVar("ctxSessionID")
@@ -31,6 +35,7 @@ class LlumoSessionContext(LlumoClient):
31
35
  self.threadLogger = None
32
36
  self.threadSessionID = None
33
37
  self.threadLlumoRun = None
38
+ self.isLangchain = False
34
39
 
35
40
  def start(self):
36
41
  self.threadLogger = _ctxLogger.set(self.logger)
@@ -68,25 +73,37 @@ class LlumoSessionContext(LlumoClient):
68
73
 
69
74
  currentTime = datetime(2025, 8, 2, 10, 20, 15, tzinfo=timezone.utc)
70
75
  createdAt = currentTime.strftime("%Y-%m-%dT%H:%M:%S.000Z")
76
+
77
+
78
+
71
79
  llumoRun = {
72
80
  "logID": LlumoRunID,
73
81
  "runName": runName,
74
82
  "sessionID": self.sessionID,
75
83
  "playgroundID": self.logger.getPlaygroundID(),
76
84
  "workspaceID": self.logger.getWorkspaceID(),
77
- "source": "SDK",
85
+ "source": "SDK_LANGCHAIN" if self.isLangchain else "SDK_OTHERS",
78
86
  "rowID": rowID,
79
87
  "columnID": columnID,
80
88
  "email": self.logger.getUserEmailID(),
81
89
  "createdAt": createdAt,
82
90
  "createdBy": self.logger.getUserEmailID(),
83
- "status": "SUCCESS",
91
+ "status": "",
84
92
  "flow": [],
85
- "latency": 4200,
86
93
  "feedback": "",
87
94
  "dump": "",
88
95
  "steps": [],
96
+ "format": "listofsteps",
97
+ "logData":{
98
+ "inputTokens": "",
99
+ "outputTokens":"",
100
+ "totalTokens": "",
101
+ "cost": "",
102
+ "modelsUsed": "gpt-4o",
103
+
104
+ }
89
105
  }
106
+
90
107
  self.threadLlumoRun = _ctxLlumoRun.set(llumoRun)
91
108
 
92
109
  def endLlumoRun(self):
@@ -104,20 +121,59 @@ class LlumoSessionContext(LlumoClient):
104
121
  ]
105
122
  run["steps"] = clean_steps
106
123
 
124
+ llm_step = False
125
+ inputTokens = 0
126
+ outputTokens = 0
127
+ for item in run["steps"]:
128
+ if item.get("stepType") == "LLM":
129
+ llm_step = True
130
+ outputTokens = len(item["metadata"].get("output", 0)) / 4
131
+
132
+
133
+ if item.get("stepType") == "QUERY":
134
+ inputTokens = len(item["metadata"].get("query", 0)) / 4
135
+
136
+ # 2. If no LLM step, set zeros and continue
137
+ if llm_step == False:
138
+ run["logData"]["inputTokens"] = 0
139
+ run["logData"]["outputTokens"] = 0
140
+ run["logData"]["totalTokens"] = 0
141
+ run["logData"]["cost"] = 0
142
+ run["logData"]["modelsUsed"] = "gpt-4o"
143
+
144
+ INPUT_TOKEN_PRICE = 0.0000025
145
+ OUTPUT_TOKEN_PRICE = 0.00001
146
+ cost = (inputTokens * INPUT_TOKEN_PRICE) + (outputTokens * OUTPUT_TOKEN_PRICE)
147
+
148
+ run["logData"]["inputTokens"] = math.ceil(inputTokens)
149
+ run["logData"]["outputTokens"] = math.ceil(outputTokens)
150
+ run["logData"]["totalTokens"] = math.ceil(inputTokens + outputTokens)
151
+ run["logData"]["cost"] = round(cost, 8)
152
+ # run["latency"] = round(random.uniform(1,1.6),2)
107
153
  # print(run["runName"]) # optional debug log
108
154
 
109
155
  # STEP 3: Send the payload
110
- url = "https://app.llumo.ai/api/create-debug-log"
156
+ # url = "https://app.llumo.ai/api/create-debug-log"
157
+ url = "https://backend-api.llumo.ai/api/v1/get-debug-log-for-New-SDK"
158
+ workspaceID = self.logger.getWorkspaceID()
159
+
160
+ # Encode to Base64
161
+ workspaceIDEncoded = base64.b64encode(workspaceID.encode()).decode()
162
+
111
163
  headers = {
112
- "Authorization": f"Bearer {self.logger.getWorkspaceID()}",
164
+ "Authorization": f"Bearer {workspaceIDEncoded}",
113
165
  "Content-Type": "application/json",
114
166
  }
115
167
 
168
+
169
+
116
170
  try:
117
- # print(run)
118
- response = requests.post(url, headers=headers, json=run, timeout=20)
171
+ # print("[PAYLOAD]: ",run)
172
+ payload = run
173
+ response = requests.post(url, headers=headers, json=payload, timeout=20)
119
174
  response.raise_for_status()
120
- # print(response.json())
175
+ # print("[PAYLOAD]: ",response.json())
176
+
121
177
  except requests.exceptions.Timeout:
122
178
  # print("Request timed out.")
123
179
  pass
@@ -110,7 +110,8 @@ class LlumoSocketClient:
110
110
  except Exception as e:
111
111
  # print(f"[DEBUG] Connection failed with error: {e}")
112
112
  self._connected = False
113
- # raise RuntimeError(f"WebSocket connection failed: {e}")
113
+ # raise RuntimeError(f"WebSocket
114
+ # connection failed: {e}")
114
115
  print("It seems your internet connection is a bit unstable. This might take a little longer than usual—thanks for your patience!")
115
116
 
116
117
  def listenForResults(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llumo
3
- Version: 0.2.35
3
+ Version: 0.2.37
4
4
  Summary: Python SDK for interacting with the Llumo ai API.
5
5
  Home-page: https://www.llumo.ai/
6
6
  Author: Llumo
@@ -39,6 +39,10 @@ def read_requirements():
39
39
  "tqdm==4.67.1",
40
40
  "google-generativeai==0.8.5",
41
41
  "websocket-client==1.8.0",
42
+ "pandas",
43
+ "python-dateutil",
44
+ "numpy",
45
+ "langchain_core",
42
46
 
43
47
  ]
44
48
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes