ai-watcher 0.2.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.
- ai_watcher-0.2.0/PKG-INFO +152 -0
- ai_watcher-0.2.0/README.md +143 -0
- ai_watcher-0.2.0/ai_watcher.egg-info/PKG-INFO +152 -0
- ai_watcher-0.2.0/ai_watcher.egg-info/SOURCES.txt +13 -0
- ai_watcher-0.2.0/ai_watcher.egg-info/dependency_links.txt +1 -0
- ai_watcher-0.2.0/ai_watcher.egg-info/top_level.txt +1 -0
- ai_watcher-0.2.0/aiwatcher/__init__.py +24 -0
- ai_watcher-0.2.0/aiwatcher/chain.py +29 -0
- ai_watcher-0.2.0/aiwatcher/client.py +253 -0
- ai_watcher-0.2.0/aiwatcher/decorators.py +67 -0
- ai_watcher-0.2.0/aiwatcher/exceptions.py +21 -0
- ai_watcher-0.2.0/aiwatcher/fingerprint.py +25 -0
- ai_watcher-0.2.0/aiwatcher/session.py +162 -0
- ai_watcher-0.2.0/pyproject.toml +19 -0
- ai_watcher-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-watcher
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AI agent observability and control — AgentWatch SDK
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://agentwatch.vercel.app
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# ai-watcher
|
|
11
|
+
|
|
12
|
+
Python SDK for [AgentWatch](https://agentwatch.vercel.app) — AI agent observability and control.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install ai-watcher
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export AGENTWATCH_API_KEY=aw_live_...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### One-liner wrapper (Lambda / serverless)
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from agentwatch import track_llm
|
|
30
|
+
|
|
31
|
+
result = track_llm(
|
|
32
|
+
'classify-document',
|
|
33
|
+
lambda: openai.chat.completions.create(...),
|
|
34
|
+
{
|
|
35
|
+
'human_id': customer_id,
|
|
36
|
+
'agent_name': 'doc-classifier',
|
|
37
|
+
'model': 'gpt-4o',
|
|
38
|
+
'framework': 'aws-lambda',
|
|
39
|
+
'session_name': 'Classify: invoice',
|
|
40
|
+
'input': {'doc_type': 'invoice', 'pages': 3},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Chained model calls (multi-step pipeline)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from agentwatch import track_chain
|
|
49
|
+
|
|
50
|
+
results = track_chain(
|
|
51
|
+
steps=[
|
|
52
|
+
{
|
|
53
|
+
'action': 'extract',
|
|
54
|
+
'model': 'gpt-4o',
|
|
55
|
+
'fn': lambda: openai.chat.completions.create(...),
|
|
56
|
+
'input': {'pages': 3},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
'action': 'classify',
|
|
60
|
+
'model': 'claude-sonnet-4-20250514',
|
|
61
|
+
'fn': lambda: anthropic.messages.create(...),
|
|
62
|
+
'input': {'text': '...'},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
opts={
|
|
66
|
+
'human_id': customer_id,
|
|
67
|
+
'agent_name': 'shipping-pipeline',
|
|
68
|
+
'session_name': 'Process Shipping Document',
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Both wrappers are zero-dependency and **never break your app** — if AgentWatch
|
|
74
|
+
is unreachable, the underlying function still runs.
|
|
75
|
+
|
|
76
|
+
### Session context manager (full control)
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from agentwatch import Session, tool
|
|
80
|
+
|
|
81
|
+
with Session(human_id="you@example.com") as session:
|
|
82
|
+
|
|
83
|
+
@tool(session)
|
|
84
|
+
def search_web(query: str) -> str:
|
|
85
|
+
return "results..."
|
|
86
|
+
|
|
87
|
+
result = search_web("AI agent security")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Full Session config
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from agentwatch import Session, tool
|
|
94
|
+
|
|
95
|
+
with Session(
|
|
96
|
+
api_key="aw_live_...",
|
|
97
|
+
human_id="sarah@acme.com",
|
|
98
|
+
agent_name="billing-agent",
|
|
99
|
+
agent_version="2.0.0",
|
|
100
|
+
model="claude-sonnet-4-20250514",
|
|
101
|
+
system_prompt="You are a billing assistant.",
|
|
102
|
+
tools=["send_invoice", "fetch_invoice"],
|
|
103
|
+
framework="langchain",
|
|
104
|
+
) as session:
|
|
105
|
+
|
|
106
|
+
@tool(session, action_class="send", data_scope="financial")
|
|
107
|
+
def send_invoice(recipient: str, amount: float) -> dict:
|
|
108
|
+
return {"sent": True}
|
|
109
|
+
|
|
110
|
+
send_invoice("client@acme.com", 1200.00)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## API reference
|
|
114
|
+
|
|
115
|
+
### `track_llm(action, fn, opts)`
|
|
116
|
+
|
|
117
|
+
| Field | Type | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `action` | `str` | Name of the operation (e.g. `"classify-document"`) |
|
|
120
|
+
| `fn` | `Callable` | Zero-argument callable that makes the model call |
|
|
121
|
+
| `opts` | `dict` | Session options (see below) |
|
|
122
|
+
|
|
123
|
+
### `track_chain(steps, opts)`
|
|
124
|
+
|
|
125
|
+
Each step: `{'action': str, 'fn': callable, 'model': str, 'input': dict}`
|
|
126
|
+
|
|
127
|
+
### Common opts fields
|
|
128
|
+
|
|
129
|
+
| Key | Default | Description |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `human_id` | `"anonymous"` | User or customer identifier |
|
|
132
|
+
| `agent_name` | `"agent"` | Name of the agent/pipeline |
|
|
133
|
+
| `model` | `"unknown"` | Default model (overridden per step in `track_chain`) |
|
|
134
|
+
| `framework` | `"python"` | Runtime (e.g. `"aws-lambda"`, `"langchain"`) |
|
|
135
|
+
| `session_name` | action name | Human-readable session title in the dashboard |
|
|
136
|
+
| `input` | `None` | Input metadata to log with the event |
|
|
137
|
+
|
|
138
|
+
## Exceptions
|
|
139
|
+
|
|
140
|
+
| Exception | When raised |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `ExecutionBlockedException` | Policy blocked the tool call |
|
|
143
|
+
| `HitlDeniedException` | Human reviewer denied the action |
|
|
144
|
+
| `AgentwatchAPIError` | Non-2xx response from the API |
|
|
145
|
+
| `AgentwatchConnectionError` | Network error after retries |
|
|
146
|
+
|
|
147
|
+
## Environment variables
|
|
148
|
+
|
|
149
|
+
| Variable | Default | Description |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `AGENTWATCH_API_KEY` | — | Required. Your `aw_live_...` key. |
|
|
152
|
+
| `AGENTWATCH_API_URL` | `https://agentwatch.vercel.app` | Override for self-hosted. |
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# ai-watcher
|
|
2
|
+
|
|
3
|
+
Python SDK for [AgentWatch](https://agentwatch.vercel.app) — AI agent observability and control.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ai-watcher
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export AGENTWATCH_API_KEY=aw_live_...
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### One-liner wrapper (Lambda / serverless)
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from agentwatch import track_llm
|
|
21
|
+
|
|
22
|
+
result = track_llm(
|
|
23
|
+
'classify-document',
|
|
24
|
+
lambda: openai.chat.completions.create(...),
|
|
25
|
+
{
|
|
26
|
+
'human_id': customer_id,
|
|
27
|
+
'agent_name': 'doc-classifier',
|
|
28
|
+
'model': 'gpt-4o',
|
|
29
|
+
'framework': 'aws-lambda',
|
|
30
|
+
'session_name': 'Classify: invoice',
|
|
31
|
+
'input': {'doc_type': 'invoice', 'pages': 3},
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Chained model calls (multi-step pipeline)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from agentwatch import track_chain
|
|
40
|
+
|
|
41
|
+
results = track_chain(
|
|
42
|
+
steps=[
|
|
43
|
+
{
|
|
44
|
+
'action': 'extract',
|
|
45
|
+
'model': 'gpt-4o',
|
|
46
|
+
'fn': lambda: openai.chat.completions.create(...),
|
|
47
|
+
'input': {'pages': 3},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
'action': 'classify',
|
|
51
|
+
'model': 'claude-sonnet-4-20250514',
|
|
52
|
+
'fn': lambda: anthropic.messages.create(...),
|
|
53
|
+
'input': {'text': '...'},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
opts={
|
|
57
|
+
'human_id': customer_id,
|
|
58
|
+
'agent_name': 'shipping-pipeline',
|
|
59
|
+
'session_name': 'Process Shipping Document',
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Both wrappers are zero-dependency and **never break your app** — if AgentWatch
|
|
65
|
+
is unreachable, the underlying function still runs.
|
|
66
|
+
|
|
67
|
+
### Session context manager (full control)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from agentwatch import Session, tool
|
|
71
|
+
|
|
72
|
+
with Session(human_id="you@example.com") as session:
|
|
73
|
+
|
|
74
|
+
@tool(session)
|
|
75
|
+
def search_web(query: str) -> str:
|
|
76
|
+
return "results..."
|
|
77
|
+
|
|
78
|
+
result = search_web("AI agent security")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Full Session config
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from agentwatch import Session, tool
|
|
85
|
+
|
|
86
|
+
with Session(
|
|
87
|
+
api_key="aw_live_...",
|
|
88
|
+
human_id="sarah@acme.com",
|
|
89
|
+
agent_name="billing-agent",
|
|
90
|
+
agent_version="2.0.0",
|
|
91
|
+
model="claude-sonnet-4-20250514",
|
|
92
|
+
system_prompt="You are a billing assistant.",
|
|
93
|
+
tools=["send_invoice", "fetch_invoice"],
|
|
94
|
+
framework="langchain",
|
|
95
|
+
) as session:
|
|
96
|
+
|
|
97
|
+
@tool(session, action_class="send", data_scope="financial")
|
|
98
|
+
def send_invoice(recipient: str, amount: float) -> dict:
|
|
99
|
+
return {"sent": True}
|
|
100
|
+
|
|
101
|
+
send_invoice("client@acme.com", 1200.00)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API reference
|
|
105
|
+
|
|
106
|
+
### `track_llm(action, fn, opts)`
|
|
107
|
+
|
|
108
|
+
| Field | Type | Description |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `action` | `str` | Name of the operation (e.g. `"classify-document"`) |
|
|
111
|
+
| `fn` | `Callable` | Zero-argument callable that makes the model call |
|
|
112
|
+
| `opts` | `dict` | Session options (see below) |
|
|
113
|
+
|
|
114
|
+
### `track_chain(steps, opts)`
|
|
115
|
+
|
|
116
|
+
Each step: `{'action': str, 'fn': callable, 'model': str, 'input': dict}`
|
|
117
|
+
|
|
118
|
+
### Common opts fields
|
|
119
|
+
|
|
120
|
+
| Key | Default | Description |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `human_id` | `"anonymous"` | User or customer identifier |
|
|
123
|
+
| `agent_name` | `"agent"` | Name of the agent/pipeline |
|
|
124
|
+
| `model` | `"unknown"` | Default model (overridden per step in `track_chain`) |
|
|
125
|
+
| `framework` | `"python"` | Runtime (e.g. `"aws-lambda"`, `"langchain"`) |
|
|
126
|
+
| `session_name` | action name | Human-readable session title in the dashboard |
|
|
127
|
+
| `input` | `None` | Input metadata to log with the event |
|
|
128
|
+
|
|
129
|
+
## Exceptions
|
|
130
|
+
|
|
131
|
+
| Exception | When raised |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `ExecutionBlockedException` | Policy blocked the tool call |
|
|
134
|
+
| `HitlDeniedException` | Human reviewer denied the action |
|
|
135
|
+
| `AgentwatchAPIError` | Non-2xx response from the API |
|
|
136
|
+
| `AgentwatchConnectionError` | Network error after retries |
|
|
137
|
+
|
|
138
|
+
## Environment variables
|
|
139
|
+
|
|
140
|
+
| Variable | Default | Description |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `AGENTWATCH_API_KEY` | — | Required. Your `aw_live_...` key. |
|
|
143
|
+
| `AGENTWATCH_API_URL` | `https://agentwatch.vercel.app` | Override for self-hosted. |
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-watcher
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AI agent observability and control — AgentWatch SDK
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://agentwatch.vercel.app
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# ai-watcher
|
|
11
|
+
|
|
12
|
+
Python SDK for [AgentWatch](https://agentwatch.vercel.app) — AI agent observability and control.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install ai-watcher
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export AGENTWATCH_API_KEY=aw_live_...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### One-liner wrapper (Lambda / serverless)
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from agentwatch import track_llm
|
|
30
|
+
|
|
31
|
+
result = track_llm(
|
|
32
|
+
'classify-document',
|
|
33
|
+
lambda: openai.chat.completions.create(...),
|
|
34
|
+
{
|
|
35
|
+
'human_id': customer_id,
|
|
36
|
+
'agent_name': 'doc-classifier',
|
|
37
|
+
'model': 'gpt-4o',
|
|
38
|
+
'framework': 'aws-lambda',
|
|
39
|
+
'session_name': 'Classify: invoice',
|
|
40
|
+
'input': {'doc_type': 'invoice', 'pages': 3},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Chained model calls (multi-step pipeline)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from agentwatch import track_chain
|
|
49
|
+
|
|
50
|
+
results = track_chain(
|
|
51
|
+
steps=[
|
|
52
|
+
{
|
|
53
|
+
'action': 'extract',
|
|
54
|
+
'model': 'gpt-4o',
|
|
55
|
+
'fn': lambda: openai.chat.completions.create(...),
|
|
56
|
+
'input': {'pages': 3},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
'action': 'classify',
|
|
60
|
+
'model': 'claude-sonnet-4-20250514',
|
|
61
|
+
'fn': lambda: anthropic.messages.create(...),
|
|
62
|
+
'input': {'text': '...'},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
opts={
|
|
66
|
+
'human_id': customer_id,
|
|
67
|
+
'agent_name': 'shipping-pipeline',
|
|
68
|
+
'session_name': 'Process Shipping Document',
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Both wrappers are zero-dependency and **never break your app** — if AgentWatch
|
|
74
|
+
is unreachable, the underlying function still runs.
|
|
75
|
+
|
|
76
|
+
### Session context manager (full control)
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from agentwatch import Session, tool
|
|
80
|
+
|
|
81
|
+
with Session(human_id="you@example.com") as session:
|
|
82
|
+
|
|
83
|
+
@tool(session)
|
|
84
|
+
def search_web(query: str) -> str:
|
|
85
|
+
return "results..."
|
|
86
|
+
|
|
87
|
+
result = search_web("AI agent security")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Full Session config
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from agentwatch import Session, tool
|
|
94
|
+
|
|
95
|
+
with Session(
|
|
96
|
+
api_key="aw_live_...",
|
|
97
|
+
human_id="sarah@acme.com",
|
|
98
|
+
agent_name="billing-agent",
|
|
99
|
+
agent_version="2.0.0",
|
|
100
|
+
model="claude-sonnet-4-20250514",
|
|
101
|
+
system_prompt="You are a billing assistant.",
|
|
102
|
+
tools=["send_invoice", "fetch_invoice"],
|
|
103
|
+
framework="langchain",
|
|
104
|
+
) as session:
|
|
105
|
+
|
|
106
|
+
@tool(session, action_class="send", data_scope="financial")
|
|
107
|
+
def send_invoice(recipient: str, amount: float) -> dict:
|
|
108
|
+
return {"sent": True}
|
|
109
|
+
|
|
110
|
+
send_invoice("client@acme.com", 1200.00)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## API reference
|
|
114
|
+
|
|
115
|
+
### `track_llm(action, fn, opts)`
|
|
116
|
+
|
|
117
|
+
| Field | Type | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `action` | `str` | Name of the operation (e.g. `"classify-document"`) |
|
|
120
|
+
| `fn` | `Callable` | Zero-argument callable that makes the model call |
|
|
121
|
+
| `opts` | `dict` | Session options (see below) |
|
|
122
|
+
|
|
123
|
+
### `track_chain(steps, opts)`
|
|
124
|
+
|
|
125
|
+
Each step: `{'action': str, 'fn': callable, 'model': str, 'input': dict}`
|
|
126
|
+
|
|
127
|
+
### Common opts fields
|
|
128
|
+
|
|
129
|
+
| Key | Default | Description |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `human_id` | `"anonymous"` | User or customer identifier |
|
|
132
|
+
| `agent_name` | `"agent"` | Name of the agent/pipeline |
|
|
133
|
+
| `model` | `"unknown"` | Default model (overridden per step in `track_chain`) |
|
|
134
|
+
| `framework` | `"python"` | Runtime (e.g. `"aws-lambda"`, `"langchain"`) |
|
|
135
|
+
| `session_name` | action name | Human-readable session title in the dashboard |
|
|
136
|
+
| `input` | `None` | Input metadata to log with the event |
|
|
137
|
+
|
|
138
|
+
## Exceptions
|
|
139
|
+
|
|
140
|
+
| Exception | When raised |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `ExecutionBlockedException` | Policy blocked the tool call |
|
|
143
|
+
| `HitlDeniedException` | Human reviewer denied the action |
|
|
144
|
+
| `AgentwatchAPIError` | Non-2xx response from the API |
|
|
145
|
+
| `AgentwatchConnectionError` | Network error after retries |
|
|
146
|
+
|
|
147
|
+
## Environment variables
|
|
148
|
+
|
|
149
|
+
| Variable | Default | Description |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `AGENTWATCH_API_KEY` | — | Required. Your `aw_live_...` key. |
|
|
152
|
+
| `AGENTWATCH_API_URL` | `https://agentwatch.vercel.app` | Override for self-hosted. |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ai_watcher.egg-info/PKG-INFO
|
|
4
|
+
ai_watcher.egg-info/SOURCES.txt
|
|
5
|
+
ai_watcher.egg-info/dependency_links.txt
|
|
6
|
+
ai_watcher.egg-info/top_level.txt
|
|
7
|
+
aiwatcher/__init__.py
|
|
8
|
+
aiwatcher/chain.py
|
|
9
|
+
aiwatcher/client.py
|
|
10
|
+
aiwatcher/decorators.py
|
|
11
|
+
aiwatcher/exceptions.py
|
|
12
|
+
aiwatcher/fingerprint.py
|
|
13
|
+
aiwatcher/session.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiwatcher
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .client import track_chain, track_llm
|
|
2
|
+
from .decorators import tool
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
AgentwatchAPIError,
|
|
5
|
+
AgentwatchConnectionError,
|
|
6
|
+
AgentwatchError,
|
|
7
|
+
ExecutionBlockedException,
|
|
8
|
+
HitlDeniedException,
|
|
9
|
+
)
|
|
10
|
+
from .session import Session
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'Session',
|
|
14
|
+
'tool',
|
|
15
|
+
'track_llm',
|
|
16
|
+
'track_chain',
|
|
17
|
+
'ExecutionBlockedException',
|
|
18
|
+
'HitlDeniedException',
|
|
19
|
+
'AgentwatchError',
|
|
20
|
+
'AgentwatchAPIError',
|
|
21
|
+
'AgentwatchConnectionError',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
__version__ = '0.2.0'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def stable_json(value: Any) -> str:
|
|
7
|
+
"""Compact, key-sorted JSON — must match TypeScript stableStringify in src/lib/chain.ts."""
|
|
8
|
+
return json.dumps(value, sort_keys=True, separators=(',', ':'))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_entry_hash(
|
|
12
|
+
session_id: str,
|
|
13
|
+
sequence_num: int,
|
|
14
|
+
event_type: str,
|
|
15
|
+
action: Optional[str],
|
|
16
|
+
input_data: Any,
|
|
17
|
+
output_data: Any,
|
|
18
|
+
prev_hash: str,
|
|
19
|
+
) -> str:
|
|
20
|
+
payload = '|'.join([
|
|
21
|
+
session_id,
|
|
22
|
+
str(sequence_num),
|
|
23
|
+
event_type,
|
|
24
|
+
action or '',
|
|
25
|
+
stable_json(input_data), # json.dumps(None) → 'null', matches TS stableStringify(null)
|
|
26
|
+
stable_json(output_data),
|
|
27
|
+
prev_hash,
|
|
28
|
+
])
|
|
29
|
+
return hashlib.sha256(payload.encode('utf-8')).hexdigest()
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.request
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
|
7
|
+
|
|
8
|
+
from .exceptions import AgentwatchAPIError, AgentwatchConnectionError
|
|
9
|
+
|
|
10
|
+
T = TypeVar('T')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentwatchClient:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_url: str,
|
|
17
|
+
api_key: str,
|
|
18
|
+
max_retries: int = 3,
|
|
19
|
+
timeout: int = 30,
|
|
20
|
+
):
|
|
21
|
+
self.api_url = api_url.rstrip('/')
|
|
22
|
+
self.api_key = api_key
|
|
23
|
+
self.max_retries = max_retries
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
|
|
26
|
+
def post(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
27
|
+
url = f"{self.api_url}{path}"
|
|
28
|
+
# Keep falsy values (0, False, [], {}) but drop None
|
|
29
|
+
clean = {k: v for k, v in data.items() if v is not None}
|
|
30
|
+
payload = json.dumps(clean).encode('utf-8')
|
|
31
|
+
|
|
32
|
+
last_exc: Optional[Exception] = None
|
|
33
|
+
for attempt in range(self.max_retries):
|
|
34
|
+
try:
|
|
35
|
+
req = urllib.request.Request(
|
|
36
|
+
url,
|
|
37
|
+
data=payload,
|
|
38
|
+
headers={
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'X-API-Key': self.api_key,
|
|
41
|
+
},
|
|
42
|
+
method='POST',
|
|
43
|
+
)
|
|
44
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
45
|
+
return json.loads(resp.read().decode('utf-8'))
|
|
46
|
+
except urllib.error.HTTPError as e:
|
|
47
|
+
body = e.read().decode('utf-8', errors='replace')
|
|
48
|
+
raise AgentwatchAPIError(e.code, body) from e
|
|
49
|
+
except urllib.error.URLError as e:
|
|
50
|
+
last_exc = e
|
|
51
|
+
if attempt < self.max_retries - 1:
|
|
52
|
+
time.sleep(min(2 ** attempt, 8))
|
|
53
|
+
|
|
54
|
+
raise AgentwatchConnectionError(
|
|
55
|
+
f"POST {path} failed after {self.max_retries} attempts: {last_exc}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def get(self, path: str) -> Dict[str, Any]:
|
|
59
|
+
url = f"{self.api_url}{path}"
|
|
60
|
+
req = urllib.request.Request(
|
|
61
|
+
url,
|
|
62
|
+
headers={'X-API-Key': self.api_key},
|
|
63
|
+
method='GET',
|
|
64
|
+
)
|
|
65
|
+
try:
|
|
66
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
67
|
+
return json.loads(resp.read().decode('utf-8'))
|
|
68
|
+
except urllib.error.HTTPError as e:
|
|
69
|
+
body = e.read().decode('utf-8', errors='replace')
|
|
70
|
+
raise AgentwatchAPIError(e.code, body) from e
|
|
71
|
+
|
|
72
|
+
def poll_hitl(
|
|
73
|
+
self,
|
|
74
|
+
hitl_id: str,
|
|
75
|
+
timeout: int = 900,
|
|
76
|
+
poll_interval: int = 5,
|
|
77
|
+
) -> Dict[str, Any]:
|
|
78
|
+
path = f"/api/v1/ingest/hitl/{hitl_id}/status"
|
|
79
|
+
deadline = time.monotonic() + timeout
|
|
80
|
+
|
|
81
|
+
while time.monotonic() < deadline:
|
|
82
|
+
try:
|
|
83
|
+
data = self.get(path)
|
|
84
|
+
if data.get('status') != 'pending':
|
|
85
|
+
return data
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
remaining = deadline - time.monotonic()
|
|
89
|
+
if remaining <= 0:
|
|
90
|
+
break
|
|
91
|
+
time.sleep(min(poll_interval, max(remaining, 0)))
|
|
92
|
+
|
|
93
|
+
return {'status': 'expired'}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── Convenience wrappers ──────────────────────────────────────────────────────
|
|
97
|
+
# Lazy import of Session inside functions to avoid the circular import that
|
|
98
|
+
# would occur if imported at module level (session.py imports AgentwatchClient).
|
|
99
|
+
|
|
100
|
+
def track_llm(
|
|
101
|
+
action: str,
|
|
102
|
+
fn: Callable[[], T],
|
|
103
|
+
opts: Optional[Dict[str, Any]] = None,
|
|
104
|
+
) -> T:
|
|
105
|
+
"""
|
|
106
|
+
Wrap any synchronous AI/LLM call with AgentWatch telemetry.
|
|
107
|
+
Zero dependencies. Never breaks the host app.
|
|
108
|
+
|
|
109
|
+
Usage:
|
|
110
|
+
from aiwatcher import track_llm
|
|
111
|
+
|
|
112
|
+
result = track_llm('classify-document',
|
|
113
|
+
lambda: openai.chat.completions.create(...),
|
|
114
|
+
{
|
|
115
|
+
'human_id': customer_id,
|
|
116
|
+
'agent_name': 'doc-classifier',
|
|
117
|
+
'model': 'gpt-4o',
|
|
118
|
+
'framework': 'aws-lambda',
|
|
119
|
+
'session_name': 'Classify: invoice',
|
|
120
|
+
'input': {'doc_type': 'invoice', 'pages': 3}
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
"""
|
|
124
|
+
from .session import Session # lazy — session.py imports this module; defer to call time
|
|
125
|
+
|
|
126
|
+
o = opts or {}
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
session = Session(
|
|
130
|
+
human_id=o.get('human_id', 'anonymous'),
|
|
131
|
+
agent_name=o.get('agent_name', 'agent'),
|
|
132
|
+
framework=o.get('framework', 'python'),
|
|
133
|
+
model=o.get('model', 'unknown'),
|
|
134
|
+
metadata={'session_name': o.get('session_name', action)},
|
|
135
|
+
product_context=o.get('product_context'),
|
|
136
|
+
)
|
|
137
|
+
session.__enter__()
|
|
138
|
+
print(f"[AgentWatch] session/start: {session.session_id}", file=sys.stderr)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(f"[AgentWatch] ERROR in {action}: {type(e).__name__}: {e}", file=sys.stderr)
|
|
141
|
+
return fn() # AgentWatch down — still run fn()
|
|
142
|
+
|
|
143
|
+
t0 = time.monotonic()
|
|
144
|
+
caught: Optional[BaseException] = None
|
|
145
|
+
result: Any = None
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
result = fn()
|
|
149
|
+
latency_ms = int((time.monotonic() - t0) * 1000)
|
|
150
|
+
try:
|
|
151
|
+
session.log_event(
|
|
152
|
+
event_type='llm_call',
|
|
153
|
+
agent_name=session.agent_name,
|
|
154
|
+
action=action,
|
|
155
|
+
input_data=o.get('input'),
|
|
156
|
+
output_data={'success': True},
|
|
157
|
+
model=o.get('model'),
|
|
158
|
+
latency_ms=latency_ms,
|
|
159
|
+
product_context=o.get('product_context'),
|
|
160
|
+
outcome_context=o.get('outcome_context'),
|
|
161
|
+
)
|
|
162
|
+
print(f"[AgentWatch] event posted seq={session.sequence_num}: {action}", file=sys.stderr)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"[AgentWatch] ERROR in {action}: {type(e).__name__}: {e}", file=sys.stderr)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
caught = e
|
|
167
|
+
finally:
|
|
168
|
+
try:
|
|
169
|
+
session.__exit__(type(caught) if caught else None, caught, None)
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
if caught is not None:
|
|
174
|
+
raise caught # type: ignore[misc]
|
|
175
|
+
return result # type: ignore[return-value]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def track_chain(
|
|
179
|
+
steps: List[Dict[str, Any]],
|
|
180
|
+
opts: Optional[Dict[str, Any]] = None,
|
|
181
|
+
) -> List[Any]:
|
|
182
|
+
"""
|
|
183
|
+
Wrap chained model calls — one session, multiple events.
|
|
184
|
+
Each step: {'action': str, 'fn': callable, 'model': str, 'input': dict}
|
|
185
|
+
|
|
186
|
+
Usage:
|
|
187
|
+
results = track_chain([
|
|
188
|
+
{'action': 'ocr', 'model': 'gpt-4o',
|
|
189
|
+
'fn': lambda: openai.chat(...), 'input': {'pages': 3}},
|
|
190
|
+
{'action': 'classify', 'model': 'claude-sonnet-4-20250514',
|
|
191
|
+
'fn': lambda: anthropic.messages(...), 'input': {'text': '...'}}
|
|
192
|
+
], {
|
|
193
|
+
'human_id': customer_id,
|
|
194
|
+
'agent_name': 'shipping-pipeline',
|
|
195
|
+
'session_name': 'Process Shipping Document'
|
|
196
|
+
})
|
|
197
|
+
"""
|
|
198
|
+
from .session import Session # lazy — see note in track_llm
|
|
199
|
+
|
|
200
|
+
o = opts or {}
|
|
201
|
+
first_model = steps[0].get('model', 'unknown') if steps else 'unknown'
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
session = Session(
|
|
205
|
+
human_id=o.get('human_id', 'anonymous'),
|
|
206
|
+
agent_name=o.get('agent_name', 'agent'),
|
|
207
|
+
framework=o.get('framework', 'python'),
|
|
208
|
+
model=o.get('model', first_model),
|
|
209
|
+
metadata={'session_name': o.get('session_name', 'chained-workflow')},
|
|
210
|
+
product_context=o.get('product_context'),
|
|
211
|
+
)
|
|
212
|
+
session.__enter__()
|
|
213
|
+
print(f"[AgentWatch] session/start: {session.session_id}", file=sys.stderr)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"[AgentWatch] ERROR session/start: {type(e).__name__}: {e}", file=sys.stderr)
|
|
216
|
+
return [step['fn']() for step in steps] # AgentWatch down — still run steps
|
|
217
|
+
|
|
218
|
+
t0 = time.monotonic()
|
|
219
|
+
results: List[Any] = []
|
|
220
|
+
caught: Optional[BaseException] = None
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
for step in steps:
|
|
224
|
+
t_step = time.monotonic()
|
|
225
|
+
step_result = step['fn']()
|
|
226
|
+
latency_ms = int((time.monotonic() - t_step) * 1000)
|
|
227
|
+
results.append(step_result)
|
|
228
|
+
try:
|
|
229
|
+
session.log_event(
|
|
230
|
+
event_type='llm_call',
|
|
231
|
+
agent_name=session.agent_name,
|
|
232
|
+
action=step['action'],
|
|
233
|
+
input_data=step.get('input'),
|
|
234
|
+
output_data={'success': True},
|
|
235
|
+
model=step.get('model', o.get('model', 'unknown')),
|
|
236
|
+
latency_ms=latency_ms,
|
|
237
|
+
product_context=step.get('product_context', o.get('product_context')),
|
|
238
|
+
outcome_context=step.get('outcome_context'),
|
|
239
|
+
)
|
|
240
|
+
print(f"[AgentWatch] event posted seq={session.sequence_num}: {step['action']}", file=sys.stderr)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"[AgentWatch] ERROR in {step['action']}: {type(e).__name__}: {e}", file=sys.stderr)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
caught = e
|
|
245
|
+
finally:
|
|
246
|
+
try:
|
|
247
|
+
session.__exit__(type(caught) if caught else None, caught, None)
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
if caught is not None:
|
|
252
|
+
raise caught # type: ignore[misc]
|
|
253
|
+
return results
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from .exceptions import ExecutionBlockedException, HitlDeniedException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def tool(
|
|
9
|
+
session: Any,
|
|
10
|
+
action_class: str = 'read',
|
|
11
|
+
data_scope: str = 'any',
|
|
12
|
+
) -> Callable:
|
|
13
|
+
"""
|
|
14
|
+
Decorator that wraps a function with AgentWatch observability and policy enforcement.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
@tool(session, action_class="send", data_scope="financial")
|
|
18
|
+
def send_invoice(recipient: str, amount: float) -> dict:
|
|
19
|
+
...
|
|
20
|
+
"""
|
|
21
|
+
def decorator(func: Callable) -> Callable:
|
|
22
|
+
@wraps(func)
|
|
23
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
24
|
+
start = time.monotonic()
|
|
25
|
+
input_data = {'args': list(args), 'kwargs': kwargs}
|
|
26
|
+
|
|
27
|
+
resp = session.log_event(
|
|
28
|
+
event_type='tool_call',
|
|
29
|
+
agent_name=session.agent_name,
|
|
30
|
+
action=func.__name__,
|
|
31
|
+
input_data=input_data,
|
|
32
|
+
output_data=None,
|
|
33
|
+
action_class=action_class,
|
|
34
|
+
data_scope=data_scope,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if resp.get('block'):
|
|
38
|
+
raise ExecutionBlockedException(
|
|
39
|
+
f"Policy blocked execution of '{func.__name__}'"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if resp.get('hitl_required'):
|
|
43
|
+
hitl_id = resp.get('hitl_request_id')
|
|
44
|
+
if hitl_id:
|
|
45
|
+
approval = session.client.poll_hitl(hitl_id, timeout=900)
|
|
46
|
+
if approval.get('status') == 'denied':
|
|
47
|
+
raise HitlDeniedException(
|
|
48
|
+
f"Human denied execution of '{func.__name__}'"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
result = func(*args, **kwargs)
|
|
52
|
+
latency = int((time.monotonic() - start) * 1000)
|
|
53
|
+
|
|
54
|
+
session.log_event(
|
|
55
|
+
event_type='tool_result',
|
|
56
|
+
agent_name=session.agent_name,
|
|
57
|
+
action=func.__name__,
|
|
58
|
+
input_data=input_data,
|
|
59
|
+
output_data=result,
|
|
60
|
+
latency_ms=latency,
|
|
61
|
+
action_class=action_class,
|
|
62
|
+
data_scope=data_scope,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
return wrapper
|
|
67
|
+
return decorator
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class AgentwatchError(Exception):
|
|
2
|
+
"""Base class for all AgentWatch SDK errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AgentwatchAPIError(AgentwatchError):
|
|
6
|
+
def __init__(self, status_code: int, body: str):
|
|
7
|
+
self.status_code = status_code
|
|
8
|
+
self.body = body
|
|
9
|
+
super().__init__(f"API error {status_code}: {body}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentwatchConnectionError(AgentwatchError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExecutionBlockedException(AgentwatchError):
|
|
17
|
+
"""Raised when a tool call is blocked by policy."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HitlDeniedException(AgentwatchError):
|
|
21
|
+
"""Raised when a human reviewer denied the requested action."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _sha256(text: str) -> str:
|
|
7
|
+
return hashlib.sha256(text.encode('utf-8')).hexdigest()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compute_fingerprint(
|
|
11
|
+
model: Optional[str] = None,
|
|
12
|
+
system_prompt: Optional[str] = None,
|
|
13
|
+
tools: Optional[List[str]] = None,
|
|
14
|
+
config: Optional[Dict[str, Any]] = None,
|
|
15
|
+
) -> Dict[str, Any]:
|
|
16
|
+
fp: Dict[str, Any] = {
|
|
17
|
+
'model': model or 'unknown',
|
|
18
|
+
'prompt_hash': _sha256(system_prompt or ''),
|
|
19
|
+
'tools': sorted(tools or []),
|
|
20
|
+
}
|
|
21
|
+
if config is not None:
|
|
22
|
+
fp['config_hash'] = _sha256(
|
|
23
|
+
json.dumps(config, sort_keys=True, separators=(',', ':'))
|
|
24
|
+
)
|
|
25
|
+
return fp
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from .chain import compute_entry_hash
|
|
7
|
+
from .client import AgentwatchClient
|
|
8
|
+
from .fingerprint import compute_fingerprint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Session:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
human_id: str,
|
|
15
|
+
api_key: Optional[str] = None,
|
|
16
|
+
api_url: Optional[str] = None,
|
|
17
|
+
**kwargs: Any,
|
|
18
|
+
):
|
|
19
|
+
self.api_key = api_key or os.environ.get('AGENTWATCH_API_KEY')
|
|
20
|
+
self.api_url = (
|
|
21
|
+
api_url
|
|
22
|
+
or os.environ.get('AGENTWATCH_API_URL', 'https://agentwatch-pi.vercel.app')
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if not self.api_key:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"api_key is required. Pass it directly or set AGENTWATCH_API_KEY env var."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.human_id: str = human_id
|
|
31
|
+
self.agent_name: str = kwargs.get('agent_name', 'default-agent')
|
|
32
|
+
self.agent_version: Optional[str] = kwargs.get('agent_version')
|
|
33
|
+
self.model: Optional[str] = kwargs.get('model')
|
|
34
|
+
self.system_prompt: Optional[str] = kwargs.get('system_prompt')
|
|
35
|
+
self.tools: List[str] = kwargs.get('tools') or []
|
|
36
|
+
self.framework: Optional[str] = kwargs.get('framework')
|
|
37
|
+
self.metadata: Optional[Dict[str, Any]] = kwargs.get('metadata')
|
|
38
|
+
self.product_context: Optional[Dict[str, Any]] = kwargs.get('product_context')
|
|
39
|
+
self.human_metadata: Optional[Dict[str, Any]] = kwargs.get('human_metadata')
|
|
40
|
+
|
|
41
|
+
self.sequence_num: int = 0
|
|
42
|
+
self.prev_hash: str = 'genesis'
|
|
43
|
+
self.session_id: Optional[str] = None
|
|
44
|
+
self.policy: Optional[Dict[str, Any]] = None
|
|
45
|
+
self.fingerprint_status: Optional[str] = None
|
|
46
|
+
self._start_time: Optional[float] = None
|
|
47
|
+
|
|
48
|
+
self.client = AgentwatchClient(self.api_url, self.api_key)
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> 'Session':
|
|
51
|
+
self._start_time = time.monotonic()
|
|
52
|
+
|
|
53
|
+
fingerprint = compute_fingerprint(
|
|
54
|
+
model=self.model,
|
|
55
|
+
system_prompt=self.system_prompt,
|
|
56
|
+
tools=self.tools,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
resp = self.client.post('/api/v1/ingest/session/start', {
|
|
61
|
+
'human_id': self.human_id,
|
|
62
|
+
'human_metadata': self.human_metadata,
|
|
63
|
+
'agent_name': self.agent_name,
|
|
64
|
+
'agent_version': self.agent_version,
|
|
65
|
+
'fingerprint': fingerprint,
|
|
66
|
+
'framework': self.framework,
|
|
67
|
+
'metadata': self.metadata,
|
|
68
|
+
'product_context': self.product_context,
|
|
69
|
+
})
|
|
70
|
+
self.session_id = resp['session_id']
|
|
71
|
+
self.policy = resp.get('policy')
|
|
72
|
+
self.fingerprint_status = resp.get('fingerprint_status')
|
|
73
|
+
except Exception as e:
|
|
74
|
+
# API unreachable — degrade silently. session_id stays None.
|
|
75
|
+
# log_event() and __exit__() are no-ops when session_id is None.
|
|
76
|
+
import sys
|
|
77
|
+
print(f'[AIWatcher] session/start failed (offline mode): {e}', file=sys.stderr)
|
|
78
|
+
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
|
|
82
|
+
if self.session_id and self._start_time is not None:
|
|
83
|
+
elapsed_ms = int((time.monotonic() - self._start_time) * 1000)
|
|
84
|
+
status = 'error' if exc_type is not None else 'completed'
|
|
85
|
+
try:
|
|
86
|
+
self.client.post('/api/v1/ingest/session/end', {
|
|
87
|
+
'session_id': self.session_id,
|
|
88
|
+
'status': status,
|
|
89
|
+
'duration_ms': elapsed_ms,
|
|
90
|
+
})
|
|
91
|
+
except Exception:
|
|
92
|
+
pass # Never suppress the original exception
|
|
93
|
+
return False # Propagate any exception from the with-block
|
|
94
|
+
|
|
95
|
+
def log_event(
|
|
96
|
+
self,
|
|
97
|
+
event_type: str,
|
|
98
|
+
agent_name: str,
|
|
99
|
+
action: Optional[str],
|
|
100
|
+
input_data: Any,
|
|
101
|
+
output_data: Any,
|
|
102
|
+
model: Optional[str] = None,
|
|
103
|
+
tokens_in: Optional[int] = None,
|
|
104
|
+
tokens_out: Optional[int] = None,
|
|
105
|
+
cost_usd: Optional[float] = None,
|
|
106
|
+
latency_ms: Optional[int] = None,
|
|
107
|
+
action_class: str = 'read',
|
|
108
|
+
data_scope: str = 'any',
|
|
109
|
+
product_context: Optional[Dict[str, Any]] = None,
|
|
110
|
+
outcome_context: Optional[Dict[str, Any]] = None,
|
|
111
|
+
) -> Dict[str, Any]:
|
|
112
|
+
if not self.session_id:
|
|
113
|
+
# Offline mode — API unreachable at session start, skip silently
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
self.sequence_num += 1
|
|
117
|
+
resolved_product_context = product_context or self.product_context
|
|
118
|
+
if resolved_product_context:
|
|
119
|
+
input_data = {
|
|
120
|
+
**(input_data if isinstance(input_data, dict) else {'value': input_data}),
|
|
121
|
+
'product_context': resolved_product_context,
|
|
122
|
+
}
|
|
123
|
+
if outcome_context:
|
|
124
|
+
output_data = {
|
|
125
|
+
**(output_data if isinstance(output_data, dict) else {'value': output_data}),
|
|
126
|
+
'outcome_context': outcome_context,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
entry_hash = compute_entry_hash(
|
|
130
|
+
session_id=self.session_id,
|
|
131
|
+
sequence_num=self.sequence_num,
|
|
132
|
+
event_type=event_type,
|
|
133
|
+
action=action,
|
|
134
|
+
input_data=input_data,
|
|
135
|
+
output_data=output_data,
|
|
136
|
+
prev_hash=self.prev_hash,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
|
|
140
|
+
|
|
141
|
+
resp = self.client.post('/api/v1/ingest/event', {
|
|
142
|
+
'session_id': self.session_id,
|
|
143
|
+
'event_type': event_type,
|
|
144
|
+
'agent_name': agent_name,
|
|
145
|
+
'action': action,
|
|
146
|
+
'sequence_num': self.sequence_num,
|
|
147
|
+
'input_data': input_data,
|
|
148
|
+
'output_data': output_data,
|
|
149
|
+
'model': model,
|
|
150
|
+
'tokens_in': tokens_in,
|
|
151
|
+
'tokens_out': tokens_out,
|
|
152
|
+
'cost_usd': cost_usd,
|
|
153
|
+
'latency_ms': latency_ms,
|
|
154
|
+
'prev_hash': self.prev_hash,
|
|
155
|
+
'entry_hash': entry_hash,
|
|
156
|
+
'timestamp': now,
|
|
157
|
+
'action_class': action_class,
|
|
158
|
+
'data_scope': data_scope,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
self.prev_hash = entry_hash
|
|
162
|
+
return resp
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ai-watcher"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "AI agent observability and control — AgentWatch SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://agentwatch.vercel.app"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["aiwatcher*"]
|