Public trade briefs from a tool-grounded analyst agent.
Three briefs are already computed: BRICS minerals, US–China electronics, and Vietnam/Mexico supply into the United States. You can also type a plain question about countries, a bloc, a sector, and a year window. Each brief’s bars are SQL aggregates of the same rows as the text. Across the years keeps every year of that slice. Under Wider accounts, the same countries show World Bank merchandise totals and the IMF current account. Those country totals stay off the commodity chart so a multi-trillion export figure does not flatten an HS chapter.
The panel is live data:
- UN Comtrade annual merchandise for HS 26 (minerals), 27 (energy), 61–62 (textiles), 84 and 87 (machinery and vehicles), and 85 (electronics), 2022–2024
- World Bank merchandise exports and imports, current US dollars
- IMF World Economic Outlook current account, in dollars and as a percent of GDP
Refresh all three with python scripts/export_gallery.py --live. The 18-row synthetic seed remains only for offline tests. The public Comtrade preview does not return the India reporter, so that energy corridor is omitted rather than filled with the old sample.
Live site: https://asan-macro.vercel.app
The briefs on that URL need no API key. A typed question calls the Python API. Until that API is deployed (render.yaml) and NEXT_PUBLIC_AGENT_API_URL is set on the Vercel project, the ask box says generation is not connected and leaves the closest corridor on screen. The deployed site picks up a new gallery when web/ is pushed.
-
Open the site and pick a corridor: BRICS, US–China electronics, or Vietnam and Mexico.
-
Read Summary, Key regions and sectors, So what, and What’s next. The bars beside the text are the same HS rows.
-
Read Wider accounts for World Bank goods totals and the IMF current account for those countries.
-
Or type a short question (200 characters or fewer). Name the places, the goods, and the years in ordinary words.
Examples:
How has EU energy trade with India changed since 2019?ASEAN electronics exports to the United States, 2020 to 2024Japan exports to Canada
-
The question stays on screen while the service checks the cache. On a miss it reads the question, and if that pair is not already stored it asks UN Comtrade for that slice, then writes the brief.
-
Read the answer in this order:
- Summary states the start value, the end value, and the percent change.
- Across the years is one column per year, labeled UN Comtrade and the HS chapter.
- So what sets that move beside the World Bank merchandise total and the IMF current account. Those accounts are country-wide. They are not the bilateral flow.
- What people are saying lists public links. A link is context only. It is never the source of a number on the chart.
-
A repeated question is free if it is already cached. A new generation is limited to five an hour per IP. Instruction overrides are refused before any model call. The site does not accept a file or URL upload.
If the question names no sector, the brief says so and uses total merchandise instead of guessing a chapter. A bloc question (EU, ASEAN, USMCA, G7, or BRICS) stays small: a few members and their trade with the partner you named. If Comtrade returns nothing, the page says No official rows for this question and does not fill the gap with the synthetic seed. The three corridor buttons stay up either way.
If generation is unavailable, the page says so and keeps the closest corridor. The typed search needs the Python API. Until that API is deployed and NEXT_PUBLIC_AGENT_API_URL is set, the ask box cannot fetch a new slice.
The CLI is the agent. One command reads the local database, calls the tools, asks the model, and writes a four-section brief.
pip install -r requirements.txt
cp .env.example .envPut one model in .env, then run a focus. The CLI reads the database already on disk. It does not call Comtrade for a new pair. Load the live rows first if you want official numbers, then ask.
# Local, no API key
# USE_LOCAL_LLM=1
# OLLAMA_MODEL=qwen2:7b
ollama serve
python main.py -q "US-China electronics" -o report.txt
# or a cloud key instead of Ollama
# GEMINI_API_KEY=... or OPENAI_API_KEY=...
python main.py -q "How has EU energy trade with India changed since 2019?" -o report.txtOmit -q for a full-panel read. Other formats:
python main.py -q "Vietnam textiles" -o report.html -f html
python main.py -q "Mexico manufacturing" -o report.json -f json
python main.py -q "BRICS" -o report.txt -f linkedin| Flag | What it does |
|---|---|
-q / --query |
Focus, treated as data. A country, a bloc, a sector, and a year window all count. Examples: BRICS minerals, ASEAN electronics exports to the United States, 2020 to 2024. |
-o / --output |
Report path. Default is a timestamped file in the project root. |
-f |
text (default), html, linkedin (report plus a one-liner), or json. |
-d / --data |
Load a CSV into the database before the brief. |
--data-url |
Load a CSV from an https URL on the host allowlist. |
--replace-data |
Clear trade_flows before that CSV load. |
The first run creates data/trade.db and, if it is empty, fills it with the 18-row synthetic seed so the agent can run offline. That seed is not official trade data.
To point the CLI and the API at the live HS corridors, then add World Bank and IMF rows to the same database:
python scripts/fetch_comtrade.py --corridors --replace
python -c "from macro_data import fetch_macro, load_macro; print(load_macro(fetch_macro()))"
python main.py -q "US-China electronics" -o report.txt--replace clears commodity rows before the Comtrade insert. load_macro replaces only macro_series. The model then sees those accounts through get_macro_context.
What you get back is a brief with four headings — Summary, Key regions and sectors, So what, What’s next — plus a disclaimer. Refused: means the query guard stopped the call. Error: means no model was configured, or the provider failed. A canned sample is written only when ASAN_DEMO_FALLBACK=1.
The site reads the gallery at build time. The ask box calls the API only when NEXT_PUBLIC_AGENT_API_URL is set.
python scripts/export_gallery.py --live
cd web && npm install && NEXT_PUBLIC_AGENT_API_URL=http://127.0.0.1:8000 npm run devIn another terminal, from the repo root, with the same .env the CLI uses:
uvicorn api.app:app --port 8000export_gallery.py --live writes gallery/index.json and web/data/gallery.json from a temporary database. It does not replace data/trade.db. Use the fetch_comtrade.py commands above when you want the CLI to query the live rows. The website ask box, through uvicorn, fetches a missing Comtrade slice for the question and stores it in data/trade.db on its own. Restart uvicorn after pulling this code so that path is the one answering.
Without NEXT_PUBLIC_AGENT_API_URL, npm run dev still shows the briefs and says generation is not connected.
pip install -r requirements.txt
cp .env.example .env # one LLM backend, only needed for a generated brief
python scripts/export_gallery.py --live
cd web && npm install && npm run devCLI, after the database exists:
python main.py -q "BRICS" -o report.txtASAN Macro is a trade analyst agent. The public site in web/ shows precomputed briefs and charts. The same core still runs as a CLI: one command loads data (if needed), queries a local database through tools, calls an LLM, and writes the report to disk.
| Capability | Behavior |
|---|---|
| Trade sentiment reporting | Generates a short narrative with thematic synthesis, key regions/sectors, implications (so what), and outlook (what’s next), wrapped in REPORT_START / REPORT_END. |
| Focus queries | A plain question names countries or a bloc (EU, ASEAN, USMCA, G7, BRICS), a sector, and a year window (since 2019, 2020 to 2024). No sector means total merchandise. Omit -q for a full-database read. |
| Tool-grounded analysis | The LLM is backed by ten tools over SQLite—not free-form guessing alone—so claims can be tied to stored flows, macro series, and bulletins. |
| Multi-backend LLMs | First available backend: Ollama → Gemini → OpenAI. OpenAI runs a multi-turn tool loop. Ollama and Gemini get a focus-specific tool pack, then write once. Gemini receives the system prompt. |
| Dynamic data ingest | A question on the API fetches the missing Comtrade slice and stores it. scripts/export_gallery.py --live rebuilds the three sample briefs. scripts/fetch_comtrade.py --corridors --replace loads the fixed HS corridors into data/trade.db. CSV (-d) and an allowlisted --data-url still work. |
| Multiple output formats | text (default), html, linkedin (report + short one-liner file), or json (structured fields for dashboards/APIs). |
| Evaluation | evaluation/run_evaluation.py runs multiple focus queries and scores section presence, length, and runtime. |
| Adversarial / safety probes | guard_query refuses instruction-override text before any model call (unit tested). Inputs that pass the guard are not yet recorded against a live model. |
| Scheduled runs | Cron helper (scripts/run_scheduled.sh) and GitHub Actions can ingest + generate recurring report artifacts. |
| Colab demo path | colab_setup.ipynb supports cloud runs with Gemini or OpenAI (no local Ollama). |
| Details | |
|---|---|
| Inputs | SQLite (data/trade.db: trade_flows, macro_series, rag_chunks); the gallery JSON the site imports; optional CSV/URL/Comtrade; optional focus query; .env LLM config. |
| CSV columns | year, reporter_region, partner_region, sector, flow_type, value_usd. |
| Outputs | Report file (+ console print). HTML is escaped for safer viewing; JSON exposes summary, bullets, so_what, whats_next. |
| Tool | Function |
|---|---|
list_regions / list_sectors |
Discover entities in the database |
query_trade_flows |
Filter flows by reporter, partner, sector, year range |
get_region_summary / get_sector_summary |
Aggregate values for a region or sector |
get_yoy_growth |
Year-over-year growth via SQL window logic |
get_top_flows |
Top-N flows by value |
get_trade_trends |
First-year vs last-year deltas |
get_macro_context |
World Bank merchandise totals and the IMF current account for one country, or the loaded set |
rag_retrieve |
Keyword retrieval over stored trade bulletin chunks |
# Full analysis on whatever is already in data/trade.db
python main.py -o report.txt
# Focused brief + LinkedIn one-liner
python main.py -q "BRICS" -o report.txt -f linkedin
# Ingest CSV, then analyze
python main.py -q "US-China electronics" -d sample_trade.csv -o report.html -f htmlIntended users: economists, macro strategists, and policy/business readers who need a consistent, data-tied brief instead of a manual spreadsheet-to-chatbot workflow.
End-to-end pipeline from user input to report:
flowchart TB
subgraph Inputs
U[User / Cron / GitHub Actions]
Q["Focus query (-q)"]
D["CSV / URL / Comtrade / World Bank / IMF"]
end
subgraph Orchestration["main.py"]
E[Load .env + config]
DB[ensure_db → data/trade.db]
IN[Optional ingest into trade_flows and macro_series]
A[run_agent]
OUT[Format: text / HTML / LinkedIn / JSON]
end
subgraph Agent["agent.py"]
SEL{LLM backend}
OL[Ollama — pre-gather tools → single prompt]
GE[Gemini — pre-gather tools → single prompt]
OA[OpenAI — multi-turn function calling]
end
subgraph Tools["tools.py + SQLite"]
T1[list_regions / list_sectors]
T2[query_trade_flows]
T3[get_yoy_growth / get_top_flows / get_trade_trends]
T4[get_macro_context]
T5[rag_retrieve over rag_chunks]
end
U --> E --> DB --> IN --> A
Q --> A
D --> IN
A --> SEL
SEL --> OL & GE & OA
OL & GE & OA --> Tools
Tools --> DB
OL & GE & OA --> OUT
| Step | Component | What happens |
|---|---|---|
| 1. Configure | config.py, .env |
Load API keys / Ollama settings. Backend priority: Ollama → Gemini → OpenAI. |
| 2. Persist | database.py |
Create/seed data/trade.db (trade_flows, rag_chunks) if missing. |
| 3. Ingest (optional) | live_slice.py, scripts/fetch_comtrade.py, macro_data.py |
A typed question on the API fetches the missing Comtrade slice. The CLI uses corridors, World Bank, IMF, or a CSV you loaded first. |
| 4. Ground | tools.py |
Ten tools query structured flows, macro series, and keyword RAG so the model reasons over data, not only priors. |
| 5. Reason | agent.py |
LLM synthesizes themes (e.g. BRICS, US–China, South–South). OpenAI uses a true tool-calling loop; Ollama/Gemini use deterministic pre-tooling + one synthesis call for portability. |
| 6. Deliver | main.py |
Extract REPORT_START…REPORT_END and write text / HTML / LinkedIn one-liner / structured JSON. |
| 7. Assure | evaluation/ |
Completeness metrics across focus queries; adversarial cases for prompt leak, jailbreak-style, off-topic, and gibberish inputs. |
| 8. Operate | GitHub Actions / cron | Optional scheduled ingest + report artifacts for recurring briefs. |
For diagrams and deeper run notes, see LOGIC_FLOW_DIAGRAM.md and RUN_AND_LOGIC.md.
| Design choice | My rationale |
|---|---|
| Tools over raw prompt stuffing | Map analyst questions to named tools (YoY, top flows, trends)—clearer, testable, closer to production agents. |
| Multi-backend orchestration | Local Ollama for zero-quota demos; Gemini/OpenAI when quality or function calling matters. |
| Structured report contract | Fixed sections make outputs usable for briefs and measurable in evaluation. |
| Evaluation + adversarial suite | Treat agent quality and misuse resistance as engineering problems, not afterthoughts. |
| CLI + artifacts first | Keep the surface small while proving the full data → insight loop. |
pip install -r requirements.txt
cp .env.example .env # configure one LLM backend
python main.py -q "BRICS minerals and energy" -o report.txt| Backend | Setup | Best for |
|---|---|---|
| Ollama (recommended) | ollama pull qwen2:7b · .env: USE_LOCAL_LLM=1, OLLAMA_MODEL=qwen2:7b |
Local demos, no API quota |
| Gemini | GEMINI_API_KEY=... |
Free-tier cloud runs |
| OpenAI | OPENAI_API_KEY=... |
Full multi-turn tool calling |
python main.py -q "BRICS" -o report.txt
python main.py -q "US-China electronics" -d sample_trade.csv -o report.txt
python main.py -q "BRICS" -o report.html -f html
python main.py -o report.json -f json
python scripts/fetch_comtrade.py --corridors --replace && python main.py -q "BRICS" -o report.txt
python scripts/export_gallery.py --live
python evaluation/run_evaluation.py
python evaluation/adversarial_test.py| Flag | Description |
|---|---|
-q / --query |
Focus the analysis (e.g. BRICS, US-China electronics) |
-o / --output |
Output path (default: timestamped report file) |
-d / --data |
Load CSV into DB before analysis |
--data-url |
Load CSV from a URL |
-f |
text | html | linkedin | json |
ASAN_AI_Project/
├── main.py # CLI entry: ingest → agent → format output
├── agent.py # LLM orchestration (Ollama / Gemini / OpenAI)
├── tools.py # Analyst tools over SQLite, macro series, and RAG
├── report_contract.py # Brief schema, query guard, disclaimer
├── focus.py # Countries, blocs, sectors, and the year window
├── live_slice.py # On-demand Comtrade fetch for one question
├── web_context.py # Public links, kept off the chart
├── macro_data.py # World Bank and IMF fetch, store, and sentences
├── gallery_build.py # SQL charts + three briefs + wider accounts
├── api/app.py # Cached, rate-limited live API
├── web/ # Next.js public site
├── database.py # Schema + offline seed
├── data_ingestion.py # CSV / allowlisted https ingest
├── config.py # Env, paths, model helpers
├── evaluation/
│ ├── run_evaluation.py # Completeness / runtime harness
│ └── adversarial_test.py # Misuse / injection-style probes
├── scripts/
│ ├── fetch_comtrade.py # UN Comtrade HS corridors → DB
│ ├── export_gallery.py # --live rebuilds the site JSON; default is the seed
│ └── run_scheduled.sh # Ingest + report for cron
├── .github/workflows/
│ ├── ci.yml
│ └── scheduled_report.yml
└── data/ # trade.db created on first run
Agent tools: list_regions, list_sectors, query_trade_flows, get_region_summary, get_sector_summary, get_yoy_growth, get_top_flows, get_trade_trends, get_macro_context, rag_retrieve.
| Practice | Location | Intent |
|---|---|---|
| Completeness metrics (sections, length, runtime) | evaluation/run_evaluation.py |
Measure report fitness across focus queries |
| Adversarial probes (prompt leak, jailbreak-style, off-topic, gibberish) | evaluation/adversarial_test.py |
Surface misuse and grounding failures early |
| Secrets hygiene | .env / GitHub Actions secrets |
No hardcoded API keys |
| Parameterized SQL | tools.py |
Reduce injection risk in DB tools |
| HTML escaping | main.py |
Safer rendered report artifacts |
Details: EVALUATION_AND_SAFETY.md.
- UN Comtrade corridors: DOC_COMTRADE.md ·
python scripts/fetch_comtrade.py --corridors --replace - World Bank and IMF:
python -c "from macro_data import fetch_macro, load_macro; print(load_macro(fetch_macro()))" - Site gallery:
python scripts/export_gallery.py --live - Cron / Actions: SCHEDULING.md ·
scripts/run_scheduled.sh· weekly workflow in.github/workflows/
- Open the notebook → clone →
pip install -r requirements.txt - Set
GEMINI_API_KEYorOPENAI_API_KEY(Colab cannot run Ollama) !python main.py -q "BRICS" -o report.txt
Prefer Colab Secrets over pasting keys into cells.
I completed this project alone: architecture, agent orchestration, tools/DB/RAG, ingestion and scheduling, evaluation and adversarial safety documentation, and portfolio-facing documentation. Interview talking points: LINKEDIN_PROJECT.md.
- Next.js site (
web/) with three corridor briefs, a plain-question search, a year chart, Wider accounts, a separate “What people are saying” note, and a tool trace. - FastAPI service (
api/app.py): cache, background jobs, five new generations per IP per hour. No public file or URL upload. - Pydantic brief contract, focus-specific tool pack, Gemini system prompt, disclaimer on every brief.
- Query guard and an https host allowlist for operator
--data-url. - Pytest plus a pull-request workflow. No live LLM in CI.
avg_word_countin the evaluation harness is a mean (the committed 12-run file now says 373, not the old sum of 4479).
Embedding retrieval, citation checks against a gold set, a live adversarial model run in CI, and a Comtrade-scale corpus. Keyword RAG and the OpenAI-only tool loop are unchanged on purpose for this pass.
Course and debug notes live in docs/archive.
LLM agents · function calling · RAG · prompt engineering · AI evaluation · adversarial / prompt-injection testing · secure secrets handling · SQL · data ingestion · Python · OpenAI / Gemini / Ollama · GitHub Actions
Use as needed for academic and portfolio purposes. Keep API keys out of the repository.