Spaces:
Sleeping
Sleeping
File size: 11,636 Bytes
401b16c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | 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 |