Files
enclava/backend/app/api/v1/chatbot.py
2025-08-19 09:50:15 +02:00

772 lines
28 KiB
Python

"""
Chatbot API endpoints
"""
import asyncio
from typing import Dict, Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete
from datetime import datetime
from app.db.database import get_db
from app.models.chatbot import ChatbotInstance, ChatbotConversation, ChatbotMessage, ChatbotAnalytics
from app.core.logging import log_api_request
from app.services.module_manager import module_manager
from app.core.security import get_current_user
from app.models.user import User
from app.services.api_key_auth import get_api_key_auth
from app.models.api_key import APIKey
router = APIRouter()
class ChatbotCreateRequest(BaseModel):
name: str
chatbot_type: str = "assistant"
model: str = "gpt-3.5-turbo"
system_prompt: str = ""
use_rag: bool = False
rag_collection: Optional[str] = None
rag_top_k: int = 5
temperature: float = 0.7
max_tokens: int = 1000
memory_length: int = 10
fallback_responses: List[str] = []
class ChatRequest(BaseModel):
message: str
conversation_id: Optional[str] = None
@router.get("/list")
async def list_chatbots(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get list of all chatbots for the current user"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("list_chatbots", {"user_id": user_id})
try:
# Query chatbots created by the current user
result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.created_by == str(user_id))
.order_by(ChatbotInstance.created_at.desc())
)
chatbots = result.scalars().all()
chatbot_list = []
for chatbot in chatbots:
chatbot_dict = {
"id": chatbot.id,
"name": chatbot.name,
"description": chatbot.description,
"config": chatbot.config,
"created_by": chatbot.created_by,
"created_at": chatbot.created_at.isoformat() if chatbot.created_at else None,
"updated_at": chatbot.updated_at.isoformat() if chatbot.updated_at else None,
"is_active": chatbot.is_active
}
chatbot_list.append(chatbot_dict)
return chatbot_list
except Exception as e:
log_api_request("list_chatbots_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to fetch chatbots: {str(e)}")
@router.post("/create")
async def create_chatbot(
request: ChatbotCreateRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Create a new chatbot instance"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("create_chatbot", {
"user_id": user_id,
"chatbot_name": request.name,
"chatbot_type": request.chatbot_type
})
try:
# Get the chatbot module
chatbot_module = module_manager.get_module("chatbot")
if not chatbot_module:
raise HTTPException(status_code=500, detail="Chatbot module not available")
# Import needed types
from modules.chatbot.main import ChatbotConfig
# Create chatbot config object
config = ChatbotConfig(
name=request.name,
chatbot_type=request.chatbot_type,
model=request.model,
system_prompt=request.system_prompt,
use_rag=request.use_rag,
rag_collection=request.rag_collection,
rag_top_k=request.rag_top_k,
temperature=request.temperature,
max_tokens=request.max_tokens,
memory_length=request.memory_length,
fallback_responses=request.fallback_responses
)
# Use sync database session for module compatibility
from app.db.database import SessionLocal
sync_db = SessionLocal()
try:
# Use the chatbot module's create method (which handles default prompts)
chatbot = await chatbot_module.create_chatbot(config, str(user_id), sync_db)
finally:
sync_db.close()
# Return the created chatbot
return {
"id": chatbot.id,
"name": chatbot.name,
"description": f"AI chatbot of type {request.chatbot_type}",
"config": chatbot.config.__dict__,
"created_by": chatbot.created_by,
"created_at": chatbot.created_at.isoformat() if chatbot.created_at else None,
"updated_at": chatbot.updated_at.isoformat() if chatbot.updated_at else None,
"is_active": chatbot.is_active
}
except Exception as e:
await db.rollback()
log_api_request("create_chatbot_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to create chatbot: {str(e)}")
@router.put("/update/{chatbot_id}")
async def update_chatbot(
chatbot_id: str,
request: ChatbotCreateRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Update an existing chatbot instance"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("update_chatbot", {
"user_id": user_id,
"chatbot_id": chatbot_id,
"chatbot_name": request.name
})
try:
# Get existing chatbot and verify ownership
result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
.where(ChatbotInstance.created_by == str(user_id))
)
chatbot = result.scalar_one_or_none()
if not chatbot:
raise HTTPException(status_code=404, detail="Chatbot not found or access denied")
# Update chatbot configuration
config = {
"name": request.name,
"chatbot_type": request.chatbot_type,
"model": request.model,
"system_prompt": request.system_prompt,
"use_rag": request.use_rag,
"rag_collection": request.rag_collection,
"rag_top_k": request.rag_top_k,
"temperature": request.temperature,
"max_tokens": request.max_tokens,
"memory_length": request.memory_length,
"fallback_responses": request.fallback_responses
}
# Update the chatbot
await db.execute(
update(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
.values(
name=request.name,
config=config,
updated_at=datetime.utcnow()
)
)
await db.commit()
# Return updated chatbot
updated_result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
)
updated_chatbot = updated_result.scalar_one()
return {
"id": updated_chatbot.id,
"name": updated_chatbot.name,
"description": updated_chatbot.description,
"config": updated_chatbot.config,
"created_by": updated_chatbot.created_by,
"created_at": updated_chatbot.created_at.isoformat() if updated_chatbot.created_at else None,
"updated_at": updated_chatbot.updated_at.isoformat() if updated_chatbot.updated_at else None,
"is_active": updated_chatbot.is_active
}
except HTTPException:
raise
except Exception as e:
await db.rollback()
log_api_request("update_chatbot_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to update chatbot: {str(e)}")
@router.post("/chat/{chatbot_id}")
async def chat_with_chatbot(
chatbot_id: str,
request: ChatRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Send a message to a chatbot and get a response"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("chat_with_chatbot", {
"user_id": user_id,
"chatbot_id": chatbot_id,
"message_length": len(request.message)
})
try:
# Get the chatbot instance
result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
.where(ChatbotInstance.created_by == str(user_id))
)
chatbot = result.scalar_one_or_none()
if not chatbot:
raise HTTPException(status_code=404, detail="Chatbot not found")
if not chatbot.is_active:
raise HTTPException(status_code=400, detail="Chatbot is not active")
# Get or create conversation
conversation = None
if request.conversation_id:
conv_result = await db.execute(
select(ChatbotConversation)
.where(ChatbotConversation.id == request.conversation_id)
.where(ChatbotConversation.chatbot_id == chatbot_id)
.where(ChatbotConversation.user_id == str(user_id))
)
conversation = conv_result.scalar_one_or_none()
if not conversation:
# Create new conversation
conversation = ChatbotConversation(
chatbot_id=chatbot_id,
user_id=str(user_id),
title=f"Chat {datetime.utcnow().strftime('%Y-%m-%d %H:%M')}",
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
is_active=True,
context_data={}
)
db.add(conversation)
await db.commit()
await db.refresh(conversation)
# Save user message
user_message = ChatbotMessage(
conversation_id=conversation.id,
role="user",
content=request.message,
timestamp=datetime.utcnow(),
message_metadata={},
sources=None
)
db.add(user_message)
# Get chatbot module and generate response
try:
chatbot_module = module_manager.modules.get("chatbot")
if not chatbot_module:
raise HTTPException(status_code=500, detail="Chatbot module not available")
# Use the chatbot module to generate a response
response_data = await chatbot_module.chat(
chatbot_config=chatbot.config,
message=request.message,
conversation_history=[], # TODO: Load conversation history
user_id=str(user_id)
)
response_content = response_data.get("response", "I'm sorry, I couldn't generate a response.")
except Exception as e:
# Use fallback response
fallback_responses = chatbot.config.get("fallback_responses", [
"I'm sorry, I'm having trouble processing your request right now."
])
response_content = fallback_responses[0] if fallback_responses else "I'm sorry, I couldn't process your request."
# Save assistant message
assistant_message = ChatbotMessage(
conversation_id=conversation.id,
role="assistant",
content=response_content,
timestamp=datetime.utcnow(),
message_metadata={},
sources=None
)
db.add(assistant_message)
# Update conversation timestamp
conversation.updated_at = datetime.utcnow()
await db.commit()
return {
"conversation_id": conversation.id,
"response": response_content,
"timestamp": assistant_message.timestamp.isoformat()
}
except HTTPException:
raise
except Exception as e:
await db.rollback()
log_api_request("chat_with_chatbot_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to process chat: {str(e)}")
@router.get("/conversations/{chatbot_id}")
async def get_chatbot_conversations(
chatbot_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get conversations for a chatbot"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("get_chatbot_conversations", {
"user_id": user_id,
"chatbot_id": chatbot_id
})
try:
# Verify chatbot ownership
chatbot_result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
.where(ChatbotInstance.created_by == str(user_id))
)
chatbot = chatbot_result.scalar_one_or_none()
if not chatbot:
raise HTTPException(status_code=404, detail="Chatbot not found")
# Get conversations
result = await db.execute(
select(ChatbotConversation)
.where(ChatbotConversation.chatbot_id == chatbot_id)
.where(ChatbotConversation.user_id == str(user_id))
.order_by(ChatbotConversation.updated_at.desc())
)
conversations = result.scalars().all()
conversation_list = []
for conv in conversations:
conversation_list.append({
"id": conv.id,
"title": conv.title,
"created_at": conv.created_at.isoformat() if conv.created_at else None,
"updated_at": conv.updated_at.isoformat() if conv.updated_at else None,
"is_active": conv.is_active
})
return conversation_list
except HTTPException:
raise
except Exception as e:
log_api_request("get_chatbot_conversations_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to fetch conversations: {str(e)}")
@router.get("/conversations/{conversation_id}/messages")
async def get_conversation_messages(
conversation_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get messages for a conversation"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("get_conversation_messages", {
"user_id": user_id,
"conversation_id": conversation_id
})
try:
# Verify conversation ownership
conv_result = await db.execute(
select(ChatbotConversation)
.where(ChatbotConversation.id == conversation_id)
.where(ChatbotConversation.user_id == str(user_id))
)
conversation = conv_result.scalar_one_or_none()
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
# Get messages
result = await db.execute(
select(ChatbotMessage)
.where(ChatbotMessage.conversation_id == conversation_id)
.order_by(ChatbotMessage.timestamp.asc())
)
messages = result.scalars().all()
message_list = []
for msg in messages:
message_list.append({
"id": msg.id,
"role": msg.role,
"content": msg.content,
"timestamp": msg.timestamp.isoformat() if msg.timestamp else None,
"metadata": msg.message_metadata,
"sources": msg.sources
})
return message_list
except HTTPException:
raise
except Exception as e:
log_api_request("get_conversation_messages_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to fetch messages: {str(e)}")
@router.delete("/delete/{chatbot_id}")
async def delete_chatbot(
chatbot_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Delete a chatbot and all associated conversations/messages"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("delete_chatbot", {
"user_id": user_id,
"chatbot_id": chatbot_id
})
try:
# Get existing chatbot and verify ownership
result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
.where(ChatbotInstance.created_by == str(user_id))
)
chatbot = result.scalar_one_or_none()
if not chatbot:
raise HTTPException(status_code=404, detail="Chatbot not found or access denied")
# Delete all messages associated with this chatbot's conversations
await db.execute(
delete(ChatbotMessage)
.where(ChatbotMessage.conversation_id.in_(
select(ChatbotConversation.id)
.where(ChatbotConversation.chatbot_id == chatbot_id)
))
)
# Delete all conversations associated with this chatbot
await db.execute(
delete(ChatbotConversation)
.where(ChatbotConversation.chatbot_id == chatbot_id)
)
# Delete any analytics data
await db.execute(
delete(ChatbotAnalytics)
.where(ChatbotAnalytics.chatbot_id == chatbot_id)
)
# Finally, delete the chatbot itself
await db.execute(
delete(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
)
await db.commit()
return {"message": "Chatbot deleted successfully", "chatbot_id": chatbot_id}
except HTTPException:
raise
except Exception as e:
await db.rollback()
log_api_request("delete_chatbot_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to delete chatbot: {str(e)}")
@router.post("/external/{chatbot_id}/chat")
async def external_chat_with_chatbot(
chatbot_id: str,
request: ChatRequest,
api_key: APIKey = Depends(get_api_key_auth),
db: AsyncSession = Depends(get_db)
):
"""External API endpoint for chatbot access with API key authentication"""
log_api_request("external_chat_with_chatbot", {
"chatbot_id": chatbot_id,
"api_key_id": api_key.id,
"message_length": len(request.message)
})
try:
# Check if API key can access this chatbot
if not api_key.can_access_chatbot(chatbot_id):
raise HTTPException(status_code=403, detail="API key not authorized for this chatbot")
# Get the chatbot instance
result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
)
chatbot = result.scalar_one_or_none()
if not chatbot:
raise HTTPException(status_code=404, detail="Chatbot not found")
if not chatbot.is_active:
raise HTTPException(status_code=400, detail="Chatbot is not active")
# Get or create conversation
conversation = None
if request.conversation_id:
conv_result = await db.execute(
select(ChatbotConversation)
.where(ChatbotConversation.id == request.conversation_id)
.where(ChatbotConversation.chatbot_id == chatbot_id)
)
conversation = conv_result.scalar_one_or_none()
if not conversation:
# Create new conversation with API key as the user context
conversation = ChatbotConversation(
chatbot_id=chatbot_id,
user_id=f"api_key_{api_key.id}",
title=f"API Chat {datetime.utcnow().strftime('%Y-%m-%d %H:%M')}",
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
is_active=True,
context_data={"api_key_id": api_key.id}
)
db.add(conversation)
await db.commit()
await db.refresh(conversation)
# Save user message
user_message = ChatbotMessage(
conversation_id=conversation.id,
role="user",
content=request.message,
timestamp=datetime.utcnow(),
message_metadata={"api_key_id": api_key.id},
sources=None
)
db.add(user_message)
# Get chatbot module and generate response
try:
chatbot_module = module_manager.modules.get("chatbot")
if not chatbot_module:
raise HTTPException(status_code=500, detail="Chatbot module not available")
# Use the chatbot module to generate a response
response_data = await chatbot_module.chat(
chatbot_config=chatbot.config,
message=request.message,
conversation_history=[], # TODO: Load conversation history
user_id=f"api_key_{api_key.id}"
)
response_content = response_data.get("response", "I'm sorry, I couldn't generate a response.")
sources = response_data.get("sources")
except Exception as e:
# Use fallback response
fallback_responses = chatbot.config.get("fallback_responses", [
"I'm sorry, I'm having trouble processing your request right now."
])
response_content = fallback_responses[0] if fallback_responses else "I'm sorry, I couldn't process your request."
sources = None
# Save assistant message
assistant_message = ChatbotMessage(
conversation_id=conversation.id,
role="assistant",
content=response_content,
timestamp=datetime.utcnow(),
message_metadata={"api_key_id": api_key.id},
sources=sources
)
db.add(assistant_message)
# Update conversation timestamp
conversation.updated_at = datetime.utcnow()
# Update API key usage stats
api_key.update_usage(tokens_used=len(request.message) + len(response_content), cost_cents=0)
await db.commit()
return {
"conversation_id": conversation.id,
"response": response_content,
"sources": sources,
"timestamp": assistant_message.timestamp.isoformat(),
"chatbot_id": chatbot_id
}
except HTTPException:
raise
except Exception as e:
await db.rollback()
log_api_request("external_chat_with_chatbot_error", {"error": str(e), "chatbot_id": chatbot_id})
raise HTTPException(status_code=500, detail=f"Failed to process chat: {str(e)}")
@router.post("/{chatbot_id}/api-key")
async def create_chatbot_api_key(
chatbot_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Create an API key for a specific chatbot"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("create_chatbot_api_key", {
"user_id": user_id,
"chatbot_id": chatbot_id
})
try:
# Get existing chatbot and verify ownership
result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
.where(ChatbotInstance.created_by == str(user_id))
)
chatbot = result.scalar_one_or_none()
if not chatbot:
raise HTTPException(status_code=404, detail="Chatbot not found or access denied")
# Generate API key
from app.api.v1.api_keys import generate_api_key
full_key, key_hash = generate_api_key()
key_prefix = full_key[:8]
# Create chatbot-specific API key
new_api_key = APIKey.create_chatbot_key(
user_id=user_id,
name=f"{chatbot.name} API Key",
key_hash=key_hash,
key_prefix=key_prefix,
chatbot_id=chatbot_id,
chatbot_name=chatbot.name
)
db.add(new_api_key)
await db.commit()
await db.refresh(new_api_key)
return {
"api_key_id": new_api_key.id,
"name": new_api_key.name,
"key_prefix": new_api_key.key_prefix + "...",
"secret_key": full_key, # Only returned on creation
"chatbot_id": chatbot_id,
"chatbot_name": chatbot.name,
"endpoint": f"/api/v1/chatbot/external/{chatbot_id}/chat",
"scopes": new_api_key.scopes,
"rate_limit_per_minute": new_api_key.rate_limit_per_minute,
"created_at": new_api_key.created_at.isoformat()
}
except HTTPException:
raise
except Exception as e:
await db.rollback()
log_api_request("create_chatbot_api_key_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to create chatbot API key: {str(e)}")
@router.get("/{chatbot_id}/api-keys")
async def list_chatbot_api_keys(
chatbot_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""List API keys for a specific chatbot"""
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
log_api_request("list_chatbot_api_keys", {
"user_id": user_id,
"chatbot_id": chatbot_id
})
try:
# Get existing chatbot and verify ownership
result = await db.execute(
select(ChatbotInstance)
.where(ChatbotInstance.id == chatbot_id)
.where(ChatbotInstance.created_by == str(user_id))
)
chatbot = result.scalar_one_or_none()
if not chatbot:
raise HTTPException(status_code=404, detail="Chatbot not found or access denied")
# Get API keys that can access this chatbot
api_keys_result = await db.execute(
select(APIKey)
.where(APIKey.user_id == user_id)
.where(APIKey.allowed_chatbots.contains([chatbot_id]))
.order_by(APIKey.created_at.desc())
)
api_keys = api_keys_result.scalars().all()
api_key_list = []
for api_key in api_keys:
api_key_list.append({
"id": api_key.id,
"name": api_key.name,
"key_prefix": api_key.key_prefix + "...",
"is_active": api_key.is_active,
"created_at": api_key.created_at.isoformat(),
"last_used_at": api_key.last_used_at.isoformat() if api_key.last_used_at else None,
"total_requests": api_key.total_requests,
"rate_limit_per_minute": api_key.rate_limit_per_minute,
"scopes": api_key.scopes
})
return {
"chatbot_id": chatbot_id,
"chatbot_name": chatbot.name,
"api_keys": api_key_list,
"total": len(api_key_list)
}
except HTTPException:
raise
except Exception as e:
log_api_request("list_chatbot_api_keys_error", {"error": str(e), "user_id": user_id})
raise HTTPException(status_code=500, detail=f"Failed to list chatbot API keys: {str(e)}")