| import { agenticChat, resolveMainProvider } from "./model-chat.mjs"; |
|
|
| |
| |
| |
| |
| export async function discoverNeighborhoods(prompt, env = process.env) { |
| if (env.AGENT_DISCOVER === "0") return null; |
|
|
| const messages = [ |
| { |
| role: "system", |
| content: |
| "You are a Toronto housing expert. Given a renter's prompt, choose the 6 Toronto " + |
| "neighbourhoods that best fit it, ordered best-first. For each give its real approximate " + |
| "centre as longitude/latitude. Return strict JSON only: " + |
| '{"neighbourhoods":[{"name":"East York","lng":-79.33,"lat":43.69}]}. ' + |
| "Use only real Toronto neighbourhoods. Longitude near -79, latitude near 43.7.", |
| }, |
| { role: "user", content: String(prompt || "").slice(0, 600) }, |
| ]; |
|
|
| const opts = { json: true, maxTokens: 400, timeoutMs: Number(env.AGENT_DISCOVER_TIMEOUT_MS || 45000) }; |
| |
| |
| |
| const active = resolveMainProvider(env); |
| let result = await agenticChat(messages, env, opts); |
| let discovered = result ? extractDiscovered(safeJson(result.content)) : null; |
| if ((!discovered || discovered.length < 3) && active !== "hf" && env.HF_TOKEN) { |
| result = await agenticChat(messages, env, { ...opts, provider: "hf" }); |
| discovered = result ? extractDiscovered(safeJson(result.content)) : discovered; |
| } |
| return discovered && discovered.length >= 3 ? discovered : null; |
| } |
|
|
| function extractDiscovered(parsed) { |
| const list = Array.isArray(parsed?.neighbourhoods) |
| ? parsed.neighbourhoods |
| : Array.isArray(parsed?.neighborhoods) |
| ? parsed.neighborhoods |
| : Array.isArray(parsed) |
| ? parsed |
| : []; |
|
|
| const seen = new Set(); |
| const discovered = []; |
| for (const item of list) { |
| const name = cleanName(item?.name); |
| const lng = Number(item?.lng ?? item?.longitude); |
| const lat = Number(item?.lat ?? item?.latitude); |
| if (!name || !inToronto(lng, lat)) continue; |
| const id = slug(name); |
| if (seen.has(id)) continue; |
| seen.add(id); |
| discovered.push({ id, name, center: [lng, lat] }); |
| if (discovered.length >= 7) break; |
| } |
|
|
| return discovered.length >= 3 ? discovered : null; |
| } |
|
|
| |
| |
| |
| export function buildDiscoveredRows(discovered) { |
| const neutralScores = { |
| affordability: 50, |
| safety: 50, |
| commute: 50, |
| transit: 50, |
| amenities: 50, |
| lifestyle: 50, |
| growth: 50, |
| }; |
| return discovered.map((area, index) => ({ |
| id: area.id, |
| name: area.name, |
| center: area.center, |
| radiusLng: 0.026, |
| radiusLat: 0.018, |
| seed: index + 1, |
| scores: { ...neutralScores }, |
| rentLo: 0, |
| rentHi: 0, |
| comLo: 0, |
| comHi: 0, |
| comMode: "transit", |
| trend: 0, |
| growthNote: "", |
| lifeHi: "", |
| short: "", |
| tradeoff: "", |
| })); |
| } |
|
|
| function inToronto(lng, lat) { |
| return ( |
| Number.isFinite(lng) && |
| Number.isFinite(lat) && |
| lng >= -79.7 && |
| lng <= -79.05 && |
| lat >= 43.55 && |
| lat <= 43.9 |
| ); |
| } |
|
|
| function cleanName(value) { |
| return String(value || "") |
| .replace(/\s+/g, " ") |
| .replace(/[^A-Za-z0-9 &'\-.]/g, "") |
| .trim() |
| .slice(0, 40); |
| } |
|
|
| function slug(name) { |
| return name.toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); |
| } |
|
|
| function safeJson(text) { |
| try { |
| return JSON.parse(text); |
| } catch { |
| const start = text.indexOf("{"); |
| const end = text.lastIndexOf("}"); |
| if (start >= 0 && end > start) { |
| try { |
| return JSON.parse(text.slice(start, end + 1)); |
| } catch { |
| return null; |
| } |
| } |
| return null; |
| } |
| } |
|
|