from __future__ import annotations import asyncio from types import SimpleNamespace import backend.config as backend_config import backend.routes_chat as routes_chat class _FakeEmbedClient: def embed_query(self, _text: str) -> list[float]: return [0.01, 0.02, 0.03] class _FakeEmbeddingQueryModule: @staticmethod def load_client(*_args, **_kwargs) -> _FakeEmbedClient: return _FakeEmbedClient() class _FakeRetrieverModule: @staticmethod def get_unified_context_for_llm(**_kwargs) -> dict: return { "document_chunks": [ { "chunk_id": "doc_chunk_1", "doc_id": "project-proposal-streamcore-pdf__027b5525", "document_name": "Project Proposal StreamCore.pdf", "text": "Deliver StreamLoader that reads long files by memory mapping.", "section": "page_1", "page": 1, "score": 2.0, "source_path": "Project Proposal StreamCore.pdf", } ], "related_chunks": [], "memory_chunks": [ { "chunk_id": "mem_chunk_1", "doc_id": "shared_memory", "text": 'Query: "old"\nAnswer: old memory text that should not leak.', "score": 1.2, "section": "conversation_memory", "page": 1, } ], "stats": {"doc_count": 1, "memory_count": 1, "related_count": 0}, "doc_count": 1, "memory_count": 1, "related_count": 0, "memory_raw_count": 1, "memory_dropped_count": 0, } @staticmethod def retrieve_dense(**_kwargs) -> dict: return { "retrieved_chunks": [ { "chunk_id": "doc_chunk_1", "doc_id": "project-proposal-streamcore-pdf__027b5525", "text": "Deliver StreamLoader that reads long files by memory mapping.", "score": 2.0, } ] } @staticmethod def retrieve_sparse(**_kwargs) -> dict: return { "retrieved_chunks": [ { "chunk_id": "doc_chunk_1", "doc_id": "project-proposal-streamcore-pdf__027b5525", "text": "Deliver StreamLoader that reads long files by memory mapping.", "score": 1.8, } ] } @staticmethod def rerank(_query: str, candidates: list[dict], top_n: int = 5) -> list[dict]: return candidates[:top_n] class _FakeGenerationModule: @staticmethod def generate_answer(input_data: dict) -> str: query = input_data["query"] return f""" Query: "{query}" Answer: Answer: 1. Deliver StreamLoader that reads long files by memory mapping. 2. Deliver StreamTrainer for chunk-based training. 2. Deliver StreamTrainer for chunk-based training. [shared_memory] Query: "old" [shared_memory] Answer: old memory answer fragment Evidence: Deliver StreamLoader that reads long files by memory mapping. """.strip() class _FakeMemoryModule: @staticmethod def parse_LLM_output(_raw_output: str) -> dict: return { "answer": 'Answer:\n[shared_memory] Query: "old"\nrepeated parser answer', "evidence": [ "Deliver StreamLoader that reads long files by memory mapping.", 'Query: "old"\nAnswer: old memory text that should not leak.', ], } @staticmethod def store_memory_sync(*_args, **_kwargs): return None class _FakeIndex: @staticmethod def query(**_kwargs) -> dict: return {"matches": []} class _FakePineconeClient: @staticmethod def get_index() -> _FakeIndex: return _FakeIndex() @staticmethod def retrieve_similar(*_args, **_kwargs) -> dict: return {"retrieved_chunks": []} class _FakePineconeModule: @staticmethod def get_client() -> _FakePineconeClient: return _FakePineconeClient() class _FakeVectordbConfig: RERANKER_ENABLED = False RERANKER_TOP_N = 10 CONVERSATION_MEMORY_NAMESPACE = "conversation_memory" def test_chat_answer_is_clean_and_sources_preserved(monkeypatch): modules = { "embedding_query": _FakeEmbeddingQueryModule, "vectordb_retriever": _FakeRetrieverModule, "generation": _FakeGenerationModule, "memory": _FakeMemoryModule, "vectordb_pinecone": _FakePineconeModule, "vectordb_config": _FakeVectordbConfig, } monkeypatch.setattr(backend_config, "get_stage_modules", lambda: modules) monkeypatch.setattr(routes_chat, "resolve_user", lambda _ip: "user_1") monkeypatch.setattr(routes_chat, "audit_event", lambda *_args, **_kwargs: None) monkeypatch.setattr( routes_chat, "write_prompt_reports", lambda **_kwargs: {"retrieval_report": "x", "generation_report": "y"}, ) env_map = { "TOP_K": 5, "MEMORY_TOP_K": 3, "RERANKER_TOP_N": 5, } monkeypatch.setattr( backend_config, "env_int", lambda name, default=0: int(env_map.get(name, default)), ) monkeypatch.setattr( backend_config, "env_value", lambda _name, default="": default, ) request = SimpleNamespace(client=SimpleNamespace(host="127.0.0.1")) payload = routes_chat.ChatRequest( query="tell me what are the objectives list them all numbered and the timeline for the twenty weeks", debug=False, use_multihop=False, ) response = asyncio.run(routes_chat.chat(request, payload)) assert "sources" in response and response["sources"] assert response["sources"][0]["text"] == "Deliver StreamLoader that reads long files by memory mapping." assert all(item.get("doc_id") != "shared_memory" for item in response["sources"]) assert all(item.get("section") != "conversation_memory" for item in response["sources"]) answer = response["answer"] assert "Answer:" not in answer assert "Query:" not in answer assert answer.count("Evidence used:") == 1 assert "1. Deliver StreamLoader that reads long files by memory mapping. [Project Proposal StreamCore.pdf" in answer assert "[shared_memory]" not in answer assert "conversation_memory" not in answer assert "old memory answer fragment" not in answer assert answer.count("2. Deliver StreamTrainer for chunk-based training.") == 1