diff --git a/CLAUDE.md b/CLAUDE.md index 26b90a7..7138bd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,7 @@ ## Commit Messages - NEVER add Claude attributions like "🤖 Generated with Claude Code" to commit messages +- NEVER add "Co-Authored-By: Claude " to commit messages - Keep commit messages focused on the actual changes and their purpose - Use conventional commit format when appropriate - Be concise but descriptive about what was changed and why diff --git a/Makefile b/Makefile index 7a08317..3fb13b2 100644 --- a/Makefile +++ b/Makefile @@ -201,6 +201,19 @@ clean: find . -name "*.pyc" -delete 2>/dev/null || true find . -name "*.pyo" -delete 2>/dev/null || true find . -name "*~" -delete 2>/dev/null || true + +.PHONY: init-db +init-db: + @echo "🗄️ Initializing database tables..." + @if [ -f vars.sh ]; then \ + . ./vars.sh && python init_db.py; \ + echo "✅ Database tables created successfully"; \ + else \ + echo "❌ Error: vars.sh not found. Please create it from vars.sh.sample"; \ + exit 1; \ + fi + +clean-cache: rm -rf .pytest_cache/ 2>/dev/null || true rm -rf htmlcov/ 2>/dev/null || true rm -rf .coverage 2>/dev/null || true diff --git a/activity_yaml_validator.py b/activity_yaml_validator.py index ea3b53c..3fd84c7 100644 --- a/activity_yaml_validator.py +++ b/activity_yaml_validator.py @@ -242,6 +242,12 @@ class ActivityYAMLValidator: f"Section {section_id}, step {step_id}: 'feedback_tokens_for_ai' must be a string" ) + # Validate feedback_prompts (new multi-prompt system) + if "feedback_prompts" in step: + self._validate_feedback_prompts( + step["feedback_prompts"], section_id, step_id + ) + # Validate buckets and transitions if "buckets" in step: self._validate_buckets(step["buckets"], section_id, step_id) @@ -251,6 +257,75 @@ class ActivityYAMLValidator: step["transitions"], step.get("buckets", []), section_id, step_id ) + def _validate_feedback_prompts( + self, feedback_prompts: List[Dict[str, Any]], section_id: str, step_id: str + ): + """Validate feedback_prompts structure""" + if not isinstance(feedback_prompts, list): + self.errors.append( + f"Section {section_id}, step {step_id}: 'feedback_prompts' must be a list" + ) + return + + if len(feedback_prompts) == 0: + self.errors.append( + f"Section {section_id}, step {step_id}: 'feedback_prompts' cannot be empty" + ) + return + + prompt_names = set() + for i, prompt in enumerate(feedback_prompts): + if not isinstance(prompt, dict): + self.errors.append( + f"Section {section_id}, step {step_id}: feedback_prompts[{i}] must be a dictionary" + ) + continue + + # Required fields for each prompt + required_fields = ["name", "tokens_for_ai"] + for field in required_fields: + if field not in prompt: + self.errors.append( + f"Section {section_id}, step {step_id}: feedback_prompts[{i}] missing required field '{field}'" + ) + + # Validate name uniqueness + if "name" in prompt: + if not isinstance(prompt["name"], str): + self.errors.append( + f"Section {section_id}, step {step_id}: feedback_prompts[{i}].name must be a string" + ) + else: + if prompt["name"] in prompt_names: + self.errors.append( + f"Section {section_id}, step {step_id}: duplicate feedback prompt name '{prompt['name']}'" + ) + prompt_names.add(prompt["name"]) + + # Validate tokens_for_ai + if "tokens_for_ai" in prompt: + if not isinstance(prompt["tokens_for_ai"], str): + self.errors.append( + f"Section {section_id}, step {step_id}: feedback_prompts[{i}].tokens_for_ai must be a string" + ) + # Check for STFU token usage (informational) + elif "STFU" in prompt["tokens_for_ai"]: + # This is valid - STFU token is used to suppress empty feedback messages + pass + + # Validate metadata_filter (optional) + if "metadata_filter" in prompt: + if not isinstance(prompt["metadata_filter"], list): + self.errors.append( + f"Section {section_id}, step {step_id}: feedback_prompts[{i}].metadata_filter must be a list" + ) + else: + for j, filter_key in enumerate(prompt["metadata_filter"]): + if not isinstance(filter_key, str): + self.errors.append( + f"Section {section_id}, step {step_id}: feedback_prompts[{i}].metadata_filter[{j}] must be a string" + ) + def _validate_buckets(self, buckets: List[str], section_id: str, step_id: str): """Validate buckets list""" if not isinstance(buckets, list): diff --git a/app.py b/app.py index e38f553..0925fdb 100644 --- a/app.py +++ b/app.py @@ -22,6 +22,8 @@ from flask import ( send_from_directory, jsonify, Response, + redirect, + url_for, ) from flask_socketio import SocketIO, emit, join_room, leave_room @@ -31,10 +33,12 @@ from sqlalchemy.exc import InvalidRequestError from models import db, Room, UserSession, Message, ActivityState -app = Flask(__name__) +app = Flask(__name__, instance_relative_config=True) app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-key-change-in-production") -app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///chat.db" +app.config["SQLALCHEMY_DATABASE_URI"] = ( + f"sqlite:///{os.path.join(app.instance_path, 'chat.db')}" +) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db.init_app(app) @@ -233,6 +237,28 @@ def get_models(): return jsonify({"models": list(MODEL_CLIENT_MAP.keys())}) +@app.route("/api/activities", methods=["GET"]) +def get_activities(): + """Return the list of available activities.""" + activities = [] + + if app.config.get("LOCAL_ACTIVITIES"): + # List local activity files from research directory + import os + + research_dir = "research" + if os.path.exists(research_dir): + for filename in sorted(os.listdir(research_dir)): + if filename.endswith((".yaml", ".yml")): + activities.append(f"research/{filename}") + else: + # For S3 activities, you would list from S3 + # This is a placeholder - you'd need to implement S3 listing + pass + + return jsonify({"activities": activities}) + + @app.route("/chat/") def chat(room_name): # Query all rooms so that newest is first. @@ -341,6 +367,25 @@ def search_page(): # Call the function to search messages search_results = search_messages(keywords) + # If there's exactly one search result, redirect directly to that room + if len(search_results) == 1: + room_result = search_results[0] + room_name = room_result["room_name"] + + # Build the redirect URL with current parameters + redirect_params = {} + if username and username != "guest": + redirect_params["username"] = username + + # Preserve other URL parameters like model, voice, etc. + for param in ["model", "voice"]: + value = request.args.get(param) + if value: + redirect_params[param] = value + + redirect_url = url_for("chat", room_name=room_name, **redirect_params) + return redirect(redirect_url) + return render_template( "search.html", rooms=rooms, @@ -651,6 +696,30 @@ def handle_update_message(data): ) +@socketio.on("get_activity_status") +def handle_get_activity_status(data): + """Get the current activity status for a room.""" + room_name = data["room_name"] + room = get_room(room_name) + + if room: + activity_state = ActivityState.query.filter_by(room_id=room.id).first() + + if activity_state: + emit( + "activity_status", + { + "active": True, + "activity_name": activity_state.s3_file_path, + "section_id": activity_state.section_id, + "step_id": activity_state.step_id, + }, + room=request.sid, + ) + else: + emit("activity_status", {"active": False}, room=request.sid) + + def group_consecutive_roles(messages): if not messages: return [] @@ -763,7 +832,10 @@ def chat_claude( "message_chunk", { "id": msg_id, - "content": f"**{username} ({model_name}):**\n\n{content}", + "content": content, + "username": username, + "model_name": model_name, + "is_first_chunk": True, }, room=room.name, ) @@ -921,7 +993,10 @@ def chat_gpt(username, room_name, model_name="gpt-4o-mini"): "message_chunk", { "id": msg_id, - "content": f"**{username} ({model_name}):**\n\n{content}", + "content": content, + "username": username, + "model_name": model_name, + "is_first_chunk": True, }, room=room.name, ) @@ -1035,7 +1110,10 @@ def chat_llama(username, room_name, model_name="mistral-7b-instruct-v0.2.Q3_K_L. "message_chunk", { "id": msg_id, - "content": f"**{username} ({model_name}):**\n\n{content}", + "content": content, + "username": username, + "model_name": model_name, + "is_first_chunk": True, }, room=room.name, ) @@ -1542,12 +1620,14 @@ def loop_through_steps_until_question( # Check if the current step has a question if "question" in step: - question_content = f"Question: {step['question']}" + question_content = step["question"] translated_question_content = translate_text( question_content, user_language ) new_message = Message( - username="System", content=translated_question_content, room_id=room.id + username="System (Question)", + content=translated_question_content, + room_id=room.id, ) db.session.add(new_message) db.session.commit() @@ -1596,6 +1676,8 @@ def loop_through_steps_until_question( }, room=room_name, ) + # Return to activity chooser + socketio.emit("activity_status", {"active": False}, room=room_name) break @@ -1623,6 +1705,18 @@ def start_activity(room_name, s3_file_path, username): activity_content, activity_state, room_name, username ) + # Emit activity status update + socketio.emit( + "activity_status", + { + "active": True, + "activity_name": s3_file_path, + "section_id": initial_section["section_id"], + "step_id": initial_step["step_id"], + }, + room=room_name, + ) + def cancel_activity(room_name, username): with app.app_context(): @@ -1656,6 +1750,9 @@ def cancel_activity(room_name, username): room=room_name, ) + # Emit activity status update + socketio.emit("activity_status", {"active": False}, room=room_name) + def display_activity_metadata(room_name, username): with app.app_context(): @@ -2088,33 +2185,57 @@ def handle_activity_response(room_name, user_response, username): # if "correct" or max_attempts reached. # Provide feedback based on the category - # Filter metadata for feedback if metadata_feedback_filter is specified - feedback_metadata = activity_state.dict_metadata - if "metadata_feedback_filter" in transition: - filter_keys = transition["metadata_feedback_filter"] - feedback_metadata = { - k: v - for k, v in activity_state.dict_metadata.items() - if k in filter_keys - } + # Handle feedback systems + feedback_messages = [] - feedback = provide_feedback( - transition, - category, - step["question"], - feedback_tokens_for_ai, - user_response, - user_language, - username, - json.dumps(feedback_metadata), - json.dumps(new_metadata), - ) + if "feedback_prompts" in step: + # New multi-prompt system - pass full metadata, let each prompt filter + multi_feedback_messages = provide_feedback_prompts( + transition, + category, + step["question"], + step["feedback_prompts"], + user_response, + user_language, + username, + json.dumps(activity_state.dict_metadata), # Pass full metadata + json.dumps(new_metadata), + feedback_tokens_for_ai, # Pass legacy tokens to be combined + ) + feedback_messages.extend(multi_feedback_messages) + elif feedback_tokens_for_ai: + # Legacy single feedback system - use transition-level filtering + feedback_metadata = activity_state.dict_metadata + if "metadata_feedback_filter" in transition: + filter_keys = transition["metadata_feedback_filter"] + feedback_metadata = { + k: v + for k, v in activity_state.dict_metadata.items() + if k in filter_keys + } - # Store and emit the feedback - if feedback: - # feedback is metadata language aware, doesn't need to be translated. + feedback = provide_feedback( + transition, + category, + step["question"], + feedback_tokens_for_ai, + user_response, + user_language, + username, + json.dumps(feedback_metadata), + json.dumps(new_metadata), + ) + if feedback and feedback.strip(): + feedback_messages.append( + {"name": "Feedback", "content": feedback} + ) + + # Store and emit all feedback messages + for feedback_msg in feedback_messages: new_message = Message( - username="System", content=feedback, room_id=room.id + username=f"System ({feedback_msg['name'].title()})", + content=feedback_msg["content"], + room_id=room.id, ) db.session.add(new_message) db.session.commit() @@ -2123,8 +2244,8 @@ def handle_activity_response(room_name, user_response, username): "chat_message", { "id": new_message.id, - "username": "System", - "content": feedback, + "username": f"System ({feedback_msg['name'].title()})", + "content": feedback_msg["content"], }, room=room_name, ) @@ -2199,12 +2320,12 @@ def handle_activity_response(room_name, user_response, username): db.session.commit() # Emit the question again - question_content = f"Question: {step['question']}" + question_content = step["question"] translated_question_content = translate_text( question_content, user_language ) new_message = Message( - username="System", + username="System (Question)", content=translated_question_content, room_id=room.id, ) @@ -2517,11 +2638,131 @@ def provide_feedback( json_metadata, json_new_metadata, ) - feedback += f"\n\nAI Feedback: {ai_feedback}" + feedback += f"\n\n{ai_feedback}" return feedback +def provide_feedback_prompts( + transition, + category, + question, + feedback_prompts, + user_response, + user_language, + username, + json_metadata, + json_new_metadata, + legacy_tokens_for_ai="", +): + """Generate feedback from multiple prompts""" + feedback_messages = [] + + # Parse full metadata once for filtering + full_metadata = json.loads(json_metadata) + + # Add user_response to metadata for filtering purposes + full_metadata["user_response"] = user_response + + for prompt in feedback_prompts: + prompt_name = prompt.get("name", "unnamed") + tokens_for_ai = prompt.get("tokens_for_ai", "") + + # Apply per-prompt metadata filtering if specified + prompt_metadata = full_metadata + if "metadata_filter" in prompt: + filter_keys = prompt["metadata_filter"] + prompt_metadata = { + k: v for k, v in full_metadata.items() if k in filter_keys + } + + # Check skip condition if specified + skip_condition = prompt.get("skip_condition") + if skip_condition: + should_skip = False + values = list(prompt_metadata.values()) + + if skip_condition == "all_null": + should_skip = all(value is None or value == "" or value == "None" for value in values) + elif skip_condition == "all_false": + should_skip = all(value is False or value == "False" for value in values) + elif skip_condition == "all_true": + should_skip = all(value is True or value == "True" for value in values) + + if should_skip: + print(f"DEBUG: Skipping prompt '{prompt_name}' - skip_condition '{skip_condition}' met") + continue + + # Special debug for Ship Status and Game Over + if prompt_name == "Ship Status": + print(f"DEBUG SHIP STATUS - filter_keys: {filter_keys}") + print(f"DEBUG SHIP STATUS - filtered metadata: {prompt_metadata}") + print( + f"DEBUG SHIP STATUS - user_sunk_ship_this_round = '{prompt_metadata.get('user_sunk_ship_this_round')}'" + ) + print( + f"DEBUG SHIP STATUS - ai_sunk_ship_this_round = '{prompt_metadata.get('ai_sunk_ship_this_round')}'" + ) + elif prompt_name == "Game Over": + print(f"DEBUG GAME OVER - filter_keys: {filter_keys}") + print(f"DEBUG GAME OVER - filtered metadata: {prompt_metadata}") + print( + f"DEBUG GAME OVER - game_over = '{prompt_metadata.get('game_over')}'" + ) + print( + f"DEBUG GAME OVER - user_wins = '{prompt_metadata.get('user_wins')}'" + ) + print(f"DEBUG GAME OVER - ai_wins = '{prompt_metadata.get('ai_wins')}'") + else: + if prompt_name == "Ship Status": + print( + f"DEBUG SHIP STATUS - NO metadata_filter, full metadata: {prompt_metadata}" + ) + elif prompt_name == "Game Over": + print( + f"DEBUG GAME OVER - NO metadata_filter, full metadata: {prompt_metadata}" + ) + + # Combine legacy tokens with prompt-specific tokens + if legacy_tokens_for_ai: + tokens_for_ai = legacy_tokens_for_ai + " " + tokens_for_ai + + # Add language instruction + tokens_for_ai += ( + f" You must provide the feedback in the user's language: {user_language}." + ) + + # Add transition-specific AI feedback if present + if "ai_feedback" in transition: + tokens_for_ai += f" {transition['ai_feedback'].get('tokens_for_ai', '')}" + + # Determine user_response for this prompt based on metadata filtering + filtered_user_response = user_response + if ( + "metadata_filter" in prompt + and "user_response" not in prompt["metadata_filter"] + ): + filtered_user_response = "" # Remove user response if not in filter + + ai_feedback = generate_ai_feedback( + category, + question, + filtered_user_response, + tokens_for_ai, + username, + json.dumps(prompt_metadata), # Use filtered metadata for this prompt + json_new_metadata, + ) + + # Only add feedback if it has content + if ai_feedback and ai_feedback.strip(): + feedback_messages.append( + {"name": prompt_name, "content": ai_feedback.strip()} + ) + + return feedback_messages + + def translate_text(text, target_language): # Guard clause for default language target_language = target_language.lower().split() diff --git a/research/activity29-battleship.yaml b/research/activity29-battleship.yaml index 997275d..81ef5dd 100644 --- a/research/activity29-battleship.yaml +++ b/research/activity29-battleship.yaml @@ -187,21 +187,57 @@ sections: If the user wants to exit, categorize as 'exit'. Otherwise, categorize as 'invalid_move'. feedback_tokens_for_ai: | - Write battleship feedback from the game's perspective that covers: - - 1. User's shot result - check user_hit_result in metadata: - - If "hit": Describe the impact and explosion - - If "miss": Describe the splash and fog of war - 2. AI's shot result - report where the AI fired: - - If hit: Describe the damage to the player's ship - - If miss: Describe the near miss and ocean spray - 3. CRITICAL: If ai_sunk_ship_this_round contains a ship name, express dismay that the AI destroyed the player's ship in 2 sentences describing the carnage at sea - 4. CRITICAL: If user_sunk_ship_this_round contains a ship name, celebrate the player destroying the AI ship in 2 sentences describing the carnage at sea - 5. CRITICAL: If game_over is true, announce the victory: - - If user_wins is true: Celebrate the player's total victory with excitement! - - If ai_wins is true: Express dismay at the player's defeat! - - Describe the sights and sounds of naval warfare! You are the game system rooting for the player! + You are a battleship narrator. Each prompt has its own specific role - follow the individual prompt instructions precisely. + feedback_prompts: + - name: "Shot Report" + tokens_for_ai: | + 🎯 Report ONLY the hit/miss results for both shots this turn. DO NOT report ship sinking or game over. + + Check metadata: + - user_shot: Player's target position + - user_hit_result: "hit" or "miss" + - ai_shot: AI's target position + - ai_hit_result: "hit" or "miss" + + Format: "🎯 Your shot at position [user_shot]: [user_hit_result]! 🤖 Enemy shot at position [ai_shot]: [ai_hit_result]!" + metadata_filter: + - user_shot + - ai_shot + - user_hit_result + - ai_hit_result + - user_response + + - name: "Ship Status" + tokens_for_ai: | + A ship has been destroyed! Generate a dramatic 2-3 sentence description. + + Metadata tells you which ship(s) were sunk: + - user_sunk_ship_this_round: The ship YOU destroyed (your victory) + - ai_sunk_ship_this_round: The ship that was destroyed by the enemy (your loss) + + Format: + - If user destroyed a ship: "💥 You've sunk their [ship]! [2-3 dramatic sentences imagining how this warship meets its doom]" + - If AI destroyed your ship: "🔥 Your [ship] has been destroyed! [2-3 dramatic sentences about its destruction]" + - If both: Include both messages + metadata_filter: + - user_sunk_ship_this_round + - ai_sunk_ship_this_round + skip_condition: "all_null" + + - name: "Game Over" + tokens_for_ai: | + The naval battle has ended! Generate an epic conclusion. + + Based on the metadata: + - If user_wins is true: "🎉 TOTAL VICTORY! You have destroyed all enemy ships! The seas belong to you, Admiral. Your tactical brilliance has secured complete naval supremacy." + - If ai_wins is true: "💀 DEFEAT! All your ships have been destroyed. Your fleet lies scattered across the ocean floor. The enemy's superior strategy has prevailed." + + Make it dramatic and final - this is the end of the battle! + metadata_filter: + - game_over + - user_wins + - ai_wins + skip_condition: "all_false" processing_script: | import random @@ -846,16 +882,6 @@ sections: The user shot seems valid. metadata_tmp_add: user_shot: "the-users-response" - metadata_feedback_filter: - - user_hit_result - - ai_hit_result - - ai_shot - - user_shot - - user_sunk_ship_this_round - - ai_sunk_ship_this_round - - game_over - - user_wins - - ai_wins next_section_and_step: "section_1:step_2" invalid_move: content_blocks: @@ -877,8 +903,6 @@ sections: tokens_for_ai: | If the user wants to restart or play again, categorize as 'restart'. If the user wants to exit, categorize as 'exit'. - feedback_tokens_for_ai: | - Acknowledge the user's choice appropriately. buckets: - restart - exit diff --git a/research/activity29-testship.yaml b/research/activity29-testship.yaml index ec6d0a0..4224b17 100644 --- a/research/activity29-testship.yaml +++ b/research/activity29-testship.yaml @@ -165,21 +165,57 @@ sections: If the user wants to exit, categorize as 'exit'. Otherwise, categorize as 'invalid_move'. feedback_tokens_for_ai: | - Write battleship feedback from the game's perspective that covers: - - 1. User's shot result - check user_hit_result in metadata: - - If "hit": Describe the impact and explosion - - If "miss": Describe the splash and fog of war - 2. AI's shot result - report where the AI fired: - - If hit: Describe the damage to the player's ship - - If miss: Describe the near miss and ocean spray - 3. CRITICAL: If ai_sunk_ship_this_round contains a ship name, express dismay that the AI destroyed the player's ship in 2 sentences describing the carnage at sea - 4. CRITICAL: If user_sunk_ship_this_round contains a ship name, celebrate the player destroying the AI ship in 2 sentences describing the carnage at sea - 5. CRITICAL: If game_over is true, announce the victory: - - If user_wins is true: Celebrate the player's total victory with excitement! - - If ai_wins is true: Express dismay at the player's defeat! - - Describe the sights and sounds of naval warfare! You are the game system rooting for the player! + You are a battleship narrator. Each prompt has its own specific role - follow the individual prompt instructions precisely. + feedback_prompts: + - name: "Shot Report" + tokens_for_ai: | + 🎯 Report ONLY the hit/miss results for both shots this turn. DO NOT report ship sinking or game over. + + Check metadata: + - user_shot: Player's target position + - user_hit_result: "hit" or "miss" + - ai_shot: AI's target position + - ai_hit_result: "hit" or "miss" + + Format: "🎯 Your shot at position [user_shot]: [user_hit_result]! 🤖 Enemy shot at position [ai_shot]: [ai_hit_result]!" + metadata_filter: + - user_shot + - ai_shot + - user_hit_result + - ai_hit_result + - user_response + + - name: "Ship Status" + tokens_for_ai: | + A ship has been destroyed! Generate a dramatic 2-3 sentence description. + + Metadata tells you which ship(s) were sunk: + - user_sunk_ship_this_round: The ship YOU destroyed (your victory) + - ai_sunk_ship_this_round: The ship that was destroyed by the enemy (your loss) + + Format: + - If user destroyed a ship: "💥 You've sunk their [ship]! [2-3 dramatic sentences imagining how this warship meets its doom]" + - If AI destroyed your ship: "🔥 Your [ship] has been destroyed! [2-3 dramatic sentences about its destruction]" + - If both: Include both messages + metadata_filter: + - user_sunk_ship_this_round + - ai_sunk_ship_this_round + skip_condition: "all_null" + + - name: "Game Over" + tokens_for_ai: | + The naval battle has ended! Generate an epic conclusion. + + Based on the metadata: + - If user_wins is true: "🎉 TOTAL VICTORY! You have destroyed all enemy ships! The seas belong to you, Admiral. Your tactical brilliance has secured complete naval supremacy." + - If ai_wins is true: "💀 DEFEAT! All your ships have been destroyed. Your fleet lies scattered across the ocean floor. The enemy's superior strategy has prevailed." + + Make it dramatic and final - this is the end of the battle! + metadata_filter: + - game_over + - user_wins + - ai_wins + skip_condition: "all_false" processing_script: | import random @@ -814,16 +850,6 @@ sections: The user shot seems valid. metadata_tmp_add: user_shot: "the-users-response" - metadata_feedback_filter: - - user_hit_result - - ai_hit_result - - ai_shot - - user_shot - - user_sunk_ship_this_round - - ai_sunk_ship_this_round - - game_over - - user_wins - - ai_wins next_section_and_step: "section_1:step_2" invalid_move: content_blocks: @@ -845,8 +871,6 @@ sections: tokens_for_ai: | If the user wants to restart or play again, categorize as 'restart'. If the user wants to exit, categorize as 'exit'. - feedback_tokens_for_ai: | - Acknowledge the user's choice appropriately. buckets: - restart - exit diff --git a/research/guarded_ai.py b/research/guarded_ai.py index f5e3723..4a3054b 100644 --- a/research/guarded_ai.py +++ b/research/guarded_ai.py @@ -122,7 +122,7 @@ def generate_ai_feedback(category, question, user_response, tokens_for_ai, metad return f"Error: {e}" -# Provide feedback based on the category +# Provide feedback based on the category (legacy single feedback system) def provide_feedback( transition, category, @@ -150,6 +150,68 @@ def provide_feedback( return feedback +# Provide feedback using multiple prompts (new system) +def provide_feedback_prompts( + transition, + category, + question, + feedback_prompts, + user_response, + user_language, + metadata, + legacy_tokens_for_ai="", +): + """Generate feedback from multiple prompts""" + feedback_messages = [] + + # Add user_response to metadata for filtering purposes + full_metadata = metadata.copy() + full_metadata["user_response"] = user_response + + for prompt in feedback_prompts: + prompt_name = prompt.get("name", "unnamed") + tokens_for_ai = prompt.get("tokens_for_ai", "") + + # Apply per-prompt metadata filtering if specified + prompt_metadata = full_metadata + if "metadata_filter" in prompt: + filter_keys = prompt["metadata_filter"] + prompt_metadata = { + k: v for k, v in full_metadata.items() if k in filter_keys + } + + # Combine legacy tokens with prompt-specific tokens + if legacy_tokens_for_ai: + tokens_for_ai = legacy_tokens_for_ai + " " + tokens_for_ai + + # Add language instruction + tokens_for_ai += f" Provide the feedback in {user_language}." + + # Add transition-specific AI feedback if present + if "ai_feedback" in transition: + tokens_for_ai += f" {transition['ai_feedback'].get('tokens_for_ai', '')}" + + # Determine user_response for this prompt based on metadata filtering + filtered_user_response = user_response + if ( + "metadata_filter" in prompt + and "user_response" not in prompt["metadata_filter"] + ): + filtered_user_response = "" # Remove user response if not in filter + + ai_feedback = generate_ai_feedback( + category, question, filtered_user_response, tokens_for_ai, prompt_metadata + ) + + # Only add feedback if it has content and isn't exactly the STFU token + if ai_feedback and ai_feedback.strip() and ai_feedback.strip() != "STFU": + feedback_messages.append( + {"name": prompt_name, "content": ai_feedback.strip()} + ) + + return feedback_messages + + def execute_processing_script(metadata, script): # Prepare the local environment for the script local_env = {"metadata": metadata, "script_result": None} @@ -413,16 +475,40 @@ def simulate_activity(yaml_file_path): print(f"\nMetadata: {json.dumps(metadata, indent=2)}") # Provide feedback based on the category - feedback = provide_feedback( - transition, - category, - question, - user_response, - user_language, - step.get("feedback_tokens_for_ai", ""), - metadata, - ) - print(f"\nFeedback: {feedback}") + feedback_messages = [] + + if "feedback_prompts" in step: + # New multi-prompt system - legacy tokens get combined with each prompt + multi_feedback_messages = provide_feedback_prompts( + transition, + category, + question, + step["feedback_prompts"], + user_response, + user_language, + metadata, + step.get( + "feedback_tokens_for_ai", "" + ), # Pass legacy tokens to be combined + ) + feedback_messages.extend(multi_feedback_messages) + elif step.get("feedback_tokens_for_ai"): + # Legacy single feedback system - only if no feedback_prompts + feedback = provide_feedback( + transition, + category, + question, + user_response, + user_language, + step.get("feedback_tokens_for_ai", ""), + metadata, + ) + if feedback and feedback.strip(): + feedback_messages.append({"name": "Feedback", "content": feedback}) + + # Display all feedback messages + for feedback_msg in feedback_messages: + print(f"\n{feedback_msg['name']}: {feedback_msg['content']}") if category not in [ "partial_understanding", diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..afb4d86 --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,12 @@ +/** + * Utility functions for the OpenCompletion application + */ + +/** + * Convert a string to a URL-friendly slug + * @param {string} str - The string to slugify + * @returns {string} - The slugified string + */ +function slugify(str) { + return str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, ''); +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 10684ba..71c754a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -22,6 +22,9 @@ + + + diff --git a/templates/index.html b/templates/index.html index 6bd11e2..051492f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,7 @@ Chatroom +