feat(my-deepagent): v0.2 PR #3 — FastAPI + SSE + minimal Web GUI (mydeepagent serve)
Closes the "GUI 미존재" gap from the user's first-session requirements (REPL + workflow + GUI). v0.2 PR #1's Postgres migration made a second concurrent writer safe; v0.2 PR #2a/#2b wired durable resume; this commit ships the HTTP + browser surface that uses them. No auth, no multi-tenant, single uvicorn worker — per DR-3 boundaries. v0.3+ will add auth, multi-worker fanout, LISTEN/NOTIFY SSE upgrade. Backend - `src/my_deepagent/api/`: - `app.py` create_app() factory. lifespan stores db/config/personas/ workflows on app.state. CORS allow_origin_regex http://localhost(:port)?. /static mount + /, /{page}.html for the HTML frontend. - `models.py` — pydantic v2 DTOs (extra="forbid") for every route. Auto OpenAPI/Swagger via FastAPI's response_model. - `deps.py` — get_db / get_config / get_personas / get_workflows. - `runner.py` — start_new_run / start_resume. Pre-allocates run_id via new `WorkflowEngine.run(pre_allocated_run_id=...)` so the route returns the id immediately while the engine runs in asyncio.create_task. - `sse.py` — 0.5 s poll over run_events.seq. Emits ServerSentEvent rows; sends `event: done` and HTTP-200-closes when run hits terminal. - `routes/{runs,personas,workflows,budget}.py`: GET /api/runs (list, ?limit + ?state) GET /api/runs/{id} (detail + phases + artifacts + events) POST /api/runs (start; mock-able via runner.start_new_run) POST /api/runs/{id}/resume POST /api/runs/{id}/abort GET /api/runs/{id}/events (SSE; Last-Event-ID header + ?last_event_id) GET /api/personas GET /api/workflows GET /api/budget CLI - `cli/serve.py` mydeepagent serve [--host 127.0.0.1] [--port 8000]. Loud stderr warning if --host is not loopback (no auth = footgun). uvicorn.run(factory=True, workers=1). - `cli/main.py` serve command registered. Static frontend (vanilla HTML/JS/CSS, no build system) - index.html — runs list + budget summary - new.html — start-run form (workflow select, repo path, requirements, per-role persona override) - run.html — run detail + live SSE event log + Resume/Abort buttons - app.js — fetch + EventSource. XSS policy HARDCODED at file top: textContent only, innerHTML/insertAdjacentHTML/outerHTML forbidden. - style.css — dark theme, single file. Engine - WorkflowEngine.run(... pre_allocated_run_id: UUID|None = None). None → uuid4() (existing behavior). Set → use that UUID. Backward compatible. Tests - tests/integration/test_api_read.py (5): list empty, get 404, personas seed count (12), workflows seed (>=3), budget empty. - tests/integration/test_api_write.py (5): missing template 400, extra field 422, resume 404, abort 404, mock-runner happy path. - tests/integration/test_api_sse.py (1): seed terminal run + 3 events, drain stream, assert types present + stream closes within 3 s. - tests/integration/test_api_static.py (5): index/new/run HTML 200, app.js content-type + XSS-policy substring assertion, style.css content-type. - All fixtures use httpx ASGITransport + app.router.lifespan_context (httpx does NOT auto-trigger FastAPI lifespan) + sqlite tmp_path. Gates - ruff check + ruff format --check + mypy --strict: PASS (120 source files) - pytest non-E2E: 603 PASS (12.15 s) — +16 from new API tests - pytest E2E real OpenRouter on Postgres: PASS 60.44 s (baseline 71–122 s range; well within DR-3 acceptance threshold ≤+20%) Manual browser verification deferred to a follow-up (docker compose up, mydeepagent serve, open http://localhost:8000). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,73 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **v0.2 PR #3 — FastAPI + SSE + minimal Web GUI (`mydeepagent serve`)**.
|
||||
Localhost Web UI for run start / list / detail / resume / abort + live
|
||||
event stream. Closes the v0.1.0 gap "GUI 미존재" from the user's first
|
||||
session requirements. No auth, no multi-tenant; single uvicorn worker
|
||||
(per DR-3).
|
||||
- `pyproject.toml`: runtime deps `fastapi>=0.115`,
|
||||
`uvicorn[standard]>=0.30`, `sse-starlette>=2.1` (8 transitive deps).
|
||||
- `src/my_deepagent/api/` (new tree):
|
||||
- `app.py` — `create_app(config=None) -> FastAPI` factory. lifespan
|
||||
stores `db`/`config`/`personas`/`workflows` on `app.state`.
|
||||
`CORSMiddleware(allow_origin_regex=r"^http://localhost(:\d+)?$")`.
|
||||
Static frontend mounted under `/static`, plus `/`, `/{page}.html`.
|
||||
- `models.py` — pydantic v2 DTOs (`RunSummary`, `RunDetail`,
|
||||
`PhaseInfo`, `ArtifactInfo`, `EventInfo`, `StartRunRequest`,
|
||||
`StartRunResponse`, `PersonaSummary`, `WorkflowSummary`,
|
||||
`BudgetSummary`, `BudgetScopeEntry`). All `extra="forbid"` so typos
|
||||
surface at 422 deserialization time.
|
||||
- `deps.py` — `get_db`, `get_config`, `get_personas`, `get_workflows`,
|
||||
`seed_root`. Annotated[...] wrappers in each route module.
|
||||
- `runner.py` — `start_new_run` / `start_resume` /
|
||||
`is_running`. Pre-allocates a UUID and passes it to
|
||||
`WorkflowEngine.run(pre_allocated_run_id=...)` so the route can
|
||||
return the run_id before the phase loop starts. In-memory
|
||||
`_tasks: dict[UUID, asyncio.Task]` prevents GC of in-flight tasks.
|
||||
- `sse.py` — `run_events_stream(db, run_id, last_event_id)`.
|
||||
0.5 s polling against `run_events.seq > last_event_id`; emits
|
||||
`ServerSentEvent` per row; sends `event: done` and HTTP-200-closes
|
||||
when run reaches terminal state.
|
||||
- `routes/runs.py` — GET `/api/runs?limit=&state=`, GET `/api/runs/{id}`,
|
||||
POST `/api/runs` (start), POST `/api/runs/{id}/resume`,
|
||||
POST `/api/runs/{id}/abort`, GET `/api/runs/{id}/events` (SSE).
|
||||
`Last-Event-ID` HTTP header honored alongside `?last_event_id=`.
|
||||
- `routes/personas.py` — GET `/api/personas`.
|
||||
- `routes/workflows.py` — GET `/api/workflows`.
|
||||
- `routes/budget.py` — GET `/api/budget` (day / runs / personas
|
||||
buckets with cap + warn thresholds from `Config`).
|
||||
- `src/my_deepagent/cli/serve.py` (new) — `mydeepagent serve [--host
|
||||
127.0.0.1] [--port 8000]`. Loud stderr warning when host is not
|
||||
loopback (the API is unauthenticated). Uses uvicorn factory form +
|
||||
forces `workers=1`.
|
||||
- `src/my_deepagent/cli/main.py` — `serve` command registered.
|
||||
- `src/my_deepagent/engine.py` — `WorkflowEngine.run` gained
|
||||
`pre_allocated_run_id: UUID | None = None` so the FastAPI runner can
|
||||
return the run_id immediately. Default behavior unchanged.
|
||||
- `static/` (new) — vanilla HTML/JS/CSS, no build system:
|
||||
- `index.html` — 런 목록 + 예산 (data-page="index")
|
||||
- `new.html` — 신규 run 폼 (workflow select, repo path, requirements,
|
||||
per-role persona override) (data-page="new")
|
||||
- `run.html` — run 상세 + SSE 이벤트 라이브 + resume/abort 버튼
|
||||
(data-page="run")
|
||||
- `app.js` — fetch + EventSource. **XSS policy hardcoded at the top
|
||||
of the file**: `element.textContent` only, `innerHTML` /
|
||||
`insertAdjacentHTML` / `outerHTML` forbidden.
|
||||
- `style.css` — dark theme, single file.
|
||||
- Tests (new):
|
||||
- `tests/integration/test_api_read.py` — 5 cases (list empty, get 404,
|
||||
personas seed count, workflows seed, budget empty).
|
||||
- `tests/integration/test_api_write.py` — 5 cases (missing template
|
||||
400, extra field 422, resume 404, abort 404, mock-runner happy path).
|
||||
- `tests/integration/test_api_sse.py` — 1 case: seed terminal run +
|
||||
events, drain stream, assert types present and stream closes.
|
||||
- `tests/integration/test_api_static.py` — 5 cases: index/new/run
|
||||
HTML 200, app.js content-type + XSS-policy substring, style.css
|
||||
content-type.
|
||||
All tests use `httpx.ASGITransport` + `app.router.lifespan_context`
|
||||
(httpx does not auto-trigger FastAPI lifespan) and sqlite tmp_path.
|
||||
|
||||
- **v0.2 PR #2b — `mydeepagent runs resume <id>` real implementation**.
|
||||
Closes the v0.1.0 KNOWN LIMIT where resume was an exit-2 stub. Reuses
|
||||
v0.2 PR #2a's LangGraph wiring + sweep_orphan_runs's DB state machine,
|
||||
|
||||
Reference in New Issue
Block a user