| """Tests for the generation layer: LLM client, RAG chain, and cross-doc chain.""" |
|
|
| from __future__ import annotations |
|
|
| from unittest.mock import AsyncMock, MagicMock, patch |
|
|
| import pytest |
|
|
|
|
| def test_llm_client_routes_model_correctly() -> None: |
| """Test that LLM client routes to correct model based on question type.""" |
| with patch("app.core.generation.llm_client.Groq"): |
| from app.core.generation.llm_client import LLMClient |
| import app.core.generation.llm_client as llm_module |
| llm_module._llm_client = None |
|
|
| client = LLMClient() |
| assert client.route_model("standard") == "llama-3.3-70b-versatile" |
| assert client.route_model("reasoning") == "deepseek-r1-distill-llama-70b" |
| assert client.route_model("compare") == "deepseek-r1-distill-llama-70b" |
|
|
|
|
| def test_llm_client_json_extraction() -> None: |
| """Test JSON extraction from various response formats.""" |
| from app.core.generation.llm_client import _extract_json |
|
|
| result = _extract_json('{"key": "value"}') |
| assert result == {"key": "value"} |
|
|
| result = _extract_json('```json\n{"key": "value"}\n```') |
| assert result == {"key": "value"} |
|
|
| result = _extract_json('Here is the result: {"score": 0.85}') |
| assert result["score"] == 0.85 |
|
|
|
|
| def test_llm_client_json_extraction_fails_gracefully() -> None: |
| """Test that JSON extraction raises ValueError for invalid input.""" |
| from app.core.generation.llm_client import _extract_json |
| with pytest.raises(ValueError, match="Could not extract valid JSON"): |
| _extract_json("This is not JSON at all") |
|
|
|
|
| @pytest.mark.asyncio |
| @patch("app.core.generation.rag_chain.get_llm_client") |
| @patch("app.core.generation.rag_chain.retrieve_with_self_correction") |
| @patch("app.core.generation.rag_chain.get_vector_store") |
| async def test_standard_rag_chain_returns_answer( |
| mock_vs: MagicMock, mock_retrieve: MagicMock, mock_llm: MagicMock, |
| ) -> None: |
| """Test that RAG chain returns a properly structured response.""" |
| mock_retrieve.return_value = ( |
| [{"id": "1", "score": 0.9, "payload": { |
| "text": "ML is a subset of AI.", "doc_id": "doc-1", |
| "filename": "paper.pdf", "chunk_index": 0, |
| }}], 0.85, "What is ML?", |
| ) |
| mock_client = MagicMock() |
| mock_client.route_model.return_value = "llama-3.3-70b-versatile" |
| mock_client.generate = AsyncMock(return_value="ML is a subset of AI.") |
| mock_client.primary_model = "llama-3.3-70b-versatile" |
| mock_client.reasoning_model = "deepseek-r1-distill-llama-70b" |
| mock_llm.return_value = mock_client |
| mock_store = MagicMock() |
| mock_store.list_documents = AsyncMock(return_value=[]) |
| mock_vs.return_value = mock_store |
|
|
| from app.core.generation.rag_chain import run_rag_chain |
| from app.models.schemas import QueryRequest |
| request = QueryRequest(question="What is ML?") |
| response = await run_rag_chain(request) |
|
|
| assert response.answer is not None |
| assert response.model_used == "llama-3.3-70b-versatile" |
| assert len(response.sources) == 1 |
|
|
|
|
| @pytest.mark.asyncio |
| @patch("app.core.generation.cross_doc_chain.get_llm_client") |
| @patch("app.core.generation.cross_doc_chain.get_vector_store") |
| @patch("app.core.generation.cross_doc_chain.embed_query") |
| async def test_cross_doc_chain_returns_structured_output( |
| mock_embed: MagicMock, mock_vs: MagicMock, mock_llm: MagicMock, |
| ) -> None: |
| """Test that cross-doc chain returns comparison with agreements.""" |
| mock_embed.return_value = [0.1] * 3072 |
| mock_store = MagicMock() |
| mock_store.search = AsyncMock(side_effect=[ |
| [{"id": "1", "score": 0.9, "payload": { |
| "text": "Paper A uses CNN.", "doc_id": "doc-1", |
| "filename": "a.pdf", "chunk_index": 0}}], |
| [{"id": "2", "score": 0.85, "payload": { |
| "text": "Paper B uses RNN.", "doc_id": "doc-2", |
| "filename": "b.pdf", "chunk_index": 0}}], |
| ]) |
| mock_vs.return_value = mock_store |
| mock_client = MagicMock() |
| mock_client.reasoning_model = "deepseek-r1-distill-llama-70b" |
| mock_client.generate = AsyncMock(return_value=( |
| "Comparison text\n" |
| 'AGREEMENTS_JSON: ["Both classify"]\n' |
| 'CONTRADICTIONS_JSON: ["Different arch"]' |
| )) |
| mock_llm.return_value = mock_client |
|
|
| from app.core.generation.cross_doc_chain import run_cross_doc_chain |
| from app.models.schemas import CompareRequest |
| request = CompareRequest(question="Compare", doc_ids=["doc-1", "doc-2"], aspect="methodology") |
| response = await run_cross_doc_chain(request) |
|
|
| assert response.comparison is not None |
| assert response.model_used == "deepseek-r1-distill-llama-70b" |
| assert mock_store.search.call_count == 2 |
|
|