raindrop-strands 0.0.1__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.
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: raindrop-strands
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Raindrop integration for Strands Agents
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Raindrop AI
|
|
7
|
+
Author-email: sdk@raindrop.ai
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: raindrop-ai (>=0.0.42)
|
|
15
|
+
Requires-Dist: strands-agents (>=1.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# raindrop-strands
|
|
19
|
+
|
|
20
|
+
Raindrop integration for [Strands Agents](https://strandsagents.com) (Python). Automatically captures agent invocations, model calls, tool usage, and token metrics via the Strands hook system.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install raindrop-strands strands-agents
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`strands-agents` is a required dependency.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import os
|
|
34
|
+
from strands import Agent
|
|
35
|
+
from raindrop_strands import create_raindrop_strands
|
|
36
|
+
|
|
37
|
+
raindrop = create_raindrop_strands(
|
|
38
|
+
api_key=os.environ.get("RAINDROP_API_KEY"),
|
|
39
|
+
user_id="user_123",
|
|
40
|
+
convo_id="session_456",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
agent = Agent(
|
|
44
|
+
model="us.amazon.nova-lite-v1:0",
|
|
45
|
+
system_prompt="You are a helpful assistant.",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
raindrop["handler"].register_hooks(agent)
|
|
49
|
+
|
|
50
|
+
result = agent("What is the capital of France?")
|
|
51
|
+
print(result)
|
|
52
|
+
|
|
53
|
+
raindrop["flush"]()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Omitting `api_key` disables telemetry shipping (a warning is emitted) but does not crash your application.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### `create_raindrop_strands(api_key, user_id, convo_id)`
|
|
61
|
+
|
|
62
|
+
Returns a dict with:
|
|
63
|
+
|
|
64
|
+
- `"handler"` — `RaindropStrandsHandler` instance to register on agents
|
|
65
|
+
- `"flush"` — flush pending telemetry
|
|
66
|
+
- `"shutdown"` — flush and release resources
|
|
67
|
+
|
|
68
|
+
## Testing
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cd packages/strands-python
|
|
72
|
+
poetry install --with dev
|
|
73
|
+
pytest
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
79
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# raindrop-strands
|
|
2
|
+
|
|
3
|
+
Raindrop integration for [Strands Agents](https://strandsagents.com) (Python). Automatically captures agent invocations, model calls, tool usage, and token metrics via the Strands hook system.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install raindrop-strands strands-agents
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`strands-agents` is a required dependency.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import os
|
|
17
|
+
from strands import Agent
|
|
18
|
+
from raindrop_strands import create_raindrop_strands
|
|
19
|
+
|
|
20
|
+
raindrop = create_raindrop_strands(
|
|
21
|
+
api_key=os.environ.get("RAINDROP_API_KEY"),
|
|
22
|
+
user_id="user_123",
|
|
23
|
+
convo_id="session_456",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
agent = Agent(
|
|
27
|
+
model="us.amazon.nova-lite-v1:0",
|
|
28
|
+
system_prompt="You are a helpful assistant.",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
raindrop["handler"].register_hooks(agent)
|
|
32
|
+
|
|
33
|
+
result = agent("What is the capital of France?")
|
|
34
|
+
print(result)
|
|
35
|
+
|
|
36
|
+
raindrop["flush"]()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Omitting `api_key` disables telemetry shipping (a warning is emitted) but does not crash your application.
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### `create_raindrop_strands(api_key, user_id, convo_id)`
|
|
44
|
+
|
|
45
|
+
Returns a dict with:
|
|
46
|
+
|
|
47
|
+
- `"handler"` — `RaindropStrandsHandler` instance to register on agents
|
|
48
|
+
- `"flush"` — flush pending telemetry
|
|
49
|
+
- `"shutdown"` — flush and release resources
|
|
50
|
+
|
|
51
|
+
## Testing
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd packages/strands-python
|
|
55
|
+
poetry install --with dev
|
|
56
|
+
pytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "raindrop-strands"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Raindrop integration for Strands Agents"
|
|
5
|
+
authors = ["Raindrop AI <sdk@raindrop.ai>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [{ include = "raindrop_strands" }]
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = ">=3.10,<4.0"
|
|
12
|
+
raindrop-ai = ">=0.0.42"
|
|
13
|
+
strands-agents = ">=1.0.0"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.group.dev.dependencies]
|
|
16
|
+
pytest = "^8.0"
|
|
17
|
+
|
|
18
|
+
[tool.pytest.ini_options]
|
|
19
|
+
testpaths = ["tests"]
|
|
20
|
+
python_files = ["test_*.py"]
|
|
21
|
+
python_classes = ["Test*"]
|
|
22
|
+
python_functions = ["test_*"]
|
|
23
|
+
addopts = ["-v", "--tb=short"]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["poetry-core"]
|
|
27
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Strands Agents callback handler for Raindrop observability.
|
|
3
|
+
|
|
4
|
+
Captures agent invocations, model calls, and tool usage via the Strands
|
|
5
|
+
Agents hook system and ships them to the Raindrop API.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from raindrop_strands import create_raindrop_strands
|
|
9
|
+
|
|
10
|
+
raindrop = create_raindrop_strands(api_key="rk_...")
|
|
11
|
+
agent = Agent(model=model)
|
|
12
|
+
raindrop["handler"].register_hooks(agent)
|
|
13
|
+
agent("Hello!")
|
|
14
|
+
raindrop["flush"]()
|
|
15
|
+
|
|
16
|
+
IMPORTANT: Every handler method MUST be wrapped in try/except.
|
|
17
|
+
Telemetry must NEVER crash the user's pipeline.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import uuid
|
|
24
|
+
from typing import Any, Dict, Optional
|
|
25
|
+
|
|
26
|
+
import raindrop.analytics as raindrop
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _safe_serialize(value: Any) -> str:
|
|
30
|
+
"""Safely serialize a value to string. Never throws."""
|
|
31
|
+
try:
|
|
32
|
+
return json.dumps(value)
|
|
33
|
+
except Exception:
|
|
34
|
+
return str(value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_text_from_message(message: Any) -> Optional[str]:
|
|
38
|
+
"""Extract text content from a Strands Message (TypedDict with 'content' list)."""
|
|
39
|
+
try:
|
|
40
|
+
content = message.get("content") if isinstance(message, dict) else None
|
|
41
|
+
if not content or not isinstance(content, list):
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
text_parts = []
|
|
45
|
+
for block in content:
|
|
46
|
+
if block is None:
|
|
47
|
+
continue
|
|
48
|
+
if isinstance(block, str):
|
|
49
|
+
text_parts.append(block)
|
|
50
|
+
elif isinstance(block, dict) and "text" in block:
|
|
51
|
+
text = block["text"]
|
|
52
|
+
if isinstance(text, str):
|
|
53
|
+
text_parts.append(text)
|
|
54
|
+
return "\n".join(text_parts) if text_parts else None
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _extract_user_input(messages: Any) -> Optional[str]:
|
|
60
|
+
"""Extract the last user message text from a conversation history."""
|
|
61
|
+
try:
|
|
62
|
+
if not messages or not isinstance(messages, list):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# Find the last user message
|
|
66
|
+
for msg in reversed(messages):
|
|
67
|
+
if not isinstance(msg, dict):
|
|
68
|
+
continue
|
|
69
|
+
if msg.get("role") != "user":
|
|
70
|
+
continue
|
|
71
|
+
text = _extract_text_from_message(msg)
|
|
72
|
+
if text:
|
|
73
|
+
return text
|
|
74
|
+
|
|
75
|
+
# Fallback: try the last message regardless of role
|
|
76
|
+
if messages:
|
|
77
|
+
last_msg = messages[-1]
|
|
78
|
+
if isinstance(last_msg, dict):
|
|
79
|
+
return _extract_text_from_message(last_msg)
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
except Exception:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RaindropStrandsHandler:
|
|
87
|
+
"""Strands Agents hook handler that ships events to Raindrop."""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
user_id: Optional[str] = None,
|
|
92
|
+
convo_id: Optional[str] = None,
|
|
93
|
+
):
|
|
94
|
+
self._user_id = user_id
|
|
95
|
+
self._convo_id = convo_id
|
|
96
|
+
|
|
97
|
+
# Map id(agent) -> invocation context dict.
|
|
98
|
+
# Cleaned up explicitly in on_after_invocation to prevent leaks.
|
|
99
|
+
self._invocations: Dict[int, Dict[str, Any]] = {}
|
|
100
|
+
# toolUseId -> span context for active tool calls
|
|
101
|
+
self._tool_spans: Dict[str, Dict[str, Any]] = {}
|
|
102
|
+
|
|
103
|
+
# Tracking counters for memory leak detection in tests
|
|
104
|
+
self._active_invocations = 0
|
|
105
|
+
self._active_tool_spans = 0
|
|
106
|
+
|
|
107
|
+
def register_hooks(self, agent: Any) -> None:
|
|
108
|
+
"""Register all Raindrop hook callbacks on a Strands agent.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
agent: A Strands Agent instance with an ``add_hook`` method.
|
|
112
|
+
"""
|
|
113
|
+
from strands.hooks.events import (
|
|
114
|
+
AfterInvocationEvent,
|
|
115
|
+
AfterModelCallEvent,
|
|
116
|
+
AfterToolCallEvent,
|
|
117
|
+
BeforeInvocationEvent,
|
|
118
|
+
BeforeModelCallEvent,
|
|
119
|
+
BeforeToolCallEvent,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
agent.add_hook(self.on_before_invocation, BeforeInvocationEvent)
|
|
123
|
+
agent.add_hook(self.on_after_invocation, AfterInvocationEvent)
|
|
124
|
+
agent.add_hook(self.on_before_model_call, BeforeModelCallEvent)
|
|
125
|
+
agent.add_hook(self.on_after_model_call, AfterModelCallEvent)
|
|
126
|
+
agent.add_hook(self.on_before_tool_call, BeforeToolCallEvent)
|
|
127
|
+
agent.add_hook(self.on_after_tool_call, AfterToolCallEvent)
|
|
128
|
+
|
|
129
|
+
# --- Hook callback methods ---
|
|
130
|
+
|
|
131
|
+
def on_before_invocation(self, event: Any, **kwargs: Any) -> None:
|
|
132
|
+
"""Called before an agent invocation starts."""
|
|
133
|
+
try:
|
|
134
|
+
agent = event.agent
|
|
135
|
+
event_id = str(uuid.uuid4())
|
|
136
|
+
|
|
137
|
+
# Extract input from the last user message
|
|
138
|
+
messages = getattr(agent, "messages", None)
|
|
139
|
+
input_text = _extract_user_input(messages)
|
|
140
|
+
|
|
141
|
+
ctx: Dict[str, Any] = {
|
|
142
|
+
"event_id": event_id,
|
|
143
|
+
"input": input_text,
|
|
144
|
+
"output": None,
|
|
145
|
+
"model": None,
|
|
146
|
+
"prompt_tokens": None,
|
|
147
|
+
"completion_tokens": None,
|
|
148
|
+
"tool_use_ids": set(),
|
|
149
|
+
}
|
|
150
|
+
self._invocations[id(agent)] = ctx
|
|
151
|
+
self._active_invocations += 1
|
|
152
|
+
except Exception:
|
|
153
|
+
pass # telemetry must not interfere with user's pipeline
|
|
154
|
+
|
|
155
|
+
def on_after_invocation(self, event: Any, **kwargs: Any) -> None:
|
|
156
|
+
"""Called after an agent invocation completes."""
|
|
157
|
+
try:
|
|
158
|
+
agent = event.agent
|
|
159
|
+
ctx = self._invocations.get(id(agent))
|
|
160
|
+
if not ctx:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Extract output from the result if available
|
|
164
|
+
result = getattr(event, "result", None)
|
|
165
|
+
if result is not None:
|
|
166
|
+
message = getattr(result, "message", None)
|
|
167
|
+
if message is not None:
|
|
168
|
+
output = _extract_text_from_message(message)
|
|
169
|
+
if output:
|
|
170
|
+
ctx["output"] = output
|
|
171
|
+
|
|
172
|
+
# Extract token usage from metrics
|
|
173
|
+
metrics = getattr(result, "metrics", None)
|
|
174
|
+
if metrics is not None:
|
|
175
|
+
accumulated = getattr(metrics, "accumulated_usage", None)
|
|
176
|
+
if accumulated is not None:
|
|
177
|
+
input_tokens = (
|
|
178
|
+
accumulated.get("inputTokens")
|
|
179
|
+
if isinstance(accumulated, dict)
|
|
180
|
+
else getattr(accumulated, "inputTokens", None)
|
|
181
|
+
)
|
|
182
|
+
output_tokens = (
|
|
183
|
+
accumulated.get("outputTokens")
|
|
184
|
+
if isinstance(accumulated, dict)
|
|
185
|
+
else getattr(accumulated, "outputTokens", None)
|
|
186
|
+
)
|
|
187
|
+
if input_tokens is not None:
|
|
188
|
+
ctx["prompt_tokens"] = input_tokens
|
|
189
|
+
if output_tokens is not None:
|
|
190
|
+
ctx["completion_tokens"] = output_tokens
|
|
191
|
+
|
|
192
|
+
properties: Dict[str, Any] = {}
|
|
193
|
+
if ctx.get("prompt_tokens") is not None:
|
|
194
|
+
properties["ai.usage.prompt_tokens"] = ctx["prompt_tokens"]
|
|
195
|
+
if ctx.get("completion_tokens") is not None:
|
|
196
|
+
properties["ai.usage.completion_tokens"] = ctx["completion_tokens"]
|
|
197
|
+
|
|
198
|
+
raindrop.track_ai(
|
|
199
|
+
user_id=self._user_id or "",
|
|
200
|
+
event="ai_generation",
|
|
201
|
+
event_id=ctx["event_id"],
|
|
202
|
+
input=ctx.get("input"),
|
|
203
|
+
output=ctx.get("output"),
|
|
204
|
+
model=ctx.get("model"),
|
|
205
|
+
convo_id=self._convo_id,
|
|
206
|
+
properties=properties if properties else None,
|
|
207
|
+
)
|
|
208
|
+
except Exception:
|
|
209
|
+
pass # telemetry must not interfere
|
|
210
|
+
finally:
|
|
211
|
+
try:
|
|
212
|
+
agent = event.agent
|
|
213
|
+
# Get ctx BEFORE popping, for orphan tool span cleanup
|
|
214
|
+
ctx = self._invocations.get(id(agent))
|
|
215
|
+
if ctx is not None:
|
|
216
|
+
self._invocations.pop(id(agent), None)
|
|
217
|
+
self._active_invocations = max(0, self._active_invocations - 1)
|
|
218
|
+
# Clean up any orphaned tool spans owned by this invocation
|
|
219
|
+
for tool_id in ctx.get("tool_use_ids", set()):
|
|
220
|
+
if self._tool_spans.pop(tool_id, None) is not None:
|
|
221
|
+
self._active_tool_spans = max(0, self._active_tool_spans - 1)
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
def on_before_model_call(self, event: Any, **kwargs: Any) -> None:
|
|
226
|
+
"""Called before a model call within an invocation."""
|
|
227
|
+
try:
|
|
228
|
+
# No span-based tracing in Python SDK; model call data is
|
|
229
|
+
# captured in on_after_model_call and attached to the invocation.
|
|
230
|
+
pass
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
def on_after_model_call(self, event: Any, **kwargs: Any) -> None:
|
|
235
|
+
"""Called after a model call completes."""
|
|
236
|
+
try:
|
|
237
|
+
agent = event.agent
|
|
238
|
+
ctx = self._invocations.get(id(agent))
|
|
239
|
+
if not ctx:
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Extract output from stop response
|
|
243
|
+
stop_response = getattr(event, "stop_response", None)
|
|
244
|
+
if stop_response is not None:
|
|
245
|
+
message = getattr(stop_response, "message", None)
|
|
246
|
+
if message is not None:
|
|
247
|
+
output = _extract_text_from_message(message)
|
|
248
|
+
if output:
|
|
249
|
+
ctx["output"] = output
|
|
250
|
+
|
|
251
|
+
# Extract usage from stop response
|
|
252
|
+
usage = getattr(stop_response, "usage", None)
|
|
253
|
+
if usage is not None:
|
|
254
|
+
input_tokens = (
|
|
255
|
+
usage.get("inputTokens")
|
|
256
|
+
if isinstance(usage, dict)
|
|
257
|
+
else getattr(usage, "inputTokens", None)
|
|
258
|
+
)
|
|
259
|
+
output_tokens = (
|
|
260
|
+
usage.get("outputTokens")
|
|
261
|
+
if isinstance(usage, dict)
|
|
262
|
+
else getattr(usage, "outputTokens", None)
|
|
263
|
+
)
|
|
264
|
+
if input_tokens is not None:
|
|
265
|
+
ctx["prompt_tokens"] = input_tokens
|
|
266
|
+
if output_tokens is not None:
|
|
267
|
+
ctx["completion_tokens"] = output_tokens
|
|
268
|
+
|
|
269
|
+
# Extract model name
|
|
270
|
+
model_id = getattr(stop_response, "model", None)
|
|
271
|
+
if model_id is not None:
|
|
272
|
+
ctx["model"] = str(model_id)
|
|
273
|
+
except Exception:
|
|
274
|
+
pass # telemetry must not interfere
|
|
275
|
+
|
|
276
|
+
def on_before_tool_call(self, event: Any, **kwargs: Any) -> None:
|
|
277
|
+
"""Called before a tool call within an invocation."""
|
|
278
|
+
try:
|
|
279
|
+
agent = getattr(event, "agent", None)
|
|
280
|
+
if agent is None:
|
|
281
|
+
return
|
|
282
|
+
ctx = self._invocations.get(id(agent))
|
|
283
|
+
if not ctx:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
tool_use = getattr(event, "tool_use", None)
|
|
287
|
+
if tool_use is None:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
tool_use_id = (
|
|
291
|
+
tool_use.get("toolUseId")
|
|
292
|
+
if isinstance(tool_use, dict)
|
|
293
|
+
else getattr(tool_use, "toolUseId", None)
|
|
294
|
+
)
|
|
295
|
+
tool_name = (
|
|
296
|
+
tool_use.get("name")
|
|
297
|
+
if isinstance(tool_use, dict)
|
|
298
|
+
else getattr(tool_use, "name", None)
|
|
299
|
+
)
|
|
300
|
+
tool_input = (
|
|
301
|
+
tool_use.get("input")
|
|
302
|
+
if isinstance(tool_use, dict)
|
|
303
|
+
else getattr(tool_use, "input", None)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if tool_use_id:
|
|
307
|
+
tid = str(tool_use_id)
|
|
308
|
+
self._tool_spans[tid] = {
|
|
309
|
+
"name": tool_name,
|
|
310
|
+
"input": _safe_serialize(tool_input) if tool_input is not None else None,
|
|
311
|
+
}
|
|
312
|
+
if "tool_use_ids" in ctx:
|
|
313
|
+
ctx["tool_use_ids"].add(tid)
|
|
314
|
+
self._active_tool_spans += 1
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
def on_after_tool_call(self, event: Any, **kwargs: Any) -> None:
|
|
319
|
+
"""Called after a tool call completes."""
|
|
320
|
+
try:
|
|
321
|
+
tool_use = getattr(event, "tool_use", None)
|
|
322
|
+
if tool_use is None:
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
tool_use_id = (
|
|
326
|
+
tool_use.get("toolUseId")
|
|
327
|
+
if isinstance(tool_use, dict)
|
|
328
|
+
else getattr(tool_use, "toolUseId", None)
|
|
329
|
+
)
|
|
330
|
+
if tool_use_id:
|
|
331
|
+
tid = str(tool_use_id)
|
|
332
|
+
deleted = self._tool_spans.pop(tid, None)
|
|
333
|
+
if deleted is not None:
|
|
334
|
+
self._active_tool_spans = max(0, self._active_tool_spans - 1)
|
|
335
|
+
# Also remove from invocation's tracked set
|
|
336
|
+
agent = getattr(event, "agent", None)
|
|
337
|
+
if agent is not None:
|
|
338
|
+
ctx = self._invocations.get(id(agent))
|
|
339
|
+
if ctx:
|
|
340
|
+
if "tool_use_ids" in ctx:
|
|
341
|
+
ctx["tool_use_ids"].discard(tid)
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def _map_sizes(self) -> Dict[str, int]:
|
|
347
|
+
"""Get the current sizes of internal tracking maps (for testing)."""
|
|
348
|
+
return {
|
|
349
|
+
"invocations": self._active_invocations,
|
|
350
|
+
"tool_spans": self._active_tool_spans,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def create_raindrop_strands(
|
|
355
|
+
api_key: Optional[str] = None,
|
|
356
|
+
user_id: Optional[str] = None,
|
|
357
|
+
convo_id: Optional[str] = None,
|
|
358
|
+
**kwargs: Any,
|
|
359
|
+
) -> Dict[str, Any]:
|
|
360
|
+
"""
|
|
361
|
+
Create a Raindrop-instrumented Strands Agents callback handler.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
api_key: Raindrop API key (rk_...). Omit to disable telemetry shipping.
|
|
365
|
+
user_id: Associate all events with this user.
|
|
366
|
+
convo_id: Conversation ID to group related events.
|
|
367
|
+
**kwargs: Additional options (reserved for future use).
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Dict with 'handler', 'flush', and 'shutdown'.
|
|
371
|
+
|
|
372
|
+
Usage:
|
|
373
|
+
from raindrop_strands import create_raindrop_strands
|
|
374
|
+
|
|
375
|
+
raindrop = create_raindrop_strands(api_key="rk_...")
|
|
376
|
+
agent = Agent(model=model)
|
|
377
|
+
raindrop["handler"].register_hooks(agent)
|
|
378
|
+
agent("Hello!")
|
|
379
|
+
raindrop["flush"]()
|
|
380
|
+
"""
|
|
381
|
+
import warnings
|
|
382
|
+
|
|
383
|
+
if not api_key:
|
|
384
|
+
warnings.warn(
|
|
385
|
+
"[raindrop-strands] api_key not provided; telemetry shipping is disabled",
|
|
386
|
+
stacklevel=2,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
raindrop.init(api_key=api_key or "")
|
|
390
|
+
|
|
391
|
+
handler = RaindropStrandsHandler(
|
|
392
|
+
user_id=user_id,
|
|
393
|
+
convo_id=convo_id,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"handler": handler,
|
|
398
|
+
"flush": raindrop.flush,
|
|
399
|
+
"shutdown": raindrop.shutdown,
|
|
400
|
+
}
|