dyno-phi 0.2.0__tar.gz → 0.2.3__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.0 → dyno_phi-0.2.3}/.claude/skills/phi/SKILL.md +0 -2
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/CHANGELOG.md +23 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/CLI-REFERENCE.md +1 -2
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/PKG-INFO +18 -26
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/README.md +17 -25
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/phi-plugin/skills/phi/SKILL.md +0 -2
- dyno_phi-0.2.3/src/phi/_version.py +1 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/cli.py +2 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/commands/models.py +12 -0
- dyno_phi-0.2.3/src/phi/commands/research.py +90 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/commands/structure.py +2 -4
- dyno_phi-0.2.3/src/phi/commands/tutorial.py +88 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/display.py +2 -1
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/parser.py +48 -19
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/research.py +6 -5
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/test_research.py +26 -0
- dyno_phi-0.2.0/src/phi/_version.py +0 -1
- dyno_phi-0.2.0/src/phi/commands/research.py +0 -145
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/.claude/settings.json +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/.claude-plugin/marketplace.json +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/.github/workflows/ci.yml +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/.github/workflows/publish.yml +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/.gitignore +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/.pre-commit-config.yaml +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/LICENSE +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/TEMPLATE_NEW_BIOMODAL.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/__init__.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/base.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_af2rank.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_align_structures.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_alphafold.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_bindcraft.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_boltz.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_boltzgen.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_chai1.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_esm2_predict_masked.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_esmfold.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_ligandmpnn.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_proteinmpnn.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_rf3.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_rfdiffusion3.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_rso.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/modal_tm_score.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/utils/__init__.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/utils/extract_chain_as_mol2.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/biomodals/utils/extract_ligands.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/README.md +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_009.pdb +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_047.pdb +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_106.pdb +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_188.pdb +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_246.pdb +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/fixtures/pdl1/5O45.pdb +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/examples/fixtures/pdl1/pdl1_pdb_19_132.pdb +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/phi-plugin/.claude-plugin/plugin.json +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/phi-plugin/README.md +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/pyproject.toml +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/__init__.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/api.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/commands/__init__.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/commands/auth.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/commands/datasets.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/commands/filter.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/commands/jobs.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/config.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/download.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/polling.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/structure.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/types.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/src/phi/upload.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/__init__.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/test_api.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/test_config.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/test_display.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/test_download.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/test_structure.py +0 -0
- {dyno_phi-0.2.0 → dyno_phi-0.2.3}/tests/test_upload.py +0 -0
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.3] - 2026-03-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `phi research` now accepts `--dataset_id` (underscore) as an alias for `--dataset-id` — consistent with all other commands
|
|
14
|
+
- `phi notes` command implemented: fetches and displays accumulated research notes for a dataset
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `phi design` (`rfdiffusion3`) and `phi boltzgen` no longer appear in `phi --help` or usage line — they remain accessible internally via `DYNO_ENABLE_DESIGN=1`
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Missing imports (`_request`, `PhiApiError`, `Panel`, `Markdown`, `_C_BLUE`, `_C_SAND`, `json`) in `phi notes` command
|
|
21
|
+
- mypy `Cannot assign to a method` error replaced with a proper `_FilteredSubParsersAction` subclass
|
|
22
|
+
|
|
23
|
+
## [0.2.2] - 2026-03-17
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `phi tutorial` — downloads example PD-L1 binder structures from the API and prints a step-by-step scoring walkthrough
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- GCS internal paths (`gs://...`) no longer shown in status output, research notes, or `phi fetch` results
|
|
30
|
+
- `phi design` / `phi boltzgen` feature-flagged behind `DYNO_ENABLE_DESIGN=1` env var
|
|
31
|
+
- macOS SSL certificate error when downloading tutorial files via signed URLs
|
|
32
|
+
|
|
10
33
|
## [0.2.0] - 2026-03-17
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -5,7 +5,7 @@ Submit and monitor computational biology jobs, manage datasets, run structure
|
|
|
5
5
|
prediction and inverse-folding pipelines, and download results — all from your
|
|
6
6
|
terminal.
|
|
7
7
|
|
|
8
|
-
**Version:** 0.2.
|
|
8
|
+
**Version:** 0.2.2 · **Package:** `dyno-phi` · **Requires:** Python ≥ 3.9
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -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
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dyno-phi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -97,43 +97,34 @@ phi login
|
|
|
97
97
|
|
|
98
98
|
## Quick start
|
|
99
99
|
|
|
100
|
-
###
|
|
100
|
+
### Try the tutorial
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
phi folding --fasta sequences.fasta
|
|
105
|
-
|
|
106
|
-
# Complex structure prediction (AlphaFold2 multimer)
|
|
107
|
-
phi complex_folding --fasta binder_target.fasta
|
|
102
|
+
The fastest way to get started — downloads five example PD-L1 binder structures
|
|
103
|
+
and walks you through the full pipeline:
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
phi
|
|
105
|
+
```bash
|
|
106
|
+
phi tutorial
|
|
111
107
|
```
|
|
112
108
|
|
|
113
|
-
|
|
109
|
+
This fetches the example files, prints step-by-step instructions, and leaves
|
|
110
|
+
you ready to run `phi filter`.
|
|
111
|
+
|
|
112
|
+
### Scoring your own structures
|
|
114
113
|
|
|
115
114
|
```bash
|
|
116
|
-
# 1. Upload
|
|
115
|
+
# 1. Upload PDB/CIF files
|
|
117
116
|
phi upload ./designs/
|
|
118
117
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
# Dashboard: https://design.dynotx.com/dashboard/datasets/d7c3a1b2-...
|
|
122
|
-
# Run a job against this dataset:
|
|
123
|
-
# phi folding --dataset-id d7c3a1b2-...
|
|
124
|
-
# phi complex_folding --dataset-id d7c3a1b2-...
|
|
125
|
-
# phi inverse_folding --dataset-id d7c3a1b2-...
|
|
126
|
-
# phi filter --dataset-id d7c3a1b2-... --preset default --wait
|
|
127
|
-
|
|
128
|
-
# 2. Run the full filter pipeline (inverse folding → folding → complex folding → score)
|
|
129
|
-
phi filter --dataset-id d7c3a1b2-... --preset default --wait
|
|
118
|
+
# 2. Run the full filter pipeline
|
|
119
|
+
phi filter --preset default --wait
|
|
130
120
|
|
|
131
|
-
# 3.
|
|
121
|
+
# 3. View scores and download results
|
|
122
|
+
phi scores
|
|
132
123
|
phi download --out ./results/
|
|
133
124
|
```
|
|
134
125
|
|
|
135
|
-
After each command, `phi` prints the active dataset and
|
|
136
|
-
|
|
126
|
+
After each command, `phi` prints the active dataset and a link to the
|
|
127
|
+
dashboard:
|
|
137
128
|
|
|
138
129
|
```
|
|
139
130
|
Active: dataset [d7c3a1b2-...] · job [cb4553f5-...]
|
|
@@ -146,6 +137,7 @@ Dashboard: https://design.dynotx.com/dashboard/datasets/d7c3a1b2-...
|
|
|
146
137
|
|
|
147
138
|
| Command | Alias | Description |
|
|
148
139
|
|---|---|---|
|
|
140
|
+
| `phi tutorial` | — | Download example structures and print a step-by-step walkthrough |
|
|
149
141
|
| `phi login` | — | Verify API key and print identity |
|
|
150
142
|
| `phi upload` | — | Upload PDB/CIF files or a directory |
|
|
151
143
|
| `phi fetch` | — | Download a structure from RCSB PDB or AlphaFold DB, crop, and optionally upload |
|
|
@@ -56,43 +56,34 @@ phi login
|
|
|
56
56
|
|
|
57
57
|
## Quick start
|
|
58
58
|
|
|
59
|
-
###
|
|
59
|
+
### Try the tutorial
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
phi folding --fasta sequences.fasta
|
|
64
|
-
|
|
65
|
-
# Complex structure prediction (AlphaFold2 multimer)
|
|
66
|
-
phi complex_folding --fasta binder_target.fasta
|
|
61
|
+
The fastest way to get started — downloads five example PD-L1 binder structures
|
|
62
|
+
and walks you through the full pipeline:
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
phi
|
|
64
|
+
```bash
|
|
65
|
+
phi tutorial
|
|
70
66
|
```
|
|
71
67
|
|
|
72
|
-
|
|
68
|
+
This fetches the example files, prints step-by-step instructions, and leaves
|
|
69
|
+
you ready to run `phi filter`.
|
|
70
|
+
|
|
71
|
+
### Scoring your own structures
|
|
73
72
|
|
|
74
73
|
```bash
|
|
75
|
-
# 1. Upload
|
|
74
|
+
# 1. Upload PDB/CIF files
|
|
76
75
|
phi upload ./designs/
|
|
77
76
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
# Dashboard: https://design.dynotx.com/dashboard/datasets/d7c3a1b2-...
|
|
81
|
-
# Run a job against this dataset:
|
|
82
|
-
# phi folding --dataset-id d7c3a1b2-...
|
|
83
|
-
# phi complex_folding --dataset-id d7c3a1b2-...
|
|
84
|
-
# phi inverse_folding --dataset-id d7c3a1b2-...
|
|
85
|
-
# phi filter --dataset-id d7c3a1b2-... --preset default --wait
|
|
86
|
-
|
|
87
|
-
# 2. Run the full filter pipeline (inverse folding → folding → complex folding → score)
|
|
88
|
-
phi filter --dataset-id d7c3a1b2-... --preset default --wait
|
|
77
|
+
# 2. Run the full filter pipeline
|
|
78
|
+
phi filter --preset default --wait
|
|
89
79
|
|
|
90
|
-
# 3.
|
|
80
|
+
# 3. View scores and download results
|
|
81
|
+
phi scores
|
|
91
82
|
phi download --out ./results/
|
|
92
83
|
```
|
|
93
84
|
|
|
94
|
-
After each command, `phi` prints the active dataset and
|
|
95
|
-
|
|
85
|
+
After each command, `phi` prints the active dataset and a link to the
|
|
86
|
+
dashboard:
|
|
96
87
|
|
|
97
88
|
```
|
|
98
89
|
Active: dataset [d7c3a1b2-...] · job [cb4553f5-...]
|
|
@@ -105,6 +96,7 @@ Dashboard: https://design.dynotx.com/dashboard/datasets/d7c3a1b2-...
|
|
|
105
96
|
|
|
106
97
|
| Command | Alias | Description |
|
|
107
98
|
|---|---|---|
|
|
99
|
+
| `phi tutorial` | — | Download example structures and print a step-by-step walkthrough |
|
|
108
100
|
| `phi login` | — | Verify API key and print identity |
|
|
109
101
|
| `phi upload` | — | Upload PDB/CIF files or a directory |
|
|
110
102
|
| `phi fetch` | — | Download a structure from RCSB PDB or AlphaFold DB, crop, and optionally upload |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -16,6 +16,7 @@ from phi.commands.models import (
|
|
|
16
16
|
)
|
|
17
17
|
from phi.commands.research import cmd_notes, cmd_research
|
|
18
18
|
from phi.commands.structure import cmd_fetch
|
|
19
|
+
from phi.commands.tutorial import cmd_tutorial
|
|
19
20
|
from phi.config import _load_state
|
|
20
21
|
from phi.display import _C_BLUE, _die, console
|
|
21
22
|
from phi.parser import build_parser
|
|
@@ -23,6 +24,7 @@ from phi.types import PhiApiError
|
|
|
23
24
|
|
|
24
25
|
COMMANDS = {
|
|
25
26
|
"login": cmd_login,
|
|
27
|
+
"tutorial": cmd_tutorial,
|
|
26
28
|
"upload": cmd_upload,
|
|
27
29
|
"ingest-session": cmd_ingest_session,
|
|
28
30
|
"datasets": cmd_datasets,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
4
|
+
import os
|
|
2
5
|
from pathlib import Path
|
|
3
6
|
|
|
4
7
|
from phi.api import _require_key, _submit
|
|
@@ -6,6 +9,13 @@ from phi.display import _die, _print_status, _print_submission, console
|
|
|
6
9
|
from phi.download import _download_job, _read_fasta
|
|
7
10
|
from phi.polling import _poll
|
|
8
11
|
|
|
12
|
+
_DESIGN_ENABLED = os.environ.get("DYNO_ENABLE_DESIGN", "").lower() in ("1", "true", "yes")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require_design_flag() -> None:
|
|
16
|
+
if not _DESIGN_ENABLED:
|
|
17
|
+
_die("This command is not yet available.")
|
|
18
|
+
|
|
9
19
|
|
|
10
20
|
def _run_model_job(job_type: str, params: dict, args: argparse.Namespace) -> None:
|
|
11
21
|
from phi.config import POLL_INTERVAL as _INTERVAL
|
|
@@ -92,6 +102,7 @@ def cmd_boltz(args: argparse.Namespace) -> None:
|
|
|
92
102
|
|
|
93
103
|
|
|
94
104
|
def cmd_rfdiffusion3(args: argparse.Namespace) -> None:
|
|
105
|
+
_require_design_flag()
|
|
95
106
|
params: dict = {
|
|
96
107
|
"num_designs": args.num_designs,
|
|
97
108
|
"inference_steps": args.steps,
|
|
@@ -129,6 +140,7 @@ def cmd_rfdiffusion3(args: argparse.Namespace) -> None:
|
|
|
129
140
|
|
|
130
141
|
|
|
131
142
|
def cmd_boltzgen(args: argparse.Namespace) -> None:
|
|
143
|
+
_require_design_flag()
|
|
132
144
|
params: dict = {
|
|
133
145
|
"protocol": args.protocol,
|
|
134
146
|
"num_designs": args.num_designs,
|
|
@@ -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
|
+
)
|
|
@@ -153,14 +153,12 @@ def _fetch_and_upload(
|
|
|
153
153
|
("Source ", source_url),
|
|
154
154
|
("File ", str(out_path)),
|
|
155
155
|
("Dataset ", dataset_id),
|
|
156
|
-
("GCS URI ", gcs_uri),
|
|
157
156
|
]:
|
|
158
157
|
console.print(f" [bold]{label}[/bold] {value}")
|
|
159
158
|
console.print()
|
|
160
159
|
console.print(" [dim]Next steps:[/dim]")
|
|
161
|
-
console.print(
|
|
162
|
-
|
|
163
|
-
)
|
|
160
|
+
console.print(f" [dim] phi upload {out_path}[/dim]")
|
|
161
|
+
console.print(" [dim] phi filter --preset default --wait[/dim]")
|
|
164
162
|
console.rule()
|
|
165
163
|
|
|
166
164
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import urllib.request
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from phi.api import _request
|
|
8
|
+
from phi.config import _save_state, _ssl_context
|
|
9
|
+
from phi.display import _C_BLUE, _C_SAND, _die, console
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cmd_tutorial(args: argparse.Namespace) -> None:
|
|
13
|
+
out = Path(args.out)
|
|
14
|
+
|
|
15
|
+
# ── 1. Fetch manifest (standard Clerk JWT auth, same as all endpoints) ───
|
|
16
|
+
console.print("[dim]Fetching tutorial dataset …[/]")
|
|
17
|
+
try:
|
|
18
|
+
manifest = _request("GET", "/tutorial")
|
|
19
|
+
except Exception as exc:
|
|
20
|
+
_die(
|
|
21
|
+
f"Could not reach the tutorial endpoint: {exc}\n"
|
|
22
|
+
" Check your connection and API key, then try again."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
files: list[dict] = manifest.get("files", [])
|
|
26
|
+
dataset_id: str | None = manifest.get("dataset_id")
|
|
27
|
+
message: str | None = manifest.get("message")
|
|
28
|
+
|
|
29
|
+
if not files:
|
|
30
|
+
_die("No tutorial files returned by the API.")
|
|
31
|
+
|
|
32
|
+
# ── 2. Download each file (plain HTTP — signed URLs are self-authenticating)
|
|
33
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
console.print(f" Downloading {len(files)} file(s) to [{_C_BLUE}]{out}/[/] …\n")
|
|
35
|
+
|
|
36
|
+
for entry in files:
|
|
37
|
+
filename: str = entry["filename"]
|
|
38
|
+
url: str = entry["url"]
|
|
39
|
+
dest = out / filename
|
|
40
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
try:
|
|
42
|
+
req = urllib.request.Request(url)
|
|
43
|
+
with urllib.request.urlopen(req, context=_ssl_context()) as resp:
|
|
44
|
+
dest.write_bytes(resp.read())
|
|
45
|
+
console.print(f" [bold {_C_SAND}]✓[/] {filename}")
|
|
46
|
+
except Exception as exc:
|
|
47
|
+
_die(f"Failed to download {filename}: {exc}")
|
|
48
|
+
|
|
49
|
+
# ── 3. Cache dataset_id so phi filter needs zero extra flags ─────────────
|
|
50
|
+
if dataset_id:
|
|
51
|
+
_save_state({"dataset_id": dataset_id})
|
|
52
|
+
console.print(
|
|
53
|
+
f"\n[dim]dataset_id [{_C_BLUE}]{dataset_id}[/] cached — "
|
|
54
|
+
f"run [bold]phi filter[/] to start scoring.[/]"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ── 4. Print step-by-step guide ──────────────────────────────────────────
|
|
58
|
+
if message:
|
|
59
|
+
console.print(f"\n[dim]{message}[/]")
|
|
60
|
+
|
|
61
|
+
if dataset_id:
|
|
62
|
+
upload_step = "[dim] (skipped — dataset already ready)[/]"
|
|
63
|
+
else:
|
|
64
|
+
upload_step = f" [{_C_SAND}]phi upload {out}/[/]"
|
|
65
|
+
|
|
66
|
+
console.print(f"""
|
|
67
|
+
[bold]── Tutorial: PD-L1 binder scoring pipeline ──────────────────[/]
|
|
68
|
+
|
|
69
|
+
You have {len(files)} example binder structures in [{_C_BLUE}]{out}/[/].
|
|
70
|
+
|
|
71
|
+
[bold]Step 1 — Upload[/]
|
|
72
|
+
{upload_step}
|
|
73
|
+
|
|
74
|
+
[bold]Step 2 — Run the filter pipeline[/]
|
|
75
|
+
[{_C_SAND}]phi filter --preset default --wait[/]
|
|
76
|
+
|
|
77
|
+
Runs: ProteinMPNN → ESMFold → AlphaFold2 → score
|
|
78
|
+
Typical runtime: 10–30 min for {len(files)} structures.
|
|
79
|
+
|
|
80
|
+
[bold]Step 3 — View scores[/]
|
|
81
|
+
[{_C_SAND}]phi scores[/]
|
|
82
|
+
|
|
83
|
+
[bold]Step 4 — Download results[/]
|
|
84
|
+
[{_C_SAND}]phi download --out ./results[/]
|
|
85
|
+
|
|
86
|
+
[bold]Dashboard[/]
|
|
87
|
+
[{_C_BLUE}]https://design.dynotx.com/dashboard[/]
|
|
88
|
+
""")
|
|
@@ -272,7 +272,8 @@ def _print_status(s: dict) -> None:
|
|
|
272
272
|
table.add_column("filename", style=_C_BLUE, no_wrap=True)
|
|
273
273
|
table.add_column("type", style="dim")
|
|
274
274
|
for f in files[:10]:
|
|
275
|
-
|
|
275
|
+
raw = f.get("filename") or f.get("gcs_url", "?")
|
|
276
|
+
fname = raw.split("/")[-1] if raw.startswith("gs://") else raw
|
|
276
277
|
ftype = f.get("artifact_type", "")
|
|
277
278
|
table.add_row(fname, ftype)
|
|
278
279
|
if len(files) > 10:
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
2
4
|
|
|
3
5
|
from phi._version import __version__
|
|
4
6
|
from phi.config import _FILTER_PRESETS, POLL_INTERVAL
|
|
5
7
|
|
|
6
8
|
_CLI_EPILOG = """\
|
|
9
|
+
Quick start:
|
|
10
|
+
phi tutorial # download example structures + print step-by-step guide
|
|
11
|
+
phi filter --preset default --wait
|
|
12
|
+
phi scores
|
|
13
|
+
phi download --out ./results
|
|
14
|
+
|
|
7
15
|
Fetch and prepare target structures:
|
|
8
16
|
phi fetch --pdb 4ZQK --chain A --residues 56-290 --out target.pdb
|
|
9
17
|
phi fetch --uniprot Q9NZQ7 --trim-low-confidence 70 --upload
|
|
10
18
|
|
|
11
|
-
Design (backbone generation):
|
|
12
|
-
phi design --target-pdb target.pdb --hotspots A45,A67 --num-designs 50
|
|
13
|
-
phi design --length 80 --num-designs 20
|
|
14
|
-
phi boltzgen --yaml design.yaml --protocol protein-anything --num-designs 10
|
|
15
|
-
|
|
16
19
|
Validation (fold + score):
|
|
17
20
|
phi esmfold --fasta sequences.fasta
|
|
18
21
|
phi alphafold --fasta complex.fasta
|
|
@@ -20,10 +23,10 @@ Validation (fold + score):
|
|
|
20
23
|
phi esm2 --fasta sequences.fasta
|
|
21
24
|
phi boltz --fasta complex.fasta
|
|
22
25
|
|
|
23
|
-
Batch filter pipeline
|
|
24
|
-
phi upload
|
|
25
|
-
phi filter --
|
|
26
|
-
phi download
|
|
26
|
+
Batch filter pipeline:
|
|
27
|
+
phi upload ./designs/
|
|
28
|
+
phi filter --preset default --wait
|
|
29
|
+
phi download --out ./results
|
|
27
30
|
|
|
28
31
|
Dataset management:
|
|
29
32
|
phi datasets # list your datasets
|
|
@@ -43,6 +46,8 @@ def _add_fasta_args(p: argparse.ArgumentParser) -> None:
|
|
|
43
46
|
g.add_argument("--fasta-str", metavar="FASTA", help="FASTA content as a string (for scripting)")
|
|
44
47
|
g.add_argument(
|
|
45
48
|
"--dataset-id",
|
|
49
|
+
"--dataset_id",
|
|
50
|
+
dest="dataset_id",
|
|
46
51
|
metavar="DATASET_ID",
|
|
47
52
|
help="Pre-ingested dataset ID (for batch runs of 100–50,000 files)",
|
|
48
53
|
)
|
|
@@ -54,6 +59,8 @@ def _add_pdb_args(p: argparse.ArgumentParser) -> None:
|
|
|
54
59
|
g.add_argument("--pdb-gcs", metavar="URI", help="Cloud storage URI to PDB (gs://…)")
|
|
55
60
|
g.add_argument(
|
|
56
61
|
"--dataset-id",
|
|
62
|
+
"--dataset_id",
|
|
63
|
+
dest="dataset_id",
|
|
57
64
|
metavar="DATASET_ID",
|
|
58
65
|
help="Pre-ingested dataset ID (for batch runs of 100–50,000 files)",
|
|
59
66
|
)
|
|
@@ -232,6 +239,13 @@ def _add_boltzgen_args(p: argparse.ArgumentParser) -> None:
|
|
|
232
239
|
)
|
|
233
240
|
|
|
234
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
|
+
|
|
235
249
|
def build_parser() -> argparse.ArgumentParser:
|
|
236
250
|
root = argparse.ArgumentParser(
|
|
237
251
|
prog="phi",
|
|
@@ -251,8 +265,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
251
265
|
metavar="S",
|
|
252
266
|
help=f"Seconds between status-poll requests (default: {POLL_INTERVAL})",
|
|
253
267
|
)
|
|
268
|
+
root.register("action", "parsers", _FilteredSubParsersAction)
|
|
254
269
|
sub = root.add_subparsers(dest="command", required=True)
|
|
255
270
|
|
|
271
|
+
p = sub.add_parser(
|
|
272
|
+
"tutorial",
|
|
273
|
+
help="Download example structures and print a step-by-step scoring walkthrough",
|
|
274
|
+
)
|
|
275
|
+
p.add_argument(
|
|
276
|
+
"--out",
|
|
277
|
+
metavar="DIR",
|
|
278
|
+
default="examples",
|
|
279
|
+
help="Directory to download example files into (default: ./examples)",
|
|
280
|
+
)
|
|
281
|
+
|
|
256
282
|
p = sub.add_parser("login", help="Verify API key and print connection + identity details")
|
|
257
283
|
p.add_argument("--json", action="store_true")
|
|
258
284
|
|
|
@@ -415,14 +441,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
415
441
|
p = sub.add_parser(
|
|
416
442
|
"rfdiffusion3",
|
|
417
443
|
aliases=["design"],
|
|
418
|
-
help=
|
|
444
|
+
help=argparse.SUPPRESS,
|
|
419
445
|
)
|
|
420
446
|
_add_rfdiffusion3_args(p)
|
|
421
447
|
_add_job_args(p)
|
|
422
448
|
|
|
423
449
|
p = sub.add_parser(
|
|
424
450
|
"boltzgen",
|
|
425
|
-
help=
|
|
451
|
+
help=argparse.SUPPRESS,
|
|
426
452
|
)
|
|
427
453
|
_add_boltzgen_args(p)
|
|
428
454
|
_add_job_args(p)
|
|
@@ -469,7 +495,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
469
495
|
)
|
|
470
496
|
p.add_argument(
|
|
471
497
|
"--dataset-id",
|
|
498
|
+
"--dataset_id",
|
|
472
499
|
metavar="ID",
|
|
500
|
+
dest="dataset_id",
|
|
473
501
|
help="Associate notes with a dataset and sync to cloud storage",
|
|
474
502
|
)
|
|
475
503
|
p.add_argument(
|
|
@@ -482,17 +510,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
482
510
|
"--no-save", action="store_true", help="Skip saving the report to the local notes file"
|
|
483
511
|
)
|
|
484
512
|
p.add_argument(
|
|
485
|
-
"--
|
|
486
|
-
action="store_true",
|
|
487
|
-
help="Stream results live from Modal SSE endpoint (skips job tracking)",
|
|
488
|
-
)
|
|
489
|
-
p.add_argument(
|
|
490
|
-
"--stream-url",
|
|
513
|
+
"--url",
|
|
491
514
|
metavar="URL",
|
|
492
515
|
default="https://dynotx--research-agent-streaming-fastapi-app.modal.run",
|
|
493
|
-
help="Override the SSE base URL",
|
|
516
|
+
help="Override the SSE streaming base URL",
|
|
494
517
|
)
|
|
495
|
-
|
|
518
|
+
p.add_argument("--out", metavar="DIR", help="Save report and citations to DIR")
|
|
496
519
|
|
|
497
520
|
p = sub.add_parser("notes", help="View accumulated research notes for a dataset")
|
|
498
521
|
p.add_argument(
|
|
@@ -547,6 +570,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
547
570
|
)
|
|
548
571
|
p.add_argument(
|
|
549
572
|
"--dataset-id",
|
|
573
|
+
"--dataset_id",
|
|
574
|
+
dest="dataset_id",
|
|
550
575
|
default=None,
|
|
551
576
|
metavar="ID",
|
|
552
577
|
help="Dataset of PDB/CIF designs (default: cached from last upload or `phi use`)",
|
|
@@ -640,4 +665,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
640
665
|
p.add_argument("--out", metavar="FILE", help="Save scores CSV to file")
|
|
641
666
|
p.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
642
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
|
+
|
|
643
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.0"
|
|
@@ -1,145 +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
|
-
gcs_url: str | None = data.get("gcs_url")
|
|
115
|
-
gcs_uri: str | None = 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
|
-
if gcs_url:
|
|
128
|
-
console.print(f"[dim]Download URL:[/] {gcs_url}")
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
if args.json:
|
|
132
|
-
print(json.dumps(data, indent=2))
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
console.print()
|
|
136
|
-
console.print(
|
|
137
|
-
Panel(
|
|
138
|
-
Markdown(content),
|
|
139
|
-
title=f"[{_C_BLUE}]Research Notes[/] — {dataset_id[:8]}…",
|
|
140
|
-
border_style=_C_BLUE,
|
|
141
|
-
padding=(1, 2),
|
|
142
|
-
)
|
|
143
|
-
)
|
|
144
|
-
if gcs_uri:
|
|
145
|
-
console.print(f"[dim]Storage URI:[/] {gcs_uri}")
|
|
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
|