Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ async function submit(formData?: FormData, skip?: boolean) {
const userId = 'anonymous'
const currentSystemPrompt = (await getSystemPrompt(userId)) || ''

const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google'

Comment on lines +288 to +289
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate mapProvider value before use.

The type assertion as 'mapbox' | 'google' doesn't validate that the form data actually contains one of these values. If formData.get('mapProvider') returns null or an unexpected string, this could cause issues downstream.

🛡️ Suggested validation
-  const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google'
+  const rawMapProvider = formData?.get('mapProvider')
+  const mapProvider: 'mapbox' | 'google' | undefined = 
+    rawMapProvider === 'mapbox' || rawMapProvider === 'google' 
+      ? rawMapProvider 
+      : undefined
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google'
const rawMapProvider = formData?.get('mapProvider')
const mapProvider: 'mapbox' | 'google' | undefined =
rawMapProvider === 'mapbox' || rawMapProvider === 'google'
? rawMapProvider
: undefined
🤖 Prompt for AI Agents
In @app/actions.tsx around lines 288 - 289, The code uses a blind type assertion
for mapProvider (const mapProvider = formData?.get('mapProvider') as 'mapbox' |
'google') which can be null or an unexpected string; validate the raw value
returned by formData.get('mapProvider') before asserting or using it. Replace
the assertion with logic that reads the raw value, checks it against the allowed
set ('mapbox','google'), handles null/invalid cases (e.g., return an error, set
a default, or throw), and only then casts or narrows to the union; update any
downstream usage of mapProvider to assume a validated value.

async function processEvents() {
let action: any = { object: { next: 'proceed' } }
if (!skip) {
Expand Down Expand Up @@ -330,6 +332,7 @@ async function submit(formData?: FormData, skip?: boolean) {
uiStream,
streamText,
messages,
mapProvider,
useSpecificAPI
)
answer = fullResponse
Expand Down
1,117 changes: 554 additions & 563 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Button } from './ui/button'
import { ArrowRight, Plus, Paperclip, X } from 'lucide-react'
import Textarea from 'react-textarea-autosize'
import { nanoid } from 'nanoid'
import { useSettingsStore } from '@/lib/store/settings'

Comment on lines +13 to 14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use a Zustand selector for mapProvider to avoid rerenders + validate server-side

Proposed change
-  const { mapProvider } = useSettingsStore()
+  const mapProvider = useSettingsStore(s => s.mapProvider)

Also ensure the server action/tooling treats mapProvider as untrusted input (whitelist 'mapbox' | 'google', fallback to default).

Also applies to: 29-30, 181-181

🤖 Prompt for AI Agents
In @components/chat-panel.tsx around lines 13 - 14, Replace direct reads of
mapProvider from the Zustand store with a selector to avoid component rerenders:
use useSettingsStore(s => s.mapProvider) wherever mapProvider is read in this
file (and the other noted locations). Additionally, treat mapProvider as
untrusted input on the server/tooling side: validate it against a strict
whitelist ('mapbox' | 'google') in the server action or helper that consumes
this value and fall back to the default provider if it is not one of those
values. Ensure you update all usages referenced (useSettingsStore, mapProvider
reads in chat-panel and the spots around the other mentioned lines) so both
client reads use a selector and server-side code performs the whitelist+fallback
validation.

interface ChatPanelProps {
messages: UIState
Expand All @@ -25,6 +26,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
const [, setMessages] = useUIState<typeof AI>()
const { submit, clearChat } = useActions()
// Removed mcp instance as it's no longer passed to submit
const { mapProvider } = useSettingsStore()
const [isMobile, setIsMobile] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
Expand Down Expand Up @@ -176,6 +178,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
isMobile && 'mobile-chat-input' // Apply mobile chat input styling
)}
>
<input type="hidden" name="mapProvider" value={mapProvider} />
<input
type="file"
ref={fileInputRef}
Expand Down
18 changes: 14 additions & 4 deletions components/map/google-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { useEffect } from 'react'
import { useToast } from '@/components/ui/hooks/use-toast'
import { useMapData } from './map-data-context'
import { useSettingsStore } from '@/lib/store/settings'
import { useMapLoading } from '../map-loading-context';
import { Map3D } from './map-3d'

export function GoogleMapComponent() {
const { toast } = useToast()
const { mapData } = useMapData()
const { setMapProvider } = useSettingsStore()
const { setIsMapLoaded } = useMapLoading();

const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY

Expand All @@ -25,18 +27,26 @@ export function GoogleMapComponent() {
}
}, [apiKey, setMapProvider, toast])

