diff --git a/app/actions.tsx b/app/actions.tsx index a1f5e915..0c6923bb 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -12,7 +12,12 @@ import type { FeatureCollection } from 'geojson' import { Spinner } from '@/components/ui/spinner' import { Section } from '@/components/section' import { FollowupPanel } from '@/components/followup-panel' -import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents' +import { inquire } from '@/lib/agents/inquire' +import { researcher } from '@/lib/agents/researcher' +import { taskManager } from '@/lib/agents/task-manager' +import { querySuggestor } from '@/lib/agents/query-suggestor' +import { resolutionSearch } from '@/lib/agents/resolution-search' +import { type DrawnFeature } from '@/lib/types/geospatial' import { writer } from '@/lib/agents/writer' import { saveChat, getSystemPrompt } from '@/lib/actions/chat' import { Chat, AIMessage } from '@/lib/types' diff --git a/lib/actions/suggest.ts b/lib/actions/suggest.ts index 8555461c..ad0b4436 100644 --- a/lib/actions/suggest.ts +++ b/lib/actions/suggest.ts @@ -3,7 +3,7 @@ import { createStreamableUI, createStreamableValue } from 'ai/rsc' import { CoreMessage, LanguageModel, streamObject } from 'ai' import { PartialRelated, relatedSchema } from '@/lib/schema/related' -import { getModel } from '../utils' +import { getModel } from '../utils/ai-model' import { MapData } from '@/components/map/map-data-context' export async function getSuggestions( diff --git a/lib/agents/index.tsx b/lib/agents/index.tsx deleted file mode 100644 index 7f207e4f..00000000 --- a/lib/agents/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export * from './task-manager' -export * from './inquire' -export * from './query-suggestor' -export * from './researcher' -export * from './resolution-search' diff --git a/lib/agents/inquire.tsx b/lib/agents/inquire.tsx index e15926b7..1ed18747 100644 --- a/lib/agents/inquire.tsx +++ b/lib/agents/inquire.tsx @@ -1,8 +1,10 @@ +'use server' + import { Copilot } from '@/components/copilot'; import { createStreamableUI, createStreamableValue } from 'ai/rsc'; import { CoreMessage, LanguageModel, streamObject } from 'ai'; import { PartialInquiry, inquirySchema } from '@/lib/schema/inquiry'; -import { getModel } from '../utils'; +import { getModel } from '../utils/ai-model'; // Define a plain object type for the inquiry prop interface InquiryProp { diff --git a/lib/agents/query-suggestor.tsx b/lib/agents/query-suggestor.tsx index de2b3749..446a92c2 100644 --- a/lib/agents/query-suggestor.tsx +++ b/lib/agents/query-suggestor.tsx @@ -1,9 +1,11 @@ +'use server' + import { createStreamableUI, createStreamableValue } from 'ai/rsc' import { CoreMessage, LanguageModel, streamObject } from 'ai' import { PartialRelated, relatedSchema } from '@/lib/schema/related' import { Section } from '@/components/section' import SearchRelated from '@/components/search-related' -import { getModel } from '../utils' +import { getModel } from '../utils/ai-model' export async function querySuggestor( uiStream: ReturnType, diff --git a/lib/agents/researcher.tsx b/lib/agents/researcher.tsx index ce801af4..ce8108e7 100644 --- a/lib/agents/researcher.tsx +++ b/lib/agents/researcher.tsx @@ -1,3 +1,5 @@ +'use server' + // lib/agents/researcher.tsx import { createStreamableUI, createStreamableValue } from 'ai/rsc' import { @@ -10,9 +12,9 @@ import { import { Section } from '@/components/section' import { BotMessage } from '@/components/message' import { getTools } from './tools' -import { getModel } from '../utils' +import { getModel } from '../utils/ai-model' import { MapProvider } from '@/lib/store/settings' -import { DrawnFeature } from './resolution-search' +import { type DrawnFeature } from '@/lib/types/geospatial' // This magic tag lets us write raw multi-line strings with backticks, arrows, etc. const raw = String.raw diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx index 737551e8..1f7fbf8a 100644 --- a/lib/agents/resolution-search.tsx +++ b/lib/agents/resolution-search.tsx @@ -1,8 +1,9 @@ +'use server' + import { CoreMessage, streamObject } from 'ai' -import { getModel } from '@/lib/utils' +import { getModel } from '@/lib/utils/ai-model' import { z } from 'zod' - -// This agent is now a pure data-processing module, with no UI dependencies. +import { type DrawnFeature } from '@/lib/types/geospatial' // Define the schema for the structured response from the AI. const resolutionSearchSchema = z.object({ @@ -23,13 +24,6 @@ const resolutionSearchSchema = z.object({ }).describe('A GeoJSON object containing points of interest and classified land features to be overlaid on the map.'), }) -export interface DrawnFeature { - id: string; - type: 'Polygon' | 'LineString'; - measurement: string; - geometry: any; -} - export async function resolutionSearch(messages: CoreMessage[], timezone: string = 'UTC', drawnFeatures?: DrawnFeature[]) { const localTime = new Date().toLocaleString('en-US', { timeZone: timezone, @@ -63,15 +57,16 @@ Analyze the user's prompt and the image to provide a holistic understanding of t const filteredMessages = messages.filter(msg => msg.role !== 'system'); - // Check if any message contains an image (resolution search is specifically for image analysis) + // Check if any message contains an image const hasImage = messages.some(message => Array.isArray(message.content) && message.content.some(part => part.type === 'image') ) - // Use streamObject to get partial results. + const model = await getModel(hasImage); + return streamObject({ - model: await getModel(hasImage), + model, system: systemPrompt, messages: filteredMessages, schema: resolutionSearchSchema, diff --git a/lib/agents/task-manager.tsx b/lib/agents/task-manager.tsx index 90a72b67..3328adec 100644 --- a/lib/agents/task-manager.tsx +++ b/lib/agents/task-manager.tsx @@ -1,6 +1,8 @@ +'use server' + import { CoreMessage, generateObject, LanguageModel } from 'ai' import { nextActionSchema } from '../schema/next-action' -import { getModel } from '../utils' +import { getModel } from '../utils/ai-model' // Decide whether inquiry is required for the user input export async function taskManager(messages: CoreMessage[]) { diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index ca5f9f49..35ae7d03 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -4,245 +4,76 @@ import { createStreamableUI, createStreamableValue } from 'ai/rsc'; import { BotMessage } from '@/components/message'; import { geospatialQuerySchema } from '@/lib/schema/geospatial'; -import { Client as MCPClientClass } from '@modelcontextprotocol/sdk/client/index.js'; -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'; +import { type McpResponse } from '@/lib/types/geospatial'; -// Types -export type McpClient = MCPClientClass; - -interface Location { - latitude?: number; - longitude?: number; - place_name?: string; - address?: string; -} - -interface McpResponse { - location: Location; - mapUrl?: string; -} - -interface MapboxConfig { - mapboxAccessToken: string; - version: string; - name: string; +function getGoogleStaticMapUrl(lat: number, lng: number): string { + const apiKey = process.env.GOOGLE_MAPS_API_KEY; + if (!apiKey) return ''; + return `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=14&size=600x300&maptype=roadmap&markers=color:red%7C${lat},${lng}&key=${apiKey}`; } /** * Establish connection to the MCP server with proper environment validation. */ -async function getConnectedMcpClient(): Promise { +async function getConnectedMcpClient(): Promise { const composioApiKey = process.env.COMPOSIO_API_KEY; const mapboxAccessToken = process.env.MAPBOX_ACCESS_TOKEN; const composioUserId = process.env.COMPOSIO_USER_ID; - console.log('[GeospatialTool] Environment check:', { - composioApiKey: composioApiKey ? `${composioApiKey.substring(0, 8)}...` : 'MISSING', - mapboxAccessToken: mapboxAccessToken ? `${mapboxAccessToken.substring(0, 8)}...` : 'MISSING', - composioUserId: composioUserId ? `${composioUserId.substring(0, 8)}...` : 'MISSING', - }); - if (!composioApiKey || !mapboxAccessToken || !composioUserId || !composioApiKey.trim() || !mapboxAccessToken.trim() || !composioUserId.trim()) { console.error('[GeospatialTool] Missing or empty required environment variables'); return null; } - // Load config from file or fallback - let config; - try { - // Use static import for config - let mapboxMcpConfig; - try { - mapboxMcpConfig = require('../../../mapbox_mcp_config.json'); - config = { ...mapboxMcpConfig, mapboxAccessToken }; - console.log('[GeospatialTool] Config loaded successfully'); - } catch (configError: any) { - throw configError; - } - } catch (configError: any) { - console.error('[GeospatialTool] Failed to load mapbox config:', configError.message); - config = { mapboxAccessToken, version: '1.0.0', name: 'mapbox-mcp-server' }; - console.log('[GeospatialTool] Using fallback config'); - } - - // Build Composio MCP server URL - // Note: This should be migrated to use Composio SDK directly instead of MCP client - // For now, constructing URL directly without Smithery SDK - let serverUrlToUse: URL; - try { - // Construct URL with Composio credentials - const baseUrl = 'https://api.composio.dev/v1/mcp/mapbox'; - serverUrlToUse = new URL(baseUrl); - serverUrlToUse.searchParams.set('api_key', composioApiKey); - serverUrlToUse.searchParams.set('user_id', composioUserId); - - const urlDisplay = serverUrlToUse.toString().split('?')[0]; - console.log('[GeospatialTool] Composio MCP Server URL created:', urlDisplay); - - if (!serverUrlToUse.href || !serverUrlToUse.href.startsWith('https://')) { - throw new Error('Invalid server URL generated'); - } - } catch (urlError: any) { - console.error('[GeospatialTool] Error creating Composio URL:', urlError.message); - return null; - } - - // Create transport - let transport; - try { - transport = new StreamableHTTPClientTransport(serverUrlToUse); - console.log('[GeospatialTool] Transport created successfully'); - } catch (transportError: any) { - console.error('[GeospatialTool] Failed to create transport:', transportError.message); - return null; - } - - // Create client - let client; - try { - client = new MCPClientClass({ name: 'GeospatialToolClient', version: '1.0.0' }); - console.log('[GeospatialTool] MCP Client instance created'); - } catch (clientError: any) { - console.error('[GeospatialTool] Failed to create MCP client:', clientError.message); - return null; - } - - // Connect to server try { - console.log('[GeospatialTool] Attempting to connect to MCP server...'); - await Promise.race([ - client.connect(transport), - new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout after 15 seconds')), 15000)), - ]); - console.log('[GeospatialTool] Successfully connected to MCP server'); - } catch (connectError: any) { - console.error('[GeospatialTool] MCP connection failed:', connectError.message); + // Dynamic imports to avoid Webpack issues with MCP SDK in production + const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); + const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js'); + + const authConfigId = process.env.COMPOSIO_MAPBOX_AUTH_CONFIG_ID || 'mapbox'; + const baseUrl = 'https://backend.composio.dev/mcp/client/streamable'; + const url = `${baseUrl}?userId=${composioUserId}&authConfigId=${authConfigId}&mapboxApiKey=${mapboxAccessToken}&composioApiKey=${composioApiKey}`; + + const transport = new StreamableHTTPClientTransport(new URL(url)); + const client = new Client( + { name: 'mapbox-mcp-client', version: '1.0.0' }, + { capabilities: {} } + ); + + await client.connect(transport); + return client; + } catch (error) { + console.error('[GeospatialTool] Failed to connect to MCP server:', error); return null; } - - // List tools - try { - const tools = await client.listTools(); - console.log('[GeospatialTool] Available tools:', tools.tools?.map(t => t.name) || []); - } catch (listError: any) { - console.warn('[GeospatialTool] Could not list tools:', listError.message); - } - - return client; } -/** - * Safely close the MCP client with timeout. - */ -async function closeClient(client: McpClient | null) { - if (!client) return; - try { - await Promise.race([ - client.close(), - new Promise((_, reject) => setTimeout(() => reject(new Error('Close timeout after 5 seconds')), 5000)), - ]); - console.log('[GeospatialTool] MCP client closed successfully'); - } catch (error: any) { - console.error('[GeospatialTool] Error closing MCP client:', error.message); +async function closeClient(client: any) { + if (client) { + try { + await client.close(); + } catch (error) { + console.warn('[GeospatialTool] Error closing client:', error); + } } } -/** - * Helper to generate a Google Static Map URL - */ -function getGoogleStaticMapUrl(latitude: number, longitude: number): string { - const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || process.env.GOOGLE_MAPS_API_KEY; - if (!apiKey) return ''; - return `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&zoom=15&size=640x480&scale=2&markers=color:red%7C${latitude},${longitude}&key=${apiKey}`; -} - -/** - * Main geospatial tool executor. - */ -export const geospatialTool = ({ - uiStream, - mapProvider -}: { - uiStream: ReturnType - 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. - -Static image tool: - -Generates static map images using the Mapbox static image API. Features include: - -Custom map styles (streets, outdoors, satellite, etc.) -Adjustable image dimensions and zoom levels -Support for multiple markers with custom colors and labels -Overlay options including polylines and polygons -Auto-fitting to specified coordinates - -Category search tool: - -Performs a category search using the Mapbox Search Box category search API. Features include: -Search for points of interest by category (restaurants, hotels, gas stations, etc.) -Filtering by geographic proximity -Customizable result limits -Rich metadata for each result -Support for multiple languages - -Reverse geocoding tool: - -Performs reverse geocoding using the Mapbox geocoding V6 API. Features include: -Convert geographic coordinates to human-readable addresses -Customizable levels of detail (street, neighborhood, city, etc.) -Results filtering by type (address, poi, neighborhood, etc.) -Support for multiple languages -Rich location context information - -Directions tool: - -Fetches routing directions using the Mapbox Directions API. Features include: - -Support for different routing profiles: driving (with live traffic or typical), walking, and cycling -Route from multiple waypoints (2-25 coordinate pairs) -Alternative routes option -Route annotations (distance, duration, speed, congestion) - -Scheduling options: - -Future departure time (depart_at) for driving and driving-traffic profiles -Desired arrival time (arrive_by) for driving profile only -Profile-specific optimizations: -Driving: vehicle dimension constraints (height, width, weight) -Exclusion options for routing: -Common exclusions: ferry routes, cash-only tolls -Driving-specific exclusions: tolls, motorways, unpaved roads, tunnels, country borders, state borders -Custom point exclusions (up to 50 geographic points to avoid) -GeoJSON geometry output format - -Isochrone tool: - -Computes areas that are reachable within a specified amount of times from a location using Mapbox Isochrone API. Features include: - -Support for different travel profiles (driving, walking, cycling) -Customizable travel times or distances -Multiple contour generation (e.g., 15, 30, 45 minute ranges) -Optional departure or arrival time specification -Color customization for visualization - -Search and geocode tool: -Uses the Mapbox Search Box Text Search API endpoint to power searching for and geocoding POIs, addresses, places, and any other types supported by that API. This tool consolidates the functionality that was previously provided by the ForwardGeocodeTool and PoiSearchTool (from earlier versions of this MCP server) into a single tool.` - - -, +export const geospatialTool = ({ uiStream, mapProvider }: { uiStream: any, mapProvider?: MapProvider }) => ({ + description: `Geospatial query tool for mapping, geocoding, and spatial analysis. + Use this tool for: + - Finding coordinates for a location + - Getting directions between places + - Searching for nearby points of interest + - Calculating distances + - Reverse geocoding (finding address from coordinates)`, parameters: geospatialQuerySchema, execute: async (params: z.infer) => { const { queryType, includeMap = true } = params; - console.log('[GeospatialTool] Execute called with:', params, 'and map provider:', mapProvider); const uiFeedbackStream = createStreamableValue(); uiStream.append(); @@ -250,8 +81,7 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g const selectedModel = await getSelectedModel(); if (selectedModel?.includes('gemini') && mapProvider === 'google') { - let feedbackMessage = `Processing geospatial query with Gemini...`; - uiFeedbackStream.update(feedbackMessage); + uiFeedbackStream.update(`Processing geospatial query with Gemini...`); try { const genAI = new GoogleGenerativeAI(process.env.GEMINI_3_PRO_API_KEY!); @@ -271,8 +101,6 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g 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; @@ -290,8 +118,7 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g mcpData.mapUrl = getGoogleStaticMapUrl(latitude, longitude); } - feedbackMessage = `Found location: ${place_name}`; - uiFeedbackStream.update(feedbackMessage); + uiFeedbackStream.update(`Found location: ${place_name}`); uiFeedbackStream.done(); uiStream.update(); return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: mcpData, error: null }; @@ -301,20 +128,17 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g } catch (error: any) { const toolError = `Gemini grounding error: ${error.message}`; uiFeedbackStream.update(toolError); - console.error('[GeospatialTool] Gemini execution failed:', error); uiFeedbackStream.done(); uiStream.update(); return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: null, error: toolError }; } } - let feedbackMessage = `Processing geospatial query (type: ${queryType})... Connecting to mapping service...`; - uiFeedbackStream.update(feedbackMessage); + uiFeedbackStream.update(`Connecting to mapping service...`); const mcpClient = await getConnectedMcpClient(); if (!mcpClient) { - feedbackMessage = 'Geospatial functionality is unavailable. Please check configuration.'; - uiFeedbackStream.update(feedbackMessage); + uiFeedbackStream.update('Geospatial functionality is unavailable. Please check configuration.'); uiFeedbackStream.done(); uiStream.update(); return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), timestamp: new Date().toISOString(), mcp_response: null, error: 'MCP client initialization failed' }; @@ -324,26 +148,26 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g let toolError: string | null = null; try { - feedbackMessage = `Connected to mapping service. Processing ${queryType} query...`; - uiFeedbackStream.update(feedbackMessage); + uiFeedbackStream.update(`Processing ${queryType} query...`); - // Pick appropriate tool const toolName = await (async () => { const { tools } = await mcpClient.listTools().catch(() => ({ tools: [] })); const names = new Set(tools?.map((t: any) => t.name) || []); const prefer = (...cands: string[]) => cands.find(n => names.has(n)); switch (queryType) { - case 'directions': return prefer('directions_tool') + case 'directions': return prefer('directions_tool'); case 'distance': return prefer('matrix_tool'); - case 'search': return prefer( 'isochrone_tool','category_search_tool') || 'poi_search_tool'; - case 'map': return prefer('static_map_image_tool') + case 'search': return prefer('isochrone_tool','category_search_tool') || 'poi_search_tool'; + case 'map': return prefer('static_map_image_tool'); case 'reverse': return prefer('reverse_geocode_tool'); case 'geocode': return prefer('forward_geocode_tool'); + default: return null; } })(); - // Build arguments + if (!toolName) throw new Error(`No suitable tool found for query type: ${queryType}`); + const toolArgs = (() => { switch (queryType) { case 'directions': return { waypoints: [params.origin, params.destination], includeMapPreview: includeMap, profile: params.mode }; @@ -352,45 +176,27 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g 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 }; + default: return {}; } })(); - console.log('[GeospatialTool] Calling tool:', toolName, 'with args:', toolArgs); - - // Retry logic - const MAX_RETRIES = 3; - let retryCount = 0; - let toolCallResult; - while (retryCount < MAX_RETRIES) { - try { - toolCallResult = await Promise.race([ - mcpClient.callTool({ name: toolName ?? 'unknown_tool', arguments: toolArgs }), - new Promise((_, reject) => setTimeout(() => reject(new Error('Tool call timeout')), 30000)), - ]); - break; - } catch (error: any) { - retryCount++; - if (retryCount === MAX_RETRIES) throw new Error(`Tool call failed after ${MAX_RETRIES} retries: ${error.message}`); - console.warn(`[GeospatialTool] Retry ${retryCount}/${MAX_RETRIES}: ${error.message}`); - await new Promise(resolve => setTimeout(resolve, 1000)); - } - } + const toolCallResult = await mcpClient.callTool({ name: toolName, arguments: toolArgs }); - // Extract & parse content - const serviceResponse = toolCallResult as { content?: Array<{ text?: string | null } | { [k: string]: any }> }; + const serviceResponse = toolCallResult as any; const blocks = serviceResponse?.content || []; - const textBlocks = blocks.map(b => (typeof b.text === 'string' ? b.text : null)).filter((t): t is string => !!t && t.trim().length > 0); + const textBlocks = blocks.map((b: any) => (typeof b.text === 'string' ? b.text : null)).filter((t: string | null): t is string => !!t); + if (textBlocks.length === 0) throw new Error('No content returned from mapping service'); - let content: any = textBlocks.find(t => t.startsWith('```json')) || textBlocks[0]; + let contentStr = textBlocks.find((t: string) => t.startsWith('```json')) || textBlocks[0]; const jsonRegex = /```(?:json)?\n?([\s\S]*?)\n?```/; - const match = content.match(jsonRegex); - if (match) content = match[1].trim(); + const match = contentStr.match(jsonRegex); + if (match) contentStr = match[1].trim(); - try { content = JSON.parse(content); } - catch { console.warn('[GeospatialTool] Content is not JSON, using as string:', content); } + let content; + try { content = JSON.parse(contentStr); } + catch { content = contentStr; } - // Process results if (typeof content === 'object' && content !== null) { const parsedData = content as any; if (parsedData.results?.length > 0) { @@ -403,10 +209,8 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g } } else throw new Error('Unexpected response format from mapping service'); - feedbackMessage = `Successfully processed ${queryType} query for: ${mcpData.location.place_name || JSON.stringify(params)}`; - uiFeedbackStream.update(feedbackMessage); + uiFeedbackStream.update(`Successfully processed query for: ${mcpData.location.place_name}`); - // Enhance with Google Static Map URL if provider is google and we have coordinates if (mapProvider === 'google' && mcpData.location.latitude && mcpData.location.longitude && !mcpData.mapUrl) { mcpData.mapUrl = getGoogleStaticMapUrl(mcpData.location.latitude, mcpData.location.longitude); } @@ -414,7 +218,6 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g } catch (error: any) { toolError = `Mapping service error: ${error.message}`; uiFeedbackStream.update(toolError); - console.error('[GeospatialTool] Tool execution failed:', error); } finally { await closeClient(mcpClient); uiFeedbackStream.done(); diff --git a/lib/agents/writer.tsx b/lib/agents/writer.tsx index f4e4d0ac..a18eb0c8 100644 --- a/lib/agents/writer.tsx +++ b/lib/agents/writer.tsx @@ -1,8 +1,10 @@ +'use server' + import { createStreamableUI, createStreamableValue } from 'ai/rsc' import { CoreMessage, LanguageModel, streamText as nonexperimental_streamText } from 'ai' import { Section } from '@/components/section' import { BotMessage } from '@/components/message' -import { getModel } from '../utils' +import { getModel } from '../utils/ai-model' export async function writer( dynamicSystemPrompt: string, // New parameter diff --git a/lib/types/geospatial.ts b/lib/types/geospatial.ts new file mode 100644 index 00000000..7405046f --- /dev/null +++ b/lib/types/geospatial.ts @@ -0,0 +1,18 @@ +export interface DrawnFeature { + id: string; + type: 'Polygon' | 'LineString'; + measurement: string; + geometry: any; +} + +export interface Location { + latitude?: number; + longitude?: number; + place_name?: string; + address?: string; +} + +export interface McpResponse { + location: Location; + mapUrl?: string; +} diff --git a/lib/utils/ai-model.ts b/lib/utils/ai-model.ts new file mode 100644 index 00000000..efde78c8 --- /dev/null +++ b/lib/utils/ai-model.ts @@ -0,0 +1,111 @@ +'use server' + +import { getSelectedModel } from '@/lib/actions/users' + +export async function getModel(requireVision: boolean = false) { + // Check for specific API model override + if (process.env.SPECIFIC_API_MODEL) { + const provider = process.env.SPECIFIC_API_MODEL.split(':')[0]; + const modelId = process.env.SPECIFIC_API_MODEL.split(':').slice(1).join(':'); + + if (provider === 'openai') { + const { createOpenAI } = await import('@ai-sdk/openai'); + return createOpenAI({ apiKey: process.env.OPENAI_API_KEY })(modelId); + } else if (provider === 'google') { + const { createGoogleGenerativeAI } = await import('@ai-sdk/google'); + return createGoogleGenerativeAI({ apiKey: process.env.GEMINI_3_PRO_API_KEY })(modelId); + } else if (provider === 'xai') { + const { createXai } = await import('@ai-sdk/xai'); + return createXai({ apiKey: process.env.XAI_API_KEY })(modelId); + } + } + + const selectedModel = await getSelectedModel(); + + const xaiApiKey = process.env.XAI_API_KEY; + const gemini3ProApiKey = process.env.GEMINI_3_PRO_API_KEY; + const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; + const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; + const awsRegion = process.env.AWS_REGION; + const bedrockModelId = process.env.BEDROCK_MODEL_ID || 'anthropic.claude-3-5-sonnet-20241022-v2:0'; + const openaiApiKey = process.env.OPENAI_API_KEY; + + if (selectedModel) { + switch (selectedModel) { + case 'Grok 4.2': + if (xaiApiKey) { + const { createXai } = await import('@ai-sdk/xai'); + const xai = createXai({ + apiKey: xaiApiKey, + baseURL: 'https://api.x.ai/v1', + }); + return xai(requireVision ? 'grok-vision-beta' : 'grok-beta'); + } + break; + case 'Gemini 3': + if (gemini3ProApiKey) { + const { createGoogleGenerativeAI } = await import('@ai-sdk/google'); + const google = createGoogleGenerativeAI({ + apiKey: gemini3ProApiKey, + }); + return google('gemini-1.5-pro'); + } + break; + case 'GPT-5.1': + if (openaiApiKey) { + const { createOpenAI } = await import('@ai-sdk/openai'); + const openai = createOpenAI({ + apiKey: openaiApiKey, + }); + return openai('gpt-4o'); + } + break; + } + } + + // Default behavior: Grok -> Gemini -> Bedrock -> OpenAI + if (xaiApiKey) { + try { + const { createXai } = await import('@ai-sdk/xai'); + const xai = createXai({ + apiKey: xaiApiKey, + baseURL: 'https://api.x.ai/v1', + }); + return xai(requireVision ? 'grok-vision-beta' : 'grok-beta'); + } catch (error) { + console.warn('xAI API unavailable, falling back'); + } + } + + if (gemini3ProApiKey) { + try { + const { createGoogleGenerativeAI } = await import('@ai-sdk/google'); + const google = createGoogleGenerativeAI({ + apiKey: gemini3ProApiKey, + }); + return google('gemini-1.5-pro'); + } catch (error) { + console.warn('Gemini 3 Pro API unavailable, falling back'); + } + } + + if (awsAccessKeyId && awsSecretAccessKey) { + const { createAmazonBedrock } = await import('@ai-sdk/amazon-bedrock'); + const bedrock = createAmazonBedrock({ + bedrockOptions: { + region: awsRegion, + credentials: { + accessKeyId: awsAccessKeyId, + secretAccessKey: awsSecretAccessKey, + }, + }, + }); + return bedrock(bedrockModelId); + } + + const { createOpenAI } = await import('@ai-sdk/openai'); + const openai = createOpenAI({ + apiKey: openaiApiKey, + }); + return openai('gpt-4o'); +} diff --git a/lib/utils/index.ts b/lib/utils/index.ts index f9b7eeb5..13e2705d 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1,10 +1,5 @@ import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' -import { getSelectedModel } from '@/lib/actions/users' -import { createOpenAI } from '@ai-sdk/openai' -import { createGoogleGenerativeAI } from '@ai-sdk/google' -import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock' -import { createXai } from '@ai-sdk/xai'; import { v4 as uuidv4 } from 'uuid'; export function cn(...inputs: ClassValue[]) { @@ -14,106 +9,3 @@ export function cn(...inputs: ClassValue[]) { export function generateUUID(): string { return uuidv4(); } - -export async function getModel(requireVision: boolean = false) { - const selectedModel = await getSelectedModel(); - - const xaiApiKey = process.env.XAI_API_KEY; - const gemini3ProApiKey = process.env.GEMINI_3_PRO_API_KEY; - const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; - const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; - const awsRegion = process.env.AWS_REGION; - const bedrockModelId = process.env.BEDROCK_MODEL_ID || 'anthropic.claude-3-5-sonnet-20241022-v2:0'; - const openaiApiKey = process.env.OPENAI_API_KEY; - - if (selectedModel) { - switch (selectedModel) { - case 'Grok 4.2': - if (xaiApiKey) { - const xai = createXai({ - apiKey: xaiApiKey, - baseURL: 'https://api.x.ai/v1', - }); - try { - return xai('grok-4-fast-non-reasoning'); - } catch (error) { - console.error('Selected model "Grok 4.2" is configured but failed to initialize.', error); - throw new Error('Failed to initialize selected model.'); - } - } else { - console.error('User selected "Grok 4.2" but XAI_API_KEY is not set.'); - throw new Error('Selected model is not configured.'); - } - case 'Gemini 3': - if (gemini3ProApiKey) { - const google = createGoogleGenerativeAI({ - apiKey: gemini3ProApiKey, - }); - try { - return google('gemini-3-pro-preview'); - } catch (error) { - console.error('Selected model "Gemini 3" is configured but failed to initialize.', error); - throw new Error('Failed to initialize selected model.'); - } - } else { - console.error('User selected "Gemini 3" but GEMINI_3_PRO_API_KEY is not set.'); - throw new Error('Selected model is not configured.'); - } - case 'GPT-5.1': - if (openaiApiKey) { - const openai = createOpenAI({ - apiKey: openaiApiKey, - }); - return openai('gpt-4o'); - } else { - console.error('User selected "GPT-5.1" but OPENAI_API_KEY is not set.'); - throw new Error('Selected model is not configured.'); - } - } - } - - // Default behavior: Grok -> Gemini -> Bedrock -> OpenAI - if (xaiApiKey) { - const xai = createXai({ - apiKey: xaiApiKey, - baseURL: 'https://api.x.ai/v1', - }); - try { - return xai('grok-4-fast-non-reasoning'); - } catch (error) { - console.warn('xAI API unavailable, falling back to next provider:'); - } - } - - if (gemini3ProApiKey) { - const google = createGoogleGenerativeAI({ - apiKey: gemini3ProApiKey, - }); - try { - return google('gemini-3-pro-preview'); - } catch (error) { - console.warn('Gemini 3 Pro API unavailable, falling back to next provider:', error); - } - } - - if (awsAccessKeyId && awsSecretAccessKey) { - const bedrock = createAmazonBedrock({ - bedrockOptions: { - region: awsRegion, - credentials: { - accessKeyId: awsAccessKeyId, - secretAccessKey: awsSecretAccessKey, - }, - }, - }); - const model = bedrock(bedrockModelId, { - additionalModelRequestFields: { top_k: 350 }, - }); - return model; - } - - const openai = createOpenAI({ - apiKey: openaiApiKey, - }); - return openai('gpt-4o'); -} diff --git a/mapbox_mcp/hooks.ts b/mapbox_mcp/hooks.ts index 797f8852..fd5681bf 100644 --- a/mapbox_mcp/hooks.ts +++ b/mapbox_mcp/hooks.ts @@ -1,6 +1,6 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { generateText } from 'ai'; -import { getModel } from '@/lib/utils'; +import { getModel } from '@/lib/utils/ai-model'; // Define Tool type locally if needed type Tool = { diff --git a/next.config.mjs b/next.config.mjs index 16141c8f..7cb82fad 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -9,7 +9,7 @@ const nextConfig = { bodySizeLimit: '200mb', }, }, - transpilePackages: ['QCX', 'mapbox_mcp'], // Added to transpile local packages + // Removed transpilePackages as it can cause module loading issues for local folders }; export default nextConfig