Skip to content

Commit 17fa3f7

Browse files
authored
Merge pull request #57 from emergentmethods/fix/add-deep-news
fix: Add DeepNews endpoint
2 parents d62fc2a + dbc3a9b commit 17fa3f7

4 files changed

Lines changed: 322 additions & 0 deletions

File tree

asknews_sdk/api/chat.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import Any, AsyncIterator, Dict, Iterator, List, Literal, Optional, Union
22
from uuid import UUID
33

4+
from pydantic import TypeAdapter
5+
46
from asknews_sdk.api.base import BaseAPI
57
from asknews_sdk.dto.alert import AlertLog, AlertResponse, CreateAlertRequest, UpdateAlertRequest
68
from asknews_sdk.dto.chat import (
@@ -14,6 +16,13 @@
1416
WebSearchResponse,
1517
)
1618
from asknews_sdk.dto.common import PaginatedResponse
19+
from asknews_sdk.dto.deepnews import (
20+
CreateDeepNewsRequest,
21+
CreateDeepNewsResponse,
22+
CreateDeepNewsResponseStream,
23+
CreateDeepNewsResponseStreamChunk,
24+
CreateDeepNewsResponseStreamSource,
25+
)
1726
from asknews_sdk.response import EventSource
1827

1928

@@ -444,6 +453,77 @@ def list_alert_logs(
444453
)
445454
return PaginatedResponse[AlertLog].model_validate(response.content)
446455

456+
def get_deep_news(
457+
self,
458+
messages: List[Dict[str, str]],
459+
model: Literal[
460+
"claude-3-7-sonnet-latest",
461+
"deepseek",
462+
"o3-mini",
463+
] = "deepseek",
464+
stream: bool = False,
465+
inline_citations: Literal["markdown_link", "numbered", "none"] = "markdown_link",
466+
append_references: bool = True,
467+
asknews_watermark: bool = True,
468+
journalist_mode: bool = True,
469+
conversational_awareness: bool = False,
470+
filter_params: Optional[Dict] = None,
471+
sources: Optional[List[str]] = None,
472+
search_depth: int = 3,
473+
max_depth: int = 5,
474+
return_sources: bool = True,
475+
*,
476+
http_headers: Optional[Dict] = None,
477+
) -> Union[CreateDeepNewsResponse, Iterator[CreateDeepNewsResponseStream]]:
478+
"""
479+
Get deep news research!
480+
481+
https://docs.asknews.app/en/reference#post-/v1/openai/chat/deepnews
482+
"""
483+
response = self.client.request(
484+
method="POST",
485+
endpoint="/v1/chat/deepnews",
486+
body=CreateDeepNewsRequest(
487+
messages=messages,
488+
model=model,
489+
stream=stream,
490+
inline_citations=inline_citations,
491+
append_references=append_references,
492+
asknews_watermark=asknews_watermark,
493+
journalist_mode=journalist_mode,
494+
conversational_awareness=conversational_awareness,
495+
filter_params=filter_params,
496+
sources=sources if sources else ["asknews"],
497+
search_depth=search_depth,
498+
max_depth=max_depth,
499+
return_sources=return_sources,
500+
).model_dump(mode="json"),
501+
headers={
502+
**(http_headers or {}),
503+
"Content-Type": CreateDeepNewsRequest.__content_type__,
504+
},
505+
accept=[
506+
(CreateDeepNewsResponse.__content_type__, 1.0),
507+
(CreateDeepNewsResponseStreamChunk.__content_type__, 1.0),
508+
(CreateDeepNewsResponseStreamSource.__content_type__, 1.0),
509+
],
510+
stream=stream,
511+
stream_type="lines",
512+
)
513+
514+
if stream:
515+
516+
def _stream():
517+
for event in EventSource.from_api_response(response):
518+
if event.content == "[DONE]":
519+
break
520+
521+
yield TypeAdapter(CreateDeepNewsResponseStream).validate_json(event.content)
522+
523+
return _stream()
524+
else:
525+
return CreateDeepNewsResponse.model_validate(response.content)
526+
447527

