diff --git a/app/actions.tsx b/app/actions.tsx index 167d23d4..4cd8b38b 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -30,7 +30,7 @@ import { MapQueryHandler } from '@/components/map/map-query-handler' // Define the type for related queries type RelatedQueries = { - items: { query: string }[] + items: string[] } async function submit(formData?: FormData, skip?: boolean) { diff --git a/components/search-related-optimized.tsx b/components/search-related-optimized.tsx index b2e1c701..3ee2900b 100644 --- a/components/search-related-optimized.tsx +++ b/components/search-related-optimized.tsx @@ -50,8 +50,8 @@ export const SearchRelated: React.FC = React.memo(({ // OPTIMIZATION: Memoize filtered and mapped items const relatedItems = useMemo(() => { return data?.items - ?.filter(item => item?.query !== '') - .map((item, index) => ( + ?.filter(query => query !== '') + .map((query, index) => (
= React.memo(({
)) || [] diff --git a/components/search-related.tsx b/components/search-related.tsx index b2e1c701..3ee2900b 100644 --- a/components/search-related.tsx +++ b/components/search-related.tsx @@ -50,8 +50,8 @@ export const SearchRelated: React.FC = React.memo(({ // OPTIMIZATION: Memoize filtered and mapped items const relatedItems = useMemo(() => { return data?.items - ?.filter(item => item?.query !== '') - .map((item, index) => ( + ?.filter(query => query !== '') + .map((query, index) => (
= React.memo(({
)) || [] diff --git a/components/suggestions-dropdown.tsx b/components/suggestions-dropdown.tsx index 33be6e48..99f23921 100644 --- a/components/suggestions-dropdown.tsx +++ b/components/suggestions-dropdown.tsx @@ -23,7 +23,7 @@ export const SuggestionsDropdown: React.FC = ({ const dropdownRef = useRef(null) const suggestionItems = useMemo( - () => suggestions?.items?.filter(item => item?.query) || [], + () => (suggestions?.items?.filter(query => query) as string[]) || [], [suggestions] ) @@ -47,7 +47,7 @@ export const SuggestionsDropdown: React.FC = ({ } else if (e.key === 'Enter') { e.preventDefault() if (selectedIndex >= 0 && suggestionItems[selectedIndex]) { - onSelect(suggestionItems[selectedIndex].query!) + onSelect(suggestionItems[selectedIndex]) } } else if (e.key === 'Escape') { onClose() @@ -92,8 +92,8 @@ export const SuggestionsDropdown: React.FC = ({ >

Suggestions

- {suggestionItems.map((item, index) => { - if (!item?.query) return null + {suggestionItems.map((query, index) => { + if (!query) return null return ( ) })} diff --git a/lib/agents/query-suggestor.tsx b/lib/agents/query-suggestor.tsx index 7cb8e50c..f7cd53eb 100644 --- a/lib/agents/query-suggestor.tsx +++ b/lib/agents/query-suggestor.tsx @@ -57,7 +57,7 @@ export async function querySuggestor( // OPTIMIZATION: Use a more concise system prompt to reduce token usage const result = await streamObject({ model: (await getModel()) as LanguageModel, - system: `Generate 3 follow-up queries that explore the subject matter deeper. Format as JSON with an "items" array containing objects with "query" fields. Keep queries concise and relevant.`, + system: `Generate 3 follow-up queries that explore the subject matter deeper. Format as JSON with an "items" array containing strings. Keep queries concise and relevant.`, messages, schema: relatedSchema, temperature: 0.7, // Lower temperature for more consistent results diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index 863f59b5..769ca098 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -241,7 +241,7 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g , parameters: geospatialQuerySchema, execute: async (params: z.infer) => { - const { queryType, includeMap = true } = params; + const { queryType, includeMap = true, latitude, longitude } = params; console.log('[GeospatialTool] Execute called with:', params, 'and map provider:', mapProvider); const uiFeedbackStream = createStreamableValue(); @@ -275,19 +275,19 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g // 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 { latitude: lat, longitude: lng } = place.coordinates; const place_name = place.displayName; const mcpData: McpResponse = { location: { - latitude, - longitude, + latitude: lat, + longitude: lng, place_name, }, }; if (mapProvider === 'google') { - mcpData.mapUrl = getGoogleStaticMapUrl(latitude, longitude); + mcpData.mapUrl = getGoogleStaticMapUrl(lat, lng); } feedbackMessage = `Found location: ${place_name}`; @@ -355,12 +355,18 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g return { places: [params.origin, params.destination], includeMapPreview: includeMap, mode: params.mode || 'driving' }; } case 'reverse': { - if (!params.coordinates) throw new Error("'reverse' query requires coordinates"); - return { searchText: `${params.coordinates.latitude},${params.coordinates.longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 }; + if (latitude === undefined || longitude === undefined) throw new Error("'reverse' query requires latitude and longitude"); + return { searchText: `${latitude},${longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 }; } case 'search': { if (!params.query) throw new Error("'search' query requires query"); - return { searchText: params.query, includeMapPreview: includeMap, maxResults: params.maxResults || 5, ...(params.coordinates && { proximity: `${params.coordinates.latitude},${params.coordinates.longitude}` }), ...(params.radius && { radius: params.radius }) }; + return { + searchText: params.query, + includeMapPreview: includeMap, + maxResults: params.maxResults || 5, + ...(latitude !== undefined && longitude !== undefined && { proximity: `${latitude},${longitude}` }), + ...(params.radius && { radius: params.radius }) + }; } case 'geocode': case 'map': { diff --git a/lib/schema/README.md b/lib/schema/README.md new file mode 100644 index 00000000..50644b4d --- /dev/null +++ b/lib/schema/README.md @@ -0,0 +1,22 @@ +# AI Schema Compatibility Guidelines + +To ensure maximum compatibility with OpenAI-compatible endpoints (e.g., xAI, Grok, Gemini), all Zod schemas used for structured AI output must follow these rules. + +## Core Rules + +1. **Avoid `z.literal()`**: Some endpoints do not support the `const` constraint in JSON Schema generated by `z.literal()`. Use `z.string()` and provide allowed values in the field description. +2. **Avoid `z.any()`**: Use explicit union types or more specific schemas to ensure validation reliability. +3. **Prefer Flat Optional Fields**: Avoid deeply nested required objects or complex discriminated unions. Instead, use a flat object with optional fields. +4. **Use Field Descriptions**: Convey conditional requirements (e.g., "Field X is required if `queryType` is 'search'") via the `.describe()` method on fields. +5. **Runtime Reconstruction**: If the frontend or downstream tools require a nested format (like standard GeoJSON), implement a reconstruction step in the application logic (e.g., in server actions) to transform the flat AI output into the desired structure. + +## Canonical Examples + +### Flat Schema with Conditional Requirements +See `lib/schema/geospatial.tsx` for how `queryType` guides the use of other optional top-level fields. + +### Avoiding Const and Nested GeoJSON +See `lib/schema/resolution-search.ts` for a flattened GeoJSON structure that avoids `z.literal()` and `z.any()`. + +### Runtime Reconstruction +See `app/actions.tsx` (specifically the `processResolutionSearch` function) for how flattened features are reconstructed into a standard `FeatureCollection`. diff --git a/lib/schema/geospatial.tsx b/lib/schema/geospatial.tsx index c58b7f0a..4c6161e5 100644 --- a/lib/schema/geospatial.tsx +++ b/lib/schema/geospatial.tsx @@ -5,14 +5,18 @@ import { z } from 'zod'; // queryType are conveyed to the LLM via the queryType description; runtime // behavior in the tool's execute() already tolerates missing fields per // queryType, so loosening the schema introduces no new failure modes. +// +// Compatibility Note: Following the established flattening pattern, +// nested 'coordinates' object has been replaced with top-level 'latitude' +// and 'longitude' fields. export const geospatialQuerySchema = z.object({ queryType: z.enum(['search', 'geocode', 'reverse', 'directions', 'distance', 'map']) .describe( "Type of geospatial query. Set the corresponding fields: " + - "'search' → query (optionally coordinates, radius, maxResults); " + + "'search' → query (optionally latitude/longitude, radius, maxResults); " + "'geocode' → location (optionally maxResults); " + - "'reverse' → coordinates (optionally maxResults); " + + "'reverse' → latitude + longitude (optionally maxResults); " + "'directions' → origin + destination (optionally mode); " + "'distance' → origin + destination (optionally mode); " + "'map' → location." @@ -25,12 +29,10 @@ export const geospatialQuerySchema = z.object({ .min(1) .optional() .describe("Location to geocode or render as a map (used by 'geocode' and 'map')"), - coordinates: z.object({ - latitude: z.number().min(-90).max(90), - longitude: z.number().min(-180).max(180) - }) - .optional() - .describe("Coordinates (required for 'reverse', optional proximity hint for 'search')"), + latitude: z.number().min(-90).max(90).optional() + .describe("Latitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"), + longitude: z.number().min(-180).max(180).optional() + .describe("Longitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"), origin: z.string() .min(1) .optional() diff --git a/lib/schema/related.tsx b/lib/schema/related.tsx index 77fc3835..80c6d9f7 100644 --- a/lib/schema/related.tsx +++ b/lib/schema/related.tsx @@ -1,15 +1,12 @@ import { DeepPartial } from 'ai' import { z } from 'zod' +// Simplified schema to reduce nested complexity for better model compatibility. +// Items are now plain strings instead of objects with a 'query' property. export const relatedSchema = z.object({ - items: z - .array( - z.object({ - query: z.string() - }) - ) - .length(3) + items: z.array(z.string()).length(3) }) + export type PartialRelated = DeepPartial export type Related = z.infer