celltype-cli 0.1.0__py3-none-any.whl

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 (89) hide show
  1. celltype_cli-0.1.0.dist-info/METADATA +267 -0
  2. celltype_cli-0.1.0.dist-info/RECORD +89 -0
  3. celltype_cli-0.1.0.dist-info/WHEEL +4 -0
  4. celltype_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. celltype_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. ct/__init__.py +3 -0
  7. ct/agent/__init__.py +0 -0
  8. ct/agent/case_studies.py +426 -0
  9. ct/agent/config.py +523 -0
  10. ct/agent/doctor.py +544 -0
  11. ct/agent/knowledge.py +523 -0
  12. ct/agent/loop.py +99 -0
  13. ct/agent/mcp_server.py +478 -0
  14. ct/agent/orchestrator.py +733 -0
  15. ct/agent/runner.py +656 -0
  16. ct/agent/sandbox.py +481 -0
  17. ct/agent/session.py +145 -0
  18. ct/agent/system_prompt.py +186 -0
  19. ct/agent/trace_store.py +228 -0
  20. ct/agent/trajectory.py +169 -0
  21. ct/agent/types.py +182 -0
  22. ct/agent/workflows.py +462 -0
  23. ct/api/__init__.py +1 -0
  24. ct/api/app.py +211 -0
  25. ct/api/config.py +120 -0
  26. ct/api/engine.py +124 -0
  27. ct/cli.py +1448 -0
  28. ct/data/__init__.py +0 -0
  29. ct/data/compute_providers.json +59 -0
  30. ct/data/cro_database.json +395 -0
  31. ct/data/downloader.py +238 -0
  32. ct/data/loaders.py +252 -0
  33. ct/kb/__init__.py +5 -0
  34. ct/kb/benchmarks.py +147 -0
  35. ct/kb/governance.py +106 -0
  36. ct/kb/ingest.py +415 -0
  37. ct/kb/reasoning.py +129 -0
  38. ct/kb/schema_monitor.py +162 -0
  39. ct/kb/substrate.py +387 -0
  40. ct/models/__init__.py +0 -0
  41. ct/models/llm.py +370 -0
  42. ct/tools/__init__.py +195 -0
  43. ct/tools/_compound_resolver.py +297 -0
  44. ct/tools/biomarker.py +368 -0
  45. ct/tools/cellxgene.py +282 -0
  46. ct/tools/chemistry.py +1371 -0
  47. ct/tools/claude.py +390 -0
  48. ct/tools/clinical.py +1153 -0
  49. ct/tools/clue.py +249 -0
  50. ct/tools/code.py +1069 -0
  51. ct/tools/combination.py +397 -0
  52. ct/tools/compute.py +402 -0
  53. ct/tools/cro.py +413 -0
  54. ct/tools/data_api.py +2114 -0
  55. ct/tools/design.py +295 -0
  56. ct/tools/dna.py +575 -0
  57. ct/tools/experiment.py +604 -0
  58. ct/tools/expression.py +655 -0
  59. ct/tools/files.py +957 -0
  60. ct/tools/genomics.py +1387 -0
  61. ct/tools/http_client.py +146 -0
  62. ct/tools/imaging.py +319 -0
  63. ct/tools/intel.py +223 -0
  64. ct/tools/literature.py +743 -0
  65. ct/tools/network.py +422 -0
  66. ct/tools/notification.py +111 -0
  67. ct/tools/omics.py +3330 -0
  68. ct/tools/ops.py +1230 -0
  69. ct/tools/parity.py +649 -0
  70. ct/tools/pk.py +245 -0
  71. ct/tools/protein.py +678 -0
  72. ct/tools/regulatory.py +643 -0
  73. ct/tools/remote_data.py +179 -0
  74. ct/tools/report.py +181 -0
  75. ct/tools/repurposing.py +376 -0
  76. ct/tools/safety.py +1280 -0
  77. ct/tools/shell.py +178 -0
  78. ct/tools/singlecell.py +533 -0
  79. ct/tools/statistics.py +552 -0
  80. ct/tools/structure.py +882 -0
  81. ct/tools/target.py +901 -0
  82. ct/tools/translational.py +123 -0
  83. ct/tools/viability.py +218 -0
  84. ct/ui/__init__.py +0 -0
  85. ct/ui/markdown.py +31 -0
  86. ct/ui/status.py +258 -0
  87. ct/ui/suggestions.py +567 -0
  88. ct/ui/terminal.py +1456 -0
  89. ct/ui/traces.py +112 -0
