Integrated gaurded ai via websocket frontend.

modified:   app.py
	new file:   migrations/versions/d04950c5a624_add_activitystate_table2.py
	new file:   migrations/versions/d3631b8bb652_add_activitystate_table.py
This commit is contained in:
Russell Ballestrini 2024-07-27 10:30:07 -04:00
parent 15ac4fa015
commit 6dff9dc6e9
3 changed files with 427 additions and 0 deletions

352
app.py
View file

@ -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('<img src="data:image/jpeg;base64,')
class ActivityState(db.Model):
id = db.Column(db.Integer, primary_key=True)
room_id = db.Column(db.Integer, db.ForeignKey("room.id"), nullable=False)
section_id = db.Column(db.String(128), nullable=False)
step_id = db.Column(db.String(128), nullable=False)
attempts = db.Column(db.Integer, default=0)
max_attempts = db.Column(db.Integer, default=3)
s3_file_path = db.Column(db.String(256), nullable=False)
def get_room(room_name):
"""Utility function to get room from room name."""
room = Room.query.filter_by(name=room_name).first()
@ -297,6 +308,11 @@ def handle_message(data):
commands = data["message"].splitlines()
for command in commands:
if command.startswith("/activity"):
s3_file_path = command.split(" ", 1)[1].strip()
gevent.spawn(start_activity, room_name, s3_file_path, data["username"])
# Exit early since we're starting an activity
return
if command.startswith("/s3 ls"):
# Extract the S3 file path pattern
s3_file_path_pattern = command.split(" ", 2)[2].strip()
@ -322,6 +338,13 @@ def handle_message(data):
# Cancel the most recent generation request
gevent.spawn(cancel_generation, room_name)
# Check if the user is in activity mode
activity_state = ActivityState.query.filter_by(room_id=room.id).first()
if activity_state:
gevent.spawn(
handle_activity_response, room_name, data["message"], data["username"]
)
if "dall-e-3" in data["message"]:
# Use the entire message as the prompt for DALL-E 3
# Generate the image and emit its URL
@ -1655,6 +1678,335 @@ def cancel_generation(room_name):
)
def start_activity(room_name, s3_file_path, username):
s3_client = boto3.client("s3")
bucket_name = os.environ.get("S3_BUCKET_NAME")
response = s3_client.get_object(Bucket=bucket_name, Key=s3_file_path)
activity_yaml = response["Body"].read().decode("utf-8")
activity_content = yaml.safe_load(activity_yaml)
with app.app_context():
# Save the initial state to the database
room = get_room(room_name)
initial_section = activity_content["sections"][0]
initial_step = initial_section["steps"][0]
activity_state = ActivityState(
room_id=room.id,
section_id=initial_section["section_id"],
step_id=initial_step["step_id"],
max_attempts=activity_content.get("default_max_attempts_per_step", 3),
s3_file_path=s3_file_path, # Save the S3 file path
)
db.session.add(activity_state)
db.session.commit()
# Store and emit the initial activity content
content = f"Starting Activity: {initial_section['title']}\n\n"
content += "\n\n".join(initial_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 initial question
question_content = f"Question: {initial_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,
)
def handle_activity_response(room_name, user_response, username):
with app.app_context():
room = get_room(room_name)
activity_state = ActivityState.query.filter_by(room_id=room.id).first()
if not activity_state:
return
# Load the activity YAML from S3
s3_client = boto3.client("s3")
bucket_name = os.environ.get("S3_BUCKET_NAME")
s3_file_path = activity_state.s3_file_path
try:
response = s3_client.get_object(Bucket=bucket_name, Key=s3_file_path)
activity_yaml = response["Body"].read().decode("utf-8")
activity_content = yaml.safe_load(activity_yaml)
# Find the current section and step
section = next(
s
for s in activity_content["sections"]
if s["section_id"] == activity_state.section_id
)
step = next(
s for s in section["steps"] if s["step_id"] == activity_state.step_id
)
# Categorize the user's response
category = categorize_response(
step["question"], user_response, step["buckets"], step["tokens_for_ai"]
)
# Emit the category to the frontend
socketio.emit(
"message",
{
"id": None,
"username": "System",
"content": f"Category: {category}",
},
room=room_name,
)
# Provide feedback based on the category
feedback = provide_feedback(
activity_content,
section["section_id"],
step["step_id"],
category,
step["question"],
user_response,
)
# Store and emit the feedback
new_message = Message(username="System", content=feedback, room_id=room.id)
db.session.add(new_message)
db.session.commit()
socketio.emit(
"message",
{
"id": new_message.id,
"username": "System",
"content": feedback,
},
room=room_name,
)
# Update the activity state
if (
category == "correct"
or activity_state.attempts >= 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

View file

@ -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 ###

View file

@ -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 ###