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,255 @@
1
+ """
2
+ Session Manager for persistent conversation storage.
3
+
4
+ Provides file-based persistence for chat sessions, allowing conversation
5
+ history to survive server restarts.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import uuid
11
+ from dataclasses import asdict, dataclass, field
12
+ from datetime import datetime
13
+ from typing import Dict, List, Optional
14
+
15
+ from codebuff_core.legacy_paths import resolve_jupyter_config
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ChatMessage:
22
+ """Individual chat message in a session."""
23
+
24
+ role: str # 'user' | 'assistant'
25
+ content: str
26
+ timestamp: float = field(default_factory=lambda: datetime.now().timestamp())
27
+
28
+
29
+ @dataclass
30
+ class Session:
31
+ """Chat session with message history."""
32
+
33
+ id: str
34
+ messages: List[ChatMessage] = field(default_factory=list)
35
+ created_at: float = field(default_factory=lambda: datetime.now().timestamp())
36
+ updated_at: float = field(default_factory=lambda: datetime.now().timestamp())
37
+ metadata: Optional[Dict] = None
38
+
39
+
40
+ class SessionManager:
41
+ """
42
+ Manages chat session persistence.
43
+
44
+ Singleton pattern ensures consistent state across handlers.
45
+ Sessions are stored as JSON in ~/.jupyter/codebuff_sessions.json.
46
+ """
47
+
48
+ _instance: Optional["SessionManager"] = None
49
+
50
+ def __new__(cls) -> "SessionManager":
51
+ if cls._instance is None:
52
+ cls._instance = super().__new__(cls)
53
+ return cls._instance
54
+
55
+ def __init__(self):
56
+ if hasattr(self, "_initialized") and self._initialized:
57
+ return
58
+ self._initialized = True
59
+ self._sessions: Dict[str, Session] = {}
60
+ self._storage_path = resolve_jupyter_config(
61
+ "codebuff_sessions.json", "hdsp_agent_sessions.json"
62
+ )
63
+ self._load_sessions()
64
+
65
+ @classmethod
66
+ def reset_instance(cls):
67
+ """Reset singleton instance (for testing)"""
68
+ cls._instance = None
69
+
70
+ def _load_sessions(self) -> None:
71
+ """Load sessions from persistent storage."""
72
+ try:
73
+ if self._storage_path.exists():
74
+ data = json.loads(self._storage_path.read_text(encoding="utf-8"))
75
+ for sid, sdata in data.items():
76
+ self._sessions[sid] = Session(
77
+ id=sdata["id"],
78
+ messages=[ChatMessage(**m) for m in sdata.get("messages", [])],
79
+ created_at=sdata.get("created_at", datetime.now().timestamp()),
80
+ updated_at=sdata.get("updated_at", datetime.now().timestamp()),
81
+ metadata=sdata.get("metadata"),
82
+ )
83
+ logger.info(
84
+ f"Loaded {len(self._sessions)} sessions from {self._storage_path}"
85
+ )
86
+ except json.JSONDecodeError as e:
87
+ logger.warning(f"Failed to parse sessions file: {e}. Starting fresh.")
88
+ self._sessions = {}
89
+ except Exception as e:
90
+ logger.error(f"Failed to load sessions: {e}")
91
+ self._sessions = {}
92
+
93
+ def _save_sessions(self) -> None:
94
+ """Persist sessions to storage."""
95
+ try:
96
+ data = {}
97
+ for sid, session in self._sessions.items():
98
+ data[sid] = {
99
+ "id": session.id,
100
+ "messages": [asdict(m) for m in session.messages],
101
+ "created_at": session.created_at,
102
+ "updated_at": session.updated_at,
103
+ "metadata": session.metadata,
104
+ }
105
+ self._storage_path.parent.mkdir(parents=True, exist_ok=True)
106
+ self._storage_path.write_text(
107
+ json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8"
108
+ )
109
+ except Exception as e:
110
+ logger.error(f"Failed to save sessions: {e}")
111
+
112
+ def get_session(self, session_id: str) -> Optional[Session]:
113
+ """Get session by ID."""
114
+ return self._sessions.get(session_id)
115
+
116
+ def create_session(self, session_id: Optional[str] = None) -> Session:
117
+ """Create a new session with optional custom ID."""
118
+ sid = session_id or str(uuid.uuid4())
119
+ now = datetime.now().timestamp()
120
+ session = Session(id=sid, messages=[], created_at=now, updated_at=now)
121
+ self._sessions[sid] = session
122
+ self._save_sessions()
123
+ logger.debug(f"Created new session: {sid}")
124
+ return session
125
+
126
+ def get_or_create_session(self, session_id: Optional[str] = None) -> Session:
127
+ """Get existing session or create new one."""
128
+ if session_id and session_id in self._sessions:
129
+ return self._sessions[session_id]
130
+ return self.create_session(session_id)
131
+
132
+ def add_message(self, session_id: str, role: str, content: str) -> ChatMessage:
133
+ """Add a message to a session. Creates session if needed."""
134
+ session = self._sessions.get(session_id)
135
+ if not session:
136
+ session = self.create_session(session_id)
137
+
138
+ msg = ChatMessage(role=role, content=content)
139
+ session.messages.append(msg)
140
+ session.updated_at = msg.timestamp
141
+ self._save_sessions()
142
+ return msg
143
+
144
+ def store_messages(
145
+ self, session_id: str, user_message: str, assistant_response: str
146
+ ) -> None:
147
+ """Store user and assistant message pair. Creates session if needed."""
148
+ session = self._sessions.get(session_id)
149
+ if not session:
150
+ session = self.create_session(session_id)
151
+
152
+ now = datetime.now().timestamp()
153
+ session.messages.append(
154
+ ChatMessage(role="user", content=user_message, timestamp=now)
155
+ )
156
+ session.messages.append(
157
+ ChatMessage(role="assistant", content=assistant_response, timestamp=now)
158
+ )
159
+ session.updated_at = now
160
+ self._save_sessions()
161
+
162
+ def get_recent_messages(
163
+ self, session_id: str, limit: int = 10
164
+ ) -> List[ChatMessage]:
165
+ """Get most recent messages from a session."""
166
+ session = self._sessions.get(session_id)
167
+ if not session:
168
+ return []
169
+ return session.messages[-limit:]
170
+
171
+ def build_context(
172
+ self,
173
+ session_id: str,
174
+ max_messages: int = 5,
175
+ compress: bool = False,
176
+ target_tokens: Optional[int] = None,
177
+ ) -> Optional[str]:
178
+ """Build conversation context string from recent history.
179
+
180
+ Args:
181
+ session_id: Session ID to get context from
182
+ max_messages: Maximum messages to include (before compression)
183
+ compress: Enable context compression for token efficiency
184
+ target_tokens: Target token count for compression (default: auto)
185
+
186
+ Returns:
187
+ Formatted context string or None if session empty
188
+ """
189
+ session = self._sessions.get(session_id)
190
+ if not session or not session.messages:
191
+ return None
192
+
193
+ messages = session.messages[-max_messages:]
194
+
195
+ if compress:
196
+ # Lazy import to avoid circular dependency
197
+ try:
198
+ from codebuff_core.core.context_condenser import get_context_condenser
199
+
200
+ condenser = get_context_condenser()
201
+ msg_dicts = [{"role": m.role, "content": m.content} for m in messages]
202
+ compressed, _stats = condenser.condense(msg_dicts, target_tokens)
203
+ messages_to_format = compressed
204
+ except ImportError:
205
+ # Fallback if context_condenser not available
206
+ messages_to_format = [
207
+ {"role": m.role, "content": m.content} for m in messages
208
+ ]
209
+ else:
210
+ messages_to_format = [
211
+ {"role": m.role, "content": m.content} for m in messages
212
+ ]
213
+
214
+ return "\n".join(
215
+ [
216
+ f"{'User' if msg['role'] == 'user' else 'Assistant'}: {msg['content']}"
217
+ for msg in messages_to_format
218
+ ]
219
+ )
220
+
221
+ def list_sessions(self, limit: int = 50) -> List[Session]:
222
+ """List sessions sorted by most recently updated."""
223
+ sessions = sorted(
224
+ self._sessions.values(), key=lambda s: s.updated_at, reverse=True
225
+ )
226
+ return sessions[:limit]
227
+
228
+ def delete_session(self, session_id: str) -> bool:
229
+ """Delete a session by ID."""
230
+ if session_id in self._sessions:
231
+ del self._sessions[session_id]
232
+ self._save_sessions()
233
+ logger.debug(f"Deleted session: {session_id}")
234
+ return True
235
+ return False
236
+
237
+ def clear_all_sessions(self) -> int:
238
+ """Delete all sessions. Returns count of deleted sessions."""
239
+ count = len(self._sessions)
240
+ self._sessions = {}
241
+ self._save_sessions()
242
+ logger.info(f"Cleared {count} sessions")
243
+ return count
244
+
245
+
246
+ # Singleton accessor
247
+ _session_manager: Optional[SessionManager] = None
248
+
249
+
250
+ def get_session_manager() -> SessionManager:
251
+ """Get the singleton SessionManager instance."""
252
+ global _session_manager
253
+ if _session_manager is None:
254
+ _session_manager = SessionManager()
255
+ return _session_manager
@@ -0,0 +1,3 @@
1
+ from codebuff_core.middleware.error_recovery import create_error_recovery_middleware
2
+
3
+ __all__ = ["create_error_recovery_middleware"]
@@ -0,0 +1,394 @@
1
+ """
2
+ Error Recovery Middleware - 런타임 에러 발생 시 자동 Re-plan 프롬프트 주입
3
+
4
+ HITL 도구(jupyter_cell_tool 등)에서 에러가 반환되면:
5
+ 1. ErrorClassifier로 에러를 분류 (기존 코드 재사용)
6
+ 2. ReplanDecision에 따른 re-plan 프롬프트를 HumanMessage로 주입
7
+ 3. LLM이 write_todos 업데이트 후 수정된 코드를 실행하도록 유도
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import re
13
+ from typing import Optional, Tuple
14
+
15
+ from langchain_core.messages import HumanMessage
16
+
17
+ from codebuff_core.core.error_classifier import (
18
+ ErrorAnalysis,
19
+ ReplanDecision,
20
+ get_error_classifier,
21
+ )
22
+ from codebuff_core.middleware.logging_utils import _with_middleware_logging
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # HITL tools that execute code and can produce runtime errors
27
+ HITL_ERROR_TOOLS = {
28
+ "jupyter_cell_tool",
29
+ "jupyter_cell",
30
+ "execute_command_tool",
31
+ "execute_command",
32
+ }
33
+
34
+ # Marker prefix for error recovery messages (used for retry counting)
35
+ ERROR_RECOVERY_MARKER = "[ERROR_RECOVERY]"
36
+
37
+ # Error patterns in tool output
38
+ ERROR_PATTERNS = [
39
+ r"(\w+Error):\s*(.+)",
40
+ r"(\w+Exception):\s*(.+)",
41
+ r"Traceback \(most recent call last\)",
42
+ ]
43
+
44
+
45
+ def create_error_recovery_middleware(wrap_model_call, max_retries: int = 3):
46
+ """Create middleware that injects re-plan prompts when HITL tool errors occur.
47
+
48
+ Uses ErrorClassifier (codebuff_core.core.error_classifier) to classify errors
49
+ and generates decision-specific prompts to guide the LLM toward self-correction.
50
+
51
+ Args:
52
+ wrap_model_call: LangChain's wrap_model_call decorator
53
+ max_retries: Maximum consecutive error recovery attempts (default: 3)
54
+
55
+ Returns:
56
+ Middleware function
57
+ """
58
+ classifier = get_error_classifier()
59
+
60
+ @wrap_model_call
61
+ @_with_middleware_logging("error_recovery")
62
+ def error_recovery_replan(request, handler):
63
+ messages = request.messages
64
+ if not messages:
65
+ return handler(request)
66
+
67
+ last_msg = messages[-1]
68
+
69
+ # 1. Is this a ToolMessage from a HITL tool with an error?
70
+ if not _is_hitl_tool_error(last_msg):
71
+ return handler(request)
72
+
73
+ # 2. Check consecutive error recovery count
74
+ recovery_count = _count_error_recoveries(messages)
75
+ if recovery_count >= max_retries:
76
+ logger.warning(
77
+ "Error recovery: max retries reached (%d/%d). Passing through.",
78
+ recovery_count,
79
+ max_retries,
80
+ )
81
+ return handler(request)
82
+
83
+ # 3. Extract error parts and classify
84
+ error_type, error_msg, traceback_str = _extract_error_parts(last_msg.content)
85
+ if not error_type:
86
+ return handler(request)
87
+
88
+ analysis = classifier.classify(error_type, error_msg, traceback_str)
89
+
90
+ logger.info(
91
+ "Error recovery: %s -> %s (attempt %d/%d)",
92
+ error_type,
93
+ analysis.decision.value,
94
+ recovery_count + 1,
95
+ max_retries,
96
+ )
97
+
98
+ # 4. Build re-plan prompt
99
+ todos = request.state.get("todos", [])
100
+ prompt = _build_replan_prompt(analysis, todos, error_type, error_msg)
101
+
102
+ # 5. Inject HumanMessage with re-plan instructions
103
+ new_messages = list(messages) + [HumanMessage(content=prompt)]
104
+ request = request.override(messages=new_messages)
105
+
106
+ return handler(request)
107
+
108
+ return error_recovery_replan
109
+
110
+
111
+ def _is_hitl_tool_error(msg) -> bool:
112
+ """Check if a message is a ToolMessage from a HITL tool containing an error.
113
+
114
+ Supports both plain text errors and structured JSON with execution_result.
115
+ """
116
+ if getattr(msg, "type", "") != "tool":
117
+ return False
118
+
119
+ tool_name = getattr(msg, "name", "") or ""
120
+ if tool_name not in HITL_ERROR_TOOLS:
121
+ return False
122
+
123
+ content = getattr(msg, "content", "") or ""
124
+ if not isinstance(content, str):
125
+ return False
126
+
127
+ # Check structured execution_result first (from HITL tool execution)
128
+ try:
129
+ data = json.loads(content)
130
+ if isinstance(data, dict):
131
+ exec_result = data.get("execution_result")
132
+ if isinstance(exec_result, dict):
133
+ return exec_result.get("success") is False or bool(
134
+ exec_result.get("error")
135
+ )
136
+ except (json.JSONDecodeError, TypeError, ValueError):
137
+ pass
138
+
139
+ return _has_error_indicators(content)
140
+
141
+
142
+ def _has_error_indicators(content: str) -> bool:
143
+ """Check if content contains Python error indicators."""
144
+ for pattern in ERROR_PATTERNS:
145
+ if re.search(pattern, content):
146
+ return True
147
+ return False
148
+
149
+
150
+ def _count_error_recoveries(messages) -> int:
151
+ """Count consecutive error recovery attempts since last successful HITL result.
152
+
153
+ Scans backwards through messages. Counts ERROR_RECOVERY_MARKER occurrences.
154
+ Resets count when a successful (non-error) HITL tool result is found.
155
+ """
156
+ count = 0
157
+ for msg in reversed(messages):
158
+ content = getattr(msg, "content", "") or ""
159
+ msg_type = getattr(msg, "type", "")
160
+
161
+ if isinstance(content, str) and ERROR_RECOVERY_MARKER in content:
162
+ count += 1
163
+ elif msg_type == "tool":
164
+ tool_name = getattr(msg, "name", "") or ""
165
+ if tool_name in HITL_ERROR_TOOLS:
166
+ if isinstance(content, str) and _has_error_indicators(content):
167
+ # Error result - continue counting
168
+ continue
169
+ else:
170
+ # Successful result - reset and stop
171
+ break
172
+ # Non-HITL tool result - skip
173
+ continue
174
+ elif msg_type in ("ai", "human"):
175
+ # Skip AI/human messages that aren't recovery markers
176
+ if msg_type == "human" and isinstance(content, str):
177
+ if ERROR_RECOVERY_MARKER not in content:
178
+ break
179
+ continue
180
+ else:
181
+ break
182
+
183
+ return count
184
+
185
+
186
+ def _extract_error_parts(content: str) -> Tuple[Optional[str], str, str]:
187
+ """Extract error type, message, and traceback from tool output.
188
+
189
+ Supports two formats:
190
+ 1. Structured JSON with execution_result (from HITL tool execution)
191
+ 2. Plain text with Python error patterns (traceback, ErrorType: message)
192
+
193
+ Returns:
194
+ (error_type, error_message, traceback_string)
195
+ """
196
+ if not content or not isinstance(content, str):
197
+ return None, "", ""
198
+
199
+ # Strategy 1: Structured extraction from JSON tool output with execution_result
200
+ try:
201
+ data = json.loads(content)
202
+ if isinstance(data, dict):
203
+ exec_result = data.get("execution_result")
204
+ if isinstance(exec_result, dict):
205
+ error_type = exec_result.get("error_type")
206
+ if error_type:
207
+ error_msg = exec_result.get("error", "")
208
+ # Remove error_type prefix from error message
209
+ prefix = f"{error_type}: "
210
+ if error_msg.startswith(prefix):
211
+ error_msg = error_msg[len(prefix) :]
212
+ traceback_str = exec_result.get("traceback", "") or ""
213
+ return error_type, error_msg, traceback_str
214
+ except (json.JSONDecodeError, TypeError, ValueError):
215
+ pass
216
+
217
+ # Strategy 2: Regex-based extraction from plain text
218
+ lines = content.strip().split("\n")
219
+
220
+ error_type = None
221
+ error_msg = ""
222
+
223
+ # Search from the end for error type pattern (Python convention)
224
+ for line in reversed(lines):
225
+ line = line.strip()
226
+ # Remove ANSI escape codes
227
+ line = re.sub(r"\x1b\[[0-9;]*m", "", line)
228
+
229
+ # Match "ErrorType: error message" anywhere in the line
230
+ # (re.search handles both standalone lines and embedded in JSON/dict repr)
231
+ match = re.search(r"(\w+(?:Error|Exception)):\s*(.*)", line)
232
+ if match:
233
+ error_type = match.group(1)
234
+ error_msg = match.group(2)
235
+ break
236
+
237
+ # Match standalone error type at start of line
238
+ match = re.match(r"^(\w+(?:Error|Exception))\s*$", line)
239
+ if match:
240
+ error_type = match.group(1)
241
+ break
242
+
243
+ # Extract traceback
244
+ traceback_str = ""
245
+ traceback_match = re.search(
246
+ r"(Traceback \(most recent call last\)[\s\S]*)", content
247
+ )
248
+ if traceback_match:
249
+ traceback_str = traceback_match.group(1)
250
+
251
+ return error_type, error_msg, traceback_str
252
+
253
+
254
+ def _build_replan_prompt(
255
+ analysis: ErrorAnalysis,
256
+ todos: list,
257
+ error_type: str,
258
+ error_msg: str,
259
+ ) -> str:
260
+ """Build re-plan prompt based on ErrorClassifier analysis."""
261
+ decision = analysis.decision
262
+ guidance = _get_error_guidance(error_type, error_msg)
263
+
264
+ # Pending todos context
265
+ pending_todos = [t for t in todos if t.get("status") in ("pending", "in_progress")]
266
+ todos_context = ""
267
+ if pending_todos:
268
+ todo_items = "\n".join(
269
+ f" - [{t.get('status', '?')}] {t.get('content', '')[:60]}"
270
+ for t in pending_todos[:5]
271
+ )
272
+ todos_context = f"\n\nCurrent pending tasks:\n{todo_items}"
273
+
274
+ error_preview = error_msg[:200] if error_msg else "unknown error"
275
+
276
+ if decision == ReplanDecision.REFINE:
277
+ prompt = (
278
+ f"{ERROR_RECOVERY_MARKER} The previous code execution failed "
279
+ f"with {error_type}: {error_preview}\n\n"
280
+ f"Root cause: {analysis.root_cause}\n\n"
281
+ f"Instructions:\n"
282
+ f"1. Analyze the error carefully\n"
283
+ f"2. {guidance}\n"
284
+ f"3. Call write_todos to update your plan with the fix approach\n"
285
+ f"4. Call jupyter_cell_tool with corrected code\n"
286
+ f"5. Do NOT call final_answer_tool until the error is resolved"
287
+ f"{todos_context}"
288
+ )
289
+ elif decision == ReplanDecision.INSERT_STEPS:
290
+ pkg = analysis.missing_package or "unknown"
291
+ prompt = (
292
+ f"{ERROR_RECOVERY_MARKER} The previous code execution failed "
293
+ f"because package '{pkg}' is missing.\n\n"
294
+ f"Instructions:\n"
295
+ f"1. Call jupyter_cell_tool to install: !pip install {pkg}\n"
296
+ f"2. Then re-execute the original code\n"
297
+ f"3. Call write_todos to reflect the package installation step\n"
298
+ f"4. Do NOT call final_answer_tool until the error is resolved"
299
+ f"{todos_context}"
300
+ )
301
+ elif decision == ReplanDecision.REPLACE_STEP:
302
+ prompt = (
303
+ f"{ERROR_RECOVERY_MARKER} The previous approach completely failed "
304
+ f"with {error_type}: {error_preview}\n\n"
305
+ f"Root cause: {analysis.root_cause}\n\n"
306
+ f"Instructions:\n"
307
+ f"1. The current approach is not viable - try a completely "
308
+ f"different method\n"
309
+ f"2. {guidance}\n"
310
+ f"3. Call write_todos to replace the current step with a new "
311
+ f"approach\n"
312
+ f"4. Call jupyter_cell_tool with the new approach\n"
313
+ f"5. Do NOT call final_answer_tool until the error is resolved"
314
+ f"{todos_context}"
315
+ )
316
+ elif decision == ReplanDecision.REPLAN_REMAINING:
317
+ prompt = (
318
+ f"{ERROR_RECOVERY_MARKER} A critical error occurred: "
319
+ f"{error_type}: {error_preview}\n\n"
320
+ f"Root cause: {analysis.root_cause}\n\n"
321
+ f"Instructions:\n"
322
+ f"1. This is a fundamental issue requiring replanning all "
323
+ f"remaining steps\n"
324
+ f"2. {guidance}\n"
325
+ f"3. Call write_todos to redesign the remaining plan entirely\n"
326
+ f"4. Start executing the new plan with jupyter_cell_tool\n"
327
+ f"5. Do NOT call final_answer_tool until the error is resolved"
328
+ f"{todos_context}"
329
+ )
330
+ else:
331
+ prompt = (
332
+ f"{ERROR_RECOVERY_MARKER} An error occurred: "
333
+ f"{error_type}: {error_preview}\n\n"
334
+ f"Fix the error and continue. "
335
+ f"Do NOT call final_answer_tool until resolved."
336
+ f"{todos_context}"
337
+ )
338
+
339
+ return prompt
340
+
341
+
342
+ def _get_error_guidance(error_type: str, error_msg: str) -> str:
343
+ """Get error-specific guidance for the LLM."""
344
+ guidance_map = {
345
+ "KeyError": (
346
+ "Check the actual column/key names by running "
347
+ "`df.columns.tolist()` or `print(list(dict.keys()))` first. "
348
+ "The key name may differ from what you expected "
349
+ "(case, spaces, encoding)."
350
+ ),
351
+ "FileNotFoundError": (
352
+ "Verify the file path by running "
353
+ "`import os; print(os.listdir('.'))` or checking subdirectories. "
354
+ "The file may be in a different location or have a different name."
355
+ ),
356
+ "NameError": (
357
+ "Ensure all imports and variable definitions are in the same cell. "
358
+ "Variables from previous cells may not be available if the kernel "
359
+ "was restarted."
360
+ ),
361
+ "ModuleNotFoundError": (
362
+ "Install the missing package using "
363
+ "`!pip install <package_name>` before importing."
364
+ ),
365
+ "ImportError": (
366
+ "Check if the package is installed and the import path is correct. "
367
+ "Some packages have different import names than install names "
368
+ "(e.g., `pip install scikit-learn` but `import sklearn`)."
369
+ ),
370
+ "TypeError": (
371
+ "Check function arguments and data types. Verify the correct "
372
+ "number and types of arguments are being passed."
373
+ ),
374
+ "ValueError": (
375
+ "Check input values. Verify data types, ranges, and formats "
376
+ "match what the function expects."
377
+ ),
378
+ "IndexError": (
379
+ "Check array/list bounds. Verify the data has the expected "
380
+ "number of elements before accessing by index."
381
+ ),
382
+ "AttributeError": (
383
+ "Check if the object has the expected type. Use `type(obj)` "
384
+ "and `dir(obj)` to verify available attributes and methods."
385
+ ),
386
+ "SyntaxError": (
387
+ "Check for missing colons, brackets, parentheses, or indentation errors."
388
+ ),
389
+ }
390
+
391
+ return guidance_map.get(
392
+ error_type,
393
+ f"Analyze the {error_type} carefully and fix the root cause before retrying.",
394
+ )
@@ -0,0 +1,33 @@
1
+ """
2
+ Logging utilities for middleware.
3
+
4
+ Provides helper functions for structured logging of middleware execution.
5
+ """
6
+
7
+ import logging
8
+ from functools import wraps
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ LOG_SEPARATOR = "=" * 96
13
+
14
+
15
+ def _format_middleware_marker(name: str, stage: str) -> str:
16
+ """Format a middleware execution marker."""
17
+ return "\n".join([LOG_SEPARATOR, f"MIDDLEWARE {stage}: {name}", LOG_SEPARATOR])
18
+
19
+
20
+ def _with_middleware_logging(name: str):
21
+ """Decorator to add logging around middleware execution."""
22
+
23
+ def decorator(func):
24
+ @wraps(func)
25
+ def wrapped(request, handler):
26
+ logger.info("%s", _format_middleware_marker(name, "START"))
27
+ response = func(request, handler)
28
+ logger.info("%s", _format_middleware_marker(name, "END"))
29
+ return response
30
+
31
+ return wrapped
32
+
33
+ return decorator