Update utility.py
Browse files- utility.py +78 -3
utility.py
CHANGED
|
@@ -170,7 +170,6 @@ def _analyze_image_with_vision(image_bytes: bytes) -> List[Dict]:
|
|
| 170 |
try:
|
| 171 |
image_pil = Image.open(io.BytesIO(image_bytes))
|
| 172 |
|
| 173 |
-
# FIX 1.2, 1.3: Updated prompt with step-by-step counting instructions
|
| 174 |
prompt = """
|
| 175 |
You are a bookkeeping vision model. Analyze the image (receipt, invoice, handwritten note, *catalog/menu/price list*, product photo, shelf photo). Return ONLY a valid JSON array [] of transaction objects that our TEXT PIPELINE can consume directly.
|
| 176 |
|
|
@@ -551,6 +550,52 @@ class ReportEngine:
|
|
| 551 |
}
|
| 552 |
return json.dumps(self.results, indent=2)
|
| 553 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
def generateResponse(prompt: str) -> str:
|
| 555 |
"""Generate structured JSON response from user input using Generative AI."""
|
| 556 |
if not model:
|
|
@@ -981,6 +1026,9 @@ def read_datalake(user_phone: str, query: str) -> str:
|
|
| 981 |
try:
|
| 982 |
all_dfs_with_names = _fetch_all_collections_as_dfs(user_phone)
|
| 983 |
if not all_dfs_with_names:
|
|
|
|
|
|
|
|
|
|
| 984 |
return "You have no data recorded yet. Please add some transactions first."
|
| 985 |
|
| 986 |
query_lower = query.lower()
|
|
@@ -1037,7 +1085,7 @@ def read_datalake(user_phone: str, query: str) -> str:
|
|
| 1037 |
response = llm.invoke(synthesis_prompt)
|
| 1038 |
return response.content
|
| 1039 |
|
| 1040 |
-
# --- Tier 1.5: General Temporal Reports
|
| 1041 |
subjects = ["sales", "expenses"]
|
| 1042 |
temporals = ["today", "yesterday", "week", "month", "year"]
|
| 1043 |
if any(sub in query_lower for sub in subjects) and any(temp in query_lower for temp in temporals):
|
|
@@ -1073,7 +1121,34 @@ def read_datalake(user_phone: str, query: str) -> str:
|
|
| 1073 |
response = llm.invoke(synthesis_prompt)
|
| 1074 |
return response.content
|
| 1075 |
|
| 1076 |
-
# --- Tier 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
else:
|
| 1078 |
try:
|
| 1079 |
logger.info(f"Handling '{query}' with the Fortified PandasAI Path.")
|
|
|
|
| 170 |
try:
|
| 171 |
image_pil = Image.open(io.BytesIO(image_bytes))
|
| 172 |
|
|
|
|
| 173 |
prompt = """
|
| 174 |
You are a bookkeeping vision model. Analyze the image (receipt, invoice, handwritten note, *catalog/menu/price list*, product photo, shelf photo). Return ONLY a valid JSON array [] of transaction objects that our TEXT PIPELINE can consume directly.
|
| 175 |
|
|
|
|
| 550 |
}
|
| 551 |
return json.dumps(self.results, indent=2)
|
| 552 |
|
| 553 |
+
def generate_business_snapshot(self) -> Dict[str, Any]:
|
| 554 |
+
"""
|
| 555 |
+
NEW: Creates a high-level summary of the entire business for contextual AI coaching.
|
| 556 |
+
"""
|
| 557 |
+
snapshot = {}
|
| 558 |
+
# Financial KPIs
|
| 559 |
+
sales_df = self.dfs.get('sales', pd.DataFrame())
|
| 560 |
+
expenses_df = self.dfs.get('expenses', pd.DataFrame())
|
| 561 |
+
total_revenue = sales_df['sale_total'].sum() if not sales_df.empty else 0
|
| 562 |
+
total_expenses = expenses_df['amount'].sum() if not expenses_df.empty else 0
|
| 563 |
+
net_profit = total_revenue - total_expenses
|
| 564 |
+
snapshot['financial_kpis'] = {
|
| 565 |
+
"Total Revenue": f"${total_revenue:.2f}",
|
| 566 |
+
"Total Expenses": f"${total_expenses:.2f}",
|
| 567 |
+
"Net Profit": f"${net_profit:.2f}"
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
# Inventory Overview
|
| 571 |
+
inventory_df = self.dfs.get('inventory', pd.DataFrame())
|
| 572 |
+
if not inventory_df.empty and 'item' in inventory_df.columns and 'quantity' in inventory_df.columns:
|
| 573 |
+
snapshot['inventory_overview'] = "\n".join(
|
| 574 |
+
[f"- {row['item']} ({int(row['quantity'])} units)" for _, row in inventory_df.iterrows()]
|
| 575 |
+
)
|
| 576 |
+
else:
|
| 577 |
+
snapshot['inventory_overview'] = "No inventory items recorded."
|
| 578 |
+
|
| 579 |
+
# Asset Register
|
| 580 |
+
assets_df = self.dfs.get('assets', pd.DataFrame())
|
| 581 |
+
if not assets_df.empty and 'name' in assets_df.columns and 'value' in assets_df.columns:
|
| 582 |
+
snapshot['asset_register'] = "\n".join(
|
| 583 |
+
[f"- {row['name']} (${row['value']:.2f})" for _, row in assets_df.iterrows()]
|
| 584 |
+
)
|
| 585 |
+
else:
|
| 586 |
+
snapshot['asset_register'] = "No assets recorded."
|
| 587 |
+
|
| 588 |
+
# Liabilities Ledger
|
| 589 |
+
liabilities_df = self.dfs.get('liabilities', pd.DataFrame())
|
| 590 |
+
if not liabilities_df.empty and 'creditor' in liabilities_df.columns and 'amount' in liabilities_df.columns:
|
| 591 |
+
snapshot['liabilities_ledger'] = "\n".join(
|
| 592 |
+
[f"- {row['creditor']} (${row['amount']:.2f})" for _, row in liabilities_df.iterrows()]
|
| 593 |
+
)
|
| 594 |
+
else:
|
| 595 |
+
snapshot['liabilities_ledger'] = "No liabilities recorded."
|
| 596 |
+
|
| 597 |
+
return snapshot
|
| 598 |
+
|
| 599 |
def generateResponse(prompt: str) -> str:
|
| 600 |
"""Generate structured JSON response from user input using Generative AI."""
|
| 601 |
if not model:
|
|
|
|
| 1026 |
try:
|
| 1027 |
all_dfs_with_names = _fetch_all_collections_as_dfs(user_phone)
|
| 1028 |
if not all_dfs_with_names:
|
| 1029 |
+
# Handle no data case for the business coach
|
| 1030 |
+
if any(keyword in query.lower() for keyword in ['help', 'tutorial', 'guide', 'what is', 'explain']):
|
| 1031 |
+
return "I'm Qx, your business assistant! I can help you track sales, expenses, and inventory. Once you add some data, I can give you reports and insights. How can I help you get started?"
|
| 1032 |
return "You have no data recorded yet. Please add some transactions first."
|
| 1033 |
|
| 1034 |
query_lower = query.lower()
|
|
|
|
| 1085 |
response = llm.invoke(synthesis_prompt)
|
| 1086 |
return response.content
|
| 1087 |
|
| 1088 |
+
# --- Tier 1.5: General Temporal Reports ---
|
| 1089 |
subjects = ["sales", "expenses"]
|
| 1090 |
temporals = ["today", "yesterday", "week", "month", "year"]
|
| 1091 |
if any(sub in query_lower for sub in subjects) and any(temp in query_lower for temp in temporals):
|
|
|
|
| 1121 |
response = llm.invoke(synthesis_prompt)
|
| 1122 |
return response.content
|
| 1123 |
|
| 1124 |
+
# --- Tier 3: Business Coach & Help Layer ---
|
| 1125 |
+
help_keywords = ['help', 'tutorial', 'guide', 'how do you work', 'what can you do', 'How can I']
|
| 1126 |
+
literacy_keywords = [ 'explain', 'how to', 'how do i', 'ideas for', 'advice on', "how do", ]
|
| 1127 |
+
if any(keyword in query_lower for keyword in help_keywords) or any(keyword in query_lower for keyword in literacy_keywords):
|
| 1128 |
+
logger.info(f"Handling '{query}' with the Business Coach Path.")
|
| 1129 |
+
snapshot = engine.generate_business_snapshot()
|
| 1130 |
+
snapshot_str = json.dumps(snapshot, indent=2)
|
| 1131 |
+
|
| 1132 |
+
synthesis_prompt = f"""
|
| 1133 |
+
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.
|
| 1134 |
+
|
| 1135 |
+
**IMPORTANT RULES:**
|
| 1136 |
+
1. **Use the Context:** Use the Business Snapshot as your internal knowledge 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.
|
| 1137 |
+
2. **Do NOT State the Numbers:** Do NOT repeat the numbers or lists from the snapshot directly. Synthesize them into your advice.
|
| 1138 |
+
3. **Stay in Character:** Act as a coach. Be encouraging and provide actionable advice.
|
| 1139 |
+
4. **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.
|
| 1140 |
+
5. **Format for WhatsApp:** Use *bold*, _italic_, and emojis to make your response clear and engaging.
|
| 1141 |
+
|
| 1142 |
+
**BUSINESS SNAPSHOT (INTERNAL CONTEXT ONLY):**
|
| 1143 |
+
{snapshot_str}
|
| 1144 |
+
|
| 1145 |
+
**User's Question:**
|
| 1146 |
+
"{query}"
|
| 1147 |
+
"""
|
| 1148 |
+
response = llm.invoke(synthesis_prompt)
|
| 1149 |
+
return response.content
|
| 1150 |
+
|
| 1151 |
+
# --- Tier 4: Fortified PandasAI with Graceful Fallback (Final Path) ---
|
| 1152 |
else:
|
| 1153 |
try:
|
| 1154 |
logger.info(f"Handling '{query}' with the Fortified PandasAI Path.")
|