jashdoshi77 commited on
Commit
60ff586
·
1 Parent(s): 9099b59

ai context

Browse files
Files changed (2) hide show
  1. services/chroma_service.py +98 -10
  2. 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 = "") -> dict:
486
- """Store a conversation message for persistent memory"""
 
 
 
 
 
 
 
 
 
 
 
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: "what is covered in the ABC policy document?"
285
- {"intent":"specific","needs_metadata":false,"filters":{},"sort_by":null,"sort_order":"desc","limit":null,"calculation":null,"calculation_field":null}
286
 
287
- Query: "list all policies renewing in 2026"
288
- {"intent":"list","needs_metadata":true,"filters":{"renewal_year":2026},"sort_by":"renewal_date","sort_order":"asc","limit":null,"calculation":null,"calculation_field":null}
289
 
290
- Query: "how many manufacturing companies do we have?"
291
- {"intent":"count","needs_metadata":true,"filters":{"industry":"manufacturing"},"sort_by":null,"sort_order":"desc","limit":null,"calculation":"count","calculation_field":null}
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
- # Step 1: Get filtered, sorted, and calculated metadata using AI-parsed parameters
948
- result = self._handle_metadata_query(user_id, bucket_id, query, parsed)
 
949
 
950
- context = result.get('context', '')
951
- sources = result.get('sources', {})
952
- total_docs = result.get('total_documents', 0)
953
- total_before = result.get('total_before_filter', 0)
954
- calculation = result.get('calculation')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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. Format nicely with headers, bold for values, and bullet points.
1008
- 5. Include all {limit} items - do not truncate."""
 
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. Use tables or side-by-side format where helpful.
1017
- 4. Focus on the key metrics mentioned in the query.
1018
- 5. Be thorough but organized."""
 
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. Format your response clearly with headers, bullet points, and bold text.
1027
- 4. For "list" queries, actually list ALL matching items with key details.
1028
- 5. Organize information logically (by type, by company, by date, etc.).
1029
- 6. For "summarize" queries, provide a concise overview with key statistics.
 
1030
 
1031
  Do NOT say information is missing - you have the filtered list. Do NOT ask for more documents."""
1032
 
1033
- # Step 3: Build messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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