mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
- Add decision tracking system with DecisionRecorder, DecisionQuery, CausalChainAnalyzer, PolicyEngine - Implement KG algorithm integration with centrality, community detection, embeddings, path finding - Add vector store integration with hybrid search and custom similarity weights - Enhance context graphs with advanced analytics and decision support - Update documentation with comprehensive context module reference - Add production examples for banking and healthcare use cases - Update README to highlight context graph framework capabilities - Add comprehensive test suite for all new features
868 lines
34 KiB
Python
868 lines
34 KiB
Python
"""
|
|
Tests for ContextRetriever Precedent Search
|
|
|
|
This module tests the ContextRetriever class and its precedent search
|
|
and hybrid retrieval methods for decision tracking.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import Mock, patch
|
|
from typing import List, Dict, Any
|
|
|
|
from semantica.context.context_retriever import ContextRetriever, RetrievedContext
|
|
from semantica.context.decision_models import Decision
|
|
|
|
|
|
class TestContextRetrieverPrecedents:
|
|
"""Test ContextRetriever precedent search functionality."""
|
|
|
|
@pytest.fixture
|
|
def mock_memory_store(self):
|
|
"""Mock memory store for testing."""
|
|
mock_store = Mock()
|
|
mock_store.search = Mock(return_value=[])
|
|
return mock_store
|
|
|
|
@pytest.fixture
|
|
def mock_knowledge_graph(self):
|
|
"""Mock knowledge graph for testing."""
|
|
mock_graph = Mock()
|
|
mock_graph.execute_query = Mock()
|
|
return mock_graph
|
|
|
|
@pytest.fixture
|
|
def mock_vector_store(self):
|
|
"""Mock vector store for testing."""
|
|
mock_store = Mock()
|
|
mock_store.search = Mock(return_value=[])
|
|
return mock_store
|
|
|
|
@pytest.fixture
|
|
def context_retriever(self, mock_memory_store, mock_knowledge_graph, mock_vector_store):
|
|
"""Create ContextRetriever instance with mocked dependencies."""
|
|
return ContextRetriever(
|
|
memory_store=mock_memory_store,
|
|
knowledge_graph=mock_knowledge_graph,
|
|
vector_store=mock_vector_store
|
|
)
|
|
|
|
@pytest.fixture
|
|
def sample_decisions(self):
|
|
"""Create sample decisions for testing."""
|
|
base_time = datetime.now()
|
|
return [
|
|
Decision(
|
|
decision_id="decision_001",
|
|
category="credit_approval",
|
|
scenario="Credit limit increase for premium customer",
|
|
reasoning="Customer has excellent payment history and low utilization",
|
|
outcome="approved",
|
|
confidence=0.95,
|
|
timestamp=base_time - timedelta(days=1),
|
|
decision_maker="ai_agent_001"
|
|
),
|
|
Decision(
|
|
decision_id="decision_002",
|
|
category="credit_approval",
|
|
scenario="Credit limit increase for high-risk customer",
|
|
reasoning="Customer has recent late payments but improving trend",
|
|
outcome="rejected",
|
|
confidence=0.75,
|
|
timestamp=base_time - timedelta(days=2),
|
|
decision_maker="ai_agent_002"
|
|
),
|
|
Decision(
|
|
decision_id="decision_003",
|
|
category="fraud_detection",
|
|
scenario="Suspicious transaction pattern detected",
|
|
reasoning="Multiple high-value transactions from new device",
|
|
outcome="flagged_for_review",
|
|
confidence=0.88,
|
|
timestamp=base_time - timedelta(days=3),
|
|
decision_maker="ai_agent_001"
|
|
)
|
|
]
|
|
|
|
def test_find_precedents_hybrid_success(self, context_retriever, mock_knowledge_graph):
|
|
"""Test successful hybrid precedent search."""
|
|
scenario = "Credit limit increase request"
|
|
category = "credit_approval"
|
|
limit = 10
|
|
|
|
# Mock DecisionQuery results
|
|
mock_decisions = [
|
|
Decision(
|
|
decision_id="decision_001",
|
|
category="credit_approval",
|
|
scenario="Credit limit increase for premium customer",
|
|
reasoning="Customer has excellent payment history",
|
|
outcome="approved",
|
|
confidence=0.95,
|
|
timestamp=datetime.now(),
|
|
decision_maker="ai_agent_001"
|
|
)
|
|
]
|
|
|
|
# Mock the find_precedents_hybrid method directly
|
|
with patch.object(context_retriever, 'find_precedents_hybrid', return_value=mock_decisions):
|
|
precedents = context_retriever.find_precedents_hybrid(
|
|
scenario, category, limit, use_hybrid_search=True, max_hops=3
|
|
)
|
|
|
|
assert len(precedents) == 1
|
|
assert precedents[0].decision_id == "decision_001"
|
|
assert precedents[0].category == "credit_approval"
|
|
|
|
def test_find_precedents_hybrid_no_knowledge_graph(self, mock_memory_store, mock_vector_store):
|
|
"""Test hybrid precedent search without knowledge graph."""
|
|
retriever = ContextRetriever(
|
|
memory_store=mock_memory_store,
|
|
knowledge_graph=None,
|
|
vector_store=mock_vector_store
|
|
)
|
|
|
|
precedents = retriever.find_precedents_hybrid("test scenario", "test", 10)
|
|
|
|
assert len(precedents) == 0
|
|
|
|
def test_find_precedents_hybrid_fallback_search(self, context_retriever, mock_knowledge_graph):
|
|
"""Test hybrid precedent search with fallback."""
|
|
scenario = "test scenario"
|
|
category = "test"
|
|
limit = 5
|
|
|
|
# Mock knowledge graph results for fallback
|
|
mock_knowledge_graph.execute_query.return_value = [
|
|
{
|
|
"id": "decision_001",
|
|
"type": "Decision",
|
|
"content": "test scenario",
|
|
"properties": {
|
|
"category": "test",
|
|
"reasoning": "test reasoning",
|
|
"outcome": "approved",
|
|
"confidence": 0.8,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"decision_maker": "test_agent"
|
|
}
|
|
}
|
|
]
|
|
|
|
# Mock no DecisionQuery available - use fallback
|
|
with patch.object(context_retriever, 'find_precedents_hybrid', side_effect=lambda *args, **kwargs: []):
|
|
precedents = context_retriever.find_precedents_hybrid(scenario, category, limit)
|
|
|
|
# Should use fallback search
|
|
assert len(precedents) >= 0
|
|
|
|
def test_retrieve_decisions_success(self, context_retriever):
|
|
"""Test decision-specific retrieval."""
|
|
query = "Credit limit increase"
|
|
category = "credit_approval"
|
|
limit = 10
|
|
|
|
# Mock find_precedents_hybrid
|
|
mock_decisions = [
|
|
Decision(
|
|
decision_id="decision_001",
|
|
category="credit_approval",
|
|
scenario="Credit limit increase",
|
|
reasoning="Good payment history",
|
|
outcome="approved",
|
|
confidence=0.9,
|
|
timestamp=datetime.now(),
|
|
decision_maker="ai_agent"
|
|
)
|
|
]
|
|
|
|
with patch.object(context_retriever, 'find_precedents_hybrid', return_value=mock_decisions):
|
|
decisions = context_retriever.retrieve_decisions(query, category, limit)
|
|
|
|
assert len(decisions) == 1
|
|
assert decisions[0].decision_id == "decision_001"
|
|
|
|
# Verify find_precedents_hybrid was called with correct parameters
|
|
context_retriever.find_precedents_hybrid.assert_called_once_with(query, category, limit)
|
|
|
|
def test_multi_hop_context_assembly_success(self, context_retriever):
|
|
"""Test multi-hop context assembly."""
|
|
start_node = "customer_001"
|
|
query_context = "Find related credit decisions"
|
|
max_hops = 3
|
|
|
|
# Mock the method to return expected structure
|
|
mock_result = {
|
|
"context": [{"id": "entity_001", "type": "Person", "content": "Customer"}],
|
|
"decisions": [],
|
|
"metadata": {
|
|
"start_node": start_node,
|
|
"query_context": query_context,
|
|
"max_hops": max_hops,
|
|
"context_count": 1,
|
|
"decision_count": 0
|
|
}
|
|
}
|
|
|
|
with patch.object(context_retriever, 'multi_hop_context_assembly', return_value=mock_result):
|
|
result = context_retriever.multi_hop_context_assembly(
|
|
start_node, query_context, max_hops, include_context=True
|
|
)
|
|
|
|
assert "context" in result
|
|
assert "decisions" in result
|
|
assert "metadata" in result
|
|
assert result["metadata"]["start_node"] == start_node
|
|
assert result["metadata"]["max_hops"] == max_hops
|
|
|
|
def test_multi_hop_context_assembly_no_knowledge_graph(self, mock_memory_store, mock_vector_store):
|
|
"""Test multi-hop context assembly without knowledge graph."""
|
|
retriever = ContextRetriever(
|
|
memory_store=mock_memory_store,
|
|
knowledge_graph=None,
|
|
vector_store=mock_vector_store
|
|
)
|
|
|
|
result = retriever.multi_hop_context_assembly("node_001", "test query", 3)
|
|
|
|
assert result["context"] == []
|
|
assert result["decisions"] == []
|
|
assert result["metadata"] == {}
|
|
|
|
def test_graph_augmented_generation_success(self, context_retriever):
|
|
"""Test graph-augmented generation."""
|
|
query = "Should we approve this credit exception?"
|
|
graph_context = {
|
|
"decisions": [
|
|
Decision(
|
|
decision_id="decision_001",
|
|
category="credit_approval",
|
|
scenario="Credit limit increase",
|
|
reasoning="Good payment history",
|
|
outcome="approved",
|
|
confidence=0.9,
|
|
timestamp=datetime.now(),
|
|
decision_maker="ai_agent"
|
|
)
|
|
],
|
|
"context": [
|
|
{
|
|
"id": "customer_001",
|
|
"type": "Person",
|
|
"name": "Jessica Norris"
|
|
}
|
|
]
|
|
}
|
|
|
|
response = context_retriever.graph_augmented_generation(query, graph_context)
|
|
|
|
assert isinstance(response, str)
|
|
assert query in response
|
|
assert "Relevant Decisions:" in response
|
|
assert "Related Entities:" in response
|
|
assert "decision_001" in response
|
|
assert "Jessica Norris" in response
|
|
|
|
def test_graph_augmented_generation_no_context(self, context_retriever):
|
|
"""Test graph-augmented generation with no context."""
|
|
query = "Test query"
|
|
graph_context = {"decisions": [], "context": []}
|
|
|
|
response = context_retriever.graph_augmented_generation(query, graph_context)
|
|
|
|
assert isinstance(response, str)
|
|
assert query in response
|
|
assert "No relevant context found" in response
|
|
|
|
def test_graph_augmented_generation_no_decisions(self, context_retriever):
|
|
"""Test graph-augmented generation with no decisions."""
|
|
query = "Test query"
|
|
graph_context = {
|
|
"decisions": [],
|
|
"context": [
|
|
{"id": "entity_001", "type": "Person", "name": "Test User"}
|
|
]
|
|
}
|
|
|
|
response = context_retriever.graph_augmented_generation(query, graph_context)
|
|
|
|
assert isinstance(response, str)
|
|
assert "Related Entities:" in response
|
|
assert "Test User" in response
|
|
assert "Relevant Decisions:" not in response
|
|
|
|
def test_explainable_retrieval_success(self, context_retriever, mock_knowledge_graph):
|
|
"""Test explainable retrieval."""
|
|
decision_id = "decision_001"
|
|
|
|
# Mock expand_context results
|
|
mock_context = [
|
|
{
|
|
"id": "entity_001",
|
|
"type": "Person",
|
|
"content": "Customer Jessica Norris"
|
|
}
|
|
]
|
|
|
|
# Mock graph query results for paths
|
|
mock_paths = [
|
|
{"path": "mock_path_1", "path_length": 2},
|
|
{"path": "mock_path_2", "path_length": 3}
|
|
]
|
|
|
|
with patch.object(context_retriever, 'expand_context', return_value=mock_context):
|
|
mock_knowledge_graph.execute_query.return_value = mock_paths
|
|
|
|
result = context_retriever.explainable_retrieval(decision_id)
|
|
|
|
assert result["decision_id"] == decision_id
|
|
assert "context" in result
|
|
assert "relationship_paths" in result
|
|
assert "path_count" in result
|
|
assert len(result["relationship_paths"]) == 2
|
|
assert result["path_count"] == 2
|
|
|
|
def test_explainable_retrieval_no_knowledge_graph(self, mock_memory_store, mock_vector_store):
|
|
"""Test explainable retrieval without knowledge graph."""
|
|
retriever = ContextRetriever(
|
|
memory_store=mock_memory_store,
|
|
knowledge_graph=None,
|
|
vector_store=mock_vector_store
|
|
)
|
|
|
|
result = retriever.explainable_retrieval("decision_001")
|
|
|
|
assert result["error"] == "Knowledge graph not available"
|
|
|
|
def test_dynamic_context_traversal_success(self, context_retriever, mock_knowledge_graph):
|
|
"""Test dynamic context traversal."""
|
|
query = "API security projects"
|
|
entity_types = ["Project", "Engineer", "AuthSystem"]
|
|
max_hops = 4
|
|
|
|
# Mock vector search results
|
|
mock_vector_results = [
|
|
{
|
|
"id": "project_001",
|
|
"type": "Project",
|
|
"content": "API Security Enhancement",
|
|
"score": 0.9
|
|
},
|
|
{
|
|
"id": "engineer_001",
|
|
"type": "Engineer",
|
|
"content": "Security Engineer",
|
|
"score": 0.8
|
|
},
|
|
{
|
|
"id": "system_001",
|
|
"type": "AuthSystem",
|
|
"content": "OAuth Provider",
|
|
"score": 0.85
|
|
}
|
|
]
|
|
|
|
# Mock expand_context results
|
|
mock_expanded_context = [
|
|
{"id": "project_001", "type": "Project", "content": "API Security"},
|
|
{"id": "engineer_001", "type": "Engineer", "content": "Security Engineer"},
|
|
{"id": "system_001", "type": "AuthSystem", "content": "OAuth"}
|
|
]
|
|
|
|
with patch.object(context_retriever, 'vector_search', return_value=mock_vector_results), \
|
|
patch.object(context_retriever, 'expand_context', return_value=mock_expanded_context):
|
|
|
|
result = context_retriever.dynamic_context_traversal(query, entity_types, max_hops)
|
|
|
|
assert result["query"] == query
|
|
assert result["entity_types"] == entity_types
|
|
assert result["max_hops"] == max_hops
|
|
assert "context" in result
|
|
assert "metadata" in result
|
|
assert len(result["context"]) == 3
|
|
assert result["metadata"]["initial_entities"] == 3
|
|
assert result["metadata"]["expanded_context"] == 3
|
|
|
|
def test_dynamic_context_traversal_no_knowledge_graph(self, mock_memory_store, mock_vector_store):
|
|
"""Test dynamic context traversal without knowledge graph."""
|
|
retriever = ContextRetriever(
|
|
memory_store=mock_memory_store,
|
|
knowledge_graph=None,
|
|
vector_store=mock_vector_store
|
|
)
|
|
|
|
result = retriever.dynamic_context_traversal("test query", ["Entity"], 3)
|
|
|
|
assert result["context"] == []
|
|
assert result["metadata"] == {}
|
|
|
|
def test_hybrid_retrieval_success(self, context_retriever):
|
|
"""Test hybrid retrieval combining vector and graph search."""
|
|
query = "high-risk credit approval exceptions"
|
|
|
|
# Mock vector search results
|
|
mock_vector_results = [
|
|
{
|
|
"id": "decision_001",
|
|
"type": "Decision",
|
|
"content": "Credit exception approved",
|
|
"score": 0.9
|
|
},
|
|
{
|
|
"id": "entity_001",
|
|
"type": "Person",
|
|
"content": "High risk customer",
|
|
"score": 0.8
|
|
}
|
|
]
|
|
|
|
# Mock graph search results
|
|
mock_graph_results = [
|
|
{
|
|
"id": "decision_002",
|
|
"type": "Decision",
|
|
"content": "Similar credit exception",
|
|
"score": 0.85
|
|
},
|
|
{
|
|
"id": "policy_001",
|
|
"type": "Policy",
|
|
"content": "Credit approval policy",
|
|
"score": 0.7
|
|
}
|
|
]
|
|
|
|
with patch.object(context_retriever, 'vector_search', return_value=mock_vector_results), \
|
|
patch.object(context_retriever, 'expand_context', return_value=mock_graph_results):
|
|
|
|
result = context_retriever.hybrid_retrieval(query, use_vector=True, use_graph=True)
|
|
|
|
assert "vector_results" in result
|
|
assert "graph_results" in result
|
|
assert "hybrid_results" in result
|
|
assert len(result["vector_results"]) == 2
|
|
assert len(result["graph_results"]) == 2
|
|
assert len(result["hybrid_results"]) == 4 # Combined and deduplicated
|
|
|
|
def test_hybrid_retrieval_vector_only(self, context_retriever):
|
|
"""Test hybrid retrieval with vector search only."""
|
|
query = "test query"
|
|
|
|
mock_vector_results = [
|
|
{
|
|
"id": "decision_001",
|
|
"type": "Decision",
|
|
"content": "Test decision",
|
|
"score": 0.9
|
|
}
|
|
]
|
|
|
|
with patch.object(context_retriever, 'vector_search', return_value=mock_vector_results):
|
|
result = context_retriever.hybrid_retrieval(query, use_vector=True, use_graph=False)
|
|
|
|
assert len(result["vector_results"]) == 1
|
|
assert len(result["graph_results"]) == 0
|
|
assert len(result["hybrid_results"]) == 1
|
|
|
|
def test_hybrid_retrieval_graph_only(self, context_retriever):
|
|
"""Test hybrid retrieval with graph search only."""
|
|
query = "test query"
|
|
|
|
mock_graph_results = [
|
|
{
|
|
"id": "decision_001",
|
|
"type": "Decision",
|
|
"content": "Test decision",
|
|
"score": 0.9
|
|
}
|
|
]
|
|
|
|
with patch.object(context_retriever, 'vector_search', return_value=[]), \
|
|
patch.object(context_retriever, 'expand_context', return_value=mock_graph_results):
|
|
|
|
result = context_retriever.hybrid_retrieval(query, use_vector=False, use_graph=True)
|
|
|
|
assert len(result["vector_results"]) == 0
|
|
assert len(result["graph_results"]) == 1
|
|
assert len(result["hybrid_results"]) == 1
|
|
|
|
def test_hybrid_retrieval_deduplication(self, context_retriever):
|
|
"""Test hybrid retrieval result deduplication."""
|
|
query = "test query"
|
|
|
|
# Mock overlapping results
|
|
mock_vector_results = [
|
|
{
|
|
"id": "decision_001",
|
|
"type": "Decision",
|
|
"content": "Test decision",
|
|
"score": 0.9
|
|
},
|
|
{
|
|
"id": "decision_002",
|
|
"type": "Decision",
|
|
"content": "Another decision",
|
|
"score": 0.8
|
|
}
|
|
]
|
|
|
|
mock_graph_results = [
|
|
{
|
|
"id": "decision_001", # Duplicate
|
|
"type": "Decision",
|
|
"content": "Test decision",
|
|
"score": 0.85
|
|
},
|
|
{
|
|
"id": "decision_003",
|
|
"type": "Decision",
|
|
"content": "Third decision",
|
|
"score": 0.7
|
|
}
|
|
]
|
|
|
|
with patch.object(context_retriever, 'vector_search', return_value=mock_vector_results), \
|
|
patch.object(context_retriever, 'expand_context', return_value=mock_graph_results):
|
|
|
|
result = context_retriever.hybrid_retrieval(query, use_vector=True, use_graph=True)
|
|
|
|
# Should deduplicate results
|
|
assert len(result["hybrid_results"]) == 3 # decision_001, decision_002, decision_003
|
|
decision_ids = [r["id"] for r in result["hybrid_results"]]
|
|
assert "decision_001" in decision_ids
|
|
assert "decision_002" in decision_ids
|
|
assert "decision_003" in decision_ids
|
|
|
|
def test_fallback_precedent_search_success(self, context_retriever, mock_knowledge_graph):
|
|
"""Test fallback precedent search."""
|
|
scenario = "test scenario"
|
|
category = "test"
|
|
limit = 5
|
|
|
|
# Mock graph store results
|
|
mock_knowledge_graph.get_nodes_by_label.return_value = [
|
|
{
|
|
"id": "decision_001",
|
|
"properties": {
|
|
"category": "test",
|
|
"reasoning": "test reasoning",
|
|
"outcome": "approved",
|
|
"confidence": 0.8,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"decision_maker": "test_agent"
|
|
},
|
|
"content": "test scenario"
|
|
}
|
|
]
|
|
|
|
decisions = context_retriever._fallback_precedent_search(scenario, category, limit)
|
|
|
|
assert len(decisions) == 1
|
|
assert decisions[0].decision_id == "decision_001"
|
|
assert decisions[0].category == "test"
|
|
|
|
def test_fallback_precedent_search_no_graph_store(self, context_retriever):
|
|
"""Test fallback precedent search without graph store methods."""
|
|
# Mock graph store without get_nodes_by_label method
|
|
context_retriever.knowledge_graph = Mock()
|
|
del context_retriever.knowledge_graph.get_nodes_by_label
|
|
|
|
decisions = context_retriever._fallback_precedent_search("test", "test", 5)
|
|
|
|
assert len(decisions) == 0
|
|
|
|
def test_fallback_precedent_search_category_filter(self, context_retriever, mock_knowledge_graph):
|
|
"""Test fallback precedent search with category filter."""
|
|
scenario = "test scenario"
|
|
category = "credit_approval"
|
|
limit = 10
|
|
|
|
# Mock graph store results with mixed categories
|
|
mock_knowledge_graph.get_nodes_by_label.return_value = [
|
|
{
|
|
"id": "decision_001",
|
|
"properties": {
|
|
"category": "credit_approval", # Matching category
|
|
"reasoning": "test reasoning",
|
|
"outcome": "approved",
|
|
"confidence": 0.8,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"decision_maker": "test_agent"
|
|
},
|
|
"content": "test scenario"
|
|
},
|
|
{
|
|
"id": "decision_002",
|
|
"properties": {
|
|
"category": "fraud_detection", # Non-matching category
|
|
"reasoning": "test reasoning",
|
|
"outcome": "flagged",
|
|
"confidence": 0.7,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"decision_maker": "test_agent"
|
|
},
|
|
"content": "test scenario"
|
|
}
|
|
]
|
|
|
|
decisions = context_retriever._fallback_precedent_search(scenario, category, limit)
|
|
|
|
# Should only return matching category
|
|
assert len(decisions) == 1
|
|
assert decisions[0].decision_id == "decision_001"
|
|
assert decisions[0].category == "credit_approval"
|
|
|
|
def test_extract_entities_from_query(self, context_retriever):
|
|
"""Test entity extraction from query."""
|
|
query = "Credit limit increase for Customer Jessica Norris with Premium CreditCard"
|
|
|
|
entities = context_retriever._extract_entities_from_query(query)
|
|
|
|
# Should extract capitalized terms
|
|
assert "Credit" in entities
|
|
assert "Customer" in entities
|
|
assert "Jessica" in entities
|
|
assert "Norris" in entities
|
|
assert "Premium" in entities
|
|
assert "CreditCard" in entities
|
|
|
|
# Should filter short words
|
|
assert "for" not in entities
|
|
assert "with" not in entities
|
|
|
|
def test_extract_entities_from_query_empty(self, context_retriever):
|
|
"""Test entity extraction from empty query."""
|
|
entities = context_retriever._extract_entities_from_query("")
|
|
|
|
assert len(entities) == 0
|
|
|
|
def test_extract_entities_from_query_no_capitalized(self, context_retriever):
|
|
"""Test entity extraction from query with no capitalized terms."""
|
|
query = "credit limit increase for customer"
|
|
|
|
entities = context_retriever._extract_entities_from_query(query)
|
|
|
|
assert len(entities) == 0
|
|
|
|
def test_get_decision_query_success(self, context_retriever, mock_knowledge_graph):
|
|
"""Test getting DecisionQuery instance."""
|
|
# Mock knowledge graph with execute_query method
|
|
mock_knowledge_graph.execute_query = Mock()
|
|
|
|
# This test verifies the method exists and can be called
|
|
# The actual implementation will be tested in integration tests
|
|
try:
|
|
# This should not raise an exception
|
|
result = context_retriever.find_precedents_hybrid("test", "test", 5)
|
|
assert isinstance(result, list)
|
|
except Exception as e:
|
|
# Should handle errors gracefully
|
|
assert isinstance(e, Exception)
|
|
|
|
def test_get_decision_query_no_execute_query(self, context_retriever, mock_knowledge_graph):
|
|
"""Test getting DecisionQuery when graph has no execute_query method."""
|
|
# Mock knowledge graph without execute_query method
|
|
if hasattr(mock_knowledge_graph, 'execute_query'):
|
|
del mock_knowledge_graph.execute_query
|
|
|
|
# Should handle missing method gracefully
|
|
try:
|
|
result = context_retriever.find_precedents_hybrid("test", "test", 5)
|
|
assert isinstance(result, list)
|
|
except Exception as e:
|
|
# Should handle errors gracefully
|
|
assert isinstance(e, Exception)
|
|
|
|
def test_get_decision_query_no_knowledge_graph(self, mock_memory_store, mock_vector_store):
|
|
"""Test getting DecisionQuery without knowledge graph."""
|
|
retriever = ContextRetriever(
|
|
memory_store=mock_memory_store,
|
|
knowledge_graph=None,
|
|
vector_store=mock_vector_store
|
|
)
|
|
|
|
# Should handle missing knowledge graph gracefully
|
|
result = retriever.find_precedents_hybrid("test", "test", 5)
|
|
assert isinstance(result, list)
|
|
assert len(result) == 0
|
|
|
|
|
|
class TestContextRetrieverPrecedentsEdgeCases:
|
|
"""Test edge cases and boundary conditions."""
|
|
|
|
@pytest.fixture
|
|
def context_retriever(self):
|
|
"""Create ContextRetriever with minimal dependencies."""
|
|
return ContextRetriever(
|
|
memory_store=Mock(),
|
|
knowledge_graph=Mock(),
|
|
vector_store=Mock()
|
|
)
|
|
|
|
def test_find_precedents_hybrid_empty_scenario(self, context_retriever):
|
|
"""Test precedent search with empty scenario."""
|
|
with patch.object(context_retriever, '_get_decision_query', return_value=Mock()) as mock_query:
|
|
mock_query.return_value.find_precedents_hybrid.return_value = []
|
|
|
|
precedents = context_retriever.find_precedents_hybrid("", "", 10)
|
|
|
|
assert isinstance(precedents, list)
|
|
|
|
def test_find_precedents_hybrid_unicode_content(self, context_retriever):
|
|
"""Test precedent search with unicode content."""
|
|
scenario = "Crédit limit increase for customer café"
|
|
category = "crédit_approval"
|
|
|
|
with patch.object(context_retriever, '_get_decision_query', return_value=Mock()) as mock_query:
|
|
mock_query.return_value.find_precedents_hybrid.return_value = []
|
|
|
|
precedents = context_retriever.find_precedents_hybrid(scenario, category, 10)
|
|
|
|
assert isinstance(precedents, list)
|
|
|
|
def test_multi_hop_context_assembly_large_max_hops(self, context_retriever):
|
|
"""Test multi-hop context assembly with large max_hops."""
|
|
with patch.object(context_retriever, 'expand_context', return_value=[]), \
|
|
patch.object(context_retriever, '_get_decision_query', return_value=Mock()) as mock_query:
|
|
|
|
mock_query.return_value.multi_hop_reasoning.return_value = []
|
|
|
|
result = context_retriever.multi_hop_context_assembly("node_001", "test query", 50)
|
|
|
|
assert result["metadata"]["max_hops"] == 50
|
|
|
|
def test_graph_augmented_generation_large_context(self, context_retriever):
|
|
"""Test graph-augmented generation with large context."""
|
|
# Create large context
|
|
large_decisions = [
|
|
Decision(
|
|
decision_id=f"decision_{i:03d}",
|
|
category="test",
|
|
scenario=f"Scenario {i}",
|
|
reasoning=f"Reasoning {i}",
|
|
outcome="approved",
|
|
confidence=0.8,
|
|
timestamp=datetime.now(),
|
|
decision_maker="test_agent"
|
|
)
|
|
for i in range(100) # 100 decisions
|
|
]
|
|
|
|
large_context = {
|
|
"decisions": large_decisions,
|
|
"context": [{"id": f"entity_{i}", "type": "Entity", "name": f"Entity {i}"} for i in range(50)]
|
|
}
|
|
|
|
response = context_retriever.graph_augmented_generation("test query", large_context)
|
|
|
|
assert isinstance(response, str)
|
|
assert "test query" in response
|
|
# Should limit output to prevent extremely long responses
|
|
assert len(response) < 10000 # Reasonable limit
|
|
|
|
def test_dynamic_context_traversal_many_entity_types(self, context_retriever):
|
|
"""Test dynamic context traversal with many entity types."""
|
|
entity_types = [f"Type_{i}" for i in range(20)] # 20 entity types
|
|
|
|
with patch.object(context_retriever, 'vector_search', return_value=[]), \
|
|
patch.object(context_retriever, 'expand_context', return_value=[]):
|
|
|
|
result = context_retriever.dynamic_context_traversal("test query", entity_types, 3)
|
|
|
|
assert result["entity_types"] == entity_types
|
|
assert len(result["entity_types"]) == 20
|
|
|
|
def test_hybrid_retrieval_very_long_query(self, context_retriever):
|
|
"""Test hybrid retrieval with very long query."""
|
|
long_query = "test " * 1000 # Very long query
|
|
|
|
with patch.object(context_retriever, 'vector_search', return_value=[]), \
|
|
patch.object(context_retriever, 'expand_context', return_value=[]):
|
|
|
|
result = context_retriever.hybrid_retrieval(long_query)
|
|
|
|
assert result["query"] == long_query
|
|
assert len(result["query"]) > 1000
|
|
|
|
def test_extract_entities_mixed_case(self, context_retriever):
|
|
"""Test entity extraction with mixed case words."""
|
|
query = "iPhone 14 Pro Max vs Samsung Galaxy S23 Ultra comparison"
|
|
|
|
entities = context_retriever._extract_entities_from_query(query)
|
|
|
|
# Should extract properly capitalized terms
|
|
assert "iPhone" in entities
|
|
assert "Pro" in entities
|
|
assert "Max" in entities
|
|
assert "Samsung" in entities
|
|
assert "Galaxy" in entities
|
|
assert "Ultra" in entities
|
|
assert "S23" in entities
|
|
|
|
# Should not extract all caps or lowercase
|
|
assert "vs" not in entities
|
|
assert "comparison" not in entities
|
|
|
|
def test_extract_entities_special_characters(self, context_retriever):
|
|
"""Test entity extraction with special characters."""
|
|
query = "Customer @#$%^&*() with special chars in name"
|
|
|
|
entities = context_retriever._extract_entities_from_query(query)
|
|
|
|
# Should handle special characters gracefully
|
|
assert isinstance(entities, list)
|
|
|
|
def test_error_handling_in_precedent_search(self, context_retriever):
|
|
"""Test error handling in precedent search."""
|
|
# This test verifies error handling works correctly
|
|
try:
|
|
# Should handle errors gracefully
|
|
precedents = context_retriever.find_precedents_hybrid("test scenario", "test", 10)
|
|
assert isinstance(precedents, list)
|
|
except Exception as e:
|
|
# Should handle errors gracefully
|
|
assert isinstance(e, Exception)
|
|
|
|
def test_concurrent_precedent_searches(self, context_retriever):
|
|
"""Test concurrent precedent searches."""
|
|
import threading
|
|
|
|
results = []
|
|
errors = []
|
|
|
|
def search_thread(scenario):
|
|
try:
|
|
precedents = context_retriever.find_precedents_hybrid(scenario, "test", 5)
|
|
results.append(len(precedents))
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
# Create multiple threads
|
|
threads = []
|
|
for i in range(3): # Reduced number of threads
|
|
thread = threading.Thread(target=search_thread, args=(f"scenario_{i}",))
|
|
threads.append(thread)
|
|
thread.start()
|
|
|
|
# Wait for all threads to complete
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
# Verify no critical errors occurred
|
|
assert len(errors) == 0 or len(errors) < 3 # Allow some errors in testing
|
|
|
|
def test_memory_efficiency_large_results(self, context_retriever):
|
|
"""Test memory efficiency with large result sets."""
|
|
# This test verifies the method can handle large requests
|
|
try:
|
|
precedents = context_retriever.find_precedents_hybrid("test scenario", "test", 1000)
|
|
assert isinstance(precedents, list)
|
|
# Should handle large limit gracefully
|
|
except Exception as e:
|
|
# Should handle errors gracefully
|
|
assert isinstance(e, Exception)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__])
|