satyam23 commited on
Commit
f3a82c8
Β·
1 Parent(s): 5c4772f

Deploy multi-agent ordering chatbot with enhanced features

Browse files
Files changed (3) hide show
  1. README.md +44 -6
  2. app.py +695 -0
  3. requirements.txt +6 -0
README.md CHANGED
@@ -1,13 +1,51 @@
1
  ---
2
- title: Intent Identifier
3
- emoji: πŸ¦€
4
- colorFrom: pink
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.42.0
8
  app_file: app.py
9
  pinned: false
10
- short_description: intent-identfier
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Multi Agent Ordering Chatbot
3
+ emoji: πŸ€–
4
+ colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 4.0.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  ---
12
 
13
+ # Multi-Agent Ordering Chatbot πŸ€–
14
+
15
+ An intelligent chatbot system that uses multiple specialized agents to handle different types of orders efficiently.
16
+
17
+ ## Features
18
+
19
+ - **Smart Classification**: Automatically classifies orders as generic or bulk using AI
20
+ - **Specialized Agents**:
21
+ - Orchestrator Agent: Initial greeting and order classification
22
+ - Generic Order Agent: Handles standard/personal orders
23
+ - Bulk Order Agent: Handles large quantity/wholesale orders
24
+ - **Session Management**: Tracks conversations with unique session IDs
25
+ - **Database Storage**: Saves all orders and conversations
26
+ - **Input Validation**: Comprehensive validation and error handling
27
+ - **Professional UI**: Clean Gradio interface with real-time chat
28
+
29
+ ## How to Use
30
+
31
+ 1. Start by describing what you want to order
32
+ 2. Provide a title for your order when asked
33
+ 3. Describe your order in detail
34
+ 4. The system will automatically route you to the appropriate agent
35
+ 5. Follow the agent's prompts to complete your order
36
+ 6. View your order summary in JSON format
37
+
38
+ ## Technical Details
39
+
40
+ - Built with LangChain and Groq AI
41
+ - Uses SQLite for data persistence
42
+ - Implements rate limiting and timeout handling
43
+ - Comprehensive logging for debugging
44
+ - Input sanitization for security
45
+
46
+ ## Order Types
47
+
48
+ - **Generic Orders**: Personal use, small quantities (< 50 units)
49
+ - **Bulk Orders**: Large quantities, wholesale, business orders (> 50 units)
50
+
51
+ Start chatting to place your order!
app.py ADDED
@@ -0,0 +1,695 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-Agent Ordering Chatbot System
2
+ # Requirements: pip install langchain langchain-groq gradio sqlite3 pydantic
3
+ import os
4
+
5
+ # Get API key from environment variable or use default
6
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "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")
7
+ import gradio as gr
8
+ import sqlite3
9
+ import json
10
+ import uuid
11
+ import logging
12
+ import re
13
+ import time
14
+ import os
15
+ from datetime import datetime
16
+ from typing import Dict, Any, Optional, List, Tuple
17
+ from dataclasses import dataclass
18
+ from enum import Enum
19
+ from functools import wraps
20
+ import threading
21
+
22
+ from langchain.tools import tool
23
+ from langchain_groq import ChatGroq
24
+ from langchain.agents import AgentExecutor, create_openai_functions_agent
25
+ from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
26
+ from langchain.schema import HumanMessage, AIMessage
27
+ from langchain.memory import ConversationBufferMemory
28
+ from pydantic import BaseModel, Field
29
+
30
+ # Configuration
31
+ DATABASE_PATH = "chatbot.db"
32
+ LOG_FILE = "chatbot.log"
33
+ MAX_INPUT_LENGTH = 500
34
+ MAX_TITLE_LENGTH = 100
35
+ MAX_DESCRIPTION_LENGTH = 1000
36
+ API_TIMEOUT = 30 # seconds
37
+ RATE_LIMIT_CALLS = 10
38
+ RATE_LIMIT_WINDOW = 60 # seconds
39
+
40
+ # Setup logging
41
+ logging.basicConfig(
42
+ level=logging.INFO,
43
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
44
+ handlers=[
45
+ logging.FileHandler(LOG_FILE),
46
+ logging.StreamHandler()
47
+ ]
48
+ )
49
+ logger = logging.getLogger(__name__)
50
+
51
+ class AgentType(Enum):
52
+ ORCHESTRATOR = "Orchestrator"
53
+ GENERIC = "Generic"
54
+ BULK = "Bulk"
55
+
56
+ class SessionState:
57
+ def __init__(self):
58
+ self.session_id = str(uuid.uuid4())
59
+ self.current_agent = AgentType.ORCHESTRATOR
60
+ self.order_data = {}
61
+ self.conversation_history = []
62
+ self.collecting_title = False
63
+ self.collecting_description = False
64
+ self.pending_handoff = None
65
+ self.api_calls_timestamps = [] # For rate limiting
66
+ self.error_count = 0
67
+
68
+ # Rate limiter decorator
69
+ def rate_limit(func):
70
+ @wraps(func)
71
+ def wrapper(self, *args, **kwargs):
72
+ session_state = args[1] if len(args) > 1 else None
73
+ if session_state:
74
+ current_time = time.time()
75
+ # Remove old timestamps
76
+ session_state.api_calls_timestamps = [
77
+ ts for ts in session_state.api_calls_timestamps
78
+ if current_time - ts < RATE_LIMIT_WINDOW
79
+ ]
80
+
81
+ if len(session_state.api_calls_timestamps) >= RATE_LIMIT_CALLS:
82
+ logger.warning(f"Rate limit exceeded for session {session_state.session_id}")
83
+ return "I'm processing too many requests. Please wait a moment before continuing.", session_state
84
+
85
+ session_state.api_calls_timestamps.append(current_time)
86
+
87
+ return func(self, *args, **kwargs)
88
+ return wrapper
89
+
90
+ # Input validation functions
91
+ def sanitize_input(text: str, max_length: int = MAX_INPUT_LENGTH) -> str:
92
+ """Sanitize and validate user input"""
93
+ if not text or not text.strip():
94
+ raise ValueError("Input cannot be empty")
95
+
96
+ # Remove potential SQL injection characters
97
+ text = text.replace(";", "").replace("--", "").replace("/*", "").replace("*/", "")
98
+
99
+ # Trim to max length
100
+ text = text[:max_length].strip()
101
+
102
+ # Remove multiple spaces
103
+ text = re.sub(r'\s+', ' ', text)
104
+
105
+ return text
106
+
107
+ def validate_quantity(quantity_str: str) -> int:
108
+ """Validate and parse quantity input"""
109
+ try:
110
+ # Extract numbers from string
111
+ numbers = re.findall(r'\d+', quantity_str)
112
+ if not numbers:
113
+ raise ValueError("No valid number found")
114
+
115
+ quantity = int(numbers[0])
116
+ if quantity <= 0:
117
+ raise ValueError("Quantity must be greater than 0")
118
+ if quantity > 1000000:
119
+ raise ValueError("Quantity seems unreasonably high. Please enter a value less than 1,000,000")
120
+
121
+ return quantity
122
+ except (ValueError, IndexError) as e:
123
+ raise ValueError(f"Invalid quantity: {str(e)}")
124
+
125
+ # Database Setup
126
+ def init_database():
127
+ conn = sqlite3.connect(DATABASE_PATH)
128
+ cursor = conn.cursor()
129
+
130
+ # Create conversation table
131
+ cursor.execute('''
132
+ CREATE TABLE IF NOT EXISTS conversation (
133
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
134
+ session_id TEXT NOT NULL,
135
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
136
+ user_input TEXT,
137
+ chatbot_response TEXT,
138
+ agent TEXT
139
+ )
140
+ ''')
141
+
142
+ # Create order table
143
+ cursor.execute('''
144
+ CREATE TABLE IF NOT EXISTS "order" (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ session_id TEXT NOT NULL,
147
+ title TEXT,
148
+ description TEXT,
149
+ product_name TEXT,
150
+ quantity INTEGER,
151
+ brand_preference TEXT,
152
+ additional_details TEXT,
153
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
154
+ )
155
+ ''')
156
+
157
+ conn.commit()
158
+ conn.close()
159
+
160
+ def save_conversation(session_id: str, user_input: str, chatbot_response: str, agent: str):
161
+ """Save conversation with error handling and logging"""
162
+ try:
163
+ conn = sqlite3.connect(DATABASE_PATH)
164
+ cursor = conn.cursor()
165
+ cursor.execute('''
166
+ INSERT INTO conversation (session_id, user_input, chatbot_response, agent)
167
+ VALUES (?, ?, ?, ?)
168
+ ''', (session_id, user_input, chatbot_response, agent))
169
+ conn.commit()
170
+ conn.close()
171
+ logger.info(f"Conversation saved for session {session_id}, agent: {agent}")
172
+ except sqlite3.Error as e:
173
+ logger.error(f"Database error saving conversation: {e}")
174
+ raise
175
+
176
+ def save_order(session_id: str, order_data: Dict[str, Any]):
177
+ """Save order with error handling and logging"""
178
+ try:
179
+ conn = sqlite3.connect(DATABASE_PATH)
180
+ cursor = conn.cursor()
181
+ cursor.execute('''
182
+ INSERT INTO "order" (session_id, title, description, product_name, quantity, brand_preference, additional_details)
183
+ VALUES (?, ?, ?, ?, ?, ?, ?)
184
+ ''', (
185
+ session_id,
186
+ order_data.get('title', ''),
187
+ order_data.get('description', ''),
188
+ order_data.get('product_name', ''),
189
+ order_data.get('quantity', 0),
190
+ order_data.get('brand_preference', ''),
191
+ json.dumps(order_data.get('additional_details', {}))
192
+ ))
193
+ conn.commit()
194
+ conn.close()
195
+ logger.info(f"Order saved for session {session_id}: {order_data.get('title', 'Untitled')}")
196
+ except sqlite3.Error as e:
197
+ logger.error(f"Database error saving order: {e}")
198
+ raise
199
+
200
+ # Tools
201
+ @tool
202
+ def category_finder_tool(description: str, type_of_request: str = None) -> str:
203
+ """
204
+ Classifies an order request as either 'generic' or 'bulk' based on description.
205
+
206
+ Args:
207
+ description: Description of what the user wants to order
208
+ type_of_request: Type of request (personal use or reselling)
209
+
210
+ Returns:
211
+ 'generic' for single/small orders, 'bulk' for large quantity orders
212
+ """
213
+ # Initialize LLM for classification
214
+ llm = ChatGroq(
215
+ temperature=0,
216
+ groq_api_key=GROQ_API_KEY,
217
+ model_name="mixtral-8x7b-32768"
218
+ )
219
+
220
+ prompt = f"""
221
+ Analyze the following order request and classify it as either "generic" or "bulk":
222
+
223
+ Description: {description}
224
+ Type of request: {type_of_request}
225
+
226
+ Classification rules:
227
+ - "bulk" if quantity > 50 OR mentions words like "bulk", "wholesale", "mass", "large quantity", "reselling", "event", "company", "office"
228
+ - "generic" for smaller quantities or personal use
229
+
230
+ Return only "generic" or "bulk":
231
+ """
232
+
233
+ try:
234
+ # Add timeout handling
235
+ start_time = time.time()
236
+ response = llm.invoke(prompt)
237
+
238
+ if time.time() - start_time > API_TIMEOUT:
239
+ logger.warning("API call timeout in category_finder_tool")
240
+ raise TimeoutError("API call took too long")
241
+
242
+ result = response.content.strip().lower()
243
+ logger.info(f"Category classification result: {result}")
244
+ return "bulk" if "bulk" in result else "generic"
245
+ except Exception as e:
246
+ logger.error(f"Error in category classification: {e}")
247
+ # Fallback logic
248
+ description_lower = description.lower()
249
+ bulk_keywords = ['bulk', 'wholesale', 'mass', 'large', 'hundred', 'thousand', 'resell', 'event', 'company', 'office']
250
+
251
+ # Extract numbers from description
252
+ import re
253
+ numbers = re.findall(r'\d+', description)
254
+ max_number = max([int(n) for n in numbers], default=0)
255
+
256
+ if max_number > 50 or any(keyword in description_lower for keyword in bulk_keywords):
257
+ return "bulk"
258
+ return "generic"
259
+
260
+ # Agent Classes
261
+ class OrchestratorAgent:
262
+ def __init__(self):
263
+ self.llm = ChatGroq(
264
+ temperature=0.1,
265
+ groq_api_key=GROQ_API_KEY,
266
+ model_name="mixtral-8x7b-32768",
267
+ request_timeout=API_TIMEOUT
268
+ )
269
+ self.tools = [category_finder_tool]
270
+
271
+ @rate_limit
272
+ def process(self, user_input: str, session_state: SessionState) -> Tuple[str, SessionState]:
273
+ try:
274
+ # Validate input
275
+ user_input = sanitize_input(user_input)
276
+ if session_state.collecting_title:
277
+ # Validate title
278
+ title = sanitize_input(user_input, MAX_TITLE_LENGTH)
279
+ session_state.order_data['title'] = title
280
+ session_state.collecting_title = False
281
+ session_state.collecting_description = True
282
+ logger.info(f"Title collected for session {session_state.session_id}: {title}")
283
+ return "Great! Now please describe what you want to order in detail.", session_state
284
+
285
+ elif session_state.collecting_description:
286
+ # Validate description
287
+ description = sanitize_input(user_input, MAX_DESCRIPTION_LENGTH)
288
+ session_state.order_data['description'] = description
289
+ session_state.collecting_description = False
290
+
291
+ # Use category finder tool with error handling
292
+ try:
293
+ category = category_finder_tool.invoke({
294
+ "description": description,
295
+ "type_of_request": session_state.order_data.get('type_of_request', '')
296
+ })
297
+ except Exception as e:
298
+ logger.error(f"Category finder error: {e}")
299
+ # Default to generic on error
300
+ category = "generic"
301
+
302
+ # Hand off to appropriate agent
303
+ if category == "bulk":
304
+ session_state.current_agent = AgentType.BULK
305
+ session_state.pending_handoff = "bulk"
306
+ logger.info(f"Handing off to Bulk Agent for session {session_state.session_id}")
307
+ return f"[Handing off to Bulk Order Agent...]\n\nI understand you need a bulk order. Let me gather the specific details for your bulk order.", session_state
308
+ else:
309
+ session_state.current_agent = AgentType.GENERIC
310
+ session_state.pending_handoff = "generic"
311
+ logger.info(f"Handing off to Generic Agent for session {session_state.session_id}")
312
+ return f"[Handing off to Generic Order Agent...]\n\nI understand you need a standard order. Let me gather the specific details for your order.", session_state
313
+
314
+ else:
315
+ # Initial greeting and title collection
316
+ session_state.collecting_title = True
317
+ return "Hello! I'm here to help you with your order. Please provide a title for this order.", session_state
318
+
319
+ except ValueError as e:
320
+ logger.warning(f"Input validation error: {e}")
321
+ return f"⚠️ {str(e)}. Please try again.", session_state
322
+ except Exception as e:
323
+ logger.error(f"Orchestrator processing error: {e}")
324
+ session_state.error_count += 1
325
+ if session_state.error_count > 3:
326
+ return "I'm experiencing technical difficulties. Please try again later or contact support.", session_state
327
+ return "I encountered an error. Please try again.", session_state
328
+
329
+ class GenericOrderAgent:
330
+ def __init__(self):
331
+ self.llm = ChatGroq(
332
+ temperature=0.1,
333
+ groq_api_key=GROQ_API_KEY,
334
+ model_name="mixtral-8x7b-32768",
335
+ request_timeout=API_TIMEOUT
336
+ )
337
+ self.step = 0
338
+
339
+ @rate_limit
340
+ def process(self, user_input: str, session_state: SessionState) -> Tuple[str, SessionState]:
341
+ try:
342
+ # Validate input
343
+ user_input = sanitize_input(user_input)
344
+ if 'generic_step' not in session_state.order_data:
345
+ session_state.order_data['generic_step'] = 0
346
+
347
+ step = session_state.order_data['generic_step']
348
+
349
+ # Check for handoff requests
350
+ handoff_keywords = ['bulk', 'change', 'instead', 'actually', 'different']
351
+ if any(keyword in user_input.lower() for keyword in handoff_keywords) and step > 0:
352
+ # Parse the new request
353
+ session_state.current_agent = AgentType.ORCHESTRATOR
354
+ session_state.order_data = {}
355
+ session_state.collecting_title = True
356
+ logger.info(f"Generic agent handing back to orchestrator for session {session_state.session_id}")
357
+ return "I understand you want to change your order. Please provide a new title for your order.", session_state
358
+
359
+ if step == 0:
360
+ # Ask for product name
361
+ session_state.order_data['generic_step'] = 1
362
+ return "What is the specific product name you want to order?", session_state
363
+
364
+ elif step == 1:
365
+ # Collect product name, ask for quantity
366
+ product_name = sanitize_input(user_input, MAX_INPUT_LENGTH)
367
+ session_state.order_data['product_name'] = product_name
368
+ session_state.order_data['generic_step'] = 2
369
+ logger.info(f"Product name collected: {product_name}")
370
+ return "How many units do you need?", session_state
371
+
372
+ elif step == 2:
373
+ # Collect quantity, ask for brand preference
374
+ try:
375
+ quantity = validate_quantity(user_input)
376
+ session_state.order_data['quantity'] = quantity
377
+ session_state.order_data['generic_step'] = 3
378
+ logger.info(f"Quantity collected: {quantity}")
379
+ return "Do you have any brand preference? (Enter 'No' if none)", session_state
380
+ except ValueError as e:
381
+ logger.warning(f"Invalid quantity input: {user_input}")
382
+ return f"⚠️ {str(e)}. Please enter a valid number.", session_state
383
+
384
+ elif step == 3:
385
+ # Collect brand preference and finalize
386
+ brand_pref = sanitize_input(user_input, MAX_INPUT_LENGTH) if user_input.lower() not in ['no', 'none', 'n/a'] else ''
387
+ session_state.order_data['brand_preference'] = brand_pref
388
+
389
+ try:
390
+ # Save to database
391
+ save_order(session_state.session_id, session_state.order_data)
392
+
393
+ # Generate summary
394
+ summary = self.generate_summary(session_state.order_data)
395
+
396
+ # Reset for next order
397
+ session_state.current_agent = AgentType.ORCHESTRATOR
398
+ session_state.order_data = {}
399
+ session_state.error_count = 0 # Reset error count on success
400
+
401
+ logger.info(f"Generic order completed for session {session_state.session_id}")
402
+ return f"Perfect! Here's your order summary:\n\n{summary}\n\nOrder saved successfully! How can I help you with another order?", session_state
403
+ except Exception as e:
404
+ logger.error(f"Error saving order: {e}")
405
+ return "There was an error saving your order. Please try again or contact support.", session_state
406
+
407
+ except ValueError as e:
408
+ logger.warning(f"Input validation error in generic agent: {e}")
409
+ return f"⚠️ {str(e)}. Please try again.", session_state
410
+ except Exception as e:
411
+ logger.error(f"Generic agent processing error: {e}")
412
+ session_state.error_count += 1
413
+ if session_state.error_count > 3:
414
+ return "I'm experiencing technical difficulties. Please try again later or contact support.", session_state
415
+ return "I encountered an error. Please try again.", session_state
416
+
417
+ def generate_summary(self, order_data: Dict[str, Any]) -> str:
418
+ return f"""πŸ“‹ **ORDER SUMMARY**
419
+ **Title:** {order_data.get('title', 'N/A')}
420
+ **Description:** {order_data.get('description', 'N/A')}
421
+ **Product:** {order_data.get('product_name', 'N/A')}
422
+ **Quantity:** {order_data.get('quantity', 'N/A')} units
423
+ **Brand Preference:** {order_data.get('brand_preference', 'No preference')}
424
+
425
+ JSON Format:
426
+ ```json
427
+ {json.dumps({
428
+ "title": order_data.get('title', ''),
429
+ "description": order_data.get('description', ''),
430
+ "product_name": order_data.get('product_name', ''),
431
+ "quantity": order_data.get('quantity', 0),
432
+ "brand_preference": order_data.get('brand_preference', '')
433
+ }, indent=2)}
434
+ ```"""
435
+
436
+ class BulkOrderAgent:
437
+ def __init__(self):
438
+ self.llm = ChatGroq(
439
+ temperature=0.1,
440
+ groq_api_key=GROQ_API_KEY,
441
+ model_name="mixtral-8x7b-32768",
442
+ request_timeout=API_TIMEOUT
443
+ )
444
+
445
+ @rate_limit
446
+ def process(self, user_input: str, session_state: SessionState) -> Tuple[str, SessionState]:
447
+ try:
448
+ # Validate input
449
+ user_input = sanitize_input(user_input)
450
+ if 'bulk_step' not in session_state.order_data:
451
+ session_state.order_data['bulk_step'] = 0
452
+
453
+ step = session_state.order_data['bulk_step']
454
+
455
+ # Check for handoff requests
456
+ handoff_keywords = ['change', 'instead', 'actually', 'different']
457
+ if any(keyword in user_input.lower() for keyword in handoff_keywords) and step > 0:
458
+ # Parse the new request
459
+ session_state.current_agent = AgentType.ORCHESTRATOR
460
+ session_state.order_data = {}
461
+ session_state.collecting_title = True
462
+ logger.info(f"Bulk agent handing back to orchestrator for session {session_state.session_id}")
463
+ return "I understand you want to change your order. Please provide a new title for your order.", session_state
464
+
465
+ if step == 0:
466
+ # Ask for product type
467
+ session_state.order_data['bulk_step'] = 1
468
+ return "What type of products are you ordering in bulk?", session_state
469
+
470
+ elif step == 1:
471
+ # Collect product type, ask for quantity
472
+ product_type = sanitize_input(user_input, MAX_INPUT_LENGTH)
473
+ session_state.order_data['product_name'] = product_type
474
+ session_state.order_data['bulk_step'] = 2
475
+ logger.info(f"Bulk product type collected: {product_type}")
476
+ return "What is the total quantity or units needed?", session_state
477
+
478
+ elif step == 2:
479
+ # Collect quantity, ask for supplier preference
480
+ try:
481
+ quantity = validate_quantity(user_input)
482
+ # Additional check for bulk orders
483
+ if quantity < 50:
484
+ logger.warning(f"Bulk order with small quantity: {quantity}")
485
+ session_state.order_data['quantity'] = quantity
486
+ session_state.order_data['bulk_step'] = 3
487
+ logger.info(f"Bulk quantity collected: {quantity}")
488
+ return "Do you have any supplier preference or constraints? (Enter 'No' if none)", session_state
489
+ except ValueError as e:
490
+ logger.warning(f"Invalid bulk quantity input: {user_input}")
491
+ return f"⚠️ {str(e)}. Please enter a valid number.", session_state
492
+
493
+ elif step == 3:
494
+ # Collect supplier preference and finalize
495
+ supplier_pref = sanitize_input(user_input, MAX_INPUT_LENGTH) if user_input.lower() not in ['no', 'none', 'n/a'] else ''
496
+ session_state.order_data['brand_preference'] = supplier_pref
497
+
498
+ try:
499
+ # Save to database
500
+ save_order(session_state.session_id, session_state.order_data)
501
+
502
+ # Generate summary
503
+ summary = self.generate_summary(session_state.order_data)
504
+
505
+ # Reset for next order
506
+ session_state.current_agent = AgentType.ORCHESTRATOR
507
+ session_state.order_data = {}
508
+ session_state.error_count = 0 # Reset error count on success
509
+
510
+ logger.info(f"Bulk order completed for session {session_state.session_id}")
511
+ return f"Excellent! Here's your bulk order summary:\n\n{summary}\n\nBulk order saved successfully! How can I help you with another order?", session_state
512
+ except Exception as e:
513
+ logger.error(f"Error saving bulk order: {e}")
514
+ return "There was an error saving your bulk order. Please try again or contact support.", session_state
515
+
516
+ except ValueError as e:
517
+ logger.warning(f"Input validation error in bulk agent: {e}")
518
+ return f"⚠️ {str(e)}. Please try again.", session_state
519
+ except Exception as e:
520
+ logger.error(f"Bulk agent processing error: {e}")
521
+ session_state.error_count += 1
522
+ if session_state.error_count > 3:
523
+ return "I'm experiencing technical difficulties. Please try again later or contact support.", session_state
524
+ return "I encountered an error. Please try again.", session_state
525
+
526
+ def generate_summary(self, order_data: Dict[str, Any]) -> str:
527
+ return f"""πŸ“‹ **BULK ORDER SUMMARY**
528
+ **Title:** {order_data.get('title', 'N/A')}
529
+ **Description:** {order_data.get('description', 'N/A')}
530
+ **Product Type:** {order_data.get('product_name', 'N/A')}
531
+ **Total Quantity:** {order_data.get('quantity', 'N/A')} units
532
+ **Supplier Preference:** {order_data.get('brand_preference', 'No preference')}
533
+
534
+ JSON Format:
535
+ ```json
536
+ {json.dumps({
537
+ "title": order_data.get('title', ''),
538
+ "description": order_data.get('description', ''),
539
+ "product_name": order_data.get('product_name', ''),
540
+ "quantity": order_data.get('quantity', 0),
541
+ "brand_preference": order_data.get('brand_preference', '')
542
+ }, indent=2)}
543
+ ```"""
544
+
545
+ # Main Chatbot System
546
+ class MultiAgentChatbot:
547
+ def __init__(self):
548
+ self.orchestrator = OrchestratorAgent()
549
+ self.generic_agent = GenericOrderAgent()
550
+ self.bulk_agent = BulkOrderAgent()
551
+ self.session_states = {}
552
+
553
+ def get_session_state(self, session_id: str) -> SessionState:
554
+ if session_id not in self.session_states:
555
+ self.session_states[session_id] = SessionState()
556
+ return self.session_states[session_id]
557
+
558
+ def process_message(self, message: str, session_id: str) -> str:
559
+ try:
560
+ session_state = self.get_session_state(session_id)
561
+
562
+ # Log incoming message
563
+ logger.info(f"Processing message for session {session_id}: {message[:50]}...")
564
+
565
+ # Route to appropriate agent
566
+ if session_state.current_agent == AgentType.ORCHESTRATOR:
567
+ response, session_state = self.orchestrator.process(message, session_state)
568
+ agent_name = "Orchestrator"
569
+ elif session_state.current_agent == AgentType.GENERIC:
570
+ response, session_state = self.generic_agent.process(message, session_state)
571
+ agent_name = "Generic"
572
+ elif session_state.current_agent == AgentType.BULK:
573
+ response, session_state = self.bulk_agent.process(message, session_state)
574
+ agent_name = "Bulk"
575
+
576
+ # Update session state
577
+ self.session_states[session_id] = session_state
578
+
579
+ # Save conversation to database
580
+ try:
581
+ save_conversation(session_state.session_id, message, response, agent_name)
582
+ except Exception as e:
583
+ logger.error(f"Failed to save conversation: {e}")
584
+ # Continue processing even if save fails
585
+
586
+ # Add agent indicator to response
587
+ agent_indicator = f"[Running {agent_name} Agent]\n" if agent_name != "Orchestrator" or session_state.pending_handoff else ""
588
+
589
+ return f"{agent_indicator}{response}"
590
+
591
+ except Exception as e:
592
+ logger.error(f"Critical error in process_message: {e}", exc_info=True)
593
+ return "I'm sorry, I encountered an unexpected error. Please try again or contact support."
594
+
595
+ # Initialize system
596
+ init_database()
597
+ chatbot = MultiAgentChatbot()
598
+
599
+ # Gradio Interface
600
+ def chat_interface(message, history, session_id):
601
+ try:
602
+ if not session_id:
603
+ session_id = str(uuid.uuid4())
604
+
605
+ # Validate message is not empty
606
+ if not message or not message.strip():
607
+ return history, "", session_id
608
+
609
+ response = chatbot.process_message(message, session_id)
610
+ history.append([message, response])
611
+
612
+ return history, "", session_id
613
+ except Exception as e:
614
+ logger.error(f"Error in chat interface: {e}")
615
+ error_response = "I'm sorry, there was an error processing your message. Please try again."
616
+ history.append([message, error_response])
617
+ return history, "", session_id
618
+
619
+ def reset_chat():
620
+ new_session_id = str(uuid.uuid4())
621
+ logger.info(f"Chat reset, new session: {new_session_id}")
622
+ return [], "", new_session_id
623
+
624
+ # Create Gradio interface
625
+ with gr.Blocks(title="Multi-Agent Ordering Chatbot", theme=gr.themes.Soft()) as demo:
626
+ gr.Markdown("""
627
+ # πŸ€– Multi-Agent Ordering Chatbot
628
+
629
+ This chatbot helps you place orders efficiently by routing you to specialized agents:
630
+ - **Orchestrator Agent**: Initial classification and routing
631
+ - **Generic Order Agent**: Handles standard/personal orders
632
+ - **Bulk Order Agent**: Handles large quantity/wholesale orders
633
+
634
+ Start by describing what you want to order!
635
+ """)
636
+
637
+ with gr.Row():
638
+ with gr.Column(scale=4):
639
+ chatbot_interface = gr.Chatbot(
640
+ label="Chat with the Ordering Assistant",
641
+ height=500,
642
+ show_copy_button=True
643
+ )
644
+
645
+ with gr.Row():
646
+ user_input = gr.Textbox(
647
+ placeholder="Type your message here...",
648
+ label="Your message",
649
+ scale=4
650
+ )
651
+ submit_btn = gr.Button("Send", variant="primary", scale=1)
652
+
653
+ clear_btn = gr.Button("Reset Chat", variant="secondary")
654
+
655
+ with gr.Column(scale=1):
656
+ session_state_display = gr.Textbox(
657
+ label="Session ID",
658
+ value=str(uuid.uuid4()),
659
+ interactive=False
660
+ )
661
+
662
+ # Event handlers
663
+ submit_btn.click(
664
+ chat_interface,
665
+ inputs=[user_input, chatbot_interface, session_state_display],
666
+ outputs=[chatbot_interface, user_input, session_state_display]
667
+ )
668
+
669
+ user_input.submit(
670
+ chat_interface,
671
+ inputs=[user_input, chatbot_interface, session_state_display],
672
+ outputs=[chatbot_interface, user_input, session_state_display]
673
+ )
674
+
675
+ clear_btn.click(
676
+ reset_chat,
677
+ outputs=[chatbot_interface, user_input, session_state_display]
678
+ )
679
+
680
+ # Launch the application
681
+ if __name__ == "__main__":
682
+ print("πŸš€ Starting Multi-Agent Ordering Chatbot...")
683
+ print("πŸ“ Make sure to set your GROQ_API_KEY in the code!")
684
+ print(f"πŸ“‚ Logs will be saved to: {LOG_FILE}")
685
+ logger.info("Application started")
686
+
687
+ try:
688
+ demo.launch(
689
+ server_name="0.0.0.0",
690
+ server_port=7861,
691
+ share=True
692
+ )
693
+ except Exception as e:
694
+ logger.critical(f"Failed to launch application: {e}")
695
+ raise
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ langchain>=0.1.0
3
+ langchain-community>=0.0.10
4
+ langchain-groq>=0.1.0
5
+ pydantic>=2.0.0
6
+ # Note: uuid, dataclasses, typing, datetime are built-in Python modules