Fix SPEC.yaml validation and refactor guarded_ai.py for v2.0 consistency

SPEC.yaml fixes:
- Comment out orphaned example code blocks that broke YAML parsing
- Convert progressive hints and dynamic question examples to comments
- Add placeholder keys to maintain valid YAML structure
- All examples now documented but non-executable (reference only)
- Validates with 0 errors

guarded_ai.py refactor (CLI simulator now uses v2.0 features):
- Import activity_utils.py for consistency with activity.py
- Use check_conditions() for advanced metadata conditions (gte, lt, contains, etc.)
- Use filter_content_blocks() for template rendering and conditional blocks
- Use render_template() for dynamic question text with {{variables}}
- Use resolve_conditional_navigation() for if/elif/else navigation
- Use select_weighted_random() for weighted random selection
- Use get_progressive_hint() for progressive hints system
- Create template contexts with built-in variables (current_attempt, etc.)

Benefits:
- Single source of truth for v2.0 logic (activity_utils.py)
- CLI simulator now tests all v2.0 features
- Maintainability: changes to features only need updates in one place
- Consistency: web app and CLI behave identically

All changes validated and tested.
This commit is contained in:
Claude 2025-11-10 15:19:50 +00:00
parent 2b19fc5b9d
commit bd779e06fa
No known key found for this signature in database
2 changed files with 184 additions and 79 deletions

View file

@ -818,48 +818,49 @@ transitions:
# PROGRESSIVE HINTS SYSTEM
# ==============================================================================
# Built-in system for providing hints that escalate with attempts
# Define hints at step level:
- step_id: "difficult_question"
question: "What is the capital of Burkina Faso?"
# Progressive hints based on attempt number
hints:
- attempt: 1
text: "💡 Hint: It's not the largest city in the country."
counts_as_attempt: false # Showing hint doesn't count as failure
- attempt: 2
text: "💡 Hint: The name means 'City of Honest People'."
counts_as_attempt: false
- attempt: 3
text: "💡 Strong Hint: It starts with 'Oua' and ends with 'dougou'."
counts_as_attempt: false
buckets: [correct, incorrect, need_hint]
transitions:
correct:
content_blocks:
- "Excellent! Ouagadougou is correct!"
next_section_and_step: "next:step"
incorrect:
content_blocks:
- "Not quite. Try again!"
# Hint will auto-display based on current_attempt
next_section_and_step: "current:difficult_question"
need_hint:
content_blocks:
- "Let me help you..."
counts_as_attempt: false # Requesting hint doesn't count
next_section_and_step: "current:difficult_question"
#
# Example step with progressive hints:
#
# - step_id: "difficult_question"
# question: "What is the capital of Burkina Faso?"
#
# hints:
# - attempt: 1
# text: "💡 Hint: It's not the largest city in the country."
# counts_as_attempt: false
#
# - attempt: 2
# text: "💡 Hint: The name means 'City of Honest People'."
# counts_as_attempt: false
#
# - attempt: 3
# text: "💡 Strong Hint: It starts with 'Oua' and ends with 'dougou'."
# counts_as_attempt: false
#
# buckets: [correct, incorrect, need_hint]
#
# transitions:
# correct:
# content_blocks:
# - "Excellent! Ouagadougou is correct!"
# next_section_and_step: "next:step"
#
# incorrect:
# content_blocks:
# - "Not quite. Try again!"
# next_section_and_step: "current:difficult_question"
#
# need_hint:
# content_blocks:
# - "Let me help you..."
# counts_as_attempt: false
# next_section_and_step: "current:difficult_question"
#
# Hints auto-display when attempt number matches
# Hints support template variables: "Attempt {{current_attempt}} of {{max_attempts}}"
progressive_hints_example: "See activity-test-v2-features.yaml for working example"
# ==============================================================================
# WEIGHTED RANDOM SELECTION
# ==============================================================================
@ -927,29 +928,32 @@ question: "What is 2 + 2?"
question: "What is {{metadata.num1}} + {{metadata.num2}}?"
# Example: Math quiz with random numbers
- step_id: "addition"
pre_script: |
import random
result = {
"metadata": {
"num1": random.randint(1, 10),
"num2": random.randint(1, 10)
}
}
return result
#
# - step_id: "addition"
# pre_script: |
# import random
# result = {
# "metadata": {
# "num1": random.randint(1, 10),
# "num2": random.randint(1, 10)
# }
# }
# return result
#
# question: "What is {{metadata.num1}} + {{metadata.num2}}?"
#
# tokens_for_ai: |
# Calculate the correct answer: {{metadata.num1}} + {{metadata.num2}}
# Categorize as 'correct' if their answer matches.
#
# buckets: [correct, incorrect]
#
# More personalized question examples:
# - "{{metadata.character_name}}, what is your quest?"
# - "You have {{metadata.gold}} gold. How much do you spend?"
# - "Round {{current_attempt}}: What's your move?"
question: "What is {{metadata.num1}} + {{metadata.num2}}?"
tokens_for_ai: |
Calculate the correct answer: {{metadata.num1}} + {{metadata.num2}}
Categorize as 'correct' if their answer matches.
buckets: [correct, incorrect]
# Example: Personalized questions
question: "{{metadata.character_name}}, what is your quest?"
question: "You have {{metadata.gold}} gold. How much do you spend?"
question: "Round {{current_attempt}}: What's your move?"
dynamic_question_example: "See activity-test-v2-features.yaml for working example"
# ==============================================================================
# BUILT-IN ATTEMPT COUNTER ACCESS

