From 00ef16376726e6ffe433d55a52e629935b3a04dc Mon Sep 17 00:00:00 2001 From: Xin Wang <123@example.com> Date: Mon, 30 Mar 2026 23:44:48 +0800 Subject: [PATCH] Update project files --- README.md | 32 ++-- backend/examples/cli_research.py | 2 +- backend/pyproject.toml | 4 +- backend/src/agent/configuration.py | 6 +- backend/src/agent/graph.py | 108 +++++------- backend/src/agent/prompts.py | 6 +- backend/src/agent/utils.py | 197 +++++----------------- docker-compose.yml | 6 +- frontend/package-lock.json | 13 ++ frontend/src/components/InputForm.tsx | 22 +-- frontend/src/components/WelcomeScreen.tsx | 2 +- 11 files changed, 137 insertions(+), 261 deletions(-) diff --git a/README.md b/README.md index eef0356..747f07c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Gemini Fullstack LangGraph Quickstart +# OpenAI-Compatible Fullstack LangGraph Quickstart -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. +This project demonstrates a fullstack application using a React frontend and a LangGraph-powered backend agent. The agent performs comprehensive research on a user's query by dynamically generating search terms, querying the web with Tavily, 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 an OpenAI-compatible chat model. Gemini Fullstack LangGraph @@ -8,8 +8,8 @@ This project demonstrates a fullstack application using a React frontend and a L - 💬 Fullstack application with a React frontend and LangGraph backend. - 🧠 Powered by a LangGraph agent for advanced research and conversational AI. -- 🔍 Dynamic search query generation using Google Gemini models. -- 🌐 Integrated web research via Google Search API. +- 🔍 Dynamic search query generation using an OpenAI-compatible chat model. +- 🌐 Integrated web research via Tavily Search. - 🤔 Reflective reasoning to identify knowledge gaps and refine searches. - 📄 Generates answers with citations from gathered sources. - 🔄 Hot-reloading for both frontend and backend during development. @@ -29,10 +29,15 @@ Follow these steps to get the application running locally for development and te - Node.js and npm (or yarn/pnpm) - Python 3.11+ -- **`GEMINI_API_KEY`**: The backend agent requires a Google Gemini API key. +- **`OPENAI_API_KEY`**: The backend agent requires an OpenAI-compatible API key. +- **`OPENAI_BASE_URL`**: Set this to `https://open.bigmodel.cn/api/paas/v4` for BigModel. +- **`TAVILY_API_KEY`**: Required for web search. 1. Navigate to the `backend/` directory. 2. Create a file named `.env` by copying the `backend/.env.example` file. - 3. Open the `.env` file and add your Gemini API key: `GEMINI_API_KEY="YOUR_ACTUAL_API_KEY"` + 3. Open the `.env` file and add: + `OPENAI_API_KEY="YOUR_ACTUAL_API_KEY"` + `OPENAI_BASE_URL="https://open.bigmodel.cn/api/paas/v4"` + `TAVILY_API_KEY="YOUR_ACTUAL_TAVILY_API_KEY"` **2. Install Dependencies:** @@ -67,11 +72,11 @@ The core of the backend is a LangGraph agent defined in `backend/src/agent/graph 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. -3. **Reflection & Knowledge Gap Analysis:** The agent analyzes the search results to determine if the information is sufficient or if there are knowledge gaps. It uses a Gemini model for this reflection process. +1. **Generate Initial Queries:** Based on your input, it generates a set of initial search queries using the configured chat model. +2. **Web Research:** For each query, it uses Tavily Search to gather relevant web pages and snippets. +3. **Reflection & Knowledge Gap Analysis:** The agent analyzes the search results to determine if the information is sufficient or if there are knowledge gaps. It uses the configured chat model for this reflection process. 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. +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 the configured chat model. ## CLI Example @@ -97,12 +102,12 @@ _Note: If you are not running the docker-compose.yml example or exposing the bac Run the following command from the **project root directory**: ```bash - docker build -t gemini-fullstack-langgraph -f Dockerfile . + docker build -t openai-fullstack-langgraph -f Dockerfile . ``` **2. Run the Production Server:** ```bash - GEMINI_API_KEY= LANGSMITH_API_KEY= docker-compose up + OPENAI_API_KEY= OPENAI_BASE_URL=https://open.bigmodel.cn/api/paas/v4 TAVILY_API_KEY= LANGSMITH_API_KEY= docker-compose up ``` Open your browser and navigate to `http://localhost:8123/app/` to see the application. The API will be available at `http://localhost:8123`. @@ -113,7 +118,8 @@ Open your browser and navigate to `http://localhost:8123/app/` to see the applic - [Tailwind CSS](https://tailwindcss.com/) - For styling. - [Shadcn UI](https://ui.shadcn.com/) - For components. - [LangGraph](https://github.com/langchain-ai/langgraph) - For building the backend research agent. -- [Google Gemini](https://ai.google.dev/models/gemini) - LLM for query generation, reflection, and answer synthesis. +- OpenAI-compatible chat model - LLM for query generation, reflection, and answer synthesis. +- [Tavily](https://tavily.com/) - Web search for research retrieval. ## License diff --git a/backend/examples/cli_research.py b/backend/examples/cli_research.py index a086496..f41e94e 100644 --- a/backend/examples/cli_research.py +++ b/backend/examples/cli_research.py @@ -21,7 +21,7 @@ def main() -> None: ) parser.add_argument( "--reasoning-model", - default="gemini-2.5-pro-preview-05-06", + default="GLM-4.5-Air", help="Model for the final answer", ) args = parser.parse_args() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 09eb598..9a14e58 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -11,13 +11,13 @@ requires-python = ">=3.11,<4.0" dependencies = [ "langgraph>=0.2.6", "langchain>=0.3.19", - "langchain-google-genai", + "langchain-openai", "python-dotenv>=1.0.1", "langgraph-sdk>=0.1.57", "langgraph-cli", "langgraph-api", "fastapi", - "google-genai", + "tavily-python", ] diff --git a/backend/src/agent/configuration.py b/backend/src/agent/configuration.py index e57122d..d1f3e52 100644 --- a/backend/src/agent/configuration.py +++ b/backend/src/agent/configuration.py @@ -9,21 +9,21 @@ class Configuration(BaseModel): """The configuration for the agent.""" query_generator_model: str = Field( - default="gemini-2.0-flash", + default="GLM-4.5-Air", metadata={ "description": "The name of the language model to use for the agent's query generation." }, ) reflection_model: str = Field( - default="gemini-2.5-flash", + default="GLM-4.5-Air", metadata={ "description": "The name of the language model to use for the agent's reflection." }, ) answer_model: str = Field( - default="gemini-2.5-pro", + default="GLM-4.5-Air", 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 0f19c3f..2b00029 100644 --- a/backend/src/agent/graph.py +++ b/backend/src/agent/graph.py @@ -7,7 +7,8 @@ from langgraph.types import Send from langgraph.graph import StateGraph from langgraph.graph import START, END from langchain_core.runnables import RunnableConfig -from google.genai import Client +from langchain_openai import ChatOpenAI +from tavily import TavilyClient from agent.state import ( OverallState, @@ -23,29 +24,38 @@ from agent.prompts import ( reflection_instructions, answer_instructions, ) -from langchain_google_genai import ChatGoogleGenerativeAI from agent.utils import ( - get_citations, + deduplicate_and_format_sources, get_research_topic, - insert_citation_markers, - resolve_urls, ) load_dotenv() -if os.getenv("GEMINI_API_KEY") is None: - raise ValueError("GEMINI_API_KEY is not set") +if os.getenv("OPENAI_API_KEY") is None: + raise ValueError("OPENAI_API_KEY is not set") -# Used for Google Search API -genai_client = Client(api_key=os.getenv("GEMINI_API_KEY")) +if os.getenv("TAVILY_API_KEY") is None: + raise ValueError("TAVILY_API_KEY is not set") + +tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) + + +def create_llm(model: str, temperature: float) -> ChatOpenAI: + """Create an OpenAI-compatible chat client.""" + return ChatOpenAI( + model=model, + temperature=temperature, + max_retries=2, + api_key=os.getenv("OPENAI_API_KEY"), + base_url=os.getenv("OPENAI_BASE_URL", "https://open.bigmodel.cn/api/paas/v4"), + ) # Nodes def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerationState: """LangGraph node that generates search queries based on the User's question. - Uses Gemini 2.0 Flash to create an optimized search queries for web research based on - the User's question. + Uses the configured chat model to create optimized search queries for web research. Args: state: Current graph state containing the User's question @@ -56,27 +66,19 @@ def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerati """ configurable = Configuration.from_runnable_config(config) - # check for custom initial search query count + # Check for custom initial search query count. if state.get("initial_search_query_count") is None: state["initial_search_query_count"] = configurable.number_of_initial_queries - # init Gemini 2.0 Flash - llm = ChatGoogleGenerativeAI( - model=configurable.query_generator_model, - temperature=1.0, - max_retries=2, - api_key=os.getenv("GEMINI_API_KEY"), - ) + llm = create_llm(configurable.query_generator_model, temperature=1.0) structured_llm = llm.with_structured_output(SearchQueryList) - # Format the prompt current_date = get_current_date() formatted_prompt = query_writer_instructions.format( current_date=current_date, research_topic=get_research_topic(state["messages"]), number_queries=state["initial_search_query_count"], ) - # Generate the search queries result = structured_llm.invoke(formatted_prompt) return {"search_query": result.query} @@ -93,46 +95,34 @@ def continue_to_web_research(state: QueryGenerationState): def web_research(state: WebSearchState, config: RunnableConfig) -> OverallState: - """LangGraph node that performs web research using the native Google Search API tool. - - Executes a web search using the native Google Search API tool in combination with Gemini 2.0 Flash. - - Args: - state: Current graph state containing the search query and research loop count - config: Configuration for the runnable, including search API settings - - Returns: - Dictionary with state update, including sources_gathered, research_loop_count, and web_research_results - """ - # Configure - configurable = Configuration.from_runnable_config(config) + """LangGraph node that performs web research using Tavily search.""" + Configuration.from_runnable_config(config) formatted_prompt = web_searcher_instructions.format( current_date=get_current_date(), research_topic=state["search_query"], ) + search_response = tavily_client.search( + query=state["search_query"], + topic="general", + search_depth="advanced", + max_results=5, + include_answer=False, + include_raw_content=False, + ) + sources_gathered, formatted_results = deduplicate_and_format_sources( + search_response.get("results", []), int(state["id"]) + ) - # Uses the google genai client as the langchain client doesn't return grounding metadata - response = genai_client.models.generate_content( - model=configurable.query_generator_model, - contents=formatted_prompt, - config={ - "tools": [{"google_search": {}}], - "temperature": 0, - }, + web_research_result = ( + f"{formatted_prompt}\n\n" + f"Search query: {state['search_query']}\n\n" + f"Findings:\n{formatted_results}" ) - # resolve the urls to short urls for saving tokens and time - resolved_urls = resolve_urls( - response.candidates[0].grounding_metadata.grounding_chunks, state["id"] - ) - # Gets the citations and adds them to the generated text - citations = get_citations(response, resolved_urls) - modified_text = insert_citation_markers(response.text, citations) - sources_gathered = [item for citation in citations for item in citation["segments"]] return { "sources_gathered": sources_gathered, "search_query": [state["search_query"]], - "web_research_result": [modified_text], + "web_research_result": [web_research_result], } @@ -162,13 +152,7 @@ def reflection(state: OverallState, config: RunnableConfig) -> ReflectionState: research_topic=get_research_topic(state["messages"]), summaries="\n\n---\n\n".join(state["web_research_result"]), ) - # init Reasoning Model - llm = ChatGoogleGenerativeAI( - model=reasoning_model, - temperature=1.0, - max_retries=2, - api_key=os.getenv("GEMINI_API_KEY"), - ) + llm = create_llm(reasoning_model, temperature=1.0) result = llm.with_structured_output(Reflection).invoke(formatted_prompt) return { @@ -241,13 +225,7 @@ def finalize_answer(state: OverallState, config: RunnableConfig): summaries="\n---\n\n".join(state["web_research_result"]), ) - # init Reasoning Model, default to Gemini 2.5 Flash - llm = ChatGoogleGenerativeAI( - model=reasoning_model, - temperature=0, - max_retries=2, - api_key=os.getenv("GEMINI_API_KEY"), - ) + llm = create_llm(reasoning_model, temperature=0) result = llm.invoke(formatted_prompt) # Replace the short urls with the original urls and add all used urls to the sources_gathered diff --git a/backend/src/agent/prompts.py b/backend/src/agent/prompts.py index 8963f6a..85ddb99 100644 --- a/backend/src/agent/prompts.py +++ b/backend/src/agent/prompts.py @@ -34,11 +34,11 @@ Topic: What revenue grew more last year apple stock or the number of people buyi Context: {research_topic}""" -web_searcher_instructions = """Conduct targeted Google Searches to gather the most recent, credible information on "{research_topic}" and synthesize it into a verifiable text artifact. +web_searcher_instructions = """Conduct targeted web research to gather the most recent, credible information on "{research_topic}" and synthesize it into a verifiable text artifact. Instructions: - Query should ensure that the most current information is gathered. The current date is {current_date}. -- Conduct multiple, diverse searches to gather comprehensive information. +- Review the returned web search results and extract the strongest, most relevant findings. - Consolidate key findings while meticulously tracking the source(s) for each specific piece of information. - The output should be a well-written summary or report based on your search findings. - Only include the information found in the search results, don't make up any information. @@ -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. -- 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. +- Include the sources you used from the Summaries in the answer correctly, using markdown links. THIS IS A MUST. User Context: - {research_topic} diff --git a/backend/src/agent/utils.py b/backend/src/agent/utils.py index d02c8d9..64e5086 100644 --- a/backend/src/agent/utils.py +++ b/backend/src/agent/utils.py @@ -1,166 +1,59 @@ -from typing import Any, Dict, List -from langchain_core.messages import AnyMessage, AIMessage, HumanMessage +from typing import Any + +from langchain_core.messages import AIMessage, AnyMessage, HumanMessage -def get_research_topic(messages: List[AnyMessage]) -> str: - """ - Get the research topic from the messages. - """ - # check if request has a history and combine the messages into a single string +def get_research_topic(messages: list[AnyMessage]) -> str: + """Get the research topic from the messages.""" if len(messages) == 1: - research_topic = messages[-1].content - else: - research_topic = "" - for message in messages: - if isinstance(message, HumanMessage): - research_topic += f"User: {message.content}\n" - elif isinstance(message, AIMessage): - research_topic += f"Assistant: {message.content}\n" + return str(messages[-1].content) + + research_topic = "" + for message in messages: + if isinstance(message, HumanMessage): + research_topic += f"User: {message.content}\n" + elif isinstance(message, AIMessage): + research_topic += f"Assistant: {message.content}\n" return research_topic -def resolve_urls(urls_to_resolve: List[Any], id: int) -> Dict[str, str]: - """ - Create a map of the vertex ai search urls (very long) to a short url with a unique id for each url. - Ensures each original URL gets a consistent shortened form while maintaining uniqueness. - """ - prefix = f"https://vertexaisearch.cloud.google.com/id/" - urls = [site.web.uri for site in urls_to_resolve] +def deduplicate_and_format_sources( + results: list[dict[str, Any]], search_id: int +) -> tuple[list[dict[str, str]], str]: + """Convert Tavily results into source metadata and a markdown research summary.""" + sources: list[dict[str, str]] = [] + formatted_sections: list[str] = [] + seen_urls: set[str] = set() - # Create a dictionary that maps each unique URL to its first occurrence index - resolved_map = {} - for idx, url in enumerate(urls): - if url not in resolved_map: - resolved_map[url] = f"{prefix}{id}-{idx}" + for idx, result in enumerate(results): + url = result.get("url") + if not url or url in seen_urls: + continue - return resolved_map + seen_urls.add(url) + title = result.get("title") or f"Source {idx + 1}" + short_url = f"https://search.local/{search_id}-{idx}" + content = (result.get("content") or "").strip() - -def insert_citation_markers(text, citations_list): - """ - Inserts citation markers into a text string based on start and end indices. - - Args: - text (str): The original text string. - citations_list (list): A list of dictionaries, where each dictionary - contains 'start_index', 'end_index', and - 'segment_string' (the marker to insert). - Indices are assumed to be for the original text. - - Returns: - str: The text with citation markers inserted. - """ - # Sort citations by end_index in descending order. - # If end_index is the same, secondary sort by start_index descending. - # This ensures that insertions at the end of the string don't affect - # the indices of earlier parts of the string that still need to be processed. - sorted_citations = sorted( - citations_list, key=lambda c: (c["end_index"], c["start_index"]), reverse=True - ) - - modified_text = text - for citation_info in sorted_citations: - # These indices refer to positions in the *original* text, - # but since we iterate from the end, they remain valid for insertion - # relative to the parts of the string already processed. - end_idx = citation_info["end_index"] - marker_to_insert = "" - for segment in citation_info["segments"]: - marker_to_insert += f" [{segment['label']}]({segment['short_url']})" - # Insert the citation marker at the original end_idx position - modified_text = ( - modified_text[:end_idx] + marker_to_insert + modified_text[end_idx:] + sources.append( + { + "label": title, + "short_url": short_url, + "value": url, + } ) - return modified_text - - -def get_citations(response, resolved_urls_map): - """ - Extracts and formats citation information from a Gemini model's response. - - This function processes the grounding metadata provided in the response to - construct a list of citation objects. Each citation object includes the - start and end indices of the text segment it refers to, and a string - containing formatted markdown links to the supporting web chunks. - - Args: - response: The response object from the Gemini model, expected to have - a structure including `candidates[0].grounding_metadata`. - It also relies on a `resolved_map` being available in its - scope to map chunk URIs to resolved URLs. - - Returns: - list: A list of dictionaries, where each dictionary represents a citation - and has the following keys: - - "start_index" (int): The starting character index of the cited - segment in the original text. Defaults to 0 - if not specified. - - "end_index" (int): The character index immediately after the - end of the cited segment (exclusive). - - "segments" (list[str]): A list of individual markdown-formatted - links for each grounding chunk. - - "segment_string" (str): A concatenated string of all markdown- - formatted links for the citation. - Returns an empty list if no valid candidates or grounding supports - are found, or if essential data is missing. - """ - citations = [] - - # Ensure response and necessary nested structures are present - if not response or not response.candidates: - return citations - - candidate = response.candidates[0] - if ( - not hasattr(candidate, "grounding_metadata") - or not candidate.grounding_metadata - or not hasattr(candidate.grounding_metadata, "grounding_supports") - ): - return citations - - for support in candidate.grounding_metadata.grounding_supports: - citation = {} - - # Ensure segment information is present - if not hasattr(support, "segment") or support.segment is None: - continue # Skip this support if segment info is missing - - start_index = ( - support.segment.start_index - if support.segment.start_index is not None - else 0 + formatted_sections.append( + "\n".join( + [ + f"- {title} [{title}]({short_url})", + f" URL: {url}", + f" Snippet: {content or 'No snippet returned.'}", + ] + ) ) - # Ensure end_index is present to form a valid segment - if support.segment.end_index is None: - continue # Skip if end_index is missing, as it's crucial + if not formatted_sections: + formatted_sections.append("- No Tavily results were returned for this query.") - # Add 1 to end_index to make it an exclusive end for slicing/range purposes - # (assuming the API provides an inclusive end_index) - citation["start_index"] = start_index - citation["end_index"] = support.segment.end_index - - citation["segments"] = [] - if ( - hasattr(support, "grounding_chunk_indices") - and support.grounding_chunk_indices - ): - for ind in support.grounding_chunk_indices: - try: - chunk = candidate.grounding_metadata.grounding_chunks[ind] - resolved_url = resolved_urls_map.get(chunk.web.uri, None) - citation["segments"].append( - { - "label": chunk.web.title.split(".")[:-1][0], - "short_url": resolved_url, - "value": chunk.web.uri, - } - ) - except (IndexError, AttributeError, NameError): - # Handle cases where chunk, web, uri, or resolved_map might be problematic - # For simplicity, we'll just skip adding this particular segment link - # In a production system, you might want to log this. - pass - citations.append(citation) - return citations + return sources, "\n".join(formatted_sections) diff --git a/docker-compose.yml b/docker-compose.yml index 6cec5c0..e57a1e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: retries: 5 interval: 5s langgraph-api: - image: gemini-fullstack-langgraph + image: openai-fullstack-langgraph container_name: langgraph-api ports: - "8123:8000" @@ -38,7 +38,9 @@ services: langgraph-postgres: condition: service_healthy environment: - GEMINI_API_KEY: ${GEMINI_API_KEY} + OPENAI_API_KEY: ${OPENAI_API_KEY} + OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://open.bigmodel.cn/api/paas/v4} + TAVILY_API_KEY: ${TAVILY_API_KEY} LANGSMITH_API_KEY: ${LANGSMITH_API_KEY} REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4859909..8b996e9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2259,6 +2259,7 @@ "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2268,6 +2269,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2278,6 +2280,7 @@ "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2336,6 +2339,7 @@ "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/types": "8.31.1", @@ -2531,6 +2535,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2998,6 +3003,7 @@ "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4879,6 +4885,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4888,6 +4895,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5345,6 +5353,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5439,6 +5448,7 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5663,6 +5673,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5751,6 +5762,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5802,6 +5814,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/components/InputForm.tsx b/frontend/src/components/InputForm.tsx index 97aa5c6..1af99ba 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("GLM-4.5-Air"); const handleInternalSubmit = (e?: React.FormEvent) => { if (e) e.preventDefault(); @@ -136,27 +136,11 @@ export const InputForm: React.FC = ({
- 2.0 Flash -
-
- -
- 2.5 Flash -
-
- -
- 2.5 Pro + GLM-4.5-Air
diff --git a/frontend/src/components/WelcomeScreen.tsx b/frontend/src/components/WelcomeScreen.tsx index b1015aa..c0e39a2 100644 --- a/frontend/src/components/WelcomeScreen.tsx +++ b/frontend/src/components/WelcomeScreen.tsx @@ -33,7 +33,7 @@ export const WelcomeScreen: React.FC = ({ />

- Powered by Google Gemini and LangChain LangGraph. + Powered by GLM-4.5-Air, Tavily, and LangChain LangGraph.

);