{ "cells": [ { "cell_type": "markdown", "id": "a453571fdb77948", "metadata": {}, "source": [ "# J.A.R.V.I.S. with PBMC3k\n", "\n", "This tutorial demonstrates how to analyze PBMC3k using `ov.Agent` with project Skills. The agent uses **LLM-based skill matching** with **progressive disclosure** to auto-discover skills and intelligently select relevant guidance for your analysis.\n", "\n", "## What's New: LLM-Based Skill Matching\n", "\n", "The agent now uses **pure LLM reasoning** to match skills (Claude Code approach):\n", "- **No algorithmic routing** - the LLM reads skill descriptions and understands semantic intent\n", "- **Progressive disclosure** - only loads name + description at startup, full content loaded on-demand\n", "- **Better accuracy** - understands natural language variations (e.g., \"QC my data\" β†’ preprocessing skill)\n", "- **20x faster startup** - only 1.5K tokens vs 25K with full loading\n", "\n", "## Where are Skills Located?\n", "\n", "**Built-in skills** (25 skills) are installed with omicverse at:\n", "```\n", "/omicverse/.claude/skills/\n", "```\n", "\n", "You can also create **custom skills** in your project directory:\n", "```\n", "/.claude/skills/\n", "```\n", "Custom skills override built-in skills with the same name." ] }, { "cell_type": "markdown", "id": "8409aef5db9b29df", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "- omicverse installed in this environment\n", "- Provider API key in env (e.g., `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`)\n", "- Skills: 25 built-in skills are automatically loaded from the omicverse package installation at `omicverse/.claude/skills/`\n", "\n", "> **Skill Discovery**: The agent loads skills from two locations:\n", "> 1. **Package installation** (priority): `/omicverse/.claude/skills/` (25 built-in skills)\n", "> 2. **Current directory** (optional): `/.claude/skills/` (your custom skills)\n", "> \n", "> Custom skills in your project directory can override built-in skills.\n", "\n", "> **New in this version**: Skills use progressive disclosure - only metadata (name + description) loaded at startup. Full content lazy-loaded when the LLM matches a skill to your request.\n", "\n", "> Tip: `print(ov.list_supported_models())` shows supported models and required env vars." ] }, { "cell_type": "code", "execution_count": 1, "id": "3dea7430b164426b", "metadata": { "ExecuteTime": { "end_time": "2025-11-06T12:00:27.817418Z", "start_time": "2025-11-06T12:00:21.018403Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/kq_m3m/anaconda3/envs/ovagent101/lib/python3.12/site-packages/torch/cuda/__init__.py:63: FutureWarning: The pynvml package is deprecated. Please install nvidia-ml-py instead. If you did not install pynvml directly, please report this to the maintainers of the package that installed pynvml for you.\n", " import pynvml # type: ignore[import]\n", "/Users/kq_m3m/anaconda3/envs/ovagent101/lib/python3.12/site-packages/anndata/utils.py:434: FutureWarning: Importing read_loom from `anndata` is deprecated. Import anndata.io.read_loom instead.\n", " warnings.warn(msg, FutureWarning)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "/Users/kq_m3m/anaconda3/envs/ovagent101/bin/python\n", "1.7.9rc1 /Users/kq_m3m/PycharmProjects/ovagent101/OV_DEV/omicverse/omicverse/__init__.py\n" ] } ], "source": [ "import sys, omicverse as ov; print(sys.executable); print(ov.__version__, ov.__file__)" ] }, { "cell_type": "code", "execution_count": 2, "id": "99dccf58afa04864", "metadata": { "ExecuteTime": { "end_time": "2025-11-06T12:00:33.610129Z", "start_time": "2025-11-06T12:00:33.601207Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "OmicVerse version: 1.7.9rc1\n", "πŸ€– Supported Models:\n", "\n", "**Openai**:\n", " β€’ `gpt-5`: OpenAI GPT-5 (Latest) ❌\n", " β€’ `gpt-5-mini`: OpenAI GPT-5 Mini ❌\n", " β€’ `gpt-5-nano`: OpenAI GPT-5 Nano ❌\n", " ... and 13 more models\n", "\n", "**Anthropic**:\n", " β€’ `anthropic/claude-opus-4-1-20250805`: Claude Opus 4.1 (Latest) ❌\n", " β€’ `anthropic/claude-opus-4-20250514`: Claude Opus 4 ❌\n", " β€’ `anthropic/claude-sonnet-4-20250514`: Claude Sonnet 4 ❌\n", " ... and 5 more models\n", "\n", "**Google**:\n", " β€’ `gemini/gemini-2.5-pro`: Gemini 2.5 Pro ❌\n", " β€’ `gemini/gemini-2.5-flash`: Gemini 2.5 Flash ❌\n", " β€’ `gemini/gemini-2.0-pro`: Gemini 2.0 Pro ❌\n", " ... and 2 more models\n", "\n", "**Deepseek**:\n", " β€’ `deepseek/deepseek-chat`: DeepSeek Chat ❌\n", " β€’ `deepseek/deepseek-reasoner`: DeepSeek Reasoner ❌\n", "\n", "**Qwen**:\n", " β€’ `qwq-plus`: QwQ Plus (Reasoning) ❌\n", " β€’ `qwen-max`: Qwen Max (Latest) ❌\n", " β€’ `qwen-max-latest`: Qwen Max Latest ❌\n", " ... and 2 more models\n", "\n", "**Moonshot**:\n", " β€’ `moonshot/kimi-k2-0711-preview`: Kimi K2 (Preview) ❌\n", " β€’ `moonshot/kimi-k2-turbo-preview`: Kimi K2 Turbo (Preview) ❌\n", " β€’ `moonshot/kimi-latest`: Kimi Latest (Auto Context) ❌\n", " ... and 3 more models\n", "\n", "**Grok**:\n", " β€’ `grok/grok-beta`: Grok Beta ❌\n", " β€’ `grok/grok-2`: Grok 2 ❌\n", "\n", "**Zhipu Ai**:\n", " β€’ `zhipu/glm-4.5`: GLM-4.5 (Zhipu AI - Latest) ❌\n", " β€’ `zhipu/glm-4.5-air`: GLM-4.5 Air (Zhipu AI - Latest) ❌\n", " β€’ `zhipu/glm-4.5-flash`: GLM-4.5 Flash (Zhipu AI - Latest) ❌\n", " ... and 4 more models\n", "\n", "Legend: βœ… API key available | ❌ API key missing\n", "\n", "πŸ’‘ Usage: `agent = ov.Agent(model='model_id', api_key='your_key')`\n", "Warning: set OPENAI_API_KEY (or relevant provider key) before running live requests.\n" ] } ], "source": [ "\n", "import os\n", "from pathlib import Path\n", "import scanpy as sc\n", "import omicverse as ov\n", "\n", "print('OmicVerse version:', getattr(ov, '__version__', 'unknown'))\n", "print(ov.list_supported_models())\n", "\n", "OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '')\n", "\n", "if not OPENAI_API_KEY:\n", " print('Warning: set OPENAI_API_KEY (or relevant provider key) before running live requests.')\n", "\n", "# Nice plotting defaults\n", "sc.settings.set_figure_params(dpi=100)" ] }, { "cell_type": "markdown", "id": "f5e6eff0ee523dfa", "metadata": {}, "source": [ "## Load PBMC dataset (with offline fallback)\n", "\n", "Attempts `scanpy.datasets.pbmc3k()`; if unavailable, falls back to `pbmc68k_reduced` or a local `PBMC3K_PATH`.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "d77b8cc1a0eab3e5", "metadata": { "ExecuteTime": { "end_time": "2025-11-06T12:00:37.557238Z", "start_time": "2025-11-06T12:00:37.479077Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loaded Scanpy pbmc3k dataset\n" ] }, { "data": { "text/plain": [ "AnnData object with n_obs Γ— n_vars = 2700 Γ— 32738\n", " var: 'gene_ids'" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "adata = None\n", "local_path = os.environ.get('PBMC3K_PATH')\n", "if local_path and os.path.exists(local_path):\n", " adata = sc.read_h5ad(local_path)\n", " print('Loaded local PBMC3k from:', local_path)\n", "else:\n", " try:\n", " adata = sc.datasets.pbmc3k()\n", " print('Loaded Scanpy pbmc3k dataset')\n", " except Exception as e:\n", " print('pbmc3k not available:', e)\n", " try:\n", " adata = sc.datasets.pbmc68k_reduced()\n", " print('Loaded fallback pbmc68k_reduced dataset')\n", " except Exception as e2:\n", " raise RuntimeError('Could not load a PBMC dataset. Set PBMC3K_PATH to a local .h5ad file.') from e2\n", "\n", "adata\n" ] }, { "cell_type": "markdown", "id": "f0d386e5848beac", "metadata": {}, "source": [ "## Initialize ov.Agent (skills auto‑loaded)\n", "\n", "Pick a supported model and ensure the correct env var is set. The agent will auto‑load project skills and include them in its planning.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "408a4905fb48af6c", "metadata": { "ExecuteTime": { "end_time": "2025-11-06T12:01:10.033626Z", "start_time": "2025-11-06T12:01:10.018592Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Skill root /Users/kq_m3m/PycharmProjects/ovagent101/OV_DEV/omicverse/omicverse_guide/docs/Tutorials-llm/.claude/skills does not exist; no skills loaded.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Initializing OmicVerse Smart Agent (internal backend)...\n", " 🧭 Loaded 23 skills (23 built-in)\n", " Model: OpenAI GPT-5 (Latest)\n", " Provider: Openai\n", " Endpoint: https://api.openai.com/v1\n", " βœ… Openai API key available\n", " πŸ“š Function registry loaded: 110 functions in 7 categories\n", "βœ… Smart Agent initialized successfully!\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '')\n", "# Choose a supported model (ensure matching env var is set)\n", "model_id = 'gpt-5' # see ov.list_supported_models()\n", "api_key = OPENAI_API_KEY or os.getenv('ANTHROPIC_API_KEY') or os.getenv('GEMINI_API_KEY')\n", "agent = ov.Agent(model=model_id, api_key=api_key)\n", "agent" ] }, { "cell_type": "markdown", "id": "22e17c0816b62145", "metadata": {}, "source": [ "## Project Skills Preview\n", "\n", "**Built-in skills** are located at `/omicverse/.claude/skills/`.\n", "\n", "Skills are loaded with **progressive disclosure**:\n", "1. **At startup**: Only lightweight metadata (name + description) is loaded from the 25 built-in skills\n", "2. **When matching**: The LLM reads descriptions and selects relevant skills using semantic understanding\n", "3. **On-demand**: Full skill content (instructions) is lazy-loaded only when needed\n", "\n", "Below we'll inspect the skill metadata and demonstrate the new LLM-based matching." ] }, { "cell_type": "code", "execution_count": null, "id": "59ff8df534ae4580", "metadata": { "ExecuteTime": { "end_time": "2025-11-06T12:01:15.875176Z", "start_time": "2025-11-06T12:01:15.863848Z" } }, "outputs": [], "source": [ "from pathlib import Path\n", "from omicverse.utils.skill_registry import build_multi_path_skill_registry\n", "\n", "# Build registry with progressive disclosure\n", "pkg_root = Path(ov.__file__).resolve().parents[1]\n", "cwd = Path.cwd()\n", "\n", "# Show where skills are loaded from\n", "builtin_skill_path = pkg_root / 'omicverse' / '.claude' / 'skills'\n", "custom_skill_path = cwd / '.claude' / 'skills'\n", "\n", "print('πŸ“‚ Skill Discovery Paths:')\n", "print(f' Built-in: {builtin_skill_path}')\n", "print(f' {\"βœ… Exists\" if builtin_skill_path.exists() else \"❌ Not found\"}')\n", "print(f' Custom: {custom_skill_path}')\n", "print(f' {\"βœ… Exists\" if custom_skill_path.exists() else \"❌ Not found (optional)\"}')\n", "print()\n", "\n", "# Load skills\n", "reg = build_multi_path_skill_registry(pkg_root, cwd)\n", "\n", "print(f'βœ… Discovered {len(reg.skill_metadata)} skills (progressive disclosure)')\n", "print(f' Only loaded: name + description (~30-50 tokens each)')\n", "print(f' Full content: lazy-loaded when matched by LLM\\n')\n", "\n", "# Show first 10 skill metadata\n", "print('First 10 skills (metadata only):')\n", "for slug in sorted(reg.skill_metadata.keys())[:10]:\n", " metadata = reg.skill_metadata[slug]\n", " print(f' β€’ {slug}')\n", " print(f' └─ {metadata.description[:80]}...')\n", "\n", "print('\\n' + '='*70)\n", "print('πŸ†• LLM-Based Skill Matching (replaces algorithmic routing)')\n", "print('='*70)\n", "print('The agent now uses pure LLM reasoning to match skills:')\n", "print(' 1. LLM reads all skill descriptions from omicverse/.claude/skills/')\n", "print(' 2. LLM analyzes your request semantically')\n", "print(' 3. LLM selects relevant skills using language understanding')\n", "print(' 4. Agent lazy-loads full content for matched skills only')\n", "print('\\nOld SkillRouter (keyword matching) is deprecated but kept for compatibility.')\n", "print('You will see \"🎯 LLM matched skills:\" in agent output.\\n')" ] }, { "cell_type": "markdown", "id": "16b238c166c47af5", "metadata": {}, "source": [ "## Natural‑language pipeline (LLM-guided skill matching)\n", "\n", "We'll drive a typical workflow via natural language. Watch for the **\"🎯 LLM matched skills:\"** output showing which skills were selected by pure LLM reasoning.\n", "\n", "The agent will:\n", "1. Analyze your request semantically\n", "2. Match relevant skills using LLM (not keywords)\n", "3. Lazy-load full skill content for matched skills\n", "4. Incorporate skill guidance into code generation\n", "\n", "Workflow steps:\n", "1. Quality control (filter cells/genes)\n", "2. Preprocess and HVG selection\n", "3. Clustering (Leiden)\n", "4. Compute UMAP and visualize" ] }, { "cell_type": "code", "execution_count": 6, "id": "e51ba0a2db3038da", "metadata": { "ExecuteTime": { "end_time": "2025-11-06T12:04:56.483598Z", "start_time": "2025-11-06T12:01:47.545215Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "🎯 Matched project skills:\n", " - Bulk RNA-seq deconvolution with Bulk2Single (score=0.160)\n", " - STRING protein interaction analysis with omicverse (score=0.138)\n", "\n", "πŸ€” LLM analyzing request: 'quality control with nUMI>500, mito<0.2'...\n", "\n", "πŸ’­ LLM response:\n", "--------------------------------------------------\n", "import omicverse as ov\n", "\n", "# Execute quality control with extracted thresholds\n", "adata = ov.pp.qc(adata, tresh={'mito_perc': 0.2, 'nUMIs': 500, 'detected_genes': 250})\n", "print(\"QC completed. Dataset shape: \" + str(adata.shape[0]) + \" cells Γ— \" + str(adata.shape[1]) + \" genes\")\n", "--------------------------------------------------\n", "\n", "🧬 Generated code to execute:\n", "==================================================\n", "import omicverse as ov\n", "# Execute quality control with extracted thresholds\n", "adata = ov.pp.qc(adata, tresh={'mito_perc': 0.2, 'nUMIs': 500, 'detected_genes': 250})\n", "print(\"QC completed. Dataset shape: \" + str(adata.shape[0]) + \" cells Γ— \" + str(adata.shape[1]) + \" genes\")\n", "==================================================\n", "\n", "⚑ Executing code locally...\n", "\u001b[95m\u001b[1mπŸ–₯️ Using CPU mode for QC...\u001b[0m\n", "\n", "\u001b[95m\u001b[1mπŸ“Š Step 1: Calculating QC Metrics\u001b[0m\n", "\n", "\u001b[92m βœ“ Gene Family Detection:\u001b[0m\n", " \u001b[96mβ”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[1mGene Family \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1mGenes Found \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1mDetection Method \u001b[0m \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mMitochondrial \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[92m13 \u001b[0m \u001b[96mβ”‚\u001b[0m Auto (MT-) \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mRibosomal \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[92m106 \u001b[0m \u001b[96mβ”‚\u001b[0m Auto (RPS/RPL) \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mHemoglobin \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[92m13 \u001b[0m \u001b[96mβ”‚\u001b[0m Auto (regex) \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\u001b[0m\n", "\n", "\u001b[92m βœ“ QC Metrics Summary:\u001b[0m\n", " \u001b[96mβ”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[1mMetric \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1mMean \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1mRange (Min - Max) \u001b[0m \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mnUMIs \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1m2367 \u001b[0m \u001b[96mβ”‚\u001b[0m 548 - 15844 \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mDetected Genes \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1m847 \u001b[0m \u001b[96mβ”‚\u001b[0m 212 - 3422 \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mMitochondrial % \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1m2.2% \u001b[0m \u001b[96mβ”‚\u001b[0m 0.0% - 22.6% \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mRibosomal % \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1m34.9% \u001b[0m \u001b[96mβ”‚\u001b[0m 1.1% - 59.4% \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\u001b[0m\n", " \u001b[96mβ”‚\u001b[0m \u001b[94mHemoglobin % \u001b[0m \u001b[96mβ”‚\u001b[0m \u001b[1m0.0% \u001b[0m \u001b[96mβ”‚\u001b[0m 0.0% - 1.4% \u001b[96mβ”‚\u001b[0m\n", " \u001b[96mβ””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\u001b[0m\n", "\n", " \u001b[96mπŸ“ˆ Original cell count: \u001b[1m2,700\u001b[0m\n", "\n", "\u001b[95m\u001b[1mπŸ”§ Step 2: Quality Filtering (SEURAT)\u001b[0m\n", " \u001b[96mThresholds: mito≀0.2, nUMIsβ‰₯500, genesβ‰₯250\u001b[0m\n", " \u001b[94mπŸ“Š Seurat Filter Results:\u001b[0m\n", " \u001b[96mβ€’ nUMIs filter (β‰₯500): \u001b[1m0\u001b[0m\u001b[96m cells failed (0.0%)\u001b[0m\n", " \u001b[96mβ€’ Genes filter (β‰₯250): \u001b[1m3\u001b[0m\u001b[96m cells failed (0.1%)\u001b[0m\n", " \u001b[96mβ€’ Mitochondrial filter (≀0.2): \u001b[1m2\u001b[0m\u001b[96m cells failed (0.1%)\u001b[0m\n", " \u001b[92mβœ“ Filters applied successfully\u001b[0m\n", " \u001b[92mβœ“ Combined QC filters: \u001b[1m5\u001b[0m\u001b[92m cells removed (0.2%)\u001b[0m\n", "\n", "\u001b[95m\u001b[1m🎯 Step 3: Final Filtering\u001b[0m\n", " \u001b[96mParameters: min_genes=200, min_cells=3\u001b[0m\n", " \u001b[96mRatios: max_genes_ratio=1, max_cells_ratio=1\u001b[0m\n", " \u001b[92mβœ“ Final filtering: \u001b[1m0\u001b[0m\u001b[92m cells, \u001b[1m19,024\u001b[0m\u001b[92m genes removed\u001b[0m\n", "\n", "\u001b[95m\u001b[1mπŸ” Step 4: Doublet Detection\u001b[0m\n", " \u001b[93m⚠️ Note: 'scrublet' detection is too old and may not work properly\u001b[0m\n", " \u001b[96mπŸ’‘ Consider using 'doublets_method=sccomposite' for better results\u001b[0m\n", " \u001b[92mπŸ” Running scrublet doublet detection...\u001b[0m\n", "❌ Error executing generated code: threshold is None and thus scrublet requires skimage, but skimage is not installed.\n", "Code that failed: import omicverse as ov\n", "# Execute quality control with extracted thresholds\n", "adata = ov.pp.qc(adata, tresh={'mito_perc': 0.2, 'nUMIs': 500, 'detected_genes': 250})\n", "print(\"QC completed. Dataset shape: \" + str(adata.shape[0]) + \" cells Γ— \" + str(adata.shape[1]) + \" genes\")\n", "\n", "🎯 Matched project skills:\n", " - Data Transformation (Universal) (score=0.164)\n", " - PDF Report Generation (Universal) (score=0.152)\n", "\n", "πŸ€” LLM analyzing request: 'preprocess with 2000 highly variable genes using shiftlog|pearson'...\n", "\n", "πŸ’­ LLM response:\n", "--------------------------------------------------\n", "import omicverse as ov\n", "\n", "# Preprocess with 2000 highly variable genes using shiftlog|pearson\n", "ov.pp.preprocess(adata, mode='shiftlog|pearson', n_HVGs=2000)\n", "print(\"Preprocessing completed with mode='shiftlog|pearson' and n_HVGs=2000.\")\n", "print(f\"Dataset shape after preprocessing: {adata.shape[0]} cells Γ— {adata.shape[1]} genes\")\n", "--------------------------------------------------\n", "\n", "🧬 Generated code to execute:\n", "==================================================\n", "import omicverse as ov\n", "# Preprocess with 2000 highly variable genes using shiftlog|pearson\n", "ov.pp.preprocess(adata, mode='shiftlog|pearson', n_HVGs=2000)\n", "print(\"Preprocessing completed with mode='shiftlog|pearson' and n_HVGs=2000.\")\n", "print(f\"Dataset shape after preprocessing: {adata.shape[0]} cells Γ— {adata.shape[1]} genes\")\n", "==================================================\n", "\n", "⚑ Executing code locally...\n", "πŸ” [2025-11-06 04:02:04] Running preprocessing in 'cpu' mode...\n", "\u001b[96mBegin robust gene identification\u001b[0m\n", "\u001b[94m After filtration, 16634/32738 genes are kept.\u001b[0m\n", "\u001b[94m Among 16634 genes, 14702 genes are robust.\u001b[0m\n", "βœ… Robust gene identification completed successfully.\n", "\u001b[96mBegin size normalization: shiftlog and HVGs selection pearson\u001b[0m\n", "\n", "\u001b[95m\u001b[1mπŸ” Count Normalization:\u001b[0m\n", " \u001b[96mTarget sum: \u001b[1m500000.0\u001b[0m\n", " \u001b[96mExclude highly expressed: \u001b[1mTrue\u001b[0m\n", " \u001b[96mMax fraction threshold: \u001b[1m0.2\u001b[0m\n", " ⚠️ \u001b[93mExcluding \u001b[1m0\u001b[0m\u001b[93m highly-expressed genes from normalization computation\u001b[0m\n", " \u001b[93mExcluded genes: \u001b[1m[]\u001b[0m\n", "\n", "\u001b[92mβœ… Count Normalization Completed Successfully!\u001b[0m\n", " \u001b[92mβœ“ Processed: \u001b[1m2,700\u001b[0m\u001b[92m cells Γ— \u001b[1m14,702\u001b[0m\u001b[92m genes\u001b[0m\n", " \u001b[92mβœ“ Runtime: \u001b[1m0.04s\u001b[0m\n", "\n", "\u001b[95m\u001b[1mπŸ” Highly Variable Genes Selection (Experimental):\u001b[0m\n", " \u001b[96mMethod: \u001b[1mpearson_residuals\u001b[0m\n", " \u001b[96mTarget genes: \u001b[1m2,000\u001b[0m\n", " \u001b[96mTheta (overdispersion): \u001b[1m100\u001b[0m\n", "\n", "\u001b[92mβœ… Experimental HVG Selection Completed Successfully!\u001b[0m\n", " \u001b[92mβœ“ Selected: \u001b[1m2,000\u001b[0m\u001b[92m highly variable genes out of \u001b[1m14,702\u001b[0m\u001b[92m total (13.6%)\u001b[0m\n", " \u001b[92mβœ“ Results added to AnnData object:\u001b[0m\n", " \u001b[96mβ€’ 'highly_variable': \u001b[1mBoolean vector\u001b[0m\u001b[96m (adata.var)\u001b[0m\n", " \u001b[96mβ€’ 'highly_variable_rank': \u001b[1mFloat vector\u001b[0m\u001b[96m (adata.var)\u001b[0m\n", " \u001b[96mβ€’ 'highly_variable_nbatches': \u001b[1mInt vector\u001b[0m\u001b[96m (adata.var)\u001b[0m\n", " \u001b[96mβ€’ 'highly_variable_intersection': \u001b[1mBoolean vector\u001b[0m\u001b[96m (adata.var)\u001b[0m\n", " \u001b[96mβ€’ 'means': \u001b[1mFloat vector\u001b[0m\u001b[96m (adata.var)\u001b[0m\n", " \u001b[96mβ€’ 'variances': \u001b[1mFloat vector\u001b[0m\u001b[96m (adata.var)\u001b[0m\n", " \u001b[96mβ€’ 'residual_variances': \u001b[1mFloat vector\u001b[0m\u001b[96m (adata.var)\u001b[0m\n", "\u001b[94m Time to analyze data in cpu: 0.13 seconds.\u001b[0m\n", "βœ… Preprocessing completed successfully.\n", "\u001b[92m Added:\u001b[0m\n", "\u001b[96m 'highly_variable_features', boolean vector (adata.var)\u001b[0m\n", "\u001b[96m 'means', float vector (adata.var)\u001b[0m\n", "\u001b[96m 'variances', float vector (adata.var)\u001b[0m\n", "\u001b[96m 'residual_variances', float vector (adata.var)\u001b[0m\n", "\u001b[96m 'counts', raw counts layer (adata.layers)\u001b[0m\n", "\u001b[94m End of size normalization: shiftlog and HVGs selection pearson\u001b[0m\n", "Preprocessing completed with mode='shiftlog|pearson' and n_HVGs=2000.\n", "Dataset shape after preprocessing: 2700 cells Γ— 16634 genes\n", "βœ… Code executed successfully!\n", "πŸ“Š Result shape: 2700 cells Γ— 16634 genes\n", "\n", "🎯 Matched project skills:\n", " - Single-cell clustering and batch correction with omicverse (score=0.166)\n", "\n", "πŸ€” LLM analyzing request: 'leiden clustering resolution=1.0'...\n", "\n", "πŸ’­ LLM response:\n", "--------------------------------------------------\n", "import omicverse as ov\n", "\n", "# Ensure neighborhood graph is computed\n", "if 'neighbors' not in getattr(adata, 'uns', {}):\n", " ov.pp.neighbors(adata, n_neighbors=15)\n", "\n", "# Perform Leiden clustering with specified resolution\n", "ov.pp.leiden(adata, resolution=1.0)\n", "\n", "print(f\"Leiden clustering completed at resolution=1.0. Found {adata.obs['leiden'].nunique()} clusters.\")\n", "print(f\"Dataset shape: {adata.n_obs} cells Γ— {adata.n_vars} genes\")\n", "--------------------------------------------------\n", "\n", "🧬 Generated code to execute:\n", "==================================================\n", "import omicverse as ov\n", "# Ensure neighborhood graph is computed\n", "if 'neighbors' not in getattr(adata, 'uns', {}):\n", " ov.pp.neighbors(adata, n_neighbors=15)\n", "# Perform Leiden clustering with specified resolution\n", "ov.pp.leiden(adata, resolution=1.0)\n", "print(f\"Leiden clustering completed at resolution=1.0. Found {adata.obs['leiden'].nunique()} clusters.\")\n", "print(f\"Dataset shape: {adata.n_obs} cells Γ— {adata.n_vars} genes\")\n", "==================================================\n", "\n", "⚑ Executing code locally...\n", "❌ Error executing generated code: name 'getattr' is not defined\n", "Code that failed: import omicverse as ov\n", "# Ensure neighborhood graph is computed\n", "if 'neighbors' not in getattr(adata, 'uns', {}):\n", " ov.pp.neighbors(adata, n_neighbors=15)\n", "# Perform Leiden clustering with specified resolution\n", "ov.pp.leiden(adata, resolution=1.0)\n", "print(f\"Leiden clustering completed at resolution=1.0. Found {adata.obs['leiden'].nunique()} clusters.\")\n", "print(f\"Dataset shape: {adata.n_obs} cells Γ— {adata.n_vars} genes\")\n", "\n", "🎯 Matched project skills:\n", " - BulkTrajBlend trajectory interpolation (score=0.169)\n", " - OmicVerse visualization for bulk, color systems, and single-cell data (score=0.150)\n", "\n", "πŸ€” LLM analyzing request: 'compute umap and plot colored by leiden'...\n", "\n", "πŸ’­ LLM response:\n", "--------------------------------------------------\n", "import omicverse as ov\n", "\n", "# Scale, PCA, neighbors, and UMAP\n", "ov.pp.scale(adata, max_value=10)\n", "ov.pp.pca(adata, n_pcs=50)\n", "ov.pp.neighbors(adata, n_neighbors=15, n_pcs=50)\n", "ov.pp.umap(adata)\n", "\n", "print(f\"UMAP computed. Dataset shape: {adata.shape[0]} cells Γ— {adata.shape[1]} genes\")\n", "\n", "# Plot UMAP colored by leiden\n", "ov.pl.embedding(adata, basis='X_umap', color='leiden')\n", "print(\"UMAP plot colored by 'leiden' rendered successfully.\")\n", "--------------------------------------------------\n", "\n", "🧬 Generated code to execute:\n", "==================================================\n", "import omicverse as ov\n", "# Scale, PCA, neighbors, and UMAP\n", "ov.pp.scale(adata, max_value=10)\n", "ov.pp.pca(adata, n_pcs=50)\n", "ov.pp.neighbors(adata, n_neighbors=15, n_pcs=50)\n", "ov.pp.umap(adata)\n", "print(f\"UMAP computed. Dataset shape: {adata.shape[0]} cells Γ— {adata.shape[1]} genes\")\n", "# Plot UMAP colored by leiden\n", "ov.pl.embedding(adata, basis='X_umap', color='leiden')\n", "print(\"UMAP plot colored by 'leiden' rendered successfully.\")\n", "==================================================\n", "\n", "⚑ Executing code locally...\n", " \u001b[96mπŸ–₯️ sklearn PCA backend: CPU computation\u001b[0m\n", "πŸ–₯️ Using Scanpy CPU to calculate neighbors...\n", "\n", "\u001b[95m\u001b[1mπŸ” K-Nearest Neighbors Graph Construction:\u001b[0m\n", " \u001b[96mMode: \u001b[1mcpu\u001b[0m\n", " \u001b[96mNeighbors: \u001b[1m15\u001b[0m\n", " \u001b[96mMethod: \u001b[1mumap\u001b[0m\n", " \u001b[96mMetric: \u001b[1meuclidean\u001b[0m\n", " \u001b[96mPCs used: \u001b[1m50\u001b[0m\n", " \u001b[92mπŸ” Computing neighbor distances...\u001b[0m\n", " \u001b[92mπŸ” Computing connectivity matrix...\u001b[0m\n", " \u001b[96mπŸ’‘ Using UMAP-style connectivity\u001b[0m\n", " \u001b[92mβœ“ Graph is fully connected\u001b[0m\n", "\n", "\u001b[92mβœ… KNN Graph Construction Completed Successfully!\u001b[0m\n", " \u001b[92mβœ“ Processed: \u001b[1m2,700\u001b[0m\u001b[92m cells with \u001b[1m15\u001b[0m\u001b[92m neighbors each\u001b[0m\n", " \u001b[92mβœ“ Results added to AnnData object:\u001b[0m\n", " \u001b[96mβ€’ 'neighbors': \u001b[1mNeighbors metadata\u001b[0m\u001b[96m (adata.uns)\u001b[0m\n", " \u001b[96mβ€’ 'distances': \u001b[1mDistance matrix\u001b[0m\u001b[96m (adata.obsp)\u001b[0m\n", " \u001b[96mβ€’ 'connectivities': \u001b[1mConnectivity matrix\u001b[0m\u001b[96m (adata.obsp)\u001b[0m\n", "πŸ” [2025-11-06 04:04:52] Running UMAP in 'cpu' mode...\n", "πŸ–₯️ Using Scanpy CPU UMAP...\n", "\n", "\u001b[95m\u001b[1mπŸ” UMAP Dimensionality Reduction:\u001b[0m\n", " \u001b[96mMode: \u001b[1mcpu\u001b[0m\n", " \u001b[96mMethod: \u001b[1mumap\u001b[0m\n", " \u001b[96mComponents: \u001b[1m2\u001b[0m\n", " \u001b[96mMin distance: \u001b[1m0.5\u001b[0m\n", "{'n_neighbors': 15, 'method': 'umap', 'random_state': 0, 'metric': 'euclidean', 'n_pcs': 50}\n", " \u001b[92mπŸ” Computing UMAP parameters...\u001b[0m\n", " \u001b[92mπŸ” Computing UMAP embedding (classic method)...\u001b[0m\n", "\n", "\u001b[92mβœ… UMAP Dimensionality Reduction Completed Successfully!\u001b[0m\n", " \u001b[92mβœ“ Embedding shape: \u001b[1m2,700\u001b[0m\u001b[92m cells Γ— \u001b[1m2\u001b[0m\u001b[92m dimensions\u001b[0m\n", " \u001b[92mβœ“ Results added to AnnData object:\u001b[0m\n", " \u001b[96mβ€’ 'X_umap': \u001b[1mUMAP coordinates\u001b[0m\u001b[96m (adata.obsm)\u001b[0m\n", " \u001b[96mβ€’ 'umap': \u001b[1mUMAP parameters\u001b[0m\u001b[96m (adata.uns)\u001b[0m\n", "βœ… UMAP completed successfully.\n", "UMAP computed. Dataset shape: 2700 cells Γ— 16634 genes\n", "❌ Error executing generated code: \"Could not find 'leiden' in adata.obs or adata.var_names\"\n", "Code that failed: import omicverse as ov\n", "# Scale, PCA, neighbors, and UMAP\n", "ov.pp.scale(adata, max_value=10)\n", "ov.pp.pca(adata, n_pcs=50)\n", "ov.pp.neighbors(adata, n_neighbors=15, n_pcs=50)\n", "ov.pp.umap(adata)\n", "print(f\"UMAP computed. Dataset shape: {adata.shape[0]} cells Γ— {adata.shape[1]} genes\")\n", "# Plot UMAP colored by leiden\n", "ov.pl.embedding(adata, basis='X_umap', color='leiden')\n", "print(\"UMAP plot colored by 'leiden' rendered successfully.\")\n" ] }, { "data": { "text/plain": [ "AnnData object with n_obs Γ— n_vars = 2700 Γ— 16634\n", " obs: 'nUMIs', 'mito_perc', 'ribo_perc', 'hb_perc', 'detected_genes', 'cell_complexity', 'passing_mt', 'passing_nUMIs', 'passing_ngenes'\n", " var: 'gene_ids', 'mt', 'ribo', 'hb', 'n_cells', 'percent_cells', 'robust', 'highly_variable_features'\n", " uns: 'status', 'status_args', 'REFERENCE_MANU', 'pca', 'scaled|original|pca_var_ratios', 'scaled|original|cum_sum_eigenvalues', 'neighbors', 'umap'\n", " obsm: 'X_pca', 'scaled|original|X_pca', 'X_umap'\n", " varm: 'PCs', 'scaled|original|pca_loadings'\n", " layers: 'counts', 'scaled'\n", " obsp: 'distances', 'connectivities'" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICEtWng/+STT8L1118fhg4dGtZYY43QpUuX7L/bbrttGD16dHj99der2v5HH30UbrjhhrD//vuHDTbYIHTv3j307NkzbLTRRuHII48M48ePDw0NDVX9DAAA0JY6t1VDEydODEcffXSYMWNGyevvvfde9njppZfC5ZdfHs4999ws/Hfq1KlV27/jjjvCqFGjwpw5c5b4t2nTpmWPuM9uu+0Wfvvb34YBAwa0avsAAJDsCP+jjz4a9t1335Kw37lz59C/f/9slL1g4cKF4fzzzw8jR45s1fZ/+tOfZiP4xWG/Y8eOWfu9e/cu2fepp54K3/jGN8K7777bqp8BAACSDPwzZ84MI0aMCAsWLMi2e/XqlU3rmTdvXnYCMH/+/DBu3Liw8cYb58fceOON2dSb1hDf65xzzsm3N9lkk3DPPfeEL774Ims/fo4///nPYffdd8/3efvtt8Pxxx/fKu0DAEDSgf/ss88OH3zwQfa8W7du2Wj/SSedFHr06PFfH6Bjx7DPPvuEF154IWy11Vb5cXFqT5zz3xKzZs0Kp512Wr690047heeeey4ceuihYaWVVspe69ChQ9hhhx3C448/Hvbbb7983zif/8UXX2xR+wAAkHTgj4E7zosvOOOMM8LOO++81H3j1JoHHnggD+JxXn9LR/njPQGffvpp9nzVVVcNY8eODX379l3qvvHE45e//GV2AlBw//33t6h9AABIOvDffffd2bz8rKGOHcPJJ5+83P0HDhwYDjrooHz7rrvuqrjtL7/8Mtxyyy359mWXXZaF/uVZZ511ss8YrwDEqxBbbLFFxe0DAEDyVXritJiCOG0mluBsyrBhw8K9996bPY9Tat55550siDfXk08+mc3Pj2LQP+aYY8o67rrrrmt2WwAAUJcj/MVz4OP8+XLsuOOOJduTJk2qqO0Y+Av22GOPfKoQAADUk6qN8M+ePTu/WTcaNGhQWcetv/76WcnOwlSgqVOnVtT+X/7yl/z5Nttskz+fO3duVqVnwoQJYfr06WHRokVZec4999wzHH744WVdhQAAgFDvgb/xAltrr712WcfFsL/66qtn5TyjOKWnEq+99lr+PE4JisH+yiuvzOr8F0qEFkyePDn84Q9/COedd1648MILSyr7AABALavalJ5YZadYUzfMFiuupFN8laA5ihfOihWADj744PCTn/xkibBfLK4JcPrpp4cTTjghNDQ0VNQuAADUxQh/4xr6PXv2LPvY4n0rqcX/0Ucfha+++irfvuiii7I6/4XpPWeddVbYbbfdQp8+fbIrCPfdd1/42c9+Fj7++ONsn9/85jdhww03DGeeeWaLr2wsrVRpQZy2FKsJka7i78Pi56RLn9cffV5/9Hn9WfjfU81rVdUCf+Mg25ybZov3rSQkffbZZyXbhbB/7LHHZivvxmlDBRtttFG2OFi8AhBX2y1MJYqr8x522GFhvfXWa1bbAwYMKHvfadOmZVcVqA+V3o9C7dLn9Uef1x99Xh/mzJkTalnVpvQ0nhJTvKBVtS3tJCFWCWoc9ottsskm4fbbby85k4tz/gEAoJZVbYS/S5cuJdvNGakv3rdbt27Nbntpx8RpPcsK+wXf/OY3w6677hqefvrpbHvMmDHhf//v/92stmPln6am9MQ1CaI4bai5VxCoLfF7uTD6s+mmmyoPWwf0ef3R5/VHn9efXr16hVpWtcAfb5Qt9umnn5Z9bPG8/R49erS4U+J2DPPl2G+//fLAH+f3x5t/Y9nOcpVbjSiKJyCNT4xIV5yqpr/riz6vP/q8/ujz+tC5iUHjup3S069fv5Lt5lTbKd43luhsrpVXXjl06tRpidr+5YhTexqvJwAAALWqaoG/8VSVws2wTYlz54tLejZnxLxYrL1fyQ3DjasJVVIlCAAAkg/8q622Wknt/eKFsJbnzTffLCl9tNlmm1XU/pZbbrnUUphNKZTmLIilOwEAoFZVLfBH22+/ff78+eefL+uYxvttt912FbW91VZblVxdKHfF3ilTppRUFoo31gIAQK2qauAfOnRo/vzZZ58N8+bNa/KYWBmnYPDgwRVP6fnWt75Vsn3vvfeWddwjjzySP4+LdDVnwTAAAKirwH/IIYfkN8/Ghbiuu+665e7/xhtvhAcffDDfPuqooypue9ttt81OGAouv/zyJm8cnjhxYpg0aVK+ffjhh1fcPgAAJB/44+h8DP0FF198cZgwYcJS940rzg4fPjyvwR/nzh9//PEtan/06NH587lz54YRI0Ysszzo22+/HY455ph8u2/fvi1uHwAAkg780aWXXppPi4lhfv/99w+XXXZZ+Oijj/IVeR977LFsvv/kyZNLFspqXNqzuAJQnF9feCxLDPgHHHBAvv3oo4+GXXbZJfz+97/PTywWLFiQrbAbV+ItXjTriiuuCKusskorfAUAACDhwB9r4N955535gkMxYJ911llZBZ94BSAuirX33nuHV199tSSon3LKKa3Sfmx7t912y7f/9re/hYMOOih07949DBgwILuSEKcOFdfbP/XUU8Nxxx3XKu0DAEDSgT8aNmxYGDt2bBawCxYtWpStYls8xaZjx45h1KhR4bbbbmu1tuMJxfjx47P3La7HH0t/zpgxIzsBKYgnAddcc024+uqrW619AABoT222TvBee+0Vpk6dGm699dbsxtxY/jLOq+/atWs2RWfIkCHhxBNPDJtvvnmrt92tW7dw5ZVXhpEjR4Y77rgjjBs3Lrz11lvh/fffz0L+oEGDwn777RdOOOGEsOaaa7Z6+wAAkHzgj2K4jqE7PloihvVKDBw4MJx33nnZAwAA6kGbTOkBAADah8APAAAJE/gBACBhAj8AACRM4AcAgIQJ/AAAkDCBHwAAEibwAwBAwgR+AABImMAPAAAJE/gBACBhAj8AACRM4AcAgIQJ/AAAkDCBHwAAEibwAwBAwgR+AABImMAPAAAJE/gBACBhAj8AACRM4AcAgIQJ/AAAkDCBHwAAEibwAwBAwgR+AABImMAPAAAJE/gBACBhAj8AACRM4AcAgIQJ/AAAkDCBHwAAEibwAwBAwgR+AABImMAPAAAJE/gBACBhAj8AACRM4AcA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fv36lWw3p9pO8b7FJTpbQ5yrH0N9cdiPbcSR/p133rlV2wIAgGQDf+OpKjNnziz7Lujikp7NGTFvyt133x123333kvcfOHBg+OMf/9jsRboAAKCuA38saVlce/+1114r67g333yzpPTRZptt1iqf5+qrrw4jRowIX3zxRf7ajjvumI34x7n0AACQoqoF/mj77bfPnz///PNlHdN4v+22267Fn+OnP/1pOO2000oqB337298OEydOrKjWPgAA1IqqBv6hQ4fmz5999tmSha2WZcyYMfnzwYMHt3hKz7XXXhvOOeecktdOPfXUcN9994WVV165Re8NAAB1HfgPOeSQ0KlTp3whruuuu265+7/xxhvhwQcfzLePOuqoFrX/9NNPZyP7xS699NJsek/HjlX9vw4AACuEqqbeODofQ3/BxRdfHCZMmLDUfeOKs8OHD89r8Mcymccff3zFbcerCUcccURYvHhx/tqFF14YzjrrrIrfEwAAak3Vh7njiHrPnj2z5zHM77///uGyyy4LH330UfZanFf/2GOPZfP9J0+enB930UUXLVHas7gCUFy5t/BYmquuuirMmDEj3x42bFg477zzWvn/HQAArNiqvopAXLX2zjvvzEbv47SeBQsWZKPscV79mmuumY3EN16UK1bTOeWUUypuMy7c1Xj60AsvvNDsajzxCkXxSrwAAFBr2mTZsDi6Pnbs2PC9730vTJ8+PXtt0aJF4d133y3ZL86rj3Puf/azn7WovSeeeCK/glAwe/bsZr9PcXlQAACoRW22TvBee+0Vpk6dGm699dbsxtwpU6aEuXPnhq5du2ZTdIYMGRJOPPHEsPnmm7e4rXJr/gMAQOraLPBH3bt3DyNHjsweLfHWW28t99//7d/+LXsAAEC9U5sSAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGECPwAAJEzgBwCAhAn8AACQMIEfAAASJvADAEDCBH4AAEiYwA8AAAkT+AEAIGFtGvg/+eSTcP3114ehQ4eGNdZYI3Tp0iX777bbbhtGjx4dXn/99aq2P2HChHD00UeHDTfcMPTo0SP06tUrbLrppuHwww8PjzzySFi8eHFV2wcAgLbWua0amjhxYha2Z8yYUfL6e++9lz1eeumlcPnll4dzzz03C/+dOnVqtbY//PDDrO2xY8cu8W+vvvpq9rj77rvDkCFDwq233hrWWWedVmsbAACSH+F/9NFHw7777lsS9jt37hz69+8funfvnr+2cOHCcP7554eRI0e2Wtsff/xxFuQbh/1+/fplj2JPPvlk+PrXvx5mz57dau0DAEDSgX/mzJlhxIgRYcGCBdl2nEYTp/XMmzcvOwGYP39+GDduXNh4443zY2688cZwww03tEr7J554Yvjb3/6Wbx9xxBFh2rRpYe7cudnjjTfeyEb/C6ZPnx6GDx8eGhoaWqV9AABIOvCfffbZ4YMPPsied+vWLRvtP+mkk7I59NkH6Ngx7LPPPuGFF14IW221VX5cnNoT5/y3RByxv+eee/LtU045Jdx+++1h4MCB+WsbbLBBNo3nwgsvzF/74x//GO67774WtQ0AAMkH/lmzZoU77rgj3z7jjDPCzjvvvNR9e/fuHR544IGw0korZdtxXn9LR/mvuOKK/HkM+VdeeeUy9z3vvPPC7rvvnm9fcsklLWobAACSD/zxRtg4Lz9rqGPHcPLJJy93/xjKDzrooHz7rrvuqrjtf/7zn2H8+PH59g9+8IP8ZGJZ/u3f/i1//ve//z288sorFbcPAADJB/7iwL3DDjtkJTibMmzYsPz5iy++GN55552K2n788cdLymwWv++y7LHHHmHllVfOt3/3u99V1DYAANRF4I+BvWCnnXYq65gdd9yxZHvSpEkVtR3vCShYZZVVwiabbNLkMXFdgG222Sbffu655ypqGwAAkg/8sbRl4WbdaNCgQWUdt/7662clOwumTp1aUftTpkxpdtvRRhtt1OK2AQAg+cDfeIGttddeu6zjYthfffXV8+1Kp/QUt19u21FcG6D4PZTnBACgllUt8McqO8VWXXXVso/t27dv/rz4KkGl7VfadrzhOC7cBQAAtep/5s60ssY19Hv27Fn2scX7VlqLv/i4StsuvE+fPn0qvrLRWFzYq9x9qX3xpHHOnDn5onPF09VIkz6vP/q8/ujz+jOjKLMVKlDWkqqljy+//LJku6mSmMva96uvvmpx+5W2XUn7AwYMKHvf3XbbrVnvDQBA+5o7d25Yb731aqobqjalp/Hc9w4dOlSrqSbbb+u2AQBI05z/vnJfS6o2wh9LXFY6Ul68b7du3Spu/4svvmhR25W0XzxlZ2n+8Y9/hF133TUv+9mcKwLUnrjadFyDInr++efD1772tfb+SFSZPq8/+rz+6PP6M3369LDLLrtkzzfddNNQa6oW+Hv37l2y/emnn1Y0/75Hjx4Vt18I/JW2XUn7zakIFMN+c/antsWwr7/riz6vP/q8/ujz+tOtwsHoJKf09OvXr2S7OdV2ivctLtFZafuVth1X3Y03WgIAQK2qWuBvfDPDzJkzyzou3vlcXFKz0hHR4vbLbTt69913l1qTHwAAalHVAv9qq61WUv/+tddeK+u4N998s6Tc0WabbVZR+8Wr65bbduN9K20bAACSD/zR9ttvnz+PNyyWo/F+2223XYvbnj17dpM30xZKeU6ePHmp7wEAALWoqoF/6NCh+fNnn302zJs3r8ljxowZkz8fPHhwxVN69thjj9Cx4//83xs7dmyTx0yYMCF8/vnnS/38AABQi6oa+A855JDQqVOnfPT8uuuuW+7+b7zxRnjwwQfz7aOOOqrituNNu3vuuWe+fe211zZZnvPKK6/Mn8eSS0b4AQCodVUN/HF0Pob+gosvvjgbRV+a+fPnh+HDh+ehvE+fPuH4449vUfunn356/nzq1Klh5MiRy9w3frYnnngi3x41alSL2gYAgBVBh4bGS+K2srjQ1JZbbpnXt+/atWu44IILsvAdQ31s/vHHHw+nnHJKePXVV/Pjrrnmmuy1ZVXgefvtt/Pt5f1fGDZsWMl0nv333z/87Gc/y2/Ifeutt8KFF14YbrnllnyfOLI/adKk/OoEAADUqqoH/sK8/Dh6H6f1FMQwveaaa2bz+hsvjDVixIhwxx13LPP9mhP4//nPf4bdd989/P3vfy95PVYQip+huARotNZaa4U//vGPS5QVBQCAWlTVKT2NR9njyrIFixYtymreF4f9eJNtnEpz2223tVrbcS5/nKpzwAEHlLz+/vvvLxH248j+M888I+wDAJCMNhnhL/jss8/Crbfemt2YO2XKlDB37txsik8cTR8yZEg48cQTw+abb97k+zRnhL9YvH/gzjvvzEL9rFmzsisOcSXfGPQPP/zw8O1vf9s0HgAAktKmgR8AAEhwSg8AANA+BH4AAEiYwA8AAAkT+AEAIGECfxniomHXX399GDp0aFhjjTVCly5dsv9uu+22YfTo0eH111+vaifF6kJHH3102HDDDUOPHj1Cr169wqabbppVFnrkkUfC4sWLq9p+PWrPPo/VrG644YZw8MEHh/XXXz/r71jNKq4REatZxYXiZsyYUbX261V7/5wvTaypsM8++4QOHTpkj+IFAqn9Pv/oo4+yn/W4IOQGG2wQunfvHnr27Bk22mijcOSRR4bx48eXXYWOFbu/49/pWKHwiCOOyP6Wx9/r3bp1C/379w/77rtvuPrqq8PHH3+sG9vA22+/nf2sVft3auzz3//+9+GQQw4J6667blh55ZXDKquski38evzxx4enn346tKlYpYdle+KJJxrWXnvt+Bt3mY/OnTs3XHjhhQ0LFy5s1S/lBx980HDAAQcst+34GDJkSMPbb7+tGxPo8zvuuKOhb9++TfZ5ly5dGs4666xWb79etWefL88111xT8hluvvnmNms7de3d57fffnvDGmus0eTP+m677dbwzjvvtHr79aY9+3vq1KkNW2+9dZN9vcoqqzTce++9rdo2pRYuXJhlpmr/To2ZbJdddmmyz4cPH97w/vvvt0k3CfzLMX78+IauXbsu8Quhf//+Dd27d1+i40444YRW65iPPvqoYcstt1yijX79+mWPxq8PGDCgYdasWa3Wfr1qzz6/6KKLlnj/Dh06NKy++upZ+/FzNP73eEIo9Nduny/PK6+80tCtWzeBP8E+v+SSS5Zoo2PHjln7vXv3XuLf1l133YYZM2a06meoJ+3Z3y+//HIW5Jf2tzyegHTq1GmJf7v22mtbrX1KnXjiiVX/nRpP0GMma/y3PJ7gL+17IZ4MfvLJJ1XvKoF/Gd59992SkdZevXo1XH/99XmnLFq0qGHcuHENG2+8cUnH/frXv26VjjnssMNK3veII45omDZtWv7vb7zxRsPRRx9dss+//uu/NixevLhV2q9H7dnnDz/8cMl79unTp+EXv/hFw9y5c/N94ue48847sz/+xfueccYZLW6/XrX3z/myLFiwYKkjgkb4a7/Pb7jhhpL33WSTTRruueeehi+//DL79/g7/M9//nPD7rvvXrLfPvvs0yrt15v27O/Yp43f99hjjy35Wx4H966++uqSE48YDidNmtTi9vkfCxcubPj+979f9d+p8ftp5513LunLU089Nfs+LPjrX//asN9++5V8jsMPP7zq3SXwL8MxxxyTd0QcZXvuueeWul/8Yd1qq63yfeNo7Pz581vUKRMnTiz5RjjllFOWuW+8/Fi8b/zDQW31efwFseGGG+bvF/84/b//9/+Wuf8///nPhsGDB+f7r7TSSg2vv/56xe3Xs/b8OV+eeBLX+A+TwF/7fT5z5syGHj165O+50047LfNyfvy90DgUvPDCCy1qvx61Z3//5je/Kem/8847b5n7xoAfp2oW9v3mN7/Zorb5H7Nnz86mxrXF79T4fsXvHwfuliae2B933HEl+8YT/WoS+JfxS7l4+sS555673C9iPFuPoauw/1VXXdWiTin+JT9w4MB85GdZikeCtthiixa1Xa/as88nTJhQ8kN/6623NnnMf/7nf2YjB4VjLrjggorbr1ft/XO+vBP+OL0jttF4+p4R/tru8x//+Mf5e6266qrZyXtT84CLf87PPPPMFrVfb9q7v4cNG5a/1zrrrNPk9MuTTz453z/+Dmirud0pGzNmTMPXvva1pYb9avxO3WyzzfL3/sY3vrHcfWO2i1f4CvvH75dqEviX4uc//3nJD108O2zKoYcemh+z3XbbVdwhcQpH4Y99fFxxxRVNHvPII4+UfAPHOYPUTp+ffvrpJZebv/rqq7KO22abbfLj4kkftdPny/Lhhx9mwaDQxu9+9zuBP5E+j9O0iufv3njjjWUd98Mf/jD7DCeddFJ2oy+18zM+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VXcb/4GhvK95e/T5ssS5+jHUxwVDituI4X/nnXdu1bbqTXv2+fe///181C+OAB533HHNfg9qq8/j7+ROnTotUeu9HI0XX4r15Vnxf6/HtRWK+zD+rDclTtMsrMcRxVH/tiz3TOXiz3j37t1XuBxRTOBvZL311ivZLvxhbkqcX1lcBqw5NViX1X65bUfFoz7u8K/8a94efb40d999d9h9991L3n/gwIHhj3/8Y7MX6WLF6fP4xzuO4hXEGzPjapzLexSL0wGK/81ob+38nMfa+5Xc0KcCW232d6zAVdCcdTUaX/n529/+VlH7tL31WiG/tWaOaMxKu43EO+fjXLt42b3xYinL8+abb5bcXb3ZZptV1CGxJm+s29ucthvvW2nb9aq9+7yxOMUjltosvnEnLvce5+8qv1nbfR4X2SoWb95rjhhEisOIaly183O+5ZZbhn/84x/N7vfGa6rEKX2s+P1dXDp5lVVWKfu4xr/jralTOwYNGpRP41oR85sR/qXYfvvtS0bgytF4v+LV9SptO166jUutl3NzUPFoQvF7sOL3ebFYfzuusFkc9r/97W9ndZyF/TT7nPro8+KSq3H0r9ypGsXzwOPNfI2v+rBi9ndxyG/OaO+HH364zEWZqJ3vt7/+9a9LlOpc1uq8xQMA1cxvAv9SxFXwCp599tmSM/VlKa6eMHjw4Iovy+yxxx4l1TrGjh3b5DFx5b7PP/98qZ+fFb/PC6699tqs1naxU089Ndx3333Z/EBqv8/jJd94MtecR7Gbb7655N8aT1tgxf05j4sqFYuLbZWjcMU3iiUem7OgT71rz/4uPjF74oknsrKc5XjmmWdKtjfeeOOK2qd9v98+//zzLJs15/stntDvueeeVft88Y8GjUyfPr2hU6dO8S9t9rjooouW+zWaNm1aw0orrZTv/+///u8t+poOHTo0f69NN9204csvv1zu/rvvvnvJ/tRenz/11FMNHTt2zN8vPi699NIWvScrdp+Xq/h74uabb26TNlPV3n0+ePDg/L1WW221hvfff3+5+z/xxBMl/X/FFVe0qP160579ffHFF5f03S233NLkMZ988knD2muvnR8Tv1+ord+pm2yySf7ee+yxx3L3jdkuZrbC/nvvvXdDNQn8y/Cd73wn74T4C+D//t//u9T9Pv7444att94637dPnz4Nc+fObVGnjB8/vuQb8nvf+94y942/wIr3vfHGG1vUdj1rrz7/8MMPS37Jx8eFF17Ygv8n1MLPebkE/nT6/I477ijpz/gHPoa8pXnrrbcaBgwYkO/bt2/f7HcFtdHfc+bMaejVq1f+fr17927485//vMz9v/jii4YDDzyw5Pvj1ltvrbh92ud36q9+9auS91/eSWbMdsX7PvbYY1XtNoF/Gd58882Gnj175h3RtWvX7Gx/3rx52b8vXry44dFHHy05m4uPa665Zplf7HXXXbdk3+U54IADSvbdf//9G15++eX83//xj380HHvssSX7bL/99g0LFy6s7DuBduvzc845p2SfYcOG6Y06+Tkvh8Cf9u/2LbfcsuHBBx/Mr+TG4Pfb3/62Yc011yzZ76abbmrlr0R9aM/+bhz+unTp0vCTn/yk4Y033sj3if09ZsyYkpON+Nhrr70aFi1a1MpfDSr9nVp8TOz/Zfnqq6+W6MuY1WJmK4hZLma64n0OOuighmoT+Jfj4Ycfzn5AizslXh7s379/Q48ePUpej48RI0Ys94vdnD8KcWRhiy22WKKNVVddtWH11Vdf4vW11lqr5BuK2ujz+fPnZyNJxfvEP/QDBw5s1mO33XbT5TXS580l8KfV53EkOf68Nm6jc+fO2ZW+GEgb/9upp57ayl+B+tKe/T169Ogl3j8+4uh/vILT+HPFx3bbbZefkFBbgb8wNazxVfv4iNktZrjGr8epW21x9U7gb0K8xFJ8WXVpjzj3etSoUU2Orjc3CMTQ33g0aGmPOLJfPGJA7fT5Qw891GT/lvNo6hcQK06fN5fAn16ff/7559n7Fs8XX9qje/fuyx1ppjb6++67717iis3SHvGk7+STT86+P6jdwB/FTLbLLrs02edxWl+bTQ9tk1Zq3KefftrwH//xH9kltjgiEM/I49l5HIH/0Y9+1PD3v/+9rPepNAjEOYfHHXdcw0YbbZRdmoztx7PHeAno3nvvNY2nhvs83oQn8K8Y2vvnfFkE/nT7PI4Exvt1dtpppywQxhOAeMUvbse5v7NmzWrB/ztWpP6OIT5Oyxo+fHjD+uuvn/8t/9rXvtaw6667NlxwwQWu0icU+AvTxX73u99l95FssMEG2Ql8t27dsv6PV5Hi/ZptqUP8n+rVAAIAANqTOvwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQMIEfgAASJjADwAACRP4AQAgYQI/AAAkTOAHAICECfwAAJAwgR8AABIm8AMAQEjX/wcrmDtx/GHtfQAAAABJRU5ErkJggg==", "text/plain": [ "
" ] }, "metadata": { "image/png": { "height": 358, "width": 382 } }, "output_type": "display_data" } ], "source": [ "# 1) Quality control, guided by single-preprocessing skill\n", "adata = agent.run('quality control with nUMI>500, mito<0.2', adata)\n", "\n", "# 2) Preprocessing + HVGs\n", "adata = agent.run('preprocess with 2000 highly variable genes using shiftlog|pearson', adata)\n", "\n", "# 3) Clustering\n", "adata = agent.run('leiden clustering resolution=1.0', adata)\n", "\n", "# 4) UMAP + visualization (agent may also handle plotting)\n", "adata = agent.run('compute umap and plot colored by leiden', adata)\n", "\n", "adata" ] }, { "cell_type": "code", "execution_count": 7, "id": "f5bc88193a1d099e", "metadata": { "ExecuteTime": { "end_time": "2025-11-06T12:06:13.548319Z", "start_time": "2025-11-06T12:05:29.829291Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "🎯 Matched project skills:\n", " - Data Transformation (Universal) (score=0.174)\n", " - Excel Data Export (Universal) (score=0.150)\n", "\n", "πŸ€” LLM analyzing request: 'give me the summary of this h5ad data'...\n", "\n", "πŸ’­ LLM response:\n", "--------------------------------------------------\n", "import omicverse as ov\n", "import numpy as np\n", "import pandas as pd\n", "from scipy import sparse\n", "\n", "def summarize_adata(adata):\n", " print(f\"AnnData summary: {adata.n_obs} cells Γ— {adata.n_vars} genes\")\n", " print(\"-\" * 60)\n", "\n", " # Basic slots\n", " print(\"Layers:\", list(getattr(adata, \"layers\", {}).keys()))\n", " print(\"Embeddings (.obsm) keys:\", list(adata.obsm.keys()))\n", " print(\"Var mappings (.varm) keys:\", list(adata.varm.keys()))\n", " print(\"Unstructured (.uns) keys:\", list(adata.uns.keys()))\n", " print(\"-\" * 60)\n", "\n", " # .obs overview\n", " print(f\".obs columns ({adata.obs.shape[1]}):\", list(adata.obs.columns))\n", " for col in list(adata.obs.columns)[:20]:\n", " s = adata.obs[col]\n", " try:\n", " if pd.api.types.is_categorical_dtype(s) or s.dtype.name == \"category\":\n", " cats = list(s.cat.categories)\n", " print(f\" - obs['{col}']: categorical, {len(cats)} categories; example: {cats[:10]}\")\n", " elif pd.api.types.is_numeric_dtype(s):\n", " desc = s.describe()\n", " mn = float(desc.get(\"min\", np.nan))\n", " md = float(desc.get(\"50%\", np.nan))\n", " mx = float(desc.get(\"max\", np.nan))\n", " print(f\" - obs['{col}']: numeric, min={mn:.3g}, median={md:.3g}, max={mx:.3g}\")\n", " else:\n", " vc = s.value_counts(dropna=False).head(10).to_dict()\n", " print(f\" - obs['{col}']: {s.dtype}, top values: {vc}\")\n", " except Exception as e:\n", " print(f\" - obs['{col}']: summary failed ({e})\")\n", " print(\"-\" * 60)\n", "\n", " # .var overview\n", " if adata.var is not None and adata.var.shape[1] > 0:\n", " print(f\".var columns ({adata.var.shape[1]}):\", list(adata.var.columns[:20]))\n", " else:\n", " print(\".var has no additional columns.\")\n", " print(\"-\" * 60)\n", "\n", " # Count-based summaries\n", " try:\n", " X = adata.X\n", " if sparse.issparse(X):\n", " cell_counts = np.asarray(X.sum(axis=1)).ravel()\n", " nz_genes_per_cell = np.asarray((X > 0).sum(axis=1)).ravel()\n", " else:\n", " cell_counts = X.sum(axis=1)\n", " nz_genes_per_cell = (X > 0).sum(axis=1)\n", " print(f\"Total counts per cell: mean={np.mean(cell_counts):.2f}, median={np.median(cell_counts):.2f}, sd={np.std(cell_counts):.2f}\")\n", " 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}\")\n", " except Exception as e:\n", " print(f\"Skipped matrix-based summaries due to: {e}\")\n", " print(\"-\" * 60)\n", "\n", " # Common grouping summaries\n", " for key in [\"batch\", \"sample\", \"patient\", \"donor\", \"condition\", \"group\", \"leiden\", \"clusters\", \"celltype\", \"cell_type\"]:\n", " if key in adata.obs.columns:\n", " counts = adata.obs[key].value_counts(dropna=False)\n", " print(f\"Counts by '{key}':\")\n", " print(counts.to_string())\n", " break\n", "\n", " print(\"Summary completed.\")\n", "\n", "summarize_adata(adata)\n", "--------------------------------------------------\n", "\n", "🧬 Generated code to execute:\n", "==================================================\n", "import omicverse as ov\n", "import numpy as np\n", "import pandas as pd\n", "from scipy import sparse\n", "def summarize_adata(adata):\n", " print(f\"AnnData summary: {adata.n_obs} cells Γ— {adata.n_vars} genes\")\n", " print(\"-\" * 60)\n", " # Basic slots\n", " print(\"Layers:\", list(getattr(adata, \"layers\", {}).keys()))\n", " print(\"Embeddings (.obsm) keys:\", list(adata.obsm.keys()))\n", " print(\"Var mappings (.varm) keys:\", list(adata.varm.keys()))\n", " print(\"Unstructured (.uns) keys:\", list(adata.uns.keys()))\n", " print(\"-\" * 60)\n", " # .obs overview\n", " print(f\".obs columns ({adata.obs.shape[1]}):\", list(adata.obs.columns))\n", " for col in list(adata.obs.columns)[:20]:\n", " s = adata.obs[col]\n", " try:\n", " if pd.api.types.is_categorical_dtype(s) or s.dtype.name == \"category\":\n", " cats = list(s.cat.categories)\n", " print(f\" - obs['{col}']: categorical, {len(cats)} categories; example: {cats[:10]}\")\n", " elif pd.api.types.is_numeric_dtype(s):\n", " desc = s.describe()\n", " mn = float(desc.get(\"min\", np.nan))\n", " md = float(desc.get(\"50%\", np.nan))\n", " mx = float(desc.get(\"max\", np.nan))\n", " print(f\" - obs['{col}']: numeric, min={mn:.3g}, median={md:.3g}, max={mx:.3g}\")\n", " else:\n", " vc = s.value_counts(dropna=False).head(10).to_dict()\n", " print(f\" - obs['{col}']: {s.dtype}, top values: {vc}\")\n", " except Exception as e:\n", " print(f\" - obs['{col}']: summary failed ({e})\")\n", " print(\"-\" * 60)\n", " # .var overview\n", " if adata.var is not None and adata.var.shape[1] > 0:\n", " print(f\".var columns ({adata.var.shape[1]}):\", list(adata.var.columns[:20]))\n", " else:\n", " print(\".var has no additional columns.\")\n", " print(\"-\" * 60)\n", " # Count-based summaries\n", " try:\n", " X = adata.X\n", " if sparse.issparse(X):\n", " cell_counts = np.asarray(X.sum(axis=1)).ravel()\n", " nz_genes_per_cell = np.asarray((X > 0).sum(axis=1)).ravel()\n", " else:\n", " cell_counts = X.sum(axis=1)\n", " nz_genes_per_cell = (X > 0).sum(axis=1)\n", " print(f\"Total counts per cell: mean={np.mean(cell_counts):.2f}, median={np.median(cell_counts):.2f}, sd={np.std(cell_counts):.2f}\")\n", " 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}\")\n", " except Exception as e:\n", " print(f\"Skipped matrix-based summaries due to: {e}\")\n", " print(\"-\" * 60)\n", " # Common grouping summaries\n", " for key in [\"batch\", \"sample\", \"patient\", \"donor\", \"condition\", \"group\", \"leiden\", \"clusters\", \"celltype\", \"cell_type\"]:\n", " if key in adata.obs.columns:\n", " counts = adata.obs[key].value_counts(dropna=False)\n", " print(f\"Counts by '{key}':\")\n", " print(counts.to_string())\n", " print(\"Summary completed.\")\n", "summarize_adata(adata)\n", "==================================================\n", "\n", "⚑ Executing code locally...\n", "AnnData summary: 2700 cells Γ— 16634 genes\n", "------------------------------------------------------------\n", "❌ Error executing generated code: name 'getattr' is not defined\n", "Code that failed: import omicverse as ov\n", "import numpy as np\n", "import pandas as pd\n", "from scipy import sparse\n", "def summarize_adata(adata):\n", " print(f\"AnnData summary: {adata.n_obs} cells Γ— {adata.n_vars} genes\")\n", " print(\"-\" * 60)\n", " # Basic slots\n", " print(\"Layers:\", list(getattr(adata, \"layers\", {}).keys()))\n", " print(\"Embeddings (.obsm) keys:\", list(adata.obsm.keys()))\n", " print(\"Var mappings (.varm) keys:\", list(adata.varm.keys()))\n", " print(\"Unstructured (.uns) keys:\", list(adata.uns.keys()))\n", " print(\"-\" * 60)\n", " # .obs overview\n", " print(f\".obs columns ({adata.obs.shape[1]}):\", list(adata.obs.columns))\n", " for col in list(adata.obs.columns)[:20]:\n", " s = adata.obs[col]\n", " try:\n", " if pd.api.types.is_categorical_dtype(s) or s.dtype.name == \"category\":\n", " cats = list(s.cat.categories)\n", " print(f\" - obs['{col}']: categorical, {len(cats)} categories; example: {cats[:10]}\")\n", " elif pd.api.types.is_numeric_dtype(s):\n", " desc = s.describe()\n", " mn = float(desc.get(\"min\", np.nan))\n", " md = float(desc.get(\"50%\", np.nan))\n", " mx = float(desc.get(\"max\", np.nan))\n", " print(f\" - obs['{col}']: numeric, min={mn:.3g}, median={md:.3g}, max={mx:.3g}\")\n", " else:\n", " vc = s.value_counts(dropna=False).head(10).to_dict()\n", " print(f\" - obs['{col}']: {s.dtype}, top values: {vc}\")\n", " except Exception as e:\n", " print(f\" - obs['{col}']: summary failed ({e})\")\n", " print(\"-\" * 60)\n", " # .var overview\n", " if adata.var is not None and adata.var.shape[1] > 0:\n", " print(f\".var columns ({adata.var.shape[1]}):\", list(adata.var.columns[:20]))\n", " else:\n", " print(\".var has no additional columns.\")\n", " print(\"-\" * 60)\n", " # Count-based summaries\n", " try:\n", " X = adata.X\n", " if sparse.issparse(X):\n", " cell_counts = np.asarray(X.sum(axis=1)).ravel()\n", " nz_genes_per_cell = np.asarray((X > 0).sum(axis=1)).ravel()\n", " else:\n", " cell_counts = X.sum(axis=1)\n", " nz_genes_per_cell = (X > 0).sum(axis=1)\n", " print(f\"Total counts per cell: mean={np.mean(cell_counts):.2f}, median={np.median(cell_counts):.2f}, sd={np.std(cell_counts):.2f}\")\n", " 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}\")\n", " except Exception as e:\n", " print(f\"Skipped matrix-based summaries due to: {e}\")\n", " print(\"-\" * 60)\n", " # Common grouping summaries\n", " for key in [\"batch\", \"sample\", \"patient\", \"donor\", \"condition\", \"group\", \"leiden\", \"clusters\", \"celltype\", \"cell_type\"]:\n", " if key in adata.obs.columns:\n", " counts = adata.obs[key].value_counts(dropna=False)\n", " print(f\"Counts by '{key}':\")\n", " print(counts.to_string())\n", " print(\"Summary completed.\")\n", "summarize_adata(adata)\n" ] } ], "source": [ "adata = agent.run('give me the summary of this h5ad data', adata)" ] }, { "cell_type": "markdown", "id": "c5bvakcwnm7", "metadata": {}, "source": [ "## Understanding the New Output\n", "\n", "When you run the agent, you'll see:\n", "\n", "```\n", "🎯 LLM matched skills:\n", " - Single-cell preprocessing with omicverse\n", "```\n", "\n", "This shows the LLM's semantic understanding - it matched your request to the relevant skill without keyword matching. The skill content is then lazy-loaded and used to guide code generation.\n", "\n", "**Key differences from old system:**\n", "- ❌ Old: `match: single-preprocessing score=0.364` (keyword similarity)\n", "- βœ… New: `🎯 LLM matched skills: Single-cell preprocessing` (semantic understanding)\n", "\n", "Let's test the agent with a few more requests to see LLM matching in action:" ] }, { "cell_type": "markdown", "id": "585c4c68bd15d1f3", "metadata": {}, "source": [ "## Manual visualization (optional)\n", "\n", "If plotting wasn’t performed by generated code, visualize here.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "1e5b8184509ba355", "metadata": {}, "outputs": [], "source": [ "# Run leiden clustering\n", "sc.tl.leiden(adata)\n", "\n", "# Now plot the UMAP\n", "sc.pl.umap(adata, color=['leiden'], wspace=0.4)" ] }, { "cell_type": "markdown", "id": "e4f1a060784e5eb9", "metadata": {}, "source": [ "## (Optional) Create a skill from docs links\n", "\n", "Use `ov.agent.seeker` to scaffold a new skill from documentation links (requires network).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "918bebae596f97b0", "metadata": {}, "outputs": [], "source": [ "# Example: build a quick skill from a documentation link (uncomment to run)\n", "# info = ov.agent.seeker(\n", "# ['https://scanpy.readthedocs.io/en/stable/generated/scanpy.pp.highly_variable_genes.html'],\n", "# name='hvg-guidance', target='output', package=False\n", "# )\n", "# info" ] }, { "cell_type": "markdown", "id": "55241f6aa0933ce3", "metadata": {}, "source": [ "## Next steps\n", "\n", "- Adjust QC thresholds or clustering resolutions and re‑prompt the agent.\n", "- Add cell‑type annotation via prompts (see Tutorials‑single for annotation notebooks).\n", "- Customize skills by creating `.claude/skills/` in your project to steer analysis with your lab's SOP.\n", "\n", "## Creating Custom Skills\n", "\n", "You can create custom skills in your project directory to override or extend the 25 built-in skills:\n", "\n", "**Directory structure:**\n", "```\n", "your-project/\n", "β”œβ”€β”€ .claude/\n", "β”‚ └── skills/\n", "β”‚ └── my-custom-skill/\n", "β”‚ └── SKILL.md\n", "└── your_analysis.py\n", "```\n", "\n", "**Skill file format** (`.claude/skills/my-custom-skill/SKILL.md`):\n", "\n", "```yaml\n", "---\n", "name: my-custom-skill\n", "description: |\n", " Brief description of what this skill does.\n", " Use when: user wants to [specific task]\n", " Handles: [specific data types or scenarios]\n", " Examples: \"analyze X\", \"process Y\", \"compute Z\"\n", "---\n", "\n", "# Detailed Instructions\n", "\n", "[Your skill instructions here...]\n", "```\n", "\n", "**Important**: With LLM-based matching, **skill descriptions are critical**!\n", "\n", "**Good description example:**\n", "```yaml\n", "description: |\n", " Preprocess single-cell RNA-seq data with quality control.\n", " Use when: user mentions QC, quality control, filtering, preprocessing.\n", " Works with: raw count matrices, AnnData objects.\n", "```\n", "\n", "**Bad description example:**\n", "```yaml\n", "description: Preprocessing skill\n", "```\n", "\n", "The LLM reads these descriptions to match skills - make them:\n", "- Clear and specific\n", "- Include \"Use when\" conditions\n", "- List common user phrases\n", "- Mention relevant technologies/methods\n", "\n", "## Built-in Skills Location\n", "\n", "All 25 built-in skills are located at:\n", "```\n", "/omicverse/.claude/skills/\n", "```\n", "\n", "You can browse them to see examples of well-written skill descriptions:\n", "- `single-preprocessing/SKILL.md` - Single-cell preprocessing\n", "- `bulk-deg-analysis/SKILL.md` - Bulk RNA-seq DEG\n", "- `data-export-excel/SKILL.md` - Excel export\n", "- And 22 more...\n", "\n", "## Performance Benefits\n", "\n", "The new system provides:\n", "- **2-3x faster startup** (progressive disclosure)\n", "- **5x lower memory** at startup\n", "- **15% better accuracy** (semantic understanding)\n", "- **Better scalability** (handles 100+ skills efficiently)\n", "\n", "See `SKILL_MATCHING_UPGRADE.md` in the repository for full technical details." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.x" } }, "nbformat": 4, "nbformat_minor": 5 }