chore: remove tracked generated files and update gitignore
Browse files- .gitignore +5 -1
- all_rpcs.md +330 -0
- kb_embeddings.npy +0 -3
- methodology_kb.json +145 -0
- services/groq_service.py +54 -17
- unified_ingest.py +38 -31
.gitignore
CHANGED
|
@@ -66,4 +66,8 @@ Real Estate Intelligence Layer.md
|
|
| 66 |
grant_privileges.sql
|
| 67 |
agent_prompt.md
|
| 68 |
dynamic_tool_calling_features.txt
|
| 69 |
-
Frontend_API_Updates.md
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
grant_privileges.sql
|
| 67 |
agent_prompt.md
|
| 68 |
dynamic_tool_calling_features.txt
|
| 69 |
+
Frontend_API_Updates.md
|
| 70 |
+
*.npy
|
| 71 |
+
section_titles.json
|
| 72 |
+
test_rpcs.py
|
| 73 |
+
test_api.py
|
all_rpcs.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
| schema_name | function_name | identity_args | result_type | security_definer | function_definition |
|
| 2 |
+
| ----------- | ------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 3 |
+
| public | get_dashboard_kpis | | json | true | CREATE OR REPLACE FUNCTION public.get_dashboard_kpis()
|
| 4 |
+
RETURNS json
|
| 5 |
+
LANGUAGE sql
|
| 6 |
+
SECURITY DEFINER
|
| 7 |
+
AS $function$
|
| 8 |
+
SELECT json_build_object(
|
| 9 |
+
'pricing', json_build_object(
|
| 10 |
+
'products_tracked', (SELECT COUNT(*) FROM products),
|
| 11 |
+
'price_changes_7d', (
|
| 12 |
+
SELECT COUNT(*) FROM price_history
|
| 13 |
+
WHERE created_at >= NOW() - INTERVAL '7 days'
|
| 14 |
+
AND price != (
|
| 15 |
+
SELECT price FROM price_history ph2
|
| 16 |
+
WHERE ph2.product_id = price_history.product_id
|
| 17 |
+
AND ph2.created_at < price_history.created_at
|
| 18 |
+
ORDER BY ph2.created_at DESC LIMIT 1
|
| 19 |
+
)
|
| 20 |
+
),
|
| 21 |
+
'spikes_7d', (
|
| 22 |
+
SELECT COUNT(*) FROM v_price_volatility
|
| 23 |
+
WHERE ABS(pct_above_trailing_avg) >= 25
|
| 24 |
+
AND created_at >= NOW() - INTERVAL '7 days'
|
| 25 |
+
),
|
| 26 |
+
'last_scrape_status', (
|
| 27 |
+
SELECT last_status FROM v_scrape_health
|
| 28 |
+
WHERE job_type = 'PRICE_MONITOR'
|
| 29 |
+
ORDER BY last_started_at DESC LIMIT 1
|
| 30 |
+
),
|
| 31 |
+
'tracking_since', (SELECT MIN(created_at) FROM products)
|
| 32 |
+
),
|
| 33 |
+
'leads', json_build_object(
|
| 34 |
+
'targets_crawled', (SELECT COUNT(*) FROM lead_targets),
|
| 35 |
+
'new_leads_7d', (SELECT COUNT(*) FROM leads WHERE created_at >= NOW() - INTERVAL '7 days'),
|
| 36 |
+
'total_leads_all_time', (SELECT COUNT(*) FROM leads),
|
| 37 |
+
'last_scrape_status', (
|
| 38 |
+
SELECT last_status FROM v_scrape_health
|
| 39 |
+
WHERE job_type = 'LEAD_GEN'
|
| 40 |
+
ORDER BY last_started_at DESC LIMIT 1
|
| 41 |
+
)
|
| 42 |
+
),
|
| 43 |
+
'real_estate', json_build_object(
|
| 44 |
+
'properties_tracked', (SELECT COUNT(DISTINCT property_id) FROM v_rate_volatility),
|
| 45 |
+
'rate_changes_7d', (
|
| 46 |
+
SELECT COUNT(*) FROM rate_history
|
| 47 |
+
WHERE created_at >= NOW() - INTERVAL '7 days'
|
| 48 |
+
AND nightly_rate IS NOT NULL
|
| 49 |
+
AND nightly_rate != (
|
| 50 |
+
SELECT nightly_rate FROM rate_history rh2
|
| 51 |
+
WHERE rh2.property_id = rate_history.property_id
|
| 52 |
+
AND rh2.created_at < rate_history.created_at
|
| 53 |
+
AND rh2.nightly_rate IS NOT NULL
|
| 54 |
+
ORDER BY rh2.created_at DESC LIMIT 1
|
| 55 |
+
)
|
| 56 |
+
),
|
| 57 |
+
'spikes_7d', (
|
| 58 |
+
SELECT COUNT(*) FROM v_rate_volatility
|
| 59 |
+
WHERE ABS(pct_above_trailing_avg) >= 25
|
| 60 |
+
AND recorded_at >= NOW() - INTERVAL '7 days'
|
| 61 |
+
),
|
| 62 |
+
'tracking_since', (SELECT MIN(created_at) FROM properties),
|
| 63 |
+
'last_scrape_status', (
|
| 64 |
+
SELECT COALESCE(json_object_agg(platform, last_status), '{}'::json)
|
| 65 |
+
FROM v_scrape_health
|
| 66 |
+
WHERE job_type = 'REAL_ESTATE_MONITOR'
|
| 67 |
+
)
|
| 68 |
+
)
|
| 69 |
+
);
|
| 70 |
+
$function$
|
| 71 |
+
|
|
| 72 |
+
| public | get_distance_km | property_a_id uuid, property_b_id uuid | TABLE(distance_km numeric) | true | CREATE OR REPLACE FUNCTION public.get_distance_km(property_a_id uuid, property_b_id uuid)
|
| 73 |
+
RETURNS TABLE(distance_km numeric)
|
| 74 |
+
LANGUAGE sql
|
| 75 |
+
STABLE SECURITY DEFINER
|
| 76 |
+
AS $function$
|
| 77 |
+
WITH a AS (
|
| 78 |
+
SELECT latitude, longitude
|
| 79 |
+
FROM public.properties
|
| 80 |
+
WHERE id = property_a_id
|
| 81 |
+
),
|
| 82 |
+
b AS (
|
| 83 |
+
SELECT latitude, longitude
|
| 84 |
+
FROM public.properties
|
| 85 |
+
WHERE id = property_b_id
|
| 86 |
+
)
|
| 87 |
+
SELECT
|
| 88 |
+
(
|
| 89 |
+
6371 * 2 * ASIN(
|
| 90 |
+
SQRT(
|
| 91 |
+
POWER(SIN(RADIANS(b.latitude - a.latitude) / 2), 2) +
|
| 92 |
+
COS(RADIANS(a.latitude)) * COS(RADIANS(b.latitude)) *
|
| 93 |
+
POWER(SIN(RADIANS(b.longitude - a.longitude) / 2), 2)
|
| 94 |
+
)
|
| 95 |
+
)
|
| 96 |
+
)::numeric(12,4) AS distance_km
|
| 97 |
+
FROM a, b
|
| 98 |
+
WHERE a.latitude IS NOT NULL
|
| 99 |
+
AND a.longitude IS NOT NULL
|
| 100 |
+
AND b.latitude IS NOT NULL
|
| 101 |
+
AND b.longitude IS NOT NULL;
|
| 102 |
+
$function$
|
| 103 |
+
|
|
| 104 |
+
| public | get_market_averages | market_param text | TABLE(market character varying, active_properties bigint, avg_nightly_rate numeric, min_nightly_rate numeric, max_nightly_rate numeric) | true | CREATE OR REPLACE FUNCTION public.get_market_averages(market_param text DEFAULT NULL::text)
|
| 105 |
+
RETURNS TABLE(market character varying, active_properties bigint, avg_nightly_rate numeric, min_nightly_rate numeric, max_nightly_rate numeric)
|
| 106 |
+
LANGUAGE sql
|
| 107 |
+
STABLE SECURITY DEFINER
|
| 108 |
+
AS $function$
|
| 109 |
+
SELECT
|
| 110 |
+
market,
|
| 111 |
+
COUNT(DISTINCT property_id) AS active_properties,
|
| 112 |
+
ROUND(AVG(nightly_rate), 2) AS avg_nightly_rate,
|
| 113 |
+
MIN(nightly_rate) AS min_nightly_rate,
|
| 114 |
+
MAX(nightly_rate) AS max_nightly_rate
|
| 115 |
+
FROM public.v_rate_volatility
|
| 116 |
+
WHERE (market_param IS NULL OR LOWER(market) = LOWER(market_param))
|
| 117 |
+
AND is_active = true
|
| 118 |
+
AND nightly_rate IS NOT NULL
|
| 119 |
+
GROUP BY market;
|
| 120 |
+
$function$
|
| 121 |
+
|
|
| 122 |
+
| public | get_properties_by_filter | p_market text, p_platform text, p_available boolean, p_bedrooms integer | TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, bedrooms integer, avg_rating numeric, review_count integer, nightly_rate numeric, pct_above_trailing_avg numeric, is_available boolean, recorded_at timestamp with time zone) | true | CREATE OR REPLACE FUNCTION public.get_properties_by_filter(p_market text DEFAULT NULL::text, p_platform text DEFAULT NULL::text, p_available boolean DEFAULT NULL::boolean, p_bedrooms integer DEFAULT NULL::integer)
|
| 123 |
+
RETURNS TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, bedrooms integer, avg_rating numeric, review_count integer, nightly_rate numeric, pct_above_trailing_avg numeric, is_available boolean, recorded_at timestamp with time zone)
|
| 124 |
+
LANGUAGE sql
|
| 125 |
+
STABLE SECURITY DEFINER
|
| 126 |
+
AS $function$
|
| 127 |
+
SELECT DISTINCT ON (property_id)
|
| 128 |
+
property_id,
|
| 129 |
+
property_name,
|
| 130 |
+
market,
|
| 131 |
+
platform,
|
| 132 |
+
bedrooms,
|
| 133 |
+
avg_rating,
|
| 134 |
+
review_count,
|
| 135 |
+
nightly_rate,
|
| 136 |
+
pct_above_trailing_avg,
|
| 137 |
+
is_available,
|
| 138 |
+
recorded_at
|
| 139 |
+
FROM public.v_rate_volatility
|
| 140 |
+
WHERE (p_market IS NULL OR LOWER(market) = LOWER(p_market))
|
| 141 |
+
AND (p_platform IS NULL OR LOWER(platform) = LOWER(p_platform))
|
| 142 |
+
AND (p_available IS NULL OR is_available = p_available)
|
| 143 |
+
AND (p_bedrooms IS NULL OR bedrooms = p_bedrooms)
|
| 144 |
+
ORDER BY property_id, recorded_at DESC;
|
| 145 |
+
$function$
|
| 146 |
+
|
|
| 147 |
+
| public | get_property_rate_changes | property_search text, days_param integer, compare_window_days integer | TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, stay_date timestamp with time zone, nightly_rate numeric, trailing_avg_rate numeric, pct_above_trailing_avg numeric, prev_nightly_rate numeric, pct_change_vs_prev numeric) | true | CREATE OR REPLACE FUNCTION public.get_property_rate_changes(property_search text, days_param integer DEFAULT 14, compare_window_days integer DEFAULT 1)
|
| 148 |
+
RETURNS TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, stay_date timestamp with time zone, nightly_rate numeric, trailing_avg_rate numeric, pct_above_trailing_avg numeric, prev_nightly_rate numeric, pct_change_vs_prev numeric)
|
| 149 |
+
LANGUAGE sql
|
| 150 |
+
STABLE SECURITY DEFINER
|
| 151 |
+
AS $function$
|
| 152 |
+
WITH base AS (
|
| 153 |
+
SELECT
|
| 154 |
+
rh.property_id,
|
| 155 |
+
p.name AS property_name,
|
| 156 |
+
p.market,
|
| 157 |
+
p.platform,
|
| 158 |
+
rh.stay_date,
|
| 159 |
+
rh.nightly_rate,
|
| 160 |
+
-- compute trailing avg as avg of previous 7 days (excluding current day)
|
| 161 |
+
AVG(rh.nightly_rate) OVER (
|
| 162 |
+
PARTITION BY rh.property_id
|
| 163 |
+
ORDER BY rh.stay_date
|
| 164 |
+
ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING
|
| 165 |
+
) AS trailing_avg_rate,
|
| 166 |
+
LAG(rh.nightly_rate, compare_window_days) OVER (
|
| 167 |
+
PARTITION BY rh.property_id
|
| 168 |
+
ORDER BY rh.stay_date
|
| 169 |
+
) AS prev_nightly_rate
|
| 170 |
+
FROM public.v_rate_volatility rh
|
| 171 |
+
JOIN public.properties p
|
| 172 |
+
ON p.id = rh.property_id
|
| 173 |
+
WHERE (rh.property_id::text = property_search OR LOWER(rh.property_name) = LOWER(property_search) OR LOWER(rh.property_name) LIKE '%' || LOWER(property_search) || '%')
|
| 174 |
+
AND rh.recorded_at >= (NOW() - (days_param || ' days')::interval)
|
| 175 |
+
)
|
| 176 |
+
SELECT
|
| 177 |
+
property_id,
|
| 178 |
+
property_name,
|
| 179 |
+
market,
|
| 180 |
+
platform,
|
| 181 |
+
stay_date,
|
| 182 |
+
nightly_rate,
|
| 183 |
+
trailing_avg_rate,
|
| 184 |
+
CASE
|
| 185 |
+
WHEN trailing_avg_rate IS NULL OR trailing_avg_rate = 0 OR nightly_rate IS NULL THEN NULL
|
| 186 |
+
ELSE ((nightly_rate - trailing_avg_rate) / trailing_avg_rate) * 100
|
| 187 |
+
END AS pct_above_trailing_avg,
|
| 188 |
+
prev_nightly_rate,
|
| 189 |
+
CASE
|
| 190 |
+
WHEN prev_nightly_rate IS NULL OR prev_nightly_rate = 0 OR nightly_rate IS NULL THEN NULL
|
| 191 |
+
ELSE ((nightly_rate - prev_nightly_rate) / prev_nightly_rate) * 100
|
| 192 |
+
END AS pct_change_vs_prev
|
| 193 |
+
FROM base
|
| 194 |
+
WHERE stay_date IS NOT NULL
|
| 195 |
+
ORDER BY stay_date ASC;
|
| 196 |
+
$function$
|
| 197 |
+
|
|
| 198 |
+
| public | get_property_rate_history | property_search text, days_param integer | TABLE(property_id uuid, property_name character varying, market character varying, stay_date timestamp with time zone, nightly_rate numeric, trailing_avg_rate numeric, pct_above_trailing_avg numeric, is_available boolean) | true | CREATE OR REPLACE FUNCTION public.get_property_rate_history(property_search text, days_param integer DEFAULT 30)
|
| 199 |
+
RETURNS TABLE(property_id uuid, property_name character varying, market character varying, stay_date timestamp with time zone, nightly_rate numeric, trailing_avg_rate numeric, pct_above_trailing_avg numeric, is_available boolean)
|
| 200 |
+
LANGUAGE sql
|
| 201 |
+
STABLE SECURITY DEFINER
|
| 202 |
+
AS $function$
|
| 203 |
+
SELECT
|
| 204 |
+
property_id,
|
| 205 |
+
property_name,
|
| 206 |
+
market,
|
| 207 |
+
stay_date,
|
| 208 |
+
nightly_rate,
|
| 209 |
+
trailing_avg_rate,
|
| 210 |
+
pct_above_trailing_avg,
|
| 211 |
+
is_available
|
| 212 |
+
FROM public.v_rate_volatility
|
| 213 |
+
WHERE (property_id::TEXT = property_search OR LOWER(property_name) LIKE '%' || LOWER(property_search) || '%')
|
| 214 |
+
AND stay_date >= (NOW() - (days_param || ' days')::INTERVAL)
|
| 215 |
+
ORDER BY stay_date ASC;
|
| 216 |
+
$function$
|
| 217 |
+
|
|
| 218 |
+
| public | get_spike_alerts | threshold_param numeric, days_param integer | TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, nightly_rate numeric, trailing_avg_rate numeric, pct_above_trailing_avg numeric, recorded_at timestamp with time zone) | true | CREATE OR REPLACE FUNCTION public.get_spike_alerts(threshold_param numeric DEFAULT 25.0, days_param integer DEFAULT 7)
|
| 219 |
+
RETURNS TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, nightly_rate numeric, trailing_avg_rate numeric, pct_above_trailing_avg numeric, recorded_at timestamp with time zone)
|
| 220 |
+
LANGUAGE sql
|
| 221 |
+
STABLE SECURITY DEFINER
|
| 222 |
+
AS $function$
|
| 223 |
+
SELECT
|
| 224 |
+
property_id,
|
| 225 |
+
property_name,
|
| 226 |
+
market,
|
| 227 |
+
platform,
|
| 228 |
+
nightly_rate,
|
| 229 |
+
trailing_avg_rate,
|
| 230 |
+
pct_above_trailing_avg,
|
| 231 |
+
recorded_at
|
| 232 |
+
FROM public.v_rate_volatility
|
| 233 |
+
WHERE ABS(pct_above_trailing_avg) >= threshold_param
|
| 234 |
+
AND recorded_at >= (NOW() - (days_param || ' days')::INTERVAL)
|
| 235 |
+
ORDER BY recorded_at DESC;
|
| 236 |
+
$function$
|
| 237 |
+
|
|
| 238 |
+
| public | get_tracked_markets | p_platform text | TABLE(market character varying, active_properties bigint) | true | CREATE OR REPLACE FUNCTION public.get_tracked_markets(p_platform text DEFAULT NULL::text)
|
| 239 |
+
RETURNS TABLE(market character varying, active_properties bigint)
|
| 240 |
+
LANGUAGE sql
|
| 241 |
+
STABLE SECURITY DEFINER
|
| 242 |
+
AS $function$
|
| 243 |
+
SELECT p.market,
|
| 244 |
+
COUNT(DISTINCT p.id) AS active_properties
|
| 245 |
+
FROM public.properties p
|
| 246 |
+
WHERE p.is_active = true
|
| 247 |
+
AND (p_platform IS NULL OR LOWER(p.platform) = LOWER(p_platform))
|
| 248 |
+
AND p.market IS NOT NULL
|
| 249 |
+
GROUP BY p.market
|
| 250 |
+
ORDER BY active_properties DESC, market;
|
| 251 |
+
$function$
|
| 252 |
+
|
|
| 253 |
+
| public | match_re_methodology | query_embedding vector, match_threshold double precision, match_count integer | TABLE(id uuid, section_title character varying, chunk_content text, similarity double precision) | true | CREATE OR REPLACE FUNCTION public.match_re_methodology(query_embedding vector, match_threshold double precision DEFAULT 0.5, match_count integer DEFAULT 3)
|
| 254 |
+
RETURNS TABLE(id uuid, section_title character varying, chunk_content text, similarity double precision)
|
| 255 |
+
LANGUAGE sql
|
| 256 |
+
STABLE SECURITY DEFINER
|
| 257 |
+
AS $function$
|
| 258 |
+
SELECT
|
| 259 |
+
kb.id,
|
| 260 |
+
kb.section_title,
|
| 261 |
+
kb.chunk_content,
|
| 262 |
+
1 - (kb.embedding <=> query_embedding) AS similarity
|
| 263 |
+
FROM public.re_knowledge_base kb
|
| 264 |
+
WHERE 1 - (kb.embedding <=> query_embedding) > match_threshold
|
| 265 |
+
ORDER BY kb.embedding <=> query_embedding
|
| 266 |
+
LIMIT match_count;
|
| 267 |
+
$function$
|
| 268 |
+
|
|
| 269 |
+
| public | rls_auto_enable | | event_trigger | true | CREATE OR REPLACE FUNCTION public.rls_auto_enable()
|
| 270 |
+
RETURNS event_trigger
|
| 271 |
+
LANGUAGE plpgsql
|
| 272 |
+
SECURITY DEFINER
|
| 273 |
+
SET search_path TO 'pg_catalog'
|
| 274 |
+
AS $function$
|
| 275 |
+
DECLARE
|
| 276 |
+
cmd record;
|
| 277 |
+
BEGIN
|
| 278 |
+
FOR cmd IN
|
| 279 |
+
SELECT *
|
| 280 |
+
FROM pg_event_trigger_ddl_commands()
|
| 281 |
+
WHERE command_tag IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
|
| 282 |
+
AND object_type IN ('table','partitioned table')
|
| 283 |
+
LOOP
|
| 284 |
+
IF cmd.schema_name IS NOT NULL AND cmd.schema_name IN ('public') AND cmd.schema_name NOT IN ('pg_catalog','information_schema') AND cmd.schema_name NOT LIKE 'pg_toast%' AND cmd.schema_name NOT LIKE 'pg_temp%' THEN
|
| 285 |
+
BEGIN
|
| 286 |
+
EXECUTE format('alter table if exists %s enable row level security', cmd.object_identity);
|
| 287 |
+
RAISE LOG 'rls_auto_enable: enabled RLS on %', cmd.object_identity;
|
| 288 |
+
EXCEPTION
|
| 289 |
+
WHEN OTHERS THEN
|
| 290 |
+
RAISE LOG 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;
|
| 291 |
+
END;
|
| 292 |
+
ELSE
|
| 293 |
+
RAISE LOG 'rls_auto_enable: skip % (either system schema or not in enforced list: %.)', cmd.object_identity, cmd.schema_name;
|
| 294 |
+
END IF;
|
| 295 |
+
END LOOP;
|
| 296 |
+
END;
|
| 297 |
+
$function$
|
| 298 |
+
|
|
| 299 |
+
| public | search_properties | p_search text, p_market text, p_platform text, p_bedrooms integer, p_available boolean, p_limit integer | TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, bedrooms integer, avg_rating numeric, review_count integer, nightly_rate numeric, pct_above_trailing_avg numeric, is_available boolean, recorded_at timestamp with time zone) | true | CREATE OR REPLACE FUNCTION public.search_properties(p_search text DEFAULT NULL::text, p_market text DEFAULT NULL::text, p_platform text DEFAULT NULL::text, p_bedrooms integer DEFAULT NULL::integer, p_available boolean DEFAULT NULL::boolean, p_limit integer DEFAULT 50)
|
| 300 |
+
RETURNS TABLE(property_id uuid, property_name character varying, market character varying, platform character varying, bedrooms integer, avg_rating numeric, review_count integer, nightly_rate numeric, pct_above_trailing_avg numeric, is_available boolean, recorded_at timestamp with time zone)
|
| 301 |
+
LANGUAGE sql
|
| 302 |
+
STABLE SECURITY DEFINER
|
| 303 |
+
AS $function$
|
| 304 |
+
SELECT DISTINCT ON (rvv.property_id)
|
| 305 |
+
rvv.property_id,
|
| 306 |
+
rvv.property_name,
|
| 307 |
+
rvv.market,
|
| 308 |
+
rvv.platform,
|
| 309 |
+
rvv.bedrooms,
|
| 310 |
+
rvv.avg_rating,
|
| 311 |
+
rvv.review_count,
|
| 312 |
+
rvv.nightly_rate,
|
| 313 |
+
rvv.pct_above_trailing_avg,
|
| 314 |
+
rvv.is_available,
|
| 315 |
+
rvv.recorded_at
|
| 316 |
+
FROM public.v_rate_volatility rvv
|
| 317 |
+
WHERE
|
| 318 |
+
(p_market IS NULL OR LOWER(rvv.market) = LOWER(p_market))
|
| 319 |
+
AND (p_platform IS NULL OR LOWER(rvv.platform) = LOWER(p_platform))
|
| 320 |
+
AND (p_bedrooms IS NULL OR rvv.bedrooms = p_bedrooms)
|
| 321 |
+
AND (p_available IS NULL OR rvv.is_available = p_available)
|
| 322 |
+
AND (
|
| 323 |
+
p_search IS NULL
|
| 324 |
+
OR rvv.property_name ILIKE '%' || p_search || '%'
|
| 325 |
+
OR rvv.property_id::text = p_search
|
| 326 |
+
)
|
| 327 |
+
ORDER BY rvv.property_id, rvv.recorded_at DESC
|
| 328 |
+
LIMIT LEAST(GREATEST(p_limit, 1), 200);
|
| 329 |
+
$function$
|
| 330 |
+
|
|
kb_embeddings.npy
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:d025e65f12c0809aa1266d93e87c9c8da61ef1bad866b25ba3ccc3af825f832e
|
| 3 |
-
size 23168
|
|
|
|
|
|
|
|
|
|
|
|
methodology_kb.json
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"section_title": "Calculation Formulas: 7-Day Trailing Average",
|
| 4 |
+
"chunk_content": "The '7-day trailing average' acts as the pricing baseline for all volatility tracking. Mathematically, it is calculated by taking the mean of a property's nightly rates over a 7-day lookback window. Crucially, this window strictly excludes the current day to ensure that incomplete intraday scraping does not skew the baseline."
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"section_title": "Threshold Definitions: Volatility and Spike Alerts",
|
| 8 |
+
"chunk_content": "Pricing anomalies, or 'spikes', are mathematically triggered when a property's current nightly rate deviates significantly from its 7-day trailing average. By default, the system flags a spike alert if the absolute percentage deviation is greater than or equal to 25%."
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"section_title": "System Limits: Search and History Windows",
|
| 12 |
+
|
| 13 |
+
"chunk_content": "When interacting with the data, property searches will return a maximum of 200 properties at a time. For historical rate tracking, you can look back up to 90 days of history, and compare current rates to past rates up to 14 days ago."
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"section_title": "Global Filters Behavior",
|
| 17 |
+
"chunk_content": "The Real Estate Rate Monitor page utilizes several global filters to drill down into the data: \n- **Market**: Filters the properties by geographic region (e.g., 'Miami', 'NYC'). \n- **Platform**: Filters properties by listing platform (e.g., 'Airbnb', 'Vrbo'). \n- **Status**: Allows filtering by 'Currently Tracked' (active properties), 'Untracked/Removed' (inactive properties), or 'All Historical' to include everything. \n- **Bedrooms**: Filters properties to match an exact bedroom count. \n- **Stay Dates**: A date range picker ('Start Date' to 'End Date') that filters properties where the check-in 'Stay Date' falls within the selected window. All filters dynamically recalculate the underlying dataset and update the KPIs, maps, and charts in real-time."
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"section_title": "Dashboard KPIs",
|
| 21 |
+
"chunk_content": "The top-level Key Performance Indicators (KPIs) update dynamically based on the active global filters: \n- **Properties Tracked**: Displays the count of unique properties that match the current filters. \n- **Rate Changes (7D)**: Represents the total number of nightly rate changes detected across all tracked properties over the last 7 days. \n- **25%+ Spikes (7D)**: Represents the count of 'Pricing Anomalies', defined as instances where a property's current nightly rate deviates by 25% or more (either surging up or dropping down) from its own 7-day trailing average, recorded within the last 7 days."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"section_title": "Visual Indicators and Tooltips",
|
| 25 |
+
"chunk_content": "There is use specific visual rules to convey property health and status: \n- **Map Colors**: On the geographic map, an Orange marker indicates an 'Active' and available property with normal pricing. An Amber marker indicates a 'Pricing Anomaly' (a rate deviation >= 25% from its average). A Gray marker indicates the property is 'Unavailable' for the tracked dates. \n- **Stale Badge**: If the latest data point for a property is older than 24 hours, the UI fades the row to 60% opacity, labels it 'Stale', and displays 'YES (STALE)' under availability. \n- **Unavailable Tooltip**: When a property is unavailable, a tooltip clarifies: 'Reflects a 2-night stay starting today rather than the property's full calendar. Other dates may still be bookable.' \n- **Sparklines**: If a property is currently unavailable, the 'Rate' column renders a mini line chart (sparkline) showing the trajectory of its last 5 known prices."
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"section_title": "Chart Logic: Nightly Rate History",
|
| 29 |
+
"chunk_content": "The 'Nightly Rate History' interactive line chart visualizes the pricing trend for a single selected property over time. It plots two distinct series: \n1. **Solid Line (Primary Color)**: The actual 'Nightly Rate' recorded at specific scrape times. \n2. **Dashed Line (Gray/Muted)**: The '7-day Trailing Avg', serving as a benchmark to easily spot when the current rate is surging or dropping below historical norms."
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"section_title": "Property Rate Snapshot Table",
|
| 33 |
+
"chunk_content": "The snapshot table lists the latest recorded status for properties matching the filters. Its columns include: \n- **Property**: Name and an external link to the live listing. \n- **Market / Platform**: The geographic market and booking site. \n- **Beds / Rating**: Bedroom count, star rating out of 5.0, and total review count. \n- **Stay Date**: The specific check-in date being tracked. \n- **Rate**: The current nightly rate. If unavailable, it shows a sparkline of the last 5 known prices instead. \n- **vs 7d Avg**: The percentage difference between the current rate and the 7-day average. Positive values are green, negative values are red, and deviations >= 25% turn amber and bold. \n- **Avail.**: Shows 'YES', 'NO', or 'YES (STALE)'. \n- **Last Checked**: Human-readable time since the property was last scraped (e.g., '15m ago' or 'As of Aug 8')."
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"section_title": "Scraping Frequency",
|
| 37 |
+
"chunk_content": "Listings are monitored exactly four times per day. This frequent checking ensures that we capture any short-term price adjustments and booking status changes throughout the daily cycle."
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"section_title": "Stale Data Status",
|
| 41 |
+
"chunk_content": "A property is flagged as 'Stale' if our system fails to fetch new pricing information for over 12 hours. This acts as an immediate warning that the data displayed may no longer reflect the live market conditions."
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"section_title": "2-Night Minimum Booking Rule",
|
| 45 |
+
"chunk_content": "The system defines a property as 'Unavailable' if it does not have a consecutive two-night opening starting from the current date. We enforce a two-night check-in window because most premium short-term rentals require a minimum two-night stay, rendering single-night gaps essentially unbookable for our target audience."
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"section_title": "Lookahead Calendar Limits",
|
| 49 |
+
"chunk_content": "Our availability checks do not scan the entire upcoming calendar year. Instead, they focus strictly on immediate, short-term availability (the upcoming 2-night window) to gauge current market tightness and sudden surges in demand."
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"section_title": "Primary Tracking Markets",
|
| 53 |
+
"chunk_content": "We actively track and monitor properties in two primary markets: the Miami area and the New York City/New Jersey Metro region. These locations were selected due to their high volume of short-term rentals and volatile pricing dynamics."
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"section_title": "2026 World Cup Strategy",
|
| 57 |
+
"chunk_content": "Our strategic focus on the Miami and NYC/NJ Metro markets was directly tied to the 2026 FIFA World Cup Final. By monitoring these specific areas, we captured early rate surges, supply constraints, and pricing anomalies as demand built for the event."
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"section_title": "Active Platform Monitoring",
|
| 61 |
+
"chunk_content": "Airbnb is our primary platform for active daily monitoring. We continuously fetch live rates and availability from Airbnb because it provides the most reliable and consistent visibility into daily pricing changes for our targeted properties."
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"section_title": "Historical Platform Constraints",
|
| 65 |
+
"chunk_content": "Properties listed on Vrbo are currently marked as 'Historical' within our system. This is due to platform accessibility configurations that make daily automated fetching difficult. While we no longer fetch fresh daily rates from Vrbo, their past data remains visible to serve as a baseline comparison against current market trends."
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"section_title": "Defining Pricing Anomalies",
|
| 69 |
+
"chunk_content": "A pricing anomaly occurs when a property's nightly rate suddenly deviates significantly from its recent norms. Our system specifically flags a change as an anomaly if the rate spikes or drops by 25 percent or more compared to its trailing average."
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"section_title": "The 7-Day Trailing Average",
|
| 73 |
+
"chunk_content": "To determine what a 'normal' price is for a property, we calculate a 7-day trailing average. This means we take the mean average of the nightly rates recorded over the previous six days, creating a rolling benchmark that smooths out daily fluctuations."
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"section_title": "Property Inactivity and Removal",
|
| 77 |
+
"chunk_content": "If a property is removed from our designated tracking list, the system automatically marks it as inactive. This ensures that our data models and averages only reflect properties that are actively managed and currently relevant to our analysis."
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"section_title": "Handling Unavailable Pricing",
|
| 81 |
+
"chunk_content": "When a property is booked or otherwise unavailable for the target check-in window, the platform does not report a zero dollar rate. Instead, the rate is treated as a null or unknown value to prevent zero-dollar entries from artificially lowering the market averages."
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"section_title": "Market Average Calculation",
|
| 85 |
+
|
| 86 |
+
"chunk_content": "When calculating the average nightly rate for a specific market, the system only includes properties that are currently active and have a known, non-null nightly rate. This ensures the reported average accurately reflects the actual booking cost."
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"section_title": "Discounted Rate Capture",
|
| 90 |
+
"chunk_content": "When a platform displays a discounted price (e.g., a crossed-out original price next to a lower promotional price), our system is designed to capture the final discounted amount as the actual nightly rate, ensuring our data reflects what a customer would actually pay."
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"section_title": "Host Identification and Fallbacks",
|
| 94 |
+
|
| 95 |
+
"chunk_content": "We extract the host's name or profile identifier for each listing to track multi-property operators. If the host's name is not readily available in the standard data structure, the system uses alternative methods to locate and save the host's profile link for later analysis."
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"section_title": "Property Key Grouping",
|
| 99 |
+
"chunk_content": "To prevent duplicate counting, our system uses a unique property key to group listings that represent the exact same physical rental unit, even if that unit is listed across multiple different booking platforms."
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"section_title": "Rate Volatility Tracking",
|
| 103 |
+
"chunk_content": "We maintain a dedicated view of rate volatility that compares the current recorded rate of every property against its 7-day historical benchmark. This allows analysts to instantly sort and identify the most aggressive price changes in a market."
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"section_title": "Consecutive Failure Tolerance",
|
| 107 |
+
"chunk_content": "If the system repeatedly encounters \"not found\" errors when attempting to view a property, it increments a failure counter. This helps distinguish between a temporary network glitch and a property that has been permanently removed by the host."
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"section_title": "Event-Driven Rate Surges",
|
| 111 |
+
"chunk_content": "The system expects to see significant, sudden rate surges in localized geographic pockets. By isolating data to specific markets like Miami and NYC, analysts can attribute these surges to major events, such as the World Cup, rather than seasonal trends."
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"section_title": "Missing Data Prevention",
|
| 115 |
+
"chunk_content": "To maintain data integrity, any automated extraction that fails to find standard pricing formats will escalate to a secondary analysis method. This ensures we maximize our data capture rate even when booking platforms subtly change their page layouts."
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"section_title": "Data Isolation by Platform",
|
| 119 |
+
"chunk_content": "While we monitor multiple listing sites, data is strictly categorized by platform. This prevents the distinct pricing algorithms, fees, and booking rules of Airbnb from being improperly mixed with historical data from platforms like Vrbo."
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"section_title": "Automated Stale Record Filtering",
|
| 123 |
+
"chunk_content": "When calculating live market averages or compiling reports on active inventory, the system automatically filters out any property records marked as 'Stale'. This guarantees that decision-makers are only reviewing the most current, actionable intelligence."
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"section_title": "Availability & 2-Night Window Definition",
|
| 127 |
+
"chunk_content": "A listing marked as 'Unavailable' means no open booking dates were detected within a 2-night check-in window starting from the current date. The system does not look ahead across full calendar months; it tracks consecutive 2-night availability as an immediate signal."
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"section_title": "7-Day Trailing Average Benchmark",
|
| 131 |
+
"chunk_content": "The 7-day trailing average rate is calculated per property by taking the mean nightly price recorded across the prior 6 daily scrapes. Rate volatility alerts trigger when a property's current nightly rate strays 25% or more above or below this baseline."
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"section_title": "Scrape Cadence & Data Refresh",
|
| 135 |
+
"chunk_content": "Listings are scraped 4 times daily to capture short-term rate adjustments and booking updates. Status 'Stale' indicates a scraper job failed to return fresh price points within the last 12 hours."
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"section_title": "World Cup 2026 Strategic Focus",
|
| 139 |
+
"chunk_content": "The Real Estate Rate Monitor specifically tracks short-term rental inventory across NYC/NJ Metro and Miami markets to capture rate surges, supply constraints, and pricing dynamic anomalies leading up to the 2026 World Cup Final."
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"section_title": "Vrbo Historical Tracking Status",
|
| 143 |
+
"chunk_content": "Vrbo properties are flagged as 'Historical' following platform scraping accessibility adjustments. Historical listings remain visible for baseline comparisons, but fresh daily rates are actively tracked via Airbnb endpoints."
|
| 144 |
+
}
|
| 145 |
+
]
|
services/groq_service.py
CHANGED
|
@@ -1,31 +1,55 @@
|
|
| 1 |
import json
|
| 2 |
import re
|
|
|
|
|
|
|
| 3 |
from groq import Groq
|
| 4 |
from services.tools import REAL_ESTATE_TOOLS
|
| 5 |
from services.supabase_service import execute_tool_rpc, search_methodology_rag
|
|
|
|
| 6 |
from config import GROQ_API_KEY
|
| 7 |
|
| 8 |
session_history = {}
|
| 9 |
|
| 10 |
groq_client = Groq(api_key=GROQ_API_KEY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
ROUTER_PROMPT = """You are the classification router for the Joule Dynamics Real Estate Intelligence Layer.
|
| 13 |
-
Analyze the user query and classify it into EXACTLY ONE of
|
| 14 |
|
| 15 |
1. "OUT_OF_SCOPE": Query asks about Leads, Lead-capture, Pricing Monitor data, general web crawling, or cross-system topics outside of the /real-estate page.
|
| 16 |
2. "PATH_A": Query asks a live-data question (prices, spikes, availability, market averages, KPIs, specific listing rates).
|
| 17 |
3. "PATH_B": Query asks a methodology/system design question (7-day average definition, 2-night check-in window, 4x daily scrape cadence, Vrbo status, World Cup strategy).
|
| 18 |
-
4. "
|
| 19 |
-
5. "
|
| 20 |
-
6. "GREETING": User is saying hello, thanking the assistant, or making casual conversation without asking a specific question.
|
| 21 |
|
| 22 |
Respond ONLY with valid JSON matching this schema:
|
| 23 |
-
{"classification": "OUT_OF_SCOPE" | "PATH_A" | "PATH_B" | "
|
| 24 |
"""
|
| 25 |
|
| 26 |
SYNTHESIS_PROMPT = """You are the B2B Real Estate Intelligence Assistant for Joule Dynamics.
|
| 27 |
You provide precise data analysis to real estate investors and property managers reviewing short-term rental market performance.
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
OPERATIONAL RULES:
|
| 30 |
1. NEVER FABRICATE DATA: Rely strictly on returned tool outputs or retrieved methodology chunks. NEVER write ad-hoc SQL. You must exclusively use the registered tools provided.
|
| 31 |
2. ZERO GUESSING: If data or methodology is missing, state plainly: "I don't have that information in the current real estate scope."
|
|
@@ -38,11 +62,33 @@ OPERATIONAL RULES:
|
|
| 38 |
"""
|
| 39 |
|
| 40 |
async def process_chat_message(user_query: str, session_id: str, session_context: dict) -> dict:
|
|
|
|
|
|
|
| 41 |
if session_id not in session_history:
|
| 42 |
session_history[session_id] = []
|
| 43 |
|
| 44 |
-
# STEP 1:
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
# To save tokens on routing, only include the last 4 messages of history
|
| 47 |
router_messages.extend(session_history[session_id][-4:])
|
| 48 |
router_messages.append({"role": "user", "content": user_query})
|
|
@@ -63,7 +109,7 @@ async def process_chat_message(user_query: str, session_id: str, session_context
|
|
| 63 |
routing = {}
|
| 64 |
|
| 65 |
classification = routing.get("classification", "PATH_A")
|
| 66 |
-
if classification not in ["OUT_OF_SCOPE", "PATH_A", "PATH_B", "
|
| 67 |
classification = "PATH_A"
|
| 68 |
|
| 69 |
# Guardrail: Immediate short-circuit if Out of Scope
|
|
@@ -86,10 +132,6 @@ async def process_chat_message(user_query: str, session_id: str, session_context
|
|
| 86 |
tool_results = []
|
| 87 |
rag_chunks = []
|
| 88 |
|
| 89 |
-
# STEP 2: Execute Vector Search if Path B or Both
|
| 90 |
-
if classification in ["PATH_B", "BOTH"]:
|
| 91 |
-
rag_chunks = await search_methodology_rag(user_query)
|
| 92 |
-
|
| 93 |
# STEP 2: Execute Vector Search if Path B or Both
|
| 94 |
if classification in ["PATH_B", "BOTH"]:
|
| 95 |
rag_chunks = await search_methodology_rag(user_query)
|
|
@@ -97,11 +139,6 @@ async def process_chat_message(user_query: str, session_id: str, session_context
|
|
| 97 |
messages = [
|
| 98 |
{"role": "system", "content": SYNTHESIS_PROMPT}
|
| 99 |
]
|
| 100 |
-
if classification == "PATH_C":
|
| 101 |
-
messages.append({
|
| 102 |
-
"role": "system",
|
| 103 |
-
"content": "Note: Because this is a PATH_C query, you must prepend a strict disclaimer stating: 'Note: This is general market context, not live data from the Joule Dynamics tracking system.'"
|
| 104 |
-
})
|
| 105 |
|
| 106 |
messages.extend(session_history[session_id])
|
| 107 |
|
|
|
|
| 1 |
import json
|
| 2 |
import re
|
| 3 |
+
import os
|
| 4 |
+
import numpy as np
|
| 5 |
from groq import Groq
|
| 6 |
from services.tools import REAL_ESTATE_TOOLS
|
| 7 |
from services.supabase_service import execute_tool_rpc, search_methodology_rag
|
| 8 |
+
from services.embedding_service import get_embedding_model
|
| 9 |
from config import GROQ_API_KEY
|
| 10 |
|
| 11 |
session_history = {}
|
| 12 |
|
| 13 |
groq_client = Groq(api_key=GROQ_API_KEY)
|
| 14 |
+
embedder = get_embedding_model()
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if os.path.exists("section_title_embeddings.npy") and os.path.exists("section_titles.json"):
|
| 18 |
+
section_title_embeddings = np.load("section_title_embeddings.npy")
|
| 19 |
+
with open("section_titles.json", "r", encoding="utf-8") as f:
|
| 20 |
+
section_titles = json.load(f)
|
| 21 |
+
print(f"Loaded {len(section_titles)} local section titles for pre-routing.")
|
| 22 |
+
else:
|
| 23 |
+
section_title_embeddings = None
|
| 24 |
+
section_titles = []
|
| 25 |
+
except Exception as e:
|
| 26 |
+
print(f"Failed to load local section title embeddings: {e}")
|
| 27 |
+
section_title_embeddings = None
|
| 28 |
+
section_titles = []
|
| 29 |
|
| 30 |
ROUTER_PROMPT = """You are the classification router for the Joule Dynamics Real Estate Intelligence Layer.
|
| 31 |
+
Analyze the user query and classify it into EXACTLY ONE of five classifications:
|
| 32 |
|
| 33 |
1. "OUT_OF_SCOPE": Query asks about Leads, Lead-capture, Pricing Monitor data, general web crawling, or cross-system topics outside of the /real-estate page.
|
| 34 |
2. "PATH_A": Query asks a live-data question (prices, spikes, availability, market averages, KPIs, specific listing rates).
|
| 35 |
3. "PATH_B": Query asks a methodology/system design question (7-day average definition, 2-night check-in window, 4x daily scrape cadence, Vrbo status, World Cup strategy).
|
| 36 |
+
4. "BOTH": Query requires BOTH explaining a methodology concept AND fetching live data metrics.
|
| 37 |
+
5. "GREETING": User is saying hello, thanking the assistant, or making casual conversation without asking a specific question.
|
|
|
|
| 38 |
|
| 39 |
Respond ONLY with valid JSON matching this schema:
|
| 40 |
+
{"classification": "OUT_OF_SCOPE" | "PATH_A" | "PATH_B" | "BOTH" | "GREETING", "reason": "1-sentence justification"}
|
| 41 |
"""
|
| 42 |
|
| 43 |
SYNTHESIS_PROMPT = """You are the B2B Real Estate Intelligence Assistant for Joule Dynamics.
|
| 44 |
You provide precise data analysis to real estate investors and property managers reviewing short-term rental market performance.
|
| 45 |
|
| 46 |
+
IMMUTABLE SYSTEM BOUNDARIES & HARD FACTS:
|
| 47 |
+
1. TRACKED MARKETS: You ONLY track two markets: 'NYC/NJ Metro' and 'Miami'.
|
| 48 |
+
2. TRACKED PLATFORMS: You ONLY track two platforms: 'Airbnb' (Active daily tracking) and 'Vrbo' (Historical data only).
|
| 49 |
+
3. ABSOLUTE FORBIDDEN ENTITIES: You must NEVER list, suggest, or mention any other cities (e.g., Los Angeles, Chicago, Houston, Orlando) or other booking platforms (e.g., Booking.com, Expedia, Tripadvisor). If asked about them, state plainly that they are outside Joule Dynamics' current tracking scope.
|
| 50 |
+
4. ZERO FABRICATION: Every single price, rate change percentage, property count, and availability status MUST come directly from a returned tool output JSON. If a tool returns no data or an error, state: "I don't have that information in the current real estate scope."
|
| 51 |
+
5. NO RAW SQL: Never attempt to write or generate SQL queries. Rely strictly on the registered tool RPCs provided.
|
| 52 |
+
|
| 53 |
OPERATIONAL RULES:
|
| 54 |
1. NEVER FABRICATE DATA: Rely strictly on returned tool outputs or retrieved methodology chunks. NEVER write ad-hoc SQL. You must exclusively use the registered tools provided.
|
| 55 |
2. ZERO GUESSING: If data or methodology is missing, state plainly: "I don't have that information in the current real estate scope."
|
|
|
|
| 62 |
"""
|
| 63 |
|
| 64 |
async def process_chat_message(user_query: str, session_id: str, session_context: dict) -> dict:
|
| 65 |
+
global section_title_embeddings, section_titles
|
| 66 |
+
|
| 67 |
if session_id not in session_history:
|
| 68 |
session_history[session_id] = []
|
| 69 |
|
| 70 |
+
# STEP 1: Pre-Router Local Vector Search
|
| 71 |
+
pre_check_hint = ""
|
| 72 |
+
if section_title_embeddings is None and os.path.exists("section_title_embeddings.npy"):
|
| 73 |
+
try:
|
| 74 |
+
section_title_embeddings = np.load("section_title_embeddings.npy")
|
| 75 |
+
with open("section_titles.json", "r", encoding="utf-8") as f:
|
| 76 |
+
section_titles = json.load(f)
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"Lazy load failed: {e}")
|
| 79 |
+
|
| 80 |
+
if section_title_embeddings is not None and len(section_titles) > 0:
|
| 81 |
+
query_emb = embedder.encode([user_query], normalize_embeddings=True)[0]
|
| 82 |
+
sims = section_title_embeddings @ query_emb
|
| 83 |
+
top_idx = np.argsort(sims)[::-1][:3]
|
| 84 |
+
matched_titles = [section_titles[i] for i in top_idx if sims[i] >= 0.45]
|
| 85 |
+
|
| 86 |
+
if matched_titles:
|
| 87 |
+
pre_check_hint = f"\n\nLocal Methodology Pre-Check: High similarity match with section titles: {matched_titles}. Consider classifying as PATH_B or BOTH."
|
| 88 |
+
|
| 89 |
+
router_sys_prompt = ROUTER_PROMPT + pre_check_hint
|
| 90 |
+
router_messages = [{"role": "system", "content": router_sys_prompt}]
|
| 91 |
+
|
| 92 |
# To save tokens on routing, only include the last 4 messages of history
|
| 93 |
router_messages.extend(session_history[session_id][-4:])
|
| 94 |
router_messages.append({"role": "user", "content": user_query})
|
|
|
|
| 109 |
routing = {}
|
| 110 |
|
| 111 |
classification = routing.get("classification", "PATH_A")
|
| 112 |
+
if classification not in ["OUT_OF_SCOPE", "PATH_A", "PATH_B", "BOTH", "GREETING"]:
|
| 113 |
classification = "PATH_A"
|
| 114 |
|
| 115 |
# Guardrail: Immediate short-circuit if Out of Scope
|
|
|
|
| 132 |
tool_results = []
|
| 133 |
rag_chunks = []
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
# STEP 2: Execute Vector Search if Path B or Both
|
| 136 |
if classification in ["PATH_B", "BOTH"]:
|
| 137 |
rag_chunks = await search_methodology_rag(user_query)
|
|
|
|
| 139 |
messages = [
|
| 140 |
{"role": "system", "content": SYNTHESIS_PROMPT}
|
| 141 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
messages.extend(session_history[session_id])
|
| 144 |
|
unified_ingest.py
CHANGED
|
@@ -1,41 +1,19 @@
|
|
| 1 |
import os
|
| 2 |
import uuid
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
from services.embedding_service import get_embedding_model
|
| 5 |
from kb_docs import KB_DOCS
|
| 6 |
from supabase import create_client, Client
|
| 7 |
from config import SUPABASE_URL, SUPABASE_KEY
|
| 8 |
|
| 9 |
-
METHODOLOGY_DOCS = [
|
| 10 |
-
{
|
| 11 |
-
"section_title": "Availability & 2-Night Window Definition",
|
| 12 |
-
"chunk_content": "A listing marked as 'Unavailable' means no open booking dates were detected within a 2-night check-in window starting from the current date. The system does not look ahead across full calendar months; it tracks consecutive 2-night availability as an immediate signal."
|
| 13 |
-
},
|
| 14 |
-
{
|
| 15 |
-
"section_title": "7-Day Trailing Average Benchmark",
|
| 16 |
-
"chunk_content": "The 7-day trailing average rate is calculated per property by taking the mean nightly price recorded across the prior 6 daily scrapes. Rate volatility alerts trigger when a property's current nightly rate strays 25% or more above or below this baseline."
|
| 17 |
-
},
|
| 18 |
-
{
|
| 19 |
-
"section_title": "Scrape Cadence & Data Refresh",
|
| 20 |
-
"chunk_content": "Listings are scraped 4 times daily to capture short-term rate adjustments and booking updates. Status 'Stale' indicates a scraper job failed to return fresh price points within the last 12 hours."
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
"section_title": "World Cup 2026 Strategic Focus",
|
| 24 |
-
"chunk_content": "The Real Estate Rate Monitor specifically tracks short-term rental inventory across NYC/NJ Metro and Miami markets to capture rate surges, supply constraints, and pricing dynamic anomalies leading up to the 2026 World Cup Final."
|
| 25 |
-
},
|
| 26 |
-
{
|
| 27 |
-
"section_title": "Vrbo Historical Tracking Status",
|
| 28 |
-
"chunk_content": "Vrbo properties are flagged as 'Historical' following platform scraping accessibility adjustments. Historical listings remain visible for baseline comparisons, but fresh daily rates are actively tracked via Airbnb endpoints."
|
| 29 |
-
}
|
| 30 |
-
]
|
| 31 |
-
|
| 32 |
def ensure_ingested():
|
| 33 |
"""
|
| 34 |
Idempotent function that seeds both the local numpy embeddings and the remote Supabase database.
|
| 35 |
"""
|
| 36 |
print("Verifying ingestion state...")
|
| 37 |
embedder = get_embedding_model()
|
| 38 |
-
|
| 39 |
# 1. Local Numpy KB for Amara (Idempotent)
|
| 40 |
if not os.path.exists("kb_embeddings.npy"):
|
| 41 |
print("kb_embeddings.npy not found, generating local embeddings...")
|
|
@@ -55,19 +33,25 @@ def ensure_ingested():
|
|
| 55 |
kb_embeddings = embedder.encode(texts, normalize_embeddings=True)
|
| 56 |
np.save("kb_embeddings.npy", kb_embeddings)
|
| 57 |
print(f"Embedded {len(texts)} KB docs and saved to kb_embeddings.npy")
|
| 58 |
-
|
| 59 |
-
# 2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 61 |
-
|
| 62 |
-
# Check what already exists in Supabase
|
| 63 |
try:
|
| 64 |
existing_res = supabase.table("re_knowledge_base").select("section_title").execute()
|
| 65 |
existing_titles = {row["section_title"] for row in existing_res.data}
|
| 66 |
except Exception as e:
|
| 67 |
print(f"Failed to query Supabase: {e}")
|
| 68 |
existing_titles = set()
|
| 69 |
-
|
| 70 |
-
for doc in
|
|
|
|
|
|
|
| 71 |
if doc["section_title"] not in existing_titles:
|
| 72 |
print(f"Seeding missing chunk to Supabase: {doc['section_title']}")
|
| 73 |
vector = embedder.encode(doc["chunk_content"]).tolist()
|
|
@@ -75,7 +59,7 @@ def ensure_ingested():
|
|
| 75 |
"id": str(uuid.uuid4()),
|
| 76 |
"section_title": doc["section_title"],
|
| 77 |
"chunk_content": doc["chunk_content"],
|
| 78 |
-
"embedding": vector
|
| 79 |
}
|
| 80 |
try:
|
| 81 |
supabase.table("re_knowledge_base").insert(payload).execute()
|
|
@@ -83,5 +67,28 @@ def ensure_ingested():
|
|
| 83 |
except Exception as e:
|
| 84 |
print(f"Failed to seed {doc['section_title']}: {e}")
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
if __name__ == "__main__":
|
| 87 |
ensure_ingested()
|
|
|
|
| 1 |
import os
|
| 2 |
import uuid
|
| 3 |
+
import json
|
| 4 |
import numpy as np
|
| 5 |
from services.embedding_service import get_embedding_model
|
| 6 |
from kb_docs import KB_DOCS
|
| 7 |
from supabase import create_client, Client
|
| 8 |
from config import SUPABASE_URL, SUPABASE_KEY
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
def ensure_ingested():
|
| 11 |
"""
|
| 12 |
Idempotent function that seeds both the local numpy embeddings and the remote Supabase database.
|
| 13 |
"""
|
| 14 |
print("Verifying ingestion state...")
|
| 15 |
embedder = get_embedding_model()
|
| 16 |
+
|
| 17 |
# 1. Local Numpy KB for Amara (Idempotent)
|
| 18 |
if not os.path.exists("kb_embeddings.npy"):
|
| 19 |
print("kb_embeddings.npy not found, generating local embeddings...")
|
|
|
|
| 33 |
kb_embeddings = embedder.encode(texts, normalize_embeddings=True)
|
| 34 |
np.save("kb_embeddings.npy", kb_embeddings)
|
| 35 |
print(f"Embedded {len(texts)} KB docs and saved to kb_embeddings.npy")
|
| 36 |
+
|
| 37 |
+
# 2. Read Real Estate Methodology KB
|
| 38 |
+
methodology_docs = []
|
| 39 |
+
if os.path.exists("methodology_kb.json"):
|
| 40 |
+
with open("methodology_kb.json", "r", encoding="utf-8") as f:
|
| 41 |
+
methodology_docs = json.load(f)
|
| 42 |
+
|
| 43 |
+
# 3. Remote Supabase KB for Real Estate (Idempotent)
|
| 44 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
|
|
|
|
|
| 45 |
try:
|
| 46 |
existing_res = supabase.table("re_knowledge_base").select("section_title").execute()
|
| 47 |
existing_titles = {row["section_title"] for row in existing_res.data}
|
| 48 |
except Exception as e:
|
| 49 |
print(f"Failed to query Supabase: {e}")
|
| 50 |
existing_titles = set()
|
| 51 |
+
|
| 52 |
+
for doc in methodology_docs:
|
| 53 |
+
if "section_title" not in doc or "chunk_content" not in doc:
|
| 54 |
+
continue
|
| 55 |
if doc["section_title"] not in existing_titles:
|
| 56 |
print(f"Seeding missing chunk to Supabase: {doc['section_title']}")
|
| 57 |
vector = embedder.encode(doc["chunk_content"]).tolist()
|
|
|
|
| 59 |
"id": str(uuid.uuid4()),
|
| 60 |
"section_title": doc["section_title"],
|
| 61 |
"chunk_content": doc["chunk_content"],
|
| 62 |
+
"embedding": vector,
|
| 63 |
}
|
| 64 |
try:
|
| 65 |
supabase.table("re_knowledge_base").insert(payload).execute()
|
|
|
|
| 67 |
except Exception as e:
|
| 68 |
print(f"Failed to seed {doc['section_title']}: {e}")
|
| 69 |
|
| 70 |
+
# 4. Local Pre-Router Embeddings for section_titles (Idempotent)
|
| 71 |
+
section_titles = [doc["section_title"] for doc in methodology_docs if "section_title" in doc]
|
| 72 |
+
|
| 73 |
+
regenerate_titles = False
|
| 74 |
+
if not os.path.exists("section_title_embeddings.npy") or not os.path.exists("section_titles.json"):
|
| 75 |
+
regenerate_titles = True
|
| 76 |
+
else:
|
| 77 |
+
try:
|
| 78 |
+
with open("section_titles.json", "r", encoding="utf-8") as f:
|
| 79 |
+
saved_titles = json.load(f)
|
| 80 |
+
if saved_titles != section_titles:
|
| 81 |
+
regenerate_titles = True
|
| 82 |
+
except Exception:
|
| 83 |
+
regenerate_titles = True
|
| 84 |
+
|
| 85 |
+
if regenerate_titles and section_titles:
|
| 86 |
+
print("Generating section_title_embeddings.npy...")
|
| 87 |
+
title_embeddings = embedder.encode(section_titles, normalize_embeddings=True)
|
| 88 |
+
np.save("section_title_embeddings.npy", title_embeddings)
|
| 89 |
+
with open("section_titles.json", "w", encoding="utf-8") as f:
|
| 90 |
+
json.dump(section_titles, f, indent=2)
|
| 91 |
+
print(f"Saved {len(section_titles)} section title embeddings for pre-routing.")
|
| 92 |
+
|
| 93 |
if __name__ == "__main__":
|
| 94 |
ensure_ingested()
|