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,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Document Chunking - Text splitters for different document formats.
|
|
3
|
+
|
|
4
|
+
Provides intelligent chunking strategies:
|
|
5
|
+
- Markdown: Split by headers for semantic boundaries
|
|
6
|
+
- Python: Split by class/function definitions
|
|
7
|
+
- Plain text: Character-based with overlap
|
|
8
|
+
|
|
9
|
+
Each strategy preserves context and adds relevant metadata.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import logging
|
|
14
|
+
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from codebuff_core.models.rag import ChunkingConfig
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DocumentChunker:
|
|
24
|
+
"""
|
|
25
|
+
Intelligent document chunking with format-aware splitting.
|
|
26
|
+
|
|
27
|
+
Strategies:
|
|
28
|
+
- Markdown: Split by headers (##) for semantic boundaries
|
|
29
|
+
- Python: Split by class/function definitions
|
|
30
|
+
- Plain text: Character-based with sentence boundary respect
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
chunker = DocumentChunker(config)
|
|
34
|
+
chunks = chunker.chunk_document(content, metadata={"source": "file.md"})
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config: Optional["ChunkingConfig"] = None):
|
|
38
|
+
from codebuff_core.models.rag import ChunkingConfig
|
|
39
|
+
|
|
40
|
+
self._config = config or ChunkingConfig()
|
|
41
|
+
|
|
42
|
+
def chunk_document(
|
|
43
|
+
self,
|
|
44
|
+
content: str,
|
|
45
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
46
|
+
file_type: Optional[str] = None,
|
|
47
|
+
) -> List[Dict[str, Any]]:
|
|
48
|
+
"""
|
|
49
|
+
Chunk document based on content type.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
content: Document content
|
|
53
|
+
metadata: Base metadata for all chunks
|
|
54
|
+
file_type: Override file type detection ("markdown", "python", "text")
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of chunks with content and metadata
|
|
58
|
+
"""
|
|
59
|
+
metadata = metadata or {}
|
|
60
|
+
|
|
61
|
+
# Infer file type from metadata if not provided
|
|
62
|
+
if not file_type:
|
|
63
|
+
source = metadata.get("source", "")
|
|
64
|
+
file_type = self._infer_file_type(source)
|
|
65
|
+
|
|
66
|
+
# Route to appropriate chunker
|
|
67
|
+
if file_type == "markdown" and self._config.split_by_header:
|
|
68
|
+
chunks = self._chunk_markdown(content)
|
|
69
|
+
elif file_type == "python":
|
|
70
|
+
chunks = self._chunk_python(content)
|
|
71
|
+
else:
|
|
72
|
+
chunks = self._chunk_text(content)
|
|
73
|
+
|
|
74
|
+
# Filter by minimum size and add metadata
|
|
75
|
+
result = []
|
|
76
|
+
for chunk in chunks:
|
|
77
|
+
chunk_content = chunk["content"].strip()
|
|
78
|
+
if len(chunk_content) >= self._config.min_chunk_size:
|
|
79
|
+
result.append(
|
|
80
|
+
{
|
|
81
|
+
"content": chunk_content,
|
|
82
|
+
"metadata": {**metadata, **chunk.get("metadata", {})},
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.debug(f"Chunked document into {len(result)} chunks (type={file_type})")
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def _infer_file_type(self, source: str) -> str:
|
|
90
|
+
"""Infer file type from source path"""
|
|
91
|
+
source_lower = source.lower()
|
|
92
|
+
if source_lower.endswith(".md"):
|
|
93
|
+
return "markdown"
|
|
94
|
+
elif source_lower.endswith(".py"):
|
|
95
|
+
return "python"
|
|
96
|
+
elif source_lower.endswith((".txt", ".json", ".yaml", ".yml")):
|
|
97
|
+
return "text"
|
|
98
|
+
else:
|
|
99
|
+
return "text"
|
|
100
|
+
|
|
101
|
+
def _chunk_markdown(self, content: str) -> List[Dict[str, Any]]:
|
|
102
|
+
"""
|
|
103
|
+
Split markdown by headers while preserving context.
|
|
104
|
+
|
|
105
|
+
Strategy:
|
|
106
|
+
- Split on header boundaries (# ## ### etc.)
|
|
107
|
+
- Track header hierarchy for section context
|
|
108
|
+
- Respect max chunk size with sub-splitting
|
|
109
|
+
"""
|
|
110
|
+
# Pattern for markdown headers
|
|
111
|
+
header_pattern = r"^(#{1,6})\s+(.+)$"
|
|
112
|
+
|
|
113
|
+
lines = content.split("\n")
|
|
114
|
+
chunks = []
|
|
115
|
+
current_chunk_lines = []
|
|
116
|
+
current_headers = [] # Stack of (level, text)
|
|
117
|
+
|
|
118
|
+
for line in lines:
|
|
119
|
+
header_match = re.match(header_pattern, line)
|
|
120
|
+
|
|
121
|
+
if header_match:
|
|
122
|
+
# Save current chunk if it has content
|
|
123
|
+
if current_chunk_lines:
|
|
124
|
+
chunk_content = "\n".join(current_chunk_lines).strip()
|
|
125
|
+
if chunk_content:
|
|
126
|
+
section_path = (
|
|
127
|
+
" > ".join(h[1] for h in current_headers)
|
|
128
|
+
if current_headers
|
|
129
|
+
else "Introduction"
|
|
130
|
+
)
|
|
131
|
+
chunks.append(
|
|
132
|
+
{
|
|
133
|
+
"content": chunk_content,
|
|
134
|
+
"metadata": {"section": section_path},
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Update header stack
|
|
139
|
+
level = len(header_match.group(1))
|
|
140
|
+
header_text = header_match.group(2).strip()
|
|
141
|
+
|
|
142
|
+
# Pop headers of same or lower level
|
|
143
|
+
while current_headers and current_headers[-1][0] >= level:
|
|
144
|
+
current_headers.pop()
|
|
145
|
+
|
|
146
|
+
current_headers.append((level, header_text))
|
|
147
|
+
current_chunk_lines = [line]
|
|
148
|
+
else:
|
|
149
|
+
current_chunk_lines.append(line)
|
|
150
|
+
|
|
151
|
+
# Check chunk size limit
|
|
152
|
+
chunk_text = "\n".join(current_chunk_lines)
|
|
153
|
+
if len(chunk_text) >= self._config.max_chunk_size:
|
|
154
|
+
section_path = (
|
|
155
|
+
" > ".join(h[1] for h in current_headers)
|
|
156
|
+
if current_headers
|
|
157
|
+
else "Content"
|
|
158
|
+
)
|
|
159
|
+
chunks.append(
|
|
160
|
+
{
|
|
161
|
+
"content": chunk_text.strip(),
|
|
162
|
+
"metadata": {"section": section_path},
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
# Keep overlap for context continuity
|
|
166
|
+
overlap_lines = self._get_overlap_lines(current_chunk_lines)
|
|
167
|
+
current_chunk_lines = overlap_lines
|
|
168
|
+
|
|
169
|
+
# Save final chunk
|
|
170
|
+
if current_chunk_lines:
|
|
171
|
+
chunk_content = "\n".join(current_chunk_lines).strip()
|
|
172
|
+
if chunk_content:
|
|
173
|
+
section_path = (
|
|
174
|
+
" > ".join(h[1] for h in current_headers)
|
|
175
|
+
if current_headers
|
|
176
|
+
else "Content"
|
|
177
|
+
)
|
|
178
|
+
chunks.append(
|
|
179
|
+
{"content": chunk_content, "metadata": {"section": section_path}}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return chunks
|
|
183
|
+
|
|
184
|
+
def _chunk_python(self, content: str) -> List[Dict[str, Any]]:
|
|
185
|
+
"""
|
|
186
|
+
Split Python code by class/function definitions.
|
|
187
|
+
|
|
188
|
+
Strategy:
|
|
189
|
+
- Identify top-level class and function definitions
|
|
190
|
+
- Keep each definition as a separate chunk
|
|
191
|
+
- Preserve import statements and module docstrings
|
|
192
|
+
"""
|
|
193
|
+
# Pattern for class and function definitions (top-level only)
|
|
194
|
+
def_pattern = r"^(class|def|async\s+def)\s+(\w+)"
|
|
195
|
+
|
|
196
|
+
lines = content.split("\n")
|
|
197
|
+
chunks = []
|
|
198
|
+
current_chunk_lines = []
|
|
199
|
+
current_def = None
|
|
200
|
+
in_multiline_string = False
|
|
201
|
+
|
|
202
|
+
for i, line in enumerate(lines):
|
|
203
|
+
# Track multiline strings to avoid false positives
|
|
204
|
+
triple_quote_count = line.count('"""') + line.count("'''")
|
|
205
|
+
if triple_quote_count % 2 == 1:
|
|
206
|
+
in_multiline_string = not in_multiline_string
|
|
207
|
+
|
|
208
|
+
def_match = re.match(def_pattern, line)
|
|
209
|
+
|
|
210
|
+
# Check if this is a top-level definition (not indented)
|
|
211
|
+
if (
|
|
212
|
+
def_match
|
|
213
|
+
and not line.startswith((" ", "\t"))
|
|
214
|
+
and not in_multiline_string
|
|
215
|
+
):
|
|
216
|
+
# Save current chunk
|
|
217
|
+
if current_chunk_lines:
|
|
218
|
+
chunk_content = "\n".join(current_chunk_lines).strip()
|
|
219
|
+
if chunk_content:
|
|
220
|
+
chunks.append(
|
|
221
|
+
{
|
|
222
|
+
"content": chunk_content,
|
|
223
|
+
"metadata": {"definition": current_def or "module"},
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
current_def = f"{def_match.group(1)} {def_match.group(2)}"
|
|
228
|
+
current_chunk_lines = [line]
|
|
229
|
+
else:
|
|
230
|
+
current_chunk_lines.append(line)
|
|
231
|
+
|
|
232
|
+
# Check max chunk size
|
|
233
|
+
if len("\n".join(current_chunk_lines)) >= self._config.max_chunk_size:
|
|
234
|
+
chunks.append(
|
|
235
|
+
{
|
|
236
|
+
"content": "\n".join(current_chunk_lines).strip(),
|
|
237
|
+
"metadata": {"definition": current_def or "module"},
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
overlap_lines = self._get_overlap_lines(current_chunk_lines)
|
|
241
|
+
current_chunk_lines = overlap_lines
|
|
242
|
+
|
|
243
|
+
# Save final chunk
|
|
244
|
+
if current_chunk_lines:
|
|
245
|
+
chunk_content = "\n".join(current_chunk_lines).strip()
|
|
246
|
+
if chunk_content:
|
|
247
|
+
chunks.append(
|
|
248
|
+
{
|
|
249
|
+
"content": chunk_content,
|
|
250
|
+
"metadata": {"definition": current_def or "module"},
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return chunks
|
|
255
|
+
|
|
256
|
+
def _chunk_text(self, content: str) -> List[Dict[str, Any]]:
|
|
257
|
+
"""
|
|
258
|
+
Character-based chunking with intelligent boundary detection.
|
|
259
|
+
|
|
260
|
+
Strategy:
|
|
261
|
+
- Target chunk_size characters
|
|
262
|
+
- Prefer breaking at paragraph, sentence, or word boundaries
|
|
263
|
+
- Maintain overlap for context continuity
|
|
264
|
+
"""
|
|
265
|
+
chunks = []
|
|
266
|
+
chunk_size = self._config.chunk_size
|
|
267
|
+
overlap = self._config.chunk_overlap
|
|
268
|
+
|
|
269
|
+
start = 0
|
|
270
|
+
chunk_index = 0
|
|
271
|
+
|
|
272
|
+
while start < len(content):
|
|
273
|
+
end = start + chunk_size
|
|
274
|
+
|
|
275
|
+
# Don't exceed content length
|
|
276
|
+
if end >= len(content):
|
|
277
|
+
chunk_content = content[start:].strip()
|
|
278
|
+
if chunk_content:
|
|
279
|
+
chunks.append(
|
|
280
|
+
{
|
|
281
|
+
"content": chunk_content,
|
|
282
|
+
"metadata": {"chunk_index": chunk_index},
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
# Try to find a good break point
|
|
288
|
+
break_point = self._find_break_point(content, start, end)
|
|
289
|
+
end = break_point
|
|
290
|
+
|
|
291
|
+
chunk_content = content[start:end].strip()
|
|
292
|
+
if chunk_content:
|
|
293
|
+
chunks.append(
|
|
294
|
+
{"content": chunk_content, "metadata": {"chunk_index": chunk_index}}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Move start with overlap
|
|
298
|
+
start = max(end - overlap, start + 1)
|
|
299
|
+
chunk_index += 1
|
|
300
|
+
|
|
301
|
+
# Safety check to prevent infinite loop
|
|
302
|
+
if chunk_index > 10000:
|
|
303
|
+
logger.warning("Chunk limit reached, truncating document")
|
|
304
|
+
break
|
|
305
|
+
|
|
306
|
+
return chunks
|
|
307
|
+
|
|
308
|
+
def _find_break_point(self, content: str, start: int, end: int) -> int:
|
|
309
|
+
"""
|
|
310
|
+
Find the best break point near the target end position.
|
|
311
|
+
|
|
312
|
+
Priority: paragraph > sentence > word > hard cut
|
|
313
|
+
"""
|
|
314
|
+
search_start = start + (end - start) // 2 # Search in latter half
|
|
315
|
+
|
|
316
|
+
# Try paragraph break (double newline)
|
|
317
|
+
para_break = content.rfind("\n\n", search_start, end)
|
|
318
|
+
if para_break > search_start:
|
|
319
|
+
return para_break + 2
|
|
320
|
+
|
|
321
|
+
# Find last sentence break
|
|
322
|
+
for i in range(end - 1, search_start, -1):
|
|
323
|
+
if i + 1 < len(content) and content[i] in ".!?" and content[i + 1] in " \n":
|
|
324
|
+
return i + 1
|
|
325
|
+
|
|
326
|
+
# Try word break (space or newline)
|
|
327
|
+
space_break = content.rfind(" ", search_start, end)
|
|
328
|
+
if space_break > search_start:
|
|
329
|
+
return space_break + 1
|
|
330
|
+
|
|
331
|
+
newline_break = content.rfind("\n", search_start, end)
|
|
332
|
+
if newline_break > search_start:
|
|
333
|
+
return newline_break + 1
|
|
334
|
+
|
|
335
|
+
# Hard cut at end
|
|
336
|
+
return end
|
|
337
|
+
|
|
338
|
+
def _get_overlap_lines(self, lines: List[str]) -> List[str]:
|
|
339
|
+
"""Get lines for overlap context."""
|
|
340
|
+
total_chars = 0
|
|
341
|
+
overlap_lines = []
|
|
342
|
+
|
|
343
|
+
for line in reversed(lines):
|
|
344
|
+
total_chars += len(line) + 1 # +1 for newline
|
|
345
|
+
overlap_lines.insert(0, line)
|
|
346
|
+
if total_chars >= self._config.chunk_overlap:
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
return overlap_lines
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def chunk_file(
|
|
353
|
+
file_path: Path,
|
|
354
|
+
config: Optional["ChunkingConfig"] = None,
|
|
355
|
+
base_metadata: Optional[Dict[str, Any]] = None,
|
|
356
|
+
) -> List[Dict[str, Any]]:
|
|
357
|
+
"""
|
|
358
|
+
Convenience function to chunk a file directly.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
file_path: Path to the file to chunk
|
|
362
|
+
config: Optional ChunkingConfig
|
|
363
|
+
base_metadata: Base metadata to include in all chunks
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of chunks with content and metadata
|
|
367
|
+
"""
|
|
368
|
+
content = file_path.read_text(encoding="utf-8")
|
|
369
|
+
|
|
370
|
+
metadata = base_metadata or {}
|
|
371
|
+
metadata["source"] = file_path.name
|
|
372
|
+
metadata["file_path"] = str(file_path)
|
|
373
|
+
|
|
374
|
+
chunker = DocumentChunker(config)
|
|
375
|
+
return chunker.chunk_document(content, metadata=metadata)
|