morphsdk 0.2.5__py3-none-any.whl

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.
Files changed (61) hide show
  1. morphsdk/__init__.py +54 -0
  2. morphsdk/_agent/__init__.py +64 -0
  3. morphsdk/_agent/config.py +52 -0
  4. morphsdk/_agent/explore.py +276 -0
  5. morphsdk/_agent/github.py +57 -0
  6. morphsdk/_agent/helpers.py +133 -0
  7. morphsdk/_agent/parser.py +163 -0
  8. morphsdk/_agent/runner.py +524 -0
  9. morphsdk/_agent/tools.py +171 -0
  10. morphsdk/_agent/types.py +126 -0
  11. morphsdk/_base.py +309 -0
  12. morphsdk/_client.py +245 -0
  13. morphsdk/_config.py +37 -0
  14. morphsdk/_constants.py +53 -0
  15. morphsdk/_errors.py +111 -0
  16. morphsdk/_providers/__init__.py +36 -0
  17. morphsdk/_providers/_filter.py +92 -0
  18. morphsdk/_providers/base.py +94 -0
  19. morphsdk/_providers/code_storage_http.py +104 -0
  20. morphsdk/_providers/local.py +270 -0
  21. morphsdk/_providers/remote.py +161 -0
  22. morphsdk/_version.py +1 -0
  23. morphsdk/adapters/__init__.py +1 -0
  24. morphsdk/adapters/anthropic.py +360 -0
  25. morphsdk/adapters/langchain.py +120 -0
  26. morphsdk/adapters/openai.py +500 -0
  27. morphsdk/py.typed +0 -0
  28. morphsdk/resources/__init__.py +0 -0
  29. morphsdk/resources/browser.py +919 -0
  30. morphsdk/resources/compact.py +133 -0
  31. morphsdk/resources/edit.py +506 -0
  32. morphsdk/resources/explore.py +333 -0
  33. morphsdk/resources/git.py +861 -0
  34. morphsdk/resources/github.py +1214 -0
  35. morphsdk/resources/grep.py +583 -0
  36. morphsdk/resources/mobile.py +134 -0
  37. morphsdk/resources/reflex.py +414 -0
  38. morphsdk/resources/router.py +124 -0
  39. morphsdk/resources/search.py +110 -0
  40. morphsdk/tracing/__init__.py +70 -0
  41. morphsdk/tracing/_otel.py +101 -0
  42. morphsdk/tracing/core.py +249 -0
  43. morphsdk/tracing/interaction.py +284 -0
  44. morphsdk/tracing/otel.py +75 -0
  45. morphsdk/tracing/reflex.py +58 -0
  46. morphsdk/tracing/types.py +163 -0
  47. morphsdk/types/__init__.py +140 -0
  48. morphsdk/types/browser.py +118 -0
  49. morphsdk/types/compact.py +41 -0
  50. morphsdk/types/edit.py +31 -0
  51. morphsdk/types/explore.py +42 -0
  52. morphsdk/types/git.py +25 -0
  53. morphsdk/types/github.py +111 -0
  54. morphsdk/types/grep.py +41 -0
  55. morphsdk/types/mobile.py +25 -0
  56. morphsdk/types/reflex.py +137 -0
  57. morphsdk/types/router.py +21 -0
  58. morphsdk/types/search.py +33 -0
  59. morphsdk-0.2.5.dist-info/METADATA +226 -0
  60. morphsdk-0.2.5.dist-info/RECORD +61 -0
  61. morphsdk-0.2.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,284 @@
