Skip to content

qrizan/web-research-agent

Repository files navigation

Web Research Agent

Agent riset berbasis topik input yang mencari secara otomatis dari web, Wikipedia, dan arXiv, memvalidasi sumber, membaca konten, mengevaluasi kecukupan, lalu menyusun laporan Markdown yang di-stream ke UI secara real-time.

Demo Web Research Agent

Catatan: Proyek ini dibuat sebagai catatan pembelajaran membangun aplikasi end-to-end berbasis LLM dengan LangGraph dan LCEL, bukan untuk kebutuhan produksi. Keputusan teknis, eksperimen, dan keterbatasan sistem didokumentasikan di folder documentations/.


Problem Statement

Mencari jawaban atas satu pertanyaan riset butuh waktu: membuka banyak tab, menilai sumber satu per satu, membaca, lalu merangkum. Bertanya langsung ke LLM memang cepat, tapi jawabannya tidak bisa ditelusuri ke sumber dan rawan mengarang.

Proyek ini dibangun untuk mencoba menjembatani keduanya: sebuah agent yang mengotomasi alur riset (mencari, membaca, dan merangkum dari web) sambil menjaga jawabannya tetap berdasarkan sumber yang benar-benar dibaca agent, sehingga hasilnya bisa diverifikasi. Di sisi lain, proyek ini juga menjadi eksperimen membangun agent LLM multi-langkah yang terkontrol dan transparan, dengan setiap langkah ditampilkan ke UI secara real-time.


System Design

Browser tidak pernah memanggil FastAPI langsung. Next.js berperan sebagai BFF (Backend for Frontend) yang menangani keamanan permukaan, lalu mem-proxy ke FastAPI.

Browser
   │  POST /api/research/stream  { question }
   ▼
Next.js BFF  (Vercel)
   • validasi input + filter pola injection
   • rate limit per IP via Upstash Redis
   • proxy ke FastAPI dengan header X-BFF-Token
   • teruskan SSE stream apa adanya ke browser
   │  POST /research/stream  (+ X-BFF-Token)
   ▼
FastAPI  (Render)
   • verifikasi X-BFF-Token
   • jalankan LangGraph agent
   • scan output (leakage) sebelum dikirim
   • stream hasil sebagai Server-Sent Events
   │
   ▼
LangGraph Agent (6 node, lihat Alur di bawah)

Alur LangGraph

Lima node berjalan berurutan, lalu evaluate_node memutuskan: cukup, atau ulangi fetch.

START
  │
  ▼
plan_node              tentukan query_type (general/technical) + maks. 3 query
  │
  ▼
search_node            Tavily + Wikipedia (+ arXiv jika technical)
  │                    gabung, dedup, batasi 11 URL
  ▼
validate_sources_node  cek URL bisa diakses (HEAD/GET), buang yang gagal
  │
  ▼
fetch_node  ◄───────┐  baca isi halaman via Jina Reader (maks. 3 URL/iterasi)
  │                 │
  ▼                 │
evaluate_node       │  LLM nilai: informasi sudah cukup?
  │                 │
  ├─ belum cukup ───┘  (selama iterasi < 3 dan masih ada URL baru)
  │
  ▼  cukup, atau iterasi = 3, atau URL habis
synthesize_node        LLM susun laporan Markdown, token di-stream ke UI
  │
  ▼
END

Penjelasan lengkap: documentations/ARCHITECTURE.md


Limitations

  • Bukan aplikasi instan. Satu riset butuh ~30-60 detik (1 iterasi) dan bisa beberapa menit jika loop fetch-evaluate berjalan sampai 3 iterasi, wajar untuk beberapa LLM call + validasi + fetch.
  • Cold start di production. Backend Render free tier tidur setelah ~15 menit idle; request pertama setelahnya butuh tambahan beberapa puluh detik.
  • query_type cenderung general. Pertanyaan gaya "bagaimana cara kerja X" sering dinilai general meski teknis, sehingga arXiv tidak dipanggil. Untuk sumber akademis, pakai kata kunci teknis yang eksplisit.
  • Tanda ✗ bukan berarti URL rusak. URL ditandai tidak dapat diakses ketika agent gagal membacanya, sering karena situs memblokir request otomatis (mis. DataCamp), bukan karena tautannya mati. Masih bisa dibuka manual di browser.
  • Akurasi laporan bergantung pada sumber yang terbaca. Agent merangkum dari halaman yang berhasil di-fetch dan tidak menilai kredibilitas sumber. Daftar sumber dicantumkan di laporan agar bisa diverifikasi sendiri.
  • Rate limit 5 request per IP per jam. Di Docker lokal semua request berbagi satu IP gateway; di production dihitung per user via x-forwarded-for.
  • Tidak ada automated test. Pengujian manual via curl dan browser; lihat TESTING.md.