ct/agent/doctor.py ADDED
@@ -0,0 +1,544 @@
1
+ """
2
+ Deployment readiness checks for ct.
3
+
4
+ Used by `ct doctor` and interactive `/doctor` to surface actionable setup issues.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ import logging
9
+ from pathlib import Path
10
+
11
+ from rich.table import Table
12
+
13
+ from ct.agent.config import CONFIG_FILE, Config
14
+ from ct.tools import EXPERIMENTAL_CATEGORIES, ensure_loaded, tool_load_errors
15
+
16
+ logger = logging.getLogger("ct.doctor")
17
+
18
+
19
+ @dataclass
20
+ class DoctorCheck:
21
+ name: str
22
+ status: str # "ok" | "warn" | "error"
23
+ detail: str
24
+
25
+
26
+ def _status_markup(status: str) -> str:
27
+ if status == "ok":
28
+ return "[green]ok[/green]"
29
+ if status == "warn":
30
+ return "[yellow]warn[/yellow]"
31
+ return "[red]error[/red]"
32
+
33
+
34
+ def run_checks(config: Config | None = None, session=None) -> list[DoctorCheck]:
35
+ """Run production-readiness checks and return structured results.
36
+
37
+ Args:
38
+ config: Optional Config instance. Loaded from disk if not provided.
39
+ session: Optional Session instance. When provided, runtime tool health
40
+ data (suppressed tools, failure counts) is included in the report.
41
+ """
42
+ cfg = config or Config.load()
43
+ checks: list[DoctorCheck] = []
44
+
45
+ # 1) Config file readability (best-effort: load already handled parse errors)
46
+ if CONFIG_FILE.exists():
47
+ checks.append(
48
+ DoctorCheck(
49
+ name="config_file",
50
+ status="ok",
51
+ detail=f"Using {CONFIG_FILE}",
52
+ )
53
+ )
54
+ else:
55
+ checks.append(
56
+ DoctorCheck(
57
+ name="config_file",
58
+ status="warn",
59
+ detail=f"No config file yet at {CONFIG_FILE} (defaults/env vars are used)",
60
+ )
61
+ )
62
+
63
+ # 2) LLM configuration readiness
64
+ llm_issue = cfg.llm_preflight_issue()
65
+ provider = cfg.get("llm.provider", "anthropic")
66
+ model = cfg.get("llm.model")
67
+ if llm_issue:
68
+ checks.append(DoctorCheck(name="llm", status="error", detail=llm_issue))
69
+ else:
70
+ checks.append(
71
+ DoctorCheck(
72
+ name="llm",
73
+ status="ok",
74
+ detail=f"provider={provider}, model={model}",
75
+ )
76
+ )
77
+
78
+ # 3) Output directory availability
79
+ out_dir = Path(cfg.get("sandbox.output_dir", str(Path.cwd() / "outputs")))
80
+ try:
81
+ out_dir.mkdir(parents=True, exist_ok=True)
82
+ checks.append(
83
+ DoctorCheck(name="output_dir", status="ok", detail=f"Writable: {out_dir}")
84
+ )
85
+ except OSError as exc:
86
+ checks.append(
87
+ DoctorCheck(name="output_dir", status="error", detail=f"{out_dir}: {exc}")
88
+ )
89
+
90
+ # 4) Data base directory availability
91
+ data_base = Path(cfg.get("data.base", str(Path.home() / ".ct" / "data")))
92
+ try:
93
+ data_base.mkdir(parents=True, exist_ok=True)
94
+ checks.append(
95
+ DoctorCheck(name="data_base", status="ok", detail=f"Writable: {data_base}")
96
+ )
97
+ except OSError as exc:
98
+ checks.append(
99
+ DoctorCheck(name="data_base", status="warn", detail=f"{data_base}: {exc}")
100
+ )
101
+
102
+ # 5) Tool module import health
103
+ ensure_loaded()
104
+ load_errors = tool_load_errors()
105
+ if load_errors:
106
+ sample = ", ".join(sorted(load_errors.keys())[:8])
107
+ extra = "" if len(load_errors) <= 8 else f" (+{len(load_errors) - 8} more)"
108
+ checks.append(
109
+ DoctorCheck(
110
+ name="tool_modules",
111
+ status="warn",
112
+ detail=f"{len(load_errors)} module(s) failed to load: {sample}{extra}",
113
+ )
114
+ )
115
+ else:
116
+ checks.append(
117
+ DoctorCheck(name="tool_modules", status="ok", detail="All tool modules loaded")
118
+ )
119
+
120
+ # 6) Experimental categories planning status
121
+ if cfg.get("agent.enable_experimental_tools", False):
122
+ checks.append(
123
+ DoctorCheck(
124
+ name="experimental_tools",
125
+ status="warn",
126
+ detail=(
127
+ f"Experimental categories enabled for planning: "
128
+ f"{', '.join(sorted(EXPERIMENTAL_CATEGORIES))}"
129
+ ),
130
+ )
131
+ )
132
+ else:
133
+ checks.append(
134
+ DoctorCheck(
135
+ name="experimental_tools",
136
+ status="ok",
137
+ detail=(
138
+ f"Experimental categories hidden from planning by default: "
139
+ f"{', '.join(sorted(EXPERIMENTAL_CATEGORIES))}"
140
+ ),
141
+ )
142
+ )
143
+
144
+ # 7) Grounding guardrail status
145
+ if cfg.get("agent.enforce_grounded_synthesis", True):
146
+ checks.append(
147
+ DoctorCheck(
148
+ name="grounding_guard",
149
+ status="ok",
150
+ detail="Grounded synthesis enforcement is enabled",
151
+ )
152
+ )
153
+ else:
154
+ checks.append(
155
+ DoctorCheck(
156
+ name="grounding_guard",
157
+ status="warn",
158
+ detail="Grounded synthesis enforcement is disabled",
159
+ )
160
+ )
161
+
162
+ # 8) Runtime profile
163
+ profile = str(cfg.get("agent.profile", "research"))
164
+ if profile not in {"research", "pharma", "enterprise"}:
165
+ checks.append(
166
+ DoctorCheck(
167
+ name="runtime_profile",
168
+ status="warn",
169
+ detail=f"Unknown agent.profile '{profile}'",
170
+ )
171
+ )
172
+ else:
173
+ checks.append(
174
+ DoctorCheck(
175
+ name="runtime_profile",
176
+ status="ok",
177
+ detail=f"{profile}",
178
+ )
179
+ )
180
+
181
+ synthesis_style = str(cfg.get("agent.synthesis_style", "standard")).strip().lower()
182
+ if synthesis_style not in {"standard", "pharma"}:
183
+ checks.append(
184
+ DoctorCheck(
185
+ name="synthesis_style",
186
+ status="warn",
187
+ detail=f"Unknown agent.synthesis_style '{synthesis_style}'",
188
+ )
189
+ )
190
+ elif profile == "pharma" and synthesis_style != "pharma":
191
+ checks.append(
192
+ DoctorCheck(
193
+ name="synthesis_style",
194
+ status="warn",
195
+ detail="agent.profile=pharma but synthesis style is not pharma",
196
+ )
197
+ )
198
+ else:
199
+ checks.append(
200
+ DoctorCheck(
201
+ name="synthesis_style",
202
+ status="ok",
203
+ detail=synthesis_style,
204
+ )
205
+ )
206
+
207
+ # 9) Quality gate policy
208
+ if cfg.get("agent.quality_gate_enabled", True):
209
+ strict = bool(cfg.get("agent.quality_gate_strict", False))
210
+ checks.append(
211
+ DoctorCheck(
212
+ name="quality_gate",
213
+ status="ok" if strict else "warn",
214
+ detail=(
215
+ "Strict quality gate enabled (must pass citation/actionability checks)"
216
+ if strict
217
+ else "Quality gate is warn-only (strict mode disabled)"
218
+ ),
219
+ )
220
+ )
221
+ else:
222
+ checks.append(
223
+ DoctorCheck(
224
+ name="quality_gate",
225
+ status="warn",
226
+ detail="Quality gate is disabled",
227
+ )
228
+ )
229
+
230
+ # 10) Enterprise policy layer
231
+ enforce_policy = bool(cfg.get("enterprise.enforce_policy", False))
232
+ checks.append(
233
+ DoctorCheck(
234
+ name="enterprise_policy",
235
+ status="ok" if enforce_policy else "warn",
236
+ detail=(
237
+ "Policy enforcement enabled"
238
+ if enforce_policy
239
+ else "Policy enforcement disabled (research mode)"
240
+ ),
241
+ )
242
+ )
243
+
244
+ # 11) Knowledge substrate path
245
+ substrate_path = Path(
246
+ cfg.get("knowledge.substrate_path", str(Path.home() / ".ct" / "knowledge" / "substrate.json"))
247
+ )
248
+ try:
249
+ substrate_path.parent.mkdir(parents=True, exist_ok=True)
250
+ checks.append(
251
+ DoctorCheck(
252
+ name="knowledge_substrate",
253
+ status="ok",
254
+ detail=f"Writable substrate path: {substrate_path}",
255
+ )
256
+ )
257
+ except OSError as exc:
258
+ checks.append(
259
+ DoctorCheck(
260
+ name="knowledge_substrate",
261
+ status="warn",
262
+ detail=f"Could not prepare substrate path {substrate_path}: {exc}",
263
+ )
264
+ )
265
+
266
+ # 12) Schema monitor readiness
267
+ if cfg.get("knowledge.schema_monitor_enabled", False):
268
+ baseline = Path.home() / ".ct" / "knowledge" / "schema_baselines.json"
269
+ if baseline.exists():
270
+ checks.append(
271
+ DoctorCheck(
272
+ name="schema_monitor",
273
+ status="ok",
274
+ detail=f"Baseline present: {baseline}",
275
+ )
276
+ )
277
+ else:
278
+ checks.append(
279
+ DoctorCheck(
280
+ name="schema_monitor",
281
+ status="warn",
282
+ detail="Schema monitor enabled but no baseline found. Run: ct knowledge schema-update",
283
+ )
284
+ )
285
+ else:
286
+ checks.append(
287
+ DoctorCheck(
288
+ name="schema_monitor",
289
+ status="warn",
290
+ detail="Schema monitor disabled",
291
+ )
292
+ )
293
+
294
+ # 13) Claude Code delegation policy
295
+ if cfg.get("agent.enable_claude_code_tool", False):
296
+ checks.append(
297
+ DoctorCheck(
298
+ name="claude_code_policy",
299
+ status="warn",
300
+ detail="claude.code is enabled for autonomous use (high privilege)",
301
+ )
302
+ )
303
+ else:
304
+ checks.append(
305
+ DoctorCheck(
306
+ name="claude_code_policy",
307
+ status="ok",
308
+ detail="claude.code is disabled by default (opt-in)",
309
+ )
310
+ )
311
+
312
+ # 14) Data availability — verify key datasets can be found
313
+ checks.append(_check_data_availability(cfg))
314
+
315
+ # 15) Downloads directory
316
+ checks.append(_check_downloads_dir())
317
+
318
+ # 16) API connectivity (lightweight HEAD probes)
319
+ checks.extend(_check_api_connectivity())
320
+
321
+ # 17) Runtime tool health
322
+ checks.append(_check_tool_health(session))
323
+
324
+ # 18) Preflight validation config
325
+ if cfg.get("agent.preflight_validation_enabled", True):
326
+ checks.append(
327
+ DoctorCheck(
328
+ name="preflight_validation",
329
+ status="ok",
330
+ detail="Pre-query API key validation is enabled",
331
+ )
332
+ )
333
+ else:
334
+ checks.append(
335
+ DoctorCheck(
336
+ name="preflight_validation",
337
+ status="warn",
338
+ detail="Pre-query API key validation is disabled",
339
+ )
340
+ )
341
+
342
+ return checks
343
+
344
+
345
+ def has_errors(checks: list[DoctorCheck]) -> bool:
346
+ """Return True if any check has error status."""
347
+ return any(c.status == "error" for c in checks)
348
+
349
+
350
+ def to_table(checks: list[DoctorCheck]) -> Table:
351
+ """Render doctor checks as a rich table."""
352
+ table = Table(title="ct Doctor")
353
+ table.add_column("Check", style="cyan")
354
+ table.add_column("Status")
355
+ table.add_column("Details")
356
+
357
+ for check in checks:
358
+ table.add_row(check.name, _status_markup(check.status), check.detail)
359
+
360
+ return table
361
+
362
+
363
+ # ---------------------------------------------------------------------------
364
+ # Runtime health check helpers
365
+ # ---------------------------------------------------------------------------
366
+
367
+ # Key datasets and the file patterns used by loaders
368
+ _KEY_DATASETS = {
369
+ "depmap": ("CRISPRGeneEffect.csv", ["", "depmap"]),
370
+ "prism": ("prism_LFC_COLLAPSED.csv", ["", "prism"]),
371
+ "l1000": ("l1000_landmark_only.parquet", ["", "l1000"]),
372
+ }
373
+
374
+
375
+ def _check_data_availability(cfg: Config) -> DoctorCheck:
376
+ """Check whether key datasets can be found on disk."""
377
+ data_base = Path(cfg.get("data.base", str(Path.home() / ".ct" / "data")))
378
+ search_dirs = [data_base]
379
+ # Also check ct-data sister project
380
+ ct_data = Path.home() / "Projects" / "CellType" / "ct-data"
381
+ if ct_data.exists():
382
+ search_dirs.append(ct_data)
383
+
384
+ found = []
385
+ missing = []
386
+ for name, (filename, subdirs) in _KEY_DATASETS.items():
387
+ located = False
388
+ stem = Path(filename).stem
389
+ for base_dir in search_dirs:
390
+ for sub in subdirs:
391
+ d = base_dir / sub if sub else base_dir
392
+ if (d / filename).exists():
393
+ located = True
394
+ break
395
+ parquet = d / f"{stem}.parquet"
396
+ if parquet.exists():
397
+ located = True
398
+ break
399
+ if located:
400
+ break
401
+ if located:
402
+ found.append(name)
403
+ else:
404
+ missing.append(name)
405
+
406
+ if not missing:
407
+ return DoctorCheck(
408
+ name="data_availability",
409
+ status="ok",
410
+ detail=f"Key datasets found: {', '.join(sorted(found))}",
411
+ )
412
+ if found:
413
+ return DoctorCheck(
414
+ name="data_availability",
415
+ status="warn",
416
+ detail=f"Missing datasets: {', '.join(sorted(missing))} (found: {', '.join(sorted(found))}). Run: ct data pull <name>",
417
+ )
418
+ return DoctorCheck(
419
+ name="data_availability",
420
+ status="warn",
421
+ detail=f"No key datasets found ({', '.join(sorted(missing))}). Run: ct data pull depmap",
422
+ )
423
+
424
+
425
+ def _check_downloads_dir() -> DoctorCheck:
426
+ """Verify ~/.ct/downloads/ exists and is writable."""
427
+ downloads = Path.home() / ".ct" / "downloads"
428
+ try:
429
+ downloads.mkdir(parents=True, exist_ok=True)
430
+ return DoctorCheck(
431
+ name="downloads_dir",
432
+ status="ok",
433
+ detail=f"Writable: {downloads}",
434
+ )
435
+ except OSError as exc:
436
+ return DoctorCheck(
437
+ name="downloads_dir",
438
+ status="warn",
439
+ detail=f"{downloads}: {exc}",
440
+ )
441
+
442
+
443
+ # APIs to probe with HEAD requests (short timeout, best-effort)
444
+ _API_PROBES = [
445
+ ("PubMed eutils", "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi"),
446
+ ("Enrichr", "https://maayanlab.cloud/Enrichr/"),
447
+ ("GDC API", "https://api.gdc.cancer.gov/status"),
448
+ ]
449
+
450
+
451
+ def _check_api_connectivity() -> list[DoctorCheck]:
452
+ """Quick HEAD/GET probe against key public APIs."""
453
+ checks = []
454
+ try:
455
+ import httpx
456
+ except ImportError:
457
+ checks.append(
458
+ DoctorCheck(
459
+ name="api_connectivity",
460
+ status="warn",
461
+ detail="httpx not installed — skipping API connectivity probes",
462
+ )
463
+ )
464
+ return checks
465
+
466
+ reachable = []
467
+ unreachable = []
468
+ for label, url in _API_PROBES:
469
+ try:
470
+ resp = httpx.head(url, timeout=5, follow_redirects=True)
471
+ if resp.status_code < 500:
472
+ reachable.append(label)
473
+ else:
474
+ unreachable.append(f"{label} (HTTP {resp.status_code})")
475
+ except Exception:
476
+ unreachable.append(label)
477
+
478
+ if not unreachable:
479
+ checks.append(
480
+ DoctorCheck(
481
+ name="api_connectivity",
482
+ status="ok",
483
+ detail=f"All probes passed: {', '.join(reachable)}",
484
+ )
485
+ )
486
+ elif reachable:
487
+ checks.append(
488
+ DoctorCheck(
489
+ name="api_connectivity",
490
+ status="warn",
491
+ detail=f"Unreachable: {', '.join(unreachable)} (reachable: {', '.join(reachable)})",
492
+ )
493
+ )
494
+ else:
495
+ checks.append(
496
+ DoctorCheck(
497
+ name="api_connectivity",
498
+ status="warn",
499
+ detail=f"All API probes failed: {', '.join(unreachable)}. Check network connectivity.",
500
+ )
501
+ )
502
+ return checks
503
+
504
+
505
+ def _check_tool_health(session) -> DoctorCheck:
506
+ """Report runtime tool suppression state from session."""
507
+ if session is None:
508
+ return DoctorCheck(
509
+ name="tool_health",
510
+ status="warn",
511
+ detail="No active session context; run /doctor in interactive mode for runtime tool-health diagnostics",
512
+ )
513
+
514
+ suppressed = set()
515
+ failure_counts: dict[str, int] = {}
516
+ if hasattr(session, "tool_health_suppressed_tools"):
517
+ suppressed = session.tool_health_suppressed_tools()
518
+ if hasattr(session, "_tool_health_failures"):
519
+ failure_counts = {
520
+ name: len(timestamps)
521
+ for name, timestamps in session._tool_health_failures.items()
522
+ if timestamps
523
+ }
524
+
525
+ if not suppressed and not failure_counts:
526
+ return DoctorCheck(
527
+ name="tool_health",
528
+ status="ok",
529
+ detail="No tool failures or suppressions in this session",
530
+ )
531
+
532
+ parts = []
533
+ if suppressed:
534
+ parts.append(f"Suppressed: {', '.join(sorted(suppressed))}")
535
+ if failure_counts:
536
+ failing = [f"{n}({c})" for n, c in sorted(failure_counts.items()) if n not in suppressed]
537
+ if failing:
538
+ parts.append(f"Recent failures: {', '.join(failing)}")
539
+
540
+ return DoctorCheck(
541
+ name="tool_health",
542
+ status="warn",
543
+ detail="; ".join(parts),
544
+ )