J.A.R.V.I.S. 与 PBMC3k#
本教程演示如何使用带有项目技能(Skills)的 ov.Agent 分析 PBMC3k 数据。智能体采用基于 LLM 的技能匹配与渐进式加载,自动发现技能并为您的分析智能选择相关指导。
新特性:基于 LLM 的技能匹配#
智能体现在使用纯 LLM 推理来匹配技能(Claude Code 方式):
无算法路由 — LLM 读取技能描述并理解语义意图
渐进式加载 — 启动时仅加载名称 + 描述,完整内容按需加载
更高准确率 — 理解自然语言变体(例如,”QC my data” → 预处理技能)
启动速度提升 20 倍 — 仅 1.5K tokens,而非完整加载的 25K
技能存放在哪里?#
内置技能(25 个技能)随 omicverse 安装于:
<omicverse-installation>/omicverse/.claude/skills/
您也可以在项目目录中创建自定义技能:
<your-project>/.claude/skills/
自定义技能将覆盖同名的内置技能。
前提条件#
已在当前环境中安装 omicverse
已在环境变量中设置服务商 API 密钥(例如
OPENAI_API_KEY、ANTHROPIC_API_KEY、GEMINI_API_KEY)技能:25 个内置技能从
omicverse/.claude/skills/的 omicverse 包安装路径自动加载
技能发现:智能体从两个位置加载技能:
包安装目录(优先):
<omicverse-install>/omicverse/.claude/skills/(25 个内置技能)当前目录(可选):
<your-project>/.claude/skills/(您的自定义技能)项目目录中的自定义技能可以覆盖内置技能。
本版本新增:技能采用渐进式加载——启动时仅加载元数据(名称 + 描述),LLM 匹配到技能请求时才懒加载完整内容。
提示:
print(ov.list_supported_models())可查看支持的模型和所需的环境变量。
import sys, omicverse as ov; print(sys.executable); print(ov.__version__, ov.__file__)
/Users/kq_m3m/anaconda3/envs/ovagent101/bin/python
1.7.9rc1 /Users/kq_m3m/PycharmProjects/ovagent101/OV_DEV/omicverse/omicverse/__init__.py
import os
from pathlib import Path
import scanpy as sc
import omicverse as ov
print('OmicVerse version:', getattr(ov, '__version__', 'unknown'))
print(ov.list_supported_models())
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '')
if not OPENAI_API_KEY:
print('Warning: set OPENAI_API_KEY (or relevant provider key) before running live requests.')
# Nice plotting defaults
sc.settings.set_figure_params(dpi=100)
OmicVerse version: 1.7.9rc1
🤖 Supported Models:
**Openai**:
• `gpt-5`: OpenAI GPT-5 (Latest) ❌
• `gpt-5-mini`: OpenAI GPT-5 Mini ❌
• `gpt-5-nano`: OpenAI GPT-5 Nano ❌
... and 13 more models
**Anthropic**:
• `anthropic/claude-opus-4-1-20250805`: Claude Opus 4.1 (Latest) ❌
• `anthropic/claude-opus-4-20250514`: Claude Opus 4 ❌
• `anthropic/claude-sonnet-4-20250514`: Claude Sonnet 4 ❌
... and 5 more models
**Google**:
• `gemini/gemini-2.5-pro`: Gemini 2.5 Pro ❌
• `gemini/gemini-2.5-flash`: Gemini 2.5 Flash ❌
• `gemini/gemini-2.0-pro`: Gemini 2.0 Pro ❌
... and 2 more models
**Deepseek**:
• `deepseek/deepseek-chat`: DeepSeek Chat ❌
• `deepseek/deepseek-reasoner`: DeepSeek Reasoner ❌
**Qwen**:
• `qwq-plus`: QwQ Plus (Reasoning) ❌
• `qwen-max`: Qwen Max (Latest) ❌
• `qwen-max-latest`: Qwen Max Latest ❌
... and 2 more models
**Moonshot**:
• `moonshot/kimi-k2-0711-preview`: Kimi K2 (Preview) ❌
• `moonshot/kimi-k2-turbo-preview`: Kimi K2 Turbo (Preview) ❌
• `moonshot/kimi-latest`: Kimi Latest (Auto Context) ❌
... and 3 more models
**Grok**:
• `grok/grok-beta`: Grok Beta ❌
• `grok/grok-2`: Grok 2 ❌
**Zhipu Ai**:
• `zhipu/glm-4.5`: GLM-4.5 (Zhipu AI - Latest) ❌
• `zhipu/glm-4.5-air`: GLM-4.5 Air (Zhipu AI - Latest) ❌
• `zhipu/glm-4.5-flash`: GLM-4.5 Flash (Zhipu AI - Latest) ❌
... and 4 more models
Legend: ✅ API key available | ❌ API key missing
💡 Usage: `agent = ov.Agent(model='model_id', api_key='your_key')`
Warning: set OPENAI_API_KEY (or relevant provider key) before running live requests.
加载 PBMC 数据集(含离线回退)#
尝试 scanpy.datasets.pbmc3k();若不可用,则回退至 pbmc68k_reduced 或本地 PBMC3K_PATH。
adata = None
local_path = os.environ.get('PBMC3K_PATH')
if local_path and os.path.exists(local_path):
adata = sc.read_h5ad(local_path)
print('Loaded local PBMC3k from:', local_path)
else:
try:
adata = sc.datasets.pbmc3k()
print('Loaded Scanpy pbmc3k dataset')
except Exception as e:
print('pbmc3k not available:', e)
try:
adata = sc.datasets.pbmc68k_reduced()
print('Loaded fallback pbmc68k_reduced dataset')
except Exception as e2:
raise RuntimeError('Could not load a PBMC dataset. Set PBMC3K_PATH to a local .h5ad file.') from e2
adata
Loaded Scanpy pbmc3k dataset
AnnData object with n_obs × n_vars = 2700 × 32738
var: 'gene_ids'
初始化 ov.Agent(技能自动加载)#
选择支持的模型并确保设置了正确的环境变量。智能体将自动加载项目技能并将其纳入规划。
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '')
# Choose a supported model (ensure matching env var is set)
model_id = 'gpt-5' # see ov.list_supported_models()
api_key = OPENAI_API_KEY or os.getenv('ANTHROPIC_API_KEY') or os.getenv('GEMINI_API_KEY')
agent = ov.Agent(model=model_id, api_key=api_key)
agent
Initializing OmicVerse Smart Agent (internal backend)...
🧭 Loaded 23 skills (23 built-in)
Model: OpenAI GPT-5 (Latest)
Provider: Openai
Endpoint: https://api.openai.com/v1
✅ Openai API key available
📚 Function registry loaded: 110 functions in 7 categories
✅ Smart Agent initialized successfully!
<omicverse.utils.smart_agent.OmicVerseAgent at 0x36c55c350>
项目技能预览#
内置技能位于 <omicverse-installation>/omicverse/.claude/skills/。
技能采用渐进式加载:
启动时:仅从 25 个内置技能中加载轻量级元数据(名称 + 描述)
匹配时:LLM 读取描述并使用语义理解选择相关技能
按需加载:仅在需要时懒加载完整技能内容(指令)
下面我们将检查技能元数据并演示新的基于 LLM 的匹配机制。
from pathlib import Path
from omicverse.utils.skill_registry import build_multi_path_skill_registry
# Build registry with progressive disclosure
pkg_root = Path(ov.__file__).resolve().parents[1]
cwd = Path.cwd()
# Show where skills are loaded from
builtin_skill_path = pkg_root / 'omicverse' / '.claude' / 'skills'
custom_skill_path = cwd / '.claude' / 'skills'
print('📂 Skill Discovery Paths:')
print(f' Built-in: {builtin_skill_path}')
print(f' {"✅ Exists" if builtin_skill_path.exists() else "❌ Not found"}')
print(f' Custom: {custom_skill_path}')
print(f' {"✅ Exists" if custom_skill_path.exists() else "❌ Not found (optional)"}')
print()
# Load skills
reg = build_multi_path_skill_registry(pkg_root, cwd)
print(f'✅ Discovered {len(reg.skill_metadata)} skills (progressive disclosure)')
print(f' Only loaded: name + description (~30-50 tokens each)')
print(f' Full content: lazy-loaded when matched by LLM\n')
# Show first 10 skill metadata
print('First 10 skills (metadata only):')
for slug in sorted(reg.skill_metadata.keys())[:10]:
metadata = reg.skill_metadata[slug]
print(f' • {slug}')
print(f' └─ {metadata.description[:80]}...')
print('\n' + '='*70)
print('🆕 LLM-Based Skill Matching (replaces algorithmic routing)')
print('='*70)
print('The agent now uses pure LLM reasoning to match skills:')
print(' 1. LLM reads all skill descriptions from omicverse/.claude/skills/')
print(' 2. LLM analyzes your request semantically')
print(' 3. LLM selects relevant skills using language understanding')
print(' 4. Agent lazy-loads full content for matched skills only')
print('\nOld SkillRouter (keyword matching) is deprecated but kept for compatibility.')
print('You will see "🎯 LLM matched skills:" in agent output.\n')
自然语言流水线(LLM 引导的技能匹配)#
我们将通过自然语言驱动典型的工作流。请留意**”🎯 LLM matched skills:”**输出,它展示了纯 LLM 推理所选择的技能。
智能体将:
对您的请求进行语义分析
使用 LLM(而非关键词)匹配相关技能
为匹配到的技能懒加载完整内容
将技能指导融入代码生成
工作流步骤:
质量控制(过滤细胞/基因)
预处理和高变基因选择
聚类(Leiden)
计算 UMAP 并可视化
# 1) Quality control, guided by single-preprocessing skill
adata = agent.run('quality control with nUMI>500, mito<0.2', adata)
# 2) Preprocessing + HVGs
adata = agent.run('preprocess with 2000 highly variable genes using shiftlog|pearson', adata)
# 3) Clustering
adata = agent.run('leiden clustering resolution=1.0', adata)
# 4) UMAP + visualization (agent may also handle plotting)
adata = agent.run('compute umap and plot colored by leiden', adata)
adata
🎯 Matched project skills:
- Bulk RNA-seq deconvolution with Bulk2Single (score=0.160)
- STRING protein interaction analysis with omicverse (score=0.138)
🤔 LLM analyzing request: 'quality control with nUMI>500, mito<0.2'...
💭 LLM response:
--------------------------------------------------
import omicverse as ov
# Execute quality control with extracted thresholds
adata = ov.pp.qc(adata, tresh={'mito_perc': 0.2, 'nUMIs': 500, 'detected_genes': 250})
print("QC completed. Dataset shape: " + str(adata.shape[0]) + " cells × " + str(adata.shape[1]) + " genes")
--------------------------------------------------
🧬 Generated code to execute:
==================================================
import omicverse as ov
# Execute quality control with extracted thresholds
adata = ov.pp.qc(adata, tresh={'mito_perc': 0.2, 'nUMIs': 500, 'detected_genes': 250})
print("QC completed. Dataset shape: " + str(adata.shape[0]) + " cells × " + str(adata.shape[1]) + " genes")
==================================================
⚡ Executing code locally...
🖥️ Using CPU mode for QC...
📊 Step 1: Calculating QC Metrics
✓ Gene Family Detection:
┌──────────────────────────────┬────────────────────┬────────────────────┐
│ Gene Family │ Genes Found │ Detection Method │
├──────────────────────────────┼────────────────────┼────────────────────┤
│ Mitochondrial │ 13 │ Auto (MT-) │
├──────────────────────────────┼────────────────────┼────────────────────┤
│ Ribosomal │ 106 │ Auto (RPS/RPL) │
├──────────────────────────────┼────────────────────┼────────────────────┤
│ Hemoglobin │ 13 │ Auto (regex) │
└──────────────────────────────┴────────────────────┴────────────────────┘
✓ QC Metrics Summary:
┌─────────────────────────┬────────────────────┬─────────────────────────┐
│ Metric │ Mean │ Range (Min - Max) │
├─────────────────────────┼────────────────────┼─────────────────────────┤
│ nUMIs │ 2367 │ 548 - 15844 │
├─────────────────────────┼────────────────────┼─────────────────────────┤
│ Detected Genes │ 847 │ 212 - 3422 │
├─────────────────────────┼────────────────────┼─────────────────────────┤
│ Mitochondrial % │ 2.2% │ 0.0% - 22.6% │
├─────────────────────────┼────────────────────┼─────────────────────────┤
│ Ribosomal % │ 34.9% │ 1.1% - 59.4% │
├─────────────────────────┼────────────────────┼─────────────────────────┤
│ Hemoglobin % │ 0.0% │ 0.0% - 1.4% │
└─────────────────────────┴────────────────────┴─────────────────────────┘
📈 Original cell count: 2,700
🔧 Step 2: Quality Filtering (SEURAT)
Thresholds: mito≤0.2, nUMIs≥500, genes≥250
📊 Seurat Filter Results:
• nUMIs filter (≥500): 0 cells failed (0.0%)
• Genes filter (≥250): 3 cells failed (0.1%)
• Mitochondrial filter (≤0.2): 2 cells failed (0.1%)
✓ Filters applied successfully
✓ Combined QC filters: 5 cells removed (0.2%)
🎯 Step 3: Final Filtering
Parameters: min_genes=200, min_cells=3
Ratios: max_genes_ratio=1, max_cells_ratio=1
✓ Final filtering: 0 cells, 19,024 genes removed
🔍 Step 4: Doublet Detection
⚠️ Note: 'scrublet' detection is too old and may not work properly
💡 Consider using 'doublets_method=sccomposite' for better results
🔍 Running scrublet doublet detection...
❌ Error executing generated code: threshold is None and thus scrublet requires skimage, but skimage is not installed.
Code that failed: import omicverse as ov
# Execute quality control with extracted thresholds
adata = ov.pp.qc(adata, tresh={'mito_perc': 0.2, 'nUMIs': 500, 'detected_genes': 250})
print("QC completed. Dataset shape: " + str(adata.shape[0]) + " cells × " + str(adata.shape[1]) + " genes")
🎯 Matched project skills:
- Data Transformation (Universal) (score=0.164)
- PDF Report Generation (Universal) (score=0.152)
🤔 LLM analyzing request: 'preprocess with 2000 highly variable genes using shiftlog|pearson'...
💭 LLM response:
--------------------------------------------------
import omicverse as ov
# Preprocess with 2000 highly variable genes using shiftlog|pearson
ov.pp.preprocess(adata, mode='shiftlog|pearson', n_HVGs=2000)
print("Preprocessing completed with mode='shiftlog|pearson' and n_HVGs=2000.")
print(f"Dataset shape after preprocessing: {adata.shape[0]} cells × {adata.shape[1]} genes")
--------------------------------------------------
🧬 Generated code to execute:
==================================================
import omicverse as ov
# Preprocess with 2000 highly variable genes using shiftlog|pearson
ov.pp.preprocess(adata, mode='shiftlog|pearson', n_HVGs=2000)
print("Preprocessing completed with mode='shiftlog|pearson' and n_HVGs=2000.")
print(f"Dataset shape after preprocessing: {adata.shape[0]} cells × {adata.shape[1]} genes")
==================================================
⚡ Executing code locally...
🔍 [2025-11-06 04:02:04] Running preprocessing in 'cpu' mode...
Begin robust gene identification
After filtration, 16634/32738 genes are kept.
Among 16634 genes, 14702 genes are robust.
✅ Robust gene identification completed successfully.
Begin size normalization: shiftlog and HVGs selection pearson
🔍 Count Normalization:
Target sum: 500000.0
Exclude highly expressed: True
Max fraction threshold: 0.2
⚠️ Excluding 0 highly-expressed genes from normalization computation
Excluded genes: []
✅ Count Normalization Completed Successfully!
✓ Processed: 2,700 cells × 14,702 genes
✓ Runtime: 0.04s
🔍 Highly Variable Genes Selection (Experimental):
Method: pearson_residuals
Target genes: 2,000
Theta (overdispersion): 100
✅ Experimental HVG Selection Completed Successfully!
✓ Selected: 2,000 highly variable genes out of 14,702 total (13.6%)
✓ Results added to AnnData object:
• 'highly_variable': Boolean vector (adata.var)
• 'highly_variable_rank': Float vector (adata.var)
• 'highly_variable_nbatches': Int vector (adata.var)
• 'highly_variable_intersection': Boolean vector (adata.var)
• 'means': Float vector (adata.var)
• 'variances': Float vector (adata.var)
• 'residual_variances': Float vector (adata.var)
Time to analyze data in cpu: 0.13 seconds.
✅ Preprocessing completed successfully.
Added:
'highly_variable_features', boolean vector (adata.var)
'means', float vector (adata.var)
'variances', float vector (adata.var)
'residual_variances', float vector (adata.var)
'counts', raw counts layer (adata.layers)
End of size normalization: shiftlog and HVGs selection pearson
Preprocessing completed with mode='shiftlog|pearson' and n_HVGs=2000.
Dataset shape after preprocessing: 2700 cells × 16634 genes
✅ Code executed successfully!
📊 Result shape: 2700 cells × 16634 genes
🎯 Matched project skills:
- Single-cell clustering and batch correction with omicverse (score=0.166)
🤔 LLM analyzing request: 'leiden clustering resolution=1.0'...
💭 LLM response:
--------------------------------------------------
import omicverse as ov
# Ensure neighborhood graph is computed
if 'neighbors' not in getattr(adata, 'uns', {}):
ov.pp.neighbors(adata, n_neighbors=15)
# Perform Leiden clustering with specified resolution
ov.pp.leiden(adata, resolution=1.0)
print(f"Leiden clustering completed at resolution=1.0. Found {adata.obs['leiden'].nunique()} clusters.")
print(f"Dataset shape: {adata.n_obs} cells × {adata.n_vars} genes")
--------------------------------------------------
🧬 Generated code to execute:
==================================================
import omicverse as ov
# Ensure neighborhood graph is computed
if 'neighbors' not in getattr(adata, 'uns', {}):
ov.pp.neighbors(adata, n_neighbors=15)
# Perform Leiden clustering with specified resolution
ov.pp.leiden(adata, resolution=1.0)
print(f"Leiden clustering completed at resolution=1.0. Found {adata.obs['leiden'].nunique()} clusters.")
print(f"Dataset shape: {adata.n_obs} cells × {adata.n_vars} genes")
==================================================
⚡ Executing code locally...
❌ Error executing generated code: name 'getattr' is not defined
Code that failed: import omicverse as ov
# Ensure neighborhood graph is computed
if 'neighbors' not in getattr(adata, 'uns', {}):
ov.pp.neighbors(adata, n_neighbors=15)
# Perform Leiden clustering with specified resolution
ov.pp.leiden(adata, resolution=1.0)
print(f"Leiden clustering completed at resolution=1.0. Found {adata.obs['leiden'].nunique()} clusters.")
print(f"Dataset shape: {adata.n_obs} cells × {adata.n_vars} genes")
🎯 Matched project skills:
- BulkTrajBlend trajectory interpolation (score=0.169)
- OmicVerse visualization for bulk, color systems, and single-cell data (score=0.150)
🤔 LLM analyzing request: 'compute umap and plot colored by leiden'...
💭 LLM response:
--------------------------------------------------
import omicverse as ov
# Scale, PCA, neighbors, and UMAP
ov.pp.scale(adata, max_value=10)
ov.pp.pca(adata, n_pcs=50)
ov.pp.neighbors(adata, n_neighbors=15, n_pcs=50)
ov.pp.umap(adata)
print(f"UMAP computed. Dataset shape: {adata.shape[0]} cells × {adata.shape[1]} genes")
# Plot UMAP colored by leiden
ov.pl.embedding(adata, basis='X_umap', color='leiden')
print("UMAP plot colored by 'leiden' rendered successfully.")
--------------------------------------------------
🧬 Generated code to execute:
==================================================
import omicverse as ov
# Scale, PCA, neighbors, and UMAP
ov.pp.scale(adata, max_value=10)
ov.pp.pca(adata, n_pcs=50)
ov.pp.neighbors(adata, n_neighbors=15, n_pcs=50)
ov.pp.umap(adata)
print(f"UMAP computed. Dataset shape: {adata.shape[0]} cells × {adata.shape[1]} genes")
# Plot UMAP colored by leiden
ov.pl.embedding(adata, basis='X_umap', color='leiden')
print("UMAP plot colored by 'leiden' rendered successfully.")
==================================================
⚡ Executing code locally...
🖥️ sklearn PCA backend: CPU computation
🖥️ Using Scanpy CPU to calculate neighbors...
🔍 K-Nearest Neighbors Graph Construction:
Mode: cpu
Neighbors: 15
Method: umap
Metric: euclidean
PCs used: 50
🔍 Computing neighbor distances...
🔍 Computing connectivity matrix...
💡 Using UMAP-style connectivity
✓ Graph is fully connected
✅ KNN Graph Construction Completed Successfully!
✓ Processed: 2,700 cells with 15 neighbors each
✓ Results added to AnnData object:
• 'neighbors': Neighbors metadata (adata.uns)
• 'distances': Distance matrix (adata.obsp)
• 'connectivities': Connectivity matrix (adata.obsp)
🔍 [2025-11-06 04:04:52] Running UMAP in 'cpu' mode...
🖥️ Using Scanpy CPU UMAP...
🔍 UMAP Dimensionality Reduction:
Mode: cpu
Method: umap
Components: 2
Min distance: 0.5
{'n_neighbors': 15, 'method': 'umap', 'random_state': 0, 'metric': 'euclidean', 'n_pcs': 50}
🔍 Computing UMAP parameters...
🔍 Computing UMAP embedding (classic method)...
✅ UMAP Dimensionality Reduction Completed Successfully!
✓ Embedding shape: 2,700 cells × 2 dimensions
✓ Results added to AnnData object:
• 'X_umap': UMAP coordinates (adata.obsm)
• 'umap': UMAP parameters (adata.uns)
✅ UMAP completed successfully.
UMAP computed. Dataset shape: 2700 cells × 16634 genes
❌ Error executing generated code: "Could not find 'leiden' in adata.obs or adata.var_names"
Code that failed: import omicverse as ov
# Scale, PCA, neighbors, and UMAP
ov.pp.scale(adata, max_value=10)
ov.pp.pca(adata, n_pcs=50)
ov.pp.neighbors(adata, n_neighbors=15, n_pcs=50)
ov.pp.umap(adata)
print(f"UMAP computed. Dataset shape: {adata.shape[0]} cells × {adata.shape[1]} genes")
# Plot UMAP colored by leiden
ov.pl.embedding(adata, basis='X_umap', color='leiden')
print("UMAP plot colored by 'leiden' rendered successfully.")
AnnData object with n_obs × n_vars = 2700 × 16634
obs: 'nUMIs', 'mito_perc', 'ribo_perc', 'hb_perc', 'detected_genes', 'cell_complexity', 'passing_mt', 'passing_nUMIs', 'passing_ngenes'
var: 'gene_ids', 'mt', 'ribo', 'hb', 'n_cells', 'percent_cells', 'robust', 'highly_variable_features'
uns: 'status', 'status_args', 'REFERENCE_MANU', 'pca', 'scaled|original|pca_var_ratios', 'scaled|original|cum_sum_eigenvalues', 'neighbors', 'umap'
obsm: 'X_pca', 'scaled|original|X_pca', 'X_umap'
varm: 'PCs', 'scaled|original|pca_loadings'
layers: 'counts', 'scaled'
obsp: 'distances', 'connectivities'
adata = agent.run('give me the summary of this h5ad data', adata)
🎯 Matched project skills:
- Data Transformation (Universal) (score=0.174)
- Excel Data Export (Universal) (score=0.150)
🤔 LLM analyzing request: 'give me the summary of this h5ad data'...
💭 LLM response:
--------------------------------------------------
import omicverse as ov
import numpy as np
import pandas as pd
from scipy import sparse
def summarize_adata(adata):
print(f"AnnData summary: {adata.n_obs} cells × {adata.n_vars} genes")
print("-" * 60)
# Basic slots
print("Layers:", list(getattr(adata, "layers", {}).keys()))
print("Embeddings (.obsm) keys:", list(adata.obsm.keys()))
print("Var mappings (.varm) keys:", list(adata.varm.keys()))
print("Unstructured (.uns) keys:", list(adata.uns.keys()))
print("-" * 60)
# .obs overview
print(f".obs columns ({adata.obs.shape[1]}):", list(adata.obs.columns))
for col in list(adata.obs.columns)[:20]:
s = adata.obs[col]
try:
if pd.api.types.is_categorical_dtype(s) or s.dtype.name == "category":
cats = list(s.cat.categories)
print(f" - obs['{col}']: categorical, {len(cats)} categories; example: {cats[:10]}")
elif pd.api.types.is_numeric_dtype(s):
desc = s.describe()
mn = float(desc.get("min", np.nan))
md = float(desc.get("50%", np.nan))
mx = float(desc.get("max", np.nan))
print(f" - obs['{col}']: numeric, min={mn:.3g}, median={md:.3g}, max={mx:.3g}")
else:
vc = s.value_counts(dropna=False).head(10).to_dict()
print(f" - obs['{col}']: {s.dtype}, top values: {vc}")
except Exception as e:
print(f" - obs['{col}']: summary failed ({e})")
print("-" * 60)
# .var overview
if adata.var is not None and adata.var.shape[1] > 0:
print(f".var columns ({adata.var.shape[1]}):", list(adata.var.columns[:20]))
else:
print(".var has no additional columns.")
print("-" * 60)
# Count-based summaries
try:
X = adata.X
if sparse.issparse(X):
cell_counts = np.asarray(X.sum(axis=1)).ravel()
nz_genes_per_cell = np.asarray((X > 0).sum(axis=1)).ravel()
else:
cell_counts = X.sum(axis=1)
nz_genes_per_cell = (X > 0).sum(axis=1)
print(f"Total counts per cell: mean={np.mean(cell_counts):.2f}, median={np.median(cell_counts):.2f}, sd={np.std(cell_counts):.2f}")
print(f"Detected genes per cell: mean={np.mean(nz_genes_per_cell):.1f}, median={np.median(nz_genes_per_cell):.1f}, sd={np.std(nz_genes_per_cell):.1f}")
except Exception as e:
print(f"Skipped matrix-based summaries due to: {e}")
print("-" * 60)
# Common grouping summaries
for key in ["batch", "sample", "patient", "donor", "condition", "group", "leiden", "clusters", "celltype", "cell_type"]:
if key in adata.obs.columns:
counts = adata.obs[key].value_counts(dropna=False)
print(f"Counts by '{key}':")
print(counts.to_string())
break
print("Summary completed.")
summarize_adata(adata)
--------------------------------------------------
🧬 Generated code to execute:
==================================================
import omicverse as ov
import numpy as np
import pandas as pd
from scipy import sparse
def summarize_adata(adata):
print(f"AnnData summary: {adata.n_obs} cells × {adata.n_vars} genes")
print("-" * 60)
# Basic slots
print("Layers:", list(getattr(adata, "layers", {}).keys()))
print("Embeddings (.obsm) keys:", list(adata.obsm.keys()))
print("Var mappings (.varm) keys:", list(adata.varm.keys()))
print("Unstructured (.uns) keys:", list(adata.uns.keys()))
print("-" * 60)
# .obs overview
print(f".obs columns ({adata.obs.shape[1]}):", list(adata.obs.columns))
for col in list(adata.obs.columns)[:20]:
s = adata.obs[col]
try:
if pd.api.types.is_categorical_dtype(s) or s.dtype.name == "category":
cats = list(s.cat.categories)
print(f" - obs['{col}']: categorical, {len(cats)} categories; example: {cats[:10]}")
elif pd.api.types.is_numeric_dtype(s):
desc = s.describe()
mn = float(desc.get("min", np.nan))
md = float(desc.get("50%", np.nan))
mx = float(desc.get("max", np.nan))
print(f" - obs['{col}']: numeric, min={mn:.3g}, median={md:.3g}, max={mx:.3g}")
else:
vc = s.value_counts(dropna=False).head(10).to_dict()
print(f" - obs['{col}']: {s.dtype}, top values: {vc}")
except Exception as e:
print(f" - obs['{col}']: summary failed ({e})")
print("-" * 60)
# .var overview
if adata.var is not None and adata.var.shape[1] > 0:
print(f".var columns ({adata.var.shape[1]}):", list(adata.var.columns[:20]))
else:
print(".var has no additional columns.")
print("-" * 60)
# Count-based summaries
try:
X = adata.X
if sparse.issparse(X):
cell_counts = np.asarray(X.sum(axis=1)).ravel()
nz_genes_per_cell = np.asarray((X > 0).sum(axis=1)).ravel()
else:
cell_counts = X.sum(axis=1)
nz_genes_per_cell = (X > 0).sum(axis=1)
print(f"Total counts per cell: mean={np.mean(cell_counts):.2f}, median={np.median(cell_counts):.2f}, sd={np.std(cell_counts):.2f}")
print(f"Detected genes per cell: mean={np.mean(nz_genes_per_cell):.1f}, median={np.median(nz_genes_per_cell):.1f}, sd={np.std(nz_genes_per_cell):.1f}")
except Exception as e:
print(f"Skipped matrix-based summaries due to: {e}")
print("-" * 60)
# Common grouping summaries
for key in ["batch", "sample", "patient", "donor", "condition", "group", "leiden", "clusters", "celltype", "cell_type"]:
if key in adata.obs.columns:
counts = adata.obs[key].value_counts(dropna=False)
print(f"Counts by '{key}':")
print(counts.to_string())
print("Summary completed.")
summarize_adata(adata)
==================================================
⚡ Executing code locally...
AnnData summary: 2700 cells × 16634 genes
------------------------------------------------------------
❌ Error executing generated code: name 'getattr' is not defined
Code that failed: import omicverse as ov
import numpy as np
import pandas as pd
from scipy import sparse
def summarize_adata(adata):
print(f"AnnData summary: {adata.n_obs} cells × {adata.n_vars} genes")
print("-" * 60)
# Basic slots
print("Layers:", list(getattr(adata, "layers", {}).keys()))
print("Embeddings (.obsm) keys:", list(adata.obsm.keys()))
print("Var mappings (.varm) keys:", list(adata.varm.keys()))
print("Unstructured (.uns) keys:", list(adata.uns.keys()))
print("-" * 60)
# .obs overview
print(f".obs columns ({adata.obs.shape[1]}):", list(adata.obs.columns))
for col in list(adata.obs.columns)[:20]:
s = adata.obs[col]
try:
if pd.api.types.is_categorical_dtype(s) or s.dtype.name == "category":
cats = list(s.cat.categories)
print(f" - obs['{col}']: categorical, {len(cats)} categories; example: {cats[:10]}")
elif pd.api.types.is_numeric_dtype(s):
desc = s.describe()
mn = float(desc.get("min", np.nan))
md = float(desc.get("50%", np.nan))
mx = float(desc.get("max", np.nan))
print(f" - obs['{col}']: numeric, min={mn:.3g}, median={md:.3g}, max={mx:.3g}")
else:
vc = s.value_counts(dropna=False).head(10).to_dict()
print(f" - obs['{col}']: {s.dtype}, top values: {vc}")
except Exception as e:
print(f" - obs['{col}']: summary failed ({e})")
print("-" * 60)
# .var overview
if adata.var is not None and adata.var.shape[1] > 0:
print(f".var columns ({adata.var.shape[1]}):", list(adata.var.columns[:20]))
else:
print(".var has no additional columns.")
print("-" * 60)
# Count-based summaries
try:
X = adata.X
if sparse.issparse(X):
cell_counts = np.asarray(X.sum(axis=1)).ravel()
nz_genes_per_cell = np.asarray((X > 0).sum(axis=1)).ravel()
else:
cell_counts = X.sum(axis=1)
nz_genes_per_cell = (X > 0).sum(axis=1)
print(f"Total counts per cell: mean={np.mean(cell_counts):.2f}, median={np.median(cell_counts):.2f}, sd={np.std(cell_counts):.2f}")
print(f"Detected genes per cell: mean={np.mean(nz_genes_per_cell):.1f}, median={np.median(nz_genes_per_cell):.1f}, sd={np.std(nz_genes_per_cell):.1f}")
except Exception as e:
print(f"Skipped matrix-based summaries due to: {e}")
print("-" * 60)
# Common grouping summaries
for key in ["batch", "sample", "patient", "donor", "condition", "group", "leiden", "clusters", "celltype", "cell_type"]:
if key in adata.obs.columns:
counts = adata.obs[key].value_counts(dropna=False)
print(f"Counts by '{key}':")
print(counts.to_string())
print("Summary completed.")
summarize_adata(adata)
理解新的输出格式#
运行智能体后,您将看到:
🎯 LLM matched skills:
- Single-cell preprocessing with omicverse
这展示了 LLM 的语义理解——它无需关键词匹配即可将您的请求与相关技能对应。技能内容随后被懒加载并用于指导代码生成。
与旧系统的主要区别:
❌ 旧版:
match: single-preprocessing score=0.364(关键词相似度)✅ 新版:
🎯 LLM matched skills: Single-cell preprocessing(语义理解)
让我们再测试几个请求,以观察 LLM 匹配的实际效果:
手动可视化(可选)#
如果生成的代码未执行绘图,请在此处进行可视化。
# Run leiden clustering
sc.tl.leiden(adata)
# Now plot the UMAP
sc.pl.umap(adata, color=['leiden'], wspace=0.4)
(可选)从文档链接创建技能#
使用 ov.agent.seeker 从文档链接构建新技能(需要网络连接)。
# Example: build a quick skill from a documentation link (uncomment to run)
# info = ov.agent.seeker(
# ['https://scanpy.readthedocs.io/en/stable/generated/scanpy.pp.highly_variable_genes.html'],
# name='hvg-guidance', target='output', package=False
# )
# info
后续步骤#
调整 QC 阈值或聚类分辨率并重新提示智能体。
通过提示添加细胞类型注释(注释 Notebook 请参见 Tutorials-single)。
在项目中创建
.claude/skills/来自定义技能,以便根据实验室 SOP 引导分析。
创建自定义技能#
您可以在项目目录中创建自定义技能,以覆盖或扩展 25 个内置技能:
目录结构:
your-project/
├── .claude/
│ └── skills/
│ └── my-custom-skill/
│ └── SKILL.md
└── your_analysis.py
技能文件格式(.claude/skills/my-custom-skill/SKILL.md):
---
name: my-custom-skill
description: |
Brief description of what this skill does.
Use when: user wants to [specific task]
Handles: [specific data types or scenarios]
Examples: "analyze X", "process Y", "compute Z"
---
# Detailed Instructions
[Your skill instructions here...]
重要提示:使用基于 LLM 的匹配时,技能描述至关重要!
好的描述示例:
description: |
Preprocess single-cell RNA-seq data with quality control.
Use when: user mentions QC, quality control, filtering, preprocessing.
Works with: raw count matrices, AnnData objects.
不好的描述示例:
description: Preprocessing skill
LLM 读取这些描述来匹配技能——请确保描述:
清晰且具体
包含”Use when”条件
列出常见用户短语
提及相关技术/方法
内置技能位置#
所有 25 个内置技能位于:
<omicverse-installation>/omicverse/.claude/skills/
您可以浏览它们,查看编写良好的技能描述示例:
single-preprocessing/SKILL.md— 单细胞预处理bulk-deg-analysis/SKILL.md— Bulk RNA-seq 差异表达data-export-excel/SKILL.md— Excel 导出以及其他 22 个……
性能优势#
新系统提供:
启动速度提升 2-3 倍(渐进式加载)
启动内存降低 5 倍
准确率提升 15%(语义理解)
更好的扩展性(高效处理 100+ 个技能)
完整技术细节请参见代码库中的 SKILL_MATCHING_UPGRADE.md。