Error
+{JSON.stringify(error)}
+ + +
## Features
@@ -12,7 +12,7 @@ This project demonstrates a fullstack application using a React frontend and a L
- 🌐 Integrated web research via Google Search API.
- 🤔 Reflective reasoning to identify knowledge gaps and refine searches.
- 📄 Generates answers with citations from gathered sources.
-- 🔄 Hot-reloading for both frontend and backend development.
+- 🔄 Hot-reloading for both frontend and backend during development.
## Project Structure
@@ -28,7 +28,7 @@ Follow these steps to get the application running locally for development and te
**1. Prerequisites:**
- Node.js and npm (or yarn/pnpm)
-- Python 3.8+
+- Python 3.11+
- **`GEMINI_API_KEY`**: The backend agent requires a Google Gemini API key.
1. Navigate to the `backend/` directory.
2. Create a file named `.env` by copying the `backend/.env.example` file.
@@ -65,7 +65,7 @@ _Alternatively, you can run the backend and frontend development servers separat
The core of the backend is a LangGraph agent defined in `backend/src/agent/graph.py`. It follows these steps:
-
+
1. **Generate Initial Queries:** Based on your input, it generates a set of initial search queries using a Gemini model.
2. **Web Research:** For each query, it uses the Gemini model with the Google Search API to find relevant web pages.
@@ -73,13 +73,25 @@ The core of the backend is a LangGraph agent defined in `backend/src/agent/graph
4. **Iterative Refinement:** If gaps are found or the information is insufficient, it generates follow-up queries and repeats the web research and reflection steps (up to a configured maximum number of loops).
5. **Finalize Answer:** Once the research is deemed sufficient, the agent synthesizes the gathered information into a coherent answer, including citations from the web sources, using a Gemini model.
+## CLI Example
+
+For quick one-off questions you can execute the agent from the command line. The
+script `backend/examples/cli_research.py` runs the LangGraph agent and prints the
+final answer:
+
+```bash
+cd backend
+python examples/cli_research.py "What are the latest trends in renewable energy?"
+```
+
+
## Deployment
In production, the backend server serves the optimized static frontend build. LangGraph requires a Redis instance and a Postgres database. Redis is used as a pub-sub broker to enable streaming real time output from background runs. Postgres is used to store assistants, threads, runs, persist thread state and long term memory, and to manage the state of the background task queue with 'exactly once' semantics. For more details on how to deploy the backend server, take a look at the [LangGraph Documentation](https://langchain-ai.github.io/langgraph/concepts/deployment_options/). Below is an example of how to build a Docker image that includes the optimized frontend build and the backend server and run it via `docker-compose`.
_Note: For the docker-compose.yml example you need a LangSmith API key, you can get one from [LangSmith](https://smith.langchain.com/settings)._
-_Note: If you are not running the docker-compose.yml example or exposing the backend server to the public internet, you update the `apiUrl` in the `frontend/src/App.tsx` file your host. Currently the `apiUrl` is set to `http://localhost:8123` for docker-compose or `http://localhost:2024` for development._
+_Note: If you are not running the docker-compose.yml example or exposing the backend server to the public internet, you should update the `apiUrl` in the `frontend/src/App.tsx` file to your host. Currently the `apiUrl` is set to `http://localhost:8123` for docker-compose or `http://localhost:2024` for development._
**1. Build the Docker Image:**
diff --git a/backend/examples/cli_research.py b/backend/examples/cli_research.py
new file mode 100644
index 0000000..a086496
--- /dev/null
+++ b/backend/examples/cli_research.py
@@ -0,0 +1,43 @@
+import argparse
+from langchain_core.messages import HumanMessage
+from agent.graph import graph
+
+
+def main() -> None:
+ """Run the research agent from the command line."""
+ parser = argparse.ArgumentParser(description="Run the LangGraph research agent")
+ parser.add_argument("question", help="Research question")
+ parser.add_argument(
+ "--initial-queries",
+ type=int,
+ default=3,
+ help="Number of initial search queries",
+ )
+ parser.add_argument(
+ "--max-loops",
+ type=int,
+ default=2,
+ help="Maximum number of research loops",
+ )
+ parser.add_argument(
+ "--reasoning-model",
+ default="gemini-2.5-pro-preview-05-06",
+ help="Model for the final answer",
+ )
+ args = parser.parse_args()
+
+ state = {
+ "messages": [HumanMessage(content=args.question)],
+ "initial_search_query_count": args.initial_queries,
+ "max_research_loops": args.max_loops,
+ "reasoning_model": args.reasoning_model,
+ }
+
+ result = graph.invoke(state)
+ messages = result.get("messages", [])
+ if messages:
+ print(messages[-1].content)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/src/agent/app.py b/backend/src/agent/app.py
index 090e2ed..f20f6ed 100644
--- a/backend/src/agent/app.py
+++ b/backend/src/agent/app.py
@@ -1,8 +1,7 @@
# mypy: disable - error - code = "no-untyped-def,misc"
import pathlib
-from fastapi import FastAPI, Request, Response
+from fastapi import FastAPI, Response
from fastapi.staticfiles import StaticFiles
-import fastapi.exceptions
# Define the FastAPI app
app = FastAPI()
@@ -18,7 +17,6 @@ def create_frontend_router(build_dir="../frontend/dist"):
A Starlette application serving the frontend.
"""
build_path = pathlib.Path(__file__).parent.parent.parent / build_dir
- static_files_path = build_path / "assets" # Vite uses 'assets' subdir
if not build_path.is_dir() or not (build_path / "index.html").is_file():
print(
@@ -36,21 +34,7 @@ def create_frontend_router(build_dir="../frontend/dist"):
return Route("/{path:path}", endpoint=dummy_frontend)
- build_dir = pathlib.Path(build_dir)
-
- react = FastAPI(openapi_url="")
- react.mount(
- "/assets", StaticFiles(directory=static_files_path), name="static_assets"
- )
-
- @react.get("/{path:path}")
- async def handle_catch_all(request: Request, path: str):
- fp = build_path / path
- if not fp.exists() or not fp.is_file():
- fp = build_path / "index.html"
- return fastapi.responses.FileResponse(fp)
-
- return react
+ return StaticFiles(directory=build_path, html=True)
# Mount the frontend under /app to not conflict with the LangGraph API routes
diff --git a/backend/src/agent/configuration.py b/backend/src/agent/configuration.py
index 6256dee..e57122d 100644
--- a/backend/src/agent/configuration.py
+++ b/backend/src/agent/configuration.py
@@ -16,14 +16,14 @@ class Configuration(BaseModel):
)
reflection_model: str = Field(
- default="gemini-2.5-flash-preview-04-17",
+ default="gemini-2.5-flash",
metadata={
"description": "The name of the language model to use for the agent's reflection."
},
)
answer_model: str = Field(
- default="gemini-2.5-pro-preview-05-06",
+ default="gemini-2.5-pro",
metadata={
"description": "The name of the language model to use for the agent's answer."
},
diff --git a/backend/src/agent/graph.py b/backend/src/agent/graph.py
index dae64b7..0f19c3f 100644
--- a/backend/src/agent/graph.py
+++ b/backend/src/agent/graph.py
@@ -42,9 +42,9 @@ genai_client = Client(api_key=os.getenv("GEMINI_API_KEY"))
# Nodes
def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerationState:
- """LangGraph node that generates a search queries based on the User's question.
+ """LangGraph node that generates search queries based on the User's question.
- Uses Gemini 2.0 Flash to create an optimized search query for web research based on
+ Uses Gemini 2.0 Flash to create an optimized search queries for web research based on
the User's question.
Args:
@@ -52,7 +52,7 @@ def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerati
config: Configuration for the runnable, including LLM provider settings
Returns:
- Dictionary with state update, including search_query key containing the generated query
+ Dictionary with state update, including search_query key containing the generated queries
"""
configurable = Configuration.from_runnable_config(config)
@@ -78,7 +78,7 @@ def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerati
)
# Generate the search queries
result = structured_llm.invoke(formatted_prompt)
- return {"query_list": result.query}
+ return {"search_query": result.query}
def continue_to_web_research(state: QueryGenerationState):
@@ -88,7 +88,7 @@ def continue_to_web_research(state: QueryGenerationState):
"""
return [
Send("web_research", {"search_query": search_query, "id": int(idx)})
- for idx, search_query in enumerate(state["query_list"])
+ for idx, search_query in enumerate(state["search_query"])
]
@@ -153,7 +153,7 @@ def reflection(state: OverallState, config: RunnableConfig) -> ReflectionState:
configurable = Configuration.from_runnable_config(config)
# Increment the research loop count and get the reasoning model
state["research_loop_count"] = state.get("research_loop_count", 0) + 1
- reasoning_model = state.get("reasoning_model") or configurable.reasoning_model
+ reasoning_model = state.get("reasoning_model", configurable.reflection_model)
# Format the prompt
current_date = get_current_date()
@@ -231,7 +231,7 @@ def finalize_answer(state: OverallState, config: RunnableConfig):
Dictionary with state update, including running_summary key containing the formatted final summary with sources
"""
configurable = Configuration.from_runnable_config(config)
- reasoning_model = state.get("reasoning_model") or configurable.reasoning_model
+ reasoning_model = state.get("reasoning_model") or configurable.answer_model
# Format the prompt
current_date = get_current_date()
diff --git a/backend/src/agent/prompts.py b/backend/src/agent/prompts.py
index d8fd3b9..75857e0 100644
--- a/backend/src/agent/prompts.py
+++ b/backend/src/agent/prompts.py
@@ -17,7 +17,7 @@ Instructions:
- Query should ensure that the most current information is gathered. The current date is {current_date}.
Format:
-- Format your response as a JSON object with ALL three of these exact keys:
+- Format your response as a JSON object with ALL two of these exact keys:
- "rationale": Brief explanation of why these queries are relevant
- "query": A list of search queries
@@ -87,7 +87,7 @@ Instructions:
- You have access to all the information gathered from the previous steps.
- You have access to the user's question.
- Generate a high-quality answer to the user's question based on the provided summaries and the user's question.
-- you MUST include all the citations from the summaries in the answer correctly.
+- You MUST include all the citations from the summaries in the answer correctly.
User Context:
- {research_topic}
diff --git a/backend/src/agent/state.py b/backend/src/agent/state.py
index 5a45fb4..d5ad4dc 100644
--- a/backend/src/agent/state.py
+++ b/backend/src/agent/state.py
@@ -8,8 +8,6 @@ from typing_extensions import Annotated
import operator
-from dataclasses import dataclass, field
-from typing_extensions import Annotated
class OverallState(TypedDict):
@@ -37,7 +35,7 @@ class Query(TypedDict):
class QueryGenerationState(TypedDict):
- query_list: list[Query]
+ search_query: list[Query]
class WebSearchState(TypedDict):
diff --git a/docker-compose.yml b/docker-compose.yml
index f348347..6cec5c0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,6 +4,7 @@ volumes:
services:
langgraph-redis:
image: docker.io/redis:6
+ container_name: langgraph-redis
healthcheck:
test: redis-cli ping
interval: 5s
@@ -11,6 +12,7 @@ services:
retries: 5
langgraph-postgres:
image: docker.io/postgres:16
+ container_name: langgraph-postgres
ports:
- "5433:5432"
environment:
@@ -27,6 +29,7 @@ services:
interval: 5s
langgraph-api:
image: gemini-fullstack-langgraph
+ container_name: langgraph-api
ports:
- "8123:8000"
depends_on:
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 6e68e50..d06d402 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
import { ProcessedEvent } from "@/components/ActivityTimeline";
import { WelcomeScreen } from "@/components/WelcomeScreen";
import { ChatMessagesView } from "@/components/ChatMessagesView";
+import { Button } from "@/components/ui/button";
export default function App() {
const [processedEventsTimeline, setProcessedEventsTimeline] = useState<
@@ -14,7 +15,7 @@ export default function App() {
>({});
const scrollAreaRef = useRef{JSON.stringify(error)}
+ + +