rairo commited on
Commit
f341e1d
·
1 Parent(s): d2b7577

Update utility.py

Browse files
Files changed (1) hide show
  1. utility.py +683 -266
utility.py CHANGED
@@ -1,348 +1,765 @@
 
1
  import json
2
  import os
3
  import logging
4
  from datetime import datetime
5
- from typing import List, Dict, Union, Optional
6
  from google.cloud import firestore
7
  import pandas as pd
 
 
8
  from langchain_google_genai import ChatGoogleGenerativeAI
9
  import google.generativeai as genai
 
10
 
11
  logger = logging.getLogger(__name__)
12
 
13
  db = firestore.Client.from_service_account_json("firestore-key.json")
14
 
15
- llm = ChatGoogleGenerativeAI(
16
- model="gemini-2.0-flash-thinking-exp",
17
- max_output_tokens=1024,
18
- temperature=0.01,
19
- top_k=1,
20
- top_p=0.01,
21
- )
22
-
23
- def generateResponse(prompt: str) -> str:
24
- """Generate structured response from user input"""
25
- system_prompt = """You MUST format responses EXACTLY as follows:
26
-
27
- *Intent*: [create/read/update/delete]
28
- *Transaction Type*: [purchase/sale/inventory/etc]
29
- *Details*:
30
- - Field1: Value1
31
- - Field2: Value2
32
- - Field3: Value3
33
-
34
- For multiple transactions, repeat this pattern for each transaction."""
35
-
36
- genai.configure(api_key=os.environ.get("GOOGLE_API_KEY"))
37
  model = genai.GenerativeModel(
38
- 'gemini-2.0-flash-thinking-exp',
39
  generation_config={
40
- "temperature": 0.1,
41
- "top_p": 0.01,
42
- "top_k": 1,
43
- "max_output_tokens": 1024,
 
44
  }
45
  )
46
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  try:
48
- response = model.generate_content([system_prompt, prompt])
49
- return response.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  except Exception as e:
51
- logger.error(f"Response generation failed: {e}")
52
- return "Sorry, I couldn't process that request."
 
 
53
 
54
  def parse_multiple_transactions(response_text: str) -> List[Dict]:
55
- """Parse response into structured transactions"""
56
  transactions = []
57
- current_trans = {}
58
-
59
  try:
60
- parsed = json.loads(response_text)
61
- if isinstance(parsed, dict):
62
- return [add_timestamp(parsed)]
63
- return [add_timestamp(t) for t in parsed]
64
- except json.JSONDecodeError:
65
- pass
66
-
67
- lines = [line.strip() for line in response_text.split('\n') if line.strip()]
68
-
69
- for line in lines:
70
- if line.startswith('*Intent*:'):
71
- if current_trans:
72
- transactions.append(add_timestamp(current_trans))
73
- current_trans = {}
74
- current_trans['intent'] = line.split(':', 1)[1].strip().lower()
75
- elif line.startswith('*Transaction Type*:'):
76
- current_trans['transaction_type'] = line.split(':', 1)[1].strip().lower()
77
- elif line.startswith('*Details*:'):
78
- current_trans['details'] = {}
79
- elif line.startswith('-') and 'details' in current_trans:
80
- key_value = line[1:].strip().split(':', 1)
81
- if len(key_value) == 2:
82
- key = key_value[0].strip().lower()
83
- value = key_value[1].strip()
84
- current_trans['details'][key] = value
85
-
86
- if current_trans:
87
- transactions.append(add_timestamp(current_trans))
88
-
89
- return transactions if transactions else []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  def add_timestamp(transaction: Dict) -> Dict:
92
- """Add timestamp to transaction"""
93
- transaction['created_at'] = datetime.now().isoformat()
 
94
  return transaction
95
 
 
 
96
  def create_inventory(user_phone: str, transaction_data: List[Dict]) -> bool:
97
- """Create/update inventory items"""
98
  batch = db.batch()
99
  inventory_ref = db.collection("users").document(user_phone).collection("inventory")
100
-
 
101
  for transaction in transaction_data:
102
- item_name = transaction['details'].get('item')
 
103
  if not item_name:
 
 
104
  continue
105
-
106
- quantity = int(transaction['details'].get('quantity', 0))
107
- doc_ref = inventory_ref.document(item_name)
108
-
109
- batch.set(doc_ref, {
110
- 'intent': 'create',
111
- 'transaction_type': 'inventory',
 
 
 
 
 
 
 
112
  'details': {
113
- 'item': item_name,
114
- 'quantity': firestore.Increment(quantity),
115
- **{k:v for k,v in transaction['details'].items()
116
- if k not in ['item', 'quantity']}
117
  },
118
- 'last_updated': datetime.now().isoformat()
119
- }, merge=True)
120
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  try:
122
  batch.commit()
123
- return True
 
124
  except Exception as e:
125
- logger.error(f"Inventory update failed: {e}")
126
  return False
127
 
128
- def create_sale(user_phone: str, transaction_data: List[Dict]) -> bool:
129
- """Process sales with inventory validation"""
130
- batch = db.batch()
131
  inventory_ref = db.collection("users").document(user_phone).collection("inventory")
132
  sales_ref = db.collection("users").document(user_phone).collection("sales")
133
-
134
- for transaction in transaction_data:
135
- item_name = transaction['details'].get('item')
136
- if not item_name:
137
- continue
138
-
139
- quantity = int(transaction['details'].get('quantity', 0))
140
- item_doc = inventory_ref.document(item_name).get()
141
-
142
- if not item_doc.exists:
143
- logger.error(f"Item {item_name} not found")
144
- return False
145
-
146
- current_stock = int(item_doc.to_dict().get('details', {}).get('quantity', 0))
147
- if current_stock < quantity:
148
- logger.error(f"Insufficient stock for {item_name}")
149
- return False
150
-
151
- batch.update(inventory_ref.document(item_name), {
152
- 'details.quantity': firestore.Increment(-quantity),
153
- 'last_updated': datetime.now().isoformat()
154
- })
155
-
156
- batch.set(sales_ref.document(), {
157
- **transaction,
158
- 'timestamp': datetime.now().isoformat()
159
- })
160
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  try:
162
- batch.commit()
163
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  except Exception as e:
165
- logger.error(f"Sales processing failed: {e}")
166
- return False
 
 
167
 
