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:
190
agents/junior-ae/junior-ae-v4.py
Executable file
190
agents/junior-ae/junior-ae-v4.py
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Junior AE v4 - Process ALL leads, auto-detect temperature
|
||||
- Processes notes with or without temperature prefixes
|
||||
- Auto-detects temperature from content if not in title
|
||||
- Elevates HOT/WARM leads, skips COLD
|
||||
"""
|
||||
import json, re, time, urllib.request, urllib.error
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import ssl
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
for d in [SCRIPT_DIR / "state", SCRIPT_DIR / "logs"]:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
STATE_FILE = SCRIPT_DIR / "state" / "jae-v4-state.json"
|
||||
LOG_FILE = SCRIPT_DIR / "logs" / f"jae-v4-{datetime.now().strftime('%Y%m%d')}.log"
|
||||
CRM_URL = "https://salesforce.hoaledgeriq.com/rest"
|
||||
CRM_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5M2FmNGFmNS0zZWQ0LTQ1ZDMtOWE5Zi01MDMzZjc3YTY3MjMiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiOTNhZjRhZjUtM2VkNC00NWQzLTlhOWYtNTAzM2Y3N2E2NzIzIiwiaWF0IjoxNzczMzI4NDQzLCJleHAiOjE4MDQ3ODE2NDIsImp0aSI6IjIwZjEyYzkwLTRkMDctNGJmNi1iMzk3LTZjNmU3MzlmMThjOCJ9.zeM5NvwCSGEcz99m2LYtgb0sVD6WUXcCF7SwonFg930"
|
||||
|
||||
def log(msg):
|
||||
ts = datetime.now().strftime('%H:%M:%S')
|
||||
print(f"[{ts}] {msg}")
|
||||
with open(LOG_FILE, 'a') as f:
|
||||
f.write(f"[{ts}] {msg}\n")
|
||||
|
||||
def load_state():
|
||||
if STATE_FILE.exists():
|
||||
return json.loads(STATE_FILE.read_text())
|
||||
return {"last_check": (datetime.now() - timedelta(days=7)).isoformat(), "processed": 0, "upgraded": 0, "processed_ids": []}
|
||||
|
||||
def save_state(s):
|
||||
STATE_FILE.write_text(json.dumps(s, indent=2))
|
||||
|
||||
def fetch_notes():
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{CRM_URL}/notes?limit=200&order[createdAt]=desc",
|
||||
headers={"Authorization": f"Bearer {CRM_TOKEN}", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
return json.loads(r.read().decode()).get('data', {}).get('notes', [])
|
||||
except Exception as e:
|
||||
log(f"Fetch error: {e}")
|
||||
return []
|
||||
|
||||
def detect_temp(title, body=""):
|
||||
"""Detect temperature from title or content"""
|
||||
text = f"{title} {body}".upper()
|
||||
|
||||
# Check for explicit temperature
|
||||
if 'HOT' in text or 'HIGH' in text or 'URGENT' in text:
|
||||
return 'HOT'
|
||||
if 'WARM' in text or 'MEDIUM' in text or 'INTERESTED' in text:
|
||||
return 'WARM'
|
||||
if 'COLD' in text or 'LOW' in text or 'NOT INTERESTED' in text:
|
||||
return 'COLD'
|
||||
|
||||
# Auto-detect from engagement signals
|
||||
hot_signals = ['READY', 'INTERESTED', 'WANTS', 'NEEDS', 'BUDGET', 'TIMELINE', 'SOON', 'QUICK']
|
||||
warm_signals = ['CONSIDERING', 'THINKING', 'MAYBE', 'LATER', 'RESEARCH', 'COMPARE']
|
||||
|
||||
for signal in hot_signals:
|
||||
if signal in text:
|
||||
return 'WARM' # Default to WARM if unsure
|
||||
|
||||
for signal in warm_signals:
|
||||
if signal in text:
|
||||
return 'WARM'
|
||||
|
||||
# Default to WARM for unclassified leads (better to over-qualify)
|
||||
return 'WARM'
|
||||
|
||||
def update_note_temp(note_id, new_temp):
|
||||
"""Update note title with temperature"""
|
||||
try:
|
||||
# Get current note
|
||||
req = urllib.request.Request(
|
||||
f"{CRM_URL}/notes/{note_id}",
|
||||
headers={"Authorization": f"Bearer {CRM_TOKEN}", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
note = json.loads(r.read().decode()).get('data', {})
|
||||
|
||||
# Update title
|
||||
old_title = note.get('title', '')
|
||||
new_title = re.sub(r'^(HOT|WARM|COLD):\s*', '', old_title) # Remove old temp
|
||||
new_title = f"{new_temp}: {new_title}"
|
||||
|
||||
# Patch the note
|
||||
patch_data = json.dumps({"title": new_title}).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'
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f"Update error: {e}")
|
||||
return False
|
||||
|
||||
def create_opportunity(note, temp):
|
||||
"""Create opportunity for HOT/WARM leads"""
|
||||
try:
|
||||
person_id = note.get('personId')
|
||||
if not person_id:
|
||||
log(f" Skip: No person ID")
|
||||
return False
|
||||
|
||||
# Check if opportunity already exists
|
||||
opp_name = f"Lead: {note.get('title', '')}"
|
||||
|
||||
opp_data = {
|
||||
"name": opp_name[:100],
|
||||
"stage": "NEW",
|
||||
"pointOfContactId": person_id,
|
||||
"ownerId": "ecf52aad-4827-40c9-9475-b68f3ca9a924"
|
||||
}
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{CRM_URL}/opportunities",
|
||||
data=json.dumps(opp_data).encode(),
|
||||
headers={"Authorization": f"Bearer {CRM_TOKEN}", "Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
opp = json.loads(r.read().decode())
|
||||
log(f" ✓ UPGRADED to Opportunity: {opp.get('id', 'N/A')}")
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f" ✗ Create opp error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
log("=== JAE v4 Starting - Auto-Temperature Detection ===")
|
||||
|
||||
state = load_state()
|
||||
processed_ids = state.get('processed_ids', [])
|
||||
|
||||
notes = fetch_notes()
|
||||
log(f"Fetched {len(notes)} notes")
|
||||
|
||||
upgraded = 0
|
||||
processed = 0
|
||||
|
||||
for note in notes:
|
||||
note_id = note.get('id')
|
||||
title = note.get('title', '')
|
||||
|
||||
# Skip if already processed
|
||||
if note_id in processed_ids:
|
||||
continue
|
||||
|
||||
processed += 1
|
||||
processed_ids.append(note_id)
|
||||
|
||||
# Detect temperature
|
||||
body = note.get('body', '')
|
||||
temp = detect_temp(title, body)
|
||||
|
||||
log(f"Processing: {title[:60]}... -> {temp}")
|
||||
|
||||
# Update title with temperature
|
||||
if not title.startswith(f"{temp}:"):
|
||||
update_note_temp(note_id, temp)
|
||||
|
||||
# Create opportunity for HOT/WARM
|
||||
if temp in ['HOT', 'WARM']:
|
||||
if create_opportunity(note, temp):
|
||||
upgraded += 1
|
||||
else:
|
||||
log(f" Skipped: COLD lead")
|
||||
|
||||
# Rate limit
|
||||
time.sleep(0.5)
|
||||
|
||||
# Save state
|
||||
state['processed'] = processed
|
||||
state['upgraded'] = state.get('upgraded', 0) + upgraded
|
||||
state['processed_ids'] = processed_ids[-1000:] # Keep last 1000
|
||||
state['last_check'] = datetime.now().isoformat()
|
||||
save_state(state)
|
||||
|
||||
log(f"=== Done: {processed} processed, {upgraded} upgraded ===")
|
||||
log("Waiting 3 hours...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user