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.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- 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
|