hdsp-jupyter-extension 2.0.0__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 (121) hide show
  1. agent_server/__init__.py +8 -0
  2. agent_server/core/__init__.py +92 -0
  3. agent_server/core/api_key_manager.py +427 -0
  4. agent_server/core/code_validator.py +1238 -0
  5. agent_server/core/context_condenser.py +308 -0
  6. agent_server/core/embedding_service.py +254 -0
  7. agent_server/core/error_classifier.py +577 -0
  8. agent_server/core/llm_client.py +95 -0
  9. agent_server/core/llm_service.py +649 -0
  10. agent_server/core/notebook_generator.py +274 -0
  11. agent_server/core/prompt_builder.py +35 -0
  12. agent_server/core/rag_manager.py +742 -0
  13. agent_server/core/reflection_engine.py +489 -0
  14. agent_server/core/retriever.py +248 -0
  15. agent_server/core/state_verifier.py +452 -0
  16. agent_server/core/summary_generator.py +484 -0
  17. agent_server/core/task_manager.py +198 -0
  18. agent_server/knowledge/__init__.py +9 -0
  19. agent_server/knowledge/watchdog_service.py +352 -0
  20. agent_server/main.py +160 -0
  21. agent_server/prompts/__init__.py +60 -0
  22. agent_server/prompts/file_action_prompts.py +113 -0
  23. agent_server/routers/__init__.py +9 -0
  24. agent_server/routers/agent.py +591 -0
  25. agent_server/routers/chat.py +188 -0
  26. agent_server/routers/config.py +100 -0
  27. agent_server/routers/file_resolver.py +293 -0
  28. agent_server/routers/health.py +42 -0
  29. agent_server/routers/rag.py +163 -0
  30. agent_server/schemas/__init__.py +60 -0
  31. hdsp_agent_core/__init__.py +158 -0
  32. hdsp_agent_core/factory.py +252 -0
  33. hdsp_agent_core/interfaces.py +203 -0
  34. hdsp_agent_core/knowledge/__init__.py +31 -0
  35. hdsp_agent_core/knowledge/chunking.py +356 -0
  36. hdsp_agent_core/knowledge/libraries/dask.md +188 -0
  37. hdsp_agent_core/knowledge/libraries/matplotlib.md +164 -0
  38. hdsp_agent_core/knowledge/libraries/polars.md +68 -0
  39. hdsp_agent_core/knowledge/loader.py +337 -0
  40. hdsp_agent_core/llm/__init__.py +13 -0
  41. hdsp_agent_core/llm/service.py +556 -0
  42. hdsp_agent_core/managers/__init__.py +22 -0
  43. hdsp_agent_core/managers/config_manager.py +133 -0
  44. hdsp_agent_core/managers/session_manager.py +251 -0
  45. hdsp_agent_core/models/__init__.py +115 -0
  46. hdsp_agent_core/models/agent.py +316 -0
  47. hdsp_agent_core/models/chat.py +41 -0
  48. hdsp_agent_core/models/common.py +95 -0
  49. hdsp_agent_core/models/rag.py +368 -0
  50. hdsp_agent_core/prompts/__init__.py +63 -0
  51. hdsp_agent_core/prompts/auto_agent_prompts.py +1260 -0
  52. hdsp_agent_core/prompts/cell_action_prompts.py +98 -0
  53. hdsp_agent_core/services/__init__.py +18 -0
  54. hdsp_agent_core/services/agent_service.py +438 -0
  55. hdsp_agent_core/services/chat_service.py +205 -0
  56. hdsp_agent_core/services/rag_service.py +262 -0
  57. hdsp_agent_core/tests/__init__.py +1 -0
  58. hdsp_agent_core/tests/conftest.py +102 -0
  59. hdsp_agent_core/tests/test_factory.py +251 -0
  60. hdsp_agent_core/tests/test_services.py +326 -0
  61. hdsp_jupyter_extension-2.0.0.data/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +7 -0
  62. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/build_log.json +738 -0
  63. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/install.json +5 -0
  64. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/package.json +134 -0
  65. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2607ff74c74acfa83158.js +4369 -0
  66. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +1 -0
  67. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.622c1a5918b3aafb2315.js +12496 -0
  68. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.622c1a5918b3aafb2315.js.map +1 -0
  69. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +94 -0
  70. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +1 -0
  71. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +94 -0
  72. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +1 -0
  73. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.dae97cde171e13b8c834.js +623 -0
  74. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.dae97cde171e13b8c834.js.map +1 -0
  75. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/style.js +4 -0
  76. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +507 -0
  77. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +1 -0
  78. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js +2071 -0
  79. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +1 -0
  80. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js +1059 -0
  81. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +1 -0
  82. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +376 -0
  83. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +1 -0
  84. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +60336 -0
  85. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +1 -0
  86. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js +7132 -0
  87. hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +1 -0
  88. hdsp_jupyter_extension-2.0.0.dist-info/METADATA +152 -0
  89. hdsp_jupyter_extension-2.0.0.dist-info/RECORD +121 -0
  90. hdsp_jupyter_extension-2.0.0.dist-info/WHEEL +4 -0
  91. hdsp_jupyter_extension-2.0.0.dist-info/licenses/LICENSE +21 -0
  92. jupyter_ext/__init__.py +233 -0
  93. jupyter_ext/_version.py +4 -0
  94. jupyter_ext/config.py +111 -0
  95. jupyter_ext/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +7 -0
  96. jupyter_ext/handlers.py +632 -0
  97. jupyter_ext/labextension/build_log.json +738 -0
  98. jupyter_ext/labextension/package.json +134 -0
  99. jupyter_ext/labextension/static/frontend_styles_index_js.2607ff74c74acfa83158.js +4369 -0
  100. jupyter_ext/labextension/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +1 -0
  101. jupyter_ext/labextension/static/lib_index_js.622c1a5918b3aafb2315.js +12496 -0
  102. jupyter_ext/labextension/static/lib_index_js.622c1a5918b3aafb2315.js.map +1 -0
  103. jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +94 -0
  104. jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +1 -0
  105. jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +94 -0
  106. jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +1 -0
  107. jupyter_ext/labextension/static/remoteEntry.dae97cde171e13b8c834.js +623 -0
  108. jupyter_ext/labextension/static/remoteEntry.dae97cde171e13b8c834.js.map +1 -0
  109. jupyter_ext/labextension/static/style.js +4 -0
  110. jupyter_ext/labextension/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +507 -0
  111. jupyter_ext/labextension/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +1 -0
  112. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js +2071 -0
  113. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +1 -0
  114. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js +1059 -0
  115. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +1 -0
  116. jupyter_ext/labextension/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +376 -0
  117. jupyter_ext/labextension/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +1 -0
  118. jupyter_ext/labextension/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +60336 -0
  119. jupyter_ext/labextension/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +1 -0
  120. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js +7132 -0
  121. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +1 -0
