qc-lean-mcp 1.2.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.
Files changed (40) hide show
  1. qc_lean_mcp-1.2.0/LICENSE +21 -0
  2. qc_lean_mcp-1.2.0/PKG-INFO +61 -0
  3. qc_lean_mcp-1.2.0/README.md +201 -0
  4. qc_lean_mcp-1.2.0/assets/__init__.py +0 -0
  5. qc_lean_mcp-1.2.0/assets/banner.svg +7 -0
  6. qc_lean_mcp-1.2.0/core/__init__.py +0 -0
  7. qc_lean_mcp-1.2.0/core/build_db.py +197 -0
  8. qc_lean_mcp-1.2.0/core/caches.py +110 -0
  9. qc_lean_mcp-1.2.0/core/config.py +108 -0
  10. qc_lean_mcp-1.2.0/core/db.py +395 -0
  11. qc_lean_mcp-1.2.0/core/embeddings.py +64 -0
  12. qc_lean_mcp-1.2.0/core/hot_cache.py +159 -0
  13. qc_lean_mcp-1.2.0/core/validator.py +385 -0
  14. qc_lean_mcp-1.2.0/data/__init__.py +0 -0
  15. qc_lean_mcp-1.2.0/data/qc_entries.json +162244 -0
  16. qc_lean_mcp-1.2.0/formatters/__init__.py +0 -0
  17. qc_lean_mcp-1.2.0/formatters/entry.py +178 -0
  18. qc_lean_mcp-1.2.0/formatters/errors.py +196 -0
  19. qc_lean_mcp-1.2.0/pipeline/__init__.py +0 -0
  20. qc_lean_mcp-1.2.0/pipeline/merge_and_index.py +26 -0
  21. qc_lean_mcp-1.2.0/pyproject.toml +79 -0
  22. qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/PKG-INFO +61 -0
  23. qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/SOURCES.txt +38 -0
  24. qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/dependency_links.txt +1 -0
  25. qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/entry_points.txt +2 -0
  26. qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/requires.txt +21 -0
  27. qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/top_level.txt +8 -0
  28. qc_lean_mcp-1.2.0/server.py +170 -0
  29. qc_lean_mcp-1.2.0/setup.cfg +4 -0
  30. qc_lean_mcp-1.2.0/templates/__init__.py +0 -0
  31. qc_lean_mcp-1.2.0/templates/algorithm_templates.py +135 -0
  32. qc_lean_mcp-1.2.0/tests/test_tools.py +76 -0
  33. qc_lean_mcp-1.2.0/tools/__init__.py +0 -0
  34. qc_lean_mcp-1.2.0/tools/codegen.py +204 -0
  35. qc_lean_mcp-1.2.0/tools/context.py +246 -0
  36. qc_lean_mcp-1.2.0/tools/lookup.py +217 -0
  37. qc_lean_mcp-1.2.0/tools/resources/__init__.py +0 -0
  38. qc_lean_mcp-1.2.0/tools/resources/stats.py +58 -0
  39. qc_lean_mcp-1.2.0/tools/search.py +599 -0
  40. qc_lean_mcp-1.2.0/tools/validation.py +579 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 TheFractalyst
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,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: qc-lean-mcp
3
+ Version: 1.2.0
4
+ Summary: QuantConnect/Lean MCP server - 6 tools for docs lookup, code validation, and code generation
5
+ Author: TheFractalyst
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 TheFractalyst
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/TheFractalyst/QuantConnectMCP
29
+ Project-URL: Repository, https://github.com/TheFractalyst/QuantConnectMCP
30
+ Project-URL: Issues, https://github.com/TheFractalyst/QuantConnectMCP/issues
31
+ Keywords: mcp,quantconnect,lean,algorithmic-trading,quant,trading
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Topic :: Office/Business :: Financial :: Investment
40
+ Requires-Python: >=3.10
41
+ License-File: LICENSE
42
+ Requires-Dist: fastmcp<4.0.0,>=3.0.0
43
+ Requires-Dist: chromadb<1.0.0,>=0.5.0
44
+ Requires-Dist: sentence-transformers<4.0.0,>=3.0.0
45
+ Requires-Dist: pydantic<3.0.0,>=2.0.0
46
+ Requires-Dist: python-dotenv<2.0.0,>=1.0.0
47
+ Requires-Dist: loguru<1.0.0,>=0.7.0
48
+ Requires-Dist: httpx<1.0.0,>=0.27.0
49
+ Requires-Dist: rapidfuzz<4.0.0,>=3.0.0
50
+ Requires-Dist: xxhash<4.0.0,>=3.0.0
51
+ Requires-Dist: starlette<1.0.0,>=0.36.0
52
+ Provides-Extra: pipeline
53
+ Requires-Dist: playwright>=1.40.0; extra == "pipeline"
54
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "pipeline"
55
+ Requires-Dist: lxml>=5.0.0; extra == "pipeline"
56
+ Requires-Dist: tqdm>=4.66.0; extra == "pipeline"
57
+ Provides-Extra: dev
58
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
59
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
60
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
61
+ Dynamic: license-file
@@ -0,0 +1,201 @@
1
+ [![QuantConnectMCP](/TheFractalyst/QuantConnectMCP/raw/main/assets/banner.svg)](https://github.com/TheFractalyst/QuantConnectMCP)
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/quantconnect-mcp?color=blue)](https://pypi.org/project/quantconnect-mcp/)
4
+ [![Python](https://img.shields.io/badge/Python-3.10+-3776AB?logo=python&logoColor=white)](https://python.org/)
5
+ [![ChromaDB](https://img.shields.io/badge/ChromaDB-FF6F00)](https://trychroma.com/)
6
+ [![MCP](https://img.shields.io/badge/MCP-Protocol-blue)](https://modelcontextprotocol.io/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+ [![Tests](https://github.com/TheFractalyst/QuantConnectMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/TheFractalyst/QuantConnectMCP/actions/workflows/ci.yml)
9
+
10
+ Complete QuantConnect/Lean reference documentation MCP server with 1265 entries
11
+ covering 100% of the official QuantConnect v2 documentation sitemap.
12
+
13
+ [Quick Start](#quick-start) |
14
+ [Tools](#tools) |
15
+ [Configuration](#configuration) |
16
+ [Database](#database) |
17
+ [Development](#development)
18
+
19
+ ---
20
+
21
+ ## One Command. Two Minutes. Fully Functional.
22
+
23
+ ```bash
24
+ pip install quantconnect-mcp
25
+ quantconnect-mcp
26
+ ```
27
+
28
+ That's it. On first run, the server auto-builds the ChromaDB vector store from
29
+ shipped JSON data (takes 30-60 seconds for embedding model download + indexing).
30
+ Subsequent runs start instantly.
31
+
32
+ **What you get:**
33
+
34
+ - 6 MCP tools for QuantConnect/Lean docs lookup, code validation, and template generation
35
+ - 1265 entries indexed in a local ChromaDB vector store (100% offline)
36
+ - Sub-millisecond hot cache for priority lookups
37
+ - Python AST validation for Lean algorithms
38
+ - Common Python.NET issue detection and auto-repair
39
+ - Works with Claude Desktop, Cursor, Windsurf, OpenCode, and any MCP client
40
+
41
+ **What you need:**
42
+
43
+ - Python 3.10+
44
+ - Any MCP-compatible AI client
45
+
46
+ ---
47
+
48
+ ## Why QuantConnectMCP?
49
+
50
+ AI coding assistants hallucinate Lean API syntax. The QuantConnect API is large
51
+ (QCAlgorithm, Algorithm Framework, 200+ indicators, multiple asset classes) and
52
+ constantly evolving. Models trained on older code get method signatures wrong.
53
+
54
+ QuantConnectMCP gives AI assistants **authoritative, real-time access** to the
55
+ complete QuantConnect/Lean reference:
56
+
57
+ - **100% coverage**: 1257/1257 official QuantConnect v2 docs sitemap URLs
58
+ - **Semantic search**: Vector embeddings find relevant docs by meaning, not keywords
59
+ - **Code validation**: `qc_validate` checks Python Lean algorithms for common mistakes
60
+ - **Auto-repair**: `qc_repair` fixes Python.NET bridge issues, missing self. prefix, etc.
61
+ - **Code generation**: `qc_scaffold` generates validated algorithm templates
62
+ - **100% local**: No network calls at runtime (ChromaDB auto-builds from shipped data)
63
+
64
+ ---
65
+
66
+ ## Tools
67
+
68
+ ```
69
+ +---------------------------+----------------------------------------------+
70
+ | Tool | Description |
71
+ +---------------------------+----------------------------------------------+
72
+ | qc_lookup(name, kind?) | Get complete docs for a symbol by exact name |
73
+ | qc_search(query, ...) | Semantic search across all docs |
74
+ | qc_browse(namespace, ...) | Enumerate all members of a namespace |
75
+ | qc_validate(code?, ...) | Validate Python syntax for Lean algorithms |
76
+ | qc_repair(code, context) | Fix common Python.NET issues in Lean code |
77
+ | qc_scaffold(kind, name) | Generate algorithm/indicator/research template|
78
+ +---------------------------+----------------------------------------------+
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Quick Start
84
+
85
+ ### Claude Desktop
86
+
87
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
88
+
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "quantconnect-mcp": {
93
+ "command": "quantconnect-mcp"
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Cursor / Windsurf / OpenCode
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "quantconnect-mcp": {
105
+ "command": "quantconnect-mcp"
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### SSE (HTTP)
112
+
113
+ ```bash
114
+ quantconnect-mcp
115
+ # Then connect to http://localhost:8080
116
+ ```
117
+
118
+ Or with environment variables:
119
+
120
+ ```bash
121
+ TRANSPORT=sse PORT=8080 quantconnect-mcp
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Configuration
127
+
128
+ ```
129
+ +-------------------------+---------------+--------------------------------+
130
+ | Env Var | Default | Description |
131
+ +-------------------------+---------------+--------------------------------+
132
+ | TRANSPORT | stdio | Transport: stdio or sse |
133
+ | PORT | 8080 | Port for SSE transport |
134
+ | QC_DB_PATH | ./qc_db | ChromaDB path |
135
+ | QC_COLLECTION | qc_reference | ChromaDB collection name |
136
+ | QC_EMBED_MODEL | all-MiniLM-L6 | Sentence transformer model |
137
+ | QC_MAX_RESULTS | 100 | Max search results |
138
+ | LOG_LEVEL | INFO | Logging level |
139
+ | LAZY_MODEL | 0 | Skip model preload at startup |
140
+ +-------------------------+---------------+--------------------------------+
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Database
146
+
147
+ - 1265 entries (QuantConnect v2 docs)
148
+ - 1257/1257 sitemap URLs (100% coverage)
149
+ - 20 namespaces: cloud, indicators, writing-algorithms, cli, docs, datasets,
150
+ research, securities, reality-modeling, local, engine, ai, framework, etc.
151
+ - Hot cache for sub-millisecond priority lookups
152
+ - Auto-built on first run from shipped JSON data (14MB)
153
+
154
+ To rebuild the database:
155
+
156
+ ```bash
157
+ quantconnect-mcp build
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Development
163
+
164
+ ```
165
+ git clone https://github.com/TheFractalyst/QuantConnectMCP.git
166
+ cd QuantConnectMCP
167
+ pip install -e ".[dev]"
168
+ pytest tests/ -v
169
+ ```
170
+
171
+ ### Re-index ChromaDB
172
+
173
+ ```bash
174
+ python pipeline/merge_and_index.py --reset
175
+ ```
176
+
177
+ ## Docker
178
+
179
+ ```bash
180
+ docker build -t quantconnect-mcp .
181
+ docker run -p 8080:8080 -e TRANSPORT=sse quantconnect-mcp
182
+ ```
183
+
184
+ ## Tech Stack
185
+
186
+ - **FastMCP 3.0** - MCP server framework with FileSystemProvider auto-discovery
187
+ - **ChromaDB** - Local vector database for semantic search (cosine, 384-dim)
188
+ - **SentenceTransformers** - `all-MiniLM-L6-v2` embedding model
189
+ - **RapidFuzz** - Fuzzy string matching for name lookups
190
+ - **Loguru** - Structured logging
191
+
192
+ ## Links
193
+
194
+ - [fractalyst.dev](https://fractalyst.dev/) - Portfolio
195
+ - [QuantConnect Documentation](https://www.quantconnect.com/docs/v2/) - Official docs
196
+ - [LEAN Engine](https://www.quantconnect.com/lean) - Open-source algorithmic engine
197
+ - [@TheFractalyst](https://x.com/TheFractalyst) - X / Twitter
198
+
199
+ ## License
200
+
201
+ MIT - see [LICENSE](LICENSE)
File without changes
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="120" viewBox="0 0 800 120">
2
+ <rect width="800" height="120" fill="#0d1117"/>
3
+ <rect x="0" y="0" width="6" height="120" fill="#58a6ff"/>
4
+ <text x="40" y="55" font-family="monospace" font-size="36" font-weight="bold" fill="#58a6ff">QuantConnectMCP</text>
5
+ <text x="40" y="85" font-family="monospace" font-size="16" fill="#8b949e">QuantConnect/Lean Reference - MCP Server</text>
6
+ <text x="40" y="105" font-family="monospace" font-size="13" fill="#6e7681">1265 entries | 100% sitemap coverage | 6 tools</text>
7
+ </svg>
File without changes
@@ -0,0 +1,197 @@
1
+ """
2
+ core/build_db.py
3
+ Auto-build ChromaDB from shipped JSON data on first run.
4
+
5
+ Ships data/qc_entries.json (14MB, 1265 entries) as package data.
6
+ On first server start, if no ChromaDB exists at DB_PATH, builds it automatically.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from loguru import logger
17
+
18
+ from core.config import COLLECTION, DB_PATH, EMBED_MODEL
19
+
20
+
21
+ def _data_json_path() -> Path:
22
+ return Path(__file__).resolve().parent.parent / "data" / "qc_entries.json"
23
+
24
+
25
+ def _build_document_text(entry: dict[str, Any]) -> str:
26
+ parts: list[str] = []
27
+
28
+ name = entry.get("name", "")
29
+ namespace = entry.get("namespace") or ""
30
+ category = entry.get("category", "")
31
+ parts.append(f"{category.upper()}: {name}")
32
+ if namespace:
33
+ parts.append(f"Namespace: {namespace}")
34
+
35
+ syntax = entry.get("syntax") or ""
36
+ if not isinstance(syntax, str):
37
+ syntax = str(syntax)
38
+ if syntax:
39
+ parts.append(f"Syntax: {syntax}")
40
+
41
+ description = entry.get("description") or ""
42
+ if not isinstance(description, str):
43
+ description = str(description)
44
+ if description:
45
+ parts.append(description)
46
+
47
+ returns = entry.get("returns") or ""
48
+ if not isinstance(returns, str):
49
+ returns = str(returns)
50
+ if returns:
51
+ parts.append(f"Returns: {returns}")
52
+
53
+ remarks = entry.get("remarks") or ""
54
+ if not isinstance(remarks, str):
55
+ remarks = str(remarks)
56
+ if remarks:
57
+ parts.append(f"Remarks: {remarks}")
58
+
59
+ parameters = entry.get("parameters") or []
60
+ if parameters:
61
+ parts.append("PARAMETERS:")
62
+ for p in parameters:
63
+ if isinstance(p, dict):
64
+ pname = p.get("name", "?")
65
+ ptype = p.get("type", "")
66
+ pdesc = p.get("description", "")
67
+ parts.append(f" {pname} ({ptype}): {pdesc}")
68
+
69
+ examples = entry.get("examples") or []
70
+ if examples:
71
+ parts.append("EXAMPLES:")
72
+ for ex in examples:
73
+ parts.append(str(ex))
74
+
75
+ return "\n\n".join(parts)
76
+
77
+
78
+ def _flatten_metadata(entry: dict[str, Any]) -> dict[str, Any]:
79
+ meta: dict[str, Any] = {}
80
+ meta["name"] = entry.get("name", "")
81
+ meta["category"] = entry.get("category", "")
82
+ meta["namespace"] = entry.get("namespace") or ""
83
+ meta["syntax"] = entry.get("syntax") or ""
84
+ meta["version"] = entry.get("version") or ""
85
+ meta["returns"] = entry.get("returns") or ""
86
+ meta["remarks"] = entry.get("remarks") or ""
87
+ meta["url"] = entry.get("url") or ""
88
+ meta["scraped_at"] = entry.get("scraped_at") or ""
89
+ meta["raw_description"] = entry.get("description") or ""
90
+
91
+ examples = entry.get("examples") or []
92
+ parameters = entry.get("parameters") or []
93
+ meta["has_examples"] = 1 if examples else 0
94
+ meta["example_count"] = len(examples)
95
+ meta["param_count"] = len(parameters)
96
+
97
+ if isinstance(examples, list):
98
+ meta["raw_examples"] = " ||| ".join(str(ex) for ex in examples)
99
+ elif examples:
100
+ meta["raw_examples"] = str(examples)
101
+ else:
102
+ meta["raw_examples"] = ""
103
+
104
+ meta["raw_parameters"] = json.dumps(parameters, ensure_ascii=False) if parameters else ""
105
+
106
+ sources = entry.get("sources", [])
107
+ meta["sources"] = ", ".join(sources) if isinstance(sources, list) else str(sources)
108
+
109
+ return meta
110
+
111
+
112
+ def build_db(force: bool = False) -> int:
113
+ """Build ChromaDB from shipped JSON data.
114
+
115
+ Args:
116
+ force: If True, delete existing collection and rebuild.
117
+
118
+ Returns:
119
+ Number of entries indexed.
120
+ """
121
+ json_path = _data_json_path()
122
+ if not json_path.exists():
123
+ logger.error(f"Data file not found: {json_path}")
124
+ return 0
125
+
126
+ logger.info(f"Building ChromaDB from {json_path}...")
127
+ entries = json.loads(json_path.read_text(encoding="utf-8"))
128
+ logger.info(f"Loaded {len(entries)} entries from JSON")
129
+
130
+ import chromadb
131
+ from sentence_transformers import SentenceTransformer
132
+
133
+ logger.info(f"Loading embedding model: {EMBED_MODEL}")
134
+ model = SentenceTransformer(EMBED_MODEL)
135
+ logger.info("Embedding model loaded")
136
+
137
+ client = chromadb.PersistentClient(path=DB_PATH)
138
+
139
+ if force:
140
+ try:
141
+ client.delete_collection(name=COLLECTION)
142
+ logger.info("Deleted existing collection")
143
+ except Exception:
144
+ pass
145
+
146
+ collection = client.get_or_create_collection(
147
+ name=COLLECTION,
148
+ metadata={"hnsw:space": "cosine"},
149
+ )
150
+ logger.info(f"Collection '{COLLECTION}' ready")
151
+
152
+ batch_size = 50
153
+ total = len(entries)
154
+
155
+ for i in range(0, total, batch_size):
156
+ batch = entries[i : i + batch_size]
157
+
158
+ ids: list[str] = []
159
+ docs: list[str] = []
160
+ metas: list[dict[str, Any]] = []
161
+
162
+ for entry in batch:
163
+ url = entry.get("url", "")
164
+ name = entry.get("name", "").lower().strip().replace(" ", "").replace("()", "").replace("`", "")
165
+ entry_id = hashlib.md5((url + "|" + name).encode()).hexdigest()[:16]
166
+
167
+ doc_text = _build_document_text(entry)
168
+ meta = _flatten_metadata(entry)
169
+
170
+ ids.append(entry_id)
171
+ docs.append(doc_text)
172
+ metas.append(meta)
173
+
174
+ if docs:
175
+ try:
176
+ vecs = model.encode(docs, show_progress_bar=False)
177
+ embeddings = [v.tolist() for v in vecs]
178
+ collection.upsert(
179
+ ids=ids,
180
+ documents=docs,
181
+ metadatas=metas,
182
+ embeddings=embeddings,
183
+ )
184
+ except Exception as e:
185
+ logger.error(f"Embedding failed on batch {i // batch_size + 1}: {e}")
186
+ continue
187
+
188
+ if (i + batch_size) % 250 == 0 or i + batch_size >= total:
189
+ logger.info(f"Progress: {min(i + batch_size, total)}/{total} entries processed")
190
+
191
+ count = collection.count()
192
+ logger.info(f"ChromaDB build complete: {count} entries indexed at {DB_PATH}")
193
+ return count
194
+
195
+
196
+ if __name__ == "__main__":
197
+ build_db(force=True)
@@ -0,0 +1,110 @@
1
+ """
2
+ core/caches.py
3
+ All in-process LRU caches:
4
+ - Validation cache (code hash -> compiler result)
5
+ - File validation cache (path+mtime+size -> validation result)
6
+ - Query result cache (L1: ChromaDB query results)
7
+ - Codegen cache (template generation results)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import threading
15
+ import time
16
+ from collections import OrderedDict
17
+ from typing import Optional
18
+
19
+ import xxhash
20
+ from loguru import logger
21
+
22
+ from core.config import VALIDATION_CACHE_MAX_SIZE, VALIDATION_CACHE_TTL
23
+
24
+ _VALIDATION_CACHE: OrderedDict[str, tuple[str, float]] = OrderedDict()
25
+ _VALIDATION_CACHE_LOCK = threading.Lock()
26
+
27
+
28
+ def get_cached_validation(code: str) -> Optional[dict]:
29
+ h = xxhash.xxh64(code.encode()).hexdigest()
30
+ with _VALIDATION_CACHE_LOCK:
31
+ if h in _VALIDATION_CACHE:
32
+ result_str, ts = _VALIDATION_CACHE[h]
33
+ if time.time() - ts < VALIDATION_CACHE_TTL:
34
+ try:
35
+ return json.loads(result_str)
36
+ except json.JSONDecodeError:
37
+ logger.warning("Corrupt validation cache entry - evicting")
38
+ del _VALIDATION_CACHE[h]
39
+ return None
40
+
41
+
42
+ def set_cached_validation(code: str, result: str) -> None:
43
+ h = xxhash.xxh64(code.encode()).hexdigest()
44
+ with _VALIDATION_CACHE_LOCK:
45
+ _VALIDATION_CACHE[h] = (result, time.time())
46
+ _VALIDATION_CACHE.move_to_end(h)
47
+ while len(_VALIDATION_CACHE) > VALIDATION_CACHE_MAX_SIZE:
48
+ _VALIDATION_CACHE.popitem(last=False)
49
+
50
+
51
+ _FILE_VALIDATION_CACHE: OrderedDict[tuple[str, int, int], tuple[str, float]] = OrderedDict()
52
+ _FILE_VALIDATION_CACHE_LOCK = threading.Lock()
53
+ _FILE_VALIDATION_CACHE_TTL = float(os.getenv("FILE_VALIDATION_CACHE_TTL", "1800"))
54
+ _FILE_VALIDATION_CACHE_MAX = int(os.getenv("FILE_VALIDATION_CACHE_SIZE", "200"))
55
+
56
+
57
+ def get_cached_file_validation(file_path: str, mtime_ns: int, file_size: int) -> Optional[str]:
58
+ key = (file_path, mtime_ns, file_size)
59
+ with _FILE_VALIDATION_CACHE_LOCK:
60
+ if key in _FILE_VALIDATION_CACHE:
61
+ result_str, ts = _FILE_VALIDATION_CACHE[key]
62
+ if time.time() - ts < _FILE_VALIDATION_CACHE_TTL:
63
+ logger.debug(f"File validation cache hit: {file_path}")
64
+ return result_str
65
+ else:
66
+ del _FILE_VALIDATION_CACHE[key]
67
+ return None
68
+
69
+
70
+ def set_cached_file_validation(file_path: str, mtime_ns: int, file_size: int, result: str) -> None:
71
+ key = (file_path, mtime_ns, file_size)
72
+ with _FILE_VALIDATION_CACHE_LOCK:
73
+ _FILE_VALIDATION_CACHE[key] = (result, time.time())
74
+ _FILE_VALIDATION_CACHE.move_to_end(key)
75
+ while len(_FILE_VALIDATION_CACHE) > _FILE_VALIDATION_CACHE_MAX:
76
+ _FILE_VALIDATION_CACHE.popitem(last=False)
77
+
78
+
79
+ _QUERY_RESULT_CACHE: OrderedDict[str, tuple[dict, float]] = OrderedDict()
80
+ _QUERY_CACHE_LOCK = threading.Lock()
81
+ _QUERY_CACHE_TTL = 600.0
82
+ _QUERY_CACHE_MAX = 500
83
+
84
+
85
+ _CODEGEN_CACHE: OrderedDict[str, tuple[str, float]] = OrderedDict()
86
+ _CODEGEN_CACHE_LOCK = threading.Lock()
87
+ _CODEGEN_CACHE_TTL = 600.0
88
+ _CODEGEN_CACHE_MAX = 50
89
+
90
+
91
+ def codegen_cache_key(name: str, description: str, inputs: str | None, overlay: bool) -> str:
92
+ return xxhash.xxh64(f"{name}|{description}|{inputs}|{overlay}".encode()).hexdigest()
93
+
94
+
95
+ def get_codegen_cache(key: str) -> str | None:
96
+ with _CODEGEN_CACHE_LOCK:
97
+ if key in _CODEGEN_CACHE:
98
+ result, ts = _CODEGEN_CACHE[key]
99
+ if time.time() - ts < _CODEGEN_CACHE_TTL:
100
+ return result
101
+ del _CODEGEN_CACHE[key]
102
+ return None
103
+
104
+
105
+ def set_codegen_cache(key: str, result: str) -> None:
106
+ with _CODEGEN_CACHE_LOCK:
107
+ _CODEGEN_CACHE[key] = (result, time.time())
108
+ _CODEGEN_CACHE.move_to_end(key)
109
+ while len(_CODEGEN_CACHE) > _CODEGEN_CACHE_MAX:
110
+ _CODEGEN_CACHE.popitem(last=False)