| import os |
| import json |
| from typing import List |
| from groq import Groq |
| from models import Ticket, TicketClassification, TopicTagEnum, SentimentEnum, PriorityEnum |
| import logging |
|
|
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| class TicketClassifier: |
| def __init__(self): |
| api_key = os.getenv("GROQ_API_KEY") |
| if not api_key: |
| raise ValueError("GROQ_API_KEY environment variable is required") |
| |
| self.client = Groq(api_key=api_key) |
| self.models = [ |
| "moonshotai/kimi-k2-instruct" |
| ] |
| self.model = "moonshotai/kimi-k2-instruct" |
| |
| def _create_classification_prompt(self, ticket: Ticket) -> str: |
| |
| topic_tags_list = [tag.value for tag in TopicTagEnum] |
| sentiment_list = [sentiment.value for sentiment in SentimentEnum] |
| priority_list = [priority.value for priority in PriorityEnum] |
| |
| prompt = f""" |
| You are an expert customer support analyst for Atlan, a data catalog and governance platform. |
| Analyze the following support ticket and provide a classification. |
| |
| TICKET DETAILS: |
| ID: {ticket.id} |
| Subject: {ticket.subject} |
| Body: {ticket.body} |
| |
| CLASSIFICATION REQUIREMENTS: |
| |
| 1. TOPIC TAGS (select 1-3 most relevant from the list): |
| {', '.join(topic_tags_list)} |
| |
| 2. SENTIMENT (select exactly one): |
| {', '.join(sentiment_list)} |
| |
| 3. PRIORITY (select exactly one): |
| {', '.join(priority_list)} |
| |
| PRIORITY GUIDELINES: |
| - P0 (High): Urgent issues blocking customers, production failures, security concerns |
| - P1 (Medium): Important functionality questions, configuration issues, feature requests |
| - P2 (Low): General questions, documentation requests, best practices |
| |
| RESPONSE FORMAT: |
| Please respond with a valid JSON object in this exact format: |
| {{ |
| "topic_tags": ["tag1", "tag2"], |
| "sentiment": "sentiment_value", |
| "priority": "priority_value", |
| "reasoning": "Brief explanation of your classification decision" |
| }} |
| |
| IMPORTANT: Use these exact values: |
| - For priority: "P0 (High)", "P1 (Medium)", or "P2 (Low)" |
| - For sentiment: "Frustrated", "Curious", "Angry", or "Neutral" |
| - For topic_tags: Use exact values from the topic list above |
| |
| Ensure your response is valid JSON and uses only the exact values from the lists provided above. |
| """ |
| return prompt |
|
|
| def _normalize_topic_tags(self, tags): |
| """Normalize topic tags to match enum values.""" |
| normalized_tags = [] |
| |
| for tag in tags: |
| try: |
| normalized_tags.append(TopicTagEnum(tag)) |
| except ValueError: |
| tag_lower = tag.lower() |
| if 'how' in tag_lower and 'to' in tag_lower: |
| normalized_tags.append(TopicTagEnum.HOW_TO) |
| elif 'api' in tag_lower or 'sdk' in tag_lower: |
| normalized_tags.append(TopicTagEnum.API_SDK) |
| elif 'best' in tag_lower and 'practice' in tag_lower: |
| normalized_tags.append(TopicTagEnum.BEST_PRACTICES) |
| elif 'sensitive' in tag_lower or 'pii' in tag_lower: |
| normalized_tags.append(TopicTagEnum.SENSITIVE_DATA) |
| elif 'troubleshoot' in tag_lower or 'debug' in tag_lower or 'error' in tag_lower: |
| normalized_tags.append(TopicTagEnum.TROUBLESHOOTING) |
| elif 'integrat' in tag_lower: |
| normalized_tags.append(TopicTagEnum.INTEGRATION) |
| else: |
| normalized_tags.append(TopicTagEnum.PRODUCT) |
| logger.warning(f"Unknown topic tag '{tag}', using 'Product' as fallback") |
| |
| return normalized_tags or [TopicTagEnum.PRODUCT] |
|
|
| def _normalize_sentiment(self, sentiment): |
| """Normalize sentiment to match enum values.""" |
| try: |
| return SentimentEnum(sentiment) |
| except ValueError: |
| sentiment_lower = sentiment.lower() |
| if 'frustrat' in sentiment_lower: |
| return SentimentEnum.FRUSTRATED |
| elif 'angry' in sentiment_lower or 'mad' in sentiment_lower: |
| return SentimentEnum.ANGRY |
| elif 'curious' in sentiment_lower or 'interest' in sentiment_lower: |
| return SentimentEnum.CURIOUS |
| else: |
| return SentimentEnum.NEUTRAL |
|
|
| def _normalize_priority(self, priority): |
| """Normalize priority to match enum values.""" |
| try: |
| return PriorityEnum(priority) |
| except ValueError: |
| priority_lower = str(priority).lower() |
| if 'p0' in priority_lower or 'high' in priority_lower or 'urgent' in priority_lower: |
| return PriorityEnum.P0 |
| elif 'p2' in priority_lower or 'low' in priority_lower: |
| return PriorityEnum.P2 |
| else: |
| return PriorityEnum.P1 |
|
|
| async def classify_ticket(self, ticket: Ticket) -> TicketClassification: |
| """Classify a single ticket using Groq API.""" |
| for model in self.models: |
| try: |
| prompt = self._create_classification_prompt(ticket) |
| |
| response = self.client.chat.completions.create( |
| model=model, |
| messages=[ |
| { |
| "role": "system", |
| "content": "You are an expert customer support analyst. Always respond with valid JSON." |
| }, |
| { |
| "role": "user", |
| "content": prompt |
| } |
| ], |
| temperature=0.1, |
| max_tokens=500 |
| ) |
| |
| |
| content = response.choices[0].message.content.strip() |
| logger.info(f"Raw AI response for ticket {ticket.id} using model {model}: {content}") |
| |
| |
| if content.startswith("```json"): |
| content = content[7:-3].strip() |
| elif content.startswith("```"): |
| content = content[3:-3].strip() |
| |
| try: |
| classification_data = json.loads(content) |
| except json.JSONDecodeError as e: |
| logger.error(f"JSON decode error for ticket {ticket.id} using model {model}: {e}") |
| continue |
| |
| |
| topic_tags = self._normalize_topic_tags(classification_data.get("topic_tags", ["Product"])) |
| sentiment = self._normalize_sentiment(classification_data.get("sentiment", "Neutral")) |
| priority = self._normalize_priority(classification_data.get("priority", "P1")) |
| |
| |
| return TicketClassification( |
| topic_tags=topic_tags, |
| sentiment=sentiment, |
| priority=priority, |
| reasoning=classification_data.get("reasoning", f"AI-generated classification using {model}") |
| ) |
| |
| except Exception as e: |
| logger.error(f"Error classifying ticket {ticket.id} with model {model}: {str(e)}") |
| continue |
| |
| |
| logger.error(f"All models failed for ticket {ticket.id}, using fallback") |
| return TicketClassification( |
| topic_tags=[TopicTagEnum.PRODUCT], |
| sentiment=SentimentEnum.NEUTRAL, |
| priority=PriorityEnum.P1, |
| reasoning="All AI models failed, using fallback classification" |
| ) |
|
|
| async def classify_tickets_bulk(self, tickets: List[Ticket]) -> List[TicketClassification]: |
| """Classify multiple tickets.""" |
| classifications = [] |
| |
| for ticket in tickets: |
| classification = await self.classify_ticket(ticket) |
| classifications.append(classification) |
| logger.info(f"Classified ticket {ticket.id}") |
| |
| return classifications |
|
|