diff --git a/.env.backend b/.env.backend new file mode 100644 index 0000000..ad713e2 --- /dev/null +++ b/.env.backend @@ -0,0 +1,11 @@ +# Application +DEBUG=false +HOST=0.0.0.0 +PORT=8000 + +# Database +DATABASE_URL=postgresql://wingman:wingman@postgres:5432/wingman + +# Chroma +CHROMA_HOST=chroma +CHROMA_PORT=8000 diff --git a/.env.chroma b/.env.chroma new file mode 100644 index 0000000..9460e3a --- /dev/null +++ b/.env.chroma @@ -0,0 +1 @@ +IS_PERSISTENT=TRUE diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9c1bf04 --- /dev/null +++ b/.env.example @@ -0,0 +1,103 @@ +# Wingman Environment Configuration +# Copy this file to .env and fill in your values + +# ============================================================================ +# Slack Configuration +# ============================================================================ + +# Required: Bot User OAuth Token (starts with xoxb-) +# Used for: Posting messages, reading channels, bot operations +# Scope: See SLACK_AUTH.md for required scopes +SLACK_BOT_TOKEN=xoxb-your-bot-token-here + +# Required: App-Level Token (starts with xapp-) +# Used for: Socket Mode connection (recommended for development) +# Note: Only needed if using Socket Mode +SLACK_APP_TOKEN=xapp-your-app-token-here + +# Required: Signing Secret +# Used for: Verifying requests from Slack +# Found in: Slack App Settings > Basic Information +SLACK_SIGNING_SECRET=your-signing-secret-here + +# Optional: User OAuth Token (starts with xoxp-) +# Used for: User-level actions (reading private messages, etc.) +# Note: Only needed for specific user actions +SLACK_USER_TOKEN= + +# Optional: OAuth Credentials (for OAuth flow) +# Used for: Installing the app via OAuth +SLACK_CLIENT_ID= +SLACK_CLIENT_SECRET= + +# ============================================================================ +# AI/LLM Configuration +# ============================================================================ + +# Option 1: OpenRouter (Recommended) +# Get your API key from: https://openrouter.ai/keys +OPENROUTER_API_KEY=sk-or-your-openrouter-key-here + +# Option 2: OpenAI (Alternative) +# Get your API key from: https://platform.openai.com/api-keys +OPENAI_API_KEY= + +# LLM Model to use +# OpenRouter format: "openai/gpt-4-turbo-preview", "anthropic/claude-3-opus" +# OpenAI format: "gpt-4-turbo-preview", "gpt-3.5-turbo" +LLM_MODEL=openai/gpt-4-turbo-preview + +# LLM Settings +LLM_TEMPERATURE=0.7 +LLM_MAX_TOKENS=2000 + +# ============================================================================ +# Database Configuration +# ============================================================================ + +# PostgreSQL connection (docker-compose handles this automatically) +DATABASE_URL=postgresql://wingman:wingman@postgres:5432/wingman + +# For local development outside Docker: +# DATABASE_URL=postgresql://wingman:wingman@localhost:5432/wingman + +# ============================================================================ +# Vector Store Configuration +# ============================================================================ + +# Chroma settings (docker-compose handles this automatically) +CHROMA_HOST=chroma +CHROMA_PORT=8000 +CHROMA_COLLECTION=slack_messages + +# For local development outside Docker: +# CHROMA_HOST=localhost +# CHROMA_PORT=8001 + +# ============================================================================ +# RAG Configuration +# ============================================================================ + +# Embedding model for vector search +EMBEDDING_MODEL=text-embedding-ada-002 + +# Text chunking settings +CHUNK_SIZE=1000 +CHUNK_OVERLAP=200 + +# Retrieval settings +RETRIEVAL_TOP_K=5 + +# ============================================================================ +# Application Configuration +# ============================================================================ + +# Debug mode (set to true for development) +DEBUG=false + +# Server settings +HOST=0.0.0.0 +PORT=8000 + +# Frontend API URL +NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/.env.frontend b/.env.frontend new file mode 100644 index 0000000..600de8d --- /dev/null +++ b/.env.frontend @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/.env.postgres b/.env.postgres new file mode 100644 index 0000000..f4d7952 --- /dev/null +++ b/.env.postgres @@ -0,0 +1,3 @@ +POSTGRES_DB=wingman +POSTGRES_USER=wingman +POSTGRES_PASSWORD=wingman diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fcffb8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +# Keep .env.* templates but ignore local overrides +.env.*.local + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.log + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +package-lock.json +yarn.lock + +# Next.js +.next/ +out/ +.vercel + +# Database +*.db +*.sqlite +*.sqlite3 + +# Docker +*.log + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# OS +Thumbs.db diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d4bedb4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,292 @@ +# Contributing to Wingman + +Thank you for your interest in contributing to Wingman! This document provides guidelines and instructions for contributing. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Making Changes](#making-changes) +- [Testing](#testing) +- [Submitting Changes](#submitting-changes) +- [Style Guidelines](#style-guidelines) + +## Code of Conduct + +By participating in this project, you agree to maintain a respectful and inclusive environment for everyone. + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/wingman.git` +3. Add upstream remote: `git remote add upstream https://github.com/echohello-dev/wingman.git` + +## Development Setup + +### Prerequisites + +- Docker and Docker Compose +- Python 3.11+ +- Node.js 18+ +- Git + +### Initial Setup + +```bash +# Clone the repository +git clone https://github.com/echohello-dev/wingman.git +cd wingman + +# Copy environment file +cp .env.example .env + +# Install dependencies +mise run install + +# Start services +mise run up +``` + +### Backend Development + +```bash +cd backend + +# Create virtual environment +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt + +# Run tests +pytest + +# Run backend +uvicorn app.main:app --reload +``` + +### Frontend Development + +```bash +cd frontend + +# Install dependencies +npm install + +# Run development server +npm run dev + +# Run tests +npm test + +# Build +npm run build +``` + +## Making Changes + +### Branch Naming + +Use descriptive branch names: +- `feature/add-slack-reactions` +- `fix/database-connection-issue` +- `docs/update-setup-guide` + +### Commit Messages + +Follow conventional commits: +``` +feat: add reaction-based feedback +fix: resolve database connection timeout +docs: update SETUP.md with new instructions +test: add tests for RAG engine +refactor: simplify vector store initialization +``` + +### Pull Request Process + +1. Create a feature branch from `main` +2. Make your changes +3. Write/update tests +4. Update documentation if needed +5. Run tests and linting +6. Submit a pull request + +## Testing + +### Backend Tests + +```bash +cd backend +pytest + +# With coverage +pytest --cov=app --cov-report=html + +# Specific test +pytest tests/test_api.py -v +``` + +### Frontend Tests + +```bash +cd frontend +npm test + +# With coverage +npm test -- --coverage + +# Watch mode +npm test -- --watch +``` + +### Integration Tests + +```bash +# Start all services +mise run up + +# Run integration tests +mise run test +``` + +## Style Guidelines + +### Python (Backend) + +- Follow PEP 8 +- Use type hints +- Maximum line length: 100 characters +- Use docstrings for functions and classes + +```python +def generate_response(question: str, channel_id: str = None) -> Dict[str, Any]: + """ + Generate a response using RAG + + Args: + question: The user's question + channel_id: Optional channel ID to filter context + + Returns: + Dictionary with response and metadata + """ + pass +``` + +### TypeScript (Frontend) + +- Follow ESLint configuration +- Use TypeScript strict mode +- Prefer functional components +- Use meaningful variable names + +```typescript +interface QuestionResponse { + answer: string + sources: Array> + confidence: string +} + +export async function askQuestion( + question: string, + channelId?: string +): Promise { + // Implementation +} +``` + +### Documentation + +- Use Markdown format +- Include code examples +- Keep language clear and concise +- Update docs with code changes + +## Project Structure + +``` +wingman/ +├── backend/ # Python backend +│ ├── app/ +│ │ ├── main.py # FastAPI app +│ │ ├── slack_bot.py # Slack integration +│ │ ├── rag.py # RAG engine +│ │ └── ... +│ └── tests/ # Backend tests +├── frontend/ # Next.js frontend +│ ├── app/ # Next.js app directory +│ ├── components/ # React components +│ ├── lib/ # Utilities +│ └── ... +├── docs/ # Documentation +└── docker compose.yml # Docker setup +``` + +## Common Tasks + +### Adding a New API Endpoint + +1. Add route in `backend/app/main.py` +2. Add request/response models +3. Implement handler function +4. Add tests in `backend/tests/` +5. Update API documentation + +### Adding a New Slack Event + +1. Add handler in `backend/app/slack_bot.py` +2. Register event in `_register_handlers` +3. Update Slack app event subscriptions +4. Add tests + +### Adding a Frontend Component + +1. Create component in `frontend/components/` +2. Add TypeScript types +3. Import and use in pages +4. Add styling with Tailwind +5. Add tests if applicable + +## Debugging + +### Backend Debugging + +```bash +# View logs +mise run logs-backend + +# Access container +mise run shell-backend + +# Check database +mise run shell-db +``` + +### Frontend Debugging + +```bash +# View logs +mise run logs-frontend + +# Run with verbose logging +cd frontend +npm run dev -- --verbose +``` + +## Getting Help + +- Open an issue for bugs +- Start a discussion for questions +- Check existing issues/PRs first +- Provide detailed information + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. + +## Thank You! + +Your contributions make Wingman better for everyone. Thank you for taking the time to contribute! 🙏 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..15414f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Wingman Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9fc6865 --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +.PHONY: help up down logs build clean test-backend test-frontend test + +help: ## Show this help message + @echo "Wingman - Slack Support Assistant" + @echo "" + @echo "Available commands:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' + +up: ## Start all services + docker-compose up -d + +down: ## Stop all services + docker-compose down + +logs: ## View logs from all services + docker-compose logs -f + +logs-backend: ## View backend logs + docker-compose logs -f backend + +logs-bot: ## View bot logs + docker-compose logs -f bot + +logs-frontend: ## View frontend logs + docker-compose logs -f frontend + +build: ## Build all Docker images + docker-compose build + +clean: ## Remove all containers and volumes + docker-compose down -v + +restart: ## Restart all services + docker-compose restart + +ps: ## Show running services + docker-compose ps + +test-backend: ## Run backend tests + cd backend && pytest + +test-frontend: ## Run frontend tests + cd frontend && npm test + +test: test-backend ## Run all tests + +shell-backend: ## Open shell in backend container + docker-compose exec backend /bin/sh + +shell-db: ## Open PostgreSQL shell + docker-compose exec postgres psql -U wingman -d wingman + +init: ## Initialize project (copy .env.example to .env) + cp .env.example .env + @echo "✅ Created .env file. Please edit it with your credentials." + +install-backend: ## Install backend dependencies locally + cd backend && pip install -r requirements.txt + +install-frontend: ## Install frontend dependencies locally + cd frontend && npm install + +install: install-backend install-frontend ## Install all dependencies locally + +dev-backend: ## Run backend in development mode + cd backend && uvicorn app.main:app --reload + +dev-frontend: ## Run frontend in development mode + cd frontend && npm run dev + +dev-bot: ## Run bot in development mode + cd backend && python run_bot.py diff --git a/README.md b/README.md index 0837a27..ded2040 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,207 @@ -# wingman -A Slackbot that can help enhance your channel responses +# 🛩️ Wingman - Slack Support Assistant + +An AI-powered Slack support assistant with RAG (Retrieval Augmented Generation) capabilities. Wingman uses LangChain and OpenRouter to provide intelligent responses based on your Slack threads, conversations, and documentation. + +## 🌟 Features + +- **🤖 Slack Integration**: Full Slack Bolt SDK integration with support for mentions, DMs, and slash commands +- **🧠 RAG-Powered Responses**: Uses LangChain + OpenRouter/OpenAI for context-aware answers +- **📚 Knowledge Base**: Index Slack threads and documents for intelligent retrieval +- **💾 Vector Storage**: ChromaDB for efficient semantic search +- **🗄️ PostgreSQL Database**: Persistent storage for messages and documents +- **📊 Next.js Dashboard**: Modern TypeScript dashboard for managing the assistant +- **🐳 Docker Ready**: Complete docker compose setup for local development + +## 🏗️ Architecture + +``` +wingman/ +├── backend/ # Python FastAPI + Slack Bolt backend +│ ├── app/ +│ │ ├── main.py # FastAPI application +│ │ ├── slack_bot.py # Slack Bolt bot +│ │ ├── rag.py # RAG engine +│ │ ├── vector_store.py # Chroma integration +│ │ ├── database.py # PostgreSQL models +│ │ └── config.py # Configuration +│ ├── requirements.txt +│ └── Dockerfile +├── frontend/ # Next.js/TypeScript dashboard +│ ├── app/ +│ ├── components/ +│ ├── lib/ +│ ├── package.json +│ └── Dockerfile +├── docs/ # Documentation +│ ├── SETUP.md +│ └── SLACK_AUTH.md +├── docker compose.yml +└── .env.example +``` + +## 🚀 Quick Start + +### Prerequisites + +- Docker and Docker Compose +- Slack workspace with admin access +- OpenRouter or OpenAI API key + +### 1. Clone the Repository + +```bash +git clone https://github.com/echohello-dev/wingman.git +cd wingman +``` + +### 2. Configure Environment + +```bash +cp .env.example .env +# Edit .env with your credentials +``` + +Required environment variables: +- `SLACK_BOT_TOKEN`: Bot User OAuth Token (xoxb-*) +- `SLACK_APP_TOKEN`: App-Level Token (xapp-*) for Socket Mode +- `SLACK_SIGNING_SECRET`: Signing secret from Slack app settings +- `OPENROUTER_API_KEY` or `OPENAI_API_KEY`: API key for LLM + +See [SLACK_AUTH.md](docs/SLACK_AUTH.md) for detailed Slack setup instructions. + +### 3. Start Services + +```bash +docker compose up -d +``` + +This will start: +- **Backend API** on http://localhost:8000 +- **Frontend Dashboard** on http://localhost:3000 +- **PostgreSQL** on port 5432 +- **ChromaDB** on port 8001 +- **Slack Bot** in Socket Mode + +### 4. Verify Installation + +```bash +# Check service health +docker compose ps + +# View logs +docker compose logs -f backend +docker compose logs -f bot +``` + +Visit http://localhost:3000 to access the dashboard. + +## 📖 Documentation + +- **[SETUP.md](docs/SETUP.md)**: Detailed setup and configuration guide +- **[SLACK_AUTH.md](docs/SLACK_AUTH.md)**: Slack authentication and token types + +## 🔧 Usage + +### In Slack + +1. **Mention the bot**: `@Wingman How do I reset my password?` +2. **Use slash command**: `/wingman What are the API rate limits?` +3. **Direct message**: Send a DM to Wingman for private assistance + +### Via Dashboard + +1. Navigate to http://localhost:3000 +2. Ask questions in the "Ask Question" tab +3. View indexed documents and messages + +### Via API + +```bash +# Ask a question +curl -X POST http://localhost:8000/api/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "How do I authenticate?"}' + +# Add a document +curl -X POST http://localhost:8000/api/documents \ + -H "Content-Type: application/json" \ + -d '{"title": "API Docs", "content": "...", "source": "docs"}' +``` + +## 🛠️ Development + +### Local Development (without Docker) + +#### Backend + +```bash +cd backend +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +pip install -r requirements.txt + +# Start PostgreSQL and Chroma separately or use Docker for them +docker compose up -d postgres chroma + +# Run backend +python -m uvicorn app.main:app --reload + +# Run bot separately +python run_bot.py +``` + +#### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +### Running Tests + +```bash +# Backend tests +cd backend +pytest + +# Frontend tests +cd frontend +npm test +``` + +## 🔐 Security Notes + +- Never commit `.env` files or secrets +- Use environment-specific tokens for development/production +- Rotate tokens regularly +- Follow the principle of least privilege for Slack scopes +- Review [SLACK_AUTH.md](docs/SLACK_AUTH.md) for security best practices + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## 📝 License + +This project is open source and available under the MIT License. + +## 🙏 Acknowledgments + +- Built with [FastAPI](https://fastapi.tiangolo.com/) +- [Slack Bolt](https://slack.dev/bolt-python/tutorial/getting-started) for Python +- [LangChain](https://www.langchain.com/) for RAG capabilities +- [OpenRouter](https://openrouter.ai/) for LLM access +- [ChromaDB](https://www.trychroma.com/) for vector storage +- [Next.js](https://nextjs.org/) for the dashboard + +## 📧 Support + +For issues and questions: +- Open an issue on GitHub +- Check the [documentation](docs/) +- Review Slack API docs at https://api.slack.com/ + +--- + +Made with ❤️ for better Slack support diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..7ff3314 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,23 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.env +.venv +venv/ +ENV/ +env/ +.pytest_cache +.coverage +htmlcov/ +*.log +.DS_Store +.vscode/ +.idea/ +tests/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..28e93e7 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,31 @@ +# Backend Dockerfile for Wingman +FROM python:3.14-slim + +WORKDIR /app + +# Install system dependencies and uv +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && curl -LsSf https://astral.sh/uv/install.sh | sh + +ENV PATH="/root/.cargo/bin:${PATH}" + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN uv pip install --system --no-cache -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8000/health')" + +# Run the application +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..9c75bce --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,6 @@ +""" +Wingman - Slack Support Assistant +A FastAPI + Slack Bolt backend with RAG capabilities +""" + +__version__ = "0.1.0" diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..2ed945b --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,54 @@ +""" +Configuration management for Wingman backend +""" +from pydantic_settings import BaseSettings +from typing import Optional + + +class Settings(BaseSettings): + """Application settings loaded from environment variables""" + + # Application + APP_NAME: str = "Wingman" + APP_VERSION: str = "0.1.0" + DEBUG: bool = False + + # Server + HOST: str = "0.0.0.0" + PORT: int = 8000 + + # Slack Configuration + SLACK_BOT_TOKEN: str # xoxb-* token + SLACK_APP_TOKEN: Optional[str] = None # xapp-* token for Socket Mode + SLACK_SIGNING_SECRET: str + SLACK_USER_TOKEN: Optional[str] = None # xoxp-* token for user actions + SLACK_CLIENT_ID: Optional[str] = None + SLACK_CLIENT_SECRET: Optional[str] = None + + # OpenRouter/OpenAI Configuration + OPENROUTER_API_KEY: Optional[str] = None + OPENAI_API_KEY: Optional[str] = None + LLM_MODEL: str = "openai/gpt-4-turbo-preview" + LLM_TEMPERATURE: float = 0.7 + LLM_MAX_TOKENS: int = 2000 + + # Database + DATABASE_URL: str = "postgresql://wingman:wingman@localhost:5432/wingman" + + # Chroma Vector Store + CHROMA_HOST: str = "localhost" + CHROMA_PORT: int = 8001 + CHROMA_COLLECTION: str = "slack_messages" + + # RAG Configuration + EMBEDDING_MODEL: str = "text-embedding-ada-002" + CHUNK_SIZE: int = 1000 + CHUNK_OVERLAP: int = 200 + RETRIEVAL_TOP_K: int = 5 + + class Config: + env_file = ".env" + case_sensitive = True + + +settings = Settings() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..be279e7 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,55 @@ +""" +Database models and connection management +""" +from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, JSON +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from datetime import datetime +from app.config import settings + +# Create SQLAlchemy engine +engine = create_engine(settings.DATABASE_URL, echo=settings.DEBUG) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +class SlackMessage(Base): + """Model for storing Slack messages""" + __tablename__ = "slack_messages" + + id = Column(Integer, primary_key=True, index=True) + message_ts = Column(String, unique=True, index=True) + channel_id = Column(String, index=True) + user_id = Column(String, index=True) + text = Column(Text) + thread_ts = Column(String, index=True, nullable=True) + metadata = Column(JSON, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + +class Document(Base): + """Model for storing knowledge base documents""" + __tablename__ = "documents" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + content = Column(Text) + source = Column(String) + metadata = Column(JSON, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + +def get_db(): + """Dependency for getting database session""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def init_db(): + """Initialize database tables""" + Base.metadata.create_all(bind=engine) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..a7b671a --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,238 @@ +""" +FastAPI main application +""" +from fastapi import FastAPI, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy.orm import Session +from typing import List, Dict, Any +from pydantic import BaseModel +import logging + +from app.config import settings +from app.database import get_db, init_db, SlackMessage, Document +from app.rag import rag_engine +from app.slack_bot import slack_bot + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create FastAPI app +app = FastAPI( + title=settings.APP_NAME, + version=settings.APP_VERSION, + description="Slack Support Assistant with RAG capabilities" +) + +# Configure CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Configure appropriately for production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# Request/Response Models +class QuestionRequest(BaseModel): + question: str + channel_id: str = None + + +class QuestionResponse(BaseModel): + answer: str + sources: List[Dict[str, Any]] + confidence: str + + +class DocumentRequest(BaseModel): + title: str + content: str + source: str = "api" + + +class MessageResponse(BaseModel): + id: int + message_ts: str + channel_id: str + user_id: str + text: str + + class Config: + from_attributes = True + + +# Startup/Shutdown Events +@app.on_event("startup") +async def startup_event(): + """Initialize services on startup""" + logger.info("Starting Wingman backend...") + + # Initialize database + init_db() + logger.info("Database initialized") + + # Could start Slack bot here if needed + # Note: For production, run Slack bot as a separate process + + +@app.on_event("shutdown") +async def shutdown_event(): + """Cleanup on shutdown""" + logger.info("Shutting down Wingman backend...") + + +# API Routes +@app.get("/") +async def root(): + """Root endpoint""" + return { + "name": settings.APP_NAME, + "version": settings.APP_VERSION, + "status": "running" + } + + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy"} + + +@app.post("/api/ask", response_model=QuestionResponse) +async def ask_question(request: QuestionRequest): + """ + Ask a question and get an AI-generated response + """ + try: + response = rag_engine.generate_response( + question=request.question, + channel_id=request.channel_id + ) + return QuestionResponse(**response) + except Exception as e: + logger.error(f"Error generating response: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/documents") +async def add_document(doc: DocumentRequest, db: Session = Depends(get_db)): + """ + Add a document to the knowledge base + """ + try: + # Store in database + db_doc = Document( + title=doc.title, + content=doc.content, + source=doc.source + ) + db.add(db_doc) + db.commit() + db.refresh(db_doc) + + # Index in vector store + rag_engine.index_document(doc.title, doc.content, doc.source) + + return { + "id": db_doc.id, + "message": "Document added successfully" + } + except Exception as e: + logger.error(f"Error adding document: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/documents") +async def list_documents(db: Session = Depends(get_db)): + """ + List all documents in the knowledge base + """ + try: + documents = db.query(Document).all() + return [ + { + "id": doc.id, + "title": doc.title, + "source": doc.source, + "created_at": doc.created_at + } + for doc in documents + ] + except Exception as e: + logger.error(f"Error listing documents: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/messages", response_model=List[MessageResponse]) +async def list_messages( + limit: int = 100, + channel_id: str = None, + db: Session = Depends(get_db) +): + """ + List recent Slack messages + """ + try: + query = db.query(SlackMessage) + + if channel_id: + query = query.filter(SlackMessage.channel_id == channel_id) + + messages = query.order_by(SlackMessage.created_at.desc()).limit(limit).all() + return messages + except Exception as e: + logger.error(f"Error listing messages: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/index/thread") +async def index_thread( + channel_id: str, + thread_ts: str, + db: Session = Depends(get_db) +): + """ + Index a Slack thread for retrieval + """ + try: + # Get messages from database + messages = db.query(SlackMessage).filter( + SlackMessage.channel_id == channel_id, + SlackMessage.thread_ts == thread_ts + ).all() + + if not messages: + raise HTTPException(status_code=404, detail="Thread not found") + + # Convert to dict format + message_dicts = [ + { + "text": msg.text, + "ts": msg.message_ts, + "user": msg.user_id, + "thread_ts": msg.thread_ts + } + for msg in messages + ] + + # Index thread + rag_engine.index_slack_thread(message_dicts, channel_id) + + return { + "message": f"Indexed {len(messages)} messages", + "thread_ts": thread_ts + } + except Exception as e: + logger.error(f"Error indexing thread: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "app.main:app", + host=settings.HOST, + port=settings.PORT, + reload=settings.DEBUG + ) diff --git a/backend/app/rag.py b/backend/app/rag.py new file mode 100644 index 0000000..2bb5852 --- /dev/null +++ b/backend/app/rag.py @@ -0,0 +1,152 @@ +""" +RAG (Retrieval Augmented Generation) implementation +""" +from langchain.chains import RetrievalQA +from langchain.prompts import PromptTemplate +from langchain_openai import ChatOpenAI +from typing import Dict, Any, List +from app.config import settings +from app.vector_store import vector_store + + +class RAGEngine: + """RAG engine for answering questions based on retrieved context""" + + def __init__(self): + """Initialize RAG components""" + # Initialize LLM + api_key = settings.OPENROUTER_API_KEY or settings.OPENAI_API_KEY + + # Use OpenRouter if available, otherwise OpenAI + if settings.OPENROUTER_API_KEY: + self.llm = ChatOpenAI( + model=settings.LLM_MODEL, + temperature=settings.LLM_TEMPERATURE, + max_tokens=settings.LLM_MAX_TOKENS, + openai_api_key=settings.OPENROUTER_API_KEY, + openai_api_base="https://openrouter.ai/api/v1" + ) + else: + self.llm = ChatOpenAI( + model=settings.LLM_MODEL, + temperature=settings.LLM_TEMPERATURE, + max_tokens=settings.LLM_MAX_TOKENS, + openai_api_key=settings.OPENAI_API_KEY + ) + + # Define prompt template + self.prompt_template = PromptTemplate( + template="""You are Wingman, a helpful Slack support assistant. +Use the following context from Slack threads and documentation to answer the question. +If you cannot find the answer in the context, say so and provide general guidance. + +Context: +{context} + +Question: {question} + +Answer:""", + input_variables=["context", "question"] + ) + + def generate_response(self, question: str, channel_id: str = None) -> Dict[str, Any]: + """ + Generate a response using RAG + + Args: + question: The user's question + channel_id: Optional channel ID to filter context + + Returns: + Dictionary with response and metadata + """ + # Search for relevant context + results = vector_store.similarity_search(question) + + # Filter by channel if specified + if channel_id: + results = [r for r in results if r.get("metadata", {}).get("channel_id") == channel_id] + + # Build context from results + context = "\n\n".join([ + f"From {r['metadata'].get('source', 'unknown')}:\n{r['content']}" + for r in results + ]) + + # Generate response + prompt = self.prompt_template.format(context=context, question=question) + response = self.llm.invoke(prompt) + + return { + "answer": response.content, + "sources": [r["metadata"] for r in results], + "confidence": "high" if results else "low" + } + + def index_slack_thread(self, messages: List[Dict[str, Any]], channel_id: str): + """ + Index a Slack thread for retrieval + + Args: + messages: List of message dictionaries + channel_id: Channel ID where the thread exists + """ + texts = [] + metadatas = [] + + for msg in messages: + texts.append(msg.get("text", "")) + metadatas.append({ + "source": "slack", + "channel_id": channel_id, + "message_ts": msg.get("ts"), + "user_id": msg.get("user"), + "thread_ts": msg.get("thread_ts") + }) + + if texts: + vector_store.add_documents(texts, metadatas) + + def index_document(self, title: str, content: str, source: str = "docs"): + """ + Index a document for retrieval + + Args: + title: Document title + content: Document content + source: Source of the document + """ + # Split content into chunks if needed + chunks = self._split_text(content) + + metadatas = [ + { + "source": source, + "title": title, + "chunk": i + } + for i in range(len(chunks)) + ] + + vector_store.add_documents(chunks, metadatas) + + def _split_text(self, text: str) -> List[str]: + """Simple text splitter""" + # Simple implementation - split by paragraphs or size + chunk_size = settings.CHUNK_SIZE + overlap = settings.CHUNK_OVERLAP + + chunks = [] + start = 0 + + while start < len(text): + end = start + chunk_size + chunk = text[start:end] + chunks.append(chunk) + start = end - overlap + + return chunks + + +# Global instance +rag_engine = RAGEngine() diff --git a/backend/app/slack_bot.py b/backend/app/slack_bot.py new file mode 100644 index 0000000..401e5c1 --- /dev/null +++ b/backend/app/slack_bot.py @@ -0,0 +1,170 @@ +""" +Slack Bot implementation using Slack Bolt +""" +from slack_bolt import App +from slack_bolt.adapter.socket_mode import SocketModeHandler +from slack_sdk import WebClient +from typing import Dict, Any +import logging +from app.config import settings +from app.rag import rag_engine +from app.database import SessionLocal, SlackMessage + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class SlackBot: + """Wingman Slack Bot with RAG capabilities""" + + def __init__(self): + """Initialize Slack Bot""" + self.app = App( + token=settings.SLACK_BOT_TOKEN, + signing_secret=settings.SLACK_SIGNING_SECRET + ) + self.client = WebClient(token=settings.SLACK_BOT_TOKEN) + + # Register event handlers + self._register_handlers() + + def _register_handlers(self): + """Register Slack event handlers""" + + @self.app.event("app_mention") + def handle_mention(event, say): + """Handle @mentions of the bot""" + logger.info(f"Received mention: {event}") + + try: + # Extract question from message + text = event.get("text", "") + # Remove bot mention + question = text.split(">", 1)[-1].strip() + + # Get channel and thread info + channel_id = event.get("channel") + thread_ts = event.get("thread_ts") or event.get("ts") + + # If in a thread, get thread context + if thread_ts: + thread_messages = self._get_thread_messages(channel_id, thread_ts) + # Index thread for context + rag_engine.index_slack_thread(thread_messages, channel_id) + + # Generate response using RAG + response = rag_engine.generate_response(question, channel_id) + + # Store message in database + self._store_message(event) + + # Reply in thread + say( + text=response["answer"], + thread_ts=thread_ts + ) + + except Exception as e: + logger.error(f"Error handling mention: {e}") + say( + text=f"Sorry, I encountered an error: {str(e)}", + thread_ts=event.get("thread_ts") or event.get("ts") + ) + + @self.app.event("message") + def handle_message(event, say): + """Handle direct messages""" + # Only respond to DMs, not channel messages + if event.get("channel_type") == "im": + logger.info(f"Received DM: {event}") + + try: + question = event.get("text", "") + + # Generate response + response = rag_engine.generate_response(question) + + # Store message + self._store_message(event) + + # Reply + say(text=response["answer"]) + + except Exception as e: + logger.error(f"Error handling message: {e}") + say(text=f"Sorry, I encountered an error: {str(e)}") + + @self.app.command("/wingman") + def handle_command(ack, command, say): + """Handle /wingman slash command""" + ack() + logger.info(f"Received command: {command}") + + try: + question = command.get("text", "") + + if not question: + say(text="How can I help you? Please provide a question.") + return + + # Generate response + response = rag_engine.generate_response(question) + + # Reply + say(text=response["answer"]) + + except Exception as e: + logger.error(f"Error handling command: {e}") + say(text=f"Sorry, I encountered an error: {str(e)}") + + @self.app.event("reaction_added") + def handle_reaction(event): + """Handle reactions to learn from user feedback""" + logger.info(f"Reaction added: {event}") + # Could use reactions like ✅ or ❌ to train the model + pass + + def _get_thread_messages(self, channel_id: str, thread_ts: str) -> list: + """Retrieve all messages in a thread""" + try: + result = self.client.conversations_replies( + channel=channel_id, + ts=thread_ts + ) + return result.get("messages", []) + except Exception as e: + logger.error(f"Error getting thread messages: {e}") + return [] + + def _store_message(self, event: Dict[str, Any]): + """Store message in database""" + try: + db = SessionLocal() + message = SlackMessage( + message_ts=event.get("ts"), + channel_id=event.get("channel"), + user_id=event.get("user"), + text=event.get("text"), + thread_ts=event.get("thread_ts"), + metadata=event + ) + db.add(message) + db.commit() + db.close() + except Exception as e: + logger.error(f"Error storing message: {e}") + + def start(self): + """Start the bot""" + if settings.SLACK_APP_TOKEN: + # Use Socket Mode for local development + logger.info("Starting bot in Socket Mode...") + handler = SocketModeHandler(self.app, settings.SLACK_APP_TOKEN) + handler.start() + else: + logger.info("Socket Mode not enabled. Use Socket Mode for local development.") + logger.info("For production, use a web server to handle events.") + + +# Global bot instance +slack_bot = SlackBot() diff --git a/backend/app/vector_store.py b/backend/app/vector_store.py new file mode 100644 index 0000000..08339c3 --- /dev/null +++ b/backend/app/vector_store.py @@ -0,0 +1,78 @@ +""" +Chroma vector store integration for RAG +""" +import chromadb +from chromadb.config import Settings as ChromaSettings +from langchain_community.vectorstores import Chroma +from langchain_openai import OpenAIEmbeddings +from typing import List, Dict, Any +from app.config import settings + + +class VectorStore: + """Manages vector storage and retrieval using Chroma""" + + def __init__(self): + """Initialize Chroma client and vector store""" + # Connect to Chroma server + self.client = chromadb.HttpClient( + host=settings.CHROMA_HOST, + port=settings.CHROMA_PORT + ) + + # Initialize embeddings + self.embeddings = OpenAIEmbeddings( + model=settings.EMBEDDING_MODEL, + openai_api_key=settings.OPENAI_API_KEY or settings.OPENROUTER_API_KEY + ) + + # Get or create collection + self.collection_name = settings.CHROMA_COLLECTION + self.vector_store = None + self._init_vector_store() + + def _init_vector_store(self): + """Initialize the vector store""" + try: + self.vector_store = Chroma( + client=self.client, + collection_name=self.collection_name, + embedding_function=self.embeddings + ) + except Exception as e: + print(f"Error initializing vector store: {e}") + raise + + def add_documents(self, texts: List[str], metadatas: List[Dict[str, Any]] = None): + """Add documents to the vector store""" + if not self.vector_store: + raise RuntimeError("Vector store not initialized") + + return self.vector_store.add_texts(texts=texts, metadatas=metadatas) + + def similarity_search(self, query: str, k: int = None) -> List[Dict[str, Any]]: + """Search for similar documents""" + if not self.vector_store: + raise RuntimeError("Vector store not initialized") + + k = k or settings.RETRIEVAL_TOP_K + results = self.vector_store.similarity_search(query, k=k) + + return [ + { + "content": doc.page_content, + "metadata": doc.metadata + } + for doc in results + ] + + def delete_collection(self): + """Delete the collection""" + try: + self.client.delete_collection(name=self.collection_name) + except Exception as e: + print(f"Error deleting collection: {e}") + + +# Global instance +vector_store = VectorStore() diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..9855d94 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --tb=short diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..1f684a8 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,32 @@ +# FastAPI and server dependencies +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +python-dotenv==1.0.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Slack SDK +slack-bolt==1.18.0 +slack-sdk==3.26.1 + +# LangChain and RAG +langchain==0.1.0 +langchain-community==0.0.10 +langchain-openai==0.0.2 +openai==1.6.1 + +# Vector store +chromadb==0.4.22 + +# Database +psycopg2-binary==2.9.9 +sqlalchemy==2.0.23 +alembic==1.13.1 + +# Additional utilities +httpx==0.25.2 +tenacity==8.2.3 + +# Testing +pytest==7.4.3 +pytest-asyncio==0.21.1 diff --git a/backend/run_bot.py b/backend/run_bot.py new file mode 100644 index 0000000..8d8f8e3 --- /dev/null +++ b/backend/run_bot.py @@ -0,0 +1,20 @@ +""" +Script to run the Slack bot +""" +import logging +from app.slack_bot import slack_bot +from app.database import init_db + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + logger.info("Initializing Wingman Slack Bot...") + + # Initialize database + init_db() + logger.info("Database initialized") + + # Start the bot + logger.info("Starting Slack bot...") + slack_bot.start() diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..f41d634 --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +# Tests for Wingman backend diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..5e344e5 --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,32 @@ +""" +Basic API tests for Wingman backend +""" +import pytest +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + + +def test_root_endpoint(): + """Test the root endpoint""" + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Wingman" + assert "version" in data + assert data["status"] == "running" + + +def test_health_endpoint(): + """Test the health check endpoint""" + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + + +def test_ask_endpoint_missing_question(): + """Test ask endpoint with missing question""" + response = client.post("/api/ask", json={}) + assert response.status_code == 422 # Validation error diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..ad6b482 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,105 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: wingman-postgres + env_file: + - .env.postgres + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U wingman"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - wingman-network + + # Chroma Vector Store + chroma: + image: ghcr.io/chroma-core/chroma:latest + container_name: wingman-chroma + volumes: + - chroma_data:/chroma/chroma + env_file: + - .env.chroma + ports: + - "8001:8000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - wingman-network + + # Backend API + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: wingman-backend + env_file: + - .env + - .env.backend + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + chroma: + condition: service_healthy + volumes: + - ./backend:/app + networks: + - wingman-network + restart: unless-stopped + + # Slack Bot (separate service for socket mode) + bot: + build: + context: ./backend + dockerfile: Dockerfile + container_name: wingman-bot + command: python run_bot.py + env_file: + - .env + - .env.backend + depends_on: + postgres: + condition: service_healthy + chroma: + condition: service_healthy + volumes: + - ./backend:/app + networks: + - wingman-network + restart: unless-stopped + + # Frontend Dashboard + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: wingman-frontend + env_file: + - .env.frontend + ports: + - "3000:3000" + depends_on: + - backend + networks: + - wingman-network + restart: unless-stopped + +volumes: + postgres_data: + chroma_data: + +networks: + wingman-network: + driver: bridge diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..24471d6 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,410 @@ +# Setup Guide for Wingman + +This guide will walk you through setting up Wingman, a Slack support assistant with RAG capabilities. + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Slack App Configuration](#slack-app-configuration) +3. [Environment Setup](#environment-setup) +4. [Docker Deployment](#docker-deployment) +5. [Local Development](#local-development) +6. [Troubleshooting](#troubleshooting) + +## Prerequisites + +### Required Tools + +- **Docker** (20.10+) and **Docker Compose** (2.0+) +- **Git** for cloning the repository +- **Slack workspace** with admin permissions +- **API Key** from OpenRouter or OpenAI + +### Getting API Keys + +#### OpenRouter (Recommended) + +1. Visit [OpenRouter](https://openrouter.ai/) +2. Sign up or log in +3. Navigate to [Keys](https://openrouter.ai/keys) +4. Create a new API key +5. Copy the key (starts with `sk-or-`) + +#### OpenAI (Alternative) + +1. Visit [OpenAI Platform](https://platform.openai.com/) +2. Sign up or log in +3. Navigate to [API Keys](https://platform.openai.com/api-keys) +4. Create a new API key +5. Copy the key (starts with `sk-`) + +## Slack App Configuration + +### 1. Create a Slack App + +1. Go to [Slack API Apps](https://api.slack.com/apps) +2. Click **"Create New App"** +3. Select **"From scratch"** +4. Enter app name: `Wingman` +5. Select your workspace +6. Click **"Create App"** + +### 2. Configure Bot Token Scopes + +Navigate to **OAuth & Permissions** and add these Bot Token Scopes: + +**Required Scopes:** +- `app_mentions:read` - View messages that directly mention @yourbot +- `channels:history` - View messages in public channels +- `channels:read` - View basic information about public channels +- `chat:write` - Post messages in channels +- `im:history` - View messages in direct messages +- `im:read` - View basic information about direct messages +- `im:write` - Start direct messages with people +- `users:read` - View people in the workspace +- `reactions:read` - View emoji reactions (optional) + +**Optional Scopes (for advanced features):** +- `channels:join` - Join public channels +- `groups:history` - View messages in private channels +- `groups:read` - View basic information about private channels + +### 3. Install App to Workspace + +1. In **OAuth & Permissions**, click **"Install to Workspace"** +2. Review permissions and click **"Allow"** +3. Copy the **Bot User OAuth Token** (starts with `xoxb-`) +4. Save this token for your `.env` file + +### 4. Enable Socket Mode (for development) + +Socket Mode allows your bot to receive events without exposing a public URL. + +1. Navigate to **Socket Mode** in the sidebar +2. Toggle **"Enable Socket Mode"** to ON +3. Give the token a name (e.g., "Wingman App Token") +4. Click **"Generate"** +5. Copy the **App-Level Token** (starts with `xapp-`) +6. Save this token for your `.env` file + +### 5. Subscribe to Events + +1. Navigate to **Event Subscriptions** +2. For Socket Mode: It should be automatically enabled +3. For HTTP Mode (production): Enter your Request URL (e.g., `https://yourdomain.com/slack/events`) + +**Subscribe to Bot Events:** +- `app_mention` - When the bot is mentioned +- `message.im` - Messages in direct messages +- `reaction_added` - When reactions are added (optional) + +### 6. Configure Interactivity + +1. Navigate to **Interactivity & Shortcuts** +2. Toggle **"Interactivity"** to ON +3. For Socket Mode: Leave URL empty +4. For HTTP Mode: Enter your Request URL + +### 7. Add Slash Command (Optional) + +1. Navigate to **Slash Commands** +2. Click **"Create New Command"** +3. Configure: + - Command: `/wingman` + - Request URL: Leave empty for Socket Mode + - Short Description: "Ask Wingman for help" + - Usage Hint: "your question here" +4. Click **"Save"** + +### 8. Get Signing Secret + +1. Navigate to **Basic Information** +2. Under **App Credentials**, find **Signing Secret** +3. Click **"Show"** and copy the secret +4. Save this for your `.env` file + +## Environment Setup + +### 1. Clone Repository + +```bash +git clone https://github.com/echohello-dev/wingman.git +cd wingman +``` + +### 2. Create Environment File + +```bash +cp .env.example .env +``` + +### 3. Configure Environment Variables + +Edit `.env` with your credentials: + +```bash +# Required Slack Tokens +SLACK_BOT_TOKEN=xoxb-your-bot-token-here +SLACK_APP_TOKEN=xapp-your-app-token-here +SLACK_SIGNING_SECRET=your-signing-secret-here + +# Choose one: OpenRouter (recommended) or OpenAI +OPENROUTER_API_KEY=sk-or-your-openrouter-key-here +# OR +OPENAI_API_KEY=sk-your-openai-key-here + +# Optional: Customize LLM model +LLM_MODEL=openai/gpt-4-turbo-preview +``` + +See [SLACK_AUTH.md](SLACK_AUTH.md) for more details on token types. + +## Docker Deployment + +### Start All Services + +```bash +docker compose up -d +``` + +This starts: +- **Backend API** (port 8000) +- **Frontend Dashboard** (port 3000) +- **PostgreSQL** (port 5432) +- **ChromaDB** (port 8001) +- **Slack Bot** (Socket Mode) + +### Verify Services + +```bash +# Check service status +docker compose ps + +# View logs +docker compose logs -f backend +docker compose logs -f bot +docker compose logs -f frontend + +# Check backend health +curl http://localhost:8000/health +``` + +### Stop Services + +```bash +docker compose down + +# To also remove volumes (data) +docker compose down -v +``` + +## Local Development + +For development without Docker: + +### Backend Setup + +```bash +cd backend + +# Create virtual environment +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt + +# Start dependencies (or use local Postgres/Chroma) +docker compose up -d postgres chroma + +# Run backend API +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# In a separate terminal, run the bot +python run_bot.py +``` + +### Frontend Setup + +```bash +cd frontend + +# Install dependencies +npm install + +# Start development server +npm run dev +``` + +Visit: +- Frontend: http://localhost:3000 +- Backend API: http://localhost:8000 +- API Docs: http://localhost:8000/docs + +## Testing the Bot + +### In Slack + +1. **Direct Message**: Send a DM to Wingman + ``` + Hello, Wingman! + ``` + +2. **Mention in Channel**: Invite bot to a channel and mention it + ``` + @Wingman How do I reset my password? + ``` + +3. **Slash Command**: Use the slash command + ``` + /wingman What are the API rate limits? + ``` + +### Via Dashboard + +1. Open http://localhost:3000 +2. Navigate to "Ask Question" tab +3. Enter a question and click "Ask Wingman" + +### Via API + +```bash +# Health check +curl http://localhost:8000/health + +# Ask a question +curl -X POST http://localhost:8000/api/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "How do I authenticate?"}' + +# Add a document +curl -X POST http://localhost:8000/api/documents \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Authentication Guide", + "content": "To authenticate, use your API key...", + "source": "docs" + }' + +# List messages +curl http://localhost:8000/api/messages +``` + +## Troubleshooting + +### Bot Not Responding in Slack + +**Check:** +1. Bot is running: `docker compose logs bot` +2. Socket Mode is enabled in Slack app settings +3. `SLACK_APP_TOKEN` is set correctly (starts with `xapp-`) +4. Bot is invited to the channel (for mentions) +5. Bot has required permissions + +**Common Issues:** +- "Invalid token": Check `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` +- "Not in channel": Invite bot with `/invite @Wingman` +- "Permission denied": Review OAuth scopes + +### Database Connection Issues + +```bash +# Check PostgreSQL +docker compose logs postgres + +# Connect to database +docker compose exec postgres psql -U wingman -d wingman + +# Verify connection string in .env +DATABASE_URL=postgresql://wingman:wingman@postgres:5432/wingman +``` + +### Vector Store Issues + +```bash +# Check Chroma +docker compose logs chroma + +# Verify Chroma is running +curl http://localhost:8001/api/v1/heartbeat + +# Reset Chroma data +docker compose down -v +docker compose up -d chroma +``` + +### LLM/API Issues + +**OpenRouter:** +- Verify API key is valid +- Check account credits at https://openrouter.ai/account +- Review model name format: `openai/gpt-4-turbo-preview` + +**OpenAI:** +- Verify API key is valid +- Check account credits at https://platform.openai.com/usage +- Review model name format: `gpt-4-turbo-preview` + +### Port Conflicts + +If ports are already in use: + +```bash +# Edit docker compose.yml to change ports +services: + backend: + ports: + - "8001:8000" # Use 8001 instead of 8000 + frontend: + ports: + - "3001:3000" # Use 3001 instead of 3000 +``` + +### View Application Logs + +```bash +# All services +docker compose logs -f + +# Specific service +docker compose logs -f backend +docker compose logs -f bot +docker compose logs -f frontend +docker compose logs -f postgres +docker compose logs -f chroma +``` + +## Next Steps + +- **Index Documents**: Add documents to the knowledge base via API +- **Customize Prompts**: Edit `backend/app/rag.py` to customize the RAG prompt +- **Add Reactions**: Implement feedback collection via Slack reactions +- **Deploy to Production**: Use a proper web server and HTTPS for production +- **Monitor Usage**: Track LLM API costs and usage + +## Production Considerations + +1. **Security**: + - Use environment-specific tokens + - Enable HTTPS for webhook endpoints + - Rotate tokens regularly + - Restrict database access + +2. **Scalability**: + - Use managed PostgreSQL (AWS RDS, etc.) + - Deploy Chroma in production mode + - Use proper secret management (AWS Secrets Manager, etc.) + - Add rate limiting + +3. **Monitoring**: + - Add application logging + - Monitor LLM costs + - Track bot performance + - Set up alerts + +4. **Backup**: + - Regular database backups + - Vector store backups + - Configuration backups + +For more details, see [SLACK_AUTH.md](SLACK_AUTH.md) for authentication options. diff --git a/docs/SLACK_AUTH.md b/docs/SLACK_AUTH.md new file mode 100644 index 0000000..423c0f1 --- /dev/null +++ b/docs/SLACK_AUTH.md @@ -0,0 +1,445 @@ +# Slack Authentication Guide + +This guide explains the different Slack token types and authentication options for Wingman. + +## Overview + +Slack uses different token types for different purposes. Understanding these tokens is crucial for properly configuring your bot. + +## Token Types + +### 1. Bot User OAuth Token (xoxb-*) + +**Format**: `xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx` + +**Purpose**: Used by your bot to perform actions in the workspace. + +**Use Cases**: +- Posting messages +- Reading channel history +- Reacting to messages +- Joining channels +- Most bot operations + +**How to Get**: +1. Go to your app's **OAuth & Permissions** page +2. Install the app to your workspace +3. Copy the **Bot User OAuth Token** + +**Scopes Required** (minimum): +``` +app_mentions:read +channels:history +channels:read +chat:write +im:history +im:read +im:write +users:read +``` + +**Environment Variable**: +```bash +SLACK_BOT_TOKEN=xoxb-your-bot-token +``` + +**Security Notes**: +- Treat this like a password +- Never commit to version control +- Can be regenerated if compromised +- Tied to the bot user, not a specific person + +--- + +### 2. App-Level Token (xapp-*) + +**Format**: `xapp-x-xxxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx` + +**Purpose**: Used for Socket Mode connections. + +**Use Cases**: +- Receiving events via WebSocket (Socket Mode) +- Preferred for development/testing +- No public URL required + +**How to Get**: +1. Go to your app's **Basic Information** page +2. Scroll to **App-Level Tokens** +3. Click **Generate Token and Scopes** +4. Add the `connections:write` scope +5. Copy the token + +**Required Scope**: +``` +connections:write +``` + +**Environment Variable**: +```bash +SLACK_APP_TOKEN=xapp-your-app-token +``` + +**When to Use**: +- ✅ Local development +- ✅ Testing without exposing a public URL +- ✅ Environments behind firewalls +- ❌ Production (consider HTTP mode with proper infrastructure) + +**Security Notes**: +- Less critical than bot token but still sensitive +- Only needed if using Socket Mode +- Can be regenerated + +--- + +### 3. User OAuth Token (xoxp-*) + +**Format**: `xoxp-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx` + +**Purpose**: Acts on behalf of a specific user. + +**Use Cases**: +- Reading private channels the user has access to +- Performing actions as a specific user +- Accessing user-level data +- Impersonating user actions + +**How to Get**: +1. Implement OAuth flow in your app +2. User authorizes your app +3. Exchange code for token +4. OR manually generate at **OAuth & Permissions** + +**Scopes** (examples): +``` +channels:read +channels:write +chat:write +users:read +files:read +``` + +**Environment Variable**: +```bash +SLACK_USER_TOKEN=xoxp-your-user-token +``` + +**When to Use**: +- ✅ Need to access user's private channels +- ✅ Perform actions as the user +- ✅ Read user-specific data +- ⚠️ Use sparingly - prefer bot tokens + +**Security Notes**: +- Most sensitive token type +- Represents a real user +- Can access private data +- Should have minimal scopes +- Can be revoked by user + +--- + +### 4. Workspace Token (xoxc-*) + +**Format**: `xoxc-xxxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx` + +**Purpose**: Internal Slack client token (not typically used by apps). + +**Use Cases**: +- Internal Slack operations +- Not recommended for custom apps +- Can be extracted from Slack web client + +**When to Use**: +- ❌ Generally avoid +- ⚠️ Only for advanced use cases +- ⚠️ Not officially supported + +**Security Notes**: +- Not officially documented +- Can be invalidated +- Use official token types instead + +--- + +### 5. Legacy Tokens (xoxd-*, xoxs-*) + +**Format**: Various + +**Purpose**: Older token types, mostly deprecated. + +**Status**: Being phased out by Slack + +**Recommendation**: Use modern token types (xoxb, xoxp, xapp) + +--- + +## Wingman Configuration + +### Minimum Setup (Socket Mode) + +For basic functionality with Socket Mode: + +```bash +# Required +SLACK_BOT_TOKEN=xoxb-xxx # Bot actions +SLACK_APP_TOKEN=xapp-xxx # Socket Mode +SLACK_SIGNING_SECRET=xxx # Request verification +``` + +### Recommended Setup + +```bash +# Required +SLACK_BOT_TOKEN=xoxb-xxx +SLACK_APP_TOKEN=xapp-xxx +SLACK_SIGNING_SECRET=xxx + +# Optional but recommended +SLACK_USER_TOKEN=xoxp-xxx # For private channel access +``` + +### OAuth Flow Setup + +If implementing OAuth for multi-workspace installation: + +```bash +# Required +SLACK_BOT_TOKEN=xoxb-xxx +SLACK_SIGNING_SECRET=xxx + +# OAuth +SLACK_CLIENT_ID=xxx +SLACK_CLIENT_SECRET=xxx + +# Optional +SLACK_APP_TOKEN=xapp-xxx # If using Socket Mode +``` + +## Authentication Modes + +### Socket Mode (Development) + +**Best for**: Local development, testing + +**Pros**: +- No public URL needed +- Easy to set up +- Works behind firewalls +- Real-time events + +**Cons**: +- Not ideal for production scale +- Requires App-Level Token + +**Setup**: +```bash +SLACK_BOT_TOKEN=xoxb-xxx +SLACK_APP_TOKEN=xapp-xxx +SLACK_SIGNING_SECRET=xxx +``` + +**Code**: +```python +from slack_bolt.adapter.socket_mode import SocketModeHandler + +handler = SocketModeHandler(app, SLACK_APP_TOKEN) +handler.start() +``` + +--- + +### HTTP Mode (Production) + +**Best for**: Production deployments + +**Pros**: +- Better for scale +- Standard HTTP infrastructure +- No WebSocket connection + +**Cons**: +- Requires public HTTPS endpoint +- More complex setup + +**Setup**: +```bash +SLACK_BOT_TOKEN=xoxb-xxx +SLACK_SIGNING_SECRET=xxx +# No SLACK_APP_TOKEN needed +``` + +**Requirements**: +- Public HTTPS URL +- Valid SSL certificate +- Configure Request URL in Slack app + +**Code**: +```python +from flask import Flask, request +from slack_bolt.adapter.flask import SlackRequestHandler + +app = Flask(__name__) +handler = SlackRequestHandler(bolt_app) + +@app.route("/slack/events", methods=["POST"]) +def slack_events(): + return handler.handle(request) +``` + +--- + +## Security Best Practices + +### 1. Token Storage + +❌ **Never**: +```bash +# Don't hardcode +SLACK_BOT_TOKEN = "xoxb-123456789..." + +# Don't commit +git add .env +``` + +✅ **Do**: +```bash +# Use environment variables +export SLACK_BOT_TOKEN=xoxb-xxx + +# Use .env files (gitignored) +echo ".env" >> .gitignore + +# Use secret managers (production) +# AWS Secrets Manager, HashiCorp Vault, etc. +``` + +### 2. Token Scopes + +❌ **Don't**: +- Request more scopes than needed +- Use admin scopes unnecessarily +- Keep unused scopes + +✅ **Do**: +- Follow principle of least privilege +- Review scopes regularly +- Remove unused scopes + +### 3. Token Rotation + +✅ **Do**: +- Rotate tokens periodically +- Rotate immediately if compromised +- Use different tokens per environment +- Log token usage + +### 4. Request Verification + +Always verify requests from Slack: + +```python +import hmac +import hashlib + +def verify_slack_request(request, signing_secret): + timestamp = request.headers.get('X-Slack-Request-Timestamp') + signature = request.headers.get('X-Slack-Signature') + + # Verify timestamp + if abs(time.time() - int(timestamp)) > 60 * 5: + return False + + # Verify signature + sig_basestring = f"v0:{timestamp}:{request.get_data().decode()}" + my_signature = 'v0=' + hmac.new( + signing_secret.encode(), + sig_basestring.encode(), + hashlib.sha256 + ).hexdigest() + + return hmac.compare_digest(my_signature, signature) +``` + +## Troubleshooting + +### "Invalid Token" + +**Causes**: +- Wrong token type +- Token expired/revoked +- Token not properly set + +**Solutions**: +1. Verify token starts with correct prefix (xoxb-, xapp-, xoxp-) +2. Regenerate token in Slack app settings +3. Check environment variables are loaded +4. Verify no extra whitespace in token + +### "Missing Scope" + +**Causes**: +- Token doesn't have required scope +- Scope was removed + +**Solutions**: +1. Go to **OAuth & Permissions** +2. Add required scope +3. Reinstall app to workspace +4. Get new token + +### "Not in Channel" + +**Causes**: +- Bot not invited to channel +- Trying to read without permission + +**Solutions**: +1. Invite bot: `/invite @Wingman` +2. Add `channels:join` scope for auto-join +3. Check bot is added to workspace + +### "Token Revoked" + +**Causes**: +- App uninstalled +- Token manually revoked +- Token expired + +**Solutions**: +1. Reinstall app +2. Generate new token +3. Update environment variables + +## Token Comparison + +| Feature | Bot Token (xoxb) | App Token (xapp) | User Token (xoxp) | +|---------|------------------|------------------|-------------------| +| **Purpose** | Bot actions | Socket Mode | User actions | +| **Scope** | Bot scopes | Connection only | User scopes | +| **Access** | Public channels | N/A | Private channels | +| **Required** | ✅ Yes | ⚠️ Socket Mode | ❌ Optional | +| **Sensitivity** | 🔒 High | 🔒 Medium | 🔒🔒 Very High | +| **Best For** | Most actions | Development | User context | + +## References + +- [Slack Token Types](https://api.slack.com/authentication/token-types) +- [OAuth Scopes](https://api.slack.com/scopes) +- [Socket Mode](https://api.slack.com/apis/connections/socket) +- [Request Verification](https://api.slack.com/authentication/verifying-requests-from-slack) +- [Security Best Practices](https://api.slack.com/authentication/best-practices) + +## Support + +If you encounter authentication issues: + +1. Check [Slack API Documentation](https://api.slack.com/) +2. Review [Slack Bolt Documentation](https://slack.dev/bolt-python/) +3. Verify token types and scopes +4. Check application logs +5. Open an issue on GitHub + +--- + +**Remember**: Treat all tokens as passwords. Never share them publicly or commit them to version control. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..ec1a8a2 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,171 @@ +# Quick Start Guide + +Get Wingman up and running in 5 minutes! + +## Prerequisites + +- Docker and Docker Compose installed +- Slack workspace admin access +- OpenRouter or OpenAI API key + +## Step 1: Get Your Slack Tokens + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Click **"Create New App"** → **"From scratch"** +3. Name it "Wingman" and select your workspace +4. In **OAuth & Permissions**, add these scopes: + - `app_mentions:read` + - `channels:history` + - `channels:read` + - `chat:write` + - `im:history` + - `im:read` + - `im:write` + - `users:read` +5. Click **"Install to Workspace"** and copy the **Bot Token** (xoxb-*) +6. In **Socket Mode**, enable it and generate an **App Token** (xapp-*) +7. In **Basic Information**, copy the **Signing Secret** + +## Step 2: Get Your AI API Key + +**Option A: OpenRouter (Recommended)** +- Visit [openrouter.ai](https://openrouter.ai/) +- Sign up and get your API key + +**Option B: OpenAI** +- Visit [platform.openai.com](https://platform.openai.com/) +- Sign up and get your API key + +## Step 3: Configure Wingman + +```bash +# Clone the repo +git clone https://github.com/echohello-dev/wingman.git +cd wingman + +# Copy the environment template +cp .env.example .env + +# Edit .env with your tokens +# Required: +# - SLACK_BOT_TOKEN=xoxb-... +# - SLACK_APP_TOKEN=xapp-... +# - SLACK_SIGNING_SECRET=... +# - OPENROUTER_API_KEY=... (or OPENAI_API_KEY) +``` + +## Step 4: Start Wingman + +```bash +docker compose up -d +``` + +This starts: +- Backend API: http://localhost:8000 +- Frontend Dashboard: http://localhost:3000 +- PostgreSQL database +- ChromaDB vector store +- Slack bot + +## Step 5: Test It! + +### In Slack: +1. Send a DM to Wingman: `Hello!` +2. Or mention in a channel: `@Wingman How can you help me?` +3. Or use slash command: `/wingman What are you?` + +### In Dashboard: +1. Open http://localhost:3000 +2. Type a question in the "Ask Question" tab +3. Click "Ask Wingman" + +### Via API: +```bash +curl -X POST http://localhost:8000/api/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "Hello, who are you?"}' +``` + +## Troubleshooting + +### Bot not responding? +```bash +# Check logs +docker compose logs -f bot + +# Verify environment +docker compose exec bot env | grep SLACK +``` + +### Can't access dashboard? +```bash +# Check if services are running +docker compose ps + +# Check frontend logs +docker compose logs -f frontend +``` + +### Database issues? +```bash +# Restart all services +docker compose restart + +# Or recreate everything +docker compose down -v +docker compose up -d +``` + +## What's Next? + +1. **Add Documents**: Index your documentation for better answers + ```bash + curl -X POST http://localhost:8000/api/documents \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My Guide", + "content": "Content here...", + "source": "docs" + }' + ``` + +2. **Customize Prompts**: Edit `backend/app/rag.py` to customize responses + +3. **Monitor Usage**: Check API docs at http://localhost:8000/docs + +4. **Read Full Docs**: + - [SETUP.md](docs/SETUP.md) - Detailed setup guide + - [SLACK_AUTH.md](docs/SLACK_AUTH.md) - Authentication details + - [README.md](README.md) - Full documentation + +## Common Commands + +```bash +# View all logs +docker compose logs -f + +# Stop everything +docker compose down + +# Restart a service +docker compose restart backend + +# Access database +docker compose exec postgres psql -U wingman -d wingman + +# Run backend tests +cd backend && pytest + +# Update dependencies +docker compose build +``` + +## Getting Help + +- 📖 Read the [full documentation](README.md) +- 🐛 [Open an issue](https://github.com/echohello-dev/wingman/issues) +- 💬 Check [Slack API docs](https://api.slack.com/) + +--- + +Happy chatting! 🛩️ diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..015a837 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,14 @@ +node_modules +.next +.git +.gitignore +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +README.md +.DS_Store diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..39dd6e0 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,49 @@ +# Frontend Dockerfile for Wingman +FROM node:24-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Set build-time environment variables +ARG NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 + +CMD ["node", "server.js"] diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..9c47f09 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,36 @@ +@import "tailwindcss"; + +@theme { + --color-primary-50: #f0f9ff; + --color-primary-100: #e0f2fe; + --color-primary-200: #bae6fd; + --color-primary-300: #7dd3fc; + --color-primary-400: #38bdf8; + --color-primary-500: #0ea5e9; + --color-primary-600: #0284c7; + --color-primary-700: #0369a1; + --color-primary-800: #075985; + --color-primary-900: #0c4a6e; + + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + @theme { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..4eb7253 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Wingman - Slack Support Assistant', + description: 'AI-powered Slack support assistant with RAG capabilities', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..6ca6538 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,182 @@ +'use client' + +import { useState } from 'react' +import { askQuestion, listDocuments, listMessages } from '@/lib/api' + +export default function Home() { + const [question, setQuestion] = useState('') + const [answer, setAnswer] = useState('') + const [loading, setLoading] = useState(false) + const [activeTab, setActiveTab] = useState<'ask' | 'documents' | 'messages'>('ask') + + const handleAsk = async () => { + if (!question.trim()) return + + setLoading(true) + try { + const response = await askQuestion(question) + setAnswer(response.answer) + } catch (error) { + console.error('Error asking question:', error) + setAnswer('Error: Could not get response') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

+ 🛩️ Wingman +

+

+ AI-Powered Slack Support Assistant +

+
+ + {/* Tab Navigation */} +
+
+ + + +
+
+ + {/* Ask Question Tab */} + {activeTab === 'ask' && ( +
+
+

+ Ask a Question +

+ +
+