amd-gaia 0.15.0__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.
Files changed (181) hide show
  1. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
  2. amd_gaia-0.15.1.dist-info/RECORD +178 -0
  3. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
  5. gaia/__init__.py +29 -29
  6. gaia/agents/__init__.py +19 -19
  7. gaia/agents/base/__init__.py +9 -9
  8. gaia/agents/base/agent.py +2177 -2177
  9. gaia/agents/base/api_agent.py +120 -120
  10. gaia/agents/base/console.py +1841 -1841
  11. gaia/agents/base/errors.py +237 -237
  12. gaia/agents/base/mcp_agent.py +86 -86
  13. gaia/agents/base/tools.py +83 -83
  14. gaia/agents/blender/agent.py +556 -556
  15. gaia/agents/blender/agent_simple.py +133 -135
  16. gaia/agents/blender/app.py +211 -211
  17. gaia/agents/blender/app_simple.py +41 -41
  18. gaia/agents/blender/core/__init__.py +16 -16
  19. gaia/agents/blender/core/materials.py +506 -506
  20. gaia/agents/blender/core/objects.py +316 -316
  21. gaia/agents/blender/core/rendering.py +225 -225
  22. gaia/agents/blender/core/scene.py +220 -220
  23. gaia/agents/blender/core/view.py +146 -146
  24. gaia/agents/chat/__init__.py +9 -9
  25. gaia/agents/chat/agent.py +835 -835
  26. gaia/agents/chat/app.py +1058 -1058
  27. gaia/agents/chat/session.py +508 -508
  28. gaia/agents/chat/tools/__init__.py +15 -15
  29. gaia/agents/chat/tools/file_tools.py +96 -96
  30. gaia/agents/chat/tools/rag_tools.py +1729 -1729
  31. gaia/agents/chat/tools/shell_tools.py +436 -436
  32. gaia/agents/code/__init__.py +7 -7
  33. gaia/agents/code/agent.py +549 -549
  34. gaia/agents/code/cli.py +377 -0
  35. gaia/agents/code/models.py +135 -135
  36. gaia/agents/code/orchestration/__init__.py +24 -24
  37. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  38. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  39. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  40. gaia/agents/code/orchestration/factories/base.py +63 -63
  41. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  42. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  43. gaia/agents/code/orchestration/orchestrator.py +841 -841
  44. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  45. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  46. gaia/agents/code/orchestration/steps/base.py +188 -188
  47. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  48. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  49. gaia/agents/code/orchestration/steps/python.py +307 -307
  50. gaia/agents/code/orchestration/template_catalog.py +469 -469
  51. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  52. gaia/agents/code/orchestration/workflows/base.py +80 -80
  53. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  54. gaia/agents/code/orchestration/workflows/python.py +94 -94
  55. gaia/agents/code/prompts/__init__.py +11 -11
  56. gaia/agents/code/prompts/base_prompt.py +77 -77
  57. gaia/agents/code/prompts/code_patterns.py +2036 -2036
  58. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  59. gaia/agents/code/prompts/python_prompt.py +109 -109
  60. gaia/agents/code/schema_inference.py +365 -365
  61. gaia/agents/code/system_prompt.py +41 -41
  62. gaia/agents/code/tools/__init__.py +42 -42
  63. gaia/agents/code/tools/cli_tools.py +1138 -1138
  64. gaia/agents/code/tools/code_formatting.py +319 -319
  65. gaia/agents/code/tools/code_tools.py +769 -769
  66. gaia/agents/code/tools/error_fixing.py +1347 -1347
  67. gaia/agents/code/tools/external_tools.py +180 -180
  68. gaia/agents/code/tools/file_io.py +845 -845
  69. gaia/agents/code/tools/prisma_tools.py +190 -190
  70. gaia/agents/code/tools/project_management.py +1016 -1016
  71. gaia/agents/code/tools/testing.py +321 -321
  72. gaia/agents/code/tools/typescript_tools.py +122 -122
  73. gaia/agents/code/tools/validation_parsing.py +461 -461
  74. gaia/agents/code/tools/validation_tools.py +806 -806
  75. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  76. gaia/agents/code/validators/__init__.py +16 -16
  77. gaia/agents/code/validators/antipattern_checker.py +241 -241
  78. gaia/agents/code/validators/ast_analyzer.py +197 -197
  79. gaia/agents/code/validators/requirements_validator.py +145 -145
  80. gaia/agents/code/validators/syntax_validator.py +171 -171
  81. gaia/agents/docker/__init__.py +7 -7
  82. gaia/agents/docker/agent.py +642 -642
  83. gaia/agents/emr/__init__.py +8 -8
  84. gaia/agents/emr/agent.py +1506 -1506
  85. gaia/agents/emr/cli.py +1322 -1322
  86. gaia/agents/emr/constants.py +475 -475
  87. gaia/agents/emr/dashboard/__init__.py +4 -4
  88. gaia/agents/emr/dashboard/server.py +1974 -1974
  89. gaia/agents/jira/__init__.py +11 -11
  90. gaia/agents/jira/agent.py +894 -894
  91. gaia/agents/jira/jql_templates.py +299 -299
  92. gaia/agents/routing/__init__.py +7 -7
  93. gaia/agents/routing/agent.py +567 -570
  94. gaia/agents/routing/system_prompt.py +75 -75
  95. gaia/agents/summarize/__init__.py +11 -0
  96. gaia/agents/summarize/agent.py +885 -0
  97. gaia/agents/summarize/prompts.py +129 -0
  98. gaia/api/__init__.py +23 -23
  99. gaia/api/agent_registry.py +238 -238
  100. gaia/api/app.py +305 -305
  101. gaia/api/openai_server.py +575 -575
  102. gaia/api/schemas.py +186 -186
  103. gaia/api/sse_handler.py +373 -373
  104. gaia/apps/__init__.py +4 -4
  105. gaia/apps/llm/__init__.py +6 -6
  106. gaia/apps/llm/app.py +173 -169
  107. gaia/apps/summarize/app.py +116 -633
  108. gaia/apps/summarize/html_viewer.py +133 -133
  109. gaia/apps/summarize/pdf_formatter.py +284 -284
  110. gaia/audio/__init__.py +2 -2
  111. gaia/audio/audio_client.py +439 -439
  112. gaia/audio/audio_recorder.py +269 -269
  113. gaia/audio/kokoro_tts.py +599 -599
  114. gaia/audio/whisper_asr.py +432 -432
  115. gaia/chat/__init__.py +16 -16
  116. gaia/chat/app.py +430 -430
  117. gaia/chat/prompts.py +522 -522
  118. gaia/chat/sdk.py +1228 -1225
  119. gaia/cli.py +5481 -5632
  120. gaia/database/__init__.py +10 -10
  121. gaia/database/agent.py +176 -176
  122. gaia/database/mixin.py +290 -290
  123. gaia/database/testing.py +64 -64
  124. gaia/eval/batch_experiment.py +2332 -2332
  125. gaia/eval/claude.py +542 -542
  126. gaia/eval/config.py +37 -37
  127. gaia/eval/email_generator.py +512 -512
  128. gaia/eval/eval.py +3179 -3179
  129. gaia/eval/groundtruth.py +1130 -1130
  130. gaia/eval/transcript_generator.py +582 -582
  131. gaia/eval/webapp/README.md +167 -167
  132. gaia/eval/webapp/package-lock.json +875 -875
  133. gaia/eval/webapp/package.json +20 -20
  134. gaia/eval/webapp/public/app.js +3402 -3402
  135. gaia/eval/webapp/public/index.html +87 -87
  136. gaia/eval/webapp/public/styles.css +3661 -3661
  137. gaia/eval/webapp/server.js +415 -415
  138. gaia/eval/webapp/test-setup.js +72 -72
  139. gaia/llm/__init__.py +9 -2
  140. gaia/llm/base_client.py +60 -0
  141. gaia/llm/exceptions.py +12 -0
  142. gaia/llm/factory.py +70 -0
  143. gaia/llm/lemonade_client.py +3236 -3221
  144. gaia/llm/lemonade_manager.py +294 -294
  145. gaia/llm/providers/__init__.py +9 -0
  146. gaia/llm/providers/claude.py +108 -0
  147. gaia/llm/providers/lemonade.py +120 -0
  148. gaia/llm/providers/openai_provider.py +79 -0
  149. gaia/llm/vlm_client.py +382 -382
  150. gaia/logger.py +189 -189
  151. gaia/mcp/agent_mcp_server.py +245 -245
  152. gaia/mcp/blender_mcp_client.py +138 -138
  153. gaia/mcp/blender_mcp_server.py +648 -648
  154. gaia/mcp/context7_cache.py +332 -332
  155. gaia/mcp/external_services.py +518 -518
  156. gaia/mcp/mcp_bridge.py +811 -550
  157. gaia/mcp/servers/__init__.py +6 -6
  158. gaia/mcp/servers/docker_mcp.py +83 -83
  159. gaia/perf_analysis.py +361 -0
  160. gaia/rag/__init__.py +10 -10
  161. gaia/rag/app.py +293 -293
  162. gaia/rag/demo.py +304 -304
  163. gaia/rag/pdf_utils.py +235 -235
  164. gaia/rag/sdk.py +2194 -2194
  165. gaia/security.py +163 -163
  166. gaia/talk/app.py +289 -289
  167. gaia/talk/sdk.py +538 -538
  168. gaia/testing/__init__.py +87 -87
  169. gaia/testing/assertions.py +330 -330
  170. gaia/testing/fixtures.py +333 -333
  171. gaia/testing/mocks.py +493 -493
  172. gaia/util.py +46 -46
  173. gaia/utils/__init__.py +33 -33
  174. gaia/utils/file_watcher.py +675 -675
  175. gaia/utils/parsing.py +223 -223
  176. gaia/version.py +100 -100
  177. amd_gaia-0.15.0.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -723
  180. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,806 +1,806 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
- """Validation tools for Code Agent.
4
-
5
- This module provides tools for testing and validating generated applications.
6
- Uses curl-based testing to avoid temporary files and complex setup.
7
- """
8
-
9
- import json
10
- import logging
11
- import os
12
- import subprocess
13
- import time
14
- from pathlib import Path
15
- from typing import Any, Dict
16
-
17
- from gaia.agents.base.tools import tool
18
- from gaia.agents.code.prompts.code_patterns import pluralize
19
- from gaia.agents.code.tools.cli_tools import is_port_available
20
- from gaia.agents.code.tools.web_dev_tools import read_prisma_model
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- def generate_test_payload(fields: Dict[str, str]) -> Dict[str, Any]:
26
- """Generate test payload data from field definitions.
27
-
28
- Creates appropriate test values for each field type.
29
- This is used by test_crud_api to generate realistic test data
30
- based on the actual Prisma schema.
31
-
32
- Args:
33
- fields: Dictionary mapping field names to their types
34
-
35
- Returns:
36
- Dictionary with field names mapped to test values
37
- """
38
- payload = {}
39
- for field_name, field_type in fields.items():
40
- # Skip auto-generated fields
41
- if field_name.lower() in {"id", "createdat", "updatedat"}:
42
- continue
43
-
44
- # Generate appropriate test values based on type
45
- field_type_lower = field_type.lower()
46
- if field_type_lower in ("string", "text"):
47
- payload[field_name] = f"Test {field_name.replace('_', ' ').title()}"
48
- elif field_type_lower in ("int", "number", "integer"):
49
- payload[field_name] = 42
50
- elif field_type_lower == "float":
51
- payload[field_name] = 3.14
52
- elif field_type_lower == "boolean":
53
- # Default to false for most boolean fields, true for common patterns
54
- if field_name.lower() in ("active", "enabled", "visible"):
55
- payload[field_name] = True
56
- else:
57
- payload[field_name] = False
58
- elif field_type_lower in ("datetime", "date", "timestamp"):
59
- payload[field_name] = "2025-01-01T00:00:00.000Z"
60
- else:
61
- # Default to string for unknown types
62
- payload[field_name] = f"Test {field_name}"
63
-
64
- return payload
65
-
66
-
67
- class ValidationToolsMixin:
68
- """Mixin providing validation and testing tools for the Code Agent."""
69
-
70
- def register_validation_tools(self) -> None:
71
- """Register validation tools with the agent."""
72
-
73
- @tool
74
- def test_crud_api(
75
- project_dir: str, model_name: str, port: int = 3000
76
- ) -> Dict[str, Any]:
77
- """Test CRUD API endpoints using curl.
78
-
79
- Validates that all CRUD operations work correctly by:
80
- - Creating a test record (POST)
81
- - Listing all records (GET list)
82
- - Getting single record (GET single)
83
- - Updating record (PATCH)
84
- - Deleting record (DELETE)
85
-
86
- Ensures dev server is running before testing.
87
-
88
- Args:
89
- project_dir: Path to the Next.js project directory
90
- model_name: Model name to test (e.g., "Todo", "Post")
91
- port: Port where dev server is running (default: 3000)
92
-
93
- Returns:
94
- {
95
- "success": bool,
96
- "result": {
97
- "tests_passed": int,
98
- "tests_failed": int,
99
- "results": {
100
- "POST": {"status": int, "pass": bool},
101
- "GET_LIST": {"status": int, "pass": bool},
102
- "GET_SINGLE": {"status": int, "pass": bool},
103
- "PATCH": {"status": int, "pass": bool},
104
- "DELETE": {"status": int, "pass": bool}
105
- }
106
- }
107
- }
108
-
109
- On error:
110
- {
111
- "success": False,
112
- "error": str,
113
- "error_type": "endpoint_missing" | "validation_error" | "database_error" | "runtime_error"
114
- }
115
- """
116
- server_started_by_us = False
117
- server_pid = None
118
-
119
- try:
120
- logger.info(f"Testing CRUD API for {model_name}")
121
-
122
- # Check if dev server is running (port in use = server running)
123
- server_was_running = not is_port_available(port)
124
-
125
- if not server_was_running:
126
- logger.info(f"Dev server not running on port {port}, starting...")
127
-
128
- # Start dev server in background using CLI tools mixin
129
- start_result = self._run_background_command(
130
- command="npm run dev",
131
- work_path=Path(project_dir),
132
- startup_timeout=15,
133
- expected_port=port,
134
- auto_respond="",
135
- )
136
-
137
- if not start_result.get("success"):
138
- return {
139
- "success": False,
140
- "error": "Failed to start dev server",
141
- "details": start_result,
142
- "error_type": "runtime_error",
143
- }
144
-
145
- server_started_by_us = True
146
- server_pid = start_result.get("pid")
147
- logger.info(f"Dev server started with PID {server_pid}")
148
-
149
- # Give Next.js a moment to fully initialize
150
- time.sleep(3)
151
- else:
152
- logger.info(f"Dev server already running on port {port}")
153
-
154
- base_url = f"http://localhost:{port}"
155
- resource = model_name.lower()
156
- resource_plural = pluralize(resource)
157
- api_url = f"{base_url}/api/{resource_plural}"
158
-
159
- # Phase 2 Fix (Issue #885): Read schema to get actual fields
160
- # instead of using hardcoded {"name": "Test Item"}
161
- model_info = read_prisma_model(project_dir, model_name)
162
- if model_info["success"]:
163
- # Convert Prisma types to our field types
164
- prisma_to_field_type = {
165
- "String": "string",
166
- "Int": "number",
167
- "Float": "float",
168
- "Boolean": "boolean",
169
- "DateTime": "datetime",
170
- }
171
- schema_fields = {}
172
- for field_name, prisma_type in model_info["fields"].items():
173
- if field_name.lower() not in {"id", "createdat", "updatedat"}:
174
- schema_fields[field_name] = prisma_to_field_type.get(
175
- prisma_type, "string"
176
- )
177
- test_payload = generate_test_payload(schema_fields)
178
- logger.info(f"Generated test payload from schema: {test_payload}")
179
- else:
180
- # Fallback: Use generic test payload but log warning
181
- logger.warning(
182
- f"Could not read Prisma schema for {model_name}, "
183
- f"using generic test payload. Error: {model_info.get('error')}"
184
- )
185
- test_payload = {
186
- "title": "Test Item",
187
- "description": "Test description",
188
- }
189
-
190
- # Escape the payload for shell
191
- test_payload_json = json.dumps(test_payload)
192
-
193
- results = {}
194
- created_id = None
195
-
196
- # Test 1: POST (create)
197
- logger.info("Testing POST (create)...")
198
- post_result = self._run_foreground_command(
199
- command=(
200
- f"curl -s -w '\\n%{{http_code}}' -X POST "
201
- f"-H 'Content-Type: application/json' "
202
- f"-d '{test_payload_json}' '{api_url}'"
203
- ),
204
- work_path=Path(project_dir) if project_dir else Path.cwd(),
205
- timeout=10,
206
- auto_respond="y\n",
207
- )
208
-
209
- if post_result.get("status") == "success":
210
- output = post_result.get("stdout", "")
211
- lines = output.strip().split("\n")
212
- status_code = int(lines[-1]) if lines else 0
213
- results["POST"] = {
214
- "status": status_code,
215
- "pass": status_code == 201,
216
- }
217
-
218
- # Extract created ID from response
219
- if status_code == 201 and len(lines) > 1:
220
- try:
221
- response_data = json.loads(lines[0])
222
- created_id = response_data.get("id")
223
- except Exception:
224
- logger.warning(
225
- "Could not parse POST response to extract ID"
226
- )
227
- else:
228
- results["POST"] = {
229
- "status": 0,
230
- "pass": False,
231
- "error": "Command failed",
232
- }
233
-
234
- # Test 2: GET (list)
235
- logger.info("Testing GET (list)...")
236
- get_list_result = self._run_foreground_command(
237
- command=f"curl -s -w '\\n%{{http_code}}' '{api_url}'",
238
- work_path=Path(project_dir) if project_dir else Path.cwd(),
239
- timeout=10,
240
- auto_respond="y\n",
241
- )
242
-
243
- if get_list_result.get("status") == "success":
244
- output = get_list_result.get("stdout", "")
245
- lines = output.strip().split("\n")
246
- status_code = int(lines[-1]) if lines else 0
247
- results["GET_LIST"] = {
248
- "status": status_code,
249
- "pass": status_code == 200,
250
- }
251
- else:
252
- results["GET_LIST"] = {
253
- "status": 0,
254
- "pass": False,
255
- "error": "Command failed",
256
- }
257
-
258
- # Test 3: GET (single) - only if we have an ID
259
- if created_id:
260
- logger.info(f"Testing GET (single) with ID {created_id}...")
261
- get_single_result = self._run_foreground_command(
262
- command=f"curl -s -w '\\n%{{http_code}}' '{api_url}/{created_id}'",
263
- work_path=Path(project_dir) if project_dir else Path.cwd(),
264
- timeout=10,
265
- auto_respond="y\n",
266
- )
267
-
268
- if get_single_result.get("status") == "success":
269
- output = get_single_result.get("stdout", "")
270
- lines = output.strip().split("\n")
271
- status_code = int(lines[-1]) if lines else 0
272
- results["GET_SINGLE"] = {
273
- "status": status_code,
274
- "pass": status_code == 200,
275
- }
276
- else:
277
- results["GET_SINGLE"] = {
278
- "status": 0,
279
- "pass": False,
280
- "error": "Command failed",
281
- }
282
- else:
283
- results["GET_SINGLE"] = {
284
- "status": 0,
285
- "pass": False,
286
- "error": "No ID to test",
287
- }
288
-
289
- # Test 4: PATCH (update) - only if we have an ID
290
- if created_id:
291
- logger.info(f"Testing PATCH (update) with ID {created_id}...")
292
- # Generate update payload - modify the first string field
293
- update_payload = {}
294
- for key, value in test_payload.items():
295
- if isinstance(value, str):
296
- update_payload[key] = f"Updated {value}"
297
- break
298
- if not update_payload:
299
- # Fallback: use first field with "Updated" prefix
300
- first_key = next(iter(test_payload), None)
301
- if first_key:
302
- update_payload[first_key] = "Updated Value"
303
- update_payload_json = json.dumps(update_payload)
304
-
305
- patch_result = self._run_foreground_command(
306
- command=(
307
- f"curl -s -w '\\n%{{http_code}}' -X PATCH "
308
- f"-H 'Content-Type: application/json' "
309
- f"-d '{update_payload_json}' '{api_url}/{created_id}'"
310
- ),
311
- work_path=Path(project_dir) if project_dir else Path.cwd(),
312
- timeout=10,
313
- auto_respond="y\n",
314
- )
315
-
316
- if patch_result.get("status") == "success":
317
- output = patch_result.get("stdout", "")
318
- lines = output.strip().split("\n")
319
- status_code = int(lines[-1]) if lines else 0
320
- results["PATCH"] = {
321
- "status": status_code,
322
- "pass": status_code == 200,
323
- }
324
- else:
325
- results["PATCH"] = {
326
- "status": 0,
327
- "pass": False,
328
- "error": "Command failed",
329
- }
330
- else:
331
- results["PATCH"] = {
332
- "status": 0,
333
- "pass": False,
334
- "error": "No ID to test",
335
- }
336
-
337
- # Test 5: DELETE - only if we have an ID
338
- if created_id:
339
- logger.info(f"Testing DELETE with ID {created_id}...")
340
- delete_result = self._run_foreground_command(
341
- command=f"curl -s -w '\\n%{{http_code}}' -X DELETE '{api_url}/{created_id}'",
342
- work_path=Path(project_dir) if project_dir else Path.cwd(),
343
- timeout=10,
344
- auto_respond="y\n",
345
- )
346
-
347
- if delete_result.get("status") == "success":
348
- output = delete_result.get("stdout", "")
349
- lines = output.strip().split("\n")
350
- status_code = int(lines[-1]) if lines else 0
351
- results["DELETE"] = {
352
- "status": status_code,
353
- "pass": status_code == 200,
354
- }
355
- else:
356
- results["DELETE"] = {
357
- "status": 0,
358
- "pass": False,
359
- "error": "Command failed",
360
- }
361
- else:
362
- results["DELETE"] = {
363
- "status": 0,
364
- "pass": False,
365
- "error": "No ID to test",
366
- }
367
-
368
- # Calculate summary
369
- passed = sum(1 for r in results.values() if r.get("pass", False))
370
- failed = len(results) - passed
371
-
372
- logger.info(f"Tests completed: {passed} passed, {failed} failed")
373
-
374
- return {
375
- "success": passed == len(results),
376
- "result": {
377
- "tests_passed": passed,
378
- "tests_failed": failed,
379
- "results": results,
380
- },
381
- }
382
-
383
- except Exception as e:
384
- logger.error(f"Error in test_crud_api: {e}", exc_info=True)
385
- return {
386
- "success": False,
387
- "error": str(e),
388
- "error_type": "runtime_error",
389
- }
390
-
391
- finally:
392
- # Clean up: stop dev server if we started it
393
- if server_started_by_us and server_pid:
394
- logger.info(f"Stopping dev server (PID {server_pid})...")
395
- try:
396
- self._stop_process(server_pid, force=False)
397
- logger.info("Dev server stopped successfully")
398
- except Exception as cleanup_error:
399
- logger.warning(f"Error stopping dev server: {cleanup_error}")
400
-
401
- @tool
402
- def validate_typescript(project_dir: str) -> Dict[str, Any]:
403
- """Validate TypeScript code before declaring success.
404
-
405
- Runs TypeScript compiler in no-emit mode to check for type errors
406
- without generating output files. This is the ultimate guardrail to
407
- catch import errors, missing types, and other TypeScript issues
408
- before they reach npm run build.
409
-
410
- TIER 4 ERROR MESSAGING: When validation fails, this tool returns
411
- specific rule citations to teach the LLM what went wrong.
412
-
413
- Args:
414
- project_dir: Path to the Next.js project directory
415
-
416
- Returns:
417
- On success:
418
- {
419
- "success": True,
420
- "message": "TypeScript validation passed"
421
- }
422
-
423
- On failure:
424
- {
425
- "success": False,
426
- "error": "TypeScript validation failed",
427
- "errors": str, # Full tsc error output
428
- "rule": str, # Which rule was violated
429
- "violation": str, # Specific violation
430
- "fix": str, # How to fix it
431
- "hint": "Fix the type errors listed above, then run validate_typescript again"
432
- }
433
- """
434
- try:
435
- logger.debug(f"Validating TypeScript in {project_dir}")
436
-
437
- npx_command = "npx.cmd" if os.name == "nt" else "npx"
438
-
439
- result = subprocess.run(
440
- [npx_command, "tsc", "--noEmit", "--skipLibCheck"],
441
- cwd=project_dir,
442
- capture_output=True,
443
- text=True,
444
- timeout=60,
445
- check=False,
446
- )
447
-
448
- if result.returncode == 0:
449
- logger.info("TypeScript validation passed")
450
- return {
451
- "success": True,
452
- "message": "TypeScript validation passed - no type errors found",
453
- }
454
-
455
- # Parse errors to provide specific guidance
456
- error_output = result.stderr if result.stderr else result.stdout
457
- logger.warning(f"TypeScript validation failed:\n{error_output}")
458
-
459
- # Detect common error patterns and provide rule citations
460
- rule = None
461
- violation = None
462
- fix = None
463
-
464
- if "Cannot find name" in error_output:
465
- violation = "Missing type import"
466
- rule = "Client components must use: import type { X } from '@prisma/client'"
467
- fix = "Add the missing type import at the top of the file"
468
- elif "Cannot find module '@/lib/prisma'" in error_output:
469
- violation = "Missing prisma singleton"
470
- rule = "Server components must import: import { prisma } from '@/lib/prisma'"
471
- fix = "Ensure src/lib/prisma.ts exists with the Prisma singleton"
472
- elif (
473
- "Module '\"@prisma/client\"' has no exported member" in error_output
474
- ):
475
- violation = "Prisma types not generated"
476
- rule = "Run prisma generate after schema changes"
477
- fix = "Run: npx prisma generate"
478
- elif (
479
- "'prisma' is not defined" in error_output
480
- or "Cannot find name 'prisma'" in error_output
481
- ):
482
- violation = "Direct prisma import in client component"
483
- rule = "NEVER import prisma client directly in client components"
484
- fix = "Use API routes for database access from client components"
485
-
486
- response = {
487
- "success": False,
488
- "error": "TypeScript validation failed",
489
- "errors": error_output,
490
- "hint": "Fix the type errors listed above, then run validate_typescript again",
491
- }
492
-
493
- # Add Tier 4 teaching if we detected a specific violation
494
- if rule and violation and fix:
495
- response.update({"violation": violation, "rule": rule, "fix": fix})
496
-
497
- return response
498
-
499
- except subprocess.TimeoutExpired:
500
- logger.error("TypeScript validation timed out")
501
- return {
502
- "success": False,
503
- "error": "TypeScript validation timed out after 60 seconds",
504
- "hint": "Check for infinite type recursion or very large project",
505
- }
506
- except FileNotFoundError:
507
- logger.error("TypeScript compiler not found")
508
- return {
509
- "success": False,
510
- "error": "TypeScript compiler (tsc) not found",
511
- "hint": "Ensure TypeScript is installed: npm install --save-dev typescript",
512
- }
513
- except Exception as e:
514
- logger.error(f"Error in validate_typescript: {e}", exc_info=True)
515
- return {
516
- "success": False,
517
- "error": f"Validation error: {str(e)}",
518
- "hint": "Check that project_dir is valid and contains a Next.js project",
519
- }
520
-
521
- @tool
522
- def validate_crud_structure(
523
- project_dir: str, resource_name: str
524
- ) -> Dict[str, Any]:
525
- """Validate that all required CRUD files exist before declaring success.
526
-
527
- Checks for a complete CRUD application structure including:
528
- - List page, New page, Detail page
529
- - Form component, Actions component
530
- - API routes (collection and item)
531
-
532
- This should be called AFTER building a CRUD app to ensure nothing was skipped.
533
-
534
- Args:
535
- project_dir: Path to the Next.js project directory
536
- resource_name: Resource name in singular form (e.g., "todo", "post")
537
-
538
- Returns:
539
- Dictionary with:
540
- - success: bool
541
- - missing_files: list of missing file paths
542
- - message: summary of validation result
543
- """
544
- try:
545
- project_path = Path(project_dir)
546
- resource_plural = resource_name.lower() + "s" # Simple pluralization
547
- resource_capitalized = resource_name.capitalize()
548
-
549
- # Define all required files for a complete CRUD app
550
- required_files = {
551
- "List page": f"src/app/{resource_plural}/page.tsx",
552
- "New page": f"src/app/{resource_plural}/new/page.tsx",
553
- "Detail page": f"src/app/{resource_plural}/[id]/page.tsx",
554
- "Form component": f"src/components/{resource_capitalized}Form.tsx",
555
- "Actions component": f"src/components/{resource_capitalized}Actions.tsx",
556
- "Collection API": f"src/app/api/{resource_plural}/route.ts",
557
- "Item API": f"src/app/api/{resource_plural}/[id]/route.ts",
558
- }
559
-
560
- missing_files = []
561
- existing_files = []
562
-
563
- for description, file_path in required_files.items():
564
- full_path = project_path / file_path
565
- if full_path.exists():
566
- existing_files.append(f"✅ {description}: {file_path}")
567
- else:
568
- missing_files.append(
569
- {
570
- "description": description,
571
- "path": file_path,
572
- "create_with": self._get_create_command(
573
- description, resource_name
574
- ),
575
- }
576
- )
577
-
578
- if not missing_files:
579
- logger.info(f"CRUD structure validation passed for {resource_name}")
580
- return {
581
- "success": True,
582
- "message": f"Complete CRUD structure validated for {resource_name}",
583
- "existing_files": existing_files,
584
- }
585
-
586
- # Build detailed error message with fix instructions
587
- error_details = f"Missing {len(missing_files)} required file(s) for {resource_name} CRUD app:\n\n"
588
- for item in missing_files:
589
- error_details += f"❌ {item['description']}: {item['path']}\n"
590
- error_details += f" Fix: {item['create_with']}\n\n"
591
-
592
- logger.warning(
593
- f"CRUD structure validation failed: {len(missing_files)} files missing"
594
- )
595
-
596
- return {
597
- "success": False,
598
- "error": f"Incomplete CRUD structure: {len(missing_files)} file(s) missing",
599
- "missing_files": missing_files,
600
- "existing_files": existing_files,
601
- "details": error_details,
602
- "hint": "Create the missing files using the fix commands listed above",
603
- }
604
-
605
- except Exception as e:
606
- logger.error(f"Error in validate_crud_structure: {e}", exc_info=True)
607
- return {
608
- "success": False,
609
- "error": f"Validation error: {str(e)}",
610
- "hint": "Check that project_dir is valid and resource_name is correct",
611
- }
612
-
613
- @tool
614
- def validate_styles(
615
- project_dir: str, _resource_name: str = None
616
- ) -> Dict[str, Any]:
617
- """Validate CSS files and design system consistency.
618
-
619
- This tool validates:
620
- 1. CSS files contain valid CSS (not TypeScript/JavaScript) - CRITICAL
621
- 2. globals.css has Tailwind directives
622
- 3. layout.tsx imports globals.css
623
- 4. (Optional) Custom classes used in components are defined
624
-
625
- Addresses Issue #1002: CSS file written with TypeScript code.
626
-
627
- Args:
628
- project_dir: Path to the Next.js project directory
629
- resource_name: Optional resource name for component class checks
630
-
631
- Returns:
632
- On success:
633
- {
634
- "success": True,
635
- "is_valid": True,
636
- "message": "Styling validated successfully",
637
- "files_checked": [list of files]
638
- }
639
-
640
- On failure:
641
- {
642
- "success": False,
643
- "is_valid": False,
644
- "errors": [list of CRITICAL errors],
645
- "warnings": [list of warnings],
646
- "hint": "How to fix"
647
- }
648
- """
649
- import re
650
-
651
- try:
652
- project_path = Path(project_dir)
653
- errors = []
654
- warnings = []
655
- files_checked = []
656
-
657
- # 1. Check globals.css for TypeScript content (CRITICAL)
658
- globals_css = project_path / "src" / "app" / "globals.css"
659
- if globals_css.exists():
660
- files_checked.append("src/app/globals.css")
661
- content = globals_css.read_text()
662
-
663
- # TypeScript/JavaScript detection patterns
664
- typescript_indicators = [
665
- (r"^\s*import\s+.*from", "import statement"),
666
- (
667
- r"^\s*export\s+(default|const|function|class|async)",
668
- "export statement",
669
- ),
670
- (r'"use client"|\'use client\'', "React client directive"),
671
- (r"^\s*interface\s+\w+", "TypeScript interface"),
672
- (r"^\s*type\s+\w+\s*=", "TypeScript type alias"),
673
- (r"^\s*const\s+\w+\s*[=:]", "const declaration"),
674
- (r"^\s*let\s+\w+\s*[=:]", "let declaration"),
675
- (r"^\s*function\s+\w+", "function declaration"),
676
- (r"^\s*async\s+function", "async function"),
677
- (r"<[A-Z][a-zA-Z]*[\s/>]", "JSX component tag"),
678
- (r"useState|useEffect|useRouter|usePathname", "React hook"),
679
- ]
680
-
681
- for pattern, description in typescript_indicators:
682
- if re.search(pattern, content, re.MULTILINE):
683
- errors.append(
684
- f"CRITICAL: globals.css contains {description}. "
685
- f"This file has TypeScript/JSX code instead of CSS."
686
- )
687
-
688
- # Check for balanced braces
689
- if content.count("{") != content.count("}"):
690
- errors.append("globals.css has mismatched braces")
691
-
692
- # Check for Tailwind directives
693
- has_tailwind = (
694
- "@tailwind" in content or '@import "tailwindcss' in content
695
- )
696
- if not has_tailwind and len(content.strip()) > 50:
697
- warnings.append(
698
- "globals.css is missing Tailwind directives "
699
- "(@tailwind base/components/utilities)"
700
- )
701
- else:
702
- errors.append("globals.css not found at src/app/globals.css")
703
-
704
- # 2. Check layout.tsx imports globals.css
705
- layout_tsx = project_path / "src" / "app" / "layout.tsx"
706
- if layout_tsx.exists():
707
- files_checked.append("src/app/layout.tsx")
708
- layout_content = layout_tsx.read_text()
709
-
710
- # Check for globals.css import
711
- globals_import = (
712
- './globals.css"' in layout_content
713
- or "./globals.css'" in layout_content
714
- or "@/app/globals.css" in layout_content
715
- )
716
- if not globals_import:
717
- warnings.append(
718
- "layout.tsx does not import globals.css. "
719
- "Global styles may not be applied to pages."
720
- )
721
- else:
722
- warnings.append("layout.tsx not found at src/app/layout.tsx")
723
-
724
- # 3. Check all CSS files for TypeScript content
725
- for css_file in project_path.glob("**/*.css"):
726
- if css_file == globals_css:
727
- continue # Already checked
728
-
729
- files_checked.append(str(css_file.relative_to(project_path)))
730
- css_content = css_file.read_text()
731
-
732
- # Quick check for obvious TypeScript patterns
733
- if re.search(r"^\s*import\s+", css_content, re.MULTILINE):
734
- errors.append(
735
- f"CRITICAL: {css_file.name} contains import statement. "
736
- f"This is TypeScript, not CSS."
737
- )
738
- if re.search(r"^\s*export\s+", css_content, re.MULTILINE):
739
- errors.append(
740
- f"CRITICAL: {css_file.name} contains export statement. "
741
- f"This is TypeScript, not CSS."
742
- )
743
-
744
- # Build result
745
- is_valid = len(errors) == 0
746
- if is_valid:
747
- logger.info("Styling validation passed")
748
- result = {
749
- "success": True,
750
- "is_valid": True,
751
- "message": "Styling validated successfully",
752
- "files_checked": files_checked,
753
- }
754
- if warnings:
755
- result["warnings"] = warnings
756
- return result
757
-
758
- logger.warning(f"Styling validation failed: {errors}")
759
- return {
760
- "success": False,
761
- "is_valid": False,
762
- "errors": errors,
763
- "warnings": warnings,
764
- "files_checked": files_checked,
765
- "hint": (
766
- "CRITICAL errors indicate CSS files contain TypeScript code. "
767
- "Regenerate globals.css with valid CSS content including "
768
- "Tailwind directives (@tailwind base/components/utilities)."
769
- ),
770
- }
771
-
772
- except Exception as e:
773
- logger.error(f"Error in validate_styles: {e}", exc_info=True)
774
- return {
775
- "success": False,
776
- "is_valid": False,
777
- "error": f"Validation error: {str(e)}",
778
- "hint": "Check that project_dir is valid and contains a Next.js project",
779
- }
780
-
781
- def _get_create_command(self, description: str, resource_name: str) -> str:
782
- """Get the tool command needed to create a missing file.
783
-
784
- This is a helper method used by validate_crud_structure to provide
785
- fix instructions for missing CRUD files.
786
-
787
- Args:
788
- description: Human-readable description of the file type
789
- resource_name: Singular resource name (e.g., "todo", "post")
790
-
791
- Returns:
792
- Tool command string to create the missing file
793
- """
794
- if "Detail page" in description:
795
- return f'manage_react_component(variant="detail", resource_name="{resource_name}")'
796
- elif "Actions component" in description:
797
- return f'manage_react_component(variant="actions", resource_name="{resource_name}")'
798
- elif "List page" in description:
799
- return f'manage_react_component(variant="list", resource_name="{resource_name}")'
800
- elif "New page" in description:
801
- return f'manage_react_component(variant="new", resource_name="{resource_name}")'
802
- elif "Form component" in description:
803
- return f'manage_react_component(variant="form", resource_name="{resource_name}")'
804
- elif "API" in description:
805
- return f'manage_api_endpoint(resource_name="{resource_name}", operations=["GET", "POST", "PATCH", "DELETE"])'
806
- return "Unknown - check documentation"
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """Validation tools for Code Agent.
4
+
5
+ This module provides tools for testing and validating generated applications.
6
+ Uses curl-based testing to avoid temporary files and complex setup.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import os
12
+ import subprocess
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Any, Dict
16
+
17
+ from gaia.agents.base.tools import tool
18
+ from gaia.agents.code.prompts.code_patterns import pluralize
19
+ from gaia.agents.code.tools.cli_tools import is_port_available
20
+ from gaia.agents.code.tools.web_dev_tools import read_prisma_model
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def generate_test_payload(fields: Dict[str, str]) -> Dict[str, Any]:
26
+ """Generate test payload data from field definitions.
27
+
28
+ Creates appropriate test values for each field type.
29
+ This is used by test_crud_api to generate realistic test data
30
+ based on the actual Prisma schema.
31
+
32
+ Args:
33
+ fields: Dictionary mapping field names to their types
34
+
35
+ Returns:
36
+ Dictionary with field names mapped to test values
37
+ """
38
+ payload = {}
39
+ for field_name, field_type in fields.items():
40
+ # Skip auto-generated fields
41
+ if field_name.lower() in {"id", "createdat", "updatedat"}:
42
+ continue
43
+
44
+ # Generate appropriate test values based on type
45
+ field_type_lower = field_type.lower()
46
+ if field_type_lower in ("string", "text"):
47
+ payload[field_name] = f"Test {field_name.replace('_', ' ').title()}"
48
+ elif field_type_lower in ("int", "number", "integer"):
49
+ payload[field_name] = 42
50
+ elif field_type_lower == "float":
51
+ payload[field_name] = 3.14
52
+ elif field_type_lower == "boolean":
53
+ # Default to false for most boolean fields, true for common patterns
54
+ if field_name.lower() in ("active", "enabled", "visible"):
55
+ payload[field_name] = True
56
+ else:
57
+ payload[field_name] = False
58
+ elif field_type_lower in ("datetime", "date", "timestamp"):
59
+ payload[field_name] = "2025-01-01T00:00:00.000Z"
60
+ else:
61
+ # Default to string for unknown types
62
+ payload[field_name] = f"Test {field_name}"
63
+
64
+ return payload
65
+
66
+
67
+ class ValidationToolsMixin:
68
+ """Mixin providing validation and testing tools for the Code Agent."""
69
+
70
+ def register_validation_tools(self) -> None:
71
+ """Register validation tools with the agent."""
72
+
73
+ @tool
74
+ def test_crud_api(
75
+ project_dir: str, model_name: str, port: int = 3000
76
+ ) -> Dict[str, Any]:
77
+ """Test CRUD API endpoints using curl.
78
+
79
+ Validates that all CRUD operations work correctly by:
80
+ - Creating a test record (POST)
81
+ - Listing all records (GET list)
82
+ - Getting single record (GET single)
83
+ - Updating record (PATCH)
84
+ - Deleting record (DELETE)
85
+
86
+ Ensures dev server is running before testing.
87
+
88
+ Args:
89
+ project_dir: Path to the Next.js project directory
90
+ model_name: Model name to test (e.g., "Todo", "Post")
91
+ port: Port where dev server is running (default: 3000)
92
+
93
+ Returns:
94
+ {
95
+ "success": bool,
96
+ "result": {
97
+ "tests_passed": int,
98
+ "tests_failed": int,
99
+ "results": {
100
+ "POST": {"status": int, "pass": bool},
101
+ "GET_LIST": {"status": int, "pass": bool},
102
+ "GET_SINGLE": {"status": int, "pass": bool},
103
+ "PATCH": {"status": int, "pass": bool},
104
+ "DELETE": {"status": int, "pass": bool}
105
+ }
106
+ }
107
+ }
108
+
109
+ On error:
110
+ {
111
+ "success": False,
112
+ "error": str,
113
+ "error_type": "endpoint_missing" | "validation_error" | "database_error" | "runtime_error"
114
+ }
115
+ """
116
+ server_started_by_us = False
117
+ server_pid = None
118
+
119
+ try:
120
+ logger.info(f"Testing CRUD API for {model_name}")
121
+
122
+ # Check if dev server is running (port in use = server running)
123
+ server_was_running = not is_port_available(port)
124
+
125
+ if not server_was_running:
126
+ logger.info(f"Dev server not running on port {port}, starting...")
127
+
128
+ # Start dev server in background using CLI tools mixin
129
+ start_result = self._run_background_command(
130
+ command="npm run dev",
131
+ work_path=Path(project_dir),
132
+ startup_timeout=15,
133
+ expected_port=port,
134
+ auto_respond="",
135
+ )
136
+
137
+ if not start_result.get("success"):
138
+ return {
139
+ "success": False,
140
+ "error": "Failed to start dev server",
141
+ "details": start_result,
142
+ "error_type": "runtime_error",
143
+ }
144
+
145
+ server_started_by_us = True
146
+ server_pid = start_result.get("pid")
147
+ logger.info(f"Dev server started with PID {server_pid}")
148
+
149
+ # Give Next.js a moment to fully initialize
150
+ time.sleep(3)
151
+ else:
152
+ logger.info(f"Dev server already running on port {port}")
153
+
154
+ base_url = f"http://localhost:{port}"
155
+ resource = model_name.lower()
156
+ resource_plural = pluralize(resource)
157
+ api_url = f"{base_url}/api/{resource_plural}"
158
+
159
+ # Phase 2 Fix (Issue #885): Read schema to get actual fields
160
+ # instead of using hardcoded {"name": "Test Item"}
161
+ model_info = read_prisma_model(project_dir, model_name)
162
+ if model_info["success"]:
163
+ # Convert Prisma types to our field types
164
+ prisma_to_field_type = {
165
+ "String": "string",
166
+ "Int": "number",
167
+ "Float": "float",
168
+ "Boolean": "boolean",
169
+ "DateTime": "datetime",
170
+ }
171
+ schema_fields = {}
172
+ for field_name, prisma_type in model_info["fields"].items():
173
+ if field_name.lower() not in {"id", "createdat", "updatedat"}:
174
+ schema_fields[field_name] = prisma_to_field_type.get(
175
+ prisma_type, "string"
176
+ )
177
+ test_payload = generate_test_payload(schema_fields)
178
+ logger.info(f"Generated test payload from schema: {test_payload}")
179
+ else:
180
+ # Fallback: Use generic test payload but log warning
181
+ logger.warning(
182
+ f"Could not read Prisma schema for {model_name}, "
183
+ f"using generic test payload. Error: {model_info.get('error')}"
184
+ )
185
+ test_payload = {
186
+ "title": "Test Item",
187
+ "description": "Test description",
188
+ }
189
+
190
+ # Escape the payload for shell
191
+ test_payload_json = json.dumps(test_payload)
192
+
193
+ results = {}
194
+ created_id = None
195
+
196
+ # Test 1: POST (create)
197
+ logger.info("Testing POST (create)...")
198
+ post_result = self._run_foreground_command(
199
+ command=(
200
+ f"curl -s -w '\\n%{{http_code}}' -X POST "
201
+ f"-H 'Content-Type: application/json' "
202
+ f"-d '{test_payload_json}' '{api_url}'"
203
+ ),
204
+ work_path=Path(project_dir) if project_dir else Path.cwd(),
205
+ timeout=10,
206
+ auto_respond="y\n",
207
+ )
208
+
209
+ if post_result.get("status") == "success":
210
+ output = post_result.get("stdout", "")
211
+ lines = output.strip().split("\n")
212
+ status_code = int(lines[-1]) if lines else 0
213
+ results["POST"] = {
214
+ "status": status_code,
215
+ "pass": status_code == 201,
216
+ }
217
+
218
+ # Extract created ID from response
219
+ if status_code == 201 and len(lines) > 1:
220
+ try:
221
+ response_data = json.loads(lines[0])
222
+ created_id = response_data.get("id")
223
+ except Exception:
224
+ logger.warning(
225
+ "Could not parse POST response to extract ID"
226
+ )
227
+ else:
228
+ results["POST"] = {
229
+ "status": 0,
230
+ "pass": False,
231
+ "error": "Command failed",
232
+ }
233
+
234
+ # Test 2: GET (list)
235
+ logger.info("Testing GET (list)...")
236
+ get_list_result = self._run_foreground_command(
237
+ command=f"curl -s -w '\\n%{{http_code}}' '{api_url}'",
238
+ work_path=Path(project_dir) if project_dir else Path.cwd(),
239
+ timeout=10,
240
+ auto_respond="y\n",
241
+ )
242
+
243
+ if get_list_result.get("status") == "success":
244
+ output = get_list_result.get("stdout", "")
245
+ lines = output.strip().split("\n")
246
+ status_code = int(lines[-1]) if lines else 0
247
+ results["GET_LIST"] = {
248
+ "status": status_code,
249
+ "pass": status_code == 200,
250
+ }
251
+ else:
252
+ results["GET_LIST"] = {
253
+ "status": 0,
254
+ "pass": False,
255
+ "error": "Command failed",
256
+ }
257
+
258
+ # Test 3: GET (single) - only if we have an ID
259
+ if created_id:
260
+ logger.info(f"Testing GET (single) with ID {created_id}...")
261
+ get_single_result = self._run_foreground_command(
262
+ command=f"curl -s -w '\\n%{{http_code}}' '{api_url}/{created_id}'",
263
+ work_path=Path(project_dir) if project_dir else Path.cwd(),
264
+ timeout=10,
265
+ auto_respond="y\n",
266
+ )
267
+
268
+ if get_single_result.get("status") == "success":
269
+ output = get_single_result.get("stdout", "")
270
+ lines = output.strip().split("\n")
271
+ status_code = int(lines[-1]) if lines else 0
272
+ results["GET_SINGLE"] = {
273
+ "status": status_code,
274
+ "pass": status_code == 200,
275
+ }
276
+ else:
277
+ results["GET_SINGLE"] = {
278
+ "status": 0,
279
+ "pass": False,
280
+ "error": "Command failed",
281
+ }
282
+ else:
283
+ results["GET_SINGLE"] = {
284
+ "status": 0,
285
+ "pass": False,
286
+ "error": "No ID to test",
287
+ }
288
+
289
+ # Test 4: PATCH (update) - only if we have an ID
290
+ if created_id:
291
+ logger.info(f"Testing PATCH (update) with ID {created_id}...")
292
+ # Generate update payload - modify the first string field
293
+ update_payload = {}
294
+ for key, value in test_payload.items():
295
+ if isinstance(value, str):
296
+ update_payload[key] = f"Updated {value}"
297
+ break
298
+ if not update_payload:
299
+ # Fallback: use first field with "Updated" prefix
300
+ first_key = next(iter(test_payload), None)
301
+ if first_key:
302
+ update_payload[first_key] = "Updated Value"
303
+ update_payload_json = json.dumps(update_payload)
304
+
305
+ patch_result = self._run_foreground_command(
306
+ command=(
307
+ f"curl -s -w '\\n%{{http_code}}' -X PATCH "
308
+ f"-H 'Content-Type: application/json' "
309
+ f"-d '{update_payload_json}' '{api_url}/{created_id}'"
310
+ ),
311
+ work_path=Path(project_dir) if project_dir else Path.cwd(),
312
+ timeout=10,
313
+ auto_respond="y\n",
314
+ )
315
+
316
+ if patch_result.get("status") == "success":
317
+ output = patch_result.get("stdout", "")
318
+ lines = output.strip().split("\n")
319
+ status_code = int(lines[-1]) if lines else 0
320
+ results["PATCH"] = {
321
+ "status": status_code,
322
+ "pass": status_code == 200,
323
+ }
324
+ else:
325
+ results["PATCH"] = {
326
+ "status": 0,
327
+ "pass": False,
328
+ "error": "Command failed",
329
+ }
330
+ else:
331
+ results["PATCH"] = {
332
+ "status": 0,
333
+ "pass": False,
334
+ "error": "No ID to test",
335
+ }
336
+
337
+ # Test 5: DELETE - only if we have an ID
338
+ if created_id:
339
+ logger.info(f"Testing DELETE with ID {created_id}...")
340
+ delete_result = self._run_foreground_command(
341
+ command=f"curl -s -w '\\n%{{http_code}}' -X DELETE '{api_url}/{created_id}'",
342
+ work_path=Path(project_dir) if project_dir else Path.cwd(),
343
+ timeout=10,
344
+ auto_respond="y\n",
345
+ )
346
+
347
+ if delete_result.get("status") == "success":
348
+ output = delete_result.get("stdout", "")
349
+ lines = output.strip().split("\n")
350
+ status_code = int(lines[-1]) if lines else 0
351
+ results["DELETE"] = {
352
+ "status": status_code,
353
+ "pass": status_code == 200,
354
+ }
355
+ else:
356
+ results["DELETE"] = {
357
+ "status": 0,
358
+ "pass": False,
359
+ "error": "Command failed",
360
+ }
361
+ else:
362
+ results["DELETE"] = {
363
+ "status": 0,
364
+ "pass": False,
365
+ "error": "No ID to test",
366
+ }
367
+
368
+ # Calculate summary
369
+ passed = sum(1 for r in results.values() if r.get("pass", False))
370
+ failed = len(results) - passed
371
+
372
+ logger.info(f"Tests completed: {passed} passed, {failed} failed")
373
+
374
+ return {
375
+ "success": passed == len(results),
376
+ "result": {
377
+ "tests_passed": passed,
378
+ "tests_failed": failed,
379
+ "results": results,
380
+ },
381
+ }
382
+
383
+ except Exception as e:
384
+ logger.error(f"Error in test_crud_api: {e}", exc_info=True)
385
+ return {
386
+ "success": False,
387
+ "error": str(e),
388
+ "error_type": "runtime_error",
389
+ }
390
+
391
+ finally:
392
+ # Clean up: stop dev server if we started it
393
+ if server_started_by_us and server_pid:
394
+ logger.info(f"Stopping dev server (PID {server_pid})...")
395
+ try:
396
+ self._stop_process(server_pid, force=False)
397
+ logger.info("Dev server stopped successfully")
398
+ except Exception as cleanup_error:
399
+ logger.warning(f"Error stopping dev server: {cleanup_error}")
400
+
401
+ @tool
402
+ def validate_typescript(project_dir: str) -> Dict[str, Any]:
403
+ """Validate TypeScript code before declaring success.
404
+
405
+ Runs TypeScript compiler in no-emit mode to check for type errors
406
+ without generating output files. This is the ultimate guardrail to
407
+ catch import errors, missing types, and other TypeScript issues
408
+ before they reach npm run build.
409
+
410
+ TIER 4 ERROR MESSAGING: When validation fails, this tool returns
411
+ specific rule citations to teach the LLM what went wrong.
412
+
413
+ Args:
414
+ project_dir: Path to the Next.js project directory
415
+
416
+ Returns:
417
+ On success:
418
+ {
419
+ "success": True,
420
+ "message": "TypeScript validation passed"
421
+ }
422
+
423
+ On failure:
424
+ {
425
+ "success": False,
426
+ "error": "TypeScript validation failed",
427
+ "errors": str, # Full tsc error output
428
+ "rule": str, # Which rule was violated
429
+ "violation": str, # Specific violation
430
+ "fix": str, # How to fix it
431
+ "hint": "Fix the type errors listed above, then run validate_typescript again"
432
+ }
433
+ """
434
+ try:
435
+ logger.debug(f"Validating TypeScript in {project_dir}")
436
+
437
+ npx_command = "npx.cmd" if os.name == "nt" else "npx"
438
+
439
+ result = subprocess.run(
440
+ [npx_command, "tsc", "--noEmit", "--skipLibCheck"],
441
+ cwd=project_dir,
442
+ capture_output=True,
443
+ text=True,
444
+ timeout=60,
445
+ check=False,
446
+ )
447
+
448
+ if result.returncode == 0:
449
+ logger.info("TypeScript validation passed")
450
+ return {
451
+ "success": True,
452
+ "message": "TypeScript validation passed - no type errors found",
453
+ }
454
+
455
+ # Parse errors to provide specific guidance
456
+ error_output = result.stderr if result.stderr else result.stdout
457
+ logger.warning(f"TypeScript validation failed:\n{error_output}")
458
+
459
+ # Detect common error patterns and provide rule citations
460
+ rule = None
461
+ violation = None
462
+ fix = None
463
+
464
+ if "Cannot find name" in error_output:
465
+ violation = "Missing type import"
466
+ rule = "Client components must use: import type { X } from '@prisma/client'"
467
+ fix = "Add the missing type import at the top of the file"
468
+ elif "Cannot find module '@/lib/prisma'" in error_output:
469
+ violation = "Missing prisma singleton"
470
+ rule = "Server components must import: import { prisma } from '@/lib/prisma'"
471
+ fix = "Ensure src/lib/prisma.ts exists with the Prisma singleton"
472
+ elif (
473
+ "Module '\"@prisma/client\"' has no exported member" in error_output
474
+ ):
475
+ violation = "Prisma types not generated"
476
+ rule = "Run prisma generate after schema changes"
477
+ fix = "Run: npx prisma generate"
478
+ elif (
479
+ "'prisma' is not defined" in error_output
480
+ or "Cannot find name 'prisma'" in error_output
481
+ ):
482
+ violation = "Direct prisma import in client component"
483
+ rule = "NEVER import prisma client directly in client components"
484
+ fix = "Use API routes for database access from client components"
485
+
486
+ response = {
487
+ "success": False,
488
+ "error": "TypeScript validation failed",
489
+ "errors": error_output,
490
+ "hint": "Fix the type errors listed above, then run validate_typescript again",
491
+ }
492
+
493
+ # Add Tier 4 teaching if we detected a specific violation
494
+ if rule and violation and fix:
495
+ response.update({"violation": violation, "rule": rule, "fix": fix})
496
+
497
+ return response
498
+
499
+ except subprocess.TimeoutExpired:
500
+ logger.error("TypeScript validation timed out")
501
+ return {
502
+ "success": False,
503
+ "error": "TypeScript validation timed out after 60 seconds",
504
+ "hint": "Check for infinite type recursion or very large project",
505
+ }
506
+ except FileNotFoundError:
507
+ logger.error("TypeScript compiler not found")
508
+ return {
509
+ "success": False,
510
+ "error": "TypeScript compiler (tsc) not found",
511
+ "hint": "Ensure TypeScript is installed: npm install --save-dev typescript",
512
+ }
513
+ except Exception as e:
514
+ logger.error(f"Error in validate_typescript: {e}", exc_info=True)
515
+ return {
516
+ "success": False,
517
+ "error": f"Validation error: {str(e)}",
518
+ "hint": "Check that project_dir is valid and contains a Next.js project",
519
+ }
520
+
521
+ @tool
522
+ def validate_crud_structure(
523
+ project_dir: str, resource_name: str
524
+ ) -> Dict[str, Any]:
525
+ """Validate that all required CRUD files exist before declaring success.
526
+
527
+ Checks for a complete CRUD application structure including:
528
+ - List page, New page, Detail page
529
+ - Form component, Actions component
530
+ - API routes (collection and item)
531
+
532
+ This should be called AFTER building a CRUD app to ensure nothing was skipped.
533
+
534
+ Args:
535
+ project_dir: Path to the Next.js project directory
536
+ resource_name: Resource name in singular form (e.g., "todo", "post")
537
+
538
+ Returns:
539
+ Dictionary with:
540
+ - success: bool
541
+ - missing_files: list of missing file paths
542
+ - message: summary of validation result
543
+ """
544
+ try:
545
+ project_path = Path(project_dir)
546
+ resource_plural = resource_name.lower() + "s" # Simple pluralization
547
+ resource_capitalized = resource_name.capitalize()
548
+
549
+ # Define all required files for a complete CRUD app
550
+ required_files = {
551
+ "List page": f"src/app/{resource_plural}/page.tsx",
552
+ "New page": f"src/app/{resource_plural}/new/page.tsx",
553
+ "Detail page": f"src/app/{resource_plural}/[id]/page.tsx",
554
+ "Form component": f"src/components/{resource_capitalized}Form.tsx",
555
+ "Actions component": f"src/components/{resource_capitalized}Actions.tsx",
556
+ "Collection API": f"src/app/api/{resource_plural}/route.ts",
557
+ "Item API": f"src/app/api/{resource_plural}/[id]/route.ts",
558
+ }
559
+
560
+ missing_files = []
561
+ existing_files = []
562
+
563
+ for description, file_path in required_files.items():
564
+ full_path = project_path / file_path
565
+ if full_path.exists():
566
+ existing_files.append(f"✅ {description}: {file_path}")
567
+ else:
568
+ missing_files.append(
569
+ {
570
+ "description": description,
571
+ "path": file_path,
572
+ "create_with": self._get_create_command(
573
+ description, resource_name
574
+ ),
575
+ }
576
+ )
577
+
578
+ if not missing_files:
579
+ logger.info(f"CRUD structure validation passed for {resource_name}")
580
+ return {
581
+ "success": True,
582
+ "message": f"Complete CRUD structure validated for {resource_name}",
583
+ "existing_files": existing_files,
584
+ }
585
+
586
+ # Build detailed error message with fix instructions
587
+ error_details = f"Missing {len(missing_files)} required file(s) for {resource_name} CRUD app:\n\n"
588
+ for item in missing_files:
589
+ error_details += f"❌ {item['description']}: {item['path']}\n"
590
+ error_details += f" Fix: {item['create_with']}\n\n"
591
+
592
+ logger.warning(
593
+ f"CRUD structure validation failed: {len(missing_files)} files missing"
594
+ )
595
+
596
+ return {
597
+ "success": False,
598
+ "error": f"Incomplete CRUD structure: {len(missing_files)} file(s) missing",
599
+ "missing_files": missing_files,
600
+ "existing_files": existing_files,
601
+ "details": error_details,
602
+ "hint": "Create the missing files using the fix commands listed above",
603
+ }
604
+
605
+ except Exception as e:
606
+ logger.error(f"Error in validate_crud_structure: {e}", exc_info=True)
607
+ return {
608
+ "success": False,
609
+ "error": f"Validation error: {str(e)}",
610
+ "hint": "Check that project_dir is valid and resource_name is correct",
611
+ }
612
+
613
+ @tool
614
+ def validate_styles(
615
+ project_dir: str, _resource_name: str = None
616
+ ) -> Dict[str, Any]:
617
+ """Validate CSS files and design system consistency.
618
+
619
+ This tool validates:
620
+ 1. CSS files contain valid CSS (not TypeScript/JavaScript) - CRITICAL
621
+ 2. globals.css has Tailwind directives
622
+ 3. layout.tsx imports globals.css
623
+ 4. (Optional) Custom classes used in components are defined
624
+
625
+ Addresses Issue #1002: CSS file written with TypeScript code.
626
+
627
+ Args:
628
+ project_dir: Path to the Next.js project directory
629
+ resource_name: Optional resource name for component class checks
630
+
631
+ Returns:
632
+ On success:
633
+ {
634
+ "success": True,
635
+ "is_valid": True,
636
+ "message": "Styling validated successfully",
637
+ "files_checked": [list of files]
638
+ }
639
+
640
+ On failure:
641
+ {
642
+ "success": False,
643
+ "is_valid": False,
644
+ "errors": [list of CRITICAL errors],
645
+ "warnings": [list of warnings],
646
+ "hint": "How to fix"
647
+ }
648
+ """
649
+ import re
650
+
651
+ try:
652
+ project_path = Path(project_dir)
653
+ errors = []
654
+ warnings = []
655
+ files_checked = []
656
+
657
+ # 1. Check globals.css for TypeScript content (CRITICAL)
658
+ globals_css = project_path / "src" / "app" / "globals.css"
659
+ if globals_css.exists():
660
+ files_checked.append("src/app/globals.css")
661
+ content = globals_css.read_text()
662
+
663
+ # TypeScript/JavaScript detection patterns
664
+ typescript_indicators = [
665
+ (r"^\s*import\s+.*from", "import statement"),
666
+ (
667
+ r"^\s*export\s+(default|const|function|class|async)",
668
+ "export statement",
669
+ ),
670
+ (r'"use client"|\'use client\'', "React client directive"),
671
+ (r"^\s*interface\s+\w+", "TypeScript interface"),
672
+ (r"^\s*type\s+\w+\s*=", "TypeScript type alias"),
673
+ (r"^\s*const\s+\w+\s*[=:]", "const declaration"),
674
+ (r"^\s*let\s+\w+\s*[=:]", "let declaration"),
675
+ (r"^\s*function\s+\w+", "function declaration"),
676
+ (r"^\s*async\s+function", "async function"),
677
+ (r"<[A-Z][a-zA-Z]*[\s/>]", "JSX component tag"),
678
+ (r"useState|useEffect|useRouter|usePathname", "React hook"),
679
+ ]
680
+
681
+ for pattern, description in typescript_indicators:
682
+ if re.search(pattern, content, re.MULTILINE):
683
+ errors.append(
684
+ f"CRITICAL: globals.css contains {description}. "
685
+ f"This file has TypeScript/JSX code instead of CSS."
686
+ )
687
+
688
+ # Check for balanced braces
689
+ if content.count("{") != content.count("}"):
690
+ errors.append("globals.css has mismatched braces")
691
+
692
+ # Check for Tailwind directives
693
+ has_tailwind = (
694
+ "@tailwind" in content or '@import "tailwindcss' in content
695
+ )
696
+ if not has_tailwind and len(content.strip()) > 50:
697
+ warnings.append(
698
+ "globals.css is missing Tailwind directives "
699
+ "(@tailwind base/components/utilities)"
700
+ )
701
+ else:
702
+ errors.append("globals.css not found at src/app/globals.css")
703
+
704
+ # 2. Check layout.tsx imports globals.css
705
+ layout_tsx = project_path / "src" / "app" / "layout.tsx"
706
+ if layout_tsx.exists():
707
+ files_checked.append("src/app/layout.tsx")
708
+ layout_content = layout_tsx.read_text()
709
+
710
+ # Check for globals.css import
711
+ globals_import = (
712
+ './globals.css"' in layout_content
713
+ or "./globals.css'" in layout_content
714
+ or "@/app/globals.css" in layout_content
715
+ )
716
+ if not globals_import:
717
+ warnings.append(
718
+ "layout.tsx does not import globals.css. "
719
+ "Global styles may not be applied to pages."
720
+ )
721
+ else:
722
+ warnings.append("layout.tsx not found at src/app/layout.tsx")
723
+
724
+ # 3. Check all CSS files for TypeScript content
725
+ for css_file in project_path.glob("**/*.css"):
726
+ if css_file == globals_css:
727
+ continue # Already checked
728
+
729
+ files_checked.append(str(css_file.relative_to(project_path)))
730
+ css_content = css_file.read_text()
731
+
732
+ # Quick check for obvious TypeScript patterns
733
+ if re.search(r"^\s*import\s+", css_content, re.MULTILINE):
734
+ errors.append(
735
+ f"CRITICAL: {css_file.name} contains import statement. "
736
+ f"This is TypeScript, not CSS."
737
+ )
738
+ if re.search(r"^\s*export\s+", css_content, re.MULTILINE):
739
+ errors.append(
740
+ f"CRITICAL: {css_file.name} contains export statement. "
741
+ f"This is TypeScript, not CSS."
742
+ )
743
+
744
+ # Build result
745
+ is_valid = len(errors) == 0
746
+ if is_valid:
747
+ logger.info("Styling validation passed")
748
+ result = {
749
+ "success": True,
750
+ "is_valid": True,
751
+ "message": "Styling validated successfully",
752
+ "files_checked": files_checked,
753
+ }
754
+ if warnings:
755
+ result["warnings"] = warnings
756
+ return result
757
+
758
+ logger.warning(f"Styling validation failed: {errors}")
759
+ return {
760
+ "success": False,
761
+ "is_valid": False,
762
+ "errors": errors,
763
+ "warnings": warnings,
764
+ "files_checked": files_checked,
765
+ "hint": (
766
+ "CRITICAL errors indicate CSS files contain TypeScript code. "
767
+ "Regenerate globals.css with valid CSS content including "
768
+ "Tailwind directives (@tailwind base/components/utilities)."
769
+ ),
770
+ }
771
+
772
+ except Exception as e:
773
+ logger.error(f"Error in validate_styles: {e}", exc_info=True)
774
+ return {
775
+ "success": False,
776
+ "is_valid": False,
777
+ "error": f"Validation error: {str(e)}",
778
+ "hint": "Check that project_dir is valid and contains a Next.js project",
779
+ }
780
+
781
+ def _get_create_command(self, description: str, resource_name: str) -> str:
782
+ """Get the tool command needed to create a missing file.
783
+
784
+ This is a helper method used by validate_crud_structure to provide
785
+ fix instructions for missing CRUD files.
786
+
787
+ Args:
788
+ description: Human-readable description of the file type
789
+ resource_name: Singular resource name (e.g., "todo", "post")
790
+
791
+ Returns:
792
+ Tool command string to create the missing file
793
+ """
794
+ if "Detail page" in description:
795
+ return f'manage_react_component(variant="detail", resource_name="{resource_name}")'
796
+ elif "Actions component" in description:
797
+ return f'manage_react_component(variant="actions", resource_name="{resource_name}")'
798
+ elif "List page" in description:
799
+ return f'manage_react_component(variant="list", resource_name="{resource_name}")'
800
+ elif "New page" in description:
801
+ return f'manage_react_component(variant="new", resource_name="{resource_name}")'
802
+ elif "Form component" in description:
803
+ return f'manage_react_component(variant="form", resource_name="{resource_name}")'
804
+ elif "API" in description:
805
+ return f'manage_api_endpoint(resource_name="{resource_name}", operations=["GET", "POST", "PATCH", "DELETE"])'
806
+ return "Unknown - check documentation"