Skip to content

State Selection 3D Chatbot Service - Complete Developer Documentation

Service: 3D Chatbot Setup State Machine
Port: 8006
Purpose: Manage 6-step setup flow for 3D chatbots with quota enforcement
Technology: FastAPI (Python 3.9+), Azure Communication Email
Code Location: /state-selection-3dchatbot-service/src/main.py (1009 lines, 20+ endpoints)
Owner: Backend Team
Last Updated: 2025-12-26


Table of Contents

  1. Service Overview
  2. State Machine Flow
  3. Complete Endpoints
  4. Quota Management System
  5. [Save Chatbot Selection]
  6. Hidden Name Management
  7. System Prompt Integration
  8. LLM Management
  9. Security Analysis
  10. Deployment

Service Overview

The State Selection 3D Chatbot Service manages the complete setup flow for 3D chatbots through a 6-step state machine (startEND). It enforces quota limits, validates selections, and coordinates with other services.

Key Responsibilities

State Machine - Track progress through 6 setup steps
Quota Enforcement - Check chatbot limits before creation
Email Notifications - Quota warnings and exceeded alerts
Validation - Verify chatbot type, purpose, avatar, voice
System Prompt Management - Fetch and store default prompts
Database Selection - Track CosmosDB vs Milvus choice
LLM Configuration - Manage multiple LLM providers

Statistics

  • Total Lines: 1009
  • Endpoints: 20+
  • State Steps: 6 (start → chatbot_type_selected → sitemap_urls_fetched → chatbot_purpose_selected → avatar_selected → END)
  • Email Integration: Azure Communication Services (⚠️ Hardcoded credentials!)

State Machine Flow

6-Step Setup Process

graph TD
    START["start<br/>(No chatbot found)"]

    STEP1["chatbot_type_selected<br/>(3D-chatbot chosen)"]

    STEP2["sitemap_urls_fetched<br/>(Data collection done)"]

    STEP3["chatbot_purpose_selected<br/>(Sales/Service/Custom)"]

    STEP4["avatar_selected<br/>(Eva, Chris, etc.)"]

    STEP5["voice_selected<br/>(Male/Female voice)"]

    END["END<br/>(Setup complete)"]

    START -->|"POST /v2/select-chatbot<br/>chatbot_type=3D-chatbot"| STEP1

    STEP1 -->|"POST /v2/save-datacollection-status<br/>(Crawl URLs)"| STEP2

    STEP2 -->|"POST /v2/select-purpose<br/>(Sales Bot, etc.)"| STEP3

    STEP3 -->|"POST /v2/select-avatar<br/>(Eva, Jack, etc.)"| STEP4

    STEP4 -->|"POST /v2/select-voice<br/>(Male_1, Female_2, etc.)"| STEP5

    STEP5 -->|"POST /v2/save-chatbot-selection<br/>(Final step)"| END

    style START fill:#ffebee
    style STEP1 fill:#fff3e0
    style STEP2 fill:#e3f2fd
    style STEP3 fill:#e8f5e9
    style STEP4 fill:#f3e5f5
    style STEP5 fill:#fce4ec
    style END fill:#c8e6c9

State Check Logic

Endpoint: GET /v2/check-selection-state

Code Location: Lines 200-227

Complete Logic:

@app.get("/v2/check-selection-state")
async def check_selection_state(user_id: str, project_id: str):
    """Check the current state of the chatbot selection process."""
    selection = selection_collection.find_one({"user_id": user_id, "project_id": project_id})

    # State 1: No selection or not 3D chatbot
    if not selection or selection.get("chatbot_type") != "3D-chatbot":
        return {"state": "start", "message": "No chatbot collection found."}

    # State 2: Type selected, waiting for data collection
    elif "chatbot_type" in selection and "sitemap_urls" not in selection:
        return {"state": "chatbot_type_selected"}

    # State 3: Data collected, waiting for purpose
    elif "chatbot_type" in selection and "sitemap_urls" in selection and "chatbot_purpose" not in selection:
        return {"state": "sitemap_urls_fetched"}

    # State 4: Purpose selected, waiting for avatar
    elif ... and "chatbot_purpose" in selection and "selection_avatar" not in selection:
        return {"state": "chatbot_purpose_selected"}

    # State 5: Avatar selected, waiting for voice
    elif ... and "selection_avatar" in selection and "selection_voice" not in selection:
        return {"state": "avatar_selected"}

    # State 6: All complete
    elif ... and "selection_voice" in selection:
        return {"state": "END"}  # Setup complete!

    else:
        return {"state": "start"}

