Update android_handler.py to drive replies from Supabase rules/templates. Goals - Load objection_rule + reply_template each poll (cache per cycle). - Match inbound text -> first active rule by priority (lower number wins). - Render template with simple variables ({{first_name}}, {{vehicle_model}}). - Respect ok_to_reply() hours; if out-of-hours, skip send and log a deferral note. - Log full metadata (intent_label, template_slug, message_text, source/model). - Service-only side-effect: open appointment_request for bookings. - Fallback to OpenAI if no rule matches. Implementation details 1) Add helpers at top-level (or a small utils section): def load_rules_and_templates(client): # objection_rule fields used: label, keywords(text[]), template_slug, priority, active, match_any # reply_template fields used: slug, body, max_len, purpose, tone, channel, active rules = (client.table('objection_rule') .select('label, keywords, template_slug, priority, active, match_any') .eq('active', True) .order('priority', asc=True) .execute()).data or [] tmpls_rows = (client.table('reply_template') .select('slug, body, max_len, purpose, tone, channel, active') .eq('active', True) .execute()).data or [] templates = {r['slug']: r for r in tmpls_rows} return rules, templates def match_rule(text: str, rules: list) -> dict | None: t = (text or '').lower() for r in rules: kws = [k.lower() for k in (r.get('keywords') or [])] if not kws: continue if r.get('match_any', True): if any(k in t for k in kws): return r else: if all(k in t for k in kws): return r return None def render_template(body: str, context: dict) -> str: out = body or '' out = out.replace('{{first_name}}', context.get('first_name') or '') out = out.replace('{{vehicle_model}}', context.get('vehicle_model') or '') return out.strip() 2) In the main poll loop: - Call rules, templates = load_rules_and_templates(client) - For each open thread with a new inbound message, try: r = match_rule(inbound_text, rules) if r: tpl = templates.get(r['template_slug']) if tpl: # Personalise msg = render_template(tpl['body'], customer_context) # Enforce max_len if provided max_len = tpl.get('max_len') or None if max_len and len(msg) > max_len: msg = msg[:max_len-1] + '…' # Respect reply window if ok_to_reply(): # send → currently we just "log" as outbound log_message( agreement_ref=agreement_number, channel='whatsapp', direction='outbound', message_text=msg, intent_label=r['label'], template_slug=r['template_slug'], source='ai', model=None ) else: # Out of hours: just log intent and schedule a nudge note log_message( agreement_ref=agreement_number, channel='whatsapp', direction='outbound', message_text='[deferred: out of hours] ' + msg, intent_label=r['label'], template_slug=r['template_slug'], source='ai', model=None ) # Service handoff: if rule/template implies service action if r['label'] in ('service_only', 'book_service') or r['template_slug'] in ('service_only','book_service'): appt = book_appt_request( agreement_ref=agreement_number, requested_by='ai', channel='whatsapp', notes='Service booking request via Lia' ) print(f"[SERVICE] booking request opened id={appt.get('id')}") continue # processed; go to next thread # No rule matched → fallback to OpenAI logic you already have # Ensure intent_label='openai_fallback', template_slug=None in log # Keep using Lia persona and ok_to_reply() check as before. 3) Console prints: - When rule fired: print(f"[RULE] label={r['label']} -> {r['template_slug']}") - When fallback used: print("[OPENAI] fallback used") Deliverables - Modify android_handler.py as above. - Keep existing deferral parser + follow-up logic intact. - Confirm with: “UPDATED handler: DB rules + templates now driving replies (with service handoff).”