codebuff-agent 2.1.74__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- jupyter_ext/tools/user_tools.py +132 -0
|
@@ -0,0 +1,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,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
|