|
|
""" |
|
|
Tests for FinEE RAG Engine |
|
|
========================== |
|
|
|
|
|
Comprehensive tests for: |
|
|
- Merchant Knowledge Base |
|
|
- Category Taxonomy |
|
|
- Vector Store |
|
|
- RAG Engine |
|
|
|
|
|
Author: Ranjit Behera |
|
|
""" |
|
|
|
|
|
import pytest |
|
|
import json |
|
|
from pathlib import Path |
|
|
import tempfile |
|
|
|
|
|
|
|
|
class TestMerchantKnowledgeBase: |
|
|
"""Tests for MerchantKnowledgeBase.""" |
|
|
|
|
|
def setup_method(self): |
|
|
"""Setup test fixtures.""" |
|
|
from finee.rag import MerchantKnowledgeBase, Merchant |
|
|
self.kb = MerchantKnowledgeBase() |
|
|
self.Merchant = Merchant |
|
|
|
|
|
def test_default_merchants_loaded(self): |
|
|
"""Test that default merchants are loaded.""" |
|
|
assert len(self.kb.merchants) > 30 |
|
|
assert "swiggy" in self.kb.merchants |
|
|
assert "amazon" in self.kb.merchants |
|
|
assert "zerodha" in self.kb.merchants |
|
|
|
|
|
def test_lookup_by_name(self): |
|
|
"""Test merchant lookup by name.""" |
|
|
merchant = self.kb.lookup("Swiggy") |
|
|
assert merchant is not None |
|
|
assert merchant.name == "Swiggy" |
|
|
assert merchant.category == "food" |
|
|
|
|
|
def test_lookup_by_vpa(self): |
|
|
"""Test merchant lookup by VPA.""" |
|
|
merchant = self.kb.lookup("swiggy@ybl") |
|
|
assert merchant is not None |
|
|
assert merchant.name == "Swiggy" |
|
|
|
|
|
def test_lookup_by_alias(self): |
|
|
"""Test merchant lookup by alias.""" |
|
|
merchant = self.kb.lookup("amzn") |
|
|
assert merchant is not None |
|
|
assert merchant.name == "Amazon" |
|
|
|
|
|
def test_lookup_partial_match(self): |
|
|
"""Test partial name matching.""" |
|
|
merchant = self.kb.lookup("netflix") |
|
|
assert merchant is not None |
|
|
assert merchant.name == "Netflix" |
|
|
|
|
|
def test_lookup_not_found(self): |
|
|
"""Test lookup returns None for unknown merchant.""" |
|
|
merchant = self.kb.lookup("unknownmerchant123") |
|
|
assert merchant is None |
|
|
|
|
|
def test_search_by_text(self): |
|
|
"""Test text-based merchant search.""" |
|
|
matches = self.kb.search("food delivery swiggy order") |
|
|
assert len(matches) > 0 |
|
|
assert matches[0].name == "Swiggy" |
|
|
|
|
|
def test_add_merchant(self): |
|
|
"""Test adding new merchant.""" |
|
|
new_merchant = self.Merchant( |
|
|
name="TestMerchant", |
|
|
vpa="test@ybl", |
|
|
category="test", |
|
|
aliases=["tm"], |
|
|
) |
|
|
self.kb.add_merchant(new_merchant) |
|
|
|
|
|
assert "testmerchant" in self.kb.merchants |
|
|
assert self.kb.lookup("test@ybl") is not None |
|
|
assert self.kb.lookup("tm") is not None |
|
|
|
|
|
def test_get_category_merchants(self): |
|
|
"""Test getting merchants by category.""" |
|
|
food_merchants = self.kb.get_category_merchants("food") |
|
|
assert len(food_merchants) > 0 |
|
|
for m in food_merchants: |
|
|
assert m.category == "food" |
|
|
|
|
|
def test_save_and_load(self): |
|
|
"""Test saving and loading KB.""" |
|
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
|
path = Path(tmpdir) / "merchants.json" |
|
|
|
|
|
|
|
|
self.kb.save(path) |
|
|
assert path.exists() |
|
|
|
|
|
|
|
|
from finee.rag import MerchantKnowledgeBase |
|
|
loaded_kb = MerchantKnowledgeBase.load(path) |
|
|
|
|
|
assert len(loaded_kb.merchants) == len(self.kb.merchants) |
|
|
|
|
|
|
|
|
class TestCategoryTaxonomy: |
|
|
"""Tests for CategoryTaxonomy.""" |
|
|
|
|
|
def setup_method(self): |
|
|
"""Setup test fixtures.""" |
|
|
from finee.rag import CategoryTaxonomy |
|
|
self.taxonomy = CategoryTaxonomy |
|
|
|
|
|
def test_get_hierarchy_simple(self): |
|
|
"""Test simple category hierarchy.""" |
|
|
hierarchy = self.taxonomy.get_hierarchy("food") |
|
|
assert hierarchy == ["food"] |
|
|
|
|
|
def test_get_hierarchy_nested(self): |
|
|
"""Test nested category hierarchy.""" |
|
|
hierarchy = self.taxonomy.get_hierarchy("grocery") |
|
|
assert "shopping" in hierarchy |
|
|
assert "grocery" in hierarchy |
|
|
|
|
|
def test_get_hierarchy_unknown(self): |
|
|
"""Test unknown category returns itself.""" |
|
|
hierarchy = self.taxonomy.get_hierarchy("unknown") |
|
|
assert hierarchy == ["unknown"] |
|
|
|
|
|
def test_infer_category_food(self): |
|
|
"""Test food category inference.""" |
|
|
category = self.taxonomy.infer_category("lunch delivery from restaurant") |
|
|
assert category == "food" |
|
|
|
|
|
def test_infer_category_investment(self): |
|
|
"""Test investment category inference.""" |
|
|
category = self.taxonomy.infer_category("SIP mutual fund trading invest") |
|
|
assert category == "investment" |
|
|
|
|
|
def test_infer_category_bills(self): |
|
|
"""Test bills category inference.""" |
|
|
category = self.taxonomy.infer_category("electricity bill payment recharge") |
|
|
assert category == "bills" |
|
|
|
|
|
def test_infer_category_unknown(self): |
|
|
"""Test unknown text returns 'other'.""" |
|
|
category = self.taxonomy.infer_category("random text here") |
|
|
assert category == "other" |
|
|
|
|
|
|
|
|
class TestSimpleVectorStore: |
|
|
"""Tests for SimpleVectorStore.""" |
|
|
|
|
|
def setup_method(self): |
|
|
"""Setup test fixtures.""" |
|
|
from finee.rag import SimpleVectorStore, Transaction |
|
|
self.store = SimpleVectorStore() |
|
|
self.Transaction = Transaction |
|
|
|
|
|
def test_add_transaction(self): |
|
|
"""Test adding transaction.""" |
|
|
txn = self.Transaction( |
|
|
id="test1", |
|
|
text="HDFC Bank Rs.500 debited Swiggy", |
|
|
amount=500.0, |
|
|
type="debit", |
|
|
merchant="Swiggy", |
|
|
category="food", |
|
|
) |
|
|
self.store.add(txn) |
|
|
|
|
|
assert len(self.store.documents) == 1 |
|
|
|
|
|
def test_search_similar(self): |
|
|
"""Test similarity search.""" |
|
|
|
|
|
transactions = [ |
|
|
self.Transaction("1", "HDFC Rs.500 Swiggy food", 500, "debit", "Swiggy", "food"), |
|
|
self.Transaction("2", "SBI Rs.1000 Amazon shopping", 1000, "debit", "Amazon", "shopping"), |
|
|
self.Transaction("3", "ICICI Rs.250 Zomato dinner", 250, "debit", "Zomato", "food"), |
|
|
] |
|
|
|
|
|
for txn in transactions: |
|
|
self.store.add(txn) |
|
|
|
|
|
|
|
|
results = self.store.search("Swiggy food delivery", limit=2) |
|
|
|
|
|
assert len(results) > 0 |
|
|
|
|
|
assert results[0][0].category == "food" |
|
|
|
|
|
def test_search_empty_store(self): |
|
|
"""Test search on empty store.""" |
|
|
results = self.store.search("any query") |
|
|
assert results == [] |
|
|
|
|
|
def test_save_and_load(self): |
|
|
"""Test saving and loading store.""" |
|
|
|
|
|
txn = self.Transaction("1", "Test transaction", 100, "debit") |
|
|
self.store.add(txn) |
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
|
path = Path(tmpdir) / "store.json" |
|
|
|
|
|
|
|
|
self.store.save(path) |
|
|
assert path.exists() |
|
|
|
|
|
|
|
|
from finee.rag import SimpleVectorStore |
|
|
loaded = SimpleVectorStore.load(path) |
|
|
|
|
|
assert len(loaded.documents) == 1 |
|
|
|
|
|
|
|
|
class TestRAGEngine: |
|
|
"""Tests for RAGEngine.""" |
|
|
|
|
|
def setup_method(self): |
|
|
"""Setup test fixtures.""" |
|
|
from finee.rag import RAGEngine |
|
|
self.rag = RAGEngine() |
|
|
|
|
|
def test_retrieve_with_merchant(self): |
|
|
"""Test retrieval with known merchant.""" |
|
|
context = self.rag.retrieve("HDFC Bank Rs.499 debited UPI:swiggy@ybl") |
|
|
|
|
|
assert context.merchant_info is not None |
|
|
assert context.merchant_info["name"] == "Swiggy" |
|
|
assert context.merchant_info["category"] == "food" |
|
|
assert context.confidence_boost > 0 |
|
|
|
|
|
def test_retrieve_with_category(self): |
|
|
"""Test category inference.""" |
|
|
context = self.rag.retrieve("Netflix subscription payment") |
|
|
|
|
|
assert context.category_hierarchy is not None |
|
|
assert "entertainment" in context.category_hierarchy |
|
|
|
|
|
def test_retrieve_investment(self): |
|
|
"""Test investment detection.""" |
|
|
context = self.rag.retrieve("Rs.25000 transferred to Zerodha trading") |
|
|
|
|
|
assert context.merchant_info is not None |
|
|
assert context.merchant_info["category"] == "investment" |
|
|
|
|
|
def test_augment_prompt(self): |
|
|
"""Test prompt augmentation.""" |
|
|
context = self.rag.retrieve("Swiggy food order") |
|
|
augmented = self.rag.augment_prompt("Swiggy food order", context) |
|
|
|
|
|
assert "Swiggy" in augmented |
|
|
assert "food" in augmented |
|
|
|
|
|
def test_add_transaction(self): |
|
|
"""Test adding transaction to history.""" |
|
|
self.rag.add_transaction( |
|
|
"Test transaction text", |
|
|
{"amount": 100, "merchant": "Test", "category": "test"} |
|
|
) |
|
|
|
|
|
assert len(self.rag.vector_store.documents) > 0 |
|
|
|
|
|
def test_save_and_load(self): |
|
|
"""Test saving and loading RAG state.""" |
|
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
|
path = Path(tmpdir) / "rag_state" |
|
|
|
|
|
|
|
|
self.rag.save(path) |
|
|
assert (path / "merchants.json").exists() |
|
|
|
|
|
|
|
|
from finee.rag import RAGEngine |
|
|
new_rag = RAGEngine() |
|
|
new_rag.load(path) |
|
|
|
|
|
assert len(new_rag.merchant_kb.merchants) > 0 |
|
|
|
|
|
|
|
|
class TestRAGIntegration: |
|
|
"""Integration tests for RAG with extraction.""" |
|
|
|
|
|
def setup_method(self): |
|
|
"""Setup test fixtures.""" |
|
|
from finee.rag import RAGEngine |
|
|
self.rag = RAGEngine() |
|
|
|
|
|
@pytest.mark.parametrize("message,expected_merchant,expected_category", [ |
|
|
("HDFC Rs.499 UPI:swiggy@ybl", "Swiggy", "food"), |
|
|
("SBI Rs.999 Netflix subscription", "Netflix", "entertainment"), |
|
|
("ICICI Rs.25000 Zerodha trading", "Zerodha", "investment"), |
|
|
("Axis Rs.1500 Amazon order", "Amazon", "shopping"), |
|
|
("Kotak Rs.350 Uber ride", "Uber", "transport"), |
|
|
("PNB Rs.100 Airtel recharge", "Airtel", "bills"), |
|
|
]) |
|
|
def test_merchant_detection(self, message, expected_merchant, expected_category): |
|
|
"""Test merchant detection for various messages.""" |
|
|
context = self.rag.retrieve(message) |
|
|
|
|
|
assert context.merchant_info is not None |
|
|
assert context.merchant_info["name"] == expected_merchant |
|
|
assert context.merchant_info["category"] == expected_category |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
pytest.main([__file__, "-v"]) |
|
|
|