tavily-python 0.7.13__tar.gz → 0.7.14__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.
Files changed (23) hide show
  1. {tavily_python-0.7.13 → tavily_python-0.7.14}/PKG-INFO +57 -2
  2. {tavily_python-0.7.13 → tavily_python-0.7.14}/README.md +56 -1
  3. {tavily_python-0.7.13 → tavily_python-0.7.14}/setup.py +1 -1
  4. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/async_tavily.py +171 -1
  5. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/tavily.py +178 -1
  6. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily_python.egg-info/PKG-INFO +57 -2
  7. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily_python.egg-info/SOURCES.txt +1 -0
  8. tavily_python-0.7.14/tests/test_research.py +149 -0
  9. {tavily_python-0.7.13 → tavily_python-0.7.14}/LICENSE +0 -0
  10. {tavily_python-0.7.13 → tavily_python-0.7.14}/setup.cfg +0 -0
  11. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/__init__.py +0 -0
  12. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/config.py +0 -0
  13. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/errors.py +0 -0
  14. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/hybrid_rag/__init__.py +0 -0
  15. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/hybrid_rag/hybrid_rag.py +0 -0
  16. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily/utils.py +0 -0
  17. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily_python.egg-info/dependency_links.txt +0 -0
  18. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily_python.egg-info/requires.txt +0 -0
  19. {tavily_python-0.7.13 → tavily_python-0.7.14}/tavily_python.egg-info/top_level.txt +0 -0
  20. {tavily_python-0.7.13 → tavily_python-0.7.14}/tests/test_crawl.py +0 -0
  21. {tavily_python-0.7.13 → tavily_python-0.7.14}/tests/test_errors.py +0 -0
  22. {tavily_python-0.7.13 → tavily_python-0.7.14}/tests/test_map.py +0 -0
  23. {tavily_python-0.7.13 → tavily_python-0.7.14}/tests/test_search.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tavily-python
