trace-agent-sdk 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,616 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from trace_agent_sdk.redaction import stable_hash, summarize_value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
TYPE_MAP = {
|
|
20
|
+
str: "string",
|
|
21
|
+
int: "integer",
|
|
22
|
+
float: "number",
|
|
23
|
+
bool: "boolean",
|
|
24
|
+
dict: "object",
|
|
25
|
+
list: "array",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ToolBinding:
|
|
31
|
+
func: Callable[..., Any]
|
|
32
|
+
name: str
|
|
33
|
+
description: str
|
|
34
|
+
redact_fields: list[str]
|
|
35
|
+
input_schema: dict[str, Any]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class TurnResult:
|
|
40
|
+
assistant_message: dict[str, Any]
|
|
41
|
+
executed_tools: list[dict[str, Any]] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CommandSession:
|
|
45
|
+
def __init__(self, run: "RunSession") -> None:
|
|
46
|
+
self.session = run
|
|
47
|
+
|
|
48
|
+
def run(
|
|
49
|
+
self,
|
|
50
|
+
command: str | list[str],
|
|
51
|
+
*,
|
|
52
|
+
cwd: str | None = None,
|
|
53
|
+
timeout: float | None = None,
|
|
54
|
+
check: bool = False,
|
|
55
|
+
shell: bool = False,
|
|
56
|
+
encoding: str = "utf-8",
|
|
57
|
+
) -> subprocess.CompletedProcess[str]:
|
|
58
|
+
started_at = time.perf_counter()
|
|
59
|
+
completed = subprocess.run(
|
|
60
|
+
command,
|
|
61
|
+
cwd=cwd,
|
|
62
|
+
timeout=timeout,
|
|
63
|
+
shell=shell,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
encoding=encoding,
|
|
67
|
+
check=False,
|
|
68
|
+
)
|
|
69
|
+
duration_ms = int((time.perf_counter() - started_at) * 1000)
|
|
70
|
+
output_payload = {"stdout": completed.stdout, "stderr": completed.stderr}
|
|
71
|
+
status = "succeeded" if completed.returncode == 0 else "failed"
|
|
72
|
+
error_code = "CommandError" if completed.returncode != 0 else None
|
|
73
|
+
error_summary = None
|
|
74
|
+
if completed.returncode != 0:
|
|
75
|
+
error_summary = summarize_value(
|
|
76
|
+
{
|
|
77
|
+
"command": self.session._render_command(command),
|
|
78
|
+
"returncode": completed.returncode,
|
|
79
|
+
"stderr": completed.stderr,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.session.record_command(
|
|
84
|
+
command,
|
|
85
|
+
status=status,
|
|
86
|
+
cwd=cwd,
|
|
87
|
+
output=output_payload,
|
|
88
|
+
duration_ms=duration_ms,
|
|
89
|
+
error_code=error_code,
|
|
90
|
+
error_summary=error_summary,
|
|
91
|
+
exit_code=completed.returncode,
|
|
92
|
+
stdout_summary=summarize_value(completed.stdout) if completed.stdout else None,
|
|
93
|
+
stderr_summary=summarize_value(completed.stderr) if completed.stderr else None,
|
|
94
|
+
)
|
|
95
|
+
if check and completed.returncode != 0:
|
|
96
|
+
raise subprocess.CalledProcessError(
|
|
97
|
+
completed.returncode,
|
|
98
|
+
completed.args,
|
|
99
|
+
output=completed.stdout,
|
|
100
|
+
stderr=completed.stderr,
|
|
101
|
+
)
|
|
102
|
+
return completed
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FileSession:
|
|
106
|
+
def __init__(self, run: "RunSession") -> None:
|
|
107
|
+
self.session = run
|
|
108
|
+
|
|
109
|
+
def read_text(self, path: str, *, encoding: str = "utf-8") -> str:
|
|
110
|
+
path_obj = Path(path)
|
|
111
|
+
content = path_obj.read_text(encoding=encoding)
|
|
112
|
+
self.session.record_file_read(path, content=content)
|
|
113
|
+
return content
|
|
114
|
+
|
|
115
|
+
def write_text(self, path: str, content: str, *, encoding: str = "utf-8") -> None:
|
|
116
|
+
path_obj = Path(path)
|
|
117
|
+
before_content = path_obj.read_text(encoding=encoding) if path_obj.exists() else None
|
|
118
|
+
change_type = "update" if before_content is not None else "create"
|
|
119
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
path_obj.write_text(content, encoding=encoding)
|
|
121
|
+
self.session.record_file_write(
|
|
122
|
+
path,
|
|
123
|
+
change_type=change_type,
|
|
124
|
+
before_content=before_content,
|
|
125
|
+
after_content=content,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def delete(self, path: str, *, encoding: str = "utf-8") -> None:
|
|
129
|
+
path_obj = Path(path)
|
|
130
|
+
if not path_obj.exists():
|
|
131
|
+
return
|
|
132
|
+
before_content = path_obj.read_text(encoding=encoding)
|
|
133
|
+
path_obj.unlink()
|
|
134
|
+
self.session.record_file_write(
|
|
135
|
+
path,
|
|
136
|
+
change_type="delete",
|
|
137
|
+
before_content=before_content,
|
|
138
|
+
after_content=None,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def patch_text(
|
|
142
|
+
self,
|
|
143
|
+
path: str,
|
|
144
|
+
updated_content: str,
|
|
145
|
+
*,
|
|
146
|
+
encoding: str = "utf-8",
|
|
147
|
+
diff_text: str | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
path_obj = Path(path)
|
|
150
|
+
before_content = path_obj.read_text(encoding=encoding) if path_obj.exists() else None
|
|
151
|
+
change_type = "update" if before_content is not None else "create"
|
|
152
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
path_obj.write_text(updated_content, encoding=encoding)
|
|
154
|
+
self.session.record_file_write(
|
|
155
|
+
path,
|
|
156
|
+
change_type=change_type,
|
|
157
|
+
before_content=before_content,
|
|
158
|
+
after_content=updated_content,
|
|
159
|
+
)
|
|
160
|
+
self.session.record_patch(
|
|
161
|
+
path,
|
|
162
|
+
diff_text=diff_text,
|
|
163
|
+
before_content=before_content,
|
|
164
|
+
after_content=updated_content,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ArtifactSession:
|
|
169
|
+
def __init__(self, run: "RunSession") -> None:
|
|
170
|
+
self.session = run
|
|
171
|
+
|
|
172
|
+
def capture(
|
|
173
|
+
self,
|
|
174
|
+
kind: str,
|
|
175
|
+
label: str,
|
|
176
|
+
summary: str,
|
|
177
|
+
*,
|
|
178
|
+
metadata: dict[str, Any] | None = None,
|
|
179
|
+
content: Any | None = None,
|
|
180
|
+
) -> dict[str, Any]:
|
|
181
|
+
return self.session.record_artifact(
|
|
182
|
+
kind,
|
|
183
|
+
label,
|
|
184
|
+
summary,
|
|
185
|
+
metadata=metadata,
|
|
186
|
+
content=content,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class RunSession:
|
|
191
|
+
def __init__(self, client: "TraceAgentClient", run_id: str, agent_name: str, goal: str, metadata: dict[str, Any] | None = None) -> None:
|
|
192
|
+
self.client = client
|
|
193
|
+
self.run_id = run_id
|
|
194
|
+
self.agent_name = agent_name
|
|
195
|
+
self.goal = goal
|
|
196
|
+
self.metadata = metadata or {}
|
|
197
|
+
self.tools: dict[str, ToolBinding] = {}
|
|
198
|
+
self.messages: list[dict[str, Any]] = []
|
|
199
|
+
self.commands = CommandSession(self)
|
|
200
|
+
self.files = FileSession(self)
|
|
201
|
+
self.artifacts = ArtifactSession(self)
|
|
202
|
+
|
|
203
|
+
def record_command(
|
|
204
|
+
self,
|
|
205
|
+
command: str | list[str],
|
|
206
|
+
*,
|
|
207
|
+
status: str = "succeeded",
|
|
208
|
+
cwd: str | None = None,
|
|
209
|
+
output: Any | None = None,
|
|
210
|
+
duration_ms: int | None = None,
|
|
211
|
+
error_code: str | None = None,
|
|
212
|
+
error_summary: str | None = None,
|
|
213
|
+
exit_code: int | None = None,
|
|
214
|
+
stdout_summary: str | None = None,
|
|
215
|
+
stderr_summary: str | None = None,
|
|
216
|
+
step_id: str | None = None,
|
|
217
|
+
parent_step_id: str | None = None,
|
|
218
|
+
metadata: dict[str, Any] | None = None,
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
command_text = self._render_command(command)
|
|
221
|
+
payload = {
|
|
222
|
+
"observations": [
|
|
223
|
+
{
|
|
224
|
+
"kind": "command",
|
|
225
|
+
"step_id": step_id or self._step_id("command"),
|
|
226
|
+
"parent_step_id": parent_step_id,
|
|
227
|
+
"status": status,
|
|
228
|
+
"command_id": str(uuid4()),
|
|
229
|
+
"command": command_text,
|
|
230
|
+
"cwd": cwd,
|
|
231
|
+
"output_summary": summarize_value(output) if output is not None else None,
|
|
232
|
+
"duration_ms": duration_ms,
|
|
233
|
+
"error_code": error_code,
|
|
234
|
+
"error_summary": error_summary,
|
|
235
|
+
"exit_code": exit_code,
|
|
236
|
+
"stdout_summary": stdout_summary,
|
|
237
|
+
"stderr_summary": stderr_summary,
|
|
238
|
+
"metadata": metadata or {},
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
return self.client._request("POST", f"/api/runs/{self.run_id}/observations", payload)
|
|
243
|
+
|
|
244
|
+
def record_file_read(
|
|
245
|
+
self,
|
|
246
|
+
path: str,
|
|
247
|
+
*,
|
|
248
|
+
content: Any | None = None,
|
|
249
|
+
content_hash: str | None = None,
|
|
250
|
+
size_bytes: int | None = None,
|
|
251
|
+
step_id: str | None = None,
|
|
252
|
+
parent_step_id: str | None = None,
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
payload = {
|
|
255
|
+
"observations": [
|
|
256
|
+
{
|
|
257
|
+
"kind": "file_read",
|
|
258
|
+
"step_id": step_id or self._step_id("file-read"),
|
|
259
|
+
"parent_step_id": parent_step_id,
|
|
260
|
+
"path": path,
|
|
261
|
+
"content_hash": content_hash or (stable_hash(content) if content is not None else None),
|
|
262
|
+
"size_bytes": size_bytes if size_bytes is not None else self._size_bytes(content),
|
|
263
|
+
"extension": os.path.splitext(path)[1] or None,
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
return self.client._request("POST", f"/api/runs/{self.run_id}/observations", payload)
|
|
268
|
+
|
|
269
|
+
def record_file_write(
|
|
270
|
+
self,
|
|
271
|
+
path: str,
|
|
272
|
+
*,
|
|
273
|
+
change_type: str = "update",
|
|
274
|
+
before_content: Any | None = None,
|
|
275
|
+
after_content: Any | None = None,
|
|
276
|
+
before_hash: str | None = None,
|
|
277
|
+
after_hash: str | None = None,
|
|
278
|
+
step_id: str | None = None,
|
|
279
|
+
parent_step_id: str | None = None,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
diff_summary, line_additions, line_deletions = self._diff_details(before_content, after_content)
|
|
282
|
+
payload = {
|
|
283
|
+
"observations": [
|
|
284
|
+
{
|
|
285
|
+
"kind": "file_write",
|
|
286
|
+
"step_id": step_id or self._step_id("file-write"),
|
|
287
|
+
"parent_step_id": parent_step_id,
|
|
288
|
+
"path": path,
|
|
289
|
+
"change_type": change_type,
|
|
290
|
+
"before_hash": before_hash or (stable_hash(before_content) if before_content is not None else None),
|
|
291
|
+
"after_hash": after_hash or (stable_hash(after_content) if after_content is not None else None),
|
|
292
|
+
"content_hash": stable_hash(after_content) if after_content is not None else after_hash,
|
|
293
|
+
"diff_summary": diff_summary,
|
|
294
|
+
"line_additions": line_additions,
|
|
295
|
+
"line_deletions": line_deletions,
|
|
296
|
+
"extension": os.path.splitext(path)[1] or None,
|
|
297
|
+
"size_bytes": self._size_bytes(after_content),
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
return self.client._request("POST", f"/api/runs/{self.run_id}/observations", payload)
|
|
302
|
+
|
|
303
|
+
def record_patch(
|
|
304
|
+
self,
|
|
305
|
+
path: str,
|
|
306
|
+
*,
|
|
307
|
+
diff_text: str | None = None,
|
|
308
|
+
before_content: Any | None = None,
|
|
309
|
+
after_content: Any | None = None,
|
|
310
|
+
before_hash: str | None = None,
|
|
311
|
+
after_hash: str | None = None,
|
|
312
|
+
step_id: str | None = None,
|
|
313
|
+
parent_step_id: str | None = None,
|
|
314
|
+
) -> dict[str, Any]:
|
|
315
|
+
generated_summary, line_additions, line_deletions = self._diff_details(before_content, after_content)
|
|
316
|
+
diff_summary = summarize_value(diff_text) if diff_text is not None else generated_summary
|
|
317
|
+
payload = {
|
|
318
|
+
"observations": [
|
|
319
|
+
{
|
|
320
|
+
"kind": "patch",
|
|
321
|
+
"step_id": step_id or self._step_id("patch"),
|
|
322
|
+
"parent_step_id": parent_step_id,
|
|
323
|
+
"path": path,
|
|
324
|
+
"before_hash": before_hash or (stable_hash(before_content) if before_content is not None else None),
|
|
325
|
+
"after_hash": after_hash or (stable_hash(after_content) if after_content is not None else None),
|
|
326
|
+
"diff_summary": diff_summary,
|
|
327
|
+
"line_additions": line_additions,
|
|
328
|
+
"line_deletions": line_deletions,
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
return self.client._request("POST", f"/api/runs/{self.run_id}/observations", payload)
|
|
333
|
+
|
|
334
|
+
def record_artifact(
|
|
335
|
+
self,
|
|
336
|
+
kind: str,
|
|
337
|
+
label: str,
|
|
338
|
+
summary: str,
|
|
339
|
+
*,
|
|
340
|
+
metadata: dict[str, Any] | None = None,
|
|
341
|
+
content: Any | None = None,
|
|
342
|
+
content_hash: str | None = None,
|
|
343
|
+
step_id: str | None = None,
|
|
344
|
+
parent_step_id: str | None = None,
|
|
345
|
+
) -> dict[str, Any]:
|
|
346
|
+
payload = {
|
|
347
|
+
"observations": [
|
|
348
|
+
{
|
|
349
|
+
"kind": "artifact",
|
|
350
|
+
"step_id": step_id or self._step_id("artifact"),
|
|
351
|
+
"parent_step_id": parent_step_id,
|
|
352
|
+
"artifact": {
|
|
353
|
+
"kind": kind,
|
|
354
|
+
"label": label,
|
|
355
|
+
"summary": summary,
|
|
356
|
+
"metadata": {
|
|
357
|
+
**(metadata or {}),
|
|
358
|
+
"content_hash": content_hash or (stable_hash(content) if content is not None else None),
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
return self.client._request("POST", f"/api/runs/{self.run_id}/observations", payload)
|
|
365
|
+
|
|
366
|
+
def tool(
|
|
367
|
+
self,
|
|
368
|
+
*,
|
|
369
|
+
name: str | None = None,
|
|
370
|
+
description: str | None = None,
|
|
371
|
+
redact_fields: list[str] | None = None,
|
|
372
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
373
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
374
|
+
tool_name = name or func.__name__
|
|
375
|
+
self.tools[tool_name] = ToolBinding(
|
|
376
|
+
func=func,
|
|
377
|
+
name=tool_name,
|
|
378
|
+
description=description or (inspect.getdoc(func) or f"Tool {tool_name}"),
|
|
379
|
+
redact_fields=redact_fields or [],
|
|
380
|
+
input_schema=self._build_input_schema(func),
|
|
381
|
+
)
|
|
382
|
+
return func
|
|
383
|
+
|
|
384
|
+
return decorator
|
|
385
|
+
|
|
386
|
+
def create_model_turn(
|
|
387
|
+
self,
|
|
388
|
+
messages: list[dict[str, Any]] | None,
|
|
389
|
+
*,
|
|
390
|
+
model: str,
|
|
391
|
+
provider: str = "openai",
|
|
392
|
+
tool_choice: str | dict[str, Any] = "auto",
|
|
393
|
+
) -> TurnResult:
|
|
394
|
+
if messages:
|
|
395
|
+
self.messages.extend(messages)
|
|
396
|
+
|
|
397
|
+
executed_tools: list[dict[str, Any]] = []
|
|
398
|
+
while True:
|
|
399
|
+
response = self.client._request(
|
|
400
|
+
"POST",
|
|
401
|
+
f"/api/runs/{self.run_id}/turns",
|
|
402
|
+
{
|
|
403
|
+
"provider": provider,
|
|
404
|
+
"model": model,
|
|
405
|
+
"messages": self.messages,
|
|
406
|
+
"tools": [self._tool_payload(tool) for tool in self.tools.values()],
|
|
407
|
+
"tool_choice": tool_choice,
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
if response["status"] == "message":
|
|
411
|
+
assistant_message = response["assistant_message"] or {"role": "assistant", "content": ""}
|
|
412
|
+
self.messages.append(assistant_message)
|
|
413
|
+
return TurnResult(assistant_message=assistant_message, executed_tools=executed_tools)
|
|
414
|
+
|
|
415
|
+
tool_calls = response["tool_calls"]
|
|
416
|
+
assistant_message = {
|
|
417
|
+
"role": "assistant",
|
|
418
|
+
"content": None,
|
|
419
|
+
"tool_calls": [
|
|
420
|
+
{
|
|
421
|
+
"id": call["id"],
|
|
422
|
+
"type": "function",
|
|
423
|
+
"function": {"name": call["name"], "arguments": json.dumps(call["arguments"])},
|
|
424
|
+
}
|
|
425
|
+
for call in tool_calls
|
|
426
|
+
],
|
|
427
|
+
}
|
|
428
|
+
self.messages.append(assistant_message)
|
|
429
|
+
results_payload = []
|
|
430
|
+
for call in tool_calls:
|
|
431
|
+
result_payload, tool_message = self._execute_tool(call)
|
|
432
|
+
executed_tools.append(result_payload)
|
|
433
|
+
results_payload.append(result_payload)
|
|
434
|
+
self.messages.append(tool_message)
|
|
435
|
+
|
|
436
|
+
self.client._request("POST", f"/api/runs/{self.run_id}/tool-results", {"results": results_payload})
|
|
437
|
+
|
|
438
|
+
def finish(self, final_output: Any | None = None, artifacts: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
439
|
+
return self.client._request(
|
|
440
|
+
"POST",
|
|
441
|
+
f"/api/runs/{self.run_id}/finish",
|
|
442
|
+
{"final_output": final_output, "artifacts": artifacts or []},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def fail(self, error_code: str, error_summary: str) -> dict[str, Any]:
|
|
446
|
+
return self.client._request(
|
|
447
|
+
"POST",
|
|
448
|
+
f"/api/runs/{self.run_id}/fail",
|
|
449
|
+
{"error_code": error_code, "error_summary": error_summary},
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def as_langchain_callback(
|
|
453
|
+
self,
|
|
454
|
+
*,
|
|
455
|
+
record_chain_events: bool = True,
|
|
456
|
+
record_retriever_events: bool = True,
|
|
457
|
+
) -> Any:
|
|
458
|
+
"""Return a LangChain callback handler bound to this run.
|
|
459
|
+
|
|
460
|
+
Usage:
|
|
461
|
+
callback = run.as_langchain_callback()
|
|
462
|
+
chain.invoke({...}, config={"callbacks": [callback]})
|
|
463
|
+
"""
|
|
464
|
+
from trace_agent_langchain import TraceAgentLangChainCallback
|
|
465
|
+
|
|
466
|
+
return TraceAgentLangChainCallback(
|
|
467
|
+
self,
|
|
468
|
+
record_chain_events=record_chain_events,
|
|
469
|
+
record_retriever_events=record_retriever_events,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def _execute_tool(self, call: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
473
|
+
binding = self.tools.get(call["name"])
|
|
474
|
+
if binding is None:
|
|
475
|
+
raise KeyError(f"Tool '{call['name']}' is not registered in this run.")
|
|
476
|
+
|
|
477
|
+
arguments = call.get("arguments") or {}
|
|
478
|
+
started_at = time.perf_counter()
|
|
479
|
+
try:
|
|
480
|
+
output = binding.func(**arguments)
|
|
481
|
+
duration_ms = int((time.perf_counter() - started_at) * 1000)
|
|
482
|
+
result_payload = {
|
|
483
|
+
"call_id": call["id"],
|
|
484
|
+
"name": binding.name,
|
|
485
|
+
"step_id": call["step_id"],
|
|
486
|
+
"status": "succeeded",
|
|
487
|
+
"args": arguments,
|
|
488
|
+
"output_summary": summarize_value(output, binding.redact_fields),
|
|
489
|
+
"output_hash": stable_hash(output),
|
|
490
|
+
"duration_ms": duration_ms,
|
|
491
|
+
"artifacts": [],
|
|
492
|
+
}
|
|
493
|
+
tool_message = {
|
|
494
|
+
"role": "tool",
|
|
495
|
+
"tool_call_id": call["id"],
|
|
496
|
+
"content": json.dumps(output, ensure_ascii=True, default=str),
|
|
497
|
+
}
|
|
498
|
+
return result_payload, tool_message
|
|
499
|
+
except Exception as exc:
|
|
500
|
+
duration_ms = int((time.perf_counter() - started_at) * 1000)
|
|
501
|
+
result_payload = {
|
|
502
|
+
"call_id": call["id"],
|
|
503
|
+
"name": binding.name,
|
|
504
|
+
"step_id": call["step_id"],
|
|
505
|
+
"status": "failed",
|
|
506
|
+
"args": arguments,
|
|
507
|
+
"output_summary": None,
|
|
508
|
+
"output_hash": stable_hash({"error": str(exc)}),
|
|
509
|
+
"error_code": type(exc).__name__,
|
|
510
|
+
"error_summary": str(exc),
|
|
511
|
+
"duration_ms": duration_ms,
|
|
512
|
+
"artifacts": [],
|
|
513
|
+
}
|
|
514
|
+
tool_message = {
|
|
515
|
+
"role": "tool",
|
|
516
|
+
"tool_call_id": call["id"],
|
|
517
|
+
"content": json.dumps({"error": str(exc)}, ensure_ascii=True),
|
|
518
|
+
}
|
|
519
|
+
return result_payload, tool_message
|
|
520
|
+
|
|
521
|
+
def _build_input_schema(self, func: Callable[..., Any]) -> dict[str, Any]:
|
|
522
|
+
signature = inspect.signature(func)
|
|
523
|
+
properties: dict[str, Any] = {}
|
|
524
|
+
required: list[str] = []
|
|
525
|
+
for parameter_name, parameter in signature.parameters.items():
|
|
526
|
+
annotation = parameter.annotation if parameter.annotation is not inspect._empty else str
|
|
527
|
+
properties[parameter_name] = {"type": TYPE_MAP.get(annotation, "string")}
|
|
528
|
+
if parameter.default is inspect._empty:
|
|
529
|
+
required.append(parameter_name)
|
|
530
|
+
return {"type": "object", "properties": properties, "required": required}
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _tool_payload(tool: ToolBinding) -> dict[str, Any]:
|
|
534
|
+
return {
|
|
535
|
+
"name": tool.name,
|
|
536
|
+
"description": tool.description,
|
|
537
|
+
"input_schema": tool.input_schema,
|
|
538
|
+
"redact_fields": tool.redact_fields,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
@staticmethod
|
|
542
|
+
def _render_command(command: str | list[str]) -> str:
|
|
543
|
+
if isinstance(command, str):
|
|
544
|
+
return command
|
|
545
|
+
return " ".join(str(part) for part in command)
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
def _step_id(prefix: str) -> str:
|
|
549
|
+
return f"{prefix}-{uuid4().hex[:8]}"
|
|
550
|
+
|
|
551
|
+
@staticmethod
|
|
552
|
+
def _size_bytes(value: Any | None) -> int | None:
|
|
553
|
+
if value is None:
|
|
554
|
+
return None
|
|
555
|
+
if isinstance(value, bytes):
|
|
556
|
+
return len(value)
|
|
557
|
+
return len(str(value).encode("utf-8"))
|
|
558
|
+
|
|
559
|
+
@staticmethod
|
|
560
|
+
def _diff_details(before_content: Any | None, after_content: Any | None) -> tuple[str | None, int | None, int | None]:
|
|
561
|
+
if before_content is None and after_content is None:
|
|
562
|
+
return None, None, None
|
|
563
|
+
|
|
564
|
+
before_lines = str(before_content or "").splitlines()
|
|
565
|
+
after_lines = str(after_content or "").splitlines()
|
|
566
|
+
diff_lines = list(difflib.unified_diff(before_lines, after_lines, lineterm="", n=1))
|
|
567
|
+
if not diff_lines:
|
|
568
|
+
return "No textual diff.", 0, 0
|
|
569
|
+
|
|
570
|
+
line_additions = sum(
|
|
571
|
+
1 for line in diff_lines if line.startswith("+") and not line.startswith("+++")
|
|
572
|
+
)
|
|
573
|
+
line_deletions = sum(
|
|
574
|
+
1 for line in diff_lines if line.startswith("-") and not line.startswith("---")
|
|
575
|
+
)
|
|
576
|
+
preview = "\n".join(diff_lines[:10])
|
|
577
|
+
return summarize_value(preview), line_additions, line_deletions
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class TraceAgentClient:
|
|
581
|
+
def __init__(
|
|
582
|
+
self,
|
|
583
|
+
base_url: str,
|
|
584
|
+
api_key: str | None = None,
|
|
585
|
+
timeout: float = 30.0,
|
|
586
|
+
http_client: httpx.Client | None = None,
|
|
587
|
+
) -> None:
|
|
588
|
+
if http_client is not None:
|
|
589
|
+
self.http = http_client
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
headers = {"Content-Type": "application/json"}
|
|
593
|
+
if api_key:
|
|
594
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
595
|
+
self.http = httpx.Client(base_url=base_url.rstrip("/"), headers=headers, timeout=timeout)
|
|
596
|
+
|
|
597
|
+
def start_run(self, agent_name: str, goal: str, metadata: dict[str, Any] | None = None) -> RunSession:
|
|
598
|
+
response = self._request("POST", "/api/runs", {"agent_name": agent_name, "goal": goal, "metadata": metadata or {}})
|
|
599
|
+
return RunSession(self, response["run_id"], agent_name, goal, metadata)
|
|
600
|
+
|
|
601
|
+
def _request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
602
|
+
response = self.http.request(method, path, json=payload)
|
|
603
|
+
if response.is_error:
|
|
604
|
+
detail = response.text
|
|
605
|
+
try:
|
|
606
|
+
body = response.json()
|
|
607
|
+
if isinstance(body, dict):
|
|
608
|
+
detail = body.get("detail", detail)
|
|
609
|
+
except Exception:
|
|
610
|
+
pass
|
|
611
|
+
raise httpx.HTTPStatusError(
|
|
612
|
+
f"{response.status_code} for {path}: {detail}",
|
|
613
|
+
request=response.request,
|
|
614
|
+
response=response,
|
|
615
|
+
)
|
|
616
|
+
return response.json()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def stable_hash(value: Any) -> str:
|
|
9
|
+
raw = json.dumps(value, ensure_ascii=True, sort_keys=True, default=str)
|
|
10
|
+
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def redact_payload(value: Any, redact_fields: list[str] | None = None) -> Any:
|
|
14
|
+
redact_fields = set(redact_fields or [])
|
|
15
|
+
if isinstance(value, dict):
|
|
16
|
+
redacted: dict[str, Any] = {}
|
|
17
|
+
for key, inner in value.items():
|
|
18
|
+
if key in redact_fields:
|
|
19
|
+
redacted[key] = "[redacted]"
|
|
20
|
+
else:
|
|
21
|
+
redacted[key] = redact_payload(inner, list(redact_fields))
|
|
22
|
+
return redacted
|
|
23
|
+
if isinstance(value, list):
|
|
24
|
+
return [redact_payload(item, list(redact_fields)) for item in value]
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
if len(value) <= 160:
|
|
27
|
+
return value
|
|
28
|
+
return f"{value[:157]}..."
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def summarize_value(value: Any, redact_fields: list[str] | None = None, max_length: int = 220) -> str:
|
|
33
|
+
redacted = redact_payload(value, redact_fields)
|
|
34
|
+
if isinstance(redacted, str):
|
|
35
|
+
return redacted
|
|
36
|
+
rendered = json.dumps(redacted, ensure_ascii=True, sort_keys=True, default=str)
|
|
37
|
+
if len(rendered) <= max_length:
|
|
38
|
+
return rendered
|
|
39
|
+
return f"{rendered[: max_length - 3]}..."
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trace-agent-sdk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: TraceAgent SDK for instrumenting AI agents.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx<1.0.0,>=0.27.0
|
|
7
|
+
Requires-Dist: pydantic<3.0.0,>=2.8.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest<9.0.0,>=8.3.2; extra == "dev"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
trace_agent_sdk/__init__.py,sha256=hjbc5GVdbWzhsw6auG-Z1uCzRyrMxIix1XuUkLo1OEw,148
|
|
2
|
+
trace_agent_sdk/client.py,sha256=HmJUgltUHoLkbVU3_Jtnxu44vci27CqBcu_1NCpIqbg,22634
|
|
3
|
+
trace_agent_sdk/redaction.py,sha256=tkhegqsHGJ8It9oNRS5Q2hRITc7XsapC70GTmWGhV4I,1347
|
|
4
|
+
trace_agent_sdk-0.0.1.dist-info/METADATA,sha256=Ld4KgeAcubvpF4HZd4jW57vw8oQbtfMWODd54joAajo,282
|
|
5
|
+
trace_agent_sdk-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
trace_agent_sdk-0.0.1.dist-info/top_level.txt,sha256=k4tULAp1RyGZKzE8cYiNi1HKfE263zHOaYE0RYp3cx0,16
|
|
7
|
+
trace_agent_sdk-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trace_agent_sdk
|