Jodinho commited on
Commit
17de32a
·
1 Parent(s): 563f14e

chore: remove tracked generated files and update gitignore

Browse files
Files changed (6) hide show
  1. .gitignore +5 -1
  2. all_rpcs.md +330 -0
  3. kb_embeddings.npy +0 -3
  4. methodology_kb.json +145 -0
  5. services/groq_service.py +54 -17
  6. 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 six classifications:
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. "PATH_C": Query asks a general real estate market context question that does not require live data metrics from our system.
19
- 5. "BOTH": Query requires BOTH explaining a methodology concept AND fetching live data metrics.
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" | "PATH_C" | "BOTH" | "GREETING", "reason": "1-sentence justification"}
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: Routing Classification (llama-3.1-8b-instant)
45
- router_messages = [{"role": "system", "content": ROUTER_PROMPT}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", "PATH_C", "BOTH", "GREETING"]:
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. Remote Supabase KB for Real Estate (Idempotent)
 
 
 
 
 
 
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 METHODOLOGY_DOCS:
 
 
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()