llumo 0.2.24__py3-none-any.whl → 0.2.25__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/callbacks-0.py ADDED
@@ -0,0 +1,258 @@
1
+ from langchain_core.callbacks.base import BaseCallbackHandler
2
+ from typing import Any, Dict, List, Optional, Union
3
+ import time
4
+ import json
5
+ from llumo.llumoSessionContext import getSessionID, getLlumoRun
6
+ from llumo.llumoSessionContext import LlumoSessionContext
7
+
8
+
9
+ class LlumoCallbackHandler(BaseCallbackHandler):
10
+ """
11
+ LangChain callback handler that integrates with Llumo logging system.
12
+ Tracks LLM calls, tool usage, agent actions, and chains.
13
+ """
14
+
15
+ def __init__(self, logger):
16
+ self.logger = logger
17
+ self.start_times = {} # Track start times for latency calculation
18
+ self.step_counters = {} # Track step counts for agents
19
+
20
+ def _get_session_context(self) -> Optional[LlumoSessionContext]:
21
+ """Get the current Llumo session context from context variables."""
22
+ try:
23
+ session_id = getSessionID()
24
+ run = getLlumoRun()
25
+ if session_id and run:
26
+ # Create a temporary context object to access logging methods
27
+ ctx = LlumoSessionContext(self.logger, session_id)
28
+ return ctx
29
+ except Exception:
30
+ pass
31
+ return None
32
+
33
+ def _safe_serialize(self, obj: Any) -> str:
34
+ """Safely serialize objects to JSON string."""
35
+ try:
36
+ return json.dumps(obj, default=str, ensure_ascii=False)
37
+ except Exception:
38
+ return str(obj)
39
+
40
+ # LLM Callbacks
41
+ def on_llm_start(
42
+ self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
43
+ ) -> None:
44
+ """Called when LLM starts generating."""
45
+ run_id = kwargs.get('run_id')
46
+ if run_id:
47
+ self.start_times[run_id] = time.time()
48
+
49
+ print("LLM started - prompts:", len(prompts))
50
+
51
+ def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
52
+ """Called when LLM generates a new token."""
53
+ # Optional: Could be used for streaming token tracking
54
+ pass
55
+
56
+ def on_llm_end(self, response: Any, **kwargs: Any) -> None:
57
+ """Called when LLM finishes generating."""
58
+ ctx = self._get_session_context()
59
+ if not ctx:
60
+ print("No Llumo session context available")
61
+ return
62
+
63
+ run_id = kwargs.get('run_id')
64
+ start_time = self.start_times.pop(run_id, time.time())
65
+ latency_ms = int((time.time() - start_time) * 1000)
66
+
67
+ # Extract LLM response details
68
+ model = getattr(response, 'model_name', 'unknown')
69
+
70
+ # Get token usage if available
71
+ token_usage = getattr(response, 'llm_output', {}).get('token_usage', {})
72
+ input_tokens = token_usage.get('prompt_tokens', 0)
73
+ output_tokens = token_usage.get('completion_tokens', 0)
74
+
75
+ # Get the generated text
76
+ if hasattr(response, 'generations') and response.generations:
77
+ output_text = response.generations[0][0].text if response.generations[0] else ""
78
+ else:
79
+ output_text = str(response)
80
+
81
+ # Get the original prompt
82
+ prompts = kwargs.get('prompts', [''])
83
+ query = prompts[0] if prompts else ""
84
+
85
+ try:
86
+ ctx.logLlmStep(
87
+ stepName=f"LLM Call - {model}",
88
+ model=model,
89
+ provider="langchain",
90
+ inputTokens=input_tokens,
91
+ outputTokens=output_tokens,
92
+ temperature=kwargs.get('temperature', 0.7),
93
+ promptTruncated=False,
94
+ latencyMs=latency_ms,
95
+ query=query,
96
+ output=output_text,
97
+ status="SUCCESS",
98
+ message=""
99
+ )
100
+ print(f"Logged LLM step: {model} ({latency_ms}ms)")
101
+ except Exception as e:
102
+ print(f"Failed to log LLM step: {e}")
103
+
104
+ def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
105
+ """Called when LLM encounters an error."""
106
+ ctx = self._get_session_context()
107
+ if not ctx:
108
+ return
109
+
110
+ run_id = kwargs.get('run_id')
111
+ start_time = self.start_times.pop(run_id, time.time())
112
+ latency_ms = int((time.time() - start_time) * 1000)
113
+
114
+ prompts = kwargs.get('prompts', [''])
115
+ query = prompts[0] if prompts else ""
116
+
117
+ try:
118
+ ctx.logLlmStep(
119
+ stepName="LLM Call - Error",
120
+ model="unknown",
121
+ provider="langchain",
122
+ inputTokens=0,
123
+ outputTokens=0,
124
+ temperature=0.7,
125
+ promptTruncated=False,
126
+ latencyMs=latency_ms,
127
+ query=query,
128
+ output="",
129
+ status="FAILURE",
130
+ message=str(error)
131
+ )
132
+ print(f"Logged LLM error: {error}")
133
+ except Exception as e:
134
+ print(f"Failed to log LLM error: {e}")
135
+
136
+ # Chain Callbacks
137
+ def on_chain_start(
138
+ self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
139
+ ) -> None:
140
+ pass
141
+
142
+ def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None:
143
+ """Called when a chain ends."""
144
+ print("Chain execution completed")
145
+
146
+ def on_chain_error(self, error: Exception, **kwargs: Any) -> None:
147
+ """Called when a chain encounters an error."""
148
+ print(f"Chain error: {error}")
149
+
150
+ # Tool Callbacks
151
+ def on_tool_start(
152
+ self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
153
+ ) -> None:
154
+ """Called when a tool starts."""
155
+ run_id = kwargs.get('run_id')
156
+ if run_id:
157
+ self.start_times[run_id] = time.time()
158
+
159
+ tool_name = serialized.get('name', 'Unknown Tool')
160
+ print(f"Tool started: {tool_name}")
161
+
162
+ def on_tool_end(self, output: str, **kwargs: Any) -> None:
163
+ """Called when a tool ends."""
164
+ ctx = self._get_session_context()
165
+ if not ctx:
166
+ return
167
+
168
+ run_id = kwargs.get('run_id')
169
+ start_time = self.start_times.pop(run_id, time.time())
170
+ latency_ms = int((time.time() - start_time) * 1000)
171
+
172
+ # Extract tool info from kwargs
173
+ serialized = kwargs.get('serialized', {})
174
+ tool_name = serialized.get('name', 'Unknown Tool')
175
+ input_str = kwargs.get('input_str', '')
176
+
177
+ try:
178
+ ctx.logToolStep(
179
+ stepName=f"Tool - {tool_name}",
180
+ toolName=tool_name,
181
+ input={"input": input_str},
182
+ output=output,
183
+ latencyMs=latency_ms,
184
+ status="SUCCESS",
185
+ message=""
186
+ )
187
+ print(f"Logged tool step: {tool_name} ({latency_ms}ms)")
188
+ except Exception as e:
189
+ print(f"Failed to log tool step: {e}")
190
+
191
+ def on_tool_error(self, error: Exception, **kwargs: Any) -> None:
192
+ """Called when a tool encounters an error."""
193
+ ctx = self._get_session_context()
194
+ if not ctx:
195
+ return
196
+
197
+ run_id = kwargs.get('run_id')
198
+ start_time = self.start_times.pop(run_id, time.time())
199
+ latency_ms = int((time.time() - start_time) * 1000)
200
+
201
+ serialized = kwargs.get('serialized', {})
202
+ tool_name = serialized.get('name', 'Unknown Tool')
203
+ input_str = kwargs.get('input_str', '')
204
+
205
+ try:
206
+ ctx.logToolStep(
207
+ stepName=f"Tool - {tool_name} - Error",
208
+ toolName=tool_name,
209
+ input={"input": input_str},
210
+ output="",
211
+ latencyMs=latency_ms,
212
+ status="FAILURE",
213
+ message=str(error)
214
+ )
215
+ print(f"Logged tool error: {tool_name} - {error}")
216
+ except Exception as e:
217
+ print(f"Failed to log tool error: {e}")
218
+
219
+ # Agent Callbacks
220
+ def on_agent_action(self, action: Any, **kwargs: Any) -> None:
221
+ """Called when an agent takes an action."""
222
+ run_id = kwargs.get('run_id')
223
+
224
+ # Track agent step count
225
+ if run_id not in self.step_counters:
226
+ self.step_counters[run_id] = 0
227
+ self.step_counters[run_id] += 1
228
+
229
+ print(f"Agent action: {getattr(action, 'tool', 'unknown')}")
230
+
231
+ def on_agent_finish(self, finish: Any, **kwargs: Any) -> None:
232
+ """Called when an agent finishes."""
233
+ ctx = self._get_session_context()
234
+ if not ctx:
235
+ return
236
+
237
+ run_id = kwargs.get('run_id')
238
+ num_steps = self.step_counters.pop(run_id, 0)
239
+
240
+ try:
241
+ ctx.logAgentStep(
242
+ stepName="Agent Execution",
243
+ agentType="langchain_agent",
244
+ agentName="LangChain Agent",
245
+ numStepsTaken=num_steps,
246
+ tools=[], # Could be populated if tool info is available
247
+ query=getattr(finish, 'log', ''),
248
+ status="SUCCESS",
249
+ message=""
250
+ )
251
+ print(f"Logged agent finish: {num_steps} steps")
252
+ except Exception as e:
253
+ print(f"Failed to log agent step: {e}")
254
+
255
+
256
+
257
+
258
+
llumo/client.py CHANGED
@@ -629,10 +629,12 @@ class LlumoClient:
629
629
  self,
