agentguard47 0.2.0__py3-none-any.whl → 0.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.
- agentguard/__init__.py +4 -0
- agentguard/cli.py +20 -0
- agentguard/evaluation.py +220 -0
- agentguard/instrument.py +183 -0
- agentguard/integrations/langchain.py +1 -1
- agentguard/viewer.py +189 -57
- {agentguard47-0.2.0.dist-info → agentguard47-0.3.0.dist-info}/METADATA +46 -2
- agentguard47-0.3.0.dist-info/RECORD +17 -0
- agentguard47-0.2.0.dist-info/RECORD +0 -15
- {agentguard47-0.2.0.dist-info → agentguard47-0.3.0.dist-info}/WHEEL +0 -0
- {agentguard47-0.2.0.dist-info → agentguard47-0.3.0.dist-info}/entry_points.txt +0 -0
- {agentguard47-0.2.0.dist-info → agentguard47-0.3.0.dist-info}/top_level.txt +0 -0
agentguard/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .guards import (
|
|
|
9
9
|
)
|
|
10
10
|
from .recording import Recorder, Replayer
|
|
11
11
|
from .sinks import HttpSink
|
|
12
|
+
from .evaluation import EvalSuite, EvalResult, AssertionResult
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"Tracer",
|
|
@@ -21,4 +22,7 @@ __all__ = [
|
|
|
21
22
|
"Recorder",
|
|
22
23
|
"Replayer",
|
|
23
24
|
"HttpSink",
|
|
25
|
+
"EvalSuite",
|
|
26
|
+
"EvalResult",
|
|
27
|
+
"AssertionResult",
|
|
24
28
|
]
|
agentguard/cli.py
CHANGED
|
@@ -81,6 +81,21 @@ def _report(path: str) -> None:
|
|
|
81
81
|
print(" Loop guard triggered: 0")
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def _eval(path: str) -> None:
|
|
85
|
+
from agentguard.evaluation import EvalSuite
|
|
86
|
+
|
|
87
|
+
result = (
|
|
88
|
+
EvalSuite(path)
|
|
89
|
+
.assert_no_loops()
|
|
90
|
+
.assert_no_errors()
|
|
91
|
+
.assert_completes_within(30.0)
|
|
92
|
+
.run()
|
|
93
|
+
)
|
|
94
|
+
print(result.summary)
|
|
95
|
+
if not result.passed:
|
|
96
|
+
raise SystemExit(1)
|
|
97
|
+
|
|
98
|
+
|
|
84
99
|
def main() -> None:
|
|
85
100
|
parser = argparse.ArgumentParser(prog="agentguard")
|
|
86
101
|
sub = parser.add_subparsers(dest="cmd")
|
|
@@ -96,6 +111,9 @@ def main() -> None:
|
|
|
96
111
|
view.add_argument("--port", type=int, default=8080)
|
|
97
112
|
view.add_argument("--no-open", action="store_true")
|
|
98
113
|
|
|
114
|
+
eval_cmd = sub.add_parser("eval", help="Run evaluation assertions on a trace")
|
|
115
|
+
eval_cmd.add_argument("path")
|
|
116
|
+
|
|
99
117
|
args = parser.parse_args()
|
|
100
118
|
if args.cmd == "summarize":
|
|
101
119
|
_summarize(args.path)
|
|
@@ -105,6 +123,8 @@ def main() -> None:
|
|
|
105
123
|
from agentguard.viewer import serve
|
|
106
124
|
|
|
107
125
|
serve(args.path, port=args.port, open_browser=not args.no_open)
|
|
126
|
+
elif args.cmd == "eval":
|
|
127
|
+
_eval(args.path)
|
|
108
128
|
else:
|
|
109
129
|
parser.print_help()
|
|
110
130
|
|
agentguard/evaluation.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Evaluation as Code — assertion-based trace analysis."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AssertionResult:
|
|
11
|
+
name: str
|
|
12
|
+
passed: bool
|
|
13
|
+
message: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class EvalResult:
|
|
18
|
+
assertions: List[AssertionResult] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def passed(self) -> bool:
|
|
22
|
+
return all(a.passed for a in self.assertions)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def summary(self) -> str:
|
|
26
|
+
total = len(self.assertions)
|
|
27
|
+
passed = sum(1 for a in self.assertions if a.passed)
|
|
28
|
+
failed = total - passed
|
|
29
|
+
lines = [f"EvalResult: {passed}/{total} passed, {failed} failed"]
|
|
30
|
+
for a in self.assertions:
|
|
31
|
+
status = "PASS" if a.passed else "FAIL"
|
|
32
|
+
lines.append(f" [{status}] {a.name}: {a.message}")
|
|
33
|
+
return "\n".join(lines)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EvalSuite:
|
|
37
|
+
"""Load a trace from JSONL and run assertions against it."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, path: str) -> None:
|
|
40
|
+
self._events = _load_events(path)
|
|
41
|
+
self._assertions: List[_Assertion] = []
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def events(self) -> List[Dict[str, Any]]:
|
|
45
|
+
return list(self._events)
|
|
46
|
+
|
|
47
|
+
def assert_no_loops(self) -> "EvalSuite":
|
|
48
|
+
"""Assert that no loop guard events were recorded."""
|
|
49
|
+
self._assertions.append(_Assertion(
|
|
50
|
+
name="no_loops",
|
|
51
|
+
check=_check_no_loops,
|
|
52
|
+
))
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def assert_tool_called(self, name: str, min_times: int = 1) -> "EvalSuite":
|
|
56
|
+
"""Assert a tool was called at least min_times."""
|
|
57
|
+
self._assertions.append(_Assertion(
|
|
58
|
+
name=f"tool_called:{name}>={min_times}",
|
|
59
|
+
check=lambda events, n=name, m=min_times: _check_tool_called(events, n, m),
|
|
60
|
+
))
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def assert_budget_under(self, tokens: Optional[int] = None, calls: Optional[int] = None) -> "EvalSuite":
|
|
64
|
+
"""Assert total token/call usage is under a limit."""
|
|
65
|
+
label_parts = []
|
|
66
|
+
if tokens is not None:
|
|
67
|
+
label_parts.append(f"tokens<{tokens}")
|
|
68
|
+
if calls is not None:
|
|
69
|
+
label_parts.append(f"calls<{calls}")
|
|
70
|
+
self._assertions.append(_Assertion(
|
|
71
|
+
name=f"budget_under:{','.join(label_parts)}",
|
|
72
|
+
check=lambda events, t=tokens, c=calls: _check_budget_under(events, t, c),
|
|
73
|
+
))
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def assert_completes_within(self, seconds: float) -> "EvalSuite":
|
|
77
|
+
"""Assert the longest span completed within a time limit."""
|
|
78
|
+
self._assertions.append(_Assertion(
|
|
79
|
+
name=f"completes_within:{seconds}s",
|
|
80
|
+
check=lambda events, s=seconds: _check_completes_within(events, s),
|
|
81
|
+
))
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def assert_event_exists(self, name: str) -> "EvalSuite":
|
|
85
|
+
"""Assert that at least one event with the given name exists."""
|
|
86
|
+
self._assertions.append(_Assertion(
|
|
87
|
+
name=f"event_exists:{name}",
|
|
88
|
+
check=lambda events, n=name: _check_event_exists(events, n),
|
|
89
|
+
))
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def assert_no_errors(self) -> "EvalSuite":
|
|
93
|
+
"""Assert no events have error data."""
|
|
94
|
+
self._assertions.append(_Assertion(
|
|
95
|
+
name="no_errors",
|
|
96
|
+
check=_check_no_errors,
|
|
97
|
+
))
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def run(self) -> EvalResult:
|
|
101
|
+
result = EvalResult()
|
|
102
|
+
for assertion in self._assertions:
|
|
103
|
+
ar = assertion.check(self._events)
|
|
104
|
+
result.assertions.append(ar)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class _Assertion:
|
|
110
|
+
name: str
|
|
111
|
+
check: Any # Callable[[List[Dict]], AssertionResult]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- check functions ---
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _check_no_loops(events: List[Dict[str, Any]]) -> AssertionResult:
|
|
118
|
+
loop_events = [e for e in events if e.get("name") == "guard.loop_detected"]
|
|
119
|
+
if loop_events:
|
|
120
|
+
return AssertionResult(
|
|
121
|
+
name="no_loops",
|
|
122
|
+
passed=False,
|
|
123
|
+
message=f"Found {len(loop_events)} loop detection event(s)",
|
|
124
|
+
)
|
|
125
|
+
return AssertionResult(name="no_loops", passed=True, message="No loops detected")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _check_tool_called(events: List[Dict[str, Any]], tool_name: str, min_times: int) -> AssertionResult:
|
|
129
|
+
name = f"tool_called:{tool_name}>={min_times}"
|
|
130
|
+
# Count tool.result events or span events with matching tool name
|
|
131
|
+
count = 0
|
|
132
|
+
for e in events:
|
|
133
|
+
ename = e.get("name", "")
|
|
134
|
+
if ename == "tool.result":
|
|
135
|
+
count += 1
|
|
136
|
+
elif ename.startswith(f"tool.{tool_name}"):
|
|
137
|
+
if e.get("phase") == "start" or e.get("kind") == "event":
|
|
138
|
+
count += 1
|
|
139
|
+
if count >= min_times:
|
|
140
|
+
return AssertionResult(name=name, passed=True, message=f"Tool called {count} time(s)")
|
|
141
|
+
return AssertionResult(name=name, passed=False, message=f"Tool called {count} time(s), expected >= {min_times}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _check_budget_under(events: List[Dict[str, Any]], max_tokens: Optional[int], max_calls: Optional[int]) -> AssertionResult:
|
|
145
|
+
parts = []
|
|
146
|
+
if max_tokens is not None:
|
|
147
|
+
parts.append(f"tokens<{max_tokens}")
|
|
148
|
+
if max_calls is not None:
|
|
149
|
+
parts.append(f"calls<{max_calls}")
|
|
150
|
+
name = f"budget_under:{','.join(parts)}"
|
|
151
|
+
|
|
152
|
+
total_tokens = 0
|
|
153
|
+
total_calls = 0
|
|
154
|
+
for e in events:
|
|
155
|
+
data = e.get("data", {})
|
|
156
|
+
if isinstance(data, dict):
|
|
157
|
+
usage = data.get("token_usage") or data.get("usage") or {}
|
|
158
|
+
if isinstance(usage, dict):
|
|
159
|
+
total_tokens += usage.get("total_tokens", 0)
|
|
160
|
+
if e.get("name", "").startswith("tool.") and e.get("kind") == "span" and e.get("phase") == "start":
|
|
161
|
+
total_calls += 1
|
|
162
|
+
|
|
163
|
+
failures = []
|
|
164
|
+
if max_tokens is not None and total_tokens >= max_tokens:
|
|
165
|
+
failures.append(f"tokens={total_tokens} >= {max_tokens}")
|
|
166
|
+
if max_calls is not None and total_calls >= max_calls:
|
|
167
|
+
failures.append(f"calls={total_calls} >= {max_calls}")
|
|
168
|
+
|
|
169
|
+
if failures:
|
|
170
|
+
return AssertionResult(name=name, passed=False, message="; ".join(failures))
|
|
171
|
+
return AssertionResult(name=name, passed=True, message=f"tokens={total_tokens}, calls={total_calls}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_completes_within(events: List[Dict[str, Any]], max_seconds: float) -> AssertionResult:
|
|
175
|
+
name = f"completes_within:{max_seconds}s"
|
|
176
|
+
max_ms = 0.0
|
|
177
|
+
for e in events:
|
|
178
|
+
dur = e.get("duration_ms")
|
|
179
|
+
if isinstance(dur, (int, float)) and dur > max_ms:
|
|
180
|
+
max_ms = dur
|
|
181
|
+
actual_seconds = max_ms / 1000.0
|
|
182
|
+
if actual_seconds <= max_seconds:
|
|
183
|
+
return AssertionResult(name=name, passed=True, message=f"Completed in {actual_seconds:.3f}s")
|
|
184
|
+
return AssertionResult(name=name, passed=False, message=f"Took {actual_seconds:.3f}s, limit is {max_seconds}s")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _check_event_exists(events: List[Dict[str, Any]], event_name: str) -> AssertionResult:
|
|
188
|
+
name = f"event_exists:{event_name}"
|
|
189
|
+
found = any(e.get("name") == event_name for e in events)
|
|
190
|
+
if found:
|
|
191
|
+
return AssertionResult(name=name, passed=True, message="Event found")
|
|
192
|
+
return AssertionResult(name=name, passed=False, message="Event not found")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _check_no_errors(events: List[Dict[str, Any]]) -> AssertionResult:
|
|
196
|
+
errors = [e for e in events if e.get("error") is not None]
|
|
197
|
+
if errors:
|
|
198
|
+
return AssertionResult(
|
|
199
|
+
name="no_errors",
|
|
200
|
+
passed=False,
|
|
201
|
+
message=f"Found {len(errors)} event(s) with errors",
|
|
202
|
+
)
|
|
203
|
+
return AssertionResult(name="no_errors", passed=True, message="No errors found")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# --- loader ---
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _load_events(path: str) -> List[Dict[str, Any]]:
|
|
210
|
+
events: List[Dict[str, Any]] = []
|
|
211
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
212
|
+
for line in f:
|
|
213
|
+
line = line.strip()
|
|
214
|
+
if not line:
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
events.append(json.loads(line))
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
continue
|
|
220
|
+
return events
|
agentguard/instrument.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Auto-instrumentation decorators and monkey-patches."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import functools
|
|
5
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def trace_agent(tracer: Any, name: Optional[str] = None) -> Callable[[F], F]:
|
|
11
|
+
"""Decorator that wraps a function in a top-level trace span.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
@trace_agent(tracer)
|
|
16
|
+
def my_agent(query: str) -> str:
|
|
17
|
+
...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def decorator(fn: F) -> F:
|
|
21
|
+
span_name = name or f"agent.{fn.__name__}"
|
|
22
|
+
|
|
23
|
+
@functools.wraps(fn)
|
|
24
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
25
|
+
with tracer.trace(span_name) as ctx:
|
|
26
|
+
kwargs["_trace_ctx"] = ctx
|
|
27
|
+
try:
|
|
28
|
+
return fn(*args, **kwargs)
|
|
29
|
+
except Exception:
|
|
30
|
+
raise
|
|
31
|
+
finally:
|
|
32
|
+
kwargs.pop("_trace_ctx", None)
|
|
33
|
+
|
|
34
|
+
# If the function doesn't accept **kwargs, fall back to simple wrapping
|
|
35
|
+
@functools.wraps(fn)
|
|
36
|
+
def simple_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
37
|
+
with tracer.trace(span_name):
|
|
38
|
+
return fn(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
# Check if function can accept _trace_ctx kwarg
|
|
41
|
+
import inspect
|
|
42
|
+
|
|
43
|
+
sig = inspect.signature(fn)
|
|
44
|
+
has_var_keyword = any(
|
|
45
|
+
p.kind == inspect.Parameter.VAR_KEYWORD
|
|
46
|
+
for p in sig.parameters.values()
|
|
47
|
+
)
|
|
48
|
+
has_trace_ctx = "_trace_ctx" in sig.parameters
|
|
49
|
+
|
|
50
|
+
if has_var_keyword or has_trace_ctx:
|
|
51
|
+
return wrapper # type: ignore[return-value]
|
|
52
|
+
return simple_wrapper # type: ignore[return-value]
|
|
53
|
+
|
|
54
|
+
return decorator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def trace_tool(tracer: Any, name: Optional[str] = None) -> Callable[[F], F]:
|
|
58
|
+
"""Decorator that wraps a function in a tool span.
|
|
59
|
+
|
|
60
|
+
Usage::
|
|
61
|
+
|
|
62
|
+
@trace_tool(tracer)
|
|
63
|
+
def search(query: str) -> str:
|
|
64
|
+
...
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def decorator(fn: F) -> F:
|
|
68
|
+
span_name = name or f"tool.{fn.__name__}"
|
|
69
|
+
|
|
70
|
+
@functools.wraps(fn)
|
|
71
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
72
|
+
with tracer.trace(span_name) as ctx:
|
|
73
|
+
result = fn(*args, **kwargs)
|
|
74
|
+
ctx.event("tool.result", data={"result": str(result)[:500]})
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
return wrapper # type: ignore[return-value]
|
|
78
|
+
|
|
79
|
+
return decorator
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def patch_openai(tracer: Any) -> None:
|
|
83
|
+
"""Monkey-patch OpenAI's ChatCompletion.create to auto-trace calls.
|
|
84
|
+
|
|
85
|
+
Safe to call even if openai is not installed — silently returns.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
import openai # noqa: F811
|
|
89
|
+
except ImportError:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
_original = None
|
|
93
|
+
|
|
94
|
+
# Support openai >= 1.0 (client-based) and < 1.0 (module-based)
|
|
95
|
+
client_cls = getattr(openai, "OpenAI", None)
|
|
96
|
+
if client_cls is not None:
|
|
97
|
+
# openai >= 1.0: patch the completions create method on the class
|
|
98
|
+
chat_completions = getattr(
|
|
99
|
+
getattr(client_cls, "chat", None), "completions", None
|
|
100
|
+
)
|
|
101
|
+
if chat_completions is not None:
|
|
102
|
+
_original = getattr(chat_completions, "create", None)
|
|
103
|
+
else:
|
|
104
|
+
# openai < 1.0
|
|
105
|
+
chat = getattr(openai, "ChatCompletion", None)
|
|
106
|
+
if chat is not None:
|
|
107
|
+
_original = getattr(chat, "create", None)
|
|
108
|
+
|
|
109
|
+
if _original is None:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
@functools.wraps(_original)
|
|
113
|
+
def traced_create(*args: Any, **kwargs: Any) -> Any:
|
|
114
|
+
model = kwargs.get("model", "unknown")
|
|
115
|
+
with tracer.trace(f"llm.openai.{model}") as ctx:
|
|
116
|
+
result = _original(*args, **kwargs)
|
|
117
|
+
# Try to extract usage
|
|
118
|
+
usage = getattr(result, "usage", None)
|
|
119
|
+
if usage is not None:
|
|
120
|
+
ctx.event(
|
|
121
|
+
"llm.result",
|
|
122
|
+
data={
|
|
123
|
+
"model": model,
|
|
124
|
+
"usage": {
|
|
125
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", 0),
|
|
126
|
+
"completion_tokens": getattr(usage, "completion_tokens", 0),
|
|
127
|
+
"total_tokens": getattr(usage, "total_tokens", 0),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
# Patch it back
|
|
134
|
+
if client_cls is not None and chat_completions is not None:
|
|
135
|
+
chat_completions.create = traced_create # type: ignore[attr-defined]
|
|
136
|
+
else:
|
|
137
|
+
chat = getattr(openai, "ChatCompletion", None)
|
|
138
|
+
if chat is not None:
|
|
139
|
+
chat.create = traced_create # type: ignore[attr-defined]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def patch_anthropic(tracer: Any) -> None:
|
|
143
|
+
"""Monkey-patch Anthropic's messages.create to auto-trace calls.
|
|
144
|
+
|
|
145
|
+
Safe to call even if anthropic is not installed — silently returns.
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
import anthropic # noqa: F811
|
|
149
|
+
except ImportError:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
client_cls = getattr(anthropic, "Anthropic", None)
|
|
153
|
+
if client_cls is None:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
messages = getattr(client_cls, "messages", None)
|
|
157
|
+
if messages is None:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
_original = getattr(messages, "create", None)
|
|
161
|
+
if _original is None:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
@functools.wraps(_original)
|
|
165
|
+
def traced_create(*args: Any, **kwargs: Any) -> Any:
|
|
166
|
+
model = kwargs.get("model", "unknown")
|
|
167
|
+
with tracer.trace(f"llm.anthropic.{model}") as ctx:
|
|
168
|
+
result = _original(*args, **kwargs)
|
|
169
|
+
usage = getattr(result, "usage", None)
|
|
170
|
+
if usage is not None:
|
|
171
|
+
ctx.event(
|
|
172
|
+
"llm.result",
|
|
173
|
+
data={
|
|
174
|
+
"model": model,
|
|
175
|
+
"usage": {
|
|
176
|
+
"input_tokens": getattr(usage, "input_tokens", 0),
|
|
177
|
+
"output_tokens": getattr(usage, "output_tokens", 0),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
messages.create = traced_create # type: ignore[attr-defined]
|
agentguard/viewer.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
"""Gantt-style trace viewer — self-contained HTML served via stdlib."""
|
|
1
2
|
from __future__ import annotations
|
|
2
3
|
|
|
3
4
|
import argparse
|
|
4
|
-
import json
|
|
5
5
|
import os
|
|
6
6
|
import threading
|
|
7
7
|
import webbrowser
|
|
@@ -10,82 +10,214 @@ from typing import Optional
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
_HTML = """<!doctype html>
|
|
13
|
-
<html lang
|
|
13
|
+
<html lang="en">
|
|
14
14
|
<head>
|
|
15
|
-
<meta charset
|
|
16
|
-
<meta name
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
17
|
<title>AgentGuard Trace Viewer</title>
|
|
18
18
|
<style>
|
|
19
|
-
:root {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
:root {
|
|
20
|
+
--bg:#0f1115; --ink:#f1f5f9; --muted:#9aa4b2; --card:#171a21;
|
|
21
|
+
--accent:#4ade80; --blue:#60a5fa; --purple:#a78bfa;
|
|
22
|
+
--green:#4ade80; --red:#f87171; --yellow:#fbbf24;
|
|
23
|
+
--border:#252a34;
|
|
24
|
+
}
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
body { margin:0; font-family:'IBM Plex Sans',system-ui,sans-serif; background:var(--bg); color:var(--ink); }
|
|
27
|
+
header { padding:16px 20px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:12px; }
|
|
28
|
+
h1 { margin:0; font-size:20px; font-weight:600; }
|
|
29
|
+
.badge { font-size:11px; background:#222833; padding:2px 8px; border-radius:10px; color:var(--muted); }
|
|
23
30
|
.wrap { padding:20px; }
|
|
24
|
-
.stats { display:grid; grid-template-columns:
|
|
25
|
-
.card { background:var(--card); padding:
|
|
26
|
-
.label { color:var(--muted); font-size:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
.stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:10px; margin-bottom:20px; }
|
|
32
|
+
.card { background:var(--card); padding:10px 14px; border-radius:8px; border:1px solid var(--border); }
|
|
33
|
+
.card .label { color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.5px; }
|
|
34
|
+
.card .val { font-size:20px; font-weight:600; margin-top:2px; }
|
|
35
|
+
.card .val.green { color:var(--green); }
|
|
36
|
+
|
|
37
|
+
/* Gantt timeline */
|
|
38
|
+
.gantt { position:relative; margin-top:12px; }
|
|
39
|
+
.gantt-row { display:flex; align-items:center; height:28px; border-bottom:1px solid #1a1e27; cursor:pointer; }
|
|
40
|
+
.gantt-row:hover { background:#1a1e27; }
|
|
41
|
+
.gantt-label { width:220px; min-width:220px; padding:0 8px; font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
42
|
+
.gantt-track { flex:1; position:relative; height:100%; }
|
|
43
|
+
.gantt-bar { position:absolute; height:16px; top:6px; border-radius:3px; min-width:2px; transition: opacity .15s; }
|
|
44
|
+
.gantt-bar:hover { opacity:.85; }
|
|
45
|
+
.gantt-bar.reasoning { background:var(--blue); }
|
|
46
|
+
.gantt-bar.tool { background:var(--green); }
|
|
47
|
+
.gantt-bar.llm { background:var(--purple); }
|
|
48
|
+
.gantt-bar.guard { background:var(--yellow); }
|
|
49
|
+
.gantt-bar.error { background:var(--red); }
|
|
50
|
+
.gantt-bar.default { background:#64748b; }
|
|
51
|
+
|
|
52
|
+
/* Detail panel */
|
|
53
|
+
.detail { display:none; background:var(--card); border:1px solid var(--border); border-radius:8px; padding:16px; margin:12px 0; font-size:13px; }
|
|
54
|
+
.detail.open { display:block; }
|
|
55
|
+
.detail pre { background:#0f1115; padding:10px; border-radius:6px; overflow-x:auto; font-size:12px; color:var(--muted); }
|
|
56
|
+
.detail .dhead { font-weight:600; margin-bottom:8px; color:var(--accent); }
|
|
57
|
+
|
|
58
|
+
/* Legend */
|
|
59
|
+
.legend { display:flex; gap:16px; margin-bottom:14px; flex-wrap:wrap; }
|
|
60
|
+
.legend-item { display:flex; align-items:center; gap:5px; font-size:12px; color:var(--muted); }
|
|
61
|
+
.legend-dot { width:10px; height:10px; border-radius:2px; }
|
|
30
62
|
</style>
|
|
31
63
|
</head>
|
|
32
64
|
<body>
|
|
33
65
|
<header>
|
|
34
66
|
<h1>AgentGuard Trace Viewer</h1>
|
|
35
|
-
<
|
|
67
|
+
<span class="badge">Gantt Timeline</span>
|
|
36
68
|
</header>
|
|
37
|
-
<div class
|
|
38
|
-
<div class
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
</thead>
|
|
49
|
-
<tbody id=\"rows\"></tbody>
|
|
50
|
-
</table>
|
|
69
|
+
<div class="wrap">
|
|
70
|
+
<div class="stats" id="stats"></div>
|
|
71
|
+
<div class="legend">
|
|
72
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--blue)"></div>Reasoning</div>
|
|
73
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div>Tool</div>
|
|
74
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div>LLM</div>
|
|
75
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div>Guard</div>
|
|
76
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div>Error</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="gantt" id="gantt"></div>
|
|
79
|
+
<div class="detail" id="detail"></div>
|
|
51
80
|
</div>
|
|
52
81
|
<script>
|
|
53
82
|
async function load() {
|
|
54
83
|
const res = await fetch('/trace');
|
|
55
84
|
const text = await res.text();
|
|
56
|
-
const lines = text.split('
|
|
85
|
+
const lines = text.split('\\n').filter(Boolean);
|
|
57
86
|
const events = lines.map(l => JSON.parse(l));
|
|
58
87
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
if (!events.length) { document.getElementById('gantt').innerHTML = '<p style="color:var(--muted)">No events found.</p>'; return; }
|
|
89
|
+
|
|
90
|
+
// Stats
|
|
91
|
+
const totalEvents = events.length;
|
|
92
|
+
const spans = events.filter(e => e.kind === 'span');
|
|
93
|
+
const spanStarts = spans.filter(e => e.phase === 'start');
|
|
94
|
+
const spanEnds = spans.filter(e => e.phase === 'end');
|
|
95
|
+
const evts = events.filter(e => e.kind === 'event');
|
|
96
|
+
const reasoning = evts.filter(e => e.name === 'reasoning.step').length;
|
|
97
|
+
const toolResults = evts.filter(e => e.name === 'tool.result').length;
|
|
98
|
+
const llmResults = evts.filter(e => e.name === 'llm.result').length;
|
|
99
|
+
const loops = evts.filter(e => e.name === 'guard.loop_detected').length;
|
|
100
|
+
const errors = events.filter(e => e.error != null).length;
|
|
101
|
+
|
|
102
|
+
let totalMs = 0;
|
|
103
|
+
spanEnds.forEach(e => { if (e.duration_ms && e.duration_ms > totalMs) totalMs = e.duration_ms; });
|
|
104
|
+
|
|
105
|
+
const totalTokens = events.reduce((acc, e) => {
|
|
106
|
+
const d = e.data || {};
|
|
107
|
+
const u = d.token_usage || d.usage || {};
|
|
108
|
+
return acc + (u.total_tokens || 0);
|
|
109
|
+
}, 0);
|
|
66
110
|
|
|
67
111
|
const statsEl = document.getElementById('stats');
|
|
68
|
-
|
|
69
|
-
['
|
|
70
|
-
['Spans',
|
|
71
|
-
['
|
|
72
|
-
['Reasoning
|
|
73
|
-
['
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
const cards = [
|
|
113
|
+
['Events', totalEvents, ''],
|
|
114
|
+
['Spans', spanStarts.length, ''],
|
|
115
|
+
['Run time', totalMs > 0 ? totalMs.toFixed(1) + ' ms' : '-', ''],
|
|
116
|
+
['Reasoning', reasoning, ''],
|
|
117
|
+
['Tool calls', toolResults, ''],
|
|
118
|
+
['LLM calls', llmResults, ''],
|
|
119
|
+
['Loop hits', loops, loops > 0 ? 'color:var(--red)' : ''],
|
|
120
|
+
['Errors', errors, errors > 0 ? 'color:var(--red)' : ''],
|
|
121
|
+
['Tokens', totalTokens || '-', ''],
|
|
122
|
+
];
|
|
123
|
+
statsEl.innerHTML = cards.map(([k,v,s]) =>
|
|
124
|
+
`<div class="card"><div class="label">${k}</div><div class="val" style="${s}">${v}</div></div>`
|
|
125
|
+
).join('');
|
|
126
|
+
|
|
127
|
+
// Build timeline rows
|
|
128
|
+
// Each event gets a row. Position = ts relative to min ts. Width = duration_ms or a small dot.
|
|
129
|
+
const minTs = Math.min(...events.map(e => e.ts));
|
|
130
|
+
const maxTs = Math.max(...events.map(e => e.ts));
|
|
131
|
+
const maxDur = Math.max(...spanEnds.map(e => e.duration_ms || 0), 1);
|
|
132
|
+
const timeRange = Math.max((maxTs - minTs) * 1000, maxDur, 1); // in ms
|
|
133
|
+
|
|
134
|
+
// Build span pairs: match start/end by span_id
|
|
135
|
+
const spanMap = {};
|
|
136
|
+
spans.forEach(e => {
|
|
137
|
+
if (!spanMap[e.span_id]) spanMap[e.span_id] = {};
|
|
138
|
+
if (e.phase === 'start') spanMap[e.span_id].start = e;
|
|
139
|
+
if (e.phase === 'end') spanMap[e.span_id].end = e;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Build display rows: spans (paired) + standalone events
|
|
143
|
+
const rows = [];
|
|
144
|
+
|
|
145
|
+
// Add span rows
|
|
146
|
+
Object.values(spanMap).forEach(pair => {
|
|
147
|
+
const s = pair.start || pair.end;
|
|
148
|
+
const dur = pair.end ? (pair.end.duration_ms || 0) : 0;
|
|
149
|
+
const startMs = (s.ts - minTs) * 1000;
|
|
150
|
+
rows.push({
|
|
151
|
+
name: s.name,
|
|
152
|
+
startMs,
|
|
153
|
+
durMs: dur,
|
|
154
|
+
type: classifyName(s.name),
|
|
155
|
+
event: s,
|
|
156
|
+
endEvent: pair.end,
|
|
157
|
+
depth: s.parent_id ? 1 : 0,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Add standalone events (not span start/end)
|
|
162
|
+
evts.forEach(e => {
|
|
163
|
+
const startMs = (e.ts - minTs) * 1000;
|
|
164
|
+
rows.push({
|
|
165
|
+
name: e.name,
|
|
166
|
+
startMs,
|
|
167
|
+
durMs: 0,
|
|
168
|
+
type: classifyName(e.name),
|
|
169
|
+
event: e,
|
|
170
|
+
endEvent: null,
|
|
171
|
+
depth: 1,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Sort by startMs
|
|
176
|
+
rows.sort((a, b) => a.startMs - b.startMs || a.depth - b.depth);
|
|
177
|
+
|
|
178
|
+
const ganttEl = document.getElementById('gantt');
|
|
179
|
+
ganttEl.innerHTML = rows.map((r, i) => {
|
|
180
|
+
const left = (r.startMs / timeRange * 100).toFixed(4);
|
|
181
|
+
const width = r.durMs > 0 ? Math.max(r.durMs / timeRange * 100, 0.3).toFixed(4) : '0.3';
|
|
182
|
+
const indent = r.depth > 0 ? ' ' : '';
|
|
183
|
+
return `<div class="gantt-row" data-idx="${i}">
|
|
184
|
+
<div class="gantt-label" title="${r.name}">${indent}${r.name}</div>
|
|
185
|
+
<div class="gantt-track">
|
|
186
|
+
<div class="gantt-bar ${r.type}" style="left:${left}%;width:${width}%" title="${r.name} ${r.durMs > 0 ? r.durMs.toFixed(1)+'ms' : 'event'}"></div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>`;
|
|
189
|
+
}).join('');
|
|
190
|
+
|
|
191
|
+
// Click to expand detail
|
|
192
|
+
const detailEl = document.getElementById('detail');
|
|
193
|
+
ganttEl.addEventListener('click', e => {
|
|
194
|
+
const row = e.target.closest('.gantt-row');
|
|
195
|
+
if (!row) return;
|
|
196
|
+
const idx = parseInt(row.dataset.idx);
|
|
197
|
+
const r = rows[idx];
|
|
198
|
+
const ev = r.endEvent || r.event;
|
|
199
|
+
detailEl.className = 'detail open';
|
|
200
|
+
detailEl.innerHTML = `
|
|
201
|
+
<div class="dhead">${r.name}</div>
|
|
202
|
+
<p><strong>Kind:</strong> ${ev.kind} <strong>Phase:</strong> ${ev.phase}
|
|
203
|
+
${r.durMs > 0 ? ' <strong>Duration:</strong> ' + r.durMs.toFixed(3) + ' ms' : ''}
|
|
204
|
+
${ev.error ? ' <strong style="color:var(--red)">Error:</strong> ' + ev.error.message : ''}
|
|
205
|
+
</p>
|
|
206
|
+
<p><strong>trace_id:</strong> ${ev.trace_id}<br><strong>span_id:</strong> ${ev.span_id}${ev.parent_id ? '<br><strong>parent_id:</strong> '+ev.parent_id : ''}</p>
|
|
207
|
+
<pre>${JSON.stringify(ev.data || {}, null, 2)}</pre>
|
|
208
|
+
`;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function classifyName(name) {
|
|
213
|
+
if (name.startsWith('reasoning')) return 'reasoning';
|
|
214
|
+
if (name.startsWith('tool')) return 'tool';
|
|
215
|
+
if (name.startsWith('llm')) return 'llm';
|
|
216
|
+
if (name.startsWith('guard')) return 'guard';
|
|
217
|
+
if (name.includes('error')) return 'error';
|
|
218
|
+
return 'default';
|
|
88
219
|
}
|
|
220
|
+
|
|
89
221
|
load();
|
|
90
222
|
</script>
|
|
91
223
|
</body>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentguard47
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Lightweight observability and evaluation primitives for multi-agent systems
|
|
5
5
|
Author: AgentGuard
|
|
6
6
|
License-Expression: MIT
|
|
@@ -105,6 +105,45 @@ replayer = Replayer("runs.jsonl")
|
|
|
105
105
|
resp = replayer.replay_call("llm", {"prompt": "hi"})
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
## Evaluation as Code
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from agentguard import EvalSuite
|
|
112
|
+
|
|
113
|
+
result = (
|
|
114
|
+
EvalSuite("traces.jsonl")
|
|
115
|
+
.assert_no_loops()
|
|
116
|
+
.assert_tool_called("search", min_times=1)
|
|
117
|
+
.assert_budget_under(tokens=50000)
|
|
118
|
+
.assert_completes_within(30.0)
|
|
119
|
+
.assert_no_errors()
|
|
120
|
+
.run()
|
|
121
|
+
)
|
|
122
|
+
print(result.summary)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Auto-Instrumentation
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from agentguard import Tracer
|
|
129
|
+
from agentguard.instrument import trace_agent, trace_tool
|
|
130
|
+
|
|
131
|
+
tracer = Tracer()
|
|
132
|
+
|
|
133
|
+
@trace_agent(tracer)
|
|
134
|
+
def my_agent(query):
|
|
135
|
+
return search(query)
|
|
136
|
+
|
|
137
|
+
@trace_tool(tracer)
|
|
138
|
+
def search(q):
|
|
139
|
+
return f"results for {q}"
|
|
140
|
+
|
|
141
|
+
# Monkey-patch OpenAI/Anthropic (safe if not installed)
|
|
142
|
+
from agentguard.instrument import patch_openai, patch_anthropic
|
|
143
|
+
patch_openai(tracer)
|
|
144
|
+
patch_anthropic(tracer)
|
|
145
|
+
```
|
|
146
|
+
|
|
108
147
|
## CLI
|
|
109
148
|
|
|
110
149
|
```bash
|
|
@@ -114,8 +153,11 @@ agentguard summarize traces.jsonl
|
|
|
114
153
|
# Human-readable report
|
|
115
154
|
agentguard report traces.jsonl
|
|
116
155
|
|
|
117
|
-
# Open trace viewer in browser
|
|
156
|
+
# Open Gantt trace viewer in browser
|
|
118
157
|
agentguard view traces.jsonl
|
|
158
|
+
|
|
159
|
+
# Run evaluation assertions
|
|
160
|
+
agentguard eval traces.jsonl
|
|
119
161
|
```
|
|
120
162
|
|
|
121
163
|
## Trace Viewer
|
|
@@ -124,6 +166,8 @@ agentguard view traces.jsonl
|
|
|
124
166
|
agentguard view traces.jsonl --port 8080
|
|
125
167
|
```
|
|
126
168
|
|
|
169
|
+
Gantt-style timeline with color-coded spans (reasoning, tool, LLM, guard, error), click-to-expand detail panel, and aggregate stats.
|
|
170
|
+
|
|
127
171
|
## Integrations
|
|
128
172
|
|
|
129
173
|
- LangChain: `agentguard.integrations.langchain`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
agentguard/__init__.py,sha256=l-c9Ub0P2AMc-h3DFGgLtUgpDSui3oM8ixwCGsuhjBE,550
|
|
2
|
+
agentguard/cli.py,sha256=a9GYss-iRa8f70tOQ94FstNjPT4PJhMX-hy9WIacYfs,3972
|
|
3
|
+
agentguard/evaluation.py,sha256=wsRHHxAJ6wh-_4q0axcZxWL8T8fvfJnIal14SIwI9jU,7858
|
|
4
|
+
agentguard/guards.py,sha256=8GS3lHtr-pGwbbBSsBYKxsla02ipwCNoTj6kMRO_v6o,3147
|
|
5
|
+
agentguard/instrument.py,sha256=EJguaWqrZGG3xGMt2NyR4lgTm7TERUPLnlOOxywQwKk,5891
|
|
6
|
+
agentguard/recording.py,sha256=rOpO_kfVyh4StY0WKLJx_ZNjHYtadO48JgLNZwouFzE,2003
|
|
7
|
+
agentguard/tracing.py,sha256=7yI4H3Q3ndbD7B7F4AOtnnYy-kZ7cbLMn1t_dXTVrQI,4133
|
|
8
|
+
agentguard/viewer.py,sha256=vpqPUGGNHKYHThcIYcafWxh5epOh-x_BuqOB6z3p_Jw,12074
|
|
9
|
+
agentguard/integrations/__init__.py,sha256=4UjBWIi91ESzy_0qwmo2ha5ZhhGGd3-2i8SOq9H9gwU,126
|
|
10
|
+
agentguard/integrations/langchain.py,sha256=ioqfWfsSaNOeQdwBcmd1ik0YdMsZZUW85dB-IqwKofI,7649
|
|
11
|
+
agentguard/sinks/__init__.py,sha256=PAgY0Y3tptapbNhnXEUYojTldfGokGZTJWVf-b58Qu0,51
|
|
12
|
+
agentguard/sinks/http.py,sha256=TRmgZWd9kl3JEEZh9ppRRbcjuW-yXznYDRrQQGgdQ1Y,2615
|
|
13
|
+
agentguard47-0.3.0.dist-info/METADATA,sha256=vQPyMg7MMUI8Two7fpxfmexLWYR0kWYvh8PenrN-ABU,4703
|
|
14
|
+
agentguard47-0.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
15
|
+
agentguard47-0.3.0.dist-info/entry_points.txt,sha256=ubpnBAHVcWmjkLmnv-gIH0-GSPofUlZfoviZ4R9i5I0,51
|
|
16
|
+
agentguard47-0.3.0.dist-info/top_level.txt,sha256=rEW5sAnjxXRs63ja2psmsMRMJJfi_3EAxf7crl1nHyw,11
|
|
17
|
+
agentguard47-0.3.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
agentguard/__init__.py,sha256=r2lM31udiHzasnZFRCSnLwDbQDohUCG-5GKyMPeC6Z8,429
|
|
2
|
-
agentguard/cli.py,sha256=GgCl7Q9VpZPMX-PsW3UYnNrC_1iy_bAcdiSa2qW685U,3485
|
|
3
|
-
agentguard/guards.py,sha256=8GS3lHtr-pGwbbBSsBYKxsla02ipwCNoTj6kMRO_v6o,3147
|
|
4
|
-
agentguard/recording.py,sha256=rOpO_kfVyh4StY0WKLJx_ZNjHYtadO48JgLNZwouFzE,2003
|
|
5
|
-
agentguard/tracing.py,sha256=7yI4H3Q3ndbD7B7F4AOtnnYy-kZ7cbLMn1t_dXTVrQI,4133
|
|
6
|
-
agentguard/viewer.py,sha256=uM21TYTXhq5zHu2tQP52bI5PFO9mmXzkP8z_m3sCkfY,4997
|
|
7
|
-
agentguard/integrations/__init__.py,sha256=4UjBWIi91ESzy_0qwmo2ha5ZhhGGd3-2i8SOq9H9gwU,126
|
|
8
|
-
agentguard/integrations/langchain.py,sha256=ppc27W6zFM2TYAQ1a2JI6bjNVWWMLmP5OapWfHQEP2o,7659
|
|
9
|
-
agentguard/sinks/__init__.py,sha256=PAgY0Y3tptapbNhnXEUYojTldfGokGZTJWVf-b58Qu0,51
|
|
10
|
-
agentguard/sinks/http.py,sha256=TRmgZWd9kl3JEEZh9ppRRbcjuW-yXznYDRrQQGgdQ1Y,2615
|
|
11
|
-
agentguard47-0.2.0.dist-info/METADATA,sha256=uvTKR_RyZrB9yxKuAuz9x1NWZ7rQnE-U3H4b9ozQvks,3742
|
|
12
|
-
agentguard47-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
-
agentguard47-0.2.0.dist-info/entry_points.txt,sha256=ubpnBAHVcWmjkLmnv-gIH0-GSPofUlZfoviZ4R9i5I0,51
|
|
14
|
-
agentguard47-0.2.0.dist-info/top_level.txt,sha256=rEW5sAnjxXRs63ja2psmsMRMJJfi_3EAxf7crl1nHyw,11
|
|
15
|
-
agentguard47-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|