Tech Stack

Komponen Tool
Backend framework FastAPI + Uvicorn
Agent orchestration LangGraph (state machine) + LangChain LCEL (komposisi chain)
LLM OpenAI gpt-4o-mini (plan, evaluate, synthesize)
Web search Tavily
Sumber akademis Wikipedia API + arXiv API
Web reader Jina Reader
HTTP client httpx + certifi
Data validation Pydantic v2
Frontend framework Next.js 14 App Router
Styling Tailwind CSS v4 + DaisyUI v5 (theme night)
Markdown render react-markdown + @tailwindcss/typography
Icons lucide-react
Rate limiting Upstash Redis via REST API
Infra lokal Docker + Docker Compose
Deployment Backend di Render (Docker), frontend di Vercel (Next.js)

Quick Start

Yang dibutuhkan: Docker + Docker Compose, API key untuk OpenAI, Tavily, dan Upstash Redis. Jina API key opsional di lokal (lihat tabel Environment Variables).

# 1. Clone repo
git clone <repo-url>
cd web-research-agent

# 2. Siapkan environment variables
cp .env.example .env
# Isi semua variabel - lihat tabel Environment Variables di bawah

# 3. Jalankan
docker compose up --build

Buka http://localhost:3000, masukkan topik riset, tunggu laporan selesai di-stream.

Catatan rate limit: 5 request per IP per jam. Di Docker, semua request dari mesin yang sama berbagi kuota ini karena IP tercatat sebagai gateway Docker network (172.31.0.1).


Environment Variables

Variable Keterangan Wajib
OPENAI_API_KEY Untuk plan_node, evaluate_node, dan synthesize_node Ya
TAVILY_API_KEY Web search via Tavily Ya
JINA_API_KEY Untuk Jina Reader (baca isi halaman). Opsional di lokal karena IP residential dilayani tanpa key, tapi wajib di production, karena Jina menolak IP datacenter (Render) dengan HTTP 401 tanpa key. Gratis di jina.ai/reader. Ya*
BFF_SHARED_SECRET Shared secret antara Next.js BFF dan FastAPI. Nilai bebas, tapi harus sama di kedua sisi. Ya
UPSTASH_REDIS_REST_URL REST URL dari Upstash Redis dashboard Ya
UPSTASH_REDIS_REST_TOKEN Token autentikasi Upstash Redis Ya
ALLOWED_ORIGINS Origin yang diizinkan CORS di backend (mis. URL Vercel). Default http://localhost:3000 untuk development. Tidak
FASTAPI_URL URL backend FastAPI. Di Docker diset otomatis ke http://backend:8000 oleh docker-compose.yml. Diisi URL Render saat deploy ke production. Ya

* JINA_API_KEY opsional untuk development lokal, wajib untuk deployment.


Testing & Observasi

Tidak ada automated test; pengujian dilakukan manual.

  • Uji fungsional & keamanan - panduan curl/browser lengkap (smoke test, auth, rate limit, injection, query teknis vs general) di documentations/TESTING.md.
  • Observasi perilaku - observations/run_observations.py menjalankan 5 topik uji lewat BFF lalu menulis ringkasan (tipe query, jumlah iterasi, waktu, cuplikan laporan) ke observations/RESULT_OBSERVATIONS.md.
# aplikasi harus sedang berjalan; 5 query = batas rate limit per jam
python3 observations/run_observations.py

5 query langsung menghabiskan kuota rate limit per jam, jadi untuk run ulang reset dulu key Upstash (perintahnya di TESTING.md).


Project Structure

web-research-agent/
├── backend/
│   ├── app/
│   │   ├── main.py                       # FastAPI app + CORS
│   │   ├── api/research.py               # SSE endpoint, auth, leakage scan
│   │   ├── graph/
│   │   │   ├── state.py                  # ResearchState + Pydantic schemas
│   │   │   ├── nodes.py                  # node + LCEL chains
│   │   │   ├── edges.py                  # routing + batas iterasi
│   │   │   └── graph.py                  # rakit & compile graph
│   │   └── tools/                        # search, wikipedia, arxiv, fetch, validate
│   ├── Dockerfile
│   └── requirements.txt
├── frontend/
│   ├── app/
│   │   ├── page.tsx                      # form input topik
│   │   ├── research/page.tsx             # UI streaming (layout 30/70)
│   │   ├── api/research/stream/route.ts  # BFF: proxy + rate limit + validasi
│   │   └── globals.css                   # Tailwind v4 + DaisyUI
│   └── Dockerfile
├── observations/                         # script uji + hasilnya
├── documentations/                       # ARCHITECTURE, DECISIONS, SECURITY, DATA_CONTRACT, TESTING
├── docker-compose.yml
└── .env.example

