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
15 changes: 7 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ This repository contains the public Python SDK for the Linkup API.

## Goal

Keep the SDK aligned with the current public, stable Linkup API while preserving a Pythonic public
interface.
Keep the SDK aligned with the current public, stable Linkup API while:

- implementing a Pythonic public interface,
- documenting the features through docstrings,
- and adding type safety through type hints or input data validation with `pydantic` models when
relevant.

## Working Rules

- Read this file before making changes.
- Prefer minimal diffs focused on the public API change being synchronized.
- Do not expose internal, beta, deprecated, or undocumented API behavior unless explicitly
requested.
Expand All @@ -34,11 +37,7 @@ When adding or changing a public API capability, update the relevant pieces toge

## Validation

Before opening a PR, run the narrowest relevant checks:

- `make lint`
- `make typecheck`
- `make test`
Before opening a PR, run the CI checks: `make lint typecheck test`.

## Non-Goals

Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,21 @@ pip install linkup-sdk[x402]

#### 📝 Search

The `search` function can be used to performs web searches. It supports two very different
complexity modes:
The `search` function can be used to performs web searches. It supports three different complexity
modes, through the `depth` parameter:

- 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?")
- `"fast"` (**beta**), for sub-second responses to simple, focused queries (must be keyword-based)
- `"standard"`, for single-iteration agentic search that can interpret the query, run parallel
sub-searches, and scrape one URL while remaining fast
- `"deep"`, for slower, more agentic and complex responses, suited to 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?")

The `search` function also supports three output types:
The `search` function also supports three output types, through the `output_type` parameter:

- 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
- with `"searchResults"`, the search will return a list of relevant documents
- with `"sourcedAnswer"`, the search will return a concise answer with sources
- with `"structured"`, the search will return a structured output according to a user-defined schema

