exa-py 1.14.20__py3-none-any.whl → 1.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of exa-py might be problematic. Click here for more details.

exa_py/websets/types.py CHANGED
@@ -243,11 +243,15 @@ class EventType(Enum):
243
243
  webset_search_updated = 'webset.search.updated'
244
244
  import_created = 'import.created'
245
245
  import_completed = 'import.completed'
246
- import_processing = 'import.processing'
247
- webset_export_created = 'webset.export.created'
248
- webset_export_completed = 'webset.export.completed'
249
246
  webset_item_created = 'webset.item.created'
250
247
  webset_item_enriched = 'webset.item.enriched'
248
+ monitor_created = 'monitor.created'
249
+ monitor_updated = 'monitor.updated'
250
+ monitor_deleted = 'monitor.deleted'
251
+ monitor_run_created = 'monitor.run.created'
252
+ monitor_run_completed = 'monitor.run.completed'
253
+ webset_export_created = 'webset.export.created'
254
+ webset_export_completed = 'webset.export.completed'
251
255
 
252
256
 
253
257
  class Format(Enum):
@@ -263,6 +267,7 @@ class Format(Enum):
263
267
  options = 'options'
264
268
  email = 'email'
265
269
  phone = 'phone'
270
+ url = 'url'
266
271
 
267
272
 
268
273
  class ImportFormat(Enum):
@@ -333,6 +338,13 @@ class ListEventsResponse(ExaBaseModel):
333
338
  WebsetSearchUpdatedEvent,
334
339
  WebsetSearchCanceledEvent,
335
340
  WebsetSearchCompletedEvent,
341
+ ImportCreatedEvent,
342
+ ImportCompletedEvent,
343
+ MonitorCreatedEvent,
344
+ MonitorUpdatedEvent,
345
+ MonitorDeletedEvent,
346
+ MonitorRunCreatedEvent,
347
+ MonitorRunCompletedEvent,
336
348
  ],
337
349
  Field(discriminator='type')
338
350
  ]]
@@ -802,6 +814,30 @@ class UpdateImport(ExaBaseModel):
802
814
  """
803
815
 
804
816
 
817
+ class UpdateEnrichmentParameters(ExaBaseModel):
818
+ """
819
+ Parameters for updating an enrichment.
820
+ """
821
+ description: Optional[str] = None
822
+ """
823
+ Provide a description of the enrichment task you want to perform to each Webset Item.
824
+ """
825
+ format: Optional[Format] = None
826
+ """
827
+ Format of the enrichment response.
828
+
829
+ We automatically select the best format based on the description. If you want to explicitly specify the format, you can do so here.
830
+ """
831
+ options: Optional[List[Option]] = None
832
+ """
833
+ When the format is options, the different options for the enrichment agent to choose from.
834
+ """
835
+ metadata: Optional[Dict[str, Any]] = None
836
+ """
837
+ Set of key-value pairs you want to associate with this object.
838
+ """
839
+
840
+
805
841
  class Option(ExaBaseModel):
806
842
  label: str
807
843
  """
@@ -1336,6 +1372,7 @@ class WebsetEnrichmentFormat(Enum):
1336
1372
  options = 'options'
1337
1373
  email = 'email'
1338
1374
  phone = 'phone'
1375
+ url = 'url'
1339
1376
 
1340
1377
 
1341
1378
  class WebsetEnrichmentOption(Option):