Documentation

Dokumen Isi
ARCHITECTURE.md Internal mechanics: ResearchState, tanggung jawab tiap node, edge routing, SSE event filtering
DECISIONS.md Keputusan teknis dengan rationale dan trade-off
SECURITY.md Kontrol keamanan dipetakan ke OWASP LLM Top 10 2025: prompt injection, output handling, system prompt leakage, unbounded consumption, plus BFF auth
DATA_CONTRACT.md Schema request/response endpoint BFF dan FastAPI, format SSE event, Pydantic schemas
TESTING.md Test manual: smoke test, backend langsung, BFF, rate limiting, query teknis vs general

Notebooks (Google Colab)

Fase eksperimen sebelum kode dipindah ke backend/. Setiap notebook memvalidasi satu bagian pipeline secara terpisah sebelum dirangkai jadi aplikasi.

Notebook Isi
01 - LCEL in Node Membangun LCEL chain (plan, evaluate, synthesize) dan membungkusnya menjadi node LangGraph
02 - Agent Graph Merangkai semua node + conditional loop fetch-evaluate menjadi graph end-to-end
03 - Streaming Menangkap event graph via astream_events() dan memilah step vs token untuk SSE

Catatan

LLM tidak mencari sumber, hanya menyusun dari yang ditemukan. Pencarian dilakukan API deterministik (Tavily, Wikipedia, arXiv) dan isi halaman diambil via Jina Reader. Peran LLM terbatas pada tiga hal: merencanakan query, menilai kecukupan, dan menyusun laporan dari konten yang di-fetch. Fakta dalam laporan berasal dari sumber yang dibaca, bukan pengetahuan internal LLM, sehingga sumbernya bisa ditelusuri. Jika tidak ada konten yang berhasil dibaca, agent menolak membuat laporan daripada mengarang (lihat D-05 di DECISIONS).

Tiga sumber dipilih karena saling melengkapi, bukan redundan. Tavily menangkap informasi terkini, Wikipedia memberi definisi dan konteks yang stabil, arXiv menyediakan rujukan akademis (hanya saat query_type = technical). Mengandalkan satu sumber membuat riset dangkal untuk sebagian jenis pertanyaan.

BFF bukan sekadar proxy. Next.js API route adalah tempat semua keamanan sisi permukaan masuk berada: validasi panjang input, injection pattern blocking, dan rate limiting. FastAPI hanya menerima request yang sudah lolos semua pemeriksaan ini dan membawa X-BFF-Token yang valid. URL FastAPI tidak pernah terekspos ke browser.

Dua lapis: LCEL untuk tiap langkah, LangGraph untuk alurnya. Tiap chain (plan, evaluate, synthesize) dibangun dengan LangChain Expression Language (LCEL) sebagai unit mandiri yang bisa diuji lepas dari graph (lihat Notebooks), lalu dibungkus menjadi node. LangGraph merangkai node-node itu sebagai state machine: semua kondisi berhenti agent (informasi cukup, iterasi maksimal, URL habis) ada di conditional edge, bukan di dalam node atau prompt LLM, sehingga perilaku agent bisa dibaca sebagai kode biasa dan tidak bisa diabaikan oleh output LLM.

Token streaming lewat satu jalur yang ketat. astream_events() menghasilkan event dari semua komponen dalam graph, termasuk token JSON internal dari plan_node dan evaluate_node. Filter berdasarkan metadata["langgraph_node"] memastikan hanya token dari synthesize_node yang diteruskan ke browser - token internal tidak pernah tampil di UI.

Spotlighting sebagai mitigasi prompt injection. Konten yang di-fetch dari web dibungkus dengan delimiter <<<EXTERNAL_CONTENT_START>>> sebelum masuk ke prompt synthesize_node. LLM diberitahu secara eksplisit bahwa konten di dalam delimiter adalah data eksternal tidak terpercaya, bukan instruksi. Teknik ini dari Microsoft Research (arXiv:2403.14720).

Semua keputusan teknis didokumentasikan dengan rationale dan trade-off di documentations/DECISIONS.md.


Referensi

About

a multi-step web research agent over the web, Wikipedia, and arXiv - built with LangGraph + LCEL, streams each step in real-time, and grounds every report in sources it actually fetches and reads.

Topics

Resources

Stars

Watchers

Forks

Contributors