diff --git a/README.md b/README.md index 42bda3a..fa646c3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,22 @@ Documentation is [available on Read the Docs](https://serpapi-python.readthedocs Change history is [available on GitHub](https://github.com/serpapi/serpapi-python/blob/master/HISTORY.md). +### Markdown output for AI agents + +For AI agents and LLM workflows, use `output="md"`. It returns search results with clean headings, links, and tables while using roughly half the tokens of JSON on average. + +```python +markdown = client.search({ + "engine": "google", + "q": "coffee", + "output": "md", +}) +``` + +Markdown works across SerpApi APIs and is returned as a plain Python string. See [Markdown Output for AI Agents](https://serpapi.com/markdown-output.md) for details. + +Raw HTML is also available with `output="html"` and is returned as a plain Python string. + ## Basic Examples in Python ### Search Bing diff --git a/serpapi/core.py b/serpapi/core.py index 0e0f96f..8622a82 100644 --- a/serpapi/core.py +++ b/serpapi/core.py @@ -35,7 +35,7 @@ def __repr__(self): return "" def search(self, params: dict = None, **kwargs): - """Fetch a page of results from SerpApi. Returns a :class:`SerpResults ` object, or unicode text (*e.g.* if ``'output': 'html'`` was passed). + """Fetch a page of results from SerpApi. Returns a :class:`SerpResults ` object for JSON responses, or unicode text for HTML and Markdown responses. The following three calls are equivalent: @@ -56,7 +56,7 @@ def search(self, params: dict = None, **kwargs): :param q: typically, this is the parameter for the search engine query. :param engine: the search engine to use. Defaults to ``google``. - :param output: the output format desired (``html`` or ``json``). Defaults to ``json``. + :param output: the output format desired (``html``, ``json``, or ``md``). Defaults to ``json``. :param api_key: the API Key to use for SerpApi.com. :param **: any additional parameters to pass to the API. @@ -84,7 +84,7 @@ def search_archive(self, params: dict = None, **kwargs): :param search_id: the Search ID of the search to retrieve from the archive. :param api_key: the API Key to use for SerpApi.com. - :param output: the output format desired (``html`` or ``json``). Defaults to ``json``. + :param output: the output format desired (``html``, ``json``, or ``md``). Defaults to ``json``. :param **: any additional parameters to pass to the API. **Learn more**: https://serpapi.com/search-archive-api diff --git a/serpapi/models.py b/serpapi/models.py index 0aa7720..000b2a3 100644 --- a/serpapi/models.py +++ b/serpapi/models.py @@ -92,6 +92,10 @@ def from_http_response(cls, r, *, client=None): Otherwise, the raw text (as a properly decoded unicode string) is returned. """ + content_type = r.headers.get("Content-Type", "").split(";", 1)[0].lower() + if content_type in {"text/html", "text/markdown"}: + return r.text + try: cls = cls(r.json(), client=client) diff --git a/tests/test_output_formats.py b/tests/test_output_formats.py new file mode 100644 index 0000000..ebafa9d --- /dev/null +++ b/tests/test_output_formats.py @@ -0,0 +1,52 @@ +from unittest.mock import Mock + +import pytest +import requests + +import serpapi + + +def response(content_type, content): + response = requests.Response() + response.status_code = 200 + response.headers["Content-Type"] = f"{content_type}; charset=utf-8" + response._content = content.encode("utf-8") + return response + + +def test_search_json_returns_serp_results(): + client = serpapi.Client(api_key="test-api-key") + client.session.request = Mock( + return_value=response( + "application/json", + '{"search_metadata": {"id": "search-123"}}', + ) + ) + + result = client.search(engine="google", q="Coffee", output="json") + + assert isinstance(result, serpapi.SerpResults) + assert result["search_metadata"]["id"] == "search-123" + assert result.client is client + _, request_kwargs = client.session.request.call_args + assert request_kwargs["params"]["output"] == "json" + + +@pytest.mark.parametrize( + ("output", "content_type"), + [ + ("html", "text/html"), + ("md", "text/markdown"), + ], +) +def test_search_text_outputs_return_raw_text(output, content_type): + content = '{"looks": "like JSON"}' + client = serpapi.Client(api_key="test-api-key") + client.session.request = Mock(return_value=response(content_type, content)) + + result = client.search(engine="google", q="Coffee", output=output) + + assert result == content + assert isinstance(result, str) + _, request_kwargs = client.session.request.call_args + assert request_kwargs["params"]["output"] == output