Skip to content
Draft
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
21 changes: 21 additions & 0 deletions services/hackbot-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM python:3.12-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app
COPY pyproject.toml uv.lock* ./
RUN uv sync --locked --no-dev || uv sync --no-dev

FROM python:3.12-slim

WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY app/ ./app/

ENV PYTHONUNBUFFERED=1
ENV PORT=8080
ENV PATH="/app/.venv/bin:$PATH"

EXPOSE 8080

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
1 change: 1 addition & 0 deletions services/hackbot-api/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
29 changes: 29 additions & 0 deletions services/hackbot-api/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
# Bugzilla
bz_base_url: str = ""
bz_api_key: str = ""

# Firefox source repo (for bug_fix tool)
source_repo: str = "/workspace/firefox"

# Agent
model: str | None = None
max_turns: int | None = None
effort: str | None = None

# Server
port: int = 8080
environment: str = "development"
sentry_dsn: str | None = None

model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
"extra": "ignore",
}


settings = Settings()
50 changes: 50 additions & 0 deletions services/hackbot-api/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import logging
from contextlib import asynccontextmanager

import sentry_sdk
from fastapi import FastAPI

from app import __version__
from app.config import settings
from app.routers import duplicate_router, triage_router

if settings.sentry_dsn:
sentry_sdk.init(
dsn=settings.sentry_dsn,
environment=settings.environment,
release=f"hackbot-api@{__version__}",
send_default_pii=True,
)

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)


@asynccontextmanager
async def lifespan(app: FastAPI):
yield


app = FastAPI(
title="Hackbot API",
description="Agentic service to accelerate Firefox development",
version=__version__,
lifespan=lifespan,
)

app.include_router(triage_router)
app.include_router(duplicate_router)


@app.get("/health")
async def health_check():
"""Health check endpoint for Cloud Run."""
return {"message": "Service is healthy", "status": "ok"}


if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=settings.port)
4 changes: 4 additions & 0 deletions services/hackbot-api/app/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from app.routers.duplicate import router as duplicate_router
from app.routers.triage import router as triage_router

__all__ = ["triage_router", "duplicate_router"]
32 changes: 32 additions & 0 deletions services/hackbot-api/app/routers/duplicate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from pathlib import Path

from fastapi import APIRouter

from app.config import settings
from app.schemas import DuplicateRequest, DuplicateResponse, DuplicateResultItem
from bugbug.tools.duplicate_bugs import DuplicateBugsTool

router = APIRouter(tags=["duplicate"])


@router.post("/duplicate", response_model=DuplicateResponse)
async def detect_duplicates(request: DuplicateRequest):
tool = DuplicateBugsTool.create()
result = await tool.run(
mode=request.mode,
base_url=settings.bz_base_url,
api_key=settings.bz_api_key,
meta_bug=request.meta_bug,
bug_ids=request.bug_ids,
local_dir=Path(request.local_dir) if request.local_dir else None,
results_dir=Path(request.results_dir) if request.results_dir else None,
model=request.model or settings.model,
max_turns=request.max_turns or settings.max_turns,
)
return DuplicateResponse(
exit_code=result.exit_code,
results=[
DuplicateResultItem(name=name, verdict=verdict)
for name, verdict in result.results
],
)
36 changes: 36 additions & 0 deletions services/hackbot-api/app/routers/triage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pathlib import Path

from fastapi import APIRouter

from app.config import settings
from app.schemas import TriageRequest, TriageResponse
from bugbug.tools.bug_fix import BugFixTool

router = APIRouter(tags=["triage"])


@router.post("/triage", response_model=TriageResponse)
async def triage_bugs(request: TriageRequest):
tool = BugFixTool.create()
result = await tool.run(
base_url=settings.bz_base_url,
api_key=settings.bz_api_key,
source_repo=Path(settings.source_repo),
bugs=request.bugs,
keywords=request.keywords,
blocks=request.blocks,
status=request.status,
instructions=request.instructions,
task=request.task,
rules_dir=Path(request.rules_dir) if request.rules_dir else None,
dry_run=request.dry_run,
newest_first=request.newest_first,
model=request.model or settings.model,
max_turns=request.max_turns or settings.max_turns,
effort=request.effort or settings.effort,
)
return TriageResponse(
exit_code=result.exit_code,
bugs_processed=result.bugs_processed,
simulated_writes=result.simulated_writes,
)
47 changes: 47 additions & 0 deletions services/hackbot-api/app/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from pydantic import BaseModel, Field

# --- Triage ---


class TriageRequest(BaseModel):
bugs: list[int] | None = None
keywords: list[str] | None = None
blocks: int | None = None
status: list[str] | None = None
instructions: str = ""
task: str | None = None
rules_dir: str | None = None
dry_run: bool = True # Default to dry-run for safety
newest_first: bool = False
model: str | None = None
max_turns: int | None = None
effort: str | None = None


class TriageResponse(BaseModel):
exit_code: int
bugs_processed: int
simulated_writes: list[dict] = Field(default_factory=list)


# --- Duplicate Detection ---


class DuplicateRequest(BaseModel):
mode: str # "local" | "bugs" | "local_to_local"
meta_bug: int | None = None
bug_ids: list[int] | None = None
local_dir: str | None = None
results_dir: str | None = None
model: str | None = None
max_turns: int | None = None


class DuplicateResultItem(BaseModel):
name: str
verdict: str


class DuplicateResponse(BaseModel):
exit_code: int
results: list[DuplicateResultItem]
24 changes: 24 additions & 0 deletions services/hackbot-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[project]
name = "hackbot-api"
version = "0.1.0"
description = "Agentic service to accelerate Firefox development"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.109.0",
"uvicorn[standard]>=0.27.0",
"pydantic>=2.6.0",
"pydantic-settings>=2.1.0",
"bugsy",
"grizzly-framework",
"prefpicker",
"PyYAML",
"claude-agent-sdk>=0.1.30",
"sentry-sdk>=2.51.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0", "httpx>=0.26.0"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]