State Responses:

State Meaning Next Step
start No chatbot or not 3D Select chatbot type
chatbot_type_selected Type=3D chosen Crawl data
sitemap_urls_fetched Data collected Select purpose
chatbot_purpose_selected Purpose chosen Select avatar
avatar_selected Avatar chosen Select voice
END Complete! Launch chatbot

Complete Endpoints

1. GET /v2/check-selection-state

Purpose: Check current step in setup flow

Request:

GET /v2/check-selection-state?user_id=User-123456&project_id=User-123456_Project_1

Response (Step 3):

{
  "state": "chatbot_purpose_selected"
}

2. POST /v2/save-datacollection-status

Purpose: Mark data collection (sitemap crawling) as complete

Code Location: Lines 230-258

Request:

POST /v2/save-datacollection-status
Content-Type: multipart/form-data

user_id=User-123456
project_id=User-123456_Project_1

Database Update:

db.selection_history.updateOne(
  { user_id: "User-123456", project_id: "User-123456_Project_1" },
  {
    $set: {
      sitemap_urls: {
        status: "completed",
      },
      last_updated: ISODate("2025-01-15T14:00:00Z"),
    },
  },
  { upsert: true }
);

Response:

{
  "message": "Sitemap status saved successfully",
  "user_id": "User-123456",
  "project_id": "User-123456_Project_1"
}

3. POST /v2/save-chatbot-selection

Purpose: FINAL STEP - Save complete chatbot configuration and launch

Code Location: Lines 278-422 (145 lines!)

This is the MOST IMPORTANT endpoint!

Request:

POST /v2/save-chatbot-selection
Content-Type: multipart/form-data

user_id=User-123456
project_id=User-123456_Project_1
chatbot_type=3D-chatbot
chatbot_purpose=Service Bot
selection_avatar=Emma
hidden_name=Support Assistant
domain=https://example.com
title_name=Example Corp
selection_voice=Female_2

Complete Flow:

Step 1: Quota Check (Lines 292-300)

chatbot_count = chatbot_collection.count_documents({"user_id": user_id})
user_limit = get_user_chatbot_limit(user_id)  # From features_per_user

if chatbot_count >= user_limit:
    send_quota_exceeded_email(user_id, chatbot_count, user_limit)
    raise HTTPException(status_code=400, detail=f"Maximum {user_limit} chatbots already created")

Step 2: Validation (Lines 302-322)

# Validate chatbot_type
if chatbot_type not in ["text-chatbot", "voice-chatbot", "3D-chatbot"]:
    return {"error": "Invalid chatbot type selected"}

# Validate chatbot_purpose
if chatbot_purpose not in ["Sales Bot", "Service Bot", "Custom Bot"]:
    return {"error": "Invalid purpose type selected"}

# Validate selection_avatar
if selection_avatar not in AVATAR_TYPES:
    return {"error": "Invalid avatar type selected"}

# Validate selection_voice
if selection_voice not in ["Male_1", "Male_2", "Male_3", "Male_IND",
                            "Female_1", "Female_2", "Female_3", "Female_4",
                            "Female_IND", "Female_IND2"]:
    return {"error": "Invalid voice type selected"}

# Validate hidden_name (if provided)
if hidden_name:
    hidden_name = hidden_name.strip()
    if not hidden_name:
        return {"error": "Hidden name cannot be empty if provided"}

Step 3: Fetch System Prompt (Lines 324-336)

system_prompt = system_prompt_collection.find_one({
    "chatbot_purpose": chatbot_purpose,  # "Service Bot"
    "model": "openai-35"  # Fixed default
})

if not system_prompt:
    return {"error": "System prompt not found for the selected purpose and model"}

content = system_prompt.get("content", "")
if not content:
    return {"error": "No content found in the system prompt"}

Step 4: Get Organization Name (Lines 344-353)

org_record = files_collection2.find_one({
    "user_id": user_id,
    "project_id": project_id
})

if not org_record or "organisation_name" not in org_record:
    return {"error": "Organisation name not found for the given user and project"}

organisation_name = org_record.get("organisation_name", "")

Step 5: Get Database Selection (Lines 355-361)

