Skip to content

Commit f472200

Browse files
author
Parekh
committed
added clinical download and web query agents
1 parent 053e127 commit f472200

10 files changed

Lines changed: 565 additions & 1 deletion

app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import tools.bih_query as bih_mod
4848
import tools.tcia_download as tcia_dl_mod
4949
import tools.idc_download as idc_dl_mod
50+
import tools.clinical_data as clin_mod
5051
from tools.shared import TOOL_REGISTRY
5152

5253

@@ -108,6 +109,7 @@ def oauth_callback(
108109
midrc_dl_mod.configure_midrc_download_tool()
109110
tcia_dl_mod.configure_tcia_download_tool()
110111
idc_dl_mod.configure_idc_download_tool()
112+
clin_mod.configure_clinical_data_tool()
111113

112114
_ = dq_mod.idc_query_runner
113115
_ = img_mod.imaging_runner
@@ -120,6 +122,7 @@ def oauth_callback(
120122
_ = midrc_dl_mod.midrc_download_runner
121123
_ = tcia_dl_mod.tcia_download_runner
122124
_ = idc_dl_mod.idc_download_runner
125+
_ = clin_mod.clinical_data_download_runner
123126

124127
ALL_TOOLS: tuple[BaseTool, ...] = tuple(TOOL_REGISTRY)
125128
TOOL_NAMES = {tool.name: tool for tool in ALL_TOOLS}
@@ -144,6 +147,7 @@ def build_graph(checkpointer=None):
144147
- Do not ask the user follow up questions unless absolutely necessary.
145148
- Do not do more than what the user requests.
146149
- Do not suggest next steps for the user unless they ask for them.
150+
- For downloading DICOM Series or histopathology tiles, always download one patient at a time. If the user requests multiple patients, call the download tool multiple times, once per patient.
147151
- When using llm based tools which generate code, be as concise as possible in your instructions, unless an error occurs, then be as specific as possible to fix the issue.
148152
- Assume that tools cannot see each other's output or the conversation history. You must pass information between tools yourself.
149153
---

app_idc.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
import tools.idc_query as dq_mod
3838
import tools.dicom_to_nifti as d2n_mod
3939
import tools.idc_download as idc_dl_mod
40+
import tools.idc_web_qa as webqa_mod
41+
import tools.pathology_download as path_mod
42+
import tools.clinical_data as clin_mod
4043
from tools.shared import TOOL_REGISTRY
4144

4245
'''
@@ -68,20 +71,34 @@ def oauth_callback(
6871
system_prompt=(_P("prompts/agent_systems/idc_query.txt").read_text()),
6972
)
7073
idc_dl_mod.configure_idc_download_tool()
74+
clin_mod.configure_clinical_data_tool()
75+
webqa_mod.configure_idc_web_qa_tool()
76+
path_mod.configure_pathology_download_tool()
7177

7278
_ = dq_mod.idc_query_runner
7379
_ = d2n_mod.dicom2nifti_runner
7480
_ = idc_dl_mod.idc_download_runner
81+
_ = webqa_mod.idc_web_qa_runner
82+
_ = clin_mod.clinical_data_download_runner
83+
_ = path_mod.pathology_download_runner
7584

7685
ALL_TOOLS: tuple[BaseTool, ...] = tuple(TOOL_REGISTRY)
7786

7887
def build_graph(checkpointer=None):
7988
policy = SystemMessage(content=(
8089
f"""
81-
You are **VoxelInsight IDC**, an AI assistant focused on IDC metadata, series downloads, and DICOM→NIfTI conversion.
90+
You are **VoxelInsight IDC**, a multi-agent assistant for IDC: metadata Q&A, web-grounded answers, radiology downloads,
91+
histopathology tiles via DICOMweb, clinical data exports (idc_index), and DICOM→NIfTI conversion.
8292
8393
Core behavior
8494
- Only answer what the user asked; request clarifications solely when required to complete a tool call.
95+
- Only when asked about VoxelInsight, answer yourself otherwise always use tools. YOU ARE NOT ALLOWED TO ANSWER DIRECTLY.
96+
- When the user asks questions about IDC documentation, use the `idc_web_qa` tool to answer them based. These are questions like "What is the purpose of IDC?", "How to access IDC data?", "What collections are available in IDC?", etc.
97+
- Primarily if the user's question is a How to or what is, use the `idc_web_qa` tool to answer them based on IDC documentation.
98+
- When the user asks for IDC data (metadata, images, clinical data), use the idc_query tool. These include questions like "How many patients are in IDC?", "List all SeriesInstanceUIDs for CT scans in collection X", "Show me a summary of the IDC metadata tables", etc.
99+
- When the user requests downloads of DICOM series, histopathology tiles, or clinical data, use the respective download tools (`idc_download`, `pathology_download`, `clinical_data_download`).
100+
- For downloading DICOM Series or histopathology tiles, always download one patient at a time. If the user requests multiple patients, call the download tool multiple times, once per patient.
101+
- When the user requests DICOM to NIfTI conversion, use the `
85102
- Tools cannot see each other’s outputs—pass important values (SeriesInstanceUIDs, directories, etc.) yourself.
86103
- Keep tool instructions brief unless retrying an error. Retry at most 3 times (when it seems reasonable/necessary) with progressively clearer directions.
87104
- Before each tool call, tell the user what you are about to do in one concise sentence.
@@ -94,7 +111,10 @@ def build_graph(checkpointer=None):
94111
- For instance do not ask the tool for notes which you could have surmised. You will receive the tools code output and code itself so you can interpret it directly.
95112
- Aim to get the result in as few tool calls as possible. Do not split into multiple calls unless absolutely necessary.
96113
- If the user wants to view or visualize the radiology imaging data without downloading, the idc_query tool can provide links to online viewers.
114+
- `idc_web_qa`: answer general IDC questions grounded in learn.canceridc.dev (or a provided IDC doc URL). Use when the user asks doc questions.
97115
- `idc_download`: download DICOM series by UID. Use exactly the IDs produced by `idc_query` and respect user cancellations.
116+
- `pathology_download`: download histopathology tiles via DICOMweb. Default size 512x512; honor user-specified size. Needs study/series/sop instance UIDs.
117+
- `clinical_data_download`: download IDC clinical data by collection using idc_index (no BigQuery). Optionally select fields and/or filter on a field value.
98118
- `dicom2nifti`: convert downloaded DICOM folders to NIfTI files. Only run it after confirming the directory exists.
99119
- Automatically returns downloaded NIfTI files as download links in the UI.
100120
@@ -289,6 +309,9 @@ def __init__(self):
289309
"midrc_download": "MIDRC Download Tool",
290310
"tcia_download": "TCIA Download Tool",
291311
"universeg": "Universeg Segmentation",
312+
"idc_web_qa": "IDC Web Q&A",
313+
"pathology_download": "Histopathology Download",
314+
"clinical_data_download": "Clinical Data Download",
292315
}
293316

294317
async def _rename_root(self, name: str):

idc_index/default__vector_store.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

idc_index/docstore.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

idc_index/graph_store.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"graph_dict": {}}

idc_index/image__vector_store.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"embedding_dict": {}, "text_id_to_ref_doc_id": {}, "metadata_dict": {}}

idc_index/index_store.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

tools/clinical_data.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from __future__ import annotations
2+
3+
import tempfile
4+
from pathlib import Path
5+
from typing import List, Optional
6+
7+
import pandas as pd
8+
from idc_index import IDCClient
9+
from pydantic import BaseModel, Field
10+
11+
from core.state import TaskResult
12+
from tools.shared import toolify_agent
13+
14+
15+
class ClinicalDataArgs(BaseModel):
16+
collection_id: str = Field(..., description="IDC collection ID (e.g., tcga_brca).")
17+
fields: Optional[List[str]] = Field(
18+
None,
19+
description="Optional list of columns to keep. If omitted, all columns are returned.",
20+
)
21+
filter_field: Optional[str] = Field(
22+
None,
23+
description="Optional column to filter on (exact match).",
24+
)
25+
filter_value: Optional[str] = Field(
26+
None,
27+
description="Value to filter the filter_field by.",
28+
)
29+
limit_rows: int = Field(
30+
5000,
31+
ge=1,
32+
le=50000,
33+
description="Maximum rows to return (safety cap). Default 5000.",
34+
)
35+
36+
37+
_CONFIGURED = False
38+
_CLIENT: Optional[IDCClient] = None
39+
40+
41+
def configure_clinical_data_tool():
42+
"""Initialize the IDC client once."""
43+
global _CONFIGURED, _CLIENT
44+
if _CONFIGURED and _CLIENT is not None:
45+
return
46+
_CLIENT = IDCClient()
47+
_CLIENT.fetch_index("clinical_index")
48+
_CONFIGURED = True
49+
50+
51+
def _available_tables_for_collection(collection_id: str) -> List[str]:
52+
if _CLIENT is None:
53+
raise RuntimeError("IDC client not initialized.")
54+
df = _CLIENT.clinical_index
55+
if "collection_id" not in df.columns:
56+
return []
57+
subset = df[df["collection_id"] == collection_id]
58+
if subset.empty:
59+
return []
60+
if "short_table_name" in subset.columns:
61+
tables = subset["short_table_name"].dropna().unique().tolist()
62+
elif "table_name" in subset.columns:
63+
tables = subset["table_name"].dropna().unique().tolist()
64+
else:
65+
tables = []
66+
return [t for t in tables if t]
67+
68+
69+
@toolify_agent(
70+
name="clinical_data_download",
71+
description="Download IDC clinical data by collection using idc_index (no BigQuery). Can limit columns and apply an equality filter.",
72+
args_schema=ClinicalDataArgs,
73+
timeout_s=180,
74+
)
75+
async def clinical_data_download_runner(
76+
collection_id: str,
77+
fields: Optional[List[str]] = None,
78+
filter_field: Optional[str] = None,
79+
filter_value: Optional[str] = None,
80+
limit_rows: int = 5000,
81+
):
82+
if not _CONFIGURED or _CLIENT is None:
83+
configure_clinical_data_tool()
84+
85+
tables = _available_tables_for_collection(collection_id)
86+
if not tables:
87+
raise ValueError(f"No clinical tables found for collection '{collection_id}'.")
88+
89+
frames: List[pd.DataFrame] = []
90+
for tbl in tables:
91+
try:
92+
df_tbl = _CLIENT.get_clinical_table(tbl)
93+
df_tbl["__source_table"] = tbl
94+
frames.append(df_tbl)
95+
except Exception as e:
96+
raise RuntimeError(f"Failed to load clinical table '{tbl}': {e}")
97+
98+
if not frames:
99+
raise RuntimeError(f"Clinical tables for '{collection_id}' could not be loaded.")
100+
101+
df = pd.concat(frames, ignore_index=True)
102+
103+
if filter_field:
104+
if filter_field not in df.columns:
105+
raise ValueError(f"Field '{filter_field}' not found in clinical data.")
106+
if filter_value is not None:
107+
df = df[df[filter_field] == filter_value]
108+
109+
if fields:
110+
missing = [f for f in fields if f not in df.columns]
111+
if missing:
112+
raise ValueError(f"Requested fields missing: {', '.join(missing)}")
113+
df = df[fields]
114+
115+
df = df.head(limit_rows)
116+
117+
tmp_dir = Path(tempfile.mkdtemp(prefix="clinical_"))
118+
csv_path = tmp_dir / f"{collection_id}_clinical.csv"
119+
df.to_csv(csv_path, index=False)
120+
121+
summary = f"Clinical data downloaded via idc_index: collection={collection_id}, rows={len(df)}, columns={len(df.columns)}"
122+
outputs = {
123+
"text": summary,
124+
"tool": "clinical_data_download",
125+
"df_preview": {"rows": df.head(50).to_dict("records"), "nrows": len(df)},
126+
}
127+
artifacts = {"files": [str(csv_path)]}
128+
return TaskResult(output=outputs, artifacts=artifacts)

0 commit comments

Comments
 (0)