keenable 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.
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .venv/
10
+ uv.lock
11
+ package-lock.json
12
+ .DS_Store
keenable-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Keenable
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,164 @@
1
+ Metadata-Version: 2.5
2
+ Name: keenable
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Keenable web search API, built for AI agents
5
+ Project-URL: Homepage, https://keenable.ai
6
+ Project-URL: Documentation, https://github.com/keenableai/keenable-sdk#readme
7
+ Project-URL: Repository, https://github.com/keenableai/keenable-sdk
8
+ Project-URL: Issues, https://github.com/keenableai/keenable-sdk/issues
9
+ Author-email: Keenable <support@keenable.ai>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,grounding,keenable,llm,rag,search,web-search
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: httpx>=0.27
25
+ Description-Content-Type: text/markdown
26
+
27
+ # keenable
28
+
29
+ Official Python SDK for [Keenable](https://keenable.ai), a web search API built
30
+ for AI agents: ranked results that come back with the page text already
31
+ extracted, plus a fetch endpoint that returns any URL as clean markdown.
32
+
33
+ **Keyless by default.** Every call works with no account and no key. An optional
34
+ `KEENABLE_API_KEY` only lifts the hourly rate limit.
35
+
36
+ ```bash
37
+ pip install keenable
38
+ ```
39
+
40
+ ## Search
41
+
42
+ ```python
43
+ from keenable import Keenable
44
+
45
+ keenable = Keenable() # or Keenable("keen_...") / set KEENABLE_API_KEY
46
+
47
+ results = keenable.search("how wafer scale chips avoid the memory bottleneck")
48
+
49
+ for result in results:
50
+ print(result.title, result.url)
51
+ print(result.snippet)
52
+ ```
53
+
54
+ Results carry the page text in `snippet`. (`description` is the page's meta
55
+ description and is empty for most pages, so build prompts from `snippet`.)
56
+
57
+ Filters are optional and combine freely:
58
+
59
+ ```python
60
+ results = keenable.search(
61
+ "post-training quantization results",
62
+ site="arxiv.org",
63
+ published_after="2026-01-01",
64
+ snippet_max_length=500,
65
+ )
66
+ ```
67
+
68
+ | Argument | What it does |
69
+ |---|---|
70
+ | `site` | Restrict to one domain, e.g. `"arxiv.org"` |
71
+ | `published_after` / `published_before` | Filter by publication date (`YYYY-MM-DD`) |
72
+ | `acquired_after` / `acquired_before` | Filter by when Keenable indexed the page |
73
+ | `snippet_max_length` | Cap the page text returned per result |
74
+ | `mode` | Search mode; `"pro"` (default) does deeper retrieval |
75
+
76
+ ## Ground a model in one line
77
+
78
+ `to_context()` renders the result set as a numbered, citable block you can drop
79
+ straight into a prompt:
80
+
81
+ ```python
82
+ context = keenable.search("cerebras inference benchmarks").to_context()
83
+
84
+ prompt = f"Answer using only these sources, citing them by number.\n\n{context}\n\nQuestion: ..."
85
+ ```
86
+
87
+ It emits `[1] Title (url)` headers followed by the page text, adds results whole
88
+ until the character budget is reached (`max_chars=12000` by default), and never
89
+ truncates a source mid-sentence.
90
+
91
+ ## Read a full page
92
+
93
+ ```python
94
+ page = keenable.fetch("https://cerebras.ai/chip")
95
+ print(page.title)
96
+ print(page.content) # markdown, boilerplate stripped
97
+ ```
98
+
99
+ ## Tool calling
100
+
101
+ The SDK ships OpenAI-compatible tool definitions, so any inference API that
102
+ speaks that schema can call Keenable directly:
103
+
104
+ ```python
105
+ from keenable import Keenable, TOOLS, run_tool_call
106
+
107
+ keenable = Keenable()
108
+ response = client.chat.completions.create(model=..., messages=messages, tools=TOOLS)
109
+
110
+ for call in response.choices[0].message.tool_calls or []:
111
+ messages.append(
112
+ {
113
+ "role": "tool",
114
+ "tool_call_id": call.id,
115
+ "content": run_tool_call(
116
+ keenable, call.function.name, call.function.arguments
117
+ ),
118
+ }
119
+ )
120
+ ```
121
+
122
+ `TOOLS` exposes `keenable_search` and `keenable_fetch`; `run_tool_call` executes
123
+ whichever the model picked and returns text ready for the `tool` message.
124
+
125
+ ## Async
126
+
127
+ `AsyncKeenable` mirrors the sync client:
128
+
129
+ ```python
130
+ import asyncio
131
+ from keenable import AsyncKeenable
132
+
133
+
134
+ async def main():
135
+ async with AsyncKeenable() as keenable:
136
+ results = await keenable.search("wafer scale engine")
137
+ print(results.to_context())
138
+
139
+
140
+ asyncio.run(main())
141
+ ```
142
+
143
+ ## Errors
144
+
145
+ All errors subclass `KeenableError`:
146
+
147
+ | Exception | When |
148
+ |---|---|
149
+ | `KeenableRateLimitError` | HTTP 429. Set `KEENABLE_API_KEY` to lift the keyless cap |
150
+ | `KeenableAuthError` | HTTP 401/403, the key was rejected |
151
+ | `KeenableAPIError` | Any other non-2xx response; carries `status_code` |
152
+ | `KeenableConnectionError` | The API could not be reached |
153
+ | `KeenableInvalidRequestError` | Bad arguments; no request was sent |
154
+
155
+ ## Configuration
156
+
157
+ | Variable | Default | Purpose |
158
+ |---|---|---|
159
+ | `KEENABLE_API_KEY` | unset | Lifts the hourly rate limit. Create one at [keenable.ai/console](https://keenable.ai/console) |
160
+ | `KEENABLE_API_URL` | `https://api.keenable.ai` | Override the API base URL |
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,138 @@
1
+ # keenable
2
+
3
+ Official Python SDK for [Keenable](https://keenable.ai), a web search API built
4
+ for AI agents: ranked results that come back with the page text already
5
+ extracted, plus a fetch endpoint that returns any URL as clean markdown.
6
+
7
+ **Keyless by default.** Every call works with no account and no key. An optional
8
+ `KEENABLE_API_KEY` only lifts the hourly rate limit.
9
+
10
+ ```bash
11
+ pip install keenable
12
+ ```
13
+
14
+ ## Search
15
+
16
+ ```python
17
+ from keenable import Keenable
18
+
19
+ keenable = Keenable() # or Keenable("keen_...") / set KEENABLE_API_KEY
20
+
21
+ results = keenable.search("how wafer scale chips avoid the memory bottleneck")
22
+
23
+ for result in results:
24
+ print(result.title, result.url)
25
+ print(result.snippet)
26
+ ```
27
+
28
+ Results carry the page text in `snippet`. (`description` is the page's meta
29
+ description and is empty for most pages, so build prompts from `snippet`.)
30
+
31
+ Filters are optional and combine freely:
32
+
33
+ ```python
34
+ results = keenable.search(
35
+ "post-training quantization results",
36
+ site="arxiv.org",
37
+ published_after="2026-01-01",
38
+ snippet_max_length=500,
39
+ )
40
+ ```
41
+
42
+ | Argument | What it does |
43
+ |---|---|
44
+ | `site` | Restrict to one domain, e.g. `"arxiv.org"` |
45
+ | `published_after` / `published_before` | Filter by publication date (`YYYY-MM-DD`) |
46
+ | `acquired_after` / `acquired_before` | Filter by when Keenable indexed the page |
47
+ | `snippet_max_length` | Cap the page text returned per result |
48
+ | `mode` | Search mode; `"pro"` (default) does deeper retrieval |
49
+
50
+ ## Ground a model in one line
51
+
52
+ `to_context()` renders the result set as a numbered, citable block you can drop
53
+ straight into a prompt:
54
+
55
+ ```python
56
+ context = keenable.search("cerebras inference benchmarks").to_context()
57
+
58
+ prompt = f"Answer using only these sources, citing them by number.\n\n{context}\n\nQuestion: ..."
59
+ ```
60
+
61
+ It emits `[1] Title (url)` headers followed by the page text, adds results whole
62
+ until the character budget is reached (`max_chars=12000` by default), and never
63
+ truncates a source mid-sentence.
64
+
65
+ ## Read a full page
66
+
67
+ ```python
68
+ page = keenable.fetch("https://cerebras.ai/chip")
69
+ print(page.title)
70
+ print(page.content) # markdown, boilerplate stripped
71
+ ```
72
+
73
+ ## Tool calling
74
+
75
+ The SDK ships OpenAI-compatible tool definitions, so any inference API that
76
+ speaks that schema can call Keenable directly:
77
+
78
+ ```python
79
+ from keenable import Keenable, TOOLS, run_tool_call
80
+
81
+ keenable = Keenable()
82
+ response = client.chat.completions.create(model=..., messages=messages, tools=TOOLS)
83
+
84
+ for call in response.choices[0].message.tool_calls or []:
85
+ messages.append(
86
+ {
87
+ "role": "tool",
88
+ "tool_call_id": call.id,
89
+ "content": run_tool_call(
90
+ keenable, call.function.name, call.function.arguments
91
+ ),
92
+ }
93
+ )
94
+ ```
95
+
96
+ `TOOLS` exposes `keenable_search` and `keenable_fetch`; `run_tool_call` executes
97
+ whichever the model picked and returns text ready for the `tool` message.
98
+
99
+ ## Async
100
+
101
+ `AsyncKeenable` mirrors the sync client:
102
+
103
+ ```python
104
+ import asyncio
105
+ from keenable import AsyncKeenable
106
+
107
+
108
+ async def main():
109
+ async with AsyncKeenable() as keenable:
110
+ results = await keenable.search("wafer scale engine")
111
+ print(results.to_context())
112
+
113
+
114
+ asyncio.run(main())
115
+ ```
116
+
117
+ ## Errors
118
+
119
+ All errors subclass `KeenableError`:
120
+
121
+ | Exception | When |
122
+ |---|---|
123
+ | `KeenableRateLimitError` | HTTP 429. Set `KEENABLE_API_KEY` to lift the keyless cap |
124
+ | `KeenableAuthError` | HTTP 401/403, the key was rejected |
125
+ | `KeenableAPIError` | Any other non-2xx response; carries `status_code` |
126
+ | `KeenableConnectionError` | The API could not be reached |
127
+ | `KeenableInvalidRequestError` | Bad arguments; no request was sent |
128
+
129
+ ## Configuration
130
+
131
+ | Variable | Default | Purpose |
132
+ |---|---|---|
133
+ | `KEENABLE_API_KEY` | unset | Lifts the hourly rate limit. Create one at [keenable.ai/console](https://keenable.ai/console) |
134
+ | `KEENABLE_API_URL` | `https://api.keenable.ai` | Override the API base URL |
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,44 @@
1
+ """Keenable: web search and page fetch for AI agents.
2
+
3
+ Keyless by default. ``Keenable()`` works with no account; an API key only lifts
4
+ the hourly rate limit.
5
+
6
+ from keenable import Keenable
7
+
8
+ keenable = Keenable()
9
+ results = keenable.search("fastest inference providers 2026")
10
+ context = results.to_context()
11
+ """
12
+
13
+ from .client import DEFAULT_BASE_URL, AsyncKeenable, Keenable
14
+ from .errors import (
15
+ KeenableAPIError,
16
+ KeenableAuthError,
17
+ KeenableConnectionError,
18
+ KeenableError,
19
+ KeenableInvalidRequestError,
20
+ KeenableRateLimitError,
21
+ )
22
+ from .models import Page, SearchResponse, SearchResult
23
+ from .tools import FETCH_TOOL, SEARCH_TOOL, TOOLS, run_tool_call
24
+
25
+ __all__ = [
26
+ "AsyncKeenable",
27
+ "DEFAULT_BASE_URL",
28
+ "FETCH_TOOL",
29
+ "Keenable",
30
+ "KeenableAPIError",
31
+ "KeenableAuthError",
32
+ "KeenableConnectionError",
33
+ "KeenableError",
34
+ "KeenableInvalidRequestError",
35
+ "KeenableRateLimitError",
36
+ "Page",
37
+ "SEARCH_TOOL",
38
+ "SearchResponse",
39
+ "SearchResult",
40
+ "TOOLS",
41
+ "run_tool_call",
42
+ ]
43
+
44
+ __version__ = "0.1.0"