diff --git a/app.py b/app.py index 29066bc..fff1db6 100644 --- a/app.py +++ b/app.py @@ -8,6 +8,7 @@ monkey.patch_all() import json +import yaml import os import boto3 @@ -98,6 +99,16 @@ class Message(db.Model): return self.content.startswith('= activity_state.max_attempts + ): + print( + f"Transitioning to next step. Category: {category}, Attempts: {activity_state.attempts}" + ) + # Move to the next step or section + next_section, next_step = get_next_step( + activity_content, section["section_id"], step["step_id"] + ) + if next_step: + activity_state.section_id = next_section["section_id"] + activity_state.step_id = next_step["step_id"] + activity_state.attempts = 0 + + db.session.add(activity_state) + db.session.commit() + + # Emit the new step content blocks + content = "\n\n".join(next_step["content_blocks"]) + new_message = Message( + username="System", content=content, room_id=room.id + ) + db.session.add(new_message) + db.session.commit() + + socketio.emit( + "message", + { + "id": new_message.id, + "username": "System", + "content": content, + }, + room=room_name, + ) + + # Emit the new question + question_content = f"Question: {next_step['question']}" + new_message = Message( + username="System", content=question_content, room_id=room.id + ) + db.session.add(new_message) + db.session.commit() + + socketio.emit( + "message", + { + "id": new_message.id, + "username": "System", + "content": question_content, + }, + room=room_name, + ) + else: + # Activity completed + db.session.delete(activity_state) + db.session.commit() + socketio.emit( + "message", + { + "id": None, + "username": "System", + "content": "Activity completed!", + }, + room=room_name, + ) + else: + activity_state.attempts += 1 + db.session.add(activity_state) + db.session.commit() + + except Exception as e: + socketio.emit( + "message", + { + "id": None, + "username": "System", + "content": f"Error processing activity response: {e}", + }, + room=room_name, + ) + + +def get_next_step(activity_content, current_section_id, current_step_id): + for section in activity_content["sections"]: + if section["section_id"] == current_section_id: + for i, step in enumerate(section["steps"]): + if step["step_id"] == current_step_id: + if i + 1 < len(section["steps"]): + return section, section["steps"][i + 1] + else: + # Move to the next section + next_section_index = ( + activity_content["sections"].index(section) + 1 + ) + if next_section_index < len(activity_content["sections"]): + next_section = activity_content["sections"][ + next_section_index + ] + return next_section, next_section["steps"][0] + return None, None + + +# Load the YAML activity file +def load_yaml_activity(file_path): + with open(file_path, "r") as file: + return yaml.safe_load(file) + + +# Categorize the user's response using gpt-4o-mini +def categorize_response(question, response, buckets, tokens_for_ai): + openai_client = OpenAI() + bucket_list = ", ".join(buckets) + messages = [ + { + "role": "system", + "content": f"{tokens_for_ai} Categorize the following response into one of the following buckets: {bucket_list}. Return ONLY a bucket label.", + }, + { + "role": "user", + "content": f"Question: {question}\nResponse: {response}\n\nCategory:", + }, + ] + + try: + completion = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=messages, + max_tokens=5, + temperature=0, + ) + category = ( + completion.choices[0] + .message.content.strip() + .lower() + .replace(" ", "_") + .strip("_") + ) + return category + except Exception as e: + return f"Error: {e}" + + +# Generate AI feedback using gpt-4o-mini +def generate_ai_feedback(category, question, user_response, tokens_for_ai): + openai_client = OpenAI() + messages = [ + { + "role": "system", + "content": "{tokens_for_ai} Generate a human-readable feedback message based on the following:", + }, + { + "role": "user", + "content": f"Question: {question}\nResponse: {user_response}\nCategory: {category}", + }, + ] + + try: + completion = openai_client.chat.completions.create( + model="gpt-4o-mini", messages=messages, max_tokens=250, temperature=0.7 + ) + feedback = completion.choices[0].message.content.strip() + return feedback + except Exception as e: + return f"Error: {e}" + + +# Provide feedback based on the category +def provide_feedback( + yaml_content, section_id, step_id, category, question, user_response +): + section = next( + (s for s in yaml_content["sections"] if s["section_id"] == section_id), None + ) + if not section: + return "Section not found." + + step = next((s for s in section["steps"] if s["step_id"] == step_id), None) + if not step: + return "Step not found." + + transition = step["transitions"].get(category, None) + if not transition: + return "Category not found." + + feedback = "\n".join(transition["content_blocks"]) + if "ai_feedback" in transition: + tokens_for_ai = ( + step["tokens_for_ai"] + " " + transition["ai_feedback"]["tokens_for_ai"] + ) + ai_feedback = generate_ai_feedback( + category, question, user_response, tokens_for_ai + ) + feedback += f"\n\nAI Feedback: {ai_feedback}" + + return feedback + + if __name__ == "__main__": import argparse diff --git a/migrations/versions/d04950c5a624_add_activitystate_table2.py b/migrations/versions/d04950c5a624_add_activitystate_table2.py new file mode 100644 index 0000000..8950a1e --- /dev/null +++ b/migrations/versions/d04950c5a624_add_activitystate_table2.py @@ -0,0 +1,34 @@ +"""Add ActivityState table2 + +Revision ID: d04950c5a624 +Revises: d3631b8bb652 +Create Date: 2024-07-27 09:36:50.422693 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "d04950c5a624" +down_revision = "d3631b8bb652" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("activity_state", schema=None) as batch_op: + batch_op.add_column( + sa.Column("s3_file_path", sa.String(length=256), nullable=False) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("activity_state", schema=None) as batch_op: + batch_op.drop_column("s3_file_path") + + # ### end Alembic commands ### diff --git a/migrations/versions/d3631b8bb652_add_activitystate_table.py b/migrations/versions/d3631b8bb652_add_activitystate_table.py new file mode 100644 index 0000000..926eb3a --- /dev/null +++ b/migrations/versions/d3631b8bb652_add_activitystate_table.py @@ -0,0 +1,41 @@ +"""Add ActivityState table + +Revision ID: d3631b8bb652 +Revises: 190d5ef26e20 +Create Date: 2024-07-27 09:33:52.544550 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "d3631b8bb652" +down_revision = "190d5ef26e20" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "activity_state", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("room_id", sa.Integer(), nullable=False), + sa.Column("section_id", sa.String(length=128), nullable=False), + sa.Column("step_id", sa.String(length=128), nullable=False), + sa.Column("attempts", sa.Integer(), nullable=True), + sa.Column("max_attempts", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["room_id"], + ["room.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("activity_state") + # ### end Alembic commands ###