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,647 @@
1
+ """
2
+ Tests for error recovery middleware.
3
+
4
+ Tests cover:
5
+ - ErrorClassifier integration (REFINE, INSERT_STEPS decisions)
6
+ - Middleware passthrough for non-error and non-HITL messages
7
+ - HumanMessage injection on error
8
+ - Max retries enforcement
9
+ - Prompt content (write_todos, jupyter_cell_tool, final_answer_tool ban)
10
+ - Consecutive error count reset on success
11
+ """
12
+
13
+ from unittest.mock import MagicMock
14
+
15
+ from langchain_core.messages import HumanMessage
16
+
17
+ from codebuff_core.middleware.error_recovery import (
18
+ ERROR_RECOVERY_MARKER,
19
+ _build_replan_prompt,
20
+ _count_error_recoveries,
21
+ _extract_error_parts,
22
+ _get_error_guidance,
23
+ _has_error_indicators,
24
+ _is_hitl_tool_error,
25
+ create_error_recovery_middleware,
26
+ )
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Helpers
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ class MockMessage:
35
+ """Minimal message mock for testing."""
36
+
37
+ def __init__(self, type="", name="", content=""):
38
+ self.type = type
39
+ self.name = name
40
+ self.content = content
41
+
42
+
43
+ class MockRequest:
44
+ """Minimal request mock for testing."""
45
+
46
+ def __init__(self, messages=None, state=None):
47
+ self.messages = messages or []
48
+ self.state = state or {}
49
+
50
+ def override(self, **kwargs):
51
+ return MockRequest(
52
+ messages=kwargs.get("messages", self.messages),
53
+ state=kwargs.get("state", self.state),
54
+ )
55
+
56
+
57
+ def mock_wrap_model_call(fn):
58
+ """Identity decorator mimicking wrap_model_call for testing."""
59
+ return fn
60
+
61
+
62
+ def make_handler(response=None):
63
+ """Create a mock handler that records calls."""
64
+ resp = response or MagicMock()
65
+ handler = MagicMock(return_value=resp)
66
+ return handler
67
+
68
+
69
+ def make_tool_msg(name, content):
70
+ """Create a ToolMessage mock."""
71
+ return MockMessage(type="tool", name=name, content=content)
72
+
73
+
74
+ def make_error_content(error_type="KeyError", error_msg="'Survived'"):
75
+ """Create typical Python error output."""
76
+ return (
77
+ f"Traceback (most recent call last):\n"
78
+ f' File "<stdin>", line 1, in <module>\n'
79
+ f"{error_type}: {error_msg}"
80
+ )
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # ErrorClassifier integration tests
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ class TestErrorClassifierIntegration:
89
+ """Test that ErrorClassifier correctly classifies errors for the middleware."""
90
+
91
+ def test_classify_keyerror_refine(self):
92
+ """KeyError should be classified as REFINE with correct guidance."""
93
+ from codebuff_core.core.error_classifier import (
94
+ ReplanDecision,
95
+ get_error_classifier,
96
+ )
97
+
98
+ classifier = get_error_classifier()
99
+ analysis = classifier.classify("KeyError", "'Survived'", "")
100
+
101
+ assert analysis.decision == ReplanDecision.REFINE
102
+ assert "키" in analysis.root_cause or "key" in analysis.root_cause.lower()
103
+
104
+ def test_classify_module_not_found_insert_steps(self):
105
+ """ModuleNotFoundError should be classified as INSERT_STEPS."""
106
+ from codebuff_core.core.error_classifier import (
107
+ ReplanDecision,
108
+ get_error_classifier,
109
+ )
110
+
111
+ classifier = get_error_classifier()
112
+ analysis = classifier.classify(
113
+ "ModuleNotFoundError",
114
+ "No module named 'seaborn'",
115
+ "ModuleNotFoundError: No module named 'seaborn'",
116
+ )
117
+
118
+ assert analysis.decision == ReplanDecision.INSERT_STEPS
119
+ assert analysis.missing_package == "seaborn"
120
+
121
+ def test_classify_file_not_found_refine(self):
122
+ """FileNotFoundError should be classified as REFINE."""
123
+ from codebuff_core.core.error_classifier import (
124
+ ReplanDecision,
125
+ get_error_classifier,
126
+ )
127
+
128
+ classifier = get_error_classifier()
129
+ analysis = classifier.classify(
130
+ "FileNotFoundError",
131
+ "[Errno 2] No such file or directory: 'data.csv'",
132
+ "",
133
+ )
134
+
135
+ assert analysis.decision == ReplanDecision.REFINE
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Helper function tests
140
+ # ---------------------------------------------------------------------------
141
+
142
+
143
+ class TestHelperFunctions:
144
+ """Test helper functions used by the middleware."""
145
+
146
+ def test_is_hitl_tool_error_true(self):
147
+ """ToolMessage from jupyter_cell_tool with error should return True."""
148
+ msg = make_tool_msg("jupyter_cell_tool", make_error_content())
149
+ assert _is_hitl_tool_error(msg) is True
150
+
151
+ def test_is_hitl_tool_error_no_error(self):
152
+ """ToolMessage from jupyter_cell_tool without error should return False."""
153
+ msg = make_tool_msg("jupyter_cell_tool", " x y\n0 1 2\n1 3 4")
154
+ assert _is_hitl_tool_error(msg) is False
155
+
156
+ def test_is_hitl_tool_error_non_hitl_tool(self):
157
+ """ToolMessage from non-HITL tool should return False."""
158
+ msg = make_tool_msg("markdown_tool", make_error_content())
159
+ assert _is_hitl_tool_error(msg) is False
160
+
161
+ def test_is_hitl_tool_error_non_tool_message(self):
162
+ """Non-tool message should return False."""
163
+ msg = MockMessage(type="human", content=make_error_content())
164
+ assert _is_hitl_tool_error(msg) is False
165
+
166
+ def test_has_error_indicators_with_traceback(self):
167
+ """Content with traceback should be detected."""
168
+ content = "Traceback (most recent call last):\n File ...\nKeyError: 'x'"
169
+ assert _has_error_indicators(content) is True
170
+
171
+ def test_has_error_indicators_with_error_line(self):
172
+ """Content with error line should be detected."""
173
+ assert _has_error_indicators("ValueError: invalid literal") is True
174
+
175
+ def test_has_error_indicators_clean_output(self):
176
+ """Clean output without errors should return False."""
177
+ assert _has_error_indicators(" x y\n0 1 2") is False
178
+
179
+ def test_extract_error_parts_keyerror(self):
180
+ """Should extract KeyError type and message from traceback."""
181
+ content = make_error_content("KeyError", "'Survived'")
182
+ error_type, error_msg, tb = _extract_error_parts(content)
183
+
184
+ assert error_type == "KeyError"
185
+ assert "'Survived'" in error_msg
186
+ assert "Traceback" in tb
187
+
188
+ def test_extract_error_parts_module_error(self):
189
+ """Should extract ModuleNotFoundError."""
190
+ content = (
191
+ "Traceback (most recent call last):\n"
192
+ ' File "<stdin>", line 1, in <module>\n'
193
+ "ModuleNotFoundError: No module named 'pandas'"
194
+ )
195
+ error_type, error_msg, tb = _extract_error_parts(content)
196
+
197
+ assert error_type == "ModuleNotFoundError"
198
+ assert "pandas" in error_msg
199
+
200
+ def test_extract_error_parts_no_error(self):
201
+ """Should return None for clean output."""
202
+ error_type, error_msg, tb = _extract_error_parts("Hello world\n42")
203
+ assert error_type is None
204
+
205
+ def test_extract_error_parts_ansi_codes(self):
206
+ """Should handle ANSI color codes in traceback."""
207
+ content = (
208
+ "Traceback (most recent call last):\n\x1b[31mKeyError\x1b[0m: 'missing_col'"
209
+ )
210
+ error_type, error_msg, tb = _extract_error_parts(content)
211
+ assert error_type == "KeyError"
212
+
213
+ def test_get_error_guidance_keyerror(self):
214
+ """KeyError guidance should mention columns/keys."""
215
+ guidance = _get_error_guidance("KeyError", "'col'")
216
+ assert "column" in guidance.lower() or "key" in guidance.lower()
217
+
218
+ def test_get_error_guidance_unknown(self):
219
+ """Unknown error type should get generic guidance."""
220
+ guidance = _get_error_guidance("CustomError", "something")
221
+ assert "CustomError" in guidance
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # Count error recoveries tests
226
+ # ---------------------------------------------------------------------------
227
+
228
+
229
+ class TestCountErrorRecoveries:
230
+ """Test consecutive error recovery counting logic."""
231
+
232
+ def test_no_recoveries(self):
233
+ """Empty messages should return 0."""
234
+ assert _count_error_recoveries([]) == 0
235
+
236
+ def test_one_recovery(self):
237
+ """Single recovery marker should return 1."""
238
+ messages = [
239
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
240
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix it"),
241
+ ]
242
+ assert _count_error_recoveries(messages) == 1
243
+
244
+ def test_multiple_recoveries(self):
245
+ """Multiple recovery markers in error chain should be counted."""
246
+ messages = [
247
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
248
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 1"),
249
+ MockMessage(type="ai", content="I'll fix it"),
250
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
251
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 2"),
252
+ MockMessage(type="ai", content="trying again"),
253
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
254
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 3"),
255
+ ]
256
+ assert _count_error_recoveries(messages) == 3
257
+
258
+ def test_consecutive_count_resets_on_success(self):
259
+ """Count should reset when a successful HITL tool result appears."""
260
+ messages = [
261
+ # First error cycle
262
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
263
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 1"),
264
+ MockMessage(type="ai", content="fixing"),
265
+ # SUCCESS - this should reset the count
266
+ make_tool_msg("jupyter_cell_tool", " x y\n0 1 2"),
267
+ MockMessage(type="ai", content="next step"),
268
+ # New error cycle - only these count
269
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
270
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 2"),
271
+ ]
272
+ assert _count_error_recoveries(messages) == 1
273
+
274
+ def test_non_hitl_tools_dont_reset(self):
275
+ """Non-HITL tool results should not affect the count."""
276
+ messages = [
277
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
278
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 1"),
279
+ MockMessage(type="ai", content="reading file"),
280
+ make_tool_msg("read_file_tool", "file content here"),
281
+ MockMessage(type="ai", content="got it"),
282
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
283
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 2"),
284
+ ]
285
+ assert _count_error_recoveries(messages) == 2
286
+
287
+
288
+ # ---------------------------------------------------------------------------
289
+ # Middleware integration tests
290
+ # ---------------------------------------------------------------------------
291
+
292
+
293
+ class TestMiddlewareIntegration:
294
+ """Test the full middleware function via create_error_recovery_middleware."""
295
+
296
+ def _create_middleware(self, max_retries=3):
297
+ """Create middleware with mock wrap_model_call."""
298
+ return create_error_recovery_middleware(
299
+ mock_wrap_model_call, max_retries=max_retries
300
+ )
301
+
302
+ def test_passthrough_no_error(self):
303
+ """Normal ToolMessage (no error) should pass through unchanged."""
304
+ middleware = self._create_middleware()
305
+ handler = make_handler()
306
+ request = MockRequest(
307
+ messages=[make_tool_msg("jupyter_cell_tool", " x y\n0 1 2\n1 3 4")]
308
+ )
309
+
310
+ middleware(request, handler)
311
+
312
+ # Handler called with original request (no extra messages)
313
+ called_request = handler.call_args[0][0]
314
+ assert len(called_request.messages) == 1
315
+
316
+ def test_passthrough_non_hitl_tool(self):
317
+ """ToolMessage from non-HITL tool should pass through."""
318
+ middleware = self._create_middleware()
319
+ handler = make_handler()
320
+ request = MockRequest(
321
+ messages=[make_tool_msg("markdown_tool", make_error_content())]
322
+ )
323
+
324
+ middleware(request, handler)
325
+
326
+ called_request = handler.call_args[0][0]
327
+ assert len(called_request.messages) == 1
328
+
329
+ def test_passthrough_empty_messages(self):
330
+ """Empty messages should pass through."""
331
+ middleware = self._create_middleware()
332
+ handler = make_handler()
333
+ request = MockRequest(messages=[])
334
+
335
+ middleware(request, handler)
336
+ handler.assert_called_once()
337
+
338
+ def test_injects_on_error(self):
339
+ """HITL tool error should inject a HumanMessage."""
340
+ middleware = self._create_middleware()
341
+ handler = make_handler()
342
+ request = MockRequest(
343
+ messages=[make_tool_msg("jupyter_cell_tool", make_error_content())],
344
+ state={"todos": []},
345
+ )
346
+
347
+ middleware(request, handler)
348
+
349
+ called_request = handler.call_args[0][0]
350
+ assert len(called_request.messages) == 2
351
+ injected = called_request.messages[1]
352
+ assert isinstance(injected, HumanMessage)
353
+ assert ERROR_RECOVERY_MARKER in injected.content
354
+
355
+ def test_max_retries_passthrough(self):
356
+ """After max_retries, should pass through without injection."""
357
+ middleware = self._create_middleware(max_retries=2)
358
+ handler = make_handler()
359
+
360
+ # Build messages with 2 existing recovery markers
361
+ messages = [
362
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
363
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 1"),
364
+ MockMessage(type="ai", content="fixing"),
365
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
366
+ MockMessage(type="human", content=f"{ERROR_RECOVERY_MARKER} fix 2"),
367
+ MockMessage(type="ai", content="trying"),
368
+ # This is the 3rd error - should hit max_retries=2
369
+ make_tool_msg("jupyter_cell_tool", make_error_content()),
370
+ ]
371
+ request = MockRequest(messages=messages, state={"todos": []})
372
+
373
+ middleware(request, handler)
374
+
375
+ # Should pass through with original messages (no new injection)
376
+ called_request = handler.call_args[0][0]
377
+ assert len(called_request.messages) == len(messages)
378
+
379
+ def test_injects_for_execute_command_tool(self):
380
+ """execute_command_tool errors should also trigger recovery."""
381
+ middleware = self._create_middleware()
382
+ handler = make_handler()
383
+ request = MockRequest(
384
+ messages=[
385
+ make_tool_msg(
386
+ "execute_command_tool",
387
+ "Traceback (most recent call last):\n"
388
+ "FileNotFoundError: [Errno 2] No such file: 'missing.sh'",
389
+ )
390
+ ],
391
+ state={"todos": []},
392
+ )
393
+
394
+ middleware(request, handler)
395
+
396
+ called_request = handler.call_args[0][0]
397
+ assert len(called_request.messages) == 2
398
+ assert ERROR_RECOVERY_MARKER in called_request.messages[1].content
399
+
400
+
401
+ # ---------------------------------------------------------------------------
402
+ # Prompt content tests
403
+ # ---------------------------------------------------------------------------
404
+
405
+
406
+ class TestPromptContent:
407
+ """Test that generated prompts contain required instructions."""
408
+
409
+ def _get_refine_prompt(self):
410
+ """Get a REFINE prompt for KeyError."""
411
+ from codebuff_core.core.error_classifier import (
412
+ ReplanDecision,
413
+ ErrorAnalysis,
414
+ )
415
+
416
+ analysis = ErrorAnalysis(
417
+ decision=ReplanDecision.REFINE,
418
+ root_cause="딕셔너리/데이터프레임 키 없음: 'Survived'",
419
+ reasoning="KeyError는 코드 수정으로 해결 가능합니다.",
420
+ )
421
+ return _build_replan_prompt(analysis, [], "KeyError", "'Survived'")
422
+
423
+ def _get_insert_steps_prompt(self):
424
+ """Get an INSERT_STEPS prompt for ModuleNotFoundError."""
425
+ from codebuff_core.core.error_classifier import (
426
+ ReplanDecision,
427
+ ErrorAnalysis,
428
+ )
429
+
430
+ analysis = ErrorAnalysis(
431
+ decision=ReplanDecision.INSERT_STEPS,
432
+ root_cause="'seaborn' 모듈이 설치되지 않음",
433
+ reasoning="ModuleNotFoundError는 항상 패키지 설치로 해결합니다.",
434
+ missing_package="seaborn",
435
+ )
436
+ return _build_replan_prompt(
437
+ analysis, [], "ModuleNotFoundError", "No module named 'seaborn'"
438
+ )
439
+
440
+ def test_prompt_contains_replan_instructions(self):
441
+ """Prompt should instruct to call write_todos and jupyter_cell_tool."""
442
+ prompt = self._get_refine_prompt()
443
+ assert "write_todos" in prompt
444
+ assert "jupyter_cell_tool" in prompt
445
+
446
+ def test_prompt_forbids_final_answer(self):
447
+ """Prompt should forbid calling final_answer_tool."""
448
+ prompt = self._get_refine_prompt()
449
+ assert "final_answer_tool" in prompt
450
+ assert "NOT" in prompt
451
+
452
+ def test_prompt_contains_error_recovery_marker(self):
453
+ """Prompt should contain the error recovery marker."""
454
+ prompt = self._get_refine_prompt()
455
+ assert ERROR_RECOVERY_MARKER in prompt
456
+
457
+ def test_refine_prompt_contains_guidance(self):
458
+ """REFINE prompt should contain error-specific guidance."""
459
+ prompt = self._get_refine_prompt()
460
+ # KeyError guidance mentions columns/keys
461
+ assert "column" in prompt.lower() or "key" in prompt.lower()
462
+
463
+ def test_insert_steps_prompt_contains_pip(self):
464
+ """INSERT_STEPS prompt should contain pip install instruction."""
465
+ prompt = self._get_insert_steps_prompt()
466
+ assert "pip install" in prompt
467
+ assert "seaborn" in prompt
468
+
469
+ def test_prompt_includes_todos_context(self):
470
+ """Prompt should include pending todos when available."""
471
+ from codebuff_core.core.error_classifier import (
472
+ ReplanDecision,
473
+ ErrorAnalysis,
474
+ )
475
+
476
+ analysis = ErrorAnalysis(
477
+ decision=ReplanDecision.REFINE,
478
+ root_cause="KeyError",
479
+ reasoning="코드 수정",
480
+ )
481
+ todos = [
482
+ {"status": "completed", "content": "Load data"},
483
+ {"status": "in_progress", "content": "Analyze survival rate"},
484
+ {"status": "pending", "content": "Create visualization"},
485
+ ]
486
+ prompt = _build_replan_prompt(analysis, todos, "KeyError", "'col'")
487
+
488
+ assert "pending tasks" in prompt.lower()
489
+ assert "Analyze survival" in prompt
490
+ assert "Create visualization" in prompt
491
+ # Completed task should not appear in pending
492
+ assert "Load data" not in prompt
493
+
494
+ def test_replace_step_prompt(self):
495
+ """REPLACE_STEP prompt should instruct a completely different method."""
496
+ from codebuff_core.core.error_classifier import (
497
+ ReplanDecision,
498
+ ErrorAnalysis,
499
+ )
500
+
501
+ analysis = ErrorAnalysis(
502
+ decision=ReplanDecision.REPLACE_STEP,
503
+ root_cause="Approach failed",
504
+ reasoning="Need different method",
505
+ )
506
+ prompt = _build_replan_prompt(analysis, [], "RuntimeError", "approach failed")
507
+ assert "different" in prompt.lower()
508
+ assert "write_todos" in prompt
509
+
510
+ def test_replan_remaining_prompt(self):
511
+ """REPLAN_REMAINING prompt should instruct full replanning."""
512
+ from codebuff_core.core.error_classifier import (
513
+ ReplanDecision,
514
+ ErrorAnalysis,
515
+ )
516
+
517
+ analysis = ErrorAnalysis(
518
+ decision=ReplanDecision.REPLAN_REMAINING,
519
+ root_cause="Critical system error",
520
+ reasoning="Full replan needed",
521
+ )
522
+ prompt = _build_replan_prompt(analysis, [], "OSError", "system error")
523
+ assert "replan" in prompt.lower() or "remaining" in prompt.lower()
524
+ assert "write_todos" in prompt
525
+
526
+
527
+ # ---------------------------------------------------------------------------
528
+ # JSON structured content tests (HITL execution_result)
529
+ # ---------------------------------------------------------------------------
530
+
531
+
532
+ class TestStructuredContent:
533
+ """Test handling of JSON-structured tool output with execution_result."""
534
+
535
+ def _make_json_content(
536
+ self, success, output="", error=None, error_type=None, traceback=None
537
+ ):
538
+ """Create JSON content mimicking jupyter_cell_tool output with execution_result."""
539
+ import json
540
+
541
+ return json.dumps(
542
+ {
543
+ "tool": "jupyter_cell",
544
+ "parameters": {"code": "print('hello')", "description": None},
545
+ "status": "complete" if success else "complete",
546
+ "message": "Code cell executed with client-reported results",
547
+ "execution_result": {
548
+ "success": success,
549
+ "output": output,
550
+ "error": error,
551
+ "error_type": error_type,
552
+ "traceback": traceback,
553
+ },
554
+ }
555
+ )
556
+
557
+ def test_is_hitl_tool_error_json_with_error(self):
558
+ """JSON content with failed execution_result should be detected as error."""
559
+ content = self._make_json_content(
560
+ success=False,
561
+ error="KeyError: 'Survived'",
562
+ error_type="KeyError",
563
+ )
564
+ msg = make_tool_msg("jupyter_cell_tool", content)
565
+ assert _is_hitl_tool_error(msg) is True
566
+
567
+ def test_is_hitl_tool_error_json_success(self):
568
+ """JSON content with successful execution_result should not be error."""
569
+ content = self._make_json_content(
570
+ success=True,
571
+ output="hello\n",
572
+ )
573
+ msg = make_tool_msg("jupyter_cell_tool", content)
574
+ assert _is_hitl_tool_error(msg) is False
575
+
576
+ def test_extract_error_parts_json_keyerror(self):
577
+ """Should extract error from JSON execution_result."""
578
+ content = self._make_json_content(
579
+ success=False,
580
+ error="KeyError: 'Survived'",
581
+ error_type="KeyError",
582
+ traceback="Traceback (most recent call last):\n File ...\nKeyError: 'Survived'",
583
+ )
584
+ error_type, error_msg, tb = _extract_error_parts(content)
585
+ assert error_type == "KeyError"
586
+ assert "'Survived'" in error_msg
587
+ assert "Traceback" in tb
588
+
589
+ def test_extract_error_parts_json_nameerror(self):
590
+ """Should extract NameError from JSON execution_result."""
591
+ content = self._make_json_content(
592
+ success=False,
593
+ error="NameError: name 'corr_matrix' is not defined",
594
+ error_type="NameError",
595
+ traceback="Traceback...\nNameError: name 'corr_matrix' is not defined",
596
+ )
597
+ error_type, error_msg, tb = _extract_error_parts(content)
598
+ assert error_type == "NameError"
599
+ assert "corr_matrix" in error_msg
600
+
601
+ def test_extract_error_parts_json_success_no_error(self):
602
+ """Successful JSON content should return no error."""
603
+ content = self._make_json_content(
604
+ success=True,
605
+ output="hello\n",
606
+ )
607
+ error_type, error_msg, tb = _extract_error_parts(content)
608
+ assert error_type is None
609
+
610
+ def test_middleware_injects_on_json_error(self):
611
+ """Middleware should inject re-plan prompt for JSON error content."""
612
+ content = self._make_json_content(
613
+ success=False,
614
+ error="KeyError: 'Survived'",
615
+ error_type="KeyError",
616
+ traceback="Traceback...\nKeyError: 'Survived'",
617
+ )
618
+ msg = make_tool_msg("jupyter_cell_tool", content)
619
+
620
+ def mock_wrap_model_call(fn):
621
+ return fn
622
+
623
+ middleware = create_error_recovery_middleware(mock_wrap_model_call)
624
+ request = MockRequest(messages=[msg])
625
+ handler_called = []
626
+
627
+ def handler(req):
628
+ handler_called.append(req)
629
+ return "ok"
630
+
631
+ middleware(request, handler)
632
+
633
+ assert len(handler_called) == 1
634
+ modified_req = handler_called[0]
635
+ # Should have injected a HumanMessage
636
+ assert len(modified_req.messages) == 2
637
+ injected = modified_req.messages[1]
638
+ assert isinstance(injected, HumanMessage)
639
+ assert ERROR_RECOVERY_MARKER in injected.content
640
+ assert "KeyError" in injected.content
641
+
642
+ def test_extract_error_parts_re_search_embedded(self):
643
+ """re.search should find errors embedded in non-standard format."""
644
+ content = "[Execution Error] NameError: name 'df' is not defined"
645
+ error_type, error_msg, tb = _extract_error_parts(content)
646
+ assert error_type == "NameError"
647
+ assert "df" in error_msg