| from __future__ import annotations |
|
|
| import asyncio |
| from types import SimpleNamespace |
|
|
| import httpx |
|
|
| from app.routes import chat |
| from app.schemas.chat import WebCitation |
| from app.services.exa_search import ( |
| ExaSearchUnavailable, |
| WebSearchResult, |
| WebSearchSource, |
| search_exa_web, |
| ) |
|
|
|
|
| class _FakeResponse: |
| def raise_for_status(self) -> None: |
| return None |
|
|
| def json(self) -> dict[str, object]: |
| return { |
| "results": [ |
| { |
| "title": "A recent explainer", |
| "url": "https://example.com/explainer", |
| "publishedDate": "2026-07-12T00:00:00.000Z", |
| "highlights": ["A useful secondary explanation."], |
| }, |
| { |
| "title": "Official examination notice", |
| "url": "https://education.gov.in/exam-notice", |
| "publishedDate": "2026-07-13T00:00:00.000Z", |
| "author": "Ministry of Education", |
| "highlights": ["The official notice and its effective date."], |
| }, |
| { |
| "title": "Unsafe result", |
| "url": "javascript:alert(1)", |
| "highlights": ["This must never reach the client."], |
| }, |
| ] |
| } |
|
|
|
|
| class _FakeClient: |
| last_request: dict[str, object] = {} |
|
|
| def __init__(self, **kwargs: object) -> None: |
| self.kwargs = kwargs |
|
|
| def __enter__(self) -> "_FakeClient": |
| return self |
|
|
| def __exit__(self, *args: object) -> None: |
| return None |
|
|
| def post(self, path: str, **kwargs: object) -> _FakeResponse: |
| _FakeClient.last_request = {"path": path, **kwargs} |
| return _FakeResponse() |
|
|
|
|
| def _settings(api_key: str | None = "test-server-key") -> SimpleNamespace: |
| return SimpleNamespace( |
| exa_api_key=api_key, |
| exa_base_url="https://api.exa.ai", |
| exa_search_timeout_seconds=5, |
| exa_search_max_results=6, |
| ) |
|
|
|
|
| def test_exa_normalises_safe_citations_and_ranks_official_first(monkeypatch) -> None: |
| monkeypatch.setattr(httpx, "Client", _FakeClient) |
|
|
| result = search_exa_web("latest official exam notice", settings=_settings()) |
|
|
| assert [source.publisher for source in result.sources] == [ |
| "education.gov.in", |
| "example.com", |
| ] |
| assert result.sources[0].is_official is True |
| assert "Official examination notice" in result.as_prompt_context() |
| assert _FakeClient.last_request["path"] == "/search" |
| request_json = _FakeClient.last_request["json"] |
| assert isinstance(request_json, dict) |
| assert request_json["type"] == "auto" |
| assert request_json["contents"] == {"highlights": True} |
|
|
|
|
| def test_exa_requires_a_server_side_key() -> None: |
| try: |
| search_exa_web("latest exam notice", settings=_settings(api_key=None)) |
| except ExaSearchUnavailable as exc: |
| assert "not configured" in str(exc) |
| else: |
| raise AssertionError("Search must stay disabled without a backend key") |
|
|
|
|
| def test_only_current_information_questions_receive_web_context(monkeypatch) -> None: |
| calls: list[str] = [] |
| source = WebSearchSource( |
| title="Official result", |
| url="https://education.gov.in/result", |
| publisher="education.gov.in", |
| published_date="2026-07-14", |
| author=None, |
| snippet="The official current result.", |
| is_official=True, |
| ) |
|
|
| def fake_search(query: str) -> WebSearchResult: |
| calls.append(query) |
| return WebSearchResult(query=query, sources=(source,)) |
|
|
| monkeypatch.setattr(chat, "search_exa_web", fake_search) |
|
|
| untouched = asyncio.run( |
| chat._prepare_current_info_context( |
| intent="study_explain", |
| question="Explain sound waves", |
| system_prompt="teacher prompt", |
| user_message="Explain sound waves", |
| evidence_label="No source attached", |
| ) |
| ) |
| assert untouched == ( |
| "teacher prompt", |
| "Explain sound waves", |
| "No source attached", |
| [], |
| ) |
| assert calls == [] |
|
|
| prompt, message, label, citations = asyncio.run( |
| chat._prepare_current_info_context( |
| intent="current_info", |
| question="Search the web for the latest official result", |
| system_prompt=chat._SYSTEM_PROMPTS["current_info"], |
| user_message="Search the web for the latest official result", |
| evidence_label="No source attached", |
| ) |
| ) |
| assert calls == ["Search the web for the latest official result"] |
| assert label == "Searched the web" |
| assert isinstance(citations[0], WebCitation) |
| assert citations[0].publisher == "education.gov.in" |
| assert "[1]" in prompt |
| assert "education.gov.in/result" in message |
|
|
|
|
| def test_chat_intent_router_keeps_web_search_explicit() -> None: |
| assert chat._detect_intent("Search the web for today's CBSE notice", False) == "current_info" |
| assert chat._detect_intent("What is the latest Kerala SSLC exam timetable?", False) == "current_info" |
| assert chat._detect_intent("What is the current Kerala SSLC syllabus?", False) == "current_info" |
| assert chat._detect_intent("Explain the current in an electric circuit", False) == "study_explain" |
| assert chat._detect_intent("Update my study plan for physics", False) == "study_explain" |
| assert chat._detect_intent("What should I study today?", False) == "study_explain" |
| assert chat._detect_intent("Help me revise my recent mistakes", False) == "study_explain" |
| assert chat._detect_intent("Explain Sound Waves from my textbook", True) == "source_answer" |
| |
| |
| |
| assert chat._detect_intent("Summarize today's lesson from this PDF", True) == "notes" |
| assert chat._detect_intent("Search the web for today's notice", True) == "current_info" |
|
|
|
|
| def test_current_information_search_failure_is_visible(monkeypatch) -> None: |
| def unavailable(_query: str) -> WebSearchResult: |
| raise ExaSearchUnavailable("Web search could not be completed.") |
|
|
| monkeypatch.setattr(chat, "search_exa_web", unavailable) |
|
|
| prompt, message, label, citations = asyncio.run( |
| chat._prepare_current_info_context( |
| intent="current_info", |
| question="Search the web for today's official notice", |
| system_prompt=chat._SYSTEM_PROMPTS["current_info"], |
| user_message="Search the web for today's official notice", |
| evidence_label="No source attached", |
| ) |
| ) |
|
|
| assert prompt == chat._SYSTEM_PROMPTS["current_info"] |
| assert message == "Search the web for today's official notice" |
| assert label == "Web search unavailable" |
| assert citations == [] |
|
|