3
- Version: 0.7.13
3
+ Version: 0.7.14
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
@@ -32,7 +32,7 @@ Dynamic: summary
32
32
  [![License](https://img.shields.io/github/license/tavily-ai/tavily-python)](https://github.com/tavily-ai/tavily-python/blob/main/LICENSE)
33
33
  [![CI](https://github.com/tavily-ai/tavily-python/actions/workflows/tests.yml/badge.svg)](https://github.com/tavily-ai/tavily-python/actions)
34
34
 
35
- The Tavily Python wrapper allows for easy interaction with the Tavily API, offering the full range of our search and extract functionalities directly from your Python programs. Easily integrate smart search and content extraction capabilities into your applications, harnessing Tavily's powerful search and extract features.
35
+ The Tavily Python wrapper allows for easy interaction with the Tavily API, offering the full range of our search, extract, crawl, map, and research functionalities directly from your Python programs. Easily integrate smart search, content extraction, and research capabilities into your applications, harnessing Tavily's powerful features.
36
36
 
37
37
  ## Installing
38
38
 
@@ -41,6 +41,7 @@ pip install tavily-python
41
41
  ```
42
42
 
43
43
  # Tavily Search
44
+
44
45
  Search lets you search the web for a given query.
45
46
 
46
47
  ## Usage
@@ -99,6 +100,7 @@ print(answer)
99
100
  This is how you get accurate and concise answers to questions, in one line of code. Perfect for usage by LLMs!
100
101
 
101
102
  # Tavily Extract
103
+
102
104
  Extract web page content from one or more specified URLs.
103
105
 
104
106
  ## Usage
@@ -203,6 +205,59 @@ for result in response["results"]:
203
205
 
204
206
  ```
205
207
 
208
+ # Tavily Research
209
+
210
+ Research lets you create comprehensive research reports on any topic, with automatic source gathering, analysis, and structured output.
211
+
212
+ ## Usage
213
+
214
+ Below are some code snippets that demonstrate how to interact with our Research API. Each step and component of this code is explained in greater detail in the API Methods section below.
215
+
216
+ ### Creating a research task and retrieving results
217
+
218
+ ```python
219
+ from tavily import TavilyClient
220
+
221
+ # Step 1. Instantiating your TavilyClient
222
+ tavily_client = TavilyClient(api_key="tvly-YOUR_API_KEY")
223
+
224
+ # Step 2. Creating a research task
225
+ response = tavily_client.research(
226
+ input="Research the latest developments in AI",
227
+ model="pro",
228
+ citation_format="apa"
229
+ )
230
+
231
+ # Step 3. Retrieving the research results
232
+ request_id = response["request_id"]
233
+ result = tavily_client.get_research(request_id)
234
+
235
+ # Step 4. Printing the research report
236
+ print(f"Status: {result['status']}")
237
+ print(f"Content: {result['content']}")
238
+ print(f"Sources: {len(result['sources'])} sources found")
239
+ ```
240
+
241
+ ### Streaming research results
242
+
243
+ ```python
244
+ from tavily import TavilyClient
245
+
246
+ # Step 1. Instantiating your TavilyClient
247
+ tavily_client = TavilyClient(api_key="tvly-YOUR_API_KEY")
248
+
249
+ # Step 2. Creating a streaming research task
250
+ stream = tavily_client.research(
251
+ input="Research the latest developments in AI",
252
+ model="pro",
253
+ stream=True
254
+ )
255
+
256
+ # Step 3. Processing the stream as it arrives
257
+ for chunk in stream:
258
+ print(chunk.decode('utf-8'))
259
+ ```
260
+
206
261
  ## Documentation
207
262
 
208
263
  For a complete guide on how to use the different endpoints and their parameters, please head to our [Python API Reference](https://docs.tavily.com/sdk/python/reference).
@@ -5,7 +5,7 @@
5
5
  [![License](https://img.shields.io/github/license/tavily-ai/tavily-python)](https://github.com/tavily-ai/tavily-python/blob/main/LICENSE)
6
6
  [![CI](https://github.com/tavily-ai/tavily-python/actions/workflows/tests.yml/badge.svg)](https://github.com/tavily-ai/tavily-python/actions)
7
7
 
8
- The Tavily Python wrapper allows for easy interaction with the Tavily API, offering the full range of our search and extract functionalities directly from your Python programs. Easily integrate smart search and content extraction capabilities into your applications, harnessing Tavily's powerful search and extract features.
8
+ The Tavily Python wrapper allows for easy interaction with the Tavily API, offering the full range of our search, extract, crawl, map, and research functionalities directly from your Python programs. Easily integrate smart search, content extraction, and research capabilities into your applications, harnessing Tavily's powerful features.
9
9
 
10
10
  ## Installing
11
11
 
@@ -14,6 +14,7 @@ pip install tavily-python
14
14
  ```
15
15
 
16
16
  # Tavily Search
17
+
17
18
  Search lets you search the web for a given query.
18
19
 
19
20
  ## Usage
@@ -72,6 +73,7 @@ print(answer)
72
73
  This is how you get accurate and concise answers to questions, in one line of code. Perfect for usage by LLMs!
73
74
 
74
75
  # Tavily Extract
76
+
75
77
  Extract web page content from one or more specified URLs.
76
78
 
77
79
  ## Usage
@@ -176,6 +178,59 @@ for result in response["results"]:
176
178
 
177
179
  ```
178
180
 
181
+ # Tavily Research
182
+
183
+ Research lets you create comprehensive research reports on any topic, with automatic source gathering, analysis, and structured output.
184
+
185
+ ## Usage
186
+
187
+ Below are some code snippets that demonstrate how to interact with our Research API. Each step and component of this code is explained in greater detail in the API Methods section below.
188
+
189
+ ### Creating a research task and retrieving results
190
+
191
+ ```python
192
+ from tavily import TavilyClient
193
+
194
+ # Step 1. Instantiating your TavilyClient
195
+ tavily_client = TavilyClient(api_key="tvly-YOUR_API_KEY")
196
+
197
+ # Step 2. Creating a research task
198
+ response = tavily_client.research(
199
+ input="Research the latest developments in AI",
200
+ model="pro",
201
+ citation_format="apa"
202
+ )
203
+
204
+ # Step 3. Retrieving the research results
205
+ request_id = response["request_id"]
206
+ result = tavily_client.get_research(request_id)
207
+
208
+ # Step 4. Printing the research report
209
+ print(f"Status: {result['status']}")
210
+ print(f"Content: {result['content']}")
211
+ print(f"Sources: {len(result['sources'])} sources found")
212
+ ```
213
+
214
+ ### Streaming research results
215
+
216
+ ```python
217
+ from tavily import TavilyClient
218
+
219
+ # Step 1. Instantiating your TavilyClient
220
+ tavily_client = TavilyClient(api_key="tvly-YOUR_API_KEY")
221
+
222
+ # Step 2. Creating a streaming research task
223
+ stream = tavily_client.research(
224
+ input="Research the latest developments in AI",
225
+ model="pro",
226
+ stream=True
227
+ )
228
+
229
+ # Step 3. Processing the stream as it arrives
230
+ for chunk in stream:
231
+ print(chunk.decode('utf-8'))
232
+ ```
233
+
179
234
  ## Documentation
180
235
 
181
236
  For a complete guide on how to use the different endpoints and their parameters, please head to our [Python API Reference](https://docs.tavily.com/sdk/python/reference).
@@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
5
5
 
6
6
  setup(
7
7
  name='tavily-python',
8
- version='0.7.13',
8
+ version='0.7.14',
9
9
  url='https://github.com/tavily-ai/tavily-python',
10
10
  author='Tavily AI',
11
11
  author_email='support@tavily.com',
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import json
3
3
  import os
4
- from typing import Literal, Sequence, Optional, List, Union
4
+ from typing import Literal, Sequence, Optional, List, Union, AsyncGenerator, Awaitable
5
5
 
6
6
  import httpx
7
7
 
@@ -576,3 +576,173 @@ class AsyncTavilyClient:
576
576
  sorted_results = sorted(all_results, key=lambda x: x["score"], reverse=True)[:max_results]
577
577
 
578
578
  return sorted_results
579
+
580
+ def _research(self,
581
+ input: str,
582
+ model: Literal["mini", "pro", "auto"] = None,
583
+ output_schema: dict = None,
584
+ stream: bool = False,
585
+ citation_format: Literal["numbered", "mla", "apa", "chicago"] = "numbered",
586
+ timeout: Optional[float] = None,
587
+ **kwargs
588
+ ) -> Union[AsyncGenerator[bytes, None], Awaitable[dict]]:
589
+ """
590
+ Internal research method to send the request to the API.
591
+ """
592
+ data = {
593
+ "input": input,
594
+ "model": model,
595
+ "output_schema": output_schema,
596
+ "stream": stream,
597
+ "citation_format": citation_format,
598
+ }
599
+
600
+ data = {k: v for k, v in data.items() if v is not None}
601
+
602
+ if kwargs:
603
+ data.update(kwargs)
604
+
605
+ if stream:
606
+ async def stream_generator() -> AsyncGenerator[bytes, None]:
607
+ try:
608
+ async with self._client_creator() as client:
609
+ async with client.stream(
610
+ "POST",
611
+ "/research",
612
+ content=json.dumps(data),
613
+ timeout=timeout
614
+ ) as response:
615
+ if response.status_code != 200:
616
+ try:
617
+ error_text = await response.aread()
618
+ error_text = error_text.decode('utf-8') if isinstance(error_text, bytes) else error_text
619
+ except Exception:
620
+ error_text = "Unknown error"
621
+
622
+ if response.status_code == 429:
623
+ raise UsageLimitExceededError(error_text)
624
+ elif response.status_code in [403,432,433]:
625
+ raise ForbiddenError(error_text)
626
+ elif response.status_code == 401:
627
+ raise InvalidAPIKeyError(error_text)
628
+ elif response.status_code == 400:
629
+ raise BadRequestError(error_text)
630
+ else:
631
+ raise Exception(f"Error {response.status_code}: {error_text}")
632
+
633
+ async for chunk in response.aiter_bytes():
634
+ if chunk:
635
+ yield chunk
636
+ except httpx.TimeoutException:
637
+ raise TimeoutError(timeout)
638
+ except Exception as e:
639
+ raise Exception(f"Error during research stream: {str(e)}")
640
+
641
+ return stream_generator()
642
+ else:
643
+ async def _make_request():
644
+ async with self._client_creator() as client:
645
+ try:
646
+ response = await client.post("/research", content=json.dumps(data), timeout=timeout)
647
+ except httpx.TimeoutException:
648
+ raise TimeoutError(timeout)
649
+
650
+ if response.status_code == 200:
651
+ return response.json()
652
+ else:
653
+ detail = ""
654
+ try:
655
+ detail = response.json().get("detail", {}).get("error", None)
656
+ except Exception:
657
+ pass
658
+
659
+ if response.status_code == 429:
660
+ raise UsageLimitExceededError(detail)
661
+ elif response.status_code in [403,432,433]:
662
+ raise ForbiddenError(detail)
663
+ elif response.status_code == 401:
664
+ raise InvalidAPIKeyError(detail)
665
+ elif response.status_code == 400:
666
+ raise BadRequestError(detail)
667
+ else:
668
+ raise response.raise_for_status()
669
+
670
+ return _make_request()
671
+
672
+ async def research(self,
673
+ input: str,
674
+ model: Literal["mini", "pro", "auto"] = None,
675
+ output_schema: dict = None,
676
+ stream: bool = False,
677
+ citation_format: Literal["numbered", "mla", "apa", "chicago"] = "numbered",
678
+ timeout: Optional[float] = None,
679
+ **kwargs
680
+ ) -> Union[dict, AsyncGenerator[bytes, None]]:
681
+ """
682
+ Research method to create a research task.
683
+
684
+ Args:
685
+ input: The research task description (required).
686
+ model: Research depth - must be either 'mini', 'pro', or 'auto'.
687
+ output_schema: Schema for the 'structured_output' response format (JSON Schema dict).
688
+ stream: Whether to stream the research task.
689
+ citation_format: Citation format - must be either 'numbered', 'mla', 'apa', or 'chicago'.
690
+ timeout: Optional HTTP request timeout in seconds.
691
+ **kwargs: Additional custom arguments.
692
+
693
+ Returns:
694
+ When stream=False: dict - the response dictionary.
695
+ When stream=True: AsyncGenerator[bytes, None] - iterate over this to get streaming chunks.
696
+ """
697
+ result = self._research(
698
+ input=input,
699
+ model=model,
700
+ output_schema=output_schema,
701
+ stream=stream,
702
+ citation_format=citation_format,
703
+ timeout=timeout,
704
+ **kwargs
705
+ )
706
+ if stream:
707
+ return result # Don't await the result, it's an AsyncGenerator that will be lazy and only execute when iterated over with async for
708
+ else:
709
+ return await result
710
+
711
+ async def get_research(self,
712
+ request_id: str
713
+ ) -> dict:
714
+ """
715
+ Get research results by request_id.
716
+
717
+ Args:
718
+ request_id: The research request ID.
719
+
720
+ Returns:
721
+ dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
722
+ """
723
+ async with self._client_creator() as client:
724
+ try:
725
+ response = await client.get(f"/research/{request_id}")
726
+ except Exception as e:
727
+ raise Exception(f"Error getting research: {e}")
728
+
729
+ if response.status_code in (200, 202):
730
+ data = response.json()
731
+ return data
732
+ else:
733
+ detail = ""
734
+ try:
735
+ detail = response.json().get("detail", {}).get("error", None)
736
+ except Exception:
737
+ pass
738
+
739
+ if response.status_code == 429:
740
+ raise UsageLimitExceededError(detail)
741
+ elif response.status_code in [403,432,433]:
742
+ raise ForbiddenError(detail)
743
+ elif response.status_code == 401:
744
+ raise InvalidAPIKeyError(detail)
745
+ elif response.status_code == 400:
746
+ raise BadRequestError(detail)
747
+ else:
748
+ raise response.raise_for_status()
@@ -2,7 +2,7 @@ import requests
2
2
  import json
3
3
  import warnings
4
4
  import os
5
- from typing import Literal, Sequence, Optional, List, Union
5
+ from typing import Literal, Sequence, Optional, List, Union, Generator
6
6
  from concurrent.futures import ThreadPoolExecutor, as_completed
7
7
  from .utils import get_max_items_from_list
8
8
  from .errors import UsageLimitExceededError, InvalidAPIKeyError, MissingAPIKeyError, BadRequestError, ForbiddenError, TimeoutError
@@ -565,6 +565,183 @@ class TavilyClient:
565
565
 
566
566
  return sorted_results
567
567
 
568
+ def _research(self,
569
+ input: str,
570
+ model: Literal["mini", "pro", "auto"] = None,
571
+ output_schema: dict = None,
572
+ stream: bool = False,
573
+ citation_format: Literal["numbered", "mla", "apa", "chicago"] = "numbered",
574
+ timeout: Optional[float] = None,
575
+ **kwargs
576
+ ) -> Union[dict, Generator[bytes, None, None]]:
577
+ """
578
+ Internal research method to send the request to the API.
579
+ """
580
+ data = {
581
+ "input": input,
582
+ "model": model,
583
+ "output_schema": output_schema,
584
+ "stream": stream,
585
+ "citation_format": citation_format,
586
+ }
587
+
588
+ data = {k: v for k, v in data.items() if v is not None}
589
+
590
+ if kwargs:
591
+ data.update(kwargs)
592
+
593
+ if stream:
594
+ try:
595
+ response = requests.post(
596
+ self.base_url + "/research",
597
+ data=json.dumps(data),
598
+ headers=self.headers,
599
+ timeout=timeout,
600
+ proxies=self.proxies,
601
+ stream=True
602
+ )
603
+ except requests.exceptions.Timeout:
604
+ raise TimeoutError(timeout)
605
+
606
+ if response.status_code != 200:
607
+ detail = ""
608
+ try:
609
+ detail = response.json().get("detail", {}).get("error", None)
610
+ except Exception:
611
+ pass
612
+
613
+ if response.status_code == 429:
614
+ raise UsageLimitExceededError(detail)
615
+ elif response.status_code in [403,432,433]:
616
+ raise ForbiddenError(detail)
617
+ elif response.status_code == 401:
618
+ raise InvalidAPIKeyError(detail)
619
+ elif response.status_code == 400:
620
+ raise BadRequestError(detail)
621
+ else:
622
+ raise response.raise_for_status()
623
+
624
+ def stream_generator() -> Generator[bytes, None, None]:
625
+ try:
626
+ for chunk in response.iter_content(chunk_size=None):
627
+ if chunk:
628
+ yield chunk
629
+ finally:
630
+ response.close()
631
+
632
+ return stream_generator()
633
+ else:
634
+ try:
635
+ response = requests.post(
636
+ self.base_url + "/research",
637
+ data=json.dumps(data),
638
+ headers=self.headers,
639
+ timeout=timeout,
640
+ proxies=self.proxies
641
+ )
642
+ except requests.exceptions.Timeout:
643
+ raise TimeoutError(timeout)
644
+
645
+ if response.status_code == 200:
646
+ return response.json()
647
+ else:
648
+ detail = ""
649
+ try:
650
+ detail = response.json().get("detail", {}).get("error", None)
651
+ except Exception:
652
+ pass
653
+
654
+ if response.status_code == 429:
655
+ raise UsageLimitExceededError(detail)
656
+ elif response.status_code in [403,432,433]:
657
+ raise ForbiddenError(detail)
658
+ elif response.status_code == 401:
659
+ raise InvalidAPIKeyError(detail)
660
+ elif response.status_code == 400:
661
+ raise BadRequestError(detail)
662
+ else:
663
+ raise response.raise_for_status()
664
+
665
+ def research(self,
666
+ input: str,
667
+ model: Literal["mini", "pro", "auto"] = None,
668
+ output_schema: dict = None,
669
+ stream: bool = False,
670
+ citation_format: Literal["numbered", "mla", "apa", "chicago"] = "numbered",
671
+ timeout: Optional[float] = None,
672
+ **kwargs
673
+ ) -> Union[dict, Generator[bytes, None, None]]:
674
+ """
675
+ Research method to create a research task.
676
+
677
+ Args:
678
+ input: The research task or question to investigate (required).
679
+ model: The model used by the research agent - must be either 'mini', 'pro', or 'auto'.
680
+ output_schema: Schema for the 'structured_output' response format (JSON Schema dict).
681
+ stream: Whether to stream the research task.
682
+ citation_format: Citation format - must be either 'numbered', 'mla', 'apa', or 'chicago'.
683
+ timeout: Optional HTTP request timeout in seconds.
684
+ **kwargs: Additional custom arguments.
685
+
686
+ Returns:
687
+ dict: Response containing request_id, created_at, status, input, and model.
688
+ """
689
+
690
+
691
+ response_dict = self._research(
692
+ input=input,
693
+ model=model,
694
+ output_schema=output_schema,
695
+ stream=stream,
696
+ citation_format=citation_format,
697
+ timeout=timeout,
698
+ **kwargs
699
+ )
700
+
701
+ return response_dict
702
+
703
+ def get_research(self,
704
+ request_id: str
705
+ ) -> dict:
706
+ """
707
+ Get research results by request_id.
708
+
709
+ Args:
710
+ request_id: The research request ID.
711
+
712
+ Returns:
713
+ dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
714
+ """
715
+ try:
716
+ response = requests.get(
717
+ self.base_url + f"/research/{request_id}",
718
+ headers=self.headers,
719
+ proxies=self.proxies,
720
+ )
721
+ except Exception as e:
722
+ raise Exception(f"Error getting research: {e}")
723
+
724
+ if response.status_code in (200, 202):
725
+ data = response.json()
726
+ return data
727
+ else:
728
+ detail = ""
729
+ try:
730
+ detail = response.json().get("detail", {}).get("error", None)
731
+ except Exception:
732
+ pass
733
+
734
+ if response.status_code == 429:
735
+ raise UsageLimitExceededError(detail)
736
+ elif response.status_code in [403,432,433]:
737
+ raise ForbiddenError(detail)
738
+ elif response.status_code == 401:
739
+ raise InvalidAPIKeyError(detail)
740
+ elif response.status_code == 400:
741
+ raise BadRequestError(detail)
742
+ else:
743
+ raise response.raise_for_status()
744
+
568
745
 
569
746
  class Client(TavilyClient):
570
747
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tavily-python
3
- Version: 0.7.13
3
+ Version: 0.7.14
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
@@ -32,7 +32,7 @@ Dynamic: summary
32
32
  [![License](https://img.shields.io/github/license/tavily-ai/tavily-python)](https://github.com/tavily-ai/tavily-python/blob/main/LICENSE)
33
33
  [![CI](https://github.com/tavily-ai/tavily-python/actions/workflows/tests.yml/badge.svg)](https://github.com/tavily-ai/tavily-python/actions)
34
34
 
35
- The Tavily Python wrapper allows for easy interaction with the Tavily API, offering the full range of our search and extract functionalities directly from your Python programs. Easily integrate smart search and content extraction capabilities into your applications, harnessing Tavily's powerful search and extract features.
35
+ The Tavily Python wrapper allows for easy interaction with the Tavily API, offering the full range of our search, extract, crawl, map, and research functionalities directly from your Python programs. Easily integrate smart search, content extraction, and research capabilities into your applications, harnessing Tavily's powerful features.
36
36
 
37
37
  ## Installing
38
38
 
@@ -41,6 +41,7 @@ pip install tavily-python
41
41
  ```
42
42
 
43
43
  # Tavily Search
44
+
44
45
  Search lets you search the web for a given query.
45
46
 
46
47
  ## Usage
@@ -99,6 +100,7 @@ print(answer)
99
100
  This is how you get accurate and concise answers to questions, in one line of code. Perfect for usage by LLMs!
100
101
 
101
102
  # Tavily Extract
103
+
102
104
  Extract web page content from one or more specified URLs.
103
105
 
104
106
  ## Usage
@@ -203,6 +205,59 @@ for result in response["results"]:
203
205
 
204
206
  ```
205
207
 
208
+ # Tavily Research
209
+
210
+ Research lets you create comprehensive research reports on any topic, with automatic source gathering, analysis, and structured output.
211
+
212
+ ## Usage
213
+
214
+ Below are some code snippets that demonstrate how to interact with our Research API. Each step and component of this code is explained in greater detail in the API Methods section below.
215
+
216
+ ### Creating a research task and retrieving results
217
+
218
+ ```python
219
+ from tavily import TavilyClient
220
+
221
+ # Step 1. Instantiating your TavilyClient
222
+ tavily_client = TavilyClient(api_key="tvly-YOUR_API_KEY")
223
+
224
+ # Step 2. Creating a research task
225
+ response = tavily_client.research(
226
+ input="Research the latest developments in AI",
227
+ model="pro",
228
+ citation_format="apa"
229
+ )
230
+
231
+ # Step 3. Retrieving the research results
232
+ request_id = response["request_id"]
233
+ result = tavily_client.get_research(request_id)
234
+
235
+ # Step 4. Printing the research report
236
+ print(f"Status: {result['status']}")
237
+ print(f"Content: {result['content']}")
238
+ print(f"Sources: {len(result['sources'])} sources found")
239
+ ```
240
+
241
+ ### Streaming research results
242
+
243
+ ```python
244
+ from tavily import TavilyClient
245
+
246
+ # Step 1. Instantiating your TavilyClient
247
+ tavily_client = TavilyClient(api_key="tvly-YOUR_API_KEY")
248
+
249
+ # Step 2. Creating a streaming research task
250
+ stream = tavily_client.research(
251
+ input="Research the latest developments in AI",
252
+ model="pro",
253
+ stream=True
254
+ )
255
+
256
+ # Step 3. Processing the stream as it arrives
257
+ for chunk in stream:
258
+ print(chunk.decode('utf-8'))
259
+ ```
260
+
206
261
  ## Documentation
207
262
 
208
263
  For a complete guide on how to use the different endpoints and their parameters, please head to our [Python API Reference](https://docs.tavily.com/sdk/python/reference).
@@ -17,4 +17,5 @@ tavily_python.egg-info/top_level.txt
17
17
  tests/test_crawl.py
18
18
  tests/test_errors.py
19
19
  tests/test_map.py
20
+ tests/test_research.py
20
21
  tests/test_search.py
@@ -0,0 +1,149 @@
1
+ import asyncio
2
+
3
+ BASE_URL = "https://api.tavily.com"
4
+
5
+ dummy_queued_response = {
6
+ "request_id": "test-request-123",
7
+ "created_at": "2024-01-01T00:00:00Z",
8
+ "status": "pending",
9
+ "input": "Research the latest developments in AI",
10
+ "model": "mini"
11
+ }
12
+
13
+ dummy_research_response = {
14
+ "request_id": "test-request-123",
15
+ "created_at": "2024-01-01T00:00:00Z",
16
+ "completed_at": "2024-01-01T00:05:00Z",
17
+ "status": "completed",
18
+ "content": "This is a comprehensive research report on AI developments...",
19
+ "sources": [
20
+ {
21
+ "title": "AI Research Paper",
22
+ "url": "https://example.com/ai-paper",
23
+ }
24
+ ]
25
+ }
26
+
27
+ def validate_research_default(request, response):
28
+ assert request.method == "POST"
29
+ assert request.url == f"{BASE_URL}/research"
30
+ assert request.headers["Authorization"] == "Bearer tvly-test"
31
+ assert request.headers["X-Client-Source"] == "tavily-python"
32
+ assert request.json().get('input') == "Research the latest developments in AI"
33
+ assert response == dummy_queued_response
34
+
35
+ def validate_research_specific(request, response):
36
+ assert request.method == "POST"
37
+ assert request.url == f"{BASE_URL}/research"
38
+ assert request.headers["Authorization"] == "Bearer tvly-test"
39
+ assert request.headers["X-Client-Source"] == "tavily-python"
40
+
41
+ request_json = request.json()
42
+ for key, value in {
43
+ "input": "Research the latest developments in AI",
44
+ "model": "mini",
45
+ "citation_format": "apa",
46
+ "stream": True,
47
+ "output_schema": {
48
+ "title": "ResearchReport",
49
+ "description": "A structured research report",
50
+ "type": "object",
51
+ "properties": {
52
+ "summary": {"type": "string"},
53
+ "key_points": {"type": "array", "items": {"type": "string"}}
54
+ }
55
+ }
56
+ }.items():
57
+ assert request_json.get(key) == value
58
+
59
+ assert response == dummy_queued_response
60
+
61
+ def validate_get_research(request, response):
62
+ assert request.method == "GET"
63
+ assert request.url == f"{BASE_URL}/research/test-request-123"
64
+ assert request.headers["Authorization"] == "Bearer tvly-test"
65
+ assert request.headers["X-Client-Source"] == "tavily-python"
66
+ assert response == dummy_research_response
67
+
68
+ def test_sync_research_defaults(sync_interceptor, sync_client):
69
+ sync_interceptor.set_response(200, json=dummy_queued_response)
70
+ response = sync_client.research("Research the latest developments in AI")
71
+ request = sync_interceptor.get_request()
72
+ validate_research_default(request, response)
73
+
74
+ def test_sync_research_specific(sync_interceptor, sync_client):
75
+ sync_interceptor.set_response(200, json=dummy_queued_response)
76
+ response = sync_client.research(
77
+ input="Research the latest developments in AI",
78
+ model="mini",
79
+ citation_format="apa",
80
+ stream=True,
81
+ timeout=300,
82
+ output_schema={
83
+ "title": "ResearchReport",
84
+ "description": "A structured research report",
85
+ "type": "object",
86
+ "properties": {
87
+ "summary": {"type": "string"},
88
+ "key_points": {"type": "array", "items": {"type": "string"}}
89
+ }
90
+ }
91
+ )
92
+
93
+ request = sync_interceptor.get_request()
94
+ # When stream=True, response is a generator
95
+ assert hasattr(response, '__iter__') and not isinstance(response, (str, dict))
96
+ validate_research_specific(request, dummy_queued_response)
97
+
98
+ def test_sync_get_research(sync_interceptor, sync_client):
99
+ sync_interceptor.set_response(200, json=dummy_research_response)
100
+ response = sync_client.get_research("test-request-123")
101
+ request = sync_interceptor.get_request()
102
+ validate_get_research(request, response)
103
+
104
+ def test_async_research_defaults(async_interceptor, async_client):
105
+ async_interceptor.set_response(200, json=dummy_queued_response)
106
+ response = asyncio.run(async_client.research("Research the latest developments in AI"))
107
+ request = async_interceptor.get_request()
108
+ validate_research_default(request, response)
109
+
110
+ def test_async_research_specific(async_interceptor, async_client):
111
+ async_interceptor.set_response(200, json=dummy_queued_response)
112
+
113
+ async def run_test():
114
+ response = await async_client.research(
115
+ input="Research the latest developments in AI",
116
+ model="mini",
117
+ citation_format="apa",
118
+ stream=True,
119
+ timeout=300,
120
+ output_schema={
121
+ "title": "ResearchReport",
122
+ "description": "A structured research report",
123
+ "type": "object",
124
+ "properties": {
125
+ "summary": {"type": "string"},
126
+ "key_points": {"type": "array", "items": {"type": "string"}}
127
+ }
128
+ }
129
+ )
130
+ # When stream=True, response is an async generator
131
+ assert hasattr(response, '__aiter__')
132
+ try:
133
+ async for chunk in response:
134
+ # Just consume one chunk to trigger the request
135
+ break
136
+ except StopAsyncIteration:
137
+ pass
138
+ return response
139
+
140
+ response = asyncio.run(run_test())
141
+ request = async_interceptor.get_request()
142
+ validate_research_specific(request, dummy_queued_response)
143
+
144
+ def test_async_get_research(async_interceptor, async_client):
145
+ async_interceptor.set_response(200, json=dummy_research_response)
146
+ response = asyncio.run(async_client.get_research("test-request-123"))
147
+ request = async_interceptor.get_request()
148
+ validate_get_research(request, response)
149
+
File without changes
File without changes