useEffect(() => {
setIsMapLoaded(true);
return () => {
setIsMapLoaded(false);
};
}, [setIsMapLoaded]);

if (!apiKey) {
return null
}

const cameraOptions = mapData.targetPosition
? { center: mapData.targetPosition, range: 1000, tilt: 60, heading: 0 }
: { center: { lat: 37.7749, lng: -122.4194 }, range: 1000, tilt: 60, heading: 0 };

return (
<APIProvider apiKey={apiKey} version="alpha">
<Map3D
style={{ width: '100%', height: '100%' }}
center={{ lat: 37.7749, lng: -122.4194, altitude: 0 }}
heading={0}
tilt={60}
range={1000}
cameraOptions={cameraOptions}
Comment on lines +41 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider memoizing cameraOptions to avoid unnecessary re-renders.

The cameraOptions object is recreated on every render, which may cause useDeepCompareEffect in Map3D to trigger more often than necessary. While useDeepCompareEffect does a deep comparison, memoizing still improves performance by avoiding the comparison overhead when dependencies haven't changed.

♻️ Suggested refactor
+'use client'
+
+import { APIProvider } from '@vis.gl/react-google-maps'
+import { useEffect, useMemo } from 'react'
+import { useToast } from '@/components/ui/hooks/use-toast'
+import { useMapData } from './map-data-context'
+import { useSettingsStore } from '@/lib/store/settings'
+import { Map3D } from './map-3d'
+
+export function GoogleMapComponent() {
+  const { toast } = useToast()
+  const { mapData } = useMapData()
+  const { setMapProvider } = useSettingsStore()
+
+  const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
+
+  useEffect(() => {
+    if (!apiKey) {
+      toast({
+        title: 'Google Maps API Key Missing',
+        description: 'The Google Maps API key is not configured. Falling back to Mapbox.',
+        variant: 'destructive',
+      })
+      setMapProvider('mapbox')
+    }
+  }, [apiKey, setMapProvider, toast])
+
+  if (!apiKey) {
+    return null
+  }
+
+  const cameraOptions = useMemo(() => ({
+    center: mapData.targetPosition ?? { lat: 37.7749, lng: -122.4194 },
+    range: 1000,
+    tilt: 60,
+    heading: 0
+  }), [mapData.targetPosition])
+
+  return (
+    <APIProvider apiKey={apiKey} version="alpha">
+      <Map3D
+        style={{ width: '100%', height: '100%' }}
+        cameraOptions={cameraOptions}
+        mode="SATELLITE"
+      />
+    </APIProvider>
+  )
+}
🤖 Prompt for AI Agents
In @components/map/google-map.tsx around lines 32 - 40, cameraOptions is being
recreated on every render causing unnecessary work; wrap its creation in
React.useMemo so it only changes when mapData.targetPosition actually changes.
Inside the component, compute cameraOptions with useMemo, using
mapData.targetPosition (or its lat/lng fields) as the dependency so the object
identity stays stable between renders and Map3D’s useDeepCompareEffect avoids
repeated work; keep the same shape ({ center, range: 1000, tilt: 60, heading: 0
}) and fallback to the SF coords when targetPosition is falsy.

mode="SATELLITE"
/>
</APIProvider>
Expand Down
6 changes: 6 additions & 0 deletions components/map/map-3d-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ declare global {
export interface Map3DProps extends google.maps.maps3d.Map3DElementOptions {
style?: CSSProperties;
onCameraChange?: (e: Map3DCameraChangeEvent) => void;
cameraOptions?: {
center?: { lat: number; lng: number };
heading?: number;
tilt?: number;
range?: number;
};
}
Comment on lines 38 to 47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Align cameraOptions types with Google Maps types (and consider roll)

Right now cameraOptions.center duplicates google.maps.LatLngLiteral; also roll exists on Map3DElementOptions but isn’t representable via cameraOptions.

Proposed change
 export interface Map3DProps extends google.maps.maps3d.Map3DElementOptions {
   style?: CSSProperties;
   onCameraChange?: (e: Map3DCameraChangeEvent) => void;
   cameraOptions?: {
-    center?: { lat: number; lng: number };
+    center?: google.maps.LatLngLiteral;
     heading?: number;
     tilt?: number;
     range?: number;
+    roll?: number;
   };
 }
🤖 Prompt for AI Agents
In @components/map/map-3d-types.ts around lines 38 - 47, cameraOptions currently
duplicates LatLng shape and omits roll; update Map3DProps to align with Google
Maps types by changing cameraOptions.center to google.maps.LatLngLiteral (or
google.maps.LatLngLiteral | google.maps.LatLng if you need both), and add roll?:
number alongside heading?: number, tilt?: number, range?: number so the shape
matches google.maps.maps3d.Map3DElementOptions camera-related fields; adjust any
usages/Map3DCameraChangeEvent typings accordingly to accept the updated center
and roll fields.


/**
Expand Down
30 changes: 20 additions & 10 deletions components/map/map-3d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,7 @@ export const Map3D = forwardRef(
props.onCameraChange(p);
});

const [customElementsReady, setCustomElementsReady] = useState(false);
useEffect(() => {
customElements.whenDefined('gmp-map-3d').then(() => {
setCustomElementsReady(true);
});
}, []);

const {center, heading, tilt, range, roll, ...map3dOptions} = props;
const {center, heading, tilt, range, roll, cameraOptions, ...map3dOptions} = props;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Clarify the interaction between individual camera props and cameraOptions.

Both individual props (center, heading, tilt, range) and cameraOptions can set the same camera properties. The JSX element (lines 69-73) uses individual props for initial rendering, while the effect (lines 41-58) applies cameraOptions. When cameraOptions is provided, the individual props may be undefined, leading to potentially inconsistent initial state.

Consider either:

  1. Deriving initial JSX values from cameraOptions when it's provided, or
  2. Documenting that cameraOptions takes precedence over individual props for dynamic updates.
🤖 Prompt for AI Agents
In @components/map/map-3d.tsx at line 32, The component currently destructures
center, heading, tilt, range and cameraOptions and uses the individual props for
the JSX initial camera while an effect applies cameraOptions later
(cameraOptions, effect block lines ~41-58 and JSX initial rendering lines
~69-73); fix this by deriving the initial JSX camera values from cameraOptions
when cameraOptions is present (i.e., when destructuring, compute
initialCenter/initialHeading/initialTilt/initialRange from cameraOptions if
provided, falling back to the individual props), so initial render and later
effect use the same source of truth and avoid undefined/inconsistent initial
state.


useDeepCompareEffect(() => {
if (!map3DElement) return;
Expand All @@ -45,13 +38,30 @@ export const Map3D = forwardRef(
Object.assign(map3DElement, map3dOptions);
}, [map3DElement, map3dOptions]);

useDeepCompareEffect(() => {
if (!map3DElement || !cameraOptions) return;

const { center, heading, tilt, range } = cameraOptions;

if (center) {
map3DElement.center = { ...center, altitude: 0 };
}
if (heading !== undefined) {
map3DElement.heading = heading;
}
if (tilt !== undefined) {
map3DElement.tilt = tilt;
}
if (range !== undefined) {
map3DElement.range = range;
}
}, [map3DElement, cameraOptions]);

useImperativeHandle<
google.maps.maps3d.Map3DElement | null,
google.maps.maps3d.Map3DElement | null
>(forwardedRef, () => map3DElement, [map3DElement]);

if (!customElementsReady) return null;

return (
<div style={props.style}>
<gmp-map-3d
Expand Down
4 changes: 1 addition & 3 deletions components/map/map-data-context.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
'use client';

import React, { createContext, useContext, useState, ReactNode } from 'react';
import { LngLatLike } from 'mapbox-gl'; // Import LngLatLike

// Define the shape of the map data you want to share
export interface MapData {
targetPosition?: LngLatLike | null; // For flying to a location
targetPosition?: { lat: number; lng: number } | null; // For flying to a location
// TODO: Add other relevant map data types later (e.g., routeGeoJSON, poiList)
mapFeature?: any | null; // Generic feature from MCP hook's processLocationQuery
Comment on lines 5 to 8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Prefer targetPosition: ... | null (non-optional) to avoid undefined/null split

If consumers don’t need to distinguish “unset” vs “cleared”, make it non-optional and initialize to null for simpler downstream checks.

Proposed change
 export interface MapData {
-  targetPosition?: { lat: number; lng: number } | null; // For flying to a location
+  targetPosition: { lat: number; lng: number } | null; // For flying to a location
   // TODO: Add other relevant map data types later (e.g., routeGeoJSON, poiList)
   mapFeature?: any | null; // Generic feature from MCP hook's processLocationQuery
   drawnFeatures?: Array<{
     id: string;
     type: 'Polygon' | 'LineString';
     measurement: string;
     geometry: any;
   }>;
   markers?: Array<{
     latitude: number;
     longitude: number;
     title?: string;
   }>;
 }

 export const MapDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
-  const [mapData, setMapData] = useState<MapData>({ drawnFeatures: [], markers: [] });
+  const [mapData, setMapData] = useState<MapData>({ targetPosition: null, drawnFeatures: [], markers: [] });

Also applies to: 29-31

drawnFeatures?: Array<{ // Added to store drawn features and their measurements
Expand Down
5 changes: 2 additions & 3 deletions components/map/map-query-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ export const MapQueryHandler: React.FC<MapQueryHandlerProps> = ({ toolOutput })
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],
targetPosition: { lat: latitude, lng: longitude },
// Optionally store more info from mcp_response if needed by MapboxMap component later
mapFeature: {
mapFeature: {
place_name,
// Potentially add mapUrl or other details from toolOutput.mcp_response
mapUrl: toolOutput.mcp_response?.mapUrl
Expand Down
7 changes: 1 addition & 6 deletions components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,14 +513,9 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
// Effect to handle map updates from MapDataContext
useEffect(() => {
if (mapData.targetPosition && map.current) {
// console.log("Mapbox.tsx: Received new targetPosition from context:", mapData.targetPosition);
// targetPosition is LngLatLike, which can be [number, number]
// updateMapPosition expects (latitude, longitude)
const [lng, lat] = mapData.targetPosition as [number, number]; // Assuming LngLatLike is [lng, lat]
const { lat, lng } = mapData.targetPosition;
if (typeof lat === 'number' && typeof lng === 'number') {
updateMapPosition(lat, lng);
} else {
// console.error("Mapbox.tsx: Invalid targetPosition format in mapData", mapData.targetPosition);
}
}
// TODO: Handle mapData.mapFeature for drawing routes, polygons, etc. in a future step.
Expand Down
11 changes: 11 additions & 0 deletions dev_server.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
$ next dev --turbo
⚠ Port 3000 is in use, using available port 3001 instead.
▲ Next.js 15.3.6 (Turbopack)
- Local: http://localhost:3001
- Network: http://192.168.0.2:3001
- Environments: .env

✓ Starting...
○ Compiling middleware ...
✓ Compiled middleware in 528ms
✓ Ready in 2.7s
8 changes: 5 additions & 3 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Section } from '@/components/section'
import { BotMessage } from '@/components/message'
import { getTools } from './tools'
import { getModel } from '../utils'
import { MapProvider } from '@/lib/store/settings'

// This magic tag lets us write raw multi-line strings with backticks, arrows, etc.
const raw = String.raw
Expand Down Expand Up @@ -78,6 +79,7 @@ export async function researcher(
uiStream: ReturnType<typeof createStreamableUI>,
streamText: ReturnType<typeof createStreamableValue<string>>,
messages: CoreMessage[],
mapProvider: MapProvider,
useSpecificModel?: boolean
) {
let fullResponse = ''
Expand All @@ -97,8 +99,8 @@ export async function researcher(
: getDefaultSystemPrompt(currentDate)

// Check if any message contains an image
const hasImage = messages.some(message =>
Array.isArray(message.content) &&
const hasImage = messages.some(message =>
Array.isArray(message.content) &&
message.content.some(part => part.type === 'image')
)

Expand All @@ -107,7 +109,7 @@ export async function researcher(
maxTokens: 4096,
system: systemPromptToUse,
messages,
tools: getTools({ uiStream, fullResponse }),
tools: getTools({ uiStream, fullResponse, mapProvider }),
})

uiStream.update(null) // remove spinner
Expand Down
69 changes: 67 additions & 2 deletions lib/agents/tools/geospatial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { Client as MCPClientClass } from '@modelcontextprotocol/sdk/client/index
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
// Smithery SDK removed - using direct URL construction
import { z } from 'zod';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { getSelectedModel } from '@/lib/actions/users';
import { MapProvider } from '@/lib/store/settings';

// Types
export type McpClient = MCPClientClass;
Expand Down Expand Up @@ -152,7 +155,13 @@ async function closeClient(client: McpClient | null) {
/**
* Main geospatial tool executor.
*/
export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof createStreamableUI> }) => ({
export const geospatialTool = ({
uiStream,
mapProvider
}: {
uiStream: ReturnType<typeof createStreamableUI>
mapProvider?: MapProvider
}) => ({
description: `Use this tool for location-based queries including:
There a plethora of tools inside this tool accessible on the mapbox mcp server where switch case into the tool of choice for that use case
If the Query is supposed to use multiple tools in a sequence you must access all the tools in the sequence and then provide a final answer based on the results of all the tools used.
Expand Down Expand Up @@ -224,11 +233,67 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g
parameters: geospatialQuerySchema,
execute: async (params: z.infer<typeof geospatialQuerySchema>) => {
const { queryType, includeMap = true } = params;
console.log('[GeospatialTool] Execute called with:', params);
console.log('[GeospatialTool] Execute called with:', params, 'and map provider:', mapProvider);

const uiFeedbackStream = createStreamableValue<string>();
uiStream.append(<BotMessage content={uiFeedbackStream.value} />);

const selectedModel = await getSelectedModel();

if (selectedModel?.includes('gemini') && mapProvider === 'google') {
let feedbackMessage = `Processing geospatial query with Gemini...`;
uiFeedbackStream.update(feedbackMessage);

try {
const genAI = new GoogleGenerativeAI(process.env.GEMINI_3_PRO_API_KEY!);
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-pro-latest',
});
Comment on lines +248 to +251
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing environment variable check for Gemini API key.

The non-null assertion (!) on GEMINI_3_PRO_API_KEY will cause a runtime error if the environment variable is not set. This should be validated before use.

🐛 Suggested fix
+      const geminiApiKey = process.env.GEMINI_3_PRO_API_KEY;
+      if (!geminiApiKey) {
+        throw new Error('GEMINI_3_PRO_API_KEY environment variable is not configured');
+      }
+
-      const genAI = new GoogleGenerativeAI(process.env.GEMINI_3_PRO_API_KEY!);
+      const genAI = new GoogleGenerativeAI(geminiApiKey);
       const model = genAI.getGenerativeModel({
         model: 'gemini-1.5-pro-latest',
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const genAI = new GoogleGenerativeAI(process.env.GEMINI_3_PRO_API_KEY!);
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-pro-latest',
});
const geminiApiKey = process.env.GEMINI_3_PRO_API_KEY;
if (!geminiApiKey) {
throw new Error('GEMINI_3_PRO_API_KEY environment variable is not configured');
}
const genAI = new GoogleGenerativeAI(geminiApiKey);
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-pro-latest',
});
🤖 Prompt for AI Agents
In @lib/agents/tools/geospatial.tsx around lines 248 - 251, The code uses a
non-null assertion on process.env.GEMINI_3_PRO_API_KEY when constructing
GoogleGenerativeAI (seen in genAI = new GoogleGenerativeAI(...)) which can crash
at runtime if the env var is missing; replace the inline assertion by first
reading const geminiKey = process.env.GEMINI_3_PRO_API_KEY and validate it
(e.g., if (!geminiKey) throw or log a clear error and exit), then pass geminiKey
into new GoogleGenerativeAI and proceed to call getGenerativeModel; ensure no
use of the `!` operator remains.


const searchText = (params as any).location || (params as any).query;
const prompt = `Find the location for: ${searchText}`;
const tools: any = [{ googleSearch: {} }];
const result = await model.generateContent({
contents: [{ role: 'user', parts: [{ text: prompt }] }],
tools,
});
Comment on lines +253 to +259
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the geospatialQuerySchema definition
ast-grep --pattern 'export const geospatialQuerySchema = $_'

Repository: QueueLab/QCX

Length of output: 6682


🏁 Script executed:

#!/bin/bash
# Also search for geospatialQuerySchema in case it's not exported or has different syntax
rg 'geospatialQuerySchema' -A 10

Repository: QueueLab/QCX

Length of output: 3467


🏁 Script executed:

#!/bin/bash
# Check the function containing lines 253-259 to understand params type
sed -n '240,270p' lib/agents/tools/geospatial.tsx

Repository: QueueLab/QCX

Length of output: 1379


Remove unsafe as any casts by using type guards based on queryType discriminator.

The params is properly typed as z.infer<typeof geospatialQuerySchema>, but the schema is a discriminated union where .location only exists on 'geocode' and 'map' types, and .query only exists on 'search' type. Accessing these fields with (params as any).location || (params as any).query bypasses type checking and would fail at runtime for 'reverse', 'directions', or 'distance' query types. Use type narrowing based on the queryType discriminator to safely access type-specific fields.

🤖 Prompt for AI Agents
In @lib/agents/tools/geospatial.tsx around lines 253 - 259, The unsafe casts
using (params as any).location || (params as any).query must be replaced with
type-narrowing on the geospatialQuerySchema discriminant: inspect
params.queryType and for 'geocode' or 'map' read params.location, for 'search'
read params.query, and handle or throw for unsupported types ('reverse',
'directions', 'distance'); assign that value to searchText and pass it into the
prompt used by model.generateContent (remove any "as any" casts and rely on the
narrowed types instead).

const response = await result.response;
const functionCalls = (response as any).functionCalls();

if (functionCalls && functionCalls.length > 0) {
const gsr = functionCalls[0];
// This is a placeholder for the actual response structure,
// as I don't have a way to inspect it at the moment.
const place = (gsr as any).results[0].place;
if (place) {
const { latitude, longitude } = place.coordinates;
const place_name = place.displayName;

const mcpData = {
location: {
latitude,
longitude,
place_name,
},
};
feedbackMessage = `Found location: ${place_name}`;
uiFeedbackStream.update(feedbackMessage);
uiFeedbackStream.done();
uiStream.update(<BotMessage content={uiFeedbackStream.value} />);
return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: mcpData, error: null };
}
}
Comment on lines +261 to +285
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unsafe access to Gemini response structure without null checks.

The code accesses nested properties (gsr.results[0].place, place.coordinates) without validation. The inline comment acknowledges this is a "placeholder" with unknown structure, making this fragile and prone to runtime errors.

🐛 Suggested fix with defensive checks
       const functionCalls = (response as any).functionCalls();

       if (functionCalls && functionCalls.length > 0) {
         const gsr = functionCalls[0];
-        // This is a placeholder for the actual response structure,
-        // as I don't have a way to inspect it at the moment.
-        const place = (gsr as any).results[0].place;
-        if (place) {
-          const { latitude, longitude } = place.coordinates;
+        const results = (gsr as any)?.results;
+        const place = results?.[0]?.place;
+        const coordinates = place?.coordinates;
+        
+        if (coordinates?.latitude != null && coordinates?.longitude != null) {
+          const { latitude, longitude } = coordinates;
           const place_name = place.displayName;

           const mcpData = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const functionCalls = (response as any).functionCalls();
if (functionCalls && functionCalls.length > 0) {
const gsr = functionCalls[0];
// This is a placeholder for the actual response structure,
// as I don't have a way to inspect it at the moment.
const place = (gsr as any).results[0].place;
if (place) {
const { latitude, longitude } = place.coordinates;
const place_name = place.displayName;
const mcpData = {
location: {
latitude,
longitude,
place_name,
},
};
feedbackMessage = `Found location: ${place_name}`;
uiFeedbackStream.update(feedbackMessage);
uiFeedbackStream.done();
uiStream.update(<BotMessage content={uiFeedbackStream.value} />);
return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: mcpData, error: null };
}
}
const functionCalls = (response as any).functionCalls();
if (functionCalls && functionCalls.length > 0) {
const gsr = functionCalls[0];
const results = (gsr as any)?.results;
const place = results?.[0]?.place;
const coordinates = place?.coordinates;
if (coordinates?.latitude != null && coordinates?.longitude != null) {
const { latitude, longitude } = coordinates;
const place_name = place.displayName;
const mcpData = {
location: {
latitude,
longitude,
place_name,
},
};
feedbackMessage = `Found location: ${place_name}`;
uiFeedbackStream.update(feedbackMessage);
uiFeedbackStream.done();
uiStream.update(<BotMessage content={uiFeedbackStream.value} />);
return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: mcpData, error: null };
}
}

throw new Error('No location found by Gemini.');
} catch (error: any) {
const toolError = `Gemini grounding error: ${error.message}`;
uiFeedbackStream.update(toolError);
console.error('[GeospatialTool] Gemini execution failed:', error);
uiFeedbackStream.done();
uiStream.update(<BotMessage content={uiFeedbackStream.value} />);
return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: null, error: toolError };
}
}
Comment on lines +241 to +295

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Gemini grounding path is built around multiple any casts and a comment stating the response structure is a placeholder. This is a correctness risk: the code is very likely to throw at runtime (functionCalls()[0].results[0].place etc.) and will turn many valid queries into tool errors.

Additionally, process.env.GEMINI_3_PRO_API_KEY! will crash the tool when unset rather than producing a controlled error and falling back to MCP.

Suggestion

Harden the Gemini branch by (1) checking the API key, (2) parsing the response defensively with optional chaining and validation, and (3) falling back to the MCP path when Gemini doesn’t return a usable location.

const apiKey = process.env.GEMINI_3_PRO_API_KEY
if (!apiKey) {
  console.warn('[GeospatialTool] GEMINI_3_PRO_API_KEY not set; falling back to MCP')
} else if (selectedModel?.includes('gemini') && mapProvider === 'google') {
  try {
    // ... call Gemini ...
    const calls = (response as any)?.functionCalls?.() ?? []
    const place = calls?.[0]?.results?.[0]?.place
    const latitude = place?.coordinates?.latitude
    const longitude = place?.coordinates?.longitude
    const place_name = place?.displayName

    if (typeof latitude === 'number' && typeof longitude === 'number' && place_name) {
      return { /* success */ }
    }

    console.warn('[GeospatialTool] Gemini returned no usable location; falling back to MCP')
  } catch (e) {
    console.warn('[GeospatialTool] Gemini failed; falling back to MCP', e)
  }
}
// continue to MCP flow

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.


let feedbackMessage = `Processing geospatial query (type: ${queryType})... Connecting to mapping service...`;
uiFeedbackStream.update(feedbackMessage);

Expand Down
12 changes: 6 additions & 6 deletions lib/agents/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { searchTool } from './search'
import { videoSearchTool } from './video-search'
import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp import

import { MapProvider } from '@/lib/store/settings'

export interface ToolProps {
uiStream: ReturnType<typeof createStreamableUI>
fullResponse: string
// mcp?: any; // Removed mcp property as it's no longer passed down for geospatialTool
mapProvider?: MapProvider
}

// Removed mcp from parameters
export const getTools = ({ uiStream, fullResponse }: ToolProps) => {
export const getTools = ({ uiStream, fullResponse, mapProvider }: ToolProps) => {
const tools: any = {
search: searchTool({
uiStream,
Expand All @@ -21,10 +22,9 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => {
uiStream,
fullResponse
}),
// geospatialTool now only requires uiStream
geospatialQueryTool: geospatialTool({
uiStream
// mcp: mcp || null // Removed mcp argument
uiStream,
mapProvider
})
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@ai-sdk/xai": "^1.2.18",
"@composio/core": "^0.3.3",
"@google-cloud/storage": "^7.18.0",
"@google/generative-ai": "^0.24.1",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "3.3.4",
"@mapbox/mapbox-gl-draw": "^1.5.0",
Expand Down