1
+ """Morph Tracing -- interactions, tools, and manual spans.
2
+
3
+ An :class:`Interaction` is one user turn / agent run. It threads association
4
+ properties (``user_id`` / ``convo_id`` / ``event_id``) onto every span created
5
+ inside it -- including the auto-instrumented LLM spans -- so a whole conversation
6
+ stitches together in the Morph UI. Built on Traceloop's association properties
7
+ and the OpenTelemetry tracer for manual workflow/tool spans.
8
+
9
+ Attribute keys and ``traceloop.span.kind`` values are kept byte-for-byte
10
+ identical to the TypeScript SDK so both emit the same wire payload.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import uuid as _uuid
17
+ from collections.abc import Iterator
18
+ from contextlib import contextmanager
19
+ from typing import Any, Callable, TypeVar
20
+
21
+ from . import _otel
22
+ from .otel import metadata as build_metadata
23
+ from .reflex import REFLEX_RUN_ATTRIBUTE, serialize_evals
24
+ from .types import (
25
+ FinishOptions,
26
+ MetadataOptions,
27
+ SpanParams,
28
+ ToolParams,
29
+ TraceContext,
30
+ TrackToolParams,
31
+ )
32
+
33
+ T = TypeVar("T")
34
+
35
+ # Traceloop semantic-convention attribute keys.
36
+ _ASSOC = "traceloop.association.properties."
37
+ _ENTITY_INPUT = "traceloop.entity.input"
38
+ _ENTITY_OUTPUT = "traceloop.entity.output"
39
+ _ENTITY_NAME = "traceloop.entity.name"
40
+ _SPAN_KIND = "traceloop.span.kind"
41
+
42
+ _TRACER_NAME = "morphsdk.tracing"
43
+
44
+
45
+ def _uuid4() -> str:
46
+ return str(_uuid.uuid4())
47
+
48
+
49
+ def _as_string(value: Any) -> str:
50
+ if value is None:
51
+ return ""
52
+ if isinstance(value, str):
53
+ return value
54
+ return json.dumps(value, separators=(",", ":"), default=str)
55
+
56
+
57
+ def _association_props(ctx: TraceContext, extra: dict[str, str]) -> dict[str, str]:
58
+ """Build the association-property bag propagated onto child spans."""
59
+ props: dict[str, str] = dict(extra)
60
+ if ctx.user_id:
61
+ props["user_id"] = ctx.user_id
62
+ if ctx.convo_id:
63
+ props["convo_id"] = ctx.convo_id
64
+ if ctx.event_id:
65
+ props["event_id"] = ctx.event_id
66
+ if ctx.event:
67
+ props["event_name"] = ctx.event
68
+ return props
69
+
70
+
71
+ class ToolSpan:
72
+ """A manual tool span the caller drives explicitly."""
73
+
74
+ def __init__(self, span: Any, trace_content: bool) -> None:
75
+ self._span = span
76
+ self._trace_content = trace_content
77
+
78
+ def set_input(self, value: Any) -> None:
79
+ if self._trace_content:
80
+ self._span.set_attribute(_ENTITY_INPUT, _as_string(value))
81
+
82
+ def set_output(self, value: Any) -> None:
83
+ if self._trace_content:
84
+ self._span.set_attribute(_ENTITY_OUTPUT, _as_string(value))
85
+
86
+ def set_error(self, error: str | BaseException) -> None:
87
+ exc = error if isinstance(error, BaseException) else Exception(error)
88
+ status = _otel.status_codes()
89
+ self._span.record_exception(exc)
90
+ self._span.set_status(status.Status(status.StatusCode.ERROR, str(exc)))
91
+
92
+ def end(self) -> None:
93
+ self._span.end()
94
+
95
+
96
+ class Interaction:
97
+ """A single traced AI interaction (one user turn / agent run)."""
98
+
99
+ def __init__(self, initial: TraceContext, trace_content: bool) -> None:
100
+ self._ctx = initial.model_copy(deep=True)
101
+ if self._ctx.event_id is None:
102
+ self._ctx.event_id = _uuid4()
103
+ self._properties: dict[str, str] = dict(initial.properties or {})
104
+ self._input = initial.input
105
+ self._trace_content = trace_content
106
+ self._workflow_span: Any = None
107
+ # Serialized once. Set as a raw attribute on the workflow span only -- NOT
108
+ # an association property, so it is not copied onto child LLM spans. Ingest
109
+ # reads `morph.reflex.run` off the workflow-span to enqueue the evals.
110
+ self._reflex_run = serialize_evals(self._ctx.evals)
111
+
112
+ # -- accessors ---------------------------------------------------------
113
+
114
+ def get_event_id(self) -> str | None:
115
+ return self._ctx.event_id
116
+
117
+ def set_input(self, value: str) -> None:
118
+ self._input = value
119
+
120
+ def set_property(self, key: str, value: str) -> None:
121
+ self._properties[key] = value
122
+
123
+ def set_properties(self, props: dict[str, str]) -> None:
124
+ self._properties.update(props)
125
+
126
+ def vercel_ai_sdk_metadata(self) -> dict[str, str]:
127
+ """Attribution metadata for OTel-emitting SDKs (e.g. Vercel AI SDK)."""
128
+ return build_metadata(
129
+ MetadataOptions(
130
+ user_id=self._ctx.user_id or "unknown",
131
+ convo_id=self._ctx.convo_id,
132
+ event_name=self._ctx.event,
133
+ event_id=self._ctx.event_id,
134
+ properties=self._properties,
135
+ )
136
+ )
137
+
138
+ # -- internals ---------------------------------------------------------
139
+
140
+ def _workflow_name(self) -> str:
141
+ return self._ctx.event or "interaction"
142
+
143
+ @contextmanager
144
+ def _assoc(self) -> Iterator[None]:
145
+ """Activate this interaction's association properties for the block.
146
+
147
+ Traceloop copies the current association properties onto every span
148
+ created while they are set, so auto-instrumented LLM spans inherit
149
+ attribution. Restored afterwards to avoid leaking across interactions.
150
+ """
151
+ traceloop = _otel.traceloop()
152
+ props = _association_props(self._ctx, self._properties)
153
+ traceloop.set_association_properties(props)
154
+ try:
155
+ yield
156
+ finally:
157
+ traceloop.set_association_properties({})
158
+
159
+ def _ensure_workflow_span(self) -> Any:
160
+ """Open the interaction workflow span once; stays active until finish()."""
161
+ if self._workflow_span is not None:
162
+ return self._workflow_span
163
+ trace = _otel.trace_api()
164
+ tracer = trace.get_tracer(_TRACER_NAME)
165
+ span = tracer.start_span(self._workflow_name())
166
+ span.set_attribute(_SPAN_KIND, "workflow")
167
+ for key, value in _association_props(self._ctx, self._properties).items():
168
+ span.set_attribute(_ASSOC + key, value)
169
+ if self._trace_content and self._input:
170
+ span.set_attribute(_ENTITY_INPUT, self._input)
171
+ if self._reflex_run:
172
+ span.set_attribute(REFLEX_RUN_ATTRIBUTE, self._reflex_run)
173
+ self._workflow_span = span
174
+ return span
175
+
176
+ @contextmanager
177
+ def _workflow_context(self) -> Iterator[None]:
178
+ """Run a block with association props + the workflow span as parent."""
179
+ trace = _otel.trace_api()
180
+ with self._assoc():
181
+ span = self._ensure_workflow_span()
182
+ with trace.use_span(span, end_on_exit=False):
183
+ yield
184
+
185
+ @staticmethod
186
+ def _as_tool_params(params: ToolParams | str | dict[str, Any]) -> ToolParams:
187
+ if isinstance(params, ToolParams):
188
+ return params
189
+ if isinstance(params, str):
190
+ return ToolParams(name=params)
191
+ return ToolParams.model_validate(params)
192
+
193
+ @staticmethod
194
+ def _as_span_params(params: SpanParams | str | dict[str, Any]) -> SpanParams:
195
+ if isinstance(params, SpanParams):
196
+ return params
197
+ if isinstance(params, str):
198
+ return SpanParams(name=params)
199
+ return SpanParams.model_validate(params)
200
+
201
+ # -- spans -------------------------------------------------------------
202
+
203
+ def start_tool_span(self, params: ToolParams | str | dict[str, Any]) -> ToolSpan:
204
+ """Start a tool span you end manually via :meth:`ToolSpan.end`."""
205
+ trace = _otel.trace_api()
206
+ tool = self._as_tool_params(params)
207
+ tracer = trace.get_tracer(_TRACER_NAME)
208
+ span = tracer.start_span(tool.name)
209
+ span.set_attribute(_SPAN_KIND, "tool")
210
+ span.set_attribute(_ENTITY_NAME, tool.name)
211
+ for key, value in _association_props(self._ctx, self._properties).items():
212
+ span.set_attribute(_ASSOC + key, value)
213
+ if tool.properties:
214
+ for key, value in tool.properties.items():
215
+ span.set_attribute(key, value)
216
+ return ToolSpan(span, self._trace_content)
217
+
218
+ def run(self, fn: Callable[[], T]) -> T:
219
+ """Run *fn* with this interaction's association properties active.
220
+
221
+ Required so auto-instrumented OpenAI/Anthropic spans inherit
222
+ ``user_id`` / ``convo_id`` / tags.
223
+ """
224
+ with self._workflow_context():
225
+ return fn()
226
+
227
+ def with_span(self, params: SpanParams | str | dict[str, Any], fn: Callable[[], T]) -> T:
228
+ """Run *fn* inside a traced task span; LLM calls within inherit attribution."""
229
+ trace = _otel.trace_api()
230
+ task = self._as_span_params(params)
231
+ with self._workflow_context():
232
+ tracer = trace.get_tracer(_TRACER_NAME)
233
+ with tracer.start_as_current_span(task.name) as span:
234
+ span.set_attribute(_SPAN_KIND, "task")
235
+ span.set_attribute(_ENTITY_NAME, task.name)
236
+ if task.properties:
237
+ for key, value in task.properties.items():
238
+ span.set_attribute(key, value)
239
+ return fn()
240
+
241
+ def with_tool(self, params: ToolParams | str | dict[str, Any], fn: Callable[[], T]) -> T:
242
+ """Run *fn* inside a traced tool span."""
243
+ trace = _otel.trace_api()
244
+ tool = self._as_tool_params(params)
245
+ with self._workflow_context():
246
+ tracer = trace.get_tracer(_TRACER_NAME)
247
+ with tracer.start_as_current_span(tool.name) as span:
248
+ span.set_attribute(_SPAN_KIND, "tool")
249
+ span.set_attribute(_ENTITY_NAME, tool.name)
250
+ if tool.properties:
251
+ for key, value in tool.properties.items():
252
+ span.set_attribute(key, value)
253
+ return fn()
254
+
255
+ def track_tool(self, params: TrackToolParams | dict[str, Any]) -> None:
256
+ """Record an already-completed tool invocation as a single span."""
257
+ if isinstance(params, TrackToolParams):
258
+ track = params
259
+ else:
260
+ track = TrackToolParams.model_validate(params)
261
+ span = self.start_tool_span(ToolParams(name=track.name, properties=track.properties))
262
+ if track.input is not None:
263
+ span.set_input(track.input)
264
+ if track.output is not None:
265
+ span.set_output(track.output)
266
+ if track.error:
267
+ span.set_error(track.error)
268
+ span.end()
269
+
270
+ def finish(self, opts: FinishOptions | str | dict[str, Any]) -> None:
271
+ """End the interaction with its final output."""
272
+ if isinstance(opts, str):
273
+ output = opts
274
+ else:
275
+ finish = opts if isinstance(opts, FinishOptions) else FinishOptions.model_validate(opts)
276
+ output = finish.output
277
+ if finish.properties:
278
+ self._properties.update(finish.properties)
279
+ with self._assoc():
280
+ span = self._ensure_workflow_span()
281
+ if self._trace_content:
282
+ span.set_attribute(_ENTITY_OUTPUT, output)
283
+ span.end()
284
+ self._workflow_span = None
@@ -0,0 +1,75 @@
1
+ """Morph Tracing -- OpenTelemetry attribution helper.
2
+
3
+ LLM SDKs that emit their own OpenTelemetry spans (e.g. the Vercel AI SDK) only
4
+ need each call tagged with attribution metadata so Morph can stitch the spans to
5
+ a user / conversation / event. :func:`metadata` builds that tag bag.
6
+
7
+ This module has **no** OpenTelemetry dependency -- it returns a plain ``dict`` of
8
+ snake_case keys, byte-for-byte identical to the TypeScript ``otel.ts`` helper::
9
+
10
+ from morphsdk.tracing import metadata
11
+
12
+ tags = metadata(user_id="user-123", convo_id="convo-456")
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import uuid as _uuid
18
+
19
+ from .types import MetadataOptions
20
+
21
+ # Reserved keys Morph owns. Custom ``properties`` cannot overwrite these so
22
+ # attribution (user_id / convo_id / event_id / event_name) stays intact.
23
+ _RESERVED_KEYS = frozenset({"user_id", "convo_id", "event_id", "event_name"})
24
+
25
+
26
+ def _uuid4() -> str:
27
+ return str(_uuid.uuid4())
28
+
29
+
30
+ def metadata(
31
+ opts: MetadataOptions | dict[str, object] | None = None,
32
+ *,
33
+ user_id: str | None = None,
34
+ convo_id: str | None = None,
35
+ event_name: str | None = None,
36
+ event_id: str | None = None,
37
+ properties: dict[str, str] | None = None,
38
+ ) -> dict[str, str]:
39
+ """Build the attribution metadata bag for OTel-emitting LLM SDKs.
40
+
41
+ Accepts either a :class:`MetadataOptions` (or plain dict) positionally, or
42
+ keyword arguments. A fresh ``event_id`` is generated per call when omitted,
43
+ matching the TypeScript helper.
44
+
45
+ The returned keys are snake_case (``user_id``, ``convo_id``, ...) -- exactly
46
+ what Morph's ClickHouse views read.
47
+ """
48
+ if opts is not None:
49
+ resolved = (
50
+ opts if isinstance(opts, MetadataOptions) else MetadataOptions.model_validate(opts)
51
+ )
52
+ else:
53
+ if user_id is None:
54
+ raise TypeError("metadata() requires user_id")
55
+ resolved = MetadataOptions(
56
+ user_id=user_id,
57
+ convo_id=convo_id,
58
+ event_name=event_name,
59
+ event_id=event_id,
60
+ properties=properties,
61
+ )
62
+
63
+ result: dict[str, str] = {
64
+ "user_id": resolved.user_id,
65
+ "event_id": resolved.event_id or _uuid4(),
66
+ }
67
+ if resolved.convo_id:
68
+ result["convo_id"] = resolved.convo_id
69
+ if resolved.event_name:
70
+ result["event_name"] = resolved.event_name
71
+ if resolved.properties:
72
+ for key, value in resolved.properties.items():
73
+ if key not in _RESERVED_KEYS:
74
+ result[key] = value
75
+ return result
@@ -0,0 +1,58 @@
1
+ """Morph Tracing -- eval selection (which Reflexes to run on a trace).
2
+
3
+ Mirror of the TypeScript ``morphsdk/tracing/reflex.ts``. The public API is ``evals`` (see
4
+ :data:`EvalSelection`): ``{"user": [...], "assistant": [...]}`` choosing which role each model
5
+ classifies. The selection is serialized onto a single ``morph.reflex.run`` attribute on the
6
+ interaction's workflow span (NOT an association property, so it is not copied onto child LLM
7
+ spans); Morph's ingest reads it and runs the classifications async. Plain nouns in, wire detail
8
+ hidden.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+
15
+ from .types import EvalSelection
16
+
17
+ #: Raw span attribute key carrying the serialized selection (internal wire detail).
18
+ REFLEX_RUN_ATTRIBUTE = "morph.reflex.run"
19
+
20
+ # Public role -> backend transform id. user = the user's message, assistant = the agent's output.
21
+ _USER_TRANSFORM = "user_message"
22
+ _ASSISTANT_TRANSFORM = "assistant_message"
23
+
24
+
25
+ def normalize_evals(evals: EvalSelection | None) -> list[dict[str, str]]:
26
+ """Normalize a public ``evals`` selection into deduped ``{"model","transform"}`` wire entries.
27
+ ``user`` models run on the user turn, ``assistant`` models on the agent turn. Drops blank
28
+ models; first-seen order."""
29
+ if not evals:
30
+ return []
31
+ out: list[dict[str, str]] = []
32
+ seen: set[str] = set()
33
+
34
+ def add(models: list[str] | None, transform: str) -> None:
35
+ for raw in models or []:
36
+ model = raw.strip() if isinstance(raw, str) else ""
37
+ if not model:
38
+ continue
39
+ key = f"{model} {transform}"
40
+ if key in seen:
41
+ continue
42
+ seen.add(key)
43
+ out.append({"model": model, "transform": transform})
44
+
45
+ add(evals.get("user"), _USER_TRANSFORM)
46
+ add(evals.get("assistant"), _ASSISTANT_TRANSFORM)
47
+ return out
48
+
49
+
50
+ def serialize_evals(evals: EvalSelection | None) -> str | None:
51
+ """Serialize an ``evals`` selection to the ``morph.reflex.run`` string, or ``None`` when there
52
+ is nothing to run (so the attribute is omitted)."""
53
+ entries = normalize_evals(evals)
54
+ if not entries:
55
+ return None
56
+ # ensure_ascii=False to match TS JSON.stringify (raw UTF-8) byte-for-byte for non-ASCII model
57
+ # names; without it Python emits \uXXXX escapes and the wire string diverges from the TS SDK.
58
+ return json.dumps(entries, separators=(",", ":"), ensure_ascii=False)
@@ -0,0 +1,163 @@
1
+ """Morph Tracing -- public types.
2
+
3
+ Morph Tracing instruments the top AI SDKs (OpenAI, Anthropic, LangChain,
4
+ Bedrock, Vertex, Cohere, Together, LlamaIndex, ...) and ships OpenTelemetry
5
+ spans to Morph. It is a thin, Morph-branded layer over OpenLLMetry / Traceloop,
6
+ mirroring the TypeScript ``morphsdk/tracing`` module byte-for-byte on the wire.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel, ConfigDict
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Eval selection
17
+ # ---------------------------------------------------------------------------
18
+
19
+ # Always explicit per role — no implicit default, since a model run on the wrong role gives a
20
+ # meaningless label. Keys: "user" (the user's message), "assistant" (the agent's output).
21
+ EvalSelection = dict[str, list[str]]
22
+ """Reflexes (evals) to run automatically on a trace, keyed by which role each model classifies::
23
+
24
+ evals={"user": ["jailbreak", "guardrail"]}
25
+ evals={"user": ["jailbreak"], "assistant": ["leaked-thinking"]}
26
+ """
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Configuration
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ class MorphTracingConfig(BaseModel):
35
+ """Configuration for :func:`morphsdk.tracing.morph_tracing`."""
36
+
37
+ model_config = ConfigDict(extra="forbid")
38
+
39
+ api_key: str | None = None
40
+ """Morph API key. Defaults to ``MORPH_API_KEY``. Sent as
41
+ ``Authorization: Bearer <api_key>`` to the trace ingest endpoint."""
42
+
43
+ base_url: str | None = None
44
+ """Base URL for the Morph ingest API. Traces are POSTed to
45
+ ``{base_url}/v1/traces``. Defaults to ``MORPH_TRACES_URL`` then
46
+ ``https://api.morphllm.com``."""
47
+
48
+ app_name: str | None = None
49
+ """Service/app name attached to every span. Defaults to ``"morph-app"``."""
50
+
51
+ disabled: bool = False
52
+ """When true, the SDK initializes nothing and ships nothing."""
53
+
54
+ disable_batching: bool | None = None
55
+ """Send spans immediately instead of batching. Defaults to true outside
56
+ production (``MORPH_ENVIRONMENT``/``NODE_ENV`` != ``production``)."""
57
+
58
+ trace_content: bool = True
59
+ """Capture prompt/response content on spans. Set false for
60
+ zero-data-retention."""
61
+
62
+ instrument_modules: set[str] | None = None
63
+ """Explicit set of instrument names to enable. Omit to auto-instrument all
64
+ supported libraries."""
65
+
66
+ use_external_otel: bool = False
67
+ """Set true when you run your own OpenTelemetry pipeline. Morph will not
68
+ start its own; use :meth:`MorphTracing.span_processor` instead."""
69
+
70
+ headers: dict[str, str] | None = None
71
+ """Extra headers merged onto the OTLP exporter request."""
72
+
73
+ debug: bool | None = None
74
+ """Verbose ``[morph-tracing]`` logging. Defaults to
75
+ ``MORPH_TRACING_DEBUG=1``."""
76
+
77
+ evals: EvalSelection | None = None
78
+ """Default evals to run on every interaction's trace. Overridable per-interaction via
79
+ ``begin({"evals": ...})``. Requires ``begin()`` so the trace carries an event id.
80
+ Omit for none."""
81
+
82
+
83
+ class TraceContext(BaseModel):
84
+ """Identity + content for a single traced AI interaction."""
85
+
86
+ model_config = ConfigDict(extra="forbid")
87
+
88
+ event_id: str | None = None
89
+ """Stable identifier for the interaction; auto-generated if omitted."""
90
+
91
+ user_id: str | None = None
92
+ """End-user identifier."""
93
+
94
+ convo_id: str | None = None
95
+ """Conversation/session identifier grouping related interactions."""
96
+
97
+ event: str | None = None
98
+ """Event name (defaults to ``"ai_generation"`` on the backend)."""
99
+
100
+ input: str | None = None
101
+ """User input for this interaction."""
102
+
103
+ properties: dict[str, str] | None = None
104
+ """Arbitrary string metadata propagated onto spans."""
105
+
106
+ evals: EvalSelection | None = None
107
+ """Evals to run automatically on this interaction's trace. Overrides any default set in
108
+ ``morph_tracing({"evals": ...})``. Omit to inherit the default (or run none)."""
109
+
110
+
111
+ class SpanParams(BaseModel):
112
+ """Parameters for a manual task span."""
113
+
114
+ model_config = ConfigDict(extra="forbid")
115
+
116
+ name: str
117
+ properties: dict[str, str] | None = None
118
+
119
+
120
+ class ToolParams(BaseModel):
121
+ """Parameters for a tool span."""
122
+
123
+ model_config = ConfigDict(extra="forbid")
124
+
125
+ name: str
126
+ version: int | None = None
127
+ properties: dict[str, str] | None = None
128
+
129
+
130
+ class TrackToolParams(BaseModel):
131
+ """Record a tool invocation that has already completed."""
132
+
133
+ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
134
+
135
+ name: str
136
+ input: Any | None = None
137
+ output: Any | None = None
138
+ duration_ms: int | None = None
139
+ error: str | BaseException | None = None
140
+ properties: dict[str, str] | None = None
141
+
142
+
143
+ class FinishOptions(BaseModel):
144
+ """Options for ending an interaction."""
145
+
146
+ model_config = ConfigDict(extra="forbid")
147
+
148
+ output: str
149
+ properties: dict[str, str] | None = None
150
+
151
+
152
+ class MetadataOptions(BaseModel):
153
+ """Metadata payload for the Vercel AI SDK / OTel attribution."""
154
+
155
+ model_config = ConfigDict(extra="forbid")
156
+
157
+ user_id: str
158
+ convo_id: str | None = None
159
+ event_name: str | None = None
160
+ event_id: str | None = None
161
+ properties: dict[str, str] | None = None
162
+ """Custom tags, e.g. ``{"source": "support-bot"}`` ->
163
+ ``association.properties.source``."""