View file

@ -3,8 +3,23 @@ import yaml
import json
import random
import os
import sys
from openai import OpenAI
# Add parent directory to path to import activity_utils
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Import activity utilities for v2.0 features
from activity_utils import (
render_template,
check_conditions,
filter_content_blocks,
resolve_conditional_navigation,
select_weighted_random,
get_progressive_hint,
create_template_context
)
# Global model-client mapping
MODEL_CLIENT_MAP = {}
@ -366,11 +381,32 @@ def simulate_activity(yaml_file_path):
# Get the user's language preference from metadata
user_language = metadata.get("language", "English")
# Translate and print all content blocks once per step
# Initialize attempts and max_attempts for this step
attempts = 0
step_max_attempts = step.get("max_attempts_per_step", max_attempts)
# Create template context for rendering
context = create_template_context(
metadata=metadata,
current_attempt=attempts,
max_attempts=step_max_attempts,
current_section=current_section_id,
current_step=current_step_id,
username="User"
)
# Translate and print all content blocks once per step (v2.0 with templates & conditionals)
if "content_blocks" in step:
content = "\n\n".join(step["content_blocks"])
translated_content = translate_text(content, user_language, feedback_model)
print(translated_content)
# Filter and render content blocks
filtered_blocks = filter_content_blocks(
step["content_blocks"],
metadata,
context
)
if filtered_blocks:
content = "\n\n".join(filtered_blocks)
translated_content = translate_text(content, user_language, feedback_model)
print(translated_content)
# Skip classification and feedback if there's no question
if "question" not in step:
@ -379,12 +415,22 @@ def simulate_activity(yaml_file_path):
)
continue
question = step["question"]
# Render template variables in question (v2.0)
question = render_template(step["question"], context)
translated_question = translate_text(question, user_language, feedback_model)
print(f"\nQuestion: {translated_question}")
attempts = 0
while attempts < max_attempts:
while attempts < step_max_attempts:
# Update context with current attempt
context = create_template_context(
metadata=metadata,
current_attempt=attempts + 1, # 1-indexed for display
max_attempts=step_max_attempts,
current_section=current_section_id,
current_step=current_step_id,
username="User"
)
user_response = input("\nYour Response: ")
# Roll for random buckets BEFORE categorization
@ -470,24 +516,42 @@ def simulate_activity(yaml_file_path):
print(f"Processing transition for bucket: '{bucket_name}'")
print(f"{'='*60}")
# Check metadata conditions
# Check metadata conditions (v2.0 advanced conditions)
if "metadata_conditions" in transition:
conditions_met = all(
metadata.get(key) == value
for key, value in transition["metadata_conditions"].items()
conditions_met = check_conditions(
metadata,
transition["metadata_conditions"]
)
if not conditions_met:
print(f"⚠️ Skipping '{bucket_name}' - metadata conditions not met")
print(f"Current Metadata: {json.dumps(metadata, indent=2)}")
continue
# Print transition content blocks if they exist
# Print transition content blocks if they exist (v2.0 with templates & conditionals)
if "content_blocks" in transition:
transition_content = "\n\n".join(transition["content_blocks"])
translated_transition_content = translate_text(
transition_content, user_language, feedback_model
# Create template context
context = create_template_context(
metadata=metadata,
current_attempt=attempts,
max_attempts=max_attempts,
current_section=current_section_id,
current_step=current_step_id,
username="User"
)
print(translated_transition_content)
# Filter and render content blocks (supports conditional blocks and templates)
filtered_blocks = filter_content_blocks(
transition["content_blocks"],
metadata,
context
)
if filtered_blocks:
transition_content = "\n\n".join(filtered_blocks)
translated_transition_content = translate_text(
transition_content, user_language, feedback_model
)
print(translated_transition_content)
# Update metadata based on user actions
if "metadata_add" in transition:
@ -604,6 +668,19 @@ def simulate_activity(yaml_file_path):
metadata[random_key] = random_value
metadata_tmp_keys.append(random_key) # Track temporary keys
# Handle metadata_weighted_random (v2.0)
if "metadata_weighted_random" in transition:
for key, weighted_options in transition["metadata_weighted_random"].items():
selected_value = select_weighted_random(weighted_options)
metadata[key] = selected_value
# Handle metadata_tmp_weighted_random (v2.0)
if "metadata_tmp_weighted_random" in transition:
for key, weighted_options in transition["metadata_tmp_weighted_random"].items():
selected_value = select_weighted_random(weighted_options)
metadata[key] = selected_value
metadata_tmp_keys.append(key)
# Execute the processing script if it exists
if "processing_script" in step and transition.get(
"run_processing_script", False
@ -674,6 +751,24 @@ def simulate_activity(yaml_file_path):
# End of multi-bucket processing loop
# Check for progressive hints (v2.0)
if "hints" in step and attempts > 0:
hint_context = create_template_context(
metadata=metadata,
current_attempt=attempts + 1, # Next attempt
max_attempts=step_max_attempts,
current_section=current_section_id,
current_step=current_step_id,
username="User"
)
hint = get_progressive_hint(step["hints"], attempts + 1, hint_context)
if hint:
translated_hint = translate_text(hint['text'], user_language, feedback_model)
print(f"\n💡 Hint: {translated_hint}")
# If hint doesn't count as attempt, adjust counting
if not hint['counts_as_attempt']:
any_counts_as_attempt = False
# Check if we should break or continue attempting
if category not in [
"partial_understanding",
@ -688,7 +783,7 @@ def simulate_activity(yaml_file_path):
if any_counts_as_attempt:
attempts += 1
if attempts == max_attempts:
if attempts == step_max_attempts:
print("\nMaximum attempts reached. Moving to the next step.")
# Remove temporary metadata at the end of the step
@ -697,8 +792,14 @@ def simulate_activity(yaml_file_path):
del metadata[key]
# Use the final navigation target (from LAST processed transition)
# v2.0: Resolve conditional navigation
if final_next_section_and_step:
current_section_id, current_step_id = final_next_section_and_step.split(":")
resolved_navigation = resolve_conditional_navigation(
final_next_section_and_step,
metadata
)
if resolved_navigation:
current_section_id, current_step_id = resolved_navigation.split(":")
else:
# No navigation specified, move to next step automatically
current_section_id, current_step_id = get_next_section_and_step(