usegradient 1.3.1__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.
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ import time
5
+ from contextlib import contextmanager
6
+ from contextvars import ContextVar
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Iterator, Mapping
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class RunContext:
13
+ project: str = "gradient"
14
+ run_id: str = field(default_factory=lambda: "local-" + secrets.token_hex(8))
15
+ session_id: str | None = None
16
+ user_id: str | None = None
17
+ metadata: dict[str, Any] = field(default_factory=dict)
18
+ tags: list[str] = field(default_factory=list)
19
+ secrets: dict[str, str] = field(default_factory=dict, repr=False)
20
+ trace: Any | None = None
21
+ timeout_seconds: float | None = None
22
+ replay_config: dict[str, Any] = field(default_factory=dict)
23
+ environment: dict[str, Any] = field(default_factory=dict)
24
+ cancelled: bool = False
25
+ started_at: float = field(default_factory=time.time)
26
+
27
+ def child(self, **updates: Any) -> "RunContext":
28
+ values = {
29
+ "project": self.project,
30
+ "run_id": self.run_id,
31
+ "session_id": self.session_id,
32
+ "user_id": self.user_id,
33
+ "metadata": dict(self.metadata),
34
+ "tags": list(self.tags),
35
+ "secrets": dict(self.secrets),
36
+ "trace": self.trace,
37
+ "timeout_seconds": self.timeout_seconds,
38
+ "replay_config": dict(self.replay_config),
39
+ "environment": dict(self.environment),
40
+ "cancelled": self.cancelled,
41
+ "started_at": self.started_at,
42
+ }
43
+ values.update(updates)
44
+ return RunContext(**values)
45
+
46
+ def check_cancelled(self) -> None:
47
+ if self.cancelled:
48
+ raise RuntimeError("Gradient run was cancelled")
49
+
50
+ def to_event(self) -> dict[str, Any]:
51
+ return {
52
+ "project": self.project,
53
+ "run_id": self.run_id,
54
+ "session_id": self.session_id,
55
+ "user_id": self.user_id,
56
+ "metadata": dict(self.metadata),
57
+ "tags": list(self.tags),
58
+ "replay_config": dict(self.replay_config),
59
+ "environment": dict(self.environment),
60
+ }
61
+
62
+ @classmethod
63
+ def from_value(
64
+ cls,
65
+ value: "RunContext | Mapping[str, Any] | None" = None,
66
+ **defaults: Any,
67
+ ) -> "RunContext":
68
+ if isinstance(value, RunContext):
69
+ return value.child(**defaults) if defaults else value
70
+ payload = dict(value or {})
71
+ payload.update({key: item for key, item in defaults.items() if item is not None})
72
+ return RunContext(**payload)
73
+
74
+
75
+ _current_context: ContextVar[RunContext | None] = ContextVar(
76
+ "gradient_run_context",
77
+ default=None,
78
+ )
79
+
80
+
81
+ def current_context() -> RunContext | None:
82
+ return _current_context.get()
83
+
84
+
85
+ @contextmanager
86
+ def use_context(context: RunContext) -> Iterator[RunContext]:
87
+ token = _current_context.set(context)
88
+ try:
89
+ yield context
90
+ finally:
91
+ _current_context.reset(token)
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Mapping
6
+ import json
7
+ import os
8
+ import tomllib
9
+
10
+ from .errors import GradientAuthError
11
+
12
+ DEFAULT_API_URL = "https://console.usegradient.dev"
13
+ DEFAULT_REGISTRY_URL = "https://registry.usegradient.dev"
14
+ DEFAULT_PROXY_URL = "https://proxy.usegradient.dev"
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class Credentials:
19
+ api_key: str = ""
20
+ api_url: str = DEFAULT_API_URL
21
+ registry_url: str = DEFAULT_REGISTRY_URL
22
+ proxy_url: str = DEFAULT_PROXY_URL
23
+ slug: str = ""
24
+
25
+
26
+ def _credentials_path(home: str | Path | None = None) -> Path:
27
+ root = Path(home).expanduser() if home else Path.home()
28
+ return root / ".gradient" / "credentials"
29
+
30
+
31
+ def _read_credentials_file(path: Path) -> Credentials:
32
+ if not path.exists():
33
+ return Credentials()
34
+ with path.open("rb") as f:
35
+ raw = tomllib.load(f)
36
+ return Credentials(
37
+ api_key=str(raw.get("api_key", "")),
38
+ api_url=str(raw.get("api_url", "") or DEFAULT_API_URL),
39
+ registry_url=str(raw.get("registry_url", "") or DEFAULT_REGISTRY_URL),
40
+ proxy_url=str(raw.get("proxy_url", "") or DEFAULT_PROXY_URL),
41
+ slug=str(raw.get("slug", "")),
42
+ )
43
+
44
+
45
+ def load_credentials(
46
+ *,
47
+ path: str | Path | None = None,
48
+ env: Mapping[str, str] | None = None,
49
+ home: str | Path | None = None,
50
+ require_api_key: bool = False,
51
+ ) -> Credentials:
52
+ """Load Gradient credentials from disk and environment variables."""
53
+
54
+ values = _read_credentials_file(Path(path).expanduser() if path else _credentials_path(home))
55
+ environ = os.environ if env is None else env
56
+
57
+ if environ.get("GRADIENT_API_KEY"):
58
+ values.api_key = environ["GRADIENT_API_KEY"]
59
+ if environ.get("GRADIENT_API_URL"):
60
+ values.api_url = environ["GRADIENT_API_URL"]
61
+ if environ.get("GRADIENT_REGISTRY_URL"):
62
+ values.registry_url = environ["GRADIENT_REGISTRY_URL"]
63
+ if environ.get("GRADIENT_PROXY_URL"):
64
+ values.proxy_url = environ["GRADIENT_PROXY_URL"]
65
+
66
+ values.api_url = (values.api_url or DEFAULT_API_URL).rstrip("/")
67
+ values.registry_url = (values.registry_url or DEFAULT_REGISTRY_URL).rstrip("/")
68
+ values.proxy_url = (values.proxy_url or DEFAULT_PROXY_URL).rstrip("/")
69
+
70
+ if require_api_key and not values.api_key:
71
+ raise GradientAuthError(
72
+ "not logged in. Set GRADIENT_API_KEY or run `gradient login --api-key grad_live_...`."
73
+ )
74
+ return values
75
+
76
+
77
+ def save_credentials(credentials: Credentials, *, path: str | Path | None = None) -> Path:
78
+ """Write credentials in the same TOML format used by the Gradient CLI."""
79
+
80
+ target = Path(path).expanduser() if path else _credentials_path()
81
+ target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
82
+ lines = [
83
+ f"api_key = {json.dumps(credentials.api_key)}",
84
+ f"api_url = {json.dumps(credentials.api_url.rstrip('/'))}",
85
+ f"registry_url = {json.dumps(credentials.registry_url)}",
86
+ f"proxy_url = {json.dumps(credentials.proxy_url)}",
87
+ f"slug = {json.dumps(credentials.slug)}",
88
+ "",
89
+ ]
90
+ target.write_text("\n".join(lines), encoding="utf-8")
91
+ target.chmod(0o600)
92
+ return target
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import inspect
5
+ import json
6
+ from contextlib import contextmanager
7
+ from typing import Any, Callable, TypeVar
8
+
9
+ from .tools import tool_from_function
10
+ from .tracing import get_tracer
11
+
12
+ F = TypeVar("F", bound=Callable[..., Any])
13
+
14
+
15
+ def _serialize_call_args(fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
16
+ signature = inspect.signature(fn)
17
+ bound = signature.bind_partial(*args, **kwargs)
18
+ bound.apply_defaults()
19
+ payload = {key: value for key, value in bound.arguments.items() if key != "self"}
20
+ return json.dumps(payload, default=str)
21
+
22
+
23
+ def _wrap_callable(
24
+ fn: F,
25
+ *,
26
+ name: str | None,
27
+ kind: str,
28
+ extra_attributes: dict[str, Any] | None = None,
29
+ register_tool: bool = False,
30
+ description: str | None = None,
31
+ parameters: dict[str, Any] | None = None,
32
+ ) -> F:
33
+ span_name = name or fn.__name__
34
+ tool_obj = tool_from_function(fn, name=span_name, description=description, parameters=parameters) if register_tool else None
35
+
36
+ if inspect.iscoroutinefunction(fn):
37
+
38
+ @functools.wraps(fn)
39
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
40
+ tracer = get_tracer()
41
+ with tracer.start_span(span_name, kind=kind, attributes=extra_attributes or {}) as span:
42
+ span.set_attribute("input.value", _serialize_call_args(fn, args, kwargs))
43
+ try:
44
+ result = await fn(*args, **kwargs)
45
+ span.set_output(result)
46
+ return result
47
+ except Exception as exc:
48
+ span.record_exception(exc)
49
+ raise
50
+
51
+ if tool_obj is not None:
52
+ async_wrapper.gradient_tool = tool_obj # type: ignore[attr-defined]
53
+ return async_wrapper # type: ignore[return-value]
54
+
55
+ @functools.wraps(fn)
56
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
57
+ tracer = get_tracer()
58
+ with tracer.start_span(span_name, kind=kind, attributes=extra_attributes or {}) as span:
59
+ span.set_attribute("input.value", _serialize_call_args(fn, args, kwargs))
60
+ try:
61
+ result = fn(*args, **kwargs)
62
+ span.set_output(result)
63
+ return result
64
+ except Exception as exc:
65
+ span.record_exception(exc)
66
+ raise
67
+
68
+ if tool_obj is not None:
69
+ sync_wrapper.gradient_tool = tool_obj # type: ignore[attr-defined]
70
+ return sync_wrapper # type: ignore[return-value]
71
+
72
+
73
+ def tool(
74
+ fn: F | None = None,
75
+ *,
76
+ name: str | None = None,
77
+ description: str | None = None,
78
+ parameters: dict[str, Any] | None = None,
79
+ ) -> Callable[[F], F] | F:
80
+ def decorator(inner: F) -> F:
81
+ return _wrap_callable(
82
+ inner,
83
+ name=name or inner.__name__,
84
+ kind="TOOL",
85
+ register_tool=True,
86
+ description=description,
87
+ parameters=parameters,
88
+ )
89
+
90
+ if fn is not None:
91
+ return decorator(fn)
92
+ return decorator
93
+
94
+
95
+ def llm(
96
+ fn: F | None = None,
97
+ *,
98
+ name: str | None = None,
99
+ provider: str | None = None,
100
+ model: str | None = None,
101
+ ) -> Callable[[F], F] | F:
102
+ attrs: dict[str, Any] = {}
103
+ if provider:
104
+ attrs["llm.provider"] = provider
105
+ if model:
106
+ attrs["llm.model_name"] = model
107
+
108
+ def decorator(inner: F) -> F:
109
+ return _wrap_callable(inner, name=name or inner.__name__, kind="LLM", extra_attributes=attrs)
110
+
111
+ if fn is not None:
112
+ return decorator(fn)
113
+ return decorator
114
+
115
+
116
+ def chain(fn: F | None = None, *, name: str | None = None) -> Callable[[F], F] | F:
117
+ def decorator(inner: F) -> F:
118
+ return _wrap_callable(inner, name=name or inner.__name__, kind="CHAIN")
119
+
120
+ if fn is not None:
121
+ return decorator(fn)
122
+ return decorator
123
+
124
+
125
+ def task(fn: F | None = None, *, name: str | None = None) -> Callable[[F], F] | F:
126
+ def decorator(inner: F) -> F:
127
+ return _wrap_callable(inner, name=name or inner.__name__, kind="CHAIN")
128
+
129
+ if fn is not None:
130
+ return decorator(fn)
131
+ return decorator
132
+
133
+
134
+ workflow = chain
135
+
136
+
137
+ def agent(fn: F | None = None, *, name: str | None = None) -> Callable[[F], F] | F:
138
+ def decorator(inner: F) -> F:
139
+ return _wrap_callable(inner, name=name or inner.__name__, kind="AGENT")
140
+
141
+ if fn is not None:
142
+ return decorator(fn)
143
+ return decorator
144
+
145
+
146
+ agent.step = task # type: ignore[attr-defined]
147
+
148
+
149
+ def retriever(fn: F | None = None, *, name: str | None = None) -> Callable[[F], F] | F:
150
+ def decorator(inner: F) -> F:
151
+ return _wrap_callable(inner, name=name or inner.__name__, kind="RETRIEVER")
152
+
153
+ if fn is not None:
154
+ return decorator(fn)
155
+ return decorator
156
+
157
+
158
+ def embedding(fn: F | None = None, *, name: str | None = None) -> Callable[[F], F] | F:
159
+ def decorator(inner: F) -> F:
160
+ return _wrap_callable(inner, name=name or inner.__name__, kind="EMBEDDING")
161
+
162
+ if fn is not None:
163
+ return decorator(fn)
164
+ return decorator
165
+
166
+
167
+ class _TraceContext:
168
+ def __init__(self, name: str, kind: str = "CHAIN", attributes: dict[str, Any] | None = None) -> None:
169
+ self.name = name
170
+ self.kind = kind
171
+ self.attributes = attributes or {}
172
+ self._span = None
173
+
174
+ def __enter__(self):
175
+ tracer = get_tracer()
176
+ self._span = tracer.start_span(self.name, kind=self.kind, attributes=self.attributes)
177
+ return self._span.__enter__()
178
+
179
+ def __exit__(self, exc_type, exc, tb):
180
+ if self._span is not None:
181
+ return self._span.__exit__(exc_type, exc, tb)
182
+ return False
183
+
184
+
185
+ def trace(name: str | None = None, *, kind: str = "CHAIN", attributes: dict[str, Any] | None = None):
186
+ if callable(name):
187
+ fn = name
188
+ return _wrap_callable(fn, name=fn.__name__, kind=kind, extra_attributes=attributes)
189
+
190
+ resolved_name = name or "trace"
191
+
192
+ def decorator(fn: F) -> F:
193
+ return _wrap_callable(fn, name=resolved_name, kind=kind, extra_attributes=attributes)
194
+
195
+ return decorator if name is not None else _TraceContext(resolved_name, kind=kind, attributes=attributes)
196
+
197
+
198
+ @contextmanager
199
+ def trace_context(name: str, *, kind: str = "CHAIN", attributes: dict[str, Any] | None = None):
200
+ tracer = get_tracer()
201
+ with tracer.start_span(name, kind=kind, attributes=attributes or {}) as span:
202
+ yield span
gradient_sdk/errors.py ADDED
@@ -0,0 +1,30 @@
1
+ class GradientError(Exception):
2
+ """Base exception for Gradient SDK errors."""
3
+
4
+
5
+ class GradientAuthError(GradientError):
6
+ """Raised when the API key is missing, invalid, or revoked."""
7
+
8
+
9
+ class GradientAPIError(GradientError):
10
+ """Raised for non-2xx Gradient API responses."""
11
+
12
+ def __init__(
13
+ self,
14
+ status: int,
15
+ method: str,
16
+ path: str,
17
+ message: str,
18
+ *,
19
+ fly_request_id: str | None = None,
20
+ response_body: str | None = None,
21
+ ) -> None:
22
+ self.status = status
23
+ self.method = method
24
+ self.path = path
25
+ self.message = message
26
+ self.fly_request_id = fly_request_id
27
+ self.response_body = response_body
28
+ suffix = f" (fly-request-id={fly_request_id})" if fly_request_id else ""
29
+ super().__init__(f"Gradient API {status} {method} {path}: {message}{suffix}")
30
+
gradient_sdk/graph.py ADDED
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections.abc import AsyncIterator, Iterator
5
+ from typing import Any, Callable
6
+
7
+ from .tracing import get_tracer
8
+
9
+ START = "__start__"
10
+ END = "__end__"
11
+
12
+
13
+ class CompiledGraph:
14
+ def __init__(
15
+ self,
16
+ *,
17
+ name: str,
18
+ nodes: dict[str, Callable[..., Any]],
19
+ edges: dict[str, list[str]],
20
+ conditional_edges: dict[str, tuple[Callable[..., Any], dict[str, str] | None]],
21
+ entry_point: str,
22
+ recursion_limit: int,
23
+ ) -> None:
24
+ self.name = name
25
+ self.nodes = nodes
26
+ self.edges = edges
27
+ self.conditional_edges = conditional_edges
28
+ self.entry_point = entry_point
29
+ self.recursion_limit = recursion_limit
30
+
31
+ def invoke(self, state: dict[str, Any], *, config: dict[str, Any] | None = None) -> dict[str, Any]:
32
+ tracer = get_tracer()
33
+ with tracer.start_span(f"workflow.{self.name}", kind="CHAIN") as root_span:
34
+ root_span.set_attribute("input.value", str(state))
35
+ current = dict(state)
36
+ current_node = self.entry_point
37
+ steps = 0
38
+ while current_node not in (END, None):
39
+ if steps >= self.recursion_limit:
40
+ raise RuntimeError(f"workflow recursion limit exceeded ({self.recursion_limit})")
41
+ steps += 1
42
+ if current_node not in self.nodes:
43
+ raise KeyError(f"unknown workflow node: {current_node}")
44
+ node_fn = self.nodes[current_node]
45
+ with tracer.start_span(f"workflow.node.{current_node}", kind="CHAIN") as node_span:
46
+ node_span.set_attribute("workflow.node", current_node)
47
+ update = self._call_node(node_fn, current, config or {})
48
+ if update:
49
+ current.update(update)
50
+ node_span.set_output(update or {})
51
+ current_node = self._resolve_next(current_node, current)
52
+ root_span.set_output(current)
53
+ return current
54
+
55
+ async def ainvoke(self, state: dict[str, Any], *, config: dict[str, Any] | None = None) -> dict[str, Any]:
56
+ tracer = get_tracer()
57
+ with tracer.start_span(f"workflow.{self.name}", kind="CHAIN") as root_span:
58
+ root_span.set_attribute("input.value", str(state))
59
+ current = dict(state)
60
+ current_node = self.entry_point
61
+ steps = 0
62
+ while current_node not in (END, None):
63
+ if steps >= self.recursion_limit:
64
+ raise RuntimeError(f"workflow recursion limit exceeded ({self.recursion_limit})")
65
+ steps += 1
66
+ if current_node not in self.nodes:
67
+ raise KeyError(f"unknown workflow node: {current_node}")
68
+ node_fn = self.nodes[current_node]
69
+ with tracer.start_span(f"workflow.node.{current_node}", kind="CHAIN") as node_span:
70
+ node_span.set_attribute("workflow.node", current_node)
71
+ update = await self._acall_node(node_fn, current, config or {})
72
+ if update:
73
+ current.update(update)
74
+ node_span.set_output(update or {})
75
+ current_node = self._resolve_next(current_node, current)
76
+ root_span.set_output(current)
77
+ return current
78
+
79
+ def stream(self, state: dict[str, Any], *, config: dict[str, Any] | None = None) -> Iterator[tuple[str, dict[str, Any]]]:
80
+ current = dict(state)
81
+ current_node = self.entry_point
82
+ steps = 0
83
+ while current_node not in (END, None):
84
+ if steps >= self.recursion_limit:
85
+ raise RuntimeError(f"workflow recursion limit exceeded ({self.recursion_limit})")
86
+ steps += 1
87
+ if current_node not in self.nodes:
88
+ raise KeyError(f"unknown workflow node: {current_node}")
89
+ update = self._call_node(self.nodes[current_node], current, config or {})
90
+ if update:
91
+ current.update(update)
92
+ yield current_node, update or {}
93
+ current_node = self._resolve_next(current_node, current)
94
+
95
+ async def astream(
96
+ self,
97
+ state: dict[str, Any],
98
+ *,
99
+ config: dict[str, Any] | None = None,
100
+ ) -> AsyncIterator[tuple[str, dict[str, Any]]]:
101
+ current = dict(state)
102
+ current_node = self.entry_point
103
+ steps = 0
104
+ while current_node not in (END, None):
105
+ if steps >= self.recursion_limit:
106
+ raise RuntimeError(f"workflow recursion limit exceeded ({self.recursion_limit})")
107
+ steps += 1
108
+ if current_node not in self.nodes:
109
+ raise KeyError(f"unknown workflow node: {current_node}")
110
+ update = await self._acall_node(self.nodes[current_node], current, config or {})
111
+ if update:
112
+ current.update(update)
113
+ yield current_node, update or {}
114
+ current_node = self._resolve_next(current_node, current)
115
+
116
+ def _resolve_next(self, node: str, state: dict[str, Any]) -> str | None:
117
+ if node in self.conditional_edges:
118
+ router, mapping = self.conditional_edges[node]
119
+ route = router(state)
120
+ if mapping is not None:
121
+ return mapping.get(route, route)
122
+ return route
123
+ next_nodes = self.edges.get(node, [])
124
+ if not next_nodes:
125
+ return END
126
+ return next_nodes[0]
127
+
128
+ def _call_node(self, fn: Callable[..., Any], state: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
129
+ result = fn(state, config=config) if _accepts_config(fn) else fn(state)
130
+ if inspect.isawaitable(result):
131
+ raise TypeError("async node functions require CompiledGraph.ainvoke()")
132
+ return result
133
+
134
+ async def _acall_node(self, fn: Callable[..., Any], state: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
135
+ if inspect.iscoroutinefunction(fn):
136
+ return await fn(state, config=config) if _accepts_config(fn) else await fn(state)
137
+ return fn(state, config=config) if _accepts_config(fn) else fn(state)
138
+
139
+
140
+ def _accepts_config(fn: Callable[..., Any]) -> bool:
141
+ try:
142
+ return "config" in inspect.signature(fn).parameters
143
+ except (TypeError, ValueError):
144
+ return False
145
+
146
+
147
+ class StateGraph:
148
+ def __init__(self, state_schema: Any = dict, *, name: str = "workflow") -> None:
149
+ self.state_schema = state_schema
150
+ self.name = name
151
+ self._nodes: dict[str, Callable[..., Any]] = {}
152
+ self._edges: dict[str, list[str]] = {}
153
+ self._conditional_edges: dict[str, tuple[Callable[..., Any], dict[str, str] | None]] = {}
154
+ self._entry_point: str | None = None
155
+
156
+ def add_node(self, name: str, fn: Callable[..., Any]) -> "StateGraph":
157
+ self._nodes[name] = fn
158
+ return self
159
+
160
+ def add_edge(self, source: str, target: str) -> "StateGraph":
161
+ if source == START:
162
+ self._entry_point = target
163
+ return self
164
+ self._edges.setdefault(source, []).append(target)
165
+ return self
166
+
167
+ def add_conditional_edges(
168
+ self,
169
+ source: str,
170
+ router: Callable[..., Any],
171
+ mapping: dict[str, str] | None = None,
172
+ ) -> "StateGraph":
173
+ self._conditional_edges[source] = (router, mapping)
174
+ return self
175
+
176
+ def set_entry_point(self, name: str) -> "StateGraph":
177
+ self._entry_point = name
178
+ return self
179
+
180
+ def compile(self, *, recursion_limit: int = 25) -> CompiledGraph:
181
+ if not self._entry_point:
182
+ raise ValueError("workflow entry point is not set")
183
+ return CompiledGraph(
184
+ name=self.name,
185
+ nodes=dict(self._nodes),
186
+ edges={key: list(value) for key, value in self._edges.items()},
187
+ conditional_edges=dict(self._conditional_edges),
188
+ entry_point=self._entry_point,
189
+ recursion_limit=recursion_limit,
190
+ )
@@ -0,0 +1,7 @@
1
+ """Provider integrations for automatic LLM tracing."""
2
+
3
+ from .agent import generate, run_agent_loop
4
+ from .anthropic import wrap_anthropic
5
+ from .openai import wrap_openai
6
+
7
+ __all__ = ["generate", "run_agent_loop", "wrap_anthropic", "wrap_openai"]