Business_Chatbot / src /transaction_clarifier.py
Ancastal's picture
Upload folder using huggingface_hub
401b16c verified
import openai
import os
import json
from typing import Dict, Any, Optional, List, Tuple
from enum import Enum
from pydantic import BaseModel
from models import EntityExtraction
class ClarificationStatus(str, Enum):
COMPLETE = "complete"
NEEDS_CLARIFICATION = "needs_clarification"
CANCELLED = "cancelled"
class ClarificationRequest(BaseModel):
missing_fields: List[str]
questions: List[str]
suggested_values: Dict[str, Any] = {}
explanation: str
class TransactionClarifier:
def __init__(self, api_key: Optional[str] = None):
"""Initialize OpenAI client for transaction clarification"""
self.client = openai.OpenAI(
api_key=api_key or os.getenv('OPENAI_API_KEY')
)
def analyze_transaction_completeness(self, entities: EntityExtraction) -> Tuple[ClarificationStatus, Optional[ClarificationRequest]]:
"""
Analyze if a transaction has all necessary information
Args:
entities: Extracted entities from user input
Returns:
Tuple of (status, clarification_request)
"""
# Define required and optional fields based on transaction type
if entities.transaction_type == "purchase":
required_fields = ["product", "quantity", "supplier", "unit_price"]
optional_fields = ["total_amount"]
elif entities.transaction_type == "sale":
required_fields = ["product", "quantity", "customer", "unit_price"]
optional_fields = ["total_amount"]
else:
return ClarificationStatus.COMPLETE, None
# Check for missing required fields
missing_fields = []
entity_dict = entities.dict()
for field in required_fields:
if not entity_dict.get(field):
missing_fields.append(field)
# If all required fields are present, transaction is complete
if not missing_fields:
return ClarificationStatus.COMPLETE, None
# Generate intelligent clarification request
clarification = self._generate_clarification_request(entities, missing_fields)
return ClarificationStatus.NEEDS_CLARIFICATION, clarification
def _generate_clarification_request(self, entities: EntityExtraction, missing_fields: List[str]) -> ClarificationRequest:
"""Generate intelligent questions for missing information"""
# Prepare context about what we already know
known_info = {}
entity_dict = entities.dict()
for field, value in entity_dict.items():
if value is not None and field != "notes":
known_info[field] = value
system_prompt = f"""You are a helpful business assistant helping complete a {entities.transaction_type} transaction.
Generate natural, conversational questions to gather missing information. The user should be able to:
1. Provide the missing information
2. Say "N/A" or "skip" if the information is not available/applicable
3. Ask for suggestions if they're unsure
Create personalized questions based on the context of what we already know.
Return your response in this exact JSON format:
{{
"questions": ["question1", "question2", ...],
"suggested_values": {{"field": "suggested_value", ...}},
"explanation": "Brief explanation of why we need this information"
}}
Missing fields to ask about: {missing_fields}
Transaction type: {entities.transaction_type}
"""
user_prompt = f"""We're processing a {entities.transaction_type} transaction and need to gather some missing information.
What we already know:
{json.dumps(known_info, indent=2)}
Missing fields: {missing_fields}
Generate friendly, specific questions to gather the missing information. Make suggestions when appropriate."""
try:
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.3,
max_tokens=400
)
response_text = response.choices[0].message.content.strip()
try:
result_dict = json.loads(response_text)
return ClarificationRequest(
missing_fields=missing_fields,
questions=result_dict.get("questions", []),
suggested_values=result_dict.get("suggested_values", {}),
explanation=result_dict.get("explanation", "I need some additional information to complete this transaction.")
)
except (json.JSONDecodeError, KeyError) as e:
# Fallback to simple questions
return self._generate_fallback_questions(entities, missing_fields)
except Exception as e:
print(f"Error generating clarification: {e}")
return self._generate_fallback_questions(entities, missing_fields)
def _generate_fallback_questions(self, entities: EntityExtraction, missing_fields: List[str]) -> ClarificationRequest:
"""Generate fallback questions when LLM fails"""
question_templates = {
"product": "What product or item is involved in this transaction?",
"quantity": f"How many units {'were purchased' if entities.transaction_type == 'purchase' else 'were sold'}?",
"supplier": "Which supplier or vendor is this purchase from?",
"customer": "Who is the customer for this sale?",
"unit_price": "What is the price per unit?",
"total_amount": "What is the total amount for this transaction?"
}
questions = []
for field in missing_fields:
questions.append(question_templates.get(field, f"What is the {field.replace('_', ' ')}?"))
return ClarificationRequest(
missing_fields=missing_fields,
questions=questions,
suggested_values={},
explanation="I need some additional information to complete this transaction."
)
def process_clarification_response(self, original_entities: EntityExtraction,
missing_fields: List[str],
user_response: str) -> Tuple[EntityExtraction, bool]:
"""
Process user's response to clarification questions
Args:
original_entities: Original extracted entities
missing_fields: Fields we asked about
user_response: User's response to our questions
Returns:
Tuple of (updated_entities, is_complete)
"""
system_prompt = f"""You are processing a user's response to clarification questions about a {original_entities.transaction_type} transaction.
Extract the missing information from the user's response. The user may:
1. Provide specific values for the missing fields
2. Say "N/A", "skip", "not applicable", or similar to indicate the field should be null
3. Ask for help or say they don't know
Missing fields we asked about: {missing_fields}
Return a JSON object with the extracted values. Use null for fields that are N/A or skipped.
Example response format:
{{
"product": "extracted product name",
"quantity": 10,
"supplier": null,
"unit_price": 5.99,
"interpretation": "Brief explanation of what you extracted"
}}"""
user_prompt = f"""Original transaction: {original_entities.transaction_type}
Missing fields: {missing_fields}
User's response: "{user_response}"
Extract the values for the missing fields from the user's response."""
try:
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.1,
max_tokens=300
)
response_text = response.choices[0].message.content.strip()
try:
extracted_values = json.loads(response_text)
# Update original entities with extracted values
updated_entities = self._update_entities(original_entities, extracted_values, missing_fields)
# Check if transaction is now complete
status, _ = self.analyze_transaction_completeness(updated_entities)
is_complete = (status == ClarificationStatus.COMPLETE)
return updated_entities, is_complete
except (json.JSONDecodeError, KeyError) as e:
print(f"Error parsing clarification response: {e}")
return original_entities, False
except Exception as e:
print(f"Error processing clarification: {e}")
return original_entities, False
def _update_entities(self, original_entities: EntityExtraction,
extracted_values: Dict[str, Any],
missing_fields: List[str]) -> EntityExtraction:
"""Update entities with extracted clarification values"""
# Convert to dict for easier manipulation
entity_dict = original_entities.dict()
# Update with extracted values
for field in missing_fields:
if field in extracted_values:
value = extracted_values[field]
# Handle type conversions
if field in ["quantity"] and value is not None:
try:
entity_dict[field] = int(value)
except (ValueError, TypeError):
entity_dict[field] = None
elif field in ["unit_price", "total_amount"] and value is not None:
try:
entity_dict[field] = float(value)
except (ValueError, TypeError):
entity_dict[field] = None
else:
entity_dict[field] = value
# Recalculate total if we have quantity and unit_price
if entity_dict.get("quantity") and entity_dict.get("unit_price"):
entity_dict["total_amount"] = entity_dict["quantity"] * entity_dict["unit_price"]
return EntityExtraction(**entity_dict)
def format_clarification_message(self, clarification: ClarificationRequest) -> str:
"""Format clarification request as a user-friendly message"""
message = f"📝 {clarification.explanation}\n\n"
for i, question in enumerate(clarification.questions, 1):
message += f"{i}. {question}\n"
# Add suggestions if available
if clarification.suggested_values:
message += "\n💡 Suggestions:\n"
for field, suggestion in clarification.suggested_values.items():
message += f" • {field.replace('_', ' ').title()}: {suggestion}\n"
message += "\n✨ You can say 'N/A' or 'skip' for any information that's not available."
message += "\n📞 Please provide the missing information in your next message."
return message