knowledge-rag 3.3.1__tar.gz → 3.4.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.
@@ -28,6 +28,7 @@ documents/**/*.csv
28
28
 
29
29
  # FastEmbed model cache
30
30
  .cache/
31
+ models_cache/
31
32
  *.onnx
32
33
 
33
34
  # Local scripts (not part of distribution)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: knowledge-rag
3
- Version: 3.3.1
3
+ Version: 3.4.0
4
4
  Summary: Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 12 MCP Tools. Zero external servers.
5
5
  Project-URL: Homepage, https://github.com/lyonzin/knowledge-rag
6
6
  Project-URL: Repository, https://github.com/lyonzin/knowledge-rag
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  <div align="center">
40
40
 
41
- ![Version](https://img.shields.io/badge/version-3.3.1-blue.svg)
41
+ ![Version](https://img.shields.io/badge/version-3.4.0-blue.svg)
42
42
  ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-green.svg)
43
43
  ![License](https://img.shields.io/badge/license-MIT-yellow.svg)
44
44
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)
@@ -61,7 +61,7 @@ Your documents become instantly searchable inside Claude Code — with reranking
61
61
 
62
62
  **12 MCP Tools** | **Hybrid Search + Cross-Encoder Reranking** | **Markdown-Aware Chunking** | **100% Local, Zero Cloud**
63
63
 
64
- [What's New](#whats-new-in-v330) | [Installation](#installation) | [Configuration](#configuration) | [API Reference](#api-reference) | [Architecture](#architecture)
64
+ [What's New](#whats-new-in-v340) | [Installation](#installation) | [Configuration](#configuration) | [API Reference](#api-reference) | [Architecture](#architecture)
65
65
 
66
66
  </div>
67
67
 
@@ -108,6 +108,40 @@ After the initial rebuild, startup and queries are faster than v2.x because ther
108
108
 
109
109
  ---
110
110
 
111
+ ## What's New in v3.4.0
112
+
113
+ ### Persistent Model Cache
114
+ Embedding models (~250MB) are now stored in `models_cache/` instead of `/tmp`. No more re-downloading after system reboots on Linux. Configurable via `paths.models_cache_dir` in config.yaml.
115
+
116
+ ### Exclude Patterns
117
+ Skip files and directories during indexing with glob patterns. Configure under `documents.exclude_patterns` in config.yaml:
118
+ ```yaml
119
+ documents:
120
+ exclude_patterns:
121
+ - "node_modules"
122
+ - ".venv"
123
+ - "__pycache__"
124
+ - ".git"
125
+ - "*.tmp"
126
+ ```
127
+ Patterns match against both full relative paths and individual path components — `"node_modules"` excludes it at any depth.
128
+
129
+ ### Jupyter Notebook Support (.ipynb)
130
+ Native parser extracts only markdown and code cell sources — no outputs, no execution counts, no base64-encoded images. Clean content for high-quality retrieval. Added to default supported formats.
131
+
132
+ ### MCP Protocol Stability
133
+ `stdout` is redirected to `stderr` before the MCP server starts. Prevents random library `print()` calls from corrupting the JSON-RPC stream — fixes "Failed to connect" errors in VS Code and IDE integrations.
134
+
135
+ ### File Watcher Resilience (Linux)
136
+ Server no longer crashes when system inotify limits are reached. Falls back gracefully to manual indexing with a clear warning message.
137
+
138
+ ### MetaTrader Support (.mq4, .mqh)
139
+ MQL4/MQL5 source files can now be indexed as code. Not enabled by default — add `.mq4` and `.mqh` to your `supported_formats` list to activate.
140
+
141
+ *Community credit: Features inspired by ideas from [@Hohlas](https://github.com/Hohlas) in [PR #18](https://github.com/lyonzin/knowledge-rag/pull/18).*
142
+
143
+ ---
144
+
111
145
  ## What's New in v3.3.0
112
146
 
113
147
  ### YAML Configuration System
@@ -966,6 +1000,7 @@ Pre-built configurations for common use cases. Each preset is a complete `config
966
1000
  |-------|---------|-------------|
967
1001
  | `paths.documents_dir` | `./documents` | Root folder scanned recursively for documents |
968
1002
  | `paths.data_dir` | `./data` | Internal storage for ChromaDB and index metadata |
1003
+ | `paths.models_cache_dir` | `./models_cache` | Persistent cache for embedding models (~250MB). Prevents re-download after reboots |
969
1004
 
970
1005
  Relative paths resolve from the project root. Absolute paths work too. The `KNOWLEDGE_RAG_DIR` environment variable overrides the project root.
971
1006
 
@@ -973,7 +1008,8 @@ Relative paths resolve from the project root. Absolute paths work too. The `KNOW
973
1008
 
974
1009
  | Field | Default | Description |
975
1010
  |-------|---------|-------------|
976
- | `documents.supported_formats` | .md .txt .pdf .py .json .docx .xlsx .pptx .csv | File extensions to index |
1011
+ | `documents.supported_formats` | .md .txt .pdf .py .json .docx .xlsx .pptx .csv .ipynb | File extensions to index |
1012
+ | `documents.exclude_patterns` | `[]` (empty) | Glob patterns for files/dirs to skip during indexing |
977
1013
  | `documents.chunking.chunk_size` | 1000 | Max characters per chunk |
978
1014
  | `documents.chunking.chunk_overlap` | 200 | Characters shared between consecutive chunks |
979
1015
 
@@ -1090,7 +1126,8 @@ knowledge-rag/
1090
1126
  ├── data/
1091
1127
  │ ├── chroma_db/ # ChromaDB vector database
1092
1128
  │ └── index_metadata.json # Incremental indexing state
1093
- ├── tests/ # Test suite (55 tests)
1129
+ ├── models_cache/ # Persistent embedding model cache
1130
+ ├── tests/ # Test suite (70+ tests)
1094
1131
  ├── venv/ # Python virtual environment
1095
1132
  ├── requirements.txt # Python dependencies
1096
1133
  ├── LICENSE # MIT License
@@ -1163,7 +1200,7 @@ pip install rank-bm25
1163
1200
 
1164
1201
  ### "ModuleNotFoundError: No module named 'mcp_server'"
1165
1202
 
1166
- This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#configure-mcp-for-claude-code)).
1203
+ This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#installation)).
1167
1204
 
1168
1205
  ### Dimension mismatch after upgrade
1169
1206
 
@@ -1195,6 +1232,27 @@ With ~200 documents, expect ~300-500MB RAM. The embedding model (~50MB) and rera
1195
1232
 
1196
1233
  ## Changelog
1197
1234
 
1235
+ ### v3.4.0 (2026-04-16)
1236
+
1237
+ - **NEW**: `models_cache_dir` — persistent embedding model cache, prevents re-download after reboots
1238
+ - **NEW**: `exclude_patterns` — glob-based file/directory exclusion during indexing (under `documents:` section)
1239
+ - **NEW**: Jupyter Notebook (.ipynb) parser — extracts markdown and code cell sources only, ignores outputs and base64
1240
+ - **NEW**: MCP stdout protection — redirects stdout to stderr before server start, fixes "Failed to connect" errors
1241
+ - **NEW**: File watcher resilience — graceful fallback when Linux inotify limits are reached
1242
+ - **NEW**: MetaTrader (.mq4, .mqh) support — opt-in code parsing for MQL4/MQL5 files
1243
+ - **NEW**: 23 new tests (exclude patterns, ipynb parser, stdout protection)
1244
+ - **IMPROVED**: All 4 presets updated with new config fields
1245
+ - Community credit: [@Hohlas](https://github.com/Hohlas) ([PR #18](https://github.com/lyonzin/knowledge-rag/pull/18))
1246
+
1247
+ ### v3.3.2 (2026-04-06)
1248
+
1249
+ - **FIX**: Full type validation on all YAML config values — wrong types warn and fall back to defaults
1250
+ - **FIX**: Bounds validation for chunk_size, chunk_overlap, default_results, max_results, embedding_dim
1251
+ - **FIX**: `keyword_routes` with string values instead of lists detected and removed with warning
1252
+ - **FIX**: `reranker_enabled: "yes"` (string) corrected to boolean with warning
1253
+ - **FIX**: Synced version strings across all source files
1254
+ - **FIX**: Broken README anchor, duplicate keyword, error handling in `knowledge-rag init`
1255
+
1198
1256
  ### v3.3.1 (2026-04-06)
1199
1257
 
1200
1258
  - **FIX**: YAML null values (`category_mappings:` without value) no longer crash the server — falls back to defaults
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div align="center">
4
4
 
5
- ![Version](https://img.shields.io/badge/version-3.3.1-blue.svg)
5
+ ![Version](https://img.shields.io/badge/version-3.4.0-blue.svg)
6
6
  ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-green.svg)
7
7
  ![License](https://img.shields.io/badge/license-MIT-yellow.svg)
8
8
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)
@@ -25,7 +25,7 @@ Your documents become instantly searchable inside Claude Code — with reranking
25
25
 
26
26
  **12 MCP Tools** | **Hybrid Search + Cross-Encoder Reranking** | **Markdown-Aware Chunking** | **100% Local, Zero Cloud**
27
27
 
28
- [What's New](#whats-new-in-v330) | [Installation](#installation) | [Configuration](#configuration) | [API Reference](#api-reference) | [Architecture](#architecture)
28
+ [What's New](#whats-new-in-v340) | [Installation](#installation) | [Configuration](#configuration) | [API Reference](#api-reference) | [Architecture](#architecture)
29
29
 
30
30
  </div>
31
31
 
@@ -72,6 +72,40 @@ After the initial rebuild, startup and queries are faster than v2.x because ther
72
72
 
73
73
  ---
74
74
 
75
+ ## What's New in v3.4.0
76
+
77
+ ### Persistent Model Cache
78
+ Embedding models (~250MB) are now stored in `models_cache/` instead of `/tmp`. No more re-downloading after system reboots on Linux. Configurable via `paths.models_cache_dir` in config.yaml.
79
+
80
+ ### Exclude Patterns
81
+ Skip files and directories during indexing with glob patterns. Configure under `documents.exclude_patterns` in config.yaml:
82
+ ```yaml
83
+ documents:
84
+ exclude_patterns:
85
+ - "node_modules"
86
+ - ".venv"
87
+ - "__pycache__"
88
+ - ".git"
89
+ - "*.tmp"
90
+ ```
91
+ Patterns match against both full relative paths and individual path components — `"node_modules"` excludes it at any depth.
92
+
93
+ ### Jupyter Notebook Support (.ipynb)
94
+ Native parser extracts only markdown and code cell sources — no outputs, no execution counts, no base64-encoded images. Clean content for high-quality retrieval. Added to default supported formats.
95
+
96
+ ### MCP Protocol Stability
97
+ `stdout` is redirected to `stderr` before the MCP server starts. Prevents random library `print()` calls from corrupting the JSON-RPC stream — fixes "Failed to connect" errors in VS Code and IDE integrations.
98
+
99
+ ### File Watcher Resilience (Linux)
100
+ Server no longer crashes when system inotify limits are reached. Falls back gracefully to manual indexing with a clear warning message.
101
+
102
+ ### MetaTrader Support (.mq4, .mqh)
103
+ MQL4/MQL5 source files can now be indexed as code. Not enabled by default — add `.mq4` and `.mqh` to your `supported_formats` list to activate.
104
+
105
+ *Community credit: Features inspired by ideas from [@Hohlas](https://github.com/Hohlas) in [PR #18](https://github.com/lyonzin/knowledge-rag/pull/18).*
106
+
107
+ ---
108
+
75
109
  ## What's New in v3.3.0
76
110
 
77
111
  ### YAML Configuration System
@@ -930,6 +964,7 @@ Pre-built configurations for common use cases. Each preset is a complete `config
930
964
  |-------|---------|-------------|
931
965
  | `paths.documents_dir` | `./documents` | Root folder scanned recursively for documents |
932
966
  | `paths.data_dir` | `./data` | Internal storage for ChromaDB and index metadata |
967
+ | `paths.models_cache_dir` | `./models_cache` | Persistent cache for embedding models (~250MB). Prevents re-download after reboots |
933
968
 
934
969
  Relative paths resolve from the project root. Absolute paths work too. The `KNOWLEDGE_RAG_DIR` environment variable overrides the project root.
935
970
 
@@ -937,7 +972,8 @@ Relative paths resolve from the project root. Absolute paths work too. The `KNOW
937
972
 
938
973
  | Field | Default | Description |
939
974
  |-------|---------|-------------|
940
- | `documents.supported_formats` | .md .txt .pdf .py .json .docx .xlsx .pptx .csv | File extensions to index |
975
+ | `documents.supported_formats` | .md .txt .pdf .py .json .docx .xlsx .pptx .csv .ipynb | File extensions to index |
976
+ | `documents.exclude_patterns` | `[]` (empty) | Glob patterns for files/dirs to skip during indexing |
941
977
  | `documents.chunking.chunk_size` | 1000 | Max characters per chunk |
942
978
  | `documents.chunking.chunk_overlap` | 200 | Characters shared between consecutive chunks |
943
979
 
@@ -1054,7 +1090,8 @@ knowledge-rag/
1054
1090
  ├── data/
1055
1091
  │ ├── chroma_db/ # ChromaDB vector database
1056
1092
  │ └── index_metadata.json # Incremental indexing state
1057
- ├── tests/ # Test suite (55 tests)
1093
+ ├── models_cache/ # Persistent embedding model cache
1094
+ ├── tests/ # Test suite (70+ tests)
1058
1095
  ├── venv/ # Python virtual environment
1059
1096
  ├── requirements.txt # Python dependencies
1060
1097
  ├── LICENSE # MIT License
@@ -1127,7 +1164,7 @@ pip install rank-bm25
1127
1164
 
1128
1165
  ### "ModuleNotFoundError: No module named 'mcp_server'"
1129
1166
 
1130
- This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#configure-mcp-for-claude-code)).
1167
+ This occurs when Claude Code doesn't set the working directory correctly. Use the `cmd /c "cd /d ... && python"` wrapper in your MCP config (see [Installation](#installation)).
1131
1168
 
1132
1169
  ### Dimension mismatch after upgrade
1133
1170
 
@@ -1159,6 +1196,27 @@ With ~200 documents, expect ~300-500MB RAM. The embedding model (~50MB) and rera
1159
1196
 
1160
1197
  ## Changelog
1161
1198
 
1199
+ ### v3.4.0 (2026-04-16)
1200
+
1201
+ - **NEW**: `models_cache_dir` — persistent embedding model cache, prevents re-download after reboots
1202
+ - **NEW**: `exclude_patterns` — glob-based file/directory exclusion during indexing (under `documents:` section)
1203
+ - **NEW**: Jupyter Notebook (.ipynb) parser — extracts markdown and code cell sources only, ignores outputs and base64
1204
+ - **NEW**: MCP stdout protection — redirects stdout to stderr before server start, fixes "Failed to connect" errors
1205
+ - **NEW**: File watcher resilience — graceful fallback when Linux inotify limits are reached
1206
+ - **NEW**: MetaTrader (.mq4, .mqh) support — opt-in code parsing for MQL4/MQL5 files
1207
+ - **NEW**: 23 new tests (exclude patterns, ipynb parser, stdout protection)
1208
+ - **IMPROVED**: All 4 presets updated with new config fields
1209
+ - Community credit: [@Hohlas](https://github.com/Hohlas) ([PR #18](https://github.com/lyonzin/knowledge-rag/pull/18))
1210
+
1211
+ ### v3.3.2 (2026-04-06)
1212
+
1213
+ - **FIX**: Full type validation on all YAML config values — wrong types warn and fall back to defaults
1214
+ - **FIX**: Bounds validation for chunk_size, chunk_overlap, default_results, max_results, embedding_dim
1215
+ - **FIX**: `keyword_routes` with string values instead of lists detected and removed with warning
1216
+ - **FIX**: `reranker_enabled: "yes"` (string) corrected to boolean with warning
1217
+ - **FIX**: Synced version strings across all source files
1218
+ - **FIX**: Broken README anchor, duplicate keyword, error handling in `knowledge-rag init`
1219
+
1162
1220
  ### v3.3.1 (2026-04-06)
1163
1221
 
1164
1222
  - **FIX**: YAML null values (`category_mappings:` without value) no longer crash the server — falls back to defaults
@@ -39,6 +39,11 @@ paths:
39
39
  # You generally don't need to touch this.
40
40
  data_dir: "./data"
41
41
 
42
+ # Persistent cache for embedding models (~250MB).
43
+ # Prevents re-downloading after reboots (especially on Linux where /tmp is cleared).
44
+ # Default: ./models_cache (relative to project root)
45
+ models_cache_dir: "./models_cache"
46
+
42
47
 
43
48
  # ============================================================================
44
49
  # DOCUMENTS
@@ -58,6 +63,25 @@ documents:
58
63
  # - .csv # CSV data files
59
64
  # - .py # Python source code
60
65
  # - .json # JSON files
66
+ # - .ipynb # Jupyter Notebooks (extracts markdown + code cells)
67
+ # - .mq4 # MetaTrader MQL4 source (opt-in, add to enable)
68
+ # - .mqh # MetaTrader MQL4/5 headers (opt-in, add to enable)
69
+
70
+ # Exclude patterns — skip files/directories matching these patterns.
71
+ # Uses fnmatch glob syntax. Patterns match against relative paths AND
72
+ # individual path components (so "node_modules" matches at any depth).
73
+ #
74
+ # Examples:
75
+ # exclude_patterns:
76
+ # - "node_modules" # Skip node_modules at any depth
77
+ # - ".git" # Skip .git directories
78
+ # - "__pycache__" # Skip Python cache
79
+ # - ".venv" # Skip virtual environments
80
+ # - "*.tmp" # Skip .tmp files anywhere
81
+ # - "drafts/*" # Skip everything in top-level drafts/
82
+ #
83
+ # Default: [] (nothing excluded — all supported files are indexed)
84
+ exclude_patterns: []
61
85
 
62
86
  # How documents are split into searchable chunks.
63
87
  #
@@ -1,6 +1,6 @@
1
1
  """Knowledge RAG MCP Server - Local Retrieval-Augmented Generation System"""
2
2
 
3
- __version__ = "3.2.3"
3
+ __version__ = "3.4.0"
4
4
  __author__ = "Ailton Rocha (Lyon.)"
5
5
 
6
6
  from .config import Config
@@ -1,4 +1,4 @@
1
- """Configuration for Knowledge RAG System v3.1 — YAML-configurable"""
1
+ """Configuration for Knowledge RAG System v3.4.0 — YAML-configurable"""
2
2
 
3
3
  import os
4
4
  from dataclasses import dataclass, field
@@ -15,7 +15,7 @@ import yaml
15
15
  _source_dir = Path(__file__).parent.parent
16
16
 
17
17
 
18
- _SUPPORTED_SUFFIXES = frozenset([".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"])
18
+ _SUPPORTED_SUFFIXES = frozenset([".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv", ".ipynb"])
19
19
 
20
20
 
21
21
  def _has_documents(path: Path) -> bool:
@@ -73,7 +73,19 @@ def _get(section: str, key: str, default):
73
73
  if not isinstance(s, dict):
74
74
  return default
75
75
  val = s.get(key)
76
- return val if val is not None else default
76
+ if val is None:
77
+ return default
78
+ # Skip type check when default is None (caller handles validation)
79
+ if default is None:
80
+ return val
81
+ # YAML parses "yes"/"no" as bool, but explicit string "yes" stays str
82
+ if not isinstance(val, type(default)):
83
+ print(
84
+ f"[WARN] config.yaml: {section}.{key} has wrong type "
85
+ f"(expected {type(default).__name__}, got {type(val).__name__}), using default"
86
+ )
87
+ return default
88
+ return val
77
89
 
78
90
 
79
91
  def _get_top(key: str, default):
@@ -81,7 +93,8 @@ def _get_top(key: str, default):
81
93
  val = _yaml.get(key)
82
94
  if val is None:
83
95
  return default
84
- if not isinstance(val, type(default)):
96
+ if not isinstance(val, dict):
97
+ print(f"[WARN] config.yaml: {key} has wrong type (expected dict, got {type(val).__name__}), using default")
85
98
  return default
86
99
  return val
87
100
 
@@ -155,7 +168,6 @@ _DEFAULT_KEYWORD_ROUTES = {
155
168
  "deserialization",
156
169
  "ysoserial",
157
170
  "upload bypass",
158
- "reverse shell",
159
171
  "web shell",
160
172
  "hash cracking",
161
173
  "hashcat",
@@ -345,6 +357,9 @@ class Config:
345
357
  documents_dir: Path = field(
346
358
  default_factory=lambda: _resolve_path(_get("paths", "documents_dir", None), BASE_DIR / "documents")
347
359
  )
360
+ models_cache_dir: Path = field(
361
+ default_factory=lambda: _resolve_path(_get("paths", "models_cache_dir", None), BASE_DIR / "models_cache")
362
+ )
348
363
 
349
364
  # Chunking
350
365
  chunk_size: int = field(
@@ -407,10 +422,15 @@ class Config:
407
422
  # Supported formats
408
423
  supported_formats: List[str] = field(
409
424
  default_factory=lambda: _get(
410
- "documents", "supported_formats", [".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"]
425
+ "documents",
426
+ "supported_formats",
427
+ [".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv", ".ipynb"],
411
428
  )
412
429
  )
413
430
 
431
+ # Exclude patterns for directory traversal
432
+ exclude_patterns: List[str] = field(default_factory=lambda: _get("documents", "exclude_patterns", []))
433
+
414
434
  # Category mappings
415
435
  category_mappings: Dict[str, str] = field(
416
436
  default_factory=lambda: _get_top("category_mappings", _DEFAULT_CATEGORY_MAPPINGS)
@@ -431,10 +451,52 @@ class Config:
431
451
  max_results: int = field(default_factory=lambda: _get("search", "max_results", 20))
432
452
 
433
453
  def __post_init__(self):
434
- """Ensure directories exist"""
454
+ """Validate config values and ensure directories exist."""
455
+ # Bounds validation
456
+ if not isinstance(self.chunk_size, int) or self.chunk_size < 100:
457
+ print(f"[WARN] chunk_size={self.chunk_size} invalid, using 1000")
458
+ self.chunk_size = 1000
459
+ if not isinstance(self.chunk_overlap, int) or self.chunk_overlap < 0:
460
+ print(f"[WARN] chunk_overlap={self.chunk_overlap} invalid, using 200")
461
+ self.chunk_overlap = 200
462
+ if self.chunk_overlap >= self.chunk_size:
463
+ print(
464
+ f"[WARN] chunk_overlap ({self.chunk_overlap}) >= chunk_size ({self.chunk_size}), using {self.chunk_size // 5}"
465
+ )
466
+ self.chunk_overlap = self.chunk_size // 5
467
+ if not isinstance(self.default_results, int) or self.default_results < 1:
468
+ self.default_results = 5
469
+ if not isinstance(self.max_results, int) or self.max_results < 1:
470
+ self.max_results = 20
471
+ if not isinstance(self.embedding_dim, int) or self.embedding_dim < 1:
472
+ self.embedding_dim = 384
473
+ if not isinstance(self.reranker_enabled, bool):
474
+ print(f"[WARN] reranker_enabled={self.reranker_enabled!r} invalid, using True")
475
+ self.reranker_enabled = True
476
+ if not isinstance(self.reranker_top_k_multiplier, int) or self.reranker_top_k_multiplier < 1:
477
+ self.reranker_top_k_multiplier = 3
478
+ if not isinstance(self.supported_formats, list) or not self.supported_formats:
479
+ print("[WARN] supported_formats is empty or invalid, using defaults")
480
+ self.supported_formats = [".md", ".txt", ".pdf", ".py", ".json", ".docx", ".xlsx", ".pptx", ".csv"]
481
+
482
+ # Validate exclude_patterns is a list of strings
483
+ if not isinstance(self.exclude_patterns, list):
484
+ print(f"[WARN] exclude_patterns={self.exclude_patterns!r} invalid, using []")
485
+ self.exclude_patterns = []
486
+ else:
487
+ self.exclude_patterns = [p for p in self.exclude_patterns if isinstance(p, str)]
488
+
489
+ # Validate keyword_routes values are lists (not strings)
490
+ for cat, keywords in list(self.keyword_routes.items()):
491
+ if not isinstance(keywords, list):
492
+ print(f"[WARN] keyword_routes.{cat} is not a list, removing")
493
+ del self.keyword_routes[cat]
494
+
495
+ # Ensure directories exist
435
496
  self.data_dir.mkdir(parents=True, exist_ok=True)
436
497
  self.chroma_dir.mkdir(parents=True, exist_ok=True)
437
498
  self.documents_dir.mkdir(parents=True, exist_ok=True)
499
+ self.models_cache_dir.mkdir(parents=True, exist_ok=True)
438
500
 
439
501
 
440
502
  # Global config instance
@@ -1,9 +1,10 @@
1
1
  """Document Ingestion System for Knowledge RAG
2
2
 
3
3
  Multi-format document parsing, chunking, and metadata extraction.
4
- Supports: MD, PDF, TXT, PY, JSON, DOCX, XLSX, PPTX, CSV
4
+ Supports: MD, PDF, TXT, PY, JSON, DOCX, XLSX, PPTX, CSV, IPYNB, MQH, MQ4
5
5
  """
6
6
 
7
+ import fnmatch
7
8
  import hashlib
8
9
  import json
9
10
  import os
@@ -103,6 +104,9 @@ class DocumentParser:
103
104
  ".xlsx": self._parse_xlsx,
104
105
  ".pptx": self._parse_pptx,
105
106
  ".csv": self._parse_csv,
107
+ ".ipynb": self._parse_ipynb,
108
+ ".mqh": self._parse_code,
109
+ ".mq4": self._parse_code,
106
110
  }
107
111
 
108
112
  def parse_file(self, filepath: Path) -> Optional[Document]:
@@ -151,12 +155,41 @@ class DocumentParser:
151
155
 
152
156
  return doc
153
157
 
158
+ @staticmethod
159
+ def _should_exclude(path: Path, base_dir: Path, patterns: List[str]) -> bool:
160
+ """Check if a path matches any exclude pattern.
161
+
162
+ Uses fnmatch on the relative path (forward-slash normalized) and
163
+ also checks each path component individually for simple name patterns.
164
+ """
165
+ if not patterns:
166
+ return False
167
+
168
+ try:
169
+ rel = path.relative_to(base_dir)
170
+ except ValueError:
171
+ rel = path
172
+
173
+ rel_str = str(rel).replace("\\", "/")
174
+
175
+ for pattern in patterns:
176
+ # Full relative path match (e.g., "docs/drafts/*.tmp")
177
+ if fnmatch.fnmatch(rel_str, pattern):
178
+ return True
179
+ # Check each component (e.g., "node_modules" matches any/node_modules/deep)
180
+ for part in rel.parts:
181
+ if fnmatch.fnmatch(part, pattern):
182
+ return True
183
+
184
+ return False
185
+
154
186
  def parse_directory(self, directory: Path = None) -> List[Document]:
155
187
  """Parse all supported files in a directory recursively (follows symlinks)."""
156
188
  directory = Path(directory) if directory else config.documents_dir
157
189
  documents = []
158
190
  seen_dirs = set()
159
191
  supported = set(config.supported_formats)
192
+ exclude = config.exclude_patterns
160
193
 
161
194
  for root, dirs, files in os.walk(directory, followlinks=True):
162
195
  real_root = os.path.realpath(root)
@@ -165,10 +198,16 @@ class DocumentParser:
165
198
  continue
166
199
  seen_dirs.add(real_root)
167
200
 
201
+ # Filter out excluded directories in-place (prevents os.walk from descending)
202
+ if exclude:
203
+ dirs[:] = [d for d in dirs if not self._should_exclude(Path(root) / d, directory, exclude)]
204
+
168
205
  for fname in files:
169
206
  filepath = Path(root) / fname
170
207
  if filepath.suffix.lower() not in supported:
171
208
  continue
209
+ if exclude and self._should_exclude(filepath, directory, exclude):
210
+ continue
172
211
  try:
173
212
  doc = self.parse_file(filepath)
174
213
  if doc:
@@ -437,6 +476,59 @@ class DocumentParser:
437
476
  content = "\n".join(parts)
438
477
  return content, metadata
439
478
 
479
+ def _parse_ipynb(self, filepath: Path) -> tuple[str, Dict]:
480
+ """Parse Jupyter Notebook, extracting only markdown and code cell sources.
481
+
482
+ Ignores outputs, execution counts, cell metadata, and base64 images.
483
+ """
484
+ raw = filepath.read_text(encoding="utf-8", errors="ignore")
485
+ metadata = {
486
+ "type": "jupyter_notebook",
487
+ "title": filepath.stem,
488
+ "file_size": filepath.stat().st_size,
489
+ "modified": datetime.fromtimestamp(filepath.stat().st_mtime).isoformat(),
490
+ }
491
+
492
+ try:
493
+ nb = json.loads(raw)
494
+ except json.JSONDecodeError:
495
+ metadata["is_valid_json"] = False
496
+ return raw, metadata
497
+
498
+ metadata["is_valid_json"] = True
499
+ metadata["nbformat"] = nb.get("nbformat", 0)
500
+ kernel = nb.get("metadata", {}).get("kernelspec", {})
501
+ metadata["kernel"] = kernel.get("display_name", kernel.get("name", "unknown"))
502
+
503
+ cells = nb.get("cells", [])
504
+ metadata["cells"] = len(cells)
505
+ code_cells = 0
506
+ markdown_cells = 0
507
+
508
+ parts = []
509
+ for cell in cells:
510
+ cell_type = cell.get("cell_type", "")
511
+ source = cell.get("source", "")
512
+
513
+ if isinstance(source, list):
514
+ source = "".join(source)
515
+
516
+ if not source or not source.strip():
517
+ continue
518
+
519
+ if cell_type == "markdown":
520
+ parts.append(source)
521
+ markdown_cells += 1
522
+ elif cell_type == "code":
523
+ parts.append(f"```python\n{source}\n```")
524
+ code_cells += 1
525
+
526
+ metadata["code_cells"] = code_cells
527
+ metadata["markdown_cells"] = markdown_cells
528
+
529
+ content = "\n\n".join(parts)
530
+ return content, metadata
531
+
440
532
  # =========================================================================
441
533
  # Chunking
442
534
  # =========================================================================
@@ -19,8 +19,8 @@ Features:
19
19
  - CRUD operations via MCP tools (add, update, remove docs)
20
20
 
21
21
  Autor: Lyon (Ailton Rocha)
22
- Versao: 3.0.0
23
- Data: 2026-03-19
22
+ Versao: 3.4.0
23
+ Data: 2026-04-16
24
24
  """
25
25
 
26
26
  import hashlib
@@ -141,7 +141,7 @@ class FastEmbedEmbeddings:
141
141
  self.model_name = model or config.embedding_model
142
142
  self._dim = config.embedding_dim
143
143
  print(f"[INFO] Loading embedding model: {self.model_name} ({self._dim}D)...")
144
- self._model = TextEmbedding(model_name=self.model_name)
144
+ self._model = TextEmbedding(model_name=self.model_name, cache_dir=str(config.models_cache_dir))
145
145
  print("[INFO] Embedding model loaded successfully")
146
146
 
147
147
  def __call__(self, input: List[str]) -> List[List[float]]:
@@ -1836,32 +1836,37 @@ def _handle_init():
1836
1836
 
1837
1837
  cwd = Path.cwd()
1838
1838
 
1839
- # Copy config.example.yaml
1840
- src = data_dir / "config.example.yaml"
1841
- if src.exists():
1842
- dst = cwd / "config.example.yaml"
1843
- shutil.copy2(src, dst)
1844
- print(f"[OK] {dst}")
1845
-
1846
- # Copy presets
1847
- presets_dir = cwd / "presets"
1848
- presets_dir.mkdir(exist_ok=True)
1849
- for f in data_dir.glob("*.yaml"):
1850
- if f.name == "config.example.yaml":
1851
- continue
1852
- dst = presets_dir / f.name
1853
- shutil.copy2(f, dst)
1854
- print(f"[OK] {dst}")
1855
-
1856
- # Create documents dir
1857
- docs_dir = cwd / "documents"
1858
- docs_dir.mkdir(exist_ok=True)
1859
- print(f"[OK] {docs_dir}/")
1860
-
1861
- print("\nDone. Quick start:")
1862
- print(" cp presets/general.yaml config.yaml # or cybersecurity, developer, research")
1863
- print(" # Add your documents to documents/")
1864
- print(" # Restart Claude Code")
1839
+ try:
1840
+ # Copy config.example.yaml
1841
+ src = data_dir / "config.example.yaml"
1842
+ if src.exists():
1843
+ dst = cwd / "config.example.yaml"
1844
+ shutil.copy2(src, dst)
1845
+ print(f"[OK] {dst}")
1846
+
1847
+ # Copy presets
1848
+ presets_dir = cwd / "presets"
1849
+ presets_dir.mkdir(exist_ok=True)
1850
+ for f in data_dir.glob("*.yaml"):
1851
+ if f.name == "config.example.yaml":
1852
+ continue
1853
+ dst = presets_dir / f.name
1854
+ shutil.copy2(f, dst)
1855
+ print(f"[OK] {dst}")
1856
+
1857
+ # Create documents dir
1858
+ docs_dir = cwd / "documents"
1859
+ docs_dir.mkdir(exist_ok=True)
1860
+ print(f"[OK] {docs_dir}/")
1861
+
1862
+ print("\nDone. Quick start:")
1863
+ print(" cp presets/general.yaml config.yaml # or cybersecurity, developer, research")
1864
+ print(" # Add your documents to documents/")
1865
+ print(" # Restart Claude Code")
1866
+ except PermissionError:
1867
+ print("[ERROR] Permission denied. Run from a writable directory.")
1868
+ except OSError as e:
1869
+ print(f"[ERROR] Failed to write files: {e}")
1865
1870
 
1866
1871
 
1867
1872
  def main():
@@ -1894,12 +1899,21 @@ def main():
1894
1899
  print(f"[INFO] Indexed {stats['indexed']} documents with {stats['chunks_added']} chunks")
1895
1900
 
1896
1901
  # Start file watcher for auto-reindex on document changes
1897
- watcher = DocumentWatcher(get_orchestrator, debounce_seconds=5.0)
1898
- observer = Observer()
1899
- observer.schedule(watcher, str(config.documents_dir), recursive=True)
1900
- observer.daemon = True
1901
- observer.start()
1902
- print(f"[WATCHER] Monitoring {config.documents_dir} for changes")
1902
+ try:
1903
+ watcher = DocumentWatcher(get_orchestrator, debounce_seconds=5.0)
1904
+ observer = Observer()
1905
+ observer.schedule(watcher, str(config.documents_dir), recursive=True)
1906
+ observer.daemon = True
1907
+ observer.start()
1908
+ print(f"[WATCHER] Monitoring {config.documents_dir} for changes")
1909
+ except Exception as e:
1910
+ print(f"[WARN] Failed to start file watcher: {e}")
1911
+ print("[WARN] Auto-reindexing disabled. Use reindex_documents tool manually.")
1912
+
1913
+ # Redirect stdout to stderr to protect MCP stdio JSON-RPC stream.
1914
+ # Libraries (fastembed, chromadb, watchdog) may print() at runtime,
1915
+ # which would corrupt the protocol. This ONLY affects the server process.
1916
+ sys.stdout = sys.stderr
1903
1917
 
1904
1918
  mcp.run()
1905
1919
 
@@ -22,6 +22,7 @@
22
22
  paths:
23
23
  documents_dir: "./documents"
24
24
  data_dir: "./data"
25
+ models_cache_dir: "./models_cache"
25
26
 
26
27
 
27
28
  # ============================================================================
@@ -39,6 +40,9 @@ documents:
39
40
  - .xlsx
40
41
  - .pptx
41
42
  - .csv
43
+ - .ipynb # Jupyter Notebooks (exploit dev, research)
44
+
45
+ exclude_patterns: []
42
46
 
43
47
  chunking:
44
48
  chunk_size: 1000
@@ -17,6 +17,7 @@
17
17
  paths:
18
18
  documents_dir: "./documents"
19
19
  data_dir: "./data"
20
+ models_cache_dir: "./models_cache"
20
21
 
21
22
 
22
23
  # ============================================================================
@@ -32,6 +33,16 @@ documents:
32
33
  - .py # Python source code
33
34
  - .json # API schemas, configs
34
35
  - .csv # Data files, logs
36
+ - .ipynb # Jupyter Notebooks
37
+
38
+ exclude_patterns:
39
+ - "node_modules"
40
+ - ".venv"
41
+ - "__pycache__"
42
+ - ".git"
43
+ - "dist"
44
+ - "build"
45
+ - ".next"
35
46
 
36
47
  chunking:
37
48
  chunk_size: 1200 # Slightly larger for code + prose docs
@@ -26,6 +26,7 @@
26
26
  paths:
27
27
  documents_dir: "./documents"
28
28
  data_dir: "./data"
29
+ models_cache_dir: "./models_cache"
29
30
 
30
31
 
31
32
  # ============================================================================
@@ -39,6 +40,8 @@ documents:
39
40
  - .pdf
40
41
  - .docx
41
42
 
43
+ exclude_patterns: []
44
+
42
45
  chunking:
43
46
  chunk_size: 1000
44
47
  chunk_overlap: 200
@@ -17,6 +17,7 @@
17
17
  paths:
18
18
  documents_dir: "./documents"
19
19
  data_dir: "./data"
20
+ models_cache_dir: "./models_cache"
20
21
 
21
22
 
22
23
  # ============================================================================
@@ -32,6 +33,9 @@ documents:
32
33
  - .xlsx # Data tables, survey results
33
34
  - .pptx # Presentations, lecture slides
34
35
  - .csv # Experiment data, datasets
36
+ - .ipynb # Jupyter Notebooks (analysis, experiments)
37
+
38
+ exclude_patterns: []
35
39
 
36
40
  chunking:
37
41
  chunk_size: 1500 # Larger chunks — academic text needs more context
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "knowledge-rag"
7
- version = "3.3.1"
7
+ version = "3.4.0"
8
8
  description = "Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 12 MCP Tools. Zero external servers."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes