mcp server
anchor-journalism
World Bank data assistant for investigative journalists — ask questions, get cited tables and methodological notes from real World Bank APIs, with built-in anti-hallucination guardrails
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 22, 2026⚓ Anchor Journalism
World Bank data assistant for investigative journalists. Ask questions in Portuguese, English, or Spanish — the agent queries World Bank APIs and returns formatted tables with cited sources and methodological notes. Anti-hallucination is the core design constraint.
Versão em Português abaixo / Versión en Español → README.es.md
Table of Contents
- About
- Features
- Tech Stack
- File Structure
- Prerequisites
- Installation
- Configuration
- Running the Server
- Usage
- HTTP API
- LLM Providers
- Supported Indicators
- Anti-Hallucination by Design
- Conversation History
- Data360 API Integration Methodology
- Security & Data Handling Protocols
- Deployment (Docker)
- Deployment (Render)
- MCP Server
- Governance & Stability
- Data Guide
- Troubleshooting
1. About
Anchor Journalism is a conversational agent that connects investigative journalists directly to World Bank APIs. Instead of navigating complex data portals, you ask a question in natural language — the system fetches real data, formats it into comparative tables, and cites the source precisely.
Example query:
"How has female labor force participation evolved in Brazil vs. Colombia between 2014 and 2024?"
Example response:
| Country | 2014 | 2019 | 2024 |
|---|---|---|---|
| Brazil | 54.2% | 53.8% | 54.1% |
| Colombia | 52.1% | 52.7% | 53.4% |
[WDI: SL.TLF.TOTL.FE.ZS | BRA;COL | 2014:2024 | Retrieved: 2026-05-05 | URL: ...]
Architecture Overview
A user query passes through five layers before returning a cited, verified answer:
flowchart TD
User["👤 User"] --> FE["Frontend\nHTML · JS · Chart.js"]
FE -->|"POST /api/chat\nor /api/chat/stream"| Flask["Flask Server\napp.py\n(rate limit · validation · CORS)"]
Flask --> AL["Agent Loop\nlib/agent/agent_loop.py\nmax 5 iterations · temperature=0"]
AL <-->|"chat completions\n(tool calling)"| LLM["LLM Providers\nOllama · Gemini\nOpenAI · Anthropic"]
AL <-->|"indicator / document tools"| WB["World Bank APIs\nData360 · WDS · WDI Classic"]
AL -->|"response + source seal"| Flask
Flask -->|"JSON or SSE tokens"| FE
Anti-hallucination by design: if the World Bank API returns no data, the agent declares the absence explicitly — never estimates.
2. Features
- Verified Multi-Language Heuristics — robustly detects Portuguese, Spanish, and English using advanced word-boundary markers, preventing language drift in investigative reports.
- Data360 Metadata Integration — uses official World Bank metadata (e.g., the latest $4.20/day poverty line for LMICs) directly from the Data360 API for high-fidelity technical documentation.
- Proactive Fact-Checking & Ranking Engine — automatically verifies ranking and position claims (e.g., "is country X the 9th most...") by querying global datasets (
countries=['all']) and calculating the true rank on the backend (compute_ranking). Features Intelligent Slice Thinning to automatically keep only the Top 10 + target country + neighboring ranks to prevent LLM context overflows. - Transparent Search Translation — automatically translates search terms from Portuguese or Spanish to English before querying both World Bank indicator APIs and the document library (WDS), significantly increasing hit rates without user effort.
- Dynamic Historical Data Fallback — when the requested year has no data for a country, the system automatically fetches the most recent available historical years (up to 3) and displays them with a ⚠️ warning, keeping this fallback data isolated in
historical_fallback_datato prevent empty-table responses and arithmetic calculations pollution. - Currency Value Formatting — raw decimal currency figures in text (e.g.,
2347.44 dólares americanos) are automatically formatted with reader-friendly localized notation:US$ 2.347for Portuguese,US$ 2,347for English/Spanish. - Multilingual chat — accepts queries in Portuguese, English, or Spanish; responds in the same language
- Methodological Interpretability — automatic retrieval of official definitions, methodology, limitations, geographic coverage (
ref_area), time range (time_period), measurement type (aggregation_measure), and SDMX codelist reference (code_list) via the Data360 API (get_metadatatool) - Document Intelligence — search official World Bank reports (WDS API) and extract text from PDFs (
pdfplumber) - Interactive PCN Badges (Proof-Carrying Numbers) — SHA-256 cryptographic hashes for every data figure; clickable inline badges (v) open a modal with the raw World Bank JSON payload for granular verification
- Data Freshness Transparency — dynamic detection of the latest available data point (e.g. "Latest point is 2023") displayed in a high-visibility transparency card, with full PT/EN/ES localized notes and database-specific update cycles
- Indicator Disaggregation & Codelists — identifies available data dimensions (e.g. by Sex, Age, or Rural/Urban) using the
get_disaggregationtool and resolves readable names to standard codes viafind_codelist_value, enabling deep investigative reporting on inequalities - Gini Scale Clarification — automatic notes clarifying the 0–100 scale used by the World Bank API vs. the academic 0–1 scale.
- Chronological Integrity — strict rules prevent the agent from filling missing year rows with data from prior years, ensuring tables reflect literal API results only.
- Data Coverage Warning — automated visual warnings (⚠️) when the World Bank has a data lag or gap for the requested period.
- Investigative OSINT Guide — integrated guide with practical scenarios for fact-checking, comparative reporting, and data crossing
/country:NameCommand — instantly list all supported indicators and macro-categories for a specific country- Navigation Reset — click the anchor logo anytime to clear the chat and return to the home screen
search_indicator— finds indicators by topic via the Data360 API (vector search)get_time_series— time series with automatic pagination for multiple countrieslist_countries— real countries only (regional aggregates excluded)- Phrase-to-code mapping — resolves PT/EN/ES phrases to WB indicator codes automatically
- Strict Validation — uses Pydantic models to prevent structural hallucinations in API responses
- Methodological caveats — automatic notes for 23+ indicators with known limitations (WGI, labor, GDP, poverty, CO2, forest)
- Anti-hallucination —
data_available: falseflag propagated to the LLM when no data exists - Source citation — every figure cited with code, countries, period, date, and URL
- 5 LLM providers — built-in Ollama and Gemini (no key needed); bring your own key for OpenAI (GPT-4.1), Anthropic (claude-haiku-4-5-20251001), or Gemini 3.5 Flash (3.5 Flash)
- BYOK (Bring Your Own Key) — user-supplied keys sent via
X-User-Api-Keyheader; stored insessionStorageonly, never logged or persisted server-side - Vega-Lite dynamic charts —
generate_chart_spectool generates backend-validated Vega-Lite specs (line, bar, area, grouped_bar, scatter); rendered in the browser viavegaEmbed. LLM calls the tool only when chart type is justified (explicit user request, disaggregated data, or trend visualization). Prevents chart hallucinations by keeping all data in Python. - Streaming responses —
POST /api/chat/streamreturnstext/event-streamtokens for real-time rendering - File attachment — upload PDF (max 20 pages), CSV, or TXT to cross-reference with World Bank data
- Reduced-motion support — respects
prefers-reduced-motionmedia query - Language selector — auto-detect or manually select PT / EN / ES
- Conversation history — browser localStorage persists chat across page reloads (max 50 messages)
- Response Caching & Cache Bypass — World Bank API responses are cached in-memory (1h default, 24h for country list). Supports demand-driven cache bypass to force real-time fresh fetches via
X-Force-Refresh: trueHTTP header,?force_refresh=trueURL query parameter, orforce_refresh: trueJSON payload key. - SSRF protection — PDF URL allowlist (only World Bank domains); rejects external URLs from LLM tool calls
- XSS & BYOK Protection — DOMPurify sanitizes all LLM output before rendering, with a strict client-side
escapeHTMLfallback mechanism to prevent XSS attacks and safeguard BYOK API keys even if the global DOMPurify script fails to load. - Upload limit — 10 MB cap enforced at the HTTP layer; PDF extraction limited to 20 pages
- Security headers — CSP,
X-Frame-Options,X-Content-Type-Optionson every response - Structured logging — JSON format via
LOG_FORMAT=jsonenv var for production observability (Render, Datadog) - Docker deployment — production-ready with gunicorn (2 workers, timeout 120s)
- CI/CD — GitHub Actions with matrix Python 3.11 + 3.12, ruff + mypy (non-blocking) + pytest on push/PR
- Health endpoint —
GET /healthpings the World Bank API; returns{"status":"ok","wb_api":true/false}
3. Tech Stack
| Layer | Technology |
|---|---|
| Backend | Python 3.11+ + Flask |
| Data Validation | Pydantic (strict schemas) |
| PDF Extraction | pdfplumber (optimized text extraction) |
| MCP Server | FastMCP (Model Context Protocol) |
| LLM — Option 1 | Ollama (gpt-oss:120b-cloud) via OpenAI-compatible API |
| LLM — Option 2 | Gemini 2.5 Flash via OpenAI-compatible API |
| Agent loop | Manual implementation using openai Python SDK |
| WB Classic API | requests → api.worldbank.org/v2 |
| WB Data360 API | requests → data360api.worldbank.org |
| Frontend | HTML + CSS + Pure JavaScript (no build step) |
| MD Rendering | marked.js (local hosting, GFM tables) |
| Configuration | python-dotenv + .env |
| Rate limiting | flask-limiter — 10 req/min on /api/chat |
| Response cache | flask-caching + SimpleCache (in-memory, configurable via CACHE_TYPE) |
| Production server | gunicorn — 2 workers, timeout 120s |
| Testing | pytest + responses (HTTP mocking) — 137 tests |
| Linting | ruff (rules E, F, I) + pre-commit hook |
| Type checking | mypy (non-blocking CI step) |
| CI/CD | GitHub Actions — matrix Python 3.11 + 3.12 |
| Deployment | Docker (python:3.11-slim) + .dockerignore |
| Persistence | Browser localStorage for history (no backend DB) |
4. File Structure
anchor_journalism/
├── app.py # Flask entry point — POST /api/chat, GET /health
│
├── lib/
│ ├── __init__.py
│ ├── cache.py # Flask-Caching singleton + helpers cache_get/set/clear
│ ├── models.py # Pydantic models for strict data validation
│ │
│ ├── wb_client/
│ │ ├── __init__.py # Re-exports get_time_series, list_countries, search_indicator
│ │ ├── base.py # BaseClient with retry logic and Source Seals (SHA-256)
│ │ ├── data360_api.py # get_time_series(), search_indicator(), list_countries()
│ │ ├── docs_client.py # search_documents() — World Bank Documents API
│ │ └── wdi_client.py # WDIClient: fetch_time_series (Primary Data360 → Fallback Classic),
│ │ # _fetch_time_series_classic, fetch_indicator_metadata, search_indicator
│ │
│ ├── indicator_map/
│ │ ├── __init__.py # find_indicators() (best match, accent normalization) + get_caveat()
│ │ ├── phrases.py # PHRASE_TO_INDICATORS — PT/EN/ES phrases → WB codes
│ │ └── caveats.py # INDICATOR_CAVEATS — 12+ methodological notes (WGI, labor, energy,
│ │ # health, poverty, CO2, forest) — always in English (avoids drift)
│ │
│ └── agent/
│ ├── __init__.py
│ ├── system_prompt.py # get_system_prompt(language) — anti-hallucination rules
│ ├── tools.py # TOOL_DEFINITIONS — function calling schemas
│ └── agent_loop.py # run_agent() — agent tool-use loop (max 5 iterations)
│
├── mcp_src/ # Model Context Protocol (FastMCP) implementation
│ └── anchor_mcp/
│ ├── main.py # MCP Server entry point
│ └── tools/ # Tools exposed via MCP (indicators, documents)
│
├── static/
│ ├── index.html # Main chat UI
│ ├── about.html / faq.html / policy.html / data_guide.html # Institutional pages
│ ├── js/
│ │ ├── app.js # Chat logic, provider/lang state, local history
│ │ ├── shared.js # Theme, i18n, lang selector, status badge
│ │ ├── i18n.js # Translation strings (PT/EN/ES)
│ │ └── glossary.js # Glossary data by language
│ └── css/
│ └── styles.css # Global styles (no build step)
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixtures (Data360 + Classic API mocks) + auto cache_clear
│ ├── test_app.py # Flask endpoints + BYOK integration
│ ├── test_agent_loop.py # BYOK validation, UserKeyError, _ScrubKeyFilter
│ ├── test_data_api.py # Data360 time series, Classic API fallback, multi-country
│ ├── test_data360_api.py # search_indicator, empty results
│ ├── test_docs_client.py # search_documents, PDF extraction, SSRF blocking
│ ├── test_indicator_map.py # phrase matching (accents, best match), caveats
│ ├── test_milestone3.py # get_indicator_metadata integration
│ ├── test_models.py # Pydantic schema validation
│ ├── test_provenance_ui.py # provenance page /verify/<id>
│ └── test_security.py # Source Seal (SHA-256) integrity tests
│
├── .github/workflows/ci.yml # GitHub Actions — ruff + mypy + pytest on push/PR
│
├── Dockerfile # Production: python:3.11-slim + gunicorn
├── .dockerignore # Excludes .venv, .env, __pycache__, tests, .git
├── .pre-commit-config.yaml # ruff + ruff-format hooks
│
├── requirements.txt # Python dependencies (flask, pydantic, pdfplumber, ...)
├── requirements-dev.txt # Dev: pytest, pytest-cov, responses, ruff, mypy
├── .env.example # Environment variables template (+ CACHE_TYPE)
├── .env # Your actual keys (gitignored)
├── .gitignore
├── ruff.toml # Linting: select E,F,I; ignore E501
├── TESTS.md # Detailed documentation of the test suite, Ruff, and CI
└── README.md
5. Prerequisites
- Python 3.11 or higher — verify with
python --version - pip (included with Python)
- API Key for at least one LLM provider:
- Ollama: run locally or use the server-side key
- Gemini: get a key at aistudio.google.com (free tier: 250 req/day)
- Internet connection — the server calls World Bank and LLM APIs in real-time
6. Installation
6.1 Clone the repository
git clone https://github.com/reichaves/anchor_journalism.git
cd anchor_journalism
6.2 Create and activate a virtual environment (recommended)
# Create
python -m venv .venv
# Activate — macOS/Linux
source .venv/bin/activate
# Activate — Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Activate — Windows (CMD)
.venv\Scripts\activate.bat
6.3 Install dependencies
pip install -r requirements.txt
| Package | Version | Purpose |
|---|---|---|
flask | ≥ 3.0 | Web server |
flask-cors | ≥ 4.0 | Cross-origin requests |
flask-limiter | ≥ 3.5 | Rate limiting |
openai | ≥ 1.60 | OpenAI-compatible client (Ollama + Gemini) |
python-dotenv | ≥ 1.0 | Load variables from .env |
requests | ≥ 2.31 | World Bank API calls |
7. Configuration
7.1 Create .env file
cp .env.example .env
7.2 Edit .env with your keys
# Ollama (gpt-oss:120b-cloud)
OLLAMA_API_KEY=ollama_YOUR_KEY_HERE
# Gemini 2.5 Flash — get at aistudio.google.com
GOOGLE_GENERATIVE_AI_API_KEY=AIzaYOUR_KEY_HERE
# Optional: Server port (default: 5000)
PORT=5000
# Optional: Enable Flask debug mode (default: false)
FLASK_DEBUG=false
# Optional: Allowed CORS origins (default: http://localhost:5000)
ALLOWED_ORIGINS=http://localhost:5000
# Optional: Response cache backend (default: SimpleCache = in-memory)
CACHE_TYPE=SimpleCache
# Optional: Cache timeout in seconds for WB responses (default: 3600 = 1h)
CACHE_DEFAULT_TIMEOUT=3600
# Optional: Restrict access with a password (unset = open access)
# Note: The default testing access code is WbMp182026 (used on https://anchor-journalism.com/)
ACCESS_CODE=WbMp182026
# Optional: JSON logs for production observability (Render, Datadog)
LOG_FORMAT=json
You only need one of the two LLM keys. Leave the other blank. The selector in the UI allows switching providers.
8. Running the Server
python app.py
Expected output:
* Running on http://127.0.0.1:5000
Open in your browser: http://localhost:5000
Smoke test (before declaring ready):
python -m py_compile app.py lib/agent/agent_loop.py lib/wb_client/wdi_client.py lib/wb_client/data360_api.py lib/agent/system_prompt.py
9. Usage
9.1 Web Interface
- Open
http://localhost:5000 - Click the ⚙️ Settings icon in the top right
- Select a provider: Ollama or Gemini (built-in, no key) — or unlock OpenAI, Claude, or Gemini 3.5 Flash by entering your key via 🔑 API Keys
- Select response language (Auto, PT, EN, or ES)
- Type your question and press Enter (or click Send)
- The assistant will query World Bank APIs and return a formatted answer
9.2 Example Queries
Transparency and Scope:
/country:Mexico
(Shows all indicators the assistant can fetch for Mexico)
Economy:
How has GDP per capita evolved for Brazil, Argentina, and Chile in the last 10 years?
Gender and Labor:
What is the difference in female labor force participation between Brazil and Colombia from 2010 to 2023?
Governance:
Compare corruption control indices for Brazil, Mexico, and Colombia in the last 5 years.
Environment:
How did CO2 emissions per capita evolve in Brazil between 2000 and 2020?
Health:
What was the life expectancy in Brazil, Argentina, and Chile in 2022?
Poverty:
Which Latin American countries have the highest extreme poverty rate?
9.3 Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Enter | Send message |
Shift + Enter | New line in input field |
Cmd/Ctrl + K | Clear conversation |
10. HTTP API
POST /api/chat
Main endpoint. Receives user query and returns agent response.
Rate limit: 10 requests/minute per IP.
Request:
{
"query": "How has Brazil's GDP evolved?",
"history": [
{ "role": "user", "content": "Previous user message" },
{ "role": "assistant", "content": "Previous assistant response" }
],
"provider": "ollama",
"language": "en"
}
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | User question |
history | array | No | Previous messages — items must have role "user" or "assistant" (max 100) |
provider | string | No | "ollama" (default), "gemini", "openai", "anthropic", or "gemini-user" |
language | string | No | "auto" (default), "pt", "en", or "es" |
force_refresh | boolean | No | If true, bypasses cache layers to force a fresh real-time fetch from World Bank APIs (default: false) |
For BYOK providers (openai, anthropic, gemini-user), include the user key header:
X-User-Api-Key: sk-...
Cache Bypass Options
You can bypass response caching on demand to ensure real-time fresh data retrieval by using any of the following:
- HTTP Header: Pass
X-Force-Refresh: true. - Query Parameter: Append
?force_refresh=trueto the URL. - Request Body: Send
"force_refresh": truein the JSON request payload.
POST /api/chat/stream
Streaming variant. Same request body as /api/chat. Returns text/event-stream with tokens for real-time rendering.
data: {"token": "In "}
data: {"token": "2023"}
...
data: [DONE]
Response (200):
{
"answer": "In 2023, Brazil's GDP was US$ 2.17 trillion...\n\n[WDI: NY.GDP.MKTP.CD | BRA | 2023 | Retrieved: 2026-05-05 | URL: ...]"
}
Response (400):
{ "error": "query is required" }
Response (429):
{ "error": "10 per 1 minute" }
Response (500):
{ "error": "Internal error — please try again." }
curl example:
curl -X POST http://localhost:5000/api/chat \
-H "Content-Type: application/json" \
-d '{"query": "Brazil GDP in 2022", "history": [], "provider": "ollama", "language": "en"}'
11. LLM Providers
All providers are accessed via the OpenAI-compatible chat completions API using the openai Python SDK.
Built-in providers (no key needed in the UI)
| Provider | Model | Env var |
|---|---|---|
| Ollama | gpt-oss:120b-cloud | OLLAMA_API_KEY |
| Gemini | gemini-2.5-flash | GOOGLE_GENERATIVE_AI_API_KEY |
BYOK providers (bring your own key)
Keys are entered in the 🔑 API Keys modal in the UI and stored in sessionStorage only — never logged or sent to the server outside the immediate request.
| Provider | Model | Key Prefix | Notes |
|---|---|---|---|
| OpenAI | gpt-4.1 | sk- | temperature=0 |
| Anthropic (Claude) | claude-haiku-4-5-20251001 | sk-ant- | temperature=1 (Anthropic API restriction with thinking enabled) |
| Gemini 3.5 Flash | gemini-3.5-flash | AIza | temperature=0 |
Adding a new provider
Add an entry to PROVIDER_CONFIGS in lib/agent/agent_loop.py. The provider must expose an OpenAI-compatible chat completions endpoint with tool-calling support.
12. Supported Indicators
The system automatically recognizes the following phrases (PT/EN/ES) and maps them to WB codes:
| Topic | Phrases (examples) | WB Codes |
|---|---|---|
| Female Participation | participação feminina, female labor, participación femenina | SL.TLF.TOTL.FE.ZS, SL.TLF.CACT.FM.ZS |
| GDP | pib, gdp | NY.GDP.MKTP.CD, NY.GDP.PCAP.CD |
| GDP per capita | pib per cápita, gdp per capita | NY.GDP.PCAP.CD |
| Unemployment | desempleo, unemployment, desemprego | SL.UEM.TOTL.ZS, SL.UEM.TOTL.FE.ZS, SL.UEM.TOTL.MA.ZS |
| Corruption | corrupción, corruption, corrupção | CC.EST |
| Governance | gobernanza, governance, governança | CC.EST, GE.EST, RL.EST, VA.EST, RQ.EST |
| Poverty | pobreza, poverty | SI.POV.NAHC, SI.POV.DDAY |
| Inequality | desigualdad, inequality, desigualdade | SI.POV.GINI |
| GHG / CO2 Emissions | co2 emissions, ghg emissions, emissões co2 | EN.GHG.ALL.PC.CE.AR5, EN.GHG.ALL.MT.CE.AR5 |
| Child Mortality | mortalidad infantil, child mortality, mortalidade infantil | SH.DYN.MORT |
| Life Expectancy | esperanza de vida, life expectancy, expectativa de vida | SP.DYN.LE00.IN |
| Education | educación, education, educação | SE.ADT.LITR.ZS, SE.PRM.NENR |
For indicators not in this list, the agent uses the search_indicator tool via the Data360 API.
Adding indicators: edit
lib/indicator_map/phrases.py(PT/EN/ES phrases → codes) and optionallylib/indicator_map/caveats.py(methodological notes). All perception-based WGI indices (e.g.PV.EST,RL.EST,VA.EST,RQ.EST) must have methodological caveats. Do NOT prefix caveat text with "Methodological note:" — RULE 13 adds that prefix automatically during rendering.
13. Anti-Hallucination by Design
The system is built to never fabricate data:
wdi_client.py:fetch_time_series()— Data360 API is always queried first (data360api.worldbank.org). If it returns no records, the Classic API (api.worldbank.org/v2) is tried transparently. Only when both return empty isdata_available: falsereturned — never an estimate. Do not skip the Data360 call; response formats differ and call order matters.agent_loop.py— passes the literal tool result to the LLM without modificationsystem_prompt.py— rule #2: "If the tool returnsdata_available: false, state explicitly that the World Bank has no data for this request"temperature=0— deterministic responses, no speculation
Why the two-API fallback exists: The WB's Data360 API does not carry all WDI series in the
WB_WDIdatabase. WGI governance indicators use a separate Data360 database (WB_WGI) with different indicator IDs (GOV_WGI_CC,GOV_WGI_GE, etc.) and require filteringCOMP_BREAKDOWN_1=WGI_ESTto get the -2.5/+2.5 estimate rather than percentile ranks — this mapping is in_WGI_MAPinwdi_client.py. IEA-sourced CO2 indicators (EN.ATM.CO2E.PC,EN.ATM.CO2E.KT) were deleted from the World Bank in 2023. The phrase map now resolves CO2/GHG queries to the current Climate CAIT equivalents:EN.GHG.ALL.PC.CE.AR5(per capita) andEN.GHG.ALL.MT.CE.AR5(total). Both APIs ultimately source from the same World Bank data warehouse.
This chain must not be broken. Never add interpolation, estimation, or fallback values between the API response and the LLM.
14. Conversation History
The web interface automatically saves your conversation to the browser's localStorage:
- Persistence — messages survive page reloads and browser restarts
- Automatic save — every successful response is saved immediately
- Auto-trim — if history exceeds 50 messages, the 10 oldest are removed
- Clear button — click Clear in the header to delete all history (also removes from localStorage)
- Corruption handling — if
JSON.parsefails, data is discarded and a fresh session starts - Zero backend impact — all storage is client-side; no database or server changes needed
15. Data360 API Integration Methodology
Anchor Journalism prioritizes the World Bank's OData-compliant Data360 API over classic interfaces to deliver structured, high-fidelity data queries:
- Dynamic Endpoint Resolution: The database client (WDIClient) translates classical WDI codes into corresponding Data360 indicator schemas (e.g., mapping
SL.TLF.TOTL.FE.ZStoWB_WDI_SL_TLF_TOTL_FE_ZS). It routes queries tohttps://data360api.worldbank.org/data360/datausing OData filters. - Classic Fallback Logic: If the Data360 query fails or returns an empty payload, WDIClient performs a transparent fallback call to the Classic World Bank API v2 (
https://api.worldbank.org/v2). - WGI Estimates & Percentiles: Mappings for Worldwide Governance Indicators (e.g.,
CC.ESTin_WGI_MAP) append theCOMP_BREAKDOWN_1=WGI_ESTquery parameter to extract raw governance estimates (-2.5 to 2.5) rather than percentile ranks. - Disaggregation and Codelist Mapping: The engine calls
get_disaggregationandfind_codelist_valueto fetch specific dimensions (e.g. Sex, Age) and map numeric keys to SDMX standard codes dynamically. - Chronological Integrity & historical_fallback_data: If the target period returns empty for a country, up to 3 years of fallback data is collected and isolated under the
historical_fallback_datafield ofCountryData. This isolates historical metrics from the primary request payload, protecting downstream math calculations. - Data360 Verification URLs: Non-WDI database indicator codes (e.g.,
WB_ESG_*,OWID_CB_*) generate browser-friendly verification URLs pointed directly atdata360api.worldbank.orgrather than the classic API, preventing "indicator not found" errors when verifying discontinued or archived series (such as CO₂ per capitaWB_ESG_EN_ATM_CO2E_PC). - Vega-Lite Automation: Clean series are handled in Python and sent to
generate_chart_specto construct backend-validated Vega-Lite JSON charts, bypassing frontend generation and preventing LLM visualization hallucinations.
16. Security & Data Handling Protocols
To safeguard information integrity and user credentials, the project enforces multi-layer security measures:
- Volatile BYOK (Bring Your Own Key) Isolation: User-supplied API keys (OpenAI, Anthropic, Gemini 3.5 Flash) are stored exclusively in Javascript memory closures (
apiKeysMemoryin app.js). They are never written to cookies, local storage, session storage, or logs. - XSS & Key Exposure Shielding: Markdown output is sanitized using
DOMPurifybefore injecting into the SSE stream. A redundant nativeescapeHTMLfunction is deployed as a fail-safe against remote script delivery issues. Additionally, the SSE stream uses a backend thread-based keep-alive mechanism to send blank comments every 5 seconds, preventing cloud load balancers (such as Render's) from terminating the connection due to a 30-second idle timeout during slow API requests. - SSRF Protection & Constrained Scrapes: Agent-led document scraping only accepts verified URLs matching the
worldbank.orgdomain. External resource requests are rejected. - Log Scrubbing & PII Redaction: Terminal and file logging employ a custom
_ScrubKeyFilterto scrub API keys, credentials, and PII (such as 11-digit CPFs or 14-digit CNPJs) to comply with ethical OSINT and data guidelines. - IP-Based Rate Limiting: Utilizes
flask-limiterbacked by a SQLite database (rate_limits.db) operating in WAL mode to throttle client requests dynamically (defaulting to 10 req/min on/api/chat). - Source Seals (Auditable Proveniência): Each successful API fetch generates a cryptographic SHA-256 signature (computed over
response.content + URL + timestamp). These seals are saved in the SQLite databaseanchor.db(db.py) and render as clickable badges[v](Proof-Carrying Numbers) in frontend tables, linking to the public validation page/verify/<query_id>.
17. Deployment (Docker)
17.1 Build
docker build -t anchor .
17.2 Run
docker run --rm -p 5000:5000 --env-file .env anchor
17.3 Verify
curl http://localhost:5000/health
# → {"status":"ok","wb_api":true}
17.4 Image details
| Base | python:3.11-slim |
| Server | gunicorn — 2 workers, --timeout 120 |
| Port | 5000 |
| Excluded | .venv, .env, __pycache__, tests/, docs/, .git/ (via .dockerignore) |
18. Deploy on Render (free cloud)
In addition to Docker, you can deploy Anchor Journalism on Render for web access.
Requirements
- Render account (create at dashboard.render.com)
- API keys:
OLLAMA_API_KEYand/orGOOGLE_GENERATIVE_AI_API_KEY
Steps
- Go to New > Web Service in the Render dashboard
- Connect the GitHub repository
reichaves/anchor_journalism - Render automatically detects the
render.yamlfile (Infrastructure as Code) - Fill in the secret environment variables:
OLLAMA_API_KEY— your Ollama keyGOOGLE_GENERATIVE_AI_API_KEY— your Gemini key
- Click Create Web Service
Free Tier Limitations
| Limitation | Impact |
|---|---|
| Sleep after 15 min inactivity | Cold start of ~30-50s on first request after inactivity |
| 512 MB RAM | Sufficient for Flask + 2 gunicorn workers |
| Custom domain configured | URL is https://anchor-journalism.com |
Verify Deployment
curl https://anchor-journalism.com/health
# → {"status":"ok"}
19. MCP Server
Anchor Journalism exposes its core tools via the Model Context Protocol (MCP). This allows other AI agents (such as Claude Desktop or Cursor) to natively query World Bank indicators and documents using the logic implemented in this repository.
19.1 Tools (6 total)
| Tool | Description |
|---|---|
get_indicator_data | Fetch WDI time series for one or more countries |
get_metadata | Retrieve detailed metadata (definition, methodology, limitations, time coverage) for an indicator |
get_disaggregation | Fetch available dimensions (Sex, Age, Urban/Rural, etc.) for an indicator — dual strategy: dedicated endpoint + column inference fallback |
find_codelist_value | Resolve readable names to standard Data360 codes (e.g. "Female" → F); 6 structural dimensions (SEX, RESIDENCE, URBANISATION, FREQ, AGE, UNIT_MULT) resolved instantly from built-in table — no API call required |
search_indicator | Semantic search for indicators via Data360 searchv2 |
search_documents | Search World Bank Reports (WDS API) |
A prompt investigate_country_economy is also included — a full country economic analysis template.
19.2 Installation
The MCP server is a separate installable package inside mcp_src/. Install it in editable mode:
# From the project root, with the venv active
# Use python -m pip if the pip.exe launcher is broken (e.g. after moving the venv)
python -m pip install "fastmcp>=0.4.1"
python -m pip install -e mcp_src/
Note:
fastmcp>=0.4.1is also listed inrequirements.txtsopip install -r requirements.txtcovers it for new installs.
19.3 Running the server
# Correct command — the package registers as anchor_mcp
python -m anchor_mcp.main
# Do NOT use: python -m mcp_src.anchor_mcp.main (wrong module path)
19.4 Smoke testing tools directly
# test_mcp_tools.py — run with: python test_mcp_tools.py
from anchor_mcp.clients.wdi_client import WDIClient
import json
client = WDIClient()
# get_metadata
result = client.fetch_indicator_metadata("NY.GDP.MKTP.CD")
print(result.metadata.name, result.metadata.time_coverage)
# get_disaggregation
result = client.fetch_disaggregation("SH.STA.MMRT")
print(result.dimensions_available, [d.id for d in result.dimensions])
# find_codelist_value — SEX, FREQ, AGE, UNIT_MULT, RESIDENCE, URBANISATION
# resolve instantly from built-in table (no API call, no 404 risk)
result = client.fetch_codelist("SEX", "Female")
print(result.items) # → [CodelistItem(id='F', name='Female')]
result = client.fetch_codelist("FREQ")
print([i.id for i in result.items]) # → ['A', 'Q', 'M', 'W', 'D']
19.5 Claude Desktop configuration
Add to your claude_desktop_config.json:
{
"mcpServers": {
"anchor": {
"command": "python",
"args": ["-m", "anchor_mcp.main"],
"cwd": "E:/Code/anchor_journalism",
"env": {
"OLLAMA_API_KEY": "your-key",
"GOOGLE_GENERATIVE_AI_API_KEY": "your-key"
}
}
}
}
20. Governance & Stability
To prevent regressions in the conversational UI, this project follows a strict Hybrid Layout Model:
- App Mode (Home): Fixed 100vh layout for the chat interface.
- Document Mode (Institutional): Fluid layout for content-heavy pages (About, FAQ, Data Guide).
For detailed engineering guidelines, see:
- GEMINI.md — Main architecture and agent rules.
- DESIGN.md — UI/UX philosophy and layout isolation rules.
- MAINTENANCE.md — Procedures for i18n sync and regression testing.
- TESTS.md — Detailed explanation of the 137 pytest test cases, Ruff linting/formatting rules, and CI workflows.
21. Data Guide
The Data Guide is a premium, trilingual catalog of all indicators and documents supported by the system. It provides "copy-paste" query examples for:
- Macro-categories: People, Prosperity, Planet, Infrastructure, and Digital.
- Official Documents: Audit reports, Procurement plans, CAS, and ICR.
22. Troubleshooting
| Issue | Probable Cause | Solution |
|---|---|---|
| Ollama quota exceeded | Daily API limit reached | Wait for reset or switch to Gemini in the Settings menu |
| Gemini model not found | GOOGLE_GENERATIVE_AI_API_KEY missing or wrong model | Check .env and PROVIDER_CONFIGS in lib/agent/agent_loop.py |
| BYOK: invalid key (401) | API key is malformed or revoked | Re-enter the key in the 🔑 API Keys modal |
| BYOK: quota exceeded (429) | BYOK provider credits exhausted | Top up your account or fall back to Ollama (built-in, free) |
| WB API timeout | World Bank API slowness | System retries 3x. If it persists, check your connectivity |
| Cache not working | CACHE_TYPE not in .env | Add CACHE_TYPE=SimpleCache to .env |
License
MIT License. Data provided by World Bank public APIs (public domain).
Authors
- Reinaldo Chaves - reichaves@gmail.com
- Luiz Fernando Toledo - luizftoledolai@gmail.com
⚓ Anchor Journalism (PT-BR)
Assistente de dados do World Bank para jornalistas investigativos. Faça perguntas em português, inglês ou espanhol — o agente consulta as APIs do World Bank e retorna tabelas formatadas com citações de fonte e notas metodológicas. Anti-alucinação é a restrição central de design.
Índice
- Sobre
- Funcionalidades
- Stack técnica
- Estrutura de arquivos
- Pré-requisitos
- Instalação
- Configuração
- Executando o servidor
- Uso
- API HTTP
- Providers LLM
- Indicadores suportados
- Anti-alucinação por design
- Histórico de conversa
- Metodologia de Integração da API Data360
- Protocolos de Segurança e Manipulação de Dados
- Deploy com Docker
- Servidor MCP
- Troubleshooting
1. Sobre
O Anchor Journalism é um agente conversacional que conecta jornalistas investigativos diretamente às APIs do World Bank. Em vez de navegar por portais de dados complexos, você faz uma pergunta em linguagem natural — o sistema busca dados reais, formata em tabelas comparativas e cita a fonte com precisão.
Exemplo de query:
"Como evoluiu a participação feminina no mercado de trabalho no Brasil vs. Colômbia entre 2014 e 2024?"
Exemplo de resposta:
| País | 2014 | 2019 | 2024 |
|---|---|---|---|
| Brasil | 54,2% | 53,8% | 54,1% |
| Colômbia | 52,1% | 52,7% | 53,4% |
[WDI: SL.TLF.TOTL.FE.ZS | BRA;COL | 2014:2024 | Consultado: 2026-05-05 | URL: ...]
Visão Geral da Arquitetura
Uma consulta percorre cinco camadas antes de retornar uma resposta citada e verificada:
flowchart TD
User["👤 User"] --> FE["Frontend\nHTML · JS · Chart.js"]
FE -->|"POST /api/chat\nor /api/chat/stream"| Flask["Flask Server\napp.py\n(rate limit · validation · CORS)"]
Flask --> AL["Agent Loop\nlib/agent/agent_loop.py\nmax 5 iterations · temperature=0"]
AL <-->|"chat completions\n(tool calling)"| LLM["LLM Providers\nOllama · Gemini\nOpenAI · Anthropic"]
AL <-->|"indicator / document tools"| WB["World Bank APIs\nData360 · WDS · WDI Classic"]
AL -->|"response + source seal"| Flask
Flask -->|"JSON or SSE tokens"| FE
Design Anti-alucinação: se a API do Banco Mundial não retornar dados, o agente declara a ausência explicitamente — nunca faz estimativas.
2. Funcionalidades
- Ordenação por Data de Atualização — permite priorizar os indicadores mais recentes passando
sort_by_date=Truepara as ferramentas de busca e descoberta, garantindo que as análises utilizem os dados mais frescos disponíveis. - Motor de Fact-checking Proativo e Ranking — verifica automaticamente afirmações sobre rankings (ex: "é o país X o 9º mais...?") consultando bases globais (
countries=['all']) e calculando a posição real no backend (compute_ranking). Possui Slice Thinning Inteligente para manter no prompt apenas os Top 10 + país alvo + vizinhos de ranking, evitando estouro de contexto do LLM. - Tradução Transparente de Buscas — traduz automaticamente termos de busca do PT/ES para Inglês antes de consultar tanto as APIs de indicadores quanto a biblioteca de documentos (WDS), aumentando drasticamente o acerto sem esforço do usuário.
- Fallback Dinâmico de Dados Históricos — quando o ano solicitado não tem dados para um país, o sistema busca automaticamente os anos históricos mais recentes disponíveis (até 3) e os exibe com aviso ⚠️, mantendo os dados de fallback isolados no campo
historical_fallback_datapara evitar poluição em cálculos aritméticos da resposta principal. - Formatação de Valores Monetários — valores decimais brutos em texto (ex:
2347.44 dólares americanos) são automaticamente formatados com notação local legível:US$ 2.347em português,US$ 2,347em inglês/espanhol. - Chat multilíngue — aceita consultas em PT/EN/ES; responde no mesmo idioma
- Interpretabilidade Metodológica — recuperação automática de definições oficiais, metodologia, limitações, cobertura geográfica (
ref_area), período coberto (time_period), tipo de medição (aggregation_measure) e referência de codelist SDMX (code_list) via Data360 API (ferramentaget_metadata) - Inteligência Documental — busca relatórios oficiais do Banco Mundial (API WDS) e extrai texto de PDFs (
pdfplumber) - PCN Badges Interativos (Proof-Carrying Numbers) — hashes criptográficos SHA-256 para cada dado extraído; badges inline (v) clicáveis abrem um modal com o JSON bruto do Banco Mundial para auditoria granular
- Transparência de Ciclo de Atualização — detecção dinâmica do último ponto de dado disponível (ex: "Último dado é de 2023") exibida em um card de transparência com notas 100% localizadas (PT/EN/ES)
- Desagregação de Indicadores & Codelists — identifica dimensões disponíveis (ex: por Sexo, Idade ou Urbano/Rural) via ferramenta
get_disaggregatione resolve nomes legíveis para códigos viafind_codelist_value, permitindo reportagens investigativas profundas sobre desigualdades - Escala de Gini Explicada — notas automáticas que esclarecem a escala 0-100 usada pela API do Banco Mundial em comparação à escala acadêmica 0-1.
- Integridade Cronológica — regras estritas impedem que o agente preencha linhas de anos faltantes com dados de anos anteriores, garantindo que as tabelas reflitam apenas os resultados literais da API.
- Guia de Investigação OSINT — guia integrada com cenários práticos para fact-checking, reportagens comparativas e cruzamento de dados
- Comando
/country:Nome— lista instantaneamente todos os indicadores e macrocategorias suportados para um país específico - Reset de Navegação — clique no logotipo da âncora a qualquer momento para limpar o chat e voltar à tela inicial
search_indicator— busca indicadores por tema via Data360 API (busca vetorial)get_time_series— série temporal com paginação automática para múltiplos paíseslist_countries— apenas países reais (agrupamentos regionais excluídos)- Mapeamento de frases — resolve frases PT/EN/ES para códigos WB automaticamente
- Validação Estrita — uso de modelos Pydantic para evitar alucinações estruturais nas respostas das APIs
- Caveats metodológicos — notas automáticas para 23+ indicadores com limitações conhecidas (WGI, trabalho, PIB, pobreza, CO2, floresta)
- Anti-alucinação — flag
data_available: falsepropagada ao LLM quando não há dados - Citação de fonte — cada número citado com código, países, período, data e URL
- 5 providers de LLM — Ollama e Gemini integrados (sem chave necessária na UI); traga sua própria chave para OpenAI (GPT-4.1), Anthropic (claude-haiku-4-5-20251001) ou Gemini 3.5 Flash (3.5 Flash)
- BYOK (Bring Your Own Key) — chaves fornecidas pelo usuário via header
X-User-Api-Key; armazenadas apenas emsessionStorage, nunca logadas ou persistidas no servidor - Gráficos Vega-Lite dinâmicos — ferramenta
generate_chart_specgera especificações Vega-Lite validadas no backend (line, bar, area, grouped_bar, scatter); renderizadas no browser viavegaEmbed. O LLM só chama a ferramenta quando o tipo de gráfico é justificado (pedido explícito do usuário, dados desagregados ou visualização de tendência). Evita alucinações de gráficos mantendo todos os dados em Python. - Streaming de respostas —
POST /api/chat/streamretornatext/event-streamcom tokens para renderização em tempo real - Anexo de arquivos — envie PDF (mãx. 20 páginas), CSV ou TXT para cruzar com dados do World Bank
- Suporte a reduced-motion — respeita a media query
prefers-reduced-motion - Seletor de idioma — detecção automática ou seleção manual (PT / EN / ES)
- Histórico de conversa — localStorage do navegador persiste o chat entre recarregamentos (máx 50 msgs)
- Cache de respostas & Bypass — respostas da WB API cacheadas em memória (1h padrão, 24h para lista de países). Suporta bypass de cache sob demanda via header
X-Force-Refresh: true, parâmetro?force_refresh=truena query string, ou chave"force_refresh": trueno payload JSON. - Proteção SSRF — allowlist de domínios para PDFs (apenas domínios World Bank); rejeita URLs externas de tool calls
- Proteção XSS e BYOK — DOMPurify sanitiza todo output do LLM antes de renderizar, contando com um mecanismo de escape estrito via
escapeHTMLcomo fallback para resguardar chaves BYOK caso o DOMPurify falhe ao carregar. - Limite de upload — cap de 10 MB na camada HTTP; extração de PDF limitada a 20 páginas
- Headers de segurança — CSP,
X-Frame-Options,X-Content-Type-Optionsem todas as respostas - Logging estruturado — formato JSON via env var
LOG_FORMAT=jsonpara observabilidade em produção - Deploy Docker — pronto para produção com gunicorn (2 workers, timeout 120s)
- CI/CD — GitHub Actions com matrix Python 3.11 + 3.12, ruff + mypy (não-bloqueante) + pytest em cada push/PR
- Health endpoint —
GET /healthpinga a API do World Bank; retorna{"status":"ok","wb_api":true/false}
3. Stack técnica
| Camada | Tecnologia |
|---|---|
| Backend | Python 3.11+ + Flask |
| Validação de Dados | Pydantic (schemas estritos) |
| Extração de PDF | pdfplumber (extração de texto otimizada) |
| Servidor MCP | FastMCP (Model Context Protocol) |
| LLM — Opção 1 | Ollama (gpt-oss:120b-cloud) via API OpenAI-compatible |
| LLM — Opção 2 | Gemini 2.5 Flash via API OpenAI-compatible |
| Agent loop | Implementação manual com o SDK Python openai |
| WB Classic API | requests → api.worldbank.org/v2 |
| WB Data360 API | requests → data360api.worldbank.org |
| Frontend | HTML + CSS + JavaScript puro (sem etapa de build) |
| Renderização MD | marked.js (hospedagem local, tabelas GFM) |
| Configuração | python-dotenv + .env |
| Rate limiting | flask-limiter — 10 req/min no /api/chat |
| Cache de respostas | flask-caching + SimpleCache (in-memory, configurável via CACHE_TYPE) |
| Servidor produção | gunicorn — 2 workers, timeout 120s |
| Testes | pytest + responses (HTTP mocking) — 137 testes |
| Linting | ruff (regras E, F, I) + pre-commit hook |
| Type checking | mypy (step não-bloqueante no CI) |
| CI/CD | GitHub Actions — matrix Python 3.11 + 3.12 |
| Deploy | Docker (python:3.11-slim) + .dockerignore |
| Persistência | localStorage do navegador para histórico (sem DB no backend) |
4. Estrutura de arquivos
anchor_journalism/
├── app.py # Entry point Flask — POST /api/chat, GET /health
│
├── lib/
│ ├── __init__.py
│ ├── cache.py # Singleton Flask-Caching + helpers cache_get/set/clear
│ ├── models.py # Modelos Pydantic para validação estrita de dados
│ │
│ ├── wb_client/
│ │ ├── __init__.py # Re-exporta get_time_series, list_countries, search_indicator
│ │ ├── base.py # BaseClient com lógica de retry e Selos de Fonte (SHA-256)
│ │ ├── data360_api.py # get_time_series(), search_indicator(), list_countries()
│ │ ├── docs_client.py # search_documents() — API de Documentos do Banco Mundial
│ │ └── wdi_client.py # WDIClient: fetch_time_series (Data360 primário → fallback Classic),
│ │ # _fetch_time_series_classic, fetch_indicator_metadata, search_indicator
│ │
│ ├── indicator_map/
│ │ ├── __init__.py # find_indicators() (maior match, normalização de acentos) + get_caveat()
│ │ ├── phrases.py # PHRASE_TO_INDICATORS — frases PT/EN/ES → códigos WB
│ │ └── caveats.py # INDICATOR_CAVEATS — 12+ notas metodológicas (WGI, trabalho, energia,
│ │ # saúde, pobreza, CO2, floresta) — sempre em inglês (evita language drift)
│ │
│ └── agent/
│ ├── __init__.py
│ ├── system_prompt.py # get_system_prompt(language) — regras anti-alucinação
│ ├── tools.py # TOOL_DEFINITIONS — schemas function calling
│ └── agent_loop.py # run_agent() — loop com tool use (máx 5 iterações)
│
├── mcp_src/ # Implementação do Servidor MCP (FastMCP)
│ └── anchor_mcp/
│ ├── main.py # Ponto de entrada do Servidor MCP
│ └── tools/ # Ferramentas expostas via MCP (indicadores, documentos)
│
├── static/
│ ├── index.html # Página principal do chat
│ ├── about.html / faq.html / policy.html / data_guide.html # Páginas institucionais
│ ├── js/
│ │ ├── app.js # Lógica do chat, estado de provider/idioma, histórico
│ │ ├── shared.js # Tema, i18n, seletor de idioma, badge de status
│ │ ├── i18n.js # Strings de tradução (PT/EN/ES)
│ │ └── glossary.js # Dados do glossário por idioma
│ └── css/
│ └── styles.css # Estilos globais (sem etapa de build)
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixtures (mocks Data360 + Classic API) + cache_clear autouse
│ ├── test_app.py # Endpoints Flask + integração BYOK
│ ├── test_agent_loop.py # Validação BYOK, UserKeyError, _ScrubKeyFilter
│ ├── test_data_api.py # Série temporal Data360, fallback Classic API, multi-país
│ ├── test_data360_api.py # search_indicator, resultados vazios
│ ├── test_docs_client.py # search_documents, extração de PDF, bloqueio SSRF
│ ├── test_indicator_map.py # phrase matching (acentos, maior match), caveats
│ ├── test_milestone3.py # integração get_indicator_metadata
│ ├── test_models.py # Validação de schemas Pydantic
│ ├── test_provenance_ui.py # página de proveniência /verify/<id>
│ └── test_security.py # Testes de integridade do Source Seal (SHA-256)
│
├── .github/workflows/ci.yml # GitHub Actions — ruff + mypy + pytest no push/PR
│
├── Dockerfile # Produção: python:3.11-slim + gunicorn
├── .dockerignore # Exclui .venv, .env, __pycache__, tests, .git
├── .pre-commit-config.yaml # Hooks ruff + ruff-format
│
├── requirements.txt # Dependências (flask, pydantic, pdfplumber, ...)
├── requirements-dev.txt # Dev: pytest, pytest-cov, responses, ruff, mypy
├── .env.example # Template de vars de ambiente (+ CACHE_TYPE)
├── .env # Suas chaves reais (gitignored)
├── .gitignore
├── ruff.toml # Linting: select E,F,I; ignore E501
├── TESTS.md # Documentação detalhada dos testes, Ruff e CI
└── README.md
5. Pré-requisitos
- Python 3.11 ou superior — verificar:
python --version - pip (vem com Python)
- Chave de API de pelo menos um provider LLM:
- Ollama: rodar localmente ou usar a chave no servidor
- Gemini: obter chave em aistudio.google.com (tier gratuito: 250 req/dia)
- Conexão à internet (chamadas em tempo real às APIs do World Bank e do LLM)
6. Instalação
# Clonar o repositório
git clone https://github.com/reichaves/anchor_journalism.git
cd anchor_journalism
# Criar e ativar ambiente virtual
python -m venv .venv
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate.bat # Windows CMD
# Instalar dependências
pip install -r requirements.txt
7. Configuração
cp .env.example .env
Edite .env com suas chaves:
OLLAMA_API_KEY=ollama_SUA_CHAVE_AQUI
GOOGLE_GENERATIVE_AI_API_KEY=AIzaSUA_CHAVE_AQUI
PORT=5000 # opcional
FLASK_DEBUG=false # opcional
ALLOWED_ORIGINS=http://localhost:5000 # opcional
CACHE_TYPE=SimpleCache # opcional — backend do cache de respostas
CACHE_DEFAULT_TIMEOUT=3600 # opcional — timeout do cache em segundos (1h)
ACCESS_CODE=WbMp182026 # opcional — restringe acesso com senha (sem definir = aberto; padrão para testes em https://anchor-journalism.com/ é WbMp182026)
LOG_FORMAT=json # opcional — logs em JSON para observabilidade em produção
Você só precisa de uma das duas chaves.
8. Executando o servidor
python app.py
# → http://localhost:5000
Smoke test:
python -m py_compile app.py lib/agent/agent_loop.py lib/wb_client/wdi_client.py lib/wb_client/data360_api.py lib/agent/system_prompt.py
9. Uso
Interface Web
- Abra
http://localhost:5000 - Clique no ícone ⚙️ de configurações no canto superior direito
- Selecione um provider: Ollama ou Gemini (integrados, sem chave) — ou desbloqueie OpenAI, Claude ou Gemini 3.5 Flash inserindo sua chave via 🔑 Chaves de API
- Selecione o idioma (Auto / PT / EN / ES)
- Digite sua pergunta e pressione Enter
Exemplos de queries
Transparência e Escopo:
/country:Brasil
(Lista todos os indicadores que o assistente consegue buscar para o Brasil)
Crescimento do PIB:
Como evoluiu o PIB per capita do Brasil, Argentina e Chile nos últimos 10 anos?
Gênero e trabalho:
Qual a diferença na participação feminina no mercado de trabalho entre Brasil e Colômbia de 2010 a 2023?
Governança:
Compare os índices de controle da corrupção do Brasil, México e Colômbia nos últimos 5 anos.
Meio ambiente:
Como evoluíram as emissões de CO2 per capita do Brasil entre 2000 e 2020?
Pobreza:
Quais países da América Latina têm maior índice de pobreza extrema?
Atalhos de teclado
| Atalho | Ação |
|---|---|
Enter | Enviar mensagem |
Shift + Enter | Nova linha no campo de entrada |
Cmd/Ctrl + K | Limpar conversa |
10. API HTTP
POST /api/chat
Limit: 10 req/min por IP.
Request:
{
"query": "PIB do Brasil em 2022",
"history": [],
"provider": "ollama",
"language": "pt"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
query | string | Sim | Pergunta do usuário |
history | array | Não | Histórico de mensagens — itens devem ter role "user" ou "assistant" (máx 100) |
provider | string | Não | "ollama" (padrão), "gemini", "openai", "anthropic", ou "gemini-user" |
language | string | Não | "auto" (padrão), "pt", "en", "es" |
force_refresh | boolean | Não | Se true, ignora as camadas de cache e força uma nova consulta em tempo real às APIs do Banco Mundial (padrão: false) |
Para providers BYOK (openai, anthropic, gemini-user), inclua o header com a chave do usuário:
X-User-Api-Key: sk-...
Opções de Bypass de Cache
Você pode forçar a atualização dos dados em tempo real (ignorando o cache) usando qualquer um dos métodos abaixo:
- Header HTTP: Passe
X-Force-Refresh: true. - Parâmetro de URL: Adicione
?force_refresh=trueà URL. - Corpo da Requisição: Envie
"force_refresh": trueno payload JSON.
POST /api/chat/stream
Variante com streaming. Mesmo corpo de requisição que /api/chat. Retorna text/event-stream com tokens para renderização em tempo real.
data: {"token": "Em "}
data: {"token": "2023"}
...
data: [DONE]
curl:
curl -X POST http://localhost:5000/api/chat \
-H "Content-Type: application/json" \
-d '{"query": "PIB do Brasil em 2022", "history": [], "provider": "ollama", "language": "pt"}'
11. Providers LLM
Providers integrados (sem chave necessária na UI)
| Provider | Modelo | Env var |
|---|---|---|
| Ollama | gpt-oss:120b-cloud | OLLAMA_API_KEY |
| Gemini | gemini-2.5-flash | GOOGLE_GENERATIVE_AI_API_KEY |
Providers BYOK (traga sua própria chave)
As chaves são inseridas no modal 🔑 Chaves de API na UI e armazenadas apenas em sessionStorage — nunca logadas ou enviadas ao servidor fora da requisição imediata.
| Provider | Modelo | Prefixo da chave | Notas |
|---|---|---|---|
| OpenAI | gpt-4.1 | sk- | temperature=0 |
| Anthropic (Claude) | claude-haiku-4-5-20251001 | sk-ant- | temperature=1 (restrição da API Anthropic com thinking habilitado) |
| Gemini 3.5 Flash | gemini-3.5-flash | AIza | temperature=0 |
12. Indicadores suportados
| Tema | Frases reconhecidas | Códigos WB |
|---|---|---|
| Participação feminina | participação feminina, female labor | SL.TLF.TOTL.FE.ZS, SL.TLF.CACT.FM.ZS |
| PIB | pib, gdp | NY.GDP.MKTP.CD, NY.GDP.PCAP.CD |
| Desemprego | desemprego, unemployment | SL.UEM.TOTL.ZS, SL.UEM.TOTL.FE.ZS, SL.UEM.TOTL.MA.ZS |
| Corrupção | corrupção, corruption | CC.EST |
| Governança | governança, governance | CC.EST, GE.EST, RL.EST, VA.EST, RQ.EST |
| Pobreza | pobreza, poverty | SI.POV.NAHC, SI.POV.DDAY |
| Desigualdade | desigualdade, inequality | SI.POV.GINI |
| Emissões GHG / CO2 | emissões co2, co2 emissions | EN.GHG.ALL.PC.CE.AR5, EN.GHG.ALL.MT.CE.AR5 |
| Mortalidade infantil | mortalidade infantil, child mortality | SH.DYN.MORT |
| Expectativa de vida | expectativa de vida, life expectancy | SP.DYN.LE00.IN |
| Educação | educação, education | SE.ADT.LITR.ZS, SE.PRM.NENR |
Para indicadores não listados, o agente usa search_indicator via Data360 API.
13. Anti-alucinação por design
O sistema foi projetado para nunca inventar dados:
wdi_client.py:fetch_time_series()— A API Data360 é sempre consultada primeiro (data360api.worldbank.org). Se não retornar registros, a Classic API (api.worldbank.org/v2) é tentada de forma transparente. Só quando ambas retornam vazio é quedata_available: falseé devolvido — nunca uma estimativa. Não pule a chamada à Data360; os formatos de resposta são diferentes e a ordem importa.agent_loop.py— repassa o resultado literal da ferramenta ao LLM, sem modificaçãosystem_prompt.py— regra #2: declara explicitamente a ausência de dados quandodata_available: falsetemperature=0— respostas determinísticas, sem especulação
Por que existe o fallback para duas APIs: A Data360 não cobre todas as séries no banco
WB_WDI. Indicadores de governança WGI usam um banco separado na Data360 (WB_WGI) com IDs diferentes (GOV_WGI_CC,GOV_WGI_GE, etc.) e exigem filtroCOMP_BREAKDOWN_1=WGI_ESTpara obter a estimativa -2,5/+2,5 em vez do ranking percentual — esse mapeamento está em_WGI_MAPemwdi_client.py. Os indicadores de CO2 da IEA (EN.ATM.CO2E.PC,EN.ATM.CO2E.KT) foram removidos do Banco Mundial em 2023. O mapeamento de frases agora resolve consultas de CO2/GHG para os equivalentes CAIT atuais:EN.GHG.ALL.PC.CE.AR5(per capita) eEN.GHG.ALL.MT.CE.AR5(total). Ambas as APIs apontam para o mesmo data warehouse do Banco Mundial.
14. Histórico de conversa
A interface web salva automaticamente sua conversa no localStorage do navegador:
- Persistência — mensagens sobrevivem a recarregamentos e reinícios do navegador
- Salvamento automático — cada resposta com sucesso é salva imediatamente
- Auto-trim — se o histórico exceder 50 mensagens, as 10 mais antigas são removidas
- Botão Clear — clique em Clear no header para deletar todo o histórico (remove também do localStorage)
- Tratamento de corrupção — se
JSON.parsefalhar, dados são descartados e sessão fresca inicia - Zero impacto no backend — todo armazenamento é client-side; sem banco de dados ou mudanças no servidor
15. Metodologia de Integração da API Data360
O Anchor Journalism prioriza a API Data360 del Banco Mundial, compatível com OData, em detrimento das interfaces clássicas para fornecer consultas estruturadas de alta fidelidade:
- Priorização Dinâmica de Endpoints: O cliente de banco de dados (WDIClient) traduz códigos clássicos do WDI em esquemas de indicadores correspondentes da API Data360 (ex: mapeando
SL.TLF.TOTL.FE.ZSparaWB_WDI_SL_TLF_TOTL_FE_ZS). As consultas são roteadas parahttps://data360api.worldbank.org/data360/datausando filtros OData. - Mecanismo de Fallback Clássico: Se a consulta na API Data360 falhar ou retornar uma lista vazia, o WDIClient executa automaticamente um fallback transparente para a API clássica do Banco Mundial v2 (
https://api.worldbank.org/v2). - WGI Estimativas & Percentis: Mapeamentos para os Worldwide Governance Indicators (ex:
CC.ESTno mapeamento_WGI_MAP) anexam automaticamente o parâmetro de buscaCOMP_BREAKDOWN_1=WGI_ESTpara extrair as estimativas brutas de governança (-2.5 a 2.5) em vez dos rankings percentuais. - Mapeamento de Desagregação e Codelists: A engine invoca
get_disaggregationefind_codelist_valuepara recuperar dimensões específicas (ex: Sexo, Idade) e associar chaves numéricas a códigos de padrão SDMX dinamicamente. - Integridade Cronológica e Dados Históricos: Caso o período desejado retorne vazio para um país, até 3 anos de dados históricos recentes são recuperados e isolados sob o campo
historical_fallback_datado objetoCountryData. Isso isola métricas históricas de fallback do payload principal, protegendo cálculos matemáticos e rankings subsequentes. - URLs de Verificação do Data360: Códigos de indicadores que não pertencem ao banco de dados WDI padrão (como
WB_ESG_*,OWID_CB_*) geram URLs de verificação amigáveis apontando diretamente paradata360api.worldbank.orgem vez da API clássica, evitando erros de "indicador não encontrado" ao verificar séries históricas descontinuadas ou arquivadas (como CO₂ per capitaWB_ESG_EN_ATM_CO2E_PC). - Automação Vega-Lite: Séries temporais limpas são estruturadas em Python e enviadas diretamente à ferramenta
generate_chart_specpara construir esquemas JSON do Vega-Lite validados no backend, prevenindo alucinações de gráficos.
16. Protocolos de Segurança e Manipulação de Dados
Para garantir a integridade da informação e a segurança das chaves do usuário, o projeto implementa uma arquitetura de proteção em múltiplas camadas:
- Isolamento Volátil de Chaves (BYOK): Chaves de API fornecidas pelos usuários (OpenAI, Anthropic, Gemini 3.5 Flash) são retidas estritamente na memória de execução JavaScript do navegador (
apiKeysMemoryem app.js). Elas nunca são gravadas em cookies, local storage, session storage ou arquivos de logs. - Defesa XSS e Proteção de Streaming: Respostas em Markdown enviadas via SSE são sanitizadas usando a biblioteca
DOMPurifyantes de serem renderizadas no DOM. Uma rotina nativa redundanteescapeHTMLatua como contingência (fail-safe) caso o carregamento de scripts externos falhe no cliente. Além disso, a conexão SSE implementa um mecanismo de keep-alive baseado em thread no backend que envia comentários em branco a cada 5 segundos para impedir que balanceadores de nuvem (como o do Render) fechem a conexão por timeout de 30 segundos de inatividade durante buscas lentas em APIs. - Proteção contra SSRF e Raspagens Restritas: A raspagem e extração de documentos pelo agente apenas aceita URLs de domínio verificado pertencentes ao Banco Mundial (
worldbank.org), bloqueando requisições a recursos externos. - Sanitização de Logs e Redação de PII: Logs de console e arquivos empregam o filtro Python customizado
_ScrubKeyFilterpara remover chaves de API, credenciais e dados sensíveis de pessoas físicas/jurídicas (como CPFs de 11 dígitos ou CNPJs de 14 dígitos). - Limitação de Requisições por IP: Controla acessos abusivos e tentativas de negação de serviço com o
flask-limiterintegrado a uma tabela SQLite dedicada (rate_limits.db) operando em modo WAL para alta concorrência. - Selos de Origem (Source Seals) e PCN: Cada consulta de dados gera uma assinatura SHA-256 (computada sobre
response.content + URL + timestamp). Esses selos são persistidos no banco de dados SQLite centralanchor.db(db.py) e expostos como badges clicáveis[v](Proof-Carrying Numbers) ao lado de cada número nas tabelas do chat, linkando para o validador público/verify/<query_id>.
17. Deploy com Docker
17.1 Build
docker build -t anchor .
17.2 Run
docker run --rm -p 5000:5000 --env-file .env anchor
17.3 Verificar
curl http://localhost:5000/health
# → {"status":"ok","wb_api":true}
17.4 Detalhes da imagem
| Base | python:3.11-slim |
| Servidor | gunicorn — 2 workers, --timeout 120 |
| Porta | 5000 |
| Excluídos | .venv, .env, __pycache__, tests/, docs/, .git/ (via .dockerignore) |
18. Servidor MCP
O Anchor Journalism expõe suas ferramentas principais via Model Context Protocol (MCP). Isso permite que outros agentes de IA (como Claude Desktop ou Cursor) consultem nativamente os indicadores e documentos do Banco Mundial usando a lógica implementada neste repositório.
18.1 Ferramentas (6 no total)
| Ferramenta | Descrição |
|---|---|
get_indicator_data | Busca séries temporais WDI para um ou mais países |
get_metadata | Metadados detalhados (definição, metodologia, limitações, cobertura temporal) |
get_disaggregation | Dimensões disponíveis (Sexo, Idade, Urbano/Rural, etc.) — estratégia dupla: endpoint dedicado + inferência por colunas de dados |
find_codelist_value | Resolve nomes legíveis para códigos Data360 (ex: "Female" → F); 6 dimensões estruturais (SEX, RESIDENCE, URBANISATION, FREQ, AGE, UNIT_MULT) resolvidas instantaneamente de tabela interna — sem chamada de API |
search_indicator | Busca semântica de indicadores via Data360 searchv2 |
search_documents | Busca relatórios do Banco Mundial (API WDS) |
O prompt investigate_country_economy também está incluído — template para análise econômica completa de um país.
18.2 Instalação
O servidor MCP é um pacote instalável separado dentro de mcp_src/. Instale em modo editable:
# Na raiz do projeto, com a venv ativa
# Use python -m pip se o pip.exe estiver quebrado (ex: após mover a venv)
python -m pip install "fastmcp>=0.4.1"
python -m pip install -e mcp_src/
Nota:
fastmcp>=0.4.1também está emrequirements.txt, entãopip install -r requirements.txtjá cobre em novas instalações.
18.3 Executando o servidor
# Comando correto — o pacote registra-se como anchor_mcp
python -m anchor_mcp.main
# NÃO use: python -m mcp_src.anchor_mcp.main (caminho de módulo errado)
18.4 Teste rápido das ferramentas
# test_mcp_tools.py — rode com: python test_mcp_tools.py
from anchor_mcp.clients.wdi_client import WDIClient
client = WDIClient()
# get_metadata
result = client.fetch_indicator_metadata("NY.GDP.MKTP.CD")
print(result.metadata.name, result.metadata.time_coverage)
# get_disaggregation
result = client.fetch_disaggregation("SH.STA.MMRT")
print(result.dimensions_available, [d.id for d in result.dimensions])
# find_codelist_value — SEX, FREQ, AGE, UNIT_MULT, RESIDENCE, URBANISATION
# resolve instantly from built-in table (no API call, no 404 risk)
result = client.fetch_codelist("SEX", "Female")
print(result.items) # → [CodelistItem(id='F', name='Female')]
result = client.fetch_codelist("FREQ")
print([i.id for i in result.items]) # → ['A', 'Q', 'M', 'W', 'D']
18.5 Configuração do Claude Desktop
Adicione ao seu claude_desktop_config.json:
{
"mcpServers": {
"anchor": {
"command": "python",
"args": ["-m", "anchor_mcp.main"],
"cwd": "E:/Code/anchor_journalism",
"env": {
"OLLAMA_API_KEY": "sua-chave",
"GOOGLE_GENERATIVE_AI_API_KEY": "sua-chave"
}
}
}
}
19. Troubleshooting
| Problema | Causa provável | Solução |
|---|---|---|
| Ollama quota exceeded | Limite diário da API Ollama esgotado | Aguarde o reset diário ou troque para Gemini no menu de configurações |
| Gemini model not found | GOOGLE_GENERATIVE_AI_API_KEY não configurada ou modelo indisponível | Verifique a chave no .env e o modelo em PROVIDER_CONFIGS em lib/agent/agent_loop.py |
| BYOK: chave inválida (401) | Chave de API malformada ou revogada | Reinsira a chave no modal 🔑 Chaves de API |
| BYOK: cota esgotada (429) | Créditos do provider BYOK esgotados | Recarregue sua conta ou use o Ollama (integrado, gratuito) |
| WB API timeout | Lentidão ou indisponibilidade da API do World Bank | O sistema faz retry automático (3x). Se persistir, verifique sua conectividade |
| Cache não está funcionando | CACHE_TYPE não configurado no .env | Adicione CACHE_TYPE=SimpleCache ao .env (ou outro backend válido do flask-caching) |
Licença
MIT License. Dados fornecidos pelas APIs públicas do World Bank (domínio público).
Autores
- Reinaldo Chaves - reichaves@gmail.com
- Luiz Fernando Toledo - luizftoledolai@gmail.com