Eniiyanu commited on
Commit
651b18e
·
verified ·
1 Parent(s): 1f9c43e

Upload 15 files

Browse files
Files changed (2) hide show
  1. rag_pipeline.py +73 -12
  2. test_response_types.py +140 -0
rag_pipeline.py CHANGED
@@ -454,11 +454,13 @@ class RAGPipeline:
454
  ),
455
  ])
456
 
457
- self.compose_prompt = ChatPromptTemplate.from_messages([
 
458
  (
459
  "system",
460
  ANTI_HALLUCINATION_SYSTEM + "\n\n" +
461
  "You are Káàntà AI, a senior Nigerian tax consultant. Build expert answers ONLY from provided facts.\n\n"
 
462
  "WRITING STYLE: Lead with specific numbers and percentages. Remove [F1] fact IDs from final output.\n\n"
463
  "PROHIBITED CONTENT:\n"
464
  "- DO NOT add generic compliance warnings like 'consult a tax professional' or 'comply with regulations'\n"
@@ -471,7 +473,7 @@ class RAGPipeline:
471
  "{answer_schema}\n"
472
  "Rules:\n"
473
  "- Every detail must reference at least one fact_id.\n"
474
- "- \"explainer\" items expand on the mechanics; \"key_points\" focus on actions/implications and must NOT repeat explainer text.\n"
475
  "- Set ask_for_income=true only when personalized calculations are impossible without it.\n"
476
  "- Keep wording concise and practical."
477
  ),
@@ -483,6 +485,48 @@ class RAGPipeline:
483
  ),
484
  ])
485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  self.chain = self._build_chain()
487
  print("RAG pipeline ready")
488
 
@@ -780,10 +824,16 @@ class RAGPipeline:
780
  )
781
  return cleaned
782
 
783
- def _compose_from_facts(self, question: str, facts: List[Dict[str, Any]]) -> Optional[str]:
784
  if not facts:
785
  return None
786
 
 
 
 
 
 
 
787
  facts_json = json.dumps({"facts": facts}, ensure_ascii=False)
788
  payload = {
789
  "question": question,
@@ -791,7 +841,7 @@ class RAGPipeline:
791
  "answer_schema": ANSWER_SCHEMA_TEXT,
792
  }
793
 
794
- raw = (self.compose_prompt | self.llm | StrOutputParser()).invoke(payload)
795
  _, final_json = self._extract_analysis_and_final(raw)
796
  structured = self._safe_json_parse(final_json)
797
  if not structured:
@@ -860,7 +910,7 @@ class RAGPipeline:
860
 
861
  return final_output
862
 
863
- def _fact_guided_answer(self, question: str) -> str:
864
  docs = self._retrieve(question)
865
  snippets = self._prepare_context_snippets(docs)
866
  if not snippets:
@@ -868,7 +918,7 @@ class RAGPipeline:
868
 
869
  try:
870
  facts = self._harvest_facts(question, snippets)
871
- response = self._compose_from_facts(question, facts)
872
  if response:
873
  return response
874
  except Exception as exc:
@@ -1113,11 +1163,22 @@ class RAGPipeline:
1113
  return "qa"
1114
 
1115
  # Stub for a future extractor chain - currently route extractor requests to QA chain with strict rules
1116
- def _extract_structured(self, question: str) -> str:
1117
- return self._fact_guided_answer(question)
 
 
 
 
 
 
 
 
 
 
1118
 
1119
- def query(self, question: str, verbose: bool = False) -> str:
1120
- """Route and answer the question with persona-aware responses."""
 
1121
  # First, check if question is tax-related
1122
  if not self._is_tax_related_question(question):
1123
  return (
@@ -1179,9 +1240,9 @@ class RAGPipeline:
1179
  if task == "summarize":
1180
  return self._summarize_chapter(question)
1181
  elif task == "extract":
1182
- return self._extract_structured(question)
1183
  else:
1184
- return self._fact_guided_answer(question)
1185
 
1186
 
1187
  def main():
 
454
  ),
455
  ])
456
 
