import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; const secretsClient = new SecretsManagerClient({}); // ── Bedrock action group event / response types ─────────────────────────────── interface ActionParameter { name: string; type: string; value: string; } interface BedrockActionEvent { messageVersion: string; agent: { name: string; id: string; alias: string; version: string }; inputText: string; sessionId: string; actionGroup: string; function: string; parameters?: ActionParameter[]; sessionAttributes: Record; promptSessionAttributes: Record; } interface BedrockActionResponse { messageVersion: string; response: { actionGroup: string; function: string; functionResponse: { responseBody: { TEXT: { body: string } }; }; }; } function makeResponse(event: BedrockActionEvent, body: string): BedrockActionResponse { return { messageVersion: '1.0', response: { actionGroup: event.actionGroup, function: event.function, functionResponse: { responseBody: { TEXT: { body } } }, }, }; } // ── Google Maps Places API (New) types ──────────────────────────────────────── interface MapsSecret { apiKey: string; } interface PlaceResult { displayName?: { text: string }; formattedAddress?: string; nationalPhoneNumber?: string; websiteUri?: string; rating?: number; userRatingCount?: number; } interface PlacesResponse { places?: PlaceResult[]; } let cachedSecret: MapsSecret | undefined; async function getMapsSecret(): Promise { if (!cachedSecret) { const res = await secretsClient.send( new GetSecretValueCommand({ SecretId: process.env.MAPS_SECRET_ARN! }), ); cachedSecret = JSON.parse(res.SecretString!) as MapsSecret; } return cachedSecret; } async function searchPlaces( apiKey: string, trade: string, location: string, ): Promise { const res = await fetch('https://places.googleapis.com/v1/places:searchText', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Goog-Api-Key': apiKey, 'X-Goog-FieldMask': [ 'places.displayName', 'places.formattedAddress', 'places.nationalPhoneNumber', 'places.websiteUri', 'places.rating', 'places.userRatingCount', ].join(','), }, body: JSON.stringify({ textQuery: `${trade} near ${location}`, maxResultCount: 5, // Bias toward business results includedType: 'contractor', }), }); if (!res.ok) { throw new Error(`Google Maps API error: ${res.status} ${res.statusText}`); } const data = (await res.json()) as PlacesResponse; return data.places ?? []; } function formatPlaces(places: PlaceResult[], trade: string, location: string): string { if (places.length === 0) { return `No ${trade} vendors found near ${location} on Google Maps.`; } const header = `⚠️ These are NEW vendors found via Google Maps — not yet vetted or in QuickBooks.\n` + `Verify credentials before engaging.\n\n`; const results = places .map((p, i) => { const lines: string[] = [`${i + 1}. ${p.displayName?.text ?? 'Unknown'}`]; if (p.formattedAddress) lines.push(` Address: ${p.formattedAddress}`); if (p.nationalPhoneNumber) lines.push(` Phone: ${p.nationalPhoneNumber}`); if (p.websiteUri) lines.push(` Website: ${p.websiteUri}`); if (p.rating !== undefined) { lines.push(` Rating: ${p.rating}/5 (${p.userRatingCount ?? 0} reviews)`); } return lines.join('\n'); }) .join('\n\n'); return header + results; } // ── Handler ─────────────────────────────────────────────────────────────────── export const handler = async (event: BedrockActionEvent): Promise => { const params = Object.fromEntries( (event.parameters ?? []).map((p) => [p.name, p.value]), ); const trade = params.trade?.trim(); const location = params.location?.trim(); if (!trade || !location) { return makeResponse(event, 'Both trade and location are required for a Google Maps search.'); } try { const secret = await getMapsSecret(); const places = await searchPlaces(secret.apiKey, trade, location); return makeResponse(event, formatPlaces(places, trade, location)); } catch (err) { console.error('Google Maps lookup error:', err); return makeResponse(event, `Google Maps search failed: ${(err as Error).message}`); } };