headroom-ai 0.2.13__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.
- headroom/__init__.py +212 -0
- headroom/cache/__init__.py +76 -0
- headroom/cache/anthropic.py +517 -0
- headroom/cache/base.py +342 -0
- headroom/cache/compression_feedback.py +613 -0
- headroom/cache/compression_store.py +814 -0
- headroom/cache/dynamic_detector.py +1026 -0
- headroom/cache/google.py +884 -0
- headroom/cache/openai.py +584 -0
- headroom/cache/registry.py +175 -0
- headroom/cache/semantic.py +451 -0
- headroom/ccr/__init__.py +77 -0
- headroom/ccr/context_tracker.py +582 -0
- headroom/ccr/mcp_server.py +319 -0
- headroom/ccr/response_handler.py +772 -0
- headroom/ccr/tool_injection.py +415 -0
- headroom/cli.py +219 -0
- headroom/client.py +977 -0
- headroom/compression/__init__.py +42 -0
- headroom/compression/detector.py +424 -0
- headroom/compression/handlers/__init__.py +22 -0
- headroom/compression/handlers/base.py +219 -0
- headroom/compression/handlers/code_handler.py +506 -0
- headroom/compression/handlers/json_handler.py +418 -0
- headroom/compression/masks.py +345 -0
- headroom/compression/universal.py +465 -0
- headroom/config.py +474 -0
- headroom/exceptions.py +192 -0
- headroom/integrations/__init__.py +159 -0
- headroom/integrations/agno/__init__.py +53 -0
- headroom/integrations/agno/hooks.py +345 -0
- headroom/integrations/agno/model.py +625 -0
- headroom/integrations/agno/providers.py +154 -0
- headroom/integrations/langchain/__init__.py +106 -0
- headroom/integrations/langchain/agents.py +326 -0
- headroom/integrations/langchain/chat_model.py +1002 -0
- headroom/integrations/langchain/langsmith.py +324 -0
- headroom/integrations/langchain/memory.py +319 -0
- headroom/integrations/langchain/providers.py +200 -0
- headroom/integrations/langchain/retriever.py +371 -0
- headroom/integrations/langchain/streaming.py +341 -0
- headroom/integrations/mcp/__init__.py +37 -0
- headroom/integrations/mcp/server.py +533 -0
- headroom/memory/__init__.py +37 -0
- headroom/memory/extractor.py +390 -0
- headroom/memory/fast_store.py +621 -0
- headroom/memory/fast_wrapper.py +311 -0
- headroom/memory/inline_extractor.py +229 -0
- headroom/memory/store.py +434 -0
- headroom/memory/worker.py +260 -0
- headroom/memory/wrapper.py +321 -0
- headroom/models/__init__.py +39 -0
- headroom/models/registry.py +687 -0
- headroom/parser.py +293 -0
- headroom/pricing/__init__.py +51 -0
- headroom/pricing/anthropic_prices.py +81 -0
- headroom/pricing/litellm_pricing.py +113 -0
- headroom/pricing/openai_prices.py +91 -0
- headroom/pricing/registry.py +188 -0
- headroom/providers/__init__.py +61 -0
- headroom/providers/anthropic.py +621 -0
- headroom/providers/base.py +131 -0
- headroom/providers/cohere.py +362 -0
- headroom/providers/google.py +427 -0
- headroom/providers/litellm.py +297 -0
- headroom/providers/openai.py +566 -0
- headroom/providers/openai_compatible.py +521 -0
- headroom/proxy/__init__.py +19 -0
- headroom/proxy/server.py +2683 -0
- headroom/py.typed +0 -0
- headroom/relevance/__init__.py +124 -0
- headroom/relevance/base.py +106 -0
- headroom/relevance/bm25.py +255 -0
- headroom/relevance/embedding.py +255 -0
- headroom/relevance/hybrid.py +259 -0
- headroom/reporting/__init__.py +5 -0
- headroom/reporting/generator.py +549 -0
- headroom/storage/__init__.py +41 -0
- headroom/storage/base.py +125 -0
- headroom/storage/jsonl.py +220 -0
- headroom/storage/sqlite.py +289 -0
- headroom/telemetry/__init__.py +91 -0
- headroom/telemetry/collector.py +764 -0
- headroom/telemetry/models.py +880 -0
- headroom/telemetry/toin.py +1579 -0
- headroom/tokenizer.py +80 -0
- headroom/tokenizers/__init__.py +75 -0
- headroom/tokenizers/base.py +210 -0
- headroom/tokenizers/estimator.py +198 -0
- headroom/tokenizers/huggingface.py +317 -0
- headroom/tokenizers/mistral.py +245 -0
- headroom/tokenizers/registry.py +398 -0
- headroom/tokenizers/tiktoken_counter.py +248 -0
- headroom/transforms/__init__.py +106 -0
- headroom/transforms/base.py +57 -0
- headroom/transforms/cache_aligner.py +357 -0
- headroom/transforms/code_compressor.py +1313 -0
- headroom/transforms/content_detector.py +335 -0
- headroom/transforms/content_router.py +1158 -0
- headroom/transforms/llmlingua_compressor.py +638 -0
- headroom/transforms/log_compressor.py +529 -0
- headroom/transforms/pipeline.py +297 -0
- headroom/transforms/rolling_window.py +350 -0
- headroom/transforms/search_compressor.py +365 -0
- headroom/transforms/smart_crusher.py +2682 -0
- headroom/transforms/text_compressor.py +259 -0
- headroom/transforms/tool_crusher.py +338 -0
- headroom/utils.py +215 -0
- headroom_ai-0.2.13.dist-info/METADATA +315 -0
- headroom_ai-0.2.13.dist-info/RECORD +114 -0
- headroom_ai-0.2.13.dist-info/WHEEL +4 -0
- headroom_ai-0.2.13.dist-info/entry_points.txt +2 -0
- headroom_ai-0.2.13.dist-info/licenses/LICENSE +190 -0
- headroom_ai-0.2.13.dist-info/licenses/NOTICE +43 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""CCR MCP Server - Exposes headroom_retrieve as an MCP tool.
|
|
2
|
+
|
|
3
|
+
This MCP server allows LLMs to retrieve compressed content via MCP instead
|
|
4
|
+
of through injected tool definitions. It connects to the Headroom proxy's
|
|
5
|
+
CompressionStore to serve retrieval requests.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# As standalone server (stdio transport)
|
|
9
|
+
python -m headroom.ccr.mcp_server
|
|
10
|
+
|
|
11
|
+
# With custom proxy URL
|
|
12
|
+
python -m headroom.ccr.mcp_server --proxy-url http://localhost:8787
|
|
13
|
+
|
|
14
|
+
# Add to Claude Code's MCP config (~/.claude/mcp.json):
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"headroom": {
|
|
18
|
+
"command": "python",
|
|
19
|
+
"args": ["-m", "headroom.ccr.mcp_server"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
When MCP is configured, the proxy will detect the tool is already present
|
|
25
|
+
and skip tool injection, avoiding duplicate tools.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import asyncio
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import os
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
# Try to import MCP SDK
|
|
38
|
+
try:
|
|
39
|
+
from mcp.server import Server
|
|
40
|
+
from mcp.server.stdio import stdio_server
|
|
41
|
+
from mcp.types import TextContent, Tool
|
|
42
|
+
|
|
43
|
+
MCP_AVAILABLE = True
|
|
44
|
+
except ImportError:
|
|
45
|
+
MCP_AVAILABLE = False
|
|
46
|
+
Server = None
|
|
47
|
+
stdio_server = None
|
|
48
|
+
|
|
49
|
+
# Try to import httpx for proxy communication
|
|
50
|
+
try:
|
|
51
|
+
import httpx
|
|
52
|
+
|
|
53
|
+
HTTPX_AVAILABLE = True
|
|
54
|
+
except ImportError:
|
|
55
|
+
HTTPX_AVAILABLE = False
|
|
56
|
+
httpx = None # type: ignore[assignment]
|
|
57
|
+
|
|
58
|
+
from .tool_injection import CCR_TOOL_NAME
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger("headroom.ccr.mcp")
|
|
61
|
+
|
|
62
|
+
# Default proxy URL (can be overridden via env or args)
|
|
63
|
+
DEFAULT_PROXY_URL = os.environ.get("HEADROOM_PROXY_URL", "http://127.0.0.1:8787")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CCRMCPServer:
|
|
67
|
+
"""MCP Server that exposes headroom_retrieve tool.
|
|
68
|
+
|
|
69
|
+
This server can operate in two modes:
|
|
70
|
+
1. HTTP mode: Calls the proxy's /v1/retrieve endpoint (default)
|
|
71
|
+
2. Direct mode: Uses CompressionStore directly (same process)
|
|
72
|
+
|
|
73
|
+
HTTP mode is recommended as it ensures consistency with the proxy.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
proxy_url: str = DEFAULT_PROXY_URL,
|
|
79
|
+
direct_mode: bool = False,
|
|
80
|
+
):
|
|
81
|
+
"""Initialize CCR MCP Server.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
proxy_url: URL of the Headroom proxy server.
|
|
85
|
+
direct_mode: If True, access CompressionStore directly instead of via HTTP.
|
|
86
|
+
"""
|
|
87
|
+
self.proxy_url = proxy_url
|
|
88
|
+
self.direct_mode = direct_mode
|
|
89
|
+
self._http_client: httpx.AsyncClient | None = None
|
|
90
|
+
|
|
91
|
+
if not MCP_AVAILABLE:
|
|
92
|
+
raise ImportError("MCP SDK not installed. Install with: pip install mcp")
|
|
93
|
+
|
|
94
|
+
if not direct_mode and not HTTPX_AVAILABLE:
|
|
95
|
+
raise ImportError(
|
|
96
|
+
"httpx not installed (required for HTTP mode). Install with: pip install httpx"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.server = Server("headroom-ccr")
|
|
100
|
+
self._setup_handlers()
|
|
101
|
+
|
|
102
|
+
def _setup_handlers(self):
|
|
103
|
+
"""Set up MCP tool handlers."""
|
|
104
|
+
|
|
105
|
+
@self.server.list_tools()
|
|
106
|
+
async def list_tools() -> list[Tool]:
|
|
107
|
+
"""Return available tools."""
|
|
108
|
+
return [
|
|
109
|
+
Tool(
|
|
110
|
+
name=CCR_TOOL_NAME,
|
|
111
|
+
description=(
|
|
112
|
+
"Retrieve original uncompressed content that was compressed "
|
|
113
|
+
"to save tokens. Use this when you need more data than what's "
|
|
114
|
+
"shown in compressed tool results. The hash is provided in "
|
|
115
|
+
"compression markers like [N items compressed... hash=abc123]."
|
|
116
|
+
),
|
|
117
|
+
inputSchema={
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"hash": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"description": "Hash key from the compression marker (e.g., 'abc123' from hash=abc123)",
|
|
123
|
+
},
|
|
124
|
+
"query": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": (
|
|
127
|
+
"Optional search query to filter results. "
|
|
128
|
+
"If provided, only returns items matching the query. "
|
|
129
|
+
"If omitted, returns all original items."
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"required": ["hash"],
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
@self.server.call_tool()
|
|
139
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
140
|
+
"""Handle tool calls."""
|
|
141
|
+
if name != CCR_TOOL_NAME:
|
|
142
|
+
return [
|
|
143
|
+
TextContent(
|
|
144
|
+
type="text",
|
|
145
|
+
text=json.dumps({"error": f"Unknown tool: {name}"}),
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
hash_key = arguments.get("hash")
|
|
150
|
+
query = arguments.get("query")
|
|
151
|
+
|
|
152
|
+
if not hash_key:
|
|
153
|
+
return [
|
|
154
|
+
TextContent(
|
|
155
|
+
type="text",
|
|
156
|
+
text=json.dumps({"error": "hash parameter is required"}),
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Retrieve content
|
|
161
|
+
try:
|
|
162
|
+
if self.direct_mode:
|
|
163
|
+
result = await self._retrieve_direct(hash_key, query)
|
|
164
|
+
else:
|
|
165
|
+
result = await self._retrieve_via_proxy(hash_key, query)
|
|
166
|
+
|
|
167
|
+
return [
|
|
168
|
+
TextContent(
|
|
169
|
+
type="text",
|
|
170
|
+
text=json.dumps(result, indent=2),
|
|
171
|
+
)
|
|
172
|
+
]
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Retrieval failed: {e}")
|
|
175
|
+
return [
|
|
176
|
+
TextContent(
|
|
177
|
+
type="text",
|
|
178
|
+
text=json.dumps({"error": str(e)}),
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
async def _retrieve_via_proxy(
|
|
183
|
+
self,
|
|
184
|
+
hash_key: str,
|
|
185
|
+
query: str | None,
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
"""Retrieve content via proxy's HTTP endpoint."""
|
|
188
|
+
if self._http_client is None:
|
|
189
|
+
self._http_client = httpx.AsyncClient(timeout=30.0)
|
|
190
|
+
|
|
191
|
+
url = f"{self.proxy_url}/v1/retrieve"
|
|
192
|
+
payload = {"hash": hash_key}
|
|
193
|
+
if query:
|
|
194
|
+
payload["query"] = query
|
|
195
|
+
|
|
196
|
+
response = await self._http_client.post(url, json=payload)
|
|
197
|
+
|
|
198
|
+
if response.status_code == 404:
|
|
199
|
+
return {
|
|
200
|
+
"error": "Entry not found or expired (TTL: 5 minutes)",
|
|
201
|
+
"hash": hash_key,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
response.raise_for_status()
|
|
205
|
+
result: dict[str, Any] = response.json()
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
async def _retrieve_direct(
|
|
209
|
+
self,
|
|
210
|
+
hash_key: str,
|
|
211
|
+
query: str | None,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
"""Retrieve content directly from CompressionStore."""
|
|
214
|
+
from headroom.cache.compression_store import get_compression_store
|
|
215
|
+
|
|
216
|
+
store = get_compression_store()
|
|
217
|
+
|
|
218
|
+
if query:
|
|
219
|
+
results = store.search(hash_key, query)
|
|
220
|
+
return {
|
|
221
|
+
"hash": hash_key,
|
|
222
|
+
"query": query,
|
|
223
|
+
"results": results,
|
|
224
|
+
"count": len(results),
|
|
225
|
+
}
|
|
226
|
+
else:
|
|
227
|
+
entry = store.retrieve(hash_key)
|
|
228
|
+
if entry:
|
|
229
|
+
return {
|
|
230
|
+
"hash": hash_key,
|
|
231
|
+
"original_content": entry.original_content,
|
|
232
|
+
"original_item_count": entry.original_item_count,
|
|
233
|
+
"compressed_item_count": entry.compressed_item_count,
|
|
234
|
+
"retrieval_count": entry.retrieval_count,
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
"error": "Entry not found or expired (TTL: 5 minutes)",
|
|
238
|
+
"hash": hash_key,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async def run_stdio(self):
|
|
242
|
+
"""Run the server with stdio transport."""
|
|
243
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
244
|
+
logger.info(f"CCR MCP Server starting (proxy: {self.proxy_url})")
|
|
245
|
+
await self.server.run(
|
|
246
|
+
read_stream,
|
|
247
|
+
write_stream,
|
|
248
|
+
self.server.create_initialization_options(),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
async def cleanup(self):
|
|
252
|
+
"""Clean up resources."""
|
|
253
|
+
if self._http_client:
|
|
254
|
+
await self._http_client.aclose()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def create_ccr_mcp_server(
|
|
258
|
+
proxy_url: str = DEFAULT_PROXY_URL,
|
|
259
|
+
direct_mode: bool = False,
|
|
260
|
+
) -> CCRMCPServer:
|
|
261
|
+
"""Create a CCR MCP server instance.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
proxy_url: URL of the Headroom proxy server.
|
|
265
|
+
direct_mode: If True, access CompressionStore directly.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
CCRMCPServer instance.
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
```python
|
|
272
|
+
server = create_ccr_mcp_server()
|
|
273
|
+
await server.run_stdio()
|
|
274
|
+
```
|
|
275
|
+
"""
|
|
276
|
+
return CCRMCPServer(proxy_url=proxy_url, direct_mode=direct_mode)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def main():
|
|
280
|
+
"""Run the CCR MCP server."""
|
|
281
|
+
parser = argparse.ArgumentParser(
|
|
282
|
+
description="CCR MCP Server - Retrieve compressed content via MCP"
|
|
283
|
+
)
|
|
284
|
+
parser.add_argument(
|
|
285
|
+
"--proxy-url",
|
|
286
|
+
default=DEFAULT_PROXY_URL,
|
|
287
|
+
help=f"Headroom proxy URL (default: {DEFAULT_PROXY_URL})",
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--direct",
|
|
291
|
+
action="store_true",
|
|
292
|
+
help="Use direct CompressionStore access instead of HTTP",
|
|
293
|
+
)
|
|
294
|
+
parser.add_argument(
|
|
295
|
+
"--debug",
|
|
296
|
+
action="store_true",
|
|
297
|
+
help="Enable debug logging",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
args = parser.parse_args()
|
|
301
|
+
|
|
302
|
+
if args.debug:
|
|
303
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
304
|
+
else:
|
|
305
|
+
logging.basicConfig(level=logging.INFO)
|
|
306
|
+
|
|
307
|
+
server = create_ccr_mcp_server(
|
|
308
|
+
proxy_url=args.proxy_url,
|
|
309
|
+
direct_mode=args.direct,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
await server.run_stdio()
|
|
314
|
+
finally:
|
|
315
|
+
await server.cleanup()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
asyncio.run(main())
|