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
27 changes: 21 additions & 6 deletions lib/agents/tools/geospatial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,27 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g
// Build arguments
const toolArgs = (() => {
switch (queryType) {
case 'directions': return { waypoints: [params.origin, params.destination], includeMapPreview: includeMap, profile: params.mode };
case 'distance': return { places: [params.origin, params.destination], includeMapPreview: includeMap, mode: params.mode || 'driving' };
case 'reverse': return { searchText: `${params.coordinates.latitude},${params.coordinates.longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 };
case 'search': return { searchText: params.query, includeMapPreview: includeMap, maxResults: params.maxResults || 5, ...(params.coordinates && { proximity: `${params.coordinates.latitude},${params.coordinates.longitude}` }), ...(params.radius && { radius: params.radius }) };
case 'geocode':
case 'map': return { searchText: params.location, includeMapPreview: includeMap, maxResults: queryType === 'geocode' ? params.maxResults || 5 : undefined };
case 'directions': {
if (!params.origin || !params.destination) throw new Error("'directions' query requires origin and destination");
return { waypoints: [params.origin, params.destination], includeMapPreview: includeMap, profile: params.mode };
}
case 'distance': {
if (!params.origin || !params.destination) throw new Error("'distance' query requires origin and destination");
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 };
}
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 }) };
}
case 'geocode':
case 'map': {
if (!params.location) throw new Error(`'${queryType}' query requires location`);
return { searchText: params.location, includeMapPreview: includeMap, maxResults: queryType === 'geocode' ? params.maxResults || 5 : undefined };
}
}
})();

Expand Down
174 changes: 57 additions & 117 deletions lib/schema/geospatial.tsx
Original file line number Diff line number Diff line change
@@ -1,124 +1,64 @@
import { z } from 'zod';

// Improved schema using discriminatedUnion for better type safety and conditional requirements
// - Enforces required fields based on queryType (e.g., destination for directions/distance)
// - Renames 'query' to 'location' for clarity in most cases, but uses 'origin' and 'destination' for directions/distance
// - Makes 'coordinates' required for 'reverse' and optional for 'search' (as proximity)
// - Adds 'mode' for directions (e.g., driving, walking) assuming tool support can be added
// - Integrates 'radius' and 'maxResults' for 'search', assuming future tool arg expansion
// - Keeps 'includeMap' consistent across all
// - Defaults queryType removed; now required to encourage explicit typing
// - For 'map', treats as general query similar to geocode/search
// Flat schema. JSON Schema output is a single object with optional fields,
// so OpenAI-compatible endpoints (xAI) accept it. Field requirements per
// 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.
Comment thread
itsautomata marked this conversation as resolved.

export const geospatialQuerySchema = z.discriminatedUnion('queryType', [
z.object({
queryType: z.literal('search'),
query: z.string()
.min(1, "Query cannot be empty")
.describe("Search term for places/POIs"),
coordinates: z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180)
})
.optional()
.describe("Optional reference point for proximity search"),
radius: z.number()
.positive()
.optional()
.describe("Search radius in kilometers"),
maxResults: z.number()
.int()
.positive()
.max(20)
.optional()
.default(5)
.describe("Maximum number of results to return"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
}),
z.object({
queryType: z.literal('geocode'),
location: z.string()
.min(1, "Location cannot be empty")
.describe("The location to geocode - address, place name, or landmark"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
maxResults: z.number()
.int()
.positive()
.max(20)
.optional()
.default(5)
.describe("Maximum number of results to return"),
}),
z.object({
queryType: z.literal('reverse'),
coordinates: z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180)
})
.describe("Coordinates for reverse geocoding"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
maxResults: z.number()
.int()
.positive()
.max(20)
.optional()
.default(5)
.describe("Maximum number of results to return"),
}),
z.object({
queryType: z.literal('directions'),
origin: z.string()
.min(1, "Origin cannot be empty")
.describe("Starting location for directions"),
destination: z.string()
.min(1, "Destination cannot be empty")
.describe("Ending location for directions"),
mode: z.enum(['driving', 'walking', 'cycling', 'transit'])
.optional()
.default('driving')
.describe("Transportation mode for directions"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
}),
z.object({
queryType: z.literal('distance'),
origin: z.string()
.min(1, "Origin cannot be empty")
.describe("Starting location for distance calculation"),
destination: z.string()
.min(1, "Destination cannot be empty")
.describe("Ending location for distance calculation"),
mode: z.enum(['driving', 'walking', 'cycling', 'transit'])
.optional()
.default('driving')
.describe("Transportation mode for distance"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
}),
z.object({
queryType: z.literal('map'),
location: z.string()
.min(1, "Location cannot be empty")
.describe("Location or area for map request"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
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); " +
"'geocode' → location (optionally maxResults); " +
"'reverse' → coordinates (optionally maxResults); " +
"'directions' → origin + destination (optionally mode); " +
"'distance' → origin + destination (optionally mode); " +
"'map' → location."
),
query: z.string()
.min(1)
.optional()
.describe("Search term for places/POIs (used by 'search')"),
location: z.string()
.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')"),
origin: z.string()
.min(1)
.optional()
.describe("Starting location (used by 'directions' and 'distance')"),
destination: z.string()
.min(1)
.optional()
.describe("Ending location (used by 'directions' and 'distance')"),
mode: z.enum(['driving', 'walking', 'cycling', 'transit'])
.optional()
.default('driving')
.describe("Transportation mode (used by 'directions' and 'distance')"),
radius: z.number()
.positive()
.optional()
.describe("Search radius in kilometers (used by 'search')"),
maxResults: z.number()
.int()
.positive()
.max(20)
.optional()
.default(5)
.describe("Maximum number of results to return"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
});

export type GeospatialQuery = z.infer<typeof geospatialQuerySchema>;

Expand Down