Spaces:
Running
Running
| from unittest.mock import MagicMock | |
| import pytest | |
| from core.domain.entities.ai_schemas import ( | |
| DebateOutcome, | |
| ForgeHypothesis, | |
| InferenceResponse, | |
| JudgeAction, | |
| SearchPlan, | |
| ) | |
| from tests.helpers.agentic_rag_factory import build_test_agentic_rag_service | |
| # Drives the full agentic RAG / forge pipeline against a live inference engine (no ollama in CI). | |
| pytestmark = pytest.mark.integration | |
| def mock_engine(): | |
| engine = MagicMock() | |
| engine.stream_generate.return_value = iter(["Generated ", "answer."]) | |
| return engine | |
| def mock_rag(): | |
| rag = MagicMock() | |
| rag.hybrid_search.return_value = [ | |
| {"title": "Local Info", "description": "Some local data"} | |
| ] | |
| return rag | |
| def mock_web(): | |
| return MagicMock() | |
| def mock_prompt_manager(): | |
| pm = MagicMock() | |
| pm.get_prompt.return_value = ("prompt", "system") | |
| return pm | |
| def mock_librarian(): | |
| return MagicMock() | |
| def mock_forge(): | |
| return MagicMock() | |
| def mock_uncertainty(): | |
| return MagicMock() | |
| def mock_debate_manager(): | |
| return MagicMock() | |
| def agentic_rag( | |
| mock_engine, | |
| mock_rag, | |
| mock_web, | |
| mock_prompt_manager, | |
| mock_librarian, | |
| mock_forge, | |
| mock_uncertainty, | |
| mock_debate_manager, | |
| ): | |
| service = build_test_agentic_rag_service( | |
| inference_engine=mock_engine, | |
| rag_service=mock_rag, | |
| web_search=mock_web, | |
| prompt_manager=mock_prompt_manager, | |
| llm_service=MagicMock(), | |
| workflow_orchestrator=MagicMock(), | |
| librarian=mock_librarian, | |
| forge=mock_forge, | |
| uncertainty_service=mock_uncertainty, | |
| debate_manager=mock_debate_manager, | |
| ) | |
| # Mock internally initialized agents to avoid extra LLM calls | |
| service.planner = MagicMock() | |
| service.scout = MagicMock() | |
| service.synthesizer = MagicMock() | |
| service.judge = MagicMock() | |
| return service | |
| def test_forge_speculation_e2e( | |
| agentic_rag, | |
| mock_engine, | |
| mock_librarian, | |
| mock_forge, | |
| mock_uncertainty, | |
| mock_debate_manager, | |
| ): | |
| """ | |
| Test end-to-end: Uncertainty triggers Librarian -> Librarian fails -> Forge speculates. | |
| """ | |
| # 1. Setup Complexity and Planner | |
| # In plan_and_solve_stream, _assess_complexity is called. | |
| # But it uses self.llm_service which uses self.inference_engine. | |
| # We can mock _assess_complexity directly or mock the llm_service call. | |
| agentic_rag._assess_complexity = MagicMock(return_value=(0, 0)) | |
| # 2. Setup Planner | |
| agentic_rag.planner.plan.return_value = SearchPlan( | |
| optimized_query="test query", | |
| requires_web=False, | |
| reasoning="Testing speculation", | |
| ) | |
| # 3. Setup Scout | |
| agentic_rag.scout.find_truth_path.return_value = "Initial truth path" | |
| # 5. Setup Uncertainty (trigger Librarian) | |
| mock_uncertainty.measure_confidence.return_value = 0.4 | |
| # 6. Setup Librarian (identify gap but fail fetch) | |
| mock_librarian.identify_gap.return_value = { | |
| "query": "missing detail", | |
| "source_type": "Web", | |
| } | |
| mock_librarian.fetch_data.return_value = None # This triggers SPECULATE | |
| # 7. Setup Forge | |
| mock_forge.generate_hypothesis.return_value = ForgeHypothesis( | |
| hypothesis="Forged hypothesis", | |
| rationale="Deduced from patterns", | |
| confidence=0.8, | |
| ) | |
| # 8. Setup Synthesizer (first pass, then second pass after speculation) | |
| agentic_rag.synthesizer.synthesize_stream.side_effect = [ | |
| iter([InferenceResponse(text="First "), InferenceResponse(text="attempt.")]), | |
| iter( | |
| [ | |
| InferenceResponse(text="Final "), | |
| InferenceResponse(text="answer "), | |
| InferenceResponse(text="with "), | |
| InferenceResponse(text="speculation."), | |
| ] | |
| ), | |
| ] | |
| # 9. Setup Debate Manager (Approve final answer) | |
| mock_debate_manager.conduct_debate.return_value = DebateOutcome( | |
| critiques={}, consensus_action=JudgeAction.APPROVE, final_reasoning="Looks good" | |
| ) | |
| # Execute | |
| events = list( | |
| agentic_rag.plan_and_solve_stream("Will GTA 6 be on PC at launch?", "Game") | |
| ) | |
| # Extract thought contents | |
| thoughts = [e["content"] for e in events if e["type"] == "thought"] | |
| # Assertions | |
| assert any("[Uncertainty] Basse confiance détectée" in t for t in thoughts) | |
| assert any( | |
| "[Librarian] Aucune donnée supplémentaire trouvée. Passage en mode spéculation..." | |
| in t | |
| for t in thoughts | |
| ) | |
| assert any( | |
| "[The Forge] Hypothèse générée : Forged hypothesis" in t for t in thoughts | |
| ) | |
| # Check final answer | |
| final_answer = "".join([e["content"] for e in events if e["type"] == "token"]) | |
| assert "Final answer with speculation." in final_answer | |
| # Verify transitions | |
| states = [t for t in thoughts if "[State Machine]" in t] | |
| assert any("État: RAGState.ACQUIRE_KNOWLEDGE" in s for s in states) | |
| assert any("État: RAGState.SPECULATE" in s for s in states) | |
| assert any("État: RAGState.SYNTHESIZE" in s for s in states) | |
| # Verify that synthesizer was called with the hypothesis in context | |
| # It should be called twice: once before Librarian, once after Forge | |
| assert agentic_rag.synthesizer.synthesize_stream.call_count == 2 | |
| last_call_args = agentic_rag.synthesizer.synthesize_stream.call_args_list[-1] | |
| args, kwargs = last_call_args | |
| # Context is the second positional argument | |
| context = args[1] | |
| assert "Forged hypothesis" in context | |
| assert "DÉDUCTION :" in context | |