Skip to content

Commit 658fef8

Browse files
committed
Adds Codex Playground feature
Implements a "Codex Playground" feature that allows users to run natural language coding tasks in a sandboxed GitHub Actions environment. This involves: - Creating a new GitHub Actions workflow (`codex-playground.yml`) that executes the specified task. - Adding API endpoints to start tasks, check task status, and retrieve logs. - Implementing a new UI component (PlaygroundModal) to interact with the new endpoints. - Adds copilot setup steps to ease the integration with Copilot. - Adds general guidelines for Copilot.
1 parent 8aa30f2 commit 658fef8

File tree

11 files changed

+752
-9
lines changed

11 files changed

+752
-9
lines changed

.github/copilot-instructions.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copilot Instructions for agunblock Repository
2+
3+
## General Guidelines
4+
- Follow the repository's existing coding style and conventions.
5+
- Prefer TypeScript or JavaScript for new code unless otherwise specified.
6+
- Use Node.js 20 features where appropriate.
7+
- Always use `npm ci` for installing dependencies in CI environments.
8+
- Ensure compatibility with GitHub Actions workflows as defined in `.github/workflows/`.
9+
10+
## GitHub Actions
11+
- Use `actions/checkout@v4` for checking out code.
12+
- Use `actions/setup-node@v4` with `node-version: "20"` and `cache: "npm"` for Node.js setup.
13+
- Keep workflow permissions minimal, e.g., `contents: read` unless more is required.
14+
- Name setup jobs as `copilot-setup-steps` for Copilot compatibility.
15+
16+
## Code Quality
17+
- Write modular, reusable functions.
18+
- Add comments for complex logic.
19+
- Prefer async/await for asynchronous code.
20+
- Use environment variables for secrets and configuration.
21+
22+
## Pull Requests & Commits
23+
- Reference related issues in commit messages and PR descriptions.
24+
- Ensure all workflows pass before merging.
25+
26+
## Dependency Management
27+
- Use `npm ci` for clean, reproducible installs.
28+
- Do not commit `node_modules` or other generated files.
29+
30+
## Security
31+
- Do not hardcode secrets or credentials.
32+
- Use GitHub Actions secrets for sensitive data.
33+
34+
## Documentation
35+
- Update relevant documentation for any new features or changes.
36+
- Use Markdown for documentation files.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Codex Playground Task
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
prompt:
6+
description: "Natural-language task for Codex"
7+
required: true
8+
task_id:
9+
description: "Unique task identifier"
10+
required: true
11+
azure_openai_endpoint:
12+
description: "Azure OpenAI endpoint"
13+
required: true
14+
azure_openai_key:
15+
description: "Azure OpenAI API key"
16+
required: true
17+
azure_openai_deployment:
18+
description: "Azure OpenAI deployment name"
19+
required: true
20+
default: "o4-mini"
21+
22+
jobs:
23+
codex-task:
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 30
26+
if: false # This ensures the workflow never runs
27+
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v4
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: "Copilot Setup Steps"
2+
3+
# Automatically run the setup steps when they are changed to allow for easy validation, and
4+
# allow manual testing through the repository's "Actions" tab
5+
on:
6+
workflow_dispatch:
7+
push:
8+
paths:
9+
- .github/workflows/copilot-setup-steps.yml
10+
pull_request:
11+
paths:
12+
- .github/workflows/copilot-setup-steps.yml
13+
14+
jobs:
15+
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
16+
copilot-setup-steps:
17+
runs-on: ubuntu-latest
18+
19+
# Set the permissions to the lowest permissions possible needed for your steps.
20+
# Copilot will be given its own token for its operations.
21+
permissions:
22+
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
23+
contents: read
24+
25+
# You can define any steps you want, and they will run before the agent starts.
26+
# If you do not check out your code, Copilot will do this for you.
27+
steps:
28+
- name: Checkout code
29+
uses: actions/checkout@v4
30+
31+
- name: Set up Node.js
32+
uses: actions/setup-node@v4
33+
with:
34+
node-version: "20"
35+
cache: "npm"
36+
37+
- name: Install JavaScript dependencies
38+
run: npm ci

