amd-gaia 0.14.3__py3-none-any.whl → 0.15.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.
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
- amd_gaia-0.15.1.dist-info/RECORD +178 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
- gaia/__init__.py +29 -29
- gaia/agents/__init__.py +19 -19
- gaia/agents/base/__init__.py +9 -9
- gaia/agents/base/agent.py +2177 -2177
- gaia/agents/base/api_agent.py +120 -120
- gaia/agents/base/console.py +1841 -1841
- gaia/agents/base/errors.py +237 -237
- gaia/agents/base/mcp_agent.py +86 -86
- gaia/agents/base/tools.py +83 -83
- gaia/agents/blender/agent.py +556 -556
- gaia/agents/blender/agent_simple.py +133 -135
- gaia/agents/blender/app.py +211 -211
- gaia/agents/blender/app_simple.py +41 -41
- gaia/agents/blender/core/__init__.py +16 -16
- gaia/agents/blender/core/materials.py +506 -506
- gaia/agents/blender/core/objects.py +316 -316
- gaia/agents/blender/core/rendering.py +225 -225
- gaia/agents/blender/core/scene.py +220 -220
- gaia/agents/blender/core/view.py +146 -146
- gaia/agents/chat/__init__.py +9 -9
- gaia/agents/chat/agent.py +835 -835
- gaia/agents/chat/app.py +1058 -1058
- gaia/agents/chat/session.py +508 -508
- gaia/agents/chat/tools/__init__.py +15 -15
- gaia/agents/chat/tools/file_tools.py +96 -96
- gaia/agents/chat/tools/rag_tools.py +1729 -1729
- gaia/agents/chat/tools/shell_tools.py +436 -436
- gaia/agents/code/__init__.py +7 -7
- gaia/agents/code/agent.py +549 -549
- gaia/agents/code/cli.py +377 -0
- gaia/agents/code/models.py +135 -135
- gaia/agents/code/orchestration/__init__.py +24 -24
- gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
- gaia/agents/code/orchestration/checklist_generator.py +713 -713
- gaia/agents/code/orchestration/factories/__init__.py +9 -9
- gaia/agents/code/orchestration/factories/base.py +63 -63
- gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
- gaia/agents/code/orchestration/factories/python_factory.py +106 -106
- gaia/agents/code/orchestration/orchestrator.py +841 -841
- gaia/agents/code/orchestration/project_analyzer.py +391 -391
- gaia/agents/code/orchestration/steps/__init__.py +67 -67
- gaia/agents/code/orchestration/steps/base.py +188 -188
- gaia/agents/code/orchestration/steps/error_handler.py +314 -314
- gaia/agents/code/orchestration/steps/nextjs.py +828 -828
- gaia/agents/code/orchestration/steps/python.py +307 -307
- gaia/agents/code/orchestration/template_catalog.py +469 -469
- gaia/agents/code/orchestration/workflows/__init__.py +14 -14
- gaia/agents/code/orchestration/workflows/base.py +80 -80
- gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
- gaia/agents/code/orchestration/workflows/python.py +94 -94
- gaia/agents/code/prompts/__init__.py +11 -11
- gaia/agents/code/prompts/base_prompt.py +77 -77
- gaia/agents/code/prompts/code_patterns.py +2036 -2036
- gaia/agents/code/prompts/nextjs_prompt.py +40 -40
- gaia/agents/code/prompts/python_prompt.py +109 -109
- gaia/agents/code/schema_inference.py +365 -365
- gaia/agents/code/system_prompt.py +41 -41
- gaia/agents/code/tools/__init__.py +42 -42
- gaia/agents/code/tools/cli_tools.py +1138 -1138
- gaia/agents/code/tools/code_formatting.py +319 -319
- gaia/agents/code/tools/code_tools.py +769 -769
- gaia/agents/code/tools/error_fixing.py +1347 -1347
- gaia/agents/code/tools/external_tools.py +180 -180
- gaia/agents/code/tools/file_io.py +845 -845
- gaia/agents/code/tools/prisma_tools.py +190 -190
- gaia/agents/code/tools/project_management.py +1016 -1016
- gaia/agents/code/tools/testing.py +321 -321
- gaia/agents/code/tools/typescript_tools.py +122 -122
- gaia/agents/code/tools/validation_parsing.py +461 -461
- gaia/agents/code/tools/validation_tools.py +806 -806
- gaia/agents/code/tools/web_dev_tools.py +1758 -1758
- gaia/agents/code/validators/__init__.py +16 -16
- gaia/agents/code/validators/antipattern_checker.py +241 -241
- gaia/agents/code/validators/ast_analyzer.py +197 -197
- gaia/agents/code/validators/requirements_validator.py +145 -145
- gaia/agents/code/validators/syntax_validator.py +171 -171
- gaia/agents/docker/__init__.py +7 -7
- gaia/agents/docker/agent.py +642 -642
- gaia/agents/emr/__init__.py +8 -8
- gaia/agents/emr/agent.py +1506 -1506
- gaia/agents/emr/cli.py +1322 -1322
- gaia/agents/emr/constants.py +475 -475
- gaia/agents/emr/dashboard/__init__.py +4 -4
- gaia/agents/emr/dashboard/server.py +1974 -1974
- gaia/agents/jira/__init__.py +11 -11
- gaia/agents/jira/agent.py +894 -894
- gaia/agents/jira/jql_templates.py +299 -299
- gaia/agents/routing/__init__.py +7 -7
- gaia/agents/routing/agent.py +567 -570
- gaia/agents/routing/system_prompt.py +75 -75
- gaia/agents/summarize/__init__.py +11 -0
- gaia/agents/summarize/agent.py +885 -0
- gaia/agents/summarize/prompts.py +129 -0
- gaia/api/__init__.py +23 -23
- gaia/api/agent_registry.py +238 -238
- gaia/api/app.py +305 -305
- gaia/api/openai_server.py +575 -575
- gaia/api/schemas.py +186 -186
- gaia/api/sse_handler.py +373 -373
- gaia/apps/__init__.py +4 -4
- gaia/apps/llm/__init__.py +6 -6
- gaia/apps/llm/app.py +173 -169
- gaia/apps/summarize/app.py +116 -633
- gaia/apps/summarize/html_viewer.py +133 -133
- gaia/apps/summarize/pdf_formatter.py +284 -284
- gaia/audio/__init__.py +2 -2
- gaia/audio/audio_client.py +439 -439
- gaia/audio/audio_recorder.py +269 -269
- gaia/audio/kokoro_tts.py +599 -599
- gaia/audio/whisper_asr.py +432 -432
- gaia/chat/__init__.py +16 -16
- gaia/chat/app.py +430 -430
- gaia/chat/prompts.py +522 -522
- gaia/chat/sdk.py +1228 -1225
- gaia/cli.py +5481 -5621
- gaia/database/__init__.py +10 -10
- gaia/database/agent.py +176 -176
- gaia/database/mixin.py +290 -290
- gaia/database/testing.py +64 -64
- gaia/eval/batch_experiment.py +2332 -2332
- gaia/eval/claude.py +542 -542
- gaia/eval/config.py +37 -37
- gaia/eval/email_generator.py +512 -512
- gaia/eval/eval.py +3179 -3179
- gaia/eval/groundtruth.py +1130 -1130
- gaia/eval/transcript_generator.py +582 -582
- gaia/eval/webapp/README.md +167 -167
- gaia/eval/webapp/package-lock.json +875 -875
- gaia/eval/webapp/package.json +20 -20
- gaia/eval/webapp/public/app.js +3402 -3402
- gaia/eval/webapp/public/index.html +87 -87
- gaia/eval/webapp/public/styles.css +3661 -3661
- gaia/eval/webapp/server.js +415 -415
- gaia/eval/webapp/test-setup.js +72 -72
- gaia/llm/__init__.py +9 -2
- gaia/llm/base_client.py +60 -0
- gaia/llm/exceptions.py +12 -0
- gaia/llm/factory.py +70 -0
- gaia/llm/lemonade_client.py +3236 -3221
- gaia/llm/lemonade_manager.py +294 -294
- gaia/llm/providers/__init__.py +9 -0
- gaia/llm/providers/claude.py +108 -0
- gaia/llm/providers/lemonade.py +120 -0
- gaia/llm/providers/openai_provider.py +79 -0
- gaia/llm/vlm_client.py +382 -382
- gaia/logger.py +189 -189
- gaia/mcp/agent_mcp_server.py +245 -245
- gaia/mcp/blender_mcp_client.py +138 -138
- gaia/mcp/blender_mcp_server.py +648 -648
- gaia/mcp/context7_cache.py +332 -332
- gaia/mcp/external_services.py +518 -518
- gaia/mcp/mcp_bridge.py +811 -550
- gaia/mcp/servers/__init__.py +6 -6
- gaia/mcp/servers/docker_mcp.py +83 -83
- gaia/perf_analysis.py +361 -0
- gaia/rag/__init__.py +10 -10
- gaia/rag/app.py +293 -293
- gaia/rag/demo.py +304 -304
- gaia/rag/pdf_utils.py +235 -235
- gaia/rag/sdk.py +2194 -2194
- gaia/security.py +163 -163
- gaia/talk/app.py +289 -289
- gaia/talk/sdk.py +538 -538
- gaia/testing/__init__.py +87 -87
- gaia/testing/assertions.py +330 -330
- gaia/testing/fixtures.py +333 -333
- gaia/testing/mocks.py +493 -493
- gaia/util.py +46 -46
- gaia/utils/__init__.py +33 -33
- gaia/utils/file_watcher.py +675 -675
- gaia/utils/parsing.py +223 -223
- gaia/version.py +100 -100
- amd_gaia-0.14.3.dist-info/RECORD +0 -168
- gaia/agents/code/app.py +0 -266
- gaia/llm/llm_client.py +0 -729
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
|
@@ -1,321 +1,321 @@
|
|
|
1
|
-
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
"""Testing tools mixin for Code Agent."""
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import shlex
|
|
7
|
-
import subprocess
|
|
8
|
-
import sys
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Optional
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TestingMixin:
|
|
15
|
-
"""Mixin providing Python code execution and testing tools.
|
|
16
|
-
|
|
17
|
-
This mixin provides tools for:
|
|
18
|
-
- Executing Python files as subprocesses
|
|
19
|
-
- Running pytest test suites
|
|
20
|
-
- Capturing and analyzing execution output
|
|
21
|
-
- Timeout management for long-running processes
|
|
22
|
-
|
|
23
|
-
Tools provided:
|
|
24
|
-
- execute_python_file: Execute Python file with arguments and capture output
|
|
25
|
-
- run_tests: Run pytest test suite for a project with timeout and error handling
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
def register_testing_tools(self) -> None:
|
|
29
|
-
"""Register testing tools."""
|
|
30
|
-
from gaia.agents.base.tools import tool
|
|
31
|
-
|
|
32
|
-
@tool
|
|
33
|
-
def execute_python_file(
|
|
34
|
-
file_path: str,
|
|
35
|
-
args: Optional[List[str]] = None,
|
|
36
|
-
timeout: int = 60,
|
|
37
|
-
working_directory: Optional[str] = None,
|
|
38
|
-
env_vars: Optional[Dict[str, str]] = None,
|
|
39
|
-
) -> Dict[str, Any]:
|
|
40
|
-
"""Execute a Python file as a subprocess and capture output.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
file_path: Path to the Python file to execute.
|
|
44
|
-
args: Optional CLI arguments (list or space-delimited string).
|
|
45
|
-
timeout: Seconds to wait before aborting execution.
|
|
46
|
-
working_directory: Directory to run the command from.
|
|
47
|
-
env_vars: Additional environment variables to inject.
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
Dictionary with execution metadata and captured output.
|
|
51
|
-
"""
|
|
52
|
-
try:
|
|
53
|
-
path = Path(file_path)
|
|
54
|
-
if not path.exists():
|
|
55
|
-
return {
|
|
56
|
-
"status": "error",
|
|
57
|
-
"error": f"File not found: {file_path}",
|
|
58
|
-
"has_errors": True,
|
|
59
|
-
}
|
|
60
|
-
if not path.is_file():
|
|
61
|
-
return {
|
|
62
|
-
"status": "error",
|
|
63
|
-
"error": f"Path is not a file: {file_path}",
|
|
64
|
-
"has_errors": True,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
resolved_file = str(path.resolve())
|
|
68
|
-
cmd = [sys.executable, resolved_file]
|
|
69
|
-
|
|
70
|
-
if args:
|
|
71
|
-
if isinstance(args, str):
|
|
72
|
-
extra_args = shlex.split(args)
|
|
73
|
-
elif isinstance(args, list):
|
|
74
|
-
extra_args = [str(a) for a in args]
|
|
75
|
-
else:
|
|
76
|
-
return {
|
|
77
|
-
"status": "error",
|
|
78
|
-
"error": "args must be a list of strings or a string",
|
|
79
|
-
"has_errors": True,
|
|
80
|
-
}
|
|
81
|
-
cmd.extend(extra_args)
|
|
82
|
-
|
|
83
|
-
if working_directory:
|
|
84
|
-
wd_path = Path(working_directory)
|
|
85
|
-
if not wd_path.exists():
|
|
86
|
-
return {
|
|
87
|
-
"status": "error",
|
|
88
|
-
"error": f"Working directory not found: {working_directory}",
|
|
89
|
-
"has_errors": True,
|
|
90
|
-
}
|
|
91
|
-
if not wd_path.is_dir():
|
|
92
|
-
return {
|
|
93
|
-
"status": "error",
|
|
94
|
-
"error": f"Working directory is not a directory: {working_directory}",
|
|
95
|
-
"has_errors": True,
|
|
96
|
-
}
|
|
97
|
-
cwd = str(wd_path.resolve())
|
|
98
|
-
else:
|
|
99
|
-
cwd = str(path.parent.resolve())
|
|
100
|
-
|
|
101
|
-
env = os.environ.copy()
|
|
102
|
-
if env_vars:
|
|
103
|
-
env.update({key: str(value) for key, value in env_vars.items()})
|
|
104
|
-
|
|
105
|
-
start_time = datetime.utcnow()
|
|
106
|
-
try:
|
|
107
|
-
result = subprocess.run(
|
|
108
|
-
cmd,
|
|
109
|
-
cwd=cwd,
|
|
110
|
-
env=env,
|
|
111
|
-
capture_output=True,
|
|
112
|
-
text=True,
|
|
113
|
-
timeout=timeout,
|
|
114
|
-
check=False,
|
|
115
|
-
)
|
|
116
|
-
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
117
|
-
except subprocess.TimeoutExpired as exc:
|
|
118
|
-
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
119
|
-
# Ensure stdout/stderr are strings, not bytes
|
|
120
|
-
stdout_str = ""
|
|
121
|
-
stderr_str = ""
|
|
122
|
-
if exc.stdout:
|
|
123
|
-
stdout_str = (
|
|
124
|
-
exc.stdout
|
|
125
|
-
if isinstance(exc.stdout, str)
|
|
126
|
-
else exc.stdout.decode("utf-8", errors="replace")
|
|
127
|
-
)
|
|
128
|
-
if exc.stderr:
|
|
129
|
-
stderr_str = (
|
|
130
|
-
exc.stderr
|
|
131
|
-
if isinstance(exc.stderr, str)
|
|
132
|
-
else exc.stderr.decode("utf-8", errors="replace")
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
"status": "error",
|
|
137
|
-
"error": f"Execution timed out after {timeout} seconds",
|
|
138
|
-
"file_path": resolved_file,
|
|
139
|
-
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
140
|
-
"stdout": stdout_str,
|
|
141
|
-
"stderr": stderr_str,
|
|
142
|
-
"has_errors": True,
|
|
143
|
-
"timed_out": True,
|
|
144
|
-
"timeout": timeout,
|
|
145
|
-
"duration_seconds": duration,
|
|
146
|
-
"cwd": cwd,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
stdout = result.stdout or ""
|
|
150
|
-
stderr = result.stderr or ""
|
|
151
|
-
truncated = False
|
|
152
|
-
max_output = 10_000
|
|
153
|
-
|
|
154
|
-
if len(stdout) > max_output:
|
|
155
|
-
stdout = stdout[:max_output] + "\n...output truncated (stdout)..."
|
|
156
|
-
truncated = True
|
|
157
|
-
|
|
158
|
-
if len(stderr) > max_output:
|
|
159
|
-
stderr = stderr[:max_output] + "\n...output truncated (stderr)..."
|
|
160
|
-
truncated = True
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
"status": "success",
|
|
164
|
-
"file_path": resolved_file,
|
|
165
|
-
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
166
|
-
"stdout": stdout,
|
|
167
|
-
"stderr": stderr,
|
|
168
|
-
"return_code": result.returncode,
|
|
169
|
-
"has_errors": result.returncode != 0,
|
|
170
|
-
"duration_seconds": duration,
|
|
171
|
-
"timeout": timeout,
|
|
172
|
-
"cwd": cwd,
|
|
173
|
-
"output_truncated": truncated,
|
|
174
|
-
}
|
|
175
|
-
except Exception as exc:
|
|
176
|
-
return {
|
|
177
|
-
"status": "error",
|
|
178
|
-
"error": str(exc),
|
|
179
|
-
"has_errors": True,
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
@tool
|
|
183
|
-
def run_tests(
|
|
184
|
-
project_path: str = ".",
|
|
185
|
-
pytest_args: Optional[List[str]] = None,
|
|
186
|
-
timeout: int = 120,
|
|
187
|
-
env_vars: Optional[Dict[str, str]] = None,
|
|
188
|
-
) -> Dict[str, Any]:
|
|
189
|
-
"""Run pytest for the specified project directory."""
|
|
190
|
-
try:
|
|
191
|
-
project_dir = Path(project_path).resolve()
|
|
192
|
-
if not project_dir.exists():
|
|
193
|
-
return {
|
|
194
|
-
"status": "error",
|
|
195
|
-
"error": f"Project path not found: {project_path}",
|
|
196
|
-
}
|
|
197
|
-
if not project_dir.is_dir():
|
|
198
|
-
return {
|
|
199
|
-
"status": "error",
|
|
200
|
-
"error": f"Project path is not a directory: {project_path}",
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if isinstance(pytest_args, str):
|
|
204
|
-
extra_args = shlex.split(pytest_args)
|
|
205
|
-
elif isinstance(pytest_args, list):
|
|
206
|
-
extra_args = [str(arg) for arg in pytest_args]
|
|
207
|
-
elif pytest_args is None:
|
|
208
|
-
extra_args = []
|
|
209
|
-
else:
|
|
210
|
-
return {
|
|
211
|
-
"status": "error",
|
|
212
|
-
"error": "pytest_args must be a list of strings or a string",
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
cmd = [sys.executable, "-m", "pytest"]
|
|
216
|
-
if not extra_args:
|
|
217
|
-
# Default to running the entire suite quietly to keep output manageable
|
|
218
|
-
extra_args = ["-q"]
|
|
219
|
-
cmd.extend(extra_args)
|
|
220
|
-
|
|
221
|
-
env = os.environ.copy()
|
|
222
|
-
if env_vars:
|
|
223
|
-
env.update({key: str(value) for key, value in env_vars.items()})
|
|
224
|
-
|
|
225
|
-
existing_pythonpath = env.get("PYTHONPATH")
|
|
226
|
-
project_pythonpath = str(project_dir)
|
|
227
|
-
if existing_pythonpath:
|
|
228
|
-
env["PYTHONPATH"] = (
|
|
229
|
-
f"{project_pythonpath}{os.pathsep}{existing_pythonpath}"
|
|
230
|
-
)
|
|
231
|
-
else:
|
|
232
|
-
env["PYTHONPATH"] = project_pythonpath
|
|
233
|
-
|
|
234
|
-
start_time = datetime.utcnow()
|
|
235
|
-
try:
|
|
236
|
-
result = subprocess.run(
|
|
237
|
-
cmd,
|
|
238
|
-
cwd=str(project_dir),
|
|
239
|
-
env=env,
|
|
240
|
-
capture_output=True,
|
|
241
|
-
text=True,
|
|
242
|
-
timeout=timeout,
|
|
243
|
-
check=False,
|
|
244
|
-
)
|
|
245
|
-
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
246
|
-
except subprocess.TimeoutExpired as exc:
|
|
247
|
-
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
248
|
-
# Ensure stdout/stderr are strings, not bytes
|
|
249
|
-
stdout_str = ""
|
|
250
|
-
stderr_str = ""
|
|
251
|
-
if hasattr(exc, "stdout") and exc.stdout:
|
|
252
|
-
stdout_str = (
|
|
253
|
-
exc.stdout
|
|
254
|
-
if isinstance(exc.stdout, str)
|
|
255
|
-
else exc.stdout.decode("utf-8", errors="replace")
|
|
256
|
-
)
|
|
257
|
-
if hasattr(exc, "stderr") and exc.stderr:
|
|
258
|
-
stderr_str = (
|
|
259
|
-
exc.stderr
|
|
260
|
-
if isinstance(exc.stderr, str)
|
|
261
|
-
else exc.stderr.decode("utf-8", errors="replace")
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
"status": "error",
|
|
266
|
-
"error": f"pytest timed out after {timeout} seconds",
|
|
267
|
-
"project_path": str(project_dir),
|
|
268
|
-
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
269
|
-
"stdout": stdout_str,
|
|
270
|
-
"stderr": stderr_str,
|
|
271
|
-
"tests_passed": False,
|
|
272
|
-
"timed_out": True,
|
|
273
|
-
"timeout": timeout,
|
|
274
|
-
"duration_seconds": duration,
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
stdout = result.stdout or ""
|
|
278
|
-
stderr = result.stderr or ""
|
|
279
|
-
truncated = False
|
|
280
|
-
max_output = 10_000
|
|
281
|
-
|
|
282
|
-
if len(stdout) > max_output:
|
|
283
|
-
stdout = stdout[:max_output] + "\n...output truncated (stdout)..."
|
|
284
|
-
truncated = True
|
|
285
|
-
|
|
286
|
-
if len(stderr) > max_output:
|
|
287
|
-
stderr = stderr[:max_output] + "\n...output truncated (stderr)..."
|
|
288
|
-
truncated = True
|
|
289
|
-
|
|
290
|
-
# Parse pytest output for failure count
|
|
291
|
-
failure_summary = ""
|
|
292
|
-
if result.returncode != 0:
|
|
293
|
-
# Look for pytest summary line like "7 failed, 75 passed"
|
|
294
|
-
import re
|
|
295
|
-
|
|
296
|
-
summary_match = re.search(r"(\d+)\s+failed", stdout)
|
|
297
|
-
if summary_match:
|
|
298
|
-
num_failed = summary_match.group(1)
|
|
299
|
-
failure_summary = (
|
|
300
|
-
f"{num_failed} test(s) failed - check stdout for details"
|
|
301
|
-
)
|
|
302
|
-
else:
|
|
303
|
-
failure_summary = "Tests failed - check stdout for details"
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
"status": "success",
|
|
307
|
-
"project_path": str(project_dir),
|
|
308
|
-
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
309
|
-
"stdout": stdout,
|
|
310
|
-
"stderr": stderr,
|
|
311
|
-
"return_code": result.returncode,
|
|
312
|
-
"tests_passed": result.returncode == 0,
|
|
313
|
-
"failure_summary": (
|
|
314
|
-
failure_summary if not result.returncode == 0 else ""
|
|
315
|
-
),
|
|
316
|
-
"duration_seconds": duration,
|
|
317
|
-
"timeout": timeout,
|
|
318
|
-
"output_truncated": truncated,
|
|
319
|
-
}
|
|
320
|
-
except Exception as exc:
|
|
321
|
-
return {"status": "error", "error": str(exc)}
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Testing tools mixin for Code Agent."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestingMixin:
|
|
15
|
+
"""Mixin providing Python code execution and testing tools.
|
|
16
|
+
|
|
17
|
+
This mixin provides tools for:
|
|
18
|
+
- Executing Python files as subprocesses
|
|
19
|
+
- Running pytest test suites
|
|
20
|
+
- Capturing and analyzing execution output
|
|
21
|
+
- Timeout management for long-running processes
|
|
22
|
+
|
|
23
|
+
Tools provided:
|
|
24
|
+
- execute_python_file: Execute Python file with arguments and capture output
|
|
25
|
+
- run_tests: Run pytest test suite for a project with timeout and error handling
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def register_testing_tools(self) -> None:
|
|
29
|
+
"""Register testing tools."""
|
|
30
|
+
from gaia.agents.base.tools import tool
|
|
31
|
+
|
|
32
|
+
@tool
|
|
33
|
+
def execute_python_file(
|
|
34
|
+
file_path: str,
|
|
35
|
+
args: Optional[List[str]] = None,
|
|
36
|
+
timeout: int = 60,
|
|
37
|
+
working_directory: Optional[str] = None,
|
|
38
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
"""Execute a Python file as a subprocess and capture output.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
file_path: Path to the Python file to execute.
|
|
44
|
+
args: Optional CLI arguments (list or space-delimited string).
|
|
45
|
+
timeout: Seconds to wait before aborting execution.
|
|
46
|
+
working_directory: Directory to run the command from.
|
|
47
|
+
env_vars: Additional environment variables to inject.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary with execution metadata and captured output.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
path = Path(file_path)
|
|
54
|
+
if not path.exists():
|
|
55
|
+
return {
|
|
56
|
+
"status": "error",
|
|
57
|
+
"error": f"File not found: {file_path}",
|
|
58
|
+
"has_errors": True,
|
|
59
|
+
}
|
|
60
|
+
if not path.is_file():
|
|
61
|
+
return {
|
|
62
|
+
"status": "error",
|
|
63
|
+
"error": f"Path is not a file: {file_path}",
|
|
64
|
+
"has_errors": True,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resolved_file = str(path.resolve())
|
|
68
|
+
cmd = [sys.executable, resolved_file]
|
|
69
|
+
|
|
70
|
+
if args:
|
|
71
|
+
if isinstance(args, str):
|
|
72
|
+
extra_args = shlex.split(args)
|
|
73
|
+
elif isinstance(args, list):
|
|
74
|
+
extra_args = [str(a) for a in args]
|
|
75
|
+
else:
|
|
76
|
+
return {
|
|
77
|
+
"status": "error",
|
|
78
|
+
"error": "args must be a list of strings or a string",
|
|
79
|
+
"has_errors": True,
|
|
80
|
+
}
|
|
81
|
+
cmd.extend(extra_args)
|
|
82
|
+
|
|
83
|
+
if working_directory:
|
|
84
|
+
wd_path = Path(working_directory)
|
|
85
|
+
if not wd_path.exists():
|
|
86
|
+
return {
|
|
87
|
+
"status": "error",
|
|
88
|
+
"error": f"Working directory not found: {working_directory}",
|
|
89
|
+
"has_errors": True,
|
|
90
|
+
}
|
|
91
|
+
if not wd_path.is_dir():
|
|
92
|
+
return {
|
|
93
|
+
"status": "error",
|
|
94
|
+
"error": f"Working directory is not a directory: {working_directory}",
|
|
95
|
+
"has_errors": True,
|
|
96
|
+
}
|
|
97
|
+
cwd = str(wd_path.resolve())
|
|
98
|
+
else:
|
|
99
|
+
cwd = str(path.parent.resolve())
|
|
100
|
+
|
|
101
|
+
env = os.environ.copy()
|
|
102
|
+
if env_vars:
|
|
103
|
+
env.update({key: str(value) for key, value in env_vars.items()})
|
|
104
|
+
|
|
105
|
+
start_time = datetime.utcnow()
|
|
106
|
+
try:
|
|
107
|
+
result = subprocess.run(
|
|
108
|
+
cmd,
|
|
109
|
+
cwd=cwd,
|
|
110
|
+
env=env,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
timeout=timeout,
|
|
114
|
+
check=False,
|
|
115
|
+
)
|
|
116
|
+
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
117
|
+
except subprocess.TimeoutExpired as exc:
|
|
118
|
+
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
119
|
+
# Ensure stdout/stderr are strings, not bytes
|
|
120
|
+
stdout_str = ""
|
|
121
|
+
stderr_str = ""
|
|
122
|
+
if exc.stdout:
|
|
123
|
+
stdout_str = (
|
|
124
|
+
exc.stdout
|
|
125
|
+
if isinstance(exc.stdout, str)
|
|
126
|
+
else exc.stdout.decode("utf-8", errors="replace")
|
|
127
|
+
)
|
|
128
|
+
if exc.stderr:
|
|
129
|
+
stderr_str = (
|
|
130
|
+
exc.stderr
|
|
131
|
+
if isinstance(exc.stderr, str)
|
|
132
|
+
else exc.stderr.decode("utf-8", errors="replace")
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"status": "error",
|
|
137
|
+
"error": f"Execution timed out after {timeout} seconds",
|
|
138
|
+
"file_path": resolved_file,
|
|
139
|
+
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
140
|
+
"stdout": stdout_str,
|
|
141
|
+
"stderr": stderr_str,
|
|
142
|
+
"has_errors": True,
|
|
143
|
+
"timed_out": True,
|
|
144
|
+
"timeout": timeout,
|
|
145
|
+
"duration_seconds": duration,
|
|
146
|
+
"cwd": cwd,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
stdout = result.stdout or ""
|
|
150
|
+
stderr = result.stderr or ""
|
|
151
|
+
truncated = False
|
|
152
|
+
max_output = 10_000
|
|
153
|
+
|
|
154
|
+
if len(stdout) > max_output:
|
|
155
|
+
stdout = stdout[:max_output] + "\n...output truncated (stdout)..."
|
|
156
|
+
truncated = True
|
|
157
|
+
|
|
158
|
+
if len(stderr) > max_output:
|
|
159
|
+
stderr = stderr[:max_output] + "\n...output truncated (stderr)..."
|
|
160
|
+
truncated = True
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"status": "success",
|
|
164
|
+
"file_path": resolved_file,
|
|
165
|
+
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
166
|
+
"stdout": stdout,
|
|
167
|
+
"stderr": stderr,
|
|
168
|
+
"return_code": result.returncode,
|
|
169
|
+
"has_errors": result.returncode != 0,
|
|
170
|
+
"duration_seconds": duration,
|
|
171
|
+
"timeout": timeout,
|
|
172
|
+
"cwd": cwd,
|
|
173
|
+
"output_truncated": truncated,
|
|
174
|
+
}
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
return {
|
|
177
|
+
"status": "error",
|
|
178
|
+
"error": str(exc),
|
|
179
|
+
"has_errors": True,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@tool
|
|
183
|
+
def run_tests(
|
|
184
|
+
project_path: str = ".",
|
|
185
|
+
pytest_args: Optional[List[str]] = None,
|
|
186
|
+
timeout: int = 120,
|
|
187
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
188
|
+
) -> Dict[str, Any]:
|
|
189
|
+
"""Run pytest for the specified project directory."""
|
|
190
|
+
try:
|
|
191
|
+
project_dir = Path(project_path).resolve()
|
|
192
|
+
if not project_dir.exists():
|
|
193
|
+
return {
|
|
194
|
+
"status": "error",
|
|
195
|
+
"error": f"Project path not found: {project_path}",
|
|
196
|
+
}
|
|
197
|
+
if not project_dir.is_dir():
|
|
198
|
+
return {
|
|
199
|
+
"status": "error",
|
|
200
|
+
"error": f"Project path is not a directory: {project_path}",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if isinstance(pytest_args, str):
|
|
204
|
+
extra_args = shlex.split(pytest_args)
|
|
205
|
+
elif isinstance(pytest_args, list):
|
|
206
|
+
extra_args = [str(arg) for arg in pytest_args]
|
|
207
|
+
elif pytest_args is None:
|
|
208
|
+
extra_args = []
|
|
209
|
+
else:
|
|
210
|
+
return {
|
|
211
|
+
"status": "error",
|
|
212
|
+
"error": "pytest_args must be a list of strings or a string",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
cmd = [sys.executable, "-m", "pytest"]
|
|
216
|
+
if not extra_args:
|
|
217
|
+
# Default to running the entire suite quietly to keep output manageable
|
|
218
|
+
extra_args = ["-q"]
|
|
219
|
+
cmd.extend(extra_args)
|
|
220
|
+
|
|
221
|
+
env = os.environ.copy()
|
|
222
|
+
if env_vars:
|
|
223
|
+
env.update({key: str(value) for key, value in env_vars.items()})
|
|
224
|
+
|
|
225
|
+
existing_pythonpath = env.get("PYTHONPATH")
|
|
226
|
+
project_pythonpath = str(project_dir)
|
|
227
|
+
if existing_pythonpath:
|
|
228
|
+
env["PYTHONPATH"] = (
|
|
229
|
+
f"{project_pythonpath}{os.pathsep}{existing_pythonpath}"
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
env["PYTHONPATH"] = project_pythonpath
|
|
233
|
+
|
|
234
|
+
start_time = datetime.utcnow()
|
|
235
|
+
try:
|
|
236
|
+
result = subprocess.run(
|
|
237
|
+
cmd,
|
|
238
|
+
cwd=str(project_dir),
|
|
239
|
+
env=env,
|
|
240
|
+
capture_output=True,
|
|
241
|
+
text=True,
|
|
242
|
+
timeout=timeout,
|
|
243
|
+
check=False,
|
|
244
|
+
)
|
|
245
|
+
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
246
|
+
except subprocess.TimeoutExpired as exc:
|
|
247
|
+
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
248
|
+
# Ensure stdout/stderr are strings, not bytes
|
|
249
|
+
stdout_str = ""
|
|
250
|
+
stderr_str = ""
|
|
251
|
+
if hasattr(exc, "stdout") and exc.stdout:
|
|
252
|
+
stdout_str = (
|
|
253
|
+
exc.stdout
|
|
254
|
+
if isinstance(exc.stdout, str)
|
|
255
|
+
else exc.stdout.decode("utf-8", errors="replace")
|
|
256
|
+
)
|
|
257
|
+
if hasattr(exc, "stderr") and exc.stderr:
|
|
258
|
+
stderr_str = (
|
|
259
|
+
exc.stderr
|
|
260
|
+
if isinstance(exc.stderr, str)
|
|
261
|
+
else exc.stderr.decode("utf-8", errors="replace")
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"status": "error",
|
|
266
|
+
"error": f"pytest timed out after {timeout} seconds",
|
|
267
|
+
"project_path": str(project_dir),
|
|
268
|
+
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
269
|
+
"stdout": stdout_str,
|
|
270
|
+
"stderr": stderr_str,
|
|
271
|
+
"tests_passed": False,
|
|
272
|
+
"timed_out": True,
|
|
273
|
+
"timeout": timeout,
|
|
274
|
+
"duration_seconds": duration,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
stdout = result.stdout or ""
|
|
278
|
+
stderr = result.stderr or ""
|
|
279
|
+
truncated = False
|
|
280
|
+
max_output = 10_000
|
|
281
|
+
|
|
282
|
+
if len(stdout) > max_output:
|
|
283
|
+
stdout = stdout[:max_output] + "\n...output truncated (stdout)..."
|
|
284
|
+
truncated = True
|
|
285
|
+
|
|
286
|
+
if len(stderr) > max_output:
|
|
287
|
+
stderr = stderr[:max_output] + "\n...output truncated (stderr)..."
|
|
288
|
+
truncated = True
|
|
289
|
+
|
|
290
|
+
# Parse pytest output for failure count
|
|
291
|
+
failure_summary = ""
|
|
292
|
+
if result.returncode != 0:
|
|
293
|
+
# Look for pytest summary line like "7 failed, 75 passed"
|
|
294
|
+
import re
|
|
295
|
+
|
|
296
|
+
summary_match = re.search(r"(\d+)\s+failed", stdout)
|
|
297
|
+
if summary_match:
|
|
298
|
+
num_failed = summary_match.group(1)
|
|
299
|
+
failure_summary = (
|
|
300
|
+
f"{num_failed} test(s) failed - check stdout for details"
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
failure_summary = "Tests failed - check stdout for details"
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"status": "success",
|
|
307
|
+
"project_path": str(project_dir),
|
|
308
|
+
"command": " ".join(shlex.quote(part) for part in cmd),
|
|
309
|
+
"stdout": stdout,
|
|
310
|
+
"stderr": stderr,
|
|
311
|
+
"return_code": result.returncode,
|
|
312
|
+
"tests_passed": result.returncode == 0,
|
|
313
|
+
"failure_summary": (
|
|
314
|
+
failure_summary if not result.returncode == 0 else ""
|
|
315
|
+
),
|
|
316
|
+
"duration_seconds": duration,
|
|
317
|
+
"timeout": timeout,
|
|
318
|
+
"output_truncated": truncated,
|
|
319
|
+
}
|
|
320
|
+
except Exception as exc:
|
|
321
|
+
return {"status": "error", "error": str(exc)}
|