Spaces:
Running
Running
jashdoshi77
commited on
Commit
·
60ff586
1
Parent(s):
9099b59
ai context
Browse files- services/chroma_service.py +98 -10
- services/rag_service.py +259 -47
services/chroma_service.py
CHANGED
|
@@ -482,21 +482,49 @@ class ChromaService:
|
|
| 482 |
# ==================== Conversation Memory Operations ====================
|
| 483 |
|
| 484 |
def store_conversation(self, user_id: str, role: str, content: str,
|
| 485 |
-
bucket_id: str = "", chat_id: str = ""
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
import time
|
|
|
|
| 488 |
msg_id = f"{user_id}_{int(time.time() * 1000)}"
|
| 489 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
self.conversations_collection.add(
|
| 491 |
ids=[msg_id],
|
| 492 |
documents=[content],
|
| 493 |
-
metadatas=[
|
| 494 |
-
"user_id": user_id,
|
| 495 |
-
"role": role, # 'user' or 'assistant'
|
| 496 |
-
"bucket_id": bucket_id,
|
| 497 |
-
"chat_id": chat_id,
|
| 498 |
-
"timestamp": time.time()
|
| 499 |
-
}]
|
| 500 |
)
|
| 501 |
return {"msg_id": msg_id}
|
| 502 |
|
|
@@ -525,13 +553,73 @@ class ChromaService:
|
|
| 525 |
"content": results['documents'][i],
|
| 526 |
"timestamp": results['metadatas'][i]['timestamp'],
|
| 527 |
"bucket_id": results['metadatas'][i].get('bucket_id', ''),
|
| 528 |
-
"chat_id": results['metadatas'][i].get('chat_id', '')
|
|
|
|
|
|
|
| 529 |
})
|
| 530 |
|
| 531 |
# Sort by timestamp (newest last) and limit
|
| 532 |
messages.sort(key=lambda x: x['timestamp'])
|
| 533 |
return messages[-limit:]
|
| 534 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
def clear_conversation(self, user_id: str, bucket_id: str = None) -> bool:
|
| 536 |
"""Clear conversation history for a user"""
|
| 537 |
if bucket_id:
|
|
|
|
| 482 |
# ==================== Conversation Memory Operations ====================
|
| 483 |
|
| 484 |
def store_conversation(self, user_id: str, role: str, content: str,
|
| 485 |
+
bucket_id: str = "", chat_id: str = "",
|
| 486 |
+
query_context: dict = None, format_preference: str = None) -> dict:
|
| 487 |
+
"""Store a conversation message for persistent memory.
|
| 488 |
+
|
| 489 |
+
Args:
|
| 490 |
+
user_id: User ID
|
| 491 |
+
role: 'user' or 'assistant'
|
| 492 |
+
content: Message content
|
| 493 |
+
bucket_id: Optional bucket ID
|
| 494 |
+
chat_id: Optional chat session ID
|
| 495 |
+
query_context: Optional dict with query data for format reuse (NEW)
|
| 496 |
+
format_preference: Optional format preference used (NEW)
|
| 497 |
+
"""
|
| 498 |
import time
|
| 499 |
+
import json
|
| 500 |
msg_id = f"{user_id}_{int(time.time() * 1000)}"
|
| 501 |
|
| 502 |
+
metadata = {
|
| 503 |
+
"user_id": user_id,
|
| 504 |
+
"role": role, # 'user' or 'assistant'
|
| 505 |
+
"bucket_id": bucket_id,
|
| 506 |
+
"chat_id": chat_id,
|
| 507 |
+
"timestamp": time.time()
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
# Store format preference if provided
|
| 511 |
+
if format_preference:
|
| 512 |
+
metadata["format_preference"] = format_preference
|
| 513 |
+
|
| 514 |
+
# Store query context as JSON string (for format reuse)
|
| 515 |
+
# Limited to 1000 chars to avoid storage issues
|
| 516 |
+
if query_context and role == 'assistant':
|
| 517 |
+
try:
|
| 518 |
+
context_str = json.dumps(query_context)
|
| 519 |
+
if len(context_str) <= 5000:
|
| 520 |
+
metadata["query_context"] = context_str
|
| 521 |
+
except:
|
| 522 |
+
pass
|
| 523 |
+
|
| 524 |
self.conversations_collection.add(
|
| 525 |
ids=[msg_id],
|
| 526 |
documents=[content],
|
| 527 |
+
metadatas=[metadata]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
)
|
| 529 |
return {"msg_id": msg_id}
|
| 530 |
|
|
|
|
| 553 |
"content": results['documents'][i],
|
| 554 |
"timestamp": results['metadatas'][i]['timestamp'],
|
| 555 |
"bucket_id": results['metadatas'][i].get('bucket_id', ''),
|
| 556 |
+
"chat_id": results['metadatas'][i].get('chat_id', ''),
|
| 557 |
+
"format_preference": results['metadatas'][i].get('format_preference', ''),
|
| 558 |
+
"query_context": results['metadatas'][i].get('query_context', '')
|
| 559 |
})
|
| 560 |
|
| 561 |
# Sort by timestamp (newest last) and limit
|
| 562 |
messages.sort(key=lambda x: x['timestamp'])
|
| 563 |
return messages[-limit:]
|
| 564 |
|
| 565 |
+
def get_last_query_context(self, user_id: str, chat_id: str) -> dict:
|
| 566 |
+
"""
|
| 567 |
+
Get the most recent query's data context for format reuse.
|
| 568 |
+
|
| 569 |
+
Returns dict with:
|
| 570 |
+
- context: The document data from previous query
|
| 571 |
+
- format_preference: The format used in previous response
|
| 572 |
+
- found: True if context was found
|
| 573 |
+
"""
|
| 574 |
+
import json
|
| 575 |
+
|
| 576 |
+
try:
|
| 577 |
+
# Get recent messages for this chat
|
| 578 |
+
where_clause = {
|
| 579 |
+
"$and": [
|
| 580 |
+
{"user_id": user_id},
|
| 581 |
+
{"chat_id": chat_id},
|
| 582 |
+
{"role": "assistant"}
|
| 583 |
+
]
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
results = self.conversations_collection.get(where=where_clause)
|
| 587 |
+
|
| 588 |
+
if not results['ids']:
|
| 589 |
+
return {"found": False, "context": None, "format_preference": None}
|
| 590 |
+
|
| 591 |
+
# Find the most recent message with query_context
|
| 592 |
+
messages = []
|
| 593 |
+
for i, msg_id in enumerate(results['ids']):
|
| 594 |
+
messages.append({
|
| 595 |
+
"msg_id": msg_id,
|
| 596 |
+
"timestamp": results['metadatas'][i].get('timestamp', 0),
|
| 597 |
+
"query_context": results['metadatas'][i].get('query_context', ''),
|
| 598 |
+
"format_preference": results['metadatas'][i].get('format_preference', '')
|
| 599 |
+
})
|
| 600 |
+
|
| 601 |
+
# Sort by timestamp descending (newest first)
|
| 602 |
+
messages.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 603 |
+
|
| 604 |
+
# Find first message with query_context
|
| 605 |
+
for msg in messages:
|
| 606 |
+
if msg.get('query_context'):
|
| 607 |
+
try:
|
| 608 |
+
context = json.loads(msg['query_context'])
|
| 609 |
+
return {
|
| 610 |
+
"found": True,
|
| 611 |
+
"context": context,
|
| 612 |
+
"format_preference": msg.get('format_preference')
|
| 613 |
+
}
|
| 614 |
+
except:
|
| 615 |
+
continue
|
| 616 |
+
|
| 617 |
+
return {"found": False, "context": None, "format_preference": None}
|
| 618 |
+
|
| 619 |
+
except Exception as e:
|
| 620 |
+
print(f"[QUERY CONTEXT] Error retrieving last context: {e}")
|
| 621 |
+
return {"found": False, "context": None, "format_preference": None}
|
| 622 |
+
|
| 623 |
def clear_conversation(self, user_id: str, bucket_id: str = None) -> bool:
|
| 624 |
"""Clear conversation history for a user"""
|
| 625 |
if bucket_id:
|
services/rag_service.py
CHANGED
|
@@ -221,6 +221,8 @@ class RAGService:
|
|
| 221 |
- limit: number of results (or None for all)
|
| 222 |
- calculation: sum|average|max|min (or None)
|
| 223 |
- calculation_field: field for calculation
|
|
|
|
|
|
|
| 224 |
"""
|
| 225 |
import json
|
| 226 |
|
|
@@ -233,6 +235,16 @@ CRITICAL RULES:
|
|
| 233 |
3. When user asks for "top N" of something, set both limit AND sort_by appropriately
|
| 234 |
4. Keywords like "manufacturing", "healthcare", "retail", "IT", "construction" are INDUSTRIES - put them in filters
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
Available fields for filtering:
|
| 237 |
- is_manufacturing (boolean): True ONLY if asking specifically about manufacturing flag
|
| 238 |
- policy_type (string): fire, marine, motor, health, liability, property, engineering, etc.
|
|
@@ -262,36 +274,23 @@ Return ONLY valid JSON (no markdown, no explanation):
|
|
| 262 |
"sort_order": "desc" or "asc",
|
| 263 |
"limit": number or null,
|
| 264 |
"calculation": "sum|average|max|min|count" or null,
|
| 265 |
-
"calculation_field": "premium_amount|sum_insured" or null
|
|
|
|
|
|
|
| 266 |
}
|
| 267 |
|
| 268 |
Examples:
|
| 269 |
Query: "top 5 manufacturing policies by premium"
|
| 270 |
-
{"intent":"rank","needs_metadata":true,"filters":{"industry":"manufacturing"},"sort_by":"premium_amount","sort_order":"desc","limit":5,"calculation":null,"calculation_field":null}
|
| 271 |
-
|
| 272 |
-
Query: "top 5 manufacturing and top 5 healthcare policies"
|
| 273 |
-
{"intent":"compare","needs_metadata":true,"filters":{"industry":"manufacturing, healthcare"},"sort_by":"premium_amount","sort_order":"desc","limit":5,"calculation":null,"calculation_field":null}
|
| 274 |
-
|
| 275 |
-
Query: "compare manufacturing and healthcare industries"
|
| 276 |
-
{"intent":"compare","needs_metadata":true,"filters":{"industry":"manufacturing, healthcare"},"sort_by":"sum_insured","sort_order":"desc","limit":10,"calculation":null,"calculation_field":null}
|
| 277 |
-
|
| 278 |
-
Query: "list policies from IT and retail sectors"
|
| 279 |
-
{"intent":"list","needs_metadata":true,"filters":{"industry":"it, retail"},"sort_by":null,"sort_order":"desc","limit":null,"calculation":null,"calculation_field":null}
|
| 280 |
-
|
| 281 |
-
Query: "total sum insured for all fire policies"
|
| 282 |
-
{"intent":"calculate","needs_metadata":true,"filters":{"policy_type":"fire"},"sort_by":null,"sort_order":"desc","limit":null,"calculation":"sum","calculation_field":"sum_insured"}
|
| 283 |
|
| 284 |
-
Query: "
|
| 285 |
-
{"intent":"
|
| 286 |
|
| 287 |
-
Query: "list all policies
|
| 288 |
-
{"intent":"list","needs_metadata":true,"filters":{"
|
| 289 |
|
| 290 |
-
Query: "
|
| 291 |
-
{"intent":"
|
| 292 |
-
|
| 293 |
-
Query: "top 5 health policies by sum insured"
|
| 294 |
-
{"intent":"rank","needs_metadata":true,"filters":{"policy_type":"health"},"sort_by":"sum_insured","sort_order":"desc","limit":5,"calculation":null,"calculation_field":null}"""
|
| 295 |
|
| 296 |
messages = [
|
| 297 |
{"role": "system", "content": system_prompt},
|
|
@@ -304,12 +303,19 @@ Query: "top 5 health policies by sum insured"
|
|
| 304 |
|
| 305 |
# Parse JSON response
|
| 306 |
parsed = json.loads(response.strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
print(f"[AI QUERY PARSER] Parsed: {json.dumps(parsed, indent=2)}")
|
| 308 |
return parsed
|
| 309 |
|
| 310 |
except Exception as e:
|
| 311 |
print(f"[AI QUERY PARSER] Error: {e}, falling back to pattern matching")
|
| 312 |
-
# Fallback to basic detection
|
| 313 |
return {
|
| 314 |
"intent": "specific",
|
| 315 |
"needs_metadata": False,
|
|
@@ -318,7 +324,9 @@ Query: "top 5 health policies by sum insured"
|
|
| 318 |
"sort_order": "desc",
|
| 319 |
"limit": None,
|
| 320 |
"calculation": None,
|
| 321 |
-
"calculation_field": None
|
|
|
|
|
|
|
| 322 |
}
|
| 323 |
|
| 324 |
def _call_deepseek_sync(self, messages: list, max_tokens: int = 500) -> str:
|
|
@@ -348,6 +356,130 @@ Query: "top 5 health policies by sum insured"
|
|
| 348 |
else:
|
| 349 |
raise Exception(f"DeepSeek API error: {response.status_code}")
|
| 350 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
def _detect_query_type(self, query: str, history: list[dict] = None) -> str:
|
| 352 |
"""
|
| 353 |
Detect the type of query to optimize retrieval and response.
|
|
@@ -944,14 +1076,45 @@ Summary: {summary[:300] if summary else 'No summary available'}
|
|
| 944 |
"""
|
| 945 |
print(f"[METADATA STREAM] Handling AI-parsed query: intent={parsed.get('intent')}")
|
| 946 |
|
| 947 |
-
#
|
| 948 |
-
|
|
|
|
| 949 |
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 955 |
|
| 956 |
# Check if we have any data
|
| 957 |
if not context or total_docs == 0:
|
|
@@ -971,6 +1134,10 @@ Summary: {summary[:300] if summary else 'No summary available'}
|
|
| 971 |
# Step 2: Build AI prompt based on parsed intent
|
| 972 |
intent = parsed.get('intent', 'list')
|
| 973 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 974 |
if intent == 'count':
|
| 975 |
system_prompt = f"""You are Iribl AI, a document analysis assistant answering a COUNT query.
|
| 976 |
|
|
@@ -978,7 +1145,9 @@ CRITICAL INSTRUCTIONS:
|
|
| 978 |
1. The count has been computed: {total_docs} documents match the criteria.
|
| 979 |
2. State the count clearly and directly.
|
| 980 |
3. If filters were applied, mention what was filtered.
|
| 981 |
-
4. Brief context about what was counted is helpful.
|
|
|
|
|
|
|
| 982 |
|
| 983 |
elif intent == 'calculate':
|
| 984 |
calc_info = ""
|
|
@@ -990,7 +1159,9 @@ CRITICAL INSTRUCTIONS:
|
|
| 990 |
1. The calculation results have been computed from {total_docs} documents.{calc_info}
|
| 991 |
2. Present the numbers clearly with proper formatting (₹ for currency, commas for thousands).
|
| 992 |
3. Explain what the numbers mean in business context.
|
| 993 |
-
4. Include document counts to show the calculation scope.
|
|
|
|
|
|
|
| 994 |
|
| 995 |
Present the data accurately - these are pre-computed from actual document metadata."""
|
| 996 |
|
|
@@ -1004,8 +1175,9 @@ CRITICAL INSTRUCTIONS:
|
|
| 1004 |
1. You have been given the top {limit} documents sorted by {sort_by} ({sort_order}).
|
| 1005 |
2. Present them as a clear ranked list with the ranking number.
|
| 1006 |
3. Highlight the key metric ({sort_by}) for each item.
|
| 1007 |
-
4.
|
| 1008 |
-
|
|
|
|
| 1009 |
|
| 1010 |
elif intent == 'compare':
|
| 1011 |
system_prompt = f"""You are Iribl AI, a document analysis assistant answering a COMPARISON query.
|
|
@@ -1013,9 +1185,10 @@ CRITICAL INSTRUCTIONS:
|
|
| 1013 |
CRITICAL INSTRUCTIONS:
|
| 1014 |
1. You have metadata for {total_docs} relevant documents.
|
| 1015 |
2. Create a clear comparison highlighting differences and similarities.
|
| 1016 |
-
3.
|
| 1017 |
-
4.
|
| 1018 |
-
|
|
|
|
| 1019 |
|
| 1020 |
else: # list, summarize, or other
|
| 1021 |
system_prompt = f"""You are Iribl AI, a document analysis assistant. You are answering a query that requires information from {total_docs} documents.
|
|
@@ -1023,16 +1196,44 @@ CRITICAL INSTRUCTIONS:
|
|
| 1023 |
CRITICAL INSTRUCTIONS:
|
| 1024 |
1. You have been given metadata for {total_docs} documents (from {total_before} total).
|
| 1025 |
2. Your answer must be COMPREHENSIVE - include ALL relevant items from the data provided.
|
| 1026 |
-
3.
|
| 1027 |
-
4.
|
| 1028 |
-
5.
|
| 1029 |
-
|
|
|
|
| 1030 |
|
| 1031 |
Do NOT say information is missing - you have the filtered list. Do NOT ask for more documents."""
|
| 1032 |
|
| 1033 |
-
# Step 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
messages = [{"role": "system", "content": system_prompt}]
|
| 1035 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
user_message = f"""Based on the following document metadata and any calculations, answer my question.
|
| 1037 |
|
| 1038 |
DOCUMENT DATA:
|
|
@@ -1040,7 +1241,7 @@ DOCUMENT DATA:
|
|
| 1040 |
|
| 1041 |
QUESTION: {query}
|
| 1042 |
|
| 1043 |
-
Instructions: Provide a complete, well-formatted answer based on ALL the data above."""
|
| 1044 |
|
| 1045 |
messages.append({"role": "user", "content": user_message})
|
| 1046 |
|
|
@@ -1081,7 +1282,7 @@ Instructions: Provide a complete, well-formatted answer based on ALL the data ab
|
|
| 1081 |
print(f"[METADATA STREAM] Model {model_key} failed: {e}")
|
| 1082 |
continue
|
| 1083 |
|
| 1084 |
-
# Step 5: Store conversation
|
| 1085 |
if full_response and chat_id:
|
| 1086 |
try:
|
| 1087 |
chroma_service.store_conversation(
|
|
@@ -1091,13 +1292,24 @@ Instructions: Provide a complete, well-formatted answer based on ALL the data ab
|
|
| 1091 |
bucket_id=bucket_id or "",
|
| 1092 |
chat_id=chat_id
|
| 1093 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
chroma_service.store_conversation(
|
| 1095 |
user_id=user_id,
|
| 1096 |
role="assistant",
|
| 1097 |
content=full_response,
|
| 1098 |
bucket_id=bucket_id or "",
|
| 1099 |
-
chat_id=chat_id
|
|
|
|
|
|
|
| 1100 |
)
|
|
|
|
| 1101 |
except Exception as e:
|
| 1102 |
print(f"[METADATA STREAM] Failed to store conversation: {e}")
|
| 1103 |
|
|
|
|
| 221 |
- limit: number of results (or None for all)
|
| 222 |
- calculation: sum|average|max|min (or None)
|
| 223 |
- calculation_field: field for calculation
|
| 224 |
+
- format_preference: table|list|bullets|paragraph (or None for default)
|
| 225 |
+
- is_format_change: True if query is asking to reformat previous answer
|
| 226 |
"""
|
| 227 |
import json
|
| 228 |
|
|
|
|
| 235 |
3. When user asks for "top N" of something, set both limit AND sort_by appropriately
|
| 236 |
4. Keywords like "manufacturing", "healthcare", "retail", "IT", "construction" are INDUSTRIES - put them in filters
|
| 237 |
|
| 238 |
+
FORMAT DETECTION (NEW):
|
| 239 |
+
1. Detect if user explicitly asks for a specific format:
|
| 240 |
+
- "as a table", "in table format", "show table" -> format_preference: "table"
|
| 241 |
+
- "as a list", "list format", "numbered list" -> format_preference: "list"
|
| 242 |
+
- "bullet points", "bullets" -> format_preference: "bullets"
|
| 243 |
+
- "in paragraph", "prose", "narrative" -> format_preference: "paragraph"
|
| 244 |
+
2. Detect if query is ONLY asking to reformat (no new data request):
|
| 245 |
+
- "show that as a table", "convert to list", "in bullet points" -> is_format_change: true
|
| 246 |
+
- These typically use pronouns like "that", "this", "it" or "the above"
|
| 247 |
+
|
| 248 |
Available fields for filtering:
|
| 249 |
- is_manufacturing (boolean): True ONLY if asking specifically about manufacturing flag
|
| 250 |
- policy_type (string): fire, marine, motor, health, liability, property, engineering, etc.
|
|
|
|
| 274 |
"sort_order": "desc" or "asc",
|
| 275 |
"limit": number or null,
|
| 276 |
"calculation": "sum|average|max|min|count" or null,
|
| 277 |
+
"calculation_field": "premium_amount|sum_insured" or null,
|
| 278 |
+
"format_preference": "table|list|bullets|paragraph" or null,
|
| 279 |
+
"is_format_change": true or false
|
| 280 |
}
|
| 281 |
|
| 282 |
Examples:
|
| 283 |
Query: "top 5 manufacturing policies by premium"
|
| 284 |
+
{"intent":"rank","needs_metadata":true,"filters":{"industry":"manufacturing"},"sort_by":"premium_amount","sort_order":"desc","limit":5,"calculation":null,"calculation_field":null,"format_preference":null,"is_format_change":false}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
+
Query: "show that as a table"
|
| 287 |
+
{"intent":"list","needs_metadata":false,"filters":{},"sort_by":null,"sort_order":"desc","limit":null,"calculation":null,"calculation_field":null,"format_preference":"table","is_format_change":true}
|
| 288 |
|
| 289 |
+
Query: "list all fire policies in bullet points"
|
| 290 |
+
{"intent":"list","needs_metadata":true,"filters":{"policy_type":"fire"},"sort_by":null,"sort_order":"desc","limit":null,"calculation":null,"calculation_field":null,"format_preference":"bullets","is_format_change":false}
|
| 291 |
|
| 292 |
+
Query: "top 5 health policies by sum insured as a table"
|
| 293 |
+
{"intent":"rank","needs_metadata":true,"filters":{"policy_type":"health"},"sort_by":"sum_insured","sort_order":"desc","limit":5,"calculation":null,"calculation_field":null,"format_preference":"table","is_format_change":false}"""
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
messages = [
|
| 296 |
{"role": "system", "content": system_prompt},
|
|
|
|
| 303 |
|
| 304 |
# Parse JSON response
|
| 305 |
parsed = json.loads(response.strip())
|
| 306 |
+
|
| 307 |
+
# Ensure new fields have defaults if AI doesn't include them
|
| 308 |
+
if 'format_preference' not in parsed:
|
| 309 |
+
parsed['format_preference'] = None
|
| 310 |
+
if 'is_format_change' not in parsed:
|
| 311 |
+
parsed['is_format_change'] = False
|
| 312 |
+
|
| 313 |
print(f"[AI QUERY PARSER] Parsed: {json.dumps(parsed, indent=2)}")
|
| 314 |
return parsed
|
| 315 |
|
| 316 |
except Exception as e:
|
| 317 |
print(f"[AI QUERY PARSER] Error: {e}, falling back to pattern matching")
|
| 318 |
+
# Fallback to basic detection with new fields
|
| 319 |
return {
|
| 320 |
"intent": "specific",
|
| 321 |
"needs_metadata": False,
|
|
|
|
| 324 |
"sort_order": "desc",
|
| 325 |
"limit": None,
|
| 326 |
"calculation": None,
|
| 327 |
+
"calculation_field": None,
|
| 328 |
+
"format_preference": None,
|
| 329 |
+
"is_format_change": False
|
| 330 |
}
|
| 331 |
|
| 332 |
def _call_deepseek_sync(self, messages: list, max_tokens: int = 500) -> str:
|
|
|
|
| 356 |
else:
|
| 357 |
raise Exception(f"DeepSeek API error: {response.status_code}")
|
| 358 |
|
| 359 |
+
def _is_format_only_request(self, query: str, parsed: dict) -> bool:
|
| 360 |
+
"""
|
| 361 |
+
Detect if query is only asking to reformat the previous answer.
|
| 362 |
+
Uses AI parsing result and fallback pattern matching.
|
| 363 |
+
|
| 364 |
+
Returns True if this is a format-change-only request.
|
| 365 |
+
"""
|
| 366 |
+
# First check AI parsing result
|
| 367 |
+
if parsed.get('is_format_change', False):
|
| 368 |
+
return True
|
| 369 |
+
|
| 370 |
+
# Fallback: pattern matching for common reformat requests
|
| 371 |
+
query_lower = query.lower().strip()
|
| 372 |
+
|
| 373 |
+
# Patterns that indicate format-only requests (with pronouns or references)
|
| 374 |
+
format_only_patterns = [
|
| 375 |
+
'show that as', 'show this as', 'show it as',
|
| 376 |
+
'convert to', 'change to', 'format as',
|
| 377 |
+
'in table format', 'as a table', 'as table',
|
| 378 |
+
'in list format', 'as a list', 'as list',
|
| 379 |
+
'in bullet', 'as bullet', 'with bullets',
|
| 380 |
+
'reformat', 'reformatted',
|
| 381 |
+
'same thing but', 'same data but', 'same info but'
|
| 382 |
+
]
|
| 383 |
+
|
| 384 |
+
for pattern in format_only_patterns:
|
| 385 |
+
if pattern in query_lower:
|
| 386 |
+
# Check for pronouns indicating reference to previous answer
|
| 387 |
+
if any(pronoun in query_lower for pronoun in ['that', 'this', 'it', 'them', 'above', 'previous']):
|
| 388 |
+
print(f"[FORMAT DETECT] Detected format-only request via pattern: '{pattern}'")
|
| 389 |
+
return True
|
| 390 |
+
|
| 391 |
+
return False
|
| 392 |
+
|
| 393 |
+
def _validate_metadata(self, metadata: dict) -> dict:
|
| 394 |
+
"""
|
| 395 |
+
Sanity check metadata values and flag anomalies.
|
| 396 |
+
Returns validated metadata with warnings logged for suspicious values.
|
| 397 |
+
|
| 398 |
+
Checks:
|
| 399 |
+
- Negative monetary amounts
|
| 400 |
+
- Dates too far in future (> 2100) or past (< 1900)
|
| 401 |
+
- Extremely large numerical values
|
| 402 |
+
"""
|
| 403 |
+
validated = metadata.copy()
|
| 404 |
+
warnings = []
|
| 405 |
+
|
| 406 |
+
# Check sum_insured
|
| 407 |
+
sum_insured = metadata.get('sum_insured', 0)
|
| 408 |
+
if isinstance(sum_insured, (int, float)):
|
| 409 |
+
if sum_insured < 0:
|
| 410 |
+
warnings.append(f"Negative sum_insured: {sum_insured}")
|
| 411 |
+
validated['sum_insured'] = 0
|
| 412 |
+
elif sum_insured > 1e15: # More than 1 quadrillion
|
| 413 |
+
warnings.append(f"Extremely large sum_insured: {sum_insured}")
|
| 414 |
+
|
| 415 |
+
# Check premium_amount
|
| 416 |
+
premium = metadata.get('premium_amount', 0)
|
| 417 |
+
if isinstance(premium, (int, float)):
|
| 418 |
+
if premium < 0:
|
| 419 |
+
warnings.append(f"Negative premium_amount: {premium}")
|
| 420 |
+
validated['premium_amount'] = 0
|
| 421 |
+
elif premium > 1e12: # More than 1 trillion
|
| 422 |
+
warnings.append(f"Extremely large premium_amount: {premium}")
|
| 423 |
+
|
| 424 |
+
# Check renewal_year
|
| 425 |
+
renewal_year = metadata.get('renewal_year', 0)
|
| 426 |
+
if isinstance(renewal_year, int) and renewal_year > 0:
|
| 427 |
+
if renewal_year < 1900:
|
| 428 |
+
warnings.append(f"Renewal year too old: {renewal_year}")
|
| 429 |
+
elif renewal_year > 2100:
|
| 430 |
+
warnings.append(f"Renewal year too far in future: {renewal_year}")
|
| 431 |
+
validated['renewal_year'] = 0
|
| 432 |
+
|
| 433 |
+
# Check dates
|
| 434 |
+
for date_field in ['policy_start_date', 'policy_end_date', 'renewal_date']:
|
| 435 |
+
date_value = metadata.get(date_field, '')
|
| 436 |
+
if date_value and isinstance(date_value, str):
|
| 437 |
+
# Extract year from date string
|
| 438 |
+
import re
|
| 439 |
+
year_match = re.search(r'(19|20|21)\d{2}', date_value)
|
| 440 |
+
if year_match:
|
| 441 |
+
year = int(year_match.group())
|
| 442 |
+
if year > 2100 or year < 1900:
|
| 443 |
+
warnings.append(f"Invalid year in {date_field}: {date_value}")
|
| 444 |
+
|
| 445 |
+
# Log warnings
|
| 446 |
+
if warnings:
|
| 447 |
+
doc_title = metadata.get('document_title', 'Unknown')
|
| 448 |
+
print(f"[METADATA VALIDATION] Warnings for '{doc_title}':")
|
| 449 |
+
for w in warnings:
|
| 450 |
+
print(f" - {w}")
|
| 451 |
+
|
| 452 |
+
return validated
|
| 453 |
+
|
| 454 |
+
def _get_format_instructions(self, format_preference: str) -> str:
|
| 455 |
+
"""
|
| 456 |
+
Get specific formatting instructions based on user's format preference.
|
| 457 |
+
Returns markdown-compatible formatting guidance.
|
| 458 |
+
"""
|
| 459 |
+
format_map = {
|
| 460 |
+
"table": """FORMAT: Present data in a markdown table.
|
| 461 |
+
- Use | column | headers | with |---| separator line
|
| 462 |
+
- Keep columns aligned and consistent
|
| 463 |
+
- Include all requested data in table rows""",
|
| 464 |
+
|
| 465 |
+
"list": """FORMAT: Present as a numbered list.
|
| 466 |
+
1. Each item on its own line with number prefix
|
| 467 |
+
2. Include key details after the number
|
| 468 |
+
3. Use consistent formatting for all items""",
|
| 469 |
+
|
| 470 |
+
"bullets": """FORMAT: Use bullet points.
|
| 471 |
+
- Each item as a bullet point
|
| 472 |
+
- Sub-details can be indented bullets
|
| 473 |
+
- Keep bullets concise and scannable""",
|
| 474 |
+
|
| 475 |
+
"paragraph": """FORMAT: Write in flowing prose paragraphs.
|
| 476 |
+
- Use complete sentences and natural language
|
| 477 |
+
- Group related information into paragraphs
|
| 478 |
+
- Avoid lists or tables unless absolutely necessary"""
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
return format_map.get(format_preference, "")
|
| 482 |
+
|
| 483 |
def _detect_query_type(self, query: str, history: list[dict] = None) -> str:
|
| 484 |
"""
|
| 485 |
Detect the type of query to optimize retrieval and response.
|
|
|
|
| 1076 |
"""
|
| 1077 |
print(f"[METADATA STREAM] Handling AI-parsed query: intent={parsed.get('intent')}")
|
| 1078 |
|
| 1079 |
+
# Get format preference from parsed query
|
| 1080 |
+
format_preference = parsed.get('format_preference')
|
| 1081 |
+
is_format_change = self._is_format_only_request(query, parsed)
|
| 1082 |
|
| 1083 |
+
print(f"[METADATA STREAM] Format preference: {format_preference}, is_format_change: {is_format_change}")
|
| 1084 |
+
|
| 1085 |
+
# Step 1: Check if this is a format-change-only request (reuse previous data)
|
| 1086 |
+
context = None
|
| 1087 |
+
sources = {}
|
| 1088 |
+
total_docs = 0
|
| 1089 |
+
total_before = 0
|
| 1090 |
+
calculation = None
|
| 1091 |
+
|
| 1092 |
+
if is_format_change and chat_id:
|
| 1093 |
+
# Try to get previous query's context data
|
| 1094 |
+
print("[METADATA STREAM] Format-only request detected, attempting to reuse previous data...")
|
| 1095 |
+
try:
|
| 1096 |
+
prev_context = chroma_service.get_last_query_context(user_id, chat_id)
|
| 1097 |
+
if prev_context.get('found') and prev_context.get('context'):
|
| 1098 |
+
cached_data = prev_context['context']
|
| 1099 |
+
context = cached_data.get('context', '')
|
| 1100 |
+
sources = cached_data.get('sources', {})
|
| 1101 |
+
total_docs = cached_data.get('total_documents', 0)
|
| 1102 |
+
total_before = cached_data.get('total_before_filter', 0)
|
| 1103 |
+
calculation = cached_data.get('calculation')
|
| 1104 |
+
print(f"[METADATA STREAM] Reusing cached data: {total_docs} documents")
|
| 1105 |
+
except Exception as e:
|
| 1106 |
+
print(f"[METADATA STREAM] Failed to get cached context: {e}")
|
| 1107 |
+
|
| 1108 |
+
# If no cached data available (or not a format change), get fresh data
|
| 1109 |
+
if not context:
|
| 1110 |
+
print("[METADATA STREAM] Getting fresh data from metadata query...")
|
| 1111 |
+
result = self._handle_metadata_query(user_id, bucket_id, query, parsed)
|
| 1112 |
+
|
| 1113 |
+
context = result.get('context', '')
|
| 1114 |
+
sources = result.get('sources', {})
|
| 1115 |
+
total_docs = result.get('total_documents', 0)
|
| 1116 |
+
total_before = result.get('total_before_filter', 0)
|
| 1117 |
+
calculation = result.get('calculation')
|
| 1118 |
|
| 1119 |
# Check if we have any data
|
| 1120 |
if not context or total_docs == 0:
|
|
|
|
| 1134 |
# Step 2: Build AI prompt based on parsed intent
|
| 1135 |
intent = parsed.get('intent', 'list')
|
| 1136 |
|
| 1137 |
+
# Get format-specific instructions if user specified a preference
|
| 1138 |
+
format_instructions = self._get_format_instructions(format_preference) if format_preference else ""
|
| 1139 |
+
conciseness_directive = "\n\nIMPORTANT: Be concise and direct. No preambles or verbose explanations. Get straight to the formatted answer." if format_preference else ""
|
| 1140 |
+
|
| 1141 |
if intent == 'count':
|
| 1142 |
system_prompt = f"""You are Iribl AI, a document analysis assistant answering a COUNT query.
|
| 1143 |
|
|
|
|
| 1145 |
1. The count has been computed: {total_docs} documents match the criteria.
|
| 1146 |
2. State the count clearly and directly.
|
| 1147 |
3. If filters were applied, mention what was filtered.
|
| 1148 |
+
4. Brief context about what was counted is helpful.{conciseness_directive}
|
| 1149 |
+
|
| 1150 |
+
{format_instructions}"""
|
| 1151 |
|
| 1152 |
elif intent == 'calculate':
|
| 1153 |
calc_info = ""
|
|
|
|
| 1159 |
1. The calculation results have been computed from {total_docs} documents.{calc_info}
|
| 1160 |
2. Present the numbers clearly with proper formatting (₹ for currency, commas for thousands).
|
| 1161 |
3. Explain what the numbers mean in business context.
|
| 1162 |
+
4. Include document counts to show the calculation scope.{conciseness_directive}
|
| 1163 |
+
|
| 1164 |
+
{format_instructions}
|
| 1165 |
|
| 1166 |
Present the data accurately - these are pre-computed from actual document metadata."""
|
| 1167 |
|
|
|
|
| 1175 |
1. You have been given the top {limit} documents sorted by {sort_by} ({sort_order}).
|
| 1176 |
2. Present them as a clear ranked list with the ranking number.
|
| 1177 |
3. Highlight the key metric ({sort_by}) for each item.
|
| 1178 |
+
4. Include all {limit} items - do not truncate.{conciseness_directive}
|
| 1179 |
+
|
| 1180 |
+
{format_instructions if format_instructions else "FORMAT: Use numbered list format with bold for values."}"""
|
| 1181 |
|
| 1182 |
elif intent == 'compare':
|
| 1183 |
system_prompt = f"""You are Iribl AI, a document analysis assistant answering a COMPARISON query.
|
|
|
|
| 1185 |
CRITICAL INSTRUCTIONS:
|
| 1186 |
1. You have metadata for {total_docs} relevant documents.
|
| 1187 |
2. Create a clear comparison highlighting differences and similarities.
|
| 1188 |
+
3. Focus on the key metrics mentioned in the query.
|
| 1189 |
+
4. Be thorough but organized.{conciseness_directive}
|
| 1190 |
+
|
| 1191 |
+
{format_instructions if format_instructions else "FORMAT: Use tables or side-by-side format where helpful."}"""
|
| 1192 |
|
| 1193 |
else: # list, summarize, or other
|
| 1194 |
system_prompt = f"""You are Iribl AI, a document analysis assistant. You are answering a query that requires information from {total_docs} documents.
|
|
|
|
| 1196 |
CRITICAL INSTRUCTIONS:
|
| 1197 |
1. You have been given metadata for {total_docs} documents (from {total_before} total).
|
| 1198 |
2. Your answer must be COMPREHENSIVE - include ALL relevant items from the data provided.
|
| 1199 |
+
3. For "list" queries, actually list ALL matching items with key details.
|
| 1200 |
+
4. Organize information logically (by type, by company, by date, etc.).
|
| 1201 |
+
5. For "summarize" queries, provide a concise overview with key statistics.{conciseness_directive}
|
| 1202 |
+
|
| 1203 |
+
{format_instructions if format_instructions else "FORMAT: Use headers, bullet points, and bold text for clarity."}
|
| 1204 |
|
| 1205 |
Do NOT say information is missing - you have the filtered list. Do NOT ask for more documents."""
|
| 1206 |
|
| 1207 |
+
# Step 3: Load conversation history for memory (CRITICAL FOR CONTEXT)
|
| 1208 |
+
stored_history = []
|
| 1209 |
+
if chat_id:
|
| 1210 |
+
try:
|
| 1211 |
+
all_history = chroma_service.get_conversation_history(
|
| 1212 |
+
user_id=user_id,
|
| 1213 |
+
bucket_id=bucket_id,
|
| 1214 |
+
limit=50
|
| 1215 |
+
)
|
| 1216 |
+
# Filter to only this chat's messages
|
| 1217 |
+
stored_history = [msg for msg in all_history
|
| 1218 |
+
if msg.get('chat_id', '') == chat_id]
|
| 1219 |
+
stored_history = stored_history[-self.max_history:]
|
| 1220 |
+
print(f"[METADATA STREAM] Loaded {len(stored_history)} history messages")
|
| 1221 |
+
except Exception as e:
|
| 1222 |
+
print(f"[METADATA STREAM] Failed to load history: {e}")
|
| 1223 |
+
|
| 1224 |
+
# Step 4: Build messages with conversation history
|
| 1225 |
messages = [{"role": "system", "content": system_prompt}]
|
| 1226 |
|
| 1227 |
+
# Add conversation history for context (CRITICAL for follow-ups)
|
| 1228 |
+
for msg in stored_history:
|
| 1229 |
+
messages.append({
|
| 1230 |
+
"role": msg['role'],
|
| 1231 |
+
"content": msg['content']
|
| 1232 |
+
})
|
| 1233 |
+
|
| 1234 |
+
# Build user message with format emphasis if specified
|
| 1235 |
+
format_reminder = f"\n\nREMINDER: Present the response in {format_preference} format." if format_preference else ""
|
| 1236 |
+
|
| 1237 |
user_message = f"""Based on the following document metadata and any calculations, answer my question.
|
| 1238 |
|
| 1239 |
DOCUMENT DATA:
|
|
|
|
| 1241 |
|
| 1242 |
QUESTION: {query}
|
| 1243 |
|
| 1244 |
+
Instructions: Provide a complete, well-formatted answer based on ALL the data above.{format_reminder}"""
|
| 1245 |
|
| 1246 |
messages.append({"role": "user", "content": user_message})
|
| 1247 |
|
|
|
|
| 1282 |
print(f"[METADATA STREAM] Model {model_key} failed: {e}")
|
| 1283 |
continue
|
| 1284 |
|
| 1285 |
+
# Step 5: Store conversation WITH query context for format reuse
|
| 1286 |
if full_response and chat_id:
|
| 1287 |
try:
|
| 1288 |
chroma_service.store_conversation(
|
|
|
|
| 1292 |
bucket_id=bucket_id or "",
|
| 1293 |
chat_id=chat_id
|
| 1294 |
)
|
| 1295 |
+
# Store context data for potential format-change reuse
|
| 1296 |
+
query_context_data = {
|
| 1297 |
+
'context': context,
|
| 1298 |
+
'sources': sources,
|
| 1299 |
+
'total_documents': total_docs,
|
| 1300 |
+
'total_before_filter': total_before,
|
| 1301 |
+
'calculation': calculation
|
| 1302 |
+
}
|
| 1303 |
chroma_service.store_conversation(
|
| 1304 |
user_id=user_id,
|
| 1305 |
role="assistant",
|
| 1306 |
content=full_response,
|
| 1307 |
bucket_id=bucket_id or "",
|
| 1308 |
+
chat_id=chat_id,
|
| 1309 |
+
query_context=query_context_data,
|
| 1310 |
+
format_preference=format_preference
|
| 1311 |
)
|
| 1312 |
+
print(f"[METADATA STREAM] Stored conversation with query context for reuse")
|
| 1313 |
except Exception as e:
|
| 1314 |
print(f"[METADATA STREAM] Failed to store conversation: {e}")
|
| 1315 |
|