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/tools/claude.py ADDED
@@ -0,0 +1,390 @@
1
+ """
2
+ Claude reasoning tools and Claude Code integration for ct.
3
+
4
+ Uses Claude as a reasoning engine for open-ended questions that don't fit
5
+ pre-built tools, and delegates complex coding/editing tasks to Claude Code CLI.
6
+ """
7
+
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ from ct.tools import registry
12
+
13
+
14
+ REASON_SYSTEM_PROMPT = """\
15
+ You are a drug discovery research expert embedded in celltype-cli, \
16
+ an autonomous research agent.
17
+
18
+ You are being called as a reasoning tool — the planner determined that \
19
+ this question requires expert scientific reasoning rather than a pre-built \
20
+ computational tool or code execution.
21
+
22
+ {context_section}
23
+
24
+ Guidelines:
25
+ 1. Be specific and quantitative where possible
26
+ 2. Cite mechanisms, pathways, and known biology
27
+ 3. Distinguish between established facts and hypotheses
28
+ 4. If data would strengthen your answer, say which datasets/analyses to run next
29
+ 5. Structure your response with clear sections
30
+ 6. Keep your response focused and under 800 words
31
+ """
32
+
33
+ COMPARE_SYSTEM_PROMPT = """\
34
+ You are a drug discovery research expert embedded in celltype-cli.
35
+
36
+ You are being called to compare and evaluate options. Provide a structured \
37
+ comparison with clear criteria, trade-offs, and a recommendation.
38
+
39
+ {context_section}
40
+
41
+ Guidelines:
42
+ 1. Use a structured format (table or criteria-based comparison)
43
+ 2. Be specific about advantages and disadvantages of each option
44
+ 3. Provide a clear recommendation with rationale
45
+ 4. Note any caveats or conditions that would change the recommendation
46
+ """
47
+
48
+ SUMMARIZE_SYSTEM_PROMPT = """\
49
+ You are a drug discovery research expert embedded in celltype-cli.
50
+
51
+ You are being called to synthesize and summarize information. Distill the \
52
+ key findings into a concise, actionable summary.
53
+
54
+ {context_section}
55
+
56
+ Guidelines:
57
+ 1. Lead with the most important finding
58
+ 2. Use bullet points for clarity
59
+ 3. Highlight actionable next steps
60
+ 4. Note any gaps or uncertainties in the data
61
+ 5. Keep the summary under 500 words
62
+ """
63
+
64
+
65
+ def _build_context_section(prior_results: dict = None) -> str:
66
+ """Format prior step results as context for the reasoning LLM."""
67
+ if not prior_results:
68
+ return ""
69
+
70
+ lines = ["You have access to results from prior analysis steps:\n"]
71
+ for step_id, result in prior_results.items():
72
+ if isinstance(result, dict):
73
+ summary = result.get("summary", str(result)[:500])
74
+ else:
75
+ summary = str(result)[:500]
76
+ lines.append(f"- Step {step_id}: {summary}")
77
+
78
+ return "\n".join(lines)
79
+
80
+
81
+ @registry.register(
82
+ name="claude.reason",
83
+ description="Expert reasoning about drug discovery questions using Claude",
84
+ category="claude",
85
+ parameters={
86
+ "goal": "The question or reasoning task to address",
87
+ "context": "Additional context (e.g., prior findings, constraints)",
88
+ },
89
+ usage_guide=(
90
+ "Use when the query requires expert scientific reasoning, interpretation, "
91
+ "or hypothesis generation that no pre-built tool covers. Good for: "
92
+ "mechanism-of-action reasoning, experimental design advice, literature "
93
+ "interpretation, risk assessment rationale, strategic recommendations. "
94
+ "Do NOT use for tasks that a pre-built tool handles (data retrieval, "
95
+ "similarity search, etc.) — those are faster and cheaper."
96
+ ),
97
+ )
98
+ def reason(goal: str, context: str = "", _session=None,
99
+ _prior_results=None, **kwargs) -> dict:
100
+ """Use Claude for expert reasoning on drug discovery questions."""
101
+ if _session is None:
102
+ return {
103
+ "summary": "Reasoning unavailable: no active session.",
104
+ "error": "No session provided.",
105
+ }
106
+
107
+ llm = _session.get_llm()
108
+ context_section = _build_context_section(_prior_results)
109
+
110
+ user_msg = f"Question: {goal}"
111
+ if context:
112
+ user_msg += f"\n\nAdditional context: {context}"
113
+
114
+ system = REASON_SYSTEM_PROMPT.format(context_section=context_section)
115
+
116
+ from ct.ui.status import ThinkingStatus
117
+ try:
118
+ with ThinkingStatus(_session.console, "reasoning"):
119
+ response = llm.chat(
120
+ system=system,
121
+ messages=[{"role": "user", "content": user_msg}],
122
+ temperature=0.3,
123
+ max_tokens=2048,
124
+ )
125
+ except Exception as e:
126
+ return {
127
+ "summary": f"LLM reasoning failed: {e}",
128
+ "error": str(e),
129
+ }
130
+
131
+ return {
132
+ "summary": response.content,
133
+ "model": getattr(response, "model", "unknown"),
134
+ "usage": getattr(response, "usage", None),
135
+ }
136
+
137
+
138
+ @registry.register(
139
+ name="claude.compare",
140
+ description="Compare and evaluate multiple options (compounds, targets, strategies)",
141
+ category="claude",
142
+ parameters={
143
+ "goal": "What to compare and the decision to make",
144
+ "options": "Comma-separated list of options to compare",
145
+ "criteria": "Evaluation criteria (optional)",
146
+ },
147
+ usage_guide=(
148
+ "Use when the user needs to choose between options — compounds, targets, "
149
+ "indications, strategies, CROs, etc. Provides structured comparison with "
150
+ "a recommendation. Combine with pre-built tools first to gather data, "
151
+ "then use claude.compare to interpret and decide."
152
+ ),
153
+ )
154
+ def compare(goal: str, options: str = "", criteria: str = "",
155
+ _session=None, _prior_results=None, **kwargs) -> dict:
156
+ """Compare multiple options using Claude's reasoning."""
157
+ if _session is None:
158
+ return {
159
+ "summary": "Comparison unavailable: no active session.",
160
+ "error": "No session provided.",
161
+ }
162
+
163
+ llm = _session.get_llm()
164
+ context_section = _build_context_section(_prior_results)
165
+
166
+ user_msg = f"Decision: {goal}"
167
+ if options:
168
+ user_msg += f"\n\nOptions to compare: {options}"
169
+ if criteria:
170
+ user_msg += f"\n\nEvaluation criteria: {criteria}"
171
+
172
+ system = COMPARE_SYSTEM_PROMPT.format(context_section=context_section)
173
+
174
+ from ct.ui.status import ThinkingStatus
175
+ try:
176
+ with ThinkingStatus(_session.console, "comparing"):
177
+ response = llm.chat(
178
+ system=system,
179
+ messages=[{"role": "user", "content": user_msg}],
180
+ temperature=0.2,
181
+ max_tokens=2048,
182
+ )
183
+ except Exception as e:
184
+ return {
185
+ "summary": f"LLM comparison failed: {e}",
186
+ "error": str(e),
187
+ }
188
+
189
+ return {
190
+ "summary": response.content,
191
+ "model": getattr(response, "model", "unknown"),
192
+ "usage": getattr(response, "usage", None),
193
+ }
194
+
195
+
196
+ @registry.register(
197
+ name="claude.summarize",
198
+ description="Synthesize and summarize research findings into actionable insights",
199
+ category="claude",
200
+ parameters={
201
+ "goal": "What to summarize and the intended audience/purpose",
202
+ "content": "Text content to summarize (optional if prior results available)",
203
+ },
204
+ usage_guide=(
205
+ "Use after multiple analysis steps to distill key findings. Good for: "
206
+ "executive summaries, decision briefs, literature synthesis. Typically "
207
+ "used as a final step after data-gathering tools have run."
208
+ ),
209
+ )
210
+ def summarize(goal: str, content: str = "", _session=None,
211
+ _prior_results=None, **kwargs) -> dict:
212
+ """Summarize and synthesize research findings."""
213
+ if _session is None:
214
+ return {
215
+ "summary": "Summarization unavailable: no active session.",
216
+ "error": "No session provided.",
217
+ }
218
+
219
+ llm = _session.get_llm()
220
+ context_section = _build_context_section(_prior_results)
221
+
222
+ user_msg = f"Summarize: {goal}"
223
+ if content:
224
+ user_msg += f"\n\nContent to summarize:\n{content}"
225
+
226
+ system = SUMMARIZE_SYSTEM_PROMPT.format(context_section=context_section)
227
+
228
+ from ct.ui.status import ThinkingStatus
229
+ try:
230
+ with ThinkingStatus(_session.console, "summarizing"):
231
+ response = llm.chat(
232
+ system=system,
233
+ messages=[{"role": "user", "content": user_msg}],
234
+ temperature=0.2,
235
+ max_tokens=1500,
236
+ )
237
+ except Exception as e:
238
+ return {
239
+ "summary": f"LLM summarization failed: {e}",
240
+ "error": str(e),
241
+ }
242
+
243
+ return {
244
+ "summary": response.content,
245
+ "model": getattr(response, "model", "unknown"),
246
+ "usage": getattr(response, "usage", None),
247
+ }
248
+
249
+
250
+ @registry.register(
251
+ name="claude.code",
252
+ description="Delegate a coding task to Claude Code (file editing, refactoring, debugging, test writing)",
253
+ category="claude",
254
+ parameters={
255
+ "task": "Description of the coding task (be specific: which files, what changes, what to test)",
256
+ "allowed_tools": "Claude Code tools to allow (default: 'Read,Edit,Write,Bash,Glob,Grep')",
257
+ "max_budget": "Max spend in USD (default: 1.0)",
258
+ },
259
+ usage_guide=(
260
+ "Use for complex coding tasks that need iterative edit-test-fix cycles: "
261
+ "refactoring code, writing tests, debugging scripts, modifying config files, "
262
+ "building analysis pipelines. Claude Code handles the full read→edit→test→fix loop. "
263
+ "Do NOT use for simple single-shot file reads or edits — use files.* tools instead. "
264
+ "Do NOT use for drug discovery research — use ct's specialized tools."
265
+ ),
266
+ )
267
+ def code(task: str, allowed_tools: str = "Read,Edit,Write,Bash,Glob,Grep",
268
+ max_budget: float = 1.0, _session=None, _prior_results=None,
269
+ **kwargs) -> dict:
270
+ """Delegate a coding task to Claude Code CLI.
271
+
272
+ Spawns `claude -p` in non-interactive mode with permission bypass and
273
+ a budget cap. Returns Claude Code's output as the tool result.
274
+ """
275
+ import os
276
+ import shutil
277
+
278
+ enabled = str(os.environ.get("CT_ENABLE_CLAUDE_CODE", "")).strip().lower() in {
279
+ "1",
280
+ "true",
281
+ "yes",
282
+ }
283
+ if _session is not None and hasattr(_session, "config"):
284
+ enabled = bool(_session.config.get("agent.enable_claude_code_tool", False))
285
+
286
+ if not enabled:
287
+ return {
288
+ "summary": (
289
+ "claude.code is disabled by policy (opt-in). "
290
+ "Enable with: ct config set agent.enable_claude_code_tool true"
291
+ ),
292
+ "error": "disabled_by_policy",
293
+ }
294
+
295
+ claude_path = shutil.which("claude")
296
+ if not claude_path:
297
+ return {
298
+ "summary": "Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code",
299
+ "error": "claude_not_found",
300
+ }
301
+
302
+ # Build context from prior results so Claude Code knows what ct has already done
303
+ context_parts = []
304
+ if _prior_results:
305
+ for step_id, result in _prior_results.items():
306
+ if isinstance(result, dict):
307
+ summary = result.get("summary", "")[:300]
308
+ else:
309
+ summary = str(result)[:300]
310
+ if summary:
311
+ context_parts.append(f"- Step {step_id}: {summary}")
312
+
313
+ full_prompt = task
314
+ if context_parts:
315
+ full_prompt = (
316
+ f"Context from prior research steps:\n"
317
+ + "\n".join(context_parts)
318
+ + f"\n\nTask: {task}"
319
+ )
320
+
321
+ cmd = [
322
+ claude_path,
323
+ "-p", full_prompt,
324
+ "--output-format", "text",
325
+ "--permission-mode", "bypassPermissions",
326
+ "--allowed-tools", allowed_tools,
327
+ "--max-budget-usd", str(max_budget),
328
+ "--no-session-persistence",
329
+ ]
330
+
331
+ try:
332
+ if _session:
333
+ from ct.ui.status import ThinkingStatus
334
+ with ThinkingStatus(_session.console, "coding"):
335
+ result = subprocess.run(
336
+ cmd,
337
+ capture_output=True,
338
+ text=True,
339
+ cwd=str(Path.cwd()),
340
+ timeout=300,
341
+ )
342
+ else:
343
+ result = subprocess.run(
344
+ cmd,
345
+ capture_output=True,
346
+ text=True,
347
+ cwd=str(Path.cwd()),
348
+ timeout=300,
349
+ )
350
+ except subprocess.TimeoutExpired:
351
+ return {
352
+ "summary": "Claude Code timed out after 5 minutes.",
353
+ "error": "timeout",
354
+ }
355
+ except Exception as e:
356
+ return {
357
+ "summary": f"Failed to run Claude Code: {e}",
358
+ "error": str(e),
359
+ }
360
+
361
+ output = result.stdout.strip()
362
+ stderr = result.stderr.strip()
363
+
364
+ # Truncate very long output
365
+ if len(output) > 15000:
366
+ output = output[:15000] + "\n... [truncated]"
367
+
368
+ if result.returncode == 0 and output:
369
+ summary = output[:2000]
370
+ if len(output) > 2000:
371
+ summary += "..."
372
+ return {
373
+ "summary": summary,
374
+ "full_output": output,
375
+ "exit_code": result.returncode,
376
+ }
377
+ elif output:
378
+ return {
379
+ "summary": f"Claude Code finished (exit {result.returncode}): {output[:1000]}",
380
+ "full_output": output,
381
+ "stderr": stderr[:2000] if stderr else "",
382
+ "exit_code": result.returncode,
383
+ }
384
+ else:
385
+ return {
386
+ "summary": f"Claude Code produced no output (exit {result.returncode}). Stderr: {stderr[:500]}",
387
+ "error": "no_output",
388
+ "stderr": stderr[:2000] if stderr else "",
389
+ "exit_code": result.returncode,
390
+ }