Skip to content

Commit

Permalink
Merge pull request #237 from priyanshuverma-dev/feat-version-control
Browse files Browse the repository at this point in the history
feat: Chatbot Version Control
  • Loading branch information
kom-senapati authored Nov 1, 2024
2 parents 7b04c45 + 848c63c commit 5864cb2
Show file tree
Hide file tree
Showing 13 changed files with 330 additions and 87 deletions.
107 changes: 88 additions & 19 deletions app/api_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import re
import os
from sqlalchemy import func
from .models import User, Chatbot, Chat, Image, Comment
from .models import User, Chatbot, Chat, Image, Comment, ChatbotVersion
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
from flask_login import login_user
from typing import Union, List, Optional, Dict
from .ai import chat_with_chatbot, text_to_mp3, translate_text
Expand Down Expand Up @@ -159,14 +160,21 @@ def api_create_chatbot() -> Response:

user = get_current_user()
chatbot: Chatbot = Chatbot(
name=chatbot_name,
avatar=f"{BOT_AVATAR_API}/{chatbot_name}",
user_id=user.id,
prompt=chatbot_prompt,
generated_by=user.username,
public=False, # Set public to default (modify as needed)
category=chatbot_category,
avatar=f"{BOT_AVATAR_API}/{chatbot_name}",
likes=0, # Default likes
reports=0, # Default reports
)
db.session.add(chatbot)
db.session.flush() # Flush to get chatbot ID before creating version

# Create the initial version of the chatbot
chatbot.create_version(
name=chatbot_name, new_prompt=chatbot_prompt, modified_by=user.username
)

user.contribution_score += 5
db.session.commit()
return jsonify({"success": True, "message": "Chatbot created."})
Expand All @@ -184,13 +192,65 @@ def api_update_chatbot(chatbot_id: int) -> Union[Response, str]:
)

data = request.get_json()
chatbot.name = data.get("name")
chatbot.prompt = data.get("prompt")
chatbot.category = data.get("category")
new_name = data.get("name")
new_prompt = data.get("prompt")
new_category = data.get("category")
# Create a new version of the chatbot with the updated prompt and other details
chatbot.create_version(
name=new_name, new_prompt=new_prompt, modified_by=user.username
)

# Update the chatbot's other fields
chatbot.avatar = f"{BOT_AVATAR_API}/{new_name}" # Update avatar if needed
chatbot.category = new_category
db.session.commit()
return jsonify({"success": True, "message": "Chatbot Updated."})


@api_bp.route("/api/chatbot/<int:chatbot_id>/revert/<int:version_id>", methods=["POST"])
@jwt_required()
def api_revert_chatbot(chatbot_id: int, version_id: int) -> Union[Response, str]:
"""API endpoint to revert a chatbot to a previous version."""
current_user = get_jwt_identity()

# Fetch the chatbot by ID and ensure it belongs to the current user
chatbot = Chatbot.query.filter_by(id=chatbot_id, user_id=current_user).first()
if chatbot is None:
return jsonify({"success": False, "message": "Chatbot not found."}), 404

# Fetch the specified version by ID
version = ChatbotVersion.query.filter_by(
id=version_id, chatbot_id=chatbot_id
).first()
if version is None:
return jsonify({"success": False, "message": "Version not found."}), 404

chatbot.latest_version_id = version.id # Update to reflect the latest version

try:
# Commit changes to the database
db.session.commit()
return jsonify(
{
"success": True,
"message": "Chatbot reverted to the selected version.",
"version": version.to_dict(),
}
) # Ensure to return the updated version info
except Exception as e:
db.session.rollback()
return (
jsonify(
{
"success": False,
"message": "Failed to revert the chatbot.",
"error": str(e),
}
),
500,
)


@api_bp.route("/api/chatbot/<int:chatbot_id>/delete", methods=["POST"])
@jwt_required()
def api_delete_chatbot(chatbot_id: int) -> Union[Response, tuple[Response, int]]:
Expand All @@ -202,7 +262,7 @@ def api_delete_chatbot(chatbot_id: int) -> Union[Response, tuple[Response, int]]
jsonify({"error": "Unauthorized access."}),
403,
)

ChatbotVersion.query.filter_by(chatbot_id=chatbot.id).delete()
db.session.delete(chatbot)
db.session.commit()

Expand Down Expand Up @@ -502,28 +562,31 @@ def api_get_data():
"leaderboard",
}
queues = [q for q in queues if q in valid_queues]

