This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
seahaven-slack-bot/lambda/maps-lookup/index.ts
Adam Moussa f7e63e50c9 Initial scaffold: Bedrock-backed Slack DM bot
- CDK stack for Sea Haven Industries internal Slack assistant
- Bedrock Agent (Claude 3.5 Sonnet) with QBO + Google Maps action groups
- VectorKnowledgeBase via @cdklabs/generative-ai-cdk-constructs (AOSS + S3)
- Slack webhook/processor Lambdas with DM-only filtering
- API Gateway HTTP API on bot.seahaven.com
- DynamoDB conversation log with 90-day TTL
- Secrets Manager references for Slack, QBO OAuth, and Google Maps
2026-04-11 23:15:15 -04:00

160 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}`);
}
};