Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions co_scientist/agents/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
2 changes: 1 addition & 1 deletion co_scientist/agents/reflection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion co_scientist/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion co_scientist/web/react_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]},
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions co_scientist/web/spa.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from __future__ import annotations

from pathlib import Path

from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse

Expand Down
17 changes: 15 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ...)
Expand Down Expand Up @@ -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"]

Expand Down
16 changes: 8 additions & 8 deletions scripts/build_bench_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 — "
Expand All @@ -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 "
Expand All @@ -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 "
Expand Down
4 changes: 2 additions & 2 deletions webapp/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion webapp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 3 additions & 3 deletions webapp/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion webapp/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading