feat: Add PlantUML syntax validation with auto-retry

When AI generates PlantUML code, run `plantuml -syntax` to validate.
If syntax errors are found, the error message is fed back to the AI
which retries with context about what went wrong.

This catches actual syntax errors like:
- Missing @startuml/@enduml tags
- Malformed arrows
- Invalid element definitions

Note: PlantUML's syntax checker only catches true syntax errors,
not semantic issues (like nesting rectangles in components).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2025-12-30 23:14:28 -04:00
parent 9e0f32513f
commit 1923053f6a
1 changed files with 45 additions and 0 deletions

View File

@ -252,11 +252,56 @@ class AIThread(QThread):
def _validate_output(self, code: str) -> tuple:
"""Validate the generated code. Returns (is_valid, error_message)."""
# For PlantUML, run syntax check via plantuml command
if self.artifact_type == 'plantuml':
return self._validate_plantuml(code)
# For other formats, use renderer validation
renderer = self._get_renderer()
if renderer:
return renderer.validate(code)
return True, None # No validation available
def _validate_plantuml(self, code: str) -> tuple:
"""Validate PlantUML code using plantuml -syntax command."""
import shutil
plantuml_cmd = shutil.which('plantuml')
if not plantuml_cmd:
# PlantUML not installed, skip validation
return True, None
try:
result = subprocess.run(
[plantuml_cmd, '-syntax', '-pipe'],
input=code,
capture_output=True,
text=True,
timeout=30
)
# Exit code 0 = valid, anything else = error
if result.returncode == 0:
return True, None
# Parse error message from stdout
# Format: "ERROR\n<line_number>\nSyntax Error?\n<description>"
error_lines = result.stdout.strip().split('\n')
error_msg = "PlantUML syntax error"
if len(error_lines) >= 2 and error_lines[0] == 'ERROR':
line_num = error_lines[1]
error_msg = f"PlantUML syntax error on line {line_num}"
if len(error_lines) >= 4:
error_msg += f": {error_lines[3]}"
return False, error_msg
except subprocess.TimeoutExpired:
return True, None # Skip validation on timeout
except Exception as e:
return True, None # Skip validation on error
def _call_ai(self, instruction: str, current_code: str) -> tuple:
"""Call artifact-ai SmartTool and return (success, response_or_error).