exa-py 1.14.14__py3-none-any.whl → 1.14.15__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/research/client.py
CHANGED
|
@@ -11,11 +11,13 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Literal
|
|
13
13
|
|
|
14
|
+
from exa_py.utils import JSONSchemaInput
|
|
15
|
+
from ..api import _convert_schema_input
|
|
16
|
+
|
|
14
17
|
if TYPE_CHECKING: # pragma: no cover – only for static analysers
|
|
15
18
|
# Import with full type info when static type-checking. `_Result` still
|
|
16
19
|
# lives in ``exa_py.api`` but the response model moved to
|
|
17
20
|
# ``exa_py.research.models``.
|
|
18
|
-
from ..api import _Result # noqa: F401
|
|
19
21
|
from .models import (
|
|
20
22
|
ResearchTask,
|
|
21
23
|
ResearchTaskId,
|
|
@@ -41,7 +43,7 @@ class ResearchClient:
|
|
|
41
43
|
instructions: str,
|
|
42
44
|
model: Literal["exa-research", "exa-research-pro"] = "exa-research",
|
|
43
45
|
output_infer_schema: bool = None,
|
|
44
|
-
output_schema:
|
|
46
|
+
output_schema: "Optional[JSONSchemaInput]" = None,
|
|
45
47
|
) -> "ResearchTaskId":
|
|
46
48
|
"""Submit a research request and return the *task identifier*."""
|
|
47
49
|
payload = {"instructions": instructions}
|
|
@@ -50,7 +52,7 @@ class ResearchClient:
|
|
|
50
52
|
if output_schema is not None or output_infer_schema is not None:
|
|
51
53
|
payload["output"] = {}
|
|
52
54
|
if output_schema is not None:
|
|
53
|
-
payload["output"]["schema"] = output_schema
|
|
55
|
+
payload["output"]["schema"] = _convert_schema_input(output_schema)
|
|
54
56
|
if output_infer_schema is not None:
|
|
55
57
|
payload["output"]["inferSchema"] = output_infer_schema
|
|
56
58
|
|
|
@@ -179,14 +181,17 @@ class AsyncResearchClient:
|
|
|
179
181
|
*,
|
|
180
182
|
instructions: str,
|
|
181
183
|
model: Literal["exa-research", "exa-research-pro"] = "exa-research",
|
|
182
|
-
output_schema:
|
|
184
|
+
output_schema: "JSONSchemaInput",
|
|
183
185
|
) -> "ResearchTaskId":
|
|
184
186
|
"""Submit a research request and return the *task identifier* (async)."""
|
|
185
187
|
|
|
188
|
+
# Convert schema using the same conversion logic as main API
|
|
189
|
+
from ..api import _convert_schema_input # noqa: WPS433 – runtime import
|
|
190
|
+
|
|
186
191
|
payload = {
|
|
187
192
|
"instructions": instructions,
|
|
188
193
|
"model": model,
|
|
189
|
-
"output": {"schema": output_schema},
|
|
194
|
+
"output": {"schema": _convert_schema_input(output_schema)},
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
raw_response: Dict[str, Any] = await self._client.async_request(
|
|
@@ -327,18 +332,20 @@ def _build_research_task(raw: Dict[str, Any]):
|
|
|
327
332
|
results = []
|
|
328
333
|
for c in cites:
|
|
329
334
|
snake_c = to_snake_case(c)
|
|
330
|
-
results.append(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
)
|
|
342
349
|
citations_parsed[key] = results
|
|
343
350
|
|
|
344
351
|
return ResearchTask(
|
exa_py/utils.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import json
|
|
2
|
-
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Optional, Union
|
|
3
4
|
from openai.types.chat import ChatCompletion
|
|
4
5
|
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic.json_schema import GenerateJsonSchema
|
|
10
|
+
|
|
6
11
|
if TYPE_CHECKING:
|
|
7
12
|
from exa_py.api import ResultWithText, SearchResponse
|
|
8
13
|
|
|
9
14
|
|
|
10
|
-
|
|
11
15
|
def maybe_get_query(completion) -> Optional[str]:
|
|
12
16
|
"""Extract query from completion if it exists."""
|
|
13
17
|
if completion.choices[0].message.tool_calls:
|
|
@@ -24,25 +28,25 @@ def add_message_to_messages(completion, messages, exa_result) -> list[dict]:
|
|
|
24
28
|
assert assistant_message.tool_calls, "Must use this with a tool call request"
|
|
25
29
|
# Remove previous exa call and results to prevent blowing up history
|
|
26
30
|
messages = [
|
|
27
|
-
message
|
|
28
|
-
for message in messages
|
|
29
|
-
if not (message.get("role") == "function")
|
|
31
|
+
message for message in messages if not (message.get("role") == "function")
|
|
30
32
|
]
|
|
31
|
-
|
|
32
|
-
messages.extend(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
|
|
34
|
+
messages.extend(
|
|
35
|
+
[
|
|
36
|
+
assistant_message,
|
|
37
|
+
{
|
|
38
|
+
"role": "tool",
|
|
39
|
+
"name": "search",
|
|
40
|
+
"tool_call_id": assistant_message.tool_calls[0].id,
|
|
41
|
+
"content": exa_result,
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
)
|
|
41
45
|
|
|
42
46
|
return messages
|
|
43
47
|
|
|
44
48
|
|
|
45
|
-
def format_exa_result(exa_result, max_len: int
|
|
49
|
+
def format_exa_result(exa_result, max_len: int = -1):
|
|
46
50
|
"""Format exa result for pasting into chat."""
|
|
47
51
|
str = [
|
|
48
52
|
f"Url: {result.url}\nTitle: {result.title}\n{result.text[:max_len]}\n"
|
|
@@ -54,18 +58,35 @@ def format_exa_result(exa_result, max_len: int=-1):
|
|
|
54
58
|
|
|
55
59
|
class ExaOpenAICompletion(ChatCompletion):
|
|
56
60
|
"""Exa wrapper for OpenAI completion."""
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
exa_result: Optional["SearchResponse[ResultWithText]"],
|
|
65
|
+
id,
|
|
66
|
+
choices,
|
|
67
|
+
created,
|
|
68
|
+
model,
|
|
69
|
+
object,
|
|
70
|
+
system_fingerprint=None,
|
|
71
|
+
usage=None,
|
|
72
|
+
):
|
|
73
|
+
super().__init__(
|
|
74
|
+
id=id,
|
|
75
|
+
choices=choices,
|
|
76
|
+
created=created,
|
|
77
|
+
model=model,
|
|
78
|
+
object=object,
|
|
79
|
+
system_fingerprint=system_fingerprint,
|
|
80
|
+
usage=usage,
|
|
81
|
+
)
|
|
59
82
|
self.exa_result = exa_result
|
|
60
|
-
|
|
61
83
|
|
|
62
84
|
@classmethod
|
|
63
85
|
def from_completion(
|
|
64
|
-
cls,
|
|
65
|
-
exa_result: Optional["SearchResponse[ResultWithText]"],
|
|
66
|
-
completion: ChatCompletion
|
|
86
|
+
cls,
|
|
87
|
+
exa_result: Optional["SearchResponse[ResultWithText]"],
|
|
88
|
+
completion: ChatCompletion,
|
|
67
89
|
):
|
|
68
|
-
|
|
69
90
|
return cls(
|
|
70
91
|
exa_result=exa_result,
|
|
71
92
|
id=completion.id,
|
|
@@ -76,3 +97,98 @@ class ExaOpenAICompletion(ChatCompletion):
|
|
|
76
97
|
system_fingerprint=completion.system_fingerprint,
|
|
77
98
|
usage=completion.usage,
|
|
78
99
|
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
JSONSchemaInput = Union[type[BaseModel], dict[str, Any]]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class InlineJsonSchemaGenerator(GenerateJsonSchema):
|
|
106
|
+
"""Custom JSON schema generator that inlines all schemas without creating $defs references."""
|
|
107
|
+
|
|
108
|
+
def generate(self, schema, mode="validation"):
|
|
109
|
+
"""Generate JSON schema normally, then post-process to inline all refs."""
|
|
110
|
+
# Let Pydantic do its normal thing first
|
|
111
|
+
result = super().generate(schema, mode)
|
|
112
|
+
|
|
113
|
+
# Post-process to inline all $ref references
|
|
114
|
+
if "$defs" in result:
|
|
115
|
+
definitions = result["$defs"]
|
|
116
|
+
inlined_result = self._inline_refs(result, definitions)
|
|
117
|
+
# Remove $defs since everything is now inlined
|
|
118
|
+
if "$defs" in inlined_result:
|
|
119
|
+
del inlined_result["$defs"]
|
|
120
|
+
return inlined_result
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
def _inline_refs(self, obj, definitions):
|
|
125
|
+
"""Recursively replace all $ref with actual definitions."""
|
|
126
|
+
if isinstance(obj, dict):
|
|
127
|
+
if "$ref" in obj and len(obj) == 1: # Pure ref object
|
|
128
|
+
ref_path = obj["$ref"]
|
|
129
|
+
if ref_path.startswith("#/$defs/"):
|
|
130
|
+
def_name = ref_path[8:] # Remove '#/$defs/'
|
|
131
|
+
if def_name in definitions:
|
|
132
|
+
# Replace the ref with the actual definition (recursively processed)
|
|
133
|
+
return self._inline_refs(definitions[def_name], definitions)
|
|
134
|
+
return obj # Return as-is if we can't resolve
|
|
135
|
+
else:
|
|
136
|
+
# Process all values in the dict
|
|
137
|
+
return {
|
|
138
|
+
key: self._inline_refs(value, definitions)
|
|
139
|
+
for key, value in obj.items()
|
|
140
|
+
}
|
|
141
|
+
elif isinstance(obj, list):
|
|
142
|
+
# Process all items in the list
|
|
143
|
+
return [self._inline_refs(item, definitions) for item in obj]
|
|
144
|
+
else:
|
|
145
|
+
# Primitive value, return as-is
|
|
146
|
+
return obj
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _convert_schema_input(schema_input: "JSONSchemaInput") -> dict[str, Any]:
|
|
150
|
+
"""Convert various schema input types to JSON Schema dict.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
schema_input: Either a Pydantic BaseModel class or a dict containing JSON Schema
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
dict: JSON Schema representation (fully inlined without $defs)
|
|
157
|
+
"""
|
|
158
|
+
# Check if it's a Pydantic model class (not instance)
|
|
159
|
+
if isinstance(schema_input, type) and issubclass(schema_input, BaseModel):
|
|
160
|
+
return schema_input.model_json_schema(
|
|
161
|
+
by_alias=False,
|
|
162
|
+
mode="serialization",
|
|
163
|
+
schema_generator=InlineJsonSchemaGenerator,
|
|
164
|
+
)
|
|
165
|
+
elif isinstance(schema_input, dict):
|
|
166
|
+
return schema_input
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Unsupported schema type: {type(schema_input)}. Expected BaseModel class or dict."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _get_package_version() -> str:
|
|
174
|
+
"""Get the package version from pyproject.toml."""
|
|
175
|
+
try:
|
|
176
|
+
try:
|
|
177
|
+
import tomllib # Python 3.11+
|
|
178
|
+
except ImportError:
|
|
179
|
+
import tomli as tomllib # fallback for older versions
|
|
180
|
+
|
|
181
|
+
import os
|
|
182
|
+
|
|
183
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
184
|
+
project_root = os.path.dirname(current_dir)
|
|
185
|
+
pyproject_path = os.path.join(project_root, "pyproject.toml")
|
|
186
|
+
|
|
187
|
+
if os.path.exists(pyproject_path):
|
|
188
|
+
with open(pyproject_path, "rb") as f:
|
|
189
|
+
data = tomllib.load(f)
|
|
190
|
+
return data.get("project", {}).get("version", "unknown")
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
return "unknown"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
exa_py/__init__.py,sha256=M2GC9oSdoV6m2msboW0vMWWl8wrth4o6gmEV4MYLGG8,66
|
|
2
|
-
exa_py/api.py,sha256=
|
|
2
|
+
exa_py/api.py,sha256=S2GfFiUSQrogwqSWqQvN2w6wb4yrbZAmgERI6NntjSQ,106657
|
|
3
3
|
exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
exa_py/research/__init__.py,sha256=QeY-j6bP4QP5tF9ytX0IeQhJvd0Wn4cJCD69U8pP7kA,271
|
|
5
|
-
exa_py/research/client.py,sha256=
|
|
5
|
+
exa_py/research/client.py,sha256=mnoTA4Qoa0TA5d8nVTR9tAU9LJElXV-MlPozgMxlUp4,12799
|
|
6
6
|
exa_py/research/models.py,sha256=j7YgRoMRp2MLgnaij7775x_hJEeV5gksKpfLwmawqxY,3704
|
|
7
|
-
exa_py/utils.py,sha256=
|
|
7
|
+
exa_py/utils.py,sha256=eYnJRAFJonwKP_mCxzAB9TnLEqoF-88stg6wh-M-Ups,6424
|
|
8
8
|
exa_py/websets/__init__.py,sha256=x7Dc0MS8raRXA7Ud6alKgnsUmLi6X9GTqfB8kOwC9iQ,179
|
|
9
9
|
exa_py/websets/_generator/pydantic/BaseModel.jinja2,sha256=RUDCmPZVamoVx1WudylscYFfDhGoNNtRYlpTvKjAiuA,1276
|
|
10
10
|
exa_py/websets/client.py,sha256=v8Y0p5PosjLkb7EYQ83g3nmoIIHmKaXF7JQVT8K5h2E,4967
|
|
@@ -27,6 +27,6 @@ exa_py/websets/searches/client.py,sha256=X3f7axWGfecmxf-2tBTX0Yf_--xToz1X8ZHbbud
|
|
|
27
27
|
exa_py/websets/types.py,sha256=W0ZV_ETW-sDXNh9fK9qROmPRHU_koy1rfTGhjCG3yrA,42231
|
|
28
28
|
exa_py/websets/webhooks/__init__.py,sha256=iTPBCxFd73z4RifLQMX6iRECx_6pwlI5qscLNjMOUHE,77
|
|
29
29
|
exa_py/websets/webhooks/client.py,sha256=zsIRMTeJU65yj-zo7Zz-gG02Prtzgcx6utGFSoY4HQQ,4222
|
|
30
|
-
exa_py-1.14.
|
|
31
|
-
exa_py-1.14.
|
|
32
|
-
exa_py-1.14.
|
|
30
|
+
exa_py-1.14.15.dist-info/METADATA,sha256=yKrbylAzoYKm2O0JcwCsFmZaOWnQ62blvqBmvA4GK74,3827
|
|
31
|
+
exa_py-1.14.15.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
32
|
+
exa_py-1.14.15.dist-info/RECORD,,
|
|
File without changes
|