Skip to content

Commit b0df22e

Browse files
jayhackcodegen-bot
and
codegen-bot
authored
feat: CodegenApp (#617)
Co-authored-by: codegen-bot <[email protected]>
1 parent b23e513 commit b0df22e

File tree

20 files changed

+1509
-180
lines changed

20 files changed

+1509
-180
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Codegen App
2+
3+
Simple example of running a codegen app.
4+
5+
## Run Locally
6+
7+
Spin up the server:
8+
9+
```
10+
codegen serve
11+
```
12+
13+
Spin up ngrok
14+
15+
```
16+
ngrok http 8000
17+
```
18+
19+
Go to Slack [app settings](https://api.slack.com/apps/A08CR9HUJ3W/event-subscriptions) and set the URL for event subscriptions
20+
21+
```
22+
{ngrok_url}/slack/events
23+
```
24+
25+
## Deploy to Modal
26+
27+
This will deploy it as a function
28+
29+
```
30+
modal deploy app.py
31+
```
32+
33+
Then you can swap in the modal URL for slack etc.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import logging
2+
3+
import modal
4+
from codegen import CodeAgent, CodegenApp
5+
from codegen.extensions.github.types.events.pull_request import PullRequestLabeledEvent
6+
from codegen.extensions.linear.types import LinearEvent
7+
from codegen.extensions.slack.types import SlackEvent
8+
from codegen.extensions.tools.github.create_pr_comment import create_pr_comment
9+
10+
# Set up logging
11+
logging.basicConfig(level=logging.INFO)
12+
logger = logging.getLogger(__name__)
13+
14+
########################################################################################################################
15+
# EVENTS
16+
########################################################################################################################
17+
18+
# Create the cg_app
19+
cg = CodegenApp(name="codegen-test", repos=["codegen-sh/Kevin-s-Adventure-Game"])
20+
21+
22+
@cg.slack.event("app_mention")
23+
async def handle_mention(event: SlackEvent):
24+
logger.info("[APP_MENTION] Received cg_app_mention event")
25+
logger.info(event)
26+
27+
# Codebase
28+
logger.info("[CODEBASE] Initializing codebase")
29+
codebase = cg.get_codebase("codegen-sh/Kevin-s-Adventure-Game")
30+
31+
# Code Agent
32+
logger.info("[CODE_AGENT] Initializing code agent")
33+
agent = CodeAgent(codebase=codebase)
34+
35+
logger.info("[CODE_AGENT] Running code agent")
36+
response = agent.run(event.text)
37+
38+
cg.slack.client.chat_postMessage(channel=event.channel, text=response, thread_ts=event.ts)
39+
return {"message": "Mentioned", "received_text": event.text, "response": response}
40+
41+
42+
@cg.github.event("pull_request:labeled")
43+
def handle_pr(event: PullRequestLabeledEvent):
44+
logger.info("PR labeled")
45+
logger.info(f"PR head sha: {event.pull_request.head.sha}")
46+
codebase = cg.get_codebase("codegen-sh/Kevin-s-Adventure-Game")
47+
48+
# =====[ Check out commit ]=====
49+
# Might require fetch?
50+
logger.info("> Checking out commit")
51+
codebase.checkout(commit=event.pull_request.head.sha)
52+
53+
logger.info("> Getting files")
54+
file = codebase.get_file("README.md")
55+
56+
# =====[ Create PR comment ]=====
57+
create_pr_comment(codebase, event.pull_request.number, f"File content:\n```python\n{file.content}\n```")
58+
59+
return {"message": "PR event handled", "num_files": len(codebase.files), "num_functions": len(codebase.functions)}
60+
61+
62+
@cg.linear.event("Issue")
63+
def handle_issue(event: LinearEvent):
64+
logger.info(f"Issue created: {event}")
65+
codebase = cg.get_codebase("codegen-sh/Kevin-s-Adventure-Game")
66+
return {"message": "Linear Issue event", "num_files": len(codebase.files), "num_functions": len(codebase.functions)}
67+
68+
69+
########################################################################################################################
70+
# MODAL DEPLOYMENT
71+
########################################################################################################################
72+
# This deploys the FastAPI app to Modal
73+
# TODO: link this up with memory snapshotting.
74+
75+
# For deploying local package
76+
REPO_URL = "https://github.com/codegen-sh/codegen-sdk.git"
77+
COMMIT_ID = "26dafad2c319968e14b90806d42c6c7aaa627bb0"
78+
79+
# Create the base image with dependencies
80+
base_image = (
81+
modal.Image.debian_slim(python_version="3.13")
82+
.apt_install("git")
83+
.pip_install(
84+
# =====[ Codegen ]=====
85+
# "codegen",
86+
f"git+{REPO_URL}@{COMMIT_ID}",
87+
# =====[ Rest ]=====
88+
"openai>=1.1.0",
89+
"fastapi[standard]",
90+
"slack_sdk",
91+
)
92+
)
93+
94+
app = modal.App("codegen-test")
95+
96+
97+
@app.function(image=base_image, secrets=[modal.Secret.from_dotenv()])
98+
@modal.asgi_app()
99+
def fastapi_app():
100+
return cg.app

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ dependencies = [
7171
"slack-sdk",
7272
"langchain-anthropic>=0.3.7",
7373
"lox>=0.12.0",
74+
"httpx>=0.28.1",
7475
]
7576

7677
license = { text = "Apache-2.0" }

src/codegen/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from codegen.agents.code_agent import CodeAgent
22
from codegen.cli.sdk.decorator import function
33
from codegen.cli.sdk.functions import Function
4+
from codegen.extensions.events.codegen_app import CodegenApp
45

56
# from codegen.extensions.index.file_index import FileIndex
67
# from codegen.extensions.langchain.agent import create_agent_with_tools, create_codebase_agent
78
from codegen.sdk.core.codebase import Codebase
89
from codegen.shared.enums.programming_language import ProgrammingLanguage
910

10-
__all__ = ["CodeAgent", "Codebase", "Function", "ProgrammingLanguage", "function"]
11+
__all__ = ["CodeAgent", "Codebase", "CodegenApp", "Function", "ProgrammingLanguage", "function"]

src/codegen/cli/cli.py

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from codegen.cli.commands.reset.main import reset_command
1717
from codegen.cli.commands.run.main import run_command
1818
from codegen.cli.commands.run_on_pr.main import run_on_pr_command
19+
from codegen.cli.commands.serve.main import serve_command
1920
from codegen.cli.commands.start.main import start_command
2021
from codegen.cli.commands.style_debug.main import style_debug_command
2122
from codegen.cli.commands.update.main import update_command
@@ -48,6 +49,7 @@ def main():
4849
main.add_command(update_command)
4950
main.add_command(config_command)
5051
main.add_command(lsp_command)
52+
main.add_command(serve_command)
5153
main.add_command(start_command)
5254

5355

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import importlib.util
2+
import logging
3+
import socket
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
from typing import Optional
8+
9+
import rich
10+
import rich_click as click
11+
import uvicorn
12+
from rich.logging import RichHandler
13+
from rich.panel import Panel
14+
15+
from codegen.extensions.events.codegen_app import CodegenApp
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
def setup_logging(debug: bool):
21+
"""Configure rich logging with colors."""
22+
logging.basicConfig(
23+
level=logging.DEBUG if debug else logging.INFO,
24+
format="%(message)s",
25+
handlers=[
26+
RichHandler(
27+
rich_tracebacks=True,
28+
tracebacks_show_locals=debug,
29+
markup=True,
30+
show_time=False,
31+
)
32+
],
33+
)
34+
35+
36+
def load_app_from_file(file_path: Path) -> CodegenApp:
37+
"""Load a CodegenApp instance from a Python file.
38+
39+
Args:
40+
file_path: Path to the Python file containing the CodegenApp
41+
42+
Returns:
43+
The CodegenApp instance from the file
44+
45+
Raises:
46+
click.ClickException: If no CodegenApp instance is found
47+
"""
48+
try:
49+
# Import the module from file path
50+
spec = importlib.util.spec_from_file_location("app_module", file_path)
51+
if not spec or not spec.loader:
52+
msg = f"Could not load module from {file_path}"
53+
raise click.ClickException(msg)
54+
55+
module = importlib.util.module_from_spec(spec)
56+
spec.loader.exec_module(module)
57+
58+
# Find CodegenApp instance
59+
for attr_name in dir(module):
60+
attr = getattr(module, attr_name)
61+
if isinstance(attr, CodegenApp):
62+
return attr
63+
64+
msg = f"No CodegenApp instance found in {file_path}"
65+
raise click.ClickException(msg)
66+
67+
except Exception as e:
68+
msg = f"Error loading app from {file_path}: {e!s}"
69+
raise click.ClickException(msg)
70+
71+
72+
def create_app_module(file_path: Path) -> str:
73+
"""Create a temporary module that exports the app for uvicorn."""
74+
# Add the file's directory to Python path
75+
file_dir = str(file_path.parent.absolute())
76+
if file_dir not in sys.path:
77+
sys.path.insert(0, file_dir)
78+
79+
# Create a module that imports and exposes the app
80+
module_name = f"codegen_app_{file_path.stem}"
81+
module_code = f"""
82+
from {file_path.stem} import app
83+
app = app.app
84+
"""
85+
module_path = file_path.parent / f"{module_name}.py"
86+
module_path.write_text(module_code)
87+
88+
return f"{module_name}:app"
89+
90+
91+
def start_ngrok(port: int) -> Optional[str]:
92+
"""Start ngrok and return the public URL"""
93+
try:
94+
import requests
95+
96+
# Start ngrok
97+
process = subprocess.Popen(["ngrok", "http", str(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
98+
99+
# Wait a moment for ngrok to start
100+
import time
101+
102+
time.sleep(2)
103+
104+
# Get the public URL from ngrok's API
105+
try:
106+
response = requests.get("http://localhost:4040/api/tunnels")
107+
data = response.json()
108+
109+
# Get the first https URL
110+
for tunnel in data["tunnels"]:
111+
if tunnel["proto"] == "https":
112+
return tunnel["public_url"]
113+
114+
logger.warning("No HTTPS tunnel found")
115+
return None
116+
117+
except requests.RequestException:
118+
logger.exception("Failed to get ngrok URL from API")
119+
logger.info("Get your public URL from: http://localhost:4040")
120+
return None
121+
122+
except FileNotFoundError:
123+
logger.exception("ngrok not found. Please install it first: https://ngrok.com/download")
124+
return None
125+
126+
127+
def find_available_port(start_port: int = 8000, max_tries: int = 100) -> int:
128+
"""Find an available port starting from start_port."""
129+
for port in range(start_port, start_port + max_tries):
130+
try:
131+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
132+
s.bind(("", port))
133+
return port
134+
except OSError:
135+
continue
136+
msg = f"No available ports found between {start_port} and {start_port + max_tries}"
137+
raise click.ClickException(msg)
138+
139+
140+
@click.command(name="serve")
141+
@click.argument("file", type=click.Path(exists=True, path_type=Path))
142+
@click.option("--host", default="127.0.0.1", help="Host to bind to")
143+
@click.option("--port", default=8000, help="Port to bind to")
144+
@click.option("--debug", is_flag=True, help="Enable debug mode with hot reloading")
145+
@click.option("--public", is_flag=True, help="Expose the server publicly using ngrok")
146+
@click.option("--workers", default=1, help="Number of worker processes")
147+
@click.option("--repos", multiple=True, help="GitHub repositories to analyze")
148+
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] = []):
149+
"""Run a CodegenApp server from a Python file.
150+
151+
FILE is the path to a Python file containing a CodegenApp instance
152+
"""
153+
# Configure rich logging
154+
setup_logging(debug)
155+
156+
try:
157+
if debug:
158+
workers = 1
159+
160+
# Find available port if the specified one is in use
161+
try:
162+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
163+
s.bind((host, port))
164+
except OSError:
165+
port = find_available_port(port)
166+
logger.warning(f"Port {port} was in use, using port {port} instead")
167+
168+
# Always create module for uvicorn
169+
app_import_string = create_app_module(file)
170+
reload_dirs = [str(file.parent)] if debug else None
171+
172+
# Print server info
173+
rich.print(
174+
Panel(
175+
f"[green]Starting CodegenApp server[/green]\n"
176+
f"[dim]File:[/dim] {file}\n"
177+
f"[dim]URL:[/dim] http://{host}:{port}\n"
178+
f"[dim]Workers:[/dim] {workers}\n"
179+
f"[dim]Debug:[/dim] {'enabled' if debug else 'disabled'}",
180+
title="[bold]Server Info[/bold]",
181+
border_style="blue",
182+
)
183+
)
184+
185+
# Start ngrok if --public flag is set
186+
if public:
187+
public_url = start_ngrok(port)
188+
if public_url:
189+
logger.info(f"Public URL: {public_url}")
190+
logger.info("Use these webhook URLs in your integrations:")
191+
logger.info(f" Slack: {public_url}/slack/events")
192+
logger.info(f" GitHub: {public_url}/github/events")
193+
logger.info(f" Linear: {public_url}/linear/events")
194+
195+
# Run the server with workers
196+
uvicorn.run(
197+
app_import_string,
198+
host=host,
199+
port=port,
200+
reload=debug,
201+
reload_dirs=reload_dirs,
202+
log_level="debug" if debug else "info",
203+
workers=workers,
204+
)
205+
206+
except Exception as e:
207+
msg = f"Server error: {e!s}"
208+
raise click.ClickException(msg)
209+
210+
211+
if __name__ == "__main__":
212+
serve_command()

0 commit comments

Comments
 (0)