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: Dict[str, Any] = None,
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: Dict[str, Any],
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(_Result(
331
- url=snake_c.get("url"),
332
- id=snake_c.get("id"),
333
- title=snake_c.get("title"),
334
- score=snake_c.get("score"),
335
- published_date=snake_c.get("published_date"),
336
- author=snake_c.get("author"),
337
- image=snake_c.get("image"),
338
- favicon=snake_c.get("favicon"),
339
- subpages=snake_c.get("subpages"),
340
- extras=snake_c.get("extras")
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
- from typing import Optional
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
- assistant_message,
34
- {
35
- "role": "tool",
36
- "name": "search",
37
- "tool_call_id": assistant_message.tool_calls[0].id,
38
- "content": exa_result,
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=-1):
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
- def __init__(self, exa_result: Optional["SearchResponse[ResultWithText]"], id, choices, created, model, object, system_fingerprint=None, usage=None):
58
- super().__init__(id=id, choices=choices, created=created, model=model, object=object, system_fingerprint=system_fingerprint, usage=usage)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exa-py
3
- Version: 1.14.14
3
+ Version: 1.14.15
4
4
  Summary: Python SDK for Exa API.
5
5
  License: MIT
6
6
  Author: Exa AI
@@ -1,10 +1,10 @@
1
1
  exa_py/__init__.py,sha256=M2GC9oSdoV6m2msboW0vMWWl8wrth4o6gmEV4MYLGG8,66
2
- exa_py/api.py,sha256=IrToa0zN39SREpwwcyuFvnoE_-pIc9uWVDkEw07IN9I,99737
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=x9b299ROfbhdMrdto6jZCHGRJE5U5V2fWHsSlnnNkWo,12471
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=sq1KOajuMWI1lygYeb9MqjXn8yqVyK0BtFZZmTwSaMY,2580
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.14.dist-info/METADATA,sha256=GGN6xgOdXmRExHuU7eMR4P8RC_dC6Z4eHSGnugUaoGA,3827
31
- exa_py-1.14.14.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
- exa_py-1.14.14.dist-info/RECORD,,
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,,