Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 55 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ A [Python SDK](https://docs.linkup.so/pages/sdk/python/python) for the
## 🌟 Features

- ✅ **Simple and intuitive API client.**
- 🔍 **Supports both standard and deep search queries.**
- 🔍 **Support all Linkup entrypoints and parameters.**
- ⚡ **Supports synchronous and asynchronous requests.**
- 🔒 **Handles authentication and request management.**

## 📦 Installation

Simply install the Linkup Python SDK using `pip`:
Simply install the Linkup Python SDK as any Python package, for instance using `pip`:

```bash
pip install linkup-sdk
Expand Down Expand Up @@ -66,30 +66,74 @@ pip install linkup-sdk

### 📋 Examples

All search queries can be used with two very different modes:
#### 📝 Search

The `search` function can be used to performs web searches. It supports two very different
complexity modes:

- with `depth="standard"`, the search will be straightforward and fast, suited for relatively simple
queries (e.g. "What's the weather in Paris today?")
- with `depth="deep"`, the search will use an agentic workflow, which makes it in general slower,
but it will be able to solve more complex queries (e.g. "What is the company profile of LangChain
accross the last few years, and how does it compare to its concurrents?")

#### 📝 Standard Search Query
The `search` function also supports three output types:

```python
from linkup import LinkupClient
- with `output_type="searchResults"`, the search will return a list of relevant documents
- with `output_type="sourcedAnswer"`, the search will return a concise answer with sources
- with `output_type="structured"`, the search will return a structured output according to a
user-defined schema

# Initialize the client (API key can be read from the environment variable or passed as an argument)
client = LinkupClient()
```python
from linkup import LinkupClient, LinkupSourcedAnswer
from typing import Any

# Perform a search query
search_response = client.search(
client = LinkupClient() # API key can be read from the environment variable or passed as an argument
search_response: Any = client.search(
query="What are the 3 major events in the life of Abraham Lincoln?",
depth="deep", # "standard" or "deep"
output_type="sourcedAnswer", # "searchResults" or "sourcedAnswer" or "structured"
structured_output_schema=None, # must be filled if output_type is "structured"
)
print(search_response)
assert isinstance(search_response, LinkupSourcedAnswer)
print(search_response.model_dump())
# Response:
# {
# answer="The three major events in the life of Abraham Lincoln are: 1. ...",
# sources=[
# {
# "name": "HISTORY",
# "url": "https://www.history.com/topics/us-presidents/abraham-lincoln",
# "snippet": "Abraham Lincoln - Facts & Summary - HISTORY ..."
# },
# ...
# ]
# }
```

#### 🪝 Fetch

The `fetch` function can be used to retrieve the content of a given web page in a cleaned up
markdown format.

You can use the `render_js` flag to execute the JavaScript code of the page before returning the
content, and ask to `include_raw_html` to the response if you feel like it.

```python
from linkup import LinkupClient, LinkupFetchResponse

client = LinkupClient() # API key can be read from the environment variable or passed as an argument
fetch_response: LinkupFetchResponse = client.fetch(
url="https://docs.linkup.so",
render_js=False,
include_raw_html=True,
)
print(fetch_response.model_dump())
# Response:
# {
# markdown="Get started for free, no credit card required...",
# raw_html="<!DOCTYPE html><html lang=\"en\"><head>...</head><body>...</body></html>"
# }
```

#### 📚 More Examples
Expand Down
17 changes: 17 additions & 0 deletions examples/5_fetch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
The Linkup fetch can output the raw content of a web page.
"""

from dotenv import load_dotenv
from rich import print

from linkup import LinkupClient

load_dotenv()
client = LinkupClient()

response = client.fetch(
url="https://docs.linkup.so",
render_js=False,
)
print(response)
16 changes: 10 additions & 6 deletions src/linkup/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from ._version import __version__
from .client import (
LinkupClient,
)
from .client import LinkupClient
from .errors import (
LinkupAuthenticationError,
LinkupFailedFetchError,
LinkupInsufficientCreditError,
LinkupInvalidRequestError,
LinkupNoResultError,
LinkupTooManyRequestsError,
LinkupUnknownError,
)
from .types import (
LinkupFetchResponse,
LinkupSearchImageResult,
LinkupSearchResults,
LinkupSearchTextResult,
Expand All @@ -21,13 +22,16 @@
"__version__",
"LinkupClient",
"LinkupAuthenticationError",
"LinkupFailedFetchError",
"LinkupInsufficientCreditError",
"LinkupInvalidRequestError",
"LinkupUnknownError",
"LinkupNoResultError",
"LinkupInsufficientCreditError",
"LinkupSearchTextResult",
"LinkupTooManyRequestsError",
"LinkupUnknownError",
"LinkupFetchResponse",
"LinkupSearchImageResult",
"LinkupSearchResults",
"LinkupSearchTextResult",
"LinkupSource",
"LinkupSourcedAnswer",
]
103 changes: 95 additions & 8 deletions src/linkup/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from linkup._version import __version__
from linkup.errors import (
LinkupAuthenticationError,
LinkupFailedFetchError,
LinkupInsufficientCreditError,
LinkupInvalidRequestError,
LinkupNoResultError,
LinkupTooManyRequestsError,
LinkupUnknownError,
)
from linkup.types import LinkupSearchResults, LinkupSourcedAnswer
from linkup.types import LinkupFetchResponse, LinkupSearchResults, LinkupSourcedAnswer


class LinkupClient:
Expand Down Expand Up @@ -115,9 +116,9 @@ def search(
timeout=None,
)
if response.status_code != 200:
self._raise_linkup_error(response)
self._raise_linkup_error(response=response)

return self._validate_search_response(
return self._parse_search_response(
response=response,
output_type=output_type,
structured_output_schema=structured_output_schema,
Expand Down Expand Up @@ -191,14 +192,80 @@ async def async_search(
timeout=None,
)
if response.status_code != 200:
self._raise_linkup_error(response)
self._raise_linkup_error(response=response)

return self._validate_search_response(
return self._parse_search_response(
response=response,
output_type=output_type,
structured_output_schema=structured_output_schema,
)

def fetch(
self,
url: str,
render_js: bool = False,
include_raw_html: bool = False,
) -> LinkupFetchResponse:
"""Fetch the content of a web page.

Args:
url: The URL of the web page to fetch.
render_js: Whether the API should render the JavaScript of the webpage.
include_raw_html: Whether to include the raw HTML of the webpage in the response.

Returns:
The response of the web page fetch, containing the web page content.
"""
params: dict[str, Union[str, bool]] = self._get_fetch_params(
url=url,
render_js=render_js,
include_raw_html=include_raw_html,
)

response: httpx.Response = self._request(
method="POST",
url="/fetch",
json=params,
timeout=None,
)
if response.status_code != 200:
self._raise_linkup_error(response=response)

return self._parse_fetch_response(response=response)

async def async_fetch(
self,
url: str,
render_js: bool = False,
include_raw_html: bool = False,
) -> LinkupFetchResponse:
"""Asynchronously fetch the content of a web page.

Args:
url: The URL of the web page to fetch.
render_js: Whether the API should render the JavaScript of the webpage.
include_raw_html: Whether to include the raw HTML of the webpage in the response.

Returns:
The response of the web page fetch, containing the web page content.
"""
params: dict[str, Union[str, bool]] = self._get_fetch_params(
url=url,
render_js=render_js,
include_raw_html=include_raw_html,
)

response: httpx.Response = await self._async_request(
method="POST",
url="/fetch",
json=params,
timeout=None,
)
if response.status_code != 200:
self._raise_linkup_error(response=response)

return self._parse_fetch_response(response=response)

def _user_agent(self) -> str: # pragma: no cover
return f"Linkup-Python/{self.__version__}"

Expand Down Expand Up @@ -240,10 +307,9 @@ def _raise_linkup_error(self, response: httpx.Response) -> None:
if "error" in error_data:
error = error_data["error"]
code = error.get("code", "")
message = error.get("message", "")
error_msg = error.get("message", "")
details = error.get("details", [])

error_msg = f"{message}"
if details and isinstance(details, list):
for detail in details:
if isinstance(detail, dict):
Expand All @@ -258,6 +324,12 @@ def _raise_linkup_error(self, response: httpx.Response) -> None:
"Try rephrasing you query.\n"
f"Original error message: {error_msg}."
)
if code == "FETCH_ERROR":
raise LinkupFailedFetchError(
"The Linkup API returned a fetch error (400). "
"The provided URL might not be found or can't be fetched.\n"
f"Original error message: {error_msg}."
)
else:
raise LinkupInvalidRequestError(
"The Linkup API returned an invalid request error (400). Make sure the "
Expand Down Expand Up @@ -341,7 +413,19 @@ def _get_search_params(
toDate=to_date.isoformat() if to_date is not None else date.today().isoformat(),
)

def _validate_search_response(
def _get_fetch_params(
self,
url: str,
render_js: bool,
include_raw_html: bool = False,
) -> dict[str, Union[str, bool]]:
return dict(
url=url,
renderJs=render_js,
includeRawHtml=include_raw_html,
)

def _parse_search_response(
self,
response: httpx.Response,
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
Expand All @@ -363,3 +447,6 @@ def _validate_search_response(
if output_base_model is None:
return response_data
return output_base_model.model_validate(response_data)

def _parse_fetch_response(self, response: httpx.Response) -> LinkupFetchResponse:
return LinkupFetchResponse.model_validate(response.json())
10 changes: 10 additions & 0 deletions src/linkup/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ class LinkupTooManyRequestsError(Exception):
pass


class LinkupFailedFetchError(Exception):
"""Failed fetch error, raised when the Linkup API search returns a 400 status code.

It is returned when the Linkup API failed to fetch the content of an URL due to technical
reasons.
"""

pass


class LinkupUnknownError(Exception):
"""Unknown error, raised when the Linkup API returns an unknown status code."""

Expand Down
19 changes: 17 additions & 2 deletions src/linkup/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Literal, Union
from typing import Literal, Optional, Union

from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict, Field


class LinkupSearchTextResult(BaseModel):
Expand Down Expand Up @@ -72,3 +72,18 @@ class LinkupSourcedAnswer(BaseModel):

answer: str
sources: list[LinkupSource]


class LinkupFetchResponse(BaseModel):
"""
The response from a Linkup web page fetch.
Attributes:
markdown: The cleaned up markdown content.
raw_html: The optional raw HTML content.
"""

model_config = ConfigDict(populate_by_name=True)

markdown: str
raw_html: Optional[str] = Field(default=None, validation_alias="rawHtml")
Loading