Latest fixxes

This commit is contained in:
Iyeoluwa Akinrinola
2025-05-16 13:23:35 +01:00
parent f00941cece
commit e82861a5db
7 changed files with 709 additions and 13 deletions
+5
View File
@@ -1,6 +1,7 @@
# API configuration
API_HOST=0.0.0.0
API_PORT=5251
PUBLIC_URL=http://your-public-url:5251 # Public URL for webhooks, needed for OpenWebUI to send channel messages
# OpenWebUI configuration
OPENWEBUI_URL=http://104.225.217.215:8080
@@ -13,3 +14,7 @@ DEFAULT_MODEL=llama3.1
# Document processing
CHUNK_SIZE=1000
CHUNK_OVERLAP=200
# AI bot configuration
AI_TRIGGERS=@ai,@bot,@assistant,@chatbot # Comma-separated list of triggers that will make the AI respond
AI_RESPOND_TO_ALL=false # Set to 'true' to make the AI respond to all messages, 'false' to only respond to mentions
+152
View File
@@ -13,6 +13,7 @@ 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
# Create FastAPI app
@@ -37,6 +38,27 @@ 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
@app.on_event("startup")
async def startup_event():
"""
Register webhook for channel messages on startup.
"""
# 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
# Register webhook
webhook_url = f"{service_url}/webhooks/channel-message"
print(f"Registering webhook for channel messages: {webhook_url}")
success = openwebui_channels.register_webhook(webhook_url)
if success:
print("Successfully registered webhook for channel messages")
else:
print("Failed to register webhook for channel messages")
# Define API models for health check
class HealthResponse(BaseModel):
"""Response model for health check."""
@@ -450,3 +472,133 @@ async def delete_chat(chat_id: str):
raise HTTPException(status_code=404, detail="Chat not found")
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(f"Received channel message webhook: {request.channel_id}, {request.user_id}, {request.message}")
# Find the chat associated with this OpenWebUI channel
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
break
if not chat_id:
print(f"No chat found for OpenWebUI channel {request.channel_id}")
return {"status": "error", "message": "No chat found for this channel"}
# 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)}"}
+13 -5
View File
@@ -3,12 +3,15 @@ Configuration settings for the AI service.
"""
import os
from dotenv import load_dotenv
# Load environment variables from .env file
import os.path
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path=dotenv_path)
# Try to load environment variables from .env file
try:
from dotenv import load_dotenv
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path=dotenv_path)
except ImportError:
print("Warning: python-dotenv not installed. Using environment variables directly.")
class Config:
"""Base configuration."""
@@ -16,6 +19,7 @@ class Config:
# API configuration
API_HOST = os.environ.get('API_HOST', '0.0.0.0')
API_PORT = int(os.environ.get('API_PORT', 5252))
PUBLIC_URL = os.environ.get('PUBLIC_URL', '') # Public URL for webhooks
# OpenWebUI configuration
OPENWEBUI_URL = os.environ.get('OPENWEBUI_URL', 'http://104.225.217.215:8080')
@@ -30,5 +34,9 @@ class Config:
CHUNK_SIZE = int(os.environ.get('CHUNK_SIZE', 1000))
CHUNK_OVERLAP = int(os.environ.get('CHUNK_OVERLAP', 200))
# AI bot configuration
AI_TRIGGERS = os.environ.get('AI_TRIGGERS', '@ai,@bot,@assistant,@chatbot').split(',')
AI_RESPOND_TO_ALL = os.environ.get('AI_RESPOND_TO_ALL', 'false').lower() == 'true'
config = Config()
+105 -8
View File
@@ -11,6 +11,7 @@ from typing import List, Dict, Any, Optional
from ai_service.config import config
from ai_service.models.model_service import model_service
from ai_service.models.model_parameters import ModelParameters
from ai_service.openwebui_channels import openwebui_channels
class ChatService:
"""Service for chat functionality."""
@@ -61,17 +62,40 @@ class ChatService:
# Generate a unique ID for the chat
chat_id = str(uuid.uuid4())
# Default title if none provided
chat_title = title or f"Chat {len(self.chats) + 1}"
# For team chats, create an OpenWebUI channel
openwebui_channel_id = None
if is_team_chat:
try:
# Create a channel in OpenWebUI
channel_response = openwebui_channels.create_channel(
name=chat_title,
description=f"Team chat created by {user_id}",
is_private=True # Team chats are private by default
)
if channel_response:
openwebui_channel_id = channel_response.get('id')
print(f"Created OpenWebUI channel with ID: {openwebui_channel_id}")
else:
print("Failed to create OpenWebUI channel, continuing with local team chat only")
except Exception as e:
print(f"Error creating OpenWebUI channel: {str(e)}")
# Create chat data
self.chats[chat_id] = {
'id': chat_id,
'title': title or f"Chat {len(self.chats) + 1}",
'title': chat_title,
'user_id': user_id,
'model_id': model_id or config.DEFAULT_MODEL,
'is_team_chat': is_team_chat,
'created_at': datetime.utcnow().isoformat(),
'updated_at': datetime.utcnow().isoformat(),
'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat(),
'messages': [],
'team_members': [user_id] if is_team_chat else []
'team_members': [user_id] if is_team_chat else [],
'openwebui_channel_id': openwebui_channel_id
}
# Save chats to file
@@ -96,20 +120,47 @@ class ChatService:
if chat_id not in self.chats:
raise ValueError(f"Chat with ID {chat_id} not found")
chat = self.chats[chat_id]
# Create message data
message = {
'id': str(uuid.uuid4()),
'content': content,
'user_id': user_id if is_user_message else None,
'is_user_message': is_user_message,
'timestamp': datetime.utcnow().isoformat()
'timestamp': datetime.now().isoformat()
}
# Add message to chat
self.chats[chat_id]['messages'].append(message)
chat['messages'].append(message)
# Update chat timestamp
self.chats[chat_id]['updated_at'] = datetime.utcnow().isoformat()
chat['updated_at'] = datetime.now().isoformat()
# If this is a team chat with an OpenWebUI channel, send the message there too
if chat['is_team_chat'] and 'openwebui_channel_id' in chat and chat['openwebui_channel_id']:
try:
# For AI responses, use a special AI user ID
# This ensures the AI's messages are properly distinguished in OpenWebUI
sender_id = "ai-assistant" if not is_user_message else user_id
# Format the message for OpenWebUI
if is_user_message:
# For user messages, use the regular format
formatted_content = content
else:
# For AI responses, add a special prefix to make it clear it's from the AI
# This helps visually distinguish AI responses in the channel
formatted_content = f"🤖 {content}"
# Send message to OpenWebUI channel
openwebui_channels.send_message(
channel_id=chat['openwebui_channel_id'],
message=formatted_content,
user_id=sender_id
)
except Exception as e:
print(f"Error sending message to OpenWebUI channel: {str(e)}")
# Save chats to file
self._save_chats()
@@ -140,7 +191,7 @@ class ChatService:
"""
user_chats = []
for chat_id, chat in self.chats.items():
for _, chat in self.chats.items():
# Include private chats owned by the user
if chat['user_id'] == user_id and not chat['is_team_chat']:
user_chats.append(chat)
@@ -173,6 +224,21 @@ class ChatService:
if not chat['is_team_chat']:
return False
# Add to OpenWebUI channel if available
if 'openwebui_channel_id' in chat and chat['openwebui_channel_id']:
try:
# Add member to OpenWebUI channel
openwebui_success = openwebui_channels.add_member(
channel_id=chat['openwebui_channel_id'],
user_id=user_id
)
if not openwebui_success:
print(f"Warning: Failed to add user {user_id} to OpenWebUI channel {chat['openwebui_channel_id']}")
except Exception as e:
print(f"Error adding member to OpenWebUI channel: {str(e)}")
# Add to local team members list
if user_id not in chat['team_members']:
chat['team_members'].append(user_id)
self._save_chats()
@@ -198,6 +264,21 @@ class ChatService:
if not chat['is_team_chat']:
return False
# Remove from OpenWebUI channel if available
if 'openwebui_channel_id' in chat and chat['openwebui_channel_id']:
try:
# Remove member from OpenWebUI channel
openwebui_success = openwebui_channels.remove_member(
channel_id=chat['openwebui_channel_id'],
user_id=user_id
)
if not openwebui_success:
print(f"Warning: Failed to remove user {user_id} from OpenWebUI channel {chat['openwebui_channel_id']}")
except Exception as e:
print(f"Error removing member from OpenWebUI channel: {str(e)}")
# Remove from local team members list
if user_id in chat['team_members']:
chat['team_members'].remove(user_id)
self._save_chats()
@@ -217,6 +298,22 @@ class ChatService:
if chat_id not in self.chats:
return False
chat = self.chats[chat_id]
# Delete OpenWebUI channel if this is a team chat
if chat['is_team_chat'] and 'openwebui_channel_id' in chat and chat['openwebui_channel_id']:
try:
# Delete the OpenWebUI channel
openwebui_success = openwebui_channels.delete_channel(
channel_id=chat['openwebui_channel_id']
)
if not openwebui_success:
print(f"Warning: Failed to delete OpenWebUI channel {chat['openwebui_channel_id']}")
except Exception as e:
print(f"Error deleting OpenWebUI channel: {str(e)}")
# Delete the chat from local storage
del self.chats[chat_id]
self._save_chats()
+275
View File
@@ -0,0 +1,275 @@
"""
OpenWebUI channels integration for team chats.
This module provides functions to interact with OpenWebUI channels API
for creating and managing team chats through OpenWebUI's channels feature.
It also includes functionality to listen for and respond to messages in OpenWebUI channels.
"""
import requests
import json
from typing import List, Dict, Any, Optional
from ai_service.config import config
class OpenWebUIChannels:
"""Class for interacting with OpenWebUI channels."""
def __init__(self):
"""Initialize the OpenWebUI channels integration."""
self.openwebui_url = config.OPENWEBUI_URL
self.openwebui_api_key = config.OPENWEBUI_API_KEY
self.headers = {
"Content-Type": "application/json"
}
# Add API key if available
if self.openwebui_api_key:
self.headers["Authorization"] = f"Bearer {self.openwebui_api_key}"
def create_channel(self, name: str, description: str = "", is_private: bool = False) -> Optional[Dict[str, Any]]:
"""
Create a new channel in OpenWebUI.
Args:
name: Name of the channel.
description: Description of the channel.
is_private: Whether the channel is private.
Returns:
Channel data if creation was successful, None otherwise.
"""
try:
# Prepare channel data
channel_data = {
"name": name,
"description": description,
"is_private": is_private
}
# Make API request to create channel
response = requests.post(
f"{self.openwebui_url}/api/channels",
headers=self.headers,
json=channel_data,
timeout=30
)
if response.status_code == 200 or response.status_code == 201:
return response.json()
else:
print(f"Error creating channel: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f"Error creating channel: {str(e)}")
return None
def get_channel(self, channel_id: str) -> Optional[Dict[str, Any]]:
"""
Get a channel by ID.
Args:
channel_id: ID of the channel.
Returns:
Channel data if found, None otherwise.
"""
try:
# Make API request to get channel
response = requests.get(
f"{self.openwebui_url}/api/channels/{channel_id}",
headers=self.headers,
timeout=30
)
if response.status_code == 200:
return response.json()
else:
print(f"Error getting channel: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f"Error getting channel: {str(e)}")
return None
def get_channels(self) -> List[Dict[str, Any]]:
"""
Get all channels.
Returns:
List of channel data.
"""
try:
# Make API request to get channels
response = requests.get(
f"{self.openwebui_url}/api/channels",
headers=self.headers,
timeout=30
)
if response.status_code == 200:
return response.json()
else:
print(f"Error getting channels: {response.status_code} - {response.text}")
return []
except Exception as e:
print(f"Error getting channels: {str(e)}")
return []
def add_member(self, channel_id: str, user_id: str) -> bool:
"""
Add a user to a channel.
Args:
channel_id: ID of the channel.
user_id: ID of the user to add.
Returns:
True if addition was successful, False otherwise.
"""
try:
# Prepare member data
member_data = {
"user_id": user_id
}
# Make API request to add member
response = requests.post(
f"{self.openwebui_url}/api/channels/{channel_id}/members",
headers=self.headers,
json=member_data,
timeout=30
)
return response.status_code == 200 or response.status_code == 201
except Exception as e:
print(f"Error adding member to channel: {str(e)}")
return False
def remove_member(self, channel_id: str, user_id: str) -> bool:
"""
Remove a user from a channel.
Args:
channel_id: ID of the channel.
user_id: ID of the user to remove.
Returns:
True if removal was successful, False otherwise.
"""
try:
# Make API request to remove member
response = requests.delete(
f"{self.openwebui_url}/api/channels/{channel_id}/members/{user_id}",
headers=self.headers,
timeout=30
)
return response.status_code == 200 or response.status_code == 204
except Exception as e:
print(f"Error removing member from channel: {str(e)}")
return False
def delete_channel(self, channel_id: str) -> bool:
"""
Delete a channel.
Args:
channel_id: ID of the channel.
Returns:
True if deletion was successful, False otherwise.
"""
try:
# Make API request to delete channel
response = requests.delete(
f"{self.openwebui_url}/api/channels/{channel_id}",
headers=self.headers,
timeout=30
)
return response.status_code == 200 or response.status_code == 204
except Exception as e:
print(f"Error deleting channel: {str(e)}")
return False
def register_webhook(self, webhook_url: str) -> bool:
"""
Register a webhook to receive channel messages.
Args:
webhook_url: URL of the webhook endpoint.
Returns:
True if registration was successful, False otherwise.
"""
try:
# Prepare webhook data
webhook_data = {
"url": webhook_url,
"events": ["channel_message"]
}
# Make API request to register webhook
response = requests.post(
f"{self.openwebui_url}/api/webhooks",
headers=self.headers,
json=webhook_data,
timeout=30
)
if response.status_code == 200 or response.status_code == 201:
print(f"Successfully registered webhook: {webhook_url}")
return True
else:
print(f"Error registering webhook: {response.status_code} - {response.text}")
return False
except Exception as e:
print(f"Error registering webhook: {str(e)}")
return False
def send_message(self, channel_id: str, message: str, user_id: str) -> Optional[Dict[str, Any]]:
"""
Send a message to a channel.
Args:
channel_id: ID of the channel.
message: Message content.
user_id: ID of the user sending the message.
Returns:
Message data if sending was successful, None otherwise.
"""
try:
# Prepare message data
message_data = {
"content": message,
"user_id": user_id
}
# Make API request to send message
response = requests.post(
f"{self.openwebui_url}/api/channels/{channel_id}/messages",
headers=self.headers,
json=message_data,
timeout=30
)
if response.status_code == 200 or response.status_code == 201:
return response.json()
else:
print(f"Error sending message to channel: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f"Error sending message to channel: {str(e)}")
return None
# Create a singleton instance
openwebui_channels = OpenWebUIChannels()