630
630
  data,
631
631
  evals: list, # list of eval metric names
632
+ session, # Add session parameter
632
633
  prompt_template="Give answer to the given query: {{query}} using the given context: {{context}}.",
633
634
  outputColName="output",
634
635
  createExperiment: bool = False,
635
636
  getDataFrame:bool =False,
637
+ playgroundID: str = None,
636
638
  _tocheck=True,
637
639
  ):
638
640
  if isinstance(data, dict):
@@ -680,12 +682,12 @@ class LlumoClient:
680
682
  listener_thread.start()
681
683
  self.validateApiKey(evalName=evals[0])
682
684
  if createExperiment:
683
- activePlayground = str(createEvalPlayground(email=self.email, workspaceID=self.workspaceID))
684
-
685
- else:
686
- activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace(
687
- "-", ""
688
- )
685
+ if playgroundID:
686
+ activePlayground = playgroundID
687
+ else:
688
+ activePlayground = str(createEvalPlayground(email=self.email, workspaceID=self.workspaceID))
689
+ else:
690
+ activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
689
691
  for evalName in evals:
690
692
  # print(f"\n======= Running evaluation for: {evalName} =======")
691
693
 
@@ -879,6 +881,30 @@ class LlumoClient:
879
881
  dataframe.at[index, evalName] = value.get("value")
