Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit 8a3da66

Browse files
add minor improvements
1 parent e613584 commit 8a3da66

4 files changed

Lines changed: 139 additions & 153 deletions

File tree

overmind_sdk/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from .tracing import init, get_tracer, set_user, set_tag, capture_exception
1212
from opentelemetry.overmind.prompt import PromptString
1313

14-
from .tracer import observe, SpanType, function, entry_point, workflow, tool
14+
from .tracer import observe, SpanType, span, function, entry_point, workflow, tool
1515

1616

17-
__version__ = "0.1.32"
17+
__version__ = "0.1.33"
1818
__all__ = [
1919
"OvermindClient",
2020
"OvermindError",
@@ -27,6 +27,7 @@
2727
"capture_exception",
2828
"PromptString",
2929
"observe",
30+
"span",
3031
"SpanType",
3132
"function",
3233
"entry_point",

overmind_sdk/tracer.py

Lines changed: 101 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
capturing inputs and outputs in OpenTelemetry style.
66
"""
77

8+
from contextlib import contextmanager
89
from enum import Enum
910
from functools import wraps
1011
import inspect
@@ -23,28 +24,40 @@ class SpanType(str, Enum):
2324
TOOL = "tool"
2425

2526

26-
def _prepare_for_otel(value: Any) -> Any:
27-
"""
28-
Prepare a value for inclusion in OpenTelemetry span attributes.
27+
_SKIP_INPUT_TYPES = (
28+
"Console", "Progress", "Live", "Table", "Panel",
29+
"TracerProvider", "Tracer", "Span",
30+
)
2931

30-
Converts complex types to serializable formats while preserving simple types.
31-
"""
32-
# Simple types can be used directly
32+
33+
def _should_skip_value(value: Any) -> bool:
34+
type_name = type(value).__name__
35+
return type_name in _SKIP_INPUT_TYPES
36+
37+
38+
def _prepare_for_otel(value: Any) -> Any:
3339
if isinstance(value, (str, int, float, bool, type(None))):
3440
return value
3541

36-
# For Pydantic models, convert to dict
42+
if _should_skip_value(value):
43+
return f"<{type(value).__name__}>"
44+
3745
if hasattr(value, "model_dump"):
3846
try:
3947
return value.model_dump()
4048
except Exception:
4149
return str(value)
4250

43-
# For dicts, lists, tuples - return as-is (will be serialized later)
4451
if isinstance(value, (dict, list, tuple)):
4552
return value
4653

47-
# For other types, convert to string
54+
if isinstance(value, (set, frozenset)):
55+
return list(value)
56+
57+
from pathlib import PurePath
58+
if isinstance(value, PurePath):
59+
return str(value)
60+
4861
return str(value)
4962

5063

@@ -54,36 +67,11 @@ def observe(span_name: Optional[str] = None, type: SpanType = SpanType.FUNCTION)
5467
5568
Captures function inputs and outputs as span attributes in OTEL style.
5669
Works with both synchronous and asynchronous functions.
57-
58-
Args:
59-
span_name: Optional name for the span. If not provided, uses the function name.
60-
type: The type of span, as a SpanType enum value.
61-
62-
Example:
63-
```python
64-
@observe(span_name="process_data", type=SpanType.FUNCTION)
65-
def process_data(user_id: int, data: dict):
66-
return {"result": "success"}
67-
68-
@observe() # Uses function name as span name
69-
async def async_operation(param: str):
70-
return await some_async_call(param)
71-
```
72-
73-
The decorator will:
74-
- Create a span with the specified name (or function name)
75-
- Set the span name and type as attributes
76-
- Capture all function arguments as span attributes (skips self/cls for class methods)
77-
- Capture the return value as a span attribute
78-
- Record exceptions if they occur
79-
- Set appropriate span status codes
8070
"""
8171

8272
def decorator(func: Callable) -> Callable:
83-
# Determine span name
8473
name = span_name or func.__name__
8574

86-
# Check if function is async
8775
is_async = inspect.iscoroutinefunction(func)
8876

8977
if is_async:
@@ -92,56 +80,42 @@ def decorator(func: Callable) -> Callable:
9280
async def async_wrapper(*args, **kwargs):
9381
tracer = get_tracer()
9482

95-
# Get function signature for better argument names
9683
sig = inspect.signature(func)
97-
bound_args = sig.bind(*args, **kwargs)
98-
bound_args.apply_defaults()
99-
100-
# Check if this is a method (has self or cls as first parameter)
10184
param_names = list(sig.parameters.keys())
10285
is_method = len(param_names) > 0 and param_names[0] in ("self", "cls")
10386
start_idx = 1 if is_method else 0
10487

105-
# Start span
106-
with tracer.start_as_current_span(name) as span:
88+
with tracer.start_as_current_span(name) as otel_span:
10789
try:
108-
span.set_attribute("name", name)
109-
span.set_attribute("type", type.value)
90+
otel_span.set_attribute("name", name)
91+
otel_span.set_attribute("type", type.value)
11092

111-
# Capture inputs
11293
inputs = {}
113-
114-
# Add positional arguments (skip self/cls if it's a method)
11594
for i, arg in enumerate(args[start_idx:], start=start_idx):
116-
if i < len(param_names):
117-
param_name = param_names[i]
118-
inputs[param_name] = _prepare_for_otel(arg)
119-
else:
120-
inputs[f"arg_{i}"] = _prepare_for_otel(arg)
95+
if _should_skip_value(arg):
96+
continue
97+
param_name = param_names[i] if i < len(param_names) else f"arg_{i}"
98+
inputs[param_name] = _prepare_for_otel(arg)
12199

122-
# Add keyword arguments
123100
for key, value in kwargs.items():
101+
if _should_skip_value(value):
102+
continue
124103
inputs[key] = _prepare_for_otel(value)
125104

126-
# Set input attributes on span (serialize the entire inputs dict)
127-
span.set_attribute("inputs", serialize(inputs))
105+
otel_span.set_attribute("inputs", serialize(inputs))
128106

129-
# Execute function
130107
result = await func(*args, **kwargs)
131108

132-
# Capture output
133109
output = _prepare_for_otel(result)
134-
span.set_attribute("outputs", serialize(output))
110+
otel_span.set_attribute("outputs", serialize(output))
135111

136-
# Set success status
137-
span.set_status(Status(StatusCode.OK))
112+
otel_span.set_status(Status(StatusCode.OK))
138113

139114
return result
140115

141116
except Exception as e:
142-
# Record exception
143-
span.record_exception(e)
144-
span.set_status(Status(StatusCode.ERROR, str(e)))
117+
otel_span.record_exception(e)
118+
otel_span.set_status(Status(StatusCode.ERROR, str(e)))
145119
raise
146120

147121
return async_wrapper
@@ -151,63 +125,95 @@ async def async_wrapper(*args, **kwargs):
151125
def sync_wrapper(*args, **kwargs):
152126
tracer = get_tracer()
153127

154-
# Get function signature for better argument names
155128
sig = inspect.signature(func)
156-
bound_args = sig.bind(*args, **kwargs)
157-
bound_args.apply_defaults()
158-
159-
# Check if this is a method (has self or cls as first parameter)
160129
param_names = list(sig.parameters.keys())
161130
is_method = len(param_names) > 0 and param_names[0] in ("self", "cls")
162131
start_idx = 1 if is_method else 0
163132

164-
# Start span
165-
with tracer.start_as_current_span(name) as span:
133+
with tracer.start_as_current_span(name) as otel_span:
166134
try:
167-
span.set_attribute("name", name)
168-
span.set_attribute("type", type.value)
135+
otel_span.set_attribute("name", name)
136+
otel_span.set_attribute("type", type.value)
169137

170-
# Capture inputs
171138
inputs = {}
172-
173-
# Add positional arguments (skip self/cls if it's a method)
174139
for i, arg in enumerate(args[start_idx:], start=start_idx):
175-
if i < len(param_names):
176-
param_name = param_names[i]
177-
inputs[param_name] = _prepare_for_otel(arg)
178-
else:
179-
inputs[f"arg_{i}"] = _prepare_for_otel(arg)
140+
if _should_skip_value(arg):
141+
continue
142+
param_name = param_names[i] if i < len(param_names) else f"arg_{i}"
143+
inputs[param_name] = _prepare_for_otel(arg)
180144

181-
# Add keyword arguments
182145
for key, value in kwargs.items():
146+
if _should_skip_value(value):
147+
continue
183148
inputs[key] = _prepare_for_otel(value)
184149

185-
# Set input attributes on span (serialize the entire inputs dict)
186-
span.set_attribute("inputs", serialize(inputs))
150+
otel_span.set_attribute("inputs", serialize(inputs))
187151

188-
# Execute function
189152
result = func(*args, **kwargs)
190153

191-
# Capture output
192154
output = _prepare_for_otel(result)
193-
span.set_attribute("outputs", serialize(output))
155+
otel_span.set_attribute("outputs", serialize(output))
194156

195-
# Set success status
196-
span.set_status(Status(StatusCode.OK))
157+
otel_span.set_status(Status(StatusCode.OK))
197158

198159
return result
199160

200161
except Exception as e:
201-
# Record exception
202-
span.record_exception(e)
203-
span.set_status(Status(StatusCode.ERROR, str(e)))
162+
otel_span.record_exception(e)
163+
otel_span.set_status(Status(StatusCode.ERROR, str(e)))
204164
raise
205165

206166
return sync_wrapper
207167

208168
return decorator
209169

210170

171+
@contextmanager
172+
def span(name: str, span_type: SpanType = SpanType.FUNCTION, attributes: dict[str, Any] | None = None):
173+
"""Context manager that creates a child span under the current trace.
174+
175+
Use this for explicit span creation in loops or conditional blocks
176+
where a decorator isn't practical.
177+
178+
Example::
179+
180+
for i in range(iterations):
181+
with span("iteration", attributes={"iteration": i, "score": score}):
182+
# ... iteration work ...
183+
set_tag("decision", "keep")
184+
"""
185+
tracer = get_tracer()
186+
with tracer.start_as_current_span(name) as otel_span:
187+
otel_span.set_attribute("name", name)
188+
otel_span.set_attribute("type", span_type.value)
189+
if attributes:
190+
for key, value in attributes.items():
191+
_safe_set_attribute(otel_span, key, value)
192+
try:
193+
yield otel_span
194+
except Exception as e:
195+
otel_span.record_exception(e)
196+
otel_span.set_status(Status(StatusCode.ERROR, str(e)))
197+
raise
198+
else:
199+
otel_span.set_status(Status(StatusCode.OK))
200+
201+
202+
def _safe_set_attribute(otel_span, key: str, value: Any) -> None:
203+
"""Set a span attribute, coercing the value to an OTel-compatible type."""
204+
if isinstance(value, (str, int, float, bool)):
205+
otel_span.set_attribute(key, value)
206+
elif value is None:
207+
otel_span.set_attribute(key, "")
208+
elif isinstance(value, (list, tuple)):
209+
if all(isinstance(v, str) for v in value):
210+
otel_span.set_attribute(key, list(value))
211+
else:
212+
otel_span.set_attribute(key, serialize(value))
213+
else:
214+
otel_span.set_attribute(key, str(value))
215+
216+
211217
def set_workflow_name(workflow_name: str) -> None:
212218
attach(set_value("workflow_name", workflow_name))
213219

@@ -217,16 +223,11 @@ def set_agent_name(agent_name: str) -> None:
217223

218224

219225
def set_conversation_id(conversation_id: str):
220-
"""
221-
Set the conversation ID for the current context.
222-
"""
223226
attach(set_value("conversation_id", conversation_id))
224227

225228

226229
def conversation(conversation_id: str):
227-
"""
228-
Decorator that automatically traces a conversation with OpenTelemetry.
229-
"""
230+
"""Decorator that sets a conversation ID in the current context."""
230231

231232
def decorator(fn: Callable) -> Callable:
232233
if inspect.iscoroutinefunction(fn):
@@ -250,60 +251,20 @@ def sync_wrapper(*args, **kwargs):
250251

251252

252253
def function(name: Optional[str] = None):
253-
"""
254-
Decorator that traces a function span.
255-
256-
Args:
257-
name: Optional span name. Defaults to the decorated function's name.
258-
259-
Example:
260-
@function(name="process_data")
261-
def process_data(user_id: int):
262-
...
263-
"""
254+
"""Decorator that traces a function span."""
264255
return observe(span_name=name, type=SpanType.FUNCTION)
265256

266257

267258
def entry_point(name: Optional[str] = None):
268-
"""
269-
Decorator that traces an entry point span.
270-
271-
Args:
272-
name: Optional span name. Defaults to the decorated function's name.
273-
274-
Example:
275-
@entry_point(name="api_handler")
276-
async def handle_request(request):
277-
...
278-
"""
259+
"""Decorator that traces an entry point span."""
279260
return observe(span_name=name, type=SpanType.ENTRY_POINT)
280261

281262

282263
def workflow(name: Optional[str] = None):
283-
"""
284-
Decorator that traces a workflow span.
285-
286-
Args:
287-
name: Optional span name. Defaults to the decorated function's name.
288-
289-
Example:
290-
@workflow(name="onboarding_flow")
291-
def run_onboarding(user_id: str):
292-
...
293-
"""
264+
"""Decorator that traces a workflow span."""
294265
return observe(span_name=name, type=SpanType.WORKFLOW)
295266

296267

297268
def tool(name: Optional[str] = None):
298-
"""
299-
Decorator that traces a tool span.
300-
301-
Args:
302-
name: Optional span name. Defaults to the decorated function's name.
303-
304-
Example:
305-
@tool(name="web_search")
306-
def search(query: str):
307-
...
308-
"""
269+
"""Decorator that traces a tool span."""
309270
return observe(span_name=name, type=SpanType.TOOL)

0 commit comments

Comments
 (0)