@@ -1873,6 +1910,104 @@ class WebsetSearchUpdatedEvent(ExaBaseModel):
1873
1910
  """
1874
1911
 
1875
1912
 
1913
+ class ImportCreatedEvent(ExaBaseModel):
1914
+ id: str
1915
+ """
1916
+ The unique identifier for the event
1917
+ """
1918
+ object: Literal['event']
1919
+ type: Literal['import.created']
1920
+ data: Import
1921
+ created_at: datetime = Field(..., alias='createdAt')
1922
+ """
1923
+ The date and time the event was created
1924
+ """
1925
+
1926
+
1927
+ class ImportCompletedEvent(ExaBaseModel):
1928
+ id: str
1929
+ """
1930
+ The unique identifier for the event
1931
+ """
1932
+ object: Literal['event']
1933
+ type: Literal['import.completed']
1934
+ data: Import
1935
+ created_at: datetime = Field(..., alias='createdAt')
1936
+ """
1937
+ The date and time the event was created
1938
+ """
1939
+
1940
+
1941
+ class MonitorCreatedEvent(ExaBaseModel):
1942
+ id: str
1943
+ """
1944
+ The unique identifier for the event
1945
+ """
1946
+ object: Literal['event']
1947
+ type: Literal['monitor.created']
1948
+ data: Monitor
1949
+ created_at: datetime = Field(..., alias='createdAt')
1950
+ """
1951
+ The date and time the event was created
1952
+ """
1953
+
1954
+
1955
+ class MonitorUpdatedEvent(ExaBaseModel):
1956
+ id: str
1957
+ """
1958
+ The unique identifier for the event
1959
+ """
1960
+ object: Literal['event']
1961
+ type: Literal['monitor.updated']
1962
+ data: Monitor
1963
+ created_at: datetime = Field(..., alias='createdAt')
1964
+ """
1965
+ The date and time the event was created
1966
+ """
1967
+
1968
+
1969
+ class MonitorDeletedEvent(ExaBaseModel):
1970
+ id: str
1971
+ """
1972
+ The unique identifier for the event
1973
+ """
1974
+ object: Literal['event']
1975
+ type: Literal['monitor.deleted']
1976
+ data: Monitor
1977
+ created_at: datetime = Field(..., alias='createdAt')
1978
+ """
1979
+ The date and time the event was created
1980
+ """
1981
+
1982
+
1983
+ class MonitorRunCreatedEvent(ExaBaseModel):
1984
+ id: str
1985
+ """
1986
+ The unique identifier for the event
1987
+ """
1988
+ object: Literal['event']
1989
+ type: Literal['monitor.run.created']
1990
+ data: MonitorRun
1991
+ created_at: datetime = Field(..., alias='createdAt')
1992
+ """
1993
+ The date and time the event was created
1994
+ """
1995
+
1996
+
1997
+ class MonitorRunCompletedEvent(ExaBaseModel):
1998
+ id: str
1999
+ """
2000
+ The unique identifier for the event
2001
+ """
2002
+ object: Literal['event']
2003
+ type: Literal['monitor.run.completed']
2004
+ data: MonitorRun
2005
+ created_at: datetime = Field(..., alias='createdAt')
2006
+ """
2007
+ The date and time the event was created
2008
+ """
2009
+
2010
+
1876
2011
  class WebsetStatus(Enum):
1877
2012
  """
1878
2013
  The status of the webset
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exa-py
3
- Version: 1.14.20
3
+ Version: 1.15.1
4
4
  Summary: Python SDK for Exa API.
5
5
  License: MIT
6
6
  Author: Exa AI
@@ -1,19 +1,22 @@
1
1
  exa_py/__init__.py,sha256=M2GC9oSdoV6m2msboW0vMWWl8wrth4o6gmEV4MYLGG8,66
2
- exa_py/api.py,sha256=TIGTu4ITw7SrhTZjx9uQNpwi1u3nDpjtYUNXQrgg5bo,107356
2
+ exa_py/api.py,sha256=gUvQ2NXtUgBlrFrWrCIUwiq-z2lAlo_gWVFncN68Vhw,108947
3
3
  exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- exa_py/research/__init__.py,sha256=QeY-j6bP4QP5tF9ytX0IeQhJvd0Wn4cJCD69U8pP7kA,271
5
- exa_py/research/client.py,sha256=mnoTA4Qoa0TA5d8nVTR9tAU9LJElXV-MlPozgMxlUp4,12799
6
- exa_py/research/models.py,sha256=j7YgRoMRp2MLgnaij7775x_hJEeV5gksKpfLwmawqxY,3704
7
- exa_py/utils.py,sha256=eYnJRAFJonwKP_mCxzAB9TnLEqoF-88stg6wh-M-Ups,6424
4
+ exa_py/research/__init__.py,sha256=qbWY5c3LYBM7L4yGVtiMs9nUg_kMcAVhMhT0DvN6EI4,1014
5
+ exa_py/research/async_client.py,sha256=jP6BF8nc29Mt4iU2ZEQGAC5dGM-Z9I4k3pzpPcCKJIs,9436
6
+ exa_py/research/base.py,sha256=0C52XGUMRwRox1JSVBrbiHZxqPI6Cgif7EFNqYxpc40,5451
7
+ exa_py/research/models.py,sha256=2gu4jckbLYHlTmln8BhDzovuqg1fejTCncqi4r0oDow,9822
8
+ exa_py/research/sync_client.py,sha256=LHMkgqUZgKCQWgLP41Fj6EWLbt_YfX4aAgvWGjNgbXM,9155
9
+ exa_py/research/utils.py,sha256=YR61UkReyK-LrvOTaX_aG_neS1DsmCkv7B6YODpwHvU,5965
10
+ exa_py/utils.py,sha256=1jwKwcJwHSbrzwgMkprXSEZ-xssCtXxwf-GTRgaFmz8,6395
8
11
  exa_py/websets/__init__.py,sha256=x7Dc0MS8raRXA7Ud6alKgnsUmLi6X9GTqfB8kOwC9iQ,179
