mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 07:24:34 +01:00
937 lines
35 KiB
Python
937 lines
35 KiB
Python
"""
|
|
Chatbot API endpoints
|
|
"""
|
|
|
|
import asyncio
|
|
import time
|
|
from typing import Dict, Any, List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
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
|
|
from app.services.conversation_service import ConversationService
|
|
|
|
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
|
|
rag_score_threshold: float = 0.02 # Lowered from default 0.3 to allow more results
|
|
temperature: float = 0.7
|
|
max_tokens: int = 1000
|
|
memory_length: int = 10
|
|
fallback_responses: List[str] = []
|
|
|
|
|
|
class ChatbotUpdateRequest(BaseModel):
|
|
name: Optional[str] = None
|
|
chatbot_type: Optional[str] = None
|
|
model: Optional[str] = None
|
|
system_prompt: Optional[str] = None
|
|
use_rag: Optional[bool] = None
|
|
rag_collection: Optional[str] = None
|
|
rag_top_k: Optional[int] = None
|
|
rag_score_threshold: Optional[float] = None
|
|
temperature: Optional[float] = None
|
|
max_tokens: Optional[int] = None
|
|
memory_length: Optional[int] = None
|
|
fallback_responses: Optional[List[str]] = None
|
|
|
|
|
|
class ChatRequest(BaseModel):
|
|
message: str
|
|
conversation_id: Optional[str] = None
|
|
|
|
|
|
# OpenAI-compatible models
|
|
class ChatMessage(BaseModel):
|
|
role: str = Field(..., description="Message role (system, user, assistant)")
|
|
content: str = Field(..., description="Message content")
|
|
|
|
|
|
class ChatbotChatCompletionRequest(BaseModel):
|
|
messages: List[ChatMessage] = Field(..., description="List of messages")
|
|
max_tokens: Optional[int] = Field(None, description="Maximum tokens to generate")
|
|
temperature: Optional[float] = Field(None, description="Temperature for sampling")
|
|
top_p: Optional[float] = Field(None, description="Top-p sampling parameter")
|
|
frequency_penalty: Optional[float] = Field(None, description="Frequency penalty")
|
|
presence_penalty: Optional[float] = Field(None, description="Presence penalty")
|
|
stop: Optional[List[str]] = Field(None, description="Stop sequences")
|
|
stream: Optional[bool] = Field(False, description="Stream response")
|
|
|
|
|
|
class ChatChoice(BaseModel):
|
|
index: int
|
|
message: ChatMessage
|
|
finish_reason: str
|
|
|
|
|
|
class ChatUsage(BaseModel):
|
|
prompt_tokens: int
|
|
completion_tokens: int
|
|
total_tokens: int
|
|
|
|
|
|
class ChatbotChatCompletionResponse(BaseModel):
|
|
id: str
|
|
object: str = "chat.completion"
|
|
created: int
|
|
model: str
|
|
choices: List[ChatChoice]
|
|
usage: ChatUsage
|
|
|
|
|
|
@router.get("/list")
|
|
@router.get("/instances")
|
|
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: ChatbotUpdateRequest,
|
|
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")
|
|
|
|
# Get existing config
|
|
existing_config = chatbot.config.copy() if chatbot.config else {}
|
|
|
|
# Update only the fields that are provided in the request
|
|
update_data = request.dict(exclude_unset=True)
|
|
|
|
# Merge with existing config, preserving unset values
|
|
for key, value in update_data.items():
|
|
existing_config[key] = value
|
|
|
|
# Update the chatbot
|
|
await db.execute(
|
|
update(ChatbotInstance)
|
|
.where(ChatbotInstance.id == chatbot_id)
|
|
.values(
|
|
name=existing_config.get("name", chatbot.name),
|
|
config=existing_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 (without persisting conversation)"""
|
|
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 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 (without persisting)
|
|
response_data = await chatbot_module.chat(
|
|
chatbot_config=chatbot.config,
|
|
message=request.message,
|
|
conversation_history=[], # Empty history for test chat
|
|
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."
|
|
|
|
# Return response without conversation ID (since we're not persisting)
|
|
return {
|
|
"response": response_content,
|
|
"sources": response_data.get("sources")
|
|
}
|
|
|
|
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.post("/{chatbot_id}/chat/completions", response_model=ChatbotChatCompletionResponse)
|
|
async def chatbot_chat_completions(
|
|
chatbot_id: str,
|
|
request: ChatbotChatCompletionRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""OpenAI-compatible chat completions endpoint for chatbot"""
|
|
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
|
|
log_api_request("chatbot_chat_completions", {
|
|
"user_id": user_id,
|
|
"chatbot_id": chatbot_id,
|
|
"messages_count": len(request.messages)
|
|
})
|
|
|
|
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")
|
|
|
|
# Find the last user message to extract conversation context
|
|
user_messages = [msg for msg in request.messages if msg.role == "user"]
|
|
if not user_messages:
|
|
raise HTTPException(status_code=400, detail="No user message found in conversation")
|
|
|
|
last_user_message = user_messages[-1].content
|
|
|
|
# Initialize conversation service
|
|
conversation_service = ConversationService(db)
|
|
|
|
# For OpenAI format, we'll try to find an existing conversation or create a new one
|
|
# We'll use a simple hash of the conversation messages as the conversation identifier
|
|
import hashlib
|
|
conv_hash = hashlib.md5(str([f"{msg.role}:{msg.content}" for msg in request.messages]).encode()).hexdigest()[:16]
|
|
|
|
# Get or create conversation
|
|
conversation = await conversation_service.get_or_create_conversation(
|
|
chatbot_id=chatbot_id,
|
|
user_id=str(user_id),
|
|
conversation_id=conv_hash
|
|
)
|
|
|
|
# Build conversation history from the request messages (excluding system messages for now)
|
|
conversation_history = []
|
|
for msg in request.messages:
|
|
if msg.role in ["user", "assistant"]:
|
|
conversation_history.append({
|
|
"role": msg.role,
|
|
"content": msg.content
|
|
})
|
|
|
|
# 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")
|
|
|
|
# Merge chatbot config with request parameters
|
|
effective_config = dict(chatbot.config)
|
|
if request.temperature is not None:
|
|
effective_config["temperature"] = request.temperature
|
|
if request.max_tokens is not None:
|
|
effective_config["max_tokens"] = request.max_tokens
|
|
|
|
# Use the chatbot module to generate a response
|
|
response_data = await chatbot_module.chat(
|
|
chatbot_config=effective_config,
|
|
message=last_user_message,
|
|
conversation_history=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 the conversation messages
|
|
for msg in request.messages:
|
|
if msg.role == "user": # Only save the new user message
|
|
await conversation_service.add_message(
|
|
conversation_id=conversation.id,
|
|
role=msg.role,
|
|
content=msg.content,
|
|
metadata={}
|
|
)
|
|
|
|
# Save assistant message
|
|
assistant_message = await conversation_service.add_message(
|
|
conversation_id=conversation.id,
|
|
role="assistant",
|
|
content=response_content,
|
|
metadata={},
|
|
sources=response_data.get("sources")
|
|
)
|
|
|
|
# Calculate usage (simple approximation)
|
|
prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
|
|
completion_tokens = len(response_content.split())
|
|
total_tokens = prompt_tokens + completion_tokens
|
|
|
|
# Create OpenAI-compatible response
|
|
response_id = f"chatbot-{chatbot_id}-{int(time.time())}"
|
|
|
|
return ChatbotChatCompletionResponse(
|
|
id=response_id,
|
|
object="chat.completion",
|
|
created=int(time.time()),
|
|
model=chatbot.config.get("model", "unknown"),
|
|
choices=[
|
|
ChatChoice(
|
|
index=0,
|
|
message=ChatMessage(role="assistant", content=response_content),
|
|
finish_reason="stop"
|
|
)
|
|
],
|
|
usage=ChatUsage(
|
|
prompt_tokens=prompt_tokens,
|
|
completion_tokens=completion_tokens,
|
|
total_tokens=total_tokens
|
|
)
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
await db.rollback()
|
|
log_api_request("chatbot_chat_completions_error", {"error": str(e), "user_id": user_id})
|
|
raise HTTPException(status_code=500, detail=f"Failed to process chat completions: {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")
|
|
|
|
# Initialize conversation service
|
|
conversation_service = ConversationService(db)
|
|
|
|
# Get or create conversation with API key context
|
|
conversation = await conversation_service.get_or_create_conversation(
|
|
chatbot_id=chatbot_id,
|
|
user_id=f"api_key_{api_key.id}",
|
|
conversation_id=request.conversation_id,
|
|
title=f"API Chat {datetime.utcnow().strftime('%Y-%m-%d %H:%M')}"
|
|
)
|
|
|
|
# Add API key metadata to conversation context if new
|
|
if not conversation.context_data.get("api_key_id"):
|
|
conversation.context_data = {"api_key_id": api_key.id}
|
|
await db.commit()
|
|
|
|
# Add user message to conversation
|
|
await conversation_service.add_message(
|
|
conversation_id=conversation.id,
|
|
role="user",
|
|
content=request.message,
|
|
metadata={"api_key_id": api_key.id}
|
|
)
|
|
|
|
# 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")
|
|
|
|
# Load conversation history for context
|
|
conversation_history = await conversation_service.get_conversation_history(
|
|
conversation_id=conversation.id,
|
|
limit=chatbot.config.get('memory_length', 10),
|
|
include_system=False
|
|
)
|
|
|
|
# Use the chatbot module to generate a response
|
|
response_data = await chatbot_module.chat(
|
|
chatbot_config=chatbot.config,
|
|
message=request.message,
|
|
conversation_history=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 using conversation service
|
|
assistant_message = await conversation_service.add_message(
|
|
conversation_id=conversation.id,
|
|
role="assistant",
|
|
content=response_content,
|
|
metadata={"api_key_id": api_key.id},
|
|
sources=sources
|
|
)
|
|
|
|
# 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("/external/{chatbot_id}/chat/completions", response_model=ChatbotChatCompletionResponse)
|
|
async def external_chatbot_chat_completions(
|
|
chatbot_id: str,
|
|
request: ChatbotChatCompletionRequest,
|
|
api_key: APIKey = Depends(get_api_key_auth),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""External OpenAI-compatible chat completions endpoint for chatbot with API key authentication"""
|
|
log_api_request("external_chatbot_chat_completions", {
|
|
"chatbot_id": chatbot_id,
|
|
"api_key_id": api_key.id,
|
|
"messages_count": len(request.messages)
|
|
})
|
|
|
|
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")
|
|
|
|
# Find the last user message to extract conversation context
|
|
user_messages = [msg for msg in request.messages if msg.role == "user"]
|
|
if not user_messages:
|
|
raise HTTPException(status_code=400, detail="No user message found in conversation")
|
|
|
|
last_user_message = user_messages[-1].content
|
|
|
|
# Initialize conversation service
|
|
conversation_service = ConversationService(db)
|
|
|
|
# For OpenAI format, we'll try to find an existing conversation or create a new one
|
|
# We'll use a simple hash of the conversation messages as the conversation identifier
|
|
import hashlib
|
|
conv_hash = hashlib.md5(str([f"{msg.role}:{msg.content}" for msg in request.messages]).encode()).hexdigest()[:16]
|
|
|
|
# Get or create conversation with API key context
|
|
conversation = await conversation_service.get_or_create_conversation(
|
|
chatbot_id=chatbot_id,
|
|
user_id=f"api_key_{api_key.id}",
|
|
conversation_id=conv_hash,
|
|
title=f"API Chat {datetime.utcnow().strftime('%Y-%m-%d %H:%M')}"
|
|
)
|
|
|
|
# Add API key metadata to conversation context if new
|
|
if not conversation.context_data.get("api_key_id"):
|
|
conversation.context_data = {"api_key_id": api_key.id}
|
|
await db.commit()
|
|
|
|
# Build conversation history from the request messages
|
|
conversation_history = []
|
|
for msg in request.messages:
|
|
if msg.role in ["user", "assistant"]:
|
|
conversation_history.append({
|
|
"role": msg.role,
|
|
"content": msg.content
|
|
})
|
|
|
|
# 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")
|
|
|
|
# Merge chatbot config with request parameters
|
|
effective_config = dict(chatbot.config)
|
|
if request.temperature is not None:
|
|
effective_config["temperature"] = request.temperature
|
|
if request.max_tokens is not None:
|
|
effective_config["max_tokens"] = request.max_tokens
|
|
|
|
# Use the chatbot module to generate a response
|
|
response_data = await chatbot_module.chat(
|
|
chatbot_config=effective_config,
|
|
message=last_user_message,
|
|
conversation_history=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 the conversation messages
|
|
for msg in request.messages:
|
|
if msg.role == "user": # Only save the new user message
|
|
await conversation_service.add_message(
|
|
conversation_id=conversation.id,
|
|
role=msg.role,
|
|
content=msg.content,
|
|
metadata={"api_key_id": api_key.id}
|
|
)
|
|
|
|
# Save assistant message using conversation service
|
|
assistant_message = await conversation_service.add_message(
|
|
conversation_id=conversation.id,
|
|
role="assistant",
|
|
content=response_content,
|
|
metadata={"api_key_id": api_key.id},
|
|
sources=sources
|
|
)
|
|
|
|
# Update API key usage stats
|
|
prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
|
|
completion_tokens = len(response_content.split())
|
|
total_tokens = prompt_tokens + completion_tokens
|
|
|
|
api_key.update_usage(tokens_used=total_tokens, cost_cents=0)
|
|
await db.commit()
|
|
|
|
# Create OpenAI-compatible response
|
|
response_id = f"chatbot-{chatbot_id}-{int(time.time())}"
|
|
|
|
return ChatbotChatCompletionResponse(
|
|
id=response_id,
|
|
object="chat.completion",
|
|
created=int(time.time()),
|
|
model=chatbot.config.get("model", "unknown"),
|
|
choices=[
|
|
ChatChoice(
|
|
index=0,
|
|
message=ChatMessage(role="assistant", content=response_content),
|
|
finish_reason="stop"
|
|
)
|
|
],
|
|
usage=ChatUsage(
|
|
prompt_tokens=prompt_tokens,
|
|
completion_tokens=completion_tokens,
|
|
total_tokens=total_tokens
|
|
)
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
await db.rollback()
|
|
log_api_request("external_chatbot_chat_completions_error", {"error": str(e), "chatbot_id": chatbot_id})
|
|
raise HTTPException(status_code=500, detail=f"Failed to process chat completions: {str(e)}")
|