168
  def read_datalake(user_phone: str, query: str) -> Union[str, Dict]:
169
- """Query user's transaction data"""
170
- from pandasai import SmartDatalake
171
-
 
 
 
 
 
 
172
  try:
173
- inventory = [doc.to_dict() for doc in
174
- db.collection("users").document(user_phone).collection("inventory").stream()]
175
- sales = [doc.to_dict() for doc in
176
- db.collection("users").document(user_phone).collection("sales").stream()]
177
-
178
- if not inventory and not sales:
179
- return "No transaction data found"
180
-
181
- inventory_df = pd.DataFrame(inventory)
182
- sales_df = pd.DataFrame(sales)
183
-
184
- lake = SmartDatalake(
185
- [inventory_df, sales_df],
186
- config={
187
- "llm": llm,
188
- "enable_cache": False,
189
- "save_logs": False
190
- }
191
- )
192
-
193
- response = lake.chat(query)
194
- return str(response)
195
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  except Exception as e:
197
- logger.error(f"Data query failed: {e}")
198
- return "Sorry, I couldn't retrieve that data."
199
-
200
- def update_transaction(user_phone: str, transaction_id: str,
201
- updates: Dict, collection: str = "inventory") -> bool:
202
- """Update existing transaction"""
203
- doc_ref = db.collection("users").document(user_phone).collection(collection).document(transaction_id)
204
-
205
- if not doc_ref.get().exists:
206
- return False
207
-
208
- update_data = {
209
- f"details.{k}": v for k,v in updates.items()
210
- }
211
- update_data['last_updated'] = datetime.now().isoformat()
212
-
 
 
 
 
 
 
 
 
213
  try:
214
- doc_ref.update(update_data)
215
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  except Exception as e:
217
- logger.error(f"Update failed: {e}")
218
- return False
 
219
 
220
- def delete_transaction(user_phone: str, transaction_data: List[Dict]) -> bool:
221
- """Delete specified transactions"""
222
  batch = db.batch()
223
- collection_map = {
224
- 'purchase': 'inventory',
225
- 'sale': 'sales',
226
- 'inventory': 'inventory'
227
- }
228
-
229
  for transaction in transaction_data:
230
- item_name = transaction['details'].get('item')
 
 
 
231
  if not item_name:
 
232
  continue
233
-
234
- trans_type = transaction.get('transaction_type', 'inventory').lower()
235
- collection = collection_map.get(trans_type, 'inventory')
236
-
237
- doc_ref = db.collection("users").document(user_phone).collection(collection).document(item_name)
 
 
 
 
 
 
 
 
 
 
 
 
238
  batch.delete(doc_ref)
239
-
 
 
 
 
 
 
 
 
240
  try:
241
- batch.commit()
242
- return True
 
 
 
 
 
 
 
 
 
 
243
  except Exception as e:
244
- logger.error(f"Deletion failed: {e}")
245
- return False
 
 
246
 
247
  def persist_temporary_transaction(transactions: List[Dict], mobile: str) -> bool:
248
- """Store transactions temporarily before confirmation"""
 
 
 
249
  try:
250
  doc_ref = db.collection("users").document(mobile).collection("temp_transactions").document("pending")
251
  doc_ref.set({
252
- "transactions": transactions,
253
  "timestamp": datetime.now().isoformat(),
254
  "status": "pending_confirmation"
255
  })
 
256
  return True
257
  except Exception as e:
258
- logger.error(f"Temp storage failed: {e}")
259
  return False
260
 
261
- def format_transaction_response(transactions: Union[List[Dict], Dict]) -> str:
262
- """Format transaction data for user display"""
 
 
 
263
  if not transactions:
264
- return "No transaction data"
265
-
266
  if isinstance(transactions, dict):
267
- transactions = [transactions]
268
-
269
- output = []
270
- for idx, trans in enumerate(transactions, 1):
271
- output.append(f"Transaction {idx}:" if len(transactions) > 1 else "Transaction Details:")
272
- output.append(f"• Intent: {trans.get('intent', 'unknown').title()}")
273
- output.append(f"• Type: {trans.get('transaction_type', 'unknown').title()}")
274
-
275
- if 'details' in trans:
276
- output.append("• Details:")
277
- for k, v in trans['details'].items():
278
- if k == 'currency':
279
- continue
280
- if 'price' in k or 'amount' in k or 'cost' in k:
281
- currency = trans['details'].get('currency', '$')
282
- output.append(f" - {k.title()}: {currency}{v}")
283
- else:
284
- output.append(f" - {k.title()}: {v}")
285
- output.append("")
286
-
287
- return "\n".join(output)
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- def fetch_transaction(user_phone: str,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  item_name: Optional[str] = None,
291
- collection: str = "inventory") -> Union[Dict, List[Dict]]:
292
- """Retrieve transaction(s) from Firestore"""
 
 
 
 
 
 
293
  col_ref = db.collection("users").document(user_phone).collection(collection)
