Skip to content
Conduit

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

reichaves/anchor_journalismcommunitylistedunreviewed

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.

GitHub Python Flask World Bank Ollama Gemini License CI Live Demo

Versão em Português abaixo / Versión en Español → README.es.md


Table of Contents

  1. About
  2. Features
  3. Tech Stack
  4. File Structure
  5. Prerequisites
  6. Installation
  7. Configuration
  8. Running the Server
  9. Usage
  10. HTTP API
  11. LLM Providers
  12. Supported Indicators
  13. Anti-Hallucination by Design
  14. Conversation History
  15. Data360 API Integration Methodology
  16. Security & Data Handling Protocols
  17. Deployment (Docker)
  18. Deployment (Render)
  19. MCP Server
  20. Governance & Stability
  21. Data Guide
  22. 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:

Country201420192024
Brazil54.2%53.8%54.1%
Colombia52.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_data to 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.347 for Portuguese, US$ 2,347 for 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_metadata tool)
  • 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_disaggregation tool and resolves readable names to standard codes via find_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:Name Command — 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 countries
  • list_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: false flag 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-Key header; stored in sessionStorage only, never logged or persisted server-side
  • Vega-Lite dynamic charts — generate_chart_spec tool generates backend-validated Vega-Lite specs (line, bar, area, grouped_bar, scatter); rendered in the browser via vegaEmbed. 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/stream returns text/event-stream tokens 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-motion media 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: true HTTP header, ?force_refresh=true URL query parameter, or force_refresh: true JSON 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 escapeHTML fallback 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-Options on every response
  • Structured logging — JSON format via LOG_FORMAT=json env 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 /health pings the World Bank API; returns {"status":"ok","wb_api":true/false}

3. Tech Stack

LayerTechnology
BackendPython 3.11+ + Flask
Data ValidationPydantic (strict schemas)
PDF Extractionpdfplumber (optimized text extraction)
MCP ServerFastMCP (Model Context Protocol)
LLM — Option 1Ollama (gpt-oss:120b-cloud) via OpenAI-compatible API
LLM — Option 2Gemini 2.5 Flash via OpenAI-compatible API
Agent loopManual implementation using openai Python SDK
WB Classic APIrequests → api.worldbank.org/v2
WB Data360 APIrequests → data360api.worldbank.org
FrontendHTML + CSS + Pure JavaScript (no build step)
MD Renderingmarked.js (local hosting, GFM tables)
Configurationpython-dotenv + .env
Rate limitingflask-limiter — 10 req/min on /api/chat
Response cacheflask-caching + SimpleCache (in-memory, configurable via CACHE_TYPE)
Production servergunicorn — 2 workers, timeout 120s
Testingpytest + responses (HTTP mocking) — 137 tests
Lintingruff (rules E, F, I) + pre-commit hook
Type checkingmypy (non-blocking CI step)
CI/CDGitHub Actions — matrix Python 3.11 + 3.12
DeploymentDocker (python:3.11-slim) + .dockerignore
PersistenceBrowser 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
PackageVersionPurpose
flask≥ 3.0Web server
flask-cors≥ 4.0Cross-origin requests
flask-limiter≥ 3.5Rate limiting
openai≥ 1.60OpenAI-compatible client (Ollama + Gemini)
python-dotenv≥ 1.0Load variables from .env
requests≥ 2.31World 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

  1. Open http://localhost:5000
  2. Click the ⚙️ Settings icon in the top right
  3. 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
  4. Select response language (Auto, PT, EN, or ES)
  5. Type your question and press Enter (or click Send)
  6. 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

ShortcutAction
EnterSend message
Shift + EnterNew line in input field
Cmd/Ctrl + KClear 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"
}
FieldTypeRequiredDescription
querystringYesUser question
historyarrayNoPrevious messages — items must have role "user" or "assistant" (max 100)
providerstringNo"ollama" (default), "gemini", "openai", "anthropic", or "gemini-user"
languagestringNo"auto" (default), "pt", "en", or "es"
force_refreshbooleanNoIf 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=true to the URL.
  • Request Body: Send "force_refresh": true in 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)

ProviderModelEnv var
Ollamagpt-oss:120b-cloudOLLAMA_API_KEY
Geminigemini-2.5-flashGOOGLE_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.

ProviderModelKey PrefixNotes
OpenAIgpt-4.1sk-temperature=0
Anthropic (Claude)claude-haiku-4-5-20251001sk-ant-temperature=1 (Anthropic API restriction with thinking enabled)
Gemini 3.5 Flashgemini-3.5-flashAIzatemperature=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:

