nova-agentops 1.3.0__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.
agent_ops/__init__.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""AgentOps SDK – observability client for AI agents."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
|
|
15
|
+
load_dotenv()
|
|
16
|
+
|
|
17
|
+
__version__ = "1.3.0"
|
|
18
|
+
__all__ = ["AgentOps", "AgentOpsSync", "AgentOpsError"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentOpsError(Exception):
|
|
22
|
+
"""Raised when the AgentOps backend returns an error."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _redact(obj: Any, keys: List[str]) -> Any:
|
|
26
|
+
if isinstance(obj, dict):
|
|
27
|
+
return {k: ("[REDACTED]" if k in keys else _redact(v, keys)) for k, v in obj.items()}
|
|
28
|
+
if isinstance(obj, list):
|
|
29
|
+
return [_redact(x, keys) for x in obj]
|
|
30
|
+
return obj
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentOps:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: Optional[str] = None,
|
|
37
|
+
api_url: Optional[str] = None,
|
|
38
|
+
timeout: float = 10.0,
|
|
39
|
+
max_retries: int = 3,
|
|
40
|
+
transport: Optional[Any] = None,
|
|
41
|
+
sample_rate: float = 1.0,
|
|
42
|
+
redact_keys: Optional[List[str]] = None,
|
|
43
|
+
offline_path: Optional[str] = None,
|
|
44
|
+
):
|
|
45
|
+
self.api_key = api_key or os.getenv("AGENT_OPS_API_KEY")
|
|
46
|
+
self.api_url = (api_url or os.getenv("AGENT_OPS_URL", "http://127.0.0.1:8001")).rstrip("/")
|
|
47
|
+
self.timeout = timeout
|
|
48
|
+
self.max_retries = max_retries
|
|
49
|
+
self.transport = transport
|
|
50
|
+
self.sample_rate = max(0.0, min(1.0, sample_rate))
|
|
51
|
+
self.redact_keys = redact_keys or []
|
|
52
|
+
self.offline_path = Path(offline_path or os.getenv("AGENT_OPS_OFFLINE_PATH", str(Path.home() / ".agentops" / "queue.jsonl")))
|
|
53
|
+
self.current_trace_id: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
def _headers(self) -> Dict[str, str]:
|
|
56
|
+
headers = {"Content-Type": "application/json"}
|
|
57
|
+
if self.api_key:
|
|
58
|
+
headers["X-API-Key"] = self.api_key
|
|
59
|
+
return headers
|
|
60
|
+
|
|
61
|
+
async def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
62
|
+
url = f"{self.api_url}{path}"
|
|
63
|
+
last_exc: Optional[Exception] = None
|
|
64
|
+
for attempt in range(1, self.max_retries + 1):
|
|
65
|
+
try:
|
|
66
|
+
async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as client:
|
|
67
|
+
response = await client.request(method, url, headers=self._headers(), **kwargs)
|
|
68
|
+
if response.status_code >= 500:
|
|
69
|
+
last_exc = AgentOpsError(f"Server error {response.status_code}: {response.text}")
|
|
70
|
+
await asyncio.sleep((0.2 * attempt) + random.random() * 0.1)
|
|
71
|
+
continue
|
|
72
|
+
return response
|
|
73
|
+
except (httpx.TimeoutException, httpx.ConnectError, httpx.NetworkError) as exc:
|
|
74
|
+
last_exc = exc
|
|
75
|
+
await asyncio.sleep((0.2 * attempt) + random.random() * 0.1)
|
|
76
|
+
if last_exc is not None:
|
|
77
|
+
raise AgentOpsError(str(last_exc))
|
|
78
|
+
raise AgentOpsError("Request failed")
|
|
79
|
+
|
|
80
|
+
def _enqueue_offline(self, method: str, path: str, payload: Any):
|
|
81
|
+
self.offline_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
with self.offline_path.open("a") as f:
|
|
83
|
+
f.write(json.dumps({"method": method, "path": path, "payload": payload}) + "\n")
|
|
84
|
+
|
|
85
|
+
async def _send_or_queue(self, method: str, path: str, **kwargs) -> Optional[httpx.Response]:
|
|
86
|
+
try:
|
|
87
|
+
return await self._request(method, path, **kwargs)
|
|
88
|
+
except AgentOpsError:
|
|
89
|
+
payload = kwargs.get("json", kwargs.get("params"))
|
|
90
|
+
self._enqueue_offline(method, path, payload)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
async def flush(self) -> int:
|
|
94
|
+
"""Replay queued offline events. Returns number flushed."""
|
|
95
|
+
if not self.offline_path.exists():
|
|
96
|
+
return 0
|
|
97
|
+
lines = self.offline_path.read_text().splitlines()
|
|
98
|
+
if not lines:
|
|
99
|
+
return 0
|
|
100
|
+
flushed = 0
|
|
101
|
+
remaining = []
|
|
102
|
+
for line in lines:
|
|
103
|
+
try:
|
|
104
|
+
ev = json.loads(line)
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
resp = await self._request(ev["method"], ev["path"], json=ev["payload"])
|
|
109
|
+
if resp.status_code < 300:
|
|
110
|
+
flushed += 1
|
|
111
|
+
else:
|
|
112
|
+
remaining.append(line)
|
|
113
|
+
except AgentOpsError:
|
|
114
|
+
remaining.append(line)
|
|
115
|
+
self.offline_path.write_text("\n".join(remaining) + ("\n" if remaining else ""))
|
|
116
|
+
return flushed
|
|
117
|
+
|
|
118
|
+
async def start_trace(self, session_name: str, metadata: Optional[Dict[str, Any]] = None) -> str:
|
|
119
|
+
response = await self._send_or_queue("POST", "/api/trace", json={"session_name": session_name, "metadata": metadata or {}})
|
|
120
|
+
if response is None:
|
|
121
|
+
raise AgentOpsError("Backend unavailable; trace queued offline")
|
|
122
|
+
if response.status_code not in (200, 201):
|
|
123
|
+
raise AgentOpsError(f"Error starting trace: {response.text}")
|
|
124
|
+
data = response.json()
|
|
125
|
+
self.current_trace_id = data["id"]
|
|
126
|
+
return self.current_trace_id
|
|
127
|
+
|
|
128
|
+
async def log_step(
|
|
129
|
+
self,
|
|
130
|
+
thought: Optional[str] = None,
|
|
131
|
+
action: Optional[str] = None,
|
|
132
|
+
observation: Optional[str] = None,
|
|
133
|
+
state_snapshot: Optional[Dict[str, Any]] = None,
|
|
134
|
+
trace_id: Optional[str] = None,
|
|
135
|
+
prompt_tokens: int = 0,
|
|
136
|
+
completion_tokens: int = 0,
|
|
137
|
+
model: Optional[str] = None,
|
|
138
|
+
cost_usd: float = 0.0,
|
|
139
|
+
usage: Optional[Dict[str, Any]] = None,
|
|
140
|
+
) -> Optional[Dict[str, Any]]:
|
|
141
|
+
if random.random() > self.sample_rate:
|
|
142
|
+
return None
|
|
143
|
+
tid = trace_id or self.current_trace_id
|
|
144
|
+
if not tid:
|
|
145
|
+
raise AgentOpsError("No active trace found. Call start_trace() first.")
|
|
146
|
+
if usage:
|
|
147
|
+
prompt_tokens = usage.get("prompt_tokens", prompt_tokens)
|
|
148
|
+
completion_tokens = usage.get("completion_tokens", completion_tokens)
|
|
149
|
+
model = usage.get("model", model)
|
|
150
|
+
cost_usd = usage.get("cost_usd", cost_usd)
|
|
151
|
+
snapshot = _redact(state_snapshot or {}, self.redact_keys)
|
|
152
|
+
payload = {
|
|
153
|
+
"trace_id": tid,
|
|
154
|
+
"thought": thought,
|
|
155
|
+
"action": action,
|
|
156
|
+
"observation": observation,
|
|
157
|
+
"state_snapshot": snapshot,
|
|
158
|
+
"prompt_tokens": prompt_tokens,
|
|
159
|
+
"completion_tokens": completion_tokens,
|
|
160
|
+
"model": model,
|
|
161
|
+
"cost_usd": cost_usd,
|
|
162
|
+
}
|
|
163
|
+
response = await self._send_or_queue("POST", "/api/log", json=payload)
|
|
164
|
+
if response is None:
|
|
165
|
+
return None
|
|
166
|
+
if response.status_code not in (200, 201):
|
|
167
|
+
raise AgentOpsError(f"Error logging step: {response.text}")
|
|
168
|
+
return response.json()
|
|
169
|
+
|
|
170
|
+
async def log_steps_batch(self, items: List[Dict[str, Any]]) -> Optional[List[Dict[str, Any]]]:
|
|
171
|
+
redacted = []
|
|
172
|
+
for it in items:
|
|
173
|
+
redacted.append({
|
|
174
|
+
"trace_id": it["trace_id"],
|
|
175
|
+
"thought": it.get("thought"),
|
|
176
|
+
"action": it.get("action"),
|
|
177
|
+
"observation": it.get("observation"),
|
|
178
|
+
"state_snapshot": _redact(it.get("state_snapshot") or {}, self.redact_keys),
|
|
179
|
+
})
|
|
180
|
+
response = await self._send_or_queue("POST", "/api/logs/batch", json={"steps": redacted})
|
|
181
|
+
if response is None:
|
|
182
|
+
return None
|
|
183
|
+
if response.status_code not in (200, 201):
|
|
184
|
+
raise AgentOpsError(f"Error logging batch: {response.text}")
|
|
185
|
+
return response.json()
|
|
186
|
+
|
|
187
|
+
async def end_trace(self, trace_id: Optional[str] = None, status: str = "completed") -> Optional[Dict[str, Any]]:
|
|
188
|
+
tid = trace_id or self.current_trace_id
|
|
189
|
+
if not tid:
|
|
190
|
+
raise AgentOpsError("No active trace found.")
|
|
191
|
+
response = await self._send_or_queue("POST", f"/api/trace/{tid}/end", json={"status": status})
|
|
192
|
+
if response is None:
|
|
193
|
+
return None
|
|
194
|
+
if response.status_code not in (200, 201):
|
|
195
|
+
raise AgentOpsError(f"Error ending trace: {response.text}")
|
|
196
|
+
if tid == self.current_trace_id:
|
|
197
|
+
self.current_trace_id = None
|
|
198
|
+
return response.json()
|
|
199
|
+
|
|
200
|
+
async def get_trace(self, trace_id: str) -> Dict[str, Any]:
|
|
201
|
+
response = await self._request("GET", f"/api/traces/{trace_id}")
|
|
202
|
+
if response.status_code != 200:
|
|
203
|
+
raise AgentOpsError(f"Error fetching trace: {response.text}")
|
|
204
|
+
return response.json()
|
|
205
|
+
|
|
206
|
+
async def list_traces(self, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
|
|
207
|
+
response = await self._request("GET", "/api/traces", params={"limit": limit, "offset": offset})
|
|
208
|
+
if response.status_code != 200:
|
|
209
|
+
raise AgentOpsError(f"Error listing traces: {response.text}")
|
|
210
|
+
return response.json()
|
|
211
|
+
|
|
212
|
+
def trace(self, session_name: str, metadata: Optional[Dict[str, Any]] = None):
|
|
213
|
+
return _TraceContext(self, session_name, metadata)
|
|
214
|
+
|
|
215
|
+
def track(self, func: Optional[Callable] = None):
|
|
216
|
+
def decorator(f: Callable):
|
|
217
|
+
@functools.wraps(f)
|
|
218
|
+
async def wrapper(*args, **kwargs):
|
|
219
|
+
args_repr = [repr(a) for a in args]
|
|
220
|
+
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
|
|
221
|
+
input_str = ", ".join(args_repr + kwargs_repr)
|
|
222
|
+
try:
|
|
223
|
+
await self.log_step(
|
|
224
|
+
thought=f"Executing function {f.__name__} with inputs: {input_str}",
|
|
225
|
+
action=f.__name__,
|
|
226
|
+
)
|
|
227
|
+
except Exception as e: # noqa: BLE001
|
|
228
|
+
print(f"AgentOps Logging Error (Intent): {e}")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
result = await f(*args, **kwargs)
|
|
232
|
+
await self.log_step(action=f.__name__, observation=str(result))
|
|
233
|
+
return result
|
|
234
|
+
except Exception as e: # noqa: BLE001
|
|
235
|
+
try:
|
|
236
|
+
await self.log_step(action=f.__name__, observation=f"ERROR: {str(e)}")
|
|
237
|
+
except Exception as log_e: # noqa: BLE001
|
|
238
|
+
print(f"AgentOps Logging Error (Error): {log_e}")
|
|
239
|
+
raise
|
|
240
|
+
|
|
241
|
+
return wrapper
|
|
242
|
+
|
|
243
|
+
if func is None:
|
|
244
|
+
return decorator
|
|
245
|
+
return decorator(func)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class _TraceContext:
|
|
249
|
+
def __init__(self, ops: AgentOps, session_name: str, metadata: Optional[Dict[str, Any]]):
|
|
250
|
+
self.ops = ops
|
|
251
|
+
self.session_name = session_name
|
|
252
|
+
self.metadata = metadata
|
|
253
|
+
self.trace_id: Optional[str] = None
|
|
254
|
+
|
|
255
|
+
async def __aenter__(self) -> str:
|
|
256
|
+
self.trace_id = await self.ops.start_trace(self.session_name, self.metadata)
|
|
257
|
+
self.ops.current_trace_id = self.trace_id
|
|
258
|
+
return self.trace_id
|
|
259
|
+
|
|
260
|
+
async def __aexit__(self, exc_type, exc, tb) -> bool:
|
|
261
|
+
try:
|
|
262
|
+
await self.ops.end_trace(self.trace_id, status="error" if exc else "completed")
|
|
263
|
+
await self.ops.flush()
|
|
264
|
+
except Exception: # noqa: BLE001
|
|
265
|
+
pass
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class AgentOpsSync:
|
|
270
|
+
"""Synchronous wrapper around AgentOps."""
|
|
271
|
+
|
|
272
|
+
def __init__(self, *args, **kwargs):
|
|
273
|
+
self._async = AgentOps(*args, **kwargs)
|
|
274
|
+
|
|
275
|
+
def start_trace(self, *a, **kw):
|
|
276
|
+
return asyncio.run(self._async.start_trace(*a, **kw))
|
|
277
|
+
|
|
278
|
+
def log_step(self, *a, **kw):
|
|
279
|
+
return asyncio.run(self._async.log_step(*a, **kw))
|
|
280
|
+
|
|
281
|
+
def end_trace(self, *a, **kw):
|
|
282
|
+
return asyncio.run(self._async.end_trace(*a, **kw))
|
|
283
|
+
|
|
284
|
+
def flush(self):
|
|
285
|
+
return asyncio.run(self._async.flush())
|
|
286
|
+
|
|
287
|
+
def trace(self, *a, **kw):
|
|
288
|
+
return self._async.trace(*a, **kw)
|
|
289
|
+
|
|
290
|
+
def track(self, *a, **kw):
|
|
291
|
+
return self._async.track(*a, **kw)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Optional framework integrations for AgentOps."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from langchain_core.callbacks import BaseCallbackHandler as _LCBase
|
|
5
|
+
except Exception: # pragma: no cover - optional dependency
|
|
6
|
+
_LCBase = object
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LangChainCallbackHandler(_LCBase):
|
|
10
|
+
"""LangChain callback handler that forwards tool events to AgentOps.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from agent_ops.integrations import LangChainCallbackHandler
|
|
14
|
+
handler = LangChainCallbackHandler(ops)
|
|
15
|
+
agent.invoke({...}, config={"callbacks": [handler]})
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, ops, trace_name: str = "langchain-run"):
|
|
19
|
+
self.ops = ops
|
|
20
|
+
self.trace_name = trace_name
|
|
21
|
+
self._trace_started = False
|
|
22
|
+
|
|
23
|
+
def on_chain_start(self, *args, **kwargs):
|
|
24
|
+
if not self._trace_started:
|
|
25
|
+
import asyncio
|
|
26
|
+
asyncio.get_event_loop().create_task(self.ops.start_trace(self.trace_name))
|
|
27
|
+
self._trace_started = True
|
|
28
|
+
|
|
29
|
+
def on_tool_start(self, serialized, input_str, **kwargs):
|
|
30
|
+
import asyncio
|
|
31
|
+
asyncio.get_event_loop().create_task(
|
|
32
|
+
self.ops.log_step(thought=f"Tool {serialized.get('name')} started", action=serialized.get("name"), state_snapshot={"input": input_str})
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def on_tool_end(self, output, **kwargs):
|
|
36
|
+
import asyncio
|
|
37
|
+
asyncio.get_event_loop().create_task(
|
|
38
|
+
self.ops.log_step(observation=str(output))
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def on_chain_end(self, *args, **kwargs):
|
|
42
|
+
import asyncio
|
|
43
|
+
asyncio.get_event_loop().create_task(self.ops.end_trace())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def openai_tool_wrapper(ops, func):
|
|
47
|
+
"""Wrap an OpenAI tool function so its calls are traced."""
|
|
48
|
+
import asyncio
|
|
49
|
+
import functools
|
|
50
|
+
|
|
51
|
+
@functools.wraps(func)
|
|
52
|
+
async def wrapped(*args, **kwargs):
|
|
53
|
+
await ops.log_step(thought=f"Calling {func.__name__}", action=func.__name__, state_snapshot={"args": args, "kwargs": kwargs})
|
|
54
|
+
if asyncio.iscoroutinefunction(func):
|
|
55
|
+
result = await func(*args, **kwargs)
|
|
56
|
+
else:
|
|
57
|
+
result = func(*args, **kwargs)
|
|
58
|
+
await ops.log_step(action=func.__name__, observation=str(result))
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
return wrapped
|
agent_ops/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nova-agentops
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Professional observability SDK for AI Agents
|
|
5
|
+
Author-email: Terry <terry@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Nova799/agent-ops-sdk
|
|
8
|
+
Keywords: ai,observability,agents,tracing,llm
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: httpx>=0.24.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
20
|
+
Provides-Extra: langchain
|
|
21
|
+
Requires-Dist: langchain-core>=0.2; extra == "langchain"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# 🚀 AgentOps SDK
|
|
27
|
+
|
|
28
|
+
**AgentOps** is a professional observability platform designed specifically for AI Agents. It acts as the "Chrome DevTools" for your LLM-powered applications, allowing you to monitor, debug, and optimize your agent's reasoning process in real-time.
|
|
29
|
+
|
|
30
|
+
## 🌟 Why AgentOps?
|
|
31
|
+
|
|
32
|
+
When building AI agents, the biggest challenge is the "Black Box" effect: you see the input and the output, but you don't know *why* the agent made a specific decision or where it got stuck.
|
|
33
|
+
|
|
34
|
+
AgentOps solves this by capturing:
|
|
35
|
+
- **The Internal Monologue**: See the exact reasoning (Thought) before an action.
|
|
36
|
+
- **Tool Execution**: Track exactly which tool was called and with what parameters.
|
|
37
|
+
- **State Snapshots**: Inspect the internal memory of your agent at any given step.
|
|
38
|
+
- **Visual Tracing**: A dynamic graph visualization of the agent's logic flow.
|
|
39
|
+
|
|
40
|
+
## 🛠️ Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Installation
|
|
43
|
+
|
|
44
|
+
You can install the SDK directly from this GitHub repository:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install git+https://github.com/Nova799/agent-ops-sdk.git
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Configuration
|
|
51
|
+
Create a `.env` file in your project root:
|
|
52
|
+
```env
|
|
53
|
+
AGENT_OPS_API_KEY=your_secret_key
|
|
54
|
+
AGENT_OPS_URL=https://api.agentops.io
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Basic Usage
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
from agent_ops import AgentOps
|
|
61
|
+
|
|
62
|
+
ops = AgentOps()
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
# Context manager handles start/end automatically
|
|
66
|
+
async with ops.trace(session_name="User Inquiry #123", metadata={"user": "alice"}):
|
|
67
|
+
await ops.log_step(
|
|
68
|
+
thought="The user is asking for the weather, I should use the weather tool",
|
|
69
|
+
action="get_weather",
|
|
70
|
+
observation="Sunny, 25°C",
|
|
71
|
+
state_snapshot={"location": "Paris"},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Automatic tracker
|
|
75
|
+
@ops.track()
|
|
76
|
+
async def my_tool(param):
|
|
77
|
+
return f"Processed {param}"
|
|
78
|
+
|
|
79
|
+
await ops.start_trace(session_name="tool run")
|
|
80
|
+
await my_tool("hello")
|
|
81
|
+
await ops.end_trace()
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Inspect traces
|
|
87
|
+
```python
|
|
88
|
+
traces = await ops.list_traces(limit=10)
|
|
89
|
+
detail = await ops.get_trace(traces[0]["id"])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 5. Sync client
|
|
93
|
+
```python
|
|
94
|
+
from agent_ops import AgentOpsSync
|
|
95
|
+
ops = AgentOpsSync()
|
|
96
|
+
ops.start_trace("demo")
|
|
97
|
+
ops.log_step(thought="hi")
|
|
98
|
+
ops.end_trace()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 6. Sampling & redaction
|
|
102
|
+
```python
|
|
103
|
+
ops = AgentOps(sample_rate=0.1, redact_keys=["password", "token"])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 📊 The Dashboard
|
|
107
|
+
Once integrated, open the AgentOps Dashboard to:
|
|
108
|
+
- **Visualize** the execution flow as an interactive node graph.
|
|
109
|
+
- **Inspect** detailed logs of every step.
|
|
110
|
+
- **Share** traces with your team for collaborative debugging.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
© 2026 AgentOps Team. Built for the future of AI Agents.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agent_ops/__init__.py,sha256=kEr_yJbns-s0PzTpsPa6YTbvenJD5Aeh-PIG-tm0xuI,11614
|
|
2
|
+
agent_ops/integrations.py,sha256=MMySWdevgo1PO9ZtdO_zlbLgyXVGITPEXw5weXJWyGw,2140
|
|
3
|
+
agent_ops/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
nova_agentops-1.3.0.dist-info/METADATA,sha256=oYl9zLLxikBR0LjvTFO6kpgxiR3U1yvcz2YDUTBAKn4,3558
|
|
5
|
+
nova_agentops-1.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
nova_agentops-1.3.0.dist-info/top_level.txt,sha256=_7kmbmMSEKvT9fYv98vQE1Cq3Qc7kDz3oiCJ1BMWpOM,10
|
|
7
|
+
nova_agentops-1.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent_ops
|