mitudrudutta commited on
Commit
e86a6ff
·
1 Parent(s): 8e6c4bb

feat: add API and deployment entrypoints

Browse files
Files changed (3) hide show
  1. Dockerfile +15 -0
  2. server/Dockerfile +80 -0
  3. server/app.py +124 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ WORKDIR /app
7
+
8
+ COPY . /app
9
+
10
+ RUN pip install --no-cache-dir --upgrade pip && \
11
+ pip install --no-cache-dir .
12
+
13
+ EXPOSE 8000
14
+
15
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
server/Dockerfile ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local OpenEnv sources)
10
+ # - Standalone environments (with openenv from PyPI/Git)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=chargeback_ops
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv is already vendored in the build context
31
+ # For standalone builds, openenv will be installed via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f http://localhost:8000/health || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
server/app.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI application for ChargebackOps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import HTTPException
6
+ from fastapi.responses import JSONResponse
7
+
8
+ try:
9
+ from openenv.core.env_server.http_server import create_app
10
+ except Exception as exc: # pragma: no cover
11
+ raise ImportError(
12
+ "openenv-core is required to run ChargebackOps. Install project dependencies first."
13
+ ) from exc
14
+
15
+ try:
16
+ from ..baseline_runner import run_baseline
17
+ from ..episode_store import get_report
18
+ from ..inference import run_inference
19
+ from ..models import (
20
+ BaselineRunResult,
21
+ ChargebackOpsAction,
22
+ ChargebackOpsObservation,
23
+ TasksResponse,
24
+ TaskSummary,
25
+ )
26
+ from ..simulation import list_tasks
27
+ from .chargeback_ops_environment import ChargebackOpsEnvironment
28
+ except ImportError: # pragma: no cover
29
+ from baseline_runner import run_baseline
30
+ from episode_store import get_report
31
+ from inference import run_inference
32
+ from models import (
33
+ BaselineRunResult,
34
+ ChargebackOpsAction,
35
+ ChargebackOpsObservation,
36
+ TasksResponse,
37
+ TaskSummary,
38
+ )
39
+ from simulation import list_tasks
40
+ from server.chargeback_ops_environment import ChargebackOpsEnvironment
41
+
42
+
43
+ app = create_app(
44
+ ChargebackOpsEnvironment,
45
+ ChargebackOpsAction,
46
+ ChargebackOpsObservation,
47
+ env_name="chargeback_ops",
48
+ max_concurrent_envs=8,
49
+ )
50
+
51
+
52
+ @app.get("/")
53
+ def root() -> JSONResponse:
54
+ """Return a lightweight root response for HF Space and validator pings."""
55
+
56
+ return JSONResponse(
57
+ {
58
+ "name": "ChargebackOps",
59
+ "status": "ok",
60
+ "docs_url": "/docs",
61
+ "health_url": "/health",
62
+ "tasks_url": "/tasks",
63
+ }
64
+ )
65
+
66
+
67
+ @app.get("/tasks", response_model=TasksResponse)
68
+ def tasks() -> TasksResponse:
69
+ """List built-in tasks and the action schema."""
70
+
71
+ return TasksResponse(
72
+ tasks=[
73
+ TaskSummary(
74
+ task_id=task.task_id,
75
+ title=task.title,
76
+ difficulty=task.difficulty,
77
+ objective=task.objective,
78
+ description=task.description,
79
+ max_steps=task.max_steps,
80
+ case_count=len(task.cases),
81
+ )
82
+ for task in list_tasks()
83
+ ],
84
+ action_schema=ChargebackOpsAction.model_json_schema(),
85
+ )
86
+
87
+
88
+ @app.get("/grader")
89
+ @app.post("/grader")
90
+ def grader(episode_id: str | None = None):
91
+ """Return a stored grade for a completed episode."""
92
+
93
+ report = get_report(episode_id)
94
+ if report is None:
95
+ raise HTTPException(
96
+ status_code=404,
97
+ detail="No completed episode report found. Finish an episode first or provide a valid episode_id.",
98
+ )
99
+ return report.model_dump()
100
+
101
+
102
+ @app.get("/baseline", response_model=BaselineRunResult)
103
+ @app.post("/baseline", response_model=BaselineRunResult)
104
+ def baseline(
105
+ provider: str | None = None,
106
+ model_name: str | None = None,
107
+ ) -> BaselineRunResult:
108
+ """Run the baseline inference policy across all tasks."""
109
+
110
+ if provider is None and model_name is None:
111
+ return run_inference()
112
+ return run_baseline(provider=provider, model_name=model_name)
113
+
114
+
115
+ def main(host: str = "0.0.0.0", port: int = 8000) -> None:
116
+ """Local entry point for uvicorn."""
117
+
118
+ import uvicorn
119
+
120
+ uvicorn.run(app, host=host, port=port)
121
+
122
+
123
+ if __name__ == "__main__": # pragma: no cover
124
+ main()