From 9db584491fc56f5757a96898731630dcd44d0a62 Mon Sep 17 00:00:00 2001 From: Duck Quang Date: Fri, 17 Jul 2026 00:47:35 +0700 Subject: [PATCH] Fix backend defects, gate CI on lint/tests, tidy packaging + docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend correctness: - web/app.py: import hypotheses repo as hyp_repo — the legacy /classic/sessions/{id} and .../hypotheses/{hid} routes referenced an unimported name (F821) and 500'd with NameError. Drop unused datetime imports. - web/react_api.py: anchor the background session task in a module-level set with a done-callback (RUF006) so a live run can't be GC'd mid-flight. - agents/generation.py, agents/reflection.py: raise ValueError("unsupported ...") instead of NotImplementedError on the dead defensive branches so they read as guards, not unfinished work. Lint: ruff check . is now zero errors (was 23) — F401/F841/I001/UP037/B905/RUF001 across web/spa.py, webapp/*, scripts/build_bench_report.py. zip strict= chosen per call site (False where slice pairing drops a leftover, True where row width matches the column list). CI: test.yml runs `ruff check .`; deploy-pages.yml installs deps + runs ruff + pytest in the build job (which deploy needs) so a red suite blocks Pages deploy. Packaging: pyproject version 1.0.0 (matches frontend), [project.urls], classifiers, author Quang Bui. Docs: git mv docs/superpowers -> docs/design (no code references). Co-Authored-By: Claude Fable 5 --- .github/workflows/deploy-pages.yml | 11 +++++++++++ .github/workflows/test.yml | 2 ++ co_scientist/agents/generation.py | 4 ++-- co_scientist/agents/reflection.py | 2 +- co_scientist/web/app.py | 2 +- co_scientist/web/react_api.py | 8 +++++++- co_scientist/web/spa.py | 2 -- .../specs/2026-07-08-ux-overhaul-design.md | 0 ...-12-chat-first-and-visual-proposal-design.md | 0 ...-07-14-byok-and-proposal-microsite-design.md | 0 pyproject.toml | 17 +++++++++++++++-- scripts/build_bench_report.py | 16 ++++++++-------- webapp/seed.py | 4 ++-- webapp/server.py | 2 +- webapp/simulator.py | 6 +++--- webapp/store.py | 2 +- 16 files changed, 54 insertions(+), 24 deletions(-) rename docs/{superpowers => design}/specs/2026-07-08-ux-overhaul-design.md (100%) rename docs/{superpowers => design}/specs/2026-07-12-chat-first-and-visual-proposal-design.md (100%) rename docs/{superpowers => design}/specs/2026-07-14-byok-and-proposal-microsite-design.md (100%) diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 24de834..ae98ea9 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -24,6 +24,17 @@ jobs: with: python-version: "3.12" + # Gate the deploy on a green suite: `deploy` needs `build`, so a failed + # lint or test here blocks publishing to Pages. + - name: Install package + dev deps + run: pip install -e ".[dev]" + + - name: Lint (ruff) + run: ruff check . + + - name: Run unit tests + run: pytest co_scientist/tests/unit -q + - name: Export static demo data run: python scripts/export_static_demo.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba31003..888b27a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,5 +16,7 @@ jobs: python-version: "3.12" - name: Install package + dev deps run: pip install -e ".[dev]" + - name: Lint (ruff) + run: ruff check . - name: Run unit tests run: pytest co_scientist/tests/unit -q diff --git a/co_scientist/agents/generation.py b/co_scientist/agents/generation.py index 89ea8f9..f3e91de 100644 --- a/co_scientist/agents/generation.py +++ b/co_scientist/agents/generation.py @@ -47,8 +47,8 @@ async def execute(self, task: Task) -> TaskResult: plan = session.research_plan if strategy != "literature": - # M3 ships only the literature strategy. - raise NotImplementedError(f"strategy {strategy!r} lands in a later milestone") + # Only the literature strategy is scheduled by the supervisor. + raise ValueError(f"unsupported strategy: {strategy!r}") # 1. Render the prompt and run the tool loop with `record_hypothesis` available. articles_block = ( diff --git a/co_scientist/agents/reflection.py b/co_scientist/agents/reflection.py index 082e1c4..d38a9f6 100644 --- a/co_scientist/agents/reflection.py +++ b/co_scientist/agents/reflection.py @@ -41,7 +41,7 @@ async def execute(self, task: Task) -> TaskResult: raise RuntimeError(f"hypothesis {hypothesis_id} missing") if kind != "full": - raise NotImplementedError(f"reflection kind {kind!r} lands in a later milestone") + raise ValueError(f"unsupported reflection kind: {kind!r}") prompt = render( "reflection.full", diff --git a/co_scientist/web/app.py b/co_scientist/web/app.py index 1a8d5cf..e983f0b 100644 --- a/co_scientist/web/app.py +++ b/co_scientist/web/app.py @@ -9,7 +9,6 @@ import asyncio import logging as stdlib_logging import os -from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -22,6 +21,7 @@ from ..config import Config, load_config from ..logging import get_logger from ..storage import db as db_mod +from ..storage.repos import hypotheses as hyp_repo from ..storage.repos import reviews as rev_repo from ..storage.repos import sessions as sess_repo from ..storage.repos import transcripts as tx_repo diff --git a/co_scientist/web/react_api.py b/co_scientist/web/react_api.py index 9a0e014..1ec7083 100644 --- a/co_scientist/web/react_api.py +++ b/co_scientist/web/react_api.py @@ -33,6 +33,10 @@ log = get_logger("react_api") +# Keep strong refs to background session tasks; the event loop only holds a weak +# ref, so without this a live run can be garbage-collected mid-flight (RUF006). +_bg_tasks: set[asyncio.Task] = set() + PROVIDERS = [ {"id": "anthropic", "label": "Anthropic", "models": ["claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"]}, {"id": "openai", "label": "OpenAI", "models": ["gpt-5", "gpt-4o", "o3-mini"]}, @@ -344,7 +348,9 @@ async def _bg() -> None: finally: live_sessions.discard("_pending") - asyncio.create_task(_bg()) + _task = asyncio.create_task(_bg()) + _bg_tasks.add(_task) + _task.add_done_callback(_bg_tasks.discard) # Session row is created within the first seconds of run_session. deadline = time.time() + 45.0 diff --git a/co_scientist/web/spa.py b/co_scientist/web/spa.py index c1c34d0..03f6ab9 100644 --- a/co_scientist/web/spa.py +++ b/co_scientist/web/spa.py @@ -2,8 +2,6 @@ from __future__ import annotations -from pathlib import Path - from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse diff --git a/docs/superpowers/specs/2026-07-08-ux-overhaul-design.md b/docs/design/specs/2026-07-08-ux-overhaul-design.md similarity index 100% rename from docs/superpowers/specs/2026-07-08-ux-overhaul-design.md rename to docs/design/specs/2026-07-08-ux-overhaul-design.md diff --git a/docs/superpowers/specs/2026-07-12-chat-first-and-visual-proposal-design.md b/docs/design/specs/2026-07-12-chat-first-and-visual-proposal-design.md similarity index 100% rename from docs/superpowers/specs/2026-07-12-chat-first-and-visual-proposal-design.md rename to docs/design/specs/2026-07-12-chat-first-and-visual-proposal-design.md diff --git a/docs/superpowers/specs/2026-07-14-byok-and-proposal-microsite-design.md b/docs/design/specs/2026-07-14-byok-and-proposal-microsite-design.md similarity index 100% rename from docs/superpowers/specs/2026-07-14-byok-and-proposal-microsite-design.md rename to docs/design/specs/2026-07-14-byok-and-proposal-microsite-design.md diff --git a/pyproject.toml b/pyproject.toml index 431fe49..6dd83de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,21 @@ build-backend = "hatchling.build" [project] name = "co-scientist" -version = "0.1.0" +version = "1.0.0" description = "Multi-agent AI Co-Scientist for tournament-style hypothesis generation" readme = "README.md" requires-python = ">=3.11,<3.14" license = { text = "Apache-2.0" } -authors = [{ name = "Co-Scientist contributors" }] +authors = [{ name = "Quang Bui" }] keywords = ["ai", "agents", "research", "anthropic", "claude"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: Apache Software License", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] dependencies = [ # LLM clients (provider is selectable via [llm] provider = ...) @@ -62,6 +70,11 @@ openai = ["openai>=1.40.0"] [project.scripts] co-scientist = "co_scientist.cli:app" +[project.urls] +Homepage = "https://github.com/duckyquang/Co-Scientist" +Repository = "https://github.com/duckyquang/Co-Scientist" +Issues = "https://github.com/duckyquang/Co-Scientist/issues" + [tool.hatch.build.targets.wheel] packages = ["co_scientist"] diff --git a/scripts/build_bench_report.py b/scripts/build_bench_report.py index ea21e84..4af2964 100644 --- a/scripts/build_bench_report.py +++ b/scripts/build_bench_report.py @@ -412,7 +412,7 @@ def _headline_findings_section() -> str: "", "| model | run 1 Δ Elo | run 2 Δ Elo |", "| --- | --- | --- |", - "| claude-haiku-4.5 | **+180** (1-9 → 10-0) | **−28** (10-2 → 8-4) |", + "| claude-haiku-4.5 | **+180** (1-9 → 10-0) | **-28** (10-2 → 8-4) |", "| openai-o1 | +43 | +29 |", "", "Haiku's *raw* record alone flipped 1-9 → 10-2 across the two runs — " @@ -427,12 +427,12 @@ def _headline_findings_section() -> str: "| gemini-2.5-flash | +172 |", "| gemini-2.5-pro | +47 |", "| gpt-5 | +26 |", - "| gemini-2.0-flash | −48 |", - "| gemini-3-flash | −36 |", - "| gemini-3-pro | −89 |", + "| gemini-2.0-flash | -48 |", + "| gemini-3-flash | -36 |", + "| gemini-3-pro | -89 |", "", "Within Google alone the 2.5 models gain (+172, +47) and the 3.x " - "models lose (−36, −89) — so there is no clean \"provider\" or " + "models lose (-36, -89) — so there is no clean \"provider\" or " "\"stronger-model\" story; an earlier draft that claimed one was " "reading noise. **The only repeatable signal is openai-o1** (pipeline " "modestly ahead in both runs). A real per-model verdict needs many " @@ -454,9 +454,9 @@ def _headline_findings_section() -> str: "| ferroptosis | 3 |", "", "At the **drug** level it's a long tail of one-offs. The only " - "compounds proposed more than once are **Itraconazole** (×5, as an " - "OXPHOS inhibitor) and **Auranofin** (×2, thioredoxin-reductase). " - "**Venetoclax** appears ×6 but as the resistance/combo context, not " + "compounds proposed more than once are **Itraconazole** (x5, as an " + "OXPHOS inhibitor) and **Auranofin** (x2, thioredoxin-reductase). " + "**Venetoclax** appears x6 but as the resistance/combo context, not " "the novel candidate. Tellingly, all three recurrent names already " "have prior AML evidence — models default to the familiar, which is " "exactly what the strict no-prior-evidence prompt forbids (no " diff --git a/webapp/seed.py b/webapp/seed.py index b818dd1..ca25c82 100644 --- a/webapp/seed.py +++ b/webapp/seed.py @@ -161,7 +161,7 @@ def build_session(conn: sqlite3.Connection, *, goal: str, status: str, match_t = start + timedelta(minutes=n_hyps * 3 + 5) for _round in range(n_rounds): random.Random(f"{sid}{_round}").shuffle(in_tournament) - for a, b in zip(in_tournament[::2], in_tournament[1::2]): + for a, b in zip(in_tournament[::2], in_tournament[1::2], strict=False): if a is b: continue mode = "debate" if r.random() < 0.3 else "pairwise" @@ -264,7 +264,6 @@ def build_session(conn: sqlite3.Connection, *, goal: str, status: str, {"goal": goal[:200], "n_initial": 3, "budget_usd": budget}, start) _transcript(conn, sid, "supervisor", "parse_goal", content.MODELS["supervisor"], start, 0.01) - ev_t = start + timedelta(minutes=1) for h in hyps: agent = "evolution" if h["created_by"] == "evolution" else "generation" cost = round(r.uniform(0.04, 0.22), 4) @@ -307,6 +306,7 @@ def build_session(conn: sqlite3.Connection, *, goal: str, status: str, top_hyps = sorted(hyps, key=lambda h: -h["elo"])[:5] ov_md = content.make_overview(goal, top_hyps) import os + from .store import REPO_ROOT ov_dir = REPO_ROOT / "data" / "artifacts" / sid / "final" ov_dir.mkdir(parents=True, exist_ok=True) diff --git a/webapp/server.py b/webapp/server.py index df325e3..06539c9 100644 --- a/webapp/server.py +++ b/webapp/server.py @@ -37,7 +37,7 @@ ] -def _json(handler: "Handler", obj, status=200): +def _json(handler: Handler, obj, status=200): body = json.dumps(obj, default=str).encode() handler.send_response(status) handler.send_header("Content-Type", "application/json") diff --git a/webapp/simulator.py b/webapp/simulator.py index fb62d56..d659806 100644 --- a/webapp/simulator.py +++ b/webapp/simulator.py @@ -14,13 +14,13 @@ import random import threading import time -from datetime import UTC, datetime, timedelta +from datetime import UTC, datetime from . import content from .seed import EXTRA_TABLES, _elo_update, _emit, _transcript from .store import REPO_ROOT, connect -_RUNNING: dict[str, "Sim"] = {} +_RUNNING: dict[str, Sim] = {} _LOCK = threading.Lock() TOKENS_PER_USD = 220_000 @@ -277,7 +277,7 @@ def _ranking(self, conn, rounds: int): for _r in range(rounds): pool = list(self.hyps) self.r.shuffle(pool) - for a, b in zip(pool[::2], pool[1::2]): + for a, b in zip(pool[::2], pool[1::2], strict=False): if a is b or not self._wait(conn, 1.2): return mode = "debate" if self.r.random() < 0.35 else "pairwise" diff --git a/webapp/store.py b/webapp/store.py index 3a1d352..33e4727 100644 --- a/webapp/store.py +++ b/webapp/store.py @@ -58,7 +58,7 @@ def connect(db_path: Path | str = DEFAULT_DB) -> sqlite3.Connection: def _rows(conn: sqlite3.Connection, sql: str, params: tuple = ()) -> list[dict]: cur = conn.execute(sql, params) cols = [c[0] for c in cur.description] - return [dict(zip(cols, r)) for r in cur.fetchall()] + return [dict(zip(cols, r, strict=True)) for r in cur.fetchall()] def _row(conn: sqlite3.Connection, sql: str, params: tuple = ()) -> dict | None: