dyno-phi 0.2.2__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.
Files changed (77) hide show
  1. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/.claude/skills/phi/SKILL.md +0 -2
  2. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/CHANGELOG.md +13 -0
  3. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/CLI-REFERENCE.md +0 -1
  4. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/PKG-INFO +1 -1
  5. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/phi-plugin/skills/phi/SKILL.md +0 -2
  6. dyno_phi-0.2.3/src/phi/_version.py +1 -0
  7. dyno_phi-0.2.3/src/phi/commands/research.py +90 -0
  8. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/parser.py +27 -10
  9. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/research.py +6 -5
  10. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/test_research.py +26 -0
  11. dyno_phi-0.2.2/src/phi/_version.py +0 -1
  12. dyno_phi-0.2.2/src/phi/commands/research.py +0 -141
  13. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/.claude/settings.json +0 -0
  14. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/.claude-plugin/marketplace.json +0 -0
  15. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/.github/workflows/ci.yml +0 -0
  16. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/.github/workflows/publish.yml +0 -0
  17. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/.gitignore +0 -0
  18. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/.pre-commit-config.yaml +0 -0
  19. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/LICENSE +0 -0
  20. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/README.md +0 -0
  21. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/TEMPLATE_NEW_BIOMODAL.py +0 -0
  22. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/__init__.py +0 -0
  23. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/base.py +0 -0
  24. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_af2rank.py +0 -0
  25. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_align_structures.py +0 -0
  26. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_alphafold.py +0 -0
  27. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_bindcraft.py +0 -0
  28. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_boltz.py +0 -0
  29. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_boltzgen.py +0 -0
  30. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_chai1.py +0 -0
  31. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_esm2_predict_masked.py +0 -0
  32. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_esmfold.py +0 -0
  33. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_ligandmpnn.py +0 -0
  34. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_proteinmpnn.py +0 -0
  35. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_rf3.py +0 -0
  36. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_rfdiffusion3.py +0 -0
  37. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_rso.py +0 -0
  38. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/modal_tm_score.py +0 -0
  39. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/utils/__init__.py +0 -0
  40. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/utils/extract_chain_as_mol2.py +0 -0
  41. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/biomodals/utils/extract_ligands.py +0 -0
  42. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/README.md +0 -0
  43. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_009.pdb +0 -0
  44. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_047.pdb +0 -0
  45. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_106.pdb +0 -0
  46. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_188.pdb +0 -0
  47. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/binders/pdl1/binder_246.pdb +0 -0
  48. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/fixtures/pdl1/5O45.pdb +0 -0
  49. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/examples/fixtures/pdl1/pdl1_pdb_19_132.pdb +0 -0
  50. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/phi-plugin/.claude-plugin/plugin.json +0 -0
  51. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/phi-plugin/README.md +0 -0
  52. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/pyproject.toml +0 -0
  53. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/__init__.py +0 -0
  54. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/api.py +0 -0
  55. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/cli.py +0 -0
  56. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/__init__.py +0 -0
  57. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/auth.py +0 -0
  58. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/datasets.py +0 -0
  59. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/filter.py +0 -0
  60. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/jobs.py +0 -0
  61. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/models.py +0 -0
  62. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/structure.py +0 -0
  63. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/commands/tutorial.py +0 -0
  64. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/config.py +0 -0
  65. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/display.py +0 -0
  66. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/download.py +0 -0
  67. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/polling.py +0 -0
  68. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/structure.py +0 -0
  69. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/types.py +0 -0
  70. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/src/phi/upload.py +0 -0
  71. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/__init__.py +0 -0
  72. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/test_api.py +0 -0
  73. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/test_config.py +0 -0
  74. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/test_display.py +0 -0
  75. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/test_download.py +0 -0
  76. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/test_structure.py +0 -0
  77. {dyno_phi-0.2.2 → dyno_phi-0.2.3}/tests/test_upload.py +0 -0
@@ -63,8 +63,6 @@ phi research \
63
63
  --dataset-id <ID>
64
64
 
65
65
  # Stream results live
66
- phi research --question "..." --stream
67
-
68
66
  # Append notes to a local file and cloud storage
69
67
  phi research --question "..." --notes-file ./research.md --dataset-id <ID>
70
68
 
@@ -7,6 +7,19 @@ 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
+
10
23
  ## [0.2.2] - 2026-03-17
11
24
 
12
25
  ### Added
@@ -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.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
@@ -63,8 +63,6 @@ phi research \
63
63
  --dataset-id <ID>
64
64
 
65
65
  # Stream results live
66
- phi research --question "..." --stream
67
-
68
66
  # Append notes to a local file and cloud storage
69
67
  phi research --question "..." --notes-file ./research.md --dataset-id <ID>
70
68
 
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -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="All-atom backbone generation: binder design, de novo, motif scaffolding (2–5 min/design)",
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="All-atom generative design from a YAML spec — proteins, peptides, small molecules (10–20 min)",
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
- "--stream",
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
- _add_job_args(p)
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
- url = f"{base_url.rstrip('/')}/research/stream?" + urllib.parse.urlencode(
155
- {"question": question, "max_turns": max_turns}
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