TopicPhrases (examples)WB Codes
Female Participationparticipação feminina, female labor, participación femeninaSL.TLF.TOTL.FE.ZS, SL.TLF.CACT.FM.ZS
GDPpib, gdpNY.GDP.MKTP.CD, NY.GDP.PCAP.CD
GDP per capitapib per cápita, gdp per capitaNY.GDP.PCAP.CD
Unemploymentdesempleo, unemployment, desempregoSL.UEM.TOTL.ZS, SL.UEM.TOTL.FE.ZS, SL.UEM.TOTL.MA.ZS
Corruptioncorrupción, corruption, corrupçãoCC.EST
Governancegobernanza, governance, governançaCC.EST, GE.EST, RL.EST, VA.EST, RQ.EST
Povertypobreza, povertySI.POV.NAHC, SI.POV.DDAY
Inequalitydesigualdad, inequality, desigualdadeSI.POV.GINI
GHG / CO2 Emissionsco2 emissions, ghg emissions, emissões co2EN.GHG.ALL.PC.CE.AR5, EN.GHG.ALL.MT.CE.AR5
Child Mortalitymortalidad infantil, child mortality, mortalidade infantilSH.DYN.MORT
Life Expectancyesperanza de vida, life expectancy, expectativa de vidaSP.DYN.LE00.IN
Educationeducación, education, educaçãoSE.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 optionally lib/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:

  1. 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 is data_available: false returned — never an estimate. Do not skip the Data360 call; response formats differ and call order matters.
  2. agent_loop.py — passes the literal tool result to the LLM without modification
  3. system_prompt.py — rule #2: "If the tool returns data_available: false, state explicitly that the World Bank has no data for this request"
  4. 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_WDI database. WGI governance indicators use a separate Data360 database (WB_WGI) with different indicator IDs (GOV_WGI_CC, GOV_WGI_GE, etc.) and require filtering COMP_BREAKDOWN_1=WGI_EST to get the -2.5/+2.5 estimate rather than percentile ranks — this mapping is in _WGI_MAP in wdi_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) and EN.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.parse fails, 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.ZS to WB_WDI_SL_TLF_TOTL_FE_ZS). It routes queries to https://data360api.worldbank.org/data360/data using 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.EST in _WGI_MAP) append the COMP_BREAKDOWN_1=WGI_EST query parameter to extract raw governance estimates (-2.5 to 2.5) rather than percentile ranks.
  • Disaggregation and Codelist Mapping: The engine calls get_disaggregation and find_codelist_value to 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_data field of CountryData. 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 at data360api.worldbank.org rather than the classic API, preventing "indicator not found" errors when verifying discontinued or archived series (such as CO₂ per capita WB_ESG_EN_ATM_CO2E_PC).
  • Vega-Lite Automation: Clean series are handled in Python and sent to generate_chart_spec to 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 (apiKeysMemory in app.js). They are never written to cookies, local storage, session storage, or logs.
  • XSS & Key Exposure Shielding: Markdown output is sanitized using DOMPurify before injecting into the SSE stream. A redundant native escapeHTML function 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.org domain. External resource requests are rejected.
  • Log Scrubbing & PII Redaction: Terminal and file logging employ a custom _ScrubKeyFilter to 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-limiter backed 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 database anchor.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

Basepython:3.11-slim
Servergunicorn — 2 workers, --timeout 120
Port5000
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_KEY and/or GOOGLE_GENERATIVE_AI_API_KEY

Steps

  1. Go to New > Web Service in the Render dashboard
  2. Connect the GitHub repository reichaves/anchor_journalism
  3. Render automatically detects the render.yaml file (Infrastructure as Code)
  4. Fill in the secret environment variables:
    • OLLAMA_API_KEY — your Ollama key
    • GOOGLE_GENERATIVE_AI_API_KEY — your Gemini key
  5. Click Create Web Service

Free Tier Limitations

LimitationImpact
Sleep after 15 min inactivityCold start of ~30-50s on first request after inactivity
512 MB RAMSufficient for Flask + 2 gunicorn workers
Custom domain configuredURL 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)

ToolDescription
get_indicator_dataFetch WDI time series for one or more countries
get_metadataRetrieve detailed metadata (definition, methodology, limitations, time coverage) for an indicator
get_disaggregationFetch available dimensions (Sex, Age, Urban/Rural, etc.) for an indicator — dual strategy: dedicated endpoint + column inference fallback
find_codelist_valueResolve 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_indicatorSemantic search for indicators via Data360 searchv2
search_documentsSearch 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.1 is also listed in requirements.txt so pip install -r requirements.txt covers 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

