struct-sdk 0.2.0__tar.gz → 0.2.2__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.
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/PKG-INFO +49 -4
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/README.md +48 -3
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/pyproject.toml +1 -1
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/src/struct_sdk/anthropic.py +31 -8
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/.gitignore +0 -0
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/LICENSE +0 -0
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/src/struct_sdk/__init__.py +0 -0
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/src/struct_sdk/claude_agent.py +0 -0
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/src/struct_sdk/core.py +0 -0
- {struct_sdk-0.2.0 → struct_sdk-0.2.2}/src/struct_sdk/langchain.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: struct-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry
|
|
5
5
|
Project-URL: Homepage, https://struct.ai
|
|
6
6
|
Project-URL: Documentation, https://struct.ai/docs
|
|
@@ -108,7 +108,7 @@ async with struct.agent(name="checkout"):
|
|
|
108
108
|
| `langchain_core` `BaseChatModel` | `chat {model}` | Skipped when an underlying provider SDK is also instrumented (e.g. `ChatAnthropic` + `anthropic` → a single span). |
|
|
109
109
|
| `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
|
|
110
110
|
| `langchain_core` `BaseRetriever` | `retrieval {name}` | |
|
|
111
|
-
| `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent` and custom graphs. `thread_id`
|
|
111
|
+
| `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent`, `langchain.agents.create_agent`, and custom graphs. Reads conversation id from any of: `configurable.thread_id` (LangGraph canonical), or `metadata.{thread_id, session_id, conversation_id}` (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in [`struct.agent(session_id=conv_id)`](#recommended-pattern-wrap-langchain-entry-points-in-structagent) — that's the struct-native replacement for `ls.tracing_context(parent=run_tree)`. |
|
|
112
112
|
|
|
113
113
|
## Framework integration
|
|
114
114
|
|
|
@@ -156,6 +156,40 @@ from langgraph.prebuilt import create_react_agent
|
|
|
156
156
|
# execute_tool spans. BaseRetriever.invoke gets retrieval spans.
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
##### Recommended pattern: wrap LangChain entry points in `struct.agent`
|
|
160
|
+
|
|
161
|
+
For multi-turn HTTP-style usage (every request continues the same
|
|
162
|
+
conversation), wrap your request handler in
|
|
163
|
+
`struct.agent(session_id=conversation_id)`. This is the struct-native
|
|
164
|
+
replacement for `with ls.tracing_context(parent=run_tree):` and gives
|
|
165
|
+
you two things you can't get from `configurable.thread_id` alone:
|
|
166
|
+
|
|
167
|
+
1. **Threading without per-call config plumbing.** Every nested
|
|
168
|
+
LangChain call inherits the conversation id via the SDK's ambient
|
|
169
|
+
contextvar — you don't have to ensure each `compiled_graph.ainvoke`
|
|
170
|
+
gets `thread_id` on its config.
|
|
171
|
+
2. **One trace per request.** `struct.agent` creates a parent OTel span
|
|
172
|
+
so all the LangChain work for the request nests under one trace
|
|
173
|
+
(clean tree, "Subagents" / "Spawned by" UI links work). Without it,
|
|
174
|
+
each `.invoke()` becomes its own root trace, and the UI's session
|
|
175
|
+
list shows a non-deterministic agent name (`omni_agent`,
|
|
176
|
+
`LangGraph`, the first sub-agent it sees…).
|
|
177
|
+
|
|
178
|
+
Migrating from LangSmith:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# Before — LangSmith convention, fragments under struct-sdk
|
|
182
|
+
with ls.tracing_context(parent=run_tree):
|
|
183
|
+
await orchestrator.ainvoke(inputs, config=config)
|
|
184
|
+
|
|
185
|
+
# After — struct-native, threads correctly, no langsmith dep
|
|
186
|
+
async with struct.agent(session_id=conversation_id):
|
|
187
|
+
await orchestrator.ainvoke(inputs, config=config)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Also works as a sync context manager (`with struct.agent(...)`) for
|
|
191
|
+
non-async handlers.
|
|
192
|
+
|
|
159
193
|
### LLM SDKs used directly — manual agent + tool scopes required
|
|
160
194
|
|
|
161
195
|
When you call an LLM SDK directly (no agent framework wrapping it), only
|
|
@@ -269,8 +303,19 @@ async def run_checkout(order_id: str):
|
|
|
269
303
|
return await charge()
|
|
270
304
|
```
|
|
271
305
|
|
|
272
|
-
|
|
273
|
-
|
|
306
|
+
Sub-agents (e.g. a `create_agent` graph invoked from inside another
|
|
307
|
+
agent's tool body) record their parent via the
|
|
308
|
+
`struct.agent.parent_session_id` attribute on the inner `invoke_agent`
|
|
309
|
+
span. This powers the UI's "Spawned by" backlink, which works for any
|
|
310
|
+
nested invocation.
|
|
311
|
+
|
|
312
|
+
The parent's "Subagents" forward list — the inverse direction —
|
|
313
|
+
additionally requires that the nested invoke shares the outer agent's
|
|
314
|
+
trace. This works automatically when the outer tool is built with the
|
|
315
|
+
`@tool` decorator (or any callback-aware wrapping) since the nested
|
|
316
|
+
invoke inherits the parent's run state. Bare `Tool(...)` constructors
|
|
317
|
+
that bypass the callback chain can break the forward link; the
|
|
318
|
+
backlink still renders.
|
|
274
319
|
|
|
275
320
|
## Semantic conventions
|
|
276
321
|
|
|
@@ -62,7 +62,7 @@ async with struct.agent(name="checkout"):
|
|
|
62
62
|
| `langchain_core` `BaseChatModel` | `chat {model}` | Skipped when an underlying provider SDK is also instrumented (e.g. `ChatAnthropic` + `anthropic` → a single span). |
|
|
63
63
|
| `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
|
|
64
64
|
| `langchain_core` `BaseRetriever` | `retrieval {name}` | |
|
|
65
|
-
| `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent` and custom graphs. `thread_id`
|
|
65
|
+
| `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent`, `langchain.agents.create_agent`, and custom graphs. Reads conversation id from any of: `configurable.thread_id` (LangGraph canonical), or `metadata.{thread_id, session_id, conversation_id}` (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in [`struct.agent(session_id=conv_id)`](#recommended-pattern-wrap-langchain-entry-points-in-structagent) — that's the struct-native replacement for `ls.tracing_context(parent=run_tree)`. |
|
|
66
66
|
|
|
67
67
|
## Framework integration
|
|
68
68
|
|
|
@@ -110,6 +110,40 @@ from langgraph.prebuilt import create_react_agent
|
|
|
110
110
|
# execute_tool spans. BaseRetriever.invoke gets retrieval spans.
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
##### Recommended pattern: wrap LangChain entry points in `struct.agent`
|
|
114
|
+
|
|
115
|
+
For multi-turn HTTP-style usage (every request continues the same
|
|
116
|
+
conversation), wrap your request handler in
|
|
117
|
+
`struct.agent(session_id=conversation_id)`. This is the struct-native
|
|
118
|
+
replacement for `with ls.tracing_context(parent=run_tree):` and gives
|
|
119
|
+
you two things you can't get from `configurable.thread_id` alone:
|
|
120
|
+
|
|
121
|
+
1. **Threading without per-call config plumbing.** Every nested
|
|
122
|
+
LangChain call inherits the conversation id via the SDK's ambient
|
|
123
|
+
contextvar — you don't have to ensure each `compiled_graph.ainvoke`
|
|
124
|
+
gets `thread_id` on its config.
|
|
125
|
+
2. **One trace per request.** `struct.agent` creates a parent OTel span
|
|
126
|
+
so all the LangChain work for the request nests under one trace
|
|
127
|
+
(clean tree, "Subagents" / "Spawned by" UI links work). Without it,
|
|
128
|
+
each `.invoke()` becomes its own root trace, and the UI's session
|
|
129
|
+
list shows a non-deterministic agent name (`omni_agent`,
|
|
130
|
+
`LangGraph`, the first sub-agent it sees…).
|
|
131
|
+
|
|
132
|
+
Migrating from LangSmith:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# Before — LangSmith convention, fragments under struct-sdk
|
|
136
|
+
with ls.tracing_context(parent=run_tree):
|
|
137
|
+
await orchestrator.ainvoke(inputs, config=config)
|
|
138
|
+
|
|
139
|
+
# After — struct-native, threads correctly, no langsmith dep
|
|
140
|
+
async with struct.agent(session_id=conversation_id):
|
|
141
|
+
await orchestrator.ainvoke(inputs, config=config)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Also works as a sync context manager (`with struct.agent(...)`) for
|
|
145
|
+
non-async handlers.
|
|
146
|
+
|
|
113
147
|
### LLM SDKs used directly — manual agent + tool scopes required
|
|
114
148
|
|
|
115
149
|
When you call an LLM SDK directly (no agent framework wrapping it), only
|
|
@@ -223,8 +257,19 @@ async def run_checkout(order_id: str):
|
|
|
223
257
|
return await charge()
|
|
224
258
|
```
|
|
225
259
|
|
|
226
|
-
|
|
227
|
-
|
|
260
|
+
Sub-agents (e.g. a `create_agent` graph invoked from inside another
|
|
261
|
+
agent's tool body) record their parent via the
|
|
262
|
+
`struct.agent.parent_session_id` attribute on the inner `invoke_agent`
|
|
263
|
+
span. This powers the UI's "Spawned by" backlink, which works for any
|
|
264
|
+
nested invocation.
|
|
265
|
+
|
|
266
|
+
The parent's "Subagents" forward list — the inverse direction —
|
|
267
|
+
additionally requires that the nested invoke shares the outer agent's
|
|
268
|
+
trace. This works automatically when the outer tool is built with the
|
|
269
|
+
`@tool` decorator (or any callback-aware wrapping) since the nested
|
|
270
|
+
invoke inherits the parent's run state. Bare `Tool(...)` constructors
|
|
271
|
+
that bypass the callback chain can break the forward link; the
|
|
272
|
+
backlink still renders.
|
|
228
273
|
|
|
229
274
|
## Semantic conventions
|
|
230
275
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "struct-sdk"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -142,8 +142,16 @@ def _create_common(
|
|
|
142
142
|
try:
|
|
143
143
|
result = yield f, args, kwargs
|
|
144
144
|
except Exception as e:
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
# Capture the type name OUTSIDE the lambda — ``except X as e``
|
|
146
|
+
# binds ``e`` only for the duration of the except block, but
|
|
147
|
+
# ``_safe`` is opaque to static analysis (ruff flags F841 +
|
|
148
|
+
# F821 thinking the lambda outlives the binding). Snapshotting
|
|
149
|
+
# to a local makes the closure capture trivially correct.
|
|
150
|
+
err_type = type(e).__name__
|
|
151
|
+
_safe(
|
|
152
|
+
lambda: host_span.set_attribute("error.type", err_type),
|
|
153
|
+
site="anthropic.create.enrich.error_type",
|
|
154
|
+
)
|
|
147
155
|
raise
|
|
148
156
|
_safe(
|
|
149
157
|
lambda: _set_response_attrs(host_span, sdk, model, result, otel_logger),
|
|
@@ -201,12 +209,27 @@ def _create_common(
|
|
|
201
209
|
try:
|
|
202
210
|
result = yield f, args, kwargs
|
|
203
211
|
except Exception as e:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
# Snapshot ``e`` outside the lambdas — ``except X as e`` unbinds
|
|
213
|
+
# ``e`` at end-of-block, and ruff (correctly) flags closures that
|
|
214
|
+
# reference it. The lambdas run synchronously inside the except
|
|
215
|
+
# via ``_safe``, so this is mechanically equivalent — just legible
|
|
216
|
+
# to static analysis. ``type(exc).__name__`` is cheap and won't
|
|
217
|
+
# raise; ``str(exc)`` runs inside ``_safe`` so a broken
|
|
218
|
+
# ``__str__`` can't mask the original exception we're re-raising.
|
|
219
|
+
exc = e
|
|
220
|
+
err_type = type(exc).__name__
|
|
221
|
+
_safe(
|
|
222
|
+
lambda: span.set_attribute("error.type", err_type),
|
|
223
|
+
site="anthropic.create.error_type",
|
|
224
|
+
)
|
|
225
|
+
_safe(
|
|
226
|
+
lambda: span.set_status(StatusCode.ERROR, str(exc)),
|
|
227
|
+
site="anthropic.create.error_status",
|
|
228
|
+
)
|
|
229
|
+
_safe(
|
|
230
|
+
lambda: span.record_exception(exc),
|
|
231
|
+
site="anthropic.create.record_exception",
|
|
232
|
+
)
|
|
210
233
|
raise
|
|
211
234
|
_safe(lambda: _set_response_attrs(span, sdk, model, result, otel_logger),
|
|
212
235
|
site="anthropic.create.set_response_attrs")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|