Skip to content

feat: CodegenApp #617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions codegen-examples/examples/codegen_app/README.md
Original file line number Diff line number Diff line change
@@ -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.
100 changes: 100 additions & 0 deletions codegen-examples/examples/codegen_app/app.py
Original file line number Diff line number Diff line change
@@ -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)}

Check failure on line 59 in codegen-examples/examples/codegen_app/app.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "len" has incompatible type overloaded function; expected "Sized" [arg-type]


@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)}

Check failure on line 66 in codegen-examples/examples/codegen_app/app.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "len" has incompatible type overloaded function; expected "Sized" [arg-type]


########################################################################################################################
# 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, secrets=[modal.Secret.from_dotenv()])
@modal.asgi_app()
def fastapi_app():
return cg.app
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
3 changes: 2 additions & 1 deletion src/codegen/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
2 changes: 2 additions & 0 deletions src/codegen/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.start.main import start_command
from codegen.cli.commands.style_debug.main import style_debug_command
from codegen.cli.commands.update.main import update_command
Expand Down Expand Up @@ -48,6 +49,7 @@ def main():
main.add_command(update_command)
main.add_command(config_command)
main.add_command(lsp_command)
main.add_command(serve_command)
main.add_command(start_command)


Expand Down
212 changes: 212 additions & 0 deletions src/codegen/cli/commands/serve/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
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
import uvicorn
from rich.logging import RichHandler
from rich.panel import Panel

from codegen.extensions.events.codegen_app import CodegenApp

logger = logging.getLogger(__name__)


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"


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")
@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
"""
# Configure rich logging
setup_logging(debug)

try:
if debug:
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"
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",
)
)

# 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,
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()
Loading