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.
- celltype_cli-0.1.0.dist-info/METADATA +267 -0
- celltype_cli-0.1.0.dist-info/RECORD +89 -0
- celltype_cli-0.1.0.dist-info/WHEEL +4 -0
- celltype_cli-0.1.0.dist-info/entry_points.txt +2 -0
- celltype_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ct/__init__.py +3 -0
- ct/agent/__init__.py +0 -0
- ct/agent/case_studies.py +426 -0
- ct/agent/config.py +523 -0
- ct/agent/doctor.py +544 -0
- ct/agent/knowledge.py +523 -0
- ct/agent/loop.py +99 -0
- ct/agent/mcp_server.py +478 -0
- ct/agent/orchestrator.py +733 -0
- ct/agent/runner.py +656 -0
- ct/agent/sandbox.py +481 -0
- ct/agent/session.py +145 -0
- ct/agent/system_prompt.py +186 -0
- ct/agent/trace_store.py +228 -0
- ct/agent/trajectory.py +169 -0
- ct/agent/types.py +182 -0
- ct/agent/workflows.py +462 -0
- ct/api/__init__.py +1 -0
- ct/api/app.py +211 -0
- ct/api/config.py +120 -0
- ct/api/engine.py +124 -0
- ct/cli.py +1448 -0
- ct/data/__init__.py +0 -0
- ct/data/compute_providers.json +59 -0
- ct/data/cro_database.json +395 -0
- ct/data/downloader.py +238 -0
- ct/data/loaders.py +252 -0
- ct/kb/__init__.py +5 -0
- ct/kb/benchmarks.py +147 -0
- ct/kb/governance.py +106 -0
- ct/kb/ingest.py +415 -0
- ct/kb/reasoning.py +129 -0
- ct/kb/schema_monitor.py +162 -0
- ct/kb/substrate.py +387 -0
- ct/models/__init__.py +0 -0
- ct/models/llm.py +370 -0
- ct/tools/__init__.py +195 -0
- ct/tools/_compound_resolver.py +297 -0
- ct/tools/biomarker.py +368 -0
- ct/tools/cellxgene.py +282 -0
- ct/tools/chemistry.py +1371 -0
- ct/tools/claude.py +390 -0
- ct/tools/clinical.py +1153 -0
- ct/tools/clue.py +249 -0
- ct/tools/code.py +1069 -0
- ct/tools/combination.py +397 -0
- ct/tools/compute.py +402 -0
- ct/tools/cro.py +413 -0
- ct/tools/data_api.py +2114 -0
- ct/tools/design.py +295 -0
- ct/tools/dna.py +575 -0
- ct/tools/experiment.py +604 -0
- ct/tools/expression.py +655 -0
- ct/tools/files.py +957 -0
- ct/tools/genomics.py +1387 -0
- ct/tools/http_client.py +146 -0
- ct/tools/imaging.py +319 -0
- ct/tools/intel.py +223 -0
- ct/tools/literature.py +743 -0
- ct/tools/network.py +422 -0
- ct/tools/notification.py +111 -0
- ct/tools/omics.py +3330 -0
- ct/tools/ops.py +1230 -0
- ct/tools/parity.py +649 -0
- ct/tools/pk.py +245 -0
- ct/tools/protein.py +678 -0
- ct/tools/regulatory.py +643 -0
- ct/tools/remote_data.py +179 -0
- ct/tools/report.py +181 -0
- ct/tools/repurposing.py +376 -0
- ct/tools/safety.py +1280 -0
- ct/tools/shell.py +178 -0
- ct/tools/singlecell.py +533 -0
- ct/tools/statistics.py +552 -0
- ct/tools/structure.py +882 -0
- ct/tools/target.py +901 -0
- ct/tools/translational.py +123 -0
- ct/tools/viability.py +218 -0
- ct/ui/__init__.py +0 -0
- ct/ui/markdown.py +31 -0
- ct/ui/status.py +258 -0
- ct/ui/suggestions.py +567 -0
- ct/ui/terminal.py +1456 -0
- 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
|
+
}
|