From 2219d82eb65b6c1b3de7a7b41375d18a28a5b264 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 31 Mar 2026 08:49:32 +0800 Subject: [PATCH] update model without structure output --- backend/examples/cli_research.py | 2 +- backend/src/agent/configuration.py | 6 +- backend/src/agent/graph.py | 75 +++++++++++++++++++- backend/src/agent/prompts.py | 4 +- frontend/src/components/ActivityTimeline.tsx | 4 +- frontend/src/components/ChatMessagesView.tsx | 8 +-- frontend/src/components/InputForm.tsx | 6 +- frontend/src/components/WelcomeScreen.tsx | 2 +- 8 files changed, 89 insertions(+), 18 deletions(-) diff --git a/backend/examples/cli_research.py b/backend/examples/cli_research.py index f41e94e..efc1580 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="GLM-4.5-Air", + default="GLM-4.7", help="Model for the final answer", ) args = parser.parse_args() diff --git a/backend/src/agent/configuration.py b/backend/src/agent/configuration.py index d1f3e52..8affba4 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="GLM-4.5-Air", + default="GLM-4.7", metadata={ "description": "The name of the language model to use for the agent's query generation." }, ) reflection_model: str = Field( - default="GLM-4.5-Air", + default="GLM-4.7", metadata={ "description": "The name of the language model to use for the agent's reflection." }, ) answer_model: str = Field( - default="GLM-4.5-Air", + default="GLM-4.7", 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 2b00029..d0956ac 100644 --- a/backend/src/agent/graph.py +++ b/backend/src/agent/graph.py @@ -1,7 +1,9 @@ import os +from typing import TypeVar from agent.tools_and_schemas import SearchQueryList, Reflection from dotenv import load_dotenv +from pydantic import BaseModel, ValidationError from langchain_core.messages import AIMessage from langgraph.types import Send from langgraph.graph import StateGraph @@ -38,6 +40,7 @@ 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")) +SchemaT = TypeVar("SchemaT", bound=BaseModel) def create_llm(model: str, temperature: float) -> ChatOpenAI: @@ -51,6 +54,63 @@ def create_llm(model: str, temperature: float) -> ChatOpenAI: ) +def _extract_json_payload(content: str) -> str: + """Extract a JSON object from a model response.""" + text = content.strip() + + if text.startswith("```"): + lines = text.splitlines() + if lines: + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + text = "\n".join(lines).strip() + + if text.startswith("json"): + text = text[4:].lstrip() + + return text + + +def _invoke_json_model( + llm: ChatOpenAI, + prompt: str, + schema: type[SchemaT], +) -> SchemaT: + """Invoke the model for JSON text and validate it with Pydantic.""" + result = llm.invoke(prompt) + content = result.content if isinstance(result.content, str) else str(result.content) + json_payload = _extract_json_payload(content) + + try: + return schema.model_validate_json(json_payload) + except ValidationError as exc: + raise ValueError( + f"Model returned invalid JSON for {schema.__name__}: {json_payload[:500]!r}" + ) from exc + + +def _supports_native_structured_output(model: str) -> bool: + """Return whether the selected model should use LangChain structured output.""" + normalized = model.strip().lower() + return normalized.startswith("glm-4.7") + + +def _invoke_structured_model( + llm: ChatOpenAI, + model: str, + prompt: str, + schema: type[SchemaT], +) -> SchemaT: + """Use native structured output when supported, otherwise fall back to JSON parsing.""" + if _supports_native_structured_output(model): + try: + return llm.with_structured_output(schema).invoke(prompt) + except ValueError: + return _invoke_json_model(llm, prompt, schema) + return _invoke_json_model(llm, prompt, schema) + + # Nodes def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerationState: """LangGraph node that generates search queries based on the User's question. @@ -71,7 +131,6 @@ def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerati state["initial_search_query_count"] = configurable.number_of_initial_queries llm = create_llm(configurable.query_generator_model, temperature=1.0) - structured_llm = llm.with_structured_output(SearchQueryList) current_date = get_current_date() formatted_prompt = query_writer_instructions.format( @@ -79,7 +138,12 @@ def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerati research_topic=get_research_topic(state["messages"]), number_queries=state["initial_search_query_count"], ) - result = structured_llm.invoke(formatted_prompt) + result = _invoke_structured_model( + llm, + configurable.query_generator_model, + formatted_prompt, + SearchQueryList, + ) return {"search_query": result.query} @@ -153,7 +217,12 @@ def reflection(state: OverallState, config: RunnableConfig) -> ReflectionState: summaries="\n\n---\n\n".join(state["web_research_result"]), ) llm = create_llm(reasoning_model, temperature=1.0) - result = llm.with_structured_output(Reflection).invoke(formatted_prompt) + result = _invoke_structured_model( + llm, + reasoning_model, + formatted_prompt, + Reflection, + ) return { "is_sufficient": result.is_sufficient, diff --git a/backend/src/agent/prompts.py b/backend/src/agent/prompts.py index 85ddb99..fd5cc2e 100644 --- a/backend/src/agent/prompts.py +++ b/backend/src/agent/prompts.py @@ -15,6 +15,7 @@ Instructions: - Queries should be diverse, if the topic is broad, generate more than 1 query. - Don't generate multiple similar queries, 1 is enough. - Query should ensure that the most current information is gathered. The current date is {current_date}. +- Return only valid JSON. Do not include markdown fences or any extra text. Format: - Format your response as a JSON object with ALL two of these exact keys: @@ -27,7 +28,7 @@ Topic: What revenue grew more last year apple stock or the number of people buyi ```json {{ "rationale": "To answer this comparative growth question accurately, we need specific data points on Apple's stock performance and iPhone sales metrics. These queries target the precise financial information needed: company revenue trends, product-specific unit sales figures, and stock price movement over the same fiscal period for direct comparison.", - "query": ["Apple total revenue growth fiscal year 2024", "iPhone unit sales growth fiscal year 2024", "Apple stock price growth fiscal year 2024"], + "query": ["Apple total revenue growth fiscal year 2024", "iPhone unit sales growth fiscal year 2024", "Apple stock price growth fiscal year 2024"] }} ``` @@ -57,6 +58,7 @@ Instructions: Requirements: - Ensure the follow-up query is self-contained and includes necessary context for web search. +- Return only valid JSON. Do not include markdown fences or any extra text. Output Format: - Format your response as a JSON object with these exact keys: diff --git a/frontend/src/components/ActivityTimeline.tsx b/frontend/src/components/ActivityTimeline.tsx index b366929..ff5feaa 100644 --- a/frontend/src/components/ActivityTimeline.tsx +++ b/frontend/src/components/ActivityTimeline.tsx @@ -59,7 +59,7 @@ export function ActivityTimeline({ }, [isLoading, processedEvents]); return ( - +
{!isTimelineCollapsed && ( - + {isLoading && processedEvents.length === 0 && (
diff --git a/frontend/src/components/ChatMessagesView.tsx b/frontend/src/components/ChatMessagesView.tsx index 1a245d8..0701365 100644 --- a/frontend/src/components/ChatMessagesView.tsx +++ b/frontend/src/components/ChatMessagesView.tsx @@ -187,9 +187,9 @@ const AiMessageBubble: React.FC = ({ const isLiveActivityForThisBubble = isLastMessage && isOverallLoading; return ( -
+
{activityForThisBubble && activityForThisBubble.length > 0 && ( -
+
+
{" "} {/* AI message row structure */}
{liveActivityEvents.length > 0 ? ( -
+
= ({ }) => { const [internalInputValue, setInternalInputValue] = useState(""); const [effort, setEffort] = useState("medium"); - const [model, setModel] = useState("GLM-4.5-Air"); + const [model, setModel] = useState("GLM-4.7"); const handleInternalSubmit = (e?: React.FormEvent) => { if (e) e.preventDefault(); @@ -136,11 +136,11 @@ export const InputForm: React.FC = ({
- GLM-4.5-Air + GLM-4.7
diff --git a/frontend/src/components/WelcomeScreen.tsx b/frontend/src/components/WelcomeScreen.tsx index c0e39a2..7e58ee2 100644 --- a/frontend/src/components/WelcomeScreen.tsx +++ b/frontend/src/components/WelcomeScreen.tsx @@ -33,7 +33,7 @@ export const WelcomeScreen: React.FC = ({ />

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

);