From 341cde4d4727d3da20dbf3567e2dd62db996e590 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 07:51:04 +0000 Subject: [PATCH] feat: Implement search mode selector and agent personas - Adds a new UI component in the chat panel to allow users to select a search mode (Standard, Geospatial, or Web Search). - The selected mode is passed to the AI agent with each request. - The researcher agent now adopts a "persona" based on the selected mode, which dynamically adjusts its system prompt and restricts its available tools to improve relevance and accuracy. - When the geospatial tool returns multiple locations, they are now rendered as a list of clickable "fly to" links in the chat, allowing the user to navigate the map to each location. --- app/actions.tsx | 5 +- components/chat-panel.tsx | 45 ++++++++++++- components/map/location-links.tsx | 45 +++++++++++++ components/map/map-query-handler.tsx | 85 +++++++++--------------- lib/agents/researcher.tsx | 98 +++++++++++++--------------- 5 files changed, 166 insertions(+), 112 deletions(-) create mode 100644 components/map/location-links.tsx diff --git a/app/actions.tsx b/app/actions.tsx index 31eb8629..16dfa373 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -133,6 +133,8 @@ async function submit(formData?: FormData, skip?: boolean) { : ((formData?.get('related_query') as string) || (formData?.get('input') as string)) + const searchMode = formData?.get('searchMode') as string || 'Standard' + if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') { const definition = userInput.toLowerCase().trim() === 'what is a planet computer?' ? `A planet computer is a proprietary environment aware system that interoperates weather forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)` @@ -324,7 +326,8 @@ async function submit(formData?: FormData, skip?: boolean) { uiStream, streamText, messages, - useSpecificAPI + useSpecificAPI, + searchMode ) answer = fullResponse toolOutputs = toolResponses diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index b0bf2166..bd10855e 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -3,13 +3,13 @@ import { useEffect, useState, useRef, ChangeEvent, forwardRef, useImperativeHandle } from 'react' import type { AI, UIState } from '@/app/actions' import { useUIState, useActions } from 'ai/rsc' -// Removed import of useGeospatialToolMcp as it's no longer used/available import { cn } from '@/lib/utils' import { UserMessage } from './user-message' import { Button } from './ui/button' -import { ArrowRight, Plus, Paperclip, X } from 'lucide-react' +import { ArrowRight, Plus, Paperclip, X, Search, MapPin, Globe } from 'lucide-react' import Textarea from 'react-textarea-autosize' import { nanoid } from 'nanoid' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' interface ChatPanelProps { messages: UIState @@ -27,6 +27,7 @@ export const ChatPanel = forwardRef(({ messages, i // Removed mcp instance as it's no longer passed to submit const [isMobile, setIsMobile] = useState(false) const [selectedFile, setSelectedFile] = useState(null) + const [searchMode, setSearchMode] = useState('Standard') const inputRef = useRef(null) const formRef = useRef(null) const fileInputRef = useRef(null) @@ -169,15 +170,53 @@ export const ChatPanel = forwardRef(({ messages, i isMobile ? 'px-2 pb-2 pt-1 h-full flex flex-col justify-center' : '' )} > +
+ + + + + + + + +
+ { + fileInputRef.current = fileInputNode + }} onChange={handleFileChange} className="hidden" accept="text/plain,image/png,image/jpeg,image/webp" diff --git a/components/map/location-links.tsx b/components/map/location-links.tsx new file mode 100644 index 00000000..6d607761 --- /dev/null +++ b/components/map/location-links.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useMapData } from './map-data-context'; +import { Button } from '@/components/ui/button'; +import { MapPin } from 'lucide-react'; + +interface Location { + latitude: number; + longitude: number; + place_name: string; +} + +interface LocationLinksProps { + locations: Location[]; +} + +export const LocationLinks: React.FC = ({ locations }) => { + const { setMapData } = useMapData(); + + const handleFlyTo = (location: Location) => { + setMapData(prevData => ({ + ...prevData, + targetPosition: [location.longitude, location.latitude], + mapFeature: { + place_name: location.place_name, + } + })); + }; + + return ( +
+ {locations.map((location, index) => ( + + ))} +
+ ); +}; diff --git a/components/map/map-query-handler.tsx b/components/map/map-query-handler.tsx index f3e36bbb..afa7dd0e 100644 --- a/components/map/map-query-handler.tsx +++ b/components/map/map-query-handler.tsx @@ -1,83 +1,60 @@ 'use client'; import { useEffect } from 'react'; -// Removed useMCPMapClient as we'll use data passed via props -import { useMapData } from './map-data-context'; +import { useMapData } from './map-data-context'; +import { LocationLinks } from './location-links'; + +interface Location { + latitude: number; + longitude: number; + place_name: string; +} -// Define the expected structure of the mcp_response from geospatialTool interface McpResponseData { - location: { - latitude?: number; - longitude?: number; - place_name?: string; - address?: string; - }; + locations?: Location[]; // Support multiple locations + location?: Location; // Support single location mapUrl?: string; } interface GeospatialToolOutput { - type: string; // e.g., "MAP_QUERY_TRIGGER" + type: string; originalUserInput: string; timestamp: string; mcp_response: McpResponseData | null; } interface MapQueryHandlerProps { - // originalUserInput: string; // Kept for now, but primary data will come from toolOutput - toolOutput?: GeospatialToolOutput | null; // The direct output from geospatialTool + toolOutput?: GeospatialToolOutput | null; } export const MapQueryHandler: React.FC = ({ toolOutput }) => { const { setMapData } = useMapData(); useEffect(() => { - if (toolOutput && toolOutput.mcp_response && toolOutput.mcp_response.location) { - const { latitude, longitude, place_name } = toolOutput.mcp_response.location; + if (toolOutput && toolOutput.mcp_response) { + const { locations, location } = toolOutput.mcp_response; - if (typeof latitude === 'number' && typeof longitude === 'number') { - console.log(`MapQueryHandler: Received data from geospatialTool. Place: ${place_name}, Lat: ${latitude}, Lng: ${longitude}`); - setMapData(prevData => ({ - ...prevData, - // Ensure coordinates are in [lng, lat] format for MapboxGL - targetPosition: [longitude, latitude], - // Optionally store more info from mcp_response if needed by MapboxMap component later - mapFeature: { - place_name, - // Potentially add mapUrl or other details from toolOutput.mcp_response - mapUrl: toolOutput.mcp_response?.mapUrl - } - })); + if (locations && locations.length > 1) { + // Multiple locations: handled by LocationLinks, no automatic fly-to } else { - console.warn("MapQueryHandler: Invalid latitude/longitude in toolOutput.mcp_response:", toolOutput.mcp_response.location); - // Clear target position if data is invalid - setMapData(prevData => ({ - ...prevData, - targetPosition: null, - mapFeature: null - })); + const singleLocation = locations && locations.length === 1 ? locations[0] : location; + if (singleLocation && typeof singleLocation.latitude === 'number' && typeof singleLocation.longitude === 'number') { + setMapData(prevData => ({ + ...prevData, + targetPosition: [singleLocation.longitude, singleLocation.latitude], + mapFeature: { + place_name: singleLocation.place_name, + mapUrl: toolOutput.mcp_response?.mapUrl + } + })); + } } - } else { - // This case handles when toolOutput or its critical parts are missing. - // Depending on requirements, could fall back to originalUserInput and useMCPMapClient, - // or simply log that no valid data was provided from the tool. - // For this subtask, we primarily focus on using the new toolOutput. - if (toolOutput) { // It exists, but data is not as expected - console.warn("MapQueryHandler: toolOutput provided, but mcp_response or location data is missing.", toolOutput); - } - // If toolOutput is null/undefined, this component might not need to do anything, - // or it's an indication that it shouldn't have been rendered/triggered. - // For now, if no valid toolOutput, we clear map data or leave it as is. - // setMapData(prevData => ({ ...prevData, targetPosition: null, mapFeature: null })); } - // The dependencies for this useEffect should be based on the props that trigger its logic. - // If originalUserInput and the old MCP client were still used as a fallback, they'd be dependencies. }, [toolOutput, setMapData]); - // This component is a handler and does not render any visible UI itself. - // Its purpose is to trigger map data updates based on AI tool results. - // If it were to use the old useMCPMapClient, mcpLoading and mcpError would be relevant. - // It could return a small status indicator or debug info if needed for development. + if (toolOutput?.mcp_response?.locations && toolOutput.mcp_response.locations.length > 1) { + return ; + } + return null; - // Example for debugging with previous client: - // return
; }; diff --git a/lib/agents/researcher.tsx b/lib/agents/researcher.tsx index e54b1428..5ecb6381 100644 --- a/lib/agents/researcher.tsx +++ b/lib/agents/researcher.tsx @@ -12,12 +12,12 @@ import { getTools } from './tools' import { getModel } from '../utils' export async function researcher( - dynamicSystemPrompt: string, // New parameter + dynamicSystemPrompt: string, uiStream: ReturnType, streamText: ReturnType>, messages: CoreMessage[], - // mcp: any, // Removed mcp parameter - useSpecificModel?: boolean + useSpecificModel?: boolean, + searchMode?: string ) { let fullResponse = '' let hasError = false @@ -28,59 +28,49 @@ export async function researcher( ) const currentDate = new Date().toLocaleString() - // Default system prompt, used if dynamicSystemPrompt is not provided - const default_system_prompt = `As a comprehensive AI assistant, you can search the web, retrieve information from URLs except from maps -here use the Geospatial tools provided, and understand geospatial queries to assist the user and display information on a map. -Current date and time: ${currentDate}. When tools are not needed, provide direct, helpful answers based on your knowledge.Match the language of your response to the user's language. -Always aim to directly address the user's question. If using information from a tool (like web search), cite the source URL. - -There are also some proconfigured example queires. - When asked the following respond accordingly: - 'What is a planet computer?' answer with the following: '"A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet' - ‘What is QCX-Terra’ Respond with ‘QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land prediction from satellite images’ - - -Tool Usage Guide: - -- For general web searches for factual information: Use the 'search' tool. -- For retrieving content from specific URLs provided by the user: Use the 'retrieve' tool. (Do not use this for URLs found in search results). - -- For any questions involving locations, places, addresses, geographical features, finding businesses or points of interest, distances between locations, or directions: You MUST use the 'geospatialQueryTool'. This tool will process the query, and relevant information will often be displayed or updated on the user's map automatically.** - Examples of queries for 'geospatialQueryTool': - Location Discovery -"Find coffee shops within walking distance of the Empire State Building" -"Show me gas stations along the route from Boston to New York" -"What restaurants are near Times Square?" -Navigation & Travel -"Get driving directions from LAX to Hollywood with current traffic" -"How long would it take to walk from Central Park to Times Square?" -"Calculate travel time from my hotel (Four Seasons) to JFK Airport by taxi during rush hour" -Visualization & Maps -"Create a map image showing the route from Golden Gate Bridge to Fisherman's Wharf with markers at both locations" -"Show me a satellite view of Manhattan with key landmarks marked" -"Generate a map highlighting all Starbucks locations within a mile of downtown Seattle" -Analysis & Planning -"Show me areas reachable within 30 minutes of downtown Portland by car" -"Calculate a travel time matrix between these 3 hotel locations (Marriott, Sheraton and Hilton) and the convention center in Denver" -"Find the optimal route visiting these 3 tourist attractions (Golden Gate, Musical Stairs and Fisherman's Wharf) in San Francisco" - - When you use 'geospatialQueryTool', you don't need to describe how the map will change; simply provide your textual answer based on the query, and trust the map will update appropriately. -`; - - const systemToUse = dynamicSystemPrompt && dynamicSystemPrompt.trim() !== '' ? dynamicSystemPrompt : default_system_prompt; - - const result = await nonexperimental_streamText({ - model: getModel() as LanguageModel, - maxTokens: 2500, - system: systemToUse, // Use the dynamic or default system prompt - messages, - tools: getTools({ - uiStream, - fullResponse, - // mcp // mcp parameter is no longer passed to getTools - }) + + let systemPrompt = `Current date and time: ${currentDate}. Match the language of your response to the user's language. Always aim to directly address the user's question. If using information from a tool (like web search), cite the source URL.`; + + const standardPrompt = `As a comprehensive AI assistant, you can search the web, retrieve information from URLs, and understand geospatial queries to assist the user and display information on a map. When tools are not needed, provide direct, helpful answers based on your knowledge. + + Tool Usage Guide: + - For general web searches: Use the 'search' tool. + - For retrieving content from specific URLs: Use the 'retrieve' tool. + - For any questions involving locations, places, or directions: You MUST use the 'geospatialQueryTool'.`; + + const geospatialPrompt = `You are a specialized Geospatial AI assistant. Your primary function is to understand and respond to geospatial queries. You MUST prioritize using the 'geospatialQueryTool' for any questions involving locations, places, addresses, geographical features, businesses, points of interest, distances, or directions. Only use other tools if geospatial queries are not applicable.`; + + const webSearchPrompt = `You are a specialized Web Search AI assistant. Your primary function is to search the web and retrieve information from URLs to answer user questions. You MUST prioritize using the 'search' and 'retrieve' tools. Only use other tools if web searches are not applicable.`; + + switch (searchMode) { + case 'Geospatial': + systemPrompt = `${geospatialPrompt}\n${systemPrompt}`; + break; + case 'Web Search': + systemPrompt = `${webSearchPrompt}\n${systemPrompt}`; + break; + default: + systemPrompt = `${standardPrompt}\n${systemPrompt}`; + break; + } + + const allTools = getTools({ uiStream, fullResponse }); + let availableTools: any = allTools; + + if (searchMode === 'Geospatial') { + availableTools = { geospatialQueryTool: allTools.geospatialQueryTool }; + } else if (searchMode === 'Web Search') { + availableTools = { search: allTools.search, retrieve: allTools.retrieve }; + } + + const result = await nonexperimental_streamText({ + model: getModel() as LanguageModel, + maxTokens: 2500, + system: systemPrompt, + messages, + tools: availableTools }) - // Remove the spinner uiStream.update(null) // Process the response