161 lines
4.7 KiB
TypeScript
161 lines
4.7 KiB
TypeScript
|
|
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<string, string>;
|
||
|
|
promptSessionAttributes: Record<string, string>;
|
||
|
|
}
|
||
|
|
|
||
|
|
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<MapsSecret> {
|
||
|
|
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<PlaceResult[]> {
|
||
|
|
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<BedrockActionResponse> => {
|
||
|
|
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}`);
|
||
|
|
}
|
||
|
|
};
|