[project] name = "openai4s" version = "0.3.0" description = "A pure-stdlib hybrid Code-as-Action scientific research agent." readme = { file = "README.md", content-type = "text/markdown" } license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.10" dependencies = [] # pure stdlib by design; LLM client uses urllib keywords = ["agent", "scientific-computing", "code-as-action", "llm"] classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", "Operating System :: MacOS", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", ] [project.urls] Homepage = "https://github.com/PKU-YuanGroup/OpenAI4S" Documentation = "https://openai4s.org/docs/" Issues = "https://github.com/PKU-YuanGroup/OpenAI4S/issues" Source = "https://github.com/PKU-YuanGroup/OpenAI4S" [project.optional-dependencies] # The kernel is spawned with sys.executable, so anything in the active venv is # visible to agent cells. These libs are OPTIONAL: the core runs stdlib-only and # every numpy/pandas/matplotlib import in-tree is guarded by try/except ImportError. # Install with: uv sync --extra science science = [ "numpy>=1.24", "pandas>=2.0", "matplotlib>=3.7", "scikit-learn>=1.3", ] # CPU-first single-cell workflows require Python 3.11 because PyDESeq2 0.5.4 # raised its floor. Pertpy 1.0.3 is the newest release compatible with both # that floor and Scanpy 1.11.5; Pertpy 1.1.1 requires Python 3.12 and imports a # Scanpy symbol that 1.11.5 removed. Keep the group separate from `science`: # the OpenAI4S core still supports Python 3.10 and remains dependency-free. # Pertpy's core hard-requires the Flax/NumPyro/ott-jax stack, so pertpy and # its mudata pin live ONLY here — never in envs/python.yml or the desktop # bundle. Install with: uv sync --extra singlecell singlecell = [ "scanpy[harmony,skmisc]==1.11.5; python_version >= '3.11'", "igraph>=0.11; python_version >= '3.11'", "leidenalg>=0.10; python_version >= '3.11'", # Pertpy calls mudata.set_options(), removed in mudata 0.4. "mudata<0.4; python_version >= '3.11'", "pertpy[de]==1.0.3; python_version >= '3.11'", "pydeseq2==0.5.4; python_version >= '3.11'", ] # Optional cheminformatics support is kept separate because RDKit does not # publish wheels for every platform supported by the core/science environment. # Install where available with: uv sync --extra chemistry chemistry = [ "rdkit>=2024.9", ] # Coverage-guided parser fuzzing is a separate Linux-only gate. Keeping Atheris # out of dev preserves ordinary macOS/Windows setup and the dependency-free # runtime while uv.lock still pins the exact CI environment. fuzz = [ "atheris==3.1.0; python_version >= '3.12' and python_version < '3.15' and sys_platform == 'linux' and platform_machine == 'x86_64'", ] [dependency-groups] # Dev/test tooling (uv installs these by default with `uv sync`). # # Floors state what the gate was actually verified against, not merely what # once resolved. Every in-repo path goes through `uv sync --locked`, so a loose # floor breaks nothing in CI -- it misleads the contributor who installs from # this file directly and gets a weaker check than CI runs. dev = [ # >=2.3 because mypy 2.0 flipped `local_partial_types` on by default, and # the strict boundary below is only ever checked under 2.x. A range that # still admitted 1.x would make the boundary's semantics depend on which # major the resolver happened to pick. "mypy>=2.3,<3", "pytest>=7.4", # The offline suite is ~6,000 tests and was the whole critical path of CI: # 1094 of the offline-test job's 1122 seconds were this one command. xdist # splits it across the runner's cores. CI runs it as `-n auto # --maxprocesses=4 --dist loadfile`; the cap and scheduler choice are # argued where they are written, in .github/workflows/ci.yml. >=3.6 is # where the `loadfile` spelling used there is stable. "pytest-xdist>=3.6", # >=3.2 because the pre-commit-hooks and isort hooks pinned in # .pre-commit-config.yaml declare `minimum_pre_commit_version: 3.2.0`, # which pre-commit validates at manifest load -- a hard error, not a skip. "pre-commit>=3.2", ] [project.scripts] openai4s = "openai4s.cli:main" [build-system] requires = ["setuptools==84.0.0"] build-backend = "setuptools.build_meta" [tool.setuptools] include-package-data = true [tool.setuptools.packages.find] where = ["."] include = [ "openai4s*", "openai4s_compute_provider*", "openai4s_worker_runtime*", # These are data-only namespace packages. Keeping them beside the installed # openai4s package preserves Config.skills_dir and `openai4s setup` without # adding a runtime resource-loader dependency. "skills*", "envs*", # The benchmark manifests. Without this an installed `openai4s benchmark` # resolved `/workflows`, found nothing, and reported zero # failures across zero workflows with exit code 0 — a silent pass on the # thing that is supposed to decide whether a release is good. "workflows*", ] [tool.setuptools.package-data] openai4s = [ "compute/templates/*.tmpl", "kernel/*.R", "server/webui/*.css", "server/webui/*.gif", "server/webui/*.html", "server/webui/*.js", "server/webui/vendor/*.js", "server/webui/vendor/fonts/*.woff2", # Dist is one directory below webui/. These globs are not recursive: # `server/webui/*.html` does not match `dist/index.html`. Dropping # either line silently omits the next UI from the wheel. "server/webui/dist/*.html", "server/webui/dist/assets/*", ] # Data-only trees (skills*, envs*, workflows*) ship whole rather than by # extension. The enumerated list needed one entry per upstream file type and # ended with the literal basename of a single vendored Snakemake file, so a # re-pin that renamed it -- or added a `Dockerfile`, `nextflow.config`, or a # `.bed` fixture -- would have dropped it from the wheel silently. "*" = ["**/*"] [tool.setuptools.exclude-package-data] "*" = ["**/__pycache__/**", "**/*.py[cod]"] [tool.pytest.ini_options] testpaths = ["tests"] # Default run = the offline gate: quiet, strict markers, and every marker that # needs live external resources deselected. Opt in explicitly, e.g.: # uv run pytest -m gpu # (a command-line -m overrides the one below). addopts = '-q --strict-markers -m "not external and not network and not live_llm and not gpu and not ssh and not lab and not docker and not browser"' markers = [ # Suite-structure markers (informational; all run by default). "unit: fast, isolated unit test", "integration: multi-component test, still offline", "e2e: end-to-end flow through the agent loop, still offline", "security: security/secret-handling regression test", "platforms: compute/endpoint/lab platform test (offline fakes)", "skills: skill catalog/loader test", "slow: slow but still offline test", "golden: golden-trace comparison test", "smoke: minimal smoke test", # A test that replaces a service with a stub must not contribute a shape to # docs/response-schemas.json: that file's whole claim is that it was # captured from real responses, and a fabricated shape published as a # promise is worse than an absent one because it gets believed. "stubbed_backend: drives routes against stubbed services; excluded from response-shape capture", # Opt-in markers (EXCLUDED from the default run and from default PR CI; # these may require secrets, hardware, or live services). "external: requires any live external resource (umbrella marker)", "network: requires real network access", "live_llm: requires a real LLM provider and API key", "gpu: requires a physical GPU", "ssh: requires a reachable SSH host", "lab: requires lab hardware", "docker: requires a Docker daemon", "browser: requires a real browser session", ] [tool.black] # Same reason as the [tool.ruff.lint] block below: state the setting instead of # inheriting whatever the pinned formatter happens to compute. Until now there # was no [tool.black] section at all, so the formatting target was per-file # autodetection -- and autodetection is not a property of the repository, it is # a property of the interpreter that runs the hook. black 26.5.1 infers a range # reaching py315 for files with no version-distinguishing syntax, then reports # on the 3.12 lint runner: # # Warning: Python 3.12 cannot parse code formatted for Python 3.15. ... # Black's safety check verifies equivalence by parsing the AST, which fails # when the running Python is older than the target version. # # That is the formatter rewriting 234 files with its AST-equivalence check # unable to run. Naming the floor from `requires-python` puts the check back in # reach and keeps the target off the interpreter. # # Pinned separately from, and ahead of, the 23.3.0 -> 26.5.1 bump so the two are # not confused: this block changes no output. `--check` is clean under 23.3.0 # with it, and the 26.5.1 reformat is byte-identical across autodetect@3.12, # autodetect@3.14, and target-version py310. line-length is black's own default # and is restated here for the same reason as the target. target-version = ["py310"] line-length = 88 [tool.isort] # Same reason as the block above and the one below: state the setting instead of # inheriting whatever the pinned formatter happens to compute. isort was the one # formatter of the three with no section here -- its entire configuration was # `--profile black` on the hook's command line -- so everything the profile does # not name was whatever the pinned version shipped, across an 8.0.1 -> 9.0.1 # major that moved exactly those defaults. # # `src_paths` is the load-bearing entry. It defaults to `(cwd/"src", cwd)`, so # first-party detection was a property of the process rather than of the # repository: run isort from anywhere but the repo root and `openai4s` is # demoted to third-party, which moves the section breaks. A relative path in a # config file resolves against that file's directory, which puts the answer back # in the repository where the other two blocks already keep theirs. # # `profile` restates the hook's `--profile black` so that a bare `isort .` -- the # default an editor integration runs -- lays imports out the way the gate does, # instead of emitting a diff the gate then reverts. It does not make that run # safe, and nothing here can: the byte-exact trees are guarded only by # pre-commit's own `exclude:`, so a bare `isort .` still rewrites 318 of the 408 # pinned files under `skills/bioskills/`. Mirroring those paths into a `skip` # here would not reach the gate either -- the hook's `args:` replaces the # upstream manifest's `--filter-files`, and without it isort ignores `skip` for # files named on the command line, which is the only way pre-commit invokes it. # # `py_version` states the union-across-versions default explicitly, which is the # correct one for a `requires-python = ">=3.10"` package that CI runs on 3.10 # through 3.14; narrowing it to the floor would misfile `tomllib`, which this # repository imports and which only became stdlib in 3.11. # # Like the black block, this changes no output: the tree is byte-identical with # and without it. profile = "black" py_version = "3" src_paths = ["."] [tool.ruff.lint] # State the rule set instead of inheriting whatever the pinned ruff calls its # default. Until now there was no [tool.ruff] section at all, so the rule set # was whatever the hook's version happened to ship: ruff 0.0.274 defaulted to # E + F, and 0.16.0 defaults to 413 rules — turning a version bump into an # unreviewed adoption of ~350 rules and 1081 findings across the tree. # # E + F reproduces the rule set this codebase was actually written against; # a normalized-name diff of 0.0.274's default against 0.16.0 under this select # differs by exactly one entry, E999/SyntaxError, which modern ruff no longer # models as a lint rule at all. # Adopting further families (S, UP, RUF, SIM, ...) is real work with real # value, but it is a lint-policy decision that belongs in its own PR, one # family at a time, where the diff can be read — not a side effect of # Dependabot moving a pin. # # The ignore list stays on the hook's command line so it remains visible at # the point of invocation; both E402 and E741 are inside the E family and so # are genuinely still doing work here. select = ["E", "F"] [tool.mypy] # Start with the orchestration boundary where type mistakes are most costly. # Imports remain opaque for now so adopting mypy does not pretend that the # entire legacy graph is strict; add modules here as their boundaries mature. python_version = "3.10" files = [ "openai4s/agent/actions.py", "openai4s/agent/engine.py", "openai4s/agent/finalize.py", "openai4s/agent/models.py", "openai4s/agent/ports.py", "openai4s/host_dispatch.py", # The orchestration contract. Strict from birth rather than retrofitted: # these two modules are the boundary a second backend is written against, # and a wrong type here is discovered on a cluster at 2am. "openai4s/orchestration/models.py", "openai4s/orchestration/ports.py", ] follow_imports = "skip" check_untyped_defs = true disallow_untyped_defs = true no_implicit_optional = true strict_equality = true # Both of these are mypy 2.x defaults, stated rather than inherited. What the # boundary checks should be readable here, not a function of which mypy major # the resolver produced -- `local_partial_types` in particular is off under # 1.x, so leaving it implicit made the strictness silently version-dependent. local_partial_types = true strict_bytes = true warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true show_error_codes = true [[tool.mypy.overrides]] module = [ "openai4s.agent.actions", "openai4s.agent.engine", "openai4s.agent.finalize", "openai4s.agent.models", "openai4s.agent.ports", ] disallow_any_generics = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_incomplete_defs = true disallow_untyped_decorators = true warn_return_any = true no_implicit_reexport = true extra_checks = true