rairo commited on
Commit
c0ef8be
·
verified ·
1 Parent(s): 3bcfa81

Update utility.py

Browse files
Files changed (1) hide show
  1. utility.py +68 -76
utility.py CHANGED
@@ -391,7 +391,8 @@ class ReportEngine:
391
  def generate_report(self) -> str:
392
  """Generates a simple Sales or Expenses report."""
393
  subject = "sales"
394
- if "expense" in self.query:
 
395
  subject = "expenses"
396
 
397
  target_df = self.dfs.get(subject, pd.DataFrame())
@@ -735,7 +736,6 @@ def create_or_update_inventory_or_service_offering(user_phone: str, transaction_
735
  continue
736
  canonical_info = _get_canonical_info(user_phone, item_name)
737
  canonical_name = canonical_info['name']
738
- canonical_name = canonical_name.replace("/", "-")
739
  if 'item' in details: details['item'] = canonical_name
740
  if 'service_name' in details: details['service_name'] = canonical_name
741
  try:
@@ -1006,9 +1006,16 @@ def _get_relative_date_context() -> str:
1006
  return "\n".join(context)
1007
 
1008
  def read_datalake(user_phone: str, query: str) -> str:
1009
- """
1010
- Implements the final Unified Strategy for robust, intelligent data analysis.
1011
- """
 
 
 
 
 
 
 
1012
  try:
1013
  all_dfs_with_names = _fetch_all_collections_as_dfs(user_phone)
1014
  if not all_dfs_with_names:
@@ -1021,13 +1028,11 @@ def read_datalake(user_phone: str, query: str) -> str:
1021
 
1022
  # --- REFACTORED ROUTING LOGIC ---
1023
 
1024
- # --- Tier 0: Simple Direct Lookups (Unchanged) ---
1025
  simple_lookup_map = {
1026
  "inventory": ["stock", "inventory", "in stock", "what do i have"],
1027
  "assets": ["asset", "assets", "my assets"],
1028
  "liabilities": ["liabilities", "i owe", "creditor", "my debts"],
1029
- "sales": ["show my sales", "list sales"],
1030
- "expenses": ["show my expenses", "list expenses"]
1031
  }
1032
  for df_name, keywords in simple_lookup_map.items():
1033
  if any(keyword in query_lower for keyword in keywords):
@@ -1037,58 +1042,53 @@ def read_datalake(user_phone: str, query: str) -> str:
1037
  return render_df_as_image(target_df_tuple[1])
1038
  return f"You don't have any {df_name} recorded yet."
1039
 
1040
- # --- Tier 1: Specific, Pre-canned Reports ---
1041
- item_report_match = re.search(r"(?:sales report for|report on|performance of)\s+([\w\s]+?)(?:\s+(?:this|last|on|in|for|today|yesterday)|$)", query_lower)
1042
  report_json = None
1043
 
1044
- if item_report_match:
1045
- item_name = item_report_match.group(1).strip()
1046
- if item_name not in ["sales", "expenses", "profit"]:
1047
- logger.info(f"Handling '{query}' with the Item Report Path for item: '{item_name}'.")
1048
- report_json = engine.generate_item_report(item_name)
 
 
 
 
 
1049
  elif "profit" in query_lower:
1050
  logger.info(f"Handling '{query}' with the Profit Report Path.")
1051
  report_json = engine.generate_profit_report()
1052
  elif any(k in query_lower for k in ["best day", "busiest day", "sales by day"]):
1053
  logger.info(f"Handling '{query}' with the Day of Week Report Path.")
1054
  report_json = engine.generate_day_of_week_report()
 
 
 
 
 
 
 
1055
 
1056
  if report_json:
1057
  report_data = json.loads(report_json)
1058
  if "error" in report_data: return report_data["error"]
1059
  synthesis_prompt = f"""
1060
  Directly synthesize a professional business report from the following JSON data. Omit conversational introductions or summaries. Present only the data-driven report, formatted for WhatsApp (*bold*, _italic_, emojis).
 
 
1061
  **IMPORTANT INSTRUCTIONS:**
1062
- - If `report_subject` is "Profitability", present a clear financial summary: start with Revenue, subtract COGS for Gross Profit, then subtract Expenses for Net Profit. Also mention other KPIs.
1063
  - If `report_subject` is "Item Report", state the item name and present its performance KPIs.
1064
  - If `report_subject` is "Day of Week Analysis", state the best day and list daily sales.
 
1065
  Here is the data summary:
1066
  {report_json}
1067
  """
1068
  response = llm.invoke(synthesis_prompt)
1069
- return response.content
1070
 
1071
- # --- Tier 1.5: General Temporal Reports ---
1072
- subjects = ["sales", "expenses"]
1073
- temporals = ["today", "yesterday", "week", "month", "year"]
1074
- if any(sub in query_lower for sub in subjects) and any(temp in query_lower for temp in temporals):
1075
- logger.info(f"Handling '{query}' with the General Temporal Report Path.")
1076
- report_json = engine.generate_report()
1077
- report_data = json.loads(report_json)
1078
- if "error" in report_data: return report_data["error"]
1079
- synthesis_prompt = f"""Synthesize a professional business report from the following JSON data. Omit conversational introductions or summaries. For sales reports, you MUST provide a creative and actionable "Insight" section at the end based on the best/worst selling items. Present only the data-driven report and the insight, formatted for WhatsApp (*bold*, _italic_, emojis).
1080
- Data: {report_json}"""
1081
- response = llm.invoke(synthesis_prompt)
1082
- return response.content
1083
-
1084
- # --- Tier 2: Predictive & Generic Summary Fallback ---
1085
  predictive_keywords = ["expect", "forecast", "predict"]
1086
- # FIX: Use specific phrases, not single keywords, to trigger summary reports.
1087
- historical_report_keywords = [
1088
- "sales report", "expense report", "performance summary",
1089
- "how did i do", "how did we do", "sales summary", "sales performance"
1090
- ]
1091
-
1092
  if any(keyword in query_lower for keyword in predictive_keywords):
1093
  logger.info(f"Handling '{query}' with the Forecasting Path.")
1094
  forecast_json = engine.generate_forecast_data()
@@ -1096,17 +1096,7 @@ def read_datalake(user_phone: str, query: str) -> str:
1096
  if "error" in forecast_data: return forecast_data["error"]
1097
  synthesis_prompt = f"Synthesize a sales forecast from the following JSON data. Omit conversational introductions or summaries. Present only the forecast. Data: {forecast_json}"
1098
  response = llm.invoke(synthesis_prompt)
1099
- return response.content
1100
-
1101
- elif any(keyword in query_lower for keyword in historical_report_keywords):
1102
- logger.info(f"Handling '{query}' with the General Reporting Path (Sales/Expense).")
1103
- report_json = engine.generate_report()
1104
- report_data = json.loads(report_json)
1105
- if "error" in report_data: return report_data["error"]
1106
- synthesis_prompt = f"""Synthesize a professional business report from the following JSON data. Omit conversational introductions or summaries. For sales reports, you MUST provide a creative and actionable "Insight" section at the end based on the best/worst selling items. Present only the data-driven report and the insight, formatted for WhatsApp (*bold*, _italic_, emojis).
1107
- Data: {report_json}"""
1108
- response = llm.invoke(synthesis_prompt)
1109
- return response.content
1110
 
1111
  # --- Tier 3: Business Coach & Help Layer ---
1112
  help_keywords = ['help', 'tutorial', 'guide', 'how do you work', 'what can you do', 'how can', 'how would']
@@ -1117,20 +1107,22 @@ def read_datalake(user_phone: str, query: str) -> str:
1117
  snapshot_str = json.dumps(snapshot, indent=2)
1118
 
1119
  synthesis_prompt = f"""
1120
- You are Qx, a friendly and insightful business coach and financial expert. The user is asking a general question. Do NOT perform any calculations. Your task is to provide a clear, helpful, and strategic answer based on their question, using your general knowledge and the business snapshot provided below for context.
 
1121
  **IMPORTANT RULES:**
1122
- 1. **Use the Context:** Use the Business Snapshot as your internal knowledge to make your advice relevant and personalized.
1123
- 2. **Do NOT State the Numbers:** Do NOT repeat the numbers or lists from the snapshot directly. Synthesize them into your advice.
1124
- 3. **Stay in Character:** Act as a coach. Be encouraging and provide actionable advice.
1125
- 4. **Handle 'Help' Queries:** If asked about your capabilities, explain that you can record transactions, generate reports, answer data questions, and provide business advice.
1126
- 5. **Format for WhatsApp:** Use *bold*, _italic_, and emojis to make your response clear and engaging.
1127
- **BUSINESS SNAPSHOT (INTERNAL CONTEXT ONLY):**
1128
  {snapshot_str}
 
1129
  **User's Question:**
1130
  "{query}"
1131
  """
1132
  response = llm.invoke(synthesis_prompt)
1133
- return response.content
1134
 
1135
  # --- Tier 4: Fortified PandasAI with Graceful Fallback (Final Path) ---
1136
  else:
@@ -1167,36 +1159,36 @@ def read_datalake(user_phone: str, query: str) -> str:
1167
  else:
1168
  return str(response)
1169
 
1170
- except NoCodeFoundError:
1171
- logger.info(f"PandasAI found no code for '{query}'. Routing to Business Coach as fallback.")
 
 
 
 
 
 
 
 
1172
  snapshot = engine.generate_business_snapshot()
1173
  snapshot_str = json.dumps(snapshot, indent=2)
1174
 
1175
  synthesis_prompt = f"""
1176
- You are Qx, a friendly and insightful business coach and financial expert. The user is asking a general question. Do NOT perform any calculations. Your task is to provide a clear, helpful, and strategic answer based on their question, using your general knowledge and the business snapshot provided below for context.
 
1177
  **IMPORTANT RULES:**
1178
- 1. **Use the Context:** Use the Business Snapshot as your internal knowledge to make your advice relevant and personalized.
1179
- 2. **Do NOT State the Numbers:** Do NOT repeat the numbers or lists from the snapshot directly. Synthesize them into your advice.
1180
- 3. **Stay in Character:** Act as a coach. Be encouraging and provide actionable advice.
1181
- 4. **Handle 'Help' Queries:** If asked about your capabilities, explain that you can record transactions, generate reports, answer data questions, and provide business advice.
1182
- 5. **Format for WhatsApp:** Use *bold*, _italic_, and emojis to make your response clear and engaging.
1183
- **BUSINESS SNAPSHOT (INTERNAL CONTEXT ONLY):**
1184
  {snapshot_str}
 
1185
  **User's Question:**
1186
  "{query}"
1187
  """
1188
  response = llm.invoke(synthesis_prompt)
1189
- return response.content
1190
-
1191
- except Exception as e:
1192
- logger.warning(f"PandasAI failed for query '{query}' with error: {e}. Falling back to general report engine.")
1193
- report_json = engine.generate_report()
1194
- report_data = json.loads(report_json)
1195
- if "error" in report_data: return report_data["error"]
1196
- synthesis_prompt = f"""I could not answer the specific question: '{query}'. Instead, here is a general summary for the period. Synthesize a report from the following JSON data. Omit any other conversational text.
1197
- Data: {report_json}"""
1198
- response = llm.invoke(synthesis_prompt)
1199
- return response.content
1200
 
1201
  except Exception as e:
1202
  logger.error(f"Data query failed for user {user_phone}, query '{query}': {e}", exc_info=True)
 
391
  def generate_report(self) -> str:
392
  """Generates a simple Sales or Expenses report."""
393
  subject = "sales"
394
+ expense_triggers = ["expense report", "expense summary", "expenses"]
395
+ if any(keyword in self.query for keyword in expense_triggers):
396
  subject = "expenses"
397
 
398
  target_df = self.dfs.get(subject, pd.DataFrame())
 
736
  continue
737
  canonical_info = _get_canonical_info(user_phone, item_name)
738
  canonical_name = canonical_info['name']
 
739
  if 'item' in details: details['item'] = canonical_name
740
  if 'service_name' in details: details['service_name'] = canonical_name
741
  try:
 
1006
  return "\n".join(context)
1007
 
1008
  def read_datalake(user_phone: str, query: str) -> str:
1009
+ """Implements the final Unified Strategy for robust, intelligent data analysis."""
1010
+ def _to_text(resp) -> str:
1011
+ try:
1012
+ if resp is None: return ""
1013
+ if hasattr(resp, "content") and resp.content is not None: return str(resp.content)
1014
+ if hasattr(resp, "text") and resp.text is not None: return str(resp.text)
1015
+ if isinstance(resp, (list, tuple)): return "".join(_to_text(r) for r in resp)
1016
+ return str(resp)
1017
+ except Exception: return str(resp)
1018
+
1019
  try:
1020
  all_dfs_with_names = _fetch_all_collections_as_dfs(user_phone)
1021
  if not all_dfs_with_names:
 
1028
 
1029
  # --- REFACTORED ROUTING LOGIC ---
1030
 
1031
+ # --- Tier 0: Simple Direct Lookups ---
1032
  simple_lookup_map = {
1033
  "inventory": ["stock", "inventory", "in stock", "what do i have"],
1034
  "assets": ["asset", "assets", "my assets"],
1035
  "liabilities": ["liabilities", "i owe", "creditor", "my debts"],
 
 
1036
  }
1037
  for df_name, keywords in simple_lookup_map.items():
1038
  if any(keyword in query_lower for keyword in keywords):
 
1042
  return render_df_as_image(target_df_tuple[1])
1043
  return f"You don't have any {df_name} recorded yet."
1044
 
1045
+ # --- Tier 1: Canned & Temporal Reports (NEW UNIFIED LOGIC) ---
 
1046
  report_json = None
1047
 
1048
+ # --- FIX --- Use precise, multi-word triggers and place this block first.
1049
+ sales_report_triggers = ["sales report", "sales summary", "sales performance", "how were my sales", "revenue report"]
1050
+ expense_report_triggers = ["expense report", "expense summary", "expense performance", "how were my expenses", "spending report"]
1051
+
1052
+ if any(trigger in query_lower for trigger in sales_report_triggers):
1053
+ logger.info(f"Handling '{query}' with the General Sales Report Path.")
1054
+ report_json = engine.generate_report()
1055
+ elif any(trigger in query_lower for trigger in expense_report_triggers):
1056
+ logger.info(f"Handling '{query}' with the General Expense Report Path.")
1057
+ report_json = engine.generate_report()
1058
  elif "profit" in query_lower:
1059
  logger.info(f"Handling '{query}' with the Profit Report Path.")
1060
  report_json = engine.generate_profit_report()
1061
  elif any(k in query_lower for k in ["best day", "busiest day", "sales by day"]):
1062
  logger.info(f"Handling '{query}' with the Day of Week Report Path.")
1063
  report_json = engine.generate_day_of_week_report()
1064
+ else:
1065
+ item_report_match = re.search(r"(?:report on|performance of)\s+([\w\s]+?)(?:\s+(?:this|last|on|in|for|today|yesterday)|$)", query_lower)
1066
+ if item_report_match:
1067
+ item_name = item_report_match.group(1).strip()
1068
+ if item_name not in ["sales", "expenses", "profit"]:
1069
+ logger.info(f"Handling '{query}' with the Item Report Path for item: '{item_name}'.")
1070
+ report_json = engine.generate_item_report(item_name)
1071
 
1072
  if report_json:
1073
  report_data = json.loads(report_json)
1074
  if "error" in report_data: return report_data["error"]
1075
  synthesis_prompt = f"""
1076
  Directly synthesize a professional business report from the following JSON data. Omit conversational introductions or summaries. Present only the data-driven report, formatted for WhatsApp (*bold*, _italic_, emojis).
1077
+ For sales reports, if helpful, provide a creative and actionable "Insight" section at the end based on the best/worst selling items.
1078
+
1079
  **IMPORTANT INSTRUCTIONS:**
1080
+ - If `report_subject` is "Profitability", present a clear financial summary.
1081
  - If `report_subject` is "Item Report", state the item name and present its performance KPIs.
1082
  - If `report_subject` is "Day of Week Analysis", state the best day and list daily sales.
1083
+
1084
  Here is the data summary:
1085
  {report_json}
1086
  """
1087
  response = llm.invoke(synthesis_prompt)
1088
+ return _to_text(response)
1089
 
1090
+ # --- Tier 2: Predictive Queries ---
 
 
 
 
 
 
 
 
 
 
 
 
 
1091
  predictive_keywords = ["expect", "forecast", "predict"]
 
 
 
 
 
 
1092
  if any(keyword in query_lower for keyword in predictive_keywords):
1093
  logger.info(f"Handling '{query}' with the Forecasting Path.")
1094
  forecast_json = engine.generate_forecast_data()
 
1096
  if "error" in forecast_data: return forecast_data["error"]
1097
  synthesis_prompt = f"Synthesize a sales forecast from the following JSON data. Omit conversational introductions or summaries. Present only the forecast. Data: {forecast_json}"
1098
  response = llm.invoke(synthesis_prompt)
1099
+ return _to_text(response)
 
 
 
 
 
 
 
 
 
 
1100
 
1101
  # --- Tier 3: Business Coach & Help Layer ---
1102
  help_keywords = ['help', 'tutorial', 'guide', 'how do you work', 'what can you do', 'how can', 'how would']
 
1107
  snapshot_str = json.dumps(snapshot, indent=2)
1108
 
1109
  synthesis_prompt = f"""
1110
+ You are Qx, a friendly and insightful business coach and financial expert. Your task is to provide a clear, helpful, and strategic answer based on the user's question, using your general business knowledge combined with the business snapshot provided below for context.
1111
+
1112
  **IMPORTANT RULES:**
1113
+ 1. **Synthesize, Don't Just Report:** Use the Business Snapshot to make your advice relevant and personalized. For example, if inventory is high for an item, you might suggest a promotion. If profit is low, you might suggest cost-cutting measures.
1114
+ 2. **Act as a Coach:** Be encouraging and provide actionable advice.
1115
+ 3. **Handle 'Help' Queries:** If asked about your capabilities, explain that you can record transactions (sales, expenses, etc.) via text or images, generate detailed reports (profit, sales by item), answer questions about their data, and provide business advice.
1116
+ 4. **Format for WhatsApp:** Use *bold*, _italic_, and emojis to make your response clear and engaging.
1117
+
1118
+ **Business Snapshot for Context:**
1119
  {snapshot_str}
1120
+
1121
  **User's Question:**
1122
  "{query}"
1123
  """
1124
  response = llm.invoke(synthesis_prompt)
1125
+ return _to_text(response)
1126
 
1127
  # --- Tier 4: Fortified PandasAI with Graceful Fallback (Final Path) ---
1128
  else:
 
1159
  else:
1160
  return str(response)
1161
 
1162
+ except Exception as e:
1163
+ # --- FIX --- Robust error checking and sanitized fallback to Business Coach.
1164
+ err_name = e.__class__.__name__
1165
+ is_no_code_error = "NoCodeFoundError" in err_name or "No code found" in str(e)
1166
+
1167
+ if is_no_code_error:
1168
+ logger.info(f"PandasAI found no code for '{query}'. Routing to Business Coach as fallback.")
1169
+ else:
1170
+ logger.warning(f"PandasAI failed for query '{query}' with error: {e}. Falling back to Business Coach.")
1171
+
1172
  snapshot = engine.generate_business_snapshot()
1173
  snapshot_str = json.dumps(snapshot, indent=2)
1174
 
1175
  synthesis_prompt = f"""
1176
+ You are Qx, a friendly and insightful business coach and financial expert. Your task is to provide a clear, helpful, and strategic answer based on the user's question, using your general business knowledge combined with the business snapshot provided below for context.
1177
+
1178
  **IMPORTANT RULES:**
1179
+ 1. **Synthesize, Don't Just Report:** Use the Business Snapshot to make your advice relevant and personalized. For example, if inventory is high for an item, you might suggest a promotion. If profit is low, you might suggest cost-cutting measures.
1180
+ 2. **Act as a Coach:** Be encouraging and provide actionable advice.
1181
+ 3. **Handle 'Help' Queries:** If asked about your capabilities, explain that you can record transactions (sales, expenses, etc.) via text or images, generate detailed reports (profit, sales by item), answer questions about their data, and provide business advice.
1182
+ 4. **Format for WhatsApp:** Use *bold*, _italic_, and emojis to make your response clear and engaging.
1183
+
1184
+ **Business Snapshot for Context:**
1185
  {snapshot_str}
1186
+
1187
  **User's Question:**
1188
  "{query}"
1189
  """
1190
  response = llm.invoke(synthesis_prompt)
1191
+ return _to_text(response)
 
 
 
 
 
 
 
 
 
 
1192
 
1193
  except Exception as e:
1194
  logger.error(f"Data query failed for user {user_phone}, query '{query}': {e}", exc_info=True)