880
882
  dataframe.at[index, f"{evalName} Reason"] = value.get("reasoning")
881
883
 
884
+ # Log the evaluation step
885
+ if session:
886
+ try:
887
+ start_time = time.time()
888
+ session.logEvalStep(
889
+ stepName=f"EVAL-{evalName}",
890
+ output=value.get("value"),
891
+ context=row.get("context", ""),
892
+ query=row.get("query", ""),
893
+ messageHistory=row.get("messageHistory", ""),
894
+ tools=row.get("tools", ""),
895
+ intermediateSteps=row.get("intermediateSteps", ""),
896
+ groundTruth=row.get("groundTruth", ""),
897
+ analyticsScore=value.get("analyticsScore", {}),
898
+ reasoning=value.get("reasoning", {}),
899
+ classification=value.get("classification", {}),
900
+ evalLabel=value.get("evalLabel", {}),
901
+ latencyMs=int((time.time() - start_time) * 1000),
902
+ status="SUCCESS",
903
+ message="",
904
+ )
905
+ except Exception as e:
906
+ print(f"Error logging eval step: {e}")
907
+
882
908
  self.socket.disconnect()
883
909
 
884
910
 
@@ -1637,8 +1663,10 @@ class LlumoClient:
1637
1663
  rowIdMapping = {} # (rowID-columnID-columnID -> (index, evalName))
1638
1664
  self.validateApiKey(evalName=evals[0])
1639
1665
  if createExperiment:
1640
- activePlayground = str(createEvalPlayground(email=self.email, workspaceID=self.workspaceID))
1641
-
1666
+ if playgroundID:
1667
+ activePlayground = playgroundID
1668
+ else:
1669
+ activePlayground = str(createEvalPlayground(email=self.email, workspaceID=self.workspaceID))
1642
1670
  else:
1643
1671
  activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace(
1644
1672
  "-", ""
llumo/llumoLogger.py ADDED
@@ -0,0 +1,57 @@
1
+ import requests
2
+
3
+
4
+ class LLUMOLogger:
5
+ def __init__(self, apiKey: str, playground: str):
6
+ self.apiKey = apiKey
7
+ self.playground = playground
8
+ self.workspaceID = None
9
+ self.playgroundID = None
10
+ self.userEmailID = None
11
+ self._authenticate()
12
+
13
+ def _authenticate(self):
14
+ url = "https://app.llumo.ai/api/get-playground-name"
15
+ try:
16
+ response = requests.post(
17
+ url,
18
+ headers={
19
+ "Authorization": f"Bearer {self.apiKey}",
20
+ "Content-Type": "application/json",
21
+ },
22
+ json={"playgroundName": self.playground},
23
+ timeout=10,
24
+ )
25
+
26
+ response.raise_for_status()
27
+ res_json = response.json()
28
+
29
+ # Navigate into the nested "data" structure
30
+ inner_data = res_json.get("data", {}).get("data", {})
31
+
32
+ self.workspaceID = inner_data.get("workspaceID")
33
+ self.playgroundID = inner_data.get("playgroundID")
34
+ self.userEmailID = inner_data.get("createdBy")
35
+
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
+
41
+ except requests.exceptions.RequestException as req_err:
42
+ raise RuntimeError(
43
+ f"Network or HTTP error during authentication: {req_err}"
44
+ )
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}")
49
+
50
+ def getWorkspaceID(self):
51
+ return self.workspaceID
52
+
53
+ def getUserEmailID(self):
54
+ return self.userEmailID
55
+
56
+ def getPlaygroundID(self):
57
+ return self.playgroundID