457
+ # SHORT compose prompt (for WhatsApp)
458
+ self.compose_prompt_short = ChatPromptTemplate.from_messages([
459
  (
460
  "system",
461
  ANTI_HALLUCINATION_SYSTEM + "\n\n" +
462
  "You are Káàntà AI, a senior Nigerian tax consultant. Build expert answers ONLY from provided facts.\n\n"
463
+ "RESPONSE STYLE: BRIEF - Answer in 3-10 concise sentences for WhatsApp. Lead with the key answer immediately. Focus on the most critical information only.\n\n"
464
  "WRITING STYLE: Lead with specific numbers and percentages. Remove [F1] fact IDs from final output.\n\n"
465
  "PROHIBITED CONTENT:\n"
466
  "- DO NOT add generic compliance warnings like 'consult a tax professional' or 'comply with regulations'\n"
 
473
  "{answer_schema}\n"
474
  "Rules:\n"
475
  "- Every detail must reference at least one fact_id.\n"
476
+ "- \"explainer\" items should be 1-9 sentences max; \"key_points\" 1 sentence each - BRIEF for WhatsApp\n"
477
  "- Set ask_for_income=true only when personalized calculations are impossible without it.\n"
478
  "- Keep wording concise and practical."
479
  ),
 
485
  ),
486
  ])
487
 
488
+ # LONG compose prompt (for PDF reports)
489
+ self.compose_prompt_long = ChatPromptTemplate.from_messages([
490
+ (
491
+ "system",
492
+ ANTI_HALLUCINATION_SYSTEM + "\n\n" +
493
+ "You are Káàntà AI, a senior Nigerian tax consultant. Build expert answers ONLY from provided facts.\n\n"
494
+ "RESPONSE STYLE: COMPREHENSIVE REPORT for PDF - Provide detailed explanation with:\n"
495
+ "- Thorough concept explanation with background context\n"
496
+ "- Multiple real-world examples with step-by-step calculations\n"
497
+ "- Tables comparing different scenarios (e.g., income brackets, tax rates)\n"
498
+ "- Numerical breakdowns showing how amounts are derived\n"
499
+ "- Specific references to Nigerian tax laws (e.g., 'Per Finance Act 2023, Section X...')\n"
500
+ "- Practical implications and edge cases\n"
501
+ "Format professionally for a PDF report with clear sections.\n\n"
502
+ "WRITING STYLE: Lead with specific numbers and percentages. Remove [F1] fact IDs from final output.\n\n"
503
+ "PROHIBITED CONTENT:\n"
504
+ "- DO NOT add generic compliance warnings like 'consult a tax professional' or 'comply with regulations'\n"
505
+ "- DO NOT add administrative penalty warnings unless they are specifically mentioned in the facts\n"
506
+ "- Focus on answering the user's question with facts, not generic advice\n\n"
507
+ "Workflow:\n"
508
+ "1. Inside <analysis></analysis>, plan the unique insights you will cover. List fact IDs you will use. "
509
+ "If a fact would appear twice, mark it as DUPLICATE and drop the repeat.\n"
510
+ "2. Inside <final></final>, output JSON that matches this schema:\n"
511
+ "{answer_schema}\n"
512
+ "Rules:\n"
513
+ "- Every detail must reference at least one fact_id.\n"
514
+ "- \"explainer\" items should provide comprehensive explanations with examples and calculations\n"
515
+ "- \"key_points\" should cover actions, implications, edge cases, and scenarios - detailed for PDF\n"
516
+ "- Set ask_for_income=true only when personalized calculations are impossible without it.\n"
517
+ "- Provide thorough, professional report-quality content."
518
+ ),
519
+ (
520
+ "human",
521
+ "Question:\n{question}\n\n"
522
+ "Verified facts (JSON):\n{facts_json}\n"
523
+ "Follow the required tag structure."
524
+ ),
525
+ ])
526
+
527
+ # Keep backward compatibility - default to short
528
+ self.compose_prompt = self.compose_prompt_short
529
+
530
  self.chain = self._build_chain()
531
  print("RAG pipeline ready")
532
 
 
824
  )
825
  return cleaned
826
 
