Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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="<YOUR API KEY>"
api_key="<YOUR 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
Expand Down
2 changes: 2 additions & 0 deletions asknews_sdk/api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion asknews_sdk/dto/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
]

Expand Down Expand Up @@ -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"
Expand Down
39 changes: 39 additions & 0 deletions tests/api/test_chat.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from inspect import isasyncgen, isgenerator

import pytest
Expand All @@ -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]):
...

Expand All @@ -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()

Expand Down
43 changes: 39 additions & 4 deletions tests/api/test_chat_model_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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)

Expand All @@ -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)
43 changes: 43 additions & 0 deletions tests/test_readme.py
Original file line number Diff line number Diff line change
@@ -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()
Loading