db_selection = selection_collection.find_one(
    {"user_id": user_id, "project_id": project_id},
    {"database_type": 1}
)
database_type = db_selection.get("database_type",
                                   os.getenv("DEFAULT_DATABASE", "cosmosdb")) \
                if db_selection else os.getenv("DEFAULT_DATABASE", "cosmosdb")

Step 6: Save to chatbot_selections (Lines 363-382)

local_tz = pytz.timezone("Asia/Kolkata")
current_time = datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(local_tz)

selection_data = {
    "user_id": user_id,
    "project_id": project_id,
    "chatbot_type": chatbot_type,
    "chatbot_purpose": chatbot_purpose,
    "avatar": selection_avatar,
    "avatar_type": AVATAR_TYPES[selection_avatar],  # e.g., "Avatar_Emma"
    "hidden_name": hidden_name if hidden_name else None,
    "title_name": organisation_name,
    "voice": selection_voice,
    "model": "openai-35",
    "domain": domain if domain else None,
    "chatbot_registered_at": current_time.strftime("%Y-%m-%d %H:%M:%S"),  # IST time
    "system_prompt": content,
    "database_type": database_type  # "cosmosdb" or "milvus"
}

result = chatbot_collection.insert_one(selection_data)

Step 7: Save to system_prompts_user (Lines 384-396)

user_system_prompt_data = {
    "user_id": user_id,
    "project_id": project_id,
    "chatbot_purpose": chatbot_purpose,
    "model": "openai-35",
    "avatar": selection_avatar,
    "system_prompt": content,
    "created_at": datetime.utcnow().isoformat(),
    "sys_prompt": []  # For advanced LLM management
}

user_system_prompt_collection.insert_one(user_system_prompt_data)

Step 8: Update selection_history (Lines 398-407)

selection_collection.update_one(
    {"user_id": user_id, "project_id": project_id},
    {
        "$set": {
            "selection_model": "openai-35",
            "timestamp": datetime.utcnow()
        }
    },
    upsert=True
)

Step 9: Quota Warning Email (Lines 409-412)

new_count = chatbot_count + 1
if new_count == user_limit:
    send_quota_warning_email(user_id, new_count, user_limit)

Response:

{
  "message": "Chatbot selection saved successfully",
  "selection_id": "507f1f77bcf86cd799439011",
  "avatar_type": "Avatar_Emma",
  "hidden_name": "Support Assistant",
  "avatar": "Emma",
  "title_name": "Example Corp",
  "system_prompt": "You are a helpful service bot..."
}

Quota Management System

Get User Limit

Function: get_user_chatbot_limit(user_id) (Lines 90-105)

Code:

def get_user_chatbot_limit(user_id: str) -> int:
    """Get the chatbot limit for a user from features_per_user collection"""
    try:
        user_features = features_per_user_collection.find_one({"user_id": user_id})
        if user_features and "no_of_chatbots" in user_features:
            return int(user_features["no_of_chatbots"])
        else:
            return 1  # Default limit
    except (ValueError, TypeError):
        return 1  # Handle conversion errors
    except Exception:
        return 1  # Handle database errors

Example:

limit = get_user_chatbot_limit("User-123456")
# Returns: 5 (for paid users) or 1 (for free users)

Quota Warning Email

Function: send_quota_warning_email() (Lines 108-148)

Triggered: When user creates their LAST allowed chatbot

Email Content:

Subject: Chatbot Quota Limit Notification

Dear User,

You have just created your 5th chatbot out of your allowed limit of 5 chatbots.

This means you have reached your maximum chatbot creation limit according to your
current subscription plan. If you need to create more chatbots, please consider
upgrading your subscription plan.

For any assistance, please contact our support team.

Thank you for using our service!

Best regards,
The Machine Agents Team

Code:

def send_quota_warning_email(user_id: str, current_count: int, limit: int):
    user = users_collection.find_one({"user_id": user_id})
    if not user or "email" not in user:
        return False

    user_email = user["email"]

    message = {
        "senderAddress": SENDER_EMAIL,
        "recipients": {"to": [{"address": user_email}]},
        "content": {
            "subject": "Chatbot Quota Limit Notification",
            "plainText": f"You have just created your {current_count}th chatbot..."
        }
    }

    email_client.begin_send(message)
    return True

Quota Exceeded Email

Function: send_quota_exceeded_email() (Lines 151-196)

Triggered: When user tries to create chatbot beyond limit

Email Content:

