usegradient 1.3.1__tar.gz → 1.3.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.
- {usegradient-1.3.1 → usegradient-1.3.2}/PKG-INFO +1 -2
- {usegradient-1.3.1 → usegradient-1.3.2}/README.md +0 -1
- {usegradient-1.3.1 → usegradient-1.3.2}/examples/sdk_first_agent.py +1 -1
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/__init__.py +2 -0
- usegradient-1.3.2/gradient_sdk/dependencies.py +217 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/integrations/agent.py +96 -5
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/runtime.py +163 -50
- {usegradient-1.3.1 → usegradient-1.3.2}/pyproject.toml +1 -1
- {usegradient-1.3.1 → usegradient-1.3.2}/tests/test_agent.py +8 -1
- usegradient-1.3.2/tests/test_dependencies.py +71 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/tests/test_runtime.py +24 -6
- {usegradient-1.3.1 → usegradient-1.3.2}/.gitignore +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/demo_walkthrough.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/agent.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/client.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/context.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/credentials.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/decorators.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/errors.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/graph.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/integrations/__init__.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/integrations/anthropic.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/integrations/openai.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/model_catalog.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/models.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/py.typed +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/session.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/tools.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/gradient_sdk/tracing.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/tests/test_client.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/tests/test_graph.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/tests/test_integrations.py +0 -0
- {usegradient-1.3.1 → usegradient-1.3.2}/tests/test_tracing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usegradient
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Python SDK for the Gradient agent runtime and observability platform
|
|
5
5
|
Project-URL: Homepage, https://usegradient.dev
|
|
6
6
|
Project-URL: Documentation, https://github.com/use-gradient/gradient/tree/main/sdk/python
|
|
@@ -98,7 +98,6 @@ if __name__ == "__main__":
|
|
|
98
98
|
project=os.getenv("GRADIENT_PROJECT", "my-first-agent"),
|
|
99
99
|
environment=Environment.python(
|
|
100
100
|
python="3.12",
|
|
101
|
-
packages=["usegradient", "anthropic"],
|
|
102
101
|
env={"ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"]},
|
|
103
102
|
),
|
|
104
103
|
)
|
|
@@ -34,7 +34,7 @@ if __name__ == "__main__":
|
|
|
34
34
|
project=os.getenv("GRADIENT_PROJECT", "my-first-agent"),
|
|
35
35
|
environment=Environment.python(
|
|
36
36
|
python="3.12",
|
|
37
|
-
|
|
37
|
+
# usegradient + anthropic are inferred from imports / Model.CLAUDE_*.
|
|
38
38
|
env={"ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"]},
|
|
39
39
|
),
|
|
40
40
|
)
|
|
@@ -10,6 +10,7 @@ from .credentials import (
|
|
|
10
10
|
save_credentials,
|
|
11
11
|
)
|
|
12
12
|
from .decorators import agent, chain, embedding, llm, retriever, task, tool, trace, trace_context, workflow
|
|
13
|
+
from .dependencies import infer_python_packages
|
|
13
14
|
from .errors import GradientAPIError, GradientAuthError, GradientError
|
|
14
15
|
from .graph import END, START, StateGraph
|
|
15
16
|
from .integrations import generate, run_agent_loop, wrap_anthropic, wrap_openai
|
|
@@ -103,6 +104,7 @@ __all__ = [
|
|
|
103
104
|
"generate",
|
|
104
105
|
"get_tracer",
|
|
105
106
|
"infer_parameters",
|
|
107
|
+
"infer_python_packages",
|
|
106
108
|
"init",
|
|
107
109
|
"llm",
|
|
108
110
|
"load_credentials",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
# Top-level import name -> PyPI distribution name.
|
|
9
|
+
# Unlisted third-party imports default to the import name itself.
|
|
10
|
+
IMPORT_TO_PACKAGE: dict[str, str | None] = {
|
|
11
|
+
"dotenv": "python-dotenv",
|
|
12
|
+
"gradient_sdk": "usegradient",
|
|
13
|
+
"PIL": "Pillow",
|
|
14
|
+
"cv2": "opencv-python",
|
|
15
|
+
"sklearn": "scikit-learn",
|
|
16
|
+
"yaml": "PyYAML",
|
|
17
|
+
"bs4": "beautifulsoup4",
|
|
18
|
+
"attr": "attrs",
|
|
19
|
+
"serial": "pyserial",
|
|
20
|
+
"jwt": "PyJWT",
|
|
21
|
+
"Crypto": "pycryptodome",
|
|
22
|
+
"OpenSSL": "pyOpenSSL",
|
|
23
|
+
"dateutil": "python-dateutil",
|
|
24
|
+
"skimage": "scikit-image",
|
|
25
|
+
"gi": None, # system/bindings package; not pip-installable in the usual way
|
|
26
|
+
"cairo": None,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def infer_python_packages(
|
|
31
|
+
entrypoint: str | Path,
|
|
32
|
+
*,
|
|
33
|
+
source_root: str | Path | None = None,
|
|
34
|
+
explicit: Iterable[str] | None = None,
|
|
35
|
+
) -> list[str]:
|
|
36
|
+
"""Infer pip packages from imports in ``entrypoint`` and local modules it uses.
|
|
37
|
+
|
|
38
|
+
Stdlib and local project modules are skipped. Known import renames
|
|
39
|
+
(for example ``dotenv`` -> ``python-dotenv``) are applied. Explicit packages
|
|
40
|
+
are always kept and appear first.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
entry = Path(entrypoint).resolve()
|
|
44
|
+
root = Path(source_root or entry.parent).resolve()
|
|
45
|
+
explicit_packages = _normalize_packages(explicit or [])
|
|
46
|
+
if not entry.exists():
|
|
47
|
+
return explicit_packages
|
|
48
|
+
|
|
49
|
+
local_names = _local_module_names(root)
|
|
50
|
+
stdlib = set(sys.stdlib_module_names)
|
|
51
|
+
top_level_imports: set[str] = set()
|
|
52
|
+
provider_packages: set[str] = set()
|
|
53
|
+
queued: list[Path] = [entry]
|
|
54
|
+
seen_files: set[Path] = set()
|
|
55
|
+
|
|
56
|
+
while queued:
|
|
57
|
+
path = queued.pop()
|
|
58
|
+
if path in seen_files or not path.is_file():
|
|
59
|
+
continue
|
|
60
|
+
seen_files.add(path)
|
|
61
|
+
provider_packages.update(_infer_provider_packages(path))
|
|
62
|
+
for module_name, level in _iter_import_modules(path):
|
|
63
|
+
if level > 0:
|
|
64
|
+
local_path = _resolve_relative_import(path, root, module_name, level)
|
|
65
|
+
if local_path is not None:
|
|
66
|
+
queued.append(local_path)
|
|
67
|
+
continue
|
|
68
|
+
if not module_name:
|
|
69
|
+
continue
|
|
70
|
+
top = module_name.split(".", 1)[0]
|
|
71
|
+
top_level_imports.add(top)
|
|
72
|
+
# Known third-party/SDK imports should not be scanned as local source,
|
|
73
|
+
# even if a same-named directory exists in the source root (monorepo).
|
|
74
|
+
if top in IMPORT_TO_PACKAGE:
|
|
75
|
+
continue
|
|
76
|
+
if top in local_names:
|
|
77
|
+
queued.extend(_local_module_files(root, module_name))
|
|
78
|
+
|
|
79
|
+
inferred: list[str] = []
|
|
80
|
+
seen: set[str] = set(explicit_packages)
|
|
81
|
+
for name in sorted(top_level_imports | provider_packages):
|
|
82
|
+
if name in stdlib:
|
|
83
|
+
continue
|
|
84
|
+
if name in IMPORT_TO_PACKAGE:
|
|
85
|
+
package = IMPORT_TO_PACKAGE[name]
|
|
86
|
+
elif name in {"anthropic", "openai"}:
|
|
87
|
+
package = name
|
|
88
|
+
elif name in local_names:
|
|
89
|
+
continue
|
|
90
|
+
else:
|
|
91
|
+
package = name
|
|
92
|
+
if not package or package in seen:
|
|
93
|
+
continue
|
|
94
|
+
seen.add(package)
|
|
95
|
+
inferred.append(package)
|
|
96
|
+
|
|
97
|
+
return [*explicit_packages, *inferred]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def merge_python_packages(
|
|
101
|
+
*,
|
|
102
|
+
entrypoint: str | Path | None,
|
|
103
|
+
source_root: str | Path,
|
|
104
|
+
explicit: Iterable[str] | None = None,
|
|
105
|
+
) -> list[str]:
|
|
106
|
+
if entrypoint:
|
|
107
|
+
return infer_python_packages(
|
|
108
|
+
entrypoint,
|
|
109
|
+
source_root=source_root,
|
|
110
|
+
explicit=explicit,
|
|
111
|
+
)
|
|
112
|
+
return _normalize_packages(explicit or [])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _normalize_packages(packages: Iterable[str]) -> list[str]:
|
|
116
|
+
result: list[str] = []
|
|
117
|
+
seen: set[str] = set()
|
|
118
|
+
for item in packages:
|
|
119
|
+
name = str(item).strip()
|
|
120
|
+
if not name or name in seen:
|
|
121
|
+
continue
|
|
122
|
+
seen.add(name)
|
|
123
|
+
result.append(name)
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _local_module_names(root: Path) -> set[str]:
|
|
128
|
+
names: set[str] = set()
|
|
129
|
+
if not root.is_dir():
|
|
130
|
+
return names
|
|
131
|
+
for child in root.iterdir():
|
|
132
|
+
if child.name.startswith("."):
|
|
133
|
+
continue
|
|
134
|
+
if child.is_file() and child.suffix == ".py" and child.name != "__init__.py":
|
|
135
|
+
names.add(child.stem)
|
|
136
|
+
elif child.is_dir() and not child.name.startswith("__"):
|
|
137
|
+
if (child / "__init__.py").exists() or any(child.rglob("*.py")):
|
|
138
|
+
names.add(child.name)
|
|
139
|
+
return names
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _local_module_files(root: Path, module_name: str) -> list[Path]:
|
|
143
|
+
parts = module_name.split(".")
|
|
144
|
+
base = root.joinpath(*parts)
|
|
145
|
+
candidates = [
|
|
146
|
+
Path(f"{base}.py"),
|
|
147
|
+
base / "__init__.py",
|
|
148
|
+
]
|
|
149
|
+
return [path.resolve() for path in candidates if path.is_file()]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _resolve_relative_import(
|
|
153
|
+
current_file: Path,
|
|
154
|
+
root: Path,
|
|
155
|
+
module_name: str | None,
|
|
156
|
+
level: int,
|
|
157
|
+
) -> Path | None:
|
|
158
|
+
current_dir = current_file.parent
|
|
159
|
+
# level=1 means current package; climb level-1 parents for package context
|
|
160
|
+
package_dir = current_dir
|
|
161
|
+
for _ in range(max(level - 1, 0)):
|
|
162
|
+
package_dir = package_dir.parent
|
|
163
|
+
if module_name:
|
|
164
|
+
target = package_dir.joinpath(*module_name.split("."))
|
|
165
|
+
else:
|
|
166
|
+
target = package_dir
|
|
167
|
+
for candidate in (Path(f"{target}.py"), target / "__init__.py"):
|
|
168
|
+
try:
|
|
169
|
+
resolved = candidate.resolve()
|
|
170
|
+
resolved.relative_to(root)
|
|
171
|
+
except Exception:
|
|
172
|
+
continue
|
|
173
|
+
if resolved.is_file():
|
|
174
|
+
return resolved
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _iter_import_modules(path: Path) -> list[tuple[str | None, int]]:
|
|
179
|
+
try:
|
|
180
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
181
|
+
except (OSError, SyntaxError):
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
found: list[tuple[str | None, int]] = []
|
|
185
|
+
for node in ast.walk(tree):
|
|
186
|
+
if isinstance(node, ast.Import):
|
|
187
|
+
for alias in node.names:
|
|
188
|
+
found.append((alias.name, 0))
|
|
189
|
+
elif isinstance(node, ast.ImportFrom):
|
|
190
|
+
found.append((node.module, int(node.level or 0)))
|
|
191
|
+
return found
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _infer_provider_packages(path: Path) -> set[str]:
|
|
195
|
+
"""Detect provider SDKs referenced via model ids/enums without direct imports."""
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
199
|
+
except (OSError, SyntaxError):
|
|
200
|
+
return set()
|
|
201
|
+
|
|
202
|
+
found: set[str] = set()
|
|
203
|
+
for node in ast.walk(tree):
|
|
204
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
205
|
+
if node.value.startswith("anthropic:"):
|
|
206
|
+
found.add("anthropic")
|
|
207
|
+
elif node.value.startswith("openai:"):
|
|
208
|
+
found.add("openai")
|
|
209
|
+
elif isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
|
|
210
|
+
if node.value.id != "Model":
|
|
211
|
+
continue
|
|
212
|
+
attr = node.attr.upper()
|
|
213
|
+
if attr.startswith("CLAUDE"):
|
|
214
|
+
found.add("anthropic")
|
|
215
|
+
elif attr.startswith("GPT"):
|
|
216
|
+
found.add("openai")
|
|
217
|
+
return found
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Any, Literal
|
|
4
|
+
from typing import Any, Literal, Mapping
|
|
5
5
|
|
|
6
6
|
from ..tools import Tool, execute_tool_calls
|
|
7
7
|
from ..tracing import get_tracer
|
|
@@ -218,16 +218,15 @@ def _generate_anthropic(
|
|
|
218
218
|
from anthropic import Anthropic
|
|
219
219
|
|
|
220
220
|
client = Anthropic()
|
|
221
|
-
|
|
222
|
-
anthropic_messages = [{"role": "user", "content": messages}]
|
|
223
|
-
else:
|
|
224
|
-
anthropic_messages = messages
|
|
221
|
+
system, anthropic_messages = _anthropic_messages(messages)
|
|
225
222
|
payload: dict[str, Any] = {
|
|
226
223
|
"model": model,
|
|
227
224
|
"max_tokens": kwargs.pop("max_tokens", 1024),
|
|
228
225
|
"messages": anthropic_messages,
|
|
229
226
|
**kwargs,
|
|
230
227
|
}
|
|
228
|
+
if system:
|
|
229
|
+
payload["system"] = system
|
|
231
230
|
if tools:
|
|
232
231
|
payload["tools"] = [tool.to_anthropic() for tool in tools]
|
|
233
232
|
response = client.messages.create(**payload)
|
|
@@ -257,6 +256,98 @@ def _generate_anthropic(
|
|
|
257
256
|
}
|
|
258
257
|
|
|
259
258
|
|
|
259
|
+
def _anthropic_messages(
|
|
260
|
+
messages: list[dict[str, Any]] | str,
|
|
261
|
+
) -> tuple[str | None, list[dict[str, Any]]]:
|
|
262
|
+
"""Convert chat-style messages into Anthropic Messages API shape.
|
|
263
|
+
|
|
264
|
+
Anthropic rejects role=system entries in ``messages`` and expects the
|
|
265
|
+
initial system prompt in the top-level ``system`` parameter instead.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
if isinstance(messages, str):
|
|
269
|
+
return None, [{"role": "user", "content": messages}]
|
|
270
|
+
|
|
271
|
+
system_parts: list[str] = []
|
|
272
|
+
converted: list[dict[str, Any]] = []
|
|
273
|
+
for message in messages:
|
|
274
|
+
role = str(message.get("role") or "user")
|
|
275
|
+
content = message.get("content", "")
|
|
276
|
+
if role == "system":
|
|
277
|
+
text = _message_text(content)
|
|
278
|
+
if text:
|
|
279
|
+
system_parts.append(text)
|
|
280
|
+
continue
|
|
281
|
+
if role == "tool":
|
|
282
|
+
converted.append(
|
|
283
|
+
{
|
|
284
|
+
"role": "user",
|
|
285
|
+
"content": [
|
|
286
|
+
{
|
|
287
|
+
"type": "tool_result",
|
|
288
|
+
"tool_use_id": message.get("tool_call_id") or message.get("id"),
|
|
289
|
+
"content": _message_text(content),
|
|
290
|
+
}
|
|
291
|
+
],
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
continue
|
|
295
|
+
if role == "assistant" and message.get("tool_calls"):
|
|
296
|
+
blocks: list[dict[str, Any]] = []
|
|
297
|
+
text = _message_text(content)
|
|
298
|
+
if text:
|
|
299
|
+
blocks.append({"type": "text", "text": text})
|
|
300
|
+
for call in message.get("tool_calls") or []:
|
|
301
|
+
arguments = call.get("arguments", call.get("input", {}))
|
|
302
|
+
if isinstance(arguments, str):
|
|
303
|
+
try:
|
|
304
|
+
arguments = json.loads(arguments)
|
|
305
|
+
except json.JSONDecodeError:
|
|
306
|
+
arguments = {"raw": arguments}
|
|
307
|
+
blocks.append(
|
|
308
|
+
{
|
|
309
|
+
"type": "tool_use",
|
|
310
|
+
"id": call.get("id"),
|
|
311
|
+
"name": call.get("name", ""),
|
|
312
|
+
"input": arguments if isinstance(arguments, dict) else {"value": arguments},
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
converted.append({"role": "assistant", "content": blocks or text})
|
|
316
|
+
continue
|
|
317
|
+
if role not in {"user", "assistant"}:
|
|
318
|
+
role = "user"
|
|
319
|
+
converted.append({"role": role, "content": content})
|
|
320
|
+
|
|
321
|
+
if not converted:
|
|
322
|
+
converted = [{"role": "user", "content": ""}]
|
|
323
|
+
return ("\n\n".join(system_parts) if system_parts else None), converted
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _message_text(content: Any) -> str:
|
|
327
|
+
if content is None:
|
|
328
|
+
return ""
|
|
329
|
+
if isinstance(content, str):
|
|
330
|
+
return content
|
|
331
|
+
if isinstance(content, list):
|
|
332
|
+
parts: list[str] = []
|
|
333
|
+
for item in content:
|
|
334
|
+
if isinstance(item, Mapping):
|
|
335
|
+
text = item.get("text")
|
|
336
|
+
if text:
|
|
337
|
+
parts.append(str(text))
|
|
338
|
+
else:
|
|
339
|
+
parts.append(json.dumps(item, default=str))
|
|
340
|
+
else:
|
|
341
|
+
parts.append(str(item))
|
|
342
|
+
return "".join(parts)
|
|
343
|
+
if isinstance(content, Mapping):
|
|
344
|
+
text = content.get("text")
|
|
345
|
+
if text is not None:
|
|
346
|
+
return str(text)
|
|
347
|
+
return json.dumps(content, default=str)
|
|
348
|
+
return str(content)
|
|
349
|
+
|
|
350
|
+
|
|
260
351
|
def _get(value: Any, key: str, default: Any = None) -> Any:
|
|
261
352
|
if isinstance(value, dict):
|
|
262
353
|
return value.get(key, default)
|
|
@@ -9,6 +9,7 @@ import io
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
import posixpath
|
|
12
|
+
import shlex
|
|
12
13
|
import sys
|
|
13
14
|
import tarfile
|
|
14
15
|
import time
|
|
@@ -17,6 +18,7 @@ from pathlib import Path
|
|
|
17
18
|
from typing import Any, Callable, Iterable, Mapping
|
|
18
19
|
|
|
19
20
|
from .client import GradientClient
|
|
21
|
+
from .dependencies import merge_python_packages
|
|
20
22
|
from .errors import GradientAPIError
|
|
21
23
|
|
|
22
24
|
DEFAULT_EXCLUDES = [
|
|
@@ -677,10 +679,19 @@ class Gradient:
|
|
|
677
679
|
source.duration_ms,
|
|
678
680
|
)
|
|
679
681
|
|
|
682
|
+
packages = self._python_packages_for_source(source)
|
|
683
|
+
environment_spec = self._environment_spec(packages=packages)
|
|
684
|
+
if packages and self.environment.runtime == "python":
|
|
685
|
+
self._stage(
|
|
686
|
+
"dependencies",
|
|
687
|
+
"ready",
|
|
688
|
+
f"Resolved packages: {', '.join(packages)}",
|
|
689
|
+
)
|
|
690
|
+
|
|
680
691
|
t0 = time.perf_counter()
|
|
681
692
|
resolved = self.client.resolve_environment(
|
|
682
693
|
project=self.project,
|
|
683
|
-
environment=
|
|
694
|
+
environment=environment_spec,
|
|
684
695
|
source=source.manifest,
|
|
685
696
|
)
|
|
686
697
|
timings["resolve_environment_ms"] = _elapsed_ms(t0)
|
|
@@ -698,7 +709,11 @@ class Gradient:
|
|
|
698
709
|
t0 = time.perf_counter()
|
|
699
710
|
traced = self.client.create_traced_machine(
|
|
700
711
|
template,
|
|
701
|
-
machine_template=_with_source_bundle(
|
|
712
|
+
machine_template=_with_source_bundle(
|
|
713
|
+
template,
|
|
714
|
+
source,
|
|
715
|
+
extra_env=self.environment.env,
|
|
716
|
+
),
|
|
702
717
|
trace_mode=trace_policy.trace_mode,
|
|
703
718
|
capture_policy=trace_policy.capture_policy(),
|
|
704
719
|
metadata={
|
|
@@ -709,6 +724,7 @@ class Gradient:
|
|
|
709
724
|
"target": source.target_ref,
|
|
710
725
|
"trace_preset": trace_policy.preset,
|
|
711
726
|
"sdk_surface": "Gradient.run",
|
|
727
|
+
"packages": packages,
|
|
712
728
|
},
|
|
713
729
|
wait=True,
|
|
714
730
|
wait_timeout_seconds=wait_timeout_seconds,
|
|
@@ -724,49 +740,40 @@ class Gradient:
|
|
|
724
740
|
timings["upload_source_ms"] = 0
|
|
725
741
|
self._stage("upload", "ready", "Mounted source bundle when the machine was created")
|
|
726
742
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
trace=False,
|
|
735
|
-
stage="dependencies",
|
|
736
|
-
)
|
|
737
|
-
timings["runtime_setup_ms"] = _elapsed_ms(t0)
|
|
738
|
-
self._stage(
|
|
739
|
-
"dependencies",
|
|
740
|
-
"ready" if setup.exit_code == 0 else "failed",
|
|
741
|
-
"Runtime dependency setup completed" if setup.exit_code == 0 else setup.stderr,
|
|
742
|
-
timings["runtime_setup_ms"],
|
|
743
|
-
)
|
|
744
|
-
if setup.exit_code != 0:
|
|
745
|
-
raise RuntimeError(setup.stderr or setup.stdout)
|
|
746
|
-
else:
|
|
747
|
-
self._stage("dependencies", "cached", "Skipped runtime install; using cached environment image", cached=True)
|
|
743
|
+
python_bin = self._prepare_remote_python(
|
|
744
|
+
machine_id=machine_id,
|
|
745
|
+
packages=packages,
|
|
746
|
+
setup_command=env_version.get("setup_command"),
|
|
747
|
+
run_timeout_seconds=run_timeout_seconds,
|
|
748
|
+
timings=timings,
|
|
749
|
+
)
|
|
748
750
|
|
|
749
751
|
payload = base64.b64encode(json.dumps(input).encode("utf-8")).decode("ascii")
|
|
750
752
|
remote_code = _remote_bootstrap(source.target_ref, payload)
|
|
751
753
|
t0 = time.perf_counter()
|
|
752
754
|
result = self._exec_machine_with_retry(
|
|
753
755
|
machine_id,
|
|
754
|
-
command=[
|
|
756
|
+
command=[python_bin, "-c", remote_code],
|
|
755
757
|
timeout_seconds=run_timeout_seconds,
|
|
756
758
|
trace=trace_policy.trace_exec,
|
|
757
759
|
watch=list(watch or ["/workspace"]),
|
|
758
760
|
stage="run",
|
|
759
761
|
)
|
|
760
762
|
timings["remote_run_ms"] = _elapsed_ms(t0)
|
|
763
|
+
failure_detail = _compact_output(result.stderr or result.stdout)
|
|
761
764
|
self._stage(
|
|
762
765
|
"run",
|
|
763
766
|
"ready" if result.exit_code == 0 else "failed",
|
|
764
|
-
|
|
767
|
+
(
|
|
768
|
+
f"Remote agent exited with {result.exit_code}"
|
|
769
|
+
if result.exit_code == 0 or not failure_detail
|
|
770
|
+
else f"Remote agent exited with {result.exit_code}: {failure_detail}"
|
|
771
|
+
),
|
|
765
772
|
timings["remote_run_ms"],
|
|
766
773
|
)
|
|
767
774
|
|
|
768
775
|
timings["total_ms"] = _elapsed_ms(overall)
|
|
769
|
-
|
|
776
|
+
handle = RunHandle(
|
|
770
777
|
run_id=run_id,
|
|
771
778
|
machine_id=machine_id,
|
|
772
779
|
environment_version_id=str(env_version["id"]),
|
|
@@ -781,6 +788,13 @@ class Gradient:
|
|
|
781
788
|
client=self.client,
|
|
782
789
|
input=input,
|
|
783
790
|
)
|
|
791
|
+
if result.exit_code != 0:
|
|
792
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
793
|
+
raise RuntimeError(
|
|
794
|
+
detail
|
|
795
|
+
or f"Remote agent exited with {result.exit_code}. Trace: {handle.trace_url}"
|
|
796
|
+
)
|
|
797
|
+
return handle
|
|
784
798
|
finally:
|
|
785
799
|
if not keep_machine:
|
|
786
800
|
t0 = time.perf_counter()
|
|
@@ -831,10 +845,13 @@ class Gradient:
|
|
|
831
845
|
source.duration_ms,
|
|
832
846
|
)
|
|
833
847
|
|
|
848
|
+
packages = self._python_packages_for_source(source)
|
|
849
|
+
environment_spec = self._environment_spec(packages=packages)
|
|
850
|
+
|
|
834
851
|
t0 = time.perf_counter()
|
|
835
852
|
resolved = self.client.resolve_environment(
|
|
836
853
|
project=self.project,
|
|
837
|
-
environment=
|
|
854
|
+
environment=environment_spec,
|
|
838
855
|
source=source.manifest,
|
|
839
856
|
)
|
|
840
857
|
timings["resolve_environment_ms"] = _elapsed_ms(t0)
|
|
@@ -856,6 +873,7 @@ class Gradient:
|
|
|
856
873
|
env_version["machine_template"],
|
|
857
874
|
source,
|
|
858
875
|
service_code=_remote_service_bootstrap(source.target_ref),
|
|
876
|
+
extra_env=self.environment.env,
|
|
859
877
|
),
|
|
860
878
|
environment_version_id=str(env_version["id"]),
|
|
861
879
|
trace_mode=trace_policy.trace_mode,
|
|
@@ -896,27 +914,13 @@ class Gradient:
|
|
|
896
914
|
timings["upload_source_ms"] = 0
|
|
897
915
|
self._stage("upload", "ready", "Mounted deployment source bundle at machine creation")
|
|
898
916
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
trace=False,
|
|
907
|
-
stage="dependencies",
|
|
908
|
-
)
|
|
909
|
-
timings["runtime_setup_ms"] = _elapsed_ms(t0)
|
|
910
|
-
self._stage(
|
|
911
|
-
"dependencies",
|
|
912
|
-
"ready" if setup.exit_code == 0 else "failed",
|
|
913
|
-
"Runtime dependency setup completed" if setup.exit_code == 0 else setup.stderr,
|
|
914
|
-
timings["runtime_setup_ms"],
|
|
915
|
-
)
|
|
916
|
-
if setup.exit_code != 0:
|
|
917
|
-
raise RuntimeError(setup.stderr or setup.stdout)
|
|
918
|
-
else:
|
|
919
|
-
self._stage("dependencies", "cached", "Skipped runtime install; using cached environment image", cached=True)
|
|
917
|
+
python_bin = self._prepare_remote_python(
|
|
918
|
+
machine_id=machine_id,
|
|
919
|
+
packages=packages,
|
|
920
|
+
setup_command=env_version.get("setup_command"),
|
|
921
|
+
run_timeout_seconds=run_timeout_seconds,
|
|
922
|
+
timings=timings,
|
|
923
|
+
)
|
|
920
924
|
|
|
921
925
|
t0 = time.perf_counter()
|
|
922
926
|
started = self._exec_machine_with_retry(
|
|
@@ -924,7 +928,7 @@ class Gradient:
|
|
|
924
928
|
command=[
|
|
925
929
|
"sh",
|
|
926
930
|
"-lc",
|
|
927
|
-
"cd /workspace && nohup
|
|
931
|
+
f"cd /workspace && nohup {shlex.quote(python_bin)} -u /tmp/gradient-service.py >/tmp/gradient-service.log 2>&1 &",
|
|
928
932
|
],
|
|
929
933
|
timeout_seconds=30,
|
|
930
934
|
trace=trace_policy.trace_exec,
|
|
@@ -1009,6 +1013,89 @@ class Gradient:
|
|
|
1009
1013
|
return _InstrumentedRunnable(target)
|
|
1010
1014
|
return target
|
|
1011
1015
|
|
|
1016
|
+
def _python_packages_for_source(self, source: SourceBundle) -> list[str]:
|
|
1017
|
+
if self.environment.runtime != "python":
|
|
1018
|
+
return list(self.environment.packages)
|
|
1019
|
+
entrypoint = (
|
|
1020
|
+
(self.source_root / source.entrypoint).resolve()
|
|
1021
|
+
if source.entrypoint
|
|
1022
|
+
else None
|
|
1023
|
+
)
|
|
1024
|
+
return merge_python_packages(
|
|
1025
|
+
entrypoint=entrypoint,
|
|
1026
|
+
source_root=self.source_root,
|
|
1027
|
+
explicit=self.environment.packages,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
def _environment_spec(self, *, packages: list[str] | None = None) -> dict[str, Any]:
|
|
1031
|
+
spec = self.environment.to_spec()
|
|
1032
|
+
if packages is not None:
|
|
1033
|
+
spec["packages"] = list(packages)
|
|
1034
|
+
return spec
|
|
1035
|
+
|
|
1036
|
+
def _prepare_remote_python(
|
|
1037
|
+
self,
|
|
1038
|
+
*,
|
|
1039
|
+
machine_id: str,
|
|
1040
|
+
packages: list[str],
|
|
1041
|
+
setup_command: str | None,
|
|
1042
|
+
run_timeout_seconds: int,
|
|
1043
|
+
timings: dict[str, int],
|
|
1044
|
+
) -> str:
|
|
1045
|
+
if self.environment.runtime == "python" and packages:
|
|
1046
|
+
command = _python_venv_install_command(packages)
|
|
1047
|
+
t0 = time.perf_counter()
|
|
1048
|
+
setup = self._exec_machine_with_retry(
|
|
1049
|
+
machine_id,
|
|
1050
|
+
command=["sh", "-lc", command],
|
|
1051
|
+
timeout_seconds=run_timeout_seconds,
|
|
1052
|
+
trace=False,
|
|
1053
|
+
stage="dependencies",
|
|
1054
|
+
)
|
|
1055
|
+
timings["runtime_setup_ms"] = _elapsed_ms(t0)
|
|
1056
|
+
detail = _compact_output(setup.stderr or setup.stdout)
|
|
1057
|
+
self._stage(
|
|
1058
|
+
"dependencies",
|
|
1059
|
+
"ready" if setup.exit_code == 0 else "failed",
|
|
1060
|
+
(
|
|
1061
|
+
f"Installed {len(packages)} package(s) into remote venv"
|
|
1062
|
+
if setup.exit_code == 0
|
|
1063
|
+
else detail or "Remote dependency install failed"
|
|
1064
|
+
),
|
|
1065
|
+
timings["runtime_setup_ms"],
|
|
1066
|
+
)
|
|
1067
|
+
if setup.exit_code != 0:
|
|
1068
|
+
raise RuntimeError(detail or setup.stderr or setup.stdout or "Remote dependency install failed")
|
|
1069
|
+
return "/tmp/gradient-venv/bin/python"
|
|
1070
|
+
|
|
1071
|
+
if setup_command and self.environment.install_at_runtime:
|
|
1072
|
+
t0 = time.perf_counter()
|
|
1073
|
+
setup = self._exec_machine_with_retry(
|
|
1074
|
+
machine_id,
|
|
1075
|
+
command=["sh", "-lc", f"cd /workspace && {setup_command}"],
|
|
1076
|
+
timeout_seconds=run_timeout_seconds,
|
|
1077
|
+
trace=False,
|
|
1078
|
+
stage="dependencies",
|
|
1079
|
+
)
|
|
1080
|
+
timings["runtime_setup_ms"] = _elapsed_ms(t0)
|
|
1081
|
+
self._stage(
|
|
1082
|
+
"dependencies",
|
|
1083
|
+
"ready" if setup.exit_code == 0 else "failed",
|
|
1084
|
+
"Runtime dependency setup completed" if setup.exit_code == 0 else setup.stderr,
|
|
1085
|
+
timings["runtime_setup_ms"],
|
|
1086
|
+
)
|
|
1087
|
+
if setup.exit_code != 0:
|
|
1088
|
+
raise RuntimeError(setup.stderr or setup.stdout)
|
|
1089
|
+
return "python"
|
|
1090
|
+
|
|
1091
|
+
self._stage(
|
|
1092
|
+
"dependencies",
|
|
1093
|
+
"cached",
|
|
1094
|
+
"Skipped runtime install; using cached environment image",
|
|
1095
|
+
cached=True,
|
|
1096
|
+
)
|
|
1097
|
+
return "python"
|
|
1098
|
+
|
|
1012
1099
|
def _exec_machine_with_retry(
|
|
1013
1100
|
self,
|
|
1014
1101
|
machine_id: str,
|
|
@@ -1133,10 +1220,13 @@ def _with_source_bundle(
|
|
|
1133
1220
|
source: SourceBundle,
|
|
1134
1221
|
*,
|
|
1135
1222
|
service_code: str | None = None,
|
|
1223
|
+
extra_env: Mapping[str, str] | None = None,
|
|
1136
1224
|
) -> dict[str, Any]:
|
|
1137
1225
|
result = copy.deepcopy(dict(template))
|
|
1138
1226
|
config = dict(result.get("config") or {})
|
|
1139
1227
|
env = dict(config.get("env") or {})
|
|
1228
|
+
if extra_env:
|
|
1229
|
+
env.update({key: str(value) for key, value in extra_env.items() if value is not None})
|
|
1140
1230
|
env["GRADIENT_SOURCE_ARCHIVE_B64"] = source.archive_b64
|
|
1141
1231
|
init_steps = [
|
|
1142
1232
|
"rm -rf /workspace",
|
|
@@ -1258,7 +1348,14 @@ def _find_global_object_ref(root: Path, target: Any) -> tuple[str, str] | None:
|
|
|
1258
1348
|
continue
|
|
1259
1349
|
for name, value in vars(module).items():
|
|
1260
1350
|
if value is target and not name.startswith("_"):
|
|
1261
|
-
|
|
1351
|
+
module_name = module.__name__
|
|
1352
|
+
if module_name == "__main__":
|
|
1353
|
+
module_name = (
|
|
1354
|
+
entrypoint[:-3].replace("/", ".")
|
|
1355
|
+
if entrypoint.endswith(".py")
|
|
1356
|
+
else entrypoint.replace("/", ".")
|
|
1357
|
+
)
|
|
1358
|
+
return entrypoint, f"{module_name}:{name}"
|
|
1262
1359
|
return None
|
|
1263
1360
|
|
|
1264
1361
|
|
|
@@ -1406,6 +1503,22 @@ def _elapsed_ms(started: float) -> int:
|
|
|
1406
1503
|
return int((time.perf_counter() - started) * 1000)
|
|
1407
1504
|
|
|
1408
1505
|
|
|
1506
|
+
def _compact_output(value: str | None, *, limit: int = 500) -> str:
|
|
1507
|
+
text = " ".join(str(value or "").split())
|
|
1508
|
+
if len(text) <= limit:
|
|
1509
|
+
return text
|
|
1510
|
+
return text[: limit - 3] + "..."
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
def _python_venv_install_command(packages: list[str]) -> str:
|
|
1514
|
+
quoted = " ".join(shlex.quote(package) for package in packages)
|
|
1515
|
+
return (
|
|
1516
|
+
"python -m venv /tmp/gradient-venv && "
|
|
1517
|
+
"/tmp/gradient-venv/bin/pip install --upgrade pip && "
|
|
1518
|
+
f"/tmp/gradient-venv/bin/pip install --no-cache-dir {quoted}"
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
|
|
1409
1522
|
def _stable_hash(value: Any) -> str:
|
|
1410
1523
|
return hashlib.sha256(
|
|
1411
1524
|
json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
|
|
@@ -138,11 +138,18 @@ class AgentTests(unittest.TestCase):
|
|
|
138
138
|
class Client:
|
|
139
139
|
messages = Messages()
|
|
140
140
|
|
|
141
|
-
run = Agent(
|
|
141
|
+
run = Agent(
|
|
142
|
+
Model.CLAUDE_HAIKU_4_5,
|
|
143
|
+
client=Client(),
|
|
144
|
+
instructions="You are a helpful assistant.",
|
|
145
|
+
)
|
|
142
146
|
result = run.run("hello")
|
|
143
147
|
|
|
144
148
|
self.assertEqual(result.text, "anthropic response")
|
|
145
149
|
self.assertEqual(calls[0]["model"], "claude-haiku-4-5")
|
|
150
|
+
self.assertEqual(calls[0]["system"], "You are a helpful assistant.")
|
|
151
|
+
self.assertEqual(calls[0]["messages"][0]["role"], "user")
|
|
152
|
+
self.assertTrue(all(message["role"] != "system" for message in calls[0]["messages"]))
|
|
146
153
|
self.assertIn("messages", calls[0])
|
|
147
154
|
|
|
148
155
|
def test_global_agent_can_be_packaged_as_runtime_target(self):
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from gradient_sdk.dependencies import infer_python_packages
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DependencyInferenceTests(unittest.TestCase):
|
|
9
|
+
def test_infers_third_party_imports_and_keeps_explicit_packages(self):
|
|
10
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
11
|
+
root = Path(tmp)
|
|
12
|
+
entry = root / "agent.py"
|
|
13
|
+
entry.write_text(
|
|
14
|
+
"\n".join(
|
|
15
|
+
[
|
|
16
|
+
"import os",
|
|
17
|
+
"from dotenv import load_dotenv",
|
|
18
|
+
"from gradient_sdk import Agent",
|
|
19
|
+
"import anthropic",
|
|
20
|
+
"from helper import value",
|
|
21
|
+
"",
|
|
22
|
+
"load_dotenv()",
|
|
23
|
+
"value = value",
|
|
24
|
+
]
|
|
25
|
+
),
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
)
|
|
28
|
+
(root / "helper.py").write_text("value = 1\n", encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
packages = infer_python_packages(
|
|
31
|
+
entry,
|
|
32
|
+
source_root=root,
|
|
33
|
+
explicit=["anthropic"],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.assertEqual(packages, ["anthropic", "python-dotenv", "usegradient"])
|
|
37
|
+
|
|
38
|
+
def test_infers_provider_from_model_enum(self):
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
40
|
+
root = Path(tmp)
|
|
41
|
+
entry = root / "agent.py"
|
|
42
|
+
entry.write_text(
|
|
43
|
+
"\n".join(
|
|
44
|
+
[
|
|
45
|
+
"from gradient_sdk import Agent, Model",
|
|
46
|
+
"agent = Agent(Model.CLAUDE_HAIKU_4_5)",
|
|
47
|
+
]
|
|
48
|
+
),
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
packages = infer_python_packages(entry, source_root=root)
|
|
52
|
+
|
|
53
|
+
self.assertEqual(packages, ["anthropic", "usegradient"])
|
|
54
|
+
|
|
55
|
+
def test_follows_local_relative_imports(self):
|
|
56
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
57
|
+
root = Path(tmp)
|
|
58
|
+
pkg = root / "app"
|
|
59
|
+
pkg.mkdir()
|
|
60
|
+
(pkg / "__init__.py").write_text("", encoding="utf-8")
|
|
61
|
+
(pkg / "tools.py").write_text("import requests\n", encoding="utf-8")
|
|
62
|
+
entry = pkg / "agent.py"
|
|
63
|
+
entry.write_text("from .tools import *\nimport json\n", encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
packages = infer_python_packages(entry, source_root=root)
|
|
66
|
+
|
|
67
|
+
self.assertEqual(packages, ["requests"])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
unittest.main()
|
|
@@ -127,7 +127,9 @@ class FakeClient:
|
|
|
127
127
|
|
|
128
128
|
def exec_machine(self, machine_id, command, **kwargs):
|
|
129
129
|
self.calls.append(("exec_machine", machine_id, command, kwargs))
|
|
130
|
-
if command[:2] == ["
|
|
130
|
+
if command[:2] == ["sh", "-lc"] and "gradient-venv" in command[2]:
|
|
131
|
+
return ExecResult(stdout="installed\n")
|
|
132
|
+
if len(command) >= 2 and command[1] == "-c":
|
|
131
133
|
return ExecResult(stdout='{"ok": true}\n')
|
|
132
134
|
return ExecResult(stdout="ok\n")
|
|
133
135
|
|
|
@@ -211,10 +213,19 @@ class RuntimeTests(unittest.TestCase):
|
|
|
211
213
|
run_exec = next(
|
|
212
214
|
call
|
|
213
215
|
for call in client.calls
|
|
214
|
-
if call[0] == "exec_machine" and call[2][
|
|
216
|
+
if call[0] == "exec_machine" and len(call[2]) >= 2 and call[2][1] == "-c"
|
|
215
217
|
)
|
|
216
218
|
self.assertTrue(run_exec[3]["trace"])
|
|
217
219
|
self.assertEqual(run_exec[3]["watch"], ["/workspace"])
|
|
220
|
+
self.assertTrue(run_exec[2][0].endswith("python"))
|
|
221
|
+
resolve_call = next(call for call in client.calls if call[0] == "resolve_environment")
|
|
222
|
+
self.assertIn("usegradient", resolve_call[1]["environment"]["packages"])
|
|
223
|
+
setup_calls = [
|
|
224
|
+
call
|
|
225
|
+
for call in client.calls
|
|
226
|
+
if call[0] == "exec_machine" and call[2][:2] == ["sh", "-lc"] and "gradient-venv" in call[2][2]
|
|
227
|
+
]
|
|
228
|
+
self.assertEqual(len(setup_calls), 1)
|
|
218
229
|
|
|
219
230
|
def test_gradient_build_returns_v1_handle(self):
|
|
220
231
|
client = FakeClient()
|
|
@@ -250,7 +261,7 @@ class RuntimeTests(unittest.TestCase):
|
|
|
250
261
|
run_exec = next(
|
|
251
262
|
call
|
|
252
263
|
for call in client.calls
|
|
253
|
-
if call[0] == "exec_machine" and call[2][
|
|
264
|
+
if call[0] == "exec_machine" and len(call[2]) >= 2 and call[2][1] == "-c"
|
|
254
265
|
)
|
|
255
266
|
self.assertFalse(run_exec[3]["trace"])
|
|
256
267
|
|
|
@@ -320,7 +331,9 @@ class RuntimeTests(unittest.TestCase):
|
|
|
320
331
|
"Fly API 412 POST ... exec request failed: EOF",
|
|
321
332
|
response_body='{"error":"failed_precondition: exec request failed: EOF"}',
|
|
322
333
|
)
|
|
323
|
-
if command[:2] == ["
|
|
334
|
+
if command[:2] == ["sh", "-lc"] and "gradient-venv" in command[2]:
|
|
335
|
+
return ExecResult(stdout="installed\n")
|
|
336
|
+
if len(command) >= 2 and command[1] == "-c":
|
|
324
337
|
return ExecResult(stdout='{"ok": true}\n')
|
|
325
338
|
return ExecResult(stdout="ok\n")
|
|
326
339
|
|
|
@@ -355,7 +368,9 @@ class RuntimeTests(unittest.TestCase):
|
|
|
355
368
|
"Fly API 412 POST ... machine not running",
|
|
356
369
|
response_body='{"error":"failed_precondition: machine not running"}',
|
|
357
370
|
)
|
|
358
|
-
if command[:2] == ["
|
|
371
|
+
if command[:2] == ["sh", "-lc"] and "gradient-venv" in command[2]:
|
|
372
|
+
return ExecResult(stdout="installed\n")
|
|
373
|
+
if len(command) >= 2 and command[1] == "-c":
|
|
359
374
|
return ExecResult(stdout='{"ok": true}\n')
|
|
360
375
|
return ExecResult(stdout="ok\n")
|
|
361
376
|
|
|
@@ -391,10 +406,13 @@ class RuntimeTests(unittest.TestCase):
|
|
|
391
406
|
serve_exec = next(
|
|
392
407
|
call
|
|
393
408
|
for call in client.calls
|
|
394
|
-
if call[0] == "exec_machine"
|
|
409
|
+
if call[0] == "exec_machine"
|
|
410
|
+
and call[2][:2] == ["sh", "-lc"]
|
|
411
|
+
and "gradient-service.py" in call[2][2]
|
|
395
412
|
)
|
|
396
413
|
self.assertTrue(serve_exec[3]["trace"])
|
|
397
414
|
self.assertEqual(serve_exec[3]["watch"], ["/workspace"])
|
|
415
|
+
self.assertIn("/tmp/gradient-venv/bin/python", serve_exec[2][2])
|
|
398
416
|
|
|
399
417
|
stopped = deployment.stop()
|
|
400
418
|
self.assertEqual(stopped.status, "stopped")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|