agobs 1.0__tar.gz
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.
- agobs-1.0/PKG-INFO +164 -0
- agobs-1.0/README.md +117 -0
- agobs-1.0/agentlens/__init__.py +48 -0
- agobs-1.0/agentlens/_diag.py +32 -0
- agobs-1.0/agentlens/adapters/__init__.py +16 -0
- agobs-1.0/agentlens/adapters/anthropic_agents.py +52 -0
- agobs-1.0/agentlens/adapters/autogen.py +61 -0
- agobs-1.0/agentlens/adapters/common.py +46 -0
- agobs-1.0/agentlens/adapters/crewai.py +66 -0
- agobs-1.0/agentlens/adapters/fastmcp.py +56 -0
- agobs-1.0/agentlens/adapters/langchain.py +183 -0
- agobs-1.0/agentlens/adapters/langgraph.py +17 -0
- agobs-1.0/agentlens/adapters/llamaindex.py +73 -0
- agobs-1.0/agentlens/adapters/mcp.py +47 -0
- agobs-1.0/agentlens/adapters/openai_agents.py +125 -0
- agobs-1.0/agentlens/adapters/pydanticai.py +92 -0
- agobs-1.0/agentlens/adapters/strands.py +92 -0
- agobs-1.0/agentlens/cli.py +57 -0
- agobs-1.0/agentlens/client.py +109 -0
- agobs-1.0/agentlens/events.py +124 -0
- agobs-1.0/agentlens/instrument/__init__.py +41 -0
- agobs-1.0/agentlens/instrument/_patch.py +119 -0
- agobs-1.0/agentlens/instrument/anthropic.py +32 -0
- agobs-1.0/agentlens/instrument/gemini.py +31 -0
- agobs-1.0/agentlens/instrument/groq.py +32 -0
- agobs-1.0/agentlens/instrument/litellm.py +33 -0
- agobs-1.0/agentlens/instrument/openai.py +32 -0
- agobs-1.0/agentlens/monitor.py +43 -0
- agobs-1.0/agentlens/server/__init__.py +0 -0
- agobs-1.0/agentlens/server/api.py +115 -0
- agobs-1.0/agentlens/server/app.py +68 -0
- agobs-1.0/agentlens/server/db.py +471 -0
- agobs-1.0/agentlens/server/docs.py +435 -0
- agobs-1.0/agentlens/server/llm.py +167 -0
- agobs-1.0/agentlens/server/pricing.py +100 -0
- agobs-1.0/agentlens/server/static/app.js +268 -0
- agobs-1.0/agentlens/server/static/fonts.css +46 -0
- agobs-1.0/agentlens/server/static/style.css +245 -0
- agobs-1.0/agentlens/server/templates/agents.html +1 -0
- agobs-1.0/agentlens/server/templates/base.html +38 -0
- agobs-1.0/agentlens/server/templates/cost.html +1 -0
- agobs-1.0/agentlens/server/templates/failures.html +1 -0
- agobs-1.0/agentlens/server/templates/memory.html +1 -0
- agobs-1.0/agentlens/server/templates/overview.html +1 -0
- agobs-1.0/agentlens/server/templates/run_detail.html +1 -0
- agobs-1.0/agentlens/server/templates/runs.html +1 -0
- agobs-1.0/agentlens/server/templates/tools.html +1 -0
- agobs-1.0/agentlens/storage.py +59 -0
- agobs-1.0/agentlens/tracing.py +178 -0
- agobs-1.0/agobs.egg-info/PKG-INFO +164 -0
- agobs-1.0/agobs.egg-info/SOURCES.txt +61 -0
- agobs-1.0/agobs.egg-info/dependency_links.txt +1 -0
- agobs-1.0/agobs.egg-info/entry_points.txt +2 -0
- agobs-1.0/agobs.egg-info/requires.txt +43 -0
- agobs-1.0/agobs.egg-info/top_level.txt +1 -0
- agobs-1.0/pyproject.toml +59 -0
- agobs-1.0/setup.cfg +4 -0
- agobs-1.0/tests/test_adapters.py +29 -0
- agobs-1.0/tests/test_api.py +33 -0
- agobs-1.0/tests/test_db.py +48 -0
- agobs-1.0/tests/test_events.py +35 -0
- agobs-1.0/tests/test_instrument.py +52 -0
- agobs-1.0/tests/test_tracing.py +49 -0
agobs-1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agobs
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: AgentObserve: local-first diagnostics & observability for AI agents. Answers 'Why did my agent behave this way?' with one line and a single SQLite file.
|
|
5
|
+
Author-email: Tharanika <sudhatharanika@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Tharanika-R-Git/AgentLens
|
|
8
|
+
Keywords: agent,observability,tracing,diagnostics,llm,tools,mcp
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: fastapi>=0.100
|
|
16
|
+
Requires-Dist: uvicorn>=0.23
|
|
17
|
+
Requires-Dist: jinja2>=3.1
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: httpx>=0.24
|
|
20
|
+
Provides-Extra: langchain
|
|
21
|
+
Requires-Dist: langchain-core>=0.1; extra == "langchain"
|
|
22
|
+
Provides-Extra: langgraph
|
|
23
|
+
Requires-Dist: langgraph>=0.1; extra == "langgraph"
|
|
24
|
+
Provides-Extra: llamaindex
|
|
25
|
+
Requires-Dist: llama-index-core>=0.10; extra == "llamaindex"
|
|
26
|
+
Provides-Extra: crewai
|
|
27
|
+
Requires-Dist: crewai>=0.1; extra == "crewai"
|
|
28
|
+
Provides-Extra: autogen
|
|
29
|
+
Requires-Dist: pyautogen>=0.2; extra == "autogen"
|
|
30
|
+
Provides-Extra: pydanticai
|
|
31
|
+
Requires-Dist: pydantic-ai>=0.0.1; extra == "pydanticai"
|
|
32
|
+
Provides-Extra: openai-agents
|
|
33
|
+
Requires-Dist: openai-agents>=0.0.1; extra == "openai-agents"
|
|
34
|
+
Provides-Extra: mcp
|
|
35
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
36
|
+
Provides-Extra: fastmcp
|
|
37
|
+
Requires-Dist: fastmcp>=0.1; extra == "fastmcp"
|
|
38
|
+
Provides-Extra: all
|
|
39
|
+
Requires-Dist: langchain-core>=0.1; extra == "all"
|
|
40
|
+
Requires-Dist: langgraph>=0.1; extra == "all"
|
|
41
|
+
Requires-Dist: llama-index-core>=0.10; extra == "all"
|
|
42
|
+
Requires-Dist: crewai>=0.1; extra == "all"
|
|
43
|
+
Requires-Dist: pyautogen>=0.2; extra == "all"
|
|
44
|
+
Requires-Dist: mcp>=1.0; extra == "all"
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
47
|
+
|
|
48
|
+
# AgentLens
|
|
49
|
+
|
|
50
|
+
**Local-first diagnostics & observability for AI agents.** One line to integrate,
|
|
51
|
+
a single SQLite file, a built-in dashboard. AgentLens answers the question other
|
|
52
|
+
tools don't:
|
|
53
|
+
|
|
54
|
+
> **Why did my agent behave this way?**
|
|
55
|
+
|
|
56
|
+
Not a prompt tracker. Not a token dashboard. A diagnostics platform for *agent
|
|
57
|
+
behavior* — tool usage, memory access, workflow paths, decisions, failures,
|
|
58
|
+
retries, latency, and cost.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install pyagentlens # core: store + dashboard + manual API + auto-patch
|
|
62
|
+
pip install "pyagentlens[langchain]" # framework adapters as extras (langgraph, crewai, …)
|
|
63
|
+
pip install "pyagentlens[all]" # every adapter
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> Package name on PyPI is `pyagentlens`; the import name is `agentlens`.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from agentlens import monitor
|
|
70
|
+
monitor.start() # auto-patches installed LLM SDKs — that's it
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Full docs:** see [`Guide.md`](./Guide.md) — in-depth reference for humans *and* AI
|
|
74
|
+
> coding agents (when-to-use-what tables, every API signature, per-framework
|
|
75
|
+
> snippets, troubleshooting, and an agent cheat sheet).
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
agentlens ui # dashboard at http://127.0.0.1:7180
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
No server to deploy. No cloud. No external services. Data lives in a hidden
|
|
82
|
+
`./.agentlens/agentlens.db` (SQLite, WAL).
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Three ways to instrument (mix freely)
|
|
87
|
+
|
|
88
|
+
### 1. Zero-config auto-patch
|
|
89
|
+
`monitor.start()` detects and patches whatever is installed — **OpenAI,
|
|
90
|
+
Anthropic, Google Gemini, Groq, LiteLLM** — so every LLM call becomes an `llm`
|
|
91
|
+
span with tokens and cost, with no code changes.
|
|
92
|
+
|
|
93
|
+
### 2. Manual (framework-agnostic, always available)
|
|
94
|
+
```python
|
|
95
|
+
import agentlens
|
|
96
|
+
agentlens.init(project="research-bot")
|
|
97
|
+
|
|
98
|
+
with agentlens.trace_session("chat"):
|
|
99
|
+
with agentlens.trace_agent("planner", role="lead"):
|
|
100
|
+
agentlens.log_decision(options=["search", "answer"], chosen="search", reason="needs fresh info")
|
|
101
|
+
agentlens.log_memory("read", "user_prefs", hit=True)
|
|
102
|
+
agentlens.log_tool("web_search", args={"q": "ai news"}, result=[...], retry_count=1)
|
|
103
|
+
agentlens.log_llm(model="gpt-4o", input_tokens=1200, output_tokens=400)
|
|
104
|
+
```
|
|
105
|
+
A tool/LLM logged with an `error=` (or a `trace_*` block that raises) is
|
|
106
|
+
automatically recorded as a failure.
|
|
107
|
+
|
|
108
|
+
### 3. Framework adapters
|
|
109
|
+
| Framework | Import | Maturity |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| LangChain | `agentlens.adapters.langchain.AgentLensCallbackHandler` | full |
|
|
112
|
+
| LangGraph | `agentlens.adapters.langgraph.AgentLensCallbackHandler` | full (LangChain callbacks) |
|
|
113
|
+
| LlamaIndex | `agentlens.adapters.llamaindex.AgentLensLlamaHandler` | full |
|
|
114
|
+
| CrewAI | `agentlens.adapters.crewai` (`step_callback`, `instrument_tools`) | best-effort |
|
|
115
|
+
| AutoGen | `agentlens.adapters.autogen.instrument_agent` | best-effort |
|
|
116
|
+
| PydanticAI | `agentlens.adapters.pydanticai.instrument_agent` | best-effort |
|
|
117
|
+
| OpenAI Agents SDK | `agentlens.adapters.openai_agents.install` | best-effort |
|
|
118
|
+
| Strands Agents | `agentlens.adapters.strands.callback_handler` | best-effort |
|
|
119
|
+
| Anthropic Agent SDK | `agentlens.adapters.anthropic_agents.traced_async_tool` | tool-level |
|
|
120
|
+
| MCP | `agentlens.adapters.mcp.instrument_server` / `traced_tool` | tool-level |
|
|
121
|
+
| FastMCP | `agentlens.adapters.fastmcp.instrument_server` | tool-level |
|
|
122
|
+
|
|
123
|
+
Plus a universal `agentlens.adapters.wrap_tool(fn)` / `@traced_tool()` that works
|
|
124
|
+
with any framework or none.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## The dashboard
|
|
129
|
+
|
|
130
|
+
- **Overview** — runs, success/failure rate, cost, p50/p95 latency.
|
|
131
|
+
- **Runs → Run detail** — the **workflow execution graph** (span tree: session →
|
|
132
|
+
workflow → agent → tool/memory/llm/decision), click any node to inspect.
|
|
133
|
+
- **Agent Explorer** — runs, failures, latency, and attributed cost per agent.
|
|
134
|
+
- **Tool Explorer** — most used / slowest / most failed tools, retries.
|
|
135
|
+
- **Memory Explorer** — reads/writes/updates/deletes and read hit-rate.
|
|
136
|
+
- **Failure Explorer** — exceptions, messages, retries, jump to the failing run.
|
|
137
|
+
- **Cost Explorer** — token usage, cost by day, by model, most expensive agents.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Try it now (no keys needed)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pip install -e .
|
|
145
|
+
python examples/demo_agent.py
|
|
146
|
+
agentlens ui
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Design
|
|
150
|
+
|
|
151
|
+
Local-first, zero-config, framework-agnostic, async-safe. Observability code
|
|
152
|
+
never crashes your app (emit failures are swallowed). One `Span` model covers
|
|
153
|
+
every event; kind-specific data lives in `attributes`. Cost is computed from an
|
|
154
|
+
editable price book (`agentlens/server/pricing.py`).
|
|
155
|
+
|
|
156
|
+
CLI: `agentlens ui [--port 7180] [--host] [--backend-store-uri PATH]`,
|
|
157
|
+
`agentlens providers`, `agentlens version`. Override the port with
|
|
158
|
+
`AGENTLENS_PORT`.
|
|
159
|
+
|
|
160
|
+
**Single process** writes straight to local SQLite. For **multiple
|
|
161
|
+
processes/workers** sharing one dashboard, run `agentlens ui` once and point each
|
|
162
|
+
worker at it: `agentlens.init(project=..., tracking_uri="http://127.0.0.1:7180")`.
|
|
163
|
+
|
|
164
|
+
See [`Guide.md`](./Guide.md) for the complete reference.
|
agobs-1.0/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# AgentLens
|
|
2
|
+
|
|
3
|
+
**Local-first diagnostics & observability for AI agents.** One line to integrate,
|
|
4
|
+
a single SQLite file, a built-in dashboard. AgentLens answers the question other
|
|
5
|
+
tools don't:
|
|
6
|
+
|
|
7
|
+
> **Why did my agent behave this way?**
|
|
8
|
+
|
|
9
|
+
Not a prompt tracker. Not a token dashboard. A diagnostics platform for *agent
|
|
10
|
+
behavior* — tool usage, memory access, workflow paths, decisions, failures,
|
|
11
|
+
retries, latency, and cost.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install pyagentlens # core: store + dashboard + manual API + auto-patch
|
|
15
|
+
pip install "pyagentlens[langchain]" # framework adapters as extras (langgraph, crewai, …)
|
|
16
|
+
pip install "pyagentlens[all]" # every adapter
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> Package name on PyPI is `pyagentlens`; the import name is `agentlens`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from agentlens import monitor
|
|
23
|
+
monitor.start() # auto-patches installed LLM SDKs — that's it
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Full docs:** see [`Guide.md`](./Guide.md) — in-depth reference for humans *and* AI
|
|
27
|
+
> coding agents (when-to-use-what tables, every API signature, per-framework
|
|
28
|
+
> snippets, troubleshooting, and an agent cheat sheet).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
agentlens ui # dashboard at http://127.0.0.1:7180
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No server to deploy. No cloud. No external services. Data lives in a hidden
|
|
35
|
+
`./.agentlens/agentlens.db` (SQLite, WAL).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Three ways to instrument (mix freely)
|
|
40
|
+
|
|
41
|
+
### 1. Zero-config auto-patch
|
|
42
|
+
`monitor.start()` detects and patches whatever is installed — **OpenAI,
|
|
43
|
+
Anthropic, Google Gemini, Groq, LiteLLM** — so every LLM call becomes an `llm`
|
|
44
|
+
span with tokens and cost, with no code changes.
|
|
45
|
+
|
|
46
|
+
### 2. Manual (framework-agnostic, always available)
|
|
47
|
+
```python
|
|
48
|
+
import agentlens
|
|
49
|
+
agentlens.init(project="research-bot")
|
|
50
|
+
|
|
51
|
+
with agentlens.trace_session("chat"):
|
|
52
|
+
with agentlens.trace_agent("planner", role="lead"):
|
|
53
|
+
agentlens.log_decision(options=["search", "answer"], chosen="search", reason="needs fresh info")
|
|
54
|
+
agentlens.log_memory("read", "user_prefs", hit=True)
|
|
55
|
+
agentlens.log_tool("web_search", args={"q": "ai news"}, result=[...], retry_count=1)
|
|
56
|
+
agentlens.log_llm(model="gpt-4o", input_tokens=1200, output_tokens=400)
|
|
57
|
+
```
|
|
58
|
+
A tool/LLM logged with an `error=` (or a `trace_*` block that raises) is
|
|
59
|
+
automatically recorded as a failure.
|
|
60
|
+
|
|
61
|
+
### 3. Framework adapters
|
|
62
|
+
| Framework | Import | Maturity |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| LangChain | `agentlens.adapters.langchain.AgentLensCallbackHandler` | full |
|
|
65
|
+
| LangGraph | `agentlens.adapters.langgraph.AgentLensCallbackHandler` | full (LangChain callbacks) |
|
|
66
|
+
| LlamaIndex | `agentlens.adapters.llamaindex.AgentLensLlamaHandler` | full |
|
|
67
|
+
| CrewAI | `agentlens.adapters.crewai` (`step_callback`, `instrument_tools`) | best-effort |
|
|
68
|
+
| AutoGen | `agentlens.adapters.autogen.instrument_agent` | best-effort |
|
|
69
|
+
| PydanticAI | `agentlens.adapters.pydanticai.instrument_agent` | best-effort |
|
|
70
|
+
| OpenAI Agents SDK | `agentlens.adapters.openai_agents.install` | best-effort |
|
|
71
|
+
| Strands Agents | `agentlens.adapters.strands.callback_handler` | best-effort |
|
|
72
|
+
| Anthropic Agent SDK | `agentlens.adapters.anthropic_agents.traced_async_tool` | tool-level |
|
|
73
|
+
| MCP | `agentlens.adapters.mcp.instrument_server` / `traced_tool` | tool-level |
|
|
74
|
+
| FastMCP | `agentlens.adapters.fastmcp.instrument_server` | tool-level |
|
|
75
|
+
|
|
76
|
+
Plus a universal `agentlens.adapters.wrap_tool(fn)` / `@traced_tool()` that works
|
|
77
|
+
with any framework or none.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## The dashboard
|
|
82
|
+
|
|
83
|
+
- **Overview** — runs, success/failure rate, cost, p50/p95 latency.
|
|
84
|
+
- **Runs → Run detail** — the **workflow execution graph** (span tree: session →
|
|
85
|
+
workflow → agent → tool/memory/llm/decision), click any node to inspect.
|
|
86
|
+
- **Agent Explorer** — runs, failures, latency, and attributed cost per agent.
|
|
87
|
+
- **Tool Explorer** — most used / slowest / most failed tools, retries.
|
|
88
|
+
- **Memory Explorer** — reads/writes/updates/deletes and read hit-rate.
|
|
89
|
+
- **Failure Explorer** — exceptions, messages, retries, jump to the failing run.
|
|
90
|
+
- **Cost Explorer** — token usage, cost by day, by model, most expensive agents.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Try it now (no keys needed)
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install -e .
|
|
98
|
+
python examples/demo_agent.py
|
|
99
|
+
agentlens ui
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Design
|
|
103
|
+
|
|
104
|
+
Local-first, zero-config, framework-agnostic, async-safe. Observability code
|
|
105
|
+
never crashes your app (emit failures are swallowed). One `Span` model covers
|
|
106
|
+
every event; kind-specific data lives in `attributes`. Cost is computed from an
|
|
107
|
+
editable price book (`agentlens/server/pricing.py`).
|
|
108
|
+
|
|
109
|
+
CLI: `agentlens ui [--port 7180] [--host] [--backend-store-uri PATH]`,
|
|
110
|
+
`agentlens providers`, `agentlens version`. Override the port with
|
|
111
|
+
`AGENTLENS_PORT`.
|
|
112
|
+
|
|
113
|
+
**Single process** writes straight to local SQLite. For **multiple
|
|
114
|
+
processes/workers** sharing one dashboard, run `agentlens ui` once and point each
|
|
115
|
+
worker at it: `agentlens.init(project=..., tracking_uri="http://127.0.0.1:7180")`.
|
|
116
|
+
|
|
117
|
+
See [`Guide.md`](./Guide.md) for the complete reference.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""AgentLens — local-first diagnostics & observability for AI agents.
|
|
2
|
+
|
|
3
|
+
Answers one question: *"Why did my agent behave this way?"*
|
|
4
|
+
|
|
5
|
+
Zero-config quickstart::
|
|
6
|
+
|
|
7
|
+
from agentlens import monitor
|
|
8
|
+
monitor.start() # auto-patches installed LLM SDKs
|
|
9
|
+
# ... run your agent ...
|
|
10
|
+
|
|
11
|
+
Manual instrumentation (framework-agnostic)::
|
|
12
|
+
|
|
13
|
+
import agentlens
|
|
14
|
+
agentlens.init(project="research-bot")
|
|
15
|
+
with agentlens.trace_agent("planner"):
|
|
16
|
+
agentlens.log_tool("web_search", args={"q": "..."}, result=[...], retry_count=1)
|
|
17
|
+
agentlens.log_memory("read", "user_prefs", hit=True)
|
|
18
|
+
agentlens.log_llm(model="gpt-4o", input_tokens=1200, output_tokens=400)
|
|
19
|
+
|
|
20
|
+
Then ``agentlens ui`` to explore the dashboard (default http://127.0.0.1:7180).
|
|
21
|
+
"""
|
|
22
|
+
from . import monitor
|
|
23
|
+
from .client import flush, get_client, init
|
|
24
|
+
from .events import Span, SpanKind
|
|
25
|
+
from .tracing import (
|
|
26
|
+
current_session_id,
|
|
27
|
+
current_trace_id,
|
|
28
|
+
log_decision,
|
|
29
|
+
log_error,
|
|
30
|
+
log_llm,
|
|
31
|
+
log_memory,
|
|
32
|
+
log_retrieval,
|
|
33
|
+
log_tool,
|
|
34
|
+
trace,
|
|
35
|
+
trace_agent,
|
|
36
|
+
trace_session,
|
|
37
|
+
trace_workflow,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"init", "flush", "get_client", "monitor",
|
|
44
|
+
"trace", "trace_session", "trace_workflow", "trace_agent",
|
|
45
|
+
"current_trace_id", "current_session_id",
|
|
46
|
+
"log_llm", "log_tool", "log_memory", "log_decision", "log_retrieval", "log_error",
|
|
47
|
+
"Span", "SpanKind", "__version__",
|
|
48
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Diagnostics for the framework adapters and auto-patchers.
|
|
2
|
+
|
|
3
|
+
Adapters hook into framework internals (callback signatures, method names) that
|
|
4
|
+
move between versions. When they drift the failure is silent — a span just stops
|
|
5
|
+
being captured. These helpers turn that silence into a visible
|
|
6
|
+
``AgentLensWarning`` so version drift is noticed instead of producing empty
|
|
7
|
+
dashboards.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import Iterable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentLensWarning(UserWarning):
|
|
16
|
+
"""Emitted when an adapter/patcher can't hook something it expected to."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def warn(message: str) -> None:
|
|
20
|
+
warnings.warn(f"[agentlens] {message}", AgentLensWarning, stacklevel=3)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_methods(obj: object, methods: Iterable[str], what: str) -> None:
|
|
24
|
+
"""Warn if ``obj`` is missing every one of ``methods`` (so the wrapper would
|
|
25
|
+
silently capture nothing). ``methods`` is treated as "at least one must
|
|
26
|
+
exist"."""
|
|
27
|
+
present = [m for m in methods if callable(getattr(obj, m, None))]
|
|
28
|
+
if not present:
|
|
29
|
+
warn(
|
|
30
|
+
f"{what}: {type(obj).__name__} has none of {list(methods)} — "
|
|
31
|
+
f"that will not be captured (framework version drift?)"
|
|
32
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Framework adapters.
|
|
2
|
+
|
|
3
|
+
Auto-patching (``monitor.start()``) covers the raw LLM SDKs. Agent frameworks
|
|
4
|
+
expose their own callback/hook systems that carry far richer structure (agent
|
|
5
|
+
boundaries, tool calls, decisions), so each framework gets a dedicated adapter
|
|
6
|
+
here. Every adapter is import-time safe: importing this package never requires
|
|
7
|
+
any framework to be installed.
|
|
8
|
+
|
|
9
|
+
Common, framework-agnostic helpers live in ``common`` (``wrap_tool``,
|
|
10
|
+
``traced_tool``) and work everywhere.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .common import traced_tool, wrap_tool
|
|
15
|
+
|
|
16
|
+
__all__ = ["traced_tool", "wrap_tool"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Anthropic Agent SDK (``claude-agent-sdk``) adapter.
|
|
2
|
+
|
|
3
|
+
The Claude Agent SDK runs tools you define and streams messages. The most
|
|
4
|
+
reliable, version-stable instrumentation point is the tools themselves: wrap
|
|
5
|
+
each tool callable so its invocation logs a tool span (args, result, errors).
|
|
6
|
+
Raw Claude API calls the SDK makes are additionally captured by
|
|
7
|
+
``monitor.start()`` auto-patching the underlying ``anthropic`` client.
|
|
8
|
+
|
|
9
|
+
Example::
|
|
10
|
+
|
|
11
|
+
from agentlens.adapters.anthropic_agents import traced_tool
|
|
12
|
+
|
|
13
|
+
@traced_tool("get_weather")
|
|
14
|
+
async def get_weather(args): ...
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import functools
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any, Callable, Optional
|
|
21
|
+
|
|
22
|
+
from .common import traced_tool, wrap_tool
|
|
23
|
+
|
|
24
|
+
instrument_tool = wrap_tool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def traced_async_tool(name: Optional[str] = None):
|
|
28
|
+
"""Async variant of :func:`traced_tool` for ``async def`` tool handlers."""
|
|
29
|
+
def deco(fn: Callable) -> Callable:
|
|
30
|
+
tool_name = name or getattr(fn, "__name__", "tool")
|
|
31
|
+
|
|
32
|
+
@functools.wraps(fn)
|
|
33
|
+
async def wrapper(*args, **kwargs):
|
|
34
|
+
from ..tracing import log_tool
|
|
35
|
+
|
|
36
|
+
t0 = time.time()
|
|
37
|
+
try:
|
|
38
|
+
result = await fn(*args, **kwargs)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
log_tool(tool_name, args={"args": args, "kwargs": kwargs}, error=e,
|
|
41
|
+
duration_ms=(time.time() - t0) * 1000.0)
|
|
42
|
+
raise
|
|
43
|
+
log_tool(tool_name, args={"args": args, "kwargs": kwargs}, result=result,
|
|
44
|
+
duration_ms=(time.time() - t0) * 1000.0)
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
return wrapper
|
|
48
|
+
|
|
49
|
+
return deco
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["traced_tool", "traced_async_tool", "wrap_tool", "instrument_tool"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""AutoGen (AG2 / pyautogen) adapter.
|
|
2
|
+
|
|
3
|
+
AutoGen conversable agents support registered reply/hook functions. This adapter
|
|
4
|
+
instruments an agent so each generated reply logs an agent span, and provides a
|
|
5
|
+
tool wrapper for function-calling tools.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from agentlens.adapters.autogen import instrument_agent
|
|
10
|
+
instrument_agent(assistant)
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .. import client as _client
|
|
17
|
+
from ..events import Span, SpanKind, jsonable
|
|
18
|
+
from .common import wrap_tool
|
|
19
|
+
|
|
20
|
+
instrument_tool = wrap_tool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def instrument_agent(agent: Any) -> Any:
|
|
24
|
+
"""Hook an AutoGen agent so its replies log agent spans. Uses
|
|
25
|
+
``register_hook('process_message_before_send')`` when available, else wraps
|
|
26
|
+
``generate_reply``."""
|
|
27
|
+
name = getattr(agent, "name", None) or type(agent).__name__
|
|
28
|
+
|
|
29
|
+
def _log(content: Any) -> None:
|
|
30
|
+
try:
|
|
31
|
+
ev = Span(project=_client.get_project(), kind=SpanKind.AGENT.value, name=name,
|
|
32
|
+
attributes={"agent_name": name, "output": jsonable(content)})
|
|
33
|
+
_client.get_client().log_event(ev.finish().model_dump())
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
if hasattr(agent, "register_hook"):
|
|
38
|
+
def hook(sender=None, message=None, recipient=None, silent=None, **kw):
|
|
39
|
+
_log(message)
|
|
40
|
+
return message
|
|
41
|
+
try:
|
|
42
|
+
agent.register_hook("process_message_before_send", hook)
|
|
43
|
+
return agent
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# fallback: wrap generate_reply
|
|
48
|
+
original = getattr(agent, "generate_reply", None)
|
|
49
|
+
if callable(original):
|
|
50
|
+
import functools
|
|
51
|
+
|
|
52
|
+
@functools.wraps(original)
|
|
53
|
+
def wrapper(*a, **k):
|
|
54
|
+
reply = original(*a, **k)
|
|
55
|
+
_log(reply)
|
|
56
|
+
return reply
|
|
57
|
+
try:
|
|
58
|
+
agent.generate_reply = wrapper
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
return agent
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Framework-agnostic instrumentation helpers.
|
|
2
|
+
|
|
3
|
+
These work with any agent framework (or none): wrap a plain callable as a tool
|
|
4
|
+
so each invocation logs a ``tool`` span — including its arguments, result,
|
|
5
|
+
duration, and any exception (which automatically feeds the Failure Explorer).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Callable, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def wrap_tool(fn: Callable, name: Optional[str] = None) -> Callable:
|
|
15
|
+
"""Return a wrapped callable that logs a ``tool`` span per call. Re-raises
|
|
16
|
+
the original exception after recording it, so behavior is unchanged."""
|
|
17
|
+
tool_name = name or getattr(fn, "__name__", None) or "tool"
|
|
18
|
+
|
|
19
|
+
@functools.wraps(fn)
|
|
20
|
+
def wrapper(*args, **kwargs):
|
|
21
|
+
from ..tracing import log_tool
|
|
22
|
+
|
|
23
|
+
t0 = time.time()
|
|
24
|
+
try:
|
|
25
|
+
result = fn(*args, **kwargs)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
log_tool(tool_name, args={"args": args, "kwargs": kwargs}, error=e,
|
|
28
|
+
duration_ms=(time.time() - t0) * 1000.0)
|
|
29
|
+
raise
|
|
30
|
+
log_tool(tool_name, args={"args": args, "kwargs": kwargs}, result=result,
|
|
31
|
+
duration_ms=(time.time() - t0) * 1000.0)
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
return wrapper
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def traced_tool(name: Optional[str] = None):
|
|
38
|
+
"""Decorator form of :func:`wrap_tool`.
|
|
39
|
+
|
|
40
|
+
@traced_tool()
|
|
41
|
+
def web_search(q): ...
|
|
42
|
+
"""
|
|
43
|
+
def deco(fn: Callable) -> Callable:
|
|
44
|
+
return wrap_tool(fn, name)
|
|
45
|
+
|
|
46
|
+
return deco
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""CrewAI adapter.
|
|
2
|
+
|
|
3
|
+
CrewAI agents/crews accept ``step_callback`` and ``task_callback`` hooks, and
|
|
4
|
+
their tools are plain callables. This adapter provides:
|
|
5
|
+
|
|
6
|
+
* ``step_callback`` / ``task_callback`` — pass to ``Agent(...)`` / ``Crew(...)``
|
|
7
|
+
to log agent-step and task spans,
|
|
8
|
+
* ``instrument_tools(tools)`` — wrap a list of tools so each invocation logs a
|
|
9
|
+
tool span (uses the framework-agnostic ``wrap_tool``).
|
|
10
|
+
|
|
11
|
+
Example::
|
|
12
|
+
|
|
13
|
+
from agentlens.adapters.crewai import step_callback, instrument_tools
|
|
14
|
+
agent = Agent(..., tools=instrument_tools(tools), step_callback=step_callback)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, List
|
|
19
|
+
|
|
20
|
+
from ..events import SpanKind, jsonable
|
|
21
|
+
from ..tracing import _log
|
|
22
|
+
from .common import wrap_tool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def step_callback(step: Any) -> None:
|
|
26
|
+
"""Log a CrewAI agent step as an agent span (best-effort field extraction).
|
|
27
|
+
|
|
28
|
+
Uses the contextvar-aware logger so the span nests under whatever trace is
|
|
29
|
+
active — wrap the crew kickoff in ``agentlens.trace_workflow(...)`` to keep a
|
|
30
|
+
crew's steps in one run instead of scattering them into top-level rows."""
|
|
31
|
+
try:
|
|
32
|
+
_log(SpanKind.AGENT.value, "crewai.step",
|
|
33
|
+
{"agent_name": getattr(step, "agent", None) and str(getattr(step, "agent")),
|
|
34
|
+
"output": jsonable(getattr(step, "output", None) or step)})
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def task_callback(task_output: Any) -> None:
|
|
40
|
+
"""Log a CrewAI task result as a workflow span (nested under the active trace)."""
|
|
41
|
+
try:
|
|
42
|
+
_log(SpanKind.WORKFLOW.value, "crewai.task",
|
|
43
|
+
{"output": jsonable(getattr(task_output, "raw", None) or task_output)})
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def instrument_tools(tools: List[Any]) -> List[Any]:
|
|
49
|
+
"""Wrap each tool's underlying callable so calls log tool spans. Works for
|
|
50
|
+
CrewAI ``BaseTool`` instances (wraps ``_run``/``run``) and plain functions."""
|
|
51
|
+
out = []
|
|
52
|
+
for t in tools or []:
|
|
53
|
+
for attr in ("_run", "run", "func"):
|
|
54
|
+
fn = getattr(t, attr, None)
|
|
55
|
+
if callable(fn):
|
|
56
|
+
name = getattr(t, "name", None) or getattr(fn, "__name__", "tool")
|
|
57
|
+
try:
|
|
58
|
+
setattr(t, attr, wrap_tool(fn, name))
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
break
|
|
62
|
+
else:
|
|
63
|
+
if callable(t):
|
|
64
|
+
t = wrap_tool(t)
|
|
65
|
+
out.append(t)
|
|
66
|
+
return out
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""FastMCP adapter.
|
|
2
|
+
|
|
3
|
+
FastMCP keeps registered tools in a tool manager; this adapter wraps each tool's
|
|
4
|
+
underlying function so calls log tool spans. Call ``instrument_server(mcp)``
|
|
5
|
+
after your tools are registered.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from agentlens.adapters.fastmcp import instrument_server
|
|
11
|
+
|
|
12
|
+
mcp = FastMCP("demo")
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
def add(a: int, b: int) -> int: return a + b
|
|
16
|
+
|
|
17
|
+
instrument_server(mcp)
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from .anthropic_agents import traced_async_tool
|
|
24
|
+
from .common import traced_tool, wrap_tool
|
|
25
|
+
|
|
26
|
+
instrument_tool = wrap_tool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def instrument_server(mcp: Any) -> Any:
|
|
30
|
+
"""Wrap each registered FastMCP tool's function with a tool-span logger."""
|
|
31
|
+
manager = getattr(mcp, "_tool_manager", None) or getattr(mcp, "tool_manager", None)
|
|
32
|
+
tools = None
|
|
33
|
+
if manager is not None:
|
|
34
|
+
tools = getattr(manager, "_tools", None) or getattr(manager, "tools", None)
|
|
35
|
+
tools = tools if tools is not None else getattr(mcp, "_tools", None)
|
|
36
|
+
|
|
37
|
+
items = []
|
|
38
|
+
if isinstance(tools, dict):
|
|
39
|
+
items = list(tools.items())
|
|
40
|
+
elif isinstance(tools, (list, tuple)):
|
|
41
|
+
items = [(getattr(t, "name", i), t) for i, t in enumerate(tools)]
|
|
42
|
+
|
|
43
|
+
for key, tool in items:
|
|
44
|
+
for attr in ("fn", "func", "handler", "callback"):
|
|
45
|
+
fn = getattr(tool, attr, None)
|
|
46
|
+
if callable(fn) and not getattr(fn, "__agentlens_patched__", False):
|
|
47
|
+
name = getattr(tool, "name", None) or str(key)
|
|
48
|
+
try:
|
|
49
|
+
setattr(tool, attr, wrap_tool(fn, name))
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
break
|
|
53
|
+
return mcp
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["traced_tool", "traced_async_tool", "wrap_tool", "instrument_tool", "instrument_server"]
|