IssueProbable CauseSolution
Ollama quota exceededDaily API limit reachedWait for reset or switch to Gemini in the Settings menu
Gemini model not foundGOOGLE_GENERATIVE_AI_API_KEY missing or wrong modelCheck .env and PROVIDER_CONFIGS in lib/agent/agent_loop.py
BYOK: invalid key (401)API key is malformed or revokedRe-enter the key in the 🔑 API Keys modal
BYOK: quota exceeded (429)BYOK provider credits exhaustedTop up your account or fall back to Ollama (built-in, free)
WB API timeoutWorld Bank API slownessSystem retries 3x. If it persists, check your connectivity
Cache not workingCACHE_TYPE not in .envAdd CACHE_TYPE=SimpleCache to .env

License

MIT License. Data provided by World Bank public APIs (public domain).

Authors


⚓ 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.

GitHub Python Flask World Bank Ollama Gemini License

English version above / Versión en Español → README.es.md


Índice

  1. Sobre
  2. Funcionalidades
  3. Stack técnica
  4. Estrutura de arquivos
  5. Pré-requisitos
  6. Instalação
  7. Configuração
  8. Executando o servidor
  9. Uso
  10. API HTTP
  11. Providers LLM
  12. Indicadores suportados
  13. Anti-alucinação por design
  14. Histórico de conversa
  15. Metodologia de Integração da API Data360
  16. Protocolos de Segurança e Manipulação de Dados
  17. Deploy com Docker
  18. Servidor MCP
  19. 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ís201420192024
Brasil54,2%53,8%54,1%
Colômbia52,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=True para 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_data para 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.347 em português, US$ 2,347 em 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 (ferramenta get_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_disaggregation e resolve nomes legíveis para códigos via find_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íses
  • list_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: false propagada 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 em sessionStorage, nunca logadas ou persistidas no servidor
  • Gráficos Vega-Lite dinâmicos — ferramenta generate_chart_spec gera especificações Vega-Lite validadas no backend (line, bar, area, grouped_bar, scatter); renderizadas no browser via vegaEmbed. 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/stream retorna text/event-stream com 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=true na query string, ou chave "force_refresh": true no 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 escapeHTML como 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-Options em todas as respostas
  • Logging estruturado — formato JSON via env var LOG_FORMAT=json para 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 /health pinga a API do World Bank; retorna {"status":"ok","wb_api":true/false}

3. Stack técnica

CamadaTecnologia
BackendPython 3.11+ + Flask
Validação de DadosPydantic (schemas estritos)
Extração de PDFpdfplumber (extração de texto otimizada)
Servidor MCPFastMCP (Model Context Protocol)
LLM — Opção 1Ollama (gpt-oss:120b-cloud) via API OpenAI-compatible
LLM — Opção 2Gemini 2.5 Flash via API OpenAI-compatible
Agent loopImplementação manual com o SDK Python openai
WB Classic APIrequests → api.worldbank.org/v2
WB Data360 APIrequests → data360api.worldbank.org
FrontendHTML + CSS + JavaScript puro (sem etapa de build)
Renderização MDmarked.js (hospedagem local, tabelas GFM)
Configuraçãopython-dotenv + .env
Rate limitingflask-limiter — 10 req/min no /api/chat
Cache de respostasflask-caching + SimpleCache (in-memory, configurável via CACHE_TYPE)
Servidor produçãogunicorn — 2 workers, timeout 120s
Testespytest + responses (HTTP mocking) — 137 testes
Lintingruff (regras E, F, I) + pre-commit hook
Type checkingmypy (step não-bloqueante no CI)
CI/CDGitHub Actions — matrix Python 3.11 + 3.12
DeployDocker (python:3.11-slim) + .dockerignore
PersistêncialocalStorage 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

  1. Abra http://localhost:5000
  2. Clique no ícone ⚙️ de configurações no canto superior direito
  3. 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
  4. Selecione o idioma (Auto / PT / EN / ES)
  5. 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

AtalhoAção
EnterEnviar mensagem
Shift + EnterNova linha no campo de entrada
Cmd/Ctrl + KLimpar 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"
}
CampoTipoObrigatórioDescrição
querystringSimPergunta do usuário
historyarrayNãoHistórico de mensagens — itens devem ter role "user" ou "assistant" (máx 100)
providerstringNão"ollama" (padrão), "gemini", "openai", "anthropic", ou "gemini-user"
languagestringNão"auto" (padrão), "pt", "en", "es"
force_refreshbooleanNãoSe 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": true no 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)

ProviderModeloEnv var
Ollamagpt-oss:120b-cloudOLLAMA_API_KEY
Geminigemini-2.5-flashGOOGLE_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.

