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.
- agent_server/__init__.py +8 -0
- agent_server/core/__init__.py +92 -0
- agent_server/core/api_key_manager.py +427 -0
- agent_server/core/code_validator.py +1238 -0
- agent_server/core/context_condenser.py +308 -0
- agent_server/core/embedding_service.py +254 -0
- agent_server/core/error_classifier.py +577 -0
- agent_server/core/llm_client.py +95 -0
- agent_server/core/llm_service.py +649 -0
- agent_server/core/notebook_generator.py +274 -0
- agent_server/core/prompt_builder.py +35 -0
- agent_server/core/rag_manager.py +742 -0
- agent_server/core/reflection_engine.py +489 -0
- agent_server/core/retriever.py +248 -0
- agent_server/core/state_verifier.py +452 -0
- agent_server/core/summary_generator.py +484 -0
- agent_server/core/task_manager.py +198 -0
- agent_server/knowledge/__init__.py +9 -0
- agent_server/knowledge/watchdog_service.py +352 -0
- agent_server/main.py +160 -0
- agent_server/prompts/__init__.py +60 -0
- agent_server/prompts/file_action_prompts.py +113 -0
- agent_server/routers/__init__.py +9 -0
- agent_server/routers/agent.py +591 -0
- agent_server/routers/chat.py +188 -0
- agent_server/routers/config.py +100 -0
- agent_server/routers/file_resolver.py +293 -0
- agent_server/routers/health.py +42 -0
- agent_server/routers/rag.py +163 -0
- agent_server/schemas/__init__.py +60 -0
- hdsp_agent_core/__init__.py +158 -0
- hdsp_agent_core/factory.py +252 -0
- hdsp_agent_core/interfaces.py +203 -0
- hdsp_agent_core/knowledge/__init__.py +31 -0
- hdsp_agent_core/knowledge/chunking.py +356 -0
- hdsp_agent_core/knowledge/libraries/dask.md +188 -0
- hdsp_agent_core/knowledge/libraries/matplotlib.md +164 -0
- hdsp_agent_core/knowledge/libraries/polars.md +68 -0
- hdsp_agent_core/knowledge/loader.py +337 -0
- hdsp_agent_core/llm/__init__.py +13 -0
- hdsp_agent_core/llm/service.py +556 -0
- hdsp_agent_core/managers/__init__.py +22 -0
- hdsp_agent_core/managers/config_manager.py +133 -0
- hdsp_agent_core/managers/session_manager.py +251 -0
- hdsp_agent_core/models/__init__.py +115 -0
- hdsp_agent_core/models/agent.py +316 -0
- hdsp_agent_core/models/chat.py +41 -0
- hdsp_agent_core/models/common.py +95 -0
- hdsp_agent_core/models/rag.py +368 -0
- hdsp_agent_core/prompts/__init__.py +63 -0
- hdsp_agent_core/prompts/auto_agent_prompts.py +1260 -0
- hdsp_agent_core/prompts/cell_action_prompts.py +98 -0
- hdsp_agent_core/services/__init__.py +18 -0
- hdsp_agent_core/services/agent_service.py +438 -0
- hdsp_agent_core/services/chat_service.py +205 -0
- hdsp_agent_core/services/rag_service.py +262 -0
- hdsp_agent_core/tests/__init__.py +1 -0
- hdsp_agent_core/tests/conftest.py +102 -0
- hdsp_agent_core/tests/test_factory.py +251 -0
- hdsp_agent_core/tests/test_services.py +326 -0
- hdsp_jupyter_extension-2.0.0.data/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +7 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/build_log.json +738 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/install.json +5 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/package.json +134 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2607ff74c74acfa83158.js +4369 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +1 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.622c1a5918b3aafb2315.js +12496 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.622c1a5918b3aafb2315.js.map +1 -0
- 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
- 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
- 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
- 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
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.dae97cde171e13b8c834.js +623 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.dae97cde171e13b8c834.js.map +1 -0
- hdsp_jupyter_extension-2.0.0.data/data/share/jupyter/labextensions/hdsp-agent/static/style.js +4 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- hdsp_jupyter_extension-2.0.0.dist-info/METADATA +152 -0
- hdsp_jupyter_extension-2.0.0.dist-info/RECORD +121 -0
- hdsp_jupyter_extension-2.0.0.dist-info/WHEEL +4 -0
- hdsp_jupyter_extension-2.0.0.dist-info/licenses/LICENSE +21 -0
- jupyter_ext/__init__.py +233 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/config.py +111 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +7 -0
- jupyter_ext/handlers.py +632 -0
- jupyter_ext/labextension/build_log.json +738 -0
- jupyter_ext/labextension/package.json +134 -0
- jupyter_ext/labextension/static/frontend_styles_index_js.2607ff74c74acfa83158.js +4369 -0
- jupyter_ext/labextension/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +1 -0
- jupyter_ext/labextension/static/lib_index_js.622c1a5918b3aafb2315.js +12496 -0
- jupyter_ext/labextension/static/lib_index_js.622c1a5918b3aafb2315.js.map +1 -0
- jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +94 -0
- jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +1 -0
- jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +94 -0
- jupyter_ext/labextension/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +1 -0
- jupyter_ext/labextension/static/remoteEntry.dae97cde171e13b8c834.js +623 -0
- jupyter_ext/labextension/static/remoteEntry.dae97cde171e13b8c834.js.map +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +507 -0
- jupyter_ext/labextension/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js +2071 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js +1059 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +376 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +60336 -0
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js +7132 -0
- 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
|