From 0feb7719ac3d3dcfdbb79c4a603daf4a0f71c6a6 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 11:31:05 -0800 Subject: [PATCH 01/15] . --- app.py | 33 ++ next.js.ipynb | 394 ++++++++++++++++++ pyproject.toml | 1 + src/codegen/extensions/events/client.py | 54 +++ src/codegen/extensions/events/codegen_app.py | 90 ++++ test_events.py | 65 +++ .../codegen_app/test_add_handlers.py | 90 ++++ 7 files changed, 727 insertions(+) create mode 100644 app.py create mode 100644 next.js.ipynb create mode 100644 src/codegen/extensions/events/client.py create mode 100644 src/codegen/extensions/events/codegen_app.py create mode 100644 test_events.py create mode 100644 tests/integration/extension/codegen_app/test_add_handlers.py diff --git a/app.py b/app.py new file mode 100644 index 000000000..c2b96d43a --- /dev/null +++ b/app.py @@ -0,0 +1,33 @@ +import logging + +from slack_sdk import WebClient + +from codegen.extensions.events.codegen_app import CodegenApp + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create the app +app = CodegenApp(name="codegen-test") + + +@app.slack.event("app_mention") +def handle_mention(client: WebClient, event): + logger.info("[APP_MENTION] Received app_mention event") + + # Example response + client.chat_postMessage(channel=event.channel, text=f"Hello! You mentioned me in {event.channel}", thread_ts=event.ts) + + return {"message": "Event handled successfully"} + + +@app.github.event("pull_request:opened") +def handle_pr(event): + logger.info(f"New PR opened: {event}") + return {"message": "PR event handled"} + + +if __name__ == "__main__": + # Run the server + app.run(port=8000) diff --git a/next.js.ipynb b/next.js.ipynb new file mode 100644 index 000000000..517b782d1 --- /dev/null +++ b/next.js.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Codegen Next.js Demo\n", + "\n", + "Based on our [Slack conversation](https://thecodegen.slack.com/archives/C07HXT5NN75/p1739994947429139?thread_ts=1738268741.592289&cid=C07HXT5NN75)\n", + "\n", + "\n", + "## Step 1: Parse codebase\n", + "- This takes ~2.5 minutes without memory snapshotting\n", + "- Pulls the repo into `/tmp/codegen/next.js" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from codegen import Codebase\n", + "\n", + "# Takes ~2.5 minutes\n", + "codebase = Codebase.from_repo(\"vercel/next.js\")" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "# Step 2: Flag Issues\n", + "\n", + "Based on our [Slack conversation](https://thecodegen.slack.com/archives/C07HXT5NN75/p1739994947429139?thread_ts=1738268741.592289&cid=C07HXT5NN75), this will flag + link all imports that:\n", + "- import *from* `DIR_NAME` directly\n", + "- the import is *outside* of DIRNAME\n", + "\n", + "This uses the [Files and Directories](https://docs.codegen.com/building-with-codegen/files-and-directories#files-and-directories) APIs, as well as [Imports](https://docs.codegen.com/building-with-codegen/imports#the-import-api)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "from codegen.sdk.core.detached_symbols.code_block import CodeBlock" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from codegen.sdk.core.detached_symbols.code_block import CodeBlock\n", + "\n", + "\n", + "def is_env_conditional(statement):\n", + " if not isinstance(statement, IfBlockStatement):\n", + " return False\n", + " return \"process.env\" in statement.condition.source\n", + "\n", + "\n", + "def get_all_statements(block: CodeBlock):\n", + " statements = []\n", + " for statement in block.statements:\n", + " statements.append(statement)\n", + " if hasattr(statement, \"code_block\"):\n", + " statements += get_all_statements(statement.code_block)\n", + " return statements" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "DIR_NAME = \"packages/next/src/\"\n", + "directory = codebase.get_directory(DIR_NAME)\n", + "\n", + "for file in directory.files:\n", + " env_conditionals = [s for s in get_all_statements(file.code_block) if is_env_conditional(s)]\n", + " for c in env_conditionals:\n", + " print(c.github_url)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "raw", + "id": "7", + "metadata": {}, + "source": [ + "file = codebase.get_file('packages/next/src/server/web/sandbox/context.ts')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "env_conditionals = [s for s in get_all_statements(file.code_block) if is_env_conditional(s)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "file.github_url" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "from codegen.sdk.core.statements.if_block_statement import IfBlockStatement\n", + "\n", + "DIR_NAME = \"packages/next/src/client/components/react-dev-overlay\"\n", + "directory = codebase.get_directory(DIR_NAME)\n", + "\n", + "\n", + "def is_env_conditional(statement):\n", + " if not isinstance(statement, IfBlockStatement):\n", + " return False\n", + " return \"process.env\" in statement.condition.source\n", + "\n", + "\n", + "for file in directory.files:\n", + " for imp in file.inbound_imports:\n", + " if imp.file not in directory:\n", + " # Grab process.env statements\n", + " file = imp.to_file\n", + " statements = get_all_statements(file.code_block.statements)\n", + " conditionals = [x for x in statements if is_env_conditional(x)]\n", + "\n", + " # See if the import is beneath one of the process.env statementss\n", + " if not any(imp.source in c.source for c in conditionals):\n", + " continue\n", + "\n", + " print(f\"❌ Detected bad import:\")\n", + " print(f\" ⚛️ File: {imp.file.filepath}\")\n", + " print(f\" ⬇️ Imported symbol: {imp.name}\")\n", + " print(f\" 🔗 Link: {imp.github_url}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "DIR_NAME = \"packages/next/src\"\n", + "directory = codebase.get_directory(DIR_NAME)\n", + "\n", + "\n", + "for file in directory.files:\n", + " # Grab process.env statements\n", + " statements = file.code_block.statements\n", + " conditionals = [x for x in statements if is_env_conditional(x)]\n", + "\n", + " for imp in file.imports:\n", + " # See if the import is beneath one of the process.env statementss\n", + " if not any(imp.source in c.source for c in conditionals):\n", + " continue\n", + "\n", + " print(f\"❌ Detected bad import:\")\n", + " print(f\" ⚛️ File: {imp.file.filepath}\")\n", + " print(f\" ⬇️ Imported symbol: {imp.name}\")\n", + " print(f\" 🔗 Link: {imp.github_url}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "for statement in imp.to_file.code_block.statements:\n", + " if isinstance(statement, IfBlockStatement):\n", + " print(statement.source)\n", + " print(statement.github_url)\n", + " raise Exception" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "def is_env_conditional(statement):\n", + " if not isinstance(statement, IfBlockStatement):\n", + " return False\n", + " return \"process.env.NODE_ENV\" in statement.condition.source" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "file = imp.to_file\n", + "statements = file.code_block.statements\n", + "conditionals = [x for x in statements if is_env_conditional(x)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "len(file.code_block.statements)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "file = imp.to_file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "for s in statements:\n", + " print(type(s))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "for statement in statements:\n", + " if \"process.env.NODE_ENV\" in statement.source:\n", + " print(statement.source)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "for statement in statements:\n", + " if isinstance(statement, IfBlockStatement):\n", + " print(statement.source)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "conditionals" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "statement.condition.source" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "imp.is_wrapped_in(IfBlockStatement)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "imp.github_url" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "imp.to_file.filepath" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 6cad97de1..953eaa258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ dependencies = [ "slack-sdk", "langchain-anthropic>=0.3.7", "lox>=0.12.0", + "httpx>=0.28.1", ] license = { text = "Apache-2.0" } diff --git a/src/codegen/extensions/events/client.py b/src/codegen/extensions/events/client.py new file mode 100644 index 000000000..7b1462978 --- /dev/null +++ b/src/codegen/extensions/events/client.py @@ -0,0 +1,54 @@ +from typing import Any + +import httpx +from pydantic import BaseModel + + +class SlackTestEvent(BaseModel): + """Helper class to construct test Slack events""" + + user: str = "U123456" + type: str = "message" + ts: str = "1234567890.123456" + client_msg_id: str = "test-msg-id" + text: str = "Hello world" + team: str = "T123456" + channel: str = "C123456" + event_ts: str = "1234567890.123456" + blocks: list = [] + + +class CodegenClient: + """Client for testing CodegenApp endpoints""" + + def __init__(self, base_url: str = "http://localhost:8000"): + self.base_url = base_url.rstrip("/") + self.client = httpx.Client() + + async def send_slack_message(self, text: str, channel: str = "C123456", event_type: str = "message", **kwargs) -> dict[str, Any]: + """Send a test Slack message event + + Args: + text: The message text + channel: The channel ID + event_type: The type of event (e.g. 'message', 'app_mention') + **kwargs: Additional fields to override in the event + """ + event = SlackTestEvent(text=text, channel=channel, type=event_type, **kwargs) + + payload = { + "token": "test_token", + "team_id": "T123456", + "api_app_id": "A123456", + "event": event.model_dump(), + "type": "event_callback", + "event_id": "Ev123456", + "event_time": 1234567890, + } + + response = await self.client.post(f"{self.base_url}/slack/events", json=payload) + return response.json() + + def close(self): + """Close the HTTP client""" + self.client.close() diff --git a/src/codegen/extensions/events/codegen_app.py b/src/codegen/extensions/events/codegen_app.py new file mode 100644 index 000000000..1b2371c29 --- /dev/null +++ b/src/codegen/extensions/events/codegen_app.py @@ -0,0 +1,90 @@ +import logging +from typing import Optional + +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse + +from .github import GitHub +from .linear import Linear +from .slack import Slack + +logger = logging.getLogger(__name__) + + +class CodegenApp: + """A FastAPI-based application for handling various code-related events.""" + + def __init__(self, name: str, modal_api_key: Optional[str] = None): + self.name = name + self._modal_api_key = modal_api_key + + # Create the FastAPI app + self.app = FastAPI(title=name) + + # Initialize event handlers + self.linear = Linear(self) + self.slack = Slack(self) + self.github = GitHub(self) + + # Register routes + self._setup_routes() + + def _setup_routes(self): + """Set up the FastAPI routes for different event types.""" + + @self.app.get("/", response_class=HTMLResponse) + async def root(): + """Render the main page.""" + return """ + + + + Codegen + + + +

codegen

