Update utility.py
Browse files- 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 |
-
|
|
|
|
| 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 |
-
|
| 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
|
| 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:
|
| 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 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 1070 |
|
| 1071 |
-
# --- Tier
|
| 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
|
| 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.
|
|
|
|
| 1121 |
**IMPORTANT RULES:**
|
| 1122 |
-
1. **
|
| 1123 |
-
2. **
|
| 1124 |
-
3. **
|
| 1125 |
-
4. **
|
| 1126 |
-
|
| 1127 |
-
**
|
| 1128 |
{snapshot_str}
|
|
|
|
| 1129 |
**User's Question:**
|
| 1130 |
"{query}"
|
| 1131 |
"""
|
| 1132 |
response = llm.invoke(synthesis_prompt)
|
| 1133 |
-
return response
|
| 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
|
| 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.
|
|
|
|
| 1177 |
**IMPORTANT RULES:**
|
| 1178 |
-
1. **
|
| 1179 |
-
2. **
|
| 1180 |
-
3. **
|
| 1181 |
-
4. **
|
| 1182 |
-
|
| 1183 |
-
**
|
| 1184 |
{snapshot_str}
|
|
|
|
| 1185 |
**User's Question:**
|
| 1186 |
"{query}"
|
| 1187 |
"""
|
| 1188 |
response = llm.invoke(synthesis_prompt)
|
| 1189 |
-
return response
|
| 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)
|