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:
2026-04-01 16:26:05 -04:00
parent 7ba19752de
commit 5319bcd30b
1074 changed files with 456376 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
# Twenty CRM Integration for HoaLedgerIQ Sales Lead Monitor
## API Configuration
- **Base URL:** `https://salesforce.hoaledgeriq.com/rest`
- **Auth Header:** `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5M2FmNGFmNS0zZWQ0LTQ1ZDMtOWE5Zi01MDMzZjc3YTY3MjMiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiOTNhZjRhZjUtM2VkNC00NWQzLTlhOWYtNTAzM2Y3N2E2NzIzIiwiaWF0IjoxNzczMzI4NDQzLCJleHAiOjE4MDQ3ODE2NDIsImp0aSI6IjIwZjEyYzkwLTRkMDctNGJmNi1iMzk3LTZjNmU3MzlmMThjOCJ9.zeM5NvwCSGEcz99m2LYtgb0sVD6WUXcCF7SwonFg930`
- **Owner ID:** `ecf52aad-4827-40c9-9475-b68f3ca9a924`
## Lead Sources
### 1. ROI Calculator Submissions
**Endpoint:** `GET https://hoaledgeriq.com/api/calc-submissions`
**Headers:**
```
x-admin-key: K9mP2vL8x4qR7nZ
```
**Response Fields:**
- `email` - Lead email address
- `homesites` - Number of homesites
- `annual_income` - Estimated annual income
- `property_type` - Type of property
- `payment_freq` - Payment frequency
- `reserve_funds` - Reserve funds amount
- `interest_2025` - Interest calculation
- `total_potential` - Total potential value
- `opt_in` - Whether user opted in
- `ai_recommendation` - AI-generated recommendation
- `created_at` - Submission timestamp
### 2. Direct Lead Form (localhost:3000)
**Endpoint:** `GET http://localhost:3000/api/leads`
**Headers:**
```
x-admin-key: K9mP2vL8x4qR7nZ
```
*Note: This endpoint is only available when the dev server is running locally.*
## CRM Create Flow: Calc Submission → Notes → Sales Review → Opportunity
Updated flow based on your sales process:
1. Leads go to **Notes** attached to People (not Opportunities)
2. Sales team reviews Notes and decides who to qualify
3. Qualified leads get promoted to Opportunities
### Previous Flow (Opportunity-first) - DEPRECATED
The integration previously created Opportunities directly. Now leads are queued in Notes for review.
### Step 1: Check if Person Exists
```bash
curl -s "https://salesforce.hoaledgeriq.com/rest/people" \
-H "Authorization: Bearer <token>"
```
Filter response for matching email.
### Step 2: Create Person (if not exists)
```bash
curl -s -X POST "https://salesforce.hoaledgeriq.com/rest/people" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": {
"firstName": "<email_local_part>",
"lastName": "(Lead)"
},
"emails": {
"primaryEmail": "<email>",
"additionalEmails": []
}
}'
```
Returns person ID (e.g., `2c28136a-2ba1-4234-be50-0130da851d3e`)
### Step 3: Create Note (Lead Queue)
```bash
curl -s -X POST "$TWENTY_BASE/notes" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"title": "Lead: <email> | <homesites> homes | $<annual_income>/y",
"body": "## 🏘️ ROI Calculator Lead\n\n**Email:** <email>\n**Homesites:** <homesites>\n...",
"personId": "<person_id>"
}'
```
Note goes to your Notes/Lead Review area in Twenty. Sales team promotes to Opportunity when qualified.
```bash
curl -s -X POST "https://salesforce.hoaledgeriq.com/rest/opportunities" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "ROI Calc - <email>",
"stage": "NEW",
"amount": {
"amountMicros": <annual_income * 1000000>,
"currencyCode": "USD"
},
"closeDate": "<date_90_days_from_now>",
"pointOfContactId": "<person_id>",
"ownerId": "ecf52aad-4827-40c9-9475-b68f3ca9a924"
}'
```
## Field Mappings
### Calc Submission → CRM
| Source Field | CRM Field | Notes |
|--------------|-----------|-------|
| email | Person.emails.primaryEmail | Primary identifier |
| email (before @) | Person.name.firstName | Parsed from email |
| "(Lead)" | Person.name.lastName | Default value |
| annual_income | Opportunity.amount.amountMicros | Multiplied by 1,000,000 |
| created_at + 90 days | Opportunity.closeDate | Auto-calculated |
| "NEW" | Opportunity.stage | Default stage |
| "ROI Calc - {email}" | Opportunity.name | Auto-generated |
### Calc Details → Opportunity Notes
Additional calc fields saved as notes:
- Homesites count
- Property type
- Payment frequency
- Reserve funds
- 2025 interest
- Total potential
- AI recommendation
## State Tracking
File: `/Users/claw/.openclaw/workspace/agents/sales-lead/state.json`
```json
{
"processed_leads": ["local_lead_id_1", "local_lead_id_2"],
"processed_calc_ids": [1, 2, 3, 4],
"crm_pushed": {
"calc_ids": [1],
"lead_ids": []
},
"last_check": "2026-03-12T13:00:00Z",
"status": "active"
}
```
## Testing
Test person created: `2c28136a-2ba1-4234-be50-0130da851d3e`
Test opportunity created: Validated working

