Integrate OpenWebUI bot with AI service

This commit is contained in:
Iyeoluwa Akinrinola
2025-05-20 02:18:46 +01:00
parent 730009ae87
commit 0a27103875
46 changed files with 1749 additions and 3012 deletions
+150 -367
View File
@@ -13,13 +13,14 @@ from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import List, Optional
import uuid
import asyncio
from datetime import datetime, timezone
from ai_service.models.model_service import model_service
from ai_service.models.chat_service import chat_service
from ai_service.openwebui_api import router as openwebui_router
from ai_service.openwebui_channels import openwebui_channels
from ai_service.config import config
from ai_service import bot_manager
# Create FastAPI app
app = FastAPI(
@@ -43,26 +44,64 @@ app.include_router(openwebui_router, prefix="/api")
# Include Ollama proxy routes
app.include_router(openwebui_router, prefix="/ollama")
# Register webhook for channel messages on startup
# API startup event
@app.on_event("startup")
async def startup_event():
"""
Register webhook for channel messages on startup.
Startup event for the API.
"""
# Get the public URL of this service
service_url = f"http://{config.API_HOST}:{config.API_PORT}"
if config.PUBLIC_URL:
service_url = config.PUBLIC_URL
print("=" * 50)
print(f"Starting AI Service API on {config.API_HOST}:{config.API_PORT}")
print(f"OpenWebUI URL: {config.OPENWEBUI_URL}")
print(f"Default model: {config.DEFAULT_MODEL}")
# Register webhook
webhook_url = f"{service_url}/webhooks/channel-message"
print(f"Registering webhook for channel messages: {webhook_url}")
# Start the OpenWebUI bot if enabled
if config.BOT_ENABLED:
print("=" * 50)
print("Starting OpenWebUI bot...")
success = openwebui_channels.register_webhook(webhook_url)
if success:
print("Successfully registered webhook for channel messages")
# Start the bot with configuration
success = await bot_manager.start_bot(
openwebui_url=config.OPENWEBUI_URL,
api_key=config.OPENWEBUI_API_KEY,
model_id=config.DEFAULT_MODEL,
system_prompt=config.BOT_SYSTEM_PROMPT,
temperature=config.BOT_TEMPERATURE,
max_tokens=config.BOT_MAX_TOKENS,
top_p=config.BOT_TOP_P,
triggers=config.BOT_TRIGGERS,
respond_to_all=config.BOT_RESPOND_TO_ALL
)
if success:
print("Bot started successfully!")
else:
print("Failed to start bot. Check the logs for details.")
print("=" * 50)
else:
print("Failed to register webhook for channel messages")
print("OpenWebUI bot is disabled. Set BOT_ENABLED=true in .env to enable it.")
print("=" * 50)
# API shutdown event
@app.on_event("shutdown")
async def shutdown_event():
"""
Shutdown event for the API.
"""
print("=" * 50)
print("Shutting down AI Service API...")
# Stop the OpenWebUI bot if it's running
if bot_manager.is_bot_running():
print("Stopping OpenWebUI bot...")
success = await bot_manager.stop_bot()
if success:
print("Bot stopped successfully!")
else:
print("Failed to stop bot. Check the logs for details.")
print("=" * 50)
# Define API models for health check
class HealthResponse(BaseModel):
@@ -137,172 +176,6 @@ async def health_check():
"""
return {"status": "healthy"}
@app.get("/test-ollama")
async def test_ollama_connection():
"""
Test the connection to the Ollama API.
Returns:
Connection status and available models from Ollama.
"""
import requests
try:
# Try to connect to Ollama API
response = requests.get(f"{config.OLLAMA_API_URL}/api/tags", timeout=config.API_TIMEOUT)
response.raise_for_status()
# Return the models from Ollama
return {
"status": "success",
"message": "Successfully connected to Ollama API",
"ollama_url": config.OLLAMA_API_URL,
"models": response.json()
}
except requests.exceptions.Timeout as e:
return {
"status": "error",
"message": f"Timeout connecting to Ollama API: {str(e)}. The request exceeded the {config.API_TIMEOUT} second timeout.",
"ollama_url": config.OLLAMA_API_URL
}
except requests.exceptions.ConnectionError as e:
return {
"status": "error",
"message": f"Connection error to Ollama API: {str(e)}. Please check if Ollama is running at {config.OLLAMA_API_URL}.",
"ollama_url": config.OLLAMA_API_URL
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to connect to Ollama API: {str(e)}",
"ollama_url": config.OLLAMA_API_URL
}
@app.post("/test-chat")
async def test_chat_completion():
"""
Test the chat completion with a simple prompt.
Returns:
Model response.
"""
try:
# Use the model service directly
response = model_service.generate_response(
model_id=config.DEFAULT_MODEL,
prompt="Hello, how are you?",
context=[],
use_rag=False
)
return {
"status": "success",
"model": config.DEFAULT_MODEL,
"response": response,
"ollama_url": config.OLLAMA_API_URL
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to get chat completion: {str(e)}",
"ollama_url": config.OLLAMA_API_URL
}
@app.post("/test-rag")
async def test_rag_completion(query: str = "What information do you have in your knowledge database?"):
"""
Test the RAG (Retrieval Augmented Generation) functionality with a query.
This endpoint tests the integration with OpenWebUI's knowledge database.
Args:
query: The question to ask about documents in the knowledge database.
Returns:
Model response using RAG.
"""
try:
# Use the model service directly with RAG enabled
response = model_service.generate_response(
model_id=config.DEFAULT_MODEL,
prompt=query,
context=[],
use_rag=True # Enable RAG
)
return {
"status": "success",
"model": config.DEFAULT_MODEL,
"query": query,
"use_rag": True,
"response": response,
"openwebui_url": config.OPENWEBUI_URL
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to get RAG completion: {str(e)}",
"openwebui_url": config.OPENWEBUI_URL
}
@app.post("/test-ollama-direct")
async def test_ollama_direct():
"""
Test the Ollama API directly with a simple chat request.
Returns:
Raw Ollama API response.
"""
import requests
try:
# Prepare a simple chat request
request_json = {
"model": config.DEFAULT_MODEL,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello, how are you?"}
],
"stream": False
}
# Make the API call to Ollama
print(f"Sending direct request to Ollama API at: {config.OLLAMA_API_URL}/api/chat")
response = requests.post(
f"{config.OLLAMA_API_URL}/api/chat",
headers={"Content-Type": "application/json"},
json=request_json,
timeout=config.API_TIMEOUT
)
response.raise_for_status()
result = response.json()
return {
"status": "success",
"ollama_url": config.OLLAMA_API_URL,
"request": request_json,
"response": result
}
except requests.exceptions.Timeout as e:
return {
"status": "error",
"message": f"Timeout connecting to Ollama API: {str(e)}. The request exceeded the {config.API_TIMEOUT} second timeout.",
"ollama_url": config.OLLAMA_API_URL
}
except requests.exceptions.ConnectionError as e:
return {
"status": "error",
"message": f"Connection error to Ollama API: {str(e)}. Please check if Ollama is running at {config.OLLAMA_API_URL}.",
"ollama_url": config.OLLAMA_API_URL
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to connect to Ollama API: {str(e)}",
"ollama_url": config.OLLAMA_API_URL
}
@app.get("/config")
async def get_config():
"""
@@ -318,38 +191,114 @@ async def get_config():
"ollama_api_url": config.OLLAMA_API_URL,
"default_model": config.DEFAULT_MODEL,
"api_timeout": config.API_TIMEOUT,
"bot": {
"enabled": config.BOT_ENABLED,
"running": bot_manager.is_bot_running(),
"model_id": config.DEFAULT_MODEL,
"system_prompt": config.BOT_SYSTEM_PROMPT[:50] + "..." if len(config.BOT_SYSTEM_PROMPT) > 50 else config.BOT_SYSTEM_PROMPT,
"temperature": config.BOT_TEMPERATURE,
"max_tokens": config.BOT_MAX_TOKENS,
"top_p": config.BOT_TOP_P,
"triggers": config.BOT_TRIGGERS,
"respond_to_all": config.BOT_RESPOND_TO_ALL
},
"available_models": list(model_service.AVAILABLE_MODELS.keys())
}
@app.get("/test-webhook")
async def test_webhook():
@app.get("/bot/status")
async def get_bot_status():
"""
Test the webhook registration.
Get the status and configuration of the OpenWebUI bot.
Returns:
Webhook registration status.
Bot status and configuration.
"""
# Get the public URL of this service
service_url = f"http://{config.API_HOST}:{config.API_PORT}"
if config.PUBLIC_URL:
service_url = config.PUBLIC_URL
# Webhook URL
webhook_url = f"{service_url}/webhooks/channel-message"
# Try to register the webhook again
success = openwebui_channels.register_webhook(webhook_url)
# Get all registered webhooks
webhooks = openwebui_channels.get_webhooks()
return {
"webhook_url": webhook_url,
"registration_success": success,
"registered_webhooks": webhooks
"enabled": config.BOT_ENABLED,
"running": bot_manager.is_bot_running(),
"config": {
"model_id": config.DEFAULT_MODEL,
"system_prompt": config.BOT_SYSTEM_PROMPT[:50] + "..." if len(config.BOT_SYSTEM_PROMPT) > 50 else config.BOT_SYSTEM_PROMPT,
"temperature": config.BOT_TEMPERATURE,
"max_tokens": config.BOT_MAX_TOKENS,
"top_p": config.BOT_TOP_P,
"triggers": config.BOT_TRIGGERS,
"respond_to_all": config.BOT_RESPOND_TO_ALL
}
}
@app.post("/bot/start")
async def start_bot(
model_id: str = None,
system_prompt: str = None,
temperature: float = None,
max_tokens: int = None,
top_p: float = None,
respond_to_all: bool = None
):
"""
Start the OpenWebUI bot with optional configuration.
Args:
model_id: ID of the model to use (default: config.DEFAULT_MODEL)
system_prompt: System prompt for the bot (default: config.BOT_SYSTEM_PROMPT)
temperature: Temperature for response generation (default: config.BOT_TEMPERATURE)
max_tokens: Maximum number of tokens to generate (default: config.BOT_MAX_TOKENS)
top_p: Top-p sampling parameter (default: config.BOT_TOP_P)
respond_to_all: Whether to respond to all messages (default: config.BOT_RESPOND_TO_ALL)
Returns:
Start status.
"""
if bot_manager.is_bot_running():
return {"status": "already_running", "message": "Bot is already running"}
# Use provided values or defaults from config
success = await bot_manager.start_bot(
openwebui_url=config.OPENWEBUI_URL,
api_key=config.OPENWEBUI_API_KEY,
model_id=model_id or config.DEFAULT_MODEL,
system_prompt=system_prompt or config.BOT_SYSTEM_PROMPT,
temperature=temperature if temperature is not None else config.BOT_TEMPERATURE,
max_tokens=max_tokens if max_tokens is not None else config.BOT_MAX_TOKENS,
top_p=top_p if top_p is not None else config.BOT_TOP_P,
triggers=config.BOT_TRIGGERS,
respond_to_all=respond_to_all if respond_to_all is not None else config.BOT_RESPOND_TO_ALL
)
if success:
return {
"status": "success",
"message": "Bot started successfully",
"config": {
"model_id": model_id or config.DEFAULT_MODEL,
"system_prompt": (system_prompt or config.BOT_SYSTEM_PROMPT)[:50] + "..." if len(system_prompt or config.BOT_SYSTEM_PROMPT) > 50 else (system_prompt or config.BOT_SYSTEM_PROMPT),
"temperature": temperature if temperature is not None else config.BOT_TEMPERATURE,
"max_tokens": max_tokens if max_tokens is not None else config.BOT_MAX_TOKENS,
"top_p": top_p if top_p is not None else config.BOT_TOP_P,
"respond_to_all": respond_to_all if respond_to_all is not None else config.BOT_RESPOND_TO_ALL
}
}
else:
return {"status": "error", "message": "Failed to start bot"}
@app.post("/bot/stop")
async def stop_bot():
"""
Stop the OpenWebUI bot.
Returns:
Stop status.
"""
if not bot_manager.is_bot_running():
return {"status": "not_running", "message": "Bot is not running"}
success = await bot_manager.stop_bot()
if success:
return {"status": "success", "message": "Bot stopped successfully"}
else:
return {"status": "error", "message": "Failed to stop bot"}
# Model endpoints
@app.get("/models", response_model=List[ModelInfo])
@@ -543,170 +492,4 @@ async def delete_chat(chat_id: str):
return {"status": "success", "message": "Chat deleted"}
# OpenWebUI Channels endpoints
@app.get("/channels")
async def get_openwebui_channels():
"""
Get all OpenWebUI channels.
Returns:
List of channels.
"""
channels = openwebui_channels.get_channels()
return channels
@app.get("/channels/{channel_id}")
async def get_openwebui_channel(channel_id: str):
"""
Get an OpenWebUI channel by ID.
Args:
channel_id: Channel ID.
Returns:
Channel information.
"""
channel = openwebui_channels.get_channel(channel_id)
if not channel:
raise HTTPException(status_code=404, detail="Channel not found")
return channel
@app.post("/channels")
async def create_openwebui_channel(name: str, description: str = "", is_private: bool = False):
"""
Create a new OpenWebUI channel.
Args:
name: Channel name.
description: Channel description.
is_private: Whether the channel is private.
Returns:
Created channel.
"""
channel = openwebui_channels.create_channel(name, description, is_private)
if not channel:
raise HTTPException(status_code=400, detail="Failed to create channel")
return channel
# Webhook endpoint for OpenWebUI channel messages
class ChannelMessageWebhook(BaseModel):
"""Model for channel message webhook."""
channel_id: str = Field(..., description="Channel ID")
message: str = Field(..., description="Message content")
user_id: str = Field(..., description="User ID")
timestamp: Optional[str] = Field(None, description="Message timestamp")
@app.post("/webhooks/channel-message")
async def channel_message_webhook(request: ChannelMessageWebhook):
"""
Webhook endpoint for receiving messages from OpenWebUI channels.
This endpoint is called by OpenWebUI when a message is sent in a channel.
The AI service will process the message and respond in the channel.
Args:
request: Channel message webhook request.
Returns:
Processing status.
"""
try:
print("=" * 50)
print("WEBHOOK RECEIVED")
print(f"Channel ID: {request.channel_id}")
print(f"User ID: {request.user_id}")
print(f"Message: {request.message}")
print(f"Timestamp: {request.timestamp}")
print("=" * 50)
# Find the chat associated with this OpenWebUI channel
print(f"Looking for chat with OpenWebUI channel ID: {request.channel_id}")
print(f"Number of chats in system: {len(chat_service.chats)}")
# Debug: Print all chats and their channel IDs
print("All chats in the system:")
for cid, chat in chat_service.chats.items():
is_team = chat.get('is_team_chat', False)
channel_id = chat.get('openwebui_channel_id', 'None')
print(f" Chat ID: {cid}, Is Team Chat: {is_team}, OpenWebUI Channel ID: {channel_id}")
chat_id = None
for cid, chat in chat_service.chats.items():
if chat.get('is_team_chat') and chat.get('openwebui_channel_id') == request.channel_id:
chat_id = cid
print(f"Found matching chat: {cid}")
break
if not chat_id:
print(f"No chat found for OpenWebUI channel {request.channel_id}")
# If no chat exists for this channel, create one
print("Creating a new team chat for this channel...")
try:
new_chat_id = chat_service.create_chat(
user_id=request.user_id,
title=f"Channel Chat {request.channel_id}",
model_id=config.DEFAULT_MODEL,
is_team_chat=True
)
# Manually set the OpenWebUI channel ID for this chat
chat_service.chats[new_chat_id]['openwebui_channel_id'] = request.channel_id
chat_service._save_chats()
print(f"Created new chat with ID {new_chat_id} for channel {request.channel_id}")
# Use this new chat
chat_id = new_chat_id
except Exception as e:
print(f"Error creating new chat for channel: {str(e)}")
return {"status": "error", "message": "No chat found for this channel and failed to create one"}
# Skip messages from the AI assistant to avoid loops
if request.user_id == "ai-assistant":
return {"status": "skipped", "message": "Skipping AI assistant message"}
# Check if we should respond to all messages or only to mentions
if not config.AI_RESPOND_TO_ALL:
# Check if the message mentions the AI using configured triggers
message_lower = request.message.lower()
is_triggered = False
for trigger in config.AI_TRIGGERS:
if trigger.lower() in message_lower:
is_triggered = True
break
# If no trigger is found, skip processing
if not is_triggered:
print(f"No AI mention found in message, skipping: {request.message[:50]}...")
return {"status": "skipped", "message": "No AI mention found in message"}
# Extract the actual message content (remove the trigger)
# This is a simple approach - for more complex cases, you might want more sophisticated parsing
processed_message = request.message
message_lower = request.message.lower()
# Only try to remove triggers if we're not responding to all messages
if not config.AI_RESPOND_TO_ALL:
for trigger in config.AI_TRIGGERS:
if trigger.lower() in message_lower:
# Remove the trigger from the message
processed_message = request.message.replace(trigger, "").strip()
break
# Process the message and generate a response
response = chat_service.get_chat_response(
chat_id=chat_id,
message=processed_message,
user_id=request.user_id
)
return {"status": "success", "message": "Message processed", "response": response}
except Exception as e:
print(f"Error processing channel message webhook: {str(e)}")
return {"status": "error", "message": f"Error processing message: {str(e)}"}