Intelligent Vehicle Dealer Network — Sri Lanka / New Zealand Market Inventory CRM · LangGraph AI Brain · WhatsApp Cloud API Delivery Layer
The Auto-Auction AI Chatbot is a production-grade, multi-tier vehicle sales automation ecosystem built for automobile dealerships operating in Sri Lanka and the New Zealand import market. Rather than a single monolithic bot, the system is deliberately split into three distinct micro-projects — each with a single, focused responsibility — that communicate over HTTP and share a common PostgreSQL database.
The architecture enforces strict separation of concerns:
| Layer | Project | Responsibility |
|---|---|---|
| 📊 Data Layer | aai-chatbot-crm (Laravel 12) |
Inventory entry, dealer management, RBAC |
| 🧠 Intelligence Layer | ai-chatbot-aai-v2 (Python / LangGraph) |
Conversational AI, query routing, DB reads |
| 💬 Delivery Layer | whatsapp-hook-aai-v2 (Python / Flask + Waitress) |
WhatsApp webhook, batching, rich UI cards |
💡 Persona: The AI presents itself as Ravi, a vehicle sales agent with 10 years of experience at Lanka Motors, Colombo, Sri Lanka — responding only in English and always in NZD (New Zealand Dollars).
flowchart TB
subgraph CRM["🏢 aai-chatbot-crm (Laravel 12)"]
direction TB
Admin["👨💼 Admin / Dealer Staff"]
LaravelApp["Laravel Application\n(PHP 8.2 + Breeze Auth)"]
LaravelDB["PostgreSQL\nInventory Database"]
Admin -->|"CRUD via Web UI"| LaravelApp
LaravelApp -->|"Eloquent ORM"| LaravelDB
end
subgraph AI["🧠 ai-chatbot-aai-v2 (FastAPI + LangGraph)"]
direction TB
FastAPI["FastAPI Server\n:9095"]
LangGraph["LangGraph State Machine\nguardrail → agent → tools → response_guardrail → handle_response"]
GPT["GPT-4o-mini\n(Primary LLM)"]
DeepSeek["DeepSeek Chat\n(Fallback LLM)"]
Tools["8 LangChain Tools\nsearch_local_db · browse_inventory\ncheck_stock_with_fallback · get_models_by_brand\nsearch_nhtsa · update_db · register_user_request\nget_current_datetime"]
Redis["Redis\nSession Store (3h TTL)"]
FastAPI --> LangGraph
LangGraph --> GPT
GPT -.->|"on error"| DeepSeek
LangGraph --> Tools
LangGraph -->|"session R/W"| Redis
end
subgraph WA["💬 whatsapp-hook-aai-v2 (Flask)"]
direction TB
MetaAPI["Meta WhatsApp\nCloud API"]
FlaskServer["**Flask + Waitress Server\n:8000**"]
Batcher["Message Batcher\n4-second window"]
Formatter["Rich UI Formatter\nImages · Buttons · Pagination"]
MetaAPI <-->|"HTTPS Webhook"| FlaskServer
FlaskServer --> Batcher
Batcher --> Formatter
end
User["👤 WhatsApp User"] <-->|"WhatsApp messages"| MetaAPI
FlaskServer <-->|"POST /chat\nlocalhost:9095"| FastAPI
Tools <-->|"READ ONLY\nSQLAlchemy"| LaravelDB
How a single user message travels through all three projects from entry to delivery:
sequenceDiagram
participant U as 👤 WhatsApp User
participant M as Meta Cloud API
participant F as Flask Webhook
participant B as Message Batcher
participant A as FastAPI Agent
participant G as LangGraph
participant D as PostgreSQL DB
participant R as Redis
U->>M: "show honda black under 10000"
M->>F: POST /webhook (JSON payload)
F->>F: Validate webhook signature
F->>B: Queue message for wa_id
Note over B: Wait 4 seconds for more messages
B->>A: POST /chat {message, session_id, dealer_whatsapp_number}
A->>R: Load conversation history (session_id)
A->>G: Invoke LangGraph with AgentState
G->>G: guardrail_node → check_guardrail
G->>G: agent_node (GPT-4o-mini + tools)
G->>D: browse_inventory(make=Honda, color=Black, max_price=10000)
D-->>G: Returns matching vehicles
G->>G: response_guardrail_node
G->>G: handle_response (format JSON)
G-->>A: {response, inventory_items, pagination, model_buttons, variant_buttons}
A->>R: Update session history
A-->>F: ChatResponse JSON
F->>M: Send vehicle image + caption (hero card)
F->>M: Send "📷 More Photos" button
F->>M: Send "➡️ Next 6 Vehicles" button (if paginated)
M->>U: Rich WhatsApp UI with vehicle cards
aai-chatbot-crm is the data entry and administration portal built with Laravel 12 (PHP 8.2). It is the single authoritative source of truth for all vehicle inventory. Staff and administrators log in to manage the full vehicle lifecycle — from first entry to final sale — across two distinct stock types:
- Local / Showroom Stock (
inventorytable): Vehicles physically present at the dealership. - Nichibo / Auction Import Stock (
nichibo_stocktable): Vehicles sourced from Japanese auction houses (Nichibo), with detailed auction-grade condition reports.
| Technology | Version | Role |
|---|---|---|
| Laravel | 12.x | MVC web framework |
| PHP | ≥ 8.2 | Runtime |
| Laravel Breeze | 2.3 | Authentication scaffolding |
| Tailwind CSS | 3.x | Frontend utility styling |
| Vite | 6.x | Frontend bundler |
| PostgreSQL | — | Primary database |
| SQLite | — | Local/dev database (default .env) |
| Redis | — | Session, cache, queue backend |
The CRM manages a rich, normalized schema. Below is a breakdown of every Eloquent model and its purpose:
| Model | Table | Purpose |
|---|---|---|
User |
users |
Admin/staff accounts with RBAC |
Dealer |
dealers |
Dealership profiles, WhatsApp numbers, stock preferences |
Vehicle |
vehicles |
Vehicle catalogue (make, model, variant, specs) |
Inventory |
inventory |
Showroom stock — physical vehicles available for sale |
InventoryImage |
inventory_images |
Ordered gallery images per showroom inventory item |
NichiboStock |
nichibo_stock |
Auction/import stock from Japanese auctions |
NichiboStockVehiclePhoto |
nichibo_stock_vehicle_photos |
Gallery images for Nichibo stock |
| Model | Table | Purpose |
|---|---|---|
Make |
makes |
Vehicle brand catalogue (Toyota, Honda, etc.) |
FuelType |
fuel_types |
Petrol, Diesel, Hybrid, Electric |
Transmission |
transmissions |
Automatic, Manual |
DriveType |
drive_types |
2WD, 4WD, AWD |
Colour |
colours |
Available body colours |
Grade |
grades |
Auction grades (auction quality rating) |
Option |
options |
Vehicle extras (drivetrain, stereo, transmission option, trim material, airbags, etc.) |
Comment |
comments |
Auction notes (notes, options, condition, accessories) |
| Model | Table | Purpose |
|---|---|---|
ExteriorCondition |
exterior_conditions |
Panel damage count, tyre tread, bumper damage, underbody |
InteriorCondition |
interior_conditions |
Smell, dirt, dashboard scratches, cigarette burns, dashboard damage |
InstrumentsAndControl |
instruments_and_controls |
Mirror conditions, power window status, front screen damage |
CigaretteBurn |
cigarette_burns |
Cigarette burn severity lookup |
DashboardScratch |
dashboard_scratches |
Dashboard scratch level lookup |
DashboardDamage |
dashboard_damages |
Dashboard damage type lookup |
Dirt |
dirt |
Interior dirt level lookup |
Smell |
smells |
Interior odour type lookup |
LeftSideMirror |
left_side_mirror |
Left mirror condition lookup |
RightSideMirror |
right_side_mirror |
Right mirror condition lookup |
PowerWindow |
power_windows |
Power window status lookup |
FrontWindowScreenDamage |
front_window_screen_damages |
Windscreen damage lookup |
Drivechain |
drivechains |
Drivetrain type lookup |
Stereo |
stereos |
Audio system type lookup |
TransmissionOption |
transmission_options |
Gearbox type lookup |
UnderbodyType |
underbody_types |
Underbody condition lookup |
| Model | Table | Purpose |
|---|---|---|
Permission |
permissions |
Named permission slugs (e.g. vehicles.create) |
User ↔ Permission |
user_permissions |
Many-to-many assignment |
Vehicle (1) ──< Inventory (M) ──< InventoryImage (M)
Vehicle (1) ──< NichiboStock (M) ──< NichiboStockVehiclePhoto (M)
NichiboStock (M) >── Grade (1)
NichiboStock (M) >── Comment (1)
NichiboStock (M) >── Option (1) >── Drivechain, Stereo, TransmissionOption
NichiboStock (M) >── ExteriorCondition (1) >── UnderbodyType
NichiboStock (M) >── InteriorCondition (1) >── Smell, Dirt, DashboardScratch, CigaretteBurn, DashboardDamage
NichiboStock (M) >── InstrumentsAndControl (1) >── LeftSideMirror, RightSideMirror, PowerWindow, FrontWindowScreenDamage
User (M) ><< Permission (M) [via user_permissions]
Inventory (M) >── Dealer (1)
All routes are protected by Laravel's auth middleware. Granular CRUD access is enforced per-resource by a custom permission middleware.
vehicles.view / vehicles.create / vehicles.edit / vehicles.delete
dealers.view / dealers.create / dealers.edit / dealers.delete
inventory.view / inventory.create / inventory.edit / inventory.delete
nichibo_stock.view / nichibo_stock.create / nichibo_stock.edit / nichibo_stock.delete
vehicle_attributes.view / vehicle_attributes.create / vehicle_attributes.edit / vehicle_attributes.delete
nichibo_attributes.view / nichibo_attributes.create / nichibo_attributes.edit / nichibo_attributes.delete
admins.view / admins.create / admins.edit / admins.delete
| Controller | File | Responsibilities |
|---|---|---|
VehicleController |
VehicleController.php |
CRUD for vehicle catalogue (make/model/variant/specs) |
DealerController |
DealerController.php |
CRUD for dealer profiles, generates DLR#### IDs |
InventoryController |
InventoryController.php (23 KB) |
Showroom stock CRUD, image uploads, status management |
NichiboStockController |
NichiboStockController.php (35 KB) |
Auction stock CRUD, full condition report management |
VehicleAttributeController |
VehicleAttributeController.php (10 KB) |
CRUD for makes, fuel types, transmissions, drive types, colours |
NichiboStockAttributeController |
NichiboStockAttributeController.php (19 KB) |
CRUD for all 14 Nichibo attribute lookup tables |
ExportController |
ExportController.php (14 KB) |
CSV/Excel export for showroom and Nichibo inventory |
Admin/AdminController |
Admin/AdminController.php |
Admin/staff user management, permission assignment, status toggle |
ProfileController |
ProfileController.php |
Authenticated user profile edit/delete |
GET /api/makes → JSON list of all makes
GET /api/fuel-types → JSON list of fuel types
GET /api/transmissions → JSON list of transmissions
GET /api/drive-types → JSON list of drive types
GET /api/colours → JSON list of colours
GET /export/inventory → Download showroom stock as CSV/Excel
GET /export/nichibo-stock → Download auction stock as CSV/Excel
- Laravel Breeze provides session-based authentication with login, registration, and password reset.
User::isSuperAdmin()— bypasses all permission checks.User::hasPermission(string $permission)— checksuser_permissionspivot table.- Custom
permissionmiddleware reads the permission slug from the route and checks against the authenticated user.
APP_NAME=Laravel
APP_ENV=local
APP_KEY= # Generated via artisan key:generate
APP_URL=http://localhost
DB_CONNECTION=pgsql # Switch to pgsql for production
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=vehicle_db
DB_USERNAME=
DB_PASSWORD=
SESSION_DRIVER=database
CACHE_STORE=database
QUEUE_CONNECTION=database
REDIS_HOST=127.0.0.1
REDIS_PORT=6379aai-chatbot-crm/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Admin/AdminController.php # Admin user management
│ │ │ ├── DealerController.php # Dealer CRUD
│ │ │ ├── ExportController.php # CSV/Excel exports
│ │ │ ├── InventoryController.php # Showroom stock CRUD (23KB)
│ │ │ ├── NichiboStockController.php # Auction stock CRUD (35KB)
│ │ │ ├── NichiboStockAttributeController.php # 14 attribute tables
│ │ │ ├── VehicleAttributeController.php # Make/fuel/transmission/colour
│ │ │ └── VehicleController.php # Vehicle catalogue CRUD
│ │ ├── Middleware/
│ │ │ └── PermissionMiddleware.php # Custom RBAC middleware
│ │ └── Requests/ # Form request validation
│ ├── Models/ # 32 Eloquent models
│ ├── Providers/
│ └── View/
├── database/
│ ├── migrations/ # 11 schema migration files
│ └── seeders/
├── routes/
│ ├── web.php # All 80+ named routes
│ └── auth.php # Breeze auth routes
├── resources/
│ └── views/ # Blade templates (Tailwind)
├── config/
├── composer.json
└── package.json
# Navigate to project directory
cd aai-chatbot-crm
# Install PHP dependencies
composer install
# Copy and configure environment
cp .env.example .env
php artisan key:generate
# Configure database in .env (PostgreSQL or SQLite for dev)
# Run migrations
php artisan migrate --seed
# Install and build frontend assets
npm install && npm run build
# Start development server (runs server + queue + logs + vite concurrently)
composer run dev
# OR just the HTTP server
php artisan serveai-chatbot-aai-v2 is the intelligence engine of the entire system. It is a FastAPI application running on port 9095 that hosts a stateful LangGraph conversational agent. The agent uses GPT-4o-mini as its primary reasoning model (with DeepSeek as a fallback), has access to 8 specialized LangChain tools to query the shared PostgreSQL database, maintains per-user conversation history in Redis, and returns structured JSON responses that the WhatsApp layer can render as rich UI.
This project strictly follows Clean Architecture (Domain → Application → Infrastructure → Interfaces), keeping all business logic in the domain layer and all external concerns in the infrastructure layer.
| Technology | Version | Role |
|---|---|---|
| FastAPI | — | Async REST API server |
| Uvicorn | — | ASGI server |
| LangGraph | — | Deterministic agent state machine |
| LangChain | — | Tool definition, message history, LLM binding |
| GPT-4o-mini | OpenAI | Primary reasoning LLM (fast, low-cost) |
| DeepSeek Chat | DeepSeek | Fallback LLM when OpenAI fails |
| Google Gemini | Optional LLM (via langchain-google-genai) |
|
| SQLAlchemy | — | ORM for PostgreSQL |
| psycopg2 | — | PostgreSQL adapter |
| Redis | — | Per-user session state (3-hour TTL) |
| Pinecone | — | Vector store (optional, for semantic search) |
| Sentence Transformers | — | Local embeddings |
| LangSmith | — | Tracing and observability |
| python-dotenv | — | Environment variable loading |
The entire conversational logic is implemented as a directed graph with conditional edges, ensuring deterministic, testable execution paths.
# src/application/agent/graph.py
graph = StateGraph(AgentState)
graph.add_node("guardrail", guardrail_node)
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.add_node("response_guardrail", response_guardrail_node)
graph.add_node("handle_response", handle_response)
graph.add_node("confirm_variant", confirm_variant)
graph.set_entry_point("guardrail")
graph.add_conditional_edges("guardrail", check_guardrail, {
"allowed": "agent",
"blocked": END
})
graph.add_conditional_edges("agent", should_continue, {
"tools": "tools",
"handle_response": "response_guardrail"
})
graph.add_edge("response_guardrail", "handle_response")
graph.add_edge("tools", "agent")
graph.add_edge("handle_response", END)
graph.add_edge("confirm_variant", END)
app = graph.compile()| Node | Function | Description |
|---|---|---|
guardrail |
guardrail_node |
Input safety filter — blocks off-topic, harmful, or irrelevant queries before they reach the main agent |
agent |
agent_node |
Core reasoning step — GPT-4o-mini with 8 bound tools analyzes the message and decides what tool to call |
tools |
tool_node |
Tool executor — runs the LangChain tool the agent chose and returns the result back to the agent |
response_guardrail |
response_guardrail_node |
Output safety filter — validates the agent's proposed response before delivery |
handle_response |
handle_response |
Response formatter — converts agent output into structured JSON for the WhatsApp layer |
confirm_variant |
confirm_variant |
Variant confirmation — handles the UI flow when a user is confirming a specific vehicle variant |
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages] # Full conversation history
user_id: str # Session / WhatsApp ID
phase: str # Current conversation phase ("initial", "confirm_variant", etc.)
data: dict # Arbitrary state data passed between nodes
dealer_id: Optional[str] # Active dealer identifier
dealer_config: Optional[Any] # DealerConfig object with stock preferencesThe agent has access to 8 tools — all thin wrappers around domain services following Clean Architecture:
tools = [
search_local_db, # Full-text search of inventory by natural language query
search_nhtsa, # Query NHTSA external vehicle safety database
update_db, # Update vehicle status (mark as sold/reserved)
check_stock_with_fallback, # Primary vehicle lookup by make/model/variant/filters
register_user_request, # Log user contact/interest requests to DB
browse_inventory, # Exploratory browsing by budget, fuel, body type
get_current_datetime, # Return current date/time in Asia/Colombo timezone
get_models_by_brand, # List all available models for a given make (showroom + import)
]
get_models_by_brand(make, dealer_id=None)
- Called when user says "show toyota" or "what Honda cars do you have?"
- Searches both showroom and import stock depending on dealer config and system stock switch priority
- Returns model list with counts, price ranges, variants, and button payloads for WhatsApp interactive buttons
browse_inventory(make, model, fuel_type, color, max_price, min_price, min_year, max_year, max_mileage, dealer_id)
- Exploratory search — returns grouped make/model listings
- Used for TYPE A queries: "show me petrol cars under 8000", "family SUV", "affordable hatchback"
- Preserves all filter parameters across multi-turn conversations
check_stock_with_fallback(make, model, variant, color, max_price, min_year, max_mileage, dealer_id)
- Specific lookup — returns paginated vehicle cards with full details and images
- Applies stock-switch logic: tries showroom first, falls back to import stock if empty (based on dealer config)
- Supports pagination: returns 6 vehicles per page with
remaining_itemsfor "Next 6" button
search_local_db(query, dealer_id)
- Full-text search across all vehicle attributes
search_nhtsa(query)
- Queries the NHTSA (National Highway Traffic Safety Administration) API for US safety data and specs
register_user_request(user_id, request_type, details)
- Logs customer interest/contact requests to the database for dealer follow-up
update_db(stock_type, stock_id, status)
- Updates vehicle status (Available → Sold/Reserved) — write access controlled by the agent only
get_current_datetime()
- Returns the current date/time in Asia/Colombo timezone for time-aware responses
# Primary: GPT-4o-mini (fast, reliable, cost-effective)
llm_openai = ChatOpenAI(model="gpt-4o-mini", max_retries=2, timeout=30)
# Guardrail-specific fast model
llm_fast = ChatOpenAI(model="gpt-4o-mini", max_retries=1, timeout=15)
# Fallback: DeepSeek (independent API, high availability)
llm_deepseek = ChatOpenAI(
model="deepseek-chat",
base_url="https://api.deepseek.com",
max_retries=1, timeout=30
)
# Chained with automatic fallback
llm_with_tools = llm_openai_with_tools.with_fallbacks([llm_deepseek_with_tools])The agent is instructed via a comprehensive ChatPromptTemplate that enforces:
- Persona: "Ravi", 10-year veteran vehicle sales agent at Lanka Motors, Colombo
- Currency: All prices must be NZD — never LKR, "million", or "M"
- Language: English only
- Format rules: Strict WhatsApp markdown formatting (bold via
*, italic via_, code via backtick) - Greeting policy: Respond to greetings with exactly "Hi, what kind of vehicle are you looking for?"
- Filter preservation: Budget, color, year, mileage filters must be carried forward through ALL subsequent tool calls
- Query type classification (4 types):
- TYPE A — Exploratory (no make/model): Use
browse_inventory - TYPE B — Brand only: Use
get_models_by_brand - TYPE C — Brand + Model: Use
check_stock_with_fallback - TYPE D — Brand + Model + Variant: Use
check_stock_with_fallbackwith variant
- TYPE A — Exploratory (no make/model): Use
The AI implements a configurable dual-stock system with three priority modes:
| Priority Mode | Behavior |
|---|---|
high (System Override) |
Always show BOTH showroom + import stock combined |
low (Dealer-Controlled) |
Dealer config decides when to show import stock |
off |
Only showroom stock is ever shown |
Per-dealer configuration in DealerConfig:
show_external_stock_when_unavailable: bool # Show auction if local empty?
prefer_local_stock: bool # Try local first?
auto_switch_on_empty: bool # Auto-fallback without asking user?src/
├── application/ # Use cases & agent orchestration
│ ├── agent/
│ │ ├── graph.py # LangGraph state machine definition
│ │ ├── nodes.py # All 6 graph node implementations (78KB)
│ │ ├── state.py # AgentState TypedDict
│ │ └── tools.py # 8 LangChain tool definitions (57KB)
│ └── dtos/ # Data Transfer Objects
│
├── core/ # Configuration & cross-cutting concerns
│ ├── config.py # All env vars, LangSmith, stock switch config
│ ├── dealer_config.py # DealerConfig dataclass + DB/memory retrieval
│ └── logger.py # Structured logging
│
├── domain/ # Business models & pure logic
│ ├── models.py # Domain entities
│ ├── repositories/ # Repository interfaces (contracts)
│ │ ├── agent_repository.py
│ │ ├── auction_repository.py
│ │ ├── contact_log_repository.py
│ │ ├── dealer_repository.py
│ │ ├── inventory_repository.py
│ │ └── vehicle_repository.py
│ └── services/ # Domain services (business rules)
│ ├── auction_service.py # Auction stock query logic
│ ├── contact_service.py # Agent assignment, lead logging
│ ├── dealer_service.py # Dealer lookup & config
│ ├── duty_calculation_service.py # Import duty computations
│ ├── inventory_service.py # Showroom stock query logic
│ ├── more_info_service.py # Extended vehicle detail fetcher
│ ├── pricing_service.py # Price calculation helpers
│ ├── stock_switch_service.py # Dual-stock switching logic (19KB)
│ └── vehicle_service.py # Vehicle info aggregation
│
└── infrastructure/ # External concerns & adapters
├── database/
│ ├── models.py # SQLAlchemy ORM models (mirrors CRM schema)
│ ├── postgres.py # SessionLocal, engine setup
│ └── redis.py # Redis client connection
├── external/
│ └── nhtsa.py # NHTSA API client
├── repositories/ # Concrete DB implementations
│ ├── postgres_inventory_repository.py
│ ├── postgres_vehicle_repository.py
│ ├── postgres_dealer_repository.py
│ └── auction_repository_impl.py
└── services/
├── session.py # Redis-backed session R/W (3h TTL)
└── user_requests.py # Customer request logging
The FastAPI application exposes the following endpoints on localhost:9095:
POST /chat
Body: {
"message": "show honda black under 10000",
"session_id": "94771234567", # WhatsApp ID
"dealer_whatsapp_number": "+94771234567" # Dealer phone → identifies which dealer
}
Response: {
"response": "Text reply from AI",
"intro_message": "Separate intro text to send first",
"inventory_items": [...], # Vehicle cards with images for WhatsApp
"pagination": {...}, # total, page_size, has_more, remaining_items
"model_buttons": [...], # Interactive buttons for brand query
"variant_buttons": [...], # Interactive buttons for model selection
"session_id": "...",
"dealer_id": "DLR0001",
"data": {}
}
GET /session/{whatsapp_number} # Get session state & history count
DELETE /session/{whatsapp_number} # Clear a specific user's session
GET /sessions # List all active sessions
DELETE /sessions/clear-all # Wipe all sessions
GET /vehicle/more-info/{stock_type}/{stock_id}
# stock_type: "showroom" or "import"
# Returns extended vehicle details for "More Info" button
GET /dealers # List all dealers
POST /dealers # Create a new dealer
POST /contact/request?user_phone=&dealer_id=&stock_type=&stock_id=&contact_method=
# Round-robin assigns a sales agent and generates a pre-filled WhatsApp link
GET /contact/agents/{dealer_id} # List all agents for a dealer (admin)
# Core
OPENAI_API_KEY= # Required — GPT-4o-mini
DB_URL=postgresql://user:pass@host:5432/vehicle_db
REDIS_URL=redis://127.0.0.1:6379
SESSION_TTL=10800 # 3 hours in seconds
# LangSmith Tracing
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=
LANGSMITH_PROJECT=Vehicle_Dealer_Bot
# Optional LLMs
DEEPSEEK_API_KEY= # Fallback LLM
GOOGLE_API_KEY= # Gemini integration
# Optional Vector Store
PINECONE_API_KEY=
PINECONE_INDEX_NAME=vehicle-dealer-bot-v2
PINECONE_ENVIRONMENT=us-east-1
# Image URL Configuration
LARAVEL_STORAGE_BASE_URL=https://your-domain.com/storage
DEFAULT_VEHICLE_IMAGE=https://your-domain.com/storage/default/no-image.jpg
# Stock Switch
SYSTEM_STOCK_SWITCH_ENABLED=true
SYSTEM_STOCK_SWITCH_PRIORITY=high # high | low | off
# Security
AUTH_TOKEN=aai-admin-secret-2026cd ai-chatbot-aai-v2
# Install dependencies
pip install -r requirements.txt
# Configure environment
cp .env.example .env
# Edit .env with your actual API keys and DB connection string
# Run the FastAPI server
python main.py
# Starts on http://localhost:9095whatsapp-hook-aai-v2 is a lightweight Flask application that acts as the bridge between the Meta WhatsApp Cloud API and the internal AI Agent. It handles all aspects of the WhatsApp protocol: webhook verification, incoming message parsing, intelligent message batching, response formatting into rich WhatsApp UI components, and outbound message delivery. It is intentionally kept thin and stateless — all AI logic lives in the agent.
| Technology | Role |
|---|---|
| Flask | Lightweight Python web framework (+ Waitress WSGI) |
| Requests | HTTP calls to the AI agent and Meta Graph API |
| python-dotenv | Environment variable loading |
| Threading | Async message processing (non-blocking webhook response) |
The hook communicates with Meta's Graph API v15.0+:
POST https://graph.facebook.com/{VERSION}/{PHONE_NUMBER_ID}/messages
Authorization: Bearer {ACCESS_TOKEN}
Message types used:
- Text message — plain
"type": "text"for conversational replies - Image message —
"type": "image"with"link"for vehicle hero photos - Interactive button message —
"type": "interactive"with up to 3 reply buttons for model/variant selection and pagination
GET /webhook # Meta webhook verification (hub.challenge)
POST /webhook # Incoming WhatsApp message handler
# Session Management (proxied to AI Agent)
GET /session/<whatsapp_number> # View session state
DELETE /session/<whatsapp_number> # Clear a user's session
GET /sessions # List all sessions
DELETE /sessions/clear-all # Clear all sessions
# Vehicle Info (proxied to AI Agent)
GET /vehicle/more-info/<stock_type>/<stock_id>
All admin endpoints require Authorization: Bearer aai-admin-secret-2026.
WhatsApp users frequently send multiple short messages in rapid succession (e.g., "hello", "I want", "toyota"). Sending each message independently to the AI would result in incomplete, confusing queries.
The batcher solves this with a 4-second sliding window:
BATCH_DELAY = 4 # seconds
MESSAGE_BATCHES = {} # {wa_id: {"messages": [], "app": Flask, "dealer_whatsapp_number": str}}
# When a new message arrives for wa_id:
# 1. Append message text to MESSAGE_BATCHES[wa_id]
# 2. Cancel any existing timer for wa_id
# 3. Start a new 4-second Timer → calls process_message_batch()
# 4. If another message arrives within 4 seconds, repeat steps 1-3
# 5. After 4 seconds of silence → combine all messages with "\n\n" separator
# → POST to http://localhost:9095/chatThe formatter module renders AI responses as WhatsApp-native UI elements:
For each vehicle in inventory_items:
- Hero image sent with vehicle description as caption (≤ 1024 chars)
- Additional images stored in
PENDING_IMAGES[wa_id]for "Show More" button - Interactive button panel sent after each card:
📷 N More Photos→ triggersshow_more_{idx}button IDℹ️ More Info→ triggersmore_info_{stock_type}_{stock_id}button ID📞 Contact→ triggerscontact_{dealer_id}button ID
Vehicle images stored in Google Drive are automatically converted for WhatsApp delivery:
# From: https://drive.google.com/file/d/{FILE_ID}/view
# To: https://drive.google.com/thumbnail?id={FILE_ID}&sz=w1000When agent returns model_buttons (brand query like "show toyota"):
[Interactive message]
Body: "🚗 TOYOTA Cars Available Now\n1. *AQUA* (NZD 7,478 - 9,985)\n..."
Buttons: ["View Aqua"] ["View Corolla"] ["View Prius"]
When agent returns variant_buttons (model query like "show toyota aqua"):
[Interactive message]
Body: "🚗 TOYOTA AQUA Variants Available Now\n1. *S* (3 vehicles)\n..."
Buttons: ["S"] ["X-URBAN"] ["G"]
When a search returns more than 6 vehicles, the hook manages pagination entirely:
- First page (6 vehicles) sent as individual cards
PENDING_PAGINATION[wa_id]stores remaining vehicles in memory"➡️ Next N Vehicles"button sent at the bottom- When user clicks
next_page:get_next_page_items()retrieves next 6 from memory- Sends next batch of vehicle cards
- Repeats until inventory exhausted
- Final message:
"✅ You've seen all available vehicles!"
The @signature_required decorator on POST /webhook validates that the incoming payload was genuinely sent by Meta:
# app/decorators/security.py
# Validates X-Hub-Signature-256 header using APP_SECRETThe GET /webhook endpoint handles Meta's initial webhook subscription by verifying hub.verify_token matches VERIFY_TOKEN from .env.
When a user taps an interactive button, the Flask hook detects the interactive message type and routes accordingly:
| Button ID Pattern | Action |
|---|---|
show_more_{N} |
Retrieve and send additional vehicle images from PENDING_IMAGES |
next_page |
Send next 6 vehicles from PENDING_PAGINATION |
show_model_{make}_{model} |
Forward to AI agent as a model selection message |
more_info_{type}_{id} |
Fetch extended vehicle info from /vehicle/more-info/{type}/{id} |
contact_{dealer_id} |
Fetch agent contact info and generate WhatsApp deep-link |
The hook sends a read receipt and typing indicator immediately upon receiving any valid message, improving the user experience:
send_typing_indicator(message_id) # Marks message as read + shows typing...ACCESS_TOKEN= # Meta WhatsApp Cloud API Bearer token
PHONE_NUMBER_ID= # WhatsApp Business phone number ID
VERSION=v15.0 # Graph API version
APP_ID= # Meta App ID (for signature verification)
APP_SECRET= # Meta App Secret (for signature verification)
VERIFY_TOKEN= # Custom token for webhook subscription verification
CHATBOT_API_URL=http://localhost:9095 # AI Agent URL
ADMIN_AUTH_TOKEN=aai-admin-secret-2026 # Admin endpoint authorizationwhatsapp-hook-aai-v2/
├── app/
│ ├── __init__.py # Flask app factory, blueprint registration
│ ├── config.py # App configuration from env vars
│ ├── views.py # Flask routes (webhook, session, vehicle proxy)
│ ├── decorators/
│ │ └── security.py # Webhook payload signature validation
│ └── utils/
│ └── whatsapp_utils.py # Core message processing logic (1341 lines, 64KB)
├── main.py # Flask app entry point
├── whatsapp.py # Standalone script to send test template message
├── requirements.txt # Flask, requests, python-dotenv
└── .env.example
cd whatsapp-hook-aai-v2
# Install dependencies
pip install -r requirements.txt
# Configure environment
cp .env.example .env
# Edit .env with your Meta WhatsApp credentials
# Start the Flask server (served via Waitress WSGI)
python main.py
# Starts on http://0.0.0.0:8000
# Expose to Meta's servers via tunnel (required for production)
ngrok http 8000
# OR
cloudflared tunnel --url http://localhost:5000
# Register the public URL in Meta for Developers:
# https://developers.facebook.com/apps → WhatsApp → Configuration → Webhook URL
# Set: https://{your-ngrok-url}/webhook (port 8000 proxied by ngrok)
# Verify token: (your VERIFY_TOKEN from .env)| Layer | Technology | Role |
|---|---|---|
| Admin UI / CRM | Laravel 12 + Tailwind CSS | Data entry, dealer management, RBAC |
| Agent Workflow | LangGraph 0.x | Deterministic state machine for AI flow |
| Primary LLM | OpenAI GPT-4o-mini | Intent understanding, conversation synthesis |
| Fallback LLM | DeepSeek Chat | High-availability LLM fallback |
| Optional LLM | Google Gemini | Additional AI capabilities |
| Messaging API | WhatsApp Cloud API v15+ | End-user front-end interface |
| Webhook Server | Flask + Waitress (Python) | Request handling, batching, UI formatting |
| Database | PostgreSQL | Single source of truth for all vehicle data |
| ORM (CRM) | Laravel Eloquent | PHP ORM for CRM data management |
| ORM (AI) | SQLAlchemy | Python ORM for AI read access |
| Caching/Sessions | Redis | Per-user conversational state (3h TTL) |
| Vector Store | Pinecone | Optional semantic search index |
| Embeddings | Sentence Transformers | Local vector embedding generation |
| Observability | LangSmith | AI trace logging and debugging |
| External APIs | NHTSA API | Vehicle safety data lookups |
| Image Storage | Google Drive / Laravel Storage | Vehicle photo hosting |
To run the complete ecosystem locally, you need all three services running simultaneously.
- PHP 8.2+ with Composer
- Python 3.11+
- PostgreSQL 14+
- Redis 7+
- Node.js 18+ (for CRM frontend assets)
- ngrok or Cloudflare Tunnel (for WhatsApp webhook exposure)
- Meta WhatsApp Business API credentials
# Create the shared database
createdb vehicle_db
# Navigate to CRM
cd aai-chatbot-crm
composer install
cp .env.example .env
# Edit .env: set DB_CONNECTION=pgsql, DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD
# Also set REDIS_HOST and REDIS_PORT
php artisan key:generate
php artisan migrate --seed
npm install && npm run build
# Start the CRM on localhost:8000
php artisan serveAccess the admin panel at
http://localhost:8000and create your first dealer profile.
cd ai-chatbot-aai-v2
pip install -r requirements.txt
cp .env.example .env
# Edit .env:
# OPENAI_API_KEY=sk-...
# DB_URL=postgresql://user:pass@127.0.0.1:5432/vehicle_db ← same DB as CRM
# REDIS_URL=redis://127.0.0.1:6379
# LANGSMITH_API_KEY=ls-... (optional, for tracing)
# DEEPSEEK_API_KEY=... (optional, for fallback)
# Start the agent on localhost:9095
python main.pyTest it:
curl -X POST http://localhost:9095/chat -H "Content-Type: application/json" -d '{"message": "hello", "session_id": "test123"}'
cd whatsapp-hook-aai-v2
pip install -r requirements.txt
cp .env.example .env
# Edit .env:
# ACCESS_TOKEN=EAAx... (from Meta for Developers)
# PHONE_NUMBER_ID=123... (from Meta for Developers)
# APP_ID=...
# APP_SECRET=...
# VERIFY_TOKEN=my-custom-token
# VERSION=v18.0
# Start the webhook on localhost:5000
python main.py
# Expose to internet for Meta
ngrok http 5000Register
https://{ngrok-url}/webhookwithVERIFY_TOKENin Meta for Developers.
Send a WhatsApp message to your registered number:
Hello
→ Bot: "Hi, what kind of vehicle are you looking for?"
Show Toyota vehicles
→ Bot: Model list with interactive buttons
Show Toyota Aqua
→ Bot: Variant list (S, X-Urban, G) with buttons
Toyota Aqua S
→ Bot: Vehicle cards with images, More Photos button, Next 6 button
| Variable | Required | Description |
|---|---|---|
APP_KEY |
✅ | Laravel application encryption key |
DB_CONNECTION |
✅ | pgsql for production |
DB_HOST / DB_DATABASE / DB_USERNAME / DB_PASSWORD |
✅ | PostgreSQL connection |
REDIS_HOST / REDIS_PORT |
Optional | Redis for sessions & queues |
SESSION_DRIVER |
Optional | database or redis |
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
✅ | GPT-4o-mini access |
DB_URL |
✅ | PostgreSQL connection string (same DB as CRM) |
REDIS_URL |
✅ | Redis for session storage |
SESSION_TTL |
Optional | Session expiry in seconds (default: 10800 = 3h) |
DEEPSEEK_API_KEY |
Optional | Fallback LLM provider |
GOOGLE_API_KEY |
Optional | Gemini model access |
LANGSMITH_API_KEY |
Optional | Trace logging |
PINECONE_API_KEY |
Optional | Vector search |
LARAVEL_STORAGE_BASE_URL |
✅ | Base URL for CRM-hosted images |
DEFAULT_VEHICLE_IMAGE |
Optional | Fallback image URL |
SYSTEM_STOCK_SWITCH_PRIORITY |
Optional | high (default), low, or off |
AUTH_TOKEN |
Optional | Admin endpoint authentication |
| Variable | Required | Description |
|---|---|---|
ACCESS_TOKEN |
✅ | Meta Graph API permanent access token |
PHONE_NUMBER_ID |
✅ | WhatsApp Business number ID |
APP_SECRET |
✅ | Meta App secret for signature verification |
VERIFY_TOKEN |
✅ | Custom token for webhook subscription |
VERSION |
✅ | Graph API version (e.g. v18.0) |
CHATBOT_API_URL |
Optional | AI Agent URL (default: http://localhost:9095) |
ADMIN_AUTH_TOKEN |
Optional | Admin endpoint token (default: aai-admin-secret-2026) |
The system is designed with layered security throughout:
| Concern | Implementation |
|---|---|
| CRM Access | Laravel Breeze session auth + custom RBAC middleware |
| DB Write Access | Only the Laravel CRM can write to the database; AI agent has read-only access |
| Webhook Integrity | X-Hub-Signature-256 HMAC signature validation on all incoming WhatsApp webhooks |
| Admin API Security | Bearer token authentication (aai-admin-secret-2026) on all session management endpoints |
| Secret Management | All keys stored in .env files — never committed to git |
| Prompt Injection | Guardrail node filters malicious or off-topic inputs before they reach the LLM |
| Response Safety | Response guardrail node validates agent output before delivery |
| LLM Read-Only Rule | The AI agent only has SELECT database access — it cannot modify inventory via SQL; update_db tool is the only write path and is constrained |
CRM (writes) ─────┐
│
▼
┌──────────────────────────────────────────┐
│ PostgreSQL Database │
│ │
│ vehicles inventory │
│ nichibo_stock dealers │
│ inventory_images agents │
│ nichibo_stock_vehicle_photos │
│ ... (30+ tables) │
└──────────────────────────────────────────┘
│
│ (READ ONLY)
▼
AI Agent (reads) → returns structured data
│
▼
WhatsApp Hook (formats & delivers)
WhatsApp message received
│
▼
Flask extracts dealer's WhatsApp display_phone_number from message metadata
│
▼
Flask POSTs to /chat with dealer_whatsapp_number field
│
▼
FastAPI calls identify_dealer(whatsapp_number=...)
│
▼
Queries dealers table WHERE whatsapp_number = ?
│
▼
Returns DealerConfig → injected into AgentState
│
▼
All tool calls scoped to that dealer_id
(only that dealer's inventory is shown)
User: "under 10000"
Bot: get_models_by_brand + browse_inventory
→ Shows make list: Toyota, Honda, Nissan (with price ranges)
→ Interactive buttons: [View Toyota] [View Honda] [View Nissan]
User: [Taps "View Toyota"] or types "Toyota"
Bot: browse_inventory(make=Toyota, max_price=10000)
→ Shows Toyota model list: Aqua (NZD 7,478-9,985), Corolla, Prius...
User: "Aqua"
Bot: check_stock_with_fallback(make=Toyota, model=Aqua, max_price=10000)
→ Shows variant list: S (3 vehicles), X-Urban (2), G (1)
→ Interactive buttons: [S] [X-Urban] [G]
User: [Taps "S"]
Bot: check_stock_with_fallback(make=Toyota, model=Aqua, variant=S, max_price=10000)
→ Sends vehicle cards: hero image + details, "📷 More Photos", "Next 6" button
User: "honda black color"
Bot: browse_inventory(make=Honda, color=Black)
→ Shows Honda models available in black
User: "VEZEL"
Bot: check_stock_with_fallback(make=Honda, model=Vezel, color=Black) ← color preserved!
→ Shows Vezel variants in black
User: "Hybrid Z"
Bot: check_stock_with_fallback(make=Honda, model=Vezel, variant="Hybrid Z", color=Black) ← all filters preserved!
→ Shows specific Vezel Hybrid Z vehicles in black
User: "show nissan vehicles"
Bot: get_models_by_brand(make=Nissan)
→ Searches both showroom AND import stock (SYSTEM_STOCK_SWITCH_PRIORITY=high)
→ Shows combined model list with source labels
User: "Serena"
Bot: check_stock_with_fallback(make=Nissan, model=Serena)
→ Tries showroom; if empty, automatically falls back to auction/Nichibo stock
→ Shows Nichibo auction vehicles with grade, FOB price, condition details
Auto-Auction-AI-Chatbot/
│
├── 📄 README.md ← This file
├── 📄 LICENSE
├── 📄 .gitattributes
│
├── 📂 aai-chatbot-crm/ ← Project 1: Laravel CRM
│ ├── app/
│ │ ├── Http/
│ │ │ ├── Controllers/ # 9 controllers + Admin subdirectory
│ │ │ ├── Middleware/ # Permission middleware
│ │ │ └── Requests/ # Form validation
│ │ ├── Models/ # 32 Eloquent models
│ │ ├── Providers/
│ │ └── View/
│ ├── database/
│ │ ├── migrations/ # 11 migration files
│ │ └── seeders/
│ ├── routes/
│ │ ├── web.php # 80+ named routes
│ │ └── auth.php
│ ├── resources/views/ # Blade templates
│ ├── config/
│ ├── composer.json # Laravel 12, PHP 8.2, Breeze
│ ├── package.json # Tailwind, Vite
│ └── .env.example
│
├── 📂 ai-chatbot-aai-v2/ ← Project 2: LangGraph AI Agent
│ ├── src/
│ │ ├── application/
│ │ │ ├── agent/
│ │ │ │ ├── graph.py # LangGraph state machine
│ │ │ │ ├── nodes.py # 6 graph nodes (78KB)
│ │ │ │ ├── state.py # AgentState TypedDict
│ │ │ │ └── tools.py # 8 LangChain tools (57KB)
│ │ │ └── dtos/
│ │ ├── core/
│ │ │ ├── config.py # All environment config
│ │ │ ├── dealer_config.py # Dealer config management
│ │ │ └── logger.py
│ │ ├── domain/
│ │ │ ├── models.py # Domain entities
│ │ │ ├── repositories/ # 6 repository interfaces
│ │ │ └── services/ # 9 domain services
│ │ ├── infrastructure/
│ │ │ ├── database/ # SQLAlchemy models, postgres, redis
│ │ │ ├── external/ # NHTSA API client
│ │ │ ├── repositories/ # 4 concrete implementations
│ │ │ └── services/ # Session, user requests
│ │ └── interfaces/
│ │ └── api/
│ │ ├── main.py # FastAPI app factory
│ │ ├── routes.py # All API endpoints (415 lines)
│ │ ├── dealer_routes.py # Dealer CRUD endpoints
│ │ └── models.py # Pydantic request/response models
│ ├── graphs/ # LangGraph visualization exports
│ ├── main.py # FastAPI entry point (port 9095)
│ ├── requirements.txt # 25 Python dependencies
│ └── .env.example
│
└── 📂 whatsapp-hook-aai-v2/ ← Project 3: Flask WhatsApp Webhook
├── app/
│ ├── __init__.py # Flask app factory
│ ├── config.py # App configuration
│ ├── views.py # Flask routes (250 lines)
│ ├── decorators/
│ │ └── security.py # HMAC signature validation
│ └── utils/
│ └── whatsapp_utils.py # Core logic: batcher, formatter, sender (1341 lines)
├── main.py # Flask entry point
├── whatsapp.py # Standalone template message sender
├── requirements.txt # Flask, requests, python-dotenv
└── .env.example
composer run dev # Start all services concurrently (server + queue + logs + vite)
php artisan serve # HTTP server only
php artisan migrate # Run migrations
php artisan migrate:fresh --seed # Reset and re-seed database
php artisan queue:listen # Process queued jobs
npm run dev # Vite HMR dev server
npm run build # Production assets
composer run test # Run PHPUnit tests
php artisan tinker # Interactive REPLpython main.py # Start FastAPI server on :9095
uvicorn src.interfaces.api.main:app --reload --port 9095 # With hot reload
# Test chat endpoint:
curl -X POST http://localhost:9095/chat \
-H "Content-Type: application/json" \
-d '{"message": "show toyota", "session_id": "test123", "dealer_whatsapp_number": "+94771234567"}'python main.py # Start Flask on :5000
ngrok http 5000 # Expose to internet for Meta webhook
cloudflared tunnel --url http://localhost:5000 # Alternative tunnel
python whatsapp.py # Send a test template messagephp artisan test # Run all PHPUnit tests
php artisan test --filter=InventoryTest # Run specific test class# Test chat locally
curl -X POST http://localhost:9095/chat \
-H "Content-Type: application/json" \
-d '{"message": "hello", "session_id": "test_001"}'
# Test session management
curl -X GET http://localhost:9095/sessions \
-H "Authorization: Bearer aai-admin-secret-2026"
# Clear a test session
curl -X DELETE http://localhost:9095/session/test_001 \
-H "Authorization: Bearer aai-admin-secret-2026"# Simulate a WhatsApp text message
curl -X POST http://localhost:5000/webhook \
-H "Content-Type: application/json" \
-d '{"object":"whatsapp_business_account","entry":[{"changes":[{"value":{"messages":[{"type":"text","text":{"body":"hello"},"id":"msg_001"}],"contacts":[{"wa_id":"94771234567","profile":{"name":"Test User"}}],"metadata":{"display_phone_number":"+94777777777","phone_number_id":"123"}}}]}]}'- 🖥️ CRM is web-only — no mobile app for admin management
- 🔒 AI agent is read-only by design — inventory status updates are the only write path
- 💬 WhatsApp interactive buttons have a limit of 3 buttons per message (Meta constraint)
- 📄 Pagination is in-memory — stored per Flask process; not persisted across restarts
- 🌐 No HTTPS between Flask ↔ Agent in local dev — use a reverse proxy (nginx) in production
- 🔄 Session state in Redis — loses conversation history on Redis restart unless persistence is enabled
- 📸 Google Drive images depend on file sharing settings being public/link-accessible
- 🔢 Button titles are limited to 20 characters by WhatsApp's API
| Flag | Project | Default | Description |
|---|---|---|---|
SYSTEM_STOCK_SWITCH_ENABLED |
AI Agent | true |
Master toggle for dual-stock system |
SYSTEM_STOCK_SWITCH_PRIORITY |
AI Agent | high |
high=both stocks, low=dealer decides, off=showroom only |
SESSION_TTL |
AI Agent | 10800 |
Redis session TTL in seconds (3h) |
BATCH_DELAY |
WhatsApp Hook | 4 |
Message batching window in seconds |
LANGSMITH_TRACING |
AI Agent | true |
Enable/disable LangSmith trace logging |
Contributions are welcome! Please follow these guidelines:
- CRM changes: Keep Eloquent models thin, use form requests for validation, preserve migration history
- AI changes: Maintain Clean Architecture layers — business logic stays in
domain/, DB ininfrastructure/ - Tool changes: Keep LangChain tools as thin wrappers; move logic to domain services
- WhatsApp changes: Keep formatting logic in
whatsapp_utils.py, routes thin inviews.py - System prompt changes: Test all 4 query type classifications before committing
- Database changes: Add migrations to CRM first, then update SQLAlchemy models in AI agent to match
- Fork the repository
- Create a feature branch:
git checkout -b feature/YourFeature - Commit your changes:
git commit -m 'feat: Add YourFeature' - Push the branch:
git push origin feature/YourFeature - Open a Pull Request
This project is proprietary and not licensed for public distribution. See individual project LICENSE files for details.