fix(search): add database_name to search results and metadata to prevent LLM hallucination - #65
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to reduce LLM hallucination by surfacing authoritative, human-readable database labels (database_name) alongside database_id in tool responses, and centralizing the database registry so the mapping is consistent across the API and MCP server.
Changes:
- Added a shared
DATABASESregistry +DB_NAME_LOOKUP, and used it to populatedatabase_namein search results and metadata responses. - Extended the API/models to support richer search workflows (multi-query / query groups) and added a new
data360_analyze_development_topicMCP tool. - Added/updated tests to cover database name injection and the new behaviors.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/data360/constants.py |
New centralized database registry and lookup map. |
src/data360/models.py |
Adds database_name, multi-query models, and typing updates. |
src/data360/api.py |
Populates database_name; adds multi-query search flow; adds topic analysis tool; adjusts get_data behavior/logging. |
src/data360/mcp_server/resources.py |
Imports centralized DATABASES instead of duplicating. |
src/data360/mcp_server/tools.py |
Exposes data360_analyze_development_topic as an MCP tool. |
src/data360/mcp_server/_server_definition.py |
Wires optional OpenAI sampling handler for server-side sampling fallback. |
tests/test_api.py |
Adds tests for database_name in search/metadata plus resilience behaviors. |
tests/test_multi_query_search.py |
New comprehensive tests for multi-query search behavior. |
tests/test_analyze_topic.py |
New tests for analyze_development_topic. |
docs/overview.md |
Documents the new analyze tool. |
README.md |
Documents optional OpenAI API key for sampling fallback. |
Comments suppressed due to low confidence (1)
src/data360/api.py:1267
- The smart time default logic still sets
start_year = current_year - 19(last 20 years) even though the docstring and surrounding comments now state the default is the last 5 years. Update the calculation to match the documented behavior (ideally reuse a single constant, e.g._DEFAULT_SUMMARY_YEARS).
# Smart time defaults: if no time range specified, default to last 5 years
if start_year is None and end_year is None:
from datetime import datetime # noqa: PLC0415
current_year = datetime.now().year
end_year = current_year
start_year = current_year - 19 # Last 20 years
_logger.info(f"Smart default: Applied time range {start_year}-{end_year}")
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ent LLM hallucination The database_id field (e.g. WB_GS) is an opaque code that LLMs routinely expand incorrectly. For example, WB_GS was described as 'World Bank - Global Statistics' when the correct name is 'Gender Statistics'. Changes: - src/data360/constants.py [NEW]: dependency-free DATABASES registry and DB_NAME_LOOKUP dict. Single source of truth. - EnrichedIndicator (models.py): add database_name field with a docstring that instructs LLMs to use this value and never guess from database_id. - _enrich_search_results (api.py): populate database_name via DB_NAME_LOOKUP for every indicator returned by data360_search_indicators. - get_metadata (api.py): inject database_name into indicator_metadata so get_data callers also receive the correct label. Survives select_fields filtering (always retained alongside requested fields). - mcp_server/resources.py: import DATABASES from constants instead of redefining it, eliminating the duplicate definition. - tests/test_api.py: add TestDatabaseNameInSearch and TestDatabaseNameInMetadata covering known id resolution, unknown id to None, registry sweep, and select_fields passthrough.
de7df7a to
38179c1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@avsolatorio add patch fix for dataset/database name |
…taset JSON file fallback
|
Thanks @avsolatorio! I have replaced the hardcoded partial list with a hybrid |
403e160 to
e56317e
Compare
Summary
LLMs were hallucinating human-readable names for opaque
database_idcodesreturned by search and data tools. For example,
WB_GSwas being presentedto users as "World Bank – Global Statistics" when the correct name is
"Gender Statistics". This happened because no grounded label was ever
surfaced in tool responses — the LLM was expanding the abbreviation by guessing.
This patch makes the correct database name available in every tool response
that returns a
database_id, so the LLM always has the authoritative labeland never needs to infer it.
Root Cause
The
DATABASESregistry (id → name mapping) already existed inmcp_server/resources.pyas a static MCP resource (data360://databases),but it was only readable if the client explicitly fetched that resource.
Tool responses (
search_indicators,get_metadata,get_data) neverincluded the corresponding label, leaving the LLM with no grounded value
to cite.
Changes
src/data360/constants.py[NEW]DATABASESregistry and a pre-builtDB_NAME_LOOKUPdict (database_id → name).api.pyandmcp_server/resources.pynowimport from here instead of duplicating the list.
src/data360/models.pyEnrichedIndicator: addeddatabase_name: str | Nonefield with a docstringthat explicitly instructs the LLM to use this value when presenting data and
never to guess from
database_id.src/data360/api.py_enrich_search_results(): populatesdatabase_namevia_DB_NAME_LOOKUPfor every indicator returned by
data360_search_indicators.get_metadata(): injectsdatabase_nameintoindicator_metadata(whichis also surfaced through
get_data's metadata field). The key is alwaysretained even when
select_fieldsrestricts other fields.src/data360/mcp_server/resources.pyDATABASESdefinition; now imports fromconstants.py.tests/test_api.pyTestDatabaseNameInSearch: three tests covering known id resolution(
WB_GS→"Gender Statistics"), unknown id →None(not a guessedstring), and a coverage sweep across all registered databases.
TestDatabaseNameInMetadata: three tests coveringget_metadatainjection,survival through
select_fieldsfiltering, and unknown id →None.Testing
Checklist
constants.py)database_nameisNonefor unknown ids — no invented fallbackdatabase_namesurvivesselect_fieldsfiltering inget_metadata