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.
Files changed (114) hide show
  1. headroom/__init__.py +212 -0
  2. headroom/cache/__init__.py +76 -0
  3. headroom/cache/anthropic.py +517 -0
  4. headroom/cache/base.py +342 -0
  5. headroom/cache/compression_feedback.py +613 -0
  6. headroom/cache/compression_store.py +814 -0
  7. headroom/cache/dynamic_detector.py +1026 -0
  8. headroom/cache/google.py +884 -0
  9. headroom/cache/openai.py +584 -0
  10. headroom/cache/registry.py +175 -0
  11. headroom/cache/semantic.py +451 -0
  12. headroom/ccr/__init__.py +77 -0
  13. headroom/ccr/context_tracker.py +582 -0
  14. headroom/ccr/mcp_server.py +319 -0
  15. headroom/ccr/response_handler.py +772 -0
  16. headroom/ccr/tool_injection.py +415 -0
  17. headroom/cli.py +219 -0
  18. headroom/client.py +977 -0
  19. headroom/compression/__init__.py +42 -0
  20. headroom/compression/detector.py +424 -0
  21. headroom/compression/handlers/__init__.py +22 -0
  22. headroom/compression/handlers/base.py +219 -0
  23. headroom/compression/handlers/code_handler.py +506 -0
  24. headroom/compression/handlers/json_handler.py +418 -0
  25. headroom/compression/masks.py +345 -0
  26. headroom/compression/universal.py +465 -0
  27. headroom/config.py +474 -0
  28. headroom/exceptions.py +192 -0
  29. headroom/integrations/__init__.py +159 -0
  30. headroom/integrations/agno/__init__.py +53 -0
  31. headroom/integrations/agno/hooks.py +345 -0
  32. headroom/integrations/agno/model.py +625 -0
  33. headroom/integrations/agno/providers.py +154 -0
  34. headroom/integrations/langchain/__init__.py +106 -0
  35. headroom/integrations/langchain/agents.py +326 -0
  36. headroom/integrations/langchain/chat_model.py +1002 -0
  37. headroom/integrations/langchain/langsmith.py +324 -0
  38. headroom/integrations/langchain/memory.py +319 -0
  39. headroom/integrations/langchain/providers.py +200 -0
  40. headroom/integrations/langchain/retriever.py +371 -0
  41. headroom/integrations/langchain/streaming.py +341 -0
  42. headroom/integrations/mcp/__init__.py +37 -0
  43. headroom/integrations/mcp/server.py +533 -0
  44. headroom/memory/__init__.py +37 -0
  45. headroom/memory/extractor.py +390 -0
  46. headroom/memory/fast_store.py +621 -0
  47. headroom/memory/fast_wrapper.py +311 -0
  48. headroom/memory/inline_extractor.py +229 -0
  49. headroom/memory/store.py +434 -0
  50. headroom/memory/worker.py +260 -0
  51. headroom/memory/wrapper.py +321 -0
  52. headroom/models/__init__.py +39 -0
  53. headroom/models/registry.py +687 -0
  54. headroom/parser.py +293 -0
  55. headroom/pricing/__init__.py +51 -0
  56. headroom/pricing/anthropic_prices.py +81 -0
  57. headroom/pricing/litellm_pricing.py +113 -0
  58. headroom/pricing/openai_prices.py +91 -0
  59. headroom/pricing/registry.py +188 -0
  60. headroom/providers/__init__.py +61 -0
  61. headroom/providers/anthropic.py +621 -0
  62. headroom/providers/base.py +131 -0
  63. headroom/providers/cohere.py +362 -0
  64. headroom/providers/google.py +427 -0
  65. headroom/providers/litellm.py +297 -0
  66. headroom/providers/openai.py +566 -0
  67. headroom/providers/openai_compatible.py +521 -0
  68. headroom/proxy/__init__.py +19 -0
  69. headroom/proxy/server.py +2683 -0
  70. headroom/py.typed +0 -0
  71. headroom/relevance/__init__.py +124 -0
  72. headroom/relevance/base.py +106 -0
  73. headroom/relevance/bm25.py +255 -0
  74. headroom/relevance/embedding.py +255 -0
  75. headroom/relevance/hybrid.py +259 -0
  76. headroom/reporting/__init__.py +5 -0
  77. headroom/reporting/generator.py +549 -0
  78. headroom/storage/__init__.py +41 -0
  79. headroom/storage/base.py +125 -0
  80. headroom/storage/jsonl.py +220 -0
  81. headroom/storage/sqlite.py +289 -0
  82. headroom/telemetry/__init__.py +91 -0
  83. headroom/telemetry/collector.py +764 -0
  84. headroom/telemetry/models.py +880 -0
  85. headroom/telemetry/toin.py +1579 -0
  86. headroom/tokenizer.py +80 -0
  87. headroom/tokenizers/__init__.py +75 -0
  88. headroom/tokenizers/base.py +210 -0
  89. headroom/tokenizers/estimator.py +198 -0
  90. headroom/tokenizers/huggingface.py +317 -0
  91. headroom/tokenizers/mistral.py +245 -0
  92. headroom/tokenizers/registry.py +398 -0
  93. headroom/tokenizers/tiktoken_counter.py +248 -0
  94. headroom/transforms/__init__.py +106 -0
  95. headroom/transforms/base.py +57 -0
  96. headroom/transforms/cache_aligner.py +357 -0
  97. headroom/transforms/code_compressor.py +1313 -0
  98. headroom/transforms/content_detector.py +335 -0
  99. headroom/transforms/content_router.py +1158 -0
  100. headroom/transforms/llmlingua_compressor.py +638 -0
  101. headroom/transforms/log_compressor.py +529 -0
  102. headroom/transforms/pipeline.py +297 -0
  103. headroom/transforms/rolling_window.py +350 -0
  104. headroom/transforms/search_compressor.py +365 -0
  105. headroom/transforms/smart_crusher.py +2682 -0
  106. headroom/transforms/text_compressor.py +259 -0
  107. headroom/transforms/tool_crusher.py +338 -0
  108. headroom/utils.py +215 -0
  109. headroom_ai-0.2.13.dist-info/METADATA +315 -0
  110. headroom_ai-0.2.13.dist-info/RECORD +114 -0
  111. headroom_ai-0.2.13.dist-info/WHEEL +4 -0
  112. headroom_ai-0.2.13.dist-info/entry_points.txt +2 -0
  113. headroom_ai-0.2.13.dist-info/licenses/LICENSE +190 -0
  114. 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())