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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ artifacts = [
]

[tool.uv.workspace]
members = ["http_service"]
members = ["http_service", "services/hackbot-api"]

[tool.ruff]
extend-exclude = ["data"]
Expand Down
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 bug_fix_router, duplicate_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(bug_fix_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.bug_fix import router as bug_fix_router
from app.routers.duplicate import router as duplicate_router

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

from fastapi import APIRouter

from app.config import settings
from app.schemas import BugFixRequest, BugFixResponse
from bugbug.tools.bug_fix.agent import BugFixTool

router = APIRouter(tags=["bug-fix"])


@router.post("/bug-fix", response_model=BugFixResponse)
async def fix_bugs(request: BugFixRequest):
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.bug_id],
model=request.model or settings.model,
max_turns=request.max_turns or settings.max_turns,
effort=request.effort or settings.effort,
)
return BugFixResponse(
exit_code=result.exit_code,
bugs_processed=result.bugs_processed,
simulated_writes=result.simulated_writes,
)
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
],
)
39 changes: 39 additions & 0 deletions services/hackbot-api/app/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from pydantic import BaseModel, Field

# --- Bug Fix ---


class BugFixRequest(BaseModel):
bug_id: int
model: str | None = None
max_turns: int | None = None
effort: str | None = None


class BugFixResponse(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]
28 changes: 28 additions & 0 deletions services/hackbot-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[project]
name = "hackbot-api"
version = "0.1.0"
description = "Agentic service to accelerate Firefox development"
requires-python = ">=3.12"
dependencies = [
"bugbug",
"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.uv.sources]
bugbug = { workspace = true }

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