590 lines
19 KiB
Python
590 lines
19 KiB
Python
|
|
"""
|
||
|
|
Study Interview Engine
|
||
|
|
|
||
|
|
Main orchestrator for the interview process.
|
||
|
|
Coordinates question flow, state management, validation, and blueprint generation.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import datetime
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, List, Any, Optional, Literal
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
from .interview_state import (
|
||
|
|
InterviewState,
|
||
|
|
InterviewPhase,
|
||
|
|
InterviewStateManager,
|
||
|
|
AnsweredQuestion,
|
||
|
|
LogEntry,
|
||
|
|
)
|
||
|
|
from .question_engine import QuestionEngine, Question
|
||
|
|
from .interview_presenter import InterviewPresenter, ClaudePresenter
|
||
|
|
from .engineering_validator import EngineeringValidator, ValidationResult, AntiPattern
|
||
|
|
from .interview_intelligence import InterviewIntelligence
|
||
|
|
from .study_blueprint import StudyBlueprint, BlueprintBuilder
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class InterviewSession:
|
||
|
|
"""Represents an active interview session."""
|
||
|
|
session_id: str
|
||
|
|
study_name: str
|
||
|
|
study_path: Path
|
||
|
|
started_at: datetime
|
||
|
|
current_phase: InterviewPhase
|
||
|
|
introspection: Dict[str, Any]
|
||
|
|
is_complete: bool = False
|
||
|
|
is_resumed: bool = False
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class NextAction:
|
||
|
|
"""What should happen after processing an answer."""
|
||
|
|
action_type: Literal["ask_question", "show_summary", "validate", "generate", "error", "confirm_warning"]
|
||
|
|
question: Optional[Question] = None
|
||
|
|
message: Optional[str] = None
|
||
|
|
warnings: List[str] = field(default_factory=list)
|
||
|
|
blueprint: Optional[StudyBlueprint] = None
|
||
|
|
anti_patterns: List[AntiPattern] = field(default_factory=list)
|
||
|
|
|
||
|
|
|
||
|
|
class StudyInterviewEngine:
|
||
|
|
"""
|
||
|
|
Main orchestrator for study interviews.
|
||
|
|
|
||
|
|
Manages the complete interview lifecycle:
|
||
|
|
1. Start or resume interview
|
||
|
|
2. Present questions via presenter
|
||
|
|
3. Process answers with validation
|
||
|
|
4. Generate blueprint for review
|
||
|
|
5. Handle modifications
|
||
|
|
6. Coordinate study generation
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
study_path: Path,
|
||
|
|
presenter: Optional[InterviewPresenter] = None
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Initialize interview engine.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
study_path: Path to the study directory
|
||
|
|
presenter: Presentation layer (defaults to ClaudePresenter)
|
||
|
|
"""
|
||
|
|
self.study_path = Path(study_path)
|
||
|
|
self.presenter = presenter or ClaudePresenter()
|
||
|
|
self.state_manager = InterviewStateManager(self.study_path)
|
||
|
|
self.question_engine = QuestionEngine()
|
||
|
|
self.validator = EngineeringValidator()
|
||
|
|
self.intelligence = InterviewIntelligence()
|
||
|
|
self.blueprint_builder = BlueprintBuilder()
|
||
|
|
|
||
|
|
# Current state
|
||
|
|
self.state: Optional[InterviewState] = None
|
||
|
|
self.introspection: Dict[str, Any] = {}
|
||
|
|
self.current_question: Optional[Question] = None
|
||
|
|
self.session: Optional[InterviewSession] = None
|
||
|
|
|
||
|
|
# Estimated questions (for progress)
|
||
|
|
self.estimated_total_questions = 12 # Will be updated based on complexity
|
||
|
|
|
||
|
|
def start_interview(
|
||
|
|
self,
|
||
|
|
study_name: str,
|
||
|
|
model_path: Optional[Path] = None,
|
||
|
|
introspection: Optional[Dict[str, Any]] = None
|
||
|
|
) -> InterviewSession:
|
||
|
|
"""
|
||
|
|
Start a new interview or resume existing one.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
study_name: Name for the study
|
||
|
|
model_path: Path to the NX model (optional)
|
||
|
|
introspection: Pre-computed introspection results (optional)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
InterviewSession representing the active interview
|
||
|
|
"""
|
||
|
|
# Check for existing state
|
||
|
|
existing_state = self.state_manager.load_state()
|
||
|
|
|
||
|
|
if existing_state and not existing_state.is_complete():
|
||
|
|
# Resume existing interview
|
||
|
|
self.state = existing_state
|
||
|
|
self.introspection = existing_state.introspection
|
||
|
|
|
||
|
|
self.session = InterviewSession(
|
||
|
|
session_id=existing_state.session_id,
|
||
|
|
study_name=existing_state.study_name,
|
||
|
|
study_path=self.study_path,
|
||
|
|
started_at=datetime.fromisoformat(existing_state.started_at),
|
||
|
|
current_phase=existing_state.get_phase(),
|
||
|
|
introspection=self.introspection,
|
||
|
|
is_resumed=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
return self.session
|
||
|
|
|
||
|
|
# Start new interview
|
||
|
|
self.state = InterviewState(
|
||
|
|
session_id=str(uuid.uuid4()),
|
||
|
|
study_name=study_name,
|
||
|
|
study_path=str(self.study_path),
|
||
|
|
current_phase=InterviewPhase.INTROSPECTION.value,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Store introspection if provided
|
||
|
|
if introspection:
|
||
|
|
self.introspection = introspection
|
||
|
|
self.state.introspection = introspection
|
||
|
|
# Move to problem definition if introspection already done
|
||
|
|
self.state.set_phase(InterviewPhase.PROBLEM_DEFINITION)
|
||
|
|
|
||
|
|
# Save initial state
|
||
|
|
self.state_manager.save_state(self.state)
|
||
|
|
|
||
|
|
self.session = InterviewSession(
|
||
|
|
session_id=self.state.session_id,
|
||
|
|
study_name=study_name,
|
||
|
|
study_path=self.study_path,
|
||
|
|
started_at=datetime.now(),
|
||
|
|
current_phase=self.state.get_phase(),
|
||
|
|
introspection=self.introspection,
|
||
|
|
)
|
||
|
|
|
||
|
|
return self.session
|
||
|
|
|
||
|
|
def get_first_question(self) -> NextAction:
|
||
|
|
"""
|
||
|
|
Get the first question to ask.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
NextAction with the first question
|
||
|
|
"""
|
||
|
|
if self.state is None:
|
||
|
|
return NextAction(
|
||
|
|
action_type="error",
|
||
|
|
message="Interview not started. Call start_interview() first."
|
||
|
|
)
|
||
|
|
|
||
|
|
# Get next question
|
||
|
|
next_q = self.question_engine.get_next_question(self.state, self.introspection)
|
||
|
|
|
||
|
|
if next_q is None:
|
||
|
|
# No questions - should not happen at start
|
||
|
|
return NextAction(
|
||
|
|
action_type="error",
|
||
|
|
message="No questions available."
|
||
|
|
)
|
||
|
|
|
||
|
|
self.current_question = next_q
|
||
|
|
|
||
|
|
return NextAction(
|
||
|
|
action_type="ask_question",
|
||
|
|
question=next_q,
|
||
|
|
message=self.presenter.present_question(
|
||
|
|
next_q,
|
||
|
|
question_number=self.state.current_question_count() + 1,
|
||
|
|
total_questions=self.estimated_total_questions,
|
||
|
|
category_name=self._get_category_name(next_q.category),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
def process_answer(self, answer: str) -> NextAction:
|
||
|
|
"""
|
||
|
|
Process user answer and determine next action.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
answer: User's answer (natural language)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
NextAction indicating what to do next
|
||
|
|
"""
|
||
|
|
if self.state is None or self.current_question is None:
|
||
|
|
return NextAction(
|
||
|
|
action_type="error",
|
||
|
|
message="No active question. Call get_first_question() or get_next_question()."
|
||
|
|
)
|
||
|
|
|
||
|
|
question = self.current_question
|
||
|
|
|
||
|
|
# 1. Parse answer based on question type
|
||
|
|
parsed = self.presenter.parse_response(answer, question)
|
||
|
|
|
||
|
|
# 2. Validate answer
|
||
|
|
is_valid, error_msg = self.question_engine.validate_answer(parsed, question)
|
||
|
|
if not is_valid:
|
||
|
|
return NextAction(
|
||
|
|
action_type="error",
|
||
|
|
message=f"Invalid answer: {error_msg}",
|
||
|
|
question=question, # Re-ask same question
|
||
|
|
)
|
||
|
|
|
||
|
|
# 3. Store answer
|
||
|
|
self._store_answer(question, answer, parsed)
|
||
|
|
|
||
|
|
# 4. Update phase if needed
|
||
|
|
self._update_phase(question)
|
||
|
|
|
||
|
|
# 5. Update complexity after initial questions
|
||
|
|
if question.category == "problem_definition":
|
||
|
|
self._update_complexity()
|
||
|
|
|
||
|
|
# 6. Check for warnings/anti-patterns
|
||
|
|
anti_patterns = self.validator.detect_anti_patterns(self.state, self.introspection)
|
||
|
|
new_warnings = [ap.description for ap in anti_patterns if ap.severity in ["error", "warning"]]
|
||
|
|
|
||
|
|
# Filter to only new warnings
|
||
|
|
existing_warnings = set(self.state.warnings)
|
||
|
|
for w in new_warnings:
|
||
|
|
if w not in existing_warnings:
|
||
|
|
self.state.add_warning(w)
|
||
|
|
|
||
|
|
# 7. Check if we should show anti-pattern warnings
|
||
|
|
blocking_patterns = [ap for ap in anti_patterns if ap.severity == "error" and not ap.acknowledged]
|
||
|
|
if blocking_patterns:
|
||
|
|
return NextAction(
|
||
|
|
action_type="confirm_warning",
|
||
|
|
message=self._format_anti_pattern_warnings(blocking_patterns),
|
||
|
|
anti_patterns=blocking_patterns,
|
||
|
|
)
|
||
|
|
|
||
|
|
# 8. Get next question
|
||
|
|
next_q = self.question_engine.get_next_question(self.state, self.introspection)
|
||
|
|
|
||
|
|
if next_q is None:
|
||
|
|
# Interview complete - generate blueprint
|
||
|
|
return self._finalize_interview()
|
||
|
|
|
||
|
|
self.current_question = next_q
|
||
|
|
|
||
|
|
return NextAction(
|
||
|
|
action_type="ask_question",
|
||
|
|
question=next_q,
|
||
|
|
message=self.presenter.present_question(
|
||
|
|
next_q,
|
||
|
|
question_number=self.state.current_question_count() + 1,
|
||
|
|
total_questions=self.estimated_total_questions,
|
||
|
|
category_name=self._get_category_name(next_q.category),
|
||
|
|
),
|
||
|
|
warnings=[w for w in self.state.warnings if w not in self.state.warnings_acknowledged],
|
||
|
|
)
|
||
|
|
|
||
|
|
def acknowledge_warnings(self, acknowledged: bool = True) -> NextAction:
|
||
|
|
"""
|
||
|
|
Acknowledge current warnings and continue.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
acknowledged: Whether user acknowledged warnings
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
NextAction (continue or abort)
|
||
|
|
"""
|
||
|
|
if not acknowledged:
|
||
|
|
return NextAction(
|
||
|
|
action_type="error",
|
||
|
|
message="Interview paused. Please fix the issues and restart, or acknowledge warnings to proceed."
|
||
|
|
)
|
||
|
|
|
||
|
|
# Mark all current warnings as acknowledged
|
||
|
|
for w in self.state.warnings:
|
||
|
|
self.state.acknowledge_warning(w)
|
||
|
|
|
||
|
|
# Continue to next question
|
||
|
|
next_q = self.question_engine.get_next_question(self.state, self.introspection)
|
||
|
|
|
||
|
|
if next_q is None:
|
||
|
|
return self._finalize_interview()
|
||
|
|
|
||
|
|
self.current_question = next_q
|
||
|
|
|
||
|
|
return NextAction(
|
||
|
|
action_type="ask_question",
|
||
|
|
question=next_q,
|
||
|
|
message=self.presenter.present_question(
|
||
|
|
next_q,
|
||
|
|
question_number=self.state.current_question_count() + 1,
|
||
|
|
total_questions=self.estimated_total_questions,
|
||
|
|
category_name=self._get_category_name(next_q.category),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
def generate_blueprint(self) -> StudyBlueprint:
|
||
|
|
"""
|
||
|
|
Generate study blueprint from interview state.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
StudyBlueprint ready for generation
|
||
|
|
"""
|
||
|
|
if self.state is None:
|
||
|
|
raise ValueError("No interview state available")
|
||
|
|
|
||
|
|
blueprint = self.blueprint_builder.from_interview_state(
|
||
|
|
self.state,
|
||
|
|
self.introspection
|
||
|
|
)
|
||
|
|
|
||
|
|
# Store in state
|
||
|
|
self.state.blueprint = blueprint.to_dict()
|
||
|
|
self.state_manager.save_state(self.state)
|
||
|
|
|
||
|
|
return blueprint
|
||
|
|
|
||
|
|
def modify_blueprint(self, changes: Dict[str, Any]) -> StudyBlueprint:
|
||
|
|
"""
|
||
|
|
Apply what-if modifications to the blueprint.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
changes: Dictionary of changes to apply
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Modified StudyBlueprint
|
||
|
|
"""
|
||
|
|
if self.state is None or self.state.blueprint is None:
|
||
|
|
raise ValueError("No blueprint available to modify")
|
||
|
|
|
||
|
|
blueprint = StudyBlueprint.from_dict(self.state.blueprint)
|
||
|
|
|
||
|
|
# Apply changes
|
||
|
|
for key, value in changes.items():
|
||
|
|
if key == "n_trials":
|
||
|
|
blueprint.n_trials = int(value)
|
||
|
|
elif key == "sampler":
|
||
|
|
blueprint.sampler = value
|
||
|
|
elif key == "add_constraint":
|
||
|
|
# Handle adding constraints
|
||
|
|
pass
|
||
|
|
elif key == "remove_constraint":
|
||
|
|
# Handle removing constraints
|
||
|
|
pass
|
||
|
|
# Add more modification types as needed
|
||
|
|
|
||
|
|
# Re-validate
|
||
|
|
validation_errors = blueprint.validate()
|
||
|
|
if validation_errors:
|
||
|
|
raise ValueError(f"Invalid modifications: {validation_errors}")
|
||
|
|
|
||
|
|
# Update state
|
||
|
|
self.state.blueprint = blueprint.to_dict()
|
||
|
|
self.state_manager.save_state(self.state)
|
||
|
|
|
||
|
|
return blueprint
|
||
|
|
|
||
|
|
def confirm_blueprint(self) -> bool:
|
||
|
|
"""
|
||
|
|
Confirm blueprint and mark interview as complete.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if successful
|
||
|
|
"""
|
||
|
|
if self.state is None:
|
||
|
|
return False
|
||
|
|
|
||
|
|
self.state.set_phase(InterviewPhase.COMPLETE)
|
||
|
|
self.state_manager.save_state(self.state)
|
||
|
|
|
||
|
|
# Finalize log
|
||
|
|
self.state_manager.finalize_log(self.state)
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
def get_current_state(self) -> Optional[InterviewState]:
|
||
|
|
"""Get current interview state."""
|
||
|
|
return self.state
|
||
|
|
|
||
|
|
def get_progress(self) -> str:
|
||
|
|
"""Get formatted progress string."""
|
||
|
|
if self.state is None:
|
||
|
|
return "No active interview"
|
||
|
|
|
||
|
|
return self.presenter.show_progress(
|
||
|
|
self.state.current_question_count(),
|
||
|
|
self.estimated_total_questions,
|
||
|
|
self._get_phase_name(self.state.current_phase)
|
||
|
|
)
|
||
|
|
|
||
|
|
def reset_interview(self) -> None:
|
||
|
|
"""Reset interview and start fresh."""
|
||
|
|
self.state_manager.delete_state()
|
||
|
|
self.state = None
|
||
|
|
self.current_question = None
|
||
|
|
self.session = None
|
||
|
|
|
||
|
|
# Private methods
|
||
|
|
|
||
|
|
def _store_answer(self, question: Question, raw: str, parsed: Any) -> None:
|
||
|
|
"""Store answer in state."""
|
||
|
|
# Create answered question record
|
||
|
|
answered = AnsweredQuestion(
|
||
|
|
question_id=question.id,
|
||
|
|
answered_at=datetime.now().isoformat(),
|
||
|
|
raw_response=raw,
|
||
|
|
parsed_value=parsed,
|
||
|
|
)
|
||
|
|
|
||
|
|
self.state.add_answered_question(answered)
|
||
|
|
|
||
|
|
# Map to answer field
|
||
|
|
self._map_answer_to_field(question.maps_to, parsed)
|
||
|
|
|
||
|
|
# Create log entry
|
||
|
|
log_entry = LogEntry(
|
||
|
|
timestamp=datetime.now(),
|
||
|
|
question_id=question.id,
|
||
|
|
question_text=question.text,
|
||
|
|
answer_raw=raw,
|
||
|
|
answer_parsed=parsed,
|
||
|
|
)
|
||
|
|
|
||
|
|
self.state_manager.append_log(log_entry)
|
||
|
|
self.state_manager.save_state(self.state)
|
||
|
|
|
||
|
|
def _map_answer_to_field(self, maps_to: str, value: Any) -> None:
|
||
|
|
"""Map parsed value to the appropriate answer field."""
|
||
|
|
if not maps_to:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Handle array indexing: "objectives[0].goal"
|
||
|
|
if "[" in maps_to:
|
||
|
|
import re
|
||
|
|
match = re.match(r"(\w+)\[(\d+)\]\.(\w+)", maps_to)
|
||
|
|
if match:
|
||
|
|
array_name, idx, field = match.groups()
|
||
|
|
idx = int(idx)
|
||
|
|
|
||
|
|
# Ensure array exists
|
||
|
|
if array_name not in self.state.answers:
|
||
|
|
self.state.answers[array_name] = []
|
||
|
|
|
||
|
|
# Ensure element exists
|
||
|
|
while len(self.state.answers[array_name]) <= idx:
|
||
|
|
self.state.answers[array_name].append({})
|
||
|
|
|
||
|
|
self.state.answers[array_name][idx][field] = value
|
||
|
|
return
|
||
|
|
|
||
|
|
# Handle nested fields: "constraints.max_stress"
|
||
|
|
if "." in maps_to:
|
||
|
|
parts = maps_to.split(".")
|
||
|
|
current = self.state.answers
|
||
|
|
|
||
|
|
for part in parts[:-1]:
|
||
|
|
if part not in current:
|
||
|
|
current[part] = {}
|
||
|
|
current = current[part]
|
||
|
|
|
||
|
|
current[parts[-1]] = value
|
||
|
|
return
|
||
|
|
|
||
|
|
# Simple field
|
||
|
|
self.state.set_answer(maps_to, value)
|
||
|
|
|
||
|
|
def _update_phase(self, question: Question) -> None:
|
||
|
|
"""Update interview phase based on question category."""
|
||
|
|
category_to_phase = {
|
||
|
|
"problem_definition": InterviewPhase.PROBLEM_DEFINITION,
|
||
|
|
"objectives": InterviewPhase.OBJECTIVES,
|
||
|
|
"constraints": InterviewPhase.CONSTRAINTS,
|
||
|
|
"design_variables": InterviewPhase.DESIGN_VARIABLES,
|
||
|
|
"physics_config": InterviewPhase.DESIGN_VARIABLES,
|
||
|
|
"optimization_settings": InterviewPhase.VALIDATION,
|
||
|
|
"validation": InterviewPhase.VALIDATION,
|
||
|
|
}
|
||
|
|
|
||
|
|
new_phase = category_to_phase.get(question.category)
|
||
|
|
if new_phase and new_phase != self.state.get_phase():
|
||
|
|
self.state.set_phase(new_phase)
|
||
|
|
|
||
|
|
def _update_complexity(self) -> None:
|
||
|
|
"""Update complexity estimate after initial questions."""
|
||
|
|
complexity = self.intelligence.determine_complexity(self.state, self.introspection)
|
||
|
|
self.state.complexity = complexity
|
||
|
|
|
||
|
|
# Adjust estimated questions
|
||
|
|
if complexity == "simple":
|
||
|
|
self.estimated_total_questions = 8
|
||
|
|
elif complexity == "moderate":
|
||
|
|
self.estimated_total_questions = 12
|
||
|
|
else:
|
||
|
|
self.estimated_total_questions = 16
|
||
|
|
|
||
|
|
def _finalize_interview(self) -> NextAction:
|
||
|
|
"""Finalize interview and show summary."""
|
||
|
|
self.state.set_phase(InterviewPhase.REVIEW)
|
||
|
|
|
||
|
|
blueprint = self.generate_blueprint()
|
||
|
|
|
||
|
|
return NextAction(
|
||
|
|
action_type="show_summary",
|
||
|
|
message=self.presenter.show_summary(blueprint),
|
||
|
|
blueprint=blueprint,
|
||
|
|
)
|
||
|
|
|
||
|
|
def _format_anti_pattern_warnings(self, patterns: List[AntiPattern]) -> str:
|
||
|
|
"""Format anti-pattern warnings for display."""
|
||
|
|
lines = ["**Issues Detected:**", ""]
|
||
|
|
|
||
|
|
for ap in patterns:
|
||
|
|
severity_icon = "X" if ap.severity == "error" else "!"
|
||
|
|
lines.append(f"[{severity_icon}] **{ap.name}**")
|
||
|
|
lines.append(f" {ap.description}")
|
||
|
|
if ap.fix_suggestion:
|
||
|
|
lines.append(f" *Suggestion*: {ap.fix_suggestion}")
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
lines.append("Would you like to proceed anyway? Type **yes** to continue or **no** to go back and fix.")
|
||
|
|
|
||
|
|
return "\n".join(lines)
|
||
|
|
|
||
|
|
def _get_category_name(self, category: str) -> str:
|
||
|
|
"""Get human-readable category name."""
|
||
|
|
names = {
|
||
|
|
"problem_definition": "Problem Definition",
|
||
|
|
"objectives": "Optimization Goals",
|
||
|
|
"constraints": "Constraints",
|
||
|
|
"design_variables": "Design Variables",
|
||
|
|
"physics_config": "Physics Configuration",
|
||
|
|
"optimization_settings": "Optimization Settings",
|
||
|
|
"validation": "Validation",
|
||
|
|
}
|
||
|
|
return names.get(category, category.replace("_", " ").title())
|
||
|
|
|
||
|
|
def _get_phase_name(self, phase: str) -> str:
|
||
|
|
"""Get human-readable phase name."""
|
||
|
|
names = {
|
||
|
|
"introspection": "Model Analysis",
|
||
|
|
"problem_definition": "Problem Definition",
|
||
|
|
"objectives": "Setting Objectives",
|
||
|
|
"constraints": "Defining Constraints",
|
||
|
|
"design_variables": "Selecting Variables",
|
||
|
|
"validation": "Validation",
|
||
|
|
"review": "Review & Confirm",
|
||
|
|
"complete": "Complete",
|
||
|
|
}
|
||
|
|
return names.get(phase, phase.replace("_", " ").title())
|
||
|
|
|
||
|
|
|
||
|
|
# Convenience function for quick interview
|
||
|
|
def run_interview(
|
||
|
|
study_path: Path,
|
||
|
|
study_name: str,
|
||
|
|
introspection: Optional[Dict[str, Any]] = None
|
||
|
|
) -> StudyInterviewEngine:
|
||
|
|
"""
|
||
|
|
Create and start an interview engine.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
study_path: Path to study directory
|
||
|
|
study_name: Study name
|
||
|
|
introspection: Optional introspection results
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Configured StudyInterviewEngine ready for use
|
||
|
|
"""
|
||
|
|
engine = StudyInterviewEngine(study_path)
|
||
|
|
engine.start_interview(study_name, introspection=introspection)
|
||
|
|
return engine
|