codebuff-agent 2.1.74__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 (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,339 @@
1
+ """
2
+ Knowledge Base - Deterministic library detection for API guides
3
+
4
+ Keyword matching + regex based library detection for loading appropriate API guides.
5
+ (No LLM calls - saves tokens and improves reliability)
6
+ """
7
+
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Set
11
+
12
+ # Library descriptions (reference)
13
+ LIBRARY_DESCRIPTIONS: Dict[str, str] = {
14
+ "matplotlib": "Visualization, graphs, charts, plot, histogram, scatter plot, EDA, data visualization, used with seaborn",
15
+ "dask": "Large-scale data processing, pandas replacement, distributed processing, lazy evaluation, dd.read_csv",
16
+ "polars": "High-performance DataFrame, pandas replacement, Rust-based, pl.read_csv",
17
+ "pyspark": "Spark-based distributed processing, big data, SparkSession",
18
+ "vaex": "Large-scale data exploration, out-of-core processing",
19
+ "modin": "pandas acceleration, parallel processing",
20
+ "ray": "Distributed computing, parallel processing framework",
21
+ }
22
+
23
+
24
+ class LibraryDetector:
25
+ """
26
+ Deterministic library detection (no LLM calls).
27
+ Uses keyword matching + regex to detect required libraries from user requests.
28
+ """
29
+
30
+ # Explicit library mention patterns (highest priority)
31
+ EXPLICIT_PATTERNS: Dict[str, str] = {
32
+ r"\bdask\b": "dask",
33
+ r"\bpolars\b": "polars",
34
+ r"\bpyspark\b": "pyspark",
35
+ r"\bvaex\b": "vaex",
36
+ r"\bmodin\b": "modin",
37
+ r"\bray\b": "ray",
38
+ r"\bmatplotlib\b": "matplotlib",
39
+ r"\bseaborn\b": "matplotlib", # seaborn -> matplotlib guide
40
+ r"\bplt\.": "matplotlib",
41
+ r"\bdd\.read": "dask",
42
+ r"\bpl\.read": "polars",
43
+ r"\bpl\.DataFrame": "polars",
44
+ }
45
+
46
+ # Keyword scores per library (0.0 ~ 1.0)
47
+ KEYWORD_SCORES: Dict[str, Dict[str, float]] = {
48
+ "dask": {
49
+ "대용량": 0.7,
50
+ "big data": 0.7,
51
+ "bigdata": 0.7,
52
+ "빅데이터": 0.7,
53
+ "lazy": 0.8,
54
+ "lazy evaluation": 0.9,
55
+ "out-of-core": 0.9,
56
+ "out of core": 0.9,
57
+ "분산 처리": 0.6,
58
+ "distributed": 0.6,
59
+ "parallel dataframe": 0.8,
60
+ "병렬 데이터프레임": 0.8,
61
+ },
62
+ "polars": {
63
+ "rust 기반": 0.9,
64
+ "rust-based": 0.9,
65
+ "fast dataframe": 0.7,
66
+ "고성능 dataframe": 0.7,
67
+ "빠른 데이터프레임": 0.7,
68
+ },
69
+ "matplotlib": {
70
+ "시각화": 0.7,
71
+ "visualization": 0.7,
72
+ "visualize": 0.7,
73
+ "plot": 0.7,
74
+ "chart": 0.7,
75
+ "graph": 0.6,
76
+ "그래프": 0.6,
77
+ "차트": 0.7,
78
+ "histogram": 0.8,
79
+ "히스토그램": 0.8,
80
+ "scatter": 0.8,
81
+ "산점도": 0.8,
82
+ "line plot": 0.8,
83
+ "라인 플롯": 0.8,
84
+ "bar chart": 0.8,
85
+ "막대 그래프": 0.8,
86
+ "eda": 0.5,
87
+ "탐색적 데이터 분석": 0.6,
88
+ "figure": 0.5,
89
+ "subplot": 0.8,
90
+ "heatmap": 0.7,
91
+ "히트맵": 0.7,
92
+ },
93
+ "pyspark": {
94
+ "spark": 0.9,
95
+ "sparksession": 0.95,
96
+ "spark session": 0.95,
97
+ "rdd": 0.9,
98
+ "hadoop": 0.7,
99
+ "클러스터": 0.6,
100
+ "cluster": 0.6,
101
+ },
102
+ "vaex": {
103
+ "vaex": 1.0,
104
+ "memory mapping": 0.8,
105
+ "메모리 매핑": 0.8,
106
+ },
107
+ "modin": {
108
+ "modin": 1.0,
109
+ "pandas 가속": 0.8,
110
+ "pandas acceleration": 0.8,
111
+ },
112
+ "ray": {
113
+ "ray": 0.9,
114
+ "분산 컴퓨팅": 0.7,
115
+ "distributed computing": 0.7,
116
+ },
117
+ }
118
+
119
+ # Score threshold
120
+ SCORE_THRESHOLD = 0.7
121
+
122
+ def detect(
123
+ self,
124
+ request: str,
125
+ available_libraries: List[str],
126
+ imported_libraries: List[str] = None,
127
+ ) -> List[str]:
128
+ """
129
+ Detect required libraries from user request.
130
+
131
+ Args:
132
+ request: User's natural language request
133
+ available_libraries: List of libraries with available guides
134
+ imported_libraries: Already imported libraries (optional)
135
+
136
+ Returns:
137
+ List of detected libraries
138
+ """
139
+ request_lower = request.lower()
140
+ detected: Set[str] = set()
141
+
142
+ # Step 1: Explicit pattern matching (highest priority)
143
+ for pattern, lib in self.EXPLICIT_PATTERNS.items():
144
+ if lib in available_libraries and re.search(
145
+ pattern, request, re.IGNORECASE
146
+ ):
147
+ detected.add(lib)
148
+
149
+ # Step 2: Keyword scoring
150
+ for lib, keywords in self.KEYWORD_SCORES.items():
151
+ if lib in detected or lib not in available_libraries:
152
+ continue
153
+
154
+ max_score = 0.0
155
+ for keyword, score in keywords.items():
156
+ if keyword.lower() in request_lower:
157
+ max_score = max(max_score, score)
158
+
159
+ if max_score >= self.SCORE_THRESHOLD:
160
+ detected.add(lib)
161
+
162
+ # Step 3: Consider already imported libraries
163
+ if imported_libraries:
164
+ for lib in imported_libraries:
165
+ lib_lower = lib.lower()
166
+ # seaborn -> matplotlib
167
+ if lib_lower == "seaborn" and "matplotlib" in available_libraries:
168
+ detected.add("matplotlib")
169
+ elif lib_lower in available_libraries:
170
+ detected.add(lib_lower)
171
+
172
+ return list(detected)
173
+
174
+
175
+ # LibraryDetector singleton instance
176
+ _library_detector_instance: Optional[LibraryDetector] = None
177
+
178
+
179
+ def get_library_detector() -> LibraryDetector:
180
+ """Get singleton LibraryDetector"""
181
+ global _library_detector_instance
182
+ if _library_detector_instance is None:
183
+ _library_detector_instance = LibraryDetector()
184
+ return _library_detector_instance
185
+
186
+
187
+ # LLM library detection prompt
188
+ LIBRARY_DETECTION_PROMPT = """Analyze the user's request and determine which libraries to use for code generation.
189
+
190
+ ## Available Library API Guides:
191
+ {library_list}
192
+
193
+ ## User Request:
194
+ {request}
195
+
196
+ ## Notebook Context:
197
+ - Already imported libraries: {imported_libraries}
198
+
199
+ ## Instructions:
200
+ 1. Analyze the user request **semantically**
201
+ 2. Select only libraries that will actually be used in code generation
202
+ 3. Example: "Apply dask" → select dask
203
+ 4. Example: "EDA with visualization" → select matplotlib
204
+ 5. Example: "Use dask instead of pandas" → select dask
205
+
206
+ ## Output Format (JSON only):
207
+ {{"libraries": ["library1", "library2"]}}
208
+
209
+ Empty array is also valid: {{"libraries": []}}
210
+ """
211
+
212
+
213
+ class KnowledgeBase:
214
+ """Library knowledge loader"""
215
+
216
+ def __init__(self, knowledge_dir: Optional[str] = None):
217
+ if knowledge_dir:
218
+ self.knowledge_dir = Path(knowledge_dir)
219
+ else:
220
+ # Default path: knowledge/libraries
221
+ self.knowledge_dir = Path(__file__).parent / "libraries"
222
+
223
+ self._cache: Dict[str, str] = {}
224
+
225
+ def get_library_list_for_prompt(self) -> str:
226
+ """Generate library list for LLM detection"""
227
+ available = self.list_available_libraries()
228
+ lines = []
229
+ for lib in available:
230
+ desc = LIBRARY_DESCRIPTIONS.get(lib, "Other library")
231
+ lines.append(f"- **{lib}**: {desc}")
232
+ return "\n".join(lines)
233
+
234
+ def get_detection_prompt(
235
+ self, request: str, imported_libraries: List[str] = None
236
+ ) -> str:
237
+ """Generate LLM library detection prompt"""
238
+ library_list = self.get_library_list_for_prompt()
239
+ imported = ", ".join(imported_libraries) if imported_libraries else "None"
240
+
241
+ return LIBRARY_DETECTION_PROMPT.format(
242
+ library_list=library_list, request=request, imported_libraries=imported
243
+ )
244
+
245
+ def load_library_guide(self, library: str) -> Optional[str]:
246
+ """
247
+ Load library guide file.
248
+
249
+ Args:
250
+ library: Library name (e.g., 'dask', 'polars')
251
+
252
+ Returns:
253
+ Guide content or None
254
+ """
255
+ # Check cache
256
+ if library in self._cache:
257
+ return self._cache[library]
258
+
259
+ # Load file
260
+ file_path = self.knowledge_dir / f"{library}.md"
261
+ if file_path.exists():
262
+ content = file_path.read_text(encoding="utf-8")
263
+ self._cache[library] = content
264
+ return content
265
+
266
+ return None
267
+
268
+ def load_libraries_knowledge(self, libraries: List[str]) -> str:
269
+ """
270
+ Load guides for specified libraries.
271
+
272
+ Args:
273
+ libraries: List of library names
274
+
275
+ Returns:
276
+ Combined guide string
277
+ """
278
+ if not libraries:
279
+ return ""
280
+
281
+ guides = []
282
+ for lib in sorted(libraries):
283
+ guide = self.load_library_guide(lib)
284
+ if guide:
285
+ guides.append(f"## {lib.upper()} Library API Guide\n\n{guide}")
286
+
287
+ if not guides:
288
+ return ""
289
+
290
+ return "\n\n---\n\n".join(guides)
291
+
292
+ def format_knowledge_section(self, libraries: List[str]) -> str:
293
+ """
294
+ Format knowledge section for prompt injection.
295
+
296
+ Args:
297
+ libraries: List of libraries detected by LLM
298
+
299
+ Returns:
300
+ Formatted knowledge section (empty string if none)
301
+ """
302
+ knowledge = self.load_libraries_knowledge(libraries)
303
+
304
+ if not knowledge:
305
+ return ""
306
+
307
+ return f"""
308
+ ## 📚 Library API Reference (MUST follow!)
309
+
310
+ Follow the API usage in the guides below. Avoid ❌ incorrect code and use ✅ correct code.
311
+
312
+ {knowledge}
313
+
314
+ ---
315
+ """
316
+
317
+ def list_available_libraries(self) -> List[str]:
318
+ """List available library guides"""
319
+ if not self.knowledge_dir.exists():
320
+ return []
321
+
322
+ return [f.stem for f in self.knowledge_dir.glob("*.md")]
323
+
324
+
325
+ # Singleton instance
326
+ _knowledge_base_instance: Optional[KnowledgeBase] = None
327
+
328
+
329
+ def get_knowledge_base() -> KnowledgeBase:
330
+ """Get singleton KnowledgeBase"""
331
+ global _knowledge_base_instance
332
+ if _knowledge_base_instance is None:
333
+ _knowledge_base_instance = KnowledgeBase()
334
+ return _knowledge_base_instance
335
+
336
+
337
+ # Alias (backward compatibility)
338
+ KnowledgeLoader = KnowledgeBase
339
+ get_knowledge_loader = get_knowledge_base
@@ -0,0 +1,354 @@
1
+ """
2
+ Watchdog Service - File system monitoring for automatic RAG re-indexing.
3
+
4
+ Features:
5
+ - Monitors knowledge base directory for changes
6
+ - Debounces rapid file changes
7
+ - Triggers incremental re-indexing
8
+ - Pattern-based file filtering
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ import threading
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING, Callable, Optional, Set
19
+
20
+ if TYPE_CHECKING:
21
+ from codebuff_core.models.rag import WatchdogConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class WatchdogService:
27
+ """
28
+ File system monitoring service for RAG knowledge base.
29
+
30
+ Features:
31
+ - Asynchronous file change detection
32
+ - Debouncing to prevent excessive re-indexing
33
+ - Pattern-based filtering (*.md, *.py, etc.)
34
+ - Ignore patterns for cache/build directories
35
+ - Callback-based notification
36
+
37
+ Usage:
38
+ service = WatchdogService(config, on_change_callback)
39
+ service.start("/path/to/knowledge")
40
+ # ... later
41
+ service.stop()
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ config: "WatchdogConfig",
47
+ on_change_callback: Optional[Callable[[Set[Path]], None]] = None,
48
+ ):
49
+ self._config = config
50
+ self._on_change = on_change_callback
51
+ self._observer = None
52
+ self._running = False
53
+ self._pending_changes: Set[Path] = set()
54
+ self._last_change_time: Optional[datetime] = None
55
+ self._debounce_task: Optional[asyncio.Task] = None
56
+ self._lock = threading.Lock()
57
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
58
+
59
+ def start(self, watch_path: Path) -> bool:
60
+ """
61
+ Start monitoring the specified directory.
62
+
63
+ Args:
64
+ watch_path: Directory to monitor
65
+
66
+ Returns:
67
+ True if monitoring started successfully
68
+ """
69
+ if not self._config.enabled:
70
+ logger.info("Watchdog disabled by configuration")
71
+ return False
72
+
73
+ if self._running:
74
+ logger.warning("Watchdog already running")
75
+ return True
76
+
77
+ try:
78
+ from watchdog.events import FileSystemEventHandler
79
+ from watchdog.observers import Observer
80
+ except ImportError:
81
+ logger.warning(
82
+ "watchdog not installed, file monitoring disabled. "
83
+ "Install with: pip install watchdog"
84
+ )
85
+ return False
86
+
87
+ if not watch_path.exists():
88
+ logger.warning(f"Watch path does not exist: {watch_path}")
89
+ return False
90
+
91
+ # Store event loop for async callbacks
92
+ try:
93
+ self._loop = asyncio.get_running_loop()
94
+ except RuntimeError:
95
+ self._loop = None
96
+
97
+ # Create event handler
98
+ handler = self._create_event_handler(FileSystemEventHandler)
99
+
100
+ # Start observer
101
+ self._observer = Observer()
102
+ self._observer.schedule(handler, str(watch_path), recursive=True)
103
+ self._observer.start()
104
+ self._running = True
105
+
106
+ logger.info(f"Watchdog started monitoring: {watch_path}")
107
+ return True
108
+
109
+ def _create_event_handler(self, base_class):
110
+ """Create a watchdog event handler class."""
111
+ service = self
112
+
113
+ class RAGEventHandler(base_class):
114
+ def on_any_event(self, event):
115
+ if event.is_directory:
116
+ return
117
+
118
+ # Get file path
119
+ file_path = Path(event.src_path)
120
+
121
+ # Check if file matches patterns
122
+ if not service._should_process(file_path):
123
+ return
124
+
125
+ # Queue the change
126
+ service._queue_change(file_path)
127
+
128
+ return RAGEventHandler()
129
+
130
+ def _should_process(self, file_path: Path) -> bool:
131
+ """Check if file should trigger re-indexing."""
132
+ file_name = file_path.name
133
+
134
+ # Check ignore patterns
135
+ for pattern in self._config.ignore_patterns:
136
+ if file_name.startswith(pattern.replace("*", "")):
137
+ return False
138
+ if pattern.startswith("*") and file_name.endswith(pattern[1:]):
139
+ return False
140
+
141
+ # Check include patterns
142
+ for pattern in self._config.patterns:
143
+ if pattern.startswith("*"):
144
+ suffix = pattern[1:]
145
+ if file_name.endswith(suffix):
146
+ return True
147
+ elif file_name == pattern:
148
+ return True
149
+
150
+ return False
151
+
152
+ def _queue_change(self, file_path: Path) -> None:
153
+ """Queue a file change for processing."""
154
+ with self._lock:
155
+ self._pending_changes.add(file_path)
156
+ self._last_change_time = datetime.now()
157
+
158
+ logger.debug(f"Queued change: {file_path}")
159
+
160
+ # Schedule debounced processing
161
+ if self._loop and self._loop.is_running():
162
+ self._loop.call_soon_threadsafe(self._schedule_debounce)
163
+
164
+ def _schedule_debounce(self) -> None:
165
+ """Schedule the debounced callback."""
166
+ if self._debounce_task and not self._debounce_task.done():
167
+ self._debounce_task.cancel()
168
+
169
+ self._debounce_task = asyncio.create_task(self._debounced_callback())
170
+
171
+ async def _debounced_callback(self) -> None:
172
+ """Wait for debounce period then trigger callback."""
173
+ try:
174
+ await asyncio.sleep(self._config.debounce_seconds)
175
+
176
+ # Get pending changes
177
+ with self._lock:
178
+ if not self._pending_changes:
179
+ return
180
+
181
+ changes = self._pending_changes.copy()
182
+ self._pending_changes.clear()
183
+
184
+ logger.info(f"Processing {len(changes)} file changes")
185
+
186
+ # Trigger callback
187
+ if self._on_change:
188
+ try:
189
+ self._on_change(changes)
190
+ except Exception as e:
191
+ logger.error(f"Error in change callback: {e}")
192
+
193
+ except asyncio.CancelledError:
194
+ # Debounce was reset, ignore
195
+ pass
196
+ except Exception as e:
197
+ logger.error(f"Error in debounced callback: {e}")
198
+
199
+ def stop(self) -> None:
200
+ """Stop file monitoring."""
201
+ if not self._running:
202
+ return
203
+
204
+ if self._observer:
205
+ self._observer.stop()
206
+ self._observer.join(timeout=5)
207
+ self._observer = None
208
+
209
+ if self._debounce_task and not self._debounce_task.done():
210
+ self._debounce_task.cancel()
211
+
212
+ self._running = False
213
+ self._pending_changes.clear()
214
+
215
+ logger.info("Watchdog stopped")
216
+
217
+ @property
218
+ def is_running(self) -> bool:
219
+ """Check if watchdog is currently running."""
220
+ return self._running
221
+
222
+ @property
223
+ def pending_count(self) -> int:
224
+ """Get number of pending changes."""
225
+ with self._lock:
226
+ return len(self._pending_changes)
227
+
228
+
229
+ class SimpleWatchdog:
230
+ """
231
+ Simplified watchdog for polling-based monitoring.
232
+
233
+ Use this when the watchdog library is not available
234
+ or for environments where filesystem events are unreliable.
235
+ """
236
+
237
+ def __init__(
238
+ self,
239
+ patterns: list[str] = None,
240
+ check_interval: float = 10.0,
241
+ on_change_callback: Optional[Callable[[Set[Path]], None]] = None,
242
+ ):
243
+ self._patterns = patterns or ["*.md", "*.py", "*.txt"]
244
+ self._check_interval = check_interval
245
+ self._on_change = on_change_callback
246
+ self._running = False
247
+ self._file_mtimes: dict[Path, float] = {}
248
+ self._check_task: Optional[asyncio.Task] = None
249
+
250
+ async def start(self, watch_path: Path) -> bool:
251
+ """Start polling-based monitoring."""
252
+ if self._running:
253
+ return True
254
+
255
+ if not watch_path.exists():
256
+ logger.warning(f"Watch path does not exist: {watch_path}")
257
+ return False
258
+
259
+ self._running = True
260
+ self._check_task = asyncio.create_task(self._poll_loop(watch_path))
261
+
262
+ logger.info(f"SimpleWatchdog started polling: {watch_path}")
263
+ return True
264
+
265
+ async def _poll_loop(self, watch_path: Path) -> None:
266
+ """Main polling loop."""
267
+ # Initial scan
268
+ self._scan_directory(watch_path)
269
+
270
+ while self._running:
271
+ try:
272
+ await asyncio.sleep(self._check_interval)
273
+
274
+ if not self._running:
275
+ break
276
+
277
+ # Check for changes
278
+ changes = self._check_changes(watch_path)
279
+
280
+ if changes and self._on_change:
281
+ try:
282
+ self._on_change(changes)
283
+ except Exception as e:
284
+ logger.error(f"Error in change callback: {e}")
285
+
286
+ except asyncio.CancelledError:
287
+ break
288
+ except Exception as e:
289
+ logger.error(f"Error in poll loop: {e}")
290
+
291
+ def _scan_directory(self, watch_path: Path) -> None:
292
+ """Scan directory and record file mtimes."""
293
+ for pattern in self._patterns:
294
+ for file_path in watch_path.rglob(pattern.lstrip("*")):
295
+ if file_path.is_file():
296
+ try:
297
+ self._file_mtimes[file_path] = file_path.stat().st_mtime
298
+ except OSError:
299
+ pass
300
+
301
+ def _check_changes(self, watch_path: Path) -> Set[Path]:
302
+ """Check for file changes since last scan."""
303
+ changes = set()
304
+ current_files: Set[Path] = set()
305
+
306
+ # Check existing files
307
+ for pattern in self._patterns:
308
+ suffix = pattern.lstrip("*")
309
+ for file_path in watch_path.rglob(f"*{suffix}"):
310
+ if not file_path.is_file():
311
+ continue
312
+
313
+ current_files.add(file_path)
314
+
315
+ try:
316
+ mtime = file_path.stat().st_mtime
317
+ except OSError:
318
+ continue
319
+
320
+ # Check if new or modified
321
+ if file_path not in self._file_mtimes:
322
+ changes.add(file_path)
323
+ self._file_mtimes[file_path] = mtime
324
+ elif self._file_mtimes[file_path] != mtime:
325
+ changes.add(file_path)
326
+ self._file_mtimes[file_path] = mtime
327
+
328
+ # Check for deleted files
329
+ deleted = set(self._file_mtimes.keys()) - current_files
330
+ for file_path in deleted:
331
+ del self._file_mtimes[file_path]
332
+ # Note: We don't add deleted files to changes
333
+ # The RAG system should handle missing files gracefully
334
+
335
+ return changes
336
+
337
+ async def stop(self) -> None:
338
+ """Stop polling."""
339
+ self._running = False
340
+
341
+ if self._check_task and not self._check_task.done():
342
+ self._check_task.cancel()
343
+ try:
344
+ await self._check_task
345
+ except asyncio.CancelledError:
346
+ pass
347
+
348
+ self._file_mtimes.clear()
349
+ logger.info("SimpleWatchdog stopped")
350
+
351
+ @property
352
+ def is_running(self) -> bool:
353
+ """Check if watchdog is currently running."""
354
+ return self._running