# Fetch data
chatbots: List[Chatbot] = Chatbot.query.filter(Chatbot.user_id == uid).all()
images: List[Image] = Image.query.filter(Image.user_id == uid).all()
system_chatbots: List[Chatbot] = Chatbot.query.filter(
Chatbot.generated_by == "system"
).all()
public_chatbots: List[Chatbot] = Chatbot.query.filter_by(public=True).all()
public_images: List[Image] = Image.query.filter_by(public=True).all()

response = {"success": True}

# Build response based on queues
if "system_bots" in queues:
system_chatbots: List[Chatbot] = (
Chatbot.query.options(
joinedload(Chatbot.latest_version)
) # Use joinedload for eager loading
.filter(
Chatbot.latest_version.has(modified_by="system")
) # Use has() for filtering on related model
.all()
)
response["system_bots"] = [bot.to_dict() for bot in system_chatbots]
if "my_bots" in queues:
chatbots: List[Chatbot] = Chatbot.query.filter(Chatbot.user_id == uid).all()
response["my_bots"] = [bot.to_dict() for bot in chatbots]
if "my_images" in queues:
images: List[Image] = Image.query.filter(Image.user_id == uid).all()
response["my_images"] = [image.to_dict() for image in images]
if "public_bots" in queues:
public_chatbots: List[Chatbot] = Chatbot.query.filter_by(public=True).all()
response["public_bots"] = [bot.to_dict() for bot in public_chatbots]
if "public_images" in queues:
public_images: List[Image] = Image.query.filter_by(public=True).all()
response["public_images"] = [image.to_dict() for image in public_images]
if "user_bots" in queues:
o_chatbots: List[Chatbot] = Chatbot.query.filter(
Expand Down Expand Up @@ -644,12 +707,18 @@ def api_get_chatbot_data(chatbot_id: str):
chatbot: Chatbot = Chatbot.query.get(chatbot_id)
if chatbot == None:
return jsonify({"success": False, "message": "Chatbot not found"}), 404
versions = (
ChatbotVersion.query.filter_by(chatbot_id=chatbot_id)
.order_by(ChatbotVersion.version_number.desc())
.all()
)
comments: List[Comment] = Comment.query.filter_by(chatbot_id=chatbot_id).all()
return (
jsonify(
{
"success": True,
"bot": chatbot.to_dict(),
"versions": [version.to_dict() for version in versions],
"comments": [comment.to_dict() for comment in comments],
}
),
Expand Down
49 changes: 28 additions & 21 deletions app/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,45 @@
from .constants import BOT_AVATAR_API, DEFAULT_CHATBOTS
import logging

# Setup logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.FileHandler("chatbot_creation.log")
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(handler)


def create_default_chatbots(db):
"""Create default chatbots if none exist."""

if Chatbot.query.count() == 0:
try:
try:
if Chatbot.query.count() == 0:
for bot_data in DEFAULT_CHATBOTS:
required_fields = ["name", "prompt", "generated_by"]
for field in required_fields:
if field not in bot_data:
logger.error(f"Missing required field '{field}' in bot_data: {bot_data}")
continue

avatar = f"{BOT_AVATAR_API}/{bot_data['name']}"
chatbot = Chatbot(
name=bot_data["name"],
prompt=bot_data["prompt"],
generated_by=bot_data["generated_by"],
user_id=bot_data["user_id"],
public=True, # Set to True as per your requirements
category="General", # Default category if not specified
likes=0, # Initialize likes
reports=0, # Initialize reports
avatar=avatar,
public=True,
user_id=None,
)

db.session.add(chatbot)
db.session.flush() # Ensure chatbot ID is available for version creation

# Create an initial version for the chatbot
chatbot.create_version(
name=bot_data["name"],
new_prompt=bot_data["prompt"],
modified_by=bot_data["generated_by"],
)

db.session.commit()
logger.info("Default chatbots created successfully.")
except Exception as e:
db.session.rollback()
error_message = f"Error creating default chatbots: {str(e)}"
flash(error_message, "error")
logger.error(error_message)
logger.info(
"Default chatbots and their initial versions created successfully."
)
except Exception as e:
db.session.rollback()
error_message = f"Error creating default chatbots: {str(e)}"
flash(error_message, "error")
logger.error(error_message)
67 changes: 54 additions & 13 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,31 +44,72 @@ class Chatbot(db.Model):
__tablename__ = "chatbots"

id: int = db.Column(db.Integer, primary_key=True)
name: str = db.Column(db.String(100), nullable=False)
avatar: str = db.Column(db.Text, nullable=False)
prompt: str = db.Column(db.Text, nullable=False)
generated_by: str = db.Column(db.String(10), nullable=False)
user_id: int = db.Column(db.Integer, nullable=True)
public: bool = db.Column(db.Boolean, default=False)
category: str = db.Column(db.Text, default="General", nullable=False)
category = db.Column(db.Text, default="General", nullable=False)
likes: int = db.Column(db.Integer, default=0, nullable=False)
reports: int = db.Column(db.Integer, default=0, nullable=False)
# Linking to the latest version
latest_version_id = db.Column(
db.Integer, db.ForeignKey("chatbot_versions.id"), nullable=True
)
latest_version = db.relationship(
"ChatbotVersion", backref="chatbot", foreign_keys=[latest_version_id]
)

def __repr__(self) -> str:
return f"<Chatbot: \nName: {self.name}\nPrompt: {self.prompt}>"

def to_dict(self) -> dict:
def create_version(self, name, new_prompt, modified_by):
version = ChatbotVersion(
chatbot_id=self.id,
version_number=(
(self.latest_version.version_number + 1) if self.latest_version else 1
),
name=name,
prompt=new_prompt,
modified_by=modified_by,
)
db.session.add(version)
db.session.flush() # Get the ID of the new version
self.latest_version_id = version.id
db.session.commit()

def to_dict(self):
return {
"id": self.id,
"name": self.name,
"avatar": self.avatar,
"prompt": self.prompt,
"public": self.public,
"user_id": self.user_id,
"category": self.category,
"generated_by": self.generated_by,
"user_id": self.user_id,
"likes": self.likes,
"avatar": self.avatar, # Include avatar in the dictionary
"reports": self.reports,
"latest_version": (
self.latest_version.to_dict() if self.latest_version else None
),
}


class ChatbotVersion(db.Model):
__tablename__ = "chatbot_versions"

id = db.Column(db.Integer, primary_key=True)
chatbot_id = db.Column(db.Integer, db.ForeignKey("chatbots.id"), nullable=False)
version_number = db.Column(db.Integer, nullable=False)
prompt = db.Column(db.Text, nullable=False)
name = db.Column(db.String(100), nullable=False) # Added field for name
modified_by = db.Column(db.String(100), nullable=False)
created_at = db.Column(
db.DateTime(timezone=True), server_default=func.now(), nullable=False
)

def to_dict(self):
return {
"id": self.id,
"chatbot_id": self.chatbot_id,
"version_number": self.version_number,
"prompt": self.prompt,
"name": self.name, # Include name in the dictionary
"modified_by": self.modified_by,
"created_at": self.created_at.isoformat(),
}


Expand Down
Binary file modified client/bun.lockb
Binary file not shown.
19 changes: 13 additions & 6 deletions client/src/components/ChatbotCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,28 @@ export function ChatbotCard({
<Link to={`/hub/${chatbot.id}`}>
<CardHeader className="flex flex-row items-center gap-4">
<Avatar className="w-16 h-16">
<AvatarImage src={chatbot.avatar} alt={chatbot.name} />
<AvatarImage
src={chatbot.avatar}
alt={chatbot.latest_version.name}
/>
<AvatarFallback>
{chatbot.name.slice(0, 2).toUpperCase()}
{chatbot.latest_version.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<h2 className="text-2xl font-bold">{chatbot.name}</h2>
<h2 className="text-2xl font-bold">
{chatbot.latest_version.name}
</h2>
<p className="text-sm text-muted-foreground">
Created by @{chatbot.generated_by}
Created by @{chatbot.latest_version.modified_by}
</p>
</div>
</CardHeader>
</Link>
<CardContent>
<p className="text-sm">"{chatbot.prompt.substring(0, 100)}"</p>
<p className="text-sm">
"{chatbot.latest_version.prompt.substring(0, 100)}"
</p>
</CardContent>
<CardFooter className="flex justify-between">
<div className="flex gap-2">
Expand Down Expand Up @@ -100,7 +107,7 @@ export function ChatbotCard({
size="icon"
onClick={() =>
shareModel.onOpen({
title: `Share Chatbot ${chatbot.name} Powered by Bot Verse`,
title: `Share Chatbot ${chatbot.latest_version.name} Powered by Bot Verse`,
shareUrl: `http://localhost:5000/hub/${chatbot.id}`,
})
}
Expand Down
Loading

0 comments on commit 5864cb2

Please sign in to comment.