diff --git a/README.md b/README.md index 622c1e5..72ba28d 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,32 @@ pip install asknews ## Usage +- Use `ask.news.search_news()` for a fast, surgical, single-search lookup when you already know the keywords or entities and do not need iterative discovery. +- Use DeepNews (`ask.chat.get_deep_news()` in this SDK) to attach your agent to deep, iterative research across news and other sources: finding and following leads, identifying unknown entities, and connecting evidence across sources. + ```python from asknews_sdk import AskNewsSDK ask = AskNewsSDK( - api_key="" + api_key="", scopes=["news", "chat", "stories", "analytics"] ) -query = "Effect of fed policy on tech sector" +query = "NVIDIA earnings" -# prompt-optimized string ready to go for any LLM: +# Fast lookup: prompt-optimized news context for your LLM. news_context = ask.news.search_news(query).as_string + +# Iterative discovery across news and other sources. +research = ask.chat.get_deep_news( + messages=[{"role": "user", "content": "Find and follow leads on AI chip supply risks."}], + sources=["asknews", "google", "wiki"], + engine="v1.5", +) +print(research.choices[0].message.content) ``` -And you will have a prompt-optimized string ready to go for any LLM. The API doesn't stop there, explore a wide range of endpoints: +The API doesn't stop there, explore a wide range of endpoints: - /stories, high level event tracking and state of the art article clustering - /forecasts, industry leading forecasting on any real-time event diff --git a/asknews_sdk/api/chat.py b/asknews_sdk/api/chat.py index c360c15..de342b4 100644 --- a/asknews_sdk/api/chat.py +++ b/asknews_sdk/api/chat.py @@ -68,10 +68,12 @@ "claude-opus-4-5-20251101", "claude-sonnet-4-6", "claude-sonnet-5", + "claude-sonnet-5-5", "gemini-2.5-flash", "o3", "gpt-5.6-sol", "gpt-6-sol", + "gpt-6.1-sol", "claude-opus-4-8", "claude-opus-5-5", "gpt-5.6-terra", diff --git a/asknews_sdk/dto/alert.py b/asknews_sdk/dto/alert.py index dc9abf1..03cb274 100644 --- a/asknews_sdk/dto/alert.py +++ b/asknews_sdk/dto/alert.py @@ -26,6 +26,7 @@ "gemini-3-pro", "claude-sonnet-4-6", "claude-sonnet-5", + "claude-sonnet-5-5", "claude-opus-4-6", "claude-fable-5", "claude-fable-5-1", @@ -35,7 +36,9 @@ "open-source-best", "gemini-3-flash", "gpt-5.6-sol", + "gpt-6.1-sol", "claude-opus-4-8", + "claude-opus-5-5", "gpt-5.6-terra", ] @@ -72,7 +75,8 @@ "claude-sonnet-5", "gpt-5.6-sol", "claude-opus-4-8", - "gpt-5.6-terra" "meta-llama/Meta-Llama-3.1-405B-Instruct", + "gpt-5.6-terra", + "meta-llama/Meta-Llama-3.1-405B-Instruct", "meta-llama/Meta-Llama-3.3-70B-Instruct", ] AlertReportModelDefault: AlertReportModel = "claude-sonnet-4-6" diff --git a/tests/api/test_chat.py b/tests/api/test_chat.py index 618f0b4..6b33d6e 100644 --- a/tests/api/test_chat.py +++ b/tests/api/test_chat.py @@ -1,3 +1,4 @@ +import json from inspect import isasyncgen, isgenerator import pytest @@ -12,13 +13,19 @@ HeadlineQuestionsResponse, ListModelResponse, ) +from asknews_sdk.dto.deepnews import CreateDeepNewsResponse from asknews_sdk.utils import build_accept_header +from tests.api.test_chat_model_types import ADVANCED_DEEPNEWS_MODELS class MockCreateChatCompletionResponse(ModelFactory[CreateChatCompletionResponse]): ... +class MockCreateDeepNewsResponse(ModelFactory[CreateDeepNewsResponse]): + sources = {"news": [], "web": [], "charts": []} + + class MockCreateChatCompletionResponseStream(ModelFactory[CreateChatCompletionResponseStream]): ... @@ -41,6 +48,38 @@ def async_chat_api(async_api_client: AsyncAPIClient): return AsyncChatAPI(async_api_client) +@pytest.mark.parametrize("model", ADVANCED_DEEPNEWS_MODELS) +def test_sync_deepnews_model_request(sync_chat_api: ChatAPI, response_mock: MockRouter, model): + mock_response = MockCreateDeepNewsResponse.build(model=model) + route = response_mock.post("/v1/chat/deepnews").respond( + content=mock_response.model_dump_json() + ) + + response = sync_chat_api.get_deep_news( + messages=[{"role": "user", "content": "query"}], model=model + ) + + assert response.model_dump(mode="json") == mock_response.model_dump(mode="json") + assert json.loads(route.calls.last.request.content)["model"] == model + + +@pytest.mark.parametrize("model", ADVANCED_DEEPNEWS_MODELS) +async def test_async_deepnews_model_request( + async_chat_api: AsyncChatAPI, response_mock: MockRouter, model +): + mock_response = MockCreateDeepNewsResponse.build(model=model) + route = response_mock.post("/v1/chat/deepnews").respond( + content=mock_response.model_dump_json() + ) + + response = await async_chat_api.get_deep_news( + messages=[{"role": "user", "content": "query"}], model=model + ) + + assert response.model_dump(mode="json") == mock_response.model_dump(mode="json") + assert json.loads(route.calls.last.request.content)["model"] == model + + def test_sync_chat_api_get_chat_completions(sync_chat_api: ChatAPI, response_mock: MockRouter): mock_response = MockCreateChatCompletionResponse.build() diff --git a/tests/api/test_chat_model_types.py b/tests/api/test_chat_model_types.py index 7e9e1b3..9584cb1 100644 --- a/tests/api/test_chat_model_types.py +++ b/tests/api/test_chat_model_types.py @@ -3,7 +3,7 @@ from typing import get_args, get_type_hints import pytest -from pydantic import BaseModel +from pydantic import BaseModel, TypeAdapter, ValidationError from asknews_sdk.api.chat import AsyncChatAPI, ChatAPI, ChatModel, DeepNewsModel from asknews_sdk.dto import alert as alert_dto @@ -12,7 +12,8 @@ GPT_6_ASTRA = "gpt-6-astra" CLAUDE_FABLE_MODELS = ("claude-fable-5", "claude-fable-5-1") -ADVANCED_DEEPNEWS_MODELS = ("claude-opus-5-5", "gpt-6-sol") +NEW_DEEPNEWS_MODELS = ("claude-sonnet-5-5", "claude-opus-5-5", "gpt-6.1-sol") +ADVANCED_DEEPNEWS_MODELS = (*NEW_DEEPNEWS_MODELS, "gpt-6-sol") def test_gpt_6_astra_is_a_direct_deepnews_model_only(): @@ -45,11 +46,10 @@ def test_gpt_6_astra_is_absent_from_alert_model_contracts(): @pytest.mark.parametrize("model", ADVANCED_DEEPNEWS_MODELS) @pytest.mark.parametrize("api_type", [ChatAPI, AsyncChatAPI]) -def test_advanced_models_are_scoped_to_deepnews_and_preserve_alert_contracts(model, api_type): +def test_advanced_models_are_scoped_to_deepnews_and_preserve_legacy_contracts(model, api_type): assert model in get_args(DeepNewsModel) assert model in get_args(get_type_hints(api_type.get_deep_news)["model"]) assert model not in get_args(ChatModel) - assert model not in get_args(alert_dto.DeepNewsModel) assert model not in get_args(alert_dto.CheckAlertModel) assert model not in get_args(alert_dto.AlertReportModel) @@ -61,8 +61,43 @@ def test_advanced_deepnews_models_serialize_in_requests(model): assert request.model_dump(mode="json")["model"] == model +@pytest.mark.parametrize("model", NEW_DEEPNEWS_MODELS) +@pytest.mark.parametrize( + "request_type", [alert_dto.CreateAlertRequest, alert_dto.UpdateAlertRequest] +) +def test_active_alert_source_and_report_models_validate_and_serialize(model, request_type): + assert model in get_args(alert_dto.DeepNewsModel) + request = request_type( + cron="0 9 * * *", + triggers=[], + sources=[{"identifier": "deepnews", "params": {"model": model}}], + report={"identifier": "deepnews", "params": {"model": model}}, + ) + payload = json.loads(request.model_dump_json()) + + assert payload["sources"][0]["params"]["model"] == model + assert payload["report"]["params"]["model"] == model + + +@pytest.mark.parametrize("model_type", [DeepNewsModel, alert_dto.DeepNewsModel]) +def test_deepnews_model_contracts_reject_misspelled_gpt_id(model_type): + with pytest.raises(ValidationError): + TypeAdapter(model_type).validate_python("gpt-sol-6.1") + + +def test_active_alert_model_defaults_are_unchanged(): + assert alert_dto.DeepNewsSourceParams().model == "gemini-3-flash" + assert alert_dto.DeepNewsReportParams().model == "claude-sonnet-4-6" + assert alert_dto.LegacyReportRequest().model == "claude-sonnet-4-6" + + @pytest.mark.parametrize("model", CLAUDE_FABLE_MODELS) def test_claude_fable_models_match_deepnews_and_alert_report_contracts(model): assert model in get_args(DeepNewsModel) assert model in get_args(alert_dto.DeepNewsModel) assert model in get_args(alert_dto.AlertReportModel) + + +@pytest.mark.parametrize("model", ["gpt-5.6-terra", "meta-llama/Meta-Llama-3.1-405B-Instruct"]) +def test_alert_report_models_are_separate_literals(model): + assert model in get_args(alert_dto.AlertReportModel) diff --git a/tests/test_readme.py b/tests/test_readme.py new file mode 100644 index 0000000..ef1463e --- /dev/null +++ b/tests/test_readme.py @@ -0,0 +1,43 @@ +import json +import re +from pathlib import Path + +from tests.api.test_chat import MockCreateDeepNewsResponse +from tests.api.test_news import MockSearchResponse + + +def test_readme_usage_example_runs_with_mocked_responses(response_mock, capsys): + readme = (Path(__file__).resolve().parents[1] / "README.md").read_text() + examples = re.findall(r"```python\n(.*?)```", readme, re.DOTALL) + assert examples + + news_response = MockSearchResponse.build(as_string="News context") + deepnews_response = MockCreateDeepNewsResponse.build() + news_route = response_mock.get("/v1/news/search").respond( + content=news_response.model_dump_json() + ) + research_route = response_mock.post("/v1/chat/deepnews").respond( + content=deepnews_response.model_dump_json() + ) + + namespace = {} + try: + for example in examples: + exec(compile(example, "README.md", "exec"), namespace) + assert namespace["news_context"] == "News context" + assert namespace["research"].model_dump(mode="json") == deepnews_response.model_dump( + mode="json" + ) + assert capsys.readouterr().out.strip() == ( + deepnews_response.choices[0].message.content.strip() + ) + assert news_route.calls.last.request.url.params["query"] == namespace["query"] + payload = json.loads(research_route.calls.last.request.content) + assert payload["messages"][0]["role"] == "user" + assert payload["sources"] == ["asknews", "google", "wiki"] + assert payload["engine"] == "v1.5" + assert payload["stream"] is False + assert payload["model"] == "claude-sonnet-4-6" + finally: + if "ask" in namespace: + namespace["ask"].close()