6ixPulse / server /research-tools.mjs
OwnPath's picture
Kokoro-82M TTS + README/diagram update + dead-code/Ollama cleanup
0cb5613 verified
Raw
History Blame Contribute Delete
48 kB
import { spawn } from "node:child_process";
import { resolve } from "node:path";
const DEFAULT_SEARCH_URLS = {
brave: "https://api.search.brave.com/res/v1/web/search",
serpapi: "https://serpapi.com/search.json",
tavily: "https://api.tavily.com/search",
google_cse: "https://customsearch.googleapis.com/customsearch/v1",
};
const TORONTO_OPEN_DATA_API =
"https://ckan0.cf.opendata.inter.prod-toronto.ca/api/3/action";
const TORONTO_DATASETS = {
neighbourhoods: {
title: "City of Toronto Open Data: Neighbourhoods",
url: "https://open.toronto.ca/dataset/neighbourhoods/",
resourceId: "5e6095fc-1bef-4776-887c-28d37f722c51",
category: "official",
sourceType: "official_geography",
agentId: "recommendation",
reliability: "high",
snippet: "Live 158-neighbourhood boundary dataset used for Toronto social planning geography.",
},
crimeRates: {
title: "Toronto Police / Toronto Open Data: Neighbourhood Crime Rates",
url: "https://open.toronto.ca/dataset/neighbourhood-crime-rates/",
resourceId: "d4160604-9f3e-4589-8821-9fd70fa350b3",
category: "safety",
sourceType: "official_safety",
agentId: "safety",
reliability: "high",
snippet:
"Annual reported crime counts and per-100,000 rates by Toronto neighbourhood.",
},
activePermits: {
title: "City of Toronto Open Data: Building Permits - Active Permits",
url: "https://open.toronto.ca/dataset/building-permits-active-permits/",
resourceId: "6d0229af-bc54-46de-9c2b-26759b01dd05",
category: "development",
sourceType: "official_development",
agentId: "growth",
reliability: "high",
snippet: "Daily active building applications and permits for Toronto development context.",
},
clearedPermits: {
title: "City of Toronto Open Data: Building Permits - Cleared Permits",
url: "https://open.toronto.ca/dataset/building-permits-cleared-permits/",
resourceId: "a96c0ba4-3026-402b-b09d-5b1268b8f810",
category: "development",
sourceType: "official_development",
agentId: "growth",
reliability: "high",
snippet: "Completed/closed building permits since 2017 for development momentum context.",
},
ttcGtfs: {
title: "Toronto Open Data: Merged GTFS - TTC Routes and Schedules",
url: "https://open.toronto.ca/dataset/merged-gtfs-ttc-routes-and-schedules/",
resourceId: "c920e221-7a1c-488b-8c5b-6d8cd4e85eaf",
category: "transit",
sourceType: "official_transit",
agentId: "commute",
reliability: "high",
snippet: "TTC route definitions, stops, schedules, and stop locations in GTFS format.",
},
statCanWds: {
title: "Statistics Canada Web Data Service",
url: "https://www.statcan.gc.ca/en/developers/wds",
category: "demographics",
sourceType: "official_demographics",
agentId: "recommendation",
reliability: "high",
snippet:
"Official Statistics Canada API for census, housing, income, labour, and population indicators.",
},
googlePlaces: {
title: "Google Maps Platform: Places API",
url: "https://developers.google.com/maps/documentation/places/web-service/overview",
category: "reviews",
sourceType: "places_reviews",
agentId: "lifestyle",
reliability: "medium",
snippet:
"Location, place details, ratings, reviews, photos, and nearby amenity context through Google Places.",
},
googleRoutes: {
title: "Google Maps Platform: Routes API",
url: "https://developers.google.com/maps/documentation/routes",
category: "transit",
sourceType: "routing",
agentId: "commute",
reliability: "medium",
snippet:
"Route computation for commute time estimates when a Google Maps Platform key is configured.",
},
cmhcRentalMarket: {
title: "CMHC Housing Market Information Portal: Rental Market Data",
url: "https://www03.cmhc-schl.gc.ca/hmip-pimh/en",
category: "rent",
sourceType: "market_rent_report",
agentId: "affordability",
reliability: "medium",
snippet:
"CMHC rental-market data portal used as official Canadian rental market context before presenting rent claims.",
},
rentalsCaRentReport: {
title: "Rentals.ca National Rent Report",
url: "https://rentals.ca/national-rent-report",
category: "rent",
sourceType: "market_rent_report",
agentId: "affordability",
reliability: "medium",
snippet:
"Canadian rent report used as market context for current rental conditions and trends.",
},
};
const officialDataCache = new Map();
export async function runHousingResearch(localRun, env = process.env) {
if (env.RESEARCH_ENABLED === "0") {
return disabledResearch("RESEARCH_ENABLED is disabled", env);
}
const official = await runAuthoritativeResearch(localRun, env);
const provider = configuredSearchProvider(env);
if (provider === "disabled") {
const facts = deriveContextualFacts({
targetNeighborhoods: official.targetNeighborhoods,
sources: official.sources,
facts: official.facts,
});
return {
enabled: official.sources.length > 0,
provider: official.sources.length ? "official-open-data" : "disabled",
generatedAt: new Date().toISOString(),
targetNeighborhoods: official.targetNeighborhoods,
queries: [],
sources: official.sources,
facts,
limitations: [
...official.limitations,
missingSearchProviderMessage(env),
],
};
}
const queries = buildHousingResearchQueries(localRun, env);
const sources = [...official.sources];
const queryReports = [];
const seenUrls = new Set(sources.map((source) => source.url));
const maxSources = positiveInt(env.RESEARCH_MAX_SOURCES, 18);
const searchStartedAt = Date.now();
const totalTimeoutMs = positiveInt(env.RESEARCH_TOTAL_TIMEOUT_MS, 45000);
let timeBudgetExhausted = false;
for (const query of queries) {
const elapsedMs = Date.now() - searchStartedAt;
if (elapsedMs >= totalTimeoutMs) {
timeBudgetExhausted = true;
queryReports.push({
...query,
resultCount: 0,
rawResultCount: 0,
error: `Research time budget exhausted after ${totalTimeoutMs}ms`,
});
break;
}
const result = await searchWeb(query.query, provider, env, {
count: positiveInt(env.RESEARCH_RESULTS_PER_QUERY, 4),
timeoutMs: Math.max(1000, totalTimeoutMs - elapsedMs),
});
const filteredResults = filterResultsForQuery(result.results, query);
queryReports.push({
...query,
resultCount: filteredResults.length,
rawResultCount: result.results.length,
error: result.error || null,
});
for (const item of filteredResults) {
const url = normalizeUrl(item.url);
if (!url || seenUrls.has(url)) continue;
seenUrls.add(url);
sources.push({
id: `S${sources.length + 1}`,
title: cleanText(item.title) || domainFromUrl(url),
url,
domain: domainFromUrl(url),
snippet: cleanText(item.snippet),
category: query.category,
neighborhood: query.neighborhood,
sourceType: query.sourceType,
agentId: query.agentId,
reliability: reliabilityFor(query.sourceType),
});
if (sources.length >= maxSources) break;
}
if (sources.length >= maxSources) break;
}
const limitations = [...official.limitations, ...researchLimitations(provider)];
if (timeBudgetExhausted) {
limitations.unshift(
`Web research stopped after ${totalTimeoutMs}ms to keep the agent response interactive.`,
);
}
const targetNeighborhoods = localRun.ranked.slice(0, neighborhoodCount(env)).map((row) => row.name);
const facts = deriveContextualFacts({
targetNeighborhoods,
sources,
facts: official.facts,
});
return {
enabled: true,
provider,
generatedAt: new Date().toISOString(),
targetNeighborhoods,
queries: queryReports,
sources,
facts,
limitations,
};
}
function deriveContextualFacts({ targetNeighborhoods, sources, facts }) {
const output = [...facts];
for (const neighborhood of targetNeighborhoods) {
if (!findFact(output, neighborhood, "rent")) {
const rentSources = sourcesForNeighborhood(sources, neighborhood).filter((source) =>
["market_listing", "market_context", "market_rent_report"].includes(source.sourceType) ||
source.agentId === "affordability",
);
if (rentSources.length) {
output.push(sourceCoverageFact({
id: `F-rent-coverage-${normalizeName(neighborhood)}`,
category: "rent",
neighborhood,
label: "Rent evidence",
value: `${rentSources.length} source${rentSources.length === 1 ? "" : "s"}`,
unit: "checked",
source: rentSources[0],
reliability: strongestReliability(rentSources),
detail:
"Market/listing sources were found for rental context. Exact rent stays hidden until listing/API values are parsed.",
}));
}
}
if (!findFact(output, neighborhood, "commute")) {
const commuteSources = sourcesForNeighborhood(sources, neighborhood).filter((source) =>
["routing", "official_transit"].includes(source.sourceType) || source.agentId === "commute",
);
if (commuteSources.length) {
output.push(sourceCoverageFact({
id: `F-commute-coverage-${normalizeName(neighborhood)}`,
category: "commute",
neighborhood,
label: "Commute evidence",
value: "Transit source",
unit: "available",
source: commuteSources[0],
reliability: strongestReliability(commuteSources),
detail:
"TTC/route data is available for commute research. Exact door-to-door time still needs a routing calculation.",
}));
}
}
if (!findFact(output, neighborhood, "growth")) {
const growthSources = sourcesForNeighborhood(sources, neighborhood).filter((source) =>
["official_development", "market_context"].includes(source.sourceType) || source.agentId === "growth",
);
if (growthSources.length) {
output.push(sourceCoverageFact({
id: `F-growth-coverage-${normalizeName(neighborhood)}`,
category: "growth",
neighborhood,
label: "Growth evidence",
value: `${growthSources.length} source${growthSources.length === 1 ? "" : "s"}`,
unit: "checked",
source: growthSources[0],
reliability: strongestReliability(growthSources),
detail:
"Permit/development sources were found for trend context. Numeric rent trend stays hidden until market data is computed.",
}));
}
}
}
return output;
}
function sourceCoverageFact({ id, category, neighborhood, label, value, unit, source, reliability, detail }) {
return {
id,
sourceId: source.id,
category,
neighborhood,
label,
value,
unit,
detail,
reliability,
generatedFrom: [source.sourceType],
};
}
function sourcesForNeighborhood(sources, neighborhood) {
const target = normalizeName(neighborhood);
return sources.filter((source) => {
const sourceNeighborhood = normalizeName(source.neighborhood);
return (
!sourceNeighborhood ||
source.neighborhood.includes(",") ||
sourceNeighborhood.includes(target) ||
target.includes(sourceNeighborhood)
);
});
}
function findFact(facts, neighborhood, category) {
const target = normalizeName(neighborhood);
return facts.find((fact) => {
const factNeighborhood = normalizeName(fact.neighborhood);
return (
fact.category === category &&
(factNeighborhood === target ||
factNeighborhood.includes(target) ||
target.includes(factNeighborhood))
);
});
}
function strongestReliability(sources) {
if (sources.some((source) => source.reliability === "high")) return "high";
if (sources.some((source) => source.reliability === "medium")) return "medium";
return "low";
}
export function configuredSearchProvider(env = process.env) {
const requested = normalizeSearchProvider(env.SEARCH_PROVIDER || "auto");
if (requested !== "auto") {
if (requested === "mcp_open_websearch" && mcpOpenWebSearchEnabled(env)) return "mcp_open_websearch";
if (requested === "brave" && env.BRAVE_SEARCH_API_KEY) return "brave";
if (requested === "serpapi" && env.SERPAPI_API_KEY) return "serpapi";
if (requested === "tavily" && env.TAVILY_API_KEY) return "tavily";
if (requested === "google_cse" && googleSearchConfigured(env)) {
return "google_cse";
}
return "disabled";
}
if (mcpOpenWebSearchEnabled(env)) return "mcp_open_websearch";
if (googleSearchConfigured(env)) return "google_cse";
if (env.SERPAPI_API_KEY) return "serpapi";
if (env.BRAVE_SEARCH_API_KEY) return "brave";
if (env.TAVILY_API_KEY) return "tavily";
return "disabled";
}
export function searchProviderStatus(env = process.env) {
const googleApiKeyConfigured = Boolean(googleSearchApiKey(env));
const googleEngineConfigured = Boolean(googleSearchEngineId(env));
const provider = configuredSearchProvider(env);
return {
provider,
requested: normalizeSearchProvider(env.SEARCH_PROVIDER || "auto"),
google: {
configured: googleApiKeyConfigured && googleEngineConfigured,
apiKeyConfigured: googleApiKeyConfigured,
engineConfigured: googleEngineConfigured,
missing: [
googleApiKeyConfigured ? null : "GOOGLE_SEARCH_API_KEY",
googleEngineConfigured ? null : "GOOGLE_SEARCH_CX",
].filter(Boolean),
aliases: {
apiKey: ["GOOGLE_SEARCH_API_KEY", "GOOGLE_CSE_API_KEY", "GOOGLE_API_KEY"],
engine: [
"GOOGLE_SEARCH_CX",
"GOOGLE_SEARCH_ENGINE_ID",
"GOOGLE_CSE_ID",
"GOOGLE_PROGRAMMABLE_SEARCH_ENGINE_ID",
],
},
},
providers: {
mcp_open_websearch: mcpOpenWebSearchEnabled(env),
google_cse: googleApiKeyConfigured && googleEngineConfigured,
serpapi: Boolean(env.SERPAPI_API_KEY),
brave: Boolean(env.BRAVE_SEARCH_API_KEY),
tavily: Boolean(env.TAVILY_API_KEY),
},
};
}
export async function runGoogleSearchProbe(queryText, env = process.env) {
const status = searchProviderStatus(env);
const query = String(queryText || "").trim() || "Toronto rental market neighbourhood reviews";
if (!status.google.configured) {
return {
ok: false,
provider: "google_cse",
query,
results: [],
error: `Google Custom Search is missing ${status.google.missing.join(" and ")}.`,
status,
};
}
const result = await searchGoogleCse(query, env, {
count: positiveInt(env.GOOGLE_SEARCH_TEST_RESULTS, 5),
});
return {
ok: !result.error,
provider: "google_cse",
query,
results: result.results,
error: result.error || null,
status,
};
}
function buildHousingResearchQueries(localRun, env) {
const neighborhoods = localRun.ranked.slice(0, neighborhoodCount(env));
const budget = localRun.parsed.budget;
const cap = localRun.parsed.cap;
const maxQueries = positiveInt(env.RESEARCH_MAX_QUERIES, researchDepth(env) === "deep" ? 18 : 10);
const context = {
budget,
cap,
context: contextualPromptTerms(localRun.prompt),
destination: inferDestination(localRun.prompt),
};
const prioritizedAgents = prioritizedResearchAgents(localRun.parsed);
const primaryQueries = [];
const secondaryQueries = [];
for (const neighborhood of neighborhoods) {
for (const agentId of prioritizedAgents) {
const agentQueries = queriesForAgent(agentId, neighborhood.name, context);
const [primary, ...secondary] = agentQueries;
if (primary) primaryQueries.push(primary);
secondaryQueries.push(...secondary);
}
}
const queries = [...primaryQueries, ...secondaryQueries];
if (researchDepth(env) !== "deep") {
return interleaveByNeighborhood(queries).slice(0, maxQueries);
}
return interleaveByCategory(queries).slice(0, maxQueries);
}
async function runAuthoritativeResearch(localRun, env) {
if (env.OFFICIAL_DATA_ENABLED === "0") {
return {
targetNeighborhoods: [],
sources: [],
facts: [],
limitations: ["OFFICIAL_DATA_ENABLED is disabled."],
};
}
const targetNeighborhoods = localRun.ranked
.slice(0, neighborhoodCount(env))
.map((row) => row.name);
const sources = [
sourceFromDataset("neighbourhoods", targetNeighborhoods),
sourceFromDataset("crimeRates", targetNeighborhoods),
sourceFromDataset("cmhcRentalMarket", targetNeighborhoods),
sourceFromDataset("rentalsCaRentReport", targetNeighborhoods),
sourceFromDataset("activePermits", targetNeighborhoods),
sourceFromDataset("clearedPermits", targetNeighborhoods),
sourceFromDataset("ttcGtfs", targetNeighborhoods),
sourceFromDataset("statCanWds", targetNeighborhoods),
];
if (env.GOOGLE_MAPS_API_KEY || env.GOOGLE_PLACES_API_KEY) {
sources.push(sourceFromDataset("googlePlaces", targetNeighborhoods));
}
if (env.GOOGLE_MAPS_API_KEY || env.GOOGLE_ROUTES_API_KEY) {
sources.push(sourceFromDataset("googleRoutes", targetNeighborhoods));
}
sources.forEach((source, index) => {
source.id = `S${index + 1}`;
});
const facts = [];
const limitations = [];
try {
const crimeRows = await fetchCkanRecords(TORONTO_DATASETS.crimeRates.resourceId, env);
const crimeSource = sources.find((source) => source.sourceType === "official_safety");
for (const neighborhood of targetNeighborhoods) {
const row = matchNeighborhoodRecord(neighborhood, crimeRows);
if (!row || !crimeSource) continue;
const fact = safetyFactFromCrimeRow(row, neighborhood, crimeSource.id);
if (fact) facts.push(fact);
}
} catch (error) {
limitations.push(
`Toronto crime-rate dataset could not be read: ${
error instanceof Error ? error.message : "Unknown data error"
}`,
);
}
// Map coordinates only (not shown as a research source): a public metadata lookup of each area's
// centre so the map can place dynamically-discovered neighbourhoods. The user-facing
// housing evidence comes from official data + deep web research, not encyclopedia text.
try {
facts.push(...(await fetchNeighborhoodGeoFacts(targetNeighborhoods, env)));
} catch {
/* coordinates are optional; ignore lookup failures */
}
return {
targetNeighborhoods,
sources,
facts,
limitations,
};
}
async function fetchNeighborhoodGeoFacts(neighborhoods, env) {
const facts = [];
const timeoutMs = positiveInt(env.WIKIPEDIA_TIMEOUT_MS, 8000);
const summaries = await Promise.all(
neighborhoods.map((neighborhood) => fetchWikipediaSummary(neighborhood, timeoutMs)),
);
summaries.forEach((summary, index) => {
if (!summary?.coordinates) return;
facts.push({
id: `F-geo-${normalizeName(neighborhoods[index])}`,
sourceId: "",
category: "geo",
neighborhood: neighborhoods[index],
label: "Approximate centre",
value: `${summary.coordinates.lat.toFixed(4)}, ${summary.coordinates.lon.toFixed(4)}`,
unit: "lat, lon",
detail: "Map placement only; not a user-facing housing claim.",
reliability: "medium",
generatedFrom: ["geo_lookup"],
lat: summary.coordinates.lat,
lon: summary.coordinates.lon,
});
});
return facts;
}
async function fetchWikipediaSummary(neighborhood, timeoutMs) {
const name = String(neighborhood || "").trim().replace(/\s+/g, " ");
if (!name) return null;
// Many Toronto areas (Parkdale, The Junction, Weston...) share names with other places,
// so try the bare title first, then the disambiguated "<name>, Toronto" form.
return (
(await fetchWikipediaSummaryByTitle(name, timeoutMs)) ||
(await fetchWikipediaSummaryByTitle(`${name}, Toronto`, timeoutMs))
);
}
async function fetchWikipediaSummaryByTitle(name, timeoutMs) {
const title = encodeURIComponent(name);
if (!title) return null;
const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${title}`;
try {
const response = await fetchWithTimeout(url, {
timeoutMs,
headers: { Accept: "application/json", "User-Agent": "6ixPulse/0.1 (housing research agent)" },
});
const data = await response.json();
// Only keep real, Toronto-relevant articles — skip disambiguation pages and off-topic hits.
if (data.type === "disambiguation" || !data.extract) return null;
const extract = cleanText(data.extract);
if (!/toronto|ontario/i.test(extract)) return null;
return {
title: data.title || name,
url: data.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${title}`,
extract: extract.slice(0, 400),
coordinates: data.coordinates
? { lat: Number(data.coordinates.lat), lon: Number(data.coordinates.lon) }
: null,
};
} catch {
return null;
}
}
// Match the Toronto Police neighbourhood crime dataset by LOCATION first: each discovered
// area's centre is tested against every neighbourhood polygon (point-in-polygon), so safety
// resolves even when the model's name doesn't match an official neighbourhood. Falls back to
// fuzzy name matching. `neighborhoods` is [{ name, center:[lng,lat] }].
export async function crimeRatesByLocation(neighborhoods, env = process.env) {
const out = {};
if (env.OFFICIAL_DATA_ENABLED === "0" || !Array.isArray(neighborhoods) || !neighborhoods.length) return out;
try {
const rows = await fetchCkanRecords(TORONTO_DATASETS.crimeRates.resourceId, env);
for (const n of neighborhoods) {
const center = Array.isArray(n.center) ? n.center : null;
let row = center && Number.isFinite(center[0])
? rows.find((r) => pointInGeometry(center, r.geometry))
: null;
if (!row) row = matchNeighborhoodRecord(n.name, rows);
if (!row) continue;
const fact = safetyFactFromCrimeRow(row, n.name, "police");
if (!fact) continue;
const year = (String(fact.label).match(/20\d\d/) || [])[0];
const countMatch = String(fact.detail).match(/^([\d,]+)/);
out[normalizeName(n.name)] = {
rate: Number(fact.value),
year,
count: countMatch ? Number(countMatch[1].replace(/,/g, "")) : null,
areaName: row.AREA_NAME,
};
}
} catch {
/* official data is optional */
}
return out;
}
function pointInGeometry(point, geometry) {
let geo = geometry;
if (typeof geo === "string") {
try {
geo = JSON.parse(geo);
} catch {
return false;
}
}
if (!geo) return false;
const polygons = geo.type === "MultiPolygon" ? geo.coordinates : geo.type === "Polygon" ? [geo.coordinates] : [];
const [x, y] = point;
for (const polygon of polygons) {
const ring = polygon?.[0];
if (Array.isArray(ring) && pointInRing(x, y, ring)) return true;
}
return false;
}
function pointInRing(x, y, ring) {
let inside = false;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const xi = ring[i][0];
const yi = ring[i][1];
const xj = ring[j][0];
const yj = ring[j][1];
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
}
return inside;
}
function sourceFromDataset(key, neighborhoods) {
const dataset = TORONTO_DATASETS[key];
return {
id: "",
title: dataset.title,
url: dataset.url,
domain: domainFromUrl(dataset.url),
snippet: dataset.snippet,
category: dataset.category,
neighborhood: neighborhoods.join(", "),
sourceType: dataset.sourceType,
agentId: dataset.agentId,
reliability: dataset.reliability,
};
}
async function fetchCkanRecords(resourceId, env) {
const cacheKey = `ckan:${resourceId}`;
const cached = officialDataCache.get(cacheKey);
if (cached && Date.now() - cached.createdAt < positiveInt(env.OFFICIAL_DATA_CACHE_MS, 600000)) {
return cached.records;
}
const url = new URL(`${env.TORONTO_OPEN_DATA_API || TORONTO_OPEN_DATA_API}/datastore_search`);
url.searchParams.set("resource_id", resourceId);
url.searchParams.set("limit", String(positiveInt(env.OFFICIAL_DATA_RECORD_LIMIT, 500)));
const response = await fetchWithTimeout(url, {
timeoutMs: positiveInt(env.RESEARCH_TIMEOUT_MS, 12000),
});
const data = await response.json();
const records = Array.isArray(data.result?.records) ? data.result.records : [];
officialDataCache.set(cacheKey, { createdAt: Date.now(), records });
return records;
}
function matchNeighborhoodRecord(name, records) {
const target = normalizeName(name);
return (
records.find((row) => normalizeName(row.AREA_NAME) === target) ||
records.find((row) => normalizeName(row.AREA_NAME).includes(target)) ||
records.find((row) => target.includes(normalizeName(row.AREA_NAME)))
);
}
function safetyFactFromCrimeRow(row, requestedName, sourceId) {
const years = Object.keys(row)
.map((key) => key.match(/_RATE_(20\d{2})$/)?.[1])
.filter(Boolean)
.map((year) => Number.parseInt(year, 10));
const year = Math.max(...years);
if (!Number.isFinite(year)) return null;
const types = ["ASSAULT", "AUTOTHEFT", "BREAKENTER", "ROBBERY", "SHOOTING", "THEFTFROMMV"];
const totalRate = types.reduce((sum, type) => {
const value = Number(row[`${type}_RATE_${year}`]);
return Number.isFinite(value) ? sum + value : sum;
}, 0);
const totalCount = types.reduce((sum, type) => {
const value = Number(row[`${type}_${year}`]);
return Number.isFinite(value) ? sum + value : sum;
}, 0);
return {
id: `F-safety-${normalizeName(requestedName)}`,
sourceId,
category: "safety",
neighborhood: row.AREA_NAME || requestedName,
label: `${year} selected reported-crime rate`,
value: Math.round(totalRate),
unit: "reported incidents per 100,000 residents",
detail: `${totalCount.toLocaleString()} selected reported incidents across assault, auto theft, break and enter, robbery, shootings, and theft from motor vehicle.`,
reliability: "high",
generatedFrom: types.flatMap((type) => [`${type}_${year}`, `${type}_RATE_${year}`]),
};
}
function query(category, sourceType, neighborhood, text, agentId = "recommendation", domains = []) {
return {
id: "",
agentId,
category,
sourceType,
neighborhood,
query: text,
domains,
};
}
function queriesForAgent(agentId, neighborhood, context) {
// Natural-language queries: keyless MCP engines ignore site: operators and lose relevance,
// so the domain lists are passed only as a soft ranking preference (see filterResultsForQuery),
// never baked into the query string. Always anchor on the full neighbourhood + "Toronto".
const area = `${neighborhood} Toronto`;
const promptContext = context.context ? ` ${context.context}` : "";
const budget = context.budget.toLocaleString();
const destination = context.destination || "Union Station";
const reddit = ["reddit.com/r/askTO", "reddit.com/r/TorontoRenting", "reddit.com/r/toronto"];
if (agentId === "affordability") {
return [
query("listings", "market_listing", neighborhood, `${area} 1 bedroom apartment for rent under $${budget}${promptContext}`, "affordability", ["rentals.ca", "realtor.ca", "condos.ca", "zumper.com", "apartments.com"]),
query("market", "market_context", neighborhood, `${area} average rent price one bedroom 2025 rental market report`, "affordability", ["rentals.ca", "zolo.ca", "housesigma.com", "wowa.ca"]),
];
}
if (agentId === "commute") {
return [
query("official", "official_context", neighborhood, `${area} commute to ${destination} how long transit TTC minutes`, "commute", ["ttc.ca", "toronto.ca", "metrolinx.com", "gotransit.com"]),
query("community", "resident_discussion", neighborhood, `${area} commute to ${destination} reddit how long does it take`, "commute", reddit),
];
}
if (agentId === "safety") {
return [
query("official", "official_context", neighborhood, `${area} neighbourhood crime rate is it safe to live`, "safety", ["toronto.ca", "torontopolice.on.ca", "data.torontopolice.on.ca"]),
query("community", "resident_discussion", neighborhood, `${area} is it a safe area reddit safety`, "safety", reddit),
];
}
if (agentId === "lifestyle") {
return [
query("reviews", "local_reviews", neighborhood, `${area} best cafes restaurants parks walkability${promptContext}`, "lifestyle", ["blogto.com", "yelp.ca", "tripadvisor.ca", "torontolife.com"]),
query("community", "resident_discussion", neighborhood, `${area} what is it like to live reddit cafes parks vibe`, "lifestyle", reddit),
];
}
if (agentId === "growth") {
return [
query("market", "market_context", neighborhood, `${area} new condo development construction property value trend`, "growth", ["urbantoronto.ca", "storeys.com", "renx.ca"]),
query("official", "official_context", neighborhood, `${area} building permits development planning growth`, "growth", ["toronto.ca", "open.toronto.ca"]),
];
}
return [
query("community", "resident_discussion", neighborhood, `${area} pros and cons of living there rent commute safety`, "recommendation", reddit),
query("market", "market_context", neighborhood, `${area} neighbourhood guide what to know before renting`, "recommendation", ["torontolife.com", "blogto.com", "liv.rent"]),
];
}
function filterResultsForQuery(results, query) {
// Every query is about a Toronto neighbourhood, so gate on relevance first: keyless
// engines happily return "East - Wikipedia" / "EAST | Merriam-Webster" for "East York".
// Dropping irrelevant hits entirely is better than feeding junk to the model — the
// evidence policy already shows an honest "needs source" when a category has nothing.
const relevant = results.filter((item) => mentionsTorontoArea(item, query.neighborhood));
if (!relevant.length) return [];
const allowed = Array.isArray(query.domains) ? query.domains : [];
if (!allowed.length) return relevant;
// The allowlist is a quality *preference*, not a hard gate (keyless engines ignore site:
// operators). Surface preferred domains first, then backfill with the rest.
const preferred = relevant.filter((item) => urlMatchesAllowedDomain(item.url, allowed));
if (!preferred.length) return relevant;
const rest = relevant.filter((item) => !urlMatchesAllowedDomain(item.url, allowed));
return [...preferred, ...rest];
}
function mentionsTorontoArea(item, neighborhood) {
const hay = `${item.title || ""} ${item.snippet || ""} ${item.url || ""}`.toLowerCase();
// Must be anchored to Toronto, or mention the *full* neighbourhood phrase. A single
// short token like "east" (from "East York") is too loose and lets "East - Wikipedia"
// and "Eastlink" through, which is exactly the junk we are trying to drop.
if (hay.includes("toronto") || hay.includes("ontario") || hay.includes(" gta")) return true;
const phrase = String(neighborhood || "").toLowerCase().trim();
return phrase.length > 4 && hay.includes(phrase);
}
function urlMatchesAllowedDomain(url, allowed) {
let parsed;
try {
parsed = new URL(url);
} catch {
return false;
}
const hostname = parsed.hostname.toLowerCase().replace(/^www\./, "");
const path = parsed.pathname.toLowerCase();
return allowed.some((entry) => {
const normalized = String(entry || "")
.trim()
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/^www\./, "");
if (!normalized) return false;
const [allowedHost, ...allowedPathParts] = normalized.split("/");
const allowedPath = allowedPathParts.length ? `/${allowedPathParts.join("/")}` : "";
const hostMatches = hostname === allowedHost || hostname.endsWith(`.${allowedHost}`);
if (!hostMatches) return false;
return !allowedPath || path.startsWith(allowedPath);
});
}
async function searchWeb(queryText, provider, env, options) {
try {
if (provider === "mcp_open_websearch") return await searchMcpOpenWebSearch(queryText, env, options);
if (provider === "brave") return await searchBrave(queryText, env, options);
if (provider === "serpapi") return await searchSerpApi(queryText, env, options);
if (provider === "tavily") return await searchTavily(queryText, env, options);
if (provider === "google_cse") return await searchGoogleCse(queryText, env, options);
return { results: [], error: "Search provider is disabled" };
} catch (error) {
return {
results: [],
error: error instanceof Error ? error.message : "Unknown search error",
};
}
}
async function searchMcpOpenWebSearch(queryText, env, options) {
const result = await callMcpWebSearchTool(queryText, env, options);
return {
results: result.results.map((item) => ({
title: item.title,
url: item.url,
snippet: item.snippet || `${item.engine || "MCP"} result`,
})),
};
}
async function callMcpWebSearchTool(queryText, env, options) {
const configuredTimeoutMs = positiveInt(env.MCP_WEB_SEARCH_TIMEOUT_MS, 14000);
const timeoutMs = Math.min(configuredTimeoutMs, positiveInt(options.timeoutMs, configuredTimeoutMs));
const command = env.MCP_WEB_SEARCH_COMMAND || process.execPath;
const args = mcpWebSearchArgs(env);
const toolPreference = env.MCP_WEB_SEARCH_TOOL || "web_search";
const child = spawn(command, args, {
cwd: process.cwd(),
env: { ...process.env, ...env },
stdio: ["pipe", "pipe", "pipe"],
});
let nextId = 1;
let outputBuffer = "";
let stderr = "";
const pending = new Map();
const timeout = setTimeout(() => {
child.kill("SIGTERM");
for (const { reject } of pending.values()) reject(new Error("MCP web search timed out"));
pending.clear();
}, timeoutMs);
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
child.stdout.on("data", (chunk) => {
outputBuffer += chunk.toString("utf8");
let newlineIndex = outputBuffer.indexOf("\n");
while (newlineIndex !== -1) {
const line = outputBuffer.slice(0, newlineIndex).trim();
outputBuffer = outputBuffer.slice(newlineIndex + 1);
if (line) handleMcpLine(line, pending);
newlineIndex = outputBuffer.indexOf("\n");
}
});
child.on("error", (error) => {
for (const { reject } of pending.values()) reject(error);
pending.clear();
});
const request = (method, params = {}) =>
new Promise((resolveRequest, rejectRequest) => {
const id = nextId++;
pending.set(id, { resolve: resolveRequest, reject: rejectRequest });
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
});
try {
await request("initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: { name: "6ixpulse", version: "0.1.0" },
});
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`);
const toolsResponse = await request("tools/list");
const tools = Array.isArray(toolsResponse?.tools) ? toolsResponse.tools : [];
const toolName =
tools.find((tool) => tool.name === toolPreference)?.name ||
tools.find((tool) => /search/i.test(tool.name))?.name ||
toolPreference;
const callResponse = await request("tools/call", {
name: toolName,
arguments: {
query: queryText,
count: positiveInt(options.count, 6),
},
});
return parseMcpSearchResult(callResponse);
} finally {
clearTimeout(timeout);
child.stdin.end();
child.kill("SIGTERM");
if (stderr && env.MCP_WEB_SEARCH_DEBUG === "1") {
console.error(stderr.slice(0, 1000));
}
}
}
function handleMcpLine(line, pending) {
let message;
try {
message = JSON.parse(line);
} catch {
return;
}
if (!message.id || !pending.has(message.id)) return;
const request = pending.get(message.id);
pending.delete(message.id);
if (message.error) {
request.reject(new Error(message.error.message || "MCP error"));
} else {
request.resolve(message.result);
}
}
function parseMcpSearchResult(callResponse) {
if (Array.isArray(callResponse?.structuredContent?.results)) {
return { results: normalizeMcpResults(callResponse.structuredContent.results) };
}
const text = Array.isArray(callResponse?.content)
? callResponse.content.map((item) => item?.text).filter(Boolean).join("\n")
: "";
if (text) {
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed.results)) return { results: normalizeMcpResults(parsed.results) };
} catch {
return { results: [] };
}
}
return { results: [] };
}
function normalizeMcpResults(results) {
return results
.map((item) => ({
title: cleanText(item.title),
url: normalizeUrl(item.url),
snippet: cleanText(item.snippet),
engine: cleanText(item.engine),
}))
.filter((item) => item.title && item.url);
}
async function searchBrave(queryText, env, options) {
const url = new URL(env.BRAVE_SEARCH_URL || DEFAULT_SEARCH_URLS.brave);
url.searchParams.set("q", queryText);
url.searchParams.set("count", String(options.count));
url.searchParams.set("country", env.SEARCH_COUNTRY || "CA");
url.searchParams.set("search_lang", env.SEARCH_LANGUAGE || "en");
url.searchParams.set("safesearch", "moderate");
const response = await fetchWithTimeout(url, {
headers: {
Accept: "application/json",
"X-Subscription-Token": env.BRAVE_SEARCH_API_KEY,
},
timeoutMs: positiveInt(env.RESEARCH_TIMEOUT_MS, 12000),
});
const data = await response.json();
return {
results: (data.web?.results || []).map((item) => ({
title: item.title,
url: item.url,
snippet: item.description,
})),
};
}
async function searchSerpApi(queryText, env, options) {
const url = new URL(env.SERPAPI_URL || DEFAULT_SEARCH_URLS.serpapi);
url.searchParams.set("engine", env.SERPAPI_ENGINE || "google");
url.searchParams.set("q", queryText);
url.searchParams.set("api_key", env.SERPAPI_API_KEY);
url.searchParams.set("num", String(options.count));
url.searchParams.set("gl", env.SEARCH_COUNTRY?.toLowerCase() || "ca");
url.searchParams.set("hl", env.SEARCH_LANGUAGE || "en");
const response = await fetchWithTimeout(url, {
timeoutMs: positiveInt(env.RESEARCH_TIMEOUT_MS, 12000),
});
const data = await response.json();
return {
results: (data.organic_results || []).map((item) => ({
title: item.title,
url: item.link,
snippet: item.snippet,
})),
};
}
async function searchTavily(queryText, env, options) {
const response = await fetchWithTimeout(env.TAVILY_URL || DEFAULT_SEARCH_URLS.tavily, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api_key: env.TAVILY_API_KEY,
query: queryText,
search_depth: researchDepth(env) === "deep" ? "advanced" : "basic",
max_results: options.count,
include_answer: false,
include_raw_content: false,
}),
timeoutMs: positiveInt(env.RESEARCH_TIMEOUT_MS, 12000),
});
const data = await response.json();
return {
results: (data.results || []).map((item) => ({
title: item.title,
url: item.url,
snippet: item.content,
})),
};
}
async function searchGoogleCse(queryText, env, options) {
const url = new URL(env.GOOGLE_CSE_URL || DEFAULT_SEARCH_URLS.google_cse);
url.searchParams.set("key", googleSearchApiKey(env));
url.searchParams.set("cx", googleSearchEngineId(env));
url.searchParams.set("q", queryText);
url.searchParams.set("num", String(Math.min(options.count, 10)));
url.searchParams.set("gl", env.SEARCH_COUNTRY?.toLowerCase() || "ca");
url.searchParams.set("hl", env.SEARCH_LANGUAGE || "en");
url.searchParams.set("lr", env.SEARCH_LANGUAGE_RESTRICT || "lang_en");
url.searchParams.set("safe", env.SEARCH_SAFE || "active");
url.searchParams.set("filter", env.SEARCH_DUPLICATE_FILTER || "1");
const response = await fetchWithTimeout(url, {
timeoutMs: positiveInt(env.RESEARCH_TIMEOUT_MS, 12000),
});
const data = await response.json();
return {
results: (data.items || []).map((item) => ({
title: item.title,
url: item.link,
snippet: item.snippet,
})),
};
}
function normalizeSearchProvider(value) {
const provider = String(value || "auto")
.trim()
.toLowerCase()
.replace(/[-\s]+/g, "_");
if (
[
"mcp",
"open_websearch",
"open_web_search",
"mcp_websearch",
"mcp_web_search",
"mcp_open_websearch",
"mcp_open_web_search",
].includes(provider)
) {
return "mcp_open_websearch";
}
if (["google", "google_cse", "google_custom_search", "custom_search"].includes(provider)) {
return "google_cse";
}
if (["serp", "serp_api"].includes(provider)) return "serpapi";
return provider;
}
function mcpOpenWebSearchEnabled(env) {
return env.MCP_WEB_SEARCH_ENABLED !== "0";
}
function mcpWebSearchArgs(env) {
if (env.MCP_WEB_SEARCH_ARGS) {
const raw = env.MCP_WEB_SEARCH_ARGS.trim();
if (raw.startsWith("[")) {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed.map(String);
} catch {
return raw.split(/\s+/).filter(Boolean);
}
}
return raw.split(/\s+/).filter(Boolean);
}
return [resolve(process.cwd(), "server/open-websearch-mcp.mjs")];
}
function prioritizedResearchAgents(parsed) {
const weights = {
affordability: parsed.weights.affordability,
commute: Math.max(parsed.weights.commute, parsed.weights.transit),
safety: parsed.weights.safety,
lifestyle: Math.max(parsed.weights.lifestyle, parsed.weights.amenities),
growth: parsed.weights.growth,
recommendation: 0.8,
};
return Object.entries(weights)
.sort((a, b) => b[1] - a[1])
.map(([agent]) => agent);
}
function inferDestination(prompt) {
const source = String(prompt || "");
if (/union\s+station|near\s+union|\bunion\b/i.test(source)) return "Union Station";
const workNear = source.match(/work(?:ing)?\s+(?:near|at|in)\s+([A-Za-z0-9 &.'-]{3,42})/i);
if (workNear) return cleanText(workNear[1]);
const commuteTo = source.match(/commut\w*\s+(?:to|near)\s+([A-Za-z0-9 &.'-]{3,42})/i);
if (commuteTo) return cleanText(commuteTo[1]);
return "Union Station";
}
function contextualPromptTerms(prompt) {
const source = String(prompt || "").toLowerCase();
const terms = [];
if (/studio|bachelor/.test(source)) terms.push("studio bachelor");
if (/1\s*bed|one bed|one-bedroom|1-bedroom/.test(source)) terms.push("1 bedroom");
if (/2\s*bed|two bed|two-bedroom|2-bedroom/.test(source)) terms.push("2 bedroom");
if (/condo/.test(source)) terms.push("condo");
if (/apartment|rental|rent/.test(source)) terms.push("apartment rental");
if (/caf|coffee/.test(source)) terms.push("cafes coffee");
if (/park|trail|green/.test(source)) terms.push("parks green space");
if (/quiet|family/.test(source)) terms.push("quiet residential");
if (/night|bar|restaurant|food/.test(source)) terms.push("restaurants nightlife");
if (/safe|safety|crime/.test(source)) terms.push("safety crime");
return [...new Set(terms)].join(" ");
}
function googleSearchConfigured(env) {
return Boolean(googleSearchApiKey(env) && googleSearchEngineId(env));
}
function googleSearchApiKey(env) {
return env.GOOGLE_SEARCH_API_KEY || env.GOOGLE_CSE_API_KEY || env.GOOGLE_API_KEY || "";
}
function googleSearchEngineId(env) {
return (
env.GOOGLE_SEARCH_CX ||
env.GOOGLE_SEARCH_ENGINE_ID ||
env.GOOGLE_CSE_ID ||
env.GOOGLE_PROGRAMMABLE_SEARCH_ENGINE_ID ||
""
);
}
async function fetchWithTimeout(input, options = {}) {
const timeoutMs = options.timeoutMs || 12000;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(input, {
...options,
signal: controller.signal,
});
if (!response.ok) {
const body = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status}: ${body.slice(0, 220) || response.statusText}`);
}
return response;
} finally {
clearTimeout(timeout);
}
}
function disabledResearch(reason, env) {
return {
enabled: false,
provider: configuredSearchProvider(env),
generatedAt: new Date().toISOString(),
targetNeighborhoods: [],
queries: [],
sources: [],
facts: [],
limitations: [reason],
};
}
function researchLimitations(provider) {
if (provider === "mcp_open_websearch") {
return [
"Open-WebSearch MCP is optional; results can be blocked, rate-limited, incomplete, or stale.",
"Search snippets are source discovery only. User-facing rent, commute, safety, review, and market claims still require computed facts.",
"Use official feeds and licensed listing sources for production-grade housing decisions.",
];
}
return [
`Search results come from ${provider}; snippets can be incomplete or stale.`,
"Listing pages, Google reviews, and social posts should be verified directly before making housing decisions.",
"Use official APIs or licensed feeds for listing, review, and social-source research.",
];
}
function missingSearchProviderMessage(env) {
const status = searchProviderStatus(env);
if (status.requested === "google_cse" && status.google.missing.length) {
return `Google Custom Search is selected but missing ${status.google.missing.join(
" and ",
)}. Add those values for listings, Reddit, review, and market-source research.`;
}
return "No web search provider is configured. Keep SEARCH_PROVIDER=disabled for official-data-only runs, or add GOOGLE_SEARCH_API_KEY + GOOGLE_SEARCH_CX, SERPAPI_API_KEY, BRAVE_SEARCH_API_KEY, or TAVILY_API_KEY.";
}
function reliabilityFor(sourceType) {
if (sourceType === "official_context") return "high";
if (sourceType === "market_listing" || sourceType === "market_context") return "medium";
if (sourceType === "local_reviews") return "medium";
return "anecdotal";
}
function researchDepth(env) {
return (env.RESEARCH_DEPTH || "deep").toLowerCase() === "standard" ? "standard" : "deep";
}
function neighborhoodCount(env) {
return Math.max(1, Math.min(4, positiveInt(env.RESEARCH_NEIGHBORHOODS, researchDepth(env) === "deep" ? 3 : 2)));
}
function positiveInt(value, fallback) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function interleaveByNeighborhood(queries) {
return queries;
}
function interleaveByCategory(queries) {
const grouped = new Map();
for (const item of queries) {
const key = item.agentId || item.category;
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push(item);
}
const output = [];
while ([...grouped.values()].some((items) => items.length)) {
for (const items of grouped.values()) {
const item = items.shift();
if (item) output.push(item);
}
}
return output;
}
function normalizeUrl(url) {
if (!url || typeof url !== "string") return "";
try {
const parsed = new URL(url);
parsed.hash = "";
return parsed.toString();
} catch {
return "";
}
}
function domainFromUrl(url) {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return "";
}
}
function cleanText(value) {
return String(value || "")
.replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function normalizeName(value) {
return String(value || "")
.toLowerCase()
.replace(/&/g, "and")
.replace(/[^a-z0-9]+/g, "")
.trim();
}