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.
- qc_lean_mcp-1.2.0/LICENSE +21 -0
- qc_lean_mcp-1.2.0/PKG-INFO +61 -0
- qc_lean_mcp-1.2.0/README.md +201 -0
- qc_lean_mcp-1.2.0/assets/__init__.py +0 -0
- qc_lean_mcp-1.2.0/assets/banner.svg +7 -0
- qc_lean_mcp-1.2.0/core/__init__.py +0 -0
- qc_lean_mcp-1.2.0/core/build_db.py +197 -0
- qc_lean_mcp-1.2.0/core/caches.py +110 -0
- qc_lean_mcp-1.2.0/core/config.py +108 -0
- qc_lean_mcp-1.2.0/core/db.py +395 -0
- qc_lean_mcp-1.2.0/core/embeddings.py +64 -0
- qc_lean_mcp-1.2.0/core/hot_cache.py +159 -0
- qc_lean_mcp-1.2.0/core/validator.py +385 -0
- qc_lean_mcp-1.2.0/data/__init__.py +0 -0
- qc_lean_mcp-1.2.0/data/qc_entries.json +162244 -0
- qc_lean_mcp-1.2.0/formatters/__init__.py +0 -0
- qc_lean_mcp-1.2.0/formatters/entry.py +178 -0
- qc_lean_mcp-1.2.0/formatters/errors.py +196 -0
- qc_lean_mcp-1.2.0/pipeline/__init__.py +0 -0
- qc_lean_mcp-1.2.0/pipeline/merge_and_index.py +26 -0
- qc_lean_mcp-1.2.0/pyproject.toml +79 -0
- qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/PKG-INFO +61 -0
- qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/SOURCES.txt +38 -0
- qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/dependency_links.txt +1 -0
- qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/entry_points.txt +2 -0
- qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/requires.txt +21 -0
- qc_lean_mcp-1.2.0/qc_lean_mcp.egg-info/top_level.txt +8 -0
- qc_lean_mcp-1.2.0/server.py +170 -0
- qc_lean_mcp-1.2.0/setup.cfg +4 -0
- qc_lean_mcp-1.2.0/templates/__init__.py +0 -0
- qc_lean_mcp-1.2.0/templates/algorithm_templates.py +135 -0
- qc_lean_mcp-1.2.0/tests/test_tools.py +76 -0
- qc_lean_mcp-1.2.0/tools/__init__.py +0 -0
- qc_lean_mcp-1.2.0/tools/codegen.py +204 -0
- qc_lean_mcp-1.2.0/tools/context.py +246 -0
- qc_lean_mcp-1.2.0/tools/lookup.py +217 -0
- qc_lean_mcp-1.2.0/tools/resources/__init__.py +0 -0
- qc_lean_mcp-1.2.0/tools/resources/stats.py +58 -0
- qc_lean_mcp-1.2.0/tools/search.py +599 -0
- 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
|
+
[](https://github.com/TheFractalyst/QuantConnectMCP)
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/quantconnect-mcp/)
|
|
4
|
+
[](https://python.org/)
|
|
5
|
+
[](https://trychroma.com/)
|
|
6
|
+
[](https://modelcontextprotocol.io/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](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)
|