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/api.py +66 -31
- exa_py/research/__init__.py +34 -5
- exa_py/research/async_client.py +310 -0
- exa_py/research/base.py +165 -0
- exa_py/research/models.py +314 -113
- exa_py/research/sync_client.py +308 -0
- exa_py/research/utils.py +222 -0
- exa_py/utils.py +1 -4
- exa_py/websets/enrichments/client.py +15 -0
- exa_py/websets/events/client.py +21 -0
- exa_py/websets/types.py +138 -3
- {exa_py-1.14.20.dist-info → exa_py-1.15.1.dist-info}/METADATA +1 -1
- {exa_py-1.14.20.dist-info → exa_py-1.15.1.dist-info}/RECORD +14 -11
- exa_py/research/client.py +0 -358
- {exa_py-1.14.20.dist-info → exa_py-1.15.1.dist-info}/WHEEL +0 -0
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,19 +1,22 @@
|
|
|
1
1
|
exa_py/__init__.py,sha256=M2GC9oSdoV6m2msboW0vMWWl8wrth4o6gmEV4MYLGG8,66
|
|
2
|
-
exa_py/api.py,sha256=
|
|
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=
|
|
5
|
-
exa_py/research/
|
|
6
|
-
exa_py/research/
|
|
7
|
-
exa_py/
|
|
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=
|
|
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
|
|
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=
|
|
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.
|
|
31
|
-
exa_py-1.
|
|
32
|
-
exa_py-1.
|
|
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
|
-
)
|
|
File without changes
|