9
12
  exa_py/websets/_generator/pydantic/BaseModel.jinja2,sha256=RUDCmPZVamoVx1WudylscYFfDhGoNNtRYlpTvKjAiuA,1276
10
13
  exa_py/websets/client.py,sha256=sKkji8QaPFnGM1-J5TB6yKJcGAEd6gk7lsnIebzXNQ8,5856
11
14
  exa_py/websets/core/__init__.py,sha256=xOyrFaqtBocMUu321Jpbk7IzIQRNZufSIGJXrKoG-Bg,323
12
15
  exa_py/websets/core/base.py,sha256=RldWYwBg2iVfkWmdPke7xjXdwb4JKeABIOgiZtqvz-4,4125
13
16
  exa_py/websets/enrichments/__init__.py,sha256=5dJIEKKceUost3RnI6PpCSB3VjUCBzxseEsIXu-ZY-Y,83
14
- exa_py/websets/enrichments/client.py,sha256=obUjn4vH6tKBMtHEBVdMzlN8in0Fx3sCP-bXx-Le1zM,2338
17
+ exa_py/websets/enrichments/client.py,sha256=Qn6B19Gf1z4pQ5CCeXMFeTJnVi2OiVx9Ck5XlcQBZVI,2999
15
18
  exa_py/websets/events/__init__.py,sha256=aFJ9O5UudtQQzndVmdB96IaM2l07qyM1B_8xKY7rp58,60
16
- exa_py/websets/events/client.py,sha256=Hzatqp3X-K0ZGe36cjFMgbhnsErcDLdGWQVirhmHjvY,3622
19
+ exa_py/websets/events/client.py,sha256=-7sQ61P0SdHj-LuYq6G0u9a_IrtmcCljyDCzZPf330U,4370
17
20
  exa_py/websets/imports/__init__.py,sha256=iEl-fZZSdcvKaqLgjMES_0RwYn7hZDCMf6BZriCrjgw,64
18
21
  exa_py/websets/imports/client.py,sha256=nJs46hxlSkZm7qjboYHNBuJ62gLmA_Yzr9fc-NDky0Y,6795
19
22
  exa_py/websets/items/__init__.py,sha256=DCWZJVtRmUjnMEkKdb5gW1LT9cHcb-J8lENMnyyBeKU,71
@@ -24,9 +27,9 @@ exa_py/websets/monitors/runs/__init__.py,sha256=TmcETf3zdQouA_vAeLiosCNL1MYJnZ0y
24
27
  exa_py/websets/monitors/runs/client.py,sha256=WnwcWCf7UKk68VCNUp8mRXBtlU8vglTSX-eoWVXzKIw,1229
25
28
  exa_py/websets/searches/__init__.py,sha256=_0Zx8ES5fFTEL3T8mhLxq_xK2t0JONx6ad6AtbvClsE,77
26
29
  exa_py/websets/searches/client.py,sha256=X3f7axWGfecmxf-2tBTX0Yf_--xToz1X8ZHbbudEzy0,1790
27
- exa_py/websets/types.py,sha256=DxO_T4Ijnd06gxFAX3f238Mt5P0_ulpY44M1kiT4y4U,47120
30
+ exa_py/websets/types.py,sha256=iDZqvt22hjhkU8_AIczQ9iHexUw9C4PHh7C66ujfhk4,50523
28
31
  exa_py/websets/webhooks/__init__.py,sha256=iTPBCxFd73z4RifLQMX6iRECx_6pwlI5qscLNjMOUHE,77
29
32
  exa_py/websets/webhooks/client.py,sha256=zS1eoWKliuiY4AIeFJdpAlPZeOINyphn7KEWANF-zaE,4384
