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.
Files changed (26) hide show
  1. langchain_serpkite-0.1.0/.github/workflows/ci.yml +22 -0
  2. langchain_serpkite-0.1.0/.github/workflows/release.yml +38 -0
  3. langchain_serpkite-0.1.0/.gitignore +9 -0
  4. langchain_serpkite-0.1.0/.python-version +1 -0
  5. langchain_serpkite-0.1.0/CONTRIBUTING.md +8 -0
  6. langchain_serpkite-0.1.0/LICENSE +21 -0
  7. langchain_serpkite-0.1.0/PKG-INFO +177 -0
  8. langchain_serpkite-0.1.0/README.md +153 -0
  9. langchain_serpkite-0.1.0/langchain_serpkite/__init__.py +24 -0
  10. langchain_serpkite-0.1.0/langchain_serpkite/_utilities.py +152 -0
  11. langchain_serpkite-0.1.0/langchain_serpkite/document_loaders.py +112 -0
  12. langchain_serpkite-0.1.0/langchain_serpkite/py.typed +0 -0
  13. langchain_serpkite-0.1.0/langchain_serpkite/retrievers.py +119 -0
  14. langchain_serpkite-0.1.0/langchain_serpkite/tools.py +191 -0
  15. langchain_serpkite-0.1.0/pyproject.toml +75 -0
  16. langchain_serpkite-0.1.0/tests/__init__.py +0 -0
  17. langchain_serpkite-0.1.0/tests/integration_tests/__init__.py +0 -0
  18. langchain_serpkite-0.1.0/tests/integration_tests/test_live.py +51 -0
  19. langchain_serpkite-0.1.0/tests/unit_tests/__init__.py +0 -0
  20. langchain_serpkite-0.1.0/tests/unit_tests/conftest.py +19 -0
  21. langchain_serpkite-0.1.0/tests/unit_tests/fixtures.py +47 -0
  22. langchain_serpkite-0.1.0/tests/unit_tests/test_document_loaders.py +72 -0
  23. langchain_serpkite-0.1.0/tests/unit_tests/test_imports.py +15 -0
  24. langchain_serpkite-0.1.0/tests/unit_tests/test_retrievers.py +118 -0
  25. langchain_serpkite-0.1.0/tests/unit_tests/test_tools.py +140 -0
  26. 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,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .benchmarks/
8
+ build/
9
+ 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)