exa-py 1.12.3__py3-none-any.whl → 1.12.4__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 +4 -39
- exa_py/research/__init__.py +3 -2
- exa_py/research/client.py +155 -180
- exa_py/research/models.py +51 -10
- {exa_py-1.12.3.dist-info → exa_py-1.12.4.dist-info}/METADATA +1 -1
- {exa_py-1.12.3.dist-info → exa_py-1.12.4.dist-info}/RECORD +7 -7
- {exa_py-1.12.3.dist-info → exa_py-1.12.4.dist-info}/WHEEL +0 -0
exa_py/api.py
CHANGED
|
@@ -39,7 +39,6 @@ from exa_py.utils import (
|
|
|
39
39
|
from .websets import WebsetsClient
|
|
40
40
|
from .websets.core.base import ExaJSONEncoder
|
|
41
41
|
from .research.client import ResearchClient, AsyncResearchClient
|
|
42
|
-
from .research.models import ResearchTaskResponse # noqa: E402,F401
|
|
43
42
|
|
|
44
43
|
is_beta = os.getenv("IS_BETA") == "True"
|
|
45
44
|
|
|
@@ -839,37 +838,6 @@ def nest_fields(original_dict: Dict, fields_to_nest: List[str], new_key: str):
|
|
|
839
838
|
return original_dict
|
|
840
839
|
|
|
841
840
|
|
|
842
|
-
@dataclass
|
|
843
|
-
class ResearchTaskResponse:
|
|
844
|
-
"""A class representing the response for a research task.
|
|
845
|
-
|
|
846
|
-
Attributes:
|
|
847
|
-
id (str): The unique identifier for the research request.
|
|
848
|
-
status (str): Status of the research request.
|
|
849
|
-
output (Optional[Dict[str, Any]]): The answer structured as JSON, if available.
|
|
850
|
-
citations (Optional[Dict[str, List[_Result]]]): List of citations used to generate the answer, grouped by root field in the output schema.
|
|
851
|
-
"""
|
|
852
|
-
|
|
853
|
-
id: str
|
|
854
|
-
status: str
|
|
855
|
-
output: Optional[Dict[str, Any]]
|
|
856
|
-
citations: Dict[str, List[_Result]]
|
|
857
|
-
|
|
858
|
-
def __str__(self):
|
|
859
|
-
output_repr = (
|
|
860
|
-
json.dumps(self.output, indent=2, ensure_ascii=False)
|
|
861
|
-
if self.output is not None
|
|
862
|
-
else "None"
|
|
863
|
-
)
|
|
864
|
-
citations_str = "\n\n".join(str(src) for src in self.citations)
|
|
865
|
-
return (
|
|
866
|
-
f"ID: {self.id}\n"
|
|
867
|
-
f"Status: {self.status}\n"
|
|
868
|
-
f"Output: {output_repr}\n\n"
|
|
869
|
-
f"Citations:\n{citations_str}"
|
|
870
|
-
)
|
|
871
|
-
|
|
872
|
-
|
|
873
841
|
class Exa:
|
|
874
842
|
"""A client for interacting with Exa API."""
|
|
875
843
|
|
|
@@ -877,7 +845,7 @@ class Exa:
|
|
|
877
845
|
self,
|
|
878
846
|
api_key: Optional[str],
|
|
879
847
|
base_url: str = "https://api.exa.ai",
|
|
880
|
-
user_agent: str = "exa-py 1.12.
|
|
848
|
+
user_agent: str = "exa-py 1.12.4",
|
|
881
849
|
):
|
|
882
850
|
"""Initialize the Exa client with the provided API key and optional base URL and user agent.
|
|
883
851
|
|
|
@@ -909,7 +877,6 @@ class Exa:
|
|
|
909
877
|
data: Optional[Union[Dict[str, Any], str]] = None,
|
|
910
878
|
method: str = "POST",
|
|
911
879
|
params: Optional[Dict[str, Any]] = None,
|
|
912
|
-
force_stream: Optional[bool] = False,
|
|
913
880
|
) -> Union[Dict[str, Any], requests.Response]:
|
|
914
881
|
"""Send a request to the Exa API, optionally streaming if data['stream'] is True.
|
|
915
882
|
|
|
@@ -934,7 +901,7 @@ class Exa:
|
|
|
934
901
|
# Otherwise, serialize the dictionary to JSON if it exists
|
|
935
902
|
json_data = json.dumps(data, cls=ExaJSONEncoder) if data else None
|
|
936
903
|
|
|
937
|
-
if
|
|
904
|
+
if data and data.get("stream"):
|
|
938
905
|
res = requests.post(
|
|
939
906
|
self.base_url + endpoint,
|
|
940
907
|
data=json_data,
|
|
@@ -1974,9 +1941,7 @@ class AsyncExa(Exa):
|
|
|
1974
1941
|
)
|
|
1975
1942
|
return self._client
|
|
1976
1943
|
|
|
1977
|
-
async def async_request(
|
|
1978
|
-
self, endpoint: str, data, force_stream: Optional[bool] = False
|
|
1979
|
-
):
|
|
1944
|
+
async def async_request(self, endpoint: str, data):
|
|
1980
1945
|
"""Send a POST request to the Exa API, optionally streaming if data['stream'] is True.
|
|
1981
1946
|
|
|
1982
1947
|
Args:
|
|
@@ -1990,7 +1955,7 @@ class AsyncExa(Exa):
|
|
|
1990
1955
|
Raises:
|
|
1991
1956
|
ValueError: If the request fails (non-200 status code).
|
|
1992
1957
|
"""
|
|
1993
|
-
if data.get("stream")
|
|
1958
|
+
if data.get("stream"):
|
|
1994
1959
|
request = httpx.Request(
|
|
1995
1960
|
"POST", self.base_url + endpoint, json=data, headers=self.headers
|
|
1996
1961
|
)
|
exa_py/research/__init__.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from .client import ResearchClient, AsyncResearchClient
|
|
2
|
-
from .models import
|
|
2
|
+
from .models import ResearchTask
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
5
|
"ResearchClient",
|
|
6
6
|
"AsyncResearchClient",
|
|
7
|
-
"
|
|
7
|
+
"ResearchTaskId",
|
|
8
|
+
"ResearchTask",
|
|
8
9
|
]
|
exa_py/research/client.py
CHANGED
|
@@ -9,14 +9,14 @@ 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
|
|
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 ResearchTask, ResearchTaskId # noqa: F401
|
|
20
20
|
|
|
21
21
|
# ---------------------------------------------------------------------------
|
|
22
22
|
# Public, user-facing clients
|
|
@@ -31,118 +31,82 @@ class ResearchClient:
|
|
|
31
31
|
# can piggy-back on its HTTP plumbing (headers, base URL, retries, …).
|
|
32
32
|
self._client = parent_client
|
|
33
33
|
|
|
34
|
-
# ------------------------------------------------------------------
|
|
35
|
-
# API surface
|
|
36
|
-
# ------------------------------------------------------------------
|
|
37
34
|
def create_task(
|
|
38
35
|
self,
|
|
39
36
|
*,
|
|
40
37
|
input_instructions: str,
|
|
41
38
|
output_schema: Dict[str, Any],
|
|
42
|
-
) -> "
|
|
43
|
-
"""Submit a research request
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
) -> "ResearchTaskId":
|
|
40
|
+
"""Submit a research request and return the *task identifier*."""
|
|
41
|
+
payload = {
|
|
42
|
+
"input": {"instructions": input_instructions},
|
|
43
|
+
"output": {"schema": output_schema},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
raw_response: Dict[str, Any] = self._client.request("/research/tasks", payload)
|
|
47
|
+
|
|
48
|
+
# Defensive checks so that we fail loudly if the contract changes.
|
|
49
|
+
if not isinstance(raw_response, dict) or "id" not in raw_response:
|
|
50
|
+
raise RuntimeError(
|
|
51
|
+
f"Unexpected response while creating research task: {raw_response}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Lazily import to avoid circular deps at runtime.
|
|
55
|
+
from .models import ResearchTaskId # noqa: WPS433 – runtime import
|
|
56
|
+
|
|
57
|
+
return ResearchTaskId(id=raw_response["id"])
|
|
58
|
+
|
|
59
|
+
def get_task(
|
|
60
|
+
self, id: str
|
|
61
|
+
) -> "ResearchTask": # noqa: D401 – imperative mood is fine
|
|
62
|
+
"""Fetch the current status / result for a research task."""
|
|
63
|
+
endpoint = f"/research/tasks/{id}"
|
|
64
|
+
|
|
65
|
+
# The new endpoint is a simple GET.
|
|
66
|
+
raw_response: Dict[str, Any] = self._client.request(endpoint, method="GET")
|
|
67
|
+
|
|
68
|
+
return _build_research_task(raw_response)
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# Convenience helpers
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def poll_task(
|
|
75
|
+
self,
|
|
76
|
+
id: str,
|
|
77
|
+
*,
|
|
78
|
+
poll_interval: float = 1.0,
|
|
79
|
+
timeout_seconds: int = 15 * 60,
|
|
80
|
+
) -> "ResearchTask":
|
|
81
|
+
"""Blocking helper that polls until task completes or fails.
|
|
53
82
|
|
|
54
83
|
Parameters
|
|
55
84
|
----------
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
id:
|
|
86
|
+
The ID of the research task to poll.
|
|
87
|
+
poll_interval:
|
|
88
|
+
Seconds to wait between successive polls (default 1s).
|
|
89
|
+
timeout_seconds:
|
|
90
|
+
Maximum time to wait before raising :class:`TimeoutError` (default 15 min).
|
|
61
91
|
"""
|
|
62
92
|
|
|
63
|
-
import
|
|
93
|
+
import time
|
|
64
94
|
|
|
65
|
-
|
|
66
|
-
"input": {"instructions": input_instructions},
|
|
67
|
-
"output": {"schema": output_schema},
|
|
68
|
-
}
|
|
95
|
+
deadline = time.monotonic() + timeout_seconds
|
|
69
96
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
while True:
|
|
98
|
+
task = self.get_task(id)
|
|
99
|
+
status = task.status.lower() if isinstance(task.status, str) else ""
|
|
73
100
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if tag_local == "error":
|
|
84
|
-
msg = payload_dict.get("error", {}).get("message", "Unknown error")
|
|
85
|
-
raise RuntimeError(f"Research task failed: {msg}")
|
|
86
|
-
if tag_local == "complete":
|
|
87
|
-
data_obj = payload_dict.get("data")
|
|
88
|
-
if data_obj is None:
|
|
89
|
-
raise RuntimeError("Malformed 'complete' chunk with no data")
|
|
90
|
-
return _parse_research_response(data_obj)
|
|
91
|
-
|
|
92
|
-
# Fallback: if looks like final object
|
|
93
|
-
if {"id", "status"}.issubset(payload_dict.keys()):
|
|
94
|
-
return _parse_research_response(payload_dict)
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
# ------------------------------------------------------------------
|
|
98
|
-
# Minimal SSE parser (sync)
|
|
99
|
-
# ------------------------------------------------------------------
|
|
100
|
-
event_name: Optional[str] = None
|
|
101
|
-
data_buf: str = ""
|
|
102
|
-
|
|
103
|
-
for raw_line in raw_response.iter_lines(decode_unicode=True):
|
|
104
|
-
line = raw_line
|
|
105
|
-
if line == "":
|
|
106
|
-
if data_buf:
|
|
107
|
-
try:
|
|
108
|
-
payload_dict = json.loads(data_buf)
|
|
109
|
-
except json.JSONDecodeError:
|
|
110
|
-
data_buf = ""
|
|
111
|
-
event_name = None
|
|
112
|
-
continue
|
|
113
|
-
maybe_resp = _handle_payload(event_name, payload_dict)
|
|
114
|
-
if maybe_resp is not None:
|
|
115
|
-
raw_response.close()
|
|
116
|
-
return maybe_resp
|
|
117
|
-
# reset after event
|
|
118
|
-
data_buf = ""
|
|
119
|
-
event_name = None
|
|
120
|
-
continue
|
|
121
|
-
|
|
122
|
-
if line.startswith("event:"):
|
|
123
|
-
event_name = line[len("event:") :].strip()
|
|
124
|
-
elif line.startswith("data:"):
|
|
125
|
-
data_buf += line[len("data:") :].strip()
|
|
126
|
-
|
|
127
|
-
# Process any remaining buffer (in case stream closed without blank line)
|
|
128
|
-
if data_buf:
|
|
129
|
-
try:
|
|
130
|
-
payload_dict = json.loads(data_buf)
|
|
131
|
-
maybe_resp = _handle_payload(event_name, payload_dict)
|
|
132
|
-
if maybe_resp is not None:
|
|
133
|
-
raw_response.close()
|
|
134
|
-
return maybe_resp
|
|
135
|
-
except json.JSONDecodeError:
|
|
136
|
-
pass
|
|
137
|
-
|
|
138
|
-
raise RuntimeError("Stream ended before completion of research task")
|
|
139
|
-
|
|
140
|
-
def get_task(self, id: str): # noqa: D401 – imperative mood is fine
|
|
141
|
-
"""Placeholder endpoint – not yet implemented on the server side."""
|
|
142
|
-
raise NotImplementedError(
|
|
143
|
-
"`exa.research.get_task` is not available yet. Please open an "
|
|
144
|
-
"issue if you need this sooner."
|
|
145
|
-
)
|
|
101
|
+
if status in {"completed", "failed", "complete", "finished", "done"}:
|
|
102
|
+
return task
|
|
103
|
+
|
|
104
|
+
if time.monotonic() > deadline:
|
|
105
|
+
raise TimeoutError(
|
|
106
|
+
f"Research task {id} did not finish within {timeout_seconds} seconds"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
time.sleep(poll_interval)
|
|
146
110
|
|
|
147
111
|
|
|
148
112
|
class AsyncResearchClient:
|
|
@@ -156,102 +120,113 @@ class AsyncResearchClient:
|
|
|
156
120
|
*,
|
|
157
121
|
input_instructions: str,
|
|
158
122
|
output_schema: Dict[str, Any],
|
|
159
|
-
) -> "
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
import json
|
|
123
|
+
) -> "ResearchTaskId":
|
|
124
|
+
"""Submit a research request and return the *task identifier* (async)."""
|
|
163
125
|
|
|
164
126
|
payload = {
|
|
165
127
|
"input": {"instructions": input_instructions},
|
|
166
128
|
"output": {"schema": output_schema},
|
|
167
129
|
}
|
|
168
130
|
|
|
169
|
-
raw_response = await self._client.async_request(
|
|
170
|
-
"/research/tasks", payload
|
|
131
|
+
raw_response: Dict[str, Any] = await self._client.async_request(
|
|
132
|
+
"/research/tasks", payload
|
|
171
133
|
)
|
|
172
134
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return None
|
|
194
|
-
|
|
195
|
-
event_name: Optional[str] = None
|
|
196
|
-
data_buf: str = ""
|
|
197
|
-
|
|
198
|
-
async for line in raw_response.aiter_lines():
|
|
199
|
-
if line == "":
|
|
200
|
-
if data_buf:
|
|
201
|
-
try:
|
|
202
|
-
payload_dict = json.loads(data_buf)
|
|
203
|
-
except json.JSONDecodeError:
|
|
204
|
-
data_buf = ""
|
|
205
|
-
event_name = None
|
|
206
|
-
continue
|
|
207
|
-
maybe_resp = await _handle_payload_async(event_name, payload_dict)
|
|
208
|
-
if maybe_resp is not None:
|
|
209
|
-
await raw_response.aclose()
|
|
210
|
-
return maybe_resp
|
|
211
|
-
data_buf = ""
|
|
212
|
-
event_name = None
|
|
213
|
-
continue
|
|
214
|
-
|
|
215
|
-
if line.startswith("event:"):
|
|
216
|
-
event_name = line[len("event:") :].strip()
|
|
217
|
-
elif line.startswith("data:"):
|
|
218
|
-
data_buf += line[len("data:") :].strip()
|
|
219
|
-
|
|
220
|
-
if data_buf:
|
|
221
|
-
try:
|
|
222
|
-
payload_dict = json.loads(data_buf)
|
|
223
|
-
maybe_resp = await _handle_payload_async(event_name, payload_dict)
|
|
224
|
-
if maybe_resp is not None:
|
|
225
|
-
await raw_response.aclose()
|
|
226
|
-
return maybe_resp
|
|
227
|
-
except json.JSONDecodeError:
|
|
228
|
-
pass
|
|
229
|
-
|
|
230
|
-
raise RuntimeError("Stream ended before completion of research task")
|
|
231
|
-
|
|
232
|
-
async def get_task(self, id: str): # noqa: D401
|
|
233
|
-
raise NotImplementedError(
|
|
234
|
-
"`exa.research.get_task` is not available yet. Please open an "
|
|
235
|
-
"issue if you need this sooner."
|
|
135
|
+
# Defensive checks so that we fail loudly if the contract changes.
|
|
136
|
+
if not isinstance(raw_response, dict) or "id" not in raw_response:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"Unexpected response while creating research task: {raw_response}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Lazily import to avoid circular deps at runtime.
|
|
142
|
+
from .models import ResearchTaskId # noqa: WPS433 – runtime import
|
|
143
|
+
|
|
144
|
+
return ResearchTaskId(id=raw_response["id"])
|
|
145
|
+
|
|
146
|
+
async def get_task(self, id: str) -> "ResearchTask": # noqa: D401
|
|
147
|
+
"""Fetch the current status / result for a research task (async)."""
|
|
148
|
+
|
|
149
|
+
endpoint = f"/research/tasks/{id}"
|
|
150
|
+
|
|
151
|
+
# Perform GET using the underlying HTTP client because `async_request`
|
|
152
|
+
# only supports POST semantics.
|
|
153
|
+
resp = await self._client.client.get(
|
|
154
|
+
self._client.base_url + endpoint, headers=self._client.headers
|
|
236
155
|
)
|
|
237
156
|
|
|
157
|
+
if resp.status_code >= 400:
|
|
158
|
+
raise RuntimeError(
|
|
159
|
+
f"Request failed with status code {resp.status_code}: {resp.text}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
raw_response: Dict[str, Any] = resp.json()
|
|
163
|
+
|
|
164
|
+
return _build_research_task(raw_response)
|
|
165
|
+
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
# Convenience helpers
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
async def poll_task(
|
|
171
|
+
self,
|
|
172
|
+
id: str,
|
|
173
|
+
*,
|
|
174
|
+
poll_interval: float = 1.0,
|
|
175
|
+
timeout_seconds: int = 15 * 60,
|
|
176
|
+
) -> "ResearchTask":
|
|
177
|
+
"""Async helper that polls until task completes or fails.
|
|
178
|
+
|
|
179
|
+
Mirrors :py:meth:`ResearchClient.poll_task` but uses ``await`` and
|
|
180
|
+
:pyfunc:`asyncio.sleep`. Raises :class:`TimeoutError` on timeout.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
import asyncio
|
|
184
|
+
import time
|
|
185
|
+
|
|
186
|
+
deadline = time.monotonic() + timeout_seconds
|
|
187
|
+
|
|
188
|
+
while True:
|
|
189
|
+
task = await self.get_task(id)
|
|
190
|
+
status = task.status.lower() if isinstance(task.status, str) else ""
|
|
191
|
+
|
|
192
|
+
if status in {"completed", "failed", "complete", "finished", "done"}:
|
|
193
|
+
return task
|
|
194
|
+
|
|
195
|
+
if time.monotonic() > deadline:
|
|
196
|
+
raise TimeoutError(
|
|
197
|
+
f"Research task {id} did not finish within {timeout_seconds} seconds"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
await asyncio.sleep(poll_interval)
|
|
201
|
+
|
|
238
202
|
|
|
239
203
|
# ---------------------------------------------------------------------------
|
|
240
204
|
# Internal helpers (lazy imports to avoid cycles)
|
|
241
205
|
# ---------------------------------------------------------------------------
|
|
242
206
|
|
|
243
207
|
|
|
244
|
-
def
|
|
245
|
-
"""
|
|
246
|
-
|
|
247
|
-
|
|
208
|
+
def _build_research_task(raw: Dict[str, Any]):
|
|
209
|
+
"""Convert raw API response into a :class:`ResearchTask` instance."""
|
|
210
|
+
|
|
211
|
+
# Defensive check – fail loudly if the API contract changes.
|
|
212
|
+
if not isinstance(raw, dict) or "id" not in raw:
|
|
213
|
+
raise RuntimeError(f"Unexpected response while fetching research task: {raw}")
|
|
214
|
+
|
|
215
|
+
# Lazily import heavy deps to avoid cycles and unnecessary startup cost.
|
|
216
|
+
from .models import ResearchTask # noqa: WPS433 – runtime import
|
|
217
|
+
from ..api import _Result, to_snake_case # noqa: WPS433 – runtime import
|
|
218
|
+
|
|
219
|
+
citations_raw = raw.get("citations", {}) or {}
|
|
220
|
+
citations_parsed = {
|
|
221
|
+
key: [_Result(**to_snake_case(c)) for c in cites]
|
|
222
|
+
for key, cites in citations_raw.items()
|
|
223
|
+
}
|
|
248
224
|
|
|
249
|
-
return
|
|
225
|
+
return ResearchTask(
|
|
250
226
|
id=raw["id"],
|
|
251
227
|
status=raw["status"],
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
},
|
|
228
|
+
instructions=raw.get("instructions", ""),
|
|
229
|
+
schema=raw.get("schema", {}),
|
|
230
|
+
data=raw.get("data"),
|
|
231
|
+
citations=citations_parsed,
|
|
257
232
|
)
|
exa_py/research/models.py
CHANGED
|
@@ -12,8 +12,27 @@ if TYPE_CHECKING: # pragma: no cover – for static analysers only
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
15
|
-
class
|
|
16
|
-
"""Structured
|
|
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.
|
|
17
36
|
|
|
18
37
|
Attributes
|
|
19
38
|
----------
|
|
@@ -21,7 +40,11 @@ class ResearchTaskResponse:
|
|
|
21
40
|
Unique identifier for the research task.
|
|
22
41
|
status:
|
|
23
42
|
Current task status
|
|
24
|
-
|
|
43
|
+
instructions:
|
|
44
|
+
Instructions for the task
|
|
45
|
+
schema:
|
|
46
|
+
Output schema defining the task
|
|
47
|
+
data:
|
|
25
48
|
JSON-serialisable answer generated by Exa (may be ``None`` until the task
|
|
26
49
|
completes).
|
|
27
50
|
citations:
|
|
@@ -31,27 +54,45 @@ class ResearchTaskResponse:
|
|
|
31
54
|
|
|
32
55
|
id: str
|
|
33
56
|
status: str
|
|
34
|
-
|
|
57
|
+
instructions: str
|
|
58
|
+
schema: Dict[str, Any]
|
|
59
|
+
data: Optional[Dict[str, Any]]
|
|
35
60
|
citations: Dict[str, List["_Result"]]
|
|
36
61
|
|
|
37
62
|
# ---------------------------------------------------------------------
|
|
38
63
|
# Pretty representation helpers
|
|
39
64
|
# ---------------------------------------------------------------------
|
|
40
65
|
def __str__(self) -> str: # pragma: no cover – convenience only
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
71
|
else "None"
|
|
45
72
|
)
|
|
46
|
-
|
|
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
|
+
|
|
47
85
|
return (
|
|
48
86
|
f"ID: {self.id}\n"
|
|
49
87
|
f"Status: {self.status}\n"
|
|
50
|
-
f"
|
|
88
|
+
f"Instructions: {self.instructions}\n"
|
|
89
|
+
f"Schema:\n{schema_repr}\n"
|
|
90
|
+
f"Data:\n{data_repr}\n\n"
|
|
51
91
|
f"Citations:\n{citations_str}"
|
|
52
92
|
)
|
|
53
93
|
|
|
54
94
|
|
|
55
95
|
__all__ = [
|
|
56
|
-
"
|
|
96
|
+
"ResearchTaskId",
|
|
97
|
+
"ResearchTask",
|
|
57
98
|
]
|
|
@@ -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=Bn7h_eRvXmwBUmJi2B2JpHAQPrHfbwKf0A-XVXLjqa0,84876
|
|
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=D1xgm4VlbWtRb1cMgshcW4dIyR7IXhOW2s7ihxcE1Jc,195
|
|
5
|
+
exa_py/research/client.py,sha256=Zno5xblfwhX8gWgc4OvI24a-ZS7_g1b32_tr6j7C7Jg,8217
|
|
6
|
+
exa_py/research/models.py,sha256=WXTnALhM9FcVQ95Tzzc5EDKU48hyPhu8RSMmipqCjOk,2982
|
|
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
|
|
@@ -19,6 +19,6 @@ exa_py/websets/searches/client.py,sha256=X3f7axWGfecmxf-2tBTX0Yf_--xToz1X8ZHbbud
|
|
|
19
19
|
exa_py/websets/types.py,sha256=jKnJFAHTFN55EzsusgDce-yux71zVbdSJ1m8utR4EjU,28096
|
|
20
20
|
exa_py/websets/webhooks/__init__.py,sha256=iTPBCxFd73z4RifLQMX6iRECx_6pwlI5qscLNjMOUHE,77
|
|
21
21
|
exa_py/websets/webhooks/client.py,sha256=zsIRMTeJU65yj-zo7Zz-gG02Prtzgcx6utGFSoY4HQQ,4222
|
|
22
|
-
exa_py-1.12.
|
|
23
|
-
exa_py-1.12.
|
|
24
|
-
exa_py-1.12.
|
|
22
|
+
exa_py-1.12.4.dist-info/METADATA,sha256=DYYA35UrWmW9ND3x5L5YcDr1tPJN05x64UV4nJsRg1k,4098
|
|
23
|
+
exa_py-1.12.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
24
|
+
exa_py-1.12.4.dist-info/RECORD,,
|
|
File without changes
|