exa-py 1.14.19__py3-none-any.whl → 1.15.0__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.

@@ -0,0 +1,165 @@
1
+ """Base client classes for the Research API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
6
+
7
+ import httpx
8
+ import requests
9
+
10
+ if TYPE_CHECKING:
11
+ from exa_py.api import Exa, AsyncExa
12
+
13
+
14
+ class ResearchBaseClient:
15
+ """Base client for synchronous Research API operations."""
16
+
17
+ def __init__(self, client: "Exa"):
18
+ """Initialize the base client.
19
+
20
+ Args:
21
+ client: The parent Exa client instance.
22
+ """
23
+ self._client = client
24
+ self.base_path = "/research/v1"
25
+
26
+ def request(
27
+ self,
28
+ endpoint: str,
29
+ method: str = "POST",
30
+ data: Optional[Union[Dict[str, Any], str]] = None,
31
+ params: Optional[Dict[str, str]] = None,
32
+ stream: bool = False,
33
+ ) -> Union[Dict[str, Any], requests.Response]:
34
+ """Make a request to the Research API.
35
+
36
+ Args:
37
+ endpoint: The API endpoint (relative to base_path).
38
+ method: HTTP method to use.
39
+ data: Request body data.
40
+ params: Query parameters.
41
+ stream: Whether to stream the response.
42
+
43
+ Returns:
44
+ The API response as a dict or raw Response for streaming.
45
+ """
46
+ full_endpoint = f"{self.base_path}{endpoint}"
47
+
48
+ if stream:
49
+ # For streaming, handle differently based on method
50
+ if method == "GET":
51
+ # For GET requests, streaming is controlled by params
52
+ # The params should already have stream=true set by the caller
53
+ return self._client.request(
54
+ full_endpoint, data=None, method=method, params=params
55
+ )
56
+ else:
57
+ # For POST requests, add stream flag to data
58
+ if data is None:
59
+ data = {}
60
+ if isinstance(data, dict):
61
+ data["stream"] = True
62
+ # The client's request method returns raw Response when streaming
63
+ return self._client.request(
64
+ full_endpoint, data=data, method=method, params=params
65
+ )
66
+ else:
67
+ return self._client.request(
68
+ full_endpoint, data=data, method=method, params=params
69
+ )
70
+
71
+ def build_pagination_params(
72
+ self, cursor: Optional[str] = None, limit: Optional[int] = None
73
+ ) -> Dict[str, str]:
74
+ """Build pagination parameters for list requests.
75
+
76
+ Args:
77
+ cursor: Pagination cursor.
78
+ limit: Maximum number of results.
79
+
80
+ Returns:
81
+ Dictionary of query parameters.
82
+ """
83
+ params = {}
84
+ if cursor is not None:
85
+ params["cursor"] = cursor
86
+ if limit is not None:
87
+ params["limit"] = str(limit)
88
+ return params
89
+
90
+
91
+ class AsyncResearchBaseClient:
92
+ """Base client for asynchronous Research API operations."""
93
+
94
+ def __init__(self, client: "AsyncExa"):
95
+ """Initialize the async base client.
96
+
97
+ Args:
98
+ client: The parent AsyncExa client instance.
99
+ """
100
+ self._client = client
101
+ self.base_path = "/research/v1"
102
+
103
+ async def request(
104
+ self,
105
+ endpoint: str,
106
+ method: str = "POST",
107
+ data: Optional[Union[Dict[str, Any], str]] = None,
108
+ params: Optional[Dict[str, str]] = None,
109
+ stream: bool = False,
110
+ ) -> Union[Dict[str, Any], httpx.Response]:
111
+ """Make an async request to the Research API.
112
+
113
+ Args:
114
+ endpoint: The API endpoint (relative to base_path).
115
+ method: HTTP method to use.
116
+ data: Request body data.
117
+ params: Query parameters.
118
+ stream: Whether to stream the response.
119
+
120
+ Returns:
121
+ The API response as a dict or raw Response for streaming.
122
+ """
123
+ full_endpoint = f"{self.base_path}{endpoint}"
124
+
125
+ if stream:
126
+ # For streaming, handle differently based on method
127
+ if method == "GET":
128
+ # For GET requests, streaming is controlled by params
129
+ # The params should already have stream=true set by the caller
130
+ return await self._client.async_request(
131
+ full_endpoint, data=None, method=method, params=params
132
+ )
133
+ else:
134
+ # For POST requests, add stream flag to data
135
+ if data is None:
136
+ data = {}
137
+ if isinstance(data, dict):
138
+ data["stream"] = True
139
+ # The async_request method returns raw Response when streaming
140
+ return await self._client.async_request(
141
+ full_endpoint, data=data, method=method, params=params
142
+ )
143
+ else:
144
+ return await self._client.async_request(
145
+ full_endpoint, data=data, method=method, params=params
146
+ )
147
+
148
+ def build_pagination_params(
149
+ self, cursor: Optional[str] = None, limit: Optional[int] = None
150
+ ) -> Dict[str, str]:
151
+ """Build pagination parameters for list requests.
152
+
153
+ Args:
154
+ cursor: Pagination cursor.
155
+ limit: Maximum number of results.
156
+
157
+ Returns:
158
+ Dictionary of query parameters.
159
+ """
160
+ params = {}
161
+ if cursor is not None:
162
+ params["cursor"] = cursor
163
+ if limit is not None:
164
+ params["limit"] = str(limit)
165
+ return params
exa_py/research/models.py CHANGED
@@ -1,120 +1,321 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
- from dataclasses import dataclass
5
- from typing import Any, Dict, List, Optional
6
-
7
- # Local import placed inside TYPE_CHECKING block to avoid runtime cycles.
8
- from typing import TYPE_CHECKING
9
-
10
- if TYPE_CHECKING: # pragma: no cover – for static analysers only
11
- from ..api import _Result # noqa: F401
12
-
13
-
14
- @dataclass
15
- class ResearchTaskId:
16
- """Structured research task ID.
17
-
18
- Attributes
19
- ----------
20
- id:
21
- Unique identifier for the research task.
22
- """
23
-
24
- id: str
25
-
26
- # ---------------------------------------------------------------------
27
- # Pretty representation helpers
28
- # ---------------------------------------------------------------------
29
- def __str__(self) -> str: # pragma: no cover – convenience only
30
- return f"ID: {self.id}\n"
31
-
32
-
33
- @dataclass
34
- class ResearchTask:
35
- """Structured research task.
36
-
37
- Attributes
38
- ----------
39
- id:
40
- Unique identifier for the research task.
41
- status:
42
- Current task status
43
- instructions:
44
- Instructions for the task
45
- schema:
46
- Output schema defining the task
47
- data:
48
- JSON-serialisable answer generated by Exa (may be ``None`` until the task
49
- completes).
50
- citations:
51
- Mapping from *root field* in the output schema to the list of search
52
- results that were used to generate that part of the answer.
53
- """
54
-
55
- id: str
56
- status: str
3
+ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
4
+
5
+ from pydantic import BaseModel, Field, RootModel
6
+
7
+
8
+ class CostDollars(BaseModel):
9
+ total: float
10
+ num_pages: Annotated[float, Field(alias="numPages")]
11
+ num_searches: Annotated[float, Field(alias="numSearches")]
12
+ reasoning_tokens: Annotated[float, Field(alias="reasoningTokens")]
13
+
14
+
15
+ class Result(BaseModel):
16
+ url: str
17
+
18
+
19
+ class ResearchThinkOperation(BaseModel):
20
+ type: Literal["think"]
21
+ content: str
22
+
23
+
24
+ class ResearchSearchOperation(BaseModel):
25
+ type: Literal["search"]
26
+ search_type: Annotated[
27
+ Literal["neural", "keyword", "auto", "fast"], Field(alias="searchType")
28
+ ]
29
+ query: str
30
+ results: List[Result]
31
+ page_tokens: Annotated[float, Field(alias="pageTokens")]
32
+ goal: Optional[str] = None
33
+
34
+
35
+ class ResearchCrawlOperation(BaseModel):
36
+ type: Literal["crawl"]
37
+ result: Result
38
+ page_tokens: Annotated[float, Field(alias="pageTokens")]
39
+ goal: Optional[str] = None
40
+
41
+
42
+ ResearchOperation = Annotated[
43
+ Union[ResearchThinkOperation, ResearchSearchOperation, ResearchCrawlOperation],
44
+ Field(discriminator="type"),
45
+ ]
46
+
47
+
48
+ class ResearchDefinitionEvent(BaseModel):
49
+ event_type: Annotated[Literal["research-definition"], Field(alias="eventType")]
50
+ research_id: Annotated[str, Field(alias="researchId")]
51
+ created_at: Annotated[
52
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
53
+ ]
54
+ instructions: str
55
+ output_schema: Annotated[Optional[Dict[str, Any]], Field(alias="outputSchema")] = (
56
+ None
57
+ )
58
+
59
+
60
+ class ResearchOutputCompleted(BaseModel):
61
+ output_type: Annotated[Literal["completed"], Field(alias="outputType")]
62
+ content: str
63
+ cost_dollars: Annotated[CostDollars, Field(alias="costDollars")]
64
+ parsed: Optional[Dict[str, Any]] = None
65
+
66
+
67
+ class ResearchOutputFailed(BaseModel):
68
+ output_type: Annotated[Literal["failed"], Field(alias="outputType")]
69
+ error: str
70
+
71
+
72
+ class ResearchOutputEvent(BaseModel):
73
+ event_type: Annotated[Literal["research-output"], Field(alias="eventType")]
74
+ research_id: Annotated[str, Field(alias="researchId")]
75
+ created_at: Annotated[
76
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
77
+ ]
78
+ output: Annotated[
79
+ Union[ResearchOutputCompleted, ResearchOutputFailed],
80
+ Field(discriminator="output_type"),
81
+ ]
82
+
83
+
84
+ class ResearchPlanDefinitionEvent(BaseModel):
85
+ event_type: Annotated[Literal["plan-definition"], Field(alias="eventType")]
86
+ research_id: Annotated[str, Field(alias="researchId")]
87
+ plan_id: Annotated[str, Field(alias="planId")]
88
+ created_at: Annotated[
89
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
90
+ ]
91
+
92
+
93
+ class ResearchPlanOperationEvent(BaseModel):
94
+ event_type: Annotated[Literal["plan-operation"], Field(alias="eventType")]
95
+ research_id: Annotated[str, Field(alias="researchId")]
96
+ plan_id: Annotated[str, Field(alias="planId")]
97
+ operation_id: Annotated[str, Field(alias="operationId")]
98
+ created_at: Annotated[
99
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
100
+ ]
101
+ data: ResearchOperation
102
+
103
+
104
+ class ResearchPlanOutputTasks(BaseModel):
105
+ output_type: Annotated[Literal["tasks"], Field(alias="outputType")]
106
+ reasoning: str
107
+ tasks_instructions: Annotated[List[str], Field(alias="tasksInstructions")]
108
+
109
+
110
+ class ResearchPlanOutputStop(BaseModel):
111
+ output_type: Annotated[Literal["stop"], Field(alias="outputType")]
112
+ reasoning: str
113
+
114
+
115
+ class ResearchPlanOutputEvent(BaseModel):
116
+ event_type: Annotated[Literal["plan-output"], Field(alias="eventType")]
117
+ research_id: Annotated[str, Field(alias="researchId")]
118
+ plan_id: Annotated[str, Field(alias="planId")]
119
+ created_at: Annotated[
120
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
121
+ ]
122
+ output: Annotated[
123
+ Union[ResearchPlanOutputTasks, ResearchPlanOutputStop],
124
+ Field(discriminator="output_type"),
125
+ ]
126
+
127
+
128
+ class ResearchTaskDefinitionEvent(BaseModel):
129
+ event_type: Annotated[Literal["task-definition"], Field(alias="eventType")]
130
+ research_id: Annotated[str, Field(alias="researchId")]
131
+ plan_id: Annotated[str, Field(alias="planId")]
132
+ task_id: Annotated[str, Field(alias="taskId")]
133
+ created_at: Annotated[
134
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
135
+ ]
57
136
  instructions: str
58
- schema: Dict[str, Any]
59
- data: Optional[Dict[str, Any]]
60
- citations: Dict[str, List["_Result"]]
61
-
62
- # ---------------------------------------------------------------------
63
- # Pretty representation helpers
64
- # ---------------------------------------------------------------------
65
- def __str__(self) -> str: # pragma: no cover – convenience only
66
- """Human-readable representation including *all* relevant fields."""
67
- schema_repr = json.dumps(self.schema, indent=2, ensure_ascii=False)
68
- data_repr = (
69
- json.dumps(self.data, indent=2, ensure_ascii=False)
70
- if self.data is not None
71
- else "None"
72
- )
73
-
74
- # Render citations grouped by the root field they belong to.
75
- if self.citations:
76
- # Each key is a root field, each value is a list of _Result objects.
77
- citations_lines = []
78
- for field, sources in self.citations.items():
79
- rendered_sources = "\n ".join(str(src) for src in sources)
80
- citations_lines.append(f"{field}:\n {rendered_sources}")
81
- citations_str = "\n\n".join(citations_lines)
82
- else:
83
- citations_str = "None"
84
-
85
- return (
86
- f"ID: {self.id}\n"
87
- f"Status: {self.status}\n"
88
- f"Instructions: {self.instructions}\n"
89
- f"Schema:\n{schema_repr}\n"
90
- f"Data:\n{data_repr}\n\n"
91
- f"Citations:\n{citations_str}"
92
- )
93
-
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
- )
137
+
138
+
139
+ class ResearchTaskOperationEvent(BaseModel):
140
+ event_type: Annotated[Literal["task-operation"], Field(alias="eventType")]
141
+ research_id: Annotated[str, Field(alias="researchId")]
142
+ plan_id: Annotated[str, Field(alias="planId")]
143
+ task_id: Annotated[str, Field(alias="taskId")]
144
+ operation_id: Annotated[str, Field(alias="operationId")]
145
+ created_at: Annotated[
146
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
147
+ ]
148
+ data: ResearchOperation
149
+
150
+
151
+ class ResearchTaskOutput(BaseModel):
152
+ output_type: Annotated[Literal["completed"], Field(alias="outputType")]
153
+ content: str
154
+
155
+
156
+ class ResearchTaskOutputEvent(BaseModel):
157
+ event_type: Annotated[Literal["task-output"], Field(alias="eventType")]
158
+ research_id: Annotated[str, Field(alias="researchId")]
159
+ plan_id: Annotated[str, Field(alias="planId")]
160
+ task_id: Annotated[str, Field(alias="taskId")]
161
+ created_at: Annotated[
162
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
163
+ ]
164
+ output: ResearchTaskOutput
165
+
166
+
167
+ ResearchMetaEvent = Union[ResearchDefinitionEvent, ResearchOutputEvent]
168
+ ResearchPlanEvent = Union[
169
+ ResearchPlanDefinitionEvent, ResearchPlanOperationEvent, ResearchPlanOutputEvent
170
+ ]
171
+ ResearchTaskEvent = Union[
172
+ ResearchTaskDefinitionEvent, ResearchTaskOperationEvent, ResearchTaskOutputEvent
173
+ ]
174
+ ResearchEvent = Union[ResearchMetaEvent, ResearchPlanEvent, ResearchTaskEvent]
175
+
176
+
177
+ class ResearchOutput(BaseModel):
178
+ content: str
179
+ parsed: Optional[Dict[str, Any]] = None
180
+
181
+
182
+ class ResearchBaseDto(BaseModel):
183
+ research_id: Annotated[
184
+ str,
185
+ Field(
186
+ alias="researchId",
187
+ description="The unique identifier for the research request",
188
+ ),
189
+ ]
190
+ created_at: Annotated[
191
+ float, Field(alias="createdAt", description="Milliseconds since epoch time")
192
+ ]
193
+ model: Annotated[
194
+ Literal["exa-research", "exa-research-pro"],
195
+ Field(description="The model used for the research request"),
196
+ ] = "exa-research"
197
+ instructions: Annotated[
198
+ str, Field(description="The instructions given to this research request")
199
+ ]
200
+
201
+
202
+ class ResearchPendingDto(ResearchBaseDto):
203
+ status: Literal["pending"]
204
+
205
+
206
+ class ResearchRunningDto(ResearchBaseDto):
207
+ status: Literal["running"]
208
+ events: Optional[List[ResearchEvent]] = None
209
+
210
+
211
+ class ResearchCompletedDto(ResearchBaseDto):
212
+ status: Literal["completed"]
213
+ events: Optional[List[ResearchEvent]] = None
214
+ output: ResearchOutput
215
+ cost_dollars: Annotated[CostDollars, Field(alias="costDollars")]
216
+
217
+
218
+ class ResearchCanceledDto(ResearchBaseDto):
219
+ status: Literal["canceled"]
220
+ events: Optional[List[ResearchEvent]] = None
221
+
222
+
223
+ class ResearchFailedDto(ResearchBaseDto):
224
+ status: Literal["failed"]
225
+ events: Optional[List[ResearchEvent]] = None
226
+ error: Annotated[
227
+ str, Field(description="A message indicating why the request failed")
228
+ ]
229
+
230
+
231
+ ResearchDto = Annotated[
232
+ Union[
233
+ ResearchPendingDto,
234
+ ResearchRunningDto,
235
+ ResearchCompletedDto,
236
+ ResearchCanceledDto,
237
+ ResearchFailedDto,
238
+ ],
239
+ Field(discriminator="status"),
240
+ ]
241
+
242
+
243
+ class ListResearchResponseDto(BaseModel):
244
+ data: Annotated[
245
+ List[ResearchDto], Field(description="The list of research requests")
246
+ ]
247
+ has_more: Annotated[
248
+ bool,
249
+ Field(
250
+ alias="hasMore",
251
+ description="Whether there are more results to paginate through",
252
+ ),
253
+ ]
254
+ next_cursor: Annotated[
255
+ Optional[str],
256
+ Field(
257
+ alias="nextCursor",
258
+ description="The cursor to paginate through the next set of results",
259
+ ),
260
+ ]
261
+
262
+
263
+ class ResearchCreateRequestDto(BaseModel):
264
+ model: Literal["exa-research", "exa-research-pro"] = "exa-research"
265
+ instructions: Annotated[
266
+ str,
267
+ Field(
268
+ description="Instructions for what research should be conducted",
269
+ max_length=4096,
270
+ ),
271
+ ]
272
+ output_schema: Annotated[Optional[Dict[str, Any]], Field(alias="outputSchema")] = (
273
+ None
274
+ )
275
+
276
+
277
+ ResearchDtoClass = RootModel[ResearchDto]
278
+ ResearchCreateRequestDtoClass = ResearchCreateRequestDto
279
+ ResearchEventDtoClass = RootModel[ResearchEvent]
280
+ ResearchOperationDtoClass = RootModel[ResearchOperation]
114
281
 
115
282
 
116
283
  __all__ = [
117
- "ResearchTaskId",
118
- "ResearchTask",
119
- "ListResearchTasksResponse",
284
+ "CostDollars",
285
+ "Result",
286
+ "ResearchThinkOperation",
287
+ "ResearchSearchOperation",
288
+ "ResearchCrawlOperation",
289
+ "ResearchOperation",
290
+ "ResearchDefinitionEvent",
291
+ "ResearchOutputCompleted",
292
+ "ResearchOutputFailed",
293
+ "ResearchOutputEvent",
294
+ "ResearchPlanDefinitionEvent",
295
+ "ResearchPlanOperationEvent",
296
+ "ResearchPlanOutputTasks",
297
+ "ResearchPlanOutputStop",
298
+ "ResearchPlanOutputEvent",
299
+ "ResearchTaskDefinitionEvent",
300
+ "ResearchTaskOperationEvent",
301
+ "ResearchTaskOutput",
302
+ "ResearchTaskOutputEvent",
303
+ "ResearchMetaEvent",
304
+ "ResearchPlanEvent",
305
+ "ResearchTaskEvent",
306
+ "ResearchEvent",
307
+ "ResearchOutput",
308
+ "ResearchBaseDto",
309
+ "ResearchPendingDto",
310
+ "ResearchRunningDto",
311
+ "ResearchCompletedDto",
312
+ "ResearchCanceledDto",
313
+ "ResearchFailedDto",
314
+ "ResearchDto",
315
+ "ListResearchResponseDto",
316
+ "ResearchCreateRequestDto",
317
+ "ResearchDtoClass",
318
+ "ResearchCreateRequestDtoClass",
319
+ "ResearchEventDtoClass",
320
+ "ResearchOperationDtoClass",
120
321
  ]