codebuff-agent 2.1.74__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,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('\\"', "&quot;")
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("[", "&#91;")
152
+ .replace("]", "&#93;")
153
+ .replace('"', "&quot;")
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('"', "&quot;")
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("[", "&#91;").replace("]", "&#93;") + "|"
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), "&quot;")}"'
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)