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/cro.py ADDED
@@ -0,0 +1,413 @@
1
+ """
2
+ CRO (Contract Research Organization) tools: search, match, compare, and contact CROs.
3
+
4
+ PLACEHOLDER IMPLEMENTATION: All CRO data comes from a static JSON file bundled with ct.
5
+ This is not a live database — CRO listings, pricing, and turnaround times may be
6
+ outdated or incomplete. A real implementation would integrate with CRO directories
7
+ or vendor APIs. Treat results as illustrative, not authoritative.
8
+ """
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from ct.tools import registry
13
+
14
+
15
+ # Module-level cache for CRO database
16
+ _cro_db_cache = None
17
+
18
+
19
+ def _load_cro_db() -> list[dict]:
20
+ """Load and cache the CRO database from JSON."""
21
+ global _cro_db_cache
22
+ if _cro_db_cache is not None:
23
+ return _cro_db_cache
24
+ db_path = Path(__file__).parent.parent / "data" / "cro_database.json"
25
+ with open(db_path) as f:
26
+ _cro_db_cache = json.load(f)
27
+ return _cro_db_cache
28
+
29
+
30
+ def _score_cro(cro: dict, assay_type: str, target: str | None,
31
+ compound: str | None, species: str, scale: str) -> float:
32
+ """Score a CRO for a specific experiment.
33
+
34
+ Weights:
35
+ - service match: 0.4
36
+ - capability match: 0.3
37
+ - therapeutic area: 0.2
38
+ - specialty match: 0.1
39
+ """
40
+ score = 0.0
41
+
42
+ # Service category match (0.4)
43
+ service_cats = [s["category"] for s in cro.get("services", [])]
44
+ if assay_type in service_cats:
45
+ score += 0.4
46
+ else:
47
+ # Partial match: check if assay_type words appear in any category
48
+ assay_words = set(assay_type.lower().replace("_", " ").split())
49
+ for cat in service_cats:
50
+ cat_words = set(cat.lower().replace("_", " ").split())
51
+ if assay_words & cat_words:
52
+ score += 0.2
53
+ break
54
+
55
+ # Capability match (0.3)
56
+ capabilities_text = " ".join(cro.get("capabilities", [])).lower()
57
+ keywords = []
58
+ if assay_type:
59
+ keywords.extend(assay_type.lower().replace("_", " ").split())
60
+ if target:
61
+ keywords.append(target.lower())
62
+ if compound:
63
+ keywords.append(compound.lower())
64
+ # Add TPD-relevant keywords
65
+ tpd_keywords = ["degradation", "protac", "molecular glue", "tpd",
66
+ "ubiquitin", "e3 ligase", "ternary", "neo-substrate"]
67
+ for kw in keywords:
68
+ if kw in capabilities_text:
69
+ score += 0.15
70
+ break
71
+ for kw in tpd_keywords:
72
+ if kw in capabilities_text:
73
+ score += 0.15
74
+ break
75
+
76
+ # Therapeutic area match (0.2) — oncology is default for TPD
77
+ therapeutic_areas = [t.lower() for t in cro.get("therapeutic_areas", [])]
78
+ if "oncology" in therapeutic_areas:
79
+ score += 0.2
80
+
81
+ # Specialty match (0.1)
82
+ specialties_text = " ".join(cro.get("specialties", [])).lower()
83
+ specialty_keywords = keywords + ["degradation", "tpd", "protac", "molecular glue"]
84
+ for kw in specialty_keywords:
85
+ if kw in specialties_text:
86
+ score += 0.1
87
+ break
88
+
89
+ # Scale bonus: large CROs score slightly higher for large-scale work
90
+ if scale == "large" and cro.get("size") == "large":
91
+ score += 0.05
92
+ elif scale == "small" and cro.get("size") in ("small", "medium"):
93
+ score += 0.05
94
+
95
+ return round(min(score, 1.0), 3)
96
+
97
+
98
+ @registry.register(
99
+ name="cro.search",
100
+ description="Search built-in CRO directory by keyword, service type, or therapeutic area",
101
+ category="cro",
102
+ parameters={
103
+ "query": "Free-text search across CRO names, capabilities, and specialties",
104
+ "service_type": "Filter by service category (e.g. cell_based_assay, structural_biology)",
105
+ "therapeutic_area": "Filter by therapeutic area (e.g. oncology, neuroscience)",
106
+ },
107
+ usage_guide="You need to find CROs that offer a specific service or work in a therapeutic area. Use for initial CRO discovery before match_experiment.",
108
+ )
109
+ def search(query: str, service_type: str = None,
110
+ therapeutic_area: str = None, **kwargs) -> dict:
111
+ """Full-text search across CRO database with optional filtering."""
112
+ db = _load_cro_db()
113
+ query_lower = query.lower()
114
+ results = []
115
+
116
+ for cro in db:
117
+ # Build searchable text
118
+ searchable = " ".join([
119
+ cro["name"],
120
+ " ".join(cro.get("capabilities", [])),
121
+ " ".join(cro.get("specialties", [])),
122
+ " ".join(cro.get("therapeutic_areas", [])),
123
+ ]).lower()
124
+
125
+ if query_lower not in searchable:
126
+ continue
127
+
128
+ # Filter by service_type
129
+ if service_type:
130
+ service_cats = [s["category"] for s in cro.get("services", [])]
131
+ if service_type not in service_cats:
132
+ continue
133
+
134
+ # Filter by therapeutic_area
135
+ if therapeutic_area:
136
+ areas = [t.lower() for t in cro.get("therapeutic_areas", [])]
137
+ if therapeutic_area.lower() not in areas:
138
+ continue
139
+
140
+ results.append({
141
+ "id": cro["id"],
142
+ "name": cro["name"],
143
+ "headquarters": cro["headquarters"],
144
+ "size": cro["size"],
145
+ "capabilities": cro.get("capabilities", []),
146
+ "specialties": cro.get("specialties", []),
147
+ "services": [s["category"] for s in cro.get("services", [])],
148
+ })
149
+
150
+ return {
151
+ "summary": f"[PLACEHOLDER] CRO search for '{query}': {len(results)} matches from static directory (not live data)",
152
+ "placeholder": True,
153
+ "query": query,
154
+ "filters": {"service_type": service_type, "therapeutic_area": therapeutic_area},
155
+ "results": results,
156
+ }
157
+
158
+
159
+ @registry.register(
160
+ name="cro.match_experiment",
161
+ description="Rank CROs from built-in directory by fit for a specific experiment type, target, and compound",
162
+ category="cro",
163
+ parameters={
164
+ "assay_type": "Experiment/assay type (e.g. cell_based_assay, structural_biology, in_vivo_efficacy)",
165
+ "target": "Target protein or gene (optional)",
166
+ "compound": "Compound name or class (optional)",
167
+ "species": "Species for the experiment (default: human)",
168
+ "scale": "Scale: small, medium, large (default: small)",
169
+ },
170
+ usage_guide="You have a specific experiment in mind and need to find the best-fit CRO. Run after experiment.design_assay to match the assay to capable vendors.",
171
+ )
172
+ def match_experiment(assay_type: str, target: str = None,
173
+ compound: str = None, species: str = "human",
174
+ scale: str = "small", **kwargs) -> dict:
175
+ """Score and rank all CROs for a specific experiment."""
176
+ db = _load_cro_db()
177
+ scored = []
178
+
179
+ for cro in db:
180
+ score = _score_cro(cro, assay_type, target, compound, species, scale)
181
+ if score > 0:
182
+ # Find matching service for pricing/turnaround
183
+ matching_service = None
184
+ for svc in cro.get("services", []):
185
+ if svc["category"] == assay_type:
186
+ matching_service = svc
187
+ break
188
+
189
+ entry = {
190
+ "id": cro["id"],
191
+ "name": cro["name"],
192
+ "score": score,
193
+ "headquarters": cro["headquarters"],
194
+ "size": cro["size"],
195
+ "relevant_capabilities": [
196
+ c for c in cro.get("capabilities", [])
197
+ if any(kw in c.lower() for kw in [
198
+ assay_type.lower().replace("_", " "),
199
+ "degradation", "protac", "tpd", "glue",
200
+ ] + ([target.lower()] if target else []))
201
+ ],
202
+ }
203
+ if matching_service:
204
+ entry["turnaround_days"] = matching_service["turnaround_days"]
205
+ entry["price_range"] = matching_service["price_range"]
206
+
207
+ scored.append(entry)
208
+
209
+ scored.sort(key=lambda x: x["score"], reverse=True)
210
+
211
+ top_names = ", ".join(s["name"] for s in scored[:3]) if scored else "none"
212
+ return {
213
+ "summary": (
214
+ f"[PLACEHOLDER] CRO matching for {assay_type}"
215
+ + (f" (target={target})" if target else "")
216
+ + (f" (compound={compound})" if compound else "")
217
+ + f": {len(scored)} CROs scored from static directory (not live data), top matches: {top_names}"
218
+ ),
219
+ "placeholder": True,
220
+ "assay_type": assay_type,
221
+ "target": target,
222
+ "compound": compound,
223
+ "species": species,
224
+ "scale": scale,
225
+ "ranked_cros": scored,
226
+ }
227
+
228
+
229
+ @registry.register(
230
+ name="cro.compare",
231
+ description="Side-by-side comparison of selected CROs from built-in directory on services, pricing, and capabilities",
232
+ category="cro",
233
+ parameters={
234
+ "cro_ids": "List of CRO IDs to compare (e.g. ['reaction-biology', 'promega'])",
235
+ },
236
+ usage_guide="You have shortlisted CROs and need to compare them on services, pricing, and capabilities before making a decision.",
237
+ )
238
+ def compare(cro_ids: list[str], **kwargs) -> dict:
239
+ """Compare selected CROs side-by-side."""
240
+ db = _load_cro_db()
241
+ id_to_cro = {cro["id"]: cro for cro in db}
242
+
243
+ comparisons = []
244
+ not_found = []
245
+
246
+ for cro_id in cro_ids:
247
+ cro = id_to_cro.get(cro_id)
248
+ if not cro:
249
+ not_found.append(cro_id)
250
+ continue
251
+
252
+ services_summary = {
253
+ s["category"]: {
254
+ "turnaround_days": s["turnaround_days"],
255
+ "price_range": s["price_range"],
256
+ }
257
+ for s in cro.get("services", [])
258
+ }
259
+
260
+ comparisons.append({
261
+ "id": cro["id"],
262
+ "name": cro["name"],
263
+ "headquarters": cro["headquarters"],
264
+ "size": cro["size"],
265
+ "website": cro["website"],
266
+ "services": services_summary,
267
+ "capabilities": cro.get("capabilities", []),
268
+ "specialties": cro.get("specialties", []),
269
+ "therapeutic_areas": cro.get("therapeutic_areas", []),
270
+ })
271
+
272
+ # Build comparison matrix for common service categories
273
+ all_categories = set()
274
+ for comp in comparisons:
275
+ all_categories.update(comp["services"].keys())
276
+
277
+ names = [c["name"] for c in comparisons]
278
+ summary = f"[PLACEHOLDER] Comparison of {len(comparisons)} CROs from static directory (not live data): {', '.join(names)}"
279
+ if not_found:
280
+ summary += f" (not found: {', '.join(not_found)})"
281
+
282
+ return {
283
+ "summary": summary,
284
+ "placeholder": True,
285
+ "comparisons": comparisons,
286
+ "service_categories": sorted(all_categories),
287
+ "not_found": not_found,
288
+ }
289
+
290
+
291
+ @registry.register(
292
+ name="cro.draft_inquiry",
293
+ description="Draft a professional inquiry email to a CRO (from built-in directory) for a specific experiment",
294
+ category="cro",
295
+ parameters={
296
+ "cro_id": "CRO identifier (e.g. 'reaction-biology')",
297
+ "experiment_description": "Description of the experiment / assay needed",
298
+ "compound": "Compound name or identifier (optional)",
299
+ "target": "Target protein or gene (optional)",
300
+ "timeline": "Desired timeline (optional, e.g. '3 months')",
301
+ },
302
+ usage_guide="You've selected a CRO and need to draft a professional inquiry email. Run after cro.match_experiment to contact the top-ranked CRO.",
303
+ )
304
+ def draft_inquiry(cro_id: str, experiment_description: str,
305
+ compound: str = None, target: str = None,
306
+ timeline: str = None, **kwargs) -> dict:
307
+ """Generate a professional inquiry email to a CRO."""
308
+ db = _load_cro_db()
309
+ id_to_cro = {cro["id"]: cro for cro in db}
310
+
311
+ cro = id_to_cro.get(cro_id)
312
+ if not cro:
313
+ return {"error": f"CRO '{cro_id}' not found in database", "summary": f"CRO '{cro_id}' not found in database"}
314
+ subject = f"Inquiry: {experiment_description[:60]}"
315
+ if compound:
316
+ subject = f"Inquiry: {experiment_description[:40]} — {compound}"
317
+
318
+ body_parts = [
319
+ f"Dear {cro['name']} Team,",
320
+ "",
321
+ f"I am writing to inquire about your services for the following study:",
322
+ "",
323
+ f"**Experiment:** {experiment_description}",
324
+ ]
325
+ if target:
326
+ body_parts.append(f"**Target:** {target}")
327
+ if compound:
328
+ body_parts.append(f"**Compound:** {compound}")
329
+ if timeline:
330
+ body_parts.append(f"**Desired Timeline:** {timeline}")
331
+
332
+ body_parts.extend([
333
+ "",
334
+ "We are interested in understanding:",
335
+ "1. Your capacity and availability for this type of study",
336
+ "2. Estimated timeline and pricing",
337
+ "3. Any relevant experience with similar projects (particularly in targeted protein degradation)",
338
+ "4. Required compound quantity and format",
339
+ "",
340
+ "Could you please provide a preliminary quote and study outline? We would also welcome a call to discuss the details.",
341
+ "",
342
+ "Thank you for your time.",
343
+ "",
344
+ "Best regards",
345
+ ])
346
+
347
+ body = "\n".join(body_parts)
348
+
349
+ return {
350
+ "summary": f"[PLACEHOLDER] Drafted inquiry to {cro['name']} ({cro['contact_email']}) re: {experiment_description[:50]} — verify CRO contact details before sending",
351
+ "placeholder": True,
352
+ "cro_id": cro_id,
353
+ "cro_name": cro["name"],
354
+ "to_email": cro["contact_email"],
355
+ "subject": subject,
356
+ "body": body,
357
+ }
358
+
359
+
360
+ @registry.register(
361
+ name="cro.send_inquiry",
362
+ description="Send a drafted inquiry email to a CRO (dry_run by default)",
363
+ category="cro",
364
+ parameters={
365
+ "cro_id": "CRO identifier",
366
+ "subject": "Email subject line",
367
+ "body": "Email body text",
368
+ "dry_run": "If True (default), simulate sending without actually delivering",
369
+ },
370
+ usage_guide="You have a finalized inquiry email and want to send it to the CRO. Always runs in dry_run mode unless explicitly overridden.",
371
+ )
372
+ def send_inquiry(cro_id: str, subject: str, body: str,
373
+ dry_run: bool = True, **kwargs) -> dict:
374
+ """Send an inquiry email to a CRO."""
375
+ db = _load_cro_db()
376
+ id_to_cro = {cro["id"]: cro for cro in db}
377
+
378
+ cro = id_to_cro.get(cro_id)
379
+ if not cro:
380
+ return {"error": f"CRO '{cro_id}' not found in database", "summary": f"CRO '{cro_id}' not found in database"}
381
+ to_email = cro["contact_email"]
382
+
383
+ if dry_run:
384
+ return {
385
+ "summary": f"[DRY RUN] Would send email to {cro['name']} ({to_email}): {subject}",
386
+ "dry_run": True,
387
+ "to_email": to_email,
388
+ "cro_name": cro["name"],
389
+ "subject": subject,
390
+ "body": body,
391
+ }
392
+
393
+ # Attempt to send via notification module
394
+ try:
395
+ from ct.tools.notification import send_email
396
+ result = send_email(to=to_email, subject=subject, body=body)
397
+ return {
398
+ "summary": f"Sent inquiry to {cro['name']} ({to_email}): {subject}",
399
+ "dry_run": False,
400
+ "to_email": to_email,
401
+ "cro_name": cro["name"],
402
+ "subject": subject,
403
+ "send_result": result,
404
+ }
405
+ except ImportError:
406
+ return {
407
+ "summary": f"[FAILED] notification module not available — email not sent to {to_email}",
408
+ "dry_run": False,
409
+ "error": "notification.send_email not available. Install notification module or use dry_run=True.",
410
+ "to_email": to_email,
411
+ "subject": subject,
412
+ "body": body,
413
+ }