Upload 15 files
Browse files- rag_pipeline.py +73 -12
- test_response_types.py +140 -0
rag_pipeline.py
CHANGED
|
@@ -454,11 +454,13 @@ class RAGPipeline:
|
|
| 454 |
),
|
| 455 |
])
|
| 456 |
|
| 457 |
-
|
|
|
|
| 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
|
| 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 = (
|
| 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 |
-
|
| 1120 |
-
|
|
|
|
| 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()
|