backend/app/main.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import json
55
import asyncio
66
import os
7-
from .models.schemas import RepositoryAnalysisRequest, RepositoryAnalysisResponse, RepositoryInfoResponse, AnalysisProgressUpdate, TaskBreakdownRequest, TaskBreakdownResponse, Task, DevinSessionRequest, DevinSessionResponse
7+
from .models.schemas import RepositoryAnalysisRequest, RepositoryAnalysisResponse, RepositoryInfoResponse, AnalysisProgressUpdate, TaskBreakdownRequest, TaskBreakdownResponse, Task, DevinSessionRequest, DevinSessionResponse, CodexPlaygroundRequest, CodexPlaygroundResponse, PlaygroundTaskStatus, RunnerTokenRequest
88
from .services.github import GitHubService
99
from .services.agent import AzureAgentService
1010
from .config import CORS_ORIGINS
@@ -340,3 +340,96 @@ async def create_devin_session(request: DevinSessionRequest):
340340
except Exception as e:
341341
logger.error(f"Error creating Devin session: {str(e)}", exc_info=True)
342342
raise HTTPException(status_code=500, detail=f"Failed to create Devin session: {str(e)}")
343+
344+
@app.post("/api/playground/start")
345+
async def start_playground_task(
346+
request: CodexPlaygroundRequest,
347+
github_service: GitHubService = Depends(get_github_service)
348+
):
349+
"""Start a new Codex playground task in GitHub Actions."""
350+
try:
351+
import uuid
352+
task_id = str(uuid.uuid4())
353+
354+
workflow_inputs = {
355+
"prompt": request.prompt,
356+
"task_id": task_id,
357+
"azure_openai_endpoint": request.azure_openai_endpoint,
358+
"azure_openai_key": request.azure_openai_key,
359+
"azure_openai_deployment": request.azure_openai_deployment
360+
}
361+
362+
success = await github_service.dispatch_workflow(
363+
owner=request.owner,
364+
repo=request.repo,
365+
workflow_id="codex-playground.yml",
366+
inputs=workflow_inputs
367+
)
368+
369+
if not success:
370+
raise HTTPException(status_code=500, detail="Failed to dispatch workflow")
371+
372+
return CodexPlaygroundResponse(
373+
task_id=task_id,
374+
status="queued"
375+
)
376+
except Exception as e:
377+
logger.error(f"Error starting playground task: {str(e)}")
378+
raise HTTPException(status_code=500, detail=str(e))
379+
380+
@app.get("/api/playground/status/{task_id}")
381+
async def get_playground_status(
382+
task_id: str,
383+
owner: str,
384+
repo: str,
385+
github_service: GitHubService = Depends(get_github_service)
386+
):
387+
"""Get status of a playground task."""
388+
try:
389+
runs = await github_service.get_workflow_runs(owner, repo, "codex-playground.yml")
390+
391+
matching_run = None
392+
for run in runs:
393+
if task_id in str(run.get("html_url", "")):
394+
matching_run = run
395+
break
396+
397+
if not matching_run:
398+
return PlaygroundTaskStatus(task_id=task_id, status="not_found")
399+
400+
return PlaygroundTaskStatus(
401+
task_id=task_id,
402+
status=matching_run["status"],
403+
workflow_run_id=matching_run["id"],
404+
logs_url=matching_run["html_url"]
405+
)
406+
except Exception as e:
407+
logger.error(f"Error getting playground status: {str(e)}")
408+
raise HTTPException(status_code=500, detail=str(e))
409+
410+
@app.get("/api/playground/logs/{task_id}")
411+
async def get_playground_logs(
412+
task_id: str,
413+
owner: str,
414+
repo: str,
415+
github_service: GitHubService = Depends(get_github_service)
416+
):
417+
"""Get logs URL for a playground task."""
418+
try:
419+
runs = await github_service.get_workflow_runs(owner, repo, "codex-playground.yml")
420+
421+
matching_run = None
422+
for run in runs:
423+
if task_id in str(run.get("html_url", "")):
424+
matching_run = run
425+
break
426+
427+
if not matching_run:
428+
raise HTTPException(status_code=404, detail="Task not found")
429+
430+
logs_url = await github_service.get_workflow_logs(owner, repo, matching_run["id"])
431+
432+
return {"logs_url": logs_url, "workflow_url": matching_run["html_url"]}
433+
except Exception as e:
434+
logger.error(f"Error getting playground logs: {str(e)}")
435+
raise HTTPException(status_code=500, detail=str(e))

