dyno-phi 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/.claude/skills/phi/SKILL.md +0 -2
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/.github/workflows/publish.yml +3 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/CHANGELOG.md +25 -1
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/CLI-REFERENCE.md +0 -1
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/PKG-INFO +12 -2
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/README.md +10 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/phi-plugin/skills/phi/SKILL.md +0 -2
- dyno_phi-0.2.4/src/phi/_version.py +1 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/cli.py +18 -1
- dyno_phi-0.2.4/src/phi/commands/research.py +90 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/parser.py +27 -10
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/research.py +6 -5
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/test_research.py +26 -0
- dyno_phi-0.2.2/src/phi/_version.py +0 -1
- dyno_phi-0.2.2/src/phi/commands/research.py +0 -141
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/.claude/settings.json +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/.claude-plugin/marketplace.json +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/.github/workflows/ci.yml +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/.gitignore +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/.pre-commit-config.yaml +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/LICENSE +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/TEMPLATE_NEW_BIOMODAL.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/__init__.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/base.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_af2rank.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_align_structures.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_alphafold.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_bindcraft.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_boltz.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_boltzgen.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_chai1.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_esm2_predict_masked.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_esmfold.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_ligandmpnn.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_proteinmpnn.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_rf3.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_rfdiffusion3.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_rso.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/modal_tm_score.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/utils/__init__.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/utils/extract_chain_as_mol2.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/biomodals/utils/extract_ligands.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/README.md +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/binders/pdl1/binder_009.pdb +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/binders/pdl1/binder_047.pdb +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/binders/pdl1/binder_106.pdb +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/binders/pdl1/binder_188.pdb +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/binders/pdl1/binder_246.pdb +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/fixtures/pdl1/5O45.pdb +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/examples/fixtures/pdl1/pdl1_pdb_19_132.pdb +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/phi-plugin/.claude-plugin/plugin.json +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/phi-plugin/README.md +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/pyproject.toml +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/__init__.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/api.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/__init__.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/auth.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/datasets.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/filter.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/jobs.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/models.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/structure.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/commands/tutorial.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/config.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/display.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/download.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/polling.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/structure.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/types.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/src/phi/upload.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/__init__.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/test_api.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/test_config.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/test_display.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/test_download.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/test_structure.py +0 -0
- {dyno_phi-0.2.2 → dyno_phi-0.2.4}/tests/test_upload.py +0 -0
|
@@ -5,7 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.2.4] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
The last release of the 0.2.x line. `dyno-phi` 1.0.0 and later is a different
|
|
11
|
+
client, published from `dynotx/phi-api`, under the same package name.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Every invocation of `phi` prints a migration notice on stderr: upgrade with
|
|
15
|
+
`pip install --upgrade dyno-phi`, use `PHI_API_KEY` and `PHI_API` in place of
|
|
16
|
+
`DYNO_API_KEY` and `DYNO_API_BASE_URL`, Python 3.12+, docs at
|
|
17
|
+
design.dynotx.com/docs.
|
|
18
|
+
- README carries the same notice. The publish workflow is retired after this
|
|
19
|
+
release; later releases come from `dynotx/phi-api`.
|
|
20
|
+
|
|
21
|
+
## [0.2.3] - 2026-03-17
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `phi research` now accepts `--dataset_id` (underscore) as an alias for `--dataset-id` — consistent with all other commands
|
|
25
|
+
- `phi notes` command implemented: fetches and displays accumulated research notes for a dataset
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `phi design` (`rfdiffusion3`) and `phi boltzgen` no longer appear in `phi --help` or usage line — they remain accessible internally via `DYNO_ENABLE_DESIGN=1`
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Missing imports (`_request`, `PhiApiError`, `Panel`, `Markdown`, `_C_BLUE`, `_C_SAND`, `json`) in `phi notes` command
|
|
32
|
+
- mypy `Cannot assign to a method` error replaced with a proper `_FilteredSubParsersAction` subclass
|
|
9
33
|
|
|
10
34
|
## [0.2.2] - 2026-03-17
|
|
11
35
|
|
|
@@ -679,7 +679,6 @@ phi research --question QUESTION [options]
|
|
|
679
679
|
| `--dataset-id ID` | — | Associate notes with a dataset and sync to cloud storage |
|
|
680
680
|
| `--notes-file FILE` | `./research.md` | Local append-only notes file |
|
|
681
681
|
| `--no-save` | — | Skip saving the report to the local notes file |
|
|
682
|
-
| `--stream` | — | Stream results live via SSE (skips job tracking) |
|
|
683
682
|
|
|
684
683
|
**Examples:**
|
|
685
684
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: dyno-phi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Phi CLI and biomodals for the dyno protein design platform
|
|
5
5
|
Project-URL: Homepage, https://dynotx.com
|
|
6
6
|
Project-URL: Repository, https://github.com/dynotx/phi-cli
|
|
@@ -41,6 +41,16 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
# dyno-phi
|
|
43
43
|
|
|
44
|
+
> **Retired.** `dyno-phi` 0.2.x is the earlier Phi client and 0.2.4 is its last
|
|
45
|
+
> release; the API it talks to has been shut down, and every 0.2.x release on PyPI
|
|
46
|
+
> is yanked. The current Phi client is `dyno-phi` 1.0 and later, published from
|
|
47
|
+
> [`dynotx/phi-api`](https://github.com/dynotx/phi-api) under the same package
|
|
48
|
+
> name: run `pip install --upgrade dyno-phi` (or `uv tool install "dyno-phi[cli]"`;
|
|
49
|
+
> Python 3.12+), set `PHI_API_KEY` and `PHI_API` in place of `DYNO_API_KEY` and
|
|
50
|
+
> `DYNO_API_BASE_URL`, and read the new command tree at
|
|
51
|
+
> [design.dynotx.com/docs](https://design.dynotx.com/docs). This repository is
|
|
52
|
+
> archived. The text below describes 0.2.x as it was.
|
|
53
|
+
|
|
44
54
|
**Phi CLI for the dyno protein structure analysis platform.**
|
|
45
55
|
|
|
46
56
|
`phi` is the command-line interface for uploading protein structures, running
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# dyno-phi
|
|
2
2
|
|
|
3
|
+
> **Retired.** `dyno-phi` 0.2.x is the earlier Phi client and 0.2.4 is its last
|
|
4
|
+
> release; the API it talks to has been shut down, and every 0.2.x release on PyPI
|
|
5
|
+
> is yanked. The current Phi client is `dyno-phi` 1.0 and later, published from
|
|
6
|
+
> [`dynotx/phi-api`](https://github.com/dynotx/phi-api) under the same package
|
|
7
|
+
> name: run `pip install --upgrade dyno-phi` (or `uv tool install "dyno-phi[cli]"`;
|
|
8
|
+
> Python 3.12+), set `PHI_API_KEY` and `PHI_API` in place of `DYNO_API_KEY` and
|
|
9
|
+
> `DYNO_API_BASE_URL`, and read the new command tree at
|
|
10
|
+
> [design.dynotx.com/docs](https://design.dynotx.com/docs). This repository is
|
|
11
|
+
> archived. The text below describes 0.2.x as it was.
|
|
12
|
+
|
|
3
13
|
**Phi CLI for the dyno protein structure analysis platform.**
|
|
4
14
|
|
|
5
15
|
`phi` is the command-line interface for uploading protein structures, running
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.4"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
+
from rich.markup import escape
|
|
4
|
+
|
|
3
5
|
import phi.config as config
|
|
4
6
|
from phi.commands.auth import cmd_login
|
|
5
7
|
from phi.commands.datasets import cmd_dataset, cmd_datasets, cmd_ingest_session, cmd_upload, cmd_use
|
|
@@ -18,7 +20,7 @@ from phi.commands.research import cmd_notes, cmd_research
|
|
|
18
20
|
from phi.commands.structure import cmd_fetch
|
|
19
21
|
from phi.commands.tutorial import cmd_tutorial
|
|
20
22
|
from phi.config import _load_state
|
|
21
|
-
from phi.display import _C_BLUE, _die, console
|
|
23
|
+
from phi.display import _C_BLUE, _die, console, err_console
|
|
22
24
|
from phi.parser import build_parser
|
|
23
25
|
from phi.types import PhiApiError
|
|
24
26
|
|
|
@@ -73,7 +75,22 @@ def _print_state_footer() -> None:
|
|
|
73
75
|
console.print(f"[dim]Dashboard: [link={dashboard_url}]{dashboard_url}[/link][/dim]")
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
MIGRATION_NOTICE = (
|
|
79
|
+
"dyno-phi 0.2.x is retired and 0.2.4 is its last release; the API it talks to "
|
|
80
|
+
"has been shut down. The current Phi client is dyno-phi 1.0 and later, with the "
|
|
81
|
+
"same package name: run `pip install --upgrade dyno-phi` (or "
|
|
82
|
+
'`uv tool install "dyno-phi[cli]"`; Python 3.12+), set PHI_API_KEY and PHI_API in '
|
|
83
|
+
"place of DYNO_API_KEY and DYNO_API_BASE_URL, and read the new command tree at "
|
|
84
|
+
"https://design.dynotx.com/docs."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _print_migration_notice() -> None:
|
|
89
|
+
err_console.print(f"[bold]Notice:[/bold] {escape(MIGRATION_NOTICE)}\n", highlight=False)
|
|
90
|
+
|
|
91
|
+
|
|
76
92
|
def main() -> None:
|
|
93
|
+
_print_migration_notice()
|
|
77
94
|
parser = build_parser()
|
|
78
95
|
args = parser.parse_args()
|
|
79
96
|
if args.poll_interval is not None:
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from phi.api import _request
|
|
12
|
+
from phi.display import _C_BLUE, _C_ROSE, _C_SAND, _die, console
|
|
13
|
+
from phi.research import _stream_research
|
|
14
|
+
from phi.types import PhiApiError
|
|
15
|
+
|
|
16
|
+
_ERROR_REPORT_PREFIXES = ("# Research Failed", "# Error", "Error: Command failed")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_error_report(report: str) -> bool:
|
|
20
|
+
stripped = report.strip()
|
|
21
|
+
return any(stripped.startswith(prefix) for prefix in _ERROR_REPORT_PREFIXES)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_research(args: argparse.Namespace) -> None:
|
|
25
|
+
notes_file: Path | None = None if args.no_save else Path(args.notes_file)
|
|
26
|
+
dataset_id: str | None = args.dataset_id
|
|
27
|
+
|
|
28
|
+
stream_url = os.environ.get("DYNO_RESEARCH_STREAM_URL") or args.url
|
|
29
|
+
|
|
30
|
+
context: str | None = None
|
|
31
|
+
if args.context_file:
|
|
32
|
+
try:
|
|
33
|
+
prior = Path(args.context_file).read_text(encoding="utf-8")
|
|
34
|
+
existing = args.context or ""
|
|
35
|
+
context = (prior + "\n\n" + existing).strip() if existing else prior
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
console.print(f"[{_C_ROSE}]Warning: could not read context file:[/] {exc}")
|
|
38
|
+
elif args.context:
|
|
39
|
+
context = args.context
|
|
40
|
+
|
|
41
|
+
_stream_research(
|
|
42
|
+
question=args.question,
|
|
43
|
+
base_url=stream_url,
|
|
44
|
+
notes_file=notes_file,
|
|
45
|
+
dataset_id=dataset_id,
|
|
46
|
+
context=context,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_notes(args: argparse.Namespace) -> None:
|
|
51
|
+
dataset_id: str = args.dataset_id
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
data = _request("GET", f"/datasets/{dataset_id}/research-notes")
|
|
55
|
+
except PhiApiError as exc:
|
|
56
|
+
_die(str(exc))
|
|
57
|
+
|
|
58
|
+
if not data.get("exists"):
|
|
59
|
+
console.print(f"[{_C_ROSE}]No research notes found[/] for dataset {dataset_id}")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
content: str = data.get("content") or ""
|
|
63
|
+
data.get("gcs_url")
|
|
64
|
+
data.get("gcs_uri")
|
|
65
|
+
|
|
66
|
+
if args.out:
|
|
67
|
+
out = Path(args.out)
|
|
68
|
+
if out.suffix.lower() == ".md":
|
|
69
|
+
dest = out
|
|
70
|
+
else:
|
|
71
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
dest = out / "research.md"
|
|
73
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
dest.write_text(content, encoding="utf-8")
|
|
75
|
+
console.print(f"[{_C_SAND}]Notes saved[/] → {dest}")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if args.json:
|
|
79
|
+
print(json.dumps(data, indent=2))
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
console.print()
|
|
83
|
+
console.print(
|
|
84
|
+
Panel(
|
|
85
|
+
Markdown(content),
|
|
86
|
+
title=f"[{_C_BLUE}]Research Notes[/] — {dataset_id[:8]}…",
|
|
87
|
+
border_style=_C_BLUE,
|
|
88
|
+
padding=(1, 2),
|
|
89
|
+
)
|
|
90
|
+
)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
2
4
|
|
|
3
5
|
from phi._version import __version__
|
|
@@ -44,6 +46,8 @@ def _add_fasta_args(p: argparse.ArgumentParser) -> None:
|
|
|
44
46
|
g.add_argument("--fasta-str", metavar="FASTA", help="FASTA content as a string (for scripting)")
|
|
45
47
|
g.add_argument(
|
|
46
48
|
"--dataset-id",
|
|
49
|
+
"--dataset_id",
|
|
50
|
+
dest="dataset_id",
|
|
47
51
|
metavar="DATASET_ID",
|
|
48
52
|
help="Pre-ingested dataset ID (for batch runs of 100–50,000 files)",
|
|
49
53
|
)
|
|
@@ -55,6 +59,8 @@ def _add_pdb_args(p: argparse.ArgumentParser) -> None:
|
|
|
55
59
|
g.add_argument("--pdb-gcs", metavar="URI", help="Cloud storage URI to PDB (gs://…)")
|
|
56
60
|
g.add_argument(
|
|
57
61
|
"--dataset-id",
|
|
62
|
+
"--dataset_id",
|
|
63
|
+
dest="dataset_id",
|
|
58
64
|
metavar="DATASET_ID",
|
|
59
65
|
help="Pre-ingested dataset ID (for batch runs of 100–50,000 files)",
|
|
60
66
|
)
|
|
@@ -233,6 +239,13 @@ def _add_boltzgen_args(p: argparse.ArgumentParser) -> None:
|
|
|
233
239
|
)
|
|
234
240
|
|
|
235
241
|
|
|
242
|
+
class _FilteredSubParsersAction(argparse._SubParsersAction):
|
|
243
|
+
"""Subparser action that omits choices with help=SUPPRESS from help output."""
|
|
244
|
+
|
|
245
|
+
def _get_subactions(self) -> list[argparse.Action]:
|
|
246
|
+
return [a for a in self._choices_actions if a.help is not argparse.SUPPRESS]
|
|
247
|
+
|
|
248
|
+
|
|
236
249
|
def build_parser() -> argparse.ArgumentParser:
|
|
237
250
|
root = argparse.ArgumentParser(
|
|
238
251
|
prog="phi",
|
|
@@ -252,6 +265,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
252
265
|
metavar="S",
|
|
253
266
|
help=f"Seconds between status-poll requests (default: {POLL_INTERVAL})",
|
|
254
267
|
)
|
|
268
|
+
root.register("action", "parsers", _FilteredSubParsersAction)
|
|
255
269
|
sub = root.add_subparsers(dest="command", required=True)
|
|
256
270
|
|
|
257
271
|
p = sub.add_parser(
|
|
@@ -427,14 +441,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
427
441
|
p = sub.add_parser(
|
|
428
442
|
"rfdiffusion3",
|
|
429
443
|
aliases=["design"],
|
|
430
|
-
help=
|
|
444
|
+
help=argparse.SUPPRESS,
|
|
431
445
|
)
|
|
432
446
|
_add_rfdiffusion3_args(p)
|
|
433
447
|
_add_job_args(p)
|
|
434
448
|
|
|
435
449
|
p = sub.add_parser(
|
|
436
450
|
"boltzgen",
|
|
437
|
-
help=
|
|
451
|
+
help=argparse.SUPPRESS,
|
|
438
452
|
)
|
|
439
453
|
_add_boltzgen_args(p)
|
|
440
454
|
_add_job_args(p)
|
|
@@ -481,7 +495,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
481
495
|
)
|
|
482
496
|
p.add_argument(
|
|
483
497
|
"--dataset-id",
|
|
498
|
+
"--dataset_id",
|
|
484
499
|
metavar="ID",
|
|
500
|
+
dest="dataset_id",
|
|
485
501
|
help="Associate notes with a dataset and sync to cloud storage",
|
|
486
502
|
)
|
|
487
503
|
p.add_argument(
|
|
@@ -494,17 +510,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
494
510
|
"--no-save", action="store_true", help="Skip saving the report to the local notes file"
|
|
495
511
|
)
|
|
496
512
|
p.add_argument(
|
|
497
|
-
"--
|
|
498
|
-
action="store_true",
|
|
499
|
-
help="Stream results live from Modal SSE endpoint (skips job tracking)",
|
|
500
|
-
)
|
|
501
|
-
p.add_argument(
|
|
502
|
-
"--stream-url",
|
|
513
|
+
"--url",
|
|
503
514
|
metavar="URL",
|
|
504
515
|
default="https://dynotx--research-agent-streaming-fastapi-app.modal.run",
|
|
505
|
-
help="Override the SSE base URL",
|
|
516
|
+
help="Override the SSE streaming base URL",
|
|
506
517
|
)
|
|
507
|
-
|
|
518
|
+
p.add_argument("--out", metavar="DIR", help="Save report and citations to DIR")
|
|
508
519
|
|
|
509
520
|
p = sub.add_parser("notes", help="View accumulated research notes for a dataset")
|
|
510
521
|
p.add_argument(
|
|
@@ -559,6 +570,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
559
570
|
)
|
|
560
571
|
p.add_argument(
|
|
561
572
|
"--dataset-id",
|
|
573
|
+
"--dataset_id",
|
|
574
|
+
dest="dataset_id",
|
|
562
575
|
default=None,
|
|
563
576
|
metavar="ID",
|
|
564
577
|
help="Dataset of PDB/CIF designs (default: cached from last upload or `phi use`)",
|
|
@@ -652,4 +665,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
652
665
|
p.add_argument("--out", metavar="FILE", help="Save scores CSV to file")
|
|
653
666
|
p.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
654
667
|
|
|
668
|
+
# Hide internal/unreleased commands from the usage line.
|
|
669
|
+
_hidden_cmds = {"rfdiffusion3", "design", "boltzgen"}
|
|
670
|
+
sub.metavar = "{" + ",".join(c for c in sub.choices if c not in _hidden_cmds) + "}"
|
|
671
|
+
|
|
655
672
|
return root
|
|
@@ -150,13 +150,14 @@ def _stream_research(
|
|
|
150
150
|
max_turns: int = 15,
|
|
151
151
|
notes_file: Path | None = None,
|
|
152
152
|
dataset_id: str | None = None,
|
|
153
|
+
context: str | None = None,
|
|
153
154
|
) -> None:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
params: dict = {"question": question, "max_turns": max_turns}
|
|
156
|
+
if context:
|
|
157
|
+
params["context"] = context
|
|
158
|
+
url = f"{base_url.rstrip('/')}/research/stream?" + urllib.parse.urlencode(params)
|
|
157
159
|
|
|
158
|
-
console.print(f"\n[{_C_BLUE}]Connecting to research stream…[/]")
|
|
159
|
-
console.print(f"[dim]{url}[/]\n")
|
|
160
|
+
console.print(f"\n[{_C_BLUE}]Connecting to research stream…[/]\n")
|
|
160
161
|
|
|
161
162
|
state = _ResearchStreamState()
|
|
162
163
|
req = urllib.request.Request(url, headers={"Accept": "text/event-stream"})
|
|
@@ -3,6 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
5
5
|
|
|
6
|
+
from phi.commands.research import _is_error_report
|
|
6
7
|
from phi.research import (
|
|
7
8
|
_build_research_section,
|
|
8
9
|
_dispatch_sse_event,
|
|
@@ -85,6 +86,31 @@ def test_build_research_section_includes_citations() -> None:
|
|
|
85
86
|
assert "Sources (2)" in section
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
def test_is_error_report_detects_research_failed() -> None:
|
|
90
|
+
report = "# Research Failed\n\nError: Command failed with exit code 1"
|
|
91
|
+
assert _is_error_report(report) is True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_is_error_report_detects_error_header() -> None:
|
|
95
|
+
assert _is_error_report("# Error\n\nSomething went wrong") is True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_is_error_report_detects_bare_error_prefix() -> None:
|
|
99
|
+
assert _is_error_report("Error: Command failed with exit code 1") is True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_is_error_report_passes_valid_report() -> None:
|
|
103
|
+
assert _is_error_report("# AAV Biology\n\nAAVs are...") is False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_is_error_report_passes_empty() -> None:
|
|
107
|
+
assert _is_error_report("") is False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_is_error_report_handles_leading_whitespace() -> None:
|
|
111
|
+
assert _is_error_report(" # Research Failed\n\nError") is True # strip() normalises whitespace
|
|
112
|
+
|
|
113
|
+
|
|
88
114
|
def test_build_research_section_handles_invalid_citations() -> None:
|
|
89
115
|
section = _build_research_section("query", "report", "not-valid-json")
|
|
90
116
|
assert "query" in section
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from rich.markdown import Markdown
|
|
8
|
-
from rich.panel import Panel
|
|
9
|
-
|
|
10
|
-
import phi.config as config
|
|
11
|
-
from phi.api import _request, _submit
|
|
12
|
-
from phi.display import _C_BLUE, _C_ROSE, _C_SAND, _die, _print_status, _print_submission, console
|
|
13
|
-
from phi.download import _download_job
|
|
14
|
-
from phi.polling import _poll
|
|
15
|
-
from phi.research import _append_research_notes, _stream_research
|
|
16
|
-
from phi.types import PhiApiError
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def cmd_research(args: argparse.Namespace) -> None:
|
|
20
|
-
notes_file: Path | None = None if args.no_save else Path(args.notes_file)
|
|
21
|
-
dataset_id: str | None = args.dataset_id
|
|
22
|
-
|
|
23
|
-
if args.stream:
|
|
24
|
-
_stream_research(
|
|
25
|
-
question=args.question,
|
|
26
|
-
base_url=args.stream_url,
|
|
27
|
-
notes_file=notes_file,
|
|
28
|
-
dataset_id=dataset_id,
|
|
29
|
-
)
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
params: dict = {
|
|
33
|
-
"question": args.question,
|
|
34
|
-
"databases": [d.strip() for d in args.databases.split(",") if d.strip()],
|
|
35
|
-
"max_papers": args.max_papers,
|
|
36
|
-
"include_structures": args.structures,
|
|
37
|
-
}
|
|
38
|
-
if args.target:
|
|
39
|
-
params["target"] = args.target
|
|
40
|
-
|
|
41
|
-
if args.context_file:
|
|
42
|
-
try:
|
|
43
|
-
prior = Path(args.context_file).read_text(encoding="utf-8")
|
|
44
|
-
existing = args.context or ""
|
|
45
|
-
params["context"] = (prior + "\n\n" + existing).strip() if existing else prior
|
|
46
|
-
except Exception as exc:
|
|
47
|
-
console.print(f"[{_C_ROSE}]Warning: could not read context file:[/] {exc}")
|
|
48
|
-
elif args.context:
|
|
49
|
-
params["context"] = args.context
|
|
50
|
-
|
|
51
|
-
result = _submit("research", params, run_id=args.run_id)
|
|
52
|
-
_print_submission(result)
|
|
53
|
-
if args.wait:
|
|
54
|
-
console.print(f"\n[dim]Polling every {config.POLL_INTERVAL}s …[/]")
|
|
55
|
-
final = _poll(result["job_id"])
|
|
56
|
-
_print_status(final)
|
|
57
|
-
|
|
58
|
-
report = (final.get("outputs") or {}).get("report_md")
|
|
59
|
-
citations_raw = (final.get("outputs") or {}).get("citations")
|
|
60
|
-
|
|
61
|
-
if report:
|
|
62
|
-
if notes_file is not None:
|
|
63
|
-
_append_research_notes(
|
|
64
|
-
notes_file=notes_file,
|
|
65
|
-
question=args.question,
|
|
66
|
-
report=report,
|
|
67
|
-
citations_raw=citations_raw,
|
|
68
|
-
dataset_id=dataset_id,
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
if args.out:
|
|
72
|
-
out = Path(args.out)
|
|
73
|
-
out.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
(out / "research_report.md").write_text(report)
|
|
75
|
-
console.print(f"\n[{_C_SAND}]Report written[/] → {out}/research_report.md")
|
|
76
|
-
if citations_raw:
|
|
77
|
-
try:
|
|
78
|
-
citations = json.loads(citations_raw)
|
|
79
|
-
(out / "citations.json").write_text(json.dumps(citations, indent=2))
|
|
80
|
-
console.print(f"[{_C_SAND}]Citations written[/] → {out}/citations.json")
|
|
81
|
-
except Exception:
|
|
82
|
-
pass
|
|
83
|
-
else:
|
|
84
|
-
console.print("\n" + "─" * 60)
|
|
85
|
-
console.print(report)
|
|
86
|
-
if citations_raw:
|
|
87
|
-
try:
|
|
88
|
-
citations = json.loads(citations_raw)
|
|
89
|
-
console.print(f"\n[{_C_BLUE}]Sources ({len(citations)})[/]")
|
|
90
|
-
for i, src in enumerate(citations[:10], 1):
|
|
91
|
-
title = src.get("title") or src.get("pmid") or str(src)
|
|
92
|
-
console.print(f" [{_C_BLUE}]{i}.[/] {title}")
|
|
93
|
-
if len(citations) > 10:
|
|
94
|
-
console.print(f" [dim]… and {len(citations) - 10} more[/]")
|
|
95
|
-
except Exception:
|
|
96
|
-
pass
|
|
97
|
-
elif args.out and final.get("status") == "completed":
|
|
98
|
-
_download_job(final, args.out)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def cmd_notes(args: argparse.Namespace) -> None:
|
|
102
|
-
dataset_id: str = args.dataset_id
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
data = _request("GET", f"/datasets/{dataset_id}/research-notes")
|
|
106
|
-
except PhiApiError as exc:
|
|
107
|
-
_die(str(exc))
|
|
108
|
-
|
|
109
|
-
if not data.get("exists"):
|
|
110
|
-
console.print(f"[{_C_ROSE}]No research notes found[/] for dataset {dataset_id}")
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
content: str = data.get("content") or ""
|
|
114
|
-
data.get("gcs_url")
|
|
115
|
-
data.get("gcs_uri")
|
|
116
|
-
|
|
117
|
-
if args.out:
|
|
118
|
-
out = Path(args.out)
|
|
119
|
-
if out.suffix.lower() == ".md":
|
|
120
|
-
dest = out
|
|
121
|
-
else:
|
|
122
|
-
out.mkdir(parents=True, exist_ok=True)
|
|
123
|
-
dest = out / "research.md"
|
|
124
|
-
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
-
dest.write_text(content, encoding="utf-8")
|
|
126
|
-
console.print(f"[{_C_SAND}]Notes saved[/] → {dest}")
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
if args.json:
|
|
130
|
-
print(json.dumps(data, indent=2))
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
console.print()
|
|
134
|
-
console.print(
|
|
135
|
-
Panel(
|
|
136
|
-
Markdown(content),
|
|
137
|
-
title=f"[{_C_BLUE}]Research Notes[/] — {dataset_id[:8]}…",
|
|
138
|
-
border_style=_C_BLUE,
|
|
139
|
-
padding=(1, 2),
|
|
140
|
-
)
|
|
141
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|