+ + + """ + + @self.app.post("/slack/events") + async def handle_slack_event(request: Request): + """Handle incoming Slack events.""" + payload = await request.json() + return self.slack.handle(payload) + + @self.app.post("/github/events") + async def handle_github_event(request: Request): + """Handle incoming GitHub events.""" + payload = await request.json() + return self.github.handle(payload, request) + + @self.app.post("/linear/events") + async def handle_linear_event(request: Request): + """Handle incoming Linear events.""" + payload = await request.json() + # Note: Linear handler needs to be implemented similar to others + return {"message": "Linear event received"} + + def run(self, host: str = "0.0.0.0", port: int = 8000, **kwargs): + """Run the FastAPI application.""" + import uvicorn + + uvicorn.run(self.app, host=host, port=port, **kwargs) diff --git a/test_events.py b/test_events.py new file mode 100644 index 000000000..1e5644ab7 --- /dev/null +++ b/test_events.py @@ -0,0 +1,65 @@ +import asyncio +import logging +import sys +from contextlib import asynccontextmanager + +from uvicorn.config import Config +from uvicorn.server import Server + +from codegen.extensions.events.client import CodegenClient +from codegen.extensions.events.codegen_app import CodegenApp + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def run_codegen_app(): + """Run the CodegenApp server as a context manager""" + # Create the app + app = CodegenApp(name="test-app") + + # Configure uvicorn + config = Config(app=app.app, host="127.0.0.1", port=8000, log_level="info") + server = Server(config=config) + + # Start the server + server_task = asyncio.create_task(server.serve()) + await asyncio.sleep(1) # Give the server a moment to start + + try: + yield server + finally: + # Shutdown the server + server.should_exit = True + await server_task + + +async def test_slack_events(): + """Test sending various Slack events to the CodegenApp""" + async with run_codegen_app(): + # Create a test client + client = CodegenClient() + + try: + # Test sending a simple message + logger.info("Sending test message...") + response = await client.send_slack_message(text="Hello codegen!", channel="C123TEST", event_type="message") + logger.info(f"Response from simple message: {response}") + + # Test sending an app mention + logger.info("Sending test app mention...") + response = await client.send_slack_message(text="<@U123BOT> help me with this code", channel="C123TEST", event_type="app_mention") + logger.info(f"Response from app mention: {response}") + + finally: + client.close() + + +if __name__ == "__main__": + try: + asyncio.run(test_slack_events()) + except KeyboardInterrupt: + logger.info("Test interrupted by user") + sys.exit(0) diff --git a/tests/integration/extension/codegen_app/test_add_handlers.py b/tests/integration/extension/codegen_app/test_add_handlers.py new file mode 100644 index 000000000..1359c9937 --- /dev/null +++ b/tests/integration/extension/codegen_app/test_add_handlers.py @@ -0,0 +1,90 @@ +import pytest +from slack_sdk import WebClient + +from codegen.extensions.events.codegen_app import CodegenApp +from codegen.extensions.events.slack import SlackEvent + + +@pytest.fixture +def app(): + """Create a test CodegenApp instance""" + return CodegenApp(name="test-handlers") + + +def test_register_slack_handler(app): + """Test registering a Slack event handler""" + + @app.slack.event("message") + def handle_message(client: WebClient, event: SlackEvent): + return {"message": "Handled slack message"} + + # Verify handler was registered + assert "message" in app.slack.registered_handlers + assert app.slack.registered_handlers["message"] is not None + + +def test_register_github_handler(app): + """Test registering a GitHub event handler""" + + @app.github.event("pull_request:opened") + def handle_pr(event: dict): + return {"message": "Handled PR"} + + # Verify handler was registered + assert "pull_request:opened" in app.github.registered_handlers + assert app.github.registered_handlers["pull_request:opened"] is not None + + +def test_register_linear_handler(app): + """Test registering a Linear event handler""" + + @app.linear.event("Issue") + def handle_issue(event: dict): + return {"message": "Handled issue"} + + # Verify handler was registered + handler = app.linear.registered_handlers[handle_issue.__qualname__] + assert handler.event_name == "Issue" + assert handler.handler_func is not None + + +def test_register_multiple_handlers(app): + """Test registering multiple handlers across different providers""" + + # Register Slack handler + @app.slack.event("message") + def handle_slack(client: WebClient, event: SlackEvent): + return {"message": "Handled slack"} + + # Register GitHub handler + @app.github.event("push") + def handle_push(event: dict): + return {"message": "Handled push"} + + # Register Linear handler + @app.linear.event("Issue") + def handle_linear(event: dict): + return {"message": "Handled linear"} + + # Verify all handlers were registered + assert "message" in app.slack.registered_handlers + assert "push" in app.github.registered_handlers + assert handle_linear.__qualname__ in app.linear.registered_handlers + + # Verify each handler is properly configured + assert app.slack.registered_handlers["message"] is not None + assert app.github.registered_handlers["push"] is not None + assert app.linear.registered_handlers[handle_linear.__qualname__].event_name == "Issue" + + +def test_handler_registration_is_isolated(app): + """Test that handlers are properly isolated between provider types""" + + @app.slack.event("message") + def handle_slack(client: WebClient, event: SlackEvent): + return {"message": "Handled slack"} + + # Verify handler is only in Slack registry + assert "message" in app.slack.registered_handlers + assert "message" not in app.github.registered_handlers + assert not any(h.event_name == "message" for h in app.linear.registered_handlers.values()) From 2bf93f9d5a2dae21f1cd200883135fd1e0004f14 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 11:51:23 -0800 Subject: [PATCH 02/15] . --- src/codegen/extensions/events/codegen_app.py | 26 ++++++- src/codegen/extensions/events/slack.py | 57 ++++++++++----- .../codegen_app/test_handler_logic.py | 73 +++++++++++++++++++ .../extension/codegen_app/test_slack.py | 0 4 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 tests/integration/extension/codegen_app/test_handler_logic.py create mode 100644 tests/integration/extension/codegen_app/test_slack.py diff --git a/src/codegen/extensions/events/codegen_app.py b/src/codegen/extensions/events/codegen_app.py index 1b2371c29..beb0395bc 100644 --- a/src/codegen/extensions/events/codegen_app.py +++ b/src/codegen/extensions/events/codegen_app.py @@ -1,5 +1,5 @@ import logging -from typing import Optional +from typing import Any, Optional from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse @@ -29,6 +29,26 @@ def __init__(self, name: str, modal_api_key: Optional[str] = None): # Register routes self._setup_routes() + async def simulate_event(self, provider: str, event_type: str, payload: dict) -> Any: + """Simulate an event without running the server. + + Args: + provider: The event provider ('slack', 'github', or 'linear') + event_type: The type of event to simulate + payload: The event payload + + Returns: + The handler's response + """ + provider_map = {"slack": self.slack, "github": self.github, "linear": self.linear} + + if provider not in provider_map: + msg = f"Unknown provider: {provider}. Must be one of {list(provider_map.keys())}" + raise ValueError(msg) + + handler = provider_map[provider] + return await handler.handle(payload) + def _setup_routes(self): """Set up the FastAPI routes for different event types.""" @@ -68,13 +88,13 @@ async def root(): async def handle_slack_event(request: Request): """Handle incoming Slack events.""" payload = await request.json() - return self.slack.handle(payload) + return await self.slack.handle(payload) @self.app.post("/github/events") async def handle_github_event(request: Request): """Handle incoming GitHub events.""" payload = await request.json() - return self.github.handle(payload, request) + return await self.github.handle(payload, request) @self.app.post("/linear/events") async def handle_linear_event(request: Request): diff --git a/src/codegen/extensions/events/slack.py b/src/codegen/extensions/events/slack.py index 464d8e15b..e9f8c4038 100644 --- a/src/codegen/extensions/events/slack.py +++ b/src/codegen/extensions/events/slack.py @@ -32,10 +32,10 @@ class SlackEvent(BaseModel): user: str type: str ts: str - client_msg_id: str + client_msg_id: str | None = None text: str - team: str - blocks: list[Block] + team: str | None = None + blocks: list[Block] | None = None channel: str event_ts: str @@ -68,21 +68,42 @@ def unsubscribe_all_handlers(self): logger.info("[HANDLERS] Clearing all handlers") self.registered_handlers.clear() - def handle(self, event: SlackWebhookPayload): + async def handle(self, event_data: dict) -> dict: + """Handle incoming Slack events. + + Args: + event_data: Raw event data dictionary from Slack + + Returns: + Response dictionary + """ + print(event_data) logger.info("[HANDLER] Handling Slack event") - if event.type == "url_verification": - return {"challenge": event.challenge} - elif event.type == "event_callback": - event = event.event - if event.type not in self.registered_handlers: + + try: + # Validate and convert to SlackWebhookPayload + event = SlackWebhookPayload.model_validate(event_data) + + if event.type == "url_verification": + return {"challenge": event.challenge} + elif event.type == "event_callback" and event.event: + if event.event.type not in self.registered_handlers: + logger.info(f"[HANDLER] No handler found for event type: {event.event.type}") + return {"message": "Event handled successfully"} + else: + handler = self.registered_handlers[event.event.type] + # Since the handler might be async, await it + result = handler(event.event) + if hasattr(result, "__await__"): + result = await result + return result + else: logger.info(f"[HANDLER] No handler found for event type: {event.type}") return {"message": "Event handled successfully"} - else: - handler = self.registered_handlers[event.type] - return handler(event) - else: - logger.info(f"[HANDLER] No handler found for event type: {event.type}") - return {"message": "Event handled successfully"} + + except Exception as e: + logger.exception(f"Error handling Slack event: {e}") + return {"error": f"Failed to handle event: {e!s}"} def event(self, event_name: str): """Decorator for registering a Slack event handler.""" @@ -93,10 +114,10 @@ def register_handler(func): func_name = func.__qualname__ logger.info(f"[EVENT] Registering function {func_name} for {event_name}") - def new_func(event): - return func(self.client, event) + async def new_func(event): + return await func(self.client, event) self.registered_handlers[event_name] = new_func - return new_func + return func # Return original function to maintain sync/async compatibility return register_handler diff --git a/tests/integration/extension/codegen_app/test_handler_logic.py b/tests/integration/extension/codegen_app/test_handler_logic.py new file mode 100644 index 000000000..6506e3c08 --- /dev/null +++ b/tests/integration/extension/codegen_app/test_handler_logic.py @@ -0,0 +1,73 @@ +import pytest +from slack_sdk import WebClient + +from codegen.extensions.events.codegen_app import CodegenApp +from codegen.extensions.events.slack import SlackEvent + + +@pytest.fixture +def app(): + """Create a test CodegenApp instance""" + return CodegenApp(name="test-handlers") + + +@pytest.fixture +def app_with_handlers(app): + """Create a CodegenApp instance with pre-registered handlers""" + + # Register an app mention handler + @app.slack.event("app_mention") + async def handle_mention(client: WebClient, event: SlackEvent): + return {"message": "Mentioned", "received_text": event.text} + + return app + + +@pytest.mark.asyncio +async def test_simulate_slack_mention(app_with_handlers): + """Test simulating a Slack app_mention event""" + # Create a test mention payload + payload = { + "token": "test_token", + "team_id": "T123456", + "api_app_id": "A123456", + "event": { + "type": "app_mention", + "user": "U123456", + "text": "<@U123BOT> help me with this code", + "ts": "1234567890.123456", + "channel": "C123456", + "event_ts": "1234567890.123456", + }, + "type": "event_callback", + "event_id": "Ev123456", + "event_time": 1234567890, + } + + # Simulate the event + response = await app_with_handlers.simulate_event(provider="slack", event_type="app_mention", payload=payload) + + # Verify the response + assert response is not None + assert response["message"] == "Mentioned" + assert response["received_text"] == "<@U123BOT> help me with this code" + + +@pytest.mark.asyncio +async def test_simulate_unknown_provider(app_with_handlers): + """Test simulating an event with an unknown provider""" + with pytest.raises(ValueError) as exc_info: + await app_with_handlers.simulate_event(provider="unknown", event_type="test", payload={}) + + assert "Unknown provider" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_simulate_unregistered_event(app_with_handlers): + """Test simulating an event type that has no registered handler""" + payload = {"event": {"type": "unknown_event", "user": "U123456"}} + + response = await app_with_handlers.simulate_event(provider="slack", event_type="unknown_event", payload=payload) + + # Should return a default response for unhandled events + assert response["message"] == "Event handled successfully" diff --git a/tests/integration/extension/codegen_app/test_slack.py b/tests/integration/extension/codegen_app/test_slack.py new file mode 100644 index 000000000..e69de29bb From 670fd03b6b94c9c269cf9a3ade0cc3d69a8cf4e0 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 13:00:55 -0800 Subject: [PATCH 03/15] . --- app.py | 11 +- codegen_app_app.py | 3 + modal_app.py | 3 + src/codegen/cli/cli.py | 2 + src/codegen/cli/commands/serve/main.py | 130 ++++++++++++++++++ src/codegen/extensions/events/client.py | 8 +- src/codegen/extensions/events/codegen_app.py | 53 ++++++- src/codegen/extensions/events/modal_app.py | 3 + .../codegen_app/test_handler_logic.py | 45 ++++++ .../extension/codegen_app/test_server.ipynb | 73 ++++++++++ 10 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 codegen_app_app.py create mode 100644 modal_app.py create mode 100644 src/codegen/cli/commands/serve/main.py create mode 100644 src/codegen/extensions/events/modal_app.py create mode 100644 tests/integration/extension/codegen_app/test_server.ipynb diff --git a/app.py b/app.py index c2b96d43a..5849362b4 100644 --- a/app.py +++ b/app.py @@ -9,17 +9,14 @@ logger = logging.getLogger(__name__) # Create the app -app = CodegenApp(name="codegen-test") +app = CodegenApp(name="codegen-test", repos=["fastapi/fastapi"]) @app.slack.event("app_mention") -def handle_mention(client: WebClient, event): +async def handle_mention(client: WebClient, event): logger.info("[APP_MENTION] Received app_mention event") - - # Example response - client.chat_postMessage(channel=event.channel, text=f"Hello! You mentioned me in {event.channel}", thread_ts=event.ts) - - return {"message": "Event handled successfully"} + codebase = app.get_codebase("fastapi/fastapi") + return {"num_files": len(codebase.files)} @app.github.event("pull_request:opened") diff --git a/codegen_app_app.py b/codegen_app_app.py new file mode 100644 index 000000000..9ef9edf98 --- /dev/null +++ b/codegen_app_app.py @@ -0,0 +1,3 @@ +from app import app + +app = app.app diff --git a/modal_app.py b/modal_app.py new file mode 100644 index 000000000..f39e0586c --- /dev/null +++ b/modal_app.py @@ -0,0 +1,3 @@ +import modal + +app = modal.App(name="codegen-events") diff --git a/src/codegen/cli/cli.py b/src/codegen/cli/cli.py index 465872c36..b1b0cc2b2 100644 --- a/src/codegen/cli/cli.py +++ b/src/codegen/cli/cli.py @@ -16,6 +16,7 @@ from codegen.cli.commands.reset.main import reset_command from codegen.cli.commands.run.main import run_command from codegen.cli.commands.run_on_pr.main import run_on_pr_command +from codegen.cli.commands.serve.main import serve_command from codegen.cli.commands.style_debug.main import style_debug_command from codegen.cli.commands.update.main import update_command @@ -47,6 +48,7 @@ def main(): main.add_command(update_command) main.add_command(config_command) main.add_command(lsp_command) +main.add_command(serve_command) if __name__ == "__main__": main() diff --git a/src/codegen/cli/commands/serve/main.py b/src/codegen/cli/commands/serve/main.py new file mode 100644 index 000000000..42ee3bc92 --- /dev/null +++ b/src/codegen/cli/commands/serve/main.py @@ -0,0 +1,130 @@ +import importlib.util +import logging +import sys +from pathlib import Path + +import rich +import rich_click as click +import uvicorn +from rich.logging import RichHandler +from rich.panel import Panel + +from codegen.extensions.events.codegen_app import CodegenApp + + +def setup_logging(debug: bool): + """Configure rich logging with colors.""" + logging.basicConfig( + level=logging.DEBUG if debug else logging.INFO, + format="%(message)s", + handlers=[ + RichHandler( + rich_tracebacks=True, + tracebacks_show_locals=debug, + markup=True, + show_time=False, + ) + ], + ) + + +def load_app_from_file(file_path: Path) -> CodegenApp: + """Load a CodegenApp instance from a Python file. + + Args: + file_path: Path to the Python file containing the CodegenApp + + Returns: + The CodegenApp instance from the file + + Raises: + click.ClickException: If no CodegenApp instance is found + """ + try: + # Import the module from file path + spec = importlib.util.spec_from_file_location("app_module", file_path) + if not spec or not spec.loader: + msg = f"Could not load module from {file_path}" + raise click.ClickException(msg) + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Find CodegenApp instance + for attr_name in dir(module): + attr = getattr(module, attr_name) + if isinstance(attr, CodegenApp): + return attr + + msg = f"No CodegenApp instance found in {file_path}" + raise click.ClickException(msg) + + except Exception as e: + msg = f"Error loading app from {file_path}: {e!s}" + raise click.ClickException(msg) + + +def create_app_module(file_path: Path) -> str: + """Create a temporary module that exports the app for uvicorn.""" + # Add the file's directory to Python path + file_dir = str(file_path.parent.absolute()) + if file_dir not in sys.path: + sys.path.insert(0, file_dir) + + # Create a module that imports and exposes the app + module_name = f"codegen_app_{file_path.stem}" + module_code = f""" +from {file_path.stem} import app +app = app.app +""" + module_path = file_path.parent / f"{module_name}.py" + module_path.write_text(module_code) + + return f"{module_name}:app" + + +@click.command(name="serve") +@click.argument("file", type=click.Path(exists=True, path_type=Path)) +@click.option("--host", default="127.0.0.1", help="Host to bind to") +@click.option("--port", default=8000, help="Port to bind to") +@click.option("--debug", is_flag=True, help="Enable debug mode with hot reloading") +def serve_command(file: Path, host: str = "127.0.0.1", port: int = 8000, debug: bool = False): + """Run a CodegenApp server from a Python file. + + FILE is the path to a Python file containing a CodegenApp instance + """ + # Configure rich logging + setup_logging(debug) + + try: + if debug: + # For debug mode, create a module that uvicorn can reload + app_import_string = create_app_module(file) + reload_dirs = [str(file.parent)] + else: + # For normal mode, load the app directly + app = load_app_from_file(file) + app_import_string = app.app + + # Print server info + rich.print( + Panel( + f"[green]Starting CodegenApp server[/green]\n[dim]File:[/dim] {file}\n[dim]URL:[/dim] http://{host}:{port}\n[dim]Debug:[/dim] {'enabled' if debug else 'disabled'}", + title="[bold]Server Info[/bold]", + border_style="blue", + ) + ) + + # Run the server + uvicorn.run( + app_import_string, + host=host, + port=port, + reload=debug, + reload_dirs=reload_dirs if debug else None, + log_level="debug" if debug else "info", + ) + + except Exception as e: + msg = f"Server error: {e!s}" + raise click.ClickException(msg) diff --git a/src/codegen/extensions/events/client.py b/src/codegen/extensions/events/client.py index 7b1462978..d2df7b66a 100644 --- a/src/codegen/extensions/events/client.py +++ b/src/codegen/extensions/events/client.py @@ -21,9 +21,9 @@ class SlackTestEvent(BaseModel): class CodegenClient: """Client for testing CodegenApp endpoints""" - def __init__(self, base_url: str = "http://localhost:8000"): + def __init__(self, base_url: str = "http://localhost:8000", timeout: float = 30.0): self.base_url = base_url.rstrip("/") - self.client = httpx.Client() + self.client = httpx.AsyncClient(timeout=timeout) async def send_slack_message(self, text: str, channel: str = "C123456", event_type: str = "message", **kwargs) -> dict[str, Any]: """Send a test Slack message event @@ -49,6 +49,6 @@ async def send_slack_message(self, text: str, channel: str = "C123456", event_ty response = await self.client.post(f"{self.base_url}/slack/events", json=payload) return response.json() - def close(self): + async def close(self): """Close the HTTP client""" - self.client.close() + await self.client.aclose() diff --git a/src/codegen/extensions/events/codegen_app.py b/src/codegen/extensions/events/codegen_app.py index beb0395bc..390cdc37f 100644 --- a/src/codegen/extensions/events/codegen_app.py +++ b/src/codegen/extensions/events/codegen_app.py @@ -4,6 +4,8 @@ from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse +from codegen.sdk.core.codebase import Codebase + from .github import GitHub from .linear import Linear from .slack import Slack @@ -14,9 +16,10 @@ class CodegenApp: """A FastAPI-based application for handling various code-related events.""" - def __init__(self, name: str, modal_api_key: Optional[str] = None): + def __init__(self, name: str, repos: Optional[list[str]] = None, modal_api_key: Optional[str] = None, tmp_dir: str = "/tmp/codegen"): self.name = name self._modal_api_key = modal_api_key + self.tmp_dir = tmp_dir # Create the FastAPI app self.app = FastAPI(title=name) @@ -26,9 +29,56 @@ def __init__(self, name: str, modal_api_key: Optional[str] = None): self.slack = Slack(self) self.github = GitHub(self) + # Initialize codebase cache + self.codebases: dict[str, Codebase] = {} + + # Parse initial repos if provided + if repos: + for repo in repos: + self._parse_repo(repo) + # Register routes self._setup_routes() + def _parse_repo(self, repo_name: str) -> None: + """Parse a GitHub repository and cache it. + + Args: + repo_name: Repository name in format "owner/repo" + """ + try: + logger.info(f"[CODEBASE] Parsing repository: {repo_name}") + self.codebases[repo_name] = Codebase.from_repo(repo_name, tmp_dir=self.tmp_dir) + logger.info(f"[CODEBASE] Successfully parsed and cached: {repo_name}") + except Exception as e: + logger.exception(f"[CODEBASE] Failed to parse repository {repo_name}: {e!s}") + raise + + def get_codebase(self, repo_name: str) -> Codebase: + """Get a cached codebase by repository name. + + Args: + repo_name: Repository name in format "owner/repo" + + Returns: + The cached Codebase instance + + Raises: + KeyError: If the repository hasn't been parsed + """ + if repo_name not in self.codebases: + msg = f"Repository {repo_name} has not been parsed. Available repos: {list(self.codebases.keys())}" + raise KeyError(msg) + return self.codebases[repo_name] + + def add_repo(self, repo_name: str) -> None: + """Add a new repository to parse and cache. + + Args: + repo_name: Repository name in format "owner/repo" + """ + self._parse_repo(repo_name) + async def simulate_event(self, provider: str, event_type: str, payload: dict) -> Any: """Simulate an event without running the server. @@ -88,6 +138,7 @@ async def root(): async def handle_slack_event(request: Request): """Handle incoming Slack events.""" payload = await request.json() + print(payload) return await self.slack.handle(payload) @self.app.post("/github/events") diff --git a/src/codegen/extensions/events/modal_app.py b/src/codegen/extensions/events/modal_app.py new file mode 100644 index 000000000..f39e0586c --- /dev/null +++ b/src/codegen/extensions/events/modal_app.py @@ -0,0 +1,3 @@ +import modal + +app = modal.App(name="codegen-events") diff --git a/tests/integration/extension/codegen_app/test_handler_logic.py b/tests/integration/extension/codegen_app/test_handler_logic.py index 6506e3c08..38120be48 100644 --- a/tests/integration/extension/codegen_app/test_handler_logic.py +++ b/tests/integration/extension/codegen_app/test_handler_logic.py @@ -1,6 +1,12 @@ +import asyncio +from contextlib import asynccontextmanager + import pytest from slack_sdk import WebClient +from uvicorn.config import Config +from uvicorn.server import Server +from codegen.extensions.events.client import CodegenClient from codegen.extensions.events.codegen_app import CodegenApp from codegen.extensions.events.slack import SlackEvent @@ -23,6 +29,45 @@ async def handle_mention(client: WebClient, event: SlackEvent): return app +@asynccontextmanager +async def run_codegen_app(app: CodegenApp): + """Run the CodegenApp server as a context manager""" + # Configure uvicorn + config = Config(app=app.app, host="127.0.0.1", port=8000, log_level="info") + server = Server(config=config) + + # Start the server + server_task = asyncio.create_task(server.serve()) + await asyncio.sleep(1) # Give the server a moment to start + + try: + yield server + finally: + # Shutdown the server + server.should_exit = True + await server_task + + +@pytest.mark.asyncio +async def test_server_slack_mention(app_with_handlers): + """Test sending a Slack mention through the actual server""" + async with run_codegen_app(app_with_handlers): + # Create a test client + client = CodegenClient() + + try: + # Send test mention + response = await client.send_slack_message(text="<@U123BOT> help me with this code", channel="C123TEST", event_type="app_mention") + + # Verify the response + assert response is not None + assert response["message"] == "Mentioned" + assert response["received_text"] == "<@U123BOT> help me with this code" + + finally: + await client.close() + + @pytest.mark.asyncio async def test_simulate_slack_mention(app_with_handlers): """Test simulating a Slack app_mention event""" diff --git a/tests/integration/extension/codegen_app/test_server.ipynb b/tests/integration/extension/codegen_app/test_server.ipynb new file mode 100644 index 000000000..0e359e206 --- /dev/null +++ b/tests/integration/extension/codegen_app/test_server.ipynb @@ -0,0 +1,73 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "import asyncio\n", + "from contextlib import asynccontextmanager\n", + "\n", + "import pytest\n", + "from slack_sdk import WebClient\n", + "from uvicorn.config import Config\n", + "from uvicorn.server import Server\n", + "\n", + "from codegen.extensions.events.client import CodegenClient\n", + "from codegen.extensions.events.codegen_app import CodegenApp\n", + "from codegen.extensions.events.slack import SlackEvent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client = CodegenClient()\n", + "\n", + "response = await client.send_slack_message(text=\"<@U123BOT> help me with this code\", channel=\"C123TEST\", event_type=\"app_mention\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 314ef551a4c04410fcf5becd28bfecee06b58a80 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 13:06:47 -0800 Subject: [PATCH 04/15] . --- app.py | 9 +-- src/codegen/extensions/events/slack.py | 59 ++----------------- src/codegen/extensions/slack/types.py | 44 ++++++++++++++ .../extension/codegen_app/test_server.ipynb | 2 +- 4 files changed, 55 insertions(+), 59 deletions(-) create mode 100644 src/codegen/extensions/slack/types.py diff --git a/app.py b/app.py index 5849362b4..7227e0eeb 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,7 @@ import logging -from slack_sdk import WebClient - from codegen.extensions.events.codegen_app import CodegenApp +from codegen.extensions.slack.types import SlackEvent # Set up logging logging.basicConfig(level=logging.INFO) @@ -13,10 +12,12 @@ @app.slack.event("app_mention") -async def handle_mention(client: WebClient, event): +async def handle_mention(event: SlackEvent): logger.info("[APP_MENTION] Received app_mention event") + logger.info(event) codebase = app.get_codebase("fastapi/fastapi") - return {"num_files": len(codebase.files)} + # Can access Slack client via app.slack.client if needed + return {"num_files": len(codebase.files), "num_functions": len(codebase.functions)} @app.github.event("pull_request:opened") diff --git a/src/codegen/extensions/events/slack.py b/src/codegen/extensions/events/slack.py index e9f8c4038..5a25726af 100644 --- a/src/codegen/extensions/events/slack.py +++ b/src/codegen/extensions/events/slack.py @@ -1,57 +1,15 @@ import logging import os -from typing import Literal -from pydantic import BaseModel, Field from slack_sdk import WebClient from codegen.extensions.events.interface import EventHandlerManagerProtocol +from codegen.extensions.slack.types import SlackWebhookPayload logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -class RichTextElement(BaseModel): - type: str - user_id: str | None = None - text: str | None = None - - -class RichTextSection(BaseModel): - type: Literal["rich_text_section"] - elements: list[RichTextElement] - - -class Block(BaseModel): - type: Literal["rich_text"] - block_id: str - elements: list[RichTextSection] - - -class SlackEvent(BaseModel): - user: str - type: str - ts: str - client_msg_id: str | None = None - text: str - team: str | None = None - blocks: list[Block] | None = None - channel: str - event_ts: str - - -class SlackWebhookPayload(BaseModel): - token: str | None = Field(None) - team_id: str | None = Field(None) - api_app_id: str | None = Field(None) - event: SlackEvent | None = Field(None) - type: str | None = Field(None) - event_id: str | None = Field(None) - event_time: int | None = Field(None) - challenge: str | None = Field(None) - subtype: str | None = Field(None) - - class Slack(EventHandlerManagerProtocol): _client: WebClient | None = None @@ -69,15 +27,7 @@ def unsubscribe_all_handlers(self): self.registered_handlers.clear() async def handle(self, event_data: dict) -> dict: - """Handle incoming Slack events. - - Args: - event_data: Raw event data dictionary from Slack - - Returns: - Response dictionary - """ - print(event_data) + """Handle incoming Slack events.""" logger.info("[HANDLER] Handling Slack event") try: @@ -115,9 +65,10 @@ def register_handler(func): logger.info(f"[EVENT] Registering function {func_name} for {event_name}") async def new_func(event): - return await func(self.client, event) + # Just pass the event, handler can access client via app.slack.client + return await func(event) self.registered_handlers[event_name] = new_func - return func # Return original function to maintain sync/async compatibility + return func return register_handler diff --git a/src/codegen/extensions/slack/types.py b/src/codegen/extensions/slack/types.py new file mode 100644 index 000000000..c9c56dc6e --- /dev/null +++ b/src/codegen/extensions/slack/types.py @@ -0,0 +1,44 @@ +from typing import Literal + +from pydantic import BaseModel, Field + + +class RichTextElement(BaseModel): + type: str + user_id: str | None = None + text: str | None = None + + +class RichTextSection(BaseModel): + type: Literal["rich_text_section"] + elements: list[RichTextElement] + + +class Block(BaseModel): + type: Literal["rich_text"] + block_id: str + elements: list[RichTextSection] + + +class SlackEvent(BaseModel): + user: str + type: str + ts: str + client_msg_id: str | None = None + text: str + team: str | None = None + blocks: list[Block] | None = None + channel: str + event_ts: str + + +class SlackWebhookPayload(BaseModel): + token: str | None = Field(None) + team_id: str | None = Field(None) + api_app_id: str | None = Field(None) + event: SlackEvent | None = Field(None) + type: str | None = Field(None) + event_id: str | None = Field(None) + event_time: int | None = Field(None) + challenge: str | None = Field(None) + subtype: str | None = Field(None) diff --git a/tests/integration/extension/codegen_app/test_server.ipynb b/tests/integration/extension/codegen_app/test_server.ipynb index 0e359e206..f13dced31 100644 --- a/tests/integration/extension/codegen_app/test_server.ipynb +++ b/tests/integration/extension/codegen_app/test_server.ipynb @@ -18,7 +18,7 @@ "\n", "from codegen.extensions.events.client import CodegenClient\n", "from codegen.extensions.events.codegen_app import CodegenApp\n", - "from codegen.extensions.events.slack import SlackEvent" + "from codegen.extensions.slack.types import SlackEvent" ] }, { From eb29c576de3b6dc5d05d68dd09e94aeb3c271e0c Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 13:20:14 -0800 Subject: [PATCH 05/15] . --- app.py | 10 +- src/codegen/extensions/events/client.py | 24 ++++ src/codegen/extensions/events/github.py | 41 +++--- .../github/types/events/pull_request.py | 47 +++++-- .../extensions/github/types/pull_request.py | 24 +++- .../codegen_app/test_handler_logic.py | 117 +++++++++++++++++- .../extension/codegen_app/test_server.ipynb | 50 ++++++++ 7 files changed, 277 insertions(+), 36 deletions(-) diff --git a/app.py b/app.py index 7227e0eeb..b76a42283 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,7 @@ import logging from codegen.extensions.events.codegen_app import CodegenApp +from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent from codegen.extensions.slack.types import SlackEvent # Set up logging @@ -20,10 +21,11 @@ async def handle_mention(event: SlackEvent): return {"num_files": len(codebase.files), "num_functions": len(codebase.functions)} -@app.github.event("pull_request:opened") -def handle_pr(event): - logger.info(f"New PR opened: {event}") - return {"message": "PR event handled"} +@app.github.event("pull_request:labeled") +def handle_pr(event: PullRequestLabeledEvent): + logger.info(f"PR labeled: {event}") + codebase = app.get_codebase("fastapi/fastapi") + return {"message": "PR event handled", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} if __name__ == "__main__": diff --git a/src/codegen/extensions/events/client.py b/src/codegen/extensions/events/client.py index d2df7b66a..37b131873 100644 --- a/src/codegen/extensions/events/client.py +++ b/src/codegen/extensions/events/client.py @@ -49,6 +49,30 @@ async def send_slack_message(self, text: str, channel: str = "C123456", event_ty response = await self.client.post(f"{self.base_url}/slack/events", json=payload) return response.json() + async def send_github_event(self, event_type: str, action: str | None = None, payload: dict | None = None) -> dict[str, Any]: + """Send a test GitHub webhook event + + Args: + event_type: The type of event (e.g. 'pull_request', 'push') + action: The action for the event (e.g. 'labeled', 'opened') + payload: The event payload + """ + # Construct headers that GitHub would send + headers = { + "x-github-event": event_type, + "x-github-delivery": "test-delivery-id", + "x-github-hook-id": "test-hook-id", + "x-github-hook-installation-target-id": "test-target-id", + "x-github-hook-installation-target-type": "repository", + } + + response = await self.client.post( + f"{self.base_url}/github/events", + json=payload, + headers=headers, + ) + return response.json() + async def close(self): """Close the HTTP client""" await self.client.aclose() diff --git a/src/codegen/extensions/events/github.py b/src/codegen/extensions/events/github.py index 0c899ebb7..fb3e417bd 100644 --- a/src/codegen/extensions/events/github.py +++ b/src/codegen/extensions/events/github.py @@ -69,7 +69,7 @@ def new_func(raw_event: dict): return register_handler - def handle(self, event: dict, request: Request): + async def handle(self, event: dict, request: Request | None = None) -> dict: """Handle both webhook events and installation callbacks.""" logger.info("[HANDLER] Handling GitHub event") @@ -89,35 +89,44 @@ def handle(self, event: dict, request: Request): }, } - # Extract headers for webhook events - headers = { - "x-github-event": request.headers.get("x-github-event"), - "x-github-delivery": request.headers.get("x-github-delivery"), - "x-github-hook-id": request.headers.get("x-github-hook-id"), - "x-github-hook-installation-target-id": request.headers.get("x-github-hook-installation-target-id"), - "x-github-hook-installation-target-type": request.headers.get("x-github-hook-installation-target-type"), - } - print(headers) + # Extract headers for webhook events if request is provided + headers = {} + if request: + headers = { + "x-github-event": request.headers.get("x-github-event"), + "x-github-delivery": request.headers.get("x-github-delivery"), + "x-github-hook-id": request.headers.get("x-github-hook-id"), + "x-github-hook-installation-target-id": request.headers.get("x-github-hook-installation-target-id"), + "x-github-hook-installation-target-type": request.headers.get("x-github-hook-installation-target-type"), + } # Handle webhook events try: - webhook = GitHubWebhookPayload.model_validate({"headers": headers, "event": event}) + # For simulation, use event data directly + if not request: + event_type = f"pull_request:{event['action']}" if "action" in event else event.get("type", "unknown") + if event_type not in self.registered_handlers: + logger.info(f"[HANDLER] No handler found for event type: {event_type}") + return {"message": "Event type not handled"} + else: + logger.info(f"[HANDLER] Handling event: {event_type}") + handler = self.registered_handlers[event_type] + return handler(event) - # Get base event type and action + # For actual webhooks, use the full payload + webhook = GitHubWebhookPayload.model_validate({"headers": headers, "event": event}) event_type = webhook.headers.event_type action = webhook.event.action - - # Combine event type and action if both exist full_event_type = f"{event_type}:{action}" if action else event_type if full_event_type not in self.registered_handlers: logger.info(f"[HANDLER] No handler found for event type: {full_event_type}") return {"message": "Event type not handled"} - else: logger.info(f"[HANDLER] Handling event: {full_event_type}") handler = self.registered_handlers[full_event_type] - return handler(event) # TODO - pass through typed values + return handler(event) + except Exception as e: logger.exception(f"Error handling webhook: {e}") raise diff --git a/src/codegen/extensions/github/types/events/pull_request.py b/src/codegen/extensions/github/types/events/pull_request.py index 13cae72c7..3691184b6 100644 --- a/src/codegen/extensions/github/types/events/pull_request.py +++ b/src/codegen/extensions/github/types/events/pull_request.py @@ -1,23 +1,52 @@ +from typing import Literal + from pydantic import BaseModel from ..base import GitHubRepository, GitHubUser from ..enterprise import GitHubEnterprise from ..installation import GitHubInstallation -from ..label import GitHubLabel from ..organization import GitHubOrganization from ..pull_request import PullRequest +class User(BaseModel): + id: int + login: str + + +class Label(BaseModel): + id: int + node_id: str + url: str + name: str + description: str | None = None + color: str + default: bool + + +class SimplePullRequest(BaseModel): + id: int + number: int + state: str + locked: bool + title: str + user: User + body: str | None = None + labels: list[Label] = [] + created_at: str + updated_at: str + draft: bool = False + + class PullRequestLabeledEvent(BaseModel): - action: str # Will be "labeled" + """Simplified version of the PR labeled event for testing""" + + action: Literal["labeled"] number: int - pull_request: PullRequest - label: GitHubLabel - repository: GitHubRepository - organization: GitHubOrganization - enterprise: GitHubEnterprise - sender: GitHubUser - installation: GitHubInstallation + pull_request: SimplePullRequest + label: Label + repository: dict # Simplified for now + sender: User class PullRequestOpenedEvent(BaseModel): diff --git a/src/codegen/extensions/github/types/pull_request.py b/src/codegen/extensions/github/types/pull_request.py index 50228d3d6..c4b58eed6 100644 --- a/src/codegen/extensions/github/types/pull_request.py +++ b/src/codegen/extensions/github/types/pull_request.py @@ -1,9 +1,8 @@ -from typing import Optional +from typing import Literal, Optional from pydantic import BaseModel from .base import GitHubRepository, GitHubUser -from .label import GitHubLabel class PullRequestRef(BaseModel): @@ -25,6 +24,16 @@ class PullRequestLinks(BaseModel): statuses: dict +class Label(BaseModel): + id: int + node_id: str + url: str + name: str + description: str | None = None + color: str + default: bool + + class PullRequest(BaseModel): url: str id: int @@ -48,7 +57,7 @@ class PullRequest(BaseModel): assignees: list[GitHubUser] requested_reviewers: list[GitHubUser] requested_teams: list[dict] - labels: list[GitHubLabel] + labels: list[Label] milestone: Optional[dict] draft: bool head: PullRequestRef @@ -69,3 +78,12 @@ class PullRequest(BaseModel): additions: int deletions: int changed_files: int + + +class PullRequestLabeledEvent(BaseModel): + action: Literal["labeled"] + number: int + pull_request: PullRequest + label: Label + repository: dict # Simplified for now + sender: dict # Simplified for now diff --git a/tests/integration/extension/codegen_app/test_handler_logic.py b/tests/integration/extension/codegen_app/test_handler_logic.py index 38120be48..5e490449a 100644 --- a/tests/integration/extension/codegen_app/test_handler_logic.py +++ b/tests/integration/extension/codegen_app/test_handler_logic.py @@ -2,13 +2,13 @@ from contextlib import asynccontextmanager import pytest -from slack_sdk import WebClient from uvicorn.config import Config from uvicorn.server import Server from codegen.extensions.events.client import CodegenClient from codegen.extensions.events.codegen_app import CodegenApp -from codegen.extensions.events.slack import SlackEvent +from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent +from codegen.extensions.slack.types import SlackEvent @pytest.fixture @@ -21,11 +21,21 @@ def app(): def app_with_handlers(app): """Create a CodegenApp instance with pre-registered handlers""" - # Register an app mention handler + # Register Slack handler @app.slack.event("app_mention") - async def handle_mention(client: WebClient, event: SlackEvent): + async def handle_mention(event: SlackEvent): return {"message": "Mentioned", "received_text": event.text} + # Register GitHub handler + @app.github.event("pull_request:labeled") + def handle_labeled(event: PullRequestLabeledEvent): + return { + "message": "PR labeled", + "pr_number": event.number, + "label": event.label.name, + "title": event.pull_request.title, + } + return app @@ -116,3 +126,102 @@ async def test_simulate_unregistered_event(app_with_handlers): # Should return a default response for unhandled events assert response["message"] == "Event handled successfully" + + +@pytest.mark.asyncio +async def test_simulate_github_pr_labeled(app_with_handlers): + """Test simulating a GitHub PR labeled event""" + # Create a test PR labeled payload + payload = { + "action": "labeled", + "number": 123, + "pull_request": { + "id": 12345, + "number": 123, + "state": "open", + "locked": False, + "title": "Test PR", + "user": {"id": 1, "login": "test-user"}, + "body": "Test PR body", + "labels": [], + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "draft": False, + }, + "label": {"id": 1, "node_id": "123", "url": "https://api.github.com/repos/test/test/labels/bug", "name": "bug", "description": "Bug report", "color": "red", "default": False}, + "repository": {"id": 1, "name": "test"}, + "sender": {"id": 1, "login": "test-user"}, + } + + # Simulate the event + response = await app_with_handlers.simulate_event(provider="github", event_type="pull_request:labeled", payload=payload) + + # Verify the response + assert response is not None + assert response["message"] == "PR labeled" + assert response["pr_number"] == 123 + assert response["label"] == "bug" + assert response["title"] == "Test PR" + + +@pytest.mark.asyncio +async def test_server_github_pr_labeled(app_with_handlers): + """Test sending a GitHub PR labeled event through the actual server""" + async with run_codegen_app(app_with_handlers): + # Create a test client + client = CodegenClient() + + try: + # Create test PR labeled payload + payload = { + "action": "labeled", + "number": 123, + "pull_request": { + "id": 12345, + "number": 123, + "node_id": "PR_123", + "state": "open", + "locked": False, + "title": "Test PR", + "user": {"id": 1, "login": "test-user", "node_id": "U_123", "type": "User"}, + "body": "Test PR body", + "labels": [], + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "draft": False, + "head": { + "label": "user:feature", + "ref": "feature", + "sha": "abc123", + "user": {"id": 1, "login": "test-user", "node_id": "U_123", "type": "User"}, + "repo": {"id": 1, "name": "test", "node_id": "R_123", "full_name": "test/test", "private": False, "owner": {"id": 1, "login": "test-user", "node_id": "U_123", "type": "User"}}, + }, + "base": { + "label": "main", + "ref": "main", + "sha": "def456", + "user": {"id": 1, "login": "test-user", "node_id": "U_123", "type": "User"}, + "repo": {"id": 1, "name": "test", "node_id": "R_123", "full_name": "test/test", "private": False, "owner": {"id": 1, "login": "test-user", "node_id": "U_123", "type": "User"}}, + }, + }, + "label": {"id": 1, "node_id": "L_123", "url": "https://api.github.com/repos/test/test/labels/bug", "name": "bug", "description": "Bug report", "color": "red", "default": False}, + "repository": {"id": 1, "name": "test", "node_id": "R_123", "full_name": "test/test", "private": False, "owner": {"id": 1, "login": "test-user", "node_id": "U_123", "type": "User"}}, + "sender": {"id": 1, "login": "test-user", "node_id": "U_123", "type": "User"}, + } + + # Send test event + response = await client.send_github_event( + event_type="pull_request", + action="labeled", + payload=payload, + ) + + # Verify the response + assert response is not None + assert response["message"] == "PR labeled" + assert response["pr_number"] == 123 + assert response["label"] == "bug" + assert response["title"] == "Test PR" + + finally: + await client.close() diff --git a/tests/integration/extension/codegen_app/test_server.ipynb b/tests/integration/extension/codegen_app/test_server.ipynb index f13dced31..258a15fb9 100644 --- a/tests/integration/extension/codegen_app/test_server.ipynb +++ b/tests/integration/extension/codegen_app/test_server.ipynb @@ -32,6 +32,56 @@ "response = await client.send_slack_message(text=\"<@U123BOT> help me with this code\", channel=\"C123TEST\", event_type=\"app_mention\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "payload = {\n", + " \"action\": \"labeled\",\n", + " \"number\": 123,\n", + " \"pull_request\": {\n", + " \"id\": 12345,\n", + " \"number\": 123,\n", + " \"node_id\": \"PR_123\",\n", + " \"state\": \"open\",\n", + " \"locked\": False,\n", + " \"title\": \"Test PR\",\n", + " \"user\": {\"id\": 1, \"login\": \"test-user\", \"node_id\": \"U_123\", \"type\": \"User\"},\n", + " \"body\": \"Test PR body\",\n", + " \"labels\": [],\n", + " \"created_at\": \"2024-01-01T00:00:00Z\",\n", + " \"updated_at\": \"2024-01-01T00:00:00Z\",\n", + " \"draft\": False,\n", + " \"head\": {\n", + " \"label\": \"user:feature\",\n", + " \"ref\": \"feature\",\n", + " \"sha\": \"abc123\",\n", + " \"user\": {\"id\": 1, \"login\": \"test-user\", \"node_id\": \"U_123\", \"type\": \"User\"},\n", + " \"repo\": {\"id\": 1, \"name\": \"test\", \"node_id\": \"R_123\", \"full_name\": \"test/test\", \"private\": False, \"owner\": {\"id\": 1, \"login\": \"test-user\", \"node_id\": \"U_123\", \"type\": \"User\"}},\n", + " },\n", + " \"base\": {\n", + " \"label\": \"main\",\n", + " \"ref\": \"main\",\n", + " \"sha\": \"def456\",\n", + " \"user\": {\"id\": 1, \"login\": \"test-user\", \"node_id\": \"U_123\", \"type\": \"User\"},\n", + " \"repo\": {\"id\": 1, \"name\": \"test\", \"node_id\": \"R_123\", \"full_name\": \"test/test\", \"private\": False, \"owner\": {\"id\": 1, \"login\": \"test-user\", \"node_id\": \"U_123\", \"type\": \"User\"}},\n", + " },\n", + " },\n", + " \"label\": {\"id\": 1, \"node_id\": \"L_123\", \"url\": \"https://api.github.com/repos/test/test/labels/bug\", \"name\": \"bug\", \"description\": \"Bug report\", \"color\": \"red\", \"default\": False},\n", + " \"repository\": {\"id\": 1, \"name\": \"test\", \"node_id\": \"R_123\", \"full_name\": \"test/test\", \"private\": False, \"owner\": {\"id\": 1, \"login\": \"test-user\", \"node_id\": \"U_123\", \"type\": \"User\"}},\n", + " \"sender\": {\"id\": 1, \"login\": \"test-user\", \"node_id\": \"U_123\", \"type\": \"User\"},\n", + "}\n", + "\n", + "# Send test event\n", + "response = await client.send_github_event(\n", + " event_type=\"pull_request\",\n", + " action=\"labeled\",\n", + " payload=payload,\n", + ")" + ] + }, { "cell_type": "code", "execution_count": null, From a8df793db2367f27a8ae95ef6c725f359956cb62 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 14:16:20 -0800 Subject: [PATCH 06/15] . --- modal_app.py | 49 +++++++- src/codegen/extensions/events/client.py | 9 ++ src/codegen/extensions/events/codegen_app.py | 4 +- src/codegen/extensions/events/linear.py | 118 +++++++++--------- .../codegen_app/test_handler_logic.py | 71 +++++++++++ 5 files changed, 187 insertions(+), 64 deletions(-) diff --git a/modal_app.py b/modal_app.py index f39e0586c..ffe74e53e 100644 --- a/modal_app.py +++ b/modal_app.py @@ -1,3 +1,50 @@ +import logging + import modal -app = modal.App(name="codegen-events") +from codegen.extensions.events.codegen_app import CodegenApp +from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent +from codegen.extensions.slack.types import SlackEvent + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create Modal image with dependencies +image = modal.Image.debian_slim().pip_install( + "fastapi[standard]", + "uvicorn", + "slack_sdk", + "pydantic", + "rich", + "httpx", +) + +# Create Modal app +app = modal.App(name="codegen-app", image=image) + +# Create CodegenApp instance +cg_app = CodegenApp(name="codegen-test", repos=["fastapi/fastapi"]) + + +@cg_app.slack.event("app_mention") +async def handle_mention(event: SlackEvent): + logger.info("[APP_MENTION] Received app_mention event") + logger.info(event) + codebase = cg_app.get_codebase("fastapi/fastapi") + return {"num_files": len(codebase.files), "num_functions": len(codebase.functions)} + + +@cg_app.github.event("pull_request:labeled") +def handle_pr(event: PullRequestLabeledEvent): + logger.info(f"PR labeled: {event}") + codebase = cg_app.get_codebase("fastapi/fastapi") + return {"message": "PR event handled", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} + + +# Create Modal ASGI app +@app.function(secrets=[modal.Secret.from_dotenv()], image=image) +@modal.asgi_app() +def fastapi_app(): + # Return the FastAPI app + return app.app diff --git a/src/codegen/extensions/events/client.py b/src/codegen/extensions/events/client.py index 37b131873..642b03354 100644 --- a/src/codegen/extensions/events/client.py +++ b/src/codegen/extensions/events/client.py @@ -73,6 +73,15 @@ async def send_github_event(self, event_type: str, action: str | None = None, pa ) return response.json() + async def send_linear_event(self, payload: dict) -> dict[str, Any]: + """Send a test Linear webhook event + + Args: + payload: The event payload + """ + response = await self.client.post(f"{self.base_url}/linear/events", json=payload) + return response.json() + async def close(self): """Close the HTTP client""" await self.client.aclose() diff --git a/src/codegen/extensions/events/codegen_app.py b/src/codegen/extensions/events/codegen_app.py index 390cdc37f..d65ea4423 100644 --- a/src/codegen/extensions/events/codegen_app.py +++ b/src/codegen/extensions/events/codegen_app.py @@ -138,7 +138,6 @@ async def root(): async def handle_slack_event(request: Request): """Handle incoming Slack events.""" payload = await request.json() - print(payload) return await self.slack.handle(payload) @self.app.post("/github/events") @@ -151,8 +150,7 @@ async def handle_github_event(request: Request): async def handle_linear_event(request: Request): """Handle incoming Linear events.""" payload = await request.json() - # Note: Linear handler needs to be implemented similar to others - return {"message": "Linear event received"} + return await self.linear.handle(payload) def run(self, host: str = "0.0.0.0", port: int = 8000, **kwargs): """Run the FastAPI application.""" diff --git a/src/codegen/extensions/events/linear.py b/src/codegen/extensions/events/linear.py index f1b97a04f..d0cdbbc6c 100644 --- a/src/codegen/extensions/events/linear.py +++ b/src/codegen/extensions/events/linear.py @@ -1,83 +1,81 @@ -import functools import logging -import os -from typing import Callable +from typing import Any, Callable, TypeVar -import modal # deptry: ignore from pydantic import BaseModel -from codegen.extensions.clients.linear import LinearClient from codegen.extensions.events.interface import EventHandlerManagerProtocol logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) - -class RegisteredWebhookHandler(BaseModel): - webhook_id: str | None = None - handler_func: Callable - event_name: str +# Type variable for event types +T = TypeVar("T", bound=BaseModel) class Linear(EventHandlerManagerProtocol): - def __init__(self, app: modal.App): + def __init__(self, app): self.app = app - self.access_token = os.environ["LINEAR_ACCESS_TOKEN"] # move to extensions config. - self.signing_secret = os.environ["LINEAR_SIGNING_SECRET"] - self.linear_team_id = os.environ["LINEAR_TEAM_ID"] self.registered_handlers = {} - self._webhook_url = None - - def subscribe_handler_to_webhook(self, handler: RegisteredWebhookHandler): - client = LinearClient(access_token=self.access_token) - web_url = modal.Function.from_name(app_name=self.app.name, name=handler.handler_func.__qualname__).web_url - result = client.register_webhook(team_id=self.linear_team_id, webhook_url=web_url, enabled=True, resource_types=[handler.event_name], secret=self.signing_secret) - return result - - def subscribe_all_handlers(self): - for handler_key in self.registered_handlers: - handler = self.registered_handlers[handler_key] - result = self.subscribe_handler_to_webhook(handler=self.registered_handlers[handler_key]) - handler.webhook_id = result - - def unsubscribe_handler_to_webhook(self, registered_handler: RegisteredWebhookHandler): - webhook_id = registered_handler.webhook_id - - client = LinearClient(access_token=self.access_token) - if webhook_id: - print(f"Unsubscribing from webhook {webhook_id}") - result = client.unregister_webhook(webhook_id) - return result - else: - print("No webhook id found for handler") - return None def unsubscribe_all_handlers(self): - for handler in self.registered_handlers: - self.unsubscribe_handler_to_webhook(self.registered_handlers[handler]) + logger.info("[HANDLERS] Clearing all handlers") + self.registered_handlers.clear() - def event(self, event_name, should_handle: Callable[[dict], bool] | None = None): - """Decorator for registering an event handler. + def event(self, event_name: str): + """Decorator for registering a Linear event handler. - :param event_name: The name of the event to handle. - :param register_hook: An optional function to call during registration, - e.g., to make an API call to register the webhook. + Args: + event_name: The type of event to handle (e.g. 'Issue', 'Comment') """ + logger.info(f"[EVENT] Registering handler for {event_name}") - def decorator(func): - # Register the handler with the app's registry. - modal_ready_func = func + def register_handler(func: Callable[[T], Any]): func_name = func.__qualname__ - self.registered_handlers[func_name] = RegisteredWebhookHandler(handler_func=modal_ready_func, event_name=event_name) - - @functools.wraps(func) - def wrapper(*args, **kwargs): - should_handle_result = should_handle(*args, **kwargs) if should_handle else True - if should_handle is None or should_handle_result: - return func(*args, **kwargs) - else: - logger.info(f"Skipping event {event_name} for {func_name}") + logger.info(f"[EVENT] Registering function {func_name} for {event_name}") + + def new_func(raw_event: dict): + # Get event type from payload + event_type = raw_event.get("type") + if event_type != event_name: + logger.info(f"[HANDLER] Event type mismatch: expected {event_name}, got {event_type}") return None - return wrapper + return func(raw_event) + + self.registered_handlers[event_name] = new_func + return func + + return register_handler - return decorator + async def handle(self, event: dict) -> dict: + """Handle incoming Linear events. + + Args: + event: The event payload from Linear + + Returns: + Response dictionary + """ + logger.info("[HANDLER] Handling Linear event") + + try: + # Extract event type + event_type = event.get("type") + if not event_type: + logger.info("[HANDLER] No event type found in payload") + return {"message": "Event type not found"} + + if event_type not in self.registered_handlers: + logger.info(f"[HANDLER] No handler found for event type: {event_type}") + return {"message": "Event handled successfully"} + else: + logger.info(f"[HANDLER] Handling event: {event_type}") + handler = self.registered_handlers[event_type] + result = handler(event) + if hasattr(result, "__await__"): + result = await result + return result + + except Exception as e: + logger.exception(f"Error handling Linear event: {e}") + return {"error": f"Failed to handle event: {e!s}"} diff --git a/tests/integration/extension/codegen_app/test_handler_logic.py b/tests/integration/extension/codegen_app/test_handler_logic.py index 5e490449a..c3cfa708c 100644 --- a/tests/integration/extension/codegen_app/test_handler_logic.py +++ b/tests/integration/extension/codegen_app/test_handler_logic.py @@ -36,6 +36,15 @@ def handle_labeled(event: PullRequestLabeledEvent): "title": event.pull_request.title, } + # Register Linear handler + @app.linear.event("Issue") + def handle_issue_created(event: dict): + return { + "message": "Issue created", + "issue_id": event["data"]["id"], + "title": event["data"]["title"], + } + return app @@ -225,3 +234,65 @@ async def test_server_github_pr_labeled(app_with_handlers): finally: await client.close() + + +@pytest.mark.asyncio +async def test_simulate_linear_issue_created(app_with_handlers): + """Test simulating a Linear issue created event""" + # Create a test issue created payload + payload = { + "action": "create", + "type": "Issue", + "data": { + "id": "abc-123", + "title": "Test Issue", + "description": "This is a test issue", + "priority": 1, + "teamId": "team-123", + }, + "url": "https://linear.app/company/issue/ABC-123", + } + + # Simulate the event + response = await app_with_handlers.simulate_event(provider="linear", event_type="Issue", payload=payload) + + # Verify the response + assert response is not None + assert response["message"] == "Issue created" + assert response["issue_id"] == "abc-123" + assert response["title"] == "Test Issue" + + +@pytest.mark.asyncio +async def test_server_linear_issue_created(app_with_handlers): + """Test sending a Linear issue created event through the actual server""" + async with run_codegen_app(app_with_handlers): + # Create a test client + client = CodegenClient() + + try: + # Create test issue created payload + payload = { + "action": "create", + "type": "Issue", + "data": { + "id": "abc-123", + "title": "Test Issue", + "description": "This is a test issue", + "priority": 1, + "teamId": "team-123", + }, + "url": "https://linear.app/company/issue/ABC-123", + } + + # Send test event + response = await client.send_linear_event(payload=payload) + + # Verify the response + assert response is not None + assert response["message"] == "Issue created" + assert response["issue_id"] == "abc-123" + assert response["title"] == "Test Issue" + + finally: + await client.close() From 43b9f5d6f6493e757987b85dd0b3cb2255870999 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 14:23:53 -0800 Subject: [PATCH 07/15] . --- app.py | 9 ++- src/codegen/extensions/events/linear.py | 7 ++- .../extensions/linear/linear_client.py | 31 +--------- src/codegen/extensions/linear/types.py | 40 +++++++++++++ .../codegen_app/test_handler_logic.py | 7 ++- .../extension/codegen_app/test_server.ipynb | 56 ++++++++++++++++++- 6 files changed, 113 insertions(+), 37 deletions(-) create mode 100644 src/codegen/extensions/linear/types.py diff --git a/app.py b/app.py index b76a42283..ed0841c27 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,7 @@ from codegen.extensions.events.codegen_app import CodegenApp from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent +from codegen.extensions.linear.types import LinearEvent from codegen.extensions.slack.types import SlackEvent # Set up logging @@ -17,7 +18,6 @@ async def handle_mention(event: SlackEvent): logger.info("[APP_MENTION] Received app_mention event") logger.info(event) codebase = app.get_codebase("fastapi/fastapi") - # Can access Slack client via app.slack.client if needed return {"num_files": len(codebase.files), "num_functions": len(codebase.functions)} @@ -28,6 +28,13 @@ def handle_pr(event: PullRequestLabeledEvent): return {"message": "PR event handled", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} +@app.linear.event("Issue") +def handle_issue(event: LinearEvent): + logger.info(f"Issue created: {event}") + codebase = app.get_codebase("fastapi/fastapi") + return {"message": "Linear Issue event", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} + + if __name__ == "__main__": # Run the server app.run(port=8000) diff --git a/src/codegen/extensions/events/linear.py b/src/codegen/extensions/events/linear.py index d0cdbbc6c..7c3bdf8db 100644 --- a/src/codegen/extensions/events/linear.py +++ b/src/codegen/extensions/events/linear.py @@ -4,6 +4,7 @@ from pydantic import BaseModel from codegen.extensions.events.interface import EventHandlerManagerProtocol +from codegen.extensions.linear.types import LinearEvent logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -29,7 +30,7 @@ def event(self, event_name: str): """ logger.info(f"[EVENT] Registering handler for {event_name}") - def register_handler(func: Callable[[T], Any]): + def register_handler(func: Callable[[LinearEvent], Any]): func_name = func.__qualname__ logger.info(f"[EVENT] Registering function {func_name} for {event_name}") @@ -40,7 +41,9 @@ def new_func(raw_event: dict): logger.info(f"[HANDLER] Event type mismatch: expected {event_name}, got {event_type}") return None - return func(raw_event) + # Parse event into LinearEvent type + event = LinearEvent.model_validate(raw_event) + return func(event) self.registered_handlers[event_name] = new_func return func diff --git a/src/codegen/extensions/linear/linear_client.py b/src/codegen/extensions/linear/linear_client.py index e27867dca..37291b806 100644 --- a/src/codegen/extensions/linear/linear_client.py +++ b/src/codegen/extensions/linear/linear_client.py @@ -4,37 +4,10 @@ from typing import Optional import requests -from pydantic import BaseModel - -logger = logging.getLogger(__name__) - - -# --- TYPES - - -class LinearUser(BaseModel): - id: str - name: str +from codegen.extensions.linear.types import LinearComment, LinearIssue, LinearTeam, LinearUser -class LinearTeam(BaseModel): - """Represents a Linear team.""" - - id: str - name: str - key: str - - -class LinearComment(BaseModel): - id: str - body: str - user: LinearUser | None = None - - -class LinearIssue(BaseModel): - id: str - title: str - description: str | None = None +logger = logging.getLogger(__name__) class LinearClient: diff --git a/src/codegen/extensions/linear/types.py b/src/codegen/extensions/linear/types.py new file mode 100644 index 000000000..fb9439399 --- /dev/null +++ b/src/codegen/extensions/linear/types.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel + + +class LinearUser(BaseModel): + id: str + name: str + + +class LinearTeam(BaseModel): + """Represents a Linear team.""" + + id: str + name: str + key: str + + +class LinearComment(BaseModel): + id: str + body: str + user: LinearUser | None = None + + +class LinearIssue(BaseModel): + id: str + title: str + description: str | None = None + priority: int | None = None + team_id: str | None = None + + +class LinearEvent(BaseModel): + """Represents a Linear webhook event.""" + + action: str # e.g. "create", "update", "remove" + type: str # e.g. "Issue", "Comment", "Project" + data: LinearIssue | LinearComment # The actual event data + url: str # URL to the resource in Linear + created_at: str | None = None # ISO timestamp + organization_id: str | None = None + team_id: str | None = None diff --git a/tests/integration/extension/codegen_app/test_handler_logic.py b/tests/integration/extension/codegen_app/test_handler_logic.py index c3cfa708c..3571b7339 100644 --- a/tests/integration/extension/codegen_app/test_handler_logic.py +++ b/tests/integration/extension/codegen_app/test_handler_logic.py @@ -8,6 +8,7 @@ from codegen.extensions.events.client import CodegenClient from codegen.extensions.events.codegen_app import CodegenApp from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent +from codegen.extensions.linear.types import LinearEvent from codegen.extensions.slack.types import SlackEvent @@ -38,11 +39,11 @@ def handle_labeled(event: PullRequestLabeledEvent): # Register Linear handler @app.linear.event("Issue") - def handle_issue_created(event: dict): + def handle_issue_created(event: LinearEvent): return { "message": "Issue created", - "issue_id": event["data"]["id"], - "title": event["data"]["title"], + "issue_id": event.data.id, + "title": event.data.title, } return app diff --git a/tests/integration/extension/codegen_app/test_server.ipynb b/tests/integration/extension/codegen_app/test_server.ipynb index 258a15fb9..3eb27f9ae 100644 --- a/tests/integration/extension/codegen_app/test_server.ipynb +++ b/tests/integration/extension/codegen_app/test_server.ipynb @@ -27,11 +27,32 @@ "metadata": {}, "outputs": [], "source": [ - "client = CodegenClient()\n", - "\n", + "client = CodegenClient()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Slack" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "response = await client.send_slack_message(text=\"<@U123BOT> help me with this code\", channel=\"C123TEST\", event_type=\"app_mention\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Github" + ] + }, { "cell_type": "code", "execution_count": null, @@ -82,6 +103,37 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Linear" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create test issue created payload\n", + "payload = {\n", + " \"action\": \"create\",\n", + " \"type\": \"Issue\",\n", + " \"data\": {\n", + " \"id\": \"abc-123\",\n", + " \"title\": \"Test Issue\",\n", + " \"description\": \"This is a test issue\",\n", + " \"priority\": 1,\n", + " \"teamId\": \"team-123\",\n", + " },\n", + " \"url\": \"https://linear.app/company/issue/ABC-123\",\n", + "}\n", + "\n", + "# Send test event\n", + "response = await client.send_linear_event(payload=payload)" + ] + }, { "cell_type": "code", "execution_count": null, From df5d37df4a04dbc252bd627f3279121cfdb15cf7 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 14:30:41 -0800 Subject: [PATCH 08/15] . --- app.py | 14 ++++++++++++-- src/codegen/__init__.py | 3 ++- .../extension/codegen_app/test_server.ipynb | 11 ++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index ed0841c27..74b2f7be1 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ import logging -from codegen.extensions.events.codegen_app import CodegenApp +from codegen import CodeAgent, CodegenApp from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent from codegen.extensions.linear.types import LinearEvent from codegen.extensions.slack.types import SlackEvent @@ -17,8 +17,18 @@ async def handle_mention(event: SlackEvent): logger.info("[APP_MENTION] Received app_mention event") logger.info(event) + + # Codebase + logger.info("[CODEBASE] Initializing codebase") codebase = app.get_codebase("fastapi/fastapi") - return {"num_files": len(codebase.files), "num_functions": len(codebase.functions)} + + # Code Agent + logger.info("[CODE_AGENT] Initializing code agent") + agent = CodeAgent(codebase=codebase) + + logger.info("[CODE_AGENT] Running code agent") + response = agent.run(event.text) + return {"message": "Mentioned", "received_text": event.text, "response": response} @app.github.event("pull_request:labeled") diff --git a/src/codegen/__init__.py b/src/codegen/__init__.py index 7f37ec124..1b9b91d17 100644 --- a/src/codegen/__init__.py +++ b/src/codegen/__init__.py @@ -1,10 +1,11 @@ from codegen.agents.code_agent import CodeAgent from codegen.cli.sdk.decorator import function from codegen.cli.sdk.functions import Function +from codegen.extensions.events.codegen_app import CodegenApp # from codegen.extensions.index.file_index import FileIndex # from codegen.extensions.langchain.agent import create_agent_with_tools, create_codebase_agent from codegen.sdk.core.codebase import Codebase from codegen.shared.enums.programming_language import ProgrammingLanguage -__all__ = ["CodeAgent", "Codebase", "Function", "ProgrammingLanguage", "function"] +__all__ = ["CodeAgent", "Codebase", "CodegenApp", "Function", "ProgrammingLanguage", "function"] diff --git a/tests/integration/extension/codegen_app/test_server.ipynb b/tests/integration/extension/codegen_app/test_server.ipynb index 3eb27f9ae..aa33c6f24 100644 --- a/tests/integration/extension/codegen_app/test_server.ipynb +++ b/tests/integration/extension/codegen_app/test_server.ipynb @@ -43,7 +43,16 @@ "metadata": {}, "outputs": [], "source": [ - "response = await client.send_slack_message(text=\"<@U123BOT> help me with this code\", channel=\"C123TEST\", event_type=\"app_mention\")" + "response = await client.send_slack_message(text=\"Put an emoji on top of the main README.md\", channel=\"C123TEST\", event_type=\"app_mention\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response" ] }, { From 3af00787d491831d2928fa5fe9bea3faf06be725 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 15:00:11 -0800 Subject: [PATCH 09/15] . --- app.py | 2 + src/codegen/cli/commands/serve/main.py | 104 ++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/app.py b/app.py index 74b2f7be1..d3470bd53 100644 --- a/app.py +++ b/app.py @@ -28,6 +28,8 @@ async def handle_mention(event: SlackEvent): logger.info("[CODE_AGENT] Running code agent") response = agent.run(event.text) + + app.slack.client.chat_postMessage(channel=event.channel, text=response, thread_ts=event.ts) return {"message": "Mentioned", "received_text": event.text, "response": response} diff --git a/src/codegen/cli/commands/serve/main.py b/src/codegen/cli/commands/serve/main.py index 42ee3bc92..3b8ff3eee 100644 --- a/src/codegen/cli/commands/serve/main.py +++ b/src/codegen/cli/commands/serve/main.py @@ -1,7 +1,10 @@ import importlib.util import logging +import socket +import subprocess import sys from pathlib import Path +from typing import Optional import rich import rich_click as click @@ -11,6 +14,8 @@ from codegen.extensions.events.codegen_app import CodegenApp +logger = logging.getLogger(__name__) + def setup_logging(debug: bool): """Configure rich logging with colors.""" @@ -83,12 +88,64 @@ def create_app_module(file_path: Path) -> str: return f"{module_name}:app" +def start_ngrok(port: int) -> Optional[str]: + """Start ngrok and return the public URL""" + try: + import requests + + # Start ngrok + process = subprocess.Popen(["ngrok", "http", str(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Wait a moment for ngrok to start + import time + + time.sleep(2) + + # Get the public URL from ngrok's API + try: + response = requests.get("http://localhost:4040/api/tunnels") + data = response.json() + + # Get the first https URL + for tunnel in data["tunnels"]: + if tunnel["proto"] == "https": + return tunnel["public_url"] + + logger.warning("No HTTPS tunnel found") + return None + + except requests.RequestException: + logger.exception("Failed to get ngrok URL from API") + logger.info("Get your public URL from: http://localhost:4040") + return None + + except FileNotFoundError: + logger.exception("ngrok not found. Please install it first: https://ngrok.com/download") + return None + + +def find_available_port(start_port: int = 8000, max_tries: int = 100) -> int: + """Find an available port starting from start_port.""" + for port in range(start_port, start_port + max_tries): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", port)) + return port + except OSError: + continue + msg = f"No available ports found between {start_port} and {start_port + max_tries}" + raise click.ClickException(msg) + + @click.command(name="serve") @click.argument("file", type=click.Path(exists=True, path_type=Path)) @click.option("--host", default="127.0.0.1", help="Host to bind to") @click.option("--port", default=8000, help="Port to bind to") @click.option("--debug", is_flag=True, help="Enable debug mode with hot reloading") -def serve_command(file: Path, host: str = "127.0.0.1", port: int = 8000, debug: bool = False): +@click.option("--public", is_flag=True, help="Expose the server publicly using ngrok") +@click.option("--workers", default=1, help="Number of worker processes") +@click.option("--repos", multiple=True, help="GitHub repositories to analyze") +def serve_command(file: Path, host: str = "127.0.0.1", port: int = 8000, debug: bool = False, public: bool = False, workers: int = 4, repos: list[str] = []): """Run a CodegenApp server from a Python file. FILE is the path to a Python file containing a CodegenApp instance @@ -98,33 +155,58 @@ def serve_command(file: Path, host: str = "127.0.0.1", port: int = 8000, debug: try: if debug: - # For debug mode, create a module that uvicorn can reload - app_import_string = create_app_module(file) - reload_dirs = [str(file.parent)] - else: - # For normal mode, load the app directly - app = load_app_from_file(file) - app_import_string = app.app + workers = 1 + + # Find available port if the specified one is in use + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((host, port)) + except OSError: + port = find_available_port(port) + logger.warning(f"Port {port} was in use, using port {port} instead") + + # Always create module for uvicorn + app_import_string = create_app_module(file) + reload_dirs = [str(file.parent)] if debug else None # Print server info rich.print( Panel( - f"[green]Starting CodegenApp server[/green]\n[dim]File:[/dim] {file}\n[dim]URL:[/dim] http://{host}:{port}\n[dim]Debug:[/dim] {'enabled' if debug else 'disabled'}", + f"[green]Starting CodegenApp server[/green]\n" + f"[dim]File:[/dim] {file}\n" + f"[dim]URL:[/dim] http://{host}:{port}\n" + f"[dim]Workers:[/dim] {workers}\n" + f"[dim]Debug:[/dim] {'enabled' if debug else 'disabled'}", title="[bold]Server Info[/bold]", border_style="blue", ) ) - # Run the server + # Start ngrok if --public flag is set + if public: + public_url = start_ngrok(port) + if public_url: + logger.info(f"Public URL: {public_url}") + logger.info("Use these webhook URLs in your integrations:") + logger.info(f" Slack: {public_url}/slack/events") + logger.info(f" GitHub: {public_url}/github/events") + logger.info(f" Linear: {public_url}/linear/events") + + # Run the server with workers uvicorn.run( app_import_string, host=host, port=port, reload=debug, - reload_dirs=reload_dirs if debug else None, + reload_dirs=reload_dirs, log_level="debug" if debug else "info", + workers=workers, ) except Exception as e: msg = f"Server error: {e!s}" raise click.ClickException(msg) + + +if __name__ == "__main__": + serve_command() From 0b6d182658a21bbff55c87459b693b32ae7aa01f Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 15:28:15 -0800 Subject: [PATCH 10/15] . --- src/codegen/extensions/github/types/events/pull_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/extensions/github/types/events/pull_request.py b/src/codegen/extensions/github/types/events/pull_request.py index 3691184b6..4d548bfbd 100644 --- a/src/codegen/extensions/github/types/events/pull_request.py +++ b/src/codegen/extensions/github/types/events/pull_request.py @@ -43,7 +43,7 @@ class PullRequestLabeledEvent(BaseModel): action: Literal["labeled"] number: int - pull_request: SimplePullRequest + pull_request: PullRequest label: Label repository: dict # Simplified for now sender: User From 4d4a700f2d201b26f5adb3625062b33d9e3f453b Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sun, 23 Feb 2025 16:07:46 -0800 Subject: [PATCH 11/15] . --- src/codegen/extensions/events/codegen_app.py | 4 ++++ src/codegen/extensions/events/github.py | 17 +++++++++++------ .../extension/codegen_app/test_server.ipynb | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/codegen/extensions/events/codegen_app.py b/src/codegen/extensions/events/codegen_app.py index d65ea4423..f2e321c1d 100644 --- a/src/codegen/extensions/events/codegen_app.py +++ b/src/codegen/extensions/events/codegen_app.py @@ -16,6 +16,10 @@ class CodegenApp: """A FastAPI-based application for handling various code-related events.""" + github: GitHub + linear: Linear + slack: Slack + def __init__(self, name: str, repos: Optional[list[str]] = None, modal_api_key: Optional[str] = None, tmp_dir: str = "/tmp/codegen"): self.name = name self._modal_api_key = modal_api_key diff --git a/src/codegen/extensions/events/github.py b/src/codegen/extensions/events/github.py index fb3e417bd..f4df1dc66 100644 --- a/src/codegen/extensions/events/github.py +++ b/src/codegen/extensions/events/github.py @@ -1,7 +1,9 @@ import logging +import os from typing import Any, Callable, TypeVar from fastapi import Request +from github import Github from pydantic import BaseModel from codegen.extensions.events.interface import EventHandlerManagerProtocol @@ -20,12 +22,15 @@ def __init__(self, app): self.app = app self.registered_handlers = {} - # TODO - add in client info - # @property - # def client(self) -> Github: - # if not self._client: - # self._client = Github(os.environ["GITHUB_TOKEN"]) - # return self._client + @property + def client(self) -> Github: + if not os.getenv("GITHUB_TOKEN"): + msg = "GITHUB_TOKEN is not set" + logger.exception(msg) + raise ValueError(msg) + if not self._client: + self._client = Github(os.getenv("GITHUB_TOKEN")) + return self._client def unsubscribe_all_handlers(self): logger.info("[HANDLERS] Clearing all handlers") diff --git a/tests/integration/extension/codegen_app/test_server.ipynb b/tests/integration/extension/codegen_app/test_server.ipynb index aa33c6f24..b882894b2 100644 --- a/tests/integration/extension/codegen_app/test_server.ipynb +++ b/tests/integration/extension/codegen_app/test_server.ipynb @@ -27,7 +27,7 @@ "metadata": {}, "outputs": [], "source": [ - "client = CodegenClient()" + "client = CodegenClient(base_url=\"https://codegen-sh-staging--codegen-test-fastapi-app.modal.run\")" ] }, { @@ -43,7 +43,7 @@ "metadata": {}, "outputs": [], "source": [ - "response = await client.send_slack_message(text=\"Put an emoji on top of the main README.md\", channel=\"C123TEST\", event_type=\"app_mention\")" + "response = await client.send_slack_message(text=\"\", channel=\"C123TEST\", event_type=\"app_mention\")" ] }, { From 26dafad2c319968e14b90806d42c6c7aaa627bb0 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Mon, 24 Feb 2025 07:40:09 -0800 Subject: [PATCH 12/15] . --- app.py | 52 --- codegen_app_app.py | 3 - modal_app.py | 50 --- next.js.ipynb | 394 ------------------ src/codegen/extensions/events/modal_app.py | 3 - .../extension/codegen_app/test_slack.py | 0 6 files changed, 502 deletions(-) delete mode 100644 app.py delete mode 100644 codegen_app_app.py delete mode 100644 modal_app.py delete mode 100644 next.js.ipynb delete mode 100644 src/codegen/extensions/events/modal_app.py delete mode 100644 tests/integration/extension/codegen_app/test_slack.py diff --git a/app.py b/app.py deleted file mode 100644 index d3470bd53..000000000 --- a/app.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging - -from codegen import CodeAgent, CodegenApp -from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent -from codegen.extensions.linear.types import LinearEvent -from codegen.extensions.slack.types import SlackEvent - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Create the app -app = CodegenApp(name="codegen-test", repos=["fastapi/fastapi"]) - - -@app.slack.event("app_mention") -async def handle_mention(event: SlackEvent): - logger.info("[APP_MENTION] Received app_mention event") - logger.info(event) - - # Codebase - logger.info("[CODEBASE] Initializing codebase") - codebase = app.get_codebase("fastapi/fastapi") - - # Code Agent - logger.info("[CODE_AGENT] Initializing code agent") - agent = CodeAgent(codebase=codebase) - - logger.info("[CODE_AGENT] Running code agent") - response = agent.run(event.text) - - app.slack.client.chat_postMessage(channel=event.channel, text=response, thread_ts=event.ts) - return {"message": "Mentioned", "received_text": event.text, "response": response} - - -@app.github.event("pull_request:labeled") -def handle_pr(event: PullRequestLabeledEvent): - logger.info(f"PR labeled: {event}") - codebase = app.get_codebase("fastapi/fastapi") - return {"message": "PR event handled", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} - - -@app.linear.event("Issue") -def handle_issue(event: LinearEvent): - logger.info(f"Issue created: {event}") - codebase = app.get_codebase("fastapi/fastapi") - return {"message": "Linear Issue event", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} - - -if __name__ == "__main__": - # Run the server - app.run(port=8000) diff --git a/codegen_app_app.py b/codegen_app_app.py deleted file mode 100644 index 9ef9edf98..000000000 --- a/codegen_app_app.py +++ /dev/null @@ -1,3 +0,0 @@ -from app import app - -app = app.app diff --git a/modal_app.py b/modal_app.py deleted file mode 100644 index ffe74e53e..000000000 --- a/modal_app.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging - -import modal - -from codegen.extensions.events.codegen_app import CodegenApp -from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent -from codegen.extensions.slack.types import SlackEvent - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Create Modal image with dependencies -image = modal.Image.debian_slim().pip_install( - "fastapi[standard]", - "uvicorn", - "slack_sdk", - "pydantic", - "rich", - "httpx", -) - -# Create Modal app -app = modal.App(name="codegen-app", image=image) - -# Create CodegenApp instance -cg_app = CodegenApp(name="codegen-test", repos=["fastapi/fastapi"]) - - -@cg_app.slack.event("app_mention") -async def handle_mention(event: SlackEvent): - logger.info("[APP_MENTION] Received app_mention event") - logger.info(event) - codebase = cg_app.get_codebase("fastapi/fastapi") - return {"num_files": len(codebase.files), "num_functions": len(codebase.functions)} - - -@cg_app.github.event("pull_request:labeled") -def handle_pr(event: PullRequestLabeledEvent): - logger.info(f"PR labeled: {event}") - codebase = cg_app.get_codebase("fastapi/fastapi") - return {"message": "PR event handled", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} - - -# Create Modal ASGI app -@app.function(secrets=[modal.Secret.from_dotenv()], image=image) -@modal.asgi_app() -def fastapi_app(): - # Return the FastAPI app - return app.app diff --git a/next.js.ipynb b/next.js.ipynb deleted file mode 100644 index 517b782d1..000000000 --- a/next.js.ipynb +++ /dev/null @@ -1,394 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Codegen Next.js Demo\n", - "\n", - "Based on our [Slack conversation](https://thecodegen.slack.com/archives/C07HXT5NN75/p1739994947429139?thread_ts=1738268741.592289&cid=C07HXT5NN75)\n", - "\n", - "\n", - "## Step 1: Parse codebase\n", - "- This takes ~2.5 minutes without memory snapshotting\n", - "- Pulls the repo into `/tmp/codegen/next.js" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1", - "metadata": {}, - "outputs": [], - "source": [ - "from codegen import Codebase\n", - "\n", - "# Takes ~2.5 minutes\n", - "codebase = Codebase.from_repo(\"vercel/next.js\")" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "# Step 2: Flag Issues\n", - "\n", - "Based on our [Slack conversation](https://thecodegen.slack.com/archives/C07HXT5NN75/p1739994947429139?thread_ts=1738268741.592289&cid=C07HXT5NN75), this will flag + link all imports that:\n", - "- import *from* `DIR_NAME` directly\n", - "- the import is *outside* of DIRNAME\n", - "\n", - "This uses the [Files and Directories](https://docs.codegen.com/building-with-codegen/files-and-directories#files-and-directories) APIs, as well as [Imports](https://docs.codegen.com/building-with-codegen/imports#the-import-api)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3", - "metadata": {}, - "outputs": [], - "source": [ - "from codegen.sdk.core.detached_symbols.code_block import CodeBlock" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4", - "metadata": {}, - "outputs": [], - "source": [ - "from codegen.sdk.core.detached_symbols.code_block import CodeBlock\n", - "\n", - "\n", - "def is_env_conditional(statement):\n", - " if not isinstance(statement, IfBlockStatement):\n", - " return False\n", - " return \"process.env\" in statement.condition.source\n", - "\n", - "\n", - "def get_all_statements(block: CodeBlock):\n", - " statements = []\n", - " for statement in block.statements:\n", - " statements.append(statement)\n", - " if hasattr(statement, \"code_block\"):\n", - " statements += get_all_statements(statement.code_block)\n", - " return statements" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5", - "metadata": {}, - "outputs": [], - "source": [ - "DIR_NAME = \"packages/next/src/\"\n", - "directory = codebase.get_directory(DIR_NAME)\n", - "\n", - "for file in directory.files:\n", - " env_conditionals = [s for s in get_all_statements(file.code_block) if is_env_conditional(s)]\n", - " for c in env_conditionals:\n", - " print(c.github_url)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "raw", - "id": "7", - "metadata": {}, - "source": [ - "file = codebase.get_file('packages/next/src/server/web/sandbox/context.ts')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8", - "metadata": {}, - "outputs": [], - "source": [ - "env_conditionals = [s for s in get_all_statements(file.code_block) if is_env_conditional(s)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10", - "metadata": {}, - "outputs": [], - "source": [ - "file.github_url" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "11", - "metadata": {}, - "outputs": [], - "source": [ - "from codegen.sdk.core.statements.if_block_statement import IfBlockStatement\n", - "\n", - "DIR_NAME = \"packages/next/src/client/components/react-dev-overlay\"\n", - "directory = codebase.get_directory(DIR_NAME)\n", - "\n", - "\n", - "def is_env_conditional(statement):\n", - " if not isinstance(statement, IfBlockStatement):\n", - " return False\n", - " return \"process.env\" in statement.condition.source\n", - "\n", - "\n", - "for file in directory.files:\n", - " for imp in file.inbound_imports:\n", - " if imp.file not in directory:\n", - " # Grab process.env statements\n", - " file = imp.to_file\n", - " statements = get_all_statements(file.code_block.statements)\n", - " conditionals = [x for x in statements if is_env_conditional(x)]\n", - "\n", - " # See if the import is beneath one of the process.env statementss\n", - " if not any(imp.source in c.source for c in conditionals):\n", - " continue\n", - "\n", - " print(f\"❌ Detected bad import:\")\n", - " print(f\" ⚛️ File: {imp.file.filepath}\")\n", - " print(f\" ⬇️ Imported symbol: {imp.name}\")\n", - " print(f\" 🔗 Link: {imp.github_url}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "12", - "metadata": {}, - "outputs": [], - "source": [ - "DIR_NAME = \"packages/next/src\"\n", - "directory = codebase.get_directory(DIR_NAME)\n", - "\n", - "\n", - "for file in directory.files:\n", - " # Grab process.env statements\n", - " statements = file.code_block.statements\n", - " conditionals = [x for x in statements if is_env_conditional(x)]\n", - "\n", - " for imp in file.imports:\n", - " # See if the import is beneath one of the process.env statementss\n", - " if not any(imp.source in c.source for c in conditionals):\n", - " continue\n", - "\n", - " print(f\"❌ Detected bad import:\")\n", - " print(f\" ⚛️ File: {imp.file.filepath}\")\n", - " print(f\" ⬇️ Imported symbol: {imp.name}\")\n", - " print(f\" 🔗 Link: {imp.github_url}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "13", - "metadata": {}, - "outputs": [], - "source": [ - "for statement in imp.to_file.code_block.statements:\n", - " if isinstance(statement, IfBlockStatement):\n", - " print(statement.source)\n", - " print(statement.github_url)\n", - " raise Exception" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14", - "metadata": {}, - "outputs": [], - "source": [ - "def is_env_conditional(statement):\n", - " if not isinstance(statement, IfBlockStatement):\n", - " return False\n", - " return \"process.env.NODE_ENV\" in statement.condition.source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "15", - "metadata": {}, - "outputs": [], - "source": [ - "file = imp.to_file\n", - "statements = file.code_block.statements\n", - "conditionals = [x for x in statements if is_env_conditional(x)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16", - "metadata": {}, - "outputs": [], - "source": [ - "len(file.code_block.statements)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17", - "metadata": {}, - "outputs": [], - "source": [ - "file = imp.to_file" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "18", - "metadata": {}, - "outputs": [], - "source": [ - "for s in statements:\n", - " print(type(s))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "19", - "metadata": {}, - "outputs": [], - "source": [ - "for statement in statements:\n", - " if \"process.env.NODE_ENV\" in statement.source:\n", - " print(statement.source)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "20", - "metadata": {}, - "outputs": [], - "source": [ - "for statement in statements:\n", - " if isinstance(statement, IfBlockStatement):\n", - " print(statement.source)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "21", - "metadata": {}, - "outputs": [], - "source": [ - "conditionals" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "22", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "23", - "metadata": {}, - "outputs": [], - "source": [ - "statement.condition.source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25", - "metadata": {}, - "outputs": [], - "source": [ - "imp.is_wrapped_in(IfBlockStatement)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26", - "metadata": {}, - "outputs": [], - "source": [ - "imp.github_url" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "27", - "metadata": {}, - "outputs": [], - "source": [ - "imp.to_file.filepath" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/codegen/extensions/events/modal_app.py b/src/codegen/extensions/events/modal_app.py deleted file mode 100644 index f39e0586c..000000000 --- a/src/codegen/extensions/events/modal_app.py +++ /dev/null @@ -1,3 +0,0 @@ -import modal - -app = modal.App(name="codegen-events") diff --git a/tests/integration/extension/codegen_app/test_slack.py b/tests/integration/extension/codegen_app/test_slack.py deleted file mode 100644 index e69de29bb..000000000 From 6e3cad8027e2f085d3e1dd1ff5b483537491f619 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Mon, 24 Feb 2025 08:19:50 -0800 Subject: [PATCH 13/15] . --- .../examples/codegen_app/README.md | 33 ++++++ codegen-examples/examples/codegen_app/app.py | 100 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 codegen-examples/examples/codegen_app/README.md create mode 100644 codegen-examples/examples/codegen_app/app.py diff --git a/codegen-examples/examples/codegen_app/README.md b/codegen-examples/examples/codegen_app/README.md new file mode 100644 index 000000000..fda3c73d4 --- /dev/null +++ b/codegen-examples/examples/codegen_app/README.md @@ -0,0 +1,33 @@ +# Codegen App + +Simple example of running a codegen app. + +## Run Locally + +Spin up the server: + +``` +codegen serve +``` + +Spin up ngrok + +``` +ngrok http 8000 +``` + +Go to Slack [app settings](https://api.slack.com/apps/A08CR9HUJ3W/event-subscriptions) and set the URL for event subscriptions + +``` +{ngrok_url}/slack/events +``` + +## Deploy to Modal + +This will deploy it as a function + +``` +modal deploy app.py +``` + +Then you can swap in the modal URL for slack etc. diff --git a/codegen-examples/examples/codegen_app/app.py b/codegen-examples/examples/codegen_app/app.py new file mode 100644 index 000000000..f98b41a92 --- /dev/null +++ b/codegen-examples/examples/codegen_app/app.py @@ -0,0 +1,100 @@ +import logging + +import modal +from codegen import CodeAgent, CodegenApp +from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent +from codegen.extensions.linear.types import LinearEvent +from codegen.extensions.slack.types import SlackEvent +from codegen.extensions.tools.github.create_pr_comment import create_pr_comment + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +######################################################################################################################## +# EVENTS +######################################################################################################################## + +# Create the cg_app +cg = CodegenApp(name="codegen-test", repos=["codegen-sh/Kevin-s-Adventure-Game"]) + + +@cg.slack.event("app_mention") +async def handle_mention(event: SlackEvent): + logger.info("[APP_MENTION] Received cg_app_mention event") + logger.info(event) + + # Codebase + logger.info("[CODEBASE] Initializing codebase") + codebase = cg.get_codebase("codegen-sh/Kevin-s-Adventure-Game") + + # Code Agent + logger.info("[CODE_AGENT] Initializing code agent") + agent = CodeAgent(codebase=codebase) + + logger.info("[CODE_AGENT] Running code agent") + response = agent.run(event.text) + + cg.slack.client.chat_postMessage(channel=event.channel, text=response, thread_ts=event.ts) + return {"message": "Mentioned", "received_text": event.text, "response": response} + + +@cg.github.event("pull_request:labeled") +def handle_pr(event: PullRequestLabeledEvent): + logger.info("PR labeled") + logger.info(f"PR head sha: {event.pull_request.head.sha}") + codebase = cg.get_codebase("codegen-sh/Kevin-s-Adventure-Game") + + # =====[ Check out commit ]===== + # Might require fetch? + logger.info("> Checking out commit") + codebase.checkout(commit=event.pull_request.head.sha) + + logger.info("> Getting files") + file = codebase.get_file("README.md") + + # =====[ Create PR comment ]===== + create_pr_comment(codebase, event.pull_request.number, f"File content:\n```python\n{file.content}\n```") + + return {"message": "PR event handled", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} + + +@cg.linear.event("Issue") +def handle_issue(event: LinearEvent): + logger.info(f"Issue created: {event}") + codebase = cg.get_codebase("codegen-sh/Kevin-s-Adventure-Game") + return {"message": "Linear Issue event", "num_files": len(codebase.files), "num_functions": len(codebase.functions)} + + +######################################################################################################################## +# MODAL DEPLOYMENT +######################################################################################################################## +# This deploys the FastAPI app to Modal +# TODO: link this up with memory snapshotting. + +# For deploying local package +REPO_URL = "https://github.com/codegen-sh/codegen-sdk.git" +COMMIT_ID = "26dafad2c319968e14b90806d42c6c7aaa627bb0" + +# Create the base image with dependencies +base_image = ( + modal.Image.debian_slim(python_version="3.13") + .apt_install("git") + .pip_install( + # =====[ Codegen ]===== + # "codegen", + f"git+{REPO_URL}@{COMMIT_ID}", + # =====[ Rest ]===== + "openai>=1.1.0", + "fastapi[standard]", + "slack_sdk", + ) +) + +app = modal.App("codegen-test") + + +@app.function(image=base_image) +@modal.asgi_app() +def fastapi_app(): + return cg.app From 48ca7f64bb79a041d544fc736c35856d3337b9ac Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Mon, 24 Feb 2025 08:25:15 -0800 Subject: [PATCH 14/15] . --- codegen-examples/examples/codegen_app/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codegen-examples/examples/codegen_app/app.py b/codegen-examples/examples/codegen_app/app.py index f98b41a92..bbf61909a 100644 --- a/codegen-examples/examples/codegen_app/app.py +++ b/codegen-examples/examples/codegen_app/app.py @@ -94,7 +94,7 @@ def handle_issue(event: LinearEvent): app = modal.App("codegen-test") -@app.function(image=base_image) +@app.function(image=base_image, secrets=[modal.Secret.from_dotenv()]) @modal.asgi_app() def fastapi_app(): return cg.app From 4305a9263bb436fdc9f94a5a7d091c517ed15955 Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Mon, 24 Feb 2025 12:47:52 -0800 Subject: [PATCH 15/15] fixed label --- src/codegen/extensions/github/types/events/pull_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/codegen/extensions/github/types/events/pull_request.py b/src/codegen/extensions/github/types/events/pull_request.py index 8fd05efe8..7838e8e87 100644 --- a/src/codegen/extensions/github/types/events/pull_request.py +++ b/src/codegen/extensions/github/types/events/pull_request.py @@ -5,6 +5,7 @@ from ..base import GitHubRepository, GitHubUser from ..enterprise import GitHubEnterprise from ..installation import GitHubInstallation +from ..label import GitHubLabel from ..organization import GitHubOrganization from ..pull_request import PullRequest