|
| 1 | +"""Slack chatbot for answering questions about FastAPI using Codegen's VectorIndex.""" |
| 2 | + |
| 3 | +import os |
| 4 | +from typing import Any |
| 5 | + |
| 6 | +import modal |
| 7 | +from codegen import Codebase |
| 8 | +from codegen.extensions import VectorIndex |
| 9 | +from codegen.sdk.enums import ProgrammingLanguage |
| 10 | +from fastapi import FastAPI, Request |
| 11 | +from openai import OpenAI |
| 12 | +from slack_bolt import App |
| 13 | +from slack_bolt.adapter.fastapi import SlackRequestHandler |
| 14 | + |
| 15 | +######################################################## |
| 16 | +# Core RAG logic |
| 17 | +######################################################## |
| 18 | + |
| 19 | + |
| 20 | +def format_response(answer: str, context: list[tuple[str, int]]) -> str: |
| 21 | + """Format the response for Slack with file links.""" |
| 22 | + response = f"*Answer:*\n{answer}\n\n*Relevant Files:*\n" |
| 23 | + for filename, score in context: |
| 24 | + github_link = f"https://github.com/codegen-sh/codegen-sdk/blob/develop/{filename}" |
| 25 | + response += f"• <{github_link}|{filename}>\n" |
| 26 | + return response |
| 27 | + |
| 28 | + |
| 29 | +def answer_question(query: str) -> tuple[str, list[tuple[str, int]]]: |
| 30 | + """Use RAG to answer a question about FastAPI.""" |
| 31 | + # Initialize codebase. Smart about caching. |
| 32 | + codebase = Codebase.from_repo("codegen-sh/codegen-sdk", programming_language=ProgrammingLanguage.PYTHON, tmp_dir="/root") |
| 33 | + |
| 34 | + # Initialize vector index |
| 35 | + index = VectorIndex(codebase) |
| 36 | + |
| 37 | + # Try to load existing index or create new one |
| 38 | + index_path = "/root/E.pkl" |
| 39 | + try: |
| 40 | + index.load(index_path) |
| 41 | + except FileNotFoundError: |
| 42 | + # Create new index if none exists |
| 43 | + index.create() |
| 44 | + index.save(index_path) |
| 45 | + |
| 46 | + # Find relevant files |
| 47 | + results = index.similarity_search(query, k=5) |
| 48 | + |
| 49 | + # Collect context from relevant files |
| 50 | + context = "" |
| 51 | + for filepath, score in results: |
| 52 | + if "#chunk" in filepath: |
| 53 | + filepath = filepath.split("#chunk")[0] |
| 54 | + file = codebase.get_file(filepath) |
| 55 | + context += f"File: {file.filepath}\n```\n{file.content}\n```\n\n" |
| 56 | + |
| 57 | + # Create prompt for OpenAI |
| 58 | + prompt = f"""You are an expert on FastAPI. Given the following code context and question, provide a clear and accurate answer. |
| 59 | +Focus on the specific code shown in the context and FastAPI's implementation details. |
| 60 | +
|
| 61 | +Note that your response will be rendered in Slack, so make sure to use Slack markdown. Keep it short + sweet, like 2 paragraphs + some code blocks max. |
| 62 | +
|
| 63 | +Question: {query} |
| 64 | +
|
| 65 | +Relevant code: |
| 66 | +{context} |
| 67 | +
|
| 68 | +Answer:""" |
| 69 | + |
| 70 | + client = OpenAI() |
| 71 | + response = client.chat.completions.create( |
| 72 | + model="gpt-4o", |
| 73 | + messages=[ |
| 74 | + {"role": "system", "content": "You are a code expert. Answer questions about the given repo based on RAG'd results."}, |
| 75 | + {"role": "user", "content": prompt}, |
| 76 | + ], |
| 77 | + temperature=0, |
| 78 | + ) |
| 79 | + |
| 80 | + return response.choices[0].message.content, results |
| 81 | + |
| 82 | + |
| 83 | +######################################################## |
| 84 | +# Modal + Slack Setup |
| 85 | +######################################################## |
| 86 | + |
| 87 | +# Create image with dependencies |
| 88 | +image = ( |
| 89 | + modal.Image.debian_slim(python_version="3.13") |
| 90 | + .apt_install("git") |
| 91 | + .pip_install( |
| 92 | + "slack-bolt>=1.18.0", |
| 93 | + "codegen>=0.6.1", |
| 94 | + "openai>=1.1.0", |
| 95 | + ) |
| 96 | +) |
| 97 | + |
| 98 | +# Create Modal app |
| 99 | +app = modal.App("codegen-slack-demo") |
| 100 | + |
| 101 | + |
| 102 | +@app.function( |
| 103 | + image=image, |
| 104 | + secrets=[modal.Secret.from_dotenv()], |
| 105 | + timeout=3600, |
| 106 | +) |
| 107 | +@modal.asgi_app() |
| 108 | +def fastapi_app(): |
| 109 | + """Create FastAPI app with Slack handlers.""" |
| 110 | + # Initialize Slack app with secrets from environment |
| 111 | + slack_app = App( |
| 112 | + token=os.environ["SLACK_BOT_TOKEN"], |
| 113 | + signing_secret=os.environ["SLACK_SIGNING_SECRET"], |
| 114 | + ) |
| 115 | + |
| 116 | + # Create FastAPI app |
| 117 | + web_app = FastAPI() |
| 118 | + handler = SlackRequestHandler(slack_app) |
| 119 | + |
| 120 | + # Store responded messages to avoid duplicates |
| 121 | + responded = {} |
| 122 | + |
| 123 | + @slack_app.event("app_mention") |
| 124 | + def handle_mention(event: dict[str, Any], say: Any) -> None: |
| 125 | + """Handle mentions of the bot in channels.""" |
| 126 | + print("#####[ Received Event ]#####") |
| 127 | + print(event) |
| 128 | + |
| 129 | + # Skip if we've already answered this question |
| 130 | + # Seems like Slack likes to double-send events while debugging (?) |
| 131 | + if event["ts"] in responded: |
| 132 | + return |
| 133 | + responded[event["ts"]] = True |
| 134 | + |
| 135 | + # Get message text without the bot mention |
| 136 | + query = event["text"].split(">", 1)[1].strip() |
| 137 | + if not query: |
| 138 | + say("Please ask a question about FastAPI!") |
| 139 | + return |
| 140 | + |
| 141 | + try: |
| 142 | + # Add typing indicator emoji |
| 143 | + slack_app.client.reactions_add( |
| 144 | + channel=event["channel"], |
| 145 | + timestamp=event["ts"], |
| 146 | + name="writing_hand", |
| 147 | + ) |
| 148 | + |
| 149 | + # Get answer using RAG |
| 150 | + answer, context = answer_question(query) |
| 151 | + |
| 152 | + # Format and send response in thread |
| 153 | + response = format_response(answer, context) |
| 154 | + say(text=response, thread_ts=event["ts"]) |
| 155 | + |
| 156 | + except Exception as e: |
| 157 | + # Send error message in thread |
| 158 | + say(text=f"Error: {str(e)}", thread_ts=event["ts"]) |
| 159 | + |
| 160 | + @web_app.post("/") |
| 161 | + async def endpoint(request: Request): |
| 162 | + """Handle Slack events and verify requests.""" |
| 163 | + return await handler.handle(request) |
| 164 | + |
| 165 | + @web_app.post("/slack/verify") |
| 166 | + async def verify(request: Request): |
| 167 | + """Handle Slack URL verification challenge.""" |
| 168 | + data = await request.json() |
| 169 | + if data["type"] == "url_verification": |
| 170 | + return {"challenge": data["challenge"]} |
| 171 | + return await handler.handle(request) |
| 172 | + |
| 173 | + return web_app |
0 commit comments