trace-agent-langchain 0.0.1__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.
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""LangChain callback handler that streams execution events into TraceAgent.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from trace_agent_sdk import TraceAgentClient
|
|
5
|
+
from trace_agent_langchain import TraceAgentLangChainCallback
|
|
6
|
+
|
|
7
|
+
client = TraceAgentClient(base_url="http://127.0.0.1:8000")
|
|
8
|
+
run = client.start_run(agent_name="my-agent", goal="Do something")
|
|
9
|
+
|
|
10
|
+
callback = run.as_langchain_callback()
|
|
11
|
+
chain.invoke({"input": "..."}, config={"callbacks": [callback]})
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
import traceback
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
22
|
+
from langchain_core.outputs import ChatResult, LLMResult
|
|
23
|
+
except ImportError: # pragma: no cover
|
|
24
|
+
BaseCallbackHandler = None # type: ignore[misc,assignment]
|
|
25
|
+
LLMResult = None # type: ignore[misc,assignment]
|
|
26
|
+
ChatResult = None # type: ignore[misc,assignment]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TraceAgentLangChainCallback(BaseCallbackHandler):
|
|
30
|
+
"""Record LangChain execution as TraceAgent observations.
|
|
31
|
+
|
|
32
|
+
The callback translates LangChain lifecycle events into HTTP POSTs to the
|
|
33
|
+
TraceAgent backend. If the backend is unreachable the callback logs a warning
|
|
34
|
+
but **never** crashes the calling chain/agent.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
run_session: Any,
|
|
40
|
+
*,
|
|
41
|
+
record_chain_events: bool = True,
|
|
42
|
+
record_retriever_events: bool = True,
|
|
43
|
+
) -> None:
|
|
44
|
+
if BaseCallbackHandler is None:
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
"langchain-core is not installed. "
|
|
47
|
+
"Install it with: uv pip install -e '.[langchain]'"
|
|
48
|
+
)
|
|
49
|
+
self.run_session = run_session
|
|
50
|
+
self.record_chain_events = record_chain_events
|
|
51
|
+
self.record_retriever_events = record_retriever_events
|
|
52
|
+
# langchain run_id -> start timestamp (for duration calculation)
|
|
53
|
+
self._starts: dict[str, float] = {}
|
|
54
|
+
|
|
55
|
+
# ── Helpers ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def _post_observations(self, observations: list[dict[str, Any]]) -> None:
|
|
58
|
+
"""Send observations to the TraceAgent backend."""
|
|
59
|
+
if not observations:
|
|
60
|
+
return
|
|
61
|
+
try:
|
|
62
|
+
self.run_session.client._request(
|
|
63
|
+
"POST",
|
|
64
|
+
f"/api/runs/{self.run_session.run_id}/observations",
|
|
65
|
+
{"observations": observations},
|
|
66
|
+
)
|
|
67
|
+
except Exception:
|
|
68
|
+
# Silently ignore backend errors so the agent keeps running.
|
|
69
|
+
traceback.print_exc()
|
|
70
|
+
|
|
71
|
+
def _post_tool_results(self, results: list[dict[str, Any]]) -> None:
|
|
72
|
+
"""Send tool results to the TraceAgent backend."""
|
|
73
|
+
if not results:
|
|
74
|
+
return
|
|
75
|
+
try:
|
|
76
|
+
self.run_session.client._request(
|
|
77
|
+
"POST",
|
|
78
|
+
f"/api/runs/{self.run_session.run_id}/tool-results",
|
|
79
|
+
{"results": results},
|
|
80
|
+
)
|
|
81
|
+
except Exception:
|
|
82
|
+
traceback.print_exc()
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _extract_token_usage(result: LLMResult | ChatResult | None) -> dict[str, Any]:
|
|
86
|
+
"""Try to extract token usage from an LLM / Chat result."""
|
|
87
|
+
if result is None:
|
|
88
|
+
return {}
|
|
89
|
+
llm_output = getattr(result, "llm_output", None) or {}
|
|
90
|
+
usage = llm_output.get("token_usage") or {}
|
|
91
|
+
# OpenAI format
|
|
92
|
+
if "prompt_tokens" in usage:
|
|
93
|
+
return {
|
|
94
|
+
"prompt_tokens": usage.get("prompt_tokens"),
|
|
95
|
+
"completion_tokens": usage.get("completion_tokens"),
|
|
96
|
+
"total_tokens": usage.get("total_tokens"),
|
|
97
|
+
}
|
|
98
|
+
# Anthropic format
|
|
99
|
+
if "input_tokens" in usage:
|
|
100
|
+
return {
|
|
101
|
+
"prompt_tokens": usage.get("input_tokens"),
|
|
102
|
+
"completion_tokens": usage.get("output_tokens"),
|
|
103
|
+
"total_tokens": (
|
|
104
|
+
(usage.get("input_tokens") or 0)
|
|
105
|
+
+ (usage.get("output_tokens") or 0)
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
return dict(usage)
|
|
109
|
+
|
|
110
|
+
# ── Chain lifecycle ─────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
def on_chain_start(
|
|
113
|
+
self,
|
|
114
|
+
serialized: dict[str, Any] | None,
|
|
115
|
+
inputs: dict[str, Any],
|
|
116
|
+
*,
|
|
117
|
+
run_id: str,
|
|
118
|
+
parent_run_id: str | None = None,
|
|
119
|
+
tags: list[str] | None = None,
|
|
120
|
+
metadata: dict[str, Any] | None = None,
|
|
121
|
+
**kwargs: Any,
|
|
122
|
+
) -> None:
|
|
123
|
+
if not self.record_chain_events:
|
|
124
|
+
return
|
|
125
|
+
self._starts[run_id] = time.time()
|
|
126
|
+
chain_type = (serialized or {}).get("name", "unknown")
|
|
127
|
+
inputs_keys = list(inputs.keys()) if isinstance(inputs, dict) else []
|
|
128
|
+
self._post_observations(
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
"kind": "chain_step",
|
|
132
|
+
"step_id": str(run_id),
|
|
133
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
134
|
+
"status": "running",
|
|
135
|
+
"input_summary": f"Chain '{chain_type}' started",
|
|
136
|
+
"metadata": {
|
|
137
|
+
"chain_type": chain_type,
|
|
138
|
+
"inputs_keys": inputs_keys,
|
|
139
|
+
"tags": tags,
|
|
140
|
+
**(metadata or {}),
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def on_chain_end(
|
|
147
|
+
self,
|
|
148
|
+
outputs: dict[str, Any],
|
|
149
|
+
*,
|
|
150
|
+
run_id: str,
|
|
151
|
+
parent_run_id: str | None = None,
|
|
152
|
+
tags: list[str] | None = None,
|
|
153
|
+
metadata: dict[str, Any] | None = None,
|
|
154
|
+
**kwargs: Any,
|
|
155
|
+
) -> None:
|
|
156
|
+
if not self.record_chain_events:
|
|
157
|
+
return
|
|
158
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
159
|
+
output_keys = list(outputs.keys()) if isinstance(outputs, dict) else []
|
|
160
|
+
self._post_observations(
|
|
161
|
+
[
|
|
162
|
+
{
|
|
163
|
+
"kind": "chain_step",
|
|
164
|
+
"step_id": str(run_id),
|
|
165
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
166
|
+
"status": "succeeded",
|
|
167
|
+
"duration_ms": duration_ms,
|
|
168
|
+
"input_summary": f"Chain completed",
|
|
169
|
+
"metadata": {
|
|
170
|
+
"output_keys": output_keys,
|
|
171
|
+
"tags": tags,
|
|
172
|
+
**(metadata or {}),
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def on_chain_error(
|
|
179
|
+
self,
|
|
180
|
+
error: BaseException,
|
|
181
|
+
*,
|
|
182
|
+
run_id: str,
|
|
183
|
+
parent_run_id: str | None = None,
|
|
184
|
+
tags: list[str] | None = None,
|
|
185
|
+
metadata: dict[str, Any] | None = None,
|
|
186
|
+
**kwargs: Any,
|
|
187
|
+
) -> None:
|
|
188
|
+
if not self.record_chain_events:
|
|
189
|
+
return
|
|
190
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
191
|
+
self._post_observations(
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
"kind": "chain_step",
|
|
195
|
+
"step_id": str(run_id),
|
|
196
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
197
|
+
"status": "failed",
|
|
198
|
+
"duration_ms": duration_ms,
|
|
199
|
+
"error_code": error.__class__.__name__,
|
|
200
|
+
"error_summary": str(error),
|
|
201
|
+
"metadata": {"tags": tags, **(metadata or {})},
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# ── LLM / Chat model lifecycle ──────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
def _on_llm_start(
|
|
209
|
+
self,
|
|
210
|
+
*,
|
|
211
|
+
run_id: str,
|
|
212
|
+
parent_run_id: str | None,
|
|
213
|
+
provider: str | None,
|
|
214
|
+
model: str | None,
|
|
215
|
+
metadata: dict[str, Any] | None,
|
|
216
|
+
) -> None:
|
|
217
|
+
self._starts[run_id] = time.time()
|
|
218
|
+
self._post_observations(
|
|
219
|
+
[
|
|
220
|
+
{
|
|
221
|
+
"kind": "llm_call",
|
|
222
|
+
"step_id": str(run_id),
|
|
223
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
224
|
+
"status": "running",
|
|
225
|
+
"provider": provider,
|
|
226
|
+
"model": model,
|
|
227
|
+
"metadata": metadata or {},
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def _on_llm_end(
|
|
233
|
+
self,
|
|
234
|
+
result: LLMResult | ChatResult | None,
|
|
235
|
+
*,
|
|
236
|
+
run_id: str,
|
|
237
|
+
parent_run_id: str | None,
|
|
238
|
+
provider: str | None,
|
|
239
|
+
model: str | None,
|
|
240
|
+
metadata: dict[str, Any] | None,
|
|
241
|
+
) -> None:
|
|
242
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
243
|
+
usage = self._extract_token_usage(result)
|
|
244
|
+
self._post_observations(
|
|
245
|
+
[
|
|
246
|
+
{
|
|
247
|
+
"kind": "llm_call",
|
|
248
|
+
"step_id": str(run_id),
|
|
249
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
250
|
+
"status": "succeeded",
|
|
251
|
+
"duration_ms": duration_ms,
|
|
252
|
+
"provider": provider,
|
|
253
|
+
"model": model,
|
|
254
|
+
"metadata": {**usage, **(metadata or {})},
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _on_llm_error(
|
|
260
|
+
self,
|
|
261
|
+
error: BaseException,
|
|
262
|
+
*,
|
|
263
|
+
run_id: str,
|
|
264
|
+
parent_run_id: str | None,
|
|
265
|
+
provider: str | None,
|
|
266
|
+
model: str | None,
|
|
267
|
+
metadata: dict[str, Any] | None,
|
|
268
|
+
) -> None:
|
|
269
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
270
|
+
self._post_observations(
|
|
271
|
+
[
|
|
272
|
+
{
|
|
273
|
+
"kind": "llm_call",
|
|
274
|
+
"step_id": str(run_id),
|
|
275
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
276
|
+
"status": "failed",
|
|
277
|
+
"duration_ms": duration_ms,
|
|
278
|
+
"provider": provider,
|
|
279
|
+
"model": model,
|
|
280
|
+
"error_code": error.__class__.__name__,
|
|
281
|
+
"error_summary": str(error),
|
|
282
|
+
"metadata": metadata or {},
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def on_llm_start(
|
|
288
|
+
self,
|
|
289
|
+
serialized: dict[str, Any] | None,
|
|
290
|
+
prompts: list[str],
|
|
291
|
+
*,
|
|
292
|
+
run_id: str,
|
|
293
|
+
parent_run_id: str | None = None,
|
|
294
|
+
tags: list[str] | None = None,
|
|
295
|
+
metadata: dict[str, Any] | None = None,
|
|
296
|
+
**kwargs: Any,
|
|
297
|
+
) -> None:
|
|
298
|
+
provider = (serialized or {}).get("name", "unknown")
|
|
299
|
+
self._on_llm_start(
|
|
300
|
+
run_id=run_id,
|
|
301
|
+
parent_run_id=parent_run_id,
|
|
302
|
+
provider=provider,
|
|
303
|
+
model=None,
|
|
304
|
+
metadata={"tags": tags, **(metadata or {})},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def on_llm_end(
|
|
308
|
+
self,
|
|
309
|
+
response: LLMResult,
|
|
310
|
+
*,
|
|
311
|
+
run_id: str,
|
|
312
|
+
parent_run_id: str | None = None,
|
|
313
|
+
tags: list[str] | None = None,
|
|
314
|
+
metadata: dict[str, Any] | None = None,
|
|
315
|
+
**kwargs: Any,
|
|
316
|
+
) -> None:
|
|
317
|
+
provider = (metadata or {}).get("name", "unknown")
|
|
318
|
+
self._on_llm_end(
|
|
319
|
+
response,
|
|
320
|
+
run_id=run_id,
|
|
321
|
+
parent_run_id=parent_run_id,
|
|
322
|
+
provider=provider,
|
|
323
|
+
model=None,
|
|
324
|
+
metadata={"tags": tags, **(metadata or {})},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def on_llm_error(
|
|
328
|
+
self,
|
|
329
|
+
error: BaseException,
|
|
330
|
+
*,
|
|
331
|
+
run_id: str,
|
|
332
|
+
parent_run_id: str | None = None,
|
|
333
|
+
tags: list[str] | None = None,
|
|
334
|
+
metadata: dict[str, Any] | None = None,
|
|
335
|
+
**kwargs: Any,
|
|
336
|
+
) -> None:
|
|
337
|
+
provider = (metadata or {}).get("name", "unknown")
|
|
338
|
+
self._on_llm_error(
|
|
339
|
+
error,
|
|
340
|
+
run_id=run_id,
|
|
341
|
+
parent_run_id=parent_run_id,
|
|
342
|
+
provider=provider,
|
|
343
|
+
model=None,
|
|
344
|
+
metadata={"tags": tags, **(metadata or {})},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def on_chat_model_start(
|
|
348
|
+
self,
|
|
349
|
+
serialized: dict[str, Any] | None,
|
|
350
|
+
messages: list[list[Any]],
|
|
351
|
+
*,
|
|
352
|
+
run_id: str,
|
|
353
|
+
parent_run_id: str | None = None,
|
|
354
|
+
tags: list[str] | None = None,
|
|
355
|
+
metadata: dict[str, Any] | None = None,
|
|
356
|
+
**kwargs: Any,
|
|
357
|
+
) -> None:
|
|
358
|
+
provider = (serialized or {}).get("name", "unknown")
|
|
359
|
+
model = (kwargs.get("invocation_params") or {}).get("model")
|
|
360
|
+
self._on_llm_start(
|
|
361
|
+
run_id=run_id,
|
|
362
|
+
parent_run_id=parent_run_id,
|
|
363
|
+
provider=provider,
|
|
364
|
+
model=model,
|
|
365
|
+
metadata={"tags": tags, **(metadata or {})},
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def on_chat_model_end(
|
|
369
|
+
self,
|
|
370
|
+
response: ChatResult,
|
|
371
|
+
*,
|
|
372
|
+
run_id: str,
|
|
373
|
+
parent_run_id: str | None = None,
|
|
374
|
+
tags: list[str] | None = None,
|
|
375
|
+
metadata: dict[str, Any] | None = None,
|
|
376
|
+
**kwargs: Any,
|
|
377
|
+
) -> None:
|
|
378
|
+
provider = (metadata or {}).get("name", "unknown")
|
|
379
|
+
model = (kwargs.get("invocation_params") or {}).get("model")
|
|
380
|
+
self._on_llm_end(
|
|
381
|
+
response,
|
|
382
|
+
run_id=run_id,
|
|
383
|
+
parent_run_id=parent_run_id,
|
|
384
|
+
provider=provider,
|
|
385
|
+
model=model,
|
|
386
|
+
metadata={"tags": tags, **(metadata or {})},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# ── Tool lifecycle ──────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
def on_tool_start(
|
|
392
|
+
self,
|
|
393
|
+
serialized: dict[str, Any] | None,
|
|
394
|
+
input_str: str,
|
|
395
|
+
*,
|
|
396
|
+
run_id: str,
|
|
397
|
+
parent_run_id: str | None = None,
|
|
398
|
+
tags: list[str] | None = None,
|
|
399
|
+
metadata: dict[str, Any] | None = None,
|
|
400
|
+
**kwargs: Any,
|
|
401
|
+
) -> None:
|
|
402
|
+
self._starts[run_id] = time.time()
|
|
403
|
+
tool_name = (serialized or {}).get("name", "unknown")
|
|
404
|
+
self._post_observations(
|
|
405
|
+
[
|
|
406
|
+
{
|
|
407
|
+
"kind": "tool_call",
|
|
408
|
+
"step_id": str(run_id),
|
|
409
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
410
|
+
"status": "running",
|
|
411
|
+
"tool_name": tool_name,
|
|
412
|
+
"input_summary": f"Tool '{tool_name}' started",
|
|
413
|
+
"metadata": {
|
|
414
|
+
"input": input_str,
|
|
415
|
+
"tags": tags,
|
|
416
|
+
**(metadata or {}),
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def on_tool_end(
|
|
423
|
+
self,
|
|
424
|
+
output: Any,
|
|
425
|
+
*,
|
|
426
|
+
run_id: str,
|
|
427
|
+
parent_run_id: str | None = None,
|
|
428
|
+
tags: list[str] | None = None,
|
|
429
|
+
metadata: dict[str, Any] | None = None,
|
|
430
|
+
**kwargs: Any,
|
|
431
|
+
) -> None:
|
|
432
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
433
|
+
tool_name = (metadata or {}).get("name", "unknown")
|
|
434
|
+
output_str = str(output) if output is not None else ""
|
|
435
|
+
self._post_tool_results(
|
|
436
|
+
[
|
|
437
|
+
{
|
|
438
|
+
"call_id": run_id,
|
|
439
|
+
"name": tool_name,
|
|
440
|
+
"step_id": run_id,
|
|
441
|
+
"status": "succeeded",
|
|
442
|
+
"output_summary": output_str[:500],
|
|
443
|
+
"output_hash": None,
|
|
444
|
+
"duration_ms": duration_ms,
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
)
|
|
448
|
+
self._post_observations(
|
|
449
|
+
[
|
|
450
|
+
{
|
|
451
|
+
"kind": "tool_call",
|
|
452
|
+
"step_id": str(run_id),
|
|
453
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
454
|
+
"status": "succeeded",
|
|
455
|
+
"tool_name": tool_name,
|
|
456
|
+
"duration_ms": duration_ms,
|
|
457
|
+
"input_summary": f"Tool '{tool_name}' succeeded",
|
|
458
|
+
"metadata": {"tags": tags, **(metadata or {})},
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def on_tool_error(
|
|
464
|
+
self,
|
|
465
|
+
error: BaseException,
|
|
466
|
+
*,
|
|
467
|
+
run_id: str,
|
|
468
|
+
parent_run_id: str | None = None,
|
|
469
|
+
tags: list[str] | None = None,
|
|
470
|
+
metadata: dict[str, Any] | None = None,
|
|
471
|
+
**kwargs: Any,
|
|
472
|
+
) -> None:
|
|
473
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
474
|
+
tool_name = (metadata or {}).get("name", "unknown")
|
|
475
|
+
error_summary = str(error)
|
|
476
|
+
self._post_tool_results(
|
|
477
|
+
[
|
|
478
|
+
{
|
|
479
|
+
"call_id": run_id,
|
|
480
|
+
"name": tool_name,
|
|
481
|
+
"step_id": run_id,
|
|
482
|
+
"status": "failed",
|
|
483
|
+
"output_summary": error_summary[:500],
|
|
484
|
+
"output_hash": None,
|
|
485
|
+
"error_code": error.__class__.__name__,
|
|
486
|
+
"error_summary": error_summary,
|
|
487
|
+
"duration_ms": duration_ms,
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
)
|
|
491
|
+
self._post_observations(
|
|
492
|
+
[
|
|
493
|
+
{
|
|
494
|
+
"kind": "tool_call",
|
|
495
|
+
"step_id": str(run_id),
|
|
496
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
497
|
+
"status": "failed",
|
|
498
|
+
"tool_name": tool_name,
|
|
499
|
+
"duration_ms": duration_ms,
|
|
500
|
+
"error_code": error.__class__.__name__,
|
|
501
|
+
"error_summary": error_summary,
|
|
502
|
+
"metadata": {"tags": tags, **(metadata or {})},
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# ── Retriever lifecycle ─────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
def on_retriever_start(
|
|
510
|
+
self,
|
|
511
|
+
serialized: dict[str, Any] | None,
|
|
512
|
+
query: str,
|
|
513
|
+
*,
|
|
514
|
+
run_id: str,
|
|
515
|
+
parent_run_id: str | None = None,
|
|
516
|
+
tags: list[str] | None = None,
|
|
517
|
+
metadata: dict[str, Any] | None = None,
|
|
518
|
+
**kwargs: Any,
|
|
519
|
+
) -> None:
|
|
520
|
+
if not self.record_retriever_events:
|
|
521
|
+
return
|
|
522
|
+
self._starts[run_id] = time.time()
|
|
523
|
+
self._post_observations(
|
|
524
|
+
[
|
|
525
|
+
{
|
|
526
|
+
"kind": "retriever_query",
|
|
527
|
+
"step_id": str(run_id),
|
|
528
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
529
|
+
"status": "running",
|
|
530
|
+
"input_summary": f"Retriever query: {query[:200]}",
|
|
531
|
+
"metadata": {"query": query, "tags": tags, **(metadata or {})},
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def on_retriever_end(
|
|
537
|
+
self,
|
|
538
|
+
documents: list[Any],
|
|
539
|
+
*,
|
|
540
|
+
run_id: str,
|
|
541
|
+
parent_run_id: str | None = None,
|
|
542
|
+
tags: list[str] | None = None,
|
|
543
|
+
metadata: dict[str, Any] | None = None,
|
|
544
|
+
**kwargs: Any,
|
|
545
|
+
) -> None:
|
|
546
|
+
if not self.record_retriever_events:
|
|
547
|
+
return
|
|
548
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
549
|
+
self._post_observations(
|
|
550
|
+
[
|
|
551
|
+
{
|
|
552
|
+
"kind": "retriever_query",
|
|
553
|
+
"step_id": str(run_id),
|
|
554
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
555
|
+
"status": "succeeded",
|
|
556
|
+
"duration_ms": duration_ms,
|
|
557
|
+
"input_summary": f"Retriever returned {len(documents)} documents",
|
|
558
|
+
"metadata": {
|
|
559
|
+
"document_count": len(documents),
|
|
560
|
+
"tags": tags,
|
|
561
|
+
**(metadata or {}),
|
|
562
|
+
},
|
|
563
|
+
}
|
|
564
|
+
]
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def on_retriever_error(
|
|
568
|
+
self,
|
|
569
|
+
error: BaseException,
|
|
570
|
+
*,
|
|
571
|
+
run_id: str,
|
|
572
|
+
parent_run_id: str | None = None,
|
|
573
|
+
tags: list[str] | None = None,
|
|
574
|
+
metadata: dict[str, Any] | None = None,
|
|
575
|
+
**kwargs: Any,
|
|
576
|
+
) -> None:
|
|
577
|
+
if not self.record_retriever_events:
|
|
578
|
+
return
|
|
579
|
+
duration_ms = int((time.time() - self._starts.pop(run_id, time.time())) * 1000)
|
|
580
|
+
self._post_observations(
|
|
581
|
+
[
|
|
582
|
+
{
|
|
583
|
+
"kind": "retriever_query",
|
|
584
|
+
"step_id": str(run_id),
|
|
585
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
586
|
+
"status": "failed",
|
|
587
|
+
"duration_ms": duration_ms,
|
|
588
|
+
"error_code": error.__class__.__name__,
|
|
589
|
+
"error_summary": str(error),
|
|
590
|
+
"metadata": {"tags": tags, **(metadata or {})},
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# ── Agent lifecycle ─────────────────────────────────────────────────────
|
|
596
|
+
|
|
597
|
+
def on_agent_action(
|
|
598
|
+
self,
|
|
599
|
+
action: Any,
|
|
600
|
+
*,
|
|
601
|
+
run_id: str,
|
|
602
|
+
parent_run_id: str | None = None,
|
|
603
|
+
tags: list[str] | None = None,
|
|
604
|
+
metadata: dict[str, Any] | None = None,
|
|
605
|
+
**kwargs: Any,
|
|
606
|
+
) -> None:
|
|
607
|
+
tool = getattr(action, "tool", None) or "unknown"
|
|
608
|
+
tool_input = getattr(action, "tool_input", None) or {}
|
|
609
|
+
self._post_observations(
|
|
610
|
+
[
|
|
611
|
+
{
|
|
612
|
+
"kind": "agent_action",
|
|
613
|
+
"step_id": str(run_id),
|
|
614
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
615
|
+
"status": "running",
|
|
616
|
+
"tool_name": tool,
|
|
617
|
+
"input_summary": f"Agent action: {tool}",
|
|
618
|
+
"metadata": {
|
|
619
|
+
"tool": tool,
|
|
620
|
+
"tool_input": tool_input,
|
|
621
|
+
"tags": tags,
|
|
622
|
+
**(metadata or {}),
|
|
623
|
+
},
|
|
624
|
+
}
|
|
625
|
+
]
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def on_agent_finish(
|
|
629
|
+
self,
|
|
630
|
+
finish: Any,
|
|
631
|
+
*,
|
|
632
|
+
run_id: str,
|
|
633
|
+
parent_run_id: str | None = None,
|
|
634
|
+
tags: list[str] | None = None,
|
|
635
|
+
metadata: dict[str, Any] | None = None,
|
|
636
|
+
**kwargs: Any,
|
|
637
|
+
) -> None:
|
|
638
|
+
output = getattr(finish, "return_values", {})
|
|
639
|
+
self._post_observations(
|
|
640
|
+
[
|
|
641
|
+
{
|
|
642
|
+
"kind": "agent_action",
|
|
643
|
+
"step_id": str(run_id),
|
|
644
|
+
"parent_step_id": str(parent_run_id) if parent_run_id else None,
|
|
645
|
+
"status": "succeeded",
|
|
646
|
+
"input_summary": "Agent finished",
|
|
647
|
+
"metadata": {
|
|
648
|
+
"output_keys": list(output.keys()),
|
|
649
|
+
"tags": tags,
|
|
650
|
+
**(metadata or {}),
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
]
|
|
654
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trace-agent-langchain
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: LangChain callback integration for TraceAgent.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: trace-agent-sdk
|
|
7
|
+
Requires-Dist: langchain-core<0.4.0,>=0.3.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest<9.0.0,>=8.3.2; extra == "dev"
|
|
10
|
+
Requires-Dist: langchain<0.4.0,>=0.3.0; extra == "dev"
|
|
11
|
+
Requires-Dist: langchain-openai<0.3.0,>=0.2.0; extra == "dev"
|
|
12
|
+
Requires-Dist: trace-agent-server; extra == "dev"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
trace_agent_langchain/__init__.py,sha256=DYp3DMO2WajFK1dmu_J-aOyiv31el5iekHUEQlAkRRQ,152
|
|
2
|
+
trace_agent_langchain/callback.py,sha256=lMntz9RMSjWFAOyythm3GYAJKe_hpVxfKXej-g2fH2E,22716
|
|
3
|
+
trace_agent_langchain-0.0.1.dist-info/METADATA,sha256=kPo2xlXEgfnB7kUp8c9apnyDHMVwFaOCW-QJQftaF5Y,459
|
|
4
|
+
trace_agent_langchain-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
trace_agent_langchain-0.0.1.dist-info/top_level.txt,sha256=gzLKO-AMGL2ZY03u3wCA70IMHodevfzUVGjdh512yrI,22
|
|
6
|
+
trace_agent_langchain-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trace_agent_langchain
|