View File

@@ -0,0 +1,21 @@
Checking calc submissions...
Checking calc submissions...
Checking calc submissions...
Checking calc submissions...
Checking calc submissions...
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=2, email=jane@example123.com
🎉 NEW LEAD: id=3, email=smith@example.com

198
agents/sales-lead/integrate.sh Executable file
View File

@@ -0,0 +1,198 @@
#!/bin/bash
# Sales Lead CRM Integration Script - v3 (Fixed deduplication)
# Polls website leads endpoint and pushes to Twenty CRM
set -e
WORKSPACE="/Users/claw/.openclaw/workspace/agents/sales-lead"
STATE_FILE="$WORKSPACE/state.json"
LOG_FILE="$WORKSPACE/integration.log"
NOTIFY_FILE="$WORKSPACE/notifications.log"
ADMIN_KEY="K9mP2vL8x4qR7nZ"
TWENTY_BASE="https://salesforce.hoaledgeriq.com/rest"
TWENTY_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5M2FmNGFmNS0zZWQ0LTQ1ZDMtOWE5Zi01MDMzZjc3YTY3MjMiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiOTNhZjRhZjUtM2VkNC00NWQzLTlhOWYtNTAzM2Y3N2E2NzIzIiwiaWF0IjoxNzczMzI4NDQzLCJleHAiOjE4MDQ3ODE2NDIsImp0aSI6IjIwZjEyYzkwLTRkMDctNGJmNi1iMzk3LTZjNmU3MzlmMThjOCJ9.zeM5NvwCSGEcz99m2LYtgb0sVD6WUXcCF7SwonFg930"
echo "[$(date)] Running CRM integration check" >> "$LOG_FILE"
mkdir -p "$WORKSPACE"
# Initialize state if not exists
if [[ ! -f "$STATE_FILE" ]]; then
echo '{"processed_leads": [], "processed_calc_ids": [], "crm_pushed": {"calc_ids": [], "lead_ids": []}, "last_check": null, "status": "active"}' > "$STATE_FILE"
fi
# Read current state
STATE=$(cat "$STATE_FILE")
PROCESSED_LEADS=$(echo "$STATE" | jq -r '.processed_leads // []')
echo "=== Checking endpoints at $(date) ===" >> "$LOG_FILE"
echo "Already processed leads: $(echo "$PROCESSED_LEADS" | jq -r '. | join(", ")')" >> "$LOG_FILE"
# Fetch leads from API
RESPONSE=$(curl -s -H "x-admin-key: $ADMIN_KEY" "https://www.hoaledgeriq.com/api/leads" 2>&1 || echo "CONNECTION_FAILED")
if [[ "$RESPONSE" == "CONNECTION_FAILED" ]]; then
echo "⚠️ hoaledgeriq.com/api/leads not accessible" >> "$LOG_FILE"
exit 1
fi
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
echo "⚠️ Invalid response from hoaledgeriq.com" >> "$LOG_FILE"
exit 1
fi
echo "✓ hoaledgeriq.com/api/leads responding" >> "$LOG_FILE"
# Save leads to temp file for processing
LEADS_TEMP=$(mktemp)
echo "$RESPONSE" | jq -r '.leads[]? | @base64' 2>/dev/null > "$LEADS_TEMP"
# Process each lead
NEW_PROCESSED="$PROCESSED_LEADS"
FOUND_COUNT=0
while IFS= read -r encoded; do
[[ -z "$encoded" ]] && continue
item=$(echo "$encoded" | base64 -d)
id=$(echo "$item" | jq -r '.id // empty')
email=$(echo "$item" | jq -r '.email // empty')
[[ -z "$id" || -z "$email" ]] && continue
# Check if already processed
if echo "$NEW_PROCESSED" | jq -e --arg id "$id" 'index($id)' >/dev/null 2>&1; then
echo "Skipping lead $id (already processed)" >> "$LOG_FILE"
continue
fi
echo "🎉 NEW LEAD: id=$id, email=$email" | tee -a "$LOG_FILE" "$NOTIFY_FILE"
# Send Telegram notification via OpenClaw
TG_MSG="🏠 NEW LEAD ALERT
Name: $display_name
Email: $email
Organization: $org_name
State: $state_loc
Role: $role
Units: $unit_count
Beta: $beta_text
Check CRM: salesforce.hoaledgeriq.com"
openclaw message send --text "$TG_MSG" 2>/dev/null || echo "Notification queued"
FOUND_COUNT=$((FOUND_COUNT + 1))
# Extract all fields
first_name=$(echo "$item" | jq -r '.first_name // ""')
last_name=$(echo "$item" | jq -r '.last_name // ""')
org_name=$(echo "$item" | jq -r '.org_name // "N/A"')
state_loc=$(echo "$item" | jq -r '.state // "N/A"')
role=$(echo "$item" | jq -r '.role // "N/A"')
unit_count=$(echo "$item" | jq -r '.unit_count // "N/A"')
beta_interest=$(echo "$item" | jq -r '.beta_interest // 0')
source=$(echo "$item" | jq -r '.source // "unknown"')
created_at=$(echo "$item" | jq -r '.created_at // "N/A"')
display_name="${first_name} ${last_name}"
[[ -z "$first_name" && -z "$last_name" ]] && display_name="Unknown Lead"
# Create Person
person_json="{\"name\":{\"firstName\":\"$first_name\",\"lastName\":\"$last_name\"},\"emails\":{\"primaryEmail\":\"$email\",\"additionalEmails\":[]}}"
person_res=$(curl -s -X POST "$TWENTY_BASE/people" \
-H "Authorization: Bearer $TWENTY_TOKEN" \
-H "Content-Type: application/json" \
-d "$person_json" 2>/dev/null)
person_id=$(echo "$person_res" | jq -r '.data.createPerson.id // empty')
if [[ -n "$person_id" ]]; then
echo "✓ Created Person: $person_id ($display_name)" >> "$LOG_FILE"
else
# Person exists - find by email
all_people=$(curl -s "$TWENTY_BASE/people" -H "Authorization: Bearer $TWENTY_TOKEN")
person_id=$(echo "$all_people" | jq -r --arg em "$email" '.data.people[] | select(.emails.primaryEmail == $em) | .id' | head -1)
[[ -n "$person_id" ]] && echo "✓ Found Person: $person_id ($display_name)" >> "$LOG_FILE"
fi
# Create Note
if [[ -n "$person_id" ]]; then
beta_text="No"
[[ "$beta_interest" == "1" ]] && beta_text="Yes"
# Build note body
note_text="## 🌐 Website Lead Submission
**Name:** $display_name
**Email:** $email
**Organization:** $org_name
**State:** $state_loc
**Role/Position:** $role
**Unit Count:** $unit_count
**Beta Interest:** $beta_text
**Source:** $source
**Submitted:** $created_at
---
*Submitted via website lead form. Review for opportunity qualification.*"
# Escape for JSON
escaped_body=$(echo "$note_text" | jq -Rs .)
note_json="{\"title\":\"🌐 Lead: $display_name | $org_name\",\"bodyV2\":{\"markdown\":"$escaped_body"}}"
note_res=$(curl -s -X POST "$TWENTY_BASE/notes" \
-H "Authorization: Bearer $TWENTY_TOKEN" \
-H "Content-Type: application/json" \
-d "$note_json" 2>/dev/null)
note_id=$(echo "$note_res" | jq -r '.data.createOneNote.id // empty')
if [[ -n "$note_id" ]]; then
echo "✓ Created Note: $note_id ($org_name, $role)" >> "$LOG_FILE"
else
echo "⚠️ Note creation response: $note_res" >> "$LOG_FILE"
fi
fi
# Add to processed list (update in memory and immediately save)
NEW_PROCESSED=$(echo "$NEW_PROCESSED" | jq --arg id "$id" '. + [$id]')
echo "$STATE" | jq --argjson processed "$NEW_PROCESSED" '.processed_leads = $processed' > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
done < "$LEADS_TEMP"
# Clean up temp file
rm -f "$LEADS_TEMP"
# Update last check timestamp
STATE=$(cat "$STATE_FILE")
echo "$STATE" | jq --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '.last_check = $ts' > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
if [[ $FOUND_COUNT -eq 0 ]]; then
echo "No new leads found" >> "$LOG_FILE"
else
echo "Processed $FOUND_COUNT new leads" >> "$LOG_FILE"
fi
echo "=== Completed at $(date) ===" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
# Telegram notification function
send_telegram_notification() {
local name="$1"
local email="$2"
local org="$3"
local state="$4"
local role="$5"
local units="$6"
local beta="$7"
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{
\"chat_id\": \"${TELEGRAM_CHAT_ID}\",
\"text\": \"🏠 NEW LEAD ALERT\\n\\nName: $name\\nEmail: $email\\nOrganization: $org\\nState: $state\\nRole: $role\\nUnits: $units\\nBeta: $beta\\n\\nCheck CRM for details\",
\"parse_mode\": \"Markdown\"
}" 2>/dev/null || echo "Telegram notification sent"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

48
agents/sales-lead/monitor.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
# HoaLedgerIQ Sales Lead Monitor
# Polls ROI Calculator endpoint every hour during business hours
STATE_FILE="/Users/claw/.openclaw/workspace/agents/sales-lead/state.json"
LOG_FILE="/Users/claw/.openclaw/workspace/agents/sales-lead/monitor.log"
INTEGRATION_LOG="/Users/claw/.openclaw/workspace/agents/sales-lead/integration.log"
# Function to check ROI Calculator submissions (primary lead source)
check_calc_submissions() {
curl -s -H "x-admin-key: K9mP2vL8x4qR7nZ" https://www.hoaledgeriq.com/api/calc-submissions 2>/dev/null
}
# Log with timestamp
log() {
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $1" >> "$LOG_FILE"
}
# Log to integration log
log_integration() {
echo "[$(date)] $1" >> "$INTEGRATION_LOG"
}
# Main monitoring loop
log "Starting lead monitor check"
CALC_SUBS=$(check_calc_submissions)
log "ROI Calc submissions response: ${#CALC_SUBS} bytes"
# Parse and check for new submissions
if [ ${#CALC_SUBS} -gt 0 ]; then
# Extract submission IDs (simplified - just count for now)
log "Processing calc submissions..."
log_integration "✓ hoaledgeriq.com/api/calc-submissions responding"
log_integration "Response size: ${#CALC_SUBS} bytes"
fi
# Update state
cat > "$STATE_FILE" << EOF
{
"processed_leads": [],
"processed_calc_ids": [1, 2, 3, 4],
"last_check": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"status": "active",
"notes": "Hourly monitoring enabled. Next check in 60 minutes."
}
EOF
log "Check complete. Next run at $(date -v+60M +"%Y-%m-%dT%H:%M:%Z")"

View File

@@ -0,0 +1,23 @@
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=1, email=john@example.com
🎉 NEW LEAD: id=2, email=jane@example123.com
🎉 NEW LEAD: id=3, email=smith@example.com

View File

@@ -0,0 +1,7 @@
{
"processed_leads": [],
"processed_calc_ids": [1, 2, 3, 4],
"last_check": "2026-04-01T20:02:25Z",
"status": "active",
"notes": "Hourly monitoring enabled. Next check in 60 minutes."
}