Subject: Chatbot Creation Failed - Quota Exceeded

Dear User,

Your attempt to create a new chatbot has been blocked because you have already
reached your maximum limit.

Current Status:
- Chatbots Created: 5
- Maximum Allowed: 5
- Remaining: 0

To create more chatbots, please upgrade your subscription plan.

Best regards,
The Machine Agents Team

Hidden Name Management

Get Hidden Name

Endpoint: GET /v2/get-hidden-name

Code Location: Lines 425-445

Purpose: Get custom display name (or fallback to avatar name)

Logic:

result = selection_collection.find_one(
    {"user_id": user_id, "project_id": project_id},
    {"_id": 0, "hidden_name": 1, "avatar": 1}
)

hidden_name = result.get("hidden_name")

if hidden_name:
    return {"hidden_name": hidden_name}  # "Support Assistant"
else:
    avatar_type = result.get("avatar", "Eva")
    return {"hidden_name": avatar_type}  # Fallback to "Emma"

Update Hidden Name

Endpoint: POST /v2/update-hidden-name

Code Location: Lines 448-488

Purpose: Update custom display name and regenerate greeting

Flow:

# 1. Update selection_collection
selection_collection.update_one(
    {"user_id": user_id, "project_id": project_id},
    {"$set": {"hidden_name": hidden_name}},
    upsert=True
)

# 2. Trigger greeting regeneration
async with httpx.AsyncClient(timeout=60.0) as client:
    greeting_response = await client.post(
        f"{HOME_PAGE_SERVICE_URL}/v2/generate-greeting",
        params={"user_id": user_id, "project_id": project_id}
    )

Why Regenerate Greeting?

The greeting says: "Hello, I'm {hidden_name}, your virtual chatbot..."

If hidden_name changes from "Emma" to "Support Assistant", the greeting must update!


System Prompt Integration

Get System Prompt

Endpoint: GET /v2/get-system-prompt

Code Location: Lines 517-575

Complex Multi-Collection Logic:

Step 1: Get default LLM from Super Admin

is_default_llm = super_admin_llm_system_prompts_collection.find_one({
    "is_default": True
})

Step 2: Get chatbot purpose

chat_purpose = selection_collection.find_one({
    "user_id": user_id,
    "project_id": project_id
}).get("chatbot_purpose")  # e.g., "Service Bot"

Step 3: Find matching prompt in default LLM

for item in is_default_llm["prompts"]:
    if item["name"] == chat_purpose:
        item["is_default"] = True
        item["is_selected"] = True
        temp_prompt = item["prompt"]

Step 4: Initialize user's sys_prompt array if empty

user_doc = user_system_prompt_collection.find_one(
    {"user_id": user_id, "project_id": project_id},
    {"sys_prompt": 1}
)

if user_doc and isinstance(user_doc.get("sys_prompt"), list) and len(user_doc["sys_prompt"]) == 0:
    user_system_prompt_collection.update_one(
        {"user_id": user_id, "project_id": project_id},
        {"$set": {"sys_prompt": [is_default_llm], "system_prompt": temp_prompt}}
    )

Response:

{
    "user_id": "User-123456",
    "project_id": "User-123456_Project_1",
    "sys_prompt": [
        {
            "_id": "...",
            "is_selected": true,
            "ai_model": {...},
            "prompts": [
                {
                    "name": "Service Bot",
                    "prompt": "You are a helpful service bot...",
                    "is_default": true,
                    "is_selected": true
                }
            ]
        }
    ],
    "system_prompt": "You are a helpful service bot..."
}

LLM Management

Multiple LLM Support

The service supports advanced LLM management where users can have multiple LLMs configured:

Collections:

  • super_admin_llm_system_prompts - Global LLM templates (in Super_Admin DB)
  • system_prompts_user - User-specific LLM configs (with sys_prompt array)

Add LLM

Endpoint: PATCH /v2/add-llm

Code Location: Lines 624-678

Purpose: Add a new LLM config from super admin to user's account


Update Test System Prompt

Endpoint: PATCH /v2/test-system-prompt

Code Location: Lines 685-749

Purpose: Test a specific prompt temporarily

Code:

# 1. Reset all is_default & is_active flags
user_system_prompt_collection.update_one(
    {"user_id": user_id, "project_id": project_id},
    {
        "$set": {
            "sys_prompt.$[].prompts.$[].is_default": False,
            "sys_prompt.$[].ai_model.is_active": False
        }
    }
)

