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,122 @@
1
+ from .client import GradientClient, default_machine_template
2
+ from .agent import Agent, AgentResult
3
+ from .context import RunContext, current_context
4
+ from .credentials import (
5
+ DEFAULT_API_URL,
6
+ DEFAULT_PROXY_URL,
7
+ DEFAULT_REGISTRY_URL,
8
+ Credentials,
9
+ load_credentials,
10
+ save_credentials,
11
+ )
12
+ from .decorators import agent, chain, embedding, llm, retriever, task, tool, trace, trace_context, workflow
13
+ from .errors import GradientAPIError, GradientAuthError, GradientError
14
+ from .graph import END, START, StateGraph
15
+ from .integrations import generate, run_agent_loop, wrap_anthropic, wrap_openai
16
+ from .models import (
17
+ AppSecret,
18
+ CreateTraceRunResponse,
19
+ CreateMachineResponse,
20
+ ExecMachineResponse,
21
+ Guest,
22
+ MachineCreateRequest,
23
+ MachineListItem,
24
+ MachineSpec,
25
+ PortEntry,
26
+ ReplayTraceRunResponse,
27
+ Restart,
28
+ ServiceConfig,
29
+ TraceEvent,
30
+ TraceRun,
31
+ TraceSpan,
32
+ WhoamiResponse,
33
+ )
34
+ from .model_catalog import Model, ModelSpec, supported_models
35
+ from .runtime import (
36
+ BuildHandle,
37
+ DeploymentHandle,
38
+ Environment,
39
+ Gradient,
40
+ ProgressEvent,
41
+ RemoteDeployment,
42
+ RemoteRunResult,
43
+ RunHandle,
44
+ Trace,
45
+ TracePolicy,
46
+ )
47
+ from .session import GradientSession, current_session, init
48
+ from .tools import Tool, execute_tool_calls, infer_parameters, tool_from_function
49
+ from .tracing import current_span, get_tracer
50
+
51
+ __all__ = [
52
+ "AppSecret",
53
+ "Agent",
54
+ "AgentResult",
55
+ "BuildHandle",
56
+ "CreateMachineResponse",
57
+ "CreateTraceRunResponse",
58
+ "Credentials",
59
+ "DEFAULT_API_URL",
60
+ "DEFAULT_PROXY_URL",
61
+ "DEFAULT_REGISTRY_URL",
62
+ "DeploymentHandle",
63
+ "END",
64
+ "GradientAPIError",
65
+ "GradientAuthError",
66
+ "Gradient",
67
+ "GradientClient",
68
+ "GradientError",
69
+ "GradientSession",
70
+ "Environment",
71
+ "ExecMachineResponse",
72
+ "Guest",
73
+ "MachineCreateRequest",
74
+ "MachineListItem",
75
+ "MachineSpec",
76
+ "Model",
77
+ "ModelSpec",
78
+ "PortEntry",
79
+ "ProgressEvent",
80
+ "ReplayTraceRunResponse",
81
+ "RemoteDeployment",
82
+ "RemoteRunResult",
83
+ "Restart",
84
+ "START",
85
+ "ServiceConfig",
86
+ "StateGraph",
87
+ "supported_models",
88
+ "Tool",
89
+ "Trace",
90
+ "TraceEvent",
91
+ "TracePolicy",
92
+ "TraceRun",
93
+ "TraceSpan",
94
+ "WhoamiResponse",
95
+ "agent",
96
+ "chain",
97
+ "current_context",
98
+ "current_session",
99
+ "current_span",
100
+ "default_machine_template",
101
+ "embedding",
102
+ "execute_tool_calls",
103
+ "generate",
104
+ "get_tracer",
105
+ "infer_parameters",
106
+ "init",
107
+ "llm",
108
+ "load_credentials",
109
+ "retriever",
110
+ "run_agent_loop",
111
+ "RunHandle",
112
+ "RunContext",
113
+ "save_credentials",
114
+ "task",
115
+ "tool",
116
+ "tool_from_function",
117
+ "trace",
118
+ "trace_context",
119
+ "workflow",
120
+ "wrap_anthropic",
121
+ "wrap_openai",
122
+ ]
gradient_sdk/agent.py ADDED
@@ -0,0 +1,368 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import json
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Iterable, Iterator, Mapping
7
+
8
+ from .context import RunContext, use_context
9
+ from .integrations.agent import generate
10
+ from .model_catalog import Model, resolve_model
11
+ from .tools import Tool, execute_tool_calls, tool_from_function
12
+ from .tracing import get_tracer
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class AgentResult:
17
+ output: Any
18
+ text: str
19
+ messages: list[dict[str, Any]]
20
+ tool_results: list[dict[str, Any]]
21
+ usage: dict[str, int]
22
+ events: list[dict[str, Any]]
23
+ context: RunContext
24
+ metadata: dict[str, Any] = field(default_factory=dict)
25
+
26
+ def __str__(self) -> str:
27
+ return self.text
28
+
29
+
30
+ class Agent:
31
+ __gradient_agent__ = True
32
+
33
+ def __init__(
34
+ self,
35
+ model: str | Callable[..., Any] | Any,
36
+ *,
37
+ tools: Iterable[Tool | Callable[..., Any]] | None = None,
38
+ instructions: str | None = None,
39
+ memory: Any | None = None,
40
+ middleware: Iterable[Callable[..., Any]] | None = None,
41
+ trace: bool = True,
42
+ gradient: Any | None = None,
43
+ name: str | None = None,
44
+ max_turns: int = 8,
45
+ retries: int = 0,
46
+ fallback_models: Iterable[str | Callable[..., Any] | Any] | None = None,
47
+ structured_output: Callable[[Any], Any] | None = None,
48
+ entrypoint: str | None = None,
49
+ client: Any | None = None,
50
+ ) -> None:
51
+ self.model = model
52
+ self.tools = [_coerce_tool(item) for item in tools or []]
53
+ self.instructions = instructions
54
+ self.memory = memory
55
+ self.middleware = list(middleware or [])
56
+ self.trace = trace
57
+ self.gradient = gradient
58
+ self.name = name or _model_name(model)
59
+ self.max_turns = max_turns
60
+ self.retries = retries
61
+ self.fallback_models = list(fallback_models or [])
62
+ self.structured_output = structured_output
63
+ self.entrypoint = entrypoint
64
+ self.client = client
65
+
66
+ def __call__(self, input: Any = None) -> Any:
67
+ return self.run(input).output
68
+
69
+ def bind(self, gradient: Any) -> "Agent":
70
+ self.gradient = gradient
71
+ return self
72
+
73
+ def run(
74
+ self,
75
+ input: Any = None,
76
+ *,
77
+ context: RunContext | Mapping[str, Any] | None = None,
78
+ **metadata: Any,
79
+ ) -> AgentResult:
80
+ ctx = RunContext.from_value(
81
+ context,
82
+ metadata={**dict(getattr(context, "metadata", {}) or {}), **metadata}
83
+ if context is not None
84
+ else metadata,
85
+ )
86
+ events: list[dict[str, Any]] = []
87
+ with use_context(ctx):
88
+ with _maybe_agent_span(self.name, self.trace, input) as span:
89
+ result = self._run_loop(input, ctx, events)
90
+ if span is not None:
91
+ span.set_output(result.output)
92
+ self._save_memory(result)
93
+ return result
94
+
95
+ def stream(
96
+ self,
97
+ input: Any = None,
98
+ *,
99
+ context: RunContext | Mapping[str, Any] | None = None,
100
+ **metadata: Any,
101
+ ) -> Iterator[dict[str, Any]]:
102
+ result = self.run(input, context=context, **metadata)
103
+ yield from result.events
104
+
105
+ def deploy(self, name: str | None = None, **kwargs: Any) -> Any:
106
+ if self.gradient is None:
107
+ raise RuntimeError("Agent.deploy requires an Agent(..., gradient=gradient) or agent.bind(gradient)")
108
+ return self.gradient.deploy(self, name=name, **kwargs)
109
+
110
+ def evaluate(
111
+ self,
112
+ dataset: Iterable[Mapping[str, Any]] | Any,
113
+ evaluators: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
114
+ *,
115
+ input_key: str = "input",
116
+ expected_key: str = "expected_output",
117
+ ) -> dict[str, Any]:
118
+ rows = list(dataset)
119
+ results: list[dict[str, Any]] = []
120
+ for index, example in enumerate(rows):
121
+ payload = example.get(input_key) if isinstance(example, Mapping) else example
122
+ expected = example.get(expected_key) if isinstance(example, Mapping) else None
123
+ started = self.run(payload)
124
+ scores = {
125
+ name: evaluator(
126
+ {
127
+ "input": payload,
128
+ "output": started.output,
129
+ "text": started.text,
130
+ "expected_output": expected,
131
+ "example": example,
132
+ }
133
+ )
134
+ for name, evaluator in (evaluators or {}).items()
135
+ }
136
+ results.append(
137
+ {
138
+ "index": index,
139
+ "input": payload,
140
+ "output": started.output,
141
+ "text": started.text,
142
+ "expected_output": expected,
143
+ "scores": scores,
144
+ "usage": started.usage,
145
+ }
146
+ )
147
+ return {"count": len(results), "results": results}
148
+
149
+ def _run_loop(
150
+ self,
151
+ input: Any,
152
+ context: RunContext,
153
+ events: list[dict[str, Any]],
154
+ ) -> AgentResult:
155
+ messages = self._initial_messages(input)
156
+ usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
157
+ tool_results: list[dict[str, Any]] = []
158
+ final_text = ""
159
+ output: Any = ""
160
+
161
+ events.append(_event("context", {"context": context.to_event()}))
162
+ events.append(_event("start", {"input": input, "agent": self.name}))
163
+
164
+ for turn in range(max(1, self.max_turns)):
165
+ context.check_cancelled()
166
+ response = self._call_model(messages, context)
167
+ _merge_usage(usage, response.get("usage", {}))
168
+ final_text = str(response.get("text") or response.get("output") or "")
169
+ output = response.get("output", final_text)
170
+ events.append(
171
+ _event(
172
+ "message",
173
+ {"role": "assistant", "content": final_text, "turn": turn + 1},
174
+ )
175
+ )
176
+ tool_calls = list(response.get("tool_calls") or [])
177
+ if not tool_calls:
178
+ break
179
+ messages.append({"role": "assistant", "content": final_text, "tool_calls": tool_calls})
180
+ events.append(_event("tool_calls", {"calls": tool_calls, "turn": turn + 1}))
181
+ executed = execute_tool_calls(self.tools, tool_calls)
182
+ tool_results.extend(executed)
183
+ events.append(_event("tool_results", {"results": executed, "turn": turn + 1}))
184
+ for item in executed:
185
+ messages.append(
186
+ {
187
+ "role": "tool",
188
+ "tool_call_id": item.get("tool_call_id"),
189
+ "name": item.get("name"),
190
+ "content": json.dumps(item.get("output", item.get("error")), default=str),
191
+ }
192
+ )
193
+
194
+ if self.structured_output is not None:
195
+ output = self.structured_output(output)
196
+ events.append(_event("final", {"output": output, "text": final_text, "usage": usage}))
197
+ return AgentResult(
198
+ output=output,
199
+ text=final_text,
200
+ messages=messages,
201
+ tool_results=tool_results,
202
+ usage=usage,
203
+ events=events,
204
+ context=context,
205
+ )
206
+
207
+ def _initial_messages(self, input: Any) -> list[dict[str, Any]]:
208
+ messages: list[dict[str, Any]] = []
209
+ if self.instructions:
210
+ messages.append({"role": "system", "content": self.instructions})
211
+ loaded_memory = _load_memory(self.memory)
212
+ messages.extend(loaded_memory)
213
+ if isinstance(input, Mapping) and isinstance(input.get("messages"), list):
214
+ messages.extend(dict(item) for item in input["messages"])
215
+ elif isinstance(input, list) and all(isinstance(item, Mapping) for item in input):
216
+ messages.extend(dict(item) for item in input)
217
+ else:
218
+ messages.append({"role": "user", "content": input if isinstance(input, str) else json.dumps(input, default=str)})
219
+ return messages
220
+
221
+ def _call_model(self, messages: list[dict[str, Any]], context: RunContext) -> dict[str, Any]:
222
+ def invoke() -> dict[str, Any]:
223
+ return self._call_model_without_middleware(messages, context)
224
+
225
+ call_next = invoke
226
+ for middleware in reversed(self.middleware):
227
+ previous = call_next
228
+ call_next = lambda middleware=middleware, previous=previous: middleware(context, previous)
229
+ return call_next()
230
+
231
+ def _call_model_without_middleware(
232
+ self,
233
+ messages: list[dict[str, Any]],
234
+ context: RunContext,
235
+ ) -> dict[str, Any]:
236
+ models = [self.model, *self.fallback_models]
237
+ last_error: BaseException | None = None
238
+ for model in models:
239
+ for _ in range(self.retries + 1):
240
+ try:
241
+ return _normalize_model_result(
242
+ _invoke_model(
243
+ model,
244
+ messages=messages,
245
+ tools=self.tools,
246
+ context=context,
247
+ client=self.client,
248
+ )
249
+ )
250
+ except Exception as exc:
251
+ last_error = exc
252
+ if last_error is not None:
253
+ raise last_error
254
+ return {"text": "", "tool_calls": [], "usage": {}}
255
+
256
+ def _save_memory(self, result: AgentResult) -> None:
257
+ if self.memory is None:
258
+ return
259
+ if hasattr(self.memory, "save"):
260
+ self.memory.save(result.messages)
261
+ elif isinstance(self.memory, list):
262
+ self.memory[:] = result.messages
263
+
264
+
265
+ def _coerce_tool(value: Tool | Callable[..., Any]) -> Tool:
266
+ if isinstance(value, Tool):
267
+ return value
268
+ tool_obj = getattr(value, "gradient_tool", None)
269
+ if isinstance(tool_obj, Tool):
270
+ return tool_obj
271
+ return tool_from_function(value)
272
+
273
+
274
+ def _load_memory(memory: Any | None) -> list[dict[str, Any]]:
275
+ if memory is None:
276
+ return []
277
+ if hasattr(memory, "load"):
278
+ loaded = memory.load()
279
+ return list(loaded or [])
280
+ if isinstance(memory, list):
281
+ return [dict(item) for item in memory if isinstance(item, Mapping)]
282
+ return []
283
+
284
+
285
+ def _invoke_model(
286
+ model: Model | str | Callable[..., Any] | Any,
287
+ *,
288
+ messages: list[dict[str, Any]],
289
+ tools: list[Tool],
290
+ context: RunContext,
291
+ client: Any | None = None,
292
+ ) -> Any:
293
+ spec = resolve_model(model)
294
+ if spec is not None:
295
+ return generate(
296
+ provider=spec.provider, # type: ignore[arg-type]
297
+ model=spec.model,
298
+ interface=spec.interface,
299
+ messages=messages,
300
+ tools=tools,
301
+ client=client,
302
+ )
303
+ if callable(model):
304
+ kwargs = {"messages": messages, "tools": tools, "context": context}
305
+ try:
306
+ signature = inspect.signature(model)
307
+ accepted = {key: value for key, value in kwargs.items() if key in signature.parameters}
308
+ if accepted:
309
+ return model(**accepted)
310
+ except (TypeError, ValueError):
311
+ pass
312
+ return model(messages)
313
+ if hasattr(model, "generate"):
314
+ return model.generate(messages=messages, tools=tools, context=context)
315
+ if hasattr(model, "invoke"):
316
+ return model.invoke(messages)
317
+ raise TypeError("Agent model must be a provider string, callable, or object with generate/invoke")
318
+
319
+
320
+ def _normalize_model_result(result: Any) -> dict[str, Any]:
321
+ if isinstance(result, str):
322
+ return {"text": result, "output": result, "tool_calls": [], "usage": {}}
323
+ if isinstance(result, Mapping):
324
+ text = result.get("text", result.get("content", result.get("output", "")))
325
+ return {
326
+ "text": str(text or ""),
327
+ "output": result.get("output", text),
328
+ "tool_calls": list(result.get("tool_calls") or result.get("toolCalls") or []),
329
+ "usage": dict(result.get("usage") or {}),
330
+ }
331
+ return {"text": str(result), "output": result, "tool_calls": [], "usage": {}}
332
+
333
+
334
+ def _merge_usage(target: dict[str, int], incoming: Mapping[str, Any]) -> None:
335
+ for key in ("prompt_tokens", "completion_tokens", "total_tokens"):
336
+ target[key] = int(target.get(key, 0)) + int(incoming.get(key, 0) or 0)
337
+
338
+
339
+ def _event(event_type: str, data: Mapping[str, Any]) -> dict[str, Any]:
340
+ return {"type": event_type, "data": dict(data)}
341
+
342
+
343
+ def _model_name(model: Any) -> str:
344
+ if isinstance(model, str):
345
+ return model
346
+ return getattr(model, "__name__", model.__class__.__name__)
347
+
348
+
349
+ class _maybe_agent_span:
350
+ def __init__(self, name: str, enabled: bool, input: Any) -> None:
351
+ self.name = name
352
+ self.enabled = enabled
353
+ self.input = input
354
+ self._span: Any = None
355
+
356
+ def __enter__(self) -> Any:
357
+ if not self.enabled:
358
+ return None
359
+ tracer = get_tracer()
360
+ self._span = tracer.start_span(f"agent.{self.name}", kind="AGENT")
361
+ entered = self._span.__enter__()
362
+ entered.set_attribute("input.value", json.dumps(self.input, default=str))
363
+ return entered
364
+
365
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
366
+ if self._span is not None:
367
+ return bool(self._span.__exit__(exc_type, exc, tb))
368
+ return False