This is a modified version of the Gemini fullstack LangGraph quickstart.
The frontend is intentionally left mostly unchanged. The backend has been rewritten to use:
- Qwen through an OpenAI-compatible API
- Tavily for fast web search
fetch_urlfor reading top search result pages- SQLite for structured long-term memory
- Chroma for semantic vector memory
- LangGraph for the agent workflow
The graph still keeps the frontend-compatible node names:
generate_query -> web_research or finalize_answer
Internally, the backend now does this:
-
generate_query- Uses Qwen to create up to three self-contained memory retrieval queries
- Reads prior SQLite memory with
search_memory - Reads semantic Chroma memory with
search_vector_memory - Locally ranks and keeps at most three memory records
- Skips fresh web search when memory is sufficient
-
web_research- Calls Tavily through
web_search - Fetches top result pages through
fetch_url - Uses Qwen to summarize evidence with citations
- Calls Tavily through
-
finalize_answer- Uses Qwen to write the final answer
- If web search was needed, checks memory plus search context once before answering
- Saves new memory only for search-backed answers, not for memory-only repeat answers
Memory runs in four practical stages:
-
Query planning
- The current question plus conversation context are rewritten into one to three self-contained memory lookup queries.
- This helps short follow-up questions retrieve the same long-term facts as the original full question.
-
Retrieval
- SQLite typed memory is searched through
search_memoryand previous answer summaries throughsearch_memory_summaries. - Chroma semantic memory is searched through
search_vector_memory. - Results from both stores are merged before ranking.
- SQLite typed memory is searched through
-
Ranking and sufficiency
- Local ranking keeps the most relevant memory records, controlled by
MEMORY_RERANK_TOP_K. - The memory-first decision checks whether the ranked memory directly answers the current question.
- If memory is sufficient, the graph skips fresh Tavily search. If not, it continues to web research.
- Local ranking keeps the most relevant memory records, controlled by
-
Write-back
- Search-backed final answers are saved as answer memory in SQLite.
- Atomic typed facts are extracted into
agent_memory_items. - Typed facts are also written to Chroma for later semantic retrieval.
- Memory-only repeat answers are not saved again, to avoid duplicating old facts.
The backend exposes read-only debug endpoints under the LangGraph API server:
GET /debug/memory
GET /debug/memory/items
GET /debug/memory/hits
GET /debug/memory/ranking
GET /debug/memory/sufficiency
GET /debug/memory/trace
Each endpoint accepts limit, for example:
curl "http://localhost:2024/debug/memory?limit=10"Current persistence guarantees:
itemsreads typed facts fromagent_memory_items.hitsreads recent retrieval hits fromagent_memory_hits.rankingis derived from hit order unless a dedicated ranking table or list function is added.traceis derived from hit records unless a dedicated trace table or list function is added.sufficiencyreturns persisted sufficiency decisions when a future table or list function exists; otherwise it returns an empty list.
The backend tools live in:
backend/src/agent/tools/
Current tools:
web_search: Tavily web searchfetch_url: fetch and extract readable text from web pagessearch_memory: search SQLite memorysearch_vector_memory: search Chroma semantic memorysave_memory: save final answer summaries to SQLitesave_vector_memory: save final answer summaries to Chromadedupe_sources: clean duplicate source URLs
Create backend/.env from backend/.env.example:
OPENAI_API_KEY=your_dashscope_api_key
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL=qwen-plus
EMBEDDING_MODEL=text-embedding-v4
SEARCH_PROVIDER=tavily
TAVILY_API_KEY=your_tavily_api_key
SEARCH_MAX_RESULTS=5
SEARCH_DEPTH=fast
FETCH_TOP_K=2
DATABASE_URL=sqlite:///./data/agent_memory.sqlite
VECTOR_DB_DIR=./data/chroma
VECTOR_COLLECTION=agent_memory
VECTOR_MEMORY_RESULTS=3
# VECTOR_MEMORY_MAX_DISTANCE=1.2
MEMORY_FIRST_ENABLED=true
MEMORY_FIRST_MIN_RECORDS=1
MEMORY_RERANK_TOP_K=3OPENAI_API_KEY is named this way because the code uses the OpenAI SDK compatibility format. For Qwen, put your DashScope API key there.
EMBEDDING_MODEL is used only for vector memory. The default text-embedding-v4 stores answer summaries in Chroma so later questions can retrieve semantically related memories even when they do not repeat the exact same words.
VECTOR_MEMORY_MAX_DISTANCE is optional. Leave it unset unless you have measured Chroma distances for your collection and want to filter weak semantic matches.
MEMORY_FIRST_ENABLED=true lets the backend answer from retrieved memory before running Tavily. This makes follow-up or repeated questions much faster. Set it to false when you always want fresh web research.
MEMORY_FIRST_MIN_RECORDS is the minimum number of ranked memories required before the backend can skip web search.
MEMORY_RERANK_TOP_K controls how many ranked memory facts are passed to answer generation.
The memory evaluation dataset lives at:
backend/tests/memory_eval_cases.json
Run it with:
cd backend
pytest tests/test_memory_eval.pyThe backend Makefile also supports passing a specific test file:
cd backend
make test TEST_FILE=tests/test_memory_eval.pyFor an end-to-end backend check with real providers, run the CLI researcher after creating backend/.env:
cd backend
pip install .
python examples/cli_research.pyFor repeat-memory testing, run the same prompt twice:
- The first run should use Tavily and then write answer memory, typed facts, and vector memory.
- The second run should show recent hits in
/debug/memory/hitsand may skip Tavily if the ranked memory is sufficient.
Use the debug endpoints to inspect the records created by each run.
Backend:
cd backend
pip install .
langgraph devFrontend:
cd frontend
npm install
npm run devOpen the frontend at:
http://localhost:5173/app
The LangGraph API runs at:
http://localhost:2024
The frontend still has the original model selector labels. The backend ignores old gemini-* selector values and uses MODEL from the environment instead. This keeps the first backend refactor focused and avoids changing the UI in the same step.