827
+ def _compose_from_facts(self, question: str, facts: List[Dict[str, Any]], response_type: str = 'short') -> Optional[str]:
828
  if not facts:
829
  return None
830
 
831
+ # Select appropriate compose prompt based on response_type
832
+ if response_type.lower() == 'long':
833
+ compose_prompt = self.compose_prompt_long
834
+ else:
835
+ compose_prompt = self.compose_prompt_short
836
+
837
  facts_json = json.dumps({"facts": facts}, ensure_ascii=False)
838
  payload = {
839
  "question": question,
 
841
  "answer_schema": ANSWER_SCHEMA_TEXT,
842
  }
843
 
844
+ raw = (compose_prompt | self.llm | StrOutputParser()).invoke(payload)
845
  _, final_json = self._extract_analysis_and_final(raw)
846
  structured = self._safe_json_parse(final_json)
847
  if not structured:
 
910
 
911
  return final_output
912
 
913
+ def _fact_guided_answer(self, question: str, response_type: str = 'short') -> str:
914
  docs = self._retrieve(question)
915
  snippets = self._prepare_context_snippets(docs)
916
  if not snippets:
 
918
 
919
  try:
920
  facts = self._harvest_facts(question, snippets)
921
+ response = self._compose_from_facts(question, facts, response_type=response_type)
922
  if response:
923
  return response
924
  except Exception as exc:
 
1163
  return "qa"
1164
 
1165
  # Stub for a future extractor chain - currently route extractor requests to QA chain with strict rules
1166
+ def _extract_structured(self, question: str, response_type: str = 'short') -> str:
1167
+ return self._fact_guided_answer(question, response_type=response_type)
1168
+
1169
+ def query(self, question: str, verbose: bool = False, response_type: str = 'short') -> str:
1170
+ """
1171
+ Route and answer the question with persona-aware responses.
1172
+
1173
+ Args:
1174
+ question: User's tax question
1175
+ verbose: If True, print debug information
1176
+ response_type: 'short' for WhatsApp messages (3-4 sentences),
1177
+ 'long' for PDF reports (comprehensive with examples)
1178
 
1179
+ Returns:
1180
+ Formatted answer based on response_type
1181
+ """
1182
  # First, check if question is tax-related
1183
  if not self._is_tax_related_question(question):
