diff --git a/README.md b/README.md index 971b1a5..2262c05 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A backend service for OpenWebUI that provides OpenWebUI-compatible API endpoints - Ollama API proxy - Chat functionality with model switching - Support for multiple LLM models (gemma3, llama3.3, llama3.1, mistral, deepseek) +- Team chat integration with OpenWebUI channels ## Technology Stack @@ -25,6 +26,7 @@ ai_service/ ├── embeddings/ # Document processing for RAG │ └── document_service.py ├── openwebui_api.py # OpenWebUI-compatible API endpoints +├── openwebui_channels.py # OpenWebUI channels integration ├── config.py # Configuration settings ├── api.py # FastAPI application └── deploy.sh # Deployment script @@ -88,3 +90,7 @@ To configure OpenWebUI to use this service as its backend: ``` 2. Restart OpenWebUI to apply the changes. + +## Team Chat Feature + +The service now integrates with OpenWebUI's channels feature to provide team chat functionality. See [TEAM_CHAT_GUIDE.md](TEAM_CHAT_GUIDE.md) for detailed instructions on how to use team chats. diff --git a/TEAM_CHAT_GUIDE.md b/TEAM_CHAT_GUIDE.md new file mode 100644 index 0000000..8712f69 --- /dev/null +++ b/TEAM_CHAT_GUIDE.md @@ -0,0 +1,153 @@ +# Team Chat Guide + +This guide explains how to use the team chat feature with OpenWebUI channels. + +## Overview + +The chatbot now integrates with OpenWebUI's channels feature to provide team chat functionality. When you create a team chat in the chatbot, it automatically creates a corresponding channel in OpenWebUI, allowing multiple users to participate in the same conversation. + +## Using Team Chats + +### AI Responses in Team Chats + +When you send a message in a team chat, the AI will respond both in your local chat and in the OpenWebUI channel. The AI's responses in OpenWebUI channels: + +- Are prefixed with a robot emoji (🤖) to clearly identify them as AI responses +- Appear with a special "ai-assistant" user ID to distinguish them from human users +- Are visible to all members of the channel in real-time + +This allows all team members to see the conversation with the AI and collaborate effectively. + +### How the AI Bot is Triggered + +The AI bot can be triggered to respond in two ways: + +1. **Through the API**: When you send a message using the `/chats/{chat_id}/messages` endpoint, the AI automatically processes it and responds. + +2. **Directly in OpenWebUI**: When someone mentions the AI in an OpenWebUI channel that's linked to a team chat: + - The message is sent to your service through a webhook + - If it contains an AI mention (like `@ai`, `@bot`, etc.), the AI processes it + - The AI sends its response back to the OpenWebUI channel + +By default, the AI only responds when explicitly mentioned with one of these triggers: +- `@ai` +- `@bot` +- `@assistant` +- `@chatbot` + +You can customize these triggers or make the AI respond to all messages by changing the settings in your `.env` file: + +``` +# To customize the triggers that activate the AI +AI_TRIGGERS=@ai,@bot,@assistant,@chatbot + +# To make the AI respond to all messages (not just mentions) +AI_RESPOND_TO_ALL=false # Change to 'true' to respond to everything +``` + +This mention-based approach ensures the AI only joins the conversation when explicitly invited, making team chats more focused and preventing the AI from responding to every message. + +### Creating a Team Chat + +You can create a team chat in two ways: + +1. **Through the API**: + ``` + POST /chats + { + "user_id": "your-user-id", + "title": "Team Chat Name", + "model_id": "llama3.1", + "is_team_chat": true + } + ``` + +2. **Through OpenWebUI**: + - Log in to OpenWebUI at http://104.225.217.215:8080/ + - Navigate to the Channels section (look for a "#" or group icon in the sidebar) + - Click "Create Channel" or "+" button + - Give your channel a name and description + - Choose whether it should be private or public + - Click "Create" + +### Adding Members to a Team Chat + +1. **Through the API**: + ``` + POST /chats/{chat_id}/members/{user_id} + ``` + +2. **Through OpenWebUI**: + - Open the channel in OpenWebUI + - Look for a "Members" or "Invite" option + - Add users by their username or email + +### Sending Messages in a Team Chat + +1. **Through the API**: + ``` + POST /chats/{chat_id}/messages + { + "message": "Your message", + "user_id": "your-user-id" + } + ``` + +2. **Through OpenWebUI**: + - Open the channel in OpenWebUI + - Type your message in the input box + - Press Enter to send + +### Viewing Team Chats + +1. **Through the API**: + ``` + GET /chats/user/{user_id} + ``` + This will return all chats for the user, including team chats where they are a member. + +2. **Through OpenWebUI**: + - Log in to OpenWebUI + - Navigate to the Channels section + - You'll see all channels you're a member of + +## Technical Implementation + +The team chat feature works by: + +1. Creating an OpenWebUI channel when a team chat is created +2. Adding members to both the local team chat and the OpenWebUI channel +3. Sending messages to both the local chat and the OpenWebUI channel +4. Deleting the OpenWebUI channel when the team chat is deleted + +## Troubleshooting + +If you encounter issues with team chats: + +1. **Channel not appearing in OpenWebUI**: + - Check if the OpenWebUI server is running + - Verify that the OpenWebUI URL and API key are correctly configured in the `.env` file + +2. **Cannot add members to a team chat**: + - Ensure the user exists in OpenWebUI + - Check if the team chat was properly created with `is_team_chat: true` + +3. **Messages not appearing in OpenWebUI channel**: + - Check the logs for any errors when sending messages + - Verify that the OpenWebUI channel ID is correctly stored in the chat data + +## API Reference + +### Team Chat Endpoints + +- `POST /chats` - Create a new chat (set `is_team_chat: true` for team chats) +- `GET /chats/user/{user_id}` - Get all chats for a user (includes team chats) +- `POST /chats/{chat_id}/members/{user_id}` - Add a user to a team chat +- `DELETE /chats/{chat_id}/members/{user_id}` - Remove a user from a team chat +- `DELETE /chats/{chat_id}` - Delete a chat (also deletes the OpenWebUI channel) + +### OpenWebUI Channel Endpoints + +- `GET /channels` - Get all OpenWebUI channels +- `GET /channels/{channel_id}` - Get an OpenWebUI channel by ID +- `POST /channels` - Create a new OpenWebUI channel diff --git a/ai_service/.env.example b/ai_service/.env.example index 8fd3757..5b297c6 100644 --- a/ai_service/.env.example +++ b/ai_service/.env.example @@ -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 diff --git a/ai_service/api.py b/ai_service/api.py index 9ec2d4e..c849b01 100644 --- a/ai_service/api.py +++ b/ai_service/api.py @@ -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)}"} diff --git a/ai_service/config.py b/ai_service/config.py index 3c065e0..7fc500f 100644 --- a/ai_service/config.py +++ b/ai_service/config.py @@ -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() diff --git a/ai_service/models/chat_service.py b/ai_service/models/chat_service.py index e9416a2..1901c03 100644 --- a/ai_service/models/chat_service.py +++ b/ai_service/models/chat_service.py @@ -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() diff --git a/ai_service/openwebui_channels.py b/ai_service/openwebui_channels.py new file mode 100644 index 0000000..d490a0b --- /dev/null +++ b/ai_service/openwebui_channels.py @@ -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()