@@ -0,0 +1,248 @@
1
+ """
2
+ Retriever - Dense vector search implementation.
3
+
4
+ Provides semantic similarity search via Qdrant vector database.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from hdsp_agent_core.models.rag import RAGConfig
14
+
15
+ from agent_server.core.embedding_service import EmbeddingService
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ChunkScoreDetails:
22
+ """청크별 점수 상세 정보"""
23
+
24
+ chunk_id: str
25
+ content: str
26
+ score: float # Vector similarity score (0-1)
27
+ rank: int
28
+ metadata: Dict[str, Any]
29
+ passed_threshold: bool
30
+
31
+
32
+ class DebugSearchResult(NamedTuple):
33
+ """디버그 검색 결과"""
34
+
35
+ chunks: List[ChunkScoreDetails]
36
+ search_ms: float
37
+ total_candidates: int
38
+
39
+
40
+ class Retriever:
41
+ """
42
+ Dense vector retrieval using Qdrant.
43
+
44
+ Features:
45
+ - Dense vector search via Qdrant
46
+ - Metadata filtering
47
+ - Score thresholding
48
+
49
+ Usage:
50
+ retriever = Retriever(client, embedding_service, config)
51
+ results = await retriever.search("query", top_k=5)
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ client, # Qdrant client
57
+ embedding_service: "EmbeddingService",
58
+ config: "RAGConfig",
59
+ ):
60
+ self._client = client
61
+ self._embedding_service = embedding_service
62
+ self._config = config
63
+
64
+ async def search(
65
+ self,
66
+ query: str,
67
+ top_k: Optional[int] = None,
68
+ filters: Optional[Dict[str, Any]] = None,
69
+ score_threshold: Optional[float] = None,
70
+ ) -> List[Dict[str, Any]]:
71
+ """
72
+ Perform dense vector search.
73
+
74
+ Args:
75
+ query: Search query
76
+ top_k: Number of results (default from config)
77
+ filters: Metadata filters
78
+ score_threshold: Minimum score (default from config)
79
+
80
+ Returns:
81
+ List of results with content, score, metadata
82
+ """
83
+ effective_top_k = top_k or self._config.top_k
84
+ effective_threshold = score_threshold or self._config.score_threshold
85
+
86
+ # Generate query embedding
87
+ query_embedding = self._embedding_service.embed_query(query)
88
+
89
+ # Build filter condition
90
+ qdrant_filter = self._build_filter(filters) if filters else None
91
+
92
+ # Dense vector search
93
+ try:
94
+ results = self._client.search(
95
+ collection_name=self._config.qdrant.collection_name,
96
+ query_vector=query_embedding,
97
+ query_filter=qdrant_filter,
98
+ limit=effective_top_k,
99
+ score_threshold=effective_threshold
100
+ * 0.5, # Lower for initial retrieval
101
+ )
102
+ except Exception as e:
103
+ logger.error(f"Search failed: {e}")
104
+ return []
105
+
106
+ if not results:
107
+ logger.debug(f"No results for query: {query[:50]}...")
108
+ return []
109
+
110
+ return self._format_results(results, effective_threshold)
111
+
112
+ def _build_filter(self, filters: Dict[str, Any]) -> Optional[Dict[str, Any]]:
113
+ """Convert filter dict to Qdrant filter format"""
114
+ if not filters:
115
+ return None
116
+
117
+ conditions = []
118
+ for key, value in filters.items():
119
+ if isinstance(value, list):
120
+ # Multiple values - any match
121
+ conditions.append(
122
+ {"should": [{"key": key, "match": {"value": v}} for v in value]}
123
+ )
124
+ else:
125
+ conditions.append({"key": key, "match": {"value": value}})
126
+
127
+ return {"must": conditions} if conditions else None
128
+
129
+ def _format_results(
130
+ self, results: List, score_threshold: float
131
+ ) -> List[Dict[str, Any]]:
132
+ """Format Qdrant results to standard format"""
133
+ formatted = []
134
+ for r in results:
135
+ if r.score < score_threshold:
136
+ continue
137
+
138
+ formatted.append(
139
+ {
140
+ "content": r.payload.get("content", ""),
141
+ "score": round(r.score, 4),
142
+ "metadata": {k: v for k, v in r.payload.items() if k != "content"},
143
+ }
144
+ )
145
+ return formatted
146
+
147
+ def search_sync(
148
+ self,
149
+ query: str,
150
+ top_k: Optional[int] = None,
151
+ filters: Optional[Dict[str, Any]] = None,
152
+ ) -> List[Dict[str, Any]]:
153
+ """
154
+ Synchronous search wrapper for non-async contexts.
155
+
156
+ Note: Qdrant client operations are synchronous,
157
+ so this is just a convenience method.
158
+ """
159
+ import asyncio
160
+
161
+ try:
162
+ asyncio.get_running_loop()
163
+ logger.warning(
164
+ "search_sync called from async context, use search() instead"
165
+ )
166
+ except RuntimeError:
167
+ pass
168
+
169
+ return asyncio.run(self.search(query, top_k, filters))
170
+
171
+ async def search_with_debug(
172
+ self,
173
+ query: str,
174
+ top_k: Optional[int] = None,
175
+ filters: Optional[Dict[str, Any]] = None,
176
+ score_threshold: Optional[float] = None,
177
+ ) -> DebugSearchResult:
178
+ """
179
+ 전체 점수 정보를 포함한 디버그 검색 수행.
180
+
181
+ Args:
182
+ query: Search query
183
+ top_k: Number of results (default from config)
184
+ filters: Metadata filters
185
+ score_threshold: Minimum score (default from config)
186
+
187
+ Returns:
188
+ DebugSearchResult with detailed scoring information
189
+ """
190
+ start_time = time.perf_counter()
191
+
192
+ effective_top_k = top_k or self._config.top_k
193
+ effective_threshold = score_threshold or self._config.score_threshold
194
+
195
+ # Generate query embedding
196
+ query_embedding = self._embedding_service.embed_query(query)
197
+
198
+ # Build filter condition
199
+ qdrant_filter = self._build_filter(filters) if filters else None
200
+
201
+ # Vector search with timing
202
+ try:
203
+ # 디버그용으로 더 많은 결과 (3배)를 낮은 threshold로 가져옴
204
+ results = self._client.search(
205
+ collection_name=self._config.qdrant.collection_name,
206
+ query_vector=query_embedding,
207
+ query_filter=qdrant_filter,
208
+ limit=effective_top_k * 3,
209
+ score_threshold=effective_threshold * 0.3,
210
+ )
211
+ except Exception as e:
212
+ logger.error(f"Search failed: {e}")
213
+ return DebugSearchResult(
214
+ chunks=[],
215
+ search_ms=0.0,
216
+ total_candidates=0,
217
+ )
218
+
219
+ search_ms = (time.perf_counter() - start_time) * 1000
220
+
221
+ if not results:
222
+ return DebugSearchResult(
223
+ chunks=[],
224
+ search_ms=round(search_ms, 2),
225
+ total_candidates=0,
226
+ )
227
+
228
+ # Build detailed results
229
+ chunks = []
230
+ for rank, result in enumerate(results, start=1):
231
+ chunks.append(
232
+ ChunkScoreDetails(
233
+ chunk_id=str(result.id),
234
+ content=result.payload.get("content", ""),
235
+ score=round(result.score, 4),
236
+ rank=rank,
237
+ metadata={
238
+ k: v for k, v in result.payload.items() if k != "content"
239
+ },
240
+ passed_threshold=result.score >= effective_threshold,
241
+ )
242
+ )
243
+
244
+ return DebugSearchResult(
245
+ chunks=chunks,
246
+ search_ms=round(search_ms, 2),
247
+ total_candidates=len(results),
248
+ )
@@ -0,0 +1,452 @@
1
+ """
2
+ State Verifier - 상태 검증 레이어 (Phase 1)
3
+ 각 단계 실행 후 예상 상태와 실제 상태 비교, 신뢰도 계산, 리플래닝 트리거 결정
4
+ LLM 호출 없이 결정론적 검증 수행
5
+ """
6
+
7
+ import re
8
+ import time
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ class MismatchType(Enum):
15
+ """상태 불일치 유형"""
16
+
17
+ VARIABLE_MISSING = "variable_missing"
18
+ VARIABLE_TYPE_MISMATCH = "variable_type_mismatch"
19
+ OUTPUT_MISSING = "output_missing"
20
+ OUTPUT_MISMATCH = "output_mismatch"
21
+ FILE_NOT_CREATED = "file_not_created"
22
+ IMPORT_FAILED = "import_failed"
23
+ EXCEPTION_OCCURRED = "exception_occurred"
24
+ PARTIAL_EXECUTION = "partial_execution"
25
+
26
+
27
+ class Severity(Enum):
28
+ """불일치 심각도"""
29
+
30
+ CRITICAL = "critical"
31
+ MAJOR = "major"
32
+ MINOR = "minor"
33
+
34
+
35
+ class Recommendation(Enum):
36
+ """권장 사항"""
37
+
38
+ PROCEED = "proceed" # confidence >= 0.8
39
+ WARNING = "warning" # 0.6 <= confidence < 0.8
40
+ REPLAN = "replan" # 0.4 <= confidence < 0.6
41
+ ESCALATE = "escalate" # confidence < 0.4
42
+
43
+
44
+ # 신뢰도 임계값
45
+ CONFIDENCE_THRESHOLDS = {
46
+ "PROCEED": 0.8,
47
+ "WARNING": 0.6,
48
+ "REPLAN": 0.4,
49
+ "ESCALATE": 0.2,
50
+ }
51
+
52
+ # 기본 가중치
53
+ DEFAULT_WEIGHTS = {
54
+ "output_match": 0.3,
55
+ "variable_creation": 0.3,
56
+ "no_exceptions": 0.25,
57
+ "execution_complete": 0.15,
58
+ }
59
+
60
+
61
+ @dataclass
62
+ class StateMismatch:
63
+ """개별 상태 불일치 상세 정보"""
64
+
65
+ type: MismatchType
66
+ severity: Severity
67
+ description: str
68
+ expected: Optional[str] = None
69
+ actual: Optional[str] = None
70
+ suggestion: Optional[str] = None
71
+
72
+ def to_dict(self) -> Dict[str, Any]:
73
+ return {
74
+ "type": self.type.value,
75
+ "severity": self.severity.value,
76
+ "description": self.description,
77
+ "expected": self.expected,
78
+ "actual": self.actual,
79
+ "suggestion": self.suggestion,
80
+ }
81
+
82
+
83
+ @dataclass
84
+ class ConfidenceScore:
85
+ """신뢰도 계산 상세 정보"""
86
+
87
+ overall: float
88
+ factors: Dict[str, float]
89
+ weights: Dict[str, float]
90
+
91
+ def to_dict(self) -> Dict[str, Any]:
92
+ return {
93
+ "overall": self.overall,
94
+ "factors": self.factors,
95
+ "weights": self.weights,
96
+ }
97
+
98
+
99
+ @dataclass
100
+ class StateVerificationResult:
101
+ """상태 검증 결과"""
102
+
103
+ is_valid: bool
104
+ confidence: float
105
+ confidence_details: ConfidenceScore
106
+ mismatches: List[StateMismatch]
107
+ recommendation: Recommendation
108
+ timestamp: int
109
+
110
+ def to_dict(self) -> Dict[str, Any]:
111
+ return {
112
+ "isValid": self.is_valid,
113
+ "confidence": self.confidence,
114
+ "confidenceDetails": self.confidence_details.to_dict(),
115
+ "mismatches": [m.to_dict() for m in self.mismatches],
116
+ "recommendation": self.recommendation.value,
117
+ "timestamp": self.timestamp,
118
+ }
119
+
120
+
121
+ class StateVerifier:
122
+ """
123
+ 상태 검증기 (결정론적, LLM 호출 없음)
124
+ - 실행 결과 기반 상태 불일치 감지
125
+ - 신뢰도 점수 계산
126
+ - 권장 사항 결정
127
+ """
128
+
129
+ # 에러 타입별 복구 제안
130
+ ERROR_SUGGESTIONS: Dict[str, str] = {
131
+ "ModuleNotFoundError": "누락된 패키지를 설치하세요 (pip install)",
132
+ "NameError": "변수가 정의되었는지 확인하세요. 이전 셀을 먼저 실행해야 할 수 있습니다.",
133
+ "SyntaxError": "코드 문법을 확인하세요",
134
+ "TypeError": "함수 인자 타입을 확인하세요",
135
+ "ValueError": "입력 값의 범위나 형식을 확인하세요",
136
+ "KeyError": "딕셔너리 키가 존재하는지 확인하세요",
137
+ "IndexError": "리스트/배열 인덱스가 범위 내인지 확인하세요",
138
+ "FileNotFoundError": "파일 경로가 올바른지 확인하세요",
139
+ "AttributeError": "객체에 해당 속성/메서드가 있는지 확인하세요",
140
+ }
141
+
142
+ # Import 에러 패턴
143
+ MODULE_ERROR_PATTERNS = [
144
+ r"No module named ['\"]([^'\"]+)['\"]",
145
+ r"cannot import name ['\"]([^'\"]+)['\"]",
146
+ ]
147
+
148
+ def __init__(self, weights: Dict[str, float] = None):
149
+ self.weights = weights or DEFAULT_WEIGHTS.copy()
150
+ self.verification_history: List[StateVerificationResult] = []
151
+
152
+ def verify(
153
+ self,
154
+ step_number: int,
155
+ executed_code: str,
156
+ execution_output: str,
157
+ execution_status: str, # "ok" or "error"
158
+ error_message: Optional[str] = None,
159
+ expected_variables: Optional[List[str]] = None,
160
+ expected_output_patterns: Optional[List[str]] = None,
161
+ previous_variables: Optional[List[str]] = None,
162
+ current_variables: Optional[List[str]] = None,
163
+ ) -> StateVerificationResult:
164
+ """
165
+ 스텝 실행 결과 검증
166
+
167
+ Args:
168
+ step_number: 실행된 스텝 번호
169
+ executed_code: 실행된 코드
170
+ execution_output: 실행 출력
171
+ execution_status: 실행 상태 ("ok" 또는 "error")
172
+ error_message: 에러 메시지 (있는 경우)
173
+ expected_variables: 예상 변수 목록
174
+ expected_output_patterns: 예상 출력 패턴 목록
175
+ previous_variables: 실행 전 변수 목록
176
+ current_variables: 실행 후 변수 목록
177
+
178
+ Returns:
179
+ StateVerificationResult: 검증 결과
180
+ """
181
+ mismatches: List[StateMismatch] = []
182
+ factors = {
183
+ "output_match": 1.0,
184
+ "variable_creation": 1.0,
185
+ "no_exceptions": 1.0,
186
+ "execution_complete": 1.0,
187
+ }
188
+
189
+ # 1. 실행 완료 여부 확인
190
+ if execution_status == "error":
191
+ factors["no_exceptions"] = 0.0
192
+ factors["execution_complete"] = 0.0
193
+
194
+ # 에러 타입 추출
195
+ error_type = self._extract_error_type(error_message or "")
196
+ suggestion = self.ERROR_SUGGESTIONS.get(
197
+ error_type, "에러 메시지를 확인하고 코드를 수정하세요"
198
+ )
199
+
200
+ mismatches.append(
201
+ StateMismatch(
202
+ type=MismatchType.EXCEPTION_OCCURRED,
203
+ severity=Severity.CRITICAL,
204
+ description=f"실행 중 예외 발생: {error_type}",
205
+ expected="에러 없음",
206
+ actual=error_message[:200] if error_message else "Unknown error",
207
+ suggestion=suggestion,
208
+ )
209
+ )
210
+
211
+ # Import 에러 특별 처리
212
+ if error_type in ("ModuleNotFoundError", "ImportError"):
213
+ import_mismatch = self._check_import_error(error_message or "")
214
+ if import_mismatch:
215
+ mismatches.append(import_mismatch)
216
+
217
+ # 2. 변수 생성 검증
218
+ if (
219
+ expected_variables
220
+ and previous_variables is not None
221
+ and current_variables is not None
222
+ ):
223
+ var_score, var_mismatches = self._verify_variables(
224
+ expected_variables, previous_variables, current_variables
225
+ )
226
+ factors["variable_creation"] = var_score
227
+ mismatches.extend(var_mismatches)
228
+
229
+ # 3. 출력 패턴 검증
230
+ if expected_output_patterns:
231
+ output_score, output_mismatches = self._verify_output_patterns(
232
+ expected_output_patterns, execution_output
233
+ )
234
+ factors["output_match"] = output_score
235
+ mismatches.extend(output_mismatches)
236
+
237
+ # 4. 신뢰도 계산
238
+ confidence_details = self._calculate_confidence(factors)
239
+
240
+ # 5. 권장 사항 결정
241
+ recommendation = self._determine_recommendation(confidence_details.overall)
242
+
243
+ # 6. 유효성 판단 (critical 불일치가 없으면 유효)
244
+ is_valid = not any(m.severity == Severity.CRITICAL for m in mismatches)
245
+
246
+ result = StateVerificationResult(
247
+ is_valid=is_valid,
248
+ confidence=confidence_details.overall,
249
+ confidence_details=confidence_details,
250
+ mismatches=mismatches,
251
+ recommendation=recommendation,
252
+ timestamp=int(time.time() * 1000),
253
+ )
254
+
255
+ # 이력 저장
256
+ self.verification_history.append(result)
257
+
258
+ return result
259
+
260
+ def _extract_error_type(self, error_message: str) -> str:
261
+ """에러 메시지에서 에러 타입 추출"""
262
+ # "TypeError: ..." 형태에서 타입 추출
263
+ if ":" in error_message:
264
+ potential_type = error_message.split(":")[0].strip()
265
+ # 알려진 에러 타입인지 확인
266
+ if potential_type in self.ERROR_SUGGESTIONS:
267
+ return potential_type
268
+
269
+ # 에러 메시지에서 키워드 검색
270
+ error_keywords = [
271
+ "ModuleNotFoundError",
272
+ "ImportError",
273
+ "NameError",
274
+ "TypeError",
275
+ "ValueError",
276
+ "KeyError",
277
+ "IndexError",
278
+ "FileNotFoundError",
279
+ "AttributeError",
280
+ "SyntaxError",
281
+ ]
282
+ for keyword in error_keywords:
283
+ if keyword in error_message:
284
+ return keyword
285
+
286
+ return "RuntimeError"
287
+
288
+ def _check_import_error(self, error_message: str) -> Optional[StateMismatch]:
289
+ """Import 에러 확인 및 누락 모듈 추출"""
290
+ for pattern in self.MODULE_ERROR_PATTERNS:
291
+ match = re.search(pattern, error_message, re.IGNORECASE)
292
+ if match:
293
+ module_name = match.group(1).split(".")[0]
294
+ return StateMismatch(
295
+ type=MismatchType.IMPORT_FAILED,
296
+ severity=Severity.CRITICAL,
297
+ description=f"모듈 '{module_name}' import 실패",
298
+ expected=f"{module_name} 모듈이 설치되어 있어야 함",
299
+ actual=error_message[:100],
300
+ suggestion=f"pip install {module_name} 또는 conda install {module_name}로 설치하세요",
301
+ )
302
+ return None
303
+
304
+ def _verify_variables(
305
+ self,
306
+ expected_vars: List[str],
307
+ previous_vars: List[str],
308
+ current_vars: List[str],
309
+ ) -> tuple[float, List[StateMismatch]]:
310
+ """변수 생성 검증"""
311
+ mismatches: List[StateMismatch] = []
312
+ previous_set = set(previous_vars)
313
+ current_set = set(current_vars)
314
+
315
+ # 새로 생성된 변수
316
+ created_vars = current_set - previous_set
317
+
318
+ match_count = 0
319
+ for expected in expected_vars:
320
+ if expected in created_vars or expected in current_set:
321
+ match_count += 1
322
+ else:
323
+ mismatches.append(
324
+ StateMismatch(
325
+ type=MismatchType.VARIABLE_MISSING,
326
+ severity=Severity.MAJOR,
327
+ description=f"예상 변수 '{expected}'가 생성되지 않음",
328
+ expected=expected,
329
+ actual="(없음)",
330
+ suggestion=f"변수 '{expected}'를 생성하는 코드가 올바르게 실행되었는지 확인하세요",
331
+ )
332
+ )
333
+
334
+ score = match_count / len(expected_vars) if expected_vars else 1.0
335
+ return score, mismatches
336
+
337
+ def _verify_output_patterns(
338
+ self,
339
+ patterns: List[str],
340
+ output: str,
341
+ ) -> tuple[float, List[StateMismatch]]:
342
+ """출력 패턴 검증"""
343
+ mismatches: List[StateMismatch] = []
344
+
345
+ match_count = 0
346
+ for pattern in patterns:
347
+ try:
348
+ regex = re.compile(pattern, re.IGNORECASE)
349
+ if regex.search(output):
350
+ match_count += 1
351
+ else:
352
+ mismatches.append(
353
+ StateMismatch(
354
+ type=MismatchType.OUTPUT_MISMATCH,
355
+ severity=Severity.MINOR,
356
+ description="출력에서 예상 패턴을 찾을 수 없음",
357
+ expected=pattern,
358
+ actual=output[:100] + ("..." if len(output) > 100 else ""),
359
+ )
360
+ )
361
+ except re.error:
362
+ # 정규식 오류 시 문자열 포함 검사
363
+ if pattern in output:
364
+ match_count += 1
365
+
366
+ score = match_count / len(patterns) if patterns else 1.0
367
+ return score, mismatches
368
+
369
+ def _calculate_confidence(self, factors: Dict[str, float]) -> ConfidenceScore:
370
+ """신뢰도 점수 계산"""
371
+ overall = sum(
372
+ factors.get(key, 0) * self.weights.get(key, 0) for key in self.weights
373
+ )
374
+
375
+ # 0과 1 사이로 클램프
376
+ overall = max(0.0, min(1.0, overall))
377
+
378
+ return ConfidenceScore(
379
+ overall=overall,
380
+ factors=factors,
381
+ weights=self.weights.copy(),
382
+ )
383
+
384
+ def _determine_recommendation(self, confidence: float) -> Recommendation:
385
+ """신뢰도에 따른 권장 사항 결정"""
386
+ if confidence >= CONFIDENCE_THRESHOLDS["PROCEED"]:
387
+ return Recommendation.PROCEED
388
+ elif confidence >= CONFIDENCE_THRESHOLDS["WARNING"]:
389
+ return Recommendation.WARNING
390
+ elif confidence >= CONFIDENCE_THRESHOLDS["REPLAN"]:
391
+ return Recommendation.REPLAN
392
+ else:
393
+ return Recommendation.ESCALATE
394
+
395
+ def get_history(self, count: int = 5) -> List[StateVerificationResult]:
396
+ """최근 검증 이력 조회"""
397
+ return self.verification_history[-count:]
398
+
399
+ def analyze_trend(self) -> Dict[str, Any]:
400
+ """신뢰도 트렌드 분석"""
401
+ if len(self.verification_history) < 2:
402
+ return {
403
+ "average": self.verification_history[0].confidence
404
+ if self.verification_history
405
+ else 1.0,
406
+ "trend": "stable",
407
+ "critical_count": sum(
408
+ 1 for v in self.verification_history if not v.is_valid
409
+ ),
410
+ }
411
+
412
+ confidences = [v.confidence for v in self.verification_history]
413
+ average = sum(confidences) / len(confidences)
414
+
415
+ # 최근 3개와 이전 비교
416
+ recent_avg = sum(confidences[-3:]) / min(3, len(confidences))
417
+ previous_avg = (
418
+ sum(confidences[:-3]) / max(1, len(confidences) - 3)
419
+ if len(confidences) > 3
420
+ else recent_avg
421
+ )
422
+
423
+ if recent_avg > previous_avg + 0.1:
424
+ trend = "improving"
425
+ elif recent_avg < previous_avg - 0.1:
426
+ trend = "declining"
427
+ else:
428
+ trend = "stable"
429
+
430
+ return {
431
+ "average": average,
432
+ "trend": trend,
433
+ "critical_count": sum(
434
+ 1 for v in self.verification_history if not v.is_valid
435
+ ),
436
+ }
437
+
438
+ def clear_history(self):
439
+ """검증 이력 초기화"""
440
+ self.verification_history = []
441
+
442
+
443
+ # 싱글톤 인스턴스
444
+ _state_verifier_instance: Optional[StateVerifier] = None
445
+
446
+
447
+ def get_state_verifier() -> StateVerifier:
448
+ """싱글톤 StateVerifier 반환"""
449
+ global _state_verifier_instance
450
+ if _state_verifier_instance is None:
451
+ _state_verifier_instance = StateVerifier()
452
+ return _state_verifier_instance