```python
from typing import Any
Expand All @@ -104,7 +104,7 @@ import linkup
client = linkup.Client() # 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"
depth="deep", # "fast" (beta), "standard", or "deep"
output_type="sourcedAnswer", # "searchResults" or "sourcedAnswer" or "structured"
structured_output_schema=None, # must be filled if output_type is "structured"
)
Expand Down Expand Up @@ -223,7 +223,7 @@ async def main() -> None:
client = linkup.Client() # API key can be read from the environment variable or passed as an argument
search_response: Any = await client.async_search(
query="What are the 3 major events in the life of Abraham Lincoln?",
depth="deep", # "standard" or "deep"
depth="deep", # "fast" (beta), "standard", or "deep"
output_type="sourcedAnswer", # "searchResults" or "sourcedAnswer" or "structured"
structured_output_schema=None, # must be filled if output_type is "structured"
)
Expand Down
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ classifiers = [
dependencies = ["httpx>=0.23.0", "pydantic>=2.0.0"]

[project.optional-dependencies]
build = ["uv>=0.10.0,<0.11.0"] # For python-semantic-release build command, used in GitHub actions
build = ["uv>=0.11.6,<0.12.0"] # For python-semantic-release build command, used in GitHub actions
x402 = ["x402[httpx,evm]>=2.0.0"]

[project.urls]
Expand All @@ -34,9 +34,8 @@ dev = [
"pytest-asyncio>=1.0.0",
"pytest-cov>=6.2.1",
"pytest-mock>=3.14.1",
"pytest>=8.4.1",
"python-dotenv>=1.1.1",
"rich>=14.1.0",
"pytest>=9.0.3",
"python-dotenv>=1.2.2",
"x402[httpx,evm]>=2.0.0",
]

Expand Down Expand Up @@ -137,7 +136,7 @@ parse_squash_commits = false

[tool.uv]
exclude-newer = "2 weeks" # Reduce risks of supply chain attacks
required-version = ">=0.10.0,<0.11.0"
required-version = ">=0.11.6,<0.12.0"

[build-system]
build-backend = "hatchling.build"
Expand Down
18 changes: 11 additions & 7 deletions src/linkup/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def __init__(
def search(
self,
query: str,
depth: Literal["standard", "deep"],
depth: Literal["fast", "standard", "deep"],
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
structured_output_schema: type[BaseModel] | dict[str, Any] | str | None = None,
include_images: bool | None = None,
Expand All @@ -111,8 +111,10 @@ def search(

Args:
query: The search query.
depth: The depth of the search. Can be either "standard", for a straighforward and
fast search, or "deep" for a more powerful agentic workflow.
depth: The depth of the search. Can be "fast" (beta), for a sub-second search (query
must be keyword-based), "standard", for a simple, straightforward search (query can
be free text), or "deep" for a more powerful agentic workflow (query can be free
text).
output_type: The type of output which is expected: "searchResults" will output raw
search results, "sourcedAnswer" will output the answer to the query and sources
supporting it, and "structured" will base the output on the format provided in
Expand Down Expand Up @@ -187,7 +189,7 @@ def search(
async def async_search(
self,
query: str,
depth: Literal["standard", "deep"],
depth: Literal["fast", "standard", "deep"],
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
structured_output_schema: type[BaseModel] | dict[str, Any] | str | None = None,
include_images: bool | None = None,
Expand All @@ -208,8 +210,10 @@ async def async_search(

Args:
query: The search query.
depth: The depth of the search. Can be either "standard", for a straighforward and
fast search, or "deep" for a more powerful agentic workflow.
depth: The depth of the search. Can be "fast" (beta), for a sub-second search (query
must be keyword-based), "standard", for a simple, straightforward search (query can
be free text), or "deep" for a more powerful agentic workflow (query can be free
text).
output_type: The type of output which is expected: "searchResults" will output raw
search results, "sourcedAnswer" will output the answer to the query and sources
supporting it, and "structured" will base the output on the format provided in
Expand Down Expand Up @@ -1134,7 +1138,7 @@ def _raise_linkup_error(self, response: httpx.Response) -> None:
def _get_search_params(
self,
query: str,
depth: Literal["standard", "deep"],
depth: Literal["fast", "standard", "deep"],
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
structured_output_schema: type[BaseModel] | str | dict[str, Any] | None,
include_images: bool | None,
Expand Down
12 changes: 6 additions & 6 deletions src/linkup/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class LinkupSearchTextResult(_LinkupBaseModel):
name: str
url: str
content: str
favicon: str = ""
favicon: str


class LinkupSearchImageResult(_LinkupBaseModel):
Expand Down Expand Up @@ -58,14 +58,14 @@ class LinkupSource(_LinkupBaseModel):
Attributes:
name: The name of the source.
url: The URL of the source.
snippet: The text excerpt supporting the Linkup answer. Can be empty for image sources.
snippet: The text excerpt supporting the Linkup answer.
favicon: The favicon URL of the source, if available.
"""

name: str
url: str
snippet: str = ""
favicon: str = ""
snippet: str
favicon: str


class LinkupSourcedAnswer(_LinkupBaseModel):
Expand Down Expand Up @@ -123,7 +123,7 @@ class LinkupSearchTaskInput(_LinkupBaseModel):

Attributes:
query: The search query.
depth: The search depth.
depth: The search depth. "fast" depth is in beta and only works with keyword-based queries.
output_type: The expected search output type.
include_images: Whether image results should be included.
from_date: The start date used to filter search sources, if any.
Expand All @@ -137,7 +137,7 @@ class LinkupSearchTaskInput(_LinkupBaseModel):
"""

query: str = Field(validation_alias="q")
depth: Literal["standard", "deep"]
depth: Literal["fast", "standard", "deep"]
output_type: Literal["searchResults", "sourcedAnswer", "structured"] = Field(
validation_alias="outputType"
)
Expand Down
34 changes: 30 additions & 4 deletions tests/unit/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class Company(BaseModel):
]
),
),
(
{"query": "query", "depth": "fast", "output_type": "searchResults"},
{"q": "query", "depth": "fast", "outputType": "searchResults"},
b'{"results": []}',
linkup.SearchResults(results=[]),
),
(
{
"query": "A long query.",
Expand Down Expand Up @@ -102,9 +108,24 @@ class Company(BaseModel):
{
"answer": "foo bar baz",
"sources": [
{"name": "foo", "url": "https://foo.com", "snippet": "lorem ipsum dolor sit amet"},
{"name": "bar", "url": "https://bar.com", "snippet": "consectetur adipiscing elit"},
{"name": "baz", "url": "https://baz.com"}
{
"name": "foo",
"url": "https://foo.com",
"snippet": "lorem ipsum dolor sit amet",
"favicon": "https://foo.com/favicon.ico"
},
{
"name": "bar",
"url": "https://bar.com",
"snippet": "consectetur adipiscing elit",
"favicon": "https://bar.com/favicon.ico"
},
{
"name": "baz",
"url": "https://baz.com",
"snippet": "",
"favicon": ""
}
]
}
""",
Expand All @@ -115,16 +136,19 @@ class Company(BaseModel):
name="foo",
url="https://foo.com",
snippet="lorem ipsum dolor sit amet",
favicon="https://foo.com/favicon.ico",
),
linkup.Source(
name="bar",
url="https://bar.com",
snippet="consectetur adipiscing elit",
favicon="https://bar.com/favicon.ico",
),
linkup.Source(
name="baz",
url="https://baz.com",
snippet="",
favicon="",
),
],
),
Expand Down Expand Up @@ -241,7 +265,8 @@ class Company(BaseModel):
"type": "text",
"name": "foo",
"url": "https://foo.com",
"content": "lorem ipsum dolor sit amet"
"content": "lorem ipsum dolor sit amet",
"favicon": "https://foo.com/favicon.ico"
},
{"type": "image", "name": "bar", "url": "https://bar.com"}
]
Expand All @@ -260,6 +285,7 @@ class Company(BaseModel):
name="foo",
url="https://foo.com",
content="lorem ipsum dolor sit amet",
favicon="https://foo.com/favicon.ico",
),
linkup.SearchImageResult(type="image", name="bar", url="https://bar.com"),
],
Expand Down
Loading
Loading