294
-
295
- if item_name:
296
- doc = col_ref.document(item_name).get()
297
- return doc.to_dict() if doc.exists else None
298
- else:
299
- return [doc.to_dict() for doc in col_ref.stream()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
  def process_intent(parsed_trans_data: List[Dict], mobile: str) -> str:
302
- """Route transactions to appropriate CRUD operation"""
303
  if not parsed_trans_data:
304
- return "Error: No transaction data provided"
305
-
306
- intent = parsed_trans_data[0].get('intent', '').lower()
307
- trans_type = parsed_trans_data[0].get('transaction_type', '').lower()
 
 
 
 
 
 
 
 
 
308
  transaction_summary = format_transaction_response(parsed_trans_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
- if intent == 'create':
311
- if trans_type in ('purchase', 'inventory'):
312
- success = create_inventory(mobile, parsed_trans_data)
313
- return ("Inventory updated successfully!\n\n" + transaction_summary) if success else \
314
- ("Failed to update inventory!\n\n" + transaction_summary)
315
- elif trans_type == 'sale':
316
- success = create_sale(mobile, parsed_trans_data)
317
- return ("Sale recorded successfully!\n\n" + transaction_summary) if success else \
318
- ("Failed to record sale! Check inventory.\n\n" + transaction_summary)
319
  else:
320
- return f"Unsupported transaction type: {trans_type}\n\n{transaction_summary}"
321
-
322
- elif intent == 'read':
323
- if 'item' in parsed_trans_data[0].get('details', {}):
324
- item = parsed_trans_data[0]['details']['item']
325
- result = fetch_transaction(mobile, item)
326
- return f"Item details:\n\n{format_transaction_response(result)}" if result else f"Item '{item}' not found"
327
  else:
328
- results = fetch_transaction(mobile)
329
- return f"All items:\n\n{format_transaction_response(results)}" if results else "No items found"
330
-
331
- elif intent == 'update':
332
- item = parsed_trans_data[0]['details'].get('item')
333
- if not item:
334
- return "Error: No item specified for update"
335
-
336
- updates = {k:v for k,v in parsed_trans_data[0]['details'].items() if k != 'item'}
337
- success = update_transaction(mobile, item, updates, trans_type if trans_type else 'inventory')
338
- return (f"Updated {item} successfully!\n\n{transaction_summary}") if success else \
339
- (f"Failed to update {item}!\n\n{transaction_summary}")
340
-
341
- elif intent == 'delete':
342
- item = parsed_trans_data[0]['details'].get('item', 'item')
343
- success = delete_transaction(mobile, parsed_trans_data)
344
- return (f"Deleted {item} successfully!") if success else \
345
- (f"Failed to delete {item}!")
346
-
347
- else:
348
- return f"Unsupported intent: {intent}\n\n{transaction_summary}"
 
1
+ # utility.py
2
  import json
3
  import os
4
  import logging
5
  from datetime import datetime
6
+ from typing import List, Dict, Union, Optional, Any
7
  from google.cloud import firestore
8
  import pandas as pd
9
+ # Remove pandasai if not strictly needed or causing issues, replace with direct Firestore queries
10
+ # from pandasai import SmartDatalake
11
  from langchain_google_genai import ChatGoogleGenerativeAI
12
  import google.generativeai as genai
13
+ import re # For currency check
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
  db = firestore.Client.from_service_account_json("firestore-key.json")
18
 
19
+ # Configure Google Generative AI (Consider making model configurable)
20
+ try:
21
+ genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
22
+ GENERATIVE_MODEL_NAME = "gemini-2.0-flash" # Or another suitable model like gemini-pro
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  model = genai.GenerativeModel(
24
+ GENERATIVE_MODEL_NAME,
25
  generation_config={
26
+ "temperature": 0.1, # Slightly higher temp might allow more flexibility if needed
27
+ "top_p": 0.9, # Adjust Top P and K for desired creativity/determinism
28
+ "top_k": 10, # Example adjustment
29
+ "max_output_tokens": 2048, # Increased token limit
30
+ "response_mime_type": "application/json", # Explicitly request JSON
31
  }
32
  )
33
+ # Also configure LangChain LLM if used elsewhere (e.g., potentially for pandasai if kept)
34
+ llm = ChatGoogleGenerativeAI(
35
+ model=GENERATIVE_MODEL_NAME,
36
+ temperature=0.1,
37
+ # Add other relevant LangChain config if needed
38
+ convert_system_message_to_human=True # Often helpful for Gemini
39
+ )
40
+ logger.info(f"Using Generative Model: {GENERATIVE_MODEL_NAME}")
41
+
42
+ except KeyError:
43
+ logger.error("GOOGLE_API_KEY environment variable not set!")
44
+ # Handle this case gracefully, maybe disable LLM features
45
+ model = None
46
+ llm = None
47
+ except Exception as e:
48
+ logger.error(f"Error configuring Generative AI: {e}")
49
+ model = None
50
+ llm = None
51
+
52
+
53
+ def generateResponse(prompt: str) -> str:
54
+ """Generate structured JSON response from user input using Generative AI."""
55
+ if not model:
56
+ logger.error("Generative AI model not configured. Cannot generate response.")
57
+ return '{"error": "Model not available"}' # Return JSON error
58
+
59
+ # Refined System Prompt requesting JSON
60
+ system_prompt = """
61
+ Analyze the user's request for business transaction management (like sales, purchases, inventory checks).
62
+ Your goal is to extract structured information about one or more transactions.
63
+ You MUST output your response as a valid JSON list containing one or more transaction objects.
64
+ Each transaction object MUST have the following keys:
65
+ - "intent": A string representing the user's goal (e.g., "create", "read", "update", "delete").
66
+ - "transaction_type": A string categorizing the transaction (e.g., "sale", "purchase", "inventory", "query", "other").
67
+ - "details": An object containing key-value pairs extracted from the request. Keys should be lowercase strings (e.g., "item", "quantity", "price", "customer", "date", "description"). Values should be strings or numbers as appropriate. Include a "currency" key (e.g., "$", "£", "€", "ZAR") if a monetary value is present. If no specific type is clear, use "query" or "other".
68
+
69
+ Example Input: "Record a sale of 5 apples at $2 each to John Doe"
70
+ Example Output:
71
+ [
72
+ {
73
+ "intent": "create",
74
+ "transaction_type": "sale",
75
+ "details": {
76
+ "item": "apples",
77
+ "quantity": 5,
78
+ "price": 2,
79
+ "currency": "$",
80
+ "customer": "John Doe"
81
+ }
82
+ }
83
+ ]
84
+
85
+ Example Input: "Add 10 bananas to stock and how many apples do I have?"
86
+ Example Output:
87
+ [
88
+ {
89
+ "intent": "create",
90
+ "transaction_type": "inventory",
91
+ "details": {
92
+ "item": "bananas",
93
+ "quantity": 10
94
+ }
95
+ },
96
+ {
97
+ "intent": "read",
98
+ "transaction_type": "query",
99
+ "details": {
100
+ "item": "apples",
101
+ "query": "check stock"
102
+ }
103
+ }
104
+ ]
105
+
106
+ If the request is unclear or doesn't seem like a transaction, try to interpret it as a "query" intent or respond with an appropriate JSON structure indicating ambiguity.
107
+ Do NOT add any text before or after the JSON list. Just output the JSON.
108
+ """
109
+
110
  try:
111
+ # Using the newer API structure if applicable, might need adjustment based on library version
112
+ full_prompt = [system_prompt, prompt] # Or construct as per specific library requirements
113
+ response = model.generate_content(full_prompt)
114
+
115
+ # Accessing the response text might differ slightly based on the exact genai version
116
+ response_text = response.text
117
+ logger.info(f"LLM Raw Response: {response_text}") # Log the raw response for debugging
118
+
119
+ # Basic validation: Check if it looks like JSON
120
+ if response_text.strip().startswith('[') and response_text.strip().endswith(']'):
121
+ return response_text
122
+ else:
123
+ logger.warning(f"LLM response does not look like a JSON list: {response_text}")
124
+ # Attempt to wrap it if it's a single object, otherwise return error
125
+ if response_text.strip().startswith('{') and response_text.strip().endswith('}'):
126
+ return f"[{response_text.strip()}]"
127
+ return '{"error": "Invalid format from LLM"}'
128
+
129
  except Exception as e:
130
+ logger.error(f"LLM Response generation failed: {e}", exc_info=True)
131
+ # Return a JSON formatted error message
132
+ return '{"error": "Failed to process request with LLM"}'
133
+
134
 
135
  def parse_multiple_transactions(response_text: str) -> List[Dict]:
136
+ """Parse JSON response from LLM into structured transactions."""
137
  transactions = []
 
 
138
  try:
139
+ # Prioritize JSON parsing
140
+ parsed_data = json.loads(response_text)
141
+
142
+ if isinstance(parsed_data, list):
143
+ # Ensure each item in the list is a dictionary and add timestamp
144
+ for item in parsed_data:
145
+ if isinstance(item, dict):
146
+ transactions.append(add_timestamp(item))
147
+ else:
148
+ logger.warning(f"Skipping non-dictionary item in JSON list: {item}")
149
+ if transactions: # Return only if we successfully parsed some transactions
150
+ return transactions
151
+ elif isinstance(parsed_data, dict) and 'error' in parsed_data:
152
+ logger.error(f"LLM returned an error: {parsed_data['error']}")
153
+ return [] # Return empty list on error
154
+ else:
155
+ logger.warning(f"Parsed JSON is not a list: {parsed_data}")
156
+ # Handle case where LLM might return a single object instead of a list
157
+ if isinstance(parsed_data, dict):
158
+ return [add_timestamp(parsed_data)]
159
+
160
+
161
+ except json.JSONDecodeError as json_err:
162
+ logger.error(f"JSONDecodeError: {json_err} - Response text: {response_text}")
163
+ # Fallback: Try simple text parsing (less reliable) ONLY if JSON fails completely
164
+ # This part is kept as a last resort but should ideally not be needed with JSON output
165
+ logger.info("Falling back to text parsing (less reliable).")
166
+ lines = [line.strip() for line in response_text.split('\n') if line.strip()]
167
+ current_trans = {}
168
+ in_details_section = False
169
+
170
+ for line in lines:
171
+ line_lower = line.lower()
172
+ if line_lower.startswith('*intent*:'):
173
+ if current_trans: transactions.append(add_timestamp(current_trans))
174
+ current_trans = {'details': {}} # Reset with details object
175
+ current_trans['intent'] = line.split(':', 1)[1].strip().lower()
176
+ in_details_section = False
177
+ elif line_lower.startswith('*transaction type*:'):
178
+ current_trans['transaction_type'] = line.split(':', 1)[1].strip().lower()
179
+ in_details_section = False
180
+ elif line_lower.startswith('*details*:'):
181
+ in_details_section = True
182
+ elif line.startswith('-') and in_details_section and 'details' in current_trans:
183
+ key_value = line[1:].strip().split(':', 1)
184
+ if len(key_value) == 2:
185
+ key = key_value[0].strip().lower().replace(" ", "_") # Normalize key
186
+ value = key_value[1].strip()
187
+ current_trans['details'][key] = value
188
+
189
+ if current_trans: # Add the last transaction
190
+ transactions.append(add_timestamp(current_trans))
191
+
192
+ if not transactions: # If fallback also failed
193
+ logger.error("Failed to parse LLM response using both JSON and text methods.")
194
+ return []
195
+
196
+ except Exception as e:
197
+ logger.error(f"Unexpected error during parsing: {e}", exc_info=True)
198
+ return [] # Return empty on unexpected error
199
+
200
+ # Final check for essential keys before returning
201
+ valid_transactions = []
202
+ for t in transactions:
203
+ if isinstance(t, dict) and 'intent' in t and 'transaction_type' in t and 'details' in t:
204
+ valid_transactions.append(t)
205
+ else:
206
+ logger.warning(f"Skipping invalid transaction structure after parsing: {t}")
207
+
208
+ return valid_transactions
209
+
210
 
211
  def add_timestamp(transaction: Dict) -> Dict:
212
+ """Add created_at timestamp to transaction if not present."""
213
+ if 'created_at' not in transaction:
214
+ transaction['created_at'] = datetime.now().isoformat()
215
  return transaction
216
 
217
+ # --- Firestore CRUD Operations (Largely unchanged, minor logging improvements) ---
218
+
219
  def create_inventory(user_phone: str, transaction_data: List[Dict]) -> bool:
220
+ """Create/update inventory items in Firestore."""
221
  batch = db.batch()
222
  inventory_ref = db.collection("users").document(user_phone).collection("inventory")
223
+ success = True
224
+
225
  for transaction in transaction_data:
226
+ details = transaction.get('details', {})
227
+ item_name = details.get('item')
228
  if not item_name:
229
+ logger.warning(f"Skipping inventory update: 'item' missing in details. Data: {transaction}")
230
+ success = False # Mark as partial failure if any item is skipped
231
  continue
232
+
233
+ try:
234
+ # Attempt to convert quantity to int, default to 0 if missing/invalid
235
+ quantity = int(details.get('quantity', 0))
236
+ except (ValueError, TypeError):
237
+ logger.warning(f"Invalid quantity for item '{item_name}'. Defaulting to 0. Data: {details.get('quantity')}")
238
+ quantity = 0
239
+
240
+ doc_ref = inventory_ref.document(item_name) # Use item name as document ID
241
+
242
+ # Prepare data, ensuring quantity is handled as an increment
243
+ item_data = {
244
+ 'intent': transaction.get('intent', 'create'),
245
+ 'transaction_type': transaction.get('transaction_type', 'inventory'),
246
  'details': {
247
+ # Merge existing details, ensuring item name is present
248
+ **{k: v for k, v in details.items() if k != 'quantity'}, # Keep other details
249
+ 'item': item_name, # Ensure item name is stored
250
+ 'quantity': firestore.Increment(quantity), # Use Increment for atomic adds
251
  },
252
+ 'last_updated': datetime.now().isoformat(),
253
+ 'created_at': transaction.get('created_at', datetime.now().isoformat()) # Store creation time
254
+ }
255
+
256
+ # Use set with merge=True to create or update the document
257
+ # We merge the 'details' field specifically if needed, but Increment handles quantity well.
258
+ # Let's refine the merge logic: update specific fields, especially quantity.
259
+ # Using set with merge=True might overwrite details if not careful.
260
+ # Let's use update for existing, set for new, or carefully manage merge.
261
+
262
+ # Simpler approach: Set with merge=True, but ensure Increment works.
263
+ # Firestore's Increment needs to be applied to an existing numeric field or it initializes to the increment value.
264
+ # This requires checking if the doc exists first for perfect merging, or rely on set(merge=True)
265
+ # which is generally fine if the structure is consistent.
266
+
267
+ batch.set(doc_ref, item_data, merge=True) # merge=True helps preserve other fields if doc exists
268
+ logger.info(f"Batched inventory update for item: {item_name}, quantity change: {quantity}")
269
+
270
  try:
271
  batch.commit()
272
+ logger.info(f"Inventory batch committed successfully for user {user_phone}.")
273
+ return success # Return True if all items processed, False if any were skipped
274
  except Exception as e:
275
+ logger.error(f"Inventory batch commit failed for user {user_phone}: {e}", exc_info=True)
276
  return False
277
 
278
+
279
+ def create_sale(user_phone: str, transaction_data: List[Dict]) -> tuple[bool, str]:
280
+ """Process sales, validate inventory, update stock, and record sale."""
281
  inventory_ref = db.collection("users").document(user_phone).collection("inventory")
282
  sales_ref = db.collection("users").document(user_phone).collection("sales")
283
+ feedback_messages = []
284
+ all_successful = True
285
+
286
+ # Use a transaction for read-modify-write operations on inventory
287
+ @firestore.transactional
288
+ def process_sale_transaction(transaction, sale_details_list):
289
+ nonlocal feedback_messages, all_successful
290
+ sale_batch = db.batch() # Batch for creating sale records (can be outside transaction if preferred)
291
+
292
+ for sale_details in sale_details_list:
293
+ item_name = sale_details.get('item')
294
+ if not item_name:
295
+ feedback_messages.append("Sale skipped: Missing item name.")
296
+ all_successful = False
297
+ continue
298
+
299
+ try:
300
+ quantity_sold = int(sale_details.get('quantity', 0))
301
+ if quantity_sold <= 0:
302
+ feedback_messages.append(f"Sale skipped for '{item_name}': Invalid quantity ({quantity_sold}).")
303
+ all_successful = False
304
+ continue
305
+ except (ValueError, TypeError):
306
+ feedback_messages.append(f"Sale skipped for '{item_name}': Invalid quantity format.")
307
+ all_successful = False
308
+ continue
309
+
310
+ item_doc_ref = inventory_ref.document(item_name)
311
+ item_snapshot = item_doc_ref.get(transaction=transaction)
312
+
313
+ if not item_snapshot.exists:
314
+ feedback_messages.append(f"Sale failed for '{item_name}': Item not found in inventory.")
315
+ all_successful = False
316
+ continue
317
+
318
+ item_data = item_snapshot.to_dict()
319
+ current_stock = int(item_data.get('details', {}).get('quantity', 0))
320
+
321
+ if current_stock < quantity_sold:
322
+ feedback_messages.append(f"Sale failed for '{item_name}': Insufficient stock (Have: {current_stock}, Need: {quantity_sold}).")
323
+ all_successful = False
324
+ continue
325
+
326
+ # Update inventory within the transaction
327
+ transaction.update(item_doc_ref, {
328
+ 'details.quantity': firestore.Increment(-quantity_sold),
329
+ 'last_updated': datetime.now().isoformat()
330
+ })
331
+
332
+ # Prepare sale record (can be added to a separate batch commit after transaction)
333
+ sale_record = {
334
+ **sale_details, # Include original details like price, customer etc.
335
+ 'timestamp': datetime.now().isoformat(), # Record time of sale processing
336
+ 'status': 'completed'
337
+ }
338
+ # Add sale record to a batch (commit after transaction succeeds)
339
+ sale_batch.set(sales_ref.document(), sale_record) # Auto-generate sale ID
340
+
341
+ feedback_messages.append(f"Sale processed for {quantity_sold} x '{item_name}'.")
342
+
343
+ # Return the batch to be committed outside the transaction function
344
+ return sale_batch
345
+
346
+
347
+ # Prepare the list of sale details dictionaries
348
+ sales_to_process = [t.get('details', {}) for t in transaction_data if t.get('transaction_type') == 'sale']
349
+
350
+ if not sales_to_process:
351
+ return False, "No valid sale transactions found."
352
+
353
  try:
354
+ # Execute the Firestore transaction
355
+ db_transaction = db.transaction()
356
+ sale_record_batch = process_sale_transaction(db_transaction, sales_to_process)
357
+
358
+ # If transaction successful, commit the batch of sale records
359
+ if all_successful and sale_record_batch:
360
+ sale_record_batch.commit()
361
+ logger.info(f"Sales records committed successfully for user {user_phone}.")
362
+ elif not all_successful:
363
+ logger.warning(f"Partial or full failure in sale processing for user {user_phone}. See feedback.")
364
+ else:
365
+ logger.info(f"No sales to record for user {user_phone}.")
366
+
367
+
368
+ return all_successful, "\n".join(feedback_messages)
369
+
370
  except Exception as e:
371
+ logger.error(f"Sales processing failed for user {user_phone}: {e}", exc_info=True)
372
+ feedback_messages.append("An unexpected error occurred during sale processing.")
373
+ return False, "\n".join(feedback_messages)
374
+
375
 
376
  def read_datalake(user_phone: str, query: str) -> Union[str, Dict]:
377
+ """Query user's transaction data from Firestore. (Replaces pandasai for simplicity/reliability)."""
378
+ # This function now performs direct Firestore queries based on keywords in the query.
379
+ # It's less "smart" than pandasai but more predictable.
380
+ # You could enhance this with more sophisticated query parsing if needed.
381
+
382
+ query_lower = query.lower()
383
+ results = []
384
+ queried_collections = set()
385
+
386
  try:
387
+ # Simple keyword matching for now
388
+ if "inventory" in query_lower or "stock" in query_lower or "how many" in query_lower or "what do i have" in query_lower:
389
+ queried_collections.add("inventory")
390
+ inventory_ref = db.collection("users").document(user_phone).collection("inventory")
391
+ docs = inventory_ref.stream()
392
+ inventory_data = [doc.to_dict() for doc in docs]
393
+ if inventory_data:
394
+ results.append({"collection": "Inventory", "data": inventory_data})
395
+ else:
396
+ results.append({"collection": "Inventory", "message": "No inventory data found."})
397
+
398
+
399
+ if "sale" in query_lower or "sold" in query_lower or "revenue" in query_lower or "customer" in query_lower:
400
+ queried_collections.add("sales")
401
+ sales_ref = db.collection("users").document(user_phone).collection("sales")
402
+ # Add ordering or filtering based on query if needed (e.g., by date, item)
403
+ docs = sales_ref.order_by("timestamp", direction=firestore.Query.DESCENDING).limit(20).stream() # Example: Get recent sales
404
+ sales_data = [doc.to_dict() for doc in docs]
405
+ if sales_data:
406
+ results.append({"collection": "Sales", "data": sales_data})
407
+ else:
408
+ results.append({"collection": "Sales", "message": "No sales data found."})
409
+
410
+ # If no specific collection keywords, maybe return summary or ask for clarification
411
+ if not queried_collections:
412
+ # Could try fetching both, or just return a message
413
+ return "Please specify if you want to query 'inventory' or 'sales' data."
414
+
415
+
416
+ # Format the results for display
417
+ if not results:
418
+ return "No data found matching your query."
419
+
420
+ # Use format_transaction_response to display the results
421
+ formatted_output = f"Query Results for '{query}':\n\n"
422
+ for res_block in results:
423
+ formatted_output += f"--- {res_block.get('collection', 'Data')} ---\n"
424
+ if "data" in res_block:
425
+ # Pass the list of dictionaries to the formatter
426
+ formatted_output += format_transaction_response(res_block["data"]) + "\n"
427
+ elif "message" in res_block:
428
+ formatted_output += res_block["message"] + "\n"
429
+ formatted_output += "\n"
430
+
431
+ return formatted_output.strip()
432
+
433
+
434
  except Exception as e:
435
+ logger.error(f"Data query failed for user {user_phone}, query '{query}': {e}", exc_info=True)
436
+ return "Sorry, I encountered an error while retrieving your data."
437
+
438
+
439
+ def update_transaction(user_phone: str, transaction_data: List[Dict]) -> tuple[bool, str]:
440
+ """Update existing transaction(s) based on provided data."""
441
+ # This needs refinement. How do we identify *which* transaction to update?
442
+ # Assuming for now the primary identifier is 'item' within the 'details'.
443
+ # And we update based on the first transaction in the list if multiple are provided for update.
444
+ if not transaction_data:
445
+ return False, "No update data provided."
446
+
447
+ update_info = transaction_data[0] # Process the first update instruction
448
+ details = update_info.get('details', {})
449
+ item_name = details.get('item')
450
+ trans_type = update_info.get('transaction_type', 'inventory').lower() # Default to inventory
451
+
452
+ if not item_name:
453
+ return False, "Cannot update: 'item' name not specified in details."
454
+
455
+ collection_map = {'purchase': 'inventory', 'sale': 'sales', 'inventory': 'inventory'}
456
+ collection_name = collection_map.get(trans_type, 'inventory')
457
+ doc_ref = db.collection("users").document(user_phone).collection(collection_name).document(item_name)
458
+
459
  try:
460
+ doc_snapshot = doc_ref.get()
461
+ if not doc_snapshot.exists:
462
+ return False, f"Cannot update: Item '{item_name}' not found in {collection_name}."
463
+
464
+ # Prepare updates, excluding the item identifier itself
465
+ updates = {f"details.{k}": v for k, v in details.items() if k != 'item'}
466
+ if not updates:
467
+ return False, "No update fields provided (only item name was given)."
468
+
469
+ updates['last_updated'] = datetime.now().isoformat()
470
+
471
+ doc_ref.update(updates)
472
+ logger.info(f"Successfully updated item '{item_name}' in {collection_name} for user {user_phone}.")
473
+ # Format the *intended* update for confirmation message
474
+ formatted_update = format_transaction_response([update_info])
475
+ return True, f"Successfully updated '{item_name}'.\n\n{formatted_update}"
476
+
477
  except Exception as e:
478
+ logger.error(f"Update failed for item '{item_name}' in {collection_name}, user {user_phone}: {e}", exc_info=True)
479
+ return False, f"Failed to update '{item_name}'. An error occurred."
480
+
481
 
482
+ def delete_transaction(user_phone: str, transaction_data: List[Dict]) -> tuple[bool, str]:
483
+ """Delete specified transactions (identified by item name)."""
484
  batch = db.batch()
485
+ collection_map = {'purchase': 'inventory', 'sale': 'sales', 'inventory': 'inventory'}
486
+ deleted_items = []
487
+ errors = []
488
+ processed = False
489
+
 
490
  for transaction in transaction_data:
491
+ details = transaction.get('details', {})
492
+ item_name = details.get('item')
493
+ trans_type = transaction.get('transaction_type', '').lower() # Get type if specified
494
+
495
  if not item_name:
496
+ errors.append("Skipped delete: 'item' name missing.")
497
  continue
498
+
499
+ # Determine collection: Use specified type, default to inventory if ambiguous/missing
500
+ collection_name = collection_map.get(trans_type, 'inventory')
501
+ # If type is 'sale', we might need a different identifier than item_name (e.g., sale ID)
502
+ # For simplicity now, we assume deleting from inventory or sales based on item name if type is sale
503
+ # This might need adjustment based on how sales are uniquely identified.
504
+ # If deleting sales, you likely need a sale ID, not just item name.
505
+ # Let's assume for now delete primarily targets inventory items by name.
506
+ if trans_type == 'sale':
507
+ errors.append(f"Deletion of specific sales by item name ('{item_name}') is not fully supported. Please specify a Sale ID or delete from inventory.")
508
+ continue # Skip deletion of sales by item name for now
509
+
510
+
511
+ doc_ref = db.collection("users").document(user_phone).collection(collection_name).document(item_name)
512
+ # Check if doc exists before adding delete to batch (optional but good practice)
513
+ # doc_snapshot = doc_ref.get()
514
+ # if doc_snapshot.exists:
515
  batch.delete(doc_ref)
516
+ deleted_items.append(f"'{item_name}' from {collection_name}")
517
+ processed = True
518
+ # else:
519
+ # errors.append(f"Item '{item_name}' not found in {collection_name}, cannot delete.")
520
+
521
+
522
+ if not processed and not errors:
523
+ return False, "No valid items found to delete."
524
+
525
  try:
526
+ if processed: # Only commit if there are items to delete
527
+ batch.commit()
528
+ logger.info(f"Deletion batch committed for user {user_phone}. Items: {deleted_items}")
529
+
530
+ final_message = ""
531
+ if deleted_items:
532
+ final_message += f"Successfully deleted: {', '.join(deleted_items)}."
533
+ if errors:
534
+ final_message += f"\nErrors/Skipped: {'; '.join(errors)}"
535
+
536
+ return processed, final_message.strip() # Return True if at least one delete was attempted
537
+
538
  except Exception as e:
539
+ logger.error(f"Deletion batch commit failed for user {user_phone}: {e}", exc_info=True)
540
+ errors.append("An error occurred during the delete operation.")
541
+ return False, "\n".join(errors)
542
+
543
 
544
  def persist_temporary_transaction(transactions: List[Dict], mobile: str) -> bool:
545
+ """Store transactions temporarily in Firestore before confirmation."""
546
+ if not transactions:
547
+ logger.warning(f"Attempted to persist empty transaction list for {mobile}")
548
+ return False
549
  try:
550
  doc_ref = db.collection("users").document(mobile).collection("temp_transactions").document("pending")
551
  doc_ref.set({
552
+ "transactions": transactions, # Store the list of transactions
553
  "timestamp": datetime.now().isoformat(),
554
  "status": "pending_confirmation"
555
  })
556
+ logger.info(f"Temporary transaction persisted for user {mobile}.")
557
  return True
558
  except Exception as e:
559
+ logger.error(f"Failed to persist temporary transaction for user {mobile}: {e}", exc_info=True)
560
  return False
561
 
562
+ # Regex to check for common currency symbols at the start of a string
563
+ CURRENCY_SYMBOL_REGEX = re.compile(r"^\s*[\$\£\€\¥\₹]") # Add more symbols as needed (e.g., ZAR R)
564
+
565
+ def format_transaction_response(transactions: Union[List[Dict], Dict, None]) -> str:
566
+ """Format transaction data for user display, fixing double currency symbols."""
567
  if not transactions:
568
+ return "No transaction data to display."
569
+
570
  if isinstance(transactions, dict):
571
+ transactions = [transactions] # Wrap single dict in a list
572
+
573
+ if not isinstance(transactions, list) or not transactions:
574
+ return "Invalid transaction data format."
575
+
576
+ output_lines = []
577
+ for idx, trans in enumerate(transactions):
578
+ if not isinstance(trans, dict):
579
+ logger.warning(f"Skipping non-dictionary item in format_transaction_response: {trans}")
580
+ continue
581
+
582
+ # Use a more descriptive title if possible
583
+ trans_desc = trans.get('transaction_type', 'Unknown Type').replace("_", " ").title()
584
+ intent_desc = trans.get('intent', 'Unknown Intent').title()
585
+ title = f"{intent_desc}: {trans_desc}"
586
+ if len(transactions) > 1:
587
+ output_lines.append(f"--- Transaction {idx + 1}: {title} ---")
588
+ else:
589
+ output_lines.append(f"--- {title} ---")
590
+
591
+ details = trans.get('details')
592
+ if isinstance(details, dict):
593
+ # Try to display key info first: item, quantity, price/amount
594
+ order = ['item', 'quantity', 'price', 'amount', 'cost', 'customer', 'date']
595
+ displayed_keys = set()
596
+
597
+ for key in order:
598
+ if key in details:
599
+ value = details[key]
600
+ key_title = key.replace("_", " ").title()
601
+ currency = details.get('currency', '$') # Default currency
602
+ value_str = str(value)
603
 
604
+ # Check for currency fields and apply symbol *only if not present*
605
+ if 'price' in key or 'amount' in key or 'cost' in key:
606
+ if not CURRENCY_SYMBOL_REGEX.match(value_str):
607
+ output_lines.append(f"• {key_title}: {currency}{value_str}")
608
+ else:
609
+ output_lines.append(f"• {key_title}: {value_str}") # Already has symbol
610
+ else:
611
+ output_lines.append(f"• {key_title}: {value_str}")
612
+ displayed_keys.add(key)
613
+
614
+ # Display remaining details
615
+ for key, value in details.items():
616
+ if key not in displayed_keys and key != 'currency': # Don't display currency key itself
617
+ key_title = key.replace("_", " ").title()
618
+ output_lines.append(f"• {key_title}: {value}")
619
+ elif details: # If details is not a dict but exists
620
+ output_lines.append(f"• Details: {details}")
621
+
622
+
623
+ # Add timestamp if available
624
+ ts = trans.get('timestamp') or trans.get('created_at') or trans.get('last_updated')
625
+ if ts:
626
+ try:
627
+ # Attempt to parse and format timestamp nicely
628
+ dt_obj = datetime.fromisoformat(ts)
629
+ output_lines.append(f"• Recorded: {dt_obj.strftime('%Y-%m-%d %H:%M:%S')}")
630
+ except ValueError:
631
+ output_lines.append(f"• Recorded: {ts}") # Fallback to raw string
632
+
633
+ output_lines.append("") # Add spacing between transactions
634
+
635
+ return "\n".join(output_lines).strip()
636
+
637
+
638
+ def fetch_transaction(user_phone: str,
639
  item_name: Optional[str] = None,
640
+ collection: str = "inventory") -> Union[Dict, List[Dict], None]:
641
+ """Retrieve transaction(s) from Firestore."""
642
+ # Ensure collection is valid
643
+ valid_collections = ["inventory", "sales"] # Add others if needed
644
+ if collection not in valid_collections:
645
+ logger.warning(f"Invalid collection specified for fetch: {collection}")
646
+ collection = "inventory" # Default to inventory
647
+
648
  col_ref = db.collection("users").document(user_phone).collection(collection)
649
+
650
+ try:
651
+ if item_name:
652
+ # Fetch a specific document by ID (item_name)
653
+ doc_ref = col_ref.document(item_name)
654
+ doc_snapshot = doc_ref.get()
655
+ if doc_snapshot.exists:
656
+ return doc_snapshot.to_dict()
657
+ else:
658
+ logger.info(f"Item '{item_name}' not found in {collection} for user {user_phone}.")
659
+ return None
660
+ else:
661
+ # Fetch all documents in the collection
662
+ docs = col_ref.stream()
663
+ results = [doc.to_dict() for doc in docs]
664
+ logger.info(f"Fetched {len(results)} items from {collection} for user {user_phone}.")
665
+ return results if results else [] # Return empty list if collection is empty
666
+
667
+ except Exception as e:
668
+ logger.error(f"Error fetching from Firestore ({collection}, item: {item_name}) for user {user_phone}: {e}", exc_info=True)
669
+ return None # Return None on error
670
+
671
 
672
  def process_intent(parsed_trans_data: List[Dict], mobile: str) -> str:
673
+ """Route transactions to appropriate CRUD operation based on parsed intent."""
674
  if not parsed_trans_data:
675
+ logger.warning(f"process_intent called with empty data for user {mobile}")
676
+ return "I couldn't understand the transaction details. Could you please try again?"
677
+
678
+ # Note: This function now handles only ONE intent type per call, determined by the FIRST transaction.
679
+ # The CRUD functions themselves handle lists if multiple items share the same intent/type.
680
+ # If the LLM returns mixed intents (e.g., add inventory AND check stock), this needs more complex handling.
681
+ # For now, we process based on the first transaction's intent.
682
+
683
+ first_transaction = parsed_trans_data[0]
684
+ intent = first_transaction.get('intent', '').lower()
685
+ trans_type = first_transaction.get('transaction_type', '').lower()
686
+
687
+ # Generate a summary of *all* transactions received for context, even if only processing the first intent type.
688
  transaction_summary = format_transaction_response(parsed_trans_data)
689
+ logger.info(f"Processing intent '{intent}' for type '{trans_type}' for user {mobile}")
690
+
691
+ response_message = ""
692
+ success = False
693
+
694
+ try:
695
+ if intent == 'create':
696
+ if trans_type in ('purchase', 'inventory'):
697
+ # Pass all transactions that are inventory creation
698
+ inventory_creates = [t for t in parsed_trans_data if t.get('intent','').lower() == 'create' and t.get('transaction_type','').lower() in ('purchase', 'inventory')]
699
+ if inventory_creates:
700
+ success = create_inventory(mobile, inventory_creates)
701
+ response_message = "Inventory updated!" if success else "Failed to update inventory."
702
+ else:
703
+ response_message = "No valid inventory creation details found."
704
+
705
+ elif trans_type == 'sale':
706
+ # Pass all transactions that are sales
707
+ sales_creates = [t for t in parsed_trans_data if t.get('intent','').lower() == 'create' and t.get('transaction_type','').lower() == 'sale']
708
+ if sales_creates:
709
+ success, feedback = create_sale(mobile, sales_creates)
710
+ response_message = f"Sale processing results:\n{feedback}" # Use detailed feedback
711
+ else:
712
+ response_message = "No valid sale creation details found."
713
+ else:
714
+ response_message = f"Sorry, I can't 'create' transactions of type: {trans_type}"
715
+
716
+ elif intent == 'read' or trans_type == 'query':
717
+ # Use the original user query text if possible, otherwise generate a query string
718
+ # For simplicity, let's just use the formatted details as a pseudo-query
719
+ query_details = first_transaction.get('details', {})
720
+ query_str = " ".join([f"{k}:{v}" for k,v in query_details.items()]) if query_details else "Show summary" # Simple query string
721
+ logger.info(f"Performing read/query for user {mobile}: {query_str}")
722
+ # Call the simplified read_datalake
723
+ read_result = read_datalake(mobile, query_str) # Pass the generated query string
724
+ # Check if read_result is an image path (if you re-introduce image reports)
725
+ # For now, assume it returns text
726
+ response_message = str(read_result)
727
+ # No separate summary needed as the result is the response
728
+ return response_message # Return directly, don't add summary again
729
+
730
+ elif intent == 'update':
731
+ # Pass all update transactions (assuming they target different items or update sequentially)
732
+ updates = [t for t in parsed_trans_data if t.get('intent','').lower() == 'update']
733
+ if updates:
734
+ # Update logic currently processes one item at a time, needs enhancement for batch updates
735
+ # For now, process the first update instruction found
736
+ success, feedback = update_transaction(mobile, updates) # Pass the list, function handles first
737
+ response_message = feedback # Use feedback from function
738
+ else:
739
+ response_message = "No valid update details found."
740
+
741
+
742
+ elif intent == 'delete':
743
+ # Pass all delete transactions
744
+ deletes = [t for t in parsed_trans_data if t.get('intent','').lower() == 'delete']
745
+ if deletes:
746
+ success, feedback = delete_transaction(mobile, deletes)
747
+ response_message = feedback # Use feedback from function
748
+ else:
749
+ response_message = "No valid deletion details found."
750
 
 
 
 
 
 
 
 
 
 
751
  else:
752
+ response_message = f"Sorry, I don't know how to handle the intent: '{intent}'"
753
+
754
+ # Combine the action result message with the summary of what was processed
755
+ # Avoid adding summary if the response message already contains it (like read_datalake)
756
+ if intent != 'read' and trans_type != 'query':
757
+ full_response = f"{response_message}\n\nSummary of processed data:\n{transaction_summary}"
 
758
  else:
759
+ full_response = response_message # Read intent already formatted output
760
+
761
+ return full_response.strip()
762
+
763
+ except Exception as e:
764
+ logger.error(f"Error processing intent '{intent}' for user {mobile}: {e}", exc_info=True)
765
+ return f"Sorry, an error occurred while processing your request for '{intent}'.\n\nDetails received:\n{transaction_summary}"