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¶
- Service Overview
- State Machine Flow
- Complete Endpoints
- Quota Management System
- [Save Chatbot Selection]
- Hidden Name Management
- System Prompt Integration
- LLM Management
- Security Analysis
- Deployment
Service Overview¶
The State Selection 3D Chatbot Service manages the complete setup flow for 3D chatbots through a 6-step state machine (start → END). 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:
Response (Step 3):
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:
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
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 (withsys_promptarray)
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 |
Related Documentation¶
- Selection Chatbot Service - Individual selection endpoints
- Create Chatbot Service - Project creation
- User Service - Quota in features_per_user
Recommendations¶
Critical (Security)¶
- ⚠️ Move Azure Email Credentials to Environment
- ⚠️ Remove Duplicate Function (reset_test_system_prompt)
- ⚠️ Add Rate Limiting
- ⚠️ Validate Email Addresses (prevent injection)
Improvements¶
- Cache Quota Lookups - Don't query DB every time
- Async Email Sending - Use background tasks
- State Transition Validation - Ensure proper order
- Audit Trail - Log all state changes
Code Quality¶
- Split Large Function - save_chatbot_selection is 145 lines
- Extract Validation Logic - Reusable validators
- Add Type Hints - Improve readability
- 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."