compresr 2.7.9__tar.gz → 2.8.2__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.
- {compresr-2.7.9 → compresr-2.8.2}/PKG-INFO +53 -7
- {compresr-2.7.9 → compresr-2.8.2}/README.md +47 -6
- {compresr-2.7.9 → compresr-2.8.2}/compresr/__init__.py +3 -0
- compresr-2.8.2/compresr/__main__.py +150 -0
- compresr-2.8.2/compresr/agents/tools/_agentcore.py +247 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/tools/web_search.py +168 -10
- compresr-2.8.2/compresr/auth.py +449 -0
- compresr-2.8.2/compresr/credentials.py +270 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langgraph/checkpoint.py +19 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/services/compression.py +1 -1
- {compresr-2.7.9 → compresr-2.8.2}/compresr/services/proxy.py +23 -25
- {compresr-2.7.9 → compresr-2.8.2}/compresr.egg-info/PKG-INFO +53 -7
- {compresr-2.7.9 → compresr-2.8.2}/compresr.egg-info/SOURCES.txt +4 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr.egg-info/entry_points.txt +1 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr.egg-info/requires.txt +6 -0
- {compresr-2.7.9 → compresr-2.8.2}/pyproject.toml +10 -1
- {compresr-2.7.9 → compresr-2.8.2}/LICENSE +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/engine.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/facades/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/facades/anthropic.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/facades/native.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/facades/openai.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/normalized.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/research/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/research/agent.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/research/facade.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/research/parser.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/research/prompts.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/research/types.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/schemas/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/schemas/anthropic.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/schemas/openai.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/agents/tools/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/clients.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/config.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/exceptions/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/exceptions/exceptions.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/client.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/compress.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/errors.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/filters.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/kernel.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/policy.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/query.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/_shared/tokens.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langchain/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langchain/middleware.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langchain/retriever.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langchain/wrappers.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langgraph/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langgraph/handoff.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langgraph/nodes.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/langgraph/store.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/litellm/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/litellm/cli.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/litellm/defaults.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/litellm/guardrail.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/litellm/initializer.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/litellm/shim_installer.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/litellm/types.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/llamaindex/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/llamaindex/memory.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/llamaindex/postprocessor.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/integrations/llamaindex/wrappers.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/retry.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/schemas/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/schemas/base.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/schemas/inference.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/schemas/usage.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/services/__init__.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr/services/base.py +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr.egg-info/dependency_links.txt +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/compresr.egg-info/top_level.txt +0 -0
- {compresr-2.7.9 → compresr-2.8.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compresr
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.2
|
|
4
4
|
Summary: Python SDK for Compresr - Intelligent prompt compression service
|
|
5
5
|
Author-email: Compresr Team <founders@compresr.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -47,10 +47,15 @@ Provides-Extra: llamaindex
|
|
|
47
47
|
Requires-Dist: llama-index-core>=0.11; extra == "llamaindex"
|
|
48
48
|
Provides-Extra: litellm
|
|
49
49
|
Requires-Dist: litellm[proxy]>=1.79; extra == "litellm"
|
|
50
|
+
Provides-Extra: agentcore
|
|
51
|
+
Requires-Dist: mcp>=1.0; extra == "agentcore"
|
|
52
|
+
Requires-Dist: nest-asyncio>=1.6; extra == "agentcore"
|
|
50
53
|
Provides-Extra: all
|
|
51
54
|
Requires-Dist: langgraph>=0.2; extra == "all"
|
|
52
55
|
Requires-Dist: llama-index-core>=0.11; extra == "all"
|
|
53
56
|
Requires-Dist: litellm[proxy]>=1.79; extra == "all"
|
|
57
|
+
Requires-Dist: mcp>=1.0; extra == "all"
|
|
58
|
+
Requires-Dist: nest-asyncio>=1.6; extra == "all"
|
|
54
59
|
Provides-Extra: langchain
|
|
55
60
|
Provides-Extra: agents
|
|
56
61
|
Provides-Extra: agents-anthropic
|
|
@@ -92,7 +97,7 @@ print(f"Saved: {result.data.tokens_saved} tokens")
|
|
|
92
97
|
print(result.data.compressed_context)
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
The default model is `
|
|
100
|
+
The default model is `latte_v2` (query-aware). Pass any other model name your
|
|
96
101
|
account has access to via `compression_model_name="..."` — the backend
|
|
97
102
|
validates.
|
|
98
103
|
|
|
@@ -180,7 +185,7 @@ client.messages.create(model="claude-haiku-4-5", max_tokens=512,
|
|
|
180
185
|
messages=[...], tools=[...])
|
|
181
186
|
|
|
182
187
|
# OpenAI shape
|
|
183
|
-
client.chat.completions.create(model="gpt-
|
|
188
|
+
client.chat.completions.create(model="gpt-5-mini", messages=[...], tools=[...])
|
|
184
189
|
|
|
185
190
|
# Native — returns a NormalizedResult
|
|
186
191
|
client.run(prompt="...", model="claude-haiku-4-5", tools=[...], max_tokens=512)
|
|
@@ -200,6 +205,46 @@ search = WebSearchTool.tavily(
|
|
|
200
205
|
# Brave: WebSearchTool.brave(api_key=..., max_results=5)
|
|
201
206
|
```
|
|
202
207
|
|
|
208
|
+
#### Amazon Bedrock AgentCore
|
|
209
|
+
|
|
210
|
+
`WebSearchTool.agentcore(...)` reaches Amazon Bedrock AgentCore web search via a
|
|
211
|
+
Cognito OAuth handshake + an MCP streamable-HTTP session. Its runtime deps are
|
|
212
|
+
optional:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install compresr[agentcore]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Config resolves from explicit args first, then env vars (the
|
|
219
|
+
`AGENTCORE_`-prefixed name takes precedence, with the bare name as fallback):
|
|
220
|
+
|
|
221
|
+
| Field | Env var (fallback) |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `gateway_url` | `AGENTCORE_GATEWAY_MCP_URL` (`GATEWAY_MCP_URL`) |
|
|
224
|
+
| `cognito_token_url` | `AGENTCORE_COGNITO_TOKEN_URL` (`COGNITO_TOKEN_URL`) |
|
|
225
|
+
| `client_id` | `AGENTCORE_COGNITO_CLIENT_ID` (`COGNITO_CLIENT_ID`) |
|
|
226
|
+
| `client_secret` | `AGENTCORE_COGNITO_CLIENT_SECRET` (`COGNITO_CLIENT_SECRET`) |
|
|
227
|
+
| `scope` | `AGENTCORE_COGNITO_SCOPE` (`COGNITO_SCOPE`) |
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
# All five values from env vars:
|
|
231
|
+
search = WebSearchTool.agentcore(max_results=5)
|
|
232
|
+
|
|
233
|
+
# Or pass them explicitly:
|
|
234
|
+
search = WebSearchTool.agentcore(
|
|
235
|
+
gateway_url="https://...gateway.../mcp",
|
|
236
|
+
cognito_token_url="https://...oauth2/token",
|
|
237
|
+
client_id="...",
|
|
238
|
+
client_secret="...", # never logged
|
|
239
|
+
scope="gateway/invoke",
|
|
240
|
+
max_results=5, # clamped to 1–25
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The tool emitted to the LLM is named `agentcore_web_search` and returns
|
|
245
|
+
plaintext. Domain filters (`allowed_domains` / `blocked_domains`) are not
|
|
246
|
+
supported by AgentCore — use Tavily for domain filtering.
|
|
247
|
+
|
|
203
248
|
### Bring your own tool
|
|
204
249
|
|
|
205
250
|
Any `@tool`-decorated function works — its string output is compressed for you:
|
|
@@ -286,7 +331,7 @@ Tavily / Brave keys are read from `TAVILY_API_KEY` / `BRAVE_SEARCH_API_KEY`
|
|
|
286
331
|
|
|
287
332
|
| Param | Purpose |
|
|
288
333
|
|---|---|
|
|
289
|
-
| `query` | Question the LLM is trying to answer — drives `
|
|
334
|
+
| `query` | Question the LLM is trying to answer — drives `latte_v2` compression |
|
|
290
335
|
| `target_compression_ratio` | `0-1` strength (e.g. `0.5` = remove 50%) or `>1` for Nx factor (`4` = 4x). Backend max: 200 |
|
|
291
336
|
| `coarse` | `True` for paragraph-level (default, faster), `False` for token-level (fine-grained) |
|
|
292
337
|
| `heuristic_chunking` | Structure-preserving chunking |
|
|
@@ -325,6 +370,7 @@ Genuinely optional integrations beyond the agents layer:
|
|
|
325
370
|
| `compresr[langgraph]` | `langgraph` (LangGraph checkpoint serializer, store, handoff tool) |
|
|
326
371
|
| `compresr[llamaindex]` | `llama-index-core` (node postprocessor, memory block, tool wrapper) |
|
|
327
372
|
| `compresr[litellm]` | `litellm[proxy]` (LiteLLM proxy guardrail) |
|
|
373
|
+
| `compresr[agentcore]` | `mcp`, `nest-asyncio` (Amazon Bedrock AgentCore web search) |
|
|
328
374
|
| `compresr[all]` | all three above |
|
|
329
375
|
|
|
330
376
|
```bash
|
|
@@ -407,9 +453,9 @@ Minimal `proxy_config.yaml`:
|
|
|
407
453
|
|
|
408
454
|
```yaml
|
|
409
455
|
model_list:
|
|
410
|
-
- model_name: gpt-
|
|
456
|
+
- model_name: gpt-5-mini
|
|
411
457
|
litellm_params:
|
|
412
|
-
model: openai/gpt-
|
|
458
|
+
model: openai/gpt-5-mini
|
|
413
459
|
api_key: os.environ/OPENAI_API_KEY
|
|
414
460
|
|
|
415
461
|
guardrails:
|
|
@@ -437,7 +483,7 @@ Per-request override (no restart, no YAML edit):
|
|
|
437
483
|
|
|
438
484
|
```json
|
|
439
485
|
{
|
|
440
|
-
"model": "gpt-
|
|
486
|
+
"model": "gpt-5-mini",
|
|
441
487
|
"messages": [...],
|
|
442
488
|
"metadata": {
|
|
443
489
|
"guardrail_config": {
|
|
@@ -29,7 +29,7 @@ print(f"Saved: {result.data.tokens_saved} tokens")
|
|
|
29
29
|
print(result.data.compressed_context)
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The default model is `
|
|
32
|
+
The default model is `latte_v2` (query-aware). Pass any other model name your
|
|
33
33
|
account has access to via `compression_model_name="..."` — the backend
|
|
34
34
|
validates.
|
|
35
35
|
|
|
@@ -117,7 +117,7 @@ client.messages.create(model="claude-haiku-4-5", max_tokens=512,
|
|
|
117
117
|
messages=[...], tools=[...])
|
|
118
118
|
|
|
119
119
|
# OpenAI shape
|
|
120
|
-
client.chat.completions.create(model="gpt-
|
|
120
|
+
client.chat.completions.create(model="gpt-5-mini", messages=[...], tools=[...])
|
|
121
121
|
|
|
122
122
|
# Native — returns a NormalizedResult
|
|
123
123
|
client.run(prompt="...", model="claude-haiku-4-5", tools=[...], max_tokens=512)
|
|
@@ -137,6 +137,46 @@ search = WebSearchTool.tavily(
|
|
|
137
137
|
# Brave: WebSearchTool.brave(api_key=..., max_results=5)
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
#### Amazon Bedrock AgentCore
|
|
141
|
+
|
|
142
|
+
`WebSearchTool.agentcore(...)` reaches Amazon Bedrock AgentCore web search via a
|
|
143
|
+
Cognito OAuth handshake + an MCP streamable-HTTP session. Its runtime deps are
|
|
144
|
+
optional:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install compresr[agentcore]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Config resolves from explicit args first, then env vars (the
|
|
151
|
+
`AGENTCORE_`-prefixed name takes precedence, with the bare name as fallback):
|
|
152
|
+
|
|
153
|
+
| Field | Env var (fallback) |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `gateway_url` | `AGENTCORE_GATEWAY_MCP_URL` (`GATEWAY_MCP_URL`) |
|
|
156
|
+
| `cognito_token_url` | `AGENTCORE_COGNITO_TOKEN_URL` (`COGNITO_TOKEN_URL`) |
|
|
157
|
+
| `client_id` | `AGENTCORE_COGNITO_CLIENT_ID` (`COGNITO_CLIENT_ID`) |
|
|
158
|
+
| `client_secret` | `AGENTCORE_COGNITO_CLIENT_SECRET` (`COGNITO_CLIENT_SECRET`) |
|
|
159
|
+
| `scope` | `AGENTCORE_COGNITO_SCOPE` (`COGNITO_SCOPE`) |
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# All five values from env vars:
|
|
163
|
+
search = WebSearchTool.agentcore(max_results=5)
|
|
164
|
+
|
|
165
|
+
# Or pass them explicitly:
|
|
166
|
+
search = WebSearchTool.agentcore(
|
|
167
|
+
gateway_url="https://...gateway.../mcp",
|
|
168
|
+
cognito_token_url="https://...oauth2/token",
|
|
169
|
+
client_id="...",
|
|
170
|
+
client_secret="...", # never logged
|
|
171
|
+
scope="gateway/invoke",
|
|
172
|
+
max_results=5, # clamped to 1–25
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The tool emitted to the LLM is named `agentcore_web_search` and returns
|
|
177
|
+
plaintext. Domain filters (`allowed_domains` / `blocked_domains`) are not
|
|
178
|
+
supported by AgentCore — use Tavily for domain filtering.
|
|
179
|
+
|
|
140
180
|
### Bring your own tool
|
|
141
181
|
|
|
142
182
|
Any `@tool`-decorated function works — its string output is compressed for you:
|
|
@@ -223,7 +263,7 @@ Tavily / Brave keys are read from `TAVILY_API_KEY` / `BRAVE_SEARCH_API_KEY`
|
|
|
223
263
|
|
|
224
264
|
| Param | Purpose |
|
|
225
265
|
|---|---|
|
|
226
|
-
| `query` | Question the LLM is trying to answer — drives `
|
|
266
|
+
| `query` | Question the LLM is trying to answer — drives `latte_v2` compression |
|
|
227
267
|
| `target_compression_ratio` | `0-1` strength (e.g. `0.5` = remove 50%) or `>1` for Nx factor (`4` = 4x). Backend max: 200 |
|
|
228
268
|
| `coarse` | `True` for paragraph-level (default, faster), `False` for token-level (fine-grained) |
|
|
229
269
|
| `heuristic_chunking` | Structure-preserving chunking |
|
|
@@ -262,6 +302,7 @@ Genuinely optional integrations beyond the agents layer:
|
|
|
262
302
|
| `compresr[langgraph]` | `langgraph` (LangGraph checkpoint serializer, store, handoff tool) |
|
|
263
303
|
| `compresr[llamaindex]` | `llama-index-core` (node postprocessor, memory block, tool wrapper) |
|
|
264
304
|
| `compresr[litellm]` | `litellm[proxy]` (LiteLLM proxy guardrail) |
|
|
305
|
+
| `compresr[agentcore]` | `mcp`, `nest-asyncio` (Amazon Bedrock AgentCore web search) |
|
|
265
306
|
| `compresr[all]` | all three above |
|
|
266
307
|
|
|
267
308
|
```bash
|
|
@@ -344,9 +385,9 @@ Minimal `proxy_config.yaml`:
|
|
|
344
385
|
|
|
345
386
|
```yaml
|
|
346
387
|
model_list:
|
|
347
|
-
- model_name: gpt-
|
|
388
|
+
- model_name: gpt-5-mini
|
|
348
389
|
litellm_params:
|
|
349
|
-
model: openai/gpt-
|
|
390
|
+
model: openai/gpt-5-mini
|
|
350
391
|
api_key: os.environ/OPENAI_API_KEY
|
|
351
392
|
|
|
352
393
|
guardrails:
|
|
@@ -374,7 +415,7 @@ Per-request override (no restart, no YAML edit):
|
|
|
374
415
|
|
|
375
416
|
```json
|
|
376
417
|
{
|
|
377
|
-
"model": "gpt-
|
|
418
|
+
"model": "gpt-5-mini",
|
|
378
419
|
"messages": [...],
|
|
379
420
|
"metadata": {
|
|
380
421
|
"guardrail_config": {
|
|
@@ -42,6 +42,7 @@ Quick Start - Agents (opt-in provider-shape facades):
|
|
|
42
42
|
from importlib.metadata import PackageNotFoundError, version
|
|
43
43
|
from typing import Any
|
|
44
44
|
|
|
45
|
+
from .auth import login, logout
|
|
45
46
|
from .clients import CompressionClient
|
|
46
47
|
from .config import MODELS
|
|
47
48
|
from .retry import RetryConfig
|
|
@@ -56,6 +57,8 @@ __all__ = [
|
|
|
56
57
|
"CompressionClient",
|
|
57
58
|
"MODELS",
|
|
58
59
|
"RetryConfig",
|
|
60
|
+
"login",
|
|
61
|
+
"logout",
|
|
59
62
|
"WebSearchTool",
|
|
60
63
|
"CompressionPolicy",
|
|
61
64
|
"ResearchResult",
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""``compresr-sdk`` CLI: login, logout, whoami, status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import configparser
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .auth import login as _login
|
|
12
|
+
from .auth import logout as _logout
|
|
13
|
+
from .credentials import (
|
|
14
|
+
DEFAULT_PROFILE,
|
|
15
|
+
LockAcquisitionError,
|
|
16
|
+
credentials_path,
|
|
17
|
+
load,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _err(msg: str) -> None:
|
|
22
|
+
print(msg, file=sys.stderr)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _cmd_login(args: argparse.Namespace) -> int:
|
|
26
|
+
try:
|
|
27
|
+
_login(
|
|
28
|
+
app_url=args.app_url,
|
|
29
|
+
base_url=args.base_url,
|
|
30
|
+
profile=args.profile,
|
|
31
|
+
timeout=args.timeout,
|
|
32
|
+
open_browser=not args.no_browser,
|
|
33
|
+
)
|
|
34
|
+
return 0
|
|
35
|
+
except (RuntimeError, TimeoutError, ValueError) as e:
|
|
36
|
+
_err(f"error: {e}")
|
|
37
|
+
return 1
|
|
38
|
+
except PermissionError as e:
|
|
39
|
+
_err(f"error: {e}")
|
|
40
|
+
_err("hint: run `chmod 600 ~/.compresr/credentials` and retry.")
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _cmd_logout(args: argparse.Namespace) -> int:
|
|
45
|
+
try:
|
|
46
|
+
removed = _logout(
|
|
47
|
+
args.profile,
|
|
48
|
+
revoke_server_key=not args.no_server_revoke,
|
|
49
|
+
base_url=args.base_url,
|
|
50
|
+
)
|
|
51
|
+
except LockAcquisitionError as e:
|
|
52
|
+
_err(f"error: {e}")
|
|
53
|
+
return 1
|
|
54
|
+
except PermissionError as e:
|
|
55
|
+
_err(f"error: {e}")
|
|
56
|
+
_err("hint: run `chmod 600 ~/.compresr/credentials` and retry.")
|
|
57
|
+
return 1
|
|
58
|
+
if removed:
|
|
59
|
+
print(f"Removed credentials for profile '{args.profile}'.")
|
|
60
|
+
return 0
|
|
61
|
+
print(f"No credentials found for profile '{args.profile}'.")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _cmd_whoami(args: argparse.Namespace) -> int:
|
|
66
|
+
try:
|
|
67
|
+
key = load(args.profile)
|
|
68
|
+
except PermissionError as e:
|
|
69
|
+
_err(f"error: {e}")
|
|
70
|
+
return 1
|
|
71
|
+
if not key:
|
|
72
|
+
_err(f"Not logged in (profile '{args.profile}'). Run `compresr-sdk login`.")
|
|
73
|
+
return 1
|
|
74
|
+
print(f"profile: {args.profile}")
|
|
75
|
+
print(f"file: {credentials_path()}")
|
|
76
|
+
print(f"api_key: {key[:8]}…{key[-4:]}")
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cmd_status(_args: argparse.Namespace) -> int:
|
|
81
|
+
from .credentials import _read_file
|
|
82
|
+
|
|
83
|
+
path = credentials_path()
|
|
84
|
+
print(f"credentials file: {path}")
|
|
85
|
+
print(f"exists: {path.exists()}")
|
|
86
|
+
if not path.exists():
|
|
87
|
+
return 0
|
|
88
|
+
try:
|
|
89
|
+
parser = _read_file()
|
|
90
|
+
except PermissionError as e:
|
|
91
|
+
_err(f"error: {e}")
|
|
92
|
+
return 1
|
|
93
|
+
except configparser.Error as e:
|
|
94
|
+
_err(f"warning: file is corrupt ({e})")
|
|
95
|
+
return 0
|
|
96
|
+
profiles = parser.sections()
|
|
97
|
+
print(f"profiles: {', '.join(profiles) if profiles else '(none)'}")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
102
|
+
p = argparse.ArgumentParser(prog="compresr-sdk", description="Compresr SDK CLI")
|
|
103
|
+
p.add_argument("--version", action="version", version=f"compresr-sdk {__version__}")
|
|
104
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
105
|
+
|
|
106
|
+
login_p = sub.add_parser("login", help="Log in via browser and store credentials")
|
|
107
|
+
login_p.add_argument("--profile", default=DEFAULT_PROFILE)
|
|
108
|
+
login_p.add_argument("--app-url", default=None, help="Override web app URL")
|
|
109
|
+
login_p.add_argument("--base-url", default=None, help="Override API base URL")
|
|
110
|
+
login_p.add_argument("--timeout", type=float, default=120.0)
|
|
111
|
+
login_p.add_argument(
|
|
112
|
+
"--no-browser", action="store_true", help="Print the URL instead of opening it"
|
|
113
|
+
)
|
|
114
|
+
login_p.set_defaults(func=_cmd_login)
|
|
115
|
+
|
|
116
|
+
logout_p = sub.add_parser(
|
|
117
|
+
"logout",
|
|
118
|
+
help="Revoke the server-side CLI key and remove stored credentials",
|
|
119
|
+
)
|
|
120
|
+
logout_p.add_argument("--profile", default=DEFAULT_PROFILE)
|
|
121
|
+
logout_p.add_argument("--base-url", default=None, help="Override API base URL")
|
|
122
|
+
logout_p.add_argument(
|
|
123
|
+
"--no-server-revoke",
|
|
124
|
+
action="store_true",
|
|
125
|
+
help="Skip the server-side key revoke and only delete local credentials.",
|
|
126
|
+
)
|
|
127
|
+
logout_p.set_defaults(func=_cmd_logout)
|
|
128
|
+
|
|
129
|
+
whoami_p = sub.add_parser("whoami", help="Show the stored API key preview")
|
|
130
|
+
whoami_p.add_argument("--profile", default=DEFAULT_PROFILE)
|
|
131
|
+
whoami_p.set_defaults(func=_cmd_whoami)
|
|
132
|
+
|
|
133
|
+
status_p = sub.add_parser("status", help="Show credentials file location and profiles")
|
|
134
|
+
status_p.set_defaults(func=_cmd_status)
|
|
135
|
+
|
|
136
|
+
return p
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main(argv: Optional[list] = None) -> int:
|
|
140
|
+
parser = _build_parser()
|
|
141
|
+
args = parser.parse_args(argv)
|
|
142
|
+
try:
|
|
143
|
+
return int(args.func(args))
|
|
144
|
+
except KeyboardInterrupt:
|
|
145
|
+
_err("\nAborted.")
|
|
146
|
+
return 130
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""AgentCore Web Search client (Amazon Bedrock AgentCore over MCP).
|
|
2
|
+
|
|
3
|
+
Ported from the standalone ``websearch_client.py`` reference. Wraps the
|
|
4
|
+
Cognito client-credentials OAuth handshake plus an MCP streamable-HTTP session
|
|
5
|
+
into a synchronous :meth:`AgentCoreClient.search` call that works in plain
|
|
6
|
+
scripts and in Jupyter (handles the already-running event-loop case).
|
|
7
|
+
|
|
8
|
+
The runtime dependencies (``mcp``, ``nest-asyncio``) are **optional** — they are
|
|
9
|
+
imported lazily inside the call so a bare ``pip install compresr`` still imports
|
|
10
|
+
fine. The error only surfaces when ``WebSearchTool.agentcore(...)`` is actually
|
|
11
|
+
invoked without the extra installed::
|
|
12
|
+
|
|
13
|
+
pip install compresr[agentcore]
|
|
14
|
+
|
|
15
|
+
Never logs the Cognito client secret or the bearer token.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import threading
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
import httpx
|
|
27
|
+
|
|
28
|
+
# Guard against an unbounded/misbehaving gateway response before buffering it
|
|
29
|
+
# in memory and parsing as JSON.
|
|
30
|
+
MAX_RESPONSE_BYTES = 1_000_000
|
|
31
|
+
|
|
32
|
+
# The gateway also exposes this built-in *tool*-search (semantic search over the
|
|
33
|
+
# gateway's own tools). It is NOT web search and must be ignored.
|
|
34
|
+
TOOL_SEARCH_META = "x_amz_bedrock_agentcore_search"
|
|
35
|
+
|
|
36
|
+
TOKEN_TIMEOUT_SECONDS = 30
|
|
37
|
+
|
|
38
|
+
# AgentCore web search accepts maxResults in the inclusive range 1..25.
|
|
39
|
+
MIN_RESULTS = 1
|
|
40
|
+
MAX_RESULTS = 25
|
|
41
|
+
|
|
42
|
+
_MISSING_DEP_MSG = (
|
|
43
|
+
"WebSearchTool.agentcore requires the 'agentcore' extra. "
|
|
44
|
+
"Install: pip install compresr[agentcore]"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class AgentCoreConfig:
|
|
50
|
+
"""Immutable connection config for the AgentCore web-search gateway."""
|
|
51
|
+
|
|
52
|
+
gateway_url: str
|
|
53
|
+
cognito_token_url: str
|
|
54
|
+
client_id: str
|
|
55
|
+
client_secret: str
|
|
56
|
+
scope: str
|
|
57
|
+
max_results: int = 5
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _clamp_results(value: int) -> int:
|
|
61
|
+
"""Clamp a requested result count to AgentCore's inclusive 1..25 range."""
|
|
62
|
+
return max(MIN_RESULTS, min(MAX_RESULTS, value))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run(coro: Any) -> Any:
|
|
66
|
+
"""Run a coroutine whether or not an event loop is already running (Jupyter)."""
|
|
67
|
+
try:
|
|
68
|
+
loop = asyncio.get_running_loop()
|
|
69
|
+
except RuntimeError:
|
|
70
|
+
return asyncio.run(coro)
|
|
71
|
+
# Inside a running loop (e.g. Jupyter): allow nested execution.
|
|
72
|
+
try:
|
|
73
|
+
import nest_asyncio
|
|
74
|
+
except ImportError as exc: # pragma: no cover - exercised via search()
|
|
75
|
+
raise ImportError(_MISSING_DEP_MSG) from exc
|
|
76
|
+
|
|
77
|
+
nest_asyncio.apply()
|
|
78
|
+
return loop.run_until_complete(coro)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AgentCoreClient:
|
|
82
|
+
"""Synchronous client for AgentCore web search over MCP.
|
|
83
|
+
|
|
84
|
+
Mints (and caches) a Cognito bearer token, then opens an MCP
|
|
85
|
+
streamable-HTTP session to call the gateway's web-search tool. A single
|
|
86
|
+
instance is shared by the built tool so the token is reused across calls.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, config: AgentCoreConfig) -> None:
|
|
90
|
+
self._config = config
|
|
91
|
+
self._token: Optional[str] = None
|
|
92
|
+
self._token_lock = threading.Lock()
|
|
93
|
+
|
|
94
|
+
# --- auth ----------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def token(self, refresh: bool = False) -> str:
|
|
97
|
+
"""Return a cached bearer token, minting a fresh one when needed.
|
|
98
|
+
|
|
99
|
+
Guarded by a lock (double-checked locking) so concurrent first-use
|
|
100
|
+
calls from multiple threads only mint one token. The client secret
|
|
101
|
+
and the resulting token are never logged.
|
|
102
|
+
"""
|
|
103
|
+
if self._token and not refresh:
|
|
104
|
+
return self._token
|
|
105
|
+
with self._token_lock:
|
|
106
|
+
# Re-check after acquiring the lock: another thread may have
|
|
107
|
+
# already minted/refreshed the token while we were waiting.
|
|
108
|
+
if self._token and not refresh:
|
|
109
|
+
return self._token
|
|
110
|
+
try:
|
|
111
|
+
resp = httpx.post(
|
|
112
|
+
self._config.cognito_token_url,
|
|
113
|
+
data={
|
|
114
|
+
"grant_type": "client_credentials",
|
|
115
|
+
"client_id": self._config.client_id,
|
|
116
|
+
"client_secret": self._config.client_secret,
|
|
117
|
+
"scope": self._config.scope,
|
|
118
|
+
},
|
|
119
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
120
|
+
timeout=TOKEN_TIMEOUT_SECONDS,
|
|
121
|
+
)
|
|
122
|
+
resp.raise_for_status()
|
|
123
|
+
self._token = resp.json()["access_token"]
|
|
124
|
+
except httpx.HTTPError as exc:
|
|
125
|
+
# Re-raise sanitized: the original exception's .request (and
|
|
126
|
+
# this frame's locals) carry the client_secret, which we do
|
|
127
|
+
# not want chained into error-monitoring tools (e.g. Sentry).
|
|
128
|
+
status = getattr(getattr(exc, "response", None), "status_code", None)
|
|
129
|
+
detail = f" (HTTP {status})" if status is not None else ""
|
|
130
|
+
raise RuntimeError(f"AgentCore Cognito token request failed{detail}.") from None
|
|
131
|
+
return self._token
|
|
132
|
+
|
|
133
|
+
# --- tool selection ------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _pick_web_search_tool(names: list[str]) -> str:
|
|
137
|
+
"""Pick the web-search tool, ignoring the semantic tool-search meta tool."""
|
|
138
|
+
candidates = [n for n in names if n != TOOL_SEARCH_META]
|
|
139
|
+
for name in candidates:
|
|
140
|
+
if "websearch" in name.lower().replace("-", "").replace("_", ""):
|
|
141
|
+
return name
|
|
142
|
+
if not candidates:
|
|
143
|
+
raise RuntimeError(f"No web-search tool found among {names}")
|
|
144
|
+
return candidates[0]
|
|
145
|
+
|
|
146
|
+
# --- search --------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
async def _search_async(self, query: str, max_results: int, *, refresh: bool) -> dict:
|
|
149
|
+
# Lazy import so a bare install imports fine; only fails on actual use.
|
|
150
|
+
try:
|
|
151
|
+
from mcp import ClientSession
|
|
152
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
153
|
+
except ImportError as exc:
|
|
154
|
+
raise ImportError(_MISSING_DEP_MSG) from exc
|
|
155
|
+
|
|
156
|
+
headers = {"Authorization": f"Bearer {self.token(refresh=refresh)}"}
|
|
157
|
+
async with streamablehttp_client(self._config.gateway_url, headers=headers) as (
|
|
158
|
+
read,
|
|
159
|
+
write,
|
|
160
|
+
_,
|
|
161
|
+
):
|
|
162
|
+
async with ClientSession(read, write) as session:
|
|
163
|
+
await session.initialize()
|
|
164
|
+
names = [t.name for t in (await session.list_tools()).tools]
|
|
165
|
+
tool = self._pick_web_search_tool(names)
|
|
166
|
+
result = await session.call_tool(tool, {"query": query, "maxResults": max_results})
|
|
167
|
+
return _parse_tool_result(result)
|
|
168
|
+
|
|
169
|
+
def search(self, query: str, max_results: Optional[int] = None) -> list[dict]:
|
|
170
|
+
"""Run a web search and return normalized result dicts.
|
|
171
|
+
|
|
172
|
+
Each result is ``{"title", "url", "content", "published_date"}`` — the
|
|
173
|
+
``content`` key matches the SDK's ``_flatten_search_results`` flattener.
|
|
174
|
+
Re-mints the bearer token once on an MCP 401 and retries.
|
|
175
|
+
"""
|
|
176
|
+
count = _clamp_results(self._config.max_results if max_results is None else max_results)
|
|
177
|
+
try:
|
|
178
|
+
payload = _run(self._search_async(query, count, refresh=False))
|
|
179
|
+
except Exception as exc: # noqa: BLE001 - inspect for a 401 to retry once
|
|
180
|
+
if _is_unauthorized(exc):
|
|
181
|
+
payload = _run(self._search_async(query, count, refresh=True))
|
|
182
|
+
else:
|
|
183
|
+
raise
|
|
184
|
+
results = payload.get("results", [])
|
|
185
|
+
if not isinstance(results, list):
|
|
186
|
+
raise RuntimeError(
|
|
187
|
+
"AgentCore web-search returned an unexpected 'results' shape "
|
|
188
|
+
f"(expected a list, got {type(results).__name__})."
|
|
189
|
+
)
|
|
190
|
+
out: list[dict] = []
|
|
191
|
+
for item in results:
|
|
192
|
+
if not isinstance(item, dict):
|
|
193
|
+
continue
|
|
194
|
+
out.append(
|
|
195
|
+
{
|
|
196
|
+
"title": item.get("title"),
|
|
197
|
+
"url": item.get("url"),
|
|
198
|
+
"content": item.get("text", "") or "",
|
|
199
|
+
"published_date": item.get("publishedDate"),
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
return out
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _parse_tool_result(result: Any) -> dict:
|
|
206
|
+
"""Validate and parse an MCP ``CallToolResult`` into a JSON dict.
|
|
207
|
+
|
|
208
|
+
Defensive against everything a misbehaving/compromised gateway could
|
|
209
|
+
return: a tool-level error (``isError=True`` with plain-text content,
|
|
210
|
+
NOT JSON), an oversized body, malformed JSON, or a JSON value that isn't
|
|
211
|
+
an object. Raises ``RuntimeError`` with a clear, secret-free message in
|
|
212
|
+
every case instead of letting a bare ``JSONDecodeError``/``AttributeError``
|
|
213
|
+
propagate.
|
|
214
|
+
"""
|
|
215
|
+
text = "".join(getattr(b, "text", "") for b in result.content)
|
|
216
|
+
if getattr(result, "isError", False):
|
|
217
|
+
# Gateway tool-level errors (bad query, throttling, auth/scope error)
|
|
218
|
+
# come back as plain text, NOT JSON. Surface them directly instead of
|
|
219
|
+
# failing on json.loads with a confusing JSONDecodeError. The text may
|
|
220
|
+
# itself contain "401" — keep it intact so the unauthorized-retry
|
|
221
|
+
# heuristic in _is_unauthorized still matches.
|
|
222
|
+
raise RuntimeError(f"AgentCore web-search tool error: {text}")
|
|
223
|
+
if len(text.encode("utf-8", errors="ignore")) > MAX_RESPONSE_BYTES:
|
|
224
|
+
raise RuntimeError(
|
|
225
|
+
f"AgentCore web-search response exceeded the {MAX_RESPONSE_BYTES} byte size guard."
|
|
226
|
+
)
|
|
227
|
+
try:
|
|
228
|
+
payload = json.loads(text)
|
|
229
|
+
except json.JSONDecodeError as exc:
|
|
230
|
+
raise RuntimeError(f"AgentCore web-search returned malformed JSON: {exc}") from None
|
|
231
|
+
if not isinstance(payload, dict):
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
"AgentCore web-search returned an unexpected JSON shape "
|
|
234
|
+
f"(expected an object, got {type(payload).__name__})."
|
|
235
|
+
)
|
|
236
|
+
return payload
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _is_unauthorized(exc: BaseException) -> bool:
|
|
240
|
+
"""Best-effort detection of an MCP/HTTP 401 to trigger a single token re-mint."""
|
|
241
|
+
status = getattr(getattr(exc, "response", None), "status_code", None)
|
|
242
|
+
if status == 401:
|
|
243
|
+
return True
|
|
244
|
+
return "401" in str(exc)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
__all__ = ["AgentCoreConfig", "AgentCoreClient"]
|