exa-py 1.13.2__py3-none-any.whl → 1.14.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 +1 -1
- exa_py/research/__init__.py +2 -1
- exa_py/research/client.py +123 -18
- exa_py/research/models.py +22 -0
- {exa_py-1.13.2.dist-info → exa_py-1.14.1.dist-info}/METADATA +3 -2
- {exa_py-1.13.2.dist-info → exa_py-1.14.1.dist-info}/RECORD +7 -7
- {exa_py-1.13.2.dist-info → exa_py-1.14.1.dist-info}/WHEEL +0 -0
exa_py/api.py
CHANGED
|
@@ -1965,7 +1965,7 @@ class AsyncExa(Exa):
|
|
|
1965
1965
|
res = await self.client.post(
|
|
1966
1966
|
self.base_url + endpoint, json=data, headers=self.headers
|
|
1967
1967
|
)
|
|
1968
|
-
if res.status_code != 200:
|
|
1968
|
+
if res.status_code != 200 and res.status_code != 201:
|
|
1969
1969
|
raise ValueError(
|
|
1970
1970
|
f"Request failed with status code {res.status_code}: {res.text}"
|
|
1971
1971
|
)
|
exa_py/research/__init__.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from .client import ResearchClient, AsyncResearchClient
|
|
2
|
-
from .models import ResearchTask
|
|
2
|
+
from .models import ResearchTask, ListResearchTasksResponse, ResearchTaskId
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
5
|
"ResearchClient",
|
|
6
6
|
"AsyncResearchClient",
|
|
7
7
|
"ResearchTaskId",
|
|
8
8
|
"ResearchTask",
|
|
9
|
+
"ListResearchTasksResponse",
|
|
9
10
|
]
|
exa_py/research/client.py
CHANGED
|
@@ -9,14 +9,18 @@ block, but at runtime we only pay the cost if/when a helper is actually used.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from typing import TYPE_CHECKING, Any, Dict
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Literal
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING: # pragma: no cover – only for static analysers
|
|
15
15
|
# Import with full type info when static type-checking. `_Result` still
|
|
16
16
|
# lives in ``exa_py.api`` but the response model moved to
|
|
17
17
|
# ``exa_py.research.models``.
|
|
18
18
|
from ..api import _Result # noqa: F401
|
|
19
|
-
from .models import
|
|
19
|
+
from .models import (
|
|
20
|
+
ResearchTask,
|
|
21
|
+
ResearchTaskId,
|
|
22
|
+
ListResearchTasksResponse,
|
|
23
|
+
) # noqa: F401
|
|
20
24
|
|
|
21
25
|
# ---------------------------------------------------------------------------
|
|
22
26
|
# Public, user-facing clients
|
|
@@ -34,16 +38,25 @@ class ResearchClient:
|
|
|
34
38
|
def create_task(
|
|
35
39
|
self,
|
|
36
40
|
*,
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
instructions: str,
|
|
42
|
+
model: Literal["exa-research", "exa-research-pro"] = "exa-research",
|
|
43
|
+
output_infer_schema: bool = None,
|
|
44
|
+
output_schema: Dict[str, Any] = None,
|
|
39
45
|
) -> "ResearchTaskId":
|
|
40
46
|
"""Submit a research request and return the *task identifier*."""
|
|
41
|
-
payload = {
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
payload = {"instructions": instructions}
|
|
48
|
+
if model is not None:
|
|
49
|
+
payload["model"] = model
|
|
50
|
+
if output_schema is not None or output_infer_schema is not None:
|
|
51
|
+
payload["output"] = {}
|
|
52
|
+
if output_schema is not None:
|
|
53
|
+
payload["output"]["schema"] = output_schema
|
|
54
|
+
if output_infer_schema is not None:
|
|
55
|
+
payload["output"]["inferSchema"] = output_infer_schema
|
|
56
|
+
|
|
57
|
+
raw_response: Dict[str, Any] = self._client.request(
|
|
58
|
+
"/research/v0/tasks", payload
|
|
59
|
+
)
|
|
47
60
|
|
|
48
61
|
# Defensive checks so that we fail loudly if the contract changes.
|
|
49
62
|
if not isinstance(raw_response, dict) or "id" not in raw_response:
|
|
@@ -56,11 +69,9 @@ class ResearchClient:
|
|
|
56
69
|
|
|
57
70
|
return ResearchTaskId(id=raw_response["id"])
|
|
58
71
|
|
|
59
|
-
def get_task(
|
|
60
|
-
self, id: str
|
|
61
|
-
) -> "ResearchTask": # noqa: D401 – imperative mood is fine
|
|
72
|
+
def get_task(self, id: str) -> "ResearchTask": # noqa: D401 – imperative mood is fine
|
|
62
73
|
"""Fetch the current status / result for a research task."""
|
|
63
|
-
endpoint = f"/research/tasks/{id}"
|
|
74
|
+
endpoint = f"/research/v0/tasks/{id}"
|
|
64
75
|
|
|
65
76
|
# The new endpoint is a simple GET.
|
|
66
77
|
raw_response: Dict[str, Any] = self._client.request(endpoint, method="GET")
|
|
@@ -108,6 +119,54 @@ class ResearchClient:
|
|
|
108
119
|
|
|
109
120
|
time.sleep(poll_interval)
|
|
110
121
|
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# Listing helpers
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def list(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
cursor: Optional[str] = None,
|
|
130
|
+
limit: Optional[int] = None,
|
|
131
|
+
) -> "ListResearchTasksResponse":
|
|
132
|
+
"""List research tasks with optional pagination.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
cursor:
|
|
137
|
+
Pagination cursor returned by a previous call (optional).
|
|
138
|
+
limit:
|
|
139
|
+
Maximum number of tasks to return (optional).
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
params = {
|
|
143
|
+
k: v for k, v in {"cursor": cursor, "limit": limit}.items() if v is not None
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
raw_response: Dict[str, Any] = self._client.request(
|
|
147
|
+
"/research/v0/tasks",
|
|
148
|
+
data=None,
|
|
149
|
+
method="GET",
|
|
150
|
+
params=params or None,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Defensive checks so that we fail loudly if the contract changes.
|
|
154
|
+
if not isinstance(raw_response, dict) or "data" not in raw_response:
|
|
155
|
+
raise RuntimeError(
|
|
156
|
+
f"Unexpected response while listing research tasks: {raw_response}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
tasks = [_build_research_task(t) for t in raw_response.get("data", [])]
|
|
160
|
+
|
|
161
|
+
# Lazy import to avoid cycles.
|
|
162
|
+
from .models import ListResearchTasksResponse # noqa: WPS433 – runtime import
|
|
163
|
+
|
|
164
|
+
return ListResearchTasksResponse(
|
|
165
|
+
data=tasks,
|
|
166
|
+
has_more=raw_response.get("hasMore", False),
|
|
167
|
+
next_cursor=raw_response.get("nextCursor"),
|
|
168
|
+
)
|
|
169
|
+
|
|
111
170
|
|
|
112
171
|
class AsyncResearchClient:
|
|
113
172
|
"""Async counterpart used via :pyattr:`AsyncExa.research`."""
|
|
@@ -118,18 +177,20 @@ class AsyncResearchClient:
|
|
|
118
177
|
async def create_task(
|
|
119
178
|
self,
|
|
120
179
|
*,
|
|
121
|
-
|
|
180
|
+
instructions: str,
|
|
181
|
+
model: Literal["exa-research", "exa-research-pro"] = "exa-research",
|
|
122
182
|
output_schema: Dict[str, Any],
|
|
123
183
|
) -> "ResearchTaskId":
|
|
124
184
|
"""Submit a research request and return the *task identifier* (async)."""
|
|
125
185
|
|
|
126
186
|
payload = {
|
|
127
|
-
"
|
|
187
|
+
"instructions": instructions,
|
|
188
|
+
"model": model,
|
|
128
189
|
"output": {"schema": output_schema},
|
|
129
190
|
}
|
|
130
191
|
|
|
131
192
|
raw_response: Dict[str, Any] = await self._client.async_request(
|
|
132
|
-
"/research/tasks", payload
|
|
193
|
+
"/research/v0/tasks", payload
|
|
133
194
|
)
|
|
134
195
|
|
|
135
196
|
# Defensive checks so that we fail loudly if the contract changes.
|
|
@@ -146,7 +207,7 @@ class AsyncResearchClient:
|
|
|
146
207
|
async def get_task(self, id: str) -> "ResearchTask": # noqa: D401
|
|
147
208
|
"""Fetch the current status / result for a research task (async)."""
|
|
148
209
|
|
|
149
|
-
endpoint = f"/research/tasks/{id}"
|
|
210
|
+
endpoint = f"/research/v0/tasks/{id}"
|
|
150
211
|
|
|
151
212
|
# Perform GET using the underlying HTTP client because `async_request`
|
|
152
213
|
# only supports POST semantics.
|
|
@@ -199,6 +260,50 @@ class AsyncResearchClient:
|
|
|
199
260
|
|
|
200
261
|
await asyncio.sleep(poll_interval)
|
|
201
262
|
|
|
263
|
+
# ------------------------------------------------------------------
|
|
264
|
+
# Listing helpers
|
|
265
|
+
# ------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
async def list(
|
|
268
|
+
self,
|
|
269
|
+
*,
|
|
270
|
+
cursor: Optional[str] = None,
|
|
271
|
+
limit: Optional[int] = None,
|
|
272
|
+
) -> "ListResearchTasksResponse":
|
|
273
|
+
"""Async list of research tasks with optional pagination."""
|
|
274
|
+
|
|
275
|
+
params = {
|
|
276
|
+
k: v for k, v in {"cursor": cursor, "limit": limit}.items() if v is not None
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
resp = await self._client.client.get(
|
|
280
|
+
self._client.base_url + "/research/v0/tasks",
|
|
281
|
+
headers=self._client.headers,
|
|
282
|
+
params=params or None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if resp.status_code >= 400:
|
|
286
|
+
raise RuntimeError(
|
|
287
|
+
f"Request failed with status code {resp.status_code}: {resp.text}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
raw_response: Dict[str, Any] = resp.json()
|
|
291
|
+
|
|
292
|
+
if not isinstance(raw_response, dict) or "data" not in raw_response:
|
|
293
|
+
raise RuntimeError(
|
|
294
|
+
f"Unexpected response while listing research tasks: {raw_response}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
tasks = [_build_research_task(t) for t in raw_response.get("data", [])]
|
|
298
|
+
|
|
299
|
+
from .models import ListResearchTasksResponse # noqa: WPS433 – runtime import
|
|
300
|
+
|
|
301
|
+
return ListResearchTasksResponse(
|
|
302
|
+
data=tasks,
|
|
303
|
+
has_more=raw_response.get("hasMore", False),
|
|
304
|
+
next_cursor=raw_response.get("nextCursor"),
|
|
305
|
+
)
|
|
306
|
+
|
|
202
307
|
|
|
203
308
|
# ---------------------------------------------------------------------------
|
|
204
309
|
# Internal helpers (lazy imports to avoid cycles)
|
exa_py/research/models.py
CHANGED
|
@@ -92,7 +92,29 @@ class ResearchTask:
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
@dataclass
|
|
96
|
+
class ListResearchTasksResponse:
|
|
97
|
+
"""Paginated list of research tasks."""
|
|
98
|
+
|
|
99
|
+
data: List[ResearchTask]
|
|
100
|
+
has_more: bool
|
|
101
|
+
next_cursor: Optional[str]
|
|
102
|
+
|
|
103
|
+
# -----------------------------------------------------------------
|
|
104
|
+
# Pretty representation helpers
|
|
105
|
+
# -----------------------------------------------------------------
|
|
106
|
+
def __str__(self) -> str: # pragma: no cover – convenience only
|
|
107
|
+
tasks_repr = "\n\n".join(str(task) for task in self.data)
|
|
108
|
+
cursor_repr = self.next_cursor or "None"
|
|
109
|
+
return (
|
|
110
|
+
f"Tasks:\n{tasks_repr}\n\n"
|
|
111
|
+
f"Has more: {self.has_more}\n"
|
|
112
|
+
f"Next cursor: {cursor_repr}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
95
116
|
__all__ = [
|
|
96
117
|
"ResearchTaskId",
|
|
97
118
|
"ResearchTask",
|
|
119
|
+
"ListResearchTasksResponse",
|
|
98
120
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: exa-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.1
|
|
4
4
|
Summary: Python SDK for Exa API.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Exa AI
|
|
@@ -126,7 +126,8 @@ exa = Exa(api_key="your-api-key")
|
|
|
126
126
|
},
|
|
127
127
|
}
|
|
128
128
|
resp = exa.research.create_task(
|
|
129
|
-
|
|
129
|
+
instructions=QUESTION,
|
|
130
|
+
model="exa-research",
|
|
130
131
|
output_schema=OUTPUT_SCHEMA,
|
|
131
132
|
)
|
|
132
133
|
```
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
exa_py/__init__.py,sha256=M2GC9oSdoV6m2msboW0vMWWl8wrth4o6gmEV4MYLGG8,66
|
|
2
|
-
exa_py/api.py,sha256=
|
|
2
|
+
exa_py/api.py,sha256=zwTEiIkWa6Gynx5NW9VMMY4Nws63gVDRSmzKdT4v2aY,84903
|
|
3
3
|
exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
exa_py/research/__init__.py,sha256=
|
|
5
|
-
exa_py/research/client.py,sha256=
|
|
6
|
-
exa_py/research/models.py,sha256=
|
|
4
|
+
exa_py/research/__init__.py,sha256=QeY-j6bP4QP5tF9ytX0IeQhJvd0Wn4cJCD69U8pP7kA,271
|
|
5
|
+
exa_py/research/client.py,sha256=C2ukFq_dE1xUfhMlHwpD9cY5rDClgn8N92pH4_FEVpE,11901
|
|
6
|
+
exa_py/research/models.py,sha256=j7YgRoMRp2MLgnaij7775x_hJEeV5gksKpfLwmawqxY,3704
|
|
7
7
|
exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
|
|
8
8
|
exa_py/websets/__init__.py,sha256=uOBAb9VrIHrPKoddGOp2ai2KgWlyUVCLMZqfbGOlboA,70
|
|
9
9
|
exa_py/websets/_generator/pydantic/BaseModel.jinja2,sha256=RUDCmPZVamoVx1WudylscYFfDhGoNNtRYlpTvKjAiuA,1276
|
|
@@ -23,6 +23,6 @@ exa_py/websets/streams/runs/client.py,sha256=ja63-G0Y74YDgsWq30aoa0llIx8I9mIwbCb
|
|
|
23
23
|
exa_py/websets/types.py,sha256=-KmC6N9LMh5fPhhXZslmUyQcSmBqIdPF-qQ27qS4GEw,34756
|
|
24
24
|
exa_py/websets/webhooks/__init__.py,sha256=iTPBCxFd73z4RifLQMX6iRECx_6pwlI5qscLNjMOUHE,77
|
|
25
25
|
exa_py/websets/webhooks/client.py,sha256=zsIRMTeJU65yj-zo7Zz-gG02Prtzgcx6utGFSoY4HQQ,4222
|
|
26
|
-
exa_py-1.
|
|
27
|
-
exa_py-1.
|
|
28
|
-
exa_py-1.
|
|
26
|
+
exa_py-1.14.1.dist-info/METADATA,sha256=Mf9xdxUdIxEXkxcXa_mTDKRt34M4QR0lR7IPF2H26k8,4120
|
|
27
|
+
exa_py-1.14.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
28
|
+
exa_py-1.14.1.dist-info/RECORD,,
|
|
File without changes
|