1184
  return (
 
1240
  if task == "summarize":
1241
  return self._summarize_chapter(question)
1242
  elif task == "extract":
1243
+ return self._extract_structured(question, response_type=response_type)
1244
  else:
1245
+ return self._fact_guided_answer(question, response_type=response_type)
1246
 
1247
 
1248
  def main():
test_response_types.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to demonstrate SHORT vs LONG response types.
4
+ Shows the difference between WhatsApp (brief) and PDF (comprehensive) outputs.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+ from rag_pipeline import RAGPipeline, DocumentStore
10
+
11
+
12
+ def test_response_types():
13
+ """Test both SHORT and LONG response types with the same question."""
14
+
15
+ print("=" * 80)
16
+ print("RESPONSE TYPE COMPARISON TEST")
17
+ print("=" * 80)
18
+
19
+ # Initialize RAG pipeline
20
+ print("\nInitializing RAG pipeline...")
21
+ vector_store_path = Path("vector_store")
22
+ doc_store = DocumentStore(
23
+ persist_dir=vector_store_path,
24
+ embedding_model="BAAI/bge-large-en-v1.5"
25
+ )
26
+
27
+ src = Path("data")
28
+ pdfs = doc_store.discover_pdfs(src)
29
+ doc_store.build_vector_store(pdfs, force_rebuild=False)
30
+
31
+ rag = RAGPipeline(
32
+ doc_store=doc_store,
33
+ model="llama-3.3-70b-versatile",
34
+ temperature=0.1,
35
+ top_k=15,
36
+ )
37
+
38
+ print("✓ RAG pipeline initialized\n")
39
+
40
+ # Test question
41
+ question = "What are the personal income tax rates in Nigeria?"
42
+
43
+ print("=" * 80)
44
+ print("TEST QUESTION:")
45
+ print(question)
46
+ print("=" * 80)
47
+
48
+ # Test SHORT response (WhatsApp)
49
+ print("\n" + "=" * 80)
50
+ print("SHORT RESPONSE (for WhatsApp)")
51
+ print("=" * 80)
52
+ print("\nExpected: 3-4 concise sentences, immediate answer, key facts only\n")
53
+
54
+ try:
55
+ short_answer = rag.query(question, verbose=False, response_type='short')
56
+ print(short_answer)
57
+
58
+ # Quality checks for SHORT
59
+ print("\n" + "-" * 80)
60
+ print("SHORT RESPONSE QUALITY CHECKS:")
61
+ word_count = len(short_answer.split())
62
+ sentence_count = short_answer.count('.') + short_answer.count('?') + short_answer.count('!')
63
+ has_numbers = any(char.isdigit() for char in short_answer)
64
+
65
+ print(f" Word count: {word_count} (target: 50-150 words for brief)")
66
+ print(f" Sentence count: ~{sentence_count}")
67
+ print(f" Contains numbers: {has_numbers}")
68
+
69
+ if word_count <= 200:
70
+ print(" ✓ PASS: Response is concise")
71
+ else:
72
+ print(" ⚠️ WARNING: Response may be too long for WhatsApp")
73
+
74
+ except Exception as e:
75
+ print(f"❌ ERROR: {e}")
76
+ import traceback
77
+ traceback.print_exc()
78
+
79
+ # Test LONG response (PDF)
80
+ print("\n\n" + "=" * 80)
81
+ print("LONG RESPONSE (for PDF Report)")
82
+ print("=" * 80)
83
+ print("\nExpected: Comprehensive with examples, calculations, tables, law references\n")
84
+
85
+ try:
86
+ long_answer = rag.query(question, verbose=False, response_type='long')
87
+ print(long_answer)
88
+
89
+ # Quality checks for LONG
90
+ print("\n" + "-" * 80)
91
+ print("LONG RESPONSE QUALITY CHECKS:")
92
+ word_count = len(long_answer.split())
93
+ has_examples = 'example' in long_answer.lower() or 'instance' in long_answer.lower()
94
+ has_calculations = '×' in long_answer or 'calculate' in long_answer.lower()
95
+ has_law_refs = 'section' in long_answer.lower() or 'act' in long_answer.lower()
96
+ has_numbers = any(char.isdigit() for char in long_answer)
97
+
98
+ print(f" Word count: {word_count} (target: 300+ words for comprehensive)")
99
+ print(f" Contains examples: {has_examples}")
100
+ print(f" Contains calculations: {has_calculations}")
101
+ print(f" Contains law references: {has_law_refs}")
102
+ print(f" Contains numbers: {has_numbers}")
103
+
104
+ if word_count >= 300:
105
+ print(" ✓ PASS: Response is comprehensive")
106
+ else:
107
+ print(" ⚠️ WARNING: Response may be too brief for PDF report")
108
+
109
+ if has_examples and has_numbers:
110
+ print(" ✓ PASS: Response includes examples and numbers")
111
+ else:
112
+ print(" ⚠️ WARNING: Response may lack examples or numbers")
113
+
114
+ except Exception as e:
115
+ print(f"❌ ERROR: {e}")
116
+ import traceback
117
+ traceback.print_exc()
118
+
119
+ # Summary
120
+ print("\n\n" + "=" * 80)
121
+ print("COMPARISON SUMMARY")
122
+ print("=" * 80)
123
+ print("\nKEY DIFFERENCES:")
124
+ print(" SHORT (WhatsApp):")
125
+ print(" - 3-4 sentences")
126
+ print(" - Immediate answer")
127
+ print(" - Key facts only")
128
+ print(" - No examples or detailed calculations")
129
+ print("")
130
+ print(" LONG (PDF Report):")
131
+ print(" - Multiple paragraphs")
132
+ print(" - Detailed explanations")
133
+ print(" - Examples with step-by-step calculations")
134
+ print(" - Law references and edge cases")
135
+ print(" - Professional report format")
136
+ print("=" * 80)
137
+
138
+
139
+ if __name__ == "__main__":
140
+ test_response_types()