- 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
195 lines
6.7 KiB
Python
195 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Junior AE v3 - Updates CRM Temp field directly"""
|
|
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-v3-state.json"
|
|
LOG_FILE = SCRIPT_DIR / "logs" / f"jae-v3-{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}
|
|
|
|
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=100&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 get_temp(title):
|
|
t = title.upper()
|
|
if 'HOT' in t: return 'HOT'
|
|
if 'WARM' in t: return 'WARM'
|
|
if 'COLD' in t: return 'COLD'
|
|
return None
|
|
|
|
def extract_url(body):
|
|
if not body:
|
|
return None
|
|
m = re.search(r'Site:\s*(https?://[^\s\n<]+)', str(body))
|
|
if m:
|
|
return m.group(1).strip()
|
|
m = re.search(r'(https?://[^\s\n<"]+)', str(body))
|
|
return m.group(1) if m else None
|
|
|
|
def validate_website(url):
|
|
if not url:
|
|
return False, "no_url"
|
|
if not url.startswith('http'):
|
|
url = 'https://' + url
|
|
|
|
try:
|
|
ssl_context = ssl._create_unverified_context()
|
|
req = urllib.request.Request(
|
|
url,
|
|
headers={
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
|
"Accept": "text/html,*/*",
|
|
}
|
|
)
|
|
with urllib.request.urlopen(req, timeout=15, context=ssl_context) as r:
|
|
content = r.read()
|
|
code = r.getcode()
|
|
|
|
if code != 200:
|
|
return False, f"http_{code}"
|
|
if len(content) < 500:
|
|
return False, "too_small"
|
|
|
|
html = content.decode('utf-8', errors='ignore')[:3000].lower()
|
|
has_title = '<title>' in html
|
|
has_body = '<body' in html
|
|
text = re.sub(r'<[^>]+>', '', html)
|
|
has_content = len(text.strip()) > 100
|
|
|
|
if has_title and has_body and has_content:
|
|
return True, "real_website"
|
|
return False, f"missing_{'title' if not has_title else 'body' if not has_body else 'content'}"
|
|
except urllib.error.HTTPError as e:
|
|
if e.code in [301, 302]:
|
|
new_url = e.headers.get('Location', '')
|
|
if new_url and new_url != url:
|
|
return validate_website(new_url)
|
|
return False, f"http_{e.code}"
|
|
except Exception as e:
|
|
return False, str(e)[:30]
|
|
|
|
def upgrade(temp):
|
|
return {'COLD': 'WARM', 'WARM': 'HOT', 'HOT': 'HOT'}.get(temp, temp)
|
|
|
|
def update_note_full(note_id, body, title, new_temp, status):
|
|
"""Update CRM Temp field and title"""
|
|
try:
|
|
# Update title to reflect new temp
|
|
new_title = title
|
|
for old in ['COLD', 'WARM', 'HOT']:
|
|
if old in title:
|
|
new_title = title.replace(old, new_temp)
|
|
break
|
|
|
|
# Update body with validation info
|
|
new_body = body + f"\n\n**JAE v3:** {datetime.now().strftime('%Y-%m-%d %H:%M')}\n" \
|
|
f"**Temp Updated:** {new_temp}\n" \
|
|
f"**Status:** {status}\n" \
|
|
f"**__JAE_Processed__**"
|
|
|
|
# CRM update - only update temp field (custom field on notes)
|
|
update_data = {
|
|
"title": new_title,
|
|
"bodyV2": {"markdown": new_body},
|
|
"temp": new_temp
|
|
}
|
|
|
|
data = json.dumps(update_data).encode()
|
|
req = urllib.request.Request(
|
|
f"{CRM_URL}/notes/{note_id}",
|
|
headers={"Authorization": f"Bearer {CRM_TOKEN}", "Content-Type": "application/json"},
|
|
data=data, method='PUT'
|
|
)
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
return True, new_title
|
|
except Exception as e:
|
|
log(f"Update failed: {e}")
|
|
return False, title
|
|
|
|
def process():
|
|
s = load_state()
|
|
log("=== JAE v3 Starting - Processing ALL notes ===")
|
|
notes = fetch_notes()
|
|
log(f"Fetched {len(notes)} notes")
|
|
|
|
for note in notes:
|
|
body = note.get('bodyV2', {}).get('markdown', '')
|
|
|
|
# Skip already processed by v3
|
|
if '__JAE_Processed__' in body:
|
|
continue
|
|
|
|
title = note.get('title', '')
|
|
note_id = note.get('id')
|
|
|
|
temp = get_temp(title)
|
|
if not temp:
|
|
log(f"Skip: no temp in title: {title[:35]}")
|
|
continue
|
|
|
|
url = extract_url(body)
|
|
if not url:
|
|
log(f"Skip: no URL: {title[:35]}")
|
|
continue
|
|
|
|
log(f"Validating: {url[:45]}")
|
|
is_valid, status = validate_website(url)
|
|
|
|
if is_valid and temp != 'HOT':
|
|
new_temp = upgrade(temp)
|
|
log(f"UPGRADE: {temp}->{new_temp} | {title[:40]}")
|
|
ok, new_title = update_note_full(note_id, body, title, new_temp, status)
|
|
if ok:
|
|
s['upgraded'] += 1
|
|
log(f" Updated title: {new_title[:50]}")
|
|
else:
|
|
# Still process to set Temp field even if not upgrading
|
|
ok, new_title = update_note_full(note_id, body, title, temp, f"verified_{status}")
|
|
log(f"Verified: {temp} | {title[:40]}")
|
|
|
|
s['processed'] += 1
|
|
time.sleep(0.5) # Rate limiting
|
|
|
|
s['last_check'] = datetime.now().isoformat()
|
|
save_state(s)
|
|
log(f"=== Done: {s['processed']} processed, {s['upgraded']} upgraded ===")
|
|
|
|
def main():
|
|
while True:
|
|
process()
|
|
log("Waiting 3 hours...")
|
|
time.sleep(10800)
|
|
|
|
if __name__ == "__main__":
|
|
main() |