ProviderModeloPrefixo da chaveNotas
OpenAIgpt-4.1sk-temperature=0
Anthropic (Claude)claude-haiku-4-5-20251001sk-ant-temperature=1 (restrição da API Anthropic com thinking habilitado)
Gemini 3.5 Flashgemini-3.5-flashAIzatemperature=0

12. Indicadores suportados

TemaFrases reconhecidasCódigos WB
Participação femininaparticipação feminina, female laborSL.TLF.TOTL.FE.ZS, SL.TLF.CACT.FM.ZS
PIBpib, gdpNY.GDP.MKTP.CD, NY.GDP.PCAP.CD
Desempregodesemprego, unemploymentSL.UEM.TOTL.ZS, SL.UEM.TOTL.FE.ZS, SL.UEM.TOTL.MA.ZS
Corrupçãocorrupção, corruptionCC.EST
Governançagovernança, governanceCC.EST, GE.EST, RL.EST, VA.EST, RQ.EST
Pobrezapobreza, povertySI.POV.NAHC, SI.POV.DDAY
Desigualdadedesigualdade, inequalitySI.POV.GINI
Emissões GHG / CO2emissões co2, co2 emissionsEN.GHG.ALL.PC.CE.AR5, EN.GHG.ALL.MT.CE.AR5
Mortalidade infantilmortalidade infantil, child mortalitySH.DYN.MORT
Expectativa de vidaexpectativa de vida, life expectancySP.DYN.LE00.IN
Educaçãoeducação, educationSE.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:

  1. 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 é que data_available: false é devolvido — nunca uma estimativa. Não pule a chamada à Data360; os formatos de resposta são diferentes e a ordem importa.
  2. agent_loop.py — repassa o resultado literal da ferramenta ao LLM, sem modificação
  3. system_prompt.py — regra #2: declara explicitamente a ausência de dados quando data_available: false
  4. temperature=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 filtro COMP_BREAKDOWN_1=WGI_EST para obter a estimativa -2,5/+2,5 em vez do ranking percentual — esse mapeamento está em _WGI_MAP em wdi_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) e EN.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.parse falhar, 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.ZS para WB_WDI_SL_TLF_TOTL_FE_ZS). As consultas são roteadas para https://data360api.worldbank.org/data360/data usando 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.EST no mapeamento _WGI_MAP) anexam automaticamente o parâmetro de busca COMP_BREAKDOWN_1=WGI_EST para 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_disaggregation e find_codelist_value para 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_data do objeto CountryData. 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 para data360api.worldbank.org em vez da API clássica, evitando erros de "indicador não encontrado" ao verificar séries históricas descontinuadas ou arquivadas (como CO₂ per capita WB_ESG_EN_ATM_CO2E_PC).
  • Automação Vega-Lite: Séries temporais limpas são estruturadas em Python e enviadas diretamente à ferramenta generate_chart_spec para 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 (apiKeysMemory em 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 DOMPurify antes de serem renderizadas no DOM. Uma rotina nativa redundante escapeHTML atua 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 _ScrubKeyFilter para 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-limiter integrado 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 central anchor.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

Basepython:3.11-slim
Servidorgunicorn — 2 workers, --timeout 120
Porta5000
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)

FerramentaDescrição
get_indicator_dataBusca séries temporais WDI para um ou mais países
get_metadataMetadados detalhados (definição, metodologia, limitações, cobertura temporal)
get_disaggregationDimensões disponíveis (Sexo, Idade, Urbano/Rural, etc.) — estratégia dupla: endpoint dedicado + inferência por colunas de dados
find_codelist_valueResolve 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_indicatorBusca semântica de indicadores via Data360 searchv2
search_documentsBusca 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.1 também está em requirements.txt, então pip install -r requirements.txt já 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

ProblemaCausa provávelSolução
Ollama quota exceededLimite diário da API Ollama esgotadoAguarde o reset diário ou troque para Gemini no menu de configurações
Gemini model not foundGOOGLE_GENERATIVE_AI_API_KEY não configurada ou modelo indisponívelVerifique 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 revogadaReinsira a chave no modal 🔑 Chaves de API
BYOK: cota esgotada (429)Créditos do provider BYOK esgotadosRecarregue sua conta ou use o Ollama (integrado, gratuito)
WB API timeoutLentidão ou indisponibilidade da API do World BankO sistema faz retry automático (3x). Se persistir, verifique sua conectividade
Cache não está funcionandoCACHE_TYPE não configurado no .envAdicione 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

↑↓ navigate openesc close