feat: Add Chatwoot Agent Bot prototype and FAQ knowledge base
- Created chatwoot-agent-bot/ with Node.js webhook server - Bot detects intent (greeting, billing, technical, features, account) - Auto-responds from FAQ knowledge base or escalates to human - FAQ-KB.md: Living knowledge base that grows with customer questions - CHATWOOT-SETUP.md: Complete deployment and configuration guide - Supports Telegram notifications on escalation - Bot runs on port 3001, ready for Chatwoot webhook integration
This commit is contained in:
239
agents/junior-ae/tier1-scorer-full.py
Normal file
239
agents/junior-ae/tier1-scorer-full.py
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tier 1 Scorer - Full Database Scan
|
||||
- Processes ALL leads JAE v5 has researched
|
||||
- Updates CRM fields directly
|
||||
- Creates filterable views
|
||||
"""
|
||||
import json, re, time, urllib.request, ssl
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
STATE_FILE = SCRIPT_DIR / "state" / "tier1-state.json"
|
||||
JAE_STATE = SCRIPT_DIR / "state" / "jae-v5-state.json"
|
||||
CRM_URL = "https://salesforce.hoaledgeriq.com/rest"
|
||||
CRM_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5M2FmNGFmNS0zZWQ0LTQ1ZDMtOWE5Zi01MDMzZjc3YTY3MjMiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiOTNhZjRhZjUtM2VkNC00NWQzLTlhOWYtNTAzM2Y3N2E2NzIzIiwiaWF0IjoxNzczMzI4NDQzLCJleHAiOjE4MDQ3ODE2NDIsImp0aSI6IjIwZjEyYzkwLTRkMDctNGJmNi1iMzk3LTZjNmU3MzlmMThjOCJ9.zeM5NvwCSGEcz99m2LYtgb0sVD6WUXcCF7SwonFg930"
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def log(msg):
|
||||
ts = datetime.now().strftime('%H:%M:%S')
|
||||
print(f"[{ts}] {msg}")
|
||||
|
||||
def fetch_all_notes_paginated():
|
||||
"""Fetch all notes with pagination"""
|
||||
all_notes = []
|
||||
has_more = True
|
||||
end_cursor = None
|
||||
|
||||
log("Fetching all leads from CRM (with pagination)...")
|
||||
|
||||
while has_more:
|
||||
try:
|
||||
url = f"{CRM_URL}/notes?limit=200&order[createdAt]=desc"
|
||||
if end_cursor:
|
||||
url += f"&after={end_cursor}"
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {CRM_TOKEN}", "Accept": "application/json"}
|
||||
)
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context))
|
||||
with opener.open(req, timeout=30) as r:
|
||||
data = json.loads(r.read().decode())
|
||||
notes = data.get('data', {}).get('notes', [])
|
||||
all_notes.extend(notes)
|
||||
|
||||
# Check pagination
|
||||
page_info = data.get('pageInfo', {})
|
||||
has_more = page_info.get('hasNextPage', False)
|
||||
end_cursor = page_info.get('endCursor')
|
||||
|
||||
log(f" Fetched {len(notes)} leads (total: {len(all_notes)})")
|
||||
|
||||
if not has_more:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
log(f"Fetch error: {e}")
|
||||
break
|
||||
|
||||
log(f"Total leads fetched: {len(all_notes)}")
|
||||
return all_notes
|
||||
|
||||
def extract_units_from_note(note):
|
||||
"""Extract unit count from note body or title"""
|
||||
body = note.get('bodyV2', {}).get('markdown', '') if isinstance(note.get('bodyV2'), dict) else ''
|
||||
title = note.get('title', '')
|
||||
text = f"{title} {body}".lower()
|
||||
|
||||
# Look for unit patterns
|
||||
patterns = [
|
||||
r'units:\s*(\d{1,4})',
|
||||
r'(\d{1,4})\s*(?:homes|units|lots|properties|residences)',
|
||||
r'community\s*of\s*(\d{1,4})',
|
||||
r'(\d{1,4})\s*home\s*owners',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text, re.IGNORECASE)
|
||||
if match:
|
||||
try:
|
||||
units = int(match.group(1))
|
||||
if 10 <= units <= 5000:
|
||||
return units
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def has_budget_pdf(note):
|
||||
"""Check if note has budget PDF"""
|
||||
body = note.get('bodyV2', {}).get('markdown', '') if isinstance(note.get('bodyV2'), dict) else ''
|
||||
title = note.get('title', '')
|
||||
text = f"{title} {body}".lower()
|
||||
|
||||
# Check for budget mentions (JAE v5 format)
|
||||
if 'budget pdf' in text or 'budget.pdf' in text or 'found budget pdf' in text:
|
||||
return True
|
||||
# Also check title patterns from JAE research
|
||||
if 'budget' in text and 'found' in text:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_website(note):
|
||||
"""Check if note has website"""
|
||||
body = note.get('bodyV2', {}).get('markdown', '') if isinstance(note.get('bodyV2'), dict) else ''
|
||||
title = note.get('title', '')
|
||||
text = f"{title} {body}"
|
||||
return 'https://' in text or 'http://' in text
|
||||
|
||||
def get_temp(note):
|
||||
"""Get temperature from note"""
|
||||
temp = note.get('temp', 'COLD')
|
||||
if temp and temp.upper() in ['HOT', 'WARM', 'COLD']:
|
||||
return temp.upper()
|
||||
|
||||
title = note.get('title', '').upper()
|
||||
if title.startswith('HOT:'):
|
||||
return 'HOT'
|
||||
if title.startswith('WARM:'):
|
||||
return 'WARM'
|
||||
return 'COLD'
|
||||
|
||||
def score_lead(note):
|
||||
"""Score a lead 1-10 based on Tier 1 criteria"""
|
||||
temp = get_temp(note)
|
||||
units = extract_units_from_note(note)
|
||||
budget_pdf = has_budget_pdf(note)
|
||||
has_site = has_website(note)
|
||||
|
||||
score = 0
|
||||
|
||||
# 1. Temperature (max 3 points)
|
||||
if temp == 'HOT':
|
||||
score += 3
|
||||
elif temp == 'WARM':
|
||||
score += 2
|
||||
|
||||
# 2. Unit Count (max 4 points)
|
||||
if units:
|
||||
if 150 <= units <= 400:
|
||||
score += 4
|
||||
elif 100 <= units < 150 or 400 < units <= 500:
|
||||
score += 3
|
||||
elif 50 <= units < 100 or 500 < units <= 1000:
|
||||
score += 2
|
||||
else:
|
||||
score += 1
|
||||
|
||||
# 3. Budget PDF (max 2 points)
|
||||
if budget_pdf:
|
||||
score += 2
|
||||
elif has_site:
|
||||
score += 1
|
||||
|
||||
# 4. Website (max 1 point)
|
||||
if has_site:
|
||||
score += 1
|
||||
|
||||
return score
|
||||
|
||||
def update_crm_note(note_id, score, tier1_label):
|
||||
"""Update CRM note with tier1_score field"""
|
||||
try:
|
||||
patch_data = json.dumps({
|
||||
"tier1Score": score,
|
||||
"tier1Label": tier1_label
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{CRM_URL}/notes/{note_id}",
|
||||
data=patch_data,
|
||||
headers={
|
||||
"Authorization": f"Bearer {CRM_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method='PATCH'
|
||||
)
|
||||
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context))
|
||||
with opener.open(req, timeout=20) as r:
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def main():
|
||||
log("=" * 60)
|
||||
log("Tier 1 Scorer - Full Database Scan")
|
||||
log("=" * 60)
|
||||
|
||||
# Fetch all notes
|
||||
notes = fetch_all_notes_paginated()
|
||||
|
||||
total_scored = 0
|
||||
crm_updates = 0
|
||||
tier1_count = 0
|
||||
|
||||
for i, note in enumerate(notes):
|
||||
note_id = note.get('id')
|
||||
title = note.get('title', '')[:50]
|
||||
|
||||
# Score the lead
|
||||
score = score_lead(note)
|
||||
|
||||
# Determine Tier 1 label
|
||||
if score >= 8:
|
||||
tier1_label = "Tier 1 - Priority"
|
||||
elif score >= 6:
|
||||
tier1_label = "Tier 1"
|
||||
else:
|
||||
tier1_label = ""
|
||||
|
||||
# Update CRM if score is 6+
|
||||
if score >= 6:
|
||||
total_scored += 1
|
||||
if update_crm_note(note_id, score, tier1_label):
|
||||
crm_updates += 1
|
||||
tier1_count += 1
|
||||
if tier1_count <= 10: # Show first 10
|
||||
log(f" ✓ {title[:40]} (Score: {score}/10)")
|
||||
|
||||
# Progress indicator
|
||||
if (i + 1) % 100 == 0:
|
||||
log(f"Processed {i+1}/{len(notes)} leads...")
|
||||
|
||||
log("\n" + "=" * 60)
|
||||
log(f"Tier 1 Scoring Complete!")
|
||||
log(f" Total leads processed: {len(notes)}")
|
||||
log(f" Tier 1 leads (6+): {tier1_count}")
|
||||
log(f" CRM updates: {crm_updates}")
|
||||
log(f"\n📊 Filter in CRM:")
|
||||
log(f" • View: tier1_score >= 6")
|
||||
log(f" • Sort by: tier1_score DESC")
|
||||
log("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user