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,289 @@
|
|
|
1
|
+
"""Mermaid syntax fixer — deterministic post-processing for LLM-generated diagrams.
|
|
2
|
+
|
|
3
|
+
Ported from frontend preprocessMermaid() (StreamingMessage.tsx:125-276)
|
|
4
|
+
with two additional categories (trailing semicolons, duplicate edges).
|
|
5
|
+
Pure Python, no external dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Public API
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fix_mermaid_blocks(text: str) -> str:
|
|
18
|
+
"""Find all ```mermaid blocks in *text* and fix each one.
|
|
19
|
+
|
|
20
|
+
Returns the original text unchanged if no mermaid blocks are present.
|
|
21
|
+
"""
|
|
22
|
+
return _MERMAID_FENCE_RE.sub(_fix_block_match, text)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def fix_mermaid_code(code: str) -> str:
|
|
26
|
+
"""Fix a single mermaid code block (fence markers excluded)."""
|
|
27
|
+
code = _fix_comments(code)
|
|
28
|
+
code = _fix_keywords(code)
|
|
29
|
+
code = _fix_escaped_quotes(code)
|
|
30
|
+
code = _fix_arrows(code)
|
|
31
|
+
code = _fix_nested_brackets(code)
|
|
32
|
+
code = _fix_node_labels(code)
|
|
33
|
+
code = _fix_edge_labels(code)
|
|
34
|
+
code = _fix_subgraph_titles(code)
|
|
35
|
+
code = _fix_empty_labels(code)
|
|
36
|
+
code = _fix_direction(code)
|
|
37
|
+
code = _fix_trailing_semicolons(code)
|
|
38
|
+
code = _fix_duplicate_edges(code)
|
|
39
|
+
return code
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Regex for fenced mermaid blocks
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
_MERMAID_FENCE_RE = re.compile(r"```mermaid\s*\n([\s\S]*?)```")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fix_block_match(m: re.Match[str]) -> str:
|
|
50
|
+
return "```mermaid\n" + fix_mermaid_code(m.group(1)) + "```"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Category 1: Remove %% comments
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fix_comments(code: str) -> str:
|
|
59
|
+
return re.sub(r"%%.*$", "", code, flags=re.MULTILINE)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Category 2: Reserved keyword collision (`end` as node ID)
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
_END_WORD = re.compile(r"\bend\b")
|
|
67
|
+
_END_SOLO_LINE = re.compile(r"^(\s*)end(\s*)$", re.MULTILINE)
|
|
68
|
+
_DIRECTIVE_LINE = re.compile(
|
|
69
|
+
r"^(\s*(?:class|style|click|classDef|linkStyle)\b.*)$", re.MULTILINE
|
|
70
|
+
)
|
|
71
|
+
_PLACEHOLDER = "__END_PH__"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _fix_keywords(code: str) -> str:
|
|
75
|
+
# Protect: `end` alone on a line (subgraph closer)
|
|
76
|
+
code = _END_SOLO_LINE.sub(rf"\g<1>{_PLACEHOLDER}\g<2>", code)
|
|
77
|
+
# Protect: `end` inside class/style/click directives
|
|
78
|
+
code = _DIRECTIVE_LINE.sub(lambda m: _END_WORD.sub(_PLACEHOLDER, m.group(0)), code)
|
|
79
|
+
# Rename remaining bare `end` → `end_node`
|
|
80
|
+
code = _END_WORD.sub("end_node", code)
|
|
81
|
+
# Restore placeholders
|
|
82
|
+
code = code.replace(_PLACEHOLDER, "end")
|
|
83
|
+
return code
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Category 3: Escaped quotes
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fix_escaped_quotes(code: str) -> str:
|
|
92
|
+
return code.replace('\\"', """)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Category 4: Arrow syntax
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
_SINGLE_ARROW = re.compile(r"(\s)->([\s\w])")
|
|
100
|
+
_DOTTED_ARROW = re.compile(r"(\s)-\.(\s)")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _fix_arrows(code: str) -> str:
|
|
104
|
+
code = _SINGLE_ARROW.sub(r"\1-->\2", code)
|
|
105
|
+
code = _DOTTED_ARROW.sub(r"\1-.->\2", code)
|
|
106
|
+
return code
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Category 5: Nested brackets & misquoted labels (depth counting)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _fix_nested_brackets(code: str) -> str:
|
|
115
|
+
return re.sub(r"^(.*)$", _fix_line_brackets, code, flags=re.MULTILINE)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _fix_line_brackets(m: re.Match[str]) -> str:
|
|
119
|
+
line = m.group(0)
|
|
120
|
+
result: list[str] = []
|
|
121
|
+
i = 0
|
|
122
|
+
n = len(line)
|
|
123
|
+
while i < n:
|
|
124
|
+
if (
|
|
125
|
+
i > 0
|
|
126
|
+
and re.match(r"\w", line[i - 1])
|
|
127
|
+
and line[i] == "["
|
|
128
|
+
and (i + 1 < n)
|
|
129
|
+
and line[i + 1] not in ("[", "(", "/", "\\")
|
|
130
|
+
):
|
|
131
|
+
depth = 1
|
|
132
|
+
j = i + 1
|
|
133
|
+
while j < n and depth > 0:
|
|
134
|
+
if line[j] == "[":
|
|
135
|
+
depth += 1
|
|
136
|
+
elif line[j] == "]":
|
|
137
|
+
depth -= 1
|
|
138
|
+
if depth > 0:
|
|
139
|
+
j += 1
|
|
140
|
+
|
|
141
|
+
if depth == 0:
|
|
142
|
+
inner = line[i + 1 : j]
|
|
143
|
+
# Strip surrounding quotes
|
|
144
|
+
if inner.startswith('"') and inner.endswith('"'):
|
|
145
|
+
inner = inner[1:-1]
|
|
146
|
+
elif inner.startswith('"'):
|
|
147
|
+
inner = inner.replace('"', "")
|
|
148
|
+
|
|
149
|
+
if re.search(r'[\[\]"]', inner):
|
|
150
|
+
escaped = (
|
|
151
|
+
inner.replace("[", "[")
|
|
152
|
+
.replace("]", "]")
|
|
153
|
+
.replace('"', """)
|
|
154
|
+
)
|
|
155
|
+
result.append(f'["{escaped}"]')
|
|
156
|
+
else:
|
|
157
|
+
result.append(line[i : j + 1])
|
|
158
|
+
i = j + 1
|
|
159
|
+
else:
|
|
160
|
+
result.append(line[i])
|
|
161
|
+
i += 1
|
|
162
|
+
else:
|
|
163
|
+
result.append(line[i])
|
|
164
|
+
i += 1
|
|
165
|
+
return "".join(result)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Category 6: Node labels — quote and encode special chars
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
_SUBROUTINE_LABEL = re.compile(r"(\b\w+)\[\[([^\]\n]*)\]\]")
|
|
173
|
+
_RECT_LABEL = re.compile(r"(\b\w+)\[(?!\[|\(|/|\\)([^\]\n]*)\]")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _sanitize_label(content: str) -> str:
|
|
177
|
+
return content.replace('"', """)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _fix_node_labels(code: str) -> str:
|
|
181
|
+
# [[content]] (subroutine) first
|
|
182
|
+
code = _SUBROUTINE_LABEL.sub(_quote_subroutine, code)
|
|
183
|
+
# [content] (rectangle)
|
|
184
|
+
code = _RECT_LABEL.sub(_quote_rect, code)
|
|
185
|
+
return code
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _quote_subroutine(m: re.Match[str]) -> str:
|
|
189
|
+
node_id, content = m.group(1), m.group(2)
|
|
190
|
+
if content.startswith('"') and content.endswith('"'):
|
|
191
|
+
return m.group(0)
|
|
192
|
+
return f'{node_id}[["{_sanitize_label(content)}"]]'
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _quote_rect(m: re.Match[str]) -> str:
|
|
196
|
+
node_id, content = m.group(1), m.group(2)
|
|
197
|
+
if content.startswith('"') and content.endswith('"'):
|
|
198
|
+
return m.group(0)
|
|
199
|
+
return f'{node_id}["{_sanitize_label(content)}"]'
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Category 7: Edge labels — brackets inside pipe-delimited labels
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
_EDGE_LABEL = re.compile(r"\|([^|]*)\|")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _fix_edge_labels(code: str) -> str:
|
|
210
|
+
return _EDGE_LABEL.sub(_escape_edge_label, code)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _escape_edge_label(m: re.Match[str]) -> str:
|
|
214
|
+
content = m.group(1)
|
|
215
|
+
if not re.search(r"[\[\]]", content):
|
|
216
|
+
return m.group(0)
|
|
217
|
+
return "|" + content.replace("[", "[").replace("]", "]") + "|"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Category 8: Subgraph — quote unquoted titles with spaces
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
_SUBGRAPH_TITLE = re.compile(
|
|
225
|
+
r'^(\s*subgraph\s+)(?!")([^\n]*[ \t][^\n]*)$', re.MULTILINE
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _fix_subgraph_titles(code: str) -> str:
|
|
230
|
+
return _SUBGRAPH_TITLE.sub(_quote_subgraph_title, code)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _quote_subgraph_title(m: re.Match[str]) -> str:
|
|
234
|
+
prefix, title = m.group(1), m.group(2)
|
|
235
|
+
# Skip `subgraph id [title]` pattern
|
|
236
|
+
if re.match(r"^\w+\s*\[", title):
|
|
237
|
+
return m.group(0)
|
|
238
|
+
return f'{prefix}"{title.replace(chr(34), """)}"'
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Category 9: Empty labels — remove A[""] patterns
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _fix_empty_labels(code: str) -> str:
|
|
247
|
+
return re.sub(r'(\b\w+)\[""\]', r"\1", code)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Category 10: Direction — fix invalid direction values
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _fix_direction(code: str) -> str:
|
|
256
|
+
return re.sub(
|
|
257
|
+
r"^(\s*direction\s+)(?!TB|BT|LR|RL|TD)(\w+)",
|
|
258
|
+
r"\g<1>TD",
|
|
259
|
+
code,
|
|
260
|
+
flags=re.MULTILINE,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Category 11 (NEW): Trailing semicolons
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _fix_trailing_semicolons(code: str) -> str:
|
|
270
|
+
return re.sub(r";\s*$", "", code, flags=re.MULTILINE)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# Category 12 (NEW): Duplicate edges
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _fix_duplicate_edges(code: str) -> str:
|
|
279
|
+
seen: set[str] = set()
|
|
280
|
+
lines: list[str] = []
|
|
281
|
+
for line in code.split("\n"):
|
|
282
|
+
stripped = line.strip()
|
|
283
|
+
# Only dedup edge lines (contain --> or --- or -.-> etc.)
|
|
284
|
+
if re.search(r"--[->]|---|\.-[>.]?->|==+>", stripped):
|
|
285
|
+
if stripped in seen:
|
|
286
|
+
continue
|
|
287
|
+
seen.add(stripped)
|
|
288
|
+
lines.append(line)
|
|
289
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Mermaid Fix Middleware — deterministic Mermaid syntax repair on LLM output.
|
|
2
|
+
|
|
3
|
+
Follows the same pattern as OutputGuardrailMiddleware (awrap_model_call)
|
|
4
|
+
and AutoFixMiddleware (awrap_tool_call + request.override).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from langchain_core.messages import AIMessage
|
|
14
|
+
|
|
15
|
+
from langchain.agents.middleware.types import (
|
|
16
|
+
AgentMiddleware,
|
|
17
|
+
ModelCallResult,
|
|
18
|
+
ModelRequest,
|
|
19
|
+
ModelResponse,
|
|
20
|
+
)
|
|
21
|
+
from langgraph.prebuilt.tool_node import ToolCallRequest
|
|
22
|
+
|
|
23
|
+
from .mermaid_fixer import fix_mermaid_blocks
|
|
24
|
+
|
|
25
|
+
from ..log_paths import get_debug_log_path
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Debug logging (same pattern as guardrail/continuation middlewares)
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
_DEBUG_LOG = get_debug_log_path()
|
|
32
|
+
|
|
33
|
+
_MERMAID_MARKER = "```mermaid"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _dbg(msg: str) -> None:
|
|
37
|
+
try:
|
|
38
|
+
ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
39
|
+
with open(_DEBUG_LOG, "a") as f:
|
|
40
|
+
f.write(f"[{ts}] MERMAID: {msg}\n")
|
|
41
|
+
except OSError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Helper: extract AIMessage from response (same as guardrail_middleware)
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _extract_ai_message(response: Any) -> AIMessage | None:
|
|
51
|
+
if isinstance(response, AIMessage):
|
|
52
|
+
return response
|
|
53
|
+
if hasattr(response, "message") and isinstance(response.message, AIMessage):
|
|
54
|
+
return response.message
|
|
55
|
+
if hasattr(response, "messages"):
|
|
56
|
+
for msg in reversed(response.messages):
|
|
57
|
+
if isinstance(msg, AIMessage):
|
|
58
|
+
return msg
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Middleware
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MermaidFixMiddleware(AgentMiddleware):
|
|
68
|
+
"""Fix Mermaid syntax errors in LLM text responses and tool calls."""
|
|
69
|
+
|
|
70
|
+
# -- Hook 1: LLM text response + tool_calls with mermaid content ---------
|
|
71
|
+
|
|
72
|
+
def awrap_model_call(
|
|
73
|
+
self,
|
|
74
|
+
request: ModelRequest,
|
|
75
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
76
|
+
) -> Awaitable[ModelCallResult]:
|
|
77
|
+
return self._impl_model(request, handler)
|
|
78
|
+
|
|
79
|
+
async def _impl_model(
|
|
80
|
+
self,
|
|
81
|
+
request: ModelRequest,
|
|
82
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
83
|
+
) -> ModelCallResult:
|
|
84
|
+
response = await handler(request)
|
|
85
|
+
ai_msg = _extract_ai_message(response)
|
|
86
|
+
if ai_msg is None:
|
|
87
|
+
return response
|
|
88
|
+
|
|
89
|
+
# Fix mermaid in text content
|
|
90
|
+
if isinstance(ai_msg.content, str) and _MERMAID_MARKER in ai_msg.content:
|
|
91
|
+
original = ai_msg.content
|
|
92
|
+
ai_msg.content = fix_mermaid_blocks(original)
|
|
93
|
+
if ai_msg.content != original:
|
|
94
|
+
_dbg("fixed mermaid in text content")
|
|
95
|
+
|
|
96
|
+
# Fix mermaid in jupyter_markdown tool_calls
|
|
97
|
+
tool_calls = getattr(ai_msg, "tool_calls", None)
|
|
98
|
+
if tool_calls:
|
|
99
|
+
for tc in tool_calls:
|
|
100
|
+
if tc.get("name") != "jupyter_markdown":
|
|
101
|
+
continue
|
|
102
|
+
args = tc.get("args", {})
|
|
103
|
+
content = args.get("content", "")
|
|
104
|
+
if isinstance(content, str) and _MERMAID_MARKER in content:
|
|
105
|
+
args["content"] = fix_mermaid_blocks(content)
|
|
106
|
+
if args["content"] != content:
|
|
107
|
+
_dbg("fixed mermaid in jupyter_markdown tool_call")
|
|
108
|
+
|
|
109
|
+
return response
|
|
110
|
+
|
|
111
|
+
# -- Hook 2: jupyter_markdown tool execution -----------------------------
|
|
112
|
+
|
|
113
|
+
async def awrap_tool_call(
|
|
114
|
+
self,
|
|
115
|
+
request: ToolCallRequest,
|
|
116
|
+
handler: Callable[[ToolCallRequest], Awaitable[Any]],
|
|
117
|
+
) -> Any:
|
|
118
|
+
tool_name = request.tool_call.get("name", "")
|
|
119
|
+
if tool_name != "jupyter_markdown":
|
|
120
|
+
return await handler(request)
|
|
121
|
+
|
|
122
|
+
args = request.tool_call.get("args", {})
|
|
123
|
+
content = args.get("content", "")
|
|
124
|
+
|
|
125
|
+
if isinstance(content, str) and _MERMAID_MARKER in content:
|
|
126
|
+
fixed = fix_mermaid_blocks(content)
|
|
127
|
+
if fixed != content:
|
|
128
|
+
modified_call = {
|
|
129
|
+
**request.tool_call,
|
|
130
|
+
"args": {**args, "content": fixed},
|
|
131
|
+
}
|
|
132
|
+
request = request.override(tool_call=modified_call)
|
|
133
|
+
_dbg("fixed mermaid in jupyter_markdown tool execution")
|
|
134
|
+
|
|
135
|
+
return await handler(request)
|