From d34c9d3363f058f3671b3cf5419393f4f8afb027 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:04:45 +0000 Subject: [PATCH 1/5] Initial plan From fae844854abeb4f242ec6ccd75d0bf5e5c402ce0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:12:54 +0000 Subject: [PATCH 2/5] Add complete OSS monorepo structure with backend, frontend, and docs Co-authored-by: johnnyhuy <27847622+johnnyhuy@users.noreply.github.com> --- .env.example | 103 +++++++++ .gitignore | 75 ++++++ README.md | 209 ++++++++++++++++- backend/.dockerignore | 23 ++ backend/Dockerfile | 27 +++ backend/app/__init__.py | 6 + backend/app/config.py | 54 +++++ backend/app/database.py | 55 +++++ backend/app/main.py | 238 +++++++++++++++++++ backend/app/rag.py | 152 ++++++++++++ backend/app/slack_bot.py | 170 ++++++++++++++ backend/app/vector_store.py | 78 +++++++ backend/requirements.txt | 28 +++ backend/run_bot.py | 20 ++ docker-compose.yml | 145 ++++++++++++ docs/SETUP.md | 410 +++++++++++++++++++++++++++++++++ docs/SLACK_AUTH.md | 445 ++++++++++++++++++++++++++++++++++++ frontend/.dockerignore | 14 ++ frontend/Dockerfile | 49 ++++ frontend/app/globals.css | 27 +++ frontend/app/layout.tsx | 22 ++ frontend/app/page.tsx | 182 +++++++++++++++ frontend/next.config.js | 9 + frontend/package.json | 29 +++ frontend/postcss.config.js | 6 + frontend/tailwind.config.ts | 29 +++ frontend/tsconfig.json | 27 +++ 27 files changed, 2630 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/app/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/rag.py create mode 100644 backend/app/slack_bot.py create mode 100644 backend/app/vector_store.py create mode 100644 backend/requirements.txt create mode 100644 backend/run_bot.py create mode 100644 docker-compose.yml create mode 100644 docs/SETUP.md create mode 100644 docs/SLACK_AUTH.md create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/next.config.js create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json 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/.gitignore b/.gitignore new file mode 100644 index 0000000..33b152b --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.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/README.md b/README.md index 0837a27..64a52da 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..5cbd13f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,27 @@ +# Backend Dockerfile for Wingman +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -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/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f9123ca --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,28 @@ +# 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 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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c706d89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,145 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: wingman-postgres + environment: + POSTGRES_DB: wingman + POSTGRES_USER: wingman + POSTGRES_PASSWORD: wingman + 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 + environment: + - IS_PERSISTENT=TRUE + 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 + environment: + # Application + DEBUG: "false" + HOST: "0.0.0.0" + PORT: "8000" + + # Slack (load from .env) + SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN} + SLACK_APP_TOKEN: ${SLACK_APP_TOKEN} + SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET} + SLACK_USER_TOKEN: ${SLACK_USER_TOKEN:-} + SLACK_CLIENT_ID: ${SLACK_CLIENT_ID:-} + SLACK_CLIENT_SECRET: ${SLACK_CLIENT_SECRET:-} + + # OpenRouter/OpenAI + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + LLM_MODEL: ${LLM_MODEL:-openai/gpt-4-turbo-preview} + + # Database + DATABASE_URL: "postgresql://wingman:wingman@postgres:5432/wingman" + + # Chroma + CHROMA_HOST: "chroma" + CHROMA_PORT: "8000" + 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 + environment: + # Slack + SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN} + SLACK_APP_TOKEN: ${SLACK_APP_TOKEN} + SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET} + + # OpenRouter/OpenAI + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + LLM_MODEL: ${LLM_MODEL:-openai/gpt-4-turbo-preview} + + # Database + DATABASE_URL: "postgresql://wingman:wingman@postgres:5432/wingman" + + # Chroma + CHROMA_HOST: "chroma" + CHROMA_PORT: "8000" + 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 + args: + NEXT_PUBLIC_API_URL: http://localhost:8000 + container_name: wingman-frontend + environment: + NEXT_PUBLIC_API_URL: http://localhost:8000 + 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..7b9a7cf --- /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/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..093aa9e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,49 @@ +# Frontend Dockerfile for Wingman +FROM node:20-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..fd81e88 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --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 +

+ +
+