tavily-python 0.7.20__tar.gz → 0.7.22__tar.gz
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.
- {tavily_python-0.7.20 → tavily_python-0.7.22}/PKG-INFO +16 -1
- {tavily_python-0.7.20 → tavily_python-0.7.22}/README.md +15 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/setup.py +1 -1
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/async_tavily.py +149 -141
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/tavily.py +4 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/PKG-INFO +16 -1
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/SOURCES.txt +2 -1
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_errors.py +4 -1
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_search.py +57 -2
- tavily_python-0.7.22/tests/test_session_pooling.py +152 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/LICENSE +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/setup.cfg +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/__init__.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/config.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/errors.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/hybrid_rag/__init__.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/hybrid_rag/hybrid_rag.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/utils.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/dependency_links.txt +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/requires.txt +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/top_level.txt +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_crawl.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_map.py +0 -0
- {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_research.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tavily-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.22
|
|
4
4
|
Summary: Python wrapper for the Tavily API
|
|
5
5
|
Home-page: https://github.com/tavily-ai/tavily-python
|
|
6
6
|
Author: Tavily AI
|
|
@@ -63,6 +63,21 @@ response = tavily_client.search("Who is Leo Messi?")
|
|
|
63
63
|
print(response)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
### Using exact match to find specific names or phrases
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from tavily import TavilyClient
|
|
70
|
+
|
|
71
|
+
client = TavilyClient(api_key="tvly-YOUR_API_KEY")
|
|
72
|
+
|
|
73
|
+
# Use exact_match=True to only return results containing the exact phrase(s) inside quotes
|
|
74
|
+
response = client.search(
|
|
75
|
+
query='"John Smith" CEO Acme Corp',
|
|
76
|
+
exact_match=True
|
|
77
|
+
)
|
|
78
|
+
print(response)
|
|
79
|
+
```
|
|
80
|
+
|
|
66
81
|
This is equivalent to directly querying our REST API.
|
|
67
82
|
|
|
68
83
|
### Generating context for a RAG Application
|
|
@@ -36,6 +36,21 @@ response = tavily_client.search("Who is Leo Messi?")
|
|
|
36
36
|
print(response)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
### Using exact match to find specific names or phrases
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from tavily import TavilyClient
|
|
43
|
+
|
|
44
|
+
client = TavilyClient(api_key="tvly-YOUR_API_KEY")
|
|
45
|
+
|
|
46
|
+
# Use exact_match=True to only return results containing the exact phrase(s) inside quotes
|
|
47
|
+
response = client.search(
|
|
48
|
+
query='"John Smith" CEO Acme Corp',
|
|
49
|
+
exact_match=True
|
|
50
|
+
)
|
|
51
|
+
print(response)
|
|
52
|
+
```
|
|
53
|
+
|
|
39
54
|
This is equivalent to directly querying our REST API.
|
|
40
55
|
|
|
41
56
|
### Generating context for a RAG Application
|
|
@@ -44,8 +44,9 @@ class AsyncTavilyClient:
|
|
|
44
44
|
tavily_project = project_id or os.getenv("TAVILY_PROJECT")
|
|
45
45
|
|
|
46
46
|
self._api_base_url = api_base_url or "https://api.tavily.com"
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
# Create a persistent client for connection pooling
|
|
49
|
+
self._client = httpx.AsyncClient(
|
|
49
50
|
headers={
|
|
50
51
|
"Content-Type": "application/json",
|
|
51
52
|
"Authorization": f"Bearer {api_key}",
|
|
@@ -57,6 +58,16 @@ class AsyncTavilyClient:
|
|
|
57
58
|
)
|
|
58
59
|
self._company_info_tags = company_info_tags
|
|
59
60
|
|
|
61
|
+
async def close(self):
|
|
62
|
+
"""Close the client and release connection pool resources."""
|
|
63
|
+
await self._client.aclose()
|
|
64
|
+
|
|
65
|
+
async def __aenter__(self):
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
69
|
+
await self.close()
|
|
70
|
+
|
|
60
71
|
async def _search(
|
|
61
72
|
self,
|
|
62
73
|
query: str,
|
|
@@ -77,6 +88,7 @@ class AsyncTavilyClient:
|
|
|
77
88
|
auto_parameters: bool = None,
|
|
78
89
|
include_favicon: bool = None,
|
|
79
90
|
include_usage: bool = None,
|
|
91
|
+
exact_match: bool = None,
|
|
80
92
|
**kwargs,
|
|
81
93
|
) -> dict:
|
|
82
94
|
"""
|
|
@@ -100,6 +112,7 @@ class AsyncTavilyClient:
|
|
|
100
112
|
"auto_parameters": auto_parameters,
|
|
101
113
|
"include_favicon": include_favicon,
|
|
102
114
|
"include_usage": include_usage,
|
|
115
|
+
"exact_match": exact_match,
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
data = {k: v for k, v in data.items() if v is not None}
|
|
@@ -109,11 +122,10 @@ class AsyncTavilyClient:
|
|
|
109
122
|
|
|
110
123
|
timeout = min(timeout, 120)
|
|
111
124
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
raise TimeoutError(timeout)
|
|
125
|
+
try:
|
|
126
|
+
response = await self._client.post("/search", content=json.dumps(data), timeout=timeout)
|
|
127
|
+
except httpx.TimeoutException:
|
|
128
|
+
raise TimeoutError(timeout)
|
|
117
129
|
|
|
118
130
|
if response.status_code == 200:
|
|
119
131
|
return response.json()
|
|
@@ -154,6 +166,7 @@ class AsyncTavilyClient:
|
|
|
154
166
|
auto_parameters: bool = None,
|
|
155
167
|
include_favicon: bool = None,
|
|
156
168
|
include_usage: bool = None,
|
|
169
|
+
exact_match: bool = None,
|
|
157
170
|
**kwargs, # Accept custom arguments
|
|
158
171
|
) -> dict:
|
|
159
172
|
"""
|
|
@@ -178,6 +191,7 @@ class AsyncTavilyClient:
|
|
|
178
191
|
auto_parameters=auto_parameters,
|
|
179
192
|
include_favicon=include_favicon,
|
|
180
193
|
include_usage=include_usage,
|
|
194
|
+
exact_match=exact_match,
|
|
181
195
|
**kwargs,
|
|
182
196
|
)
|
|
183
197
|
|
|
@@ -221,11 +235,10 @@ class AsyncTavilyClient:
|
|
|
221
235
|
if kwargs:
|
|
222
236
|
data.update(kwargs)
|
|
223
237
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
raise TimeoutError(timeout)
|
|
238
|
+
try:
|
|
239
|
+
response = await self._client.post("/extract", content=json.dumps(data), timeout=timeout)
|
|
240
|
+
except httpx.TimeoutException:
|
|
241
|
+
raise TimeoutError(timeout)
|
|
229
242
|
|
|
230
243
|
if response.status_code == 200:
|
|
231
244
|
return response.json()
|
|
@@ -283,7 +296,7 @@ class AsyncTavilyClient:
|
|
|
283
296
|
response_dict["failed_results"] = failed_results
|
|
284
297
|
|
|
285
298
|
return response_dict
|
|
286
|
-
|
|
299
|
+
|
|
287
300
|
async def _crawl(self,
|
|
288
301
|
url: str,
|
|
289
302
|
max_depth: int = None,
|
|
@@ -332,31 +345,30 @@ class AsyncTavilyClient:
|
|
|
332
345
|
|
|
333
346
|
data = {k: v for k, v in data.items() if v is not None}
|
|
334
347
|
|
|
335
|
-
|
|
348
|
+
try:
|
|
349
|
+
response = await self._client.post("/crawl", content=json.dumps(data), timeout=timeout)
|
|
350
|
+
except httpx.TimeoutException:
|
|
351
|
+
raise TimeoutError(timeout)
|
|
352
|
+
|
|
353
|
+
if response.status_code == 200:
|
|
354
|
+
return response.json()
|
|
355
|
+
else:
|
|
356
|
+
detail = ""
|
|
336
357
|
try:
|
|
337
|
-
|
|
338
|
-
except
|
|
339
|
-
|
|
358
|
+
detail = response.json().get("detail", {}).get("error", None)
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
340
361
|
|
|
341
|
-
if response.status_code ==
|
|
342
|
-
|
|
362
|
+
if response.status_code == 429:
|
|
363
|
+
raise UsageLimitExceededError(detail)
|
|
364
|
+
elif response.status_code in [403,432,433]:
|
|
365
|
+
raise ForbiddenError(detail)
|
|
366
|
+
elif response.status_code == 401:
|
|
367
|
+
raise InvalidAPIKeyError(detail)
|
|
368
|
+
elif response.status_code == 400:
|
|
369
|
+
raise BadRequestError(detail)
|
|
343
370
|
else:
|
|
344
|
-
|
|
345
|
-
try:
|
|
346
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
347
|
-
except Exception:
|
|
348
|
-
pass
|
|
349
|
-
|
|
350
|
-
if response.status_code == 429:
|
|
351
|
-
raise UsageLimitExceededError(detail)
|
|
352
|
-
elif response.status_code in [403,432,433]:
|
|
353
|
-
raise ForbiddenError(detail)
|
|
354
|
-
elif response.status_code == 401:
|
|
355
|
-
raise InvalidAPIKeyError(detail)
|
|
356
|
-
elif response.status_code == 400:
|
|
357
|
-
raise BadRequestError(detail)
|
|
358
|
-
else:
|
|
359
|
-
raise response.raise_for_status()
|
|
371
|
+
raise response.raise_for_status()
|
|
360
372
|
|
|
361
373
|
async def crawl(self,
|
|
362
374
|
url: str,
|
|
@@ -380,7 +392,7 @@ class AsyncTavilyClient:
|
|
|
380
392
|
) -> dict:
|
|
381
393
|
"""
|
|
382
394
|
Combined crawl method.
|
|
383
|
-
|
|
395
|
+
|
|
384
396
|
"""
|
|
385
397
|
response_dict = await self._crawl(url,
|
|
386
398
|
max_depth=max_depth,
|
|
@@ -402,7 +414,7 @@ class AsyncTavilyClient:
|
|
|
402
414
|
**kwargs)
|
|
403
415
|
|
|
404
416
|
return response_dict
|
|
405
|
-
|
|
417
|
+
|
|
406
418
|
async def _map(self,
|
|
407
419
|
url: str,
|
|
408
420
|
max_depth: int = None,
|
|
@@ -443,31 +455,30 @@ class AsyncTavilyClient:
|
|
|
443
455
|
|
|
444
456
|
data = {k: v for k, v in data.items() if v is not None}
|
|
445
457
|
|
|
446
|
-
|
|
458
|
+
try:
|
|
459
|
+
response = await self._client.post("/map", content=json.dumps(data), timeout=timeout)
|
|
460
|
+
except httpx.TimeoutException:
|
|
461
|
+
raise TimeoutError(timeout)
|
|
462
|
+
|
|
463
|
+
if response.status_code == 200:
|
|
464
|
+
return response.json()
|
|
465
|
+
else:
|
|
466
|
+
detail = ""
|
|
447
467
|
try:
|
|
448
|
-
|
|
449
|
-
except
|
|
450
|
-
|
|
468
|
+
detail = response.json().get("detail", {}).get("error", None)
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
451
471
|
|
|
452
|
-
if response.status_code ==
|
|
453
|
-
|
|
472
|
+
if response.status_code == 429:
|
|
473
|
+
raise UsageLimitExceededError(detail)
|
|
474
|
+
elif response.status_code in [403,432,433]:
|
|
475
|
+
raise ForbiddenError(detail)
|
|
476
|
+
elif response.status_code == 401:
|
|
477
|
+
raise InvalidAPIKeyError(detail)
|
|
478
|
+
elif response.status_code == 400:
|
|
479
|
+
raise BadRequestError(detail)
|
|
454
480
|
else:
|
|
455
|
-
|
|
456
|
-
try:
|
|
457
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
458
|
-
except Exception:
|
|
459
|
-
pass
|
|
460
|
-
|
|
461
|
-
if response.status_code == 429:
|
|
462
|
-
raise UsageLimitExceededError(detail)
|
|
463
|
-
elif response.status_code in [403,432,433]:
|
|
464
|
-
raise ForbiddenError(detail)
|
|
465
|
-
elif response.status_code == 401:
|
|
466
|
-
raise InvalidAPIKeyError(detail)
|
|
467
|
-
elif response.status_code == 400:
|
|
468
|
-
raise BadRequestError(detail)
|
|
469
|
-
else:
|
|
470
|
-
raise response.raise_for_status()
|
|
481
|
+
raise response.raise_for_status()
|
|
471
482
|
|
|
472
483
|
async def map(self,
|
|
473
484
|
url: str,
|
|
@@ -639,68 +650,66 @@ class AsyncTavilyClient:
|
|
|
639
650
|
if stream:
|
|
640
651
|
async def stream_generator() -> AsyncGenerator[bytes, None]:
|
|
641
652
|
try:
|
|
642
|
-
async with self.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
yield chunk
|
|
653
|
+
async with self._client.stream(
|
|
654
|
+
"POST",
|
|
655
|
+
"/research",
|
|
656
|
+
content=json.dumps(data),
|
|
657
|
+
timeout=timeout
|
|
658
|
+
) as response:
|
|
659
|
+
if response.status_code != 200:
|
|
660
|
+
try:
|
|
661
|
+
error_text = await response.aread()
|
|
662
|
+
error_text = error_text.decode('utf-8') if isinstance(error_text, bytes) else error_text
|
|
663
|
+
except Exception:
|
|
664
|
+
error_text = "Unknown error"
|
|
665
|
+
|
|
666
|
+
if response.status_code == 429:
|
|
667
|
+
raise UsageLimitExceededError(error_text)
|
|
668
|
+
elif response.status_code in [403,432,433]:
|
|
669
|
+
raise ForbiddenError(error_text)
|
|
670
|
+
elif response.status_code == 401:
|
|
671
|
+
raise InvalidAPIKeyError(error_text)
|
|
672
|
+
elif response.status_code == 400:
|
|
673
|
+
raise BadRequestError(error_text)
|
|
674
|
+
else:
|
|
675
|
+
raise Exception(f"Error {response.status_code}: {error_text}")
|
|
676
|
+
|
|
677
|
+
async for chunk in response.aiter_bytes():
|
|
678
|
+
if chunk:
|
|
679
|
+
yield chunk
|
|
670
680
|
except httpx.TimeoutException:
|
|
671
681
|
raise TimeoutError(timeout)
|
|
672
682
|
except Exception as e:
|
|
673
683
|
raise Exception(f"Error during research stream: {str(e)}")
|
|
674
|
-
|
|
684
|
+
|
|
675
685
|
return stream_generator()
|
|
676
686
|
else:
|
|
677
687
|
async def _make_request():
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
raise TimeoutError(timeout)
|
|
688
|
+
try:
|
|
689
|
+
response = await self._client.post("/research", content=json.dumps(data), timeout=timeout)
|
|
690
|
+
except httpx.TimeoutException:
|
|
691
|
+
raise TimeoutError(timeout)
|
|
683
692
|
|
|
684
|
-
|
|
685
|
-
|
|
693
|
+
if response.status_code == 200:
|
|
694
|
+
return response.json()
|
|
695
|
+
else:
|
|
696
|
+
detail = ""
|
|
697
|
+
try:
|
|
698
|
+
detail = response.json().get("detail", {}).get("error", None)
|
|
699
|
+
except Exception:
|
|
700
|
+
pass
|
|
701
|
+
|
|
702
|
+
if response.status_code == 429:
|
|
703
|
+
raise UsageLimitExceededError(detail)
|
|
704
|
+
elif response.status_code in [403,432,433]:
|
|
705
|
+
raise ForbiddenError(detail)
|
|
706
|
+
elif response.status_code == 401:
|
|
707
|
+
raise InvalidAPIKeyError(detail)
|
|
708
|
+
elif response.status_code == 400:
|
|
709
|
+
raise BadRequestError(detail)
|
|
686
710
|
else:
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
690
|
-
except Exception:
|
|
691
|
-
pass
|
|
692
|
-
|
|
693
|
-
if response.status_code == 429:
|
|
694
|
-
raise UsageLimitExceededError(detail)
|
|
695
|
-
elif response.status_code in [403,432,433]:
|
|
696
|
-
raise ForbiddenError(detail)
|
|
697
|
-
elif response.status_code == 401:
|
|
698
|
-
raise InvalidAPIKeyError(detail)
|
|
699
|
-
elif response.status_code == 400:
|
|
700
|
-
raise BadRequestError(detail)
|
|
701
|
-
else:
|
|
702
|
-
raise response.raise_for_status()
|
|
703
|
-
|
|
711
|
+
raise response.raise_for_status()
|
|
712
|
+
|
|
704
713
|
return _make_request()
|
|
705
714
|
|
|
706
715
|
async def research(self,
|
|
@@ -714,16 +723,16 @@ class AsyncTavilyClient:
|
|
|
714
723
|
) -> Union[dict, AsyncGenerator[bytes, None]]:
|
|
715
724
|
"""
|
|
716
725
|
Research method to create a research task.
|
|
717
|
-
|
|
726
|
+
|
|
718
727
|
Args:
|
|
719
728
|
input: The research task description (required).
|
|
720
729
|
model: Research depth - must be either 'mini', 'pro', or 'auto'.
|
|
721
730
|
output_schema: Schema for the 'structured_output' response format (JSON Schema dict).
|
|
722
731
|
stream: Whether to stream the research task.
|
|
723
732
|
citation_format: Citation format - must be either 'numbered', 'mla', 'apa', or 'chicago'.
|
|
724
|
-
timeout: Optional HTTP request timeout in seconds.
|
|
733
|
+
timeout: Optional HTTP request timeout in seconds.
|
|
725
734
|
**kwargs: Additional custom arguments.
|
|
726
|
-
|
|
735
|
+
|
|
727
736
|
Returns:
|
|
728
737
|
When stream=False: dict - the response dictionary.
|
|
729
738
|
When stream=True: AsyncGenerator[bytes, None] - iterate over this to get streaming chunks.
|
|
@@ -740,43 +749,42 @@ class AsyncTavilyClient:
|
|
|
740
749
|
if stream:
|
|
741
750
|
return result # Don't await the result, it's an AsyncGenerator that will be lazy and only execute when iterated over with async for
|
|
742
751
|
else:
|
|
743
|
-
return await result
|
|
752
|
+
return await result
|
|
744
753
|
|
|
745
754
|
async def get_research(self,
|
|
746
755
|
request_id: str
|
|
747
756
|
) -> dict:
|
|
748
757
|
"""
|
|
749
758
|
Get research results by request_id.
|
|
750
|
-
|
|
759
|
+
|
|
751
760
|
Args:
|
|
752
761
|
request_id: The research request ID.
|
|
753
|
-
|
|
762
|
+
|
|
754
763
|
Returns:
|
|
755
764
|
dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
|
|
756
765
|
"""
|
|
757
|
-
|
|
766
|
+
try:
|
|
767
|
+
response = await self._client.get(f"/research/{request_id}")
|
|
768
|
+
except Exception as e:
|
|
769
|
+
raise Exception(f"Error getting research: {e}")
|
|
770
|
+
|
|
771
|
+
if response.status_code in (200, 202):
|
|
772
|
+
data = response.json()
|
|
773
|
+
return data
|
|
774
|
+
else:
|
|
775
|
+
detail = ""
|
|
758
776
|
try:
|
|
759
|
-
|
|
760
|
-
except Exception
|
|
761
|
-
|
|
777
|
+
detail = response.json().get("detail", {}).get("error", None)
|
|
778
|
+
except Exception:
|
|
779
|
+
pass
|
|
762
780
|
|
|
763
|
-
if response.status_code
|
|
764
|
-
|
|
765
|
-
|
|
781
|
+
if response.status_code == 429:
|
|
782
|
+
raise UsageLimitExceededError(detail)
|
|
783
|
+
elif response.status_code in [403,432,433]:
|
|
784
|
+
raise ForbiddenError(detail)
|
|
785
|
+
elif response.status_code == 401:
|
|
786
|
+
raise InvalidAPIKeyError(detail)
|
|
787
|
+
elif response.status_code == 400:
|
|
788
|
+
raise BadRequestError(detail)
|
|
766
789
|
else:
|
|
767
|
-
|
|
768
|
-
try:
|
|
769
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
770
|
-
except Exception:
|
|
771
|
-
pass
|
|
772
|
-
|
|
773
|
-
if response.status_code == 429:
|
|
774
|
-
raise UsageLimitExceededError(detail)
|
|
775
|
-
elif response.status_code in [403,432,433]:
|
|
776
|
-
raise ForbiddenError(detail)
|
|
777
|
-
elif response.status_code == 401:
|
|
778
|
-
raise InvalidAPIKeyError(detail)
|
|
779
|
-
elif response.status_code == 400:
|
|
780
|
-
raise BadRequestError(detail)
|
|
781
|
-
else:
|
|
782
|
-
raise response.raise_for_status()
|
|
790
|
+
raise response.raise_for_status()
|
|
@@ -71,6 +71,7 @@ class TavilyClient:
|
|
|
71
71
|
auto_parameters: bool = None,
|
|
72
72
|
include_favicon: bool = None,
|
|
73
73
|
include_usage: bool = None,
|
|
74
|
+
exact_match: bool = None,
|
|
74
75
|
**kwargs
|
|
75
76
|
) -> dict:
|
|
76
77
|
"""
|
|
@@ -95,6 +96,7 @@ class TavilyClient:
|
|
|
95
96
|
"auto_parameters": auto_parameters,
|
|
96
97
|
"include_favicon": include_favicon,
|
|
97
98
|
"include_usage": include_usage,
|
|
99
|
+
"exact_match": exact_match,
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
data = {k: v for k, v in data.items() if v is not None}
|
|
@@ -151,6 +153,7 @@ class TavilyClient:
|
|
|
151
153
|
auto_parameters: bool = None,
|
|
152
154
|
include_favicon: bool = None,
|
|
153
155
|
include_usage: bool = None,
|
|
156
|
+
exact_match: bool = None,
|
|
154
157
|
**kwargs, # Accept custom arguments
|
|
155
158
|
) -> dict:
|
|
156
159
|
"""
|
|
@@ -175,6 +178,7 @@ class TavilyClient:
|
|
|
175
178
|
auto_parameters=auto_parameters,
|
|
176
179
|
include_favicon=include_favicon,
|
|
177
180
|
include_usage=include_usage,
|
|
181
|
+
exact_match=exact_match,
|
|
178
182
|
**kwargs)
|
|
179
183
|
response_dict.setdefault("results", [])
|
|
180
184
|
return response_dict
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tavily-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.22
|
|
4
4
|
Summary: Python wrapper for the Tavily API
|
|
5
5
|
Home-page: https://github.com/tavily-ai/tavily-python
|
|
6
6
|
Author: Tavily AI
|
|
@@ -63,6 +63,21 @@ response = tavily_client.search("Who is Leo Messi?")
|
|
|
63
63
|
print(response)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
### Using exact match to find specific names or phrases
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from tavily import TavilyClient
|
|
70
|
+
|
|
71
|
+
client = TavilyClient(api_key="tvly-YOUR_API_KEY")
|
|
72
|
+
|
|
73
|
+
# Use exact_match=True to only return results containing the exact phrase(s) inside quotes
|
|
74
|
+
response = client.search(
|
|
75
|
+
query='"John Smith" CEO Acme Corp',
|
|
76
|
+
exact_match=True
|
|
77
|
+
)
|
|
78
|
+
print(response)
|
|
79
|
+
```
|
|
80
|
+
|
|
66
81
|
This is equivalent to directly querying our REST API.
|
|
67
82
|
|
|
68
83
|
### Generating context for a RAG Application
|
|
@@ -11,7 +11,10 @@ def set_api_key():
|
|
|
11
11
|
old_key = os.getenv("TAVILY_API_KEY")
|
|
12
12
|
os.environ["TAVILY_API_KEY"] = "test_api_key"
|
|
13
13
|
yield
|
|
14
|
-
|
|
14
|
+
if old_key is not None:
|
|
15
|
+
os.environ["TAVILY_API_KEY"] = old_key
|
|
16
|
+
elif "TAVILY_API_KEY" in os.environ:
|
|
17
|
+
del os.environ["TAVILY_API_KEY"]
|
|
15
18
|
|
|
16
19
|
@pytest.fixture
|
|
17
20
|
def clear_api_key():
|
|
@@ -32,7 +32,7 @@ def validate_specific(request, response):
|
|
|
32
32
|
assert request.headers["Authorization"] == "Bearer tvly-test"
|
|
33
33
|
assert request.headers["X-Client-Source"] == "tavily-python"
|
|
34
34
|
assert request.timeout == 10
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
request_json = request.json()
|
|
37
37
|
for key, value in {
|
|
38
38
|
"query": "What is Tavily?",
|
|
@@ -44,7 +44,8 @@ def validate_specific(request, response):
|
|
|
44
44
|
"exclude_domains": ["example.com"],
|
|
45
45
|
"include_answer": "advanced",
|
|
46
46
|
"include_raw_content": True,
|
|
47
|
-
"include_images": True
|
|
47
|
+
"include_images": True,
|
|
48
|
+
"exact_match": True
|
|
48
49
|
}.items():
|
|
49
50
|
assert request_json.get(key) == value
|
|
50
51
|
|
|
@@ -69,6 +70,7 @@ def test_sync_search_specific(sync_interceptor, sync_client):
|
|
|
69
70
|
include_answer="advanced",
|
|
70
71
|
include_raw_content=True,
|
|
71
72
|
include_images=True,
|
|
73
|
+
exact_match=True,
|
|
72
74
|
timeout=10
|
|
73
75
|
)
|
|
74
76
|
|
|
@@ -94,8 +96,61 @@ def test_async_search_specific(async_interceptor, async_client):
|
|
|
94
96
|
include_answer="advanced",
|
|
95
97
|
include_raw_content=True,
|
|
96
98
|
include_images=True,
|
|
99
|
+
exact_match=True,
|
|
97
100
|
timeout=10
|
|
98
101
|
))
|
|
99
102
|
|
|
100
103
|
request = async_interceptor.get_request()
|
|
101
104
|
validate_specific(request, response)
|
|
105
|
+
|
|
106
|
+
def test_sync_search_exact_match_not_sent_by_default(sync_interceptor, sync_client):
|
|
107
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
108
|
+
sync_client.search("What is Tavily?")
|
|
109
|
+
request = sync_interceptor.get_request()
|
|
110
|
+
assert "exact_match" not in request.json()
|
|
111
|
+
|
|
112
|
+
def test_sync_search_exact_match_true(sync_interceptor, sync_client):
|
|
113
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
114
|
+
sync_client.search("What is Tavily?", exact_match=True)
|
|
115
|
+
request = sync_interceptor.get_request()
|
|
116
|
+
assert request.json()["exact_match"] is True
|
|
117
|
+
|
|
118
|
+
def test_sync_search_exact_match_false(sync_interceptor, sync_client):
|
|
119
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
120
|
+
sync_client.search("What is Tavily?", exact_match=False)
|
|
121
|
+
request = sync_interceptor.get_request()
|
|
122
|
+
assert request.json()["exact_match"] is False
|
|
123
|
+
|
|
124
|
+
def test_async_search_exact_match_not_sent_by_default(async_interceptor, async_client):
|
|
125
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
126
|
+
asyncio.run(async_client.search("What is Tavily?"))
|
|
127
|
+
request = async_interceptor.get_request()
|
|
128
|
+
assert "exact_match" not in request.json()
|
|
129
|
+
|
|
130
|
+
def test_async_search_exact_match_true(async_interceptor, async_client):
|
|
131
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
132
|
+
asyncio.run(async_client.search("What is Tavily?", exact_match=True))
|
|
133
|
+
request = async_interceptor.get_request()
|
|
134
|
+
assert request.json()["exact_match"] is True
|
|
135
|
+
|
|
136
|
+
def test_async_search_exact_match_false(async_interceptor, async_client):
|
|
137
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
138
|
+
asyncio.run(async_client.search("What is Tavily?", exact_match=False))
|
|
139
|
+
request = async_interceptor.get_request()
|
|
140
|
+
assert request.json()["exact_match"] is False
|
|
141
|
+
|
|
142
|
+
def test_sync_search_exact_match_query_quotes_escaped_in_payload(sync_interceptor, sync_client):
|
|
143
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
144
|
+
sync_client.search('"John Smith" CEO Acme Corp', exact_match=True)
|
|
145
|
+
request = sync_interceptor.get_request()
|
|
146
|
+
# The raw JSON payload should have escaped quotes for the quoted phrase
|
|
147
|
+
assert r'\"John Smith\"' in request.body
|
|
148
|
+
# But the parsed query should preserve the original quotes
|
|
149
|
+
assert request.json()["query"] == '"John Smith" CEO Acme Corp'
|
|
150
|
+
|
|
151
|
+
def test_async_search_exact_match_query_quotes_escaped_in_payload(async_interceptor, async_client):
|
|
152
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
153
|
+
asyncio.run(async_client.search('"John Smith" CEO Acme Corp', exact_match=True))
|
|
154
|
+
request = async_interceptor.get_request()
|
|
155
|
+
assert r'\"John Smith\"' in request.body
|
|
156
|
+
assert request.json()["query"] == '"John Smith" CEO Acme Corp'
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for session pooling functionality in both sync and async clients.
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import tavily.tavily as sync_tavily
|
|
8
|
+
import tavily.async_tavily as async_tavily
|
|
9
|
+
from tests.request_intercept import intercept_requests, clear_interceptor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
dummy_response = {
|
|
13
|
+
"query": "test",
|
|
14
|
+
"results": [{"title": "Test", "url": "https://test.com", "content": "Test content", "score": 0.99}],
|
|
15
|
+
"response_time": 0.1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# =============================================================================
|
|
20
|
+
# SYNC CLIENT TESTS
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
class TestSyncSessionPooling:
|
|
24
|
+
"""Test sync client session pooling and lifecycle."""
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def interceptor(self):
|
|
28
|
+
yield intercept_requests(sync_tavily)
|
|
29
|
+
clear_interceptor(sync_tavily)
|
|
30
|
+
|
|
31
|
+
def test_context_manager(self, interceptor):
|
|
32
|
+
"""Test that sync client works with context manager."""
|
|
33
|
+
interceptor.set_response(200, json=dummy_response)
|
|
34
|
+
|
|
35
|
+
with sync_tavily.TavilyClient(api_key="tvly-test") as client:
|
|
36
|
+
response = client.search("test query")
|
|
37
|
+
assert response["results"][0]["title"] == "Test"
|
|
38
|
+
|
|
39
|
+
def test_close_method(self, interceptor):
|
|
40
|
+
"""Test that close() method works without error."""
|
|
41
|
+
interceptor.set_response(200, json=dummy_response)
|
|
42
|
+
|
|
43
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test")
|
|
44
|
+
response = client.search("test query")
|
|
45
|
+
assert response["results"][0]["title"] == "Test"
|
|
46
|
+
|
|
47
|
+
# close() should not raise
|
|
48
|
+
client.close()
|
|
49
|
+
|
|
50
|
+
def test_multiple_sequential_requests(self, interceptor):
|
|
51
|
+
"""Test that multiple requests work with same client (connection reuse)."""
|
|
52
|
+
interceptor.set_response(200, json=dummy_response)
|
|
53
|
+
|
|
54
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test")
|
|
55
|
+
|
|
56
|
+
# Make multiple requests with same client
|
|
57
|
+
for i in range(3):
|
|
58
|
+
response = client.search(f"test query {i}")
|
|
59
|
+
assert response["results"][0]["title"] == "Test"
|
|
60
|
+
|
|
61
|
+
client.close()
|
|
62
|
+
|
|
63
|
+
def test_context_manager_multiple_requests(self, interceptor):
|
|
64
|
+
"""Test multiple requests within context manager."""
|
|
65
|
+
interceptor.set_response(200, json=dummy_response)
|
|
66
|
+
|
|
67
|
+
with sync_tavily.TavilyClient(api_key="tvly-test") as client:
|
|
68
|
+
for i in range(3):
|
|
69
|
+
response = client.search(f"test query {i}")
|
|
70
|
+
assert response["results"][0]["title"] == "Test"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# ASYNC CLIENT TESTS
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
class TestAsyncSessionPooling:
|
|
78
|
+
"""Test async client session pooling and lifecycle."""
|
|
79
|
+
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def interceptor(self):
|
|
82
|
+
yield intercept_requests(async_tavily)
|
|
83
|
+
clear_interceptor(async_tavily)
|
|
84
|
+
|
|
85
|
+
def test_context_manager(self, interceptor):
|
|
86
|
+
"""Test that async client works with async context manager."""
|
|
87
|
+
interceptor.set_response(200, json=dummy_response)
|
|
88
|
+
|
|
89
|
+
async def run():
|
|
90
|
+
async with async_tavily.AsyncTavilyClient(api_key="tvly-test") as client:
|
|
91
|
+
response = await client.search("test query")
|
|
92
|
+
assert response["results"][0]["title"] == "Test"
|
|
93
|
+
|
|
94
|
+
asyncio.run(run())
|
|
95
|
+
|
|
96
|
+
def test_close_method(self, interceptor):
|
|
97
|
+
"""Test that close() method works without error."""
|
|
98
|
+
interceptor.set_response(200, json=dummy_response)
|
|
99
|
+
|
|
100
|
+
async def run():
|
|
101
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test")
|
|
102
|
+
response = await client.search("test query")
|
|
103
|
+
assert response["results"][0]["title"] == "Test"
|
|
104
|
+
|
|
105
|
+
# close() should not raise
|
|
106
|
+
await client.close()
|
|
107
|
+
|
|
108
|
+
asyncio.run(run())
|
|
109
|
+
|
|
110
|
+
def test_multiple_sequential_requests(self, interceptor):
|
|
111
|
+
"""Test that multiple requests work with same client (connection reuse)."""
|
|
112
|
+
interceptor.set_response(200, json=dummy_response)
|
|
113
|
+
|
|
114
|
+
async def run():
|
|
115
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test")
|
|
116
|
+
|
|
117
|
+
# Make multiple requests with same client
|
|
118
|
+
for i in range(3):
|
|
119
|
+
response = await client.search(f"test query {i}")
|
|
120
|
+
assert response["results"][0]["title"] == "Test"
|
|
121
|
+
|
|
122
|
+
await client.close()
|
|
123
|
+
|
|
124
|
+
asyncio.run(run())
|
|
125
|
+
|
|
126
|
+
def test_context_manager_multiple_requests(self, interceptor):
|
|
127
|
+
"""Test multiple requests within async context manager."""
|
|
128
|
+
interceptor.set_response(200, json=dummy_response)
|
|
129
|
+
|
|
130
|
+
async def run():
|
|
131
|
+
async with async_tavily.AsyncTavilyClient(api_key="tvly-test") as client:
|
|
132
|
+
for i in range(3):
|
|
133
|
+
response = await client.search(f"test query {i}")
|
|
134
|
+
assert response["results"][0]["title"] == "Test"
|
|
135
|
+
|
|
136
|
+
asyncio.run(run())
|
|
137
|
+
|
|
138
|
+
def test_concurrent_requests_same_client(self, interceptor):
|
|
139
|
+
"""Test concurrent requests using same client."""
|
|
140
|
+
interceptor.set_response(200, json=dummy_response)
|
|
141
|
+
|
|
142
|
+
async def run():
|
|
143
|
+
async with async_tavily.AsyncTavilyClient(api_key="tvly-test") as client:
|
|
144
|
+
# Run multiple searches concurrently
|
|
145
|
+
tasks = [client.search(f"query {i}") for i in range(3)]
|
|
146
|
+
results = await asyncio.gather(*tasks)
|
|
147
|
+
|
|
148
|
+
assert len(results) == 3
|
|
149
|
+
for result in results:
|
|
150
|
+
assert result["results"][0]["title"] == "Test"
|
|
151
|
+
|
|
152
|
+
asyncio.run(run())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|