langchain-serpkite 0.1.0__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.
- langchain_serpkite-0.1.0/.github/workflows/ci.yml +22 -0
- langchain_serpkite-0.1.0/.github/workflows/release.yml +38 -0
- langchain_serpkite-0.1.0/.gitignore +9 -0
- langchain_serpkite-0.1.0/.python-version +1 -0
- langchain_serpkite-0.1.0/CONTRIBUTING.md +8 -0
- langchain_serpkite-0.1.0/LICENSE +21 -0
- langchain_serpkite-0.1.0/PKG-INFO +177 -0
- langchain_serpkite-0.1.0/README.md +153 -0
- langchain_serpkite-0.1.0/langchain_serpkite/__init__.py +24 -0
- langchain_serpkite-0.1.0/langchain_serpkite/_utilities.py +152 -0
- langchain_serpkite-0.1.0/langchain_serpkite/document_loaders.py +112 -0
- langchain_serpkite-0.1.0/langchain_serpkite/py.typed +0 -0
- langchain_serpkite-0.1.0/langchain_serpkite/retrievers.py +119 -0
- langchain_serpkite-0.1.0/langchain_serpkite/tools.py +191 -0
- langchain_serpkite-0.1.0/pyproject.toml +75 -0
- langchain_serpkite-0.1.0/tests/__init__.py +0 -0
- langchain_serpkite-0.1.0/tests/integration_tests/__init__.py +0 -0
- langchain_serpkite-0.1.0/tests/integration_tests/test_live.py +51 -0
- langchain_serpkite-0.1.0/tests/unit_tests/__init__.py +0 -0
- langchain_serpkite-0.1.0/tests/unit_tests/conftest.py +19 -0
- langchain_serpkite-0.1.0/tests/unit_tests/fixtures.py +47 -0
- langchain_serpkite-0.1.0/tests/unit_tests/test_document_loaders.py +72 -0
- langchain_serpkite-0.1.0/tests/unit_tests/test_imports.py +15 -0
- langchain_serpkite-0.1.0/tests/unit_tests/test_retrievers.py +118 -0
- langchain_serpkite-0.1.0/tests/unit_tests/test_tools.py +140 -0
- langchain_serpkite-0.1.0/uv.lock +3152 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Mirrored from the SerpKite monorepo (sdks/langchain-serpkite); edits here are overwritten.
|
|
2
|
+
# --no-sources: serpkite comes from PyPI here (the monorepo uses its ../python checkout).
|
|
3
|
+
name: ci
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: astral-sh/setup-uv@v7
|
|
18
|
+
- run: uv sync --no-sources
|
|
19
|
+
- run: uv run --no-sources ruff check .
|
|
20
|
+
- run: uv run --no-sources ruff format --check .
|
|
21
|
+
- run: uv run --no-sources mypy
|
|
22
|
+
- run: uv run --no-sources pytest -q
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Mirrored from the SerpKite monorepo (sdks/langchain-serpkite); edits here are overwritten.
|
|
2
|
+
# The monorepo's sdk-mirror workflow pushes tag vX.Y.Z here; this publishes PyPI `langchain-serpkite`.
|
|
3
|
+
# The serpkite version it requires must already be on PyPI.
|
|
4
|
+
name: release
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: release-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
publish:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
# PyPI trusted publisher: repo SerpKite/langchain-serpkite, workflow release.yml, environment pypi.
|
|
20
|
+
environment: pypi
|
|
21
|
+
permissions:
|
|
22
|
+
contents: write # GitHub release
|
|
23
|
+
id-token: write # PyPI trusted publishing
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
|
+
- uses: astral-sh/setup-uv@v7
|
|
27
|
+
- name: Tag matches pyproject
|
|
28
|
+
env:
|
|
29
|
+
TAG: ${{ github.ref_name }}
|
|
30
|
+
run: test "${TAG#v}" = "$(uv version --short)"
|
|
31
|
+
- run: uv sync --no-sources && uv run --no-sources pytest -q
|
|
32
|
+
- run: uv build --no-sources --out-dir dist
|
|
33
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
34
|
+
- name: GitHub release
|
|
35
|
+
env:
|
|
36
|
+
GH_TOKEN: ${{ github.token }}
|
|
37
|
+
TAG: ${{ github.ref_name }}
|
|
38
|
+
run: gh release create "$TAG" --verify-tag --generate-notes --title "$TAG" dist/*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
This repository is a read-only mirror, published automatically from the SerpKite
|
|
4
|
+
monorepo. Issues and pull requests are welcome here. Maintainers port accepted
|
|
5
|
+
changes upstream, and the next sync brings them back.
|
|
6
|
+
|
|
7
|
+
`openapi/serp-api.yaml` (where present) is the SerpKite v1 OpenAPI contract that the
|
|
8
|
+
SDK's types are generated from and checked against. Don't edit it here.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SerpKite
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-serpkite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain integration for SerpKite, the Google search API built for AI agents: tools, a retriever and a webpage loader.
|
|
5
|
+
Project-URL: Homepage, https://serpkite.com/integrations/langchain
|
|
6
|
+
Project-URL: Documentation, https://serpkite.com/docs/sdks/langchain
|
|
7
|
+
Project-URL: Repository, https://github.com/serpkite/langchain-serpkite
|
|
8
|
+
Author-email: SerpKite <support@serpkite.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,google,langchain,llm,retriever,search,serp,serpkite,tool
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: langchain-core<2,>=0.3
|
|
22
|
+
Requires-Dist: serpkite>=0.1.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# langchain-serpkite
|
|
26
|
+
|
|
27
|
+
LangChain integration for [SerpKite](https://serpkite.com), the Google search API built for AI
|
|
28
|
+
agents. The package gives you:
|
|
29
|
+
|
|
30
|
+
| Class | What it does |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `SerpKiteSearch` | Tool (`serpkite_search`) that returns Markdown, which uses few tokens, for agents |
|
|
33
|
+
| `SerpKiteSearchResults` | Tool (`serpkite_search_results_json`) that returns a list of result dicts (title, link, domain, snippet, …) |
|
|
34
|
+
| `SerpKiteRetriever` | Retriever that returns Google results as `Document`s, optionally with full page Markdown |
|
|
35
|
+
| `SerpKiteWebpageLoader` | Document loader that fetches any public URL as clean Markdown |
|
|
36
|
+
| `SerpKiteAPIWrapper` | Holds the API key and the sync and async SerpKite clients |
|
|
37
|
+
|
|
38
|
+
Every class supports both sync and async (`invoke`/`ainvoke`, `load`/`alazy_load`).
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install -U langchain-serpkite
|
|
44
|
+
export SERPKITE_API_KEY=skt_live_... # https://app.serpkite.com/keys
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from langchain_serpkite import SerpKiteSearch, SerpKiteSearchResults
|
|
51
|
+
|
|
52
|
+
search = SerpKiteSearch() # optional: endpoint="news", country="de", num=20, api_key="..."
|
|
53
|
+
print(search.invoke({"query": "best espresso machine"})) # Markdown string
|
|
54
|
+
|
|
55
|
+
papers = SerpKiteSearch(endpoint="scholar") # or news, images, videos, maps, places,
|
|
56
|
+
# shopping, patents, autocomplete, ai-mode
|
|
57
|
+
rows = SerpKiteSearchResults(max_results=5).invoke({"query": "langgraph checkpointer"})
|
|
58
|
+
# [{"position": 1, "title": "...", "link": "https://...", "domain": "...", "snippet": "..."}, ...]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The model can set `query`, `num`, `country`, `language` and `time`
|
|
62
|
+
(`hour|day|week|month|year`). Defaults set on the tool (`country=`, `language=`, `location=`,
|
|
63
|
+
`num=`) apply when the model leaves a field out. `engine=` is set on the tool only (see
|
|
64
|
+
[Search engines & fallback](#search-engines--fallback)).
|
|
65
|
+
|
|
66
|
+
## Agent (LangChain / LangGraph)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from langchain.agents import create_agent
|
|
70
|
+
from langchain_serpkite import SerpKiteSearch, SerpKiteSearchResults
|
|
71
|
+
|
|
72
|
+
agent = create_agent(
|
|
73
|
+
model="provider:model-name", # any tool-calling chat model
|
|
74
|
+
tools=[SerpKiteSearch(), SerpKiteSearchResults(max_results=5)],
|
|
75
|
+
system_prompt="You are a research assistant. Search before answering and cite links.",
|
|
76
|
+
)
|
|
77
|
+
out = agent.invoke({"messages": [{"role": "user", "content": "What changed in the EU AI Act this month?"}]})
|
|
78
|
+
print(out["messages"][-1].content)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`create_agent` runs on LangGraph. The tools work the same way with
|
|
82
|
+
`langgraph.prebuilt.create_react_agent(model, tools=[...])`, and in a custom `StateGraph` through
|
|
83
|
+
`ToolNode`.
|
|
84
|
+
|
|
85
|
+
## Retriever
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from langchain_serpkite import SerpKiteRetriever
|
|
89
|
+
|
|
90
|
+
retriever = SerpKiteRetriever(k=5, include_content=2, country="us", language="en")
|
|
91
|
+
docs = retriever.invoke("how does HNSW indexing work")
|
|
92
|
+
docs = retriever.invoke("how does HNSW indexing work", k=3) # per-call override
|
|
93
|
+
|
|
94
|
+
docs[0].page_content # page Markdown for the top include_content results, else the snippet
|
|
95
|
+
docs[0].metadata # {"title", "link", "source", "position", "domain", "snippet", "engine", ...}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `include_content` (0-5) fetches the top N result pages as Markdown for +1 credit each.
|
|
99
|
+
- `k` above 10 uses a deep search, which costs 7 credits for 100 results.
|
|
100
|
+
- `time` limits results to the last hour, day, week, month or year.
|
|
101
|
+
|
|
102
|
+
A minimal RAG chain:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
106
|
+
from langchain_core.runnables import RunnablePassthrough
|
|
107
|
+
|
|
108
|
+
prompt = ChatPromptTemplate.from_template("Answer from these sources:\n{context}\n\nQuestion: {question}")
|
|
109
|
+
chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Webpage loader
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from langchain_serpkite import SerpKiteWebpageLoader
|
|
116
|
+
|
|
117
|
+
loader = SerpKiteWebpageLoader(["https://example.com", "https://serpkite.com"], continue_on_failure=True)
|
|
118
|
+
docs = loader.load() # sync
|
|
119
|
+
docs = [d async for d in loader.alazy_load()] # async
|
|
120
|
+
docs[0].metadata # {"source", "requested_url", "status_code", "title", "description", "language", ...}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Each page costs 1 credit. Pass `include_html=True` to also get the raw HTML in
|
|
124
|
+
`metadata["html"]`.
|
|
125
|
+
|
|
126
|
+
## Search engines & fallback
|
|
127
|
+
|
|
128
|
+
By default every request is answered by Google only (`engine="google"`); SerpKite already fails
|
|
129
|
+
over across its own proxy pools. Opt in to other providers with `engine` on a tool, the retriever
|
|
130
|
+
or the shared wrapper (you set it, never the model):
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
search = SerpKiteSearch(engine="auto") # fall back when Google is unavailable
|
|
134
|
+
retriever = SerpKiteRetriever(k=5, engine=["google", "brave"]) # only these providers, in order
|
|
135
|
+
docs = retriever.invoke("espresso", engine="auto") # per-call override
|
|
136
|
+
docs[0].metadata["engine"] # "google", or e.g. "brave"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `engine` is `"google"` (default), `"auto"`, `"consensus"`, one provider (`"brave"`, `"bing"`,
|
|
140
|
+
`"yahoo"`, `"duckduckgo"`, `"mojeek"`, `"wikipedia"`) or a list. `"auto"` and `"consensus"` can't be combined
|
|
141
|
+
with other names; unknown names, or a provider that doesn't serve the endpoint, raise
|
|
142
|
+
`serpkite.BadRequestError`.
|
|
143
|
+
- The retriever puts the answering provider (`meta.engine`) into each Document's `metadata["engine"]`.
|
|
144
|
+
With `engine="consensus"` (several indexes merged and ranked by agreement; costs the sum of the
|
|
145
|
+
providers that answered) it is `"consensus"`, and `metadata["sources"]` lists the providers that
|
|
146
|
+
returned each result.
|
|
147
|
+
- Credits follow the answering provider's price.
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
Every class takes `api_key=` and `base_url=`, or a shared wrapper:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from langchain_serpkite import SerpKiteAPIWrapper, SerpKiteRetriever, SerpKiteSearch
|
|
155
|
+
|
|
156
|
+
wrapper = SerpKiteAPIWrapper(api_key="skt_live_...", country="de", language="de", max_retries=3)
|
|
157
|
+
tool = SerpKiteSearch(api_wrapper=wrapper)
|
|
158
|
+
retriever = SerpKiteRetriever(api_wrapper=wrapper, k=5)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
API errors raise `serpkite.SerpKiteError` subclasses. Each one has `status`, `code`, `message` and
|
|
162
|
+
`request_id`. See the [`serpkite`](https://pypi.org/project/serpkite/) SDK. Failed, empty and
|
|
163
|
+
blocked searches are not billed.
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
uv sync # installs ../python (serpkite) in editable mode
|
|
169
|
+
uv run pytest # unit tests: mocked HTTP, sockets disabled, LangChain standard tests
|
|
170
|
+
uv run ruff check . && uv run ruff format --check .
|
|
171
|
+
uv run mypy
|
|
172
|
+
SERPKITE_API_KEY=skt_live_... uv run pytest tests/integration_tests # live
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# langchain-serpkite
|
|
2
|
+
|
|
3
|
+
LangChain integration for [SerpKite](https://serpkite.com), the Google search API built for AI
|
|
4
|
+
agents. The package gives you:
|
|
5
|
+
|
|
6
|
+
| Class | What it does |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `SerpKiteSearch` | Tool (`serpkite_search`) that returns Markdown, which uses few tokens, for agents |
|
|
9
|
+
| `SerpKiteSearchResults` | Tool (`serpkite_search_results_json`) that returns a list of result dicts (title, link, domain, snippet, …) |
|
|
10
|
+
| `SerpKiteRetriever` | Retriever that returns Google results as `Document`s, optionally with full page Markdown |
|
|
11
|
+
| `SerpKiteWebpageLoader` | Document loader that fetches any public URL as clean Markdown |
|
|
12
|
+
| `SerpKiteAPIWrapper` | Holds the API key and the sync and async SerpKite clients |
|
|
13
|
+
|
|
14
|
+
Every class supports both sync and async (`invoke`/`ainvoke`, `load`/`alazy_load`).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -U langchain-serpkite
|
|
20
|
+
export SERPKITE_API_KEY=skt_live_... # https://app.serpkite.com/keys
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from langchain_serpkite import SerpKiteSearch, SerpKiteSearchResults
|
|
27
|
+
|
|
28
|
+
search = SerpKiteSearch() # optional: endpoint="news", country="de", num=20, api_key="..."
|
|
29
|
+
print(search.invoke({"query": "best espresso machine"})) # Markdown string
|
|
30
|
+
|
|
31
|
+
papers = SerpKiteSearch(endpoint="scholar") # or news, images, videos, maps, places,
|
|
32
|
+
# shopping, patents, autocomplete, ai-mode
|
|
33
|
+
rows = SerpKiteSearchResults(max_results=5).invoke({"query": "langgraph checkpointer"})
|
|
34
|
+
# [{"position": 1, "title": "...", "link": "https://...", "domain": "...", "snippet": "..."}, ...]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The model can set `query`, `num`, `country`, `language` and `time`
|
|
38
|
+
(`hour|day|week|month|year`). Defaults set on the tool (`country=`, `language=`, `location=`,
|
|
39
|
+
`num=`) apply when the model leaves a field out. `engine=` is set on the tool only (see
|
|
40
|
+
[Search engines & fallback](#search-engines--fallback)).
|
|
41
|
+
|
|
42
|
+
## Agent (LangChain / LangGraph)
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from langchain.agents import create_agent
|
|
46
|
+
from langchain_serpkite import SerpKiteSearch, SerpKiteSearchResults
|
|
47
|
+
|
|
48
|
+
agent = create_agent(
|
|
49
|
+
model="provider:model-name", # any tool-calling chat model
|
|
50
|
+
tools=[SerpKiteSearch(), SerpKiteSearchResults(max_results=5)],
|
|
51
|
+
system_prompt="You are a research assistant. Search before answering and cite links.",
|
|
52
|
+
)
|
|
53
|
+
out = agent.invoke({"messages": [{"role": "user", "content": "What changed in the EU AI Act this month?"}]})
|
|
54
|
+
print(out["messages"][-1].content)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`create_agent` runs on LangGraph. The tools work the same way with
|
|
58
|
+
`langgraph.prebuilt.create_react_agent(model, tools=[...])`, and in a custom `StateGraph` through
|
|
59
|
+
`ToolNode`.
|
|
60
|
+
|
|
61
|
+
## Retriever
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from langchain_serpkite import SerpKiteRetriever
|
|
65
|
+
|
|
66
|
+
retriever = SerpKiteRetriever(k=5, include_content=2, country="us", language="en")
|
|
67
|
+
docs = retriever.invoke("how does HNSW indexing work")
|
|
68
|
+
docs = retriever.invoke("how does HNSW indexing work", k=3) # per-call override
|
|
69
|
+
|
|
70
|
+
docs[0].page_content # page Markdown for the top include_content results, else the snippet
|
|
71
|
+
docs[0].metadata # {"title", "link", "source", "position", "domain", "snippet", "engine", ...}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `include_content` (0-5) fetches the top N result pages as Markdown for +1 credit each.
|
|
75
|
+
- `k` above 10 uses a deep search, which costs 7 credits for 100 results.
|
|
76
|
+
- `time` limits results to the last hour, day, week, month or year.
|
|
77
|
+
|
|
78
|
+
A minimal RAG chain:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
82
|
+
from langchain_core.runnables import RunnablePassthrough
|
|
83
|
+
|
|
84
|
+
prompt = ChatPromptTemplate.from_template("Answer from these sources:\n{context}\n\nQuestion: {question}")
|
|
85
|
+
chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Webpage loader
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from langchain_serpkite import SerpKiteWebpageLoader
|
|
92
|
+
|
|
93
|
+
loader = SerpKiteWebpageLoader(["https://example.com", "https://serpkite.com"], continue_on_failure=True)
|
|
94
|
+
docs = loader.load() # sync
|
|
95
|
+
docs = [d async for d in loader.alazy_load()] # async
|
|
96
|
+
docs[0].metadata # {"source", "requested_url", "status_code", "title", "description", "language", ...}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Each page costs 1 credit. Pass `include_html=True` to also get the raw HTML in
|
|
100
|
+
`metadata["html"]`.
|
|
101
|
+
|
|
102
|
+
## Search engines & fallback
|
|
103
|
+
|
|
104
|
+
By default every request is answered by Google only (`engine="google"`); SerpKite already fails
|
|
105
|
+
over across its own proxy pools. Opt in to other providers with `engine` on a tool, the retriever
|
|
106
|
+
or the shared wrapper (you set it, never the model):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
search = SerpKiteSearch(engine="auto") # fall back when Google is unavailable
|
|
110
|
+
retriever = SerpKiteRetriever(k=5, engine=["google", "brave"]) # only these providers, in order
|
|
111
|
+
docs = retriever.invoke("espresso", engine="auto") # per-call override
|
|
112
|
+
docs[0].metadata["engine"] # "google", or e.g. "brave"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `engine` is `"google"` (default), `"auto"`, `"consensus"`, one provider (`"brave"`, `"bing"`,
|
|
116
|
+
`"yahoo"`, `"duckduckgo"`, `"mojeek"`, `"wikipedia"`) or a list. `"auto"` and `"consensus"` can't be combined
|
|
117
|
+
with other names; unknown names, or a provider that doesn't serve the endpoint, raise
|
|
118
|
+
`serpkite.BadRequestError`.
|
|
119
|
+
- The retriever puts the answering provider (`meta.engine`) into each Document's `metadata["engine"]`.
|
|
120
|
+
With `engine="consensus"` (several indexes merged and ranked by agreement; costs the sum of the
|
|
121
|
+
providers that answered) it is `"consensus"`, and `metadata["sources"]` lists the providers that
|
|
122
|
+
returned each result.
|
|
123
|
+
- Credits follow the answering provider's price.
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
Every class takes `api_key=` and `base_url=`, or a shared wrapper:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from langchain_serpkite import SerpKiteAPIWrapper, SerpKiteRetriever, SerpKiteSearch
|
|
131
|
+
|
|
132
|
+
wrapper = SerpKiteAPIWrapper(api_key="skt_live_...", country="de", language="de", max_retries=3)
|
|
133
|
+
tool = SerpKiteSearch(api_wrapper=wrapper)
|
|
134
|
+
retriever = SerpKiteRetriever(api_wrapper=wrapper, k=5)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
API errors raise `serpkite.SerpKiteError` subclasses. Each one has `status`, `code`, `message` and
|
|
138
|
+
`request_id`. See the [`serpkite`](https://pypi.org/project/serpkite/) SDK. Failed, empty and
|
|
139
|
+
blocked searches are not billed.
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv sync # installs ../python (serpkite) in editable mode
|
|
145
|
+
uv run pytest # unit tests: mocked HTTP, sockets disabled, LangChain standard tests
|
|
146
|
+
uv run ruff check . && uv run ruff format --check .
|
|
147
|
+
uv run mypy
|
|
148
|
+
SERPKITE_API_KEY=skt_live_... uv run pytest tests/integration_tests # live
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""LangChain integration for SerpKite, the Google search API built for AI agents."""
|
|
2
|
+
|
|
3
|
+
from importlib import metadata
|
|
4
|
+
|
|
5
|
+
from langchain_serpkite._utilities import SerpKiteAPIWrapper
|
|
6
|
+
from langchain_serpkite.document_loaders import SerpKiteWebpageLoader
|
|
7
|
+
from langchain_serpkite.retrievers import SerpKiteRetriever
|
|
8
|
+
from langchain_serpkite.tools import SerpKiteSearch, SerpKiteSearchInput, SerpKiteSearchResults
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
__version__ = metadata.version(__package__ or __name__)
|
|
12
|
+
except metadata.PackageNotFoundError: # pragma: no cover - source checkout without install
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
del metadata
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"SerpKiteAPIWrapper",
|
|
18
|
+
"SerpKiteRetriever",
|
|
19
|
+
"SerpKiteSearch",
|
|
20
|
+
"SerpKiteSearchInput",
|
|
21
|
+
"SerpKiteSearchResults",
|
|
22
|
+
"SerpKiteWebpageLoader",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""SerpKiteAPIWrapper: holds the SerpKite clients shared by the tools, retriever and loader."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, Union
|
|
6
|
+
|
|
7
|
+
from langchain_core.utils import secret_from_env
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, SecretStr
|
|
9
|
+
from serpkite import AsyncSerpKite, SerpKite
|
|
10
|
+
from serpkite.types import SearchResponse, WebpageResponse
|
|
11
|
+
|
|
12
|
+
__all__ = ["SerpKiteAPIWrapper"]
|
|
13
|
+
|
|
14
|
+
VERTICALS = frozenset(
|
|
15
|
+
{
|
|
16
|
+
"search",
|
|
17
|
+
"news",
|
|
18
|
+
"images",
|
|
19
|
+
"videos",
|
|
20
|
+
"maps",
|
|
21
|
+
"places",
|
|
22
|
+
"shopping",
|
|
23
|
+
"scholar",
|
|
24
|
+
"patents",
|
|
25
|
+
"autocomplete",
|
|
26
|
+
"ai-mode",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def normalize_endpoint(endpoint: str) -> str:
|
|
32
|
+
ep = endpoint.strip().lower().replace("_", "-")
|
|
33
|
+
if ep not in VERTICALS:
|
|
34
|
+
raise ValueError(f"unknown SerpKite endpoint {endpoint!r}; use one of {', '.join(sorted(VERTICALS))}")
|
|
35
|
+
return ep
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SerpKiteAPIWrapper(BaseModel):
|
|
39
|
+
"""Thin wrapper around the ``serpkite`` SDK clients.
|
|
40
|
+
|
|
41
|
+
The API key comes from ``api_key`` or the ``SERPKITE_API_KEY`` environment variable.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
.. code-block:: python
|
|
45
|
+
|
|
46
|
+
from langchain_serpkite import SerpKiteAPIWrapper
|
|
47
|
+
|
|
48
|
+
wrapper = SerpKiteAPIWrapper(country="de")
|
|
49
|
+
print(wrapper.markdown("beste espressomaschine"))
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
api_key: SecretStr = Field(
|
|
53
|
+
default_factory=secret_from_env(
|
|
54
|
+
"SERPKITE_API_KEY",
|
|
55
|
+
error_message=(
|
|
56
|
+
"No SerpKite API key. Pass api_key=... or set the SERPKITE_API_KEY environment variable "
|
|
57
|
+
"(create a key at https://app.serpkite.com/keys)."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
"""SerpKite API key (``skt_live_…``). Defaults to ``SERPKITE_API_KEY``."""
|
|
62
|
+
base_url: Optional[str] = None
|
|
63
|
+
"""API origin; defaults to ``SERPKITE_BASE_URL`` or https://api.serpkite.com."""
|
|
64
|
+
timeout: float = 60.0
|
|
65
|
+
max_retries: int = 2
|
|
66
|
+
country: Optional[str] = None
|
|
67
|
+
"""Default country code (e.g. ``us``) when a call doesn't pass one."""
|
|
68
|
+
language: Optional[str] = None
|
|
69
|
+
"""Default interface language (e.g. ``en``)."""
|
|
70
|
+
location: Optional[str] = None
|
|
71
|
+
"""Default free-text location, e.g. ``"Austin, Texas, United States"``."""
|
|
72
|
+
engine: Optional[Union[str, list[str]]] = None
|
|
73
|
+
"""Default search providers allowed to answer: ``"google"`` (the API default, Google only),
|
|
74
|
+
``"auto"`` (fall back to other providers when Google is blocked or times out), one provider
|
|
75
|
+
(``"brave"``) or a list (``["google", "brave"]``). ``meta.engine`` names the one that answered."""
|
|
76
|
+
|
|
77
|
+
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
|
78
|
+
|
|
79
|
+
_client: Optional[SerpKite] = PrivateAttr(default=None)
|
|
80
|
+
_async_client: Optional[AsyncSerpKite] = PrivateAttr(default=None)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def client(self) -> SerpKite:
|
|
84
|
+
"""The sync SDK client (created on first use)."""
|
|
85
|
+
if self._client is None:
|
|
86
|
+
self._client = SerpKite(
|
|
87
|
+
api_key=self.api_key.get_secret_value(),
|
|
88
|
+
base_url=self.base_url,
|
|
89
|
+
timeout=self.timeout,
|
|
90
|
+
max_retries=self.max_retries,
|
|
91
|
+
)
|
|
92
|
+
return self._client
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def async_client(self) -> AsyncSerpKite:
|
|
96
|
+
"""The async SDK client (created on first use)."""
|
|
97
|
+
if self._async_client is None:
|
|
98
|
+
self._async_client = AsyncSerpKite(
|
|
99
|
+
api_key=self.api_key.get_secret_value(),
|
|
100
|
+
base_url=self.base_url,
|
|
101
|
+
timeout=self.timeout,
|
|
102
|
+
max_retries=self.max_retries,
|
|
103
|
+
)
|
|
104
|
+
return self._async_client
|
|
105
|
+
|
|
106
|
+
def _body(self, query: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
107
|
+
body: dict[str, Any] = {
|
|
108
|
+
"q": query,
|
|
109
|
+
"country": self.country,
|
|
110
|
+
"language": self.language,
|
|
111
|
+
"location": self.location,
|
|
112
|
+
"engine": self.engine,
|
|
113
|
+
}
|
|
114
|
+
body.update({k: v for k, v in params.items() if v is not None})
|
|
115
|
+
return body
|
|
116
|
+
|
|
117
|
+
# Sync -----------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def markdown(self, query: str, endpoint: str = "search", **params: Any) -> str:
|
|
120
|
+
"""Run ``query`` against ``endpoint`` and return the Markdown rendering."""
|
|
121
|
+
body = {**self._body(query, params), "format": "markdown"}
|
|
122
|
+
return str(self.client.request(normalize_endpoint(endpoint), body))
|
|
123
|
+
|
|
124
|
+
def raw(self, query: str, endpoint: str = "search", **params: Any) -> dict[str, Any]:
|
|
125
|
+
"""Run ``query`` against ``endpoint`` and return the JSON body as a dict."""
|
|
126
|
+
result: dict[str, Any] = self.client.request(normalize_endpoint(endpoint), self._body(query, params))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def search(self, query: str, **params: Any) -> SearchResponse:
|
|
130
|
+
"""Typed ``/v1/search`` call."""
|
|
131
|
+
return SearchResponse.model_validate(self.raw(query, "search", **params))
|
|
132
|
+
|
|
133
|
+
def webpage(self, url: str, include_html: bool = False) -> WebpageResponse:
|
|
134
|
+
"""Typed ``/v1/webpage`` call."""
|
|
135
|
+
return self.client.webpage(url, include_html=include_html)
|
|
136
|
+
|
|
137
|
+
# Async ----------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
async def amarkdown(self, query: str, endpoint: str = "search", **params: Any) -> str:
|
|
140
|
+
body = {**self._body(query, params), "format": "markdown"}
|
|
141
|
+
return str(await self.async_client.request(normalize_endpoint(endpoint), body))
|
|
142
|
+
|
|
143
|
+
async def araw(self, query: str, endpoint: str = "search", **params: Any) -> dict[str, Any]:
|
|
144
|
+
body = self._body(query, params)
|
|
145
|
+
result: dict[str, Any] = await self.async_client.request(normalize_endpoint(endpoint), body)
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
async def asearch(self, query: str, **params: Any) -> SearchResponse:
|
|
149
|
+
return SearchResponse.model_validate(await self.araw(query, "search", **params))
|
|
150
|
+
|
|
151
|
+
async def awebpage(self, url: str, include_html: bool = False) -> WebpageResponse:
|
|
152
|
+
return await self.async_client.webpage(url, include_html=include_html)
|