Skip to content

Commit 7972107

Browse files
authored
Overhaul MCP tools, add metadata tool, and introduce skills (#209)
1 parent be8d923 commit 7972107

23 files changed

Lines changed: 686 additions & 924 deletions

packages/datacommons-mcp/datacommons_mcp/agent_api_service.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,16 @@ async def search_indicators(
7171
"include_topics": include_topics,
7272
}
7373
return await client.post("agent/search_indicators", payload)
74+
75+
76+
async def get_variable_metadata(
77+
variable_dcids: list[str],
78+
entity_dcids: list[str],
79+
) -> dict[str, Any]:
80+
"""Retrieves rich structural metadata (definitions, coverage, and provenances) for variables."""
81+
client = _get_agent_api_client()
82+
payload = {
83+
"variable_dcids": variable_dcids,
84+
"entity_dcids": entity_dcids,
85+
}
86+
return await client.post("agent/get_variable_metadata", payload)

packages/datacommons-mcp/datacommons_mcp/agent_api_tools.py

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,109 @@
1515
Tool implementations for the Agent API-based Data Commons MCP server.
1616
"""
1717

18+
from typing import Any
19+
1820
from datacommons_mcp.agent_api_service import (
1921
get_observations as agent_api_get_observations,
2022
)
23+
from datacommons_mcp.agent_api_service import (
24+
get_variable_metadata as agent_api_get_variable_metadata,
25+
)
2126
from datacommons_mcp.agent_api_service import (
2227
search_indicators as agent_api_search_indicators,
2328
)
2429
from datacommons_mcp.data_models.observations import ObservationDateType
2530

31+
# Instruction file constants
32+
SEARCH_INDICATORS_INSTRUCTION_FILE = "tools/search_indicators.md"
33+
SEARCH_CHILD_INDICATORS_INSTRUCTION_FILE = "tools/search_child_indicators.md"
34+
GET_VARIABLE_METADATA_INSTRUCTION_FILE = "tools/get_variable_metadata.md"
35+
GET_OBSERVATIONS_INSTRUCTION_FILE = "tools/get_observations.md"
36+
GET_CHILD_OBSERVATIONS_INSTRUCTION_FILE = "tools/get_child_observations.md"
37+
38+
39+
async def search_indicators(
40+
query: str,
41+
places: list[str] | None = None,
42+
per_search_limit: int = 10,
43+
*,
44+
include_topics: bool = True,
45+
) -> dict[str, Any]:
46+
"""Searches for statistical indicators matching a natural language query."""
47+
return await agent_api_search_indicators(
48+
query=query,
49+
places=places,
50+
parent_place=None,
51+
per_search_limit=per_search_limit,
52+
include_topics=include_topics,
53+
)
54+
55+
56+
async def search_child_indicators(
57+
query: str,
58+
parent_place: str,
59+
sample_child_places: list[str],
60+
per_search_limit: int = 10,
61+
*,
62+
include_topics: bool = True,
63+
) -> dict[str, Any]:
64+
"""Searches for statistical indicators available at the child-place level."""
65+
return await agent_api_search_indicators(
66+
query=query,
67+
places=sample_child_places,
68+
parent_place=parent_place,
69+
per_search_limit=per_search_limit,
70+
include_topics=include_topics,
71+
)
72+
73+
74+
async def get_variable_metadata(
75+
variable_dcids: list[str],
76+
entity_dcids: list[str],
77+
) -> dict[str, Any]:
78+
"""Retrieves definitions, coverage, and provenances for a list of variables."""
79+
return await agent_api_get_variable_metadata(
80+
variable_dcids=variable_dcids,
81+
entity_dcids=entity_dcids,
82+
)
83+
2684

2785
async def get_observations(
2886
variable_dcid: str,
2987
place_dcid: str,
30-
child_place_type: str | None = None,
3188
source_override: str | None = None,
3289
date: str = ObservationDateType.LATEST.value,
3390
date_range_start: str | None = None,
3491
date_range_end: str | None = None,
35-
) -> dict:
36-
"""Fetches observations for a statistical variable from Data Commons."""
92+
) -> dict[str, Any]:
93+
"""Fetches time-series observations for a statistical variable at a specific place."""
3794
return await agent_api_get_observations(
3895
variable_dcid=variable_dcid,
3996
place_dcid=place_dcid,
40-
child_place_type=child_place_type,
97+
child_place_type=None,
4198
source_override=source_override,
4299
date=date,
43100
date_range_start=date_range_start,
44101
date_range_end=date_range_end,
45102
)
46103

47104

48-
async def search_indicators(
49-
query: str,
50-
places: list[str] | None = None,
51-
parent_place: str | None = None,
52-
per_search_limit: int = 10,
53-
*,
54-
include_topics: bool = True,
55-
) -> dict:
56-
"""Searches for indicators (topics and variables) in Data Commons."""
57-
return await agent_api_search_indicators(
58-
query=query,
59-
places=places,
60-
parent_place=parent_place,
61-
per_search_limit=per_search_limit,
62-
include_topics=include_topics,
105+
async def get_child_observations(
106+
variable_dcid: str,
107+
parent_place_dcid: str,
108+
child_place_type: str,
109+
source_override: str | None = None,
110+
date: str = ObservationDateType.LATEST.value,
111+
date_range_start: str | None = None,
112+
date_range_end: str | None = None,
113+
) -> dict[str, Any]:
114+
"""Fetches time-series observations for a statistical variable across child places."""
115+
return await agent_api_get_observations(
116+
variable_dcid=variable_dcid,
117+
place_dcid=parent_place_dcid,
118+
child_place_type=child_place_type,
119+
source_override=source_override,
120+
date=date,
121+
date_range_start=date_range_start,
122+
date_range_end=date_range_end,
63123
)

packages/datacommons-mcp/datacommons_mcp/app.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
MCP_SERVER_NAME = "DC MCP Server"
3737
DEFAULT_INSTRUCTIONS_PACKAGE = "datacommons_mcp.instructions"
3838
SERVER_INSTRUCTIONS_FILE = "server.md"
39+
MODE_AGENT_API = "agent_api"
40+
MODE_LOCAL = "local"
3941

4042

4143
class DCApp:
@@ -55,6 +57,9 @@ def __init__(self) -> None:
5557
logger.error("Settings error: %s", e)
5658
raise
5759

60+
# Establish active mode directory
61+
self.mode_dir = MODE_AGENT_API if self.settings.use_agent_api else MODE_LOCAL
62+
5863
# Create client only if agent APIs are NOT enabled (as fallback is not needed)
5964
self.client = None
6065
if not self.settings.use_agent_api:
@@ -91,30 +96,34 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[dict[str, Any]]:
9196
)
9297

9398
def _load_instructions(self, filename: str) -> str:
94-
"""
95-
Loads markdown content.
99+
"""Loads markdown content relative to the active mode subfolder.
100+
96101
Priority:
97-
1. DC_INSTRUCTIONS_DIR/{filename} (if set and exists)
98-
2. Package default: datacommons_mcp/instructions/{filename}
102+
1. DC_INSTRUCTIONS_DIR/{self.mode_dir}/{filename} (if set and exists)
103+
2. Package default: datacommons_mcp/instructions/{self.mode_dir}/{filename}
99104
"""
105+
path_in_mode = f"{self.mode_dir}/{filename}"
106+
100107
# Check specific override
101108
if self.settings.instructions_dir:
102-
content = read_external_content(self.settings.instructions_dir, filename)
109+
content = read_external_content(
110+
self.settings.instructions_dir, path_in_mode
111+
)
103112
if content is not None:
104113
logger.info(
105114
"Loaded custom instructions for %s from %s",
106-
filename,
115+
path_in_mode,
107116
self.settings.instructions_dir,
108117
)
109118
return content
110119
logger.debug(
111120
"Custom instructions file %s not found in %s, falling back to default.",
112-
filename,
121+
path_in_mode,
113122
self.settings.instructions_dir,
114123
)
115124

116125
# Fallback to package resources
117-
return read_package_content(DEFAULT_INSTRUCTIONS_PACKAGE, filename)
126+
return read_package_content(DEFAULT_INSTRUCTIONS_PACKAGE, path_in_mode)
118127

119128
def register_tool(self, func: Callable[..., Any], instruction_file: str) -> None:
120129
"""Register a tool with instructions loaded from a file.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Act as a Data Commons Research Assistant. This server provides direct access to a massive, unified knowledge graph of aggregated statistical data from authoritative regional and global sources like the UN, World Bank, and Census Bureau. Use it to transform natural language queries into precise statistical insights by identifying specific indicators and retrieving their observations. It contains historical and recent data points on topics like demographics, economics, health, and environment across various geographic levels. It does not contain information on topics like real-time news, subjective viewpoints, or private corporate data.
2+
3+
CRITICAL INSTRUCTION: When the user asks you to perform statistical research, search for indicators, or fetch observations, you MUST first read the skill resource at 'skill://data-commons-researcher/SKILL.md' to load the official research guidelines, place resolution heuristics, and child-sampling playbooks before calling any tools. This playbook contains the required rules and steps to successfully fulfill the query.
4+
5+
Crucially, every data point retrieved must be attributed to its original source provided in the tool output; never present statistics as "known facts" without citing the specific organization or dataset they originated from. Prioritize data integrity and transparency, ensuring that users understand both the metric and the provenance of the information provided.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
name: data-commons-researcher
3+
description: Guidelines, heuristics, and workflows for concept splitting, place resolution, variable metadata assessment, child-level sampling, and retrieving statistical observations from Data Commons.
4+
---
5+
6+
## Foundational Knowledge: Data Commons Graph Structure
7+
8+
Data Commons organizes data into two main structural hierarchies. Understanding these is key to choosing your place names and variables:
9+
10+
1. **Topics (Variable Hierarchy)**: A taxonomy of categories (e.g., `Health` -> `Clinical Data` -> `Medical Conditions`). Topics contain sub-topics and individual variables.
11+
2. **Places (Geographic Hierarchy)**: A taxonomy of spatial containment (e.g., `World` -> `Continent` -> `Country` -> `State` -> `County`).
12+
13+
### Data Availability & Efficiency Tips:
14+
15+
* **Direct Containment Efficiency**: Querying the direct child places of a parent (e.g., all counties inside California) is highly optimized and returns faster than querying arbitrary cross-border place sets.
16+
17+
---
18+
19+
## 1. The Three-Step Tool Pipeline
20+
21+
When researching statistics, always separate your work into three distinct phases to avoid context bloat:
22+
23+
1. **Discovery (`search_indicators` or `search_child_indicators`)**: Use this to find candidate variables matching the user's concept.
24+
2. **Assessment (`get_variable_metadata`)**: Pass candidate variables and target locations to retrieve structural metadata, ensuring the dataset matches the required temporal range, granularity, and source trust.
25+
3. **Retrieval (`get_observations` or `get_child_observations`)**: Fetch the actual timeseries arrays once the variables and facets have been qualified.
26+
27+
### CRITICAL: Always validate variable-place combinations first
28+
* You **MUST** call discovery tools first to verify that the variable exists for the specified place.
29+
* You **MUST** call `get_variable_metadata` to verify dataset facets (source, dates, coverage) before retrieving heavy observation arrays.
30+
* Only use DCIDs returned by the discovery tools - never guess or assume variable-place combinations.
31+
32+
---
33+
34+
## 2. Discovery Heuristics: Concept Splitting & Parameter Tuning
35+
36+
To ensure focused and accurate candidate retrieval when calling discovery tools:
37+
38+
### A. Concept Extraction & Multi-Query Splitting
39+
40+
* **Search Single Concepts**: Always search for one semantic concept at a time.
41+
* **Split Compound Queries**:
42+
* *Incorrect*: `query="health and unemployment rate"` (Causes search index confusion).
43+
* *Correct*: Split into two separate, sequential tool calls:
44+
1. `search_indicators(query="health", ...)`
45+
2. `search_indicators(query="unemployment rate", ...)`
46+
47+
### B. Parameter Configuration Guidelines
48+
49+
* **Toggling Topics (`include_topics`)**:
50+
* Set `include_topics=true` (Default) when the user's request is exploratory (e.g., *"What health data do you have?"*). Use the returned topics to map the category hierarchy.
51+
* Set `include_topics=false` when targeting a specific dataset or observation (e.g., *"Find the diabetes rate for California"*). This reduces the return payload size.
52+
* **Primary Rule**: If a user explicitly states what they want, follow their request. Otherwise, default to the guidelines above.
53+
54+
* **Setting Result Limits (`per_search_limit`)**:
55+
* Always stick to the default value of `10` to keep payloads small.
56+
* Do **not** increase the limit unless the user explicitly requests more candidate indicators.
57+
58+
---
59+
60+
## 3. Geographic Place Qualification & Fallback Recovery
61+
62+
Data Commons requires qualified geographic names to avoid database name conflicts.
63+
64+
### A. Core Qualification Rules
65+
66+
* **Never use DCIDs in Search Parameters**: Only pass qualified, human-readable English place names to `places` or `parent_place` in discovery tools (e.g., use `"California"`, not `"geoId/06"`).
67+
* **Always Qualify Naming Ambiguities**: Add parent geographic or administrative context:
68+
* *New York*: Differentiate between `"New York City, USA"` and `"New York State, USA"`.
69+
* *Washington*: Differentiate between `"Washington, DC, USA"` and `"Washington State, USA"`.
70+
* *Madrid*: Differentiate between `"Madrid, Spain"` (city) and `"Community of Madrid, Spain"` (autonomous community).
71+
* *London*: Differentiate between `"London, UK"` and `"London, Ontario, Canada"`.
72+
* *Scotland*: Differentiate between `"Scotland, UK"` and `"Scotland County, USA"`.
73+
* **Extracting names from other tools**: If you get place info from another tool, extract and use *only* the readable name, but always qualify it with geographic context.
74+
* **Child Place Indicator Discovery Rule**: When searching for indicators related to child places within a parent (e.g., states within a country), you MUST use `search_child_indicators`, passing the parent place name in `parent_place` and a diverse sample of 5-6 of its child places in the `sample_child_places` list.
75+
76+
### B. Vague & Unqualified Query Fallbacks
77+
78+
* If a user asks a general question about available data without specifying a place (e.g., *"What data do you have?"*), proactively run a global topic lookup:
79+
* Call: `search_indicators(query="", places=["World"], include_topics=true)`.
80+
* Present the high-level World topics, then ask the user which specific place or territory they are interested in.
81+
* *Example response pattern*: "Here is a general overview of the data topics available for the World. You can also ask for this information for a specific place, like 'Africa', 'India', 'California, USA', or 'Paris, France'."
82+
83+
### C. Geographic Resolution Recovery (Troubleshooting)
84+
85+
* If the search tool resolves the wrong place (e.g., the user asked about Scotland but the results attach to "Scotland County, NC"):
86+
* Re-run `search_indicators` with explicit parent parameters (e.g., set `places=["Scotland, UK"]`).
87+
88+
---
89+
90+
## 4. Playbook Recipes & Call Examples
91+
92+
### Recipe 1: Data for a Specific Place
93+
* **Goal**: Find and retrieve an indicator *about* a single place (e.g., "population of France").
94+
* **Step 1 (Discovery)**: `search_indicators(query="population", places=["France"])`
95+
* **Step 2 (Assessment)**: `get_variable_metadata(variable_dcids=["Count_Person"], entity_dcids=["country/FRA"])`
96+
* **Step 3 (Retrieval)**: `get_observations(variable_dcid="Count_Person", place_dcid="country/FRA")`
97+
98+
### Recipe 2: Sampling Child Places & Containment Data
99+
* **Goal**: Check and retrieve data across child places of a parent (e.g., "unemployment rate in Indian states" or "GDP of all countries in the World").
100+
* **Step 1 (Discovery & Sampling)**: Call `search_child_indicators` using a diverse sample of child places to verify variable availability:
101+
* *Example (States in India)*: `search_child_indicators(query="unemployment", parent_place="India", sample_child_places=["Uttar Pradesh, India", "Maharashtra, India", "Tripura, India", "Bihar, India", "Kerala, India"])`
102+
* *Example (Countries in the World)*: `search_child_indicators(query="GDP", parent_place="World", sample_child_places=["USA", "China", "Germany", "Nigeria", "Brazil"])`
103+
* **Proxy Logic rules**:
104+
1. If a sampled child place shows data in `placesWithData` for a variable, assume that variable is available across all child places of that type.
105+
2. If no sampled child place shows data, assume the variable is not available at the child level.
106+
3. **Definitiveness of Child Search**: The results of `search_child_indicators` are absolute and definitive for the targeted child places. If a variable or concept does not appear in the child search results, it is guaranteed not to exist for those child places. Do NOT run follow-up global searches (`search_indicators`) to double-check or verify if the variable exists elsewhere.
107+
4. **No Redundant Single-Place Pings**: If a variable is confirmed via `search_child_indicators` or has child place coverage, proceed directly to `get_child_observations` (using `latest` or a narrow range). Do NOT run redundant single-place `get_observations` calls to verify the variable's active status.
108+
5. Determine the common child place type (e.g. `"State"` or `"Country"`) from the returned `dcidPlaceTypeMappings`.
109+
* **Step 2 (Assessment)**: Verify facets and date ranges for the variable across the child level by passing the resolved DCIDs of the sampled child places:
110+
* `get_variable_metadata(variable_dcids=["unemployment_rate_dcid"], entity_dcids=["resolved_child_dcid_1", "resolved_child_dcid_2", "..."])`
111+
* **Step 3 (Retrieval)**: Query observations for **ALL** child places of the determined type:
112+
* `get_child_observations(variable_dcid="unemployment_rate_dcid", parent_place_dcid="country/IND", child_place_type="State")`
113+
114+
---
115+
116+
## 5. Child Place Type Determination Heuristics
117+
118+
Before calling `get_child_observations`, inspect the `dcidPlaceTypeMappings` returned by `search_child_indicators` to determine the value for the `child_place_type` parameter:
119+
1. **Common Type**: Find the place type common to ALL sampled child places.
120+
2. **Specific Type Priority**: If multiple types are common to all child places, choose the most specific type (e.g., prefer `"County"` over `"AdministrativeArea2"`).
121+
3. **Majority Fallback**: If no single type is common to all, use the type that maps to a clear majority (50%+ threshold) of the sample.
122+
4. **Resolution Failure**: If there is no common type and no majority type, child-place mode is not supported. Fall back to making individual `get_observations` calls for each child place.
123+
124+
---
125+
126+
## 6. Bounded Date Query & Date Filtering Rules
127+
128+
To prevent payload saturation and context window exhaustion when fetching time-series observations:
129+
130+
### A. Child Places Mode Constraint
131+
* When calling `get_child_observations`, **never** set `date="all"`.
132+
* **Safe Date Strategies**:
133+
* Set `date="latest"` to retrieve only the most recent data point for each child place.
134+
* Explicitly define a narrow window using `date_range_start` and `date_range_end` (e.g., `2020` to `2023`).
135+
136+
### B. Date Range Boundary Interpretations
137+
When `date="range"` is used, the date ranges are evaluated as follows:
138+
* **Start Date Only**: If only `date_range_start` is specified, the response will contain all observations starting at and after that date (inclusive).
139+
* **End Date Only**: If only `date_range_end` is specified, the response will contain all observations before and up to that date (inclusive).
140+
* **Both Boundaries**: If both are specified, the response contains observations within the provided range (inclusive).
141+
* **Default Fallback**: If you do not provide any date parameters, the tool will automatically fetch only the `'latest'` observation.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Retrieve time-series numerical observations for a statistical variable across all child places of a specific type within a parent geographic entity. Returns an array of dated observation values for each child place and their source metadata. Requires specifying the child place type (e.g., 'County' or 'State') and a bounded date range or 'latest' filter to prevent payload saturation.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Retrieve time-series numerical observations for a specific statistical variable at a target place. Returns an array of dated observation values and their source metadata. Operates in single-place mode; for child-level containment data, use get_child_observations.

0 commit comments

Comments
 (0)