Implement structured output for prompt steps (Phase 1)

Add schema validation and retry logic for AI responses:

- Add output_schema, max_retries, plain_text fields to PromptStep
- Add DEFAULT_OUTPUT_SCHEMA with output/reasoning fields
- Implement append_schema_instructions() for prompt augmentation
- Implement validate_schema() using jsonschema library
- Add retry logic with error feedback on validation failure
- Enhance substitute_variables() for nested field access ({var.field})
- Add jsonschema to dependencies
- Update GUI dialog with plain_text checkbox and max_retries spinner
- Add comprehensive tests for structured output and nested access

Tools using plain_text=True bypass validation (current behavior).
Tools without plain_text get structured JSON output with schema enforcement.

Migration Phase 1 complete. Next: Apply plain_text=True to all 72 tools.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-02-17 17:34:48 -04:00
co-authored by Claude Opus 4.5
parent f980fe05f1
commit 8581775002
6 changed files with 566 additions and 46 deletions
+40 -2
View File
@@ -6,7 +6,7 @@ from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QFormLayout, QLineEdit,
QComboBox, QPushButton, QHBoxLayout, QLabel,
QPlainTextEdit, QSplitter, QGroupBox, QTextEdit, QMessageBox,
QCheckBox
QCheckBox, QSpinBox
)
from PySide6.QtCore import Qt, QThread, Signal
@@ -73,6 +73,31 @@ class PromptStepDialog(QDialog):
self.strip_fences_check = QCheckBox("Strip markdown code fences from output")
form.addRow("", self.strip_fences_check)
# Structured output options
form.addRow(QLabel("")) # Spacer
structured_label = QLabel("<b>Structured Output</b>")
form.addRow(structured_label)
# Plain text checkbox (bypass structured output)
self.plain_text_check = QCheckBox("Plain text mode (bypass JSON validation)")
self.plain_text_check.setToolTip(
"When checked, output is returned as-is without JSON parsing.\n"
"When unchecked, output must be valid JSON matching a schema."
)
self.plain_text_check.stateChanged.connect(self._on_plain_text_changed)
form.addRow("", self.plain_text_check)
# Max retries (only visible when structured output is enabled)
self.retries_spin = QSpinBox()
self.retries_spin.setRange(0, 5)
self.retries_spin.setValue(1)
self.retries_spin.setToolTip(
"Number of retries if JSON validation fails.\n"
"The AI will receive error feedback and try again."
)
self.retries_label = QLabel("Max retries:")
form.addRow(self.retries_label, self.retries_spin)
layout.addLayout(form)
# Prompt text
@@ -103,6 +128,12 @@ class PromptStepDialog(QDialog):
layout.addLayout(buttons)
def _on_plain_text_changed(self, state):
"""Show/hide structured output options based on plain_text checkbox."""
is_plain = bool(state)
self.retries_label.setVisible(not is_plain)
self.retries_spin.setVisible(not is_plain)
def _load_step(self, step: PromptStep):
"""Load step data into form."""
# Load name
@@ -125,6 +156,11 @@ class PromptStepDialog(QDialog):
self.prompt_input.setPlainText(step.prompt)
self.strip_fences_check.setChecked(step.strip_fences)
# Structured output fields
self.plain_text_check.setChecked(step.plain_text)
self.retries_spin.setValue(step.max_retries)
self._on_plain_text_changed(step.plain_text)
def _validate_and_accept(self):
"""Validate and accept."""
prompt = self.prompt_input.toPlainText().strip()
@@ -153,7 +189,9 @@ class PromptStepDialog(QDialog):
output_var=self.output_input.text().strip(),
profile=profile,
name=name,
strip_fences=self.strip_fences_check.isChecked()
strip_fences=self.strip_fences_check.isChecked(),
plain_text=self.plain_text_check.isChecked(),
max_retries=self.retries_spin.value()
)