langchain-contextfetch 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,19 @@
1
+ """LangChain integration for ContextFetch."""
2
+
3
+ from importlib import metadata
4
+
5
+ from langchain_contextfetch.retrievers import ContextFetchRetriever
6
+ from langchain_contextfetch.tools import ContextFetchSearch
7
+
8
+ try:
9
+ __version__ = metadata.version(__package__ or "langchain_contextfetch")
10
+ except metadata.PackageNotFoundError:
11
+ # Case where package metadata is not available.
12
+ __version__ = ""
13
+ del metadata # optional, avoids polluting the results of dir(__package__)
14
+
15
+ __all__ = [
16
+ "ContextFetchRetriever",
17
+ "ContextFetchSearch",
18
+ "__version__",
19
+ ]
@@ -0,0 +1,55 @@
1
+ """Helpers shared by the retriever and the tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+
7
+ from contextfetch import ContextFetch, Result, SearchResponse
8
+ from pydantic import SecretStr
9
+
10
+ Content = Literal["text", "pruned_context"]
11
+ """Which result content to use: the whole page as Markdown, or only the chunks
12
+ relevant to the query."""
13
+
14
+ _RESULT_KEY = {"text": "text", "pruned_context": "prunedContext"}
15
+
16
+
17
+ def search(
18
+ api_key: SecretStr | None,
19
+ query: str,
20
+ *,
21
+ num_results: int,
22
+ content: Content,
23
+ include_domains: list[str] | None = None,
24
+ exclude_domains: list[str] | None = None,
25
+ start_published_date: str | None = None,
26
+ ) -> SearchResponse:
27
+ """Run one search, asking the API only for the content that will be used."""
28
+ # ContextFetch() falls back to CONTEXTFETCH_API_KEY, then to the keyless tier.
29
+ client = ContextFetch(api_key.get_secret_value() if api_key else None)
30
+ return client.search(
31
+ query,
32
+ num_results=num_results,
33
+ include_domains=include_domains,
34
+ exclude_domains=exclude_domains,
35
+ start_published_date=start_published_date,
36
+ text=content == "text",
37
+ pruned_context=content == "pruned_context",
38
+ )
39
+
40
+
41
+ def page_content(result: Result, content: Content) -> str:
42
+ """The text of a result for the chosen content type."""
43
+ return result.get(_RESULT_KEY[content]) or "" # type: ignore[return-value]
44
+
45
+
46
+ def metadata(result: Result) -> dict[str, Any]:
47
+ """Result fields other than the page content."""
48
+ return {
49
+ "id": result.get("id"),
50
+ "url": result.get("url"),
51
+ "title": result.get("title"),
52
+ "score": result.get("score"),
53
+ "published_date": result.get("publishedDate"),
54
+ "author": result.get("author"),
55
+ }
File without changes
@@ -0,0 +1,122 @@
1
+ """ContextFetch retrievers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from langchain_core.callbacks import CallbackManagerForRetrieverRun
8
+ from langchain_core.documents import Document
9
+ from langchain_core.retrievers import BaseRetriever
10
+ from langchain_core.utils import secret_from_env
11
+ from pydantic import Field, SecretStr
12
+
13
+ from langchain_contextfetch._utilities import Content, metadata, page_content, search
14
+
15
+
16
+ class ContextFetchRetriever(BaseRetriever):
17
+ """ContextFetch web search retriever.
18
+
19
+ Setup:
20
+ Install `langchain-contextfetch` and set the environment variable
21
+ `CONTEXTFETCH_API_KEY`. Without a key, requests use the keyless tier.
22
+
23
+ ```bash
24
+ pip install -U langchain-contextfetch
25
+ export CONTEXTFETCH_API_KEY="your-api-key"
26
+ ```
27
+
28
+ Key init args:
29
+ k: int
30
+ Number of results to return (1 to 25). Default 5.
31
+ content: "text" | "pruned_context"
32
+ `"text"` uses the whole page as Markdown; `"pruned_context"` uses only
33
+ the chunks of the page relevant to the query. Default `"text"`.
34
+ include_domains: list[str] | None
35
+ Only return results from these domains.
36
+ exclude_domains: list[str] | None
37
+ Never return results from these domains.
38
+ start_published_date: str | None
39
+ Only return pages published on or after this ISO date.
40
+ api_key: SecretStr | None
41
+ ContextFetch API key. Defaults to `CONTEXTFETCH_API_KEY`.
42
+
43
+ Instantiate:
44
+ ```python
45
+ from langchain_contextfetch import ContextFetchRetriever
46
+
47
+ retriever = ContextFetchRetriever(k=3)
48
+ ```
49
+
50
+ Usage:
51
+ ```python
52
+ docs = retriever.invoke("pgvector HNSW index parameters")
53
+ docs[0].metadata["url"]
54
+ ```
55
+
56
+ Use within a chain:
57
+ ```python
58
+ from langchain_core.output_parsers import StrOutputParser
59
+ from langchain_core.prompts import ChatPromptTemplate
60
+ from langchain_core.runnables import RunnablePassthrough
61
+ from langchain_anthropic import ChatAnthropic
62
+
63
+ prompt = ChatPromptTemplate.from_template(
64
+ \"\"\"Answer the question based only on the context provided.
65
+
66
+ Context: {context}
67
+
68
+ Question: {question}\"\"\"
69
+ )
70
+
71
+ model = ChatAnthropic(model="claude-sonnet-4-6")
72
+
73
+
74
+ def format_docs(docs):
75
+ return "\\n\\n".join(doc.page_content for doc in docs)
76
+
77
+
78
+ chain = (
79
+ {"context": retriever | format_docs, "question": RunnablePassthrough()}
80
+ | prompt
81
+ | model
82
+ | StrOutputParser()
83
+ )
84
+
85
+ chain.invoke("How do I tune HNSW in pgvector?")
86
+ ```
87
+ """
88
+
89
+ k: int = Field(default=5, ge=1, le=25)
90
+ """Number of results to return (1 to 25)."""
91
+ content: Content = "text"
92
+ """`"text"` (whole page as Markdown, the default) or `"pruned_context"`
93
+ (query-relevant chunks of each page)."""
94
+ include_domains: list[str] | None = None
95
+ """Only return results from these domains."""
96
+ exclude_domains: list[str] | None = None
97
+ """Never return results from these domains."""
98
+ start_published_date: str | None = None
99
+ """Only return pages published on or after this ISO date (YYYY-MM-DD)."""
100
+ api_key: SecretStr | None = Field(
101
+ default_factory=secret_from_env("CONTEXTFETCH_API_KEY", default=None)
102
+ )
103
+ """ContextFetch API key. Defaults to the `CONTEXTFETCH_API_KEY` env var."""
104
+
105
+ # Async: BaseRetriever runs this method in a thread executor, since the
106
+ # contextfetch SDK is synchronous (stdlib urllib).
107
+ def _get_relevant_documents(
108
+ self, query: str, *, run_manager: CallbackManagerForRetrieverRun, **kwargs: Any
109
+ ) -> list[Document]:
110
+ response = search(
111
+ self.api_key,
112
+ query,
113
+ num_results=kwargs.get("k", self.k),
114
+ content=self.content,
115
+ include_domains=self.include_domains,
116
+ exclude_domains=self.exclude_domains,
117
+ start_published_date=self.start_published_date,
118
+ )
119
+ return [
120
+ Document(page_content=page_content(r, self.content), metadata=metadata(r))
121
+ for r in response.get("results", [])
122
+ ]
@@ -0,0 +1,131 @@
1
+ """ContextFetch tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from contextfetch import ContextFetchError, SearchResponse
8
+ from langchain_core.callbacks import CallbackManagerForToolRun
9
+ from langchain_core.tools import BaseTool, ToolException
10
+ from langchain_core.utils import secret_from_env
11
+ from pydantic import BaseModel, Field, SecretStr
12
+
13
+ from langchain_contextfetch._utilities import Content, page_content, search
14
+
15
+
16
+ class ContextFetchSearchInput(BaseModel):
17
+ """Input schema for the ContextFetch search tool.
18
+
19
+ The Field descriptions are sent to the model when performing tool calling.
20
+ """
21
+
22
+ query: str = Field(description="The web search query.")
23
+ include_domains: list[str] | None = Field(
24
+ default=None, description="Only return results from these domains."
25
+ )
26
+ exclude_domains: list[str] | None = Field(
27
+ default=None, description="Never return results from these domains."
28
+ )
29
+ start_published_date: str | None = Field(
30
+ default=None,
31
+ description="Only return pages published on or after this date (YYYY-MM-DD).",
32
+ )
33
+
34
+
35
+ class ContextFetchSearch(BaseTool): # type: ignore[override]
36
+ """ContextFetch web search tool.
37
+
38
+ Setup:
39
+ Install `langchain-contextfetch` and set the environment variable
40
+ `CONTEXTFETCH_API_KEY`. Without a key, requests use the keyless tier.
41
+
42
+ ```bash
43
+ pip install -U langchain-contextfetch
44
+ export CONTEXTFETCH_API_KEY="your-api-key"
45
+ ```
46
+
47
+ Instantiation:
48
+ ```python
49
+ from langchain_contextfetch import ContextFetchSearch
50
+
51
+ tool = ContextFetchSearch(num_results=3)
52
+ ```
53
+
54
+ Invocation with args:
55
+ ```python
56
+ tool.invoke({"query": "pgvector HNSW index parameters"})
57
+ ```
58
+
59
+ Invocation with ToolCall:
60
+ ```python
61
+ tool.invoke(
62
+ {
63
+ "args": {"query": "pgvector HNSW index parameters"},
64
+ "id": "1",
65
+ "name": tool.name,
66
+ "type": "tool_call",
67
+ }
68
+ )
69
+ ```
70
+
71
+ Returns a `ToolMessage` whose `content` lists each result's title, URL,
72
+ published date and page content, and whose `artifact` is the raw API
73
+ response (a dict with `results`).
74
+ """
75
+
76
+ name: str = "contextfetch_search"
77
+ """The name that is passed to the model when performing tool calling."""
78
+ description: str = (
79
+ "Search the web. Returns the top results, each with its title, URL, "
80
+ "published date and page content as Markdown."
81
+ )
82
+ """The description that is passed to the model when performing tool calling."""
83
+ args_schema: type[BaseModel] = ContextFetchSearchInput
84
+ """The schema that is passed to the model when performing tool calling."""
85
+ response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
86
+
87
+ num_results: int = Field(default=5, ge=1, le=25)
88
+ """Number of results per search (1 to 25)."""
89
+ content: Content = "pruned_context"
90
+ """`"pruned_context"` (query-relevant chunks of each page, the default) or
91
+ `"text"` (whole page as Markdown)."""
92
+ api_key: SecretStr | None = Field(
93
+ default_factory=secret_from_env("CONTEXTFETCH_API_KEY", default=None)
94
+ )
95
+ """ContextFetch API key. Defaults to the `CONTEXTFETCH_API_KEY` env var."""
96
+
97
+ # Async: BaseTool runs this method in a thread executor, since the
98
+ # contextfetch SDK is synchronous (stdlib urllib).
99
+ def _run(
100
+ self,
101
+ query: str,
102
+ include_domains: list[str] | None = None,
103
+ exclude_domains: list[str] | None = None,
104
+ start_published_date: str | None = None,
105
+ *,
106
+ run_manager: CallbackManagerForToolRun | None = None,
107
+ ) -> tuple[str, SearchResponse]:
108
+ try:
109
+ response = search(
110
+ self.api_key,
111
+ query,
112
+ num_results=self.num_results,
113
+ content=self.content,
114
+ include_domains=include_domains,
115
+ exclude_domains=exclude_domains,
116
+ start_published_date=start_published_date,
117
+ )
118
+ except ContextFetchError as e:
119
+ # Raised as ToolException so handle_tool_error can pass it to the model.
120
+ raise ToolException(str(e)) from e
121
+ results = response.get("results", [])
122
+ if not results:
123
+ return "No results found.", response
124
+ content = "\n\n---\n\n".join(
125
+ f"Title: {r.get('title') or ''}\n"
126
+ f"URL: {r.get('url', '')}\n"
127
+ f"Published: {r.get('publishedDate') or 'unknown'}\n\n"
128
+ f"{page_content(r, self.content)}"
129
+ for r in results
130
+ )
131
+ return content, response
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: langchain-contextfetch
3
+ Version: 0.1.0
4
+ Summary: An integration package connecting ContextFetch and LangChain
5
+ Project-URL: Homepage, https://contextfetch.dev
6
+ Project-URL: Documentation, https://contextfetch.dev/docs/reference
7
+ Project-URL: Source Code, https://github.com/lachy3433/contextfetch/tree/main/integrations/langchain
8
+ Project-URL: Repository, https://github.com/lachy3433/contextfetch
9
+ Author: ContextFetch
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: <4.0.0,>=3.10.0
20
+ Requires-Dist: contextfetch<1.0.0,>=0.1.0
21
+ Requires-Dist: langchain-core<2.0.0,>=1.0.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # langchain-contextfetch
25
+
26
+ This package contains the LangChain integration with [ContextFetch](https://contextfetch.dev), a web search API that returns pages as Markdown.
27
+
28
+ - `ContextFetchRetriever`: a retriever that returns search results as `Document`s.
29
+ - `ContextFetchSearch`: a search tool for agents and tool-calling models.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install -U langchain-contextfetch
35
+ ```
36
+
37
+ Requires Python 3.10 or later.
38
+
39
+ ## Credentials
40
+
41
+ Set your API key in the `CONTEXTFETCH_API_KEY` environment variable, or pass `api_key=` to either class:
42
+
43
+ ```bash
44
+ export CONTEXTFETCH_API_KEY="your-api-key"
45
+ ```
46
+
47
+ Without a key, requests use ContextFetch's keyless tier, which has a small daily allowance per IP address. See the [API reference](https://contextfetch.dev/docs/reference) for keys and credits.
48
+
49
+ ## Retriever
50
+
51
+ ```python
52
+ from langchain_contextfetch import ContextFetchRetriever
53
+
54
+ retriever = ContextFetchRetriever(k=3)
55
+ docs = retriever.invoke("pgvector HNSW index parameters")
56
+
57
+ for doc in docs:
58
+ print(doc.metadata["url"], doc.metadata["title"])
59
+ ```
60
+
61
+ Each `Document` has:
62
+
63
+ - `page_content`: the page as Markdown (`content="text"`, the default), or only the chunks of the page relevant to the query (`content="pruned_context"`).
64
+ - `metadata`: `id`, `url`, `title`, `score`, `published_date` and `author`. Fields the API does not return are `None`.
65
+
66
+ Options:
67
+
68
+ | Parameter | Default | Description |
69
+ | --- | --- | --- |
70
+ | `k` | `5` | Number of results (1 to 25). Can also be passed per call: `retriever.invoke(query, k=10)`. |
71
+ | `content` | `"text"` | `"text"` or `"pruned_context"`. |
72
+ | `include_domains` | `None` | Only return results from these domains. |
73
+ | `exclude_domains` | `None` | Never return results from these domains. |
74
+ | `start_published_date` | `None` | Only return pages published on or after this date (`YYYY-MM-DD`). |
75
+ | `api_key` | `CONTEXTFETCH_API_KEY` | API key. |
76
+
77
+ ## Search tool
78
+
79
+ ```python
80
+ from langchain_contextfetch import ContextFetchSearch
81
+
82
+ tool = ContextFetchSearch(num_results=3)
83
+ tool.invoke({"query": "pgvector HNSW index parameters"})
84
+ ```
85
+
86
+ The model can set `query`, `include_domains`, `exclude_domains` and `start_published_date`. `num_results` (default 5, maximum 25), `content` and `api_key` are set when the tool is created. The tool's default `content` is `"pruned_context"`, so each result contributes only its query-relevant chunks to the model's context; use `content="text"` for whole pages.
87
+
88
+ The tool uses `response_format="content_and_artifact"`. Called with plain args as above, it returns the content string: each result's title, URL, published date and content. Called with a `ToolCall`, it returns a `ToolMessage` with that `content` and the raw API response as its `artifact`.
89
+
90
+ ```python
91
+ msg = tool.invoke(
92
+ {
93
+ "args": {"query": "pgvector HNSW index parameters"},
94
+ "id": "1",
95
+ "name": tool.name,
96
+ "type": "tool_call",
97
+ }
98
+ )
99
+ msg.artifact["results"][0]["url"]
100
+ ```
101
+
102
+ API errors (for example HTTP 402 when credits run out) are raised as `ToolException`. To pass the error message to the model instead, create the tool with `handle_tool_error=True`.
103
+
104
+ ### Use in an agent
105
+
106
+ ```python
107
+ from langchain.agents import create_agent
108
+
109
+ agent = create_agent(model="claude-sonnet-4-6", tools=[tool])
110
+ agent.invoke({"messages": [{"role": "user", "content": "What changed in Python 3.14?"}]})
111
+ ```
112
+
113
+ ## Async
114
+
115
+ Both classes support `ainvoke`. The underlying `contextfetch` SDK is synchronous, so async calls run in a thread executor (LangChain's default for tools and retrievers).
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ make test # unit tests, network disabled
121
+ make integration_test # live API tests; skipped unless CONTEXTFETCH_API_KEY is set
122
+ make lint
123
+ ```
124
+
125
+ The unit tests include LangChain's standard `ToolsUnitTests`. The integration tests run the standard `ToolsIntegrationTests` and `RetrieversIntegrationTests` suites, which make 10 searches in total.
@@ -0,0 +1,9 @@
1
+ langchain_contextfetch/__init__.py,sha256=PuSElyC8k3iCofeOMHmqV3jdsqA2qHN_oR3W-RfRQCw,564
2
+ langchain_contextfetch/_utilities.py,sha256=EgInXZQjgLJ7P3hYwRHqfceII7UJ7n28LkYjrYBErJU,1798
3
+ langchain_contextfetch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ langchain_contextfetch/retrievers.py,sha256=9_AV8_CUPwIxYkuL2eFD6Y5-yKyKV60IkcevVRt6eYg,4249
5
+ langchain_contextfetch/tools.py,sha256=LIJVmDeF7XjtUhRqYtBEb0vPXp5Cep9YLYiHQqUdhfo,4731
6
+ langchain_contextfetch-0.1.0.dist-info/METADATA,sha256=85wdlSXxbRBjzxMlK6kXQMRTS3De9nWiAHZTSK150Cs,4933
7
+ langchain_contextfetch-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
8
+ langchain_contextfetch-0.1.0.dist-info/licenses/LICENSE,sha256=RqQqv3BiTnGfaC6tdJ9oSSRKa0fzw4e7wxJwAR9Bab4,1073
9
+ langchain_contextfetch-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2026 ContextFetch
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
13
+ all 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
21
+ THE SOFTWARE.