# 2. Set only the selected one active
user_system_prompt_collection.update_one(
    {...},
    {
        "$set": {
            "sys_prompt.$[sp].ai_model.is_active": True,
            "sys_prompt.$[sp].prompts.$[p].is_default": True,
            "sys_prompt.$[sp].prompts.$[p].prompt": prompt_text
        }
    },
    array_filters=[
        {"sp._id": ObjectId(system_prompt_id), "sp.ai_model._id": ObjectId(ai_model_id)},
        {"p._id": ObjectId(prompt_id)}
    ]
)

Security Analysis

Critical Issues

1. ⚠️ HARDCODED AZURE EMAIL CREDENTIALS (Lines 83-85)

EMAIL_ENDPOINT = "https://mailing-sevice.india.communication.azure.com/"
EMAIL_ACCESS_KEY = "CgdEWi6fBCJv4c0EHOkq6ZUSML0VQSG49qXgEfrtlLmXY76HV7DeJQQJ99BBACULyCplbknJAAAAAZCSbc3T"  # ⚠️ CRITICAL!
SENDER_EMAIL = "DoNotReply@machineagents.ai"

Impact: Anyone with code access can send emails using company account (cost + reputation risk)

Fix:

EMAIL_ENDPOINT = os.getenv("AZURE_EMAIL_ENDPOINT")
EMAIL_ACCESS_KEY = os.getenv("AZURE_EMAIL_ACCESS_KEY")
SENDER_EMAIL = os.getenv("SENDER_EMAIL")

2. ⚠️ DUPLICATE reset_test_system_prompt FUNCTION (Lines 751 & 848)

Problem: Two functions with same name - second one overwrites first

Impact: Confusing code, potential bugs

Fix: Remove one or rename differently


3. ⚠️ NO RATE LIMITING

Problem: No protection on /v2/save-chatbot-selection

Impact: User could spam chatbot creation up to quota

Fix:

from slowapi import Limiter

@app.post("/v2/save-chatbot-selection")
@limiter.limit("5/minute")  # Max 5 chatbots per minute
def save_chatbot_selection(...):
    ...

Deployment

Docker Configuration

Dockerfile:

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ .

EXPOSE 8006

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8006"]

Requirements.txt

fastapi>=0.95.0
uvicorn[standard]>=0.22.0
pymongo>=4.3.3
python-multipart>=0.0.6
python-dotenv>=1.0.0
pytz>=2023.3
httpx>=0.25.0
azure-communication-email>=1.0.0
pydantic>=2.0.0
ddtrace>=1.19.0

Environment Variables

# Database
MONGO_URI=mongodb://...
MONGO_DB_NAME=Machine_agent_demo

# Email (should be added!)
AZURE_EMAIL_ENDPOINT=https://...
AZURE_EMAIL_ACCESS_KEY=...
SENDER_EMAIL=DoNotReply@machineagents.ai

# Service URLs
HOME_PAGE_SERVICE_URL=http://homepage-chatbot-service:8012

# Database default
DEFAULT_DATABASE=cosmosdb

# DataDog
DD_SERVICE=state-selection-3dchatbot-service
DD_ENV=production

Performance Metrics

Operation Latency Notes
Check state 10-20ms Simple query
Save chatbot selection 100-300ms Multi-collection writes
Quota warning email 2-5 seconds Azure email sending
Update hidden name 5-15 seconds Includes greeting regeneration


Recommendations

Critical (Security)

  1. ⚠️ Move Azure Email Credentials to Environment
  2. ⚠️ Remove Duplicate Function (reset_test_system_prompt)
  3. ⚠️ Add Rate Limiting
  4. ⚠️ Validate Email Addresses (prevent injection)

Improvements

  1. Cache Quota Lookups - Don't query DB every time
  2. Async Email Sending - Use background tasks
  3. State Transition Validation - Ensure proper order
  4. Audit Trail - Log all state changes

Code Quality

  1. Split Large Function - save_chatbot_selection is 145 lines
  2. Extract Validation Logic - Reusable validators
  3. Add Type Hints - Improve readability
  4. Unit Tests - Test state machine logic

Last Updated: 2025-12-26
Code Version: state-selection-3dchatbot-service/src/main.py (1009 lines)
Total Endpoints: 20+
Review Cycle: Monthly


"Where setup becomes success."