diff --git a/README.md b/README.md index 275a8d8c..eef0356b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This project demonstrates a fullstack application using a React frontend and a LangGraph-powered backend agent. The agent is designed to perform comprehensive research on a user's query by dynamically generating search terms, querying the web using Google Search, reflecting on the results to identify knowledge gaps, and iteratively refining its search until it can provide a well-supported answer with citations. This application serves as an example of building research-augmented conversational AI using LangGraph and Google's Gemini models. -![Gemini Fullstack LangGraph](./app.png) +Gemini Fullstack LangGraph ## 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 during 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: -![Agent Flow](./agent.png) +Agent Flow 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:** @@ -105,4 +117,4 @@ Open your browser and navigate to `http://localhost:8123/app/` to see the applic ## License -This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. diff --git a/backend/examples/cli_research.py b/backend/examples/cli_research.py new file mode 100644 index 00000000..a086496b --- /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/graph.py b/backend/src/agent/graph.py index 2d4f9d92..0f19c3f2 100644 --- a/backend/src/agent/graph.py +++ b/backend/src/agent/graph.py @@ -42,9 +42,9 @@ # 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) diff --git a/backend/src/agent/prompts.py b/backend/src/agent/prompts.py index d8fd3b9a..8963f6a6 100644 --- a/backend/src/agent/prompts.py +++ b/backend/src/agent/prompts.py @@ -17,7 +17,7 @@ def get_current_date(): - 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 @@ def get_current_date(): - 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. +- Include the sources you used from the Summaries in the answer correctly, use markdown format (e.g. [apnews](https://vertexaisearch.cloud.google.com/id/1-0)). THIS IS A MUST. User Context: - {research_topic} diff --git a/backend/src/agent/state.py b/backend/src/agent/state.py index 6644c19c..d5ad4dcd 100644 --- a/backend/src/agent/state.py +++ b/backend/src/agent/state.py @@ -8,8 +8,6 @@ import operator -from dataclasses import dataclass, field -from typing_extensions import Annotated class OverallState(TypedDict): diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 484eefa9..d06d4021 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -151,12 +151,7 @@ export default function App() { return (
-
-
+
{thread.messages.length === 0 ? ( )} -
); diff --git a/frontend/src/components/ChatMessagesView.tsx b/frontend/src/components/ChatMessagesView.tsx index cc0e2b0c..1a245d88 100644 --- a/frontend/src/components/ChatMessagesView.tsx +++ b/frontend/src/components/ChatMessagesView.tsx @@ -253,9 +253,9 @@ export function ChatMessagesView({ } }; return ( -
- -
+
+ +
{messages.map((message, index) => { const isLast = index === messages.length - 1; return ( diff --git a/frontend/src/components/InputForm.tsx b/frontend/src/components/InputForm.tsx index b6ac1eac..ba23f7da 100644 --- a/frontend/src/components/InputForm.tsx +++ b/frontend/src/components/InputForm.tsx @@ -26,7 +26,7 @@ export const InputForm: React.FC = ({ }) => { const [internalInputValue, setInternalInputValue] = useState(""); const [effort, setEffort] = useState("medium"); - const [model, setModel] = useState("gemini-2.5-flash-preview-04-17"); + const [model, setModel] = useState("gemini-2.5-flash"); const handleInternalSubmit = (e?: React.FormEvent) => { if (e) e.preventDefault(); @@ -35,10 +35,9 @@ export const InputForm: React.FC = ({ setInternalInputValue(""); }; - const handleInternalKeyDown = ( - e: React.KeyboardEvent - ) => { - if (e.key === "Enter" && !e.shiftKey) { + const handleKeyDown = (e: React.KeyboardEvent) => { + // Submit with Ctrl+Enter (Windows/Linux) or Cmd+Enter (Mac) + if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); handleInternalSubmit(); } @@ -59,9 +58,9 @@ export const InputForm: React.FC = ({