backend/app/models/schemas.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,29 @@ class DevinSessionRequest(BaseModel):
6363
class DevinSessionResponse(BaseModel):
6464
session_id: str
6565
session_url: str
66+
67+
class CodexPlaygroundRequest(BaseModel):
68+
owner: str
69+
repo: str
70+
prompt: str
71+
azure_openai_endpoint: str
72+
azure_openai_key: str
73+
azure_openai_deployment: str = "gpt-4o"
74+
75+
class CodexPlaygroundResponse(BaseModel):
76+
task_id: str
77+
workflow_run_id: Optional[int] = None
78+
runner_name: Optional[str] = None
79+
status: str
80+
81+
class PlaygroundTaskStatus(BaseModel):
82+
task_id: str
83+
status: str
84+
workflow_run_id: Optional[int] = None
85+
logs_url: Optional[str] = None
86+
artifacts_url: Optional[str] = None
87+
error: Optional[str] = None
88+
89+
class RunnerTokenRequest(BaseModel):
90+
owner: str
91+
repo: str

backend/app/services/github.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,63 @@ async def get_repository_snapshot(self, owner: str, repo: str) -> Optional[Dict[
261261
print(f"Detailed error: {repr(e)}")
262262

263263
raise RuntimeError(f"Failed to fetch repository data: {error_message}")
264+
265+
async def create_runner_token(self, owner: str, repo: str) -> Optional[Dict[str, Any]]:
266+
"""Create a registration token for GitHub Actions runners."""
267+
try:
268+
response = gh().rest.actions.create_registration_token_for_repo(
269+
owner=owner, repo=repo
270+
).parsed_data
271+
return {
272+
"token": response.token,
273+
"expires_at": response.expires_at
274+
}
275+
except Exception as e:
276+
print(f"Error creating runner token: {str(e)}")
277+
return None
278+
279+
async def dispatch_workflow(self, owner: str, repo: str, workflow_id: str, inputs: Dict[str, str]) -> Optional[bool]:
280+
"""Dispatch a workflow with given inputs."""
281+
try:
282+
gh().rest.actions.create_workflow_dispatch(
283+
owner=owner,
284+
repo=repo,
285+
workflow_id=workflow_id,
286+
ref="main",
287+
inputs=inputs
288+
)
289+
return True
290+
except Exception as e:
291+
print(f"Error dispatching workflow: {str(e)}")
292+
return False
293+
294+
async def get_workflow_runs(self, owner: str, repo: str, workflow_id: str) -> List[Dict[str, Any]]:
295+
"""Get recent workflow runs for a workflow."""
296+
try:
297+
response = gh().rest.actions.list_workflow_runs(
298+
owner=owner, repo=repo, workflow_id=workflow_id
299+
).parsed_data
300+
return [
301+
{
302+
"id": run.id,
303+
"status": run.status,
304+
"conclusion": run.conclusion,
305+
"created_at": run.created_at,
306+
"html_url": run.html_url
307+
}
308+
for run in response.workflow_runs[:5]
309+
]
310+
except Exception as e:
311+
print(f"Error getting workflow runs: {str(e)}")
312+
return []
313+
314+
async def get_workflow_logs(self, owner: str, repo: str, run_id: int) -> Optional[str]:
315+
"""Get logs for a specific workflow run."""
316+
try:
317+
response = gh().rest.actions.download_workflow_run_logs(
318+
owner=owner, repo=repo, run_id=run_id
319+
)
320+
return response.url if hasattr(response, 'url') else None
321+
except Exception as e:
322+
print(f"Error getting workflow logs: {str(e)}")
323+
return None

0 commit comments

Comments
 (0)