30
- exa_py-1.14.20.dist-info/METADATA,sha256=tkmcVljW6I0gOgd6uLMTkMXLnmJLQOSVJmeoxLW3tQw,3827
31
- exa_py-1.14.20.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
- exa_py-1.14.20.dist-info/RECORD,,
33
+ exa_py-1.15.1.dist-info/METADATA,sha256=6MtMgPdsfzWgOKeQtywu4aZASbZjYpetbiirn17CzjE,3826
34
+ exa_py-1.15.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
35
+ exa_py-1.15.1.dist-info/RECORD,,
exa_py/research/client.py DELETED
@@ -1,358 +0,0 @@
1
- """Lightweight research client wrappers for the Exa REST API.
2
-
3
- This module purposefully keeps its import surface minimal to avoid circular
4
- import problems with :pymod:`exa_py.api`. Any heavy dependencies (including
5
- `exa_py.api` itself) are imported lazily **inside** functions. This means
6
- that type-checkers still see the full, precise types via the ``TYPE_CHECKING``
7
- block, but at runtime we only pay the cost if/when a helper is actually used.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from typing import TYPE_CHECKING, Any, Dict, Optional, Literal
13
-
14
- from exa_py.utils import JSONSchemaInput
15
- from ..api import _convert_schema_input
16
-
17
- if TYPE_CHECKING: # pragma: no cover – only for static analysers
18
- # Import with full type info when static type-checking. `_Result` still
19
- # lives in ``exa_py.api`` but the response model moved to
20
- # ``exa_py.research.models``.
21
- from .models import (
22
- ResearchTask,
23
- ResearchTaskId,
24
- ListResearchTasksResponse,
25
- ) # noqa: F401
26
-
27
- # ---------------------------------------------------------------------------
28
- # Public, user-facing clients
29
- # ---------------------------------------------------------------------------
30
-
31
-
32
- class ResearchClient:
33
- """Synchronous helper namespace accessed via :pyattr:`Exa.research`."""
34
-
35
- def __init__(self, parent_client):
36
- # A reference to the *already-constructed* ``Exa`` instance so that we
37
- # can piggy-back on its HTTP plumbing (headers, base URL, retries, …).
38
- self._client = parent_client
39
-
40
- def create_task(
41
- self,
42
- *,
43
- instructions: str,
44
- model: Literal["exa-research", "exa-research-pro"] = "exa-research",
45
- output_infer_schema: bool = None,
46
- output_schema: "Optional[JSONSchemaInput]" = None,
47
- ) -> "ResearchTaskId":
48
- """Submit a research request and return the *task identifier*."""
49
- payload = {"instructions": instructions}
50
- if model is not None:
51
- payload["model"] = model
52
- if output_schema is not None or output_infer_schema is not None:
53
- payload["output"] = {}
54
- if output_schema is not None:
55
- payload["output"]["schema"] = _convert_schema_input(output_schema)
56
- if output_infer_schema is not None:
57
- payload["output"]["inferSchema"] = output_infer_schema
58
-
59
- raw_response: Dict[str, Any] = self._client.request(
60
- "/research/v0/tasks", payload
61
- )
62
-
63
- # Defensive checks so that we fail loudly if the contract changes.
64
- if not isinstance(raw_response, dict) or "id" not in raw_response:
65
- raise RuntimeError(
66
- f"Unexpected response while creating research task: {raw_response}"
67
- )
68
-
69
- # Lazily import to avoid circular deps at runtime.
70
- from .models import ResearchTaskId # noqa: WPS433 – runtime import
71
-
72
- return ResearchTaskId(id=raw_response["id"])
73
-
74
- def get_task(self, id: str) -> "ResearchTask": # noqa: D401 – imperative mood is fine
75
- """Fetch the current status / result for a research task."""
76
- endpoint = f"/research/v0/tasks/{id}"
77
-
78
- # The new endpoint is a simple GET.
79
- raw_response: Dict[str, Any] = self._client.request(endpoint, method="GET")
80
-
81
- return _build_research_task(raw_response)
82
-
83
- # ------------------------------------------------------------------
84
- # Convenience helpers
85
- # ------------------------------------------------------------------
86
-
87
- def poll_task(
88
- self,
89
- id: str,
90
- *,
91
- poll_interval: float = 1.0,
92
- timeout_seconds: int = 15 * 60,
93
- ) -> "ResearchTask":
94
- """Blocking helper that polls until task completes or fails.
95
-
96
- Parameters
97
- ----------
98
- id:
99
- The ID of the research task to poll.
100
- poll_interval:
101
- Seconds to wait between successive polls (default 1s).
102
- timeout_seconds:
103
- Maximum time to wait before raising :class:`TimeoutError` (default 15 min).
104
- """
105
-
106
- import time
107
-
108
- deadline = time.monotonic() + timeout_seconds
109
-
110
- while True:
111
- task = self.get_task(id)
112
- status = task.status.lower() if isinstance(task.status, str) else ""
113
-
114
- if status in {"completed", "failed", "complete", "finished", "done"}:
115
- return task
116
-
117
- if time.monotonic() > deadline:
118
- raise TimeoutError(
119
- f"Research task {id} did not finish within {timeout_seconds} seconds"
120
- )
121
-
122
- time.sleep(poll_interval)
123
-
124
- # ------------------------------------------------------------------
125
- # Listing helpers
126
- # ------------------------------------------------------------------
127
-
128
- def list(
129
- self,
130
- *,
131
- cursor: Optional[str] = None,
132
- limit: Optional[int] = None,
133
- ) -> "ListResearchTasksResponse":
134
- """List research tasks with optional pagination.
135
-
136
- Parameters
137
- ----------
138
- cursor:
139
- Pagination cursor returned by a previous call (optional).
140
- limit:
141
- Maximum number of tasks to return (optional).
142
- """
143
-
144
- params = {
145
- k: v for k, v in {"cursor": cursor, "limit": limit}.items() if v is not None
146
- }
147
-
148
- raw_response: Dict[str, Any] = self._client.request(
149
- "/research/v0/tasks",
150
- data=None,
151
- method="GET",
152
- params=params or None,
153
- )
154
-
155
- # Defensive checks so that we fail loudly if the contract changes.
156
- if not isinstance(raw_response, dict) or "data" not in raw_response:
157
- raise RuntimeError(
158
- f"Unexpected response while listing research tasks: {raw_response}"
159
- )
160
-
161
- tasks = [_build_research_task(t) for t in raw_response.get("data", [])]
162
-
163
- # Lazy import to avoid cycles.
164
- from .models import ListResearchTasksResponse # noqa: WPS433 – runtime import
165
-
166
- return ListResearchTasksResponse(
167
- data=tasks,
168
- has_more=raw_response.get("hasMore", False),
169
- next_cursor=raw_response.get("nextCursor"),
170
- )
171
-
172
-
173
- class AsyncResearchClient:
174
- """Async counterpart used via :pyattr:`AsyncExa.research`."""
175
-
176
- def __init__(self, parent_client):
177
- self._client = parent_client
178
-
179
- async def create_task(
180
- self,
181
- *,
182
- instructions: str,
183
- model: Literal["exa-research", "exa-research-pro"] = "exa-research",
184
- output_schema: "JSONSchemaInput",
185
- ) -> "ResearchTaskId":
186
- """Submit a research request and return the *task identifier* (async)."""
187
-
188
- # Convert schema using the same conversion logic as main API
189
- from ..api import _convert_schema_input # noqa: WPS433 – runtime import
190
-
191
- payload = {
192
- "instructions": instructions,
193
- "model": model,
194
- "output": {"schema": _convert_schema_input(output_schema)},
195
- }
196
-
197
- raw_response: Dict[str, Any] = await self._client.async_request(
198
- "/research/v0/tasks", payload
199
- )
200
-
201
- # Defensive checks so that we fail loudly if the contract changes.
202
- if not isinstance(raw_response, dict) or "id" not in raw_response:
203
- raise RuntimeError(
204
- f"Unexpected response while creating research task: {raw_response}"
205
- )
206
-
207
- # Lazily import to avoid circular deps at runtime.
208
- from .models import ResearchTaskId # noqa: WPS433 – runtime import
209
-
210
- return ResearchTaskId(id=raw_response["id"])
211
-
212
- async def get_task(self, id: str) -> "ResearchTask": # noqa: D401
213
- """Fetch the current status / result for a research task (async)."""
214
-
215
- endpoint = f"/research/v0/tasks/{id}"
216
-
217
- # Perform GET using the underlying HTTP client because `async_request`
218
- # only supports POST semantics.
219
- resp = await self._client.client.get(
220
- self._client.base_url + endpoint, headers=self._client.headers
221
- )
222
-
223
- if resp.status_code >= 400:
224
- raise RuntimeError(
225
- f"Request failed with status code {resp.status_code}: {resp.text}"
226
- )
227
-
228
- raw_response: Dict[str, Any] = resp.json()
229
-
230
- return _build_research_task(raw_response)
231
-
232
- # ------------------------------------------------------------------
233
- # Convenience helpers
234
- # ------------------------------------------------------------------
235
-
236
- async def poll_task(
237
- self,
238
- id: str,
239
- *,
240
- poll_interval: float = 1.0,
241
- timeout_seconds: int = 15 * 60,
242
- ) -> "ResearchTask":
243
- """Async helper that polls until task completes or fails.
244
-
245
- Mirrors :py:meth:`ResearchClient.poll_task` but uses ``await`` and
246
- :pyfunc:`asyncio.sleep`. Raises :class:`TimeoutError` on timeout.
247
- """
248
-
249
- import asyncio
250
- import time
251
-
252
- deadline = time.monotonic() + timeout_seconds
253
-
254
- while True:
255
- task = await self.get_task(id)
256
- status = task.status.lower() if isinstance(task.status, str) else ""
257
-
258
- if status in {"completed", "failed", "complete", "finished", "done"}:
259
- return task
260
-
261
- if time.monotonic() > deadline:
262
- raise TimeoutError(
263
- f"Research task {id} did not finish within {timeout_seconds} seconds"
264
- )
265
-
266
- await asyncio.sleep(poll_interval)
267
-
268
- # ------------------------------------------------------------------
269
- # Listing helpers
270
- # ------------------------------------------------------------------
271
-
272
- async def list(
273
- self,
274
- *,
275
- cursor: Optional[str] = None,
276
- limit: Optional[int] = None,
277
- ) -> "ListResearchTasksResponse":
278
- """Async list of research tasks with optional pagination."""
279
-
280
- params = {
281
- k: v for k, v in {"cursor": cursor, "limit": limit}.items() if v is not None
282
- }
283
-
284
- resp = await self._client.client.get(
285
- self._client.base_url + "/research/v0/tasks",
286
- headers=self._client.headers,
287
- params=params or None,
288
- )
289
-
290
- if resp.status_code >= 400:
291
- raise RuntimeError(
292
- f"Request failed with status code {resp.status_code}: {resp.text}"
293
- )
294
-
295
- raw_response: Dict[str, Any] = resp.json()
296
-
297
- if not isinstance(raw_response, dict) or "data" not in raw_response:
298
- raise RuntimeError(
299
- f"Unexpected response while listing research tasks: {raw_response}"
300
- )
301
-
302
- tasks = [_build_research_task(t) for t in raw_response.get("data", [])]
303
-
304
- from .models import ListResearchTasksResponse # noqa: WPS433 – runtime import
305
-
306
- return ListResearchTasksResponse(
307
- data=tasks,
308
- has_more=raw_response.get("hasMore", False),
309
- next_cursor=raw_response.get("nextCursor"),
310
- )
311
-
312
-
313
- # ---------------------------------------------------------------------------
314
- # Internal helpers (lazy imports to avoid cycles)
315
- # ---------------------------------------------------------------------------
316
-
317
-
318
- def _build_research_task(raw: Dict[str, Any]):
319
- """Convert raw API response into a :class:`ResearchTask` instance."""
320
-
321
- # Defensive check – fail loudly if the API contract changes.
322
- if not isinstance(raw, dict) or "id" not in raw:
323
- raise RuntimeError(f"Unexpected response while fetching research task: {raw}")
324
-
325
- # Lazily import heavy deps to avoid cycles and unnecessary startup cost.
326
- from .models import ResearchTask # noqa: WPS433 – runtime import
327
- from ..api import _Result, to_snake_case # noqa: WPS433 – runtime import
328
-
329
- citations_raw = raw.get("citations", {}) or {}
330
- citations_parsed = {}
331
- for key, cites in citations_raw.items():
332
- results = []
333
- for c in cites:
334
- snake_c = to_snake_case(c)
335
- results.append(
336
- _Result(
337
- url=snake_c.get("url"),
338
- id=snake_c.get("id"),
339
- title=snake_c.get("title"),
340
- score=snake_c.get("score"),
341
- published_date=snake_c.get("published_date"),
342
- author=snake_c.get("author"),
343
- image=snake_c.get("image"),
344
- favicon=snake_c.get("favicon"),
345
- subpages=snake_c.get("subpages"),
346
- extras=snake_c.get("extras"),
347
- )
348
- )
349
- citations_parsed[key] = results
350
-
351
- return ResearchTask(
352
- id=raw["id"],
353
- status=raw["status"],
354
- instructions=raw.get("instructions", ""),
355
- schema=raw.get("schema", {}),
356
- data=raw.get("data"),
357
- citations=citations_parsed,
358
- )