448528
class AsyncChatAPI(BaseAPI):
449529
"""
@@ -870,3 +950,72 @@ async def list_alert_logs(
870950
query={"page": page, "per_page": per_page, "all": all},
871951
)
872952
return PaginatedResponse[AlertLog].model_validate(response.content)
953+
954+
async def get_deep_news(
955+
self,
956+
messages: List[Dict[str, str]],
957+
model: Literal[
958+
"claude-3-7-sonnet-latest",
959+
"deepseek",
960+
"o3-mini",
961+
] = "deepseek",
962+
stream: bool = False,
963+
inline_citations: Literal["markdown_link", "numbered", "none"] = "markdown_link",
964+
append_references: bool = True,
965+
asknews_watermark: bool = True,
966+
journalist_mode: bool = True,
967+
conversational_awareness: bool = False,
968+
filter_params: Optional[Dict] = None,
969+
sources: Optional[List[str]] = None,
970+
search_depth: int = 3,
971+
max_depth: int = 5,
972+
*,
973+
http_headers: Optional[Dict] = None,
974+
) -> Union[CreateDeepNewsResponse, AsyncIterator[CreateDeepNewsResponseStream]]:
975+
"""
976+
Get deep news research!
977+
978+
https://docs.asknews.app/en/reference#post-/v1/openai/chat/deepnews
979+
"""
980+
response = await self.client.request(
981+
method="POST",
982+
endpoint="/v1/chat/deepnews",
983+
body=CreateDeepNewsRequest(
984+
messages=messages,
985+
model=model,
986+
stream=stream,
987+
inline_citations=inline_citations,
988+
append_references=append_references,
989+
asknews_watermark=asknews_watermark,
990+
journalist_mode=journalist_mode,
991+
conversational_awareness=conversational_awareness,
992+
filter_params=filter_params,
993+
sources=sources if sources else ["asknews"],
994+
search_depth=search_depth,
995+
max_depth=max_depth,
996+
).model_dump(mode="json"),
997+
headers={
998+
**(http_headers or {}),
999+
"Content-Type": CreateDeepNewsRequest.__content_type__,
1000+
},
1001+
accept=[
1002+
(CreateDeepNewsResponse.__content_type__, 1.0),
1003+
(CreateDeepNewsResponseStreamChunk.__content_type__, 1.0),
1004+
(CreateDeepNewsResponseStreamSource.__content_type__, 1.0),
1005+
],
1006+
stream=stream,
1007+
stream_type="lines",
1008+
)
1009+
1010+
if stream:
1011+
1012+
async def _stream():
1013+
async for event in EventSource.from_api_response(response):
1014+
if event.content == "[DONE]":
1015+
break
1016+
1017+
yield TypeAdapter(CreateDeepNewsResponseStream).validate_json(event.content)
1018+
1019+
return _stream()
1020+
else:
1021+
return CreateDeepNewsResponse.model_validate(response.content)

asknews_sdk/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ def request(
257257
try:
258258
response.raise_for_status()
259259
except HTTPStatusError as e:
260+
if stream:
261+
response.read()
262+
260263
raise_from_response(
261264
APIResponse.from_httpx_response(
262265
response=e.response,
@@ -401,6 +404,9 @@ async def request(
401404
try:
402405
response.raise_for_status()
403406
except HTTPStatusError as e:
407+
if stream:
408+
await response.aread()
409+
404410
raise_from_response(
405411
APIResponse.from_httpx_response(
406412
response=e.response,

asknews_sdk/dto/chat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ class WebSearchResult(BaseModel):
135135
published: str
136136
key_points: List[str]
137137
raw_text: str = ""
138+
as_string_key: Optional[str] = None
138139

139140

140141
class WebSearchResponse(BaseModel):

asknews_sdk/dto/deepnews.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict, List, Literal, Optional, Union
4+
5+
from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag
6+
from typing_extensions import Annotated, TypeAlias
7+
8+
from asknews_sdk.dto.base import BaseSchema
9+
from asknews_sdk.dto.chat import WebSearchResult
10+
from asknews_sdk.dto.news import SearchResponseDictItem
11+
12+
13+
def object_discriminator(v: Any) -> str:
14+
if isinstance(v, dict):
15+
return v.get("object", "")
16+
return getattr(v, "object", "")
17+
18+
19+
def kind_discriminator(v: Any) -> str:
20+
if isinstance(v, dict):
21+
return v.get("kind", "")
22+
return getattr(v, "kind", "")
23+
24+
25+
class DeepNewsSources(BaseModel):
26+
news: Annotated[List[SearchResponseDictItem], Field(title="News")]
27+
web: Annotated[List[WebSearchResult], Field(title="Web")]
28+
29+
30+
class CreateDeepNewsRequestMessage(BaseModel):
31+
role: Annotated[str, Field(title="Role")]
32+
content: Annotated[str, Field(title="Content")]
33+
name: Annotated[Optional[str], Field(None, title="Name")]
34+
function_call: Annotated[Optional[Dict[str, Any]], Field(None, title="Function Call")]
35+
36+
37+
class CreateDeepNewsResponseChoice(BaseModel):
38+
index: Annotated[int, Field(title="Index")]
39+
message: CreateDeepNewsRequestMessage
40+
finish_reason: Annotated[Optional[str], Field(None, title="Finish Reason")]
41+
42+
43+
class CreateDeepNewsResponseStreamChoice(BaseModel):
44+
index: Annotated[int, Field(title="Index")]
45+
delta: CreateDeepNewsRequestMessage
46+
finish_reason: Annotated[Optional[str], Field(None, title="Finish Reason")]
47+
48+
49+
class CreateDeepNewsResponseUsage(BaseModel):
50+
prompt_tokens: Annotated[int, Field(title="Prompt Tokens")]
51+
completion_tokens: Annotated[int, Field(title="Completion Tokens")]
52+
total_tokens: Annotated[int, Field(title="Total Tokens")]
53+
54+
55+
class CreateDeepNewsRequest(BaseSchema):
56+
model_config = ConfigDict(
57+
extra="allow",
58+
)
59+
model: Annotated[Optional[str], Field(title="Model")] = "deepseek"
60+
messages: Annotated[List[CreateDeepNewsRequestMessage], Field(title="Messages")]
61+
temperature: Annotated[Optional[float], Field(title="Temperature")] = 0.9
62+
top_p: Annotated[Optional[float], Field(title="Top P")] = 1.0
63+
n: Annotated[Optional[int], Field(title="N")] = 1
64+
stream: Annotated[Optional[bool], Field(title="Stream")] = False
65+
stop: Annotated[Optional[Union[str, List[str]]], Field(title="Stop")] = None
66+
max_tokens: Annotated[Optional[int], Field(title="Max Tokens")] = 9999
67+
presence_penalty: Annotated[Optional[int], Field(title="Presence Penalty")] = 0
68+
frequency_penalty: Annotated[Optional[int], Field(title="Frequency Penalty")] = 0
69+
user: Annotated[Optional[str], Field(title="User")] = None
70+
inline_citations: Annotated[
71+
Optional[Literal["markdown_link", "numbered", "none"]],
72+
Field(title="Type of inline citation formatting."),
73+
] = "markdown_link"
74+
journalist_mode: Annotated[
75+
Optional[bool],
76+
Field(
77+
title=(
78+
"Activate journalist mode, with improved alignment for making claims"
79+
"with supporting evidence. Improved journalistic style."
80+
),
81+
),
82+
] = True
83+
asknews_watermark: Annotated[
84+
Optional[bool], Field(title='Append "Generated by AskNews AI" watermark')
85+
] = True
86+
append_references: Annotated[Optional[bool], Field(title="Append References or not")] = True
87+
conversational_awareness: Annotated[
88+
Optional[bool], Field(title="Conversational Awareness")
89+
] = False
90+
filter_params: Annotated[
91+
Optional[Dict[str, Any]],
92+
Field(title="Any filter param available on the /news endpoint."),
93+
] = None
94+
sources: Annotated[
95+
Optional[
96+
Union[
97+
Literal["asknews", "google"],
98+
List[Literal["asknews", "google"]],
99+
]
100+
],
101+
Field(title="Sources"),
102+
] = "asknews"
103+
search_depth: Annotated[Optional[int], Field(title="Search Depth")] = 2
104+
max_depth: Annotated[Optional[int], Field(title="Max Depth")] = 4
105+
return_sources: Annotated[
106+
Optional[bool],
107+
Field(
108+
title="Return all collected sources as objects as the last token of the stream."
109+
),
110+
] = True
111+
112+
113+
class CreateDeepNewsResponse(BaseSchema):
114+
id: Annotated[str, Field(title="Id")]
115+
created: Annotated[int, Field(title="Created")]
116+
object: Annotated[Optional[str], Field("chat.completion", title="Object")]
117+
model: Annotated[Optional[str], Field("deepseek", title="Model")]
118+
usage: CreateDeepNewsResponseUsage
119+
choices: Annotated[List[CreateDeepNewsResponseChoice], Field(title="Choices")]
120+
sources: Annotated[DeepNewsSources, Field(title="Sources")]
121+
122+
123+
class CreateDeepNewsResponseStreamChunk(BaseSchema):
124+
__content_type__ = "text/event-stream"
125+
126+
id: Annotated[str, Field(title="Id")]
127+
created: Annotated[int, Field(title="Created")]
128+
object: Annotated[Optional[str], Field("chat.completion.chunk", title="Object")]
129+
model: Annotated[Optional[str], Field("deepseek", title="Model")]
130+
usage: CreateDeepNewsResponseUsage
131+
choices: Annotated[List[CreateDeepNewsResponseStreamChoice], Field(title="Choices")]
132+
133+
134+
class CreateDeepNewsResponseStreamSourcesNewsSource(BaseModel):
135+
kind: Literal["news"] = "news"
136+
data: SearchResponseDictItem
137+
138+
139+
class CreateDeepNewsResponseStreamSourcesWebSource(BaseModel):
140+
kind: Literal["web"] = "web"
141+
data: WebSearchResult
142+
143+
144+
class CreateDeepNewsResponseStreamSource(BaseSchema):
145+
__content_type__ = "text/event-stream"
146+
147+
id: Annotated[str, Field(title="Id")]
148+
created: Annotated[int, Field(title="Created")]
149+
object: Annotated[Optional[str], Field(title="Object")] = "chat.completion.sources"
150+
source: Annotated[
151+
Union[
152+
Annotated[CreateDeepNewsResponseStreamSourcesNewsSource, Tag("news")],
153+
Annotated[CreateDeepNewsResponseStreamSourcesWebSource, Tag("web")],
154+
],
155+
Field(title="Source"),
156+
Discriminator(kind_discriminator),
157+
]
158+
159+
160+
CreateDeepNewsResponseStream: TypeAlias = Annotated[
161+
Union[
162+
Annotated[CreateDeepNewsResponseStreamChunk, Tag("chat.completion.chunk")],
163+
Annotated[CreateDeepNewsResponseStreamSource, Tag("chat.completion.source")],
164+
],
165+
Discriminator(object_discriminator),
166+
]

0 commit comments

Comments
 (0)