PyGeoModel 1.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.
- ogmsServer2/__init__.py +33 -0
- ogmsServer2/base.py +38 -0
- ogmsServer2/constants.py +21 -0
- ogmsServer2/data/PGA.tif +0 -0
- ogmsServer2/data/baseclip.tif +0 -0
- ogmsServer2/data/intensity.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_67c32dc057e89375/LandSlide-outputIntensitypbtyTif.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_67c32dc057e89375/LandSlide-outputPGApbtyTif.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_7972c0716caa1213/LandSlide-outputIntensitypbtyTif.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_7972c0716caa1213/LandSlide-outputPGApbtyTif.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_a044c410df669372/LandSlide-outputIntensitypbtyTif.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_a044c410df669372/LandSlide-outputPGApbtyTif.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_b6ba56c07bf80c30/LandSlide-outputIntensitypbtyTif.tif +0 -0
- ogmsServer2/data//345/234/260/351/234/207/347/276/244/345/217/221/346/273/221/345/235/241/346/246/202/347/216/207/350/257/204/344/274/260/351/242/204/350/255/246/346/250/241/345/236/213_b6ba56c07bf80c30/LandSlide-outputPGApbtyTif.tif +0 -0
- ogmsServer2/openModel.py +393 -0
- ogmsServer2/openUtils/__init__.py +24 -0
- ogmsServer2/openUtils/exceptions.py +80 -0
- ogmsServer2/openUtils/http_client.py +247 -0
- ogmsServer2/openUtils/mdlUtils.py +116 -0
- ogmsServer2/openUtils/parameterValidator.py +55 -0
- ogmsServer2/openUtils/stateManager.py +69 -0
- pygeomodel-1.0.0.dist-info/METADATA +68 -0
- pygeomodel-1.0.0.dist-info/RECORD +28 -0
- pygeomodel-1.0.0.dist-info/WHEEL +5 -0
- pygeomodel-1.0.0.dist-info/licenses/LICENSE +21 -0
- pygeomodel-1.0.0.dist-info/top_level.txt +3 -0
- pygeomodel.py +2460 -0
- scripts.py +128 -0
pygeomodel.py
ADDED
|
@@ -0,0 +1,2460 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import gc
|
|
6
|
+
import weakref
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
# 延迟导入所有可能缺失的模块
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _lazy_import_ipywidgets():
|
|
14
|
+
"""延迟导入ipywidgets"""
|
|
15
|
+
global widgets
|
|
16
|
+
if 'widgets' not in globals():
|
|
17
|
+
import ipywidgets as widgets
|
|
18
|
+
return widgets
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _lazy_import_ipython_display():
|
|
22
|
+
"""延迟导入IPython display"""
|
|
23
|
+
global display, HTML, clear_output
|
|
24
|
+
if 'display' not in globals():
|
|
25
|
+
from IPython.display import display, HTML, clear_output
|
|
26
|
+
return display, HTML, clear_output
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# 基本模块
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _lazy_import_ipython():
|
|
33
|
+
"""延迟导入IPython"""
|
|
34
|
+
global get_ipython
|
|
35
|
+
if 'get_ipython' not in globals():
|
|
36
|
+
from IPython import get_ipython
|
|
37
|
+
return get_ipython
|
|
38
|
+
|
|
39
|
+
# 延迟导入 - 只在需要时导入
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _lazy_import_openmodel():
|
|
43
|
+
"""延迟导入openModel模块"""
|
|
44
|
+
global openModel
|
|
45
|
+
if 'openModel' not in globals():
|
|
46
|
+
import ogmsServer2.openModel as openModel
|
|
47
|
+
return openModel
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _lazy_import_requests():
|
|
51
|
+
"""延迟导入requests"""
|
|
52
|
+
global requests
|
|
53
|
+
if 'requests' not in globals():
|
|
54
|
+
import requests
|
|
55
|
+
return requests
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _lazy_import_academic_service():
|
|
59
|
+
"""延迟导入学术查询服务"""
|
|
60
|
+
global AcademicQueryService
|
|
61
|
+
if 'AcademicQueryService' not in globals():
|
|
62
|
+
from scripts import AcademicQueryService
|
|
63
|
+
return AcademicQueryService
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _lazy_import_openai():
|
|
67
|
+
"""延迟导入OpenAI"""
|
|
68
|
+
global OpenAI
|
|
69
|
+
if 'OpenAI' not in globals():
|
|
70
|
+
from openai import OpenAI
|
|
71
|
+
return OpenAI
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _lazy_import_filechooser():
|
|
75
|
+
"""延迟导入FileChooser"""
|
|
76
|
+
global FileChooser
|
|
77
|
+
if 'FileChooser' not in globals():
|
|
78
|
+
from ipyfilechooser import FileChooser
|
|
79
|
+
return FileChooser
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _lazy_import_markdown():
|
|
83
|
+
"""延迟导入markdown"""
|
|
84
|
+
global markdown, Markdown
|
|
85
|
+
if 'markdown' not in globals():
|
|
86
|
+
from markdown import markdown
|
|
87
|
+
from IPython.display import Markdown
|
|
88
|
+
return markdown, Markdown
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _lazy_import_nest_asyncio():
|
|
92
|
+
"""延迟导入nest_asyncio"""
|
|
93
|
+
global nest_asyncio
|
|
94
|
+
if 'nest_asyncio' not in globals():
|
|
95
|
+
import nest_asyncio
|
|
96
|
+
return nest_asyncio
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# 在文件开头应用nest_asyncio(如果可用)
|
|
100
|
+
try:
|
|
101
|
+
_lazy_import_nest_asyncio().apply()
|
|
102
|
+
except ImportError:
|
|
103
|
+
# 如果nest_asyncio不可用,跳过
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# 工具函数
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cleanup_memory():
|
|
110
|
+
"""清理内存的工具函数"""
|
|
111
|
+
gc.collect()
|
|
112
|
+
# 清理弱引用 - 使用更安全的方式
|
|
113
|
+
try:
|
|
114
|
+
# 尝试清理弱引用,如果失败则跳过
|
|
115
|
+
if hasattr(weakref, '_weakrefs'):
|
|
116
|
+
for obj in list(weakref._weakrefs):
|
|
117
|
+
if obj() is None:
|
|
118
|
+
weakref._weakrefs.remove(obj)
|
|
119
|
+
except (AttributeError, RuntimeError):
|
|
120
|
+
# 如果weakref._weakrefs不存在或访问失败,则跳过
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def safe_import(module_name):
|
|
125
|
+
"""安全导入模块"""
|
|
126
|
+
try:
|
|
127
|
+
return __import__(module_name)
|
|
128
|
+
except ImportError:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Model:
|
|
133
|
+
"""模型基类,用于处理模型的基本属性和操作"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, model_name, model_data):
|
|
136
|
+
mdl_json = model_data.get("mdlJson", {})
|
|
137
|
+
mdl = mdl_json.get("mdl", {})
|
|
138
|
+
|
|
139
|
+
self.id = model_data.get("_id", "")
|
|
140
|
+
self.name = model_name # 使用键名作为型名称
|
|
141
|
+
self.description = model_data.get("description", "")
|
|
142
|
+
self.author = model_data.get("author", "")
|
|
143
|
+
self.tags = model_data.get("normalTags", [])
|
|
144
|
+
self.tags_en = model_data.get("normalTagsEn", [])
|
|
145
|
+
|
|
146
|
+
self.states = mdl.get("states", [])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class GeoModeler:
|
|
150
|
+
"""智能地理建模助手,负责模型管理、推荐和交互界面"""
|
|
151
|
+
|
|
152
|
+
def __init__(self):
|
|
153
|
+
# 内存管理相关
|
|
154
|
+
self._instances = weakref.WeakSet() # 跟踪实例
|
|
155
|
+
self._instances.add(self)
|
|
156
|
+
|
|
157
|
+
# 模型数据 - 轻量级管理
|
|
158
|
+
self.models = {} # 存储已加载的模型(按需加载)
|
|
159
|
+
self.model_names = [] # 存储所有模型名称
|
|
160
|
+
self._model_cache = {} # 模型数据缓存
|
|
161
|
+
self._max_cache_size = 10 # 最大缓存模型数量
|
|
162
|
+
|
|
163
|
+
# UI状态
|
|
164
|
+
self.current_model = None
|
|
165
|
+
self.widgets = {} # 存储界面组件
|
|
166
|
+
self.page_size = 20
|
|
167
|
+
self.current_page = 1
|
|
168
|
+
self.filtered_models = []
|
|
169
|
+
|
|
170
|
+
# 上下文数据 - 延迟加载
|
|
171
|
+
self._context_cache = {}
|
|
172
|
+
self._context_cache_timeout = 300 # 5分钟缓存
|
|
173
|
+
|
|
174
|
+
# 初始化
|
|
175
|
+
self._load_model_names()
|
|
176
|
+
|
|
177
|
+
# 注册清理函数
|
|
178
|
+
import atexit
|
|
179
|
+
atexit.register(self._cleanup)
|
|
180
|
+
|
|
181
|
+
def _cleanup(self):
|
|
182
|
+
"""清理资源"""
|
|
183
|
+
try:
|
|
184
|
+
# 清理界面组件
|
|
185
|
+
for widget_key in list(self.widgets.keys()):
|
|
186
|
+
if widget_key in self.widgets:
|
|
187
|
+
widget = self.widgets[widget_key]
|
|
188
|
+
if hasattr(widget, 'close'):
|
|
189
|
+
widget.close()
|
|
190
|
+
del self.widgets[widget_key]
|
|
191
|
+
|
|
192
|
+
# 清理模型缓存
|
|
193
|
+
self.models.clear()
|
|
194
|
+
self._model_cache.clear()
|
|
195
|
+
self._context_cache.clear()
|
|
196
|
+
|
|
197
|
+
# 清理弱引用
|
|
198
|
+
cleanup_memory()
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(f"清理过程中出现错误: {e}")
|
|
202
|
+
|
|
203
|
+
def __del__(self):
|
|
204
|
+
"""析构函数"""
|
|
205
|
+
if hasattr(self, '_instances'):
|
|
206
|
+
self._instances.discard(self)
|
|
207
|
+
self._cleanup()
|
|
208
|
+
|
|
209
|
+
def _load_model_names(self):
|
|
210
|
+
"""轻量级加载 - 只加载模型名称,不加载完整数据"""
|
|
211
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
212
|
+
json_path = os.path.join(current_dir, "data", "computeModel.json")
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
with open(json_path, encoding='utf-8') as f:
|
|
216
|
+
models_data = json.load(f)
|
|
217
|
+
self.model_names = list(models_data.keys())
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"Failed to load model names: {str(e)}")
|
|
220
|
+
self.model_names = []
|
|
221
|
+
|
|
222
|
+
def _load_models(self):
|
|
223
|
+
"""加载所有模型的完整数据(保留原有方法作为兼容性接口)"""
|
|
224
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
225
|
+
json_path = os.path.join(current_dir, "data", "computeModel.json")
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
with open(json_path, encoding='utf-8') as f:
|
|
229
|
+
models_data = json.load(f)
|
|
230
|
+
for model_name, model_data in models_data.items():
|
|
231
|
+
self.models[model_name] = Model(model_name, model_data)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"Failed to load model configuration file: {str(e)}")
|
|
234
|
+
self.models = {}
|
|
235
|
+
|
|
236
|
+
def load_model_on_demand(self, model_name):
|
|
237
|
+
"""按需加载特定模型(带缓存和内存管理)"""
|
|
238
|
+
# 检查是否已加载
|
|
239
|
+
if model_name in self.models:
|
|
240
|
+
return self.models[model_name]
|
|
241
|
+
|
|
242
|
+
# 检查缓存
|
|
243
|
+
if model_name in self._model_cache:
|
|
244
|
+
model_data = self._model_cache[model_name]
|
|
245
|
+
self.models[model_name] = Model(model_name, model_data)
|
|
246
|
+
return self.models[model_name]
|
|
247
|
+
|
|
248
|
+
if model_name not in self.model_names:
|
|
249
|
+
print(f"Model '{model_name}' not found")
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
# 从文件加载特定模型数据
|
|
253
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
254
|
+
json_path = os.path.join(current_dir, "data", "computeModel.json")
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
with open(json_path, encoding='utf-8') as f:
|
|
258
|
+
models_data = json.load(f)
|
|
259
|
+
if model_name in models_data:
|
|
260
|
+
model_data = models_data[model_name]
|
|
261
|
+
|
|
262
|
+
# 添加到缓存
|
|
263
|
+
if len(self._model_cache) >= self._max_cache_size:
|
|
264
|
+
# 移除最旧的缓存项
|
|
265
|
+
oldest_key = next(iter(self._model_cache))
|
|
266
|
+
del self._model_cache[oldest_key]
|
|
267
|
+
|
|
268
|
+
self._model_cache[model_name] = model_data
|
|
269
|
+
self.models[model_name] = Model(model_name, model_data)
|
|
270
|
+
|
|
271
|
+
# 定期清理内存
|
|
272
|
+
if len(self.models) % 5 == 0:
|
|
273
|
+
cleanup_memory()
|
|
274
|
+
|
|
275
|
+
return self.models[model_name]
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f"Failed to load model '{model_name}': {str(e)}")
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def show_models(self):
|
|
281
|
+
"""显示模型列表界面"""
|
|
282
|
+
widgets = _lazy_import_ipywidgets()
|
|
283
|
+
main_widget = widgets.HBox(layout=widgets.Layout(width='100%'))
|
|
284
|
+
|
|
285
|
+
# 创建左侧面板
|
|
286
|
+
left_panel = widgets.VBox(
|
|
287
|
+
layout=widgets.Layout(width='300px', margin='10px'))
|
|
288
|
+
|
|
289
|
+
# 创建搜索框
|
|
290
|
+
search_box = widgets.Text(
|
|
291
|
+
placeholder='Search...',
|
|
292
|
+
description='Search:',
|
|
293
|
+
layout=widgets.Layout(width='100%', margin='5px 0')
|
|
294
|
+
)
|
|
295
|
+
search_box.observe(self._on_search, 'value')
|
|
296
|
+
|
|
297
|
+
# 创建分页导航容器
|
|
298
|
+
self.widgets['nav_box'] = widgets.HBox(layout=widgets.Layout(
|
|
299
|
+
width='100%',
|
|
300
|
+
margin='5px 0',
|
|
301
|
+
justify_content='space-between'
|
|
302
|
+
))
|
|
303
|
+
|
|
304
|
+
# 创建模型列表容器
|
|
305
|
+
self.widgets['model_list'] = widgets.VBox(
|
|
306
|
+
layout=widgets.Layout(width='100%'))
|
|
307
|
+
|
|
308
|
+
# 组装左侧面板
|
|
309
|
+
left_panel.children = [
|
|
310
|
+
search_box,
|
|
311
|
+
self.widgets['nav_box'],
|
|
312
|
+
self.widgets['model_list']
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
# 建右侧模型详情面板
|
|
316
|
+
right_panel = widgets.VBox(
|
|
317
|
+
layout=widgets.Layout(flex='1', margin='10px'))
|
|
318
|
+
self.widgets['model_detail_area'] = right_panel
|
|
319
|
+
|
|
320
|
+
main_widget.children = [left_panel, right_panel]
|
|
321
|
+
|
|
322
|
+
# 初始显示
|
|
323
|
+
self._update_model_list()
|
|
324
|
+
|
|
325
|
+
return main_widget
|
|
326
|
+
|
|
327
|
+
def suggest_model(self):
|
|
328
|
+
"""显示模型推荐上下文数据(优化内存使用)"""
|
|
329
|
+
# 定期清理内存
|
|
330
|
+
cleanup_memory()
|
|
331
|
+
|
|
332
|
+
# 创建 NotebookContext 实例(使用缓存)
|
|
333
|
+
import time
|
|
334
|
+
cache_key = "notebook_context"
|
|
335
|
+
current_time = time.time()
|
|
336
|
+
|
|
337
|
+
if (cache_key in self._context_cache and
|
|
338
|
+
current_time - self._context_cache[cache_key]['time'] < self._context_cache_timeout):
|
|
339
|
+
# 使用缓存的上下文
|
|
340
|
+
context_data = self._context_cache[cache_key]['data']
|
|
341
|
+
else:
|
|
342
|
+
# 创建新的上下文并缓存
|
|
343
|
+
notebook_context = NotebookContext()
|
|
344
|
+
context_data = {
|
|
345
|
+
"modeling_history": notebook_context.history_context,
|
|
346
|
+
"data_context": notebook_context.data_context
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# 更新缓存
|
|
350
|
+
self._context_cache[cache_key] = {
|
|
351
|
+
'data': context_data,
|
|
352
|
+
'time': current_time
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# 清理notebook_context对象
|
|
356
|
+
del notebook_context
|
|
357
|
+
cleanup_memory()
|
|
358
|
+
|
|
359
|
+
# 显示加载状态
|
|
360
|
+
loading_html = """
|
|
361
|
+
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px 0;">
|
|
362
|
+
<div class="loading-spinner"></div>
|
|
363
|
+
<p style="margin-top: 10px; color: #6b7280;">Getting model recommendations, please wait...</p>
|
|
364
|
+
<style>
|
|
365
|
+
.loading-spinner {
|
|
366
|
+
width: 40px;
|
|
367
|
+
height: 40px;
|
|
368
|
+
border: 4px solid rgba(79, 70, 229, 0.2);
|
|
369
|
+
border-radius: 50%;
|
|
370
|
+
border-top-color: #4f46e5;
|
|
371
|
+
animation: spin 1s linear infinite;
|
|
372
|
+
}
|
|
373
|
+
@keyframes spin {
|
|
374
|
+
to { transform: rotate(360deg); }
|
|
375
|
+
}
|
|
376
|
+
</style>
|
|
377
|
+
</div>
|
|
378
|
+
"""
|
|
379
|
+
display, HTML, _ = _lazy_import_ipython_display()
|
|
380
|
+
loading_display = display(HTML(loading_html), display_id='loading')
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
# 调用API获取模型推荐
|
|
384
|
+
requests = _lazy_import_requests()
|
|
385
|
+
import json
|
|
386
|
+
|
|
387
|
+
# API配置
|
|
388
|
+
api_url = 'https://api.dify.ai/v1/workflows/run' # 根据实际URL调整
|
|
389
|
+
api_key = 'app-CuNONc6hSct2ap07nmUgcaw9'
|
|
390
|
+
|
|
391
|
+
# 准备请求数据
|
|
392
|
+
payload = {
|
|
393
|
+
"inputs": {
|
|
394
|
+
"modeling_history": context_data["modeling_history"],
|
|
395
|
+
"data_context": context_data["data_context"]
|
|
396
|
+
},
|
|
397
|
+
"response_mode": "blocking", # 使用阻塞模式
|
|
398
|
+
"user": "jupyter_user" # 用户标识符
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# 设置请求头
|
|
402
|
+
headers = {
|
|
403
|
+
'Authorization': f'Bearer {api_key}',
|
|
404
|
+
'Content-Type': 'application/json'
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# 发送POST请求
|
|
408
|
+
response = requests.post(api_url, headers=headers, json=payload)
|
|
409
|
+
|
|
410
|
+
# 清除加载状态
|
|
411
|
+
loading_display.update(HTML(''))
|
|
412
|
+
|
|
413
|
+
# 处理响应
|
|
414
|
+
if response.status_code == 200:
|
|
415
|
+
result = response.json()
|
|
416
|
+
|
|
417
|
+
# 根据API响应解析结果 - 纠正解析路径
|
|
418
|
+
if 'data' in result and 'outputs' in result['data']:
|
|
419
|
+
# 直接获取API返回的对象,这是一个完整的JSON对象,不是文本
|
|
420
|
+
recommendation_data = result['data']['outputs']
|
|
421
|
+
|
|
422
|
+
# 检查是否直接包含model_recommendation字段
|
|
423
|
+
if 'model_recommendation' in recommendation_data:
|
|
424
|
+
model_rec = recommendation_data['model_recommendation']
|
|
425
|
+
recommended_data = recommendation_data.get(
|
|
426
|
+
'recommended_data', {})
|
|
427
|
+
else:
|
|
428
|
+
# 如果不是直接包含,可能是第二层嵌套的文本,需要解析
|
|
429
|
+
try:
|
|
430
|
+
# 尝试解析text字段中的JSON
|
|
431
|
+
text_content = recommendation_data.get(
|
|
432
|
+
'text', '{}')
|
|
433
|
+
if isinstance(text_content, str):
|
|
434
|
+
parsed_content = json.loads(text_content)
|
|
435
|
+
model_rec = parsed_content.get(
|
|
436
|
+
'model_recommendation', {})
|
|
437
|
+
recommended_data = parsed_content.get(
|
|
438
|
+
'recommended_data', {})
|
|
439
|
+
else:
|
|
440
|
+
model_rec = {}
|
|
441
|
+
recommended_data = {}
|
|
442
|
+
except:
|
|
443
|
+
model_rec = {}
|
|
444
|
+
recommended_data = {}
|
|
445
|
+
|
|
446
|
+
# 从model_rec中提取信息
|
|
447
|
+
model_name = model_rec.get('name', 'Unknown Model')
|
|
448
|
+
model_desc = model_rec.get('description', 'No Description')
|
|
449
|
+
key_strengths = model_rec.get('key_strengths', [])
|
|
450
|
+
rec_reason = model_rec.get('recommendation_reason', '')
|
|
451
|
+
app_scenario = model_rec.get('application_scenario', '')
|
|
452
|
+
|
|
453
|
+
# 从recommended_data中提取信息
|
|
454
|
+
local_data = recommended_data.get('local_data', [])
|
|
455
|
+
kb_data = recommended_data.get('knowledge_base_data', [])
|
|
456
|
+
|
|
457
|
+
if model_name != 'Unknown Model': # 确保我们至少有模型名称
|
|
458
|
+
# 构建优美的HTML展示
|
|
459
|
+
html_output = f"""
|
|
460
|
+
<style>
|
|
461
|
+
.model-rec-container {{
|
|
462
|
+
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
463
|
+
width: 100%;
|
|
464
|
+
margin: 0;
|
|
465
|
+
}}
|
|
466
|
+
.model-rec-header {{
|
|
467
|
+
background: #f8fafc;
|
|
468
|
+
color: #1e293b;
|
|
469
|
+
padding: 16px 20px;
|
|
470
|
+
border: 1px solid #e2e8f0;
|
|
471
|
+
border-radius: 8px 8px 0 0;
|
|
472
|
+
font-size: 20px;
|
|
473
|
+
font-weight: 600;
|
|
474
|
+
}}
|
|
475
|
+
.model-rec-body {{
|
|
476
|
+
background: #f8fafc;
|
|
477
|
+
border: 1px solid #e2e8f0;
|
|
478
|
+
border-top: none;
|
|
479
|
+
border-radius: 0 0 8px 8px;
|
|
480
|
+
padding: 20px;
|
|
481
|
+
display: grid;
|
|
482
|
+
grid-template-columns: 1fr 1fr;
|
|
483
|
+
gap: 20px;
|
|
484
|
+
}}
|
|
485
|
+
.model-rec-section {{
|
|
486
|
+
margin-bottom: 18px;
|
|
487
|
+
}}
|
|
488
|
+
.model-rec-title {{
|
|
489
|
+
font-size: 16px;
|
|
490
|
+
font-weight: 600;
|
|
491
|
+
color: #1e293b;
|
|
492
|
+
margin-bottom: 8px;
|
|
493
|
+
border-bottom: 1px solid #e2e8f0;
|
|
494
|
+
padding-bottom: 6px;
|
|
495
|
+
}}
|
|
496
|
+
.model-rec-name {{
|
|
497
|
+
font-size: 20px;
|
|
498
|
+
font-weight: 600;
|
|
499
|
+
color: #1e293b;
|
|
500
|
+
margin-bottom: 10px;
|
|
501
|
+
}}
|
|
502
|
+
.model-rec-desc {{
|
|
503
|
+
color: #64748b;
|
|
504
|
+
line-height: 1.6;
|
|
505
|
+
margin-bottom: 15px;
|
|
506
|
+
font-size: 14px;
|
|
507
|
+
}}
|
|
508
|
+
.model-rec-strengths {{
|
|
509
|
+
list-style-type: none;
|
|
510
|
+
padding-left: 0;
|
|
511
|
+
margin-top: 0;
|
|
512
|
+
}}
|
|
513
|
+
.model-rec-strengths li {{
|
|
514
|
+
margin-bottom: 6px;
|
|
515
|
+
padding-left: 20px;
|
|
516
|
+
position: relative;
|
|
517
|
+
color: #64748b;
|
|
518
|
+
font-size: 14px;
|
|
519
|
+
}}
|
|
520
|
+
.model-rec-strengths li:before {{
|
|
521
|
+
content: "✓";
|
|
522
|
+
position: absolute;
|
|
523
|
+
left: 0;
|
|
524
|
+
color: #059669;
|
|
525
|
+
font-weight: bold;
|
|
526
|
+
}}
|
|
527
|
+
.model-rec-data-item {{
|
|
528
|
+
background: #ffffff;
|
|
529
|
+
border: 1px solid #e2e8f0;
|
|
530
|
+
border-radius: 6px;
|
|
531
|
+
padding: 12px 15px;
|
|
532
|
+
margin-bottom: 8px;
|
|
533
|
+
}}
|
|
534
|
+
.model-rec-data-name {{
|
|
535
|
+
font-weight: 500;
|
|
536
|
+
color: #1e293b;
|
|
537
|
+
font-size: 14px;
|
|
538
|
+
}}
|
|
539
|
+
.model-rec-data-location {{
|
|
540
|
+
color: #64748b;
|
|
541
|
+
font-size: 13px;
|
|
542
|
+
margin-top: 4px;
|
|
543
|
+
}}
|
|
544
|
+
.model-rec-kb-link {{
|
|
545
|
+
color: #1e293b;
|
|
546
|
+
text-decoration: none;
|
|
547
|
+
}}
|
|
548
|
+
.model-rec-kb-link:hover {{
|
|
549
|
+
text-decoration: underline;
|
|
550
|
+
}}
|
|
551
|
+
.model-rec-tag {{
|
|
552
|
+
display: inline-block;
|
|
553
|
+
background: #e2e8f0;
|
|
554
|
+
color: #64748b;
|
|
555
|
+
border-radius: 4px;
|
|
556
|
+
padding: 3px 8px;
|
|
557
|
+
font-size: 12px;
|
|
558
|
+
margin-right: 6px;
|
|
559
|
+
margin-bottom: 4px;
|
|
560
|
+
}}
|
|
561
|
+
</style>
|
|
562
|
+
|
|
563
|
+
<div class="model-rec-container">
|
|
564
|
+
<div class="model-rec-header">Model Recommendation</div>
|
|
565
|
+
<div class="model-rec-body">
|
|
566
|
+
<div class="model-rec-section">
|
|
567
|
+
<div class="model-rec-name">{model_name}</div>
|
|
568
|
+
<div class="model-rec-desc">{model_desc}</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<div class="model-rec-section">
|
|
572
|
+
<div class="model-rec-title">Core Advantages</div>
|
|
573
|
+
<ul class="model-rec-strengths">
|
|
574
|
+
{"".join([f'<li>{strength}</li>' for strength in key_strengths])}
|
|
575
|
+
</ul>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div class="model-rec-section">
|
|
579
|
+
<div class="model-rec-title">Recommendation Reason</div>
|
|
580
|
+
<div class="model-rec-desc">{rec_reason}</div>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<div class="model-rec-section">
|
|
584
|
+
<div class="model-rec-title">Application Scenarios</div>
|
|
585
|
+
<div class="model-rec-desc">{app_scenario}</div>
|
|
586
|
+
</div>
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
# 添加推荐数据部分
|
|
590
|
+
if local_data or kb_data:
|
|
591
|
+
html_output += """
|
|
592
|
+
<div class="model-rec-section" style="grid-column: 1 / -1;">
|
|
593
|
+
<div class="model-rec-title">Recommended Data Resources</div>
|
|
594
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
# 添加本地数据列
|
|
598
|
+
html_output += """
|
|
599
|
+
<div>
|
|
600
|
+
<div style="font-weight: 500; color: #1e293b; margin-bottom: 8px; font-size: 14px;">Local Data:</div>
|
|
601
|
+
"""
|
|
602
|
+
if local_data:
|
|
603
|
+
for data_item in local_data:
|
|
604
|
+
html_output += f"""
|
|
605
|
+
<div class="model-rec-data-item">
|
|
606
|
+
<div class="model-rec-data-name">{data_item.get('name', 'Unnamed Data')}</div>
|
|
607
|
+
<div class="model-rec-data-location">📁 {data_item.get('location', 'Unknown Location')}</div>
|
|
608
|
+
</div>
|
|
609
|
+
"""
|
|
610
|
+
else:
|
|
611
|
+
html_output += """
|
|
612
|
+
<div class="model-rec-data-item">
|
|
613
|
+
<div class="model-rec-data-name">No local data available</div>
|
|
614
|
+
</div>
|
|
615
|
+
"""
|
|
616
|
+
html_output += "</div>"
|
|
617
|
+
|
|
618
|
+
# 添加知识库数据列
|
|
619
|
+
html_output += """
|
|
620
|
+
<div>
|
|
621
|
+
<div style="font-weight: 500; color: #1e293b; margin-bottom: 8px; font-size: 14px;">Data Center Data:</div>
|
|
622
|
+
"""
|
|
623
|
+
if kb_data:
|
|
624
|
+
for kb_item in kb_data:
|
|
625
|
+
kb_name = kb_item.get(
|
|
626
|
+
'name', 'Unnamed Dataset')
|
|
627
|
+
kb_url = kb_item.get('url', '#')
|
|
628
|
+
html_output += f"""
|
|
629
|
+
<div class="model-rec-data-item">
|
|
630
|
+
<div class="model-rec-data-name">{kb_name}</div>
|
|
631
|
+
<div class="model-rec-data-location">
|
|
632
|
+
<a href="{kb_url}" class="model-rec-kb-link" target="_blank">🔗 View Data</a>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
"""
|
|
636
|
+
else:
|
|
637
|
+
html_output += """
|
|
638
|
+
<div class="model-rec-data-item">
|
|
639
|
+
<div class="model-rec-data-name">No data center data available</div>
|
|
640
|
+
</div>
|
|
641
|
+
"""
|
|
642
|
+
html_output += "</div>"
|
|
643
|
+
|
|
644
|
+
# 关闭数据资源的网格容器
|
|
645
|
+
html_output += """
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
"""
|
|
649
|
+
|
|
650
|
+
# 关闭容器div
|
|
651
|
+
html_output += """
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
# 显示结果
|
|
657
|
+
display(HTML(html_output))
|
|
658
|
+
else:
|
|
659
|
+
# 处理无模型推荐的情况
|
|
660
|
+
error_msg = "No valid model recommendation information found in API response"
|
|
661
|
+
self._display_error_message(error_msg)
|
|
662
|
+
|
|
663
|
+
# 显示原始数据以便调试
|
|
664
|
+
debug_html = f"""
|
|
665
|
+
<details>
|
|
666
|
+
<summary style="cursor: pointer; color: #6b7280; margin: 10px 0;">Show Raw API Response Data</summary>
|
|
667
|
+
<pre style="background: #f1f5f9; padding: 10px; border-radius: 4px; overflow: auto; max-height: 400px;">
|
|
668
|
+
{json.dumps(result, indent=2, ensure_ascii=False)}
|
|
669
|
+
</pre>
|
|
670
|
+
</details>
|
|
671
|
+
"""
|
|
672
|
+
display(HTML(debug_html))
|
|
673
|
+
else:
|
|
674
|
+
# 处理API返回格式不符预期的情况
|
|
675
|
+
error_msg = "API response data format does not meet expectations"
|
|
676
|
+
self._display_error_message(error_msg)
|
|
677
|
+
|
|
678
|
+
# 显示原始数据以便调试
|
|
679
|
+
debug_html = f"""
|
|
680
|
+
<details>
|
|
681
|
+
<summary style="cursor: pointer; color: #6b7280; margin: 10px 0;">Show Raw API Response Data</summary>
|
|
682
|
+
<pre style="background: #f1f5f9; padding: 10px; border-radius: 4px; overflow: auto; max-height: 400px;">
|
|
683
|
+
{json.dumps(result, indent=2, ensure_ascii=False)}
|
|
684
|
+
</pre>
|
|
685
|
+
</details>
|
|
686
|
+
"""
|
|
687
|
+
display(HTML(debug_html))
|
|
688
|
+
else:
|
|
689
|
+
error_msg = f"API request failed: HTTP {response.status_code} - {response.text}"
|
|
690
|
+
self._display_error_message(error_msg)
|
|
691
|
+
|
|
692
|
+
except Exception as e:
|
|
693
|
+
# 清除加载状态
|
|
694
|
+
|
|
695
|
+
# 显示错误信息
|
|
696
|
+
self._display_error_message(
|
|
697
|
+
f"Model recommendation service call failed: {str(e)}")
|
|
698
|
+
|
|
699
|
+
# 不返回任何值,避免在Jupyter中显示不必要的调试信息
|
|
700
|
+
return None
|
|
701
|
+
|
|
702
|
+
def _display_error_message(self, message):
|
|
703
|
+
"""显示错误信息"""
|
|
704
|
+
from IPython.display import HTML, display
|
|
705
|
+
error_html = f"""
|
|
706
|
+
<div style="background: #fee2e2; border-left: 4px solid #ef4444; padding: 12px 15px; margin: 10px 0; border-radius: 4px; color: #b91c1c;">
|
|
707
|
+
<div style="font-weight: 500; margin-bottom: 5px;">Error</div>
|
|
708
|
+
<div>{message}</div>
|
|
709
|
+
</div>
|
|
710
|
+
"""
|
|
711
|
+
display(HTML(error_html))
|
|
712
|
+
|
|
713
|
+
def _show_running_spinner(self):
|
|
714
|
+
"""在右侧面板顶部显示运行中动画"""
|
|
715
|
+
display, HTML, _ = _lazy_import_ipython_display()
|
|
716
|
+
spinner_html = (
|
|
717
|
+
"<div id=\"ogms-running\" style=\"display:flex;align-items:center;gap:10px;margin:6px 0;\">"
|
|
718
|
+
"<div style=\"width:16px;height:16px;border:2px solid rgba(79,70,229,.2);"
|
|
719
|
+
"border-top-color:#4f46e5;border-radius:50%;animation:ogms-spin 1s linear infinite;\"></div>"
|
|
720
|
+
"<span style=\"font-size:13px;color:#6b7280;\">Model calculating...</span>"
|
|
721
|
+
"<style>@keyframes ogms-spin{to{transform:rotate(360deg);}}</style>"
|
|
722
|
+
"</div>"
|
|
723
|
+
)
|
|
724
|
+
display(HTML(spinner_html))
|
|
725
|
+
|
|
726
|
+
def _hide_running_spinner(self):
|
|
727
|
+
"""移除运行中动画(如果环境支持DOM更新,Notebook多次刷新会清除)"""
|
|
728
|
+
# 简单实现:不做任何事,新的输出会覆盖旧内容
|
|
729
|
+
pass
|
|
730
|
+
|
|
731
|
+
def _update_model_list(self, filter_text=''):
|
|
732
|
+
"""更新模型列表"""
|
|
733
|
+
# 更新过滤后的模型列表(轻量级搜索,只基于模型名称)
|
|
734
|
+
if filter_text.strip() == "":
|
|
735
|
+
# 无搜索条件时显示所有模型
|
|
736
|
+
self.filtered_models = sorted(self.model_names)
|
|
737
|
+
else:
|
|
738
|
+
# 有搜索条件时基于模型名称过滤
|
|
739
|
+
self.filtered_models = [
|
|
740
|
+
model_name for model_name in sorted(self.model_names)
|
|
741
|
+
if filter_text.lower() in model_name.lower()
|
|
742
|
+
]
|
|
743
|
+
|
|
744
|
+
# 重置页码
|
|
745
|
+
self.current_page = 1
|
|
746
|
+
|
|
747
|
+
# 更新显示
|
|
748
|
+
self._refresh_display()
|
|
749
|
+
|
|
750
|
+
def _refresh_display(self):
|
|
751
|
+
"""刷新当前页面显示"""
|
|
752
|
+
# 计算页面信息
|
|
753
|
+
total_models = len(self.filtered_models)
|
|
754
|
+
total_pages = max(
|
|
755
|
+
1, (total_models + self.page_size - 1) // self.page_size)
|
|
756
|
+
start_idx = (self.current_page - 1) * self.page_size
|
|
757
|
+
end_idx = min(start_idx + self.page_size, total_models)
|
|
758
|
+
|
|
759
|
+
# 更新导航按钮和页面息
|
|
760
|
+
prev_button = widgets.Button(
|
|
761
|
+
description='Previous',
|
|
762
|
+
disabled=self.current_page == 1,
|
|
763
|
+
layout=widgets.Layout(width='80px'),
|
|
764
|
+
style=widgets.ButtonStyle(button_color='#e2e8f0') # 添加柔和的背景色
|
|
765
|
+
)
|
|
766
|
+
prev_button.on_click(self._prev_page)
|
|
767
|
+
|
|
768
|
+
next_button = widgets.Button(
|
|
769
|
+
description='Next',
|
|
770
|
+
disabled=self.current_page == total_pages,
|
|
771
|
+
layout=widgets.Layout(width='80px'),
|
|
772
|
+
style=widgets.ButtonStyle(button_color='#e2e8f0') # 添加柔和的背景色
|
|
773
|
+
)
|
|
774
|
+
next_button.on_click(self._next_page)
|
|
775
|
+
|
|
776
|
+
page_info = widgets.HTML(
|
|
777
|
+
value=f'<div style="text-align: center;">Page {self.current_page}/{total_pages}</div>'
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
self.widgets['nav_box'].children = [
|
|
781
|
+
prev_button, page_info, next_button]
|
|
782
|
+
|
|
783
|
+
# 更新模型列表
|
|
784
|
+
model_buttons = []
|
|
785
|
+
for model_name in self.filtered_models[start_idx:end_idx]:
|
|
786
|
+
button = widgets.Button(
|
|
787
|
+
description=model_name,
|
|
788
|
+
layout=widgets.Layout(
|
|
789
|
+
width='100%',
|
|
790
|
+
margin='3px 0', # 增加按钮间距
|
|
791
|
+
padding='6px 10px' # 增加按钮内边距
|
|
792
|
+
),
|
|
793
|
+
style=widgets.ButtonStyle(
|
|
794
|
+
button_color='white', # 按钮背景色
|
|
795
|
+
font_weight='normal' # 字体粗细
|
|
796
|
+
)
|
|
797
|
+
)
|
|
798
|
+
button.on_click(self._on_model_button_clicked)
|
|
799
|
+
model_buttons.append(button)
|
|
800
|
+
|
|
801
|
+
self.widgets['model_list'].children = tuple(model_buttons)
|
|
802
|
+
|
|
803
|
+
def _prev_page(self, b):
|
|
804
|
+
"""转到上一页"""
|
|
805
|
+
if self.current_page > 1:
|
|
806
|
+
self.current_page -= 1
|
|
807
|
+
self._refresh_display()
|
|
808
|
+
|
|
809
|
+
def _next_page(self, b):
|
|
810
|
+
"""转到下一页"""
|
|
811
|
+
total_pages = (len(self.filtered_models) +
|
|
812
|
+
self.page_size - 1) // self.page_size
|
|
813
|
+
if self.current_page < total_pages:
|
|
814
|
+
self.current_page += 1
|
|
815
|
+
self._refresh_display()
|
|
816
|
+
|
|
817
|
+
def _on_search(self, change):
|
|
818
|
+
"""处理搜索事件"""
|
|
819
|
+
search_text = change['new']
|
|
820
|
+
self._update_model_list(search_text)
|
|
821
|
+
|
|
822
|
+
def _on_model_button_clicked(self, button):
|
|
823
|
+
"""处理模型按钮点击事件"""
|
|
824
|
+
model_name = button.description
|
|
825
|
+
# print(f"点击了模型: {model_name}") # 调试信息
|
|
826
|
+
|
|
827
|
+
# 在右侧面板显示模型界面
|
|
828
|
+
self._show_model_in_panel(model_name)
|
|
829
|
+
|
|
830
|
+
def _show_model_in_panel(self, model_name):
|
|
831
|
+
"""在侧面板中显示模型界面"""
|
|
832
|
+
if model_name not in self.model_names:
|
|
833
|
+
print(f"Error: Model '{model_name}' does not exist")
|
|
834
|
+
return
|
|
835
|
+
|
|
836
|
+
# 按需加载模型
|
|
837
|
+
model = self.load_model_on_demand(model_name)
|
|
838
|
+
if model is None:
|
|
839
|
+
print(f"Error: Failed to load model '{model_name}'")
|
|
840
|
+
return
|
|
841
|
+
|
|
842
|
+
self.current_model = model
|
|
843
|
+
|
|
844
|
+
# 创建主容器
|
|
845
|
+
main_container = widgets.VBox()
|
|
846
|
+
widgets_list = []
|
|
847
|
+
|
|
848
|
+
# 添加模型基本信息
|
|
849
|
+
model_info = widgets.HTML(value=f"""
|
|
850
|
+
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px; margin-bottom: 10px;">
|
|
851
|
+
<h3 style="margin-top: 0;">{self.current_model.name}</h3>
|
|
852
|
+
<p style="color: #666; margin-bottom: 8px;">{self.current_model.description}</p>
|
|
853
|
+
<div style="display: flex; gap: 10px;">
|
|
854
|
+
<div>
|
|
855
|
+
<span style="color: #666;">Authors' Emails: </span>
|
|
856
|
+
<span>{self.current_model.author}</span>
|
|
857
|
+
</div>
|
|
858
|
+
<div>
|
|
859
|
+
<span style="color: #666;">Tags: </span>
|
|
860
|
+
<span>{', '.join(self.current_model.tags)}</span>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
</div>
|
|
864
|
+
""")
|
|
865
|
+
widgets_list.append(model_info)
|
|
866
|
+
|
|
867
|
+
# 隐藏的触发按钮(纯widgets,用于可靠触发Python回调)
|
|
868
|
+
hidden_trigger_btn = widgets.Button(
|
|
869
|
+
description='',
|
|
870
|
+
layout=widgets.Layout(width='0px', height='0px',
|
|
871
|
+
padding='0', margin='0', border='0'),
|
|
872
|
+
style=widgets.ButtonStyle(button_color='#ffffff')
|
|
873
|
+
)
|
|
874
|
+
hidden_trigger_btn._dom_classes = ['qa-hidden-trigger']
|
|
875
|
+
# 放入极小的容器,避免影响布局
|
|
876
|
+
widgets_list.append(widgets.Box(
|
|
877
|
+
[hidden_trigger_btn], layout=widgets.Layout(width='0px', height='0px')))
|
|
878
|
+
# 保存引用,稍后绑定回调
|
|
879
|
+
self.widgets['qa_hidden_btn'] = hidden_trigger_btn
|
|
880
|
+
|
|
881
|
+
# 遍历状态
|
|
882
|
+
for i, state in enumerate(self.current_model.states):
|
|
883
|
+
state_container = widgets.VBox(
|
|
884
|
+
layout=widgets.Layout(margin='0 0 8px 0')
|
|
885
|
+
)
|
|
886
|
+
state_widgets = []
|
|
887
|
+
|
|
888
|
+
# 添加状态信息
|
|
889
|
+
state_info = widgets.HTML(value=f"""
|
|
890
|
+
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 12px; margin-bottom: 8px;">
|
|
891
|
+
<h3 style="color: #1e293b; margin: 0 0 4px 0; font-size: 16px; font-weight: 600;">{state.get('name', '')}</h3>
|
|
892
|
+
<p style="color: #64748b; margin: 0; font-size: 14px;">{state.get('desc', '')}</p>
|
|
893
|
+
</div>
|
|
894
|
+
""")
|
|
895
|
+
state_widgets.append(state_info)
|
|
896
|
+
|
|
897
|
+
# 检查该状态是否有需要用户输入的事件
|
|
898
|
+
has_input_events = False
|
|
899
|
+
for event in state.get('event', []):
|
|
900
|
+
if event.get('eventType') == 'response':
|
|
901
|
+
has_input_events = True
|
|
902
|
+
event_container = widgets.VBox(
|
|
903
|
+
layout=widgets.Layout(margin='3px 0'))
|
|
904
|
+
event_widgets = []
|
|
905
|
+
|
|
906
|
+
event_name = event.get('eventName', '')
|
|
907
|
+
optional_text = "Required" if not event.get(
|
|
908
|
+
'optional', False) else "Optional"
|
|
909
|
+
event_desc = event.get('eventDesc', '')
|
|
910
|
+
|
|
911
|
+
# 添加事件标题和描述
|
|
912
|
+
event_header = widgets.HTML(value=f"""
|
|
913
|
+
<div style="margin: 2px 0;">
|
|
914
|
+
<span style="font-weight: 500;">{event_name}</span>
|
|
915
|
+
<span style="background: {('#ef4444' if optional_text == 'Required' else '#94a3b8')};
|
|
916
|
+
color: white;
|
|
917
|
+
padding: 1px 8px;
|
|
918
|
+
border-radius: 12px;
|
|
919
|
+
font-size: 12px;
|
|
920
|
+
margin-left: 8px;">
|
|
921
|
+
{optional_text}
|
|
922
|
+
</span>
|
|
923
|
+
<div style="color: #666; margin: 1px 0 2px 0;">{event_desc}</div>
|
|
924
|
+
</div>
|
|
925
|
+
""")
|
|
926
|
+
event_widgets.append(event_header)
|
|
927
|
+
|
|
928
|
+
# 检查是否含nodes数据
|
|
929
|
+
has_nodes = False
|
|
930
|
+
nodes_data = []
|
|
931
|
+
for data_item in event.get('data', []):
|
|
932
|
+
if 'nodes' in data_item:
|
|
933
|
+
has_nodes = True
|
|
934
|
+
nodes_data = data_item['nodes']
|
|
935
|
+
|
|
936
|
+
if has_nodes:
|
|
937
|
+
# 创建表格容器
|
|
938
|
+
table_container = widgets.VBox()
|
|
939
|
+
table_widgets = []
|
|
940
|
+
|
|
941
|
+
# 添加表头
|
|
942
|
+
header = widgets.HTML(value="""
|
|
943
|
+
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 8px; padding: 8px; background: #f8fafc; border: 1px solid #e2e8f0;">
|
|
944
|
+
<div style="font-weight: 500;">Parameter Name</div>
|
|
945
|
+
<div style="font-weight: 500;">Description</div>
|
|
946
|
+
<div style="font-weight: 500;">Value</div>
|
|
947
|
+
</div>
|
|
948
|
+
""")
|
|
949
|
+
table_widgets.append(header)
|
|
950
|
+
|
|
951
|
+
# 个参数创建一行
|
|
952
|
+
for node in nodes_data:
|
|
953
|
+
# 创建行容器
|
|
954
|
+
row = widgets.HBox([
|
|
955
|
+
widgets.HTML(value=f"""
|
|
956
|
+
<div style="padding: 8px; min-width: 150px;">{node.get('text', '')}</div>
|
|
957
|
+
"""),
|
|
958
|
+
widgets.HTML(value=f"""
|
|
959
|
+
<div style="padding: 8px; min-width: 200px;">{node.get('desc', '')}</div>
|
|
960
|
+
"""),
|
|
961
|
+
widgets.Text(
|
|
962
|
+
placeholder='Please input value',
|
|
963
|
+
layout=widgets.Layout(width='150px')
|
|
964
|
+
)
|
|
965
|
+
])
|
|
966
|
+
# 存储Text widget的引用
|
|
967
|
+
self.widgets[f'node-{event_name}-{node.get("text")}'] = row.children[-1]
|
|
968
|
+
table_widgets.append(row)
|
|
969
|
+
|
|
970
|
+
table_container.children = table_widgets
|
|
971
|
+
event_widgets.append(table_container)
|
|
972
|
+
else:
|
|
973
|
+
# 创建文件选择器
|
|
974
|
+
FileChooser = _lazy_import_filechooser()
|
|
975
|
+
fc = FileChooser(
|
|
976
|
+
path='./',
|
|
977
|
+
layout=widgets.Layout(width='100%')
|
|
978
|
+
)
|
|
979
|
+
self.widgets[f'file_chooser_{event_name}'] = fc
|
|
980
|
+
event_widgets.append(fc)
|
|
981
|
+
|
|
982
|
+
event_container.children = event_widgets
|
|
983
|
+
state_widgets.append(event_container)
|
|
984
|
+
|
|
985
|
+
# 如果没有输入事件,添加提示信息
|
|
986
|
+
if not has_input_events:
|
|
987
|
+
no_input_msg = widgets.HTML(value="""
|
|
988
|
+
<div style="padding: 8px 12px;
|
|
989
|
+
background: #f8fafc;
|
|
990
|
+
border: 1px dashed #e2e8f0;
|
|
991
|
+
border-radius: 4px;
|
|
992
|
+
color: #64748b;
|
|
993
|
+
font-size: 14px;
|
|
994
|
+
margin: 4px 0;">
|
|
995
|
+
This state does not require user input
|
|
996
|
+
</div>
|
|
997
|
+
""")
|
|
998
|
+
state_widgets.append(no_input_msg)
|
|
999
|
+
|
|
1000
|
+
state_container.children = state_widgets
|
|
1001
|
+
widgets_list.append(state_container)
|
|
1002
|
+
|
|
1003
|
+
if i < len(self.current_model.states) - 1:
|
|
1004
|
+
divider = widgets.HTML(value="""
|
|
1005
|
+
<div style="padding: 0 16px;">
|
|
1006
|
+
<hr style="border: none; border-top: 2px solid #1e293b; margin: 12px 0;">
|
|
1007
|
+
</div>
|
|
1008
|
+
""")
|
|
1009
|
+
widgets_list.append(divider)
|
|
1010
|
+
|
|
1011
|
+
# 创建输出区域
|
|
1012
|
+
self.widgets['output_area'] = widgets.Output()
|
|
1013
|
+
# 将输出区域添加到widgets_list
|
|
1014
|
+
widgets_list.append(self.widgets['output_area'])
|
|
1015
|
+
|
|
1016
|
+
# 创建按钮容器(水平布局,右对齐)
|
|
1017
|
+
button_container = widgets.HBox(
|
|
1018
|
+
layout=widgets.Layout(
|
|
1019
|
+
display='flex',
|
|
1020
|
+
justify_content='flex-end',
|
|
1021
|
+
gap='10px'
|
|
1022
|
+
)
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
# 创建Run按钮(运行期间禁用)
|
|
1026
|
+
run_button = widgets.Button(
|
|
1027
|
+
description='Run',
|
|
1028
|
+
style=widgets.ButtonStyle(
|
|
1029
|
+
button_color='#4CAF50', text_color='white')
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
# 运行中动画(放在按钮右侧,默认隐藏)
|
|
1033
|
+
spinner_widget = widgets.HTML(
|
|
1034
|
+
value='', layout=widgets.Layout(margin='0 6px'))
|
|
1035
|
+
self.widgets['running_spinner'] = spinner_widget
|
|
1036
|
+
|
|
1037
|
+
def on_run_click(b):
|
|
1038
|
+
# 禁用按钮,按钮文案与图标切换为运行中
|
|
1039
|
+
run_button.disabled = True
|
|
1040
|
+
original_desc = run_button.description
|
|
1041
|
+
original_icon = getattr(run_button, 'icon', '')
|
|
1042
|
+
run_button.description = 'Model calculating...'
|
|
1043
|
+
# 在按钮内使用 fontawesome spinner 图标并注入旋转CSS
|
|
1044
|
+
try:
|
|
1045
|
+
run_button.icon = 'spinner'
|
|
1046
|
+
display, HTML, _ = _lazy_import_ipython_display()
|
|
1047
|
+
if not getattr(self, '_spinner_css_injected', False):
|
|
1048
|
+
display(HTML(
|
|
1049
|
+
'<style>@keyframes fa-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}} .fa-spinner{animation:fa-spin 1s linear infinite!important;}</style>'))
|
|
1050
|
+
self._spinner_css_injected = True
|
|
1051
|
+
except Exception:
|
|
1052
|
+
pass
|
|
1053
|
+
# 静默运行,屏蔽底层print日志
|
|
1054
|
+
import contextlib
|
|
1055
|
+
import io
|
|
1056
|
+
_buf_out, _buf_err = io.StringIO(), io.StringIO()
|
|
1057
|
+
try:
|
|
1058
|
+
with contextlib.redirect_stdout(_buf_out), contextlib.redirect_stderr(_buf_err):
|
|
1059
|
+
self._on_run_button_clicked(b)
|
|
1060
|
+
finally:
|
|
1061
|
+
# 恢复按钮状态
|
|
1062
|
+
run_button.disabled = False
|
|
1063
|
+
run_button.description = original_desc
|
|
1064
|
+
try:
|
|
1065
|
+
run_button.icon = original_icon
|
|
1066
|
+
except Exception:
|
|
1067
|
+
pass
|
|
1068
|
+
spinner_widget.value = ''
|
|
1069
|
+
|
|
1070
|
+
run_button.on_click(on_run_click)
|
|
1071
|
+
|
|
1072
|
+
# 将按钮添加到按钮容器(移除Close按钮)
|
|
1073
|
+
button_container.children = [run_button, spinner_widget]
|
|
1074
|
+
|
|
1075
|
+
# 将按钮容器添加到widgets_list
|
|
1076
|
+
widgets_list.append(button_container)
|
|
1077
|
+
|
|
1078
|
+
# 设置主容器的子组件
|
|
1079
|
+
main_container.children = widgets_list
|
|
1080
|
+
|
|
1081
|
+
# 更新右侧面板的内容
|
|
1082
|
+
self.widgets['model_detail_area'].children = [main_container]
|
|
1083
|
+
|
|
1084
|
+
def invoke_model(self, model_name):
|
|
1085
|
+
"""调用指定模型的交互界面"""
|
|
1086
|
+
if model_name not in self.model_names:
|
|
1087
|
+
raise ValueError(f"Model '{model_name}' does not exist")
|
|
1088
|
+
|
|
1089
|
+
# 按需加载模型
|
|
1090
|
+
model = self.load_model_on_demand(model_name)
|
|
1091
|
+
if model is None:
|
|
1092
|
+
raise ValueError(f"Failed to load model '{model_name}'")
|
|
1093
|
+
|
|
1094
|
+
self.current_model = model
|
|
1095
|
+
|
|
1096
|
+
# 导入widgets
|
|
1097
|
+
widgets = _lazy_import_ipywidgets()
|
|
1098
|
+
|
|
1099
|
+
# 创建主容器
|
|
1100
|
+
main_container = widgets.VBox()
|
|
1101
|
+
widgets_list = []
|
|
1102
|
+
|
|
1103
|
+
# 使用HBox布局来放置模型信息和问号按钮
|
|
1104
|
+
model_info_hbox = widgets.HBox(
|
|
1105
|
+
layout=widgets.Layout(
|
|
1106
|
+
background='#f8fafc',
|
|
1107
|
+
border='1px solid #e2e8f0',
|
|
1108
|
+
border_radius='8px',
|
|
1109
|
+
padding='10px',
|
|
1110
|
+
margin='0 0 10px 0',
|
|
1111
|
+
align_items='flex-start'
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
# 添加模型基本信息HTML
|
|
1116
|
+
model_info = widgets.HTML(
|
|
1117
|
+
value=f"""
|
|
1118
|
+
<div>
|
|
1119
|
+
<h3 style="margin-top: 0; margin-bottom: 8px;">{self.current_model.name}</h3>
|
|
1120
|
+
<p style="color: #666; margin-bottom: 8px;">{self.current_model.description}</p>
|
|
1121
|
+
<div style="display: flex; gap: 10px;">
|
|
1122
|
+
<div>
|
|
1123
|
+
<span style="color: #666;">Authors' Emails: </span>
|
|
1124
|
+
<span>{self.current_model.author}</span>
|
|
1125
|
+
</div>
|
|
1126
|
+
<div>
|
|
1127
|
+
<span style="color: #666;">Tags: </span>
|
|
1128
|
+
<span>{', '.join(self.current_model.tags)}</span>
|
|
1129
|
+
</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
""",
|
|
1133
|
+
layout=widgets.Layout(flex='1')
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
# 创建问号按钮 - 使用原有配色风格
|
|
1137
|
+
qa_toggle_button = widgets.Button(
|
|
1138
|
+
description='?',
|
|
1139
|
+
tooltip='Toggle QA Assistant',
|
|
1140
|
+
layout=widgets.Layout(
|
|
1141
|
+
width='28px',
|
|
1142
|
+
height='28px',
|
|
1143
|
+
margin='0 0 0 10px'
|
|
1144
|
+
),
|
|
1145
|
+
style=widgets.ButtonStyle(
|
|
1146
|
+
button_color='#f8fafc',
|
|
1147
|
+
text_color='#64748b',
|
|
1148
|
+
font_weight='bold'
|
|
1149
|
+
)
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
# 将信息和按钮放入HBox
|
|
1153
|
+
model_info_hbox.children = [model_info, qa_toggle_button]
|
|
1154
|
+
widgets_list.append(model_info_hbox)
|
|
1155
|
+
|
|
1156
|
+
# 遍历状态
|
|
1157
|
+
for i, state in enumerate(self.current_model.states):
|
|
1158
|
+
state_container = widgets.VBox(
|
|
1159
|
+
layout=widgets.Layout(margin='0 0 8px 0')
|
|
1160
|
+
)
|
|
1161
|
+
state_widgets = []
|
|
1162
|
+
|
|
1163
|
+
# 添加状态信息
|
|
1164
|
+
state_info = widgets.HTML(value=f"""
|
|
1165
|
+
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 12px; margin-bottom: 8px;">
|
|
1166
|
+
<h3 style="color: #1e293b; margin: 0 0 4px 0; font-size: 16px; font-weight: 600;">{state.get('name', '')}</h3>
|
|
1167
|
+
<p style="color: #64748b; margin: 0; font-size: 14px;">{state.get('desc', '')}</p>
|
|
1168
|
+
</div>
|
|
1169
|
+
""")
|
|
1170
|
+
state_widgets.append(state_info)
|
|
1171
|
+
|
|
1172
|
+
# 检查该状态是否有需要用户输入的事件
|
|
1173
|
+
has_input_events = False
|
|
1174
|
+
for event in state.get('event', []):
|
|
1175
|
+
if event.get('eventType') == 'response':
|
|
1176
|
+
has_input_events = True
|
|
1177
|
+
event_container = widgets.VBox(
|
|
1178
|
+
layout=widgets.Layout(margin='3px 0'))
|
|
1179
|
+
event_widgets = []
|
|
1180
|
+
|
|
1181
|
+
event_name = event.get('eventName', '')
|
|
1182
|
+
optional_text = "Required" if not event.get(
|
|
1183
|
+
'optional', False) else "Optional"
|
|
1184
|
+
event_desc = event.get('eventDesc', '')
|
|
1185
|
+
|
|
1186
|
+
# 添加事件标题和描述
|
|
1187
|
+
event_header = widgets.HTML(value=f"""
|
|
1188
|
+
<div style="margin: 2px 0;">
|
|
1189
|
+
<span style="font-weight: 500;">{event_name}</span>
|
|
1190
|
+
<span style="background: {('#ef4444' if optional_text == 'Required' else '#94a3b8')};
|
|
1191
|
+
color: white;
|
|
1192
|
+
padding: 1px 8px;
|
|
1193
|
+
border-radius: 12px;
|
|
1194
|
+
font-size: 12px;
|
|
1195
|
+
margin-left: 8px;">
|
|
1196
|
+
{optional_text}
|
|
1197
|
+
</span>
|
|
1198
|
+
<div style="color: #666; margin: 1px 0 2px 0;">{event_desc}</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
""")
|
|
1201
|
+
event_widgets.append(event_header)
|
|
1202
|
+
|
|
1203
|
+
# 检查是否包含nodes类数据
|
|
1204
|
+
has_nodes = False
|
|
1205
|
+
nodes_data = []
|
|
1206
|
+
for data_item in event.get('data', []):
|
|
1207
|
+
if 'nodes' in data_item:
|
|
1208
|
+
has_nodes = True
|
|
1209
|
+
nodes_data = data_item['nodes']
|
|
1210
|
+
|
|
1211
|
+
if has_nodes:
|
|
1212
|
+
# 创建表格容器
|
|
1213
|
+
table_container = widgets.VBox()
|
|
1214
|
+
table_widgets = []
|
|
1215
|
+
|
|
1216
|
+
# 添加表头
|
|
1217
|
+
header = widgets.HTML(value="""
|
|
1218
|
+
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 8px; padding: 8px; background: #f8fafc; border: 1px solid #e2e8f0;">
|
|
1219
|
+
<div style="font-weight: 500;">Parameter Name</div>
|
|
1220
|
+
<div style="font-weight: 500;">Description</div>
|
|
1221
|
+
<div style="font-weight: 500;">Value</div>
|
|
1222
|
+
</div>
|
|
1223
|
+
""")
|
|
1224
|
+
table_widgets.append(header)
|
|
1225
|
+
|
|
1226
|
+
# 为每个参数创建一行
|
|
1227
|
+
for node in nodes_data:
|
|
1228
|
+
# 创建行容器
|
|
1229
|
+
row = widgets.HBox([
|
|
1230
|
+
widgets.HTML(value=f"""
|
|
1231
|
+
<div style="padding: 8px; min-width: 150px;">{node.get('text', '')}</div>
|
|
1232
|
+
"""),
|
|
1233
|
+
widgets.HTML(value=f"""
|
|
1234
|
+
<div style="padding: 8px; min-width: 200px;">{node.get('desc', '')}</div>
|
|
1235
|
+
"""),
|
|
1236
|
+
widgets.Text(
|
|
1237
|
+
placeholder='Please input value',
|
|
1238
|
+
layout=widgets.Layout(width='150px')
|
|
1239
|
+
)
|
|
1240
|
+
])
|
|
1241
|
+
# 存储Text widget的引用
|
|
1242
|
+
self.widgets[f'node-{event_name}-{node.get("text")}'] = row.children[-1]
|
|
1243
|
+
table_widgets.append(row)
|
|
1244
|
+
|
|
1245
|
+
table_container.children = table_widgets
|
|
1246
|
+
event_widgets.append(table_container)
|
|
1247
|
+
else:
|
|
1248
|
+
# 创建文件选择器
|
|
1249
|
+
FileChooser = _lazy_import_filechooser()
|
|
1250
|
+
fc = FileChooser(
|
|
1251
|
+
path='./',
|
|
1252
|
+
layout=widgets.Layout(width='100%')
|
|
1253
|
+
)
|
|
1254
|
+
self.widgets[f'file_chooser_{event_name}'] = fc
|
|
1255
|
+
event_widgets.append(fc)
|
|
1256
|
+
|
|
1257
|
+
event_container.children = event_widgets
|
|
1258
|
+
state_widgets.append(event_container)
|
|
1259
|
+
|
|
1260
|
+
# 如果没有输入事件,添加提示信息
|
|
1261
|
+
if not has_input_events:
|
|
1262
|
+
no_input_msg = widgets.HTML(value="""
|
|
1263
|
+
<div style="padding: 8px 12px;
|
|
1264
|
+
background: #f8fafc;
|
|
1265
|
+
border: 1px dashed #e2e8f0;
|
|
1266
|
+
border-radius: 4px;
|
|
1267
|
+
color: #64748b;
|
|
1268
|
+
font-size: 14px;
|
|
1269
|
+
margin: 4px 0;">
|
|
1270
|
+
This state does not require user input
|
|
1271
|
+
</div>
|
|
1272
|
+
""")
|
|
1273
|
+
state_widgets.append(no_input_msg)
|
|
1274
|
+
|
|
1275
|
+
state_container.children = state_widgets
|
|
1276
|
+
widgets_list.append(state_container)
|
|
1277
|
+
|
|
1278
|
+
if i < len(self.current_model.states) - 1:
|
|
1279
|
+
divider = widgets.HTML(value="""
|
|
1280
|
+
<div style="padding: 0 16px;">
|
|
1281
|
+
<hr style="border: none; border-top: 2px solid #1e293b; margin: 12px 0;">
|
|
1282
|
+
</div>
|
|
1283
|
+
""")
|
|
1284
|
+
widgets_list.append(divider)
|
|
1285
|
+
|
|
1286
|
+
# 创建输出区域
|
|
1287
|
+
self.widgets['output_area'] = widgets.Output()
|
|
1288
|
+
# 将输出区域添加到widgets_list
|
|
1289
|
+
widgets_list.append(self.widgets['output_area'])
|
|
1290
|
+
|
|
1291
|
+
# 创建按钮容器(水平布局)
|
|
1292
|
+
button_container = widgets.HBox(
|
|
1293
|
+
layout=widgets.Layout(
|
|
1294
|
+
display='flex',
|
|
1295
|
+
justify_content='flex-start',
|
|
1296
|
+
gap='10px'
|
|
1297
|
+
)
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
# 创建Run按钮
|
|
1301
|
+
run_button = widgets.Button(
|
|
1302
|
+
description='Run',
|
|
1303
|
+
style=widgets.ButtonStyle(
|
|
1304
|
+
button_color='#4CAF50', text_color='white')
|
|
1305
|
+
)
|
|
1306
|
+
run_button.on_click(self._on_run_button_clicked)
|
|
1307
|
+
|
|
1308
|
+
# 创建Close按钮
|
|
1309
|
+
close_button = widgets.Button(
|
|
1310
|
+
description='Close',
|
|
1311
|
+
style=widgets.ButtonStyle(
|
|
1312
|
+
button_color='#ef4444', text_color='white'),
|
|
1313
|
+
layout=widgets.Layout(width='80px')
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
def close_model(b):
|
|
1317
|
+
main_container.close()
|
|
1318
|
+
|
|
1319
|
+
close_button.on_click(close_model)
|
|
1320
|
+
|
|
1321
|
+
# 将按钮添加到按钮容器
|
|
1322
|
+
button_container.children = [run_button, close_button]
|
|
1323
|
+
|
|
1324
|
+
# 将按钮容器添加到widgets_list
|
|
1325
|
+
widgets_list.append(button_container)
|
|
1326
|
+
|
|
1327
|
+
# 设置主容器的子组件
|
|
1328
|
+
main_container.children = widgets_list
|
|
1329
|
+
|
|
1330
|
+
# 创建水平分栏容器
|
|
1331
|
+
split_container = widgets.HBox(
|
|
1332
|
+
layout=widgets.Layout(
|
|
1333
|
+
width='100%',
|
|
1334
|
+
display='flex'
|
|
1335
|
+
)
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
# 创建左侧容器 (65%)
|
|
1339
|
+
left_panel = widgets.VBox(
|
|
1340
|
+
layout=widgets.Layout(
|
|
1341
|
+
width='60%',
|
|
1342
|
+
padding='10px'
|
|
1343
|
+
)
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
# 创建右侧容器 (35%)
|
|
1347
|
+
right_panel = widgets.VBox(
|
|
1348
|
+
layout=widgets.Layout(
|
|
1349
|
+
width='40%',
|
|
1350
|
+
padding='10px', # 增加内边距
|
|
1351
|
+
border_left='1px solid #ccc'
|
|
1352
|
+
)
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
# 创建搜索框
|
|
1356
|
+
search_box = widgets.Text(
|
|
1357
|
+
placeholder='Please input your question about this model...',
|
|
1358
|
+
description='Search:',
|
|
1359
|
+
description_width='50px',
|
|
1360
|
+
style={
|
|
1361
|
+
'description_width': 'initial',
|
|
1362
|
+
'font_family': 'PingFang SC, -apple-system, BlinkMacSystemFont, sans-serif'
|
|
1363
|
+
},
|
|
1364
|
+
layout=widgets.Layout(
|
|
1365
|
+
width='100%',
|
|
1366
|
+
margin='8px 0',
|
|
1367
|
+
padding='10px 16px',
|
|
1368
|
+
border='1px solid #d1d5db',
|
|
1369
|
+
border_radius='12px',
|
|
1370
|
+
font_size='15px',
|
|
1371
|
+
background_color='white',
|
|
1372
|
+
transition='all 0.3s ease',
|
|
1373
|
+
box_shadow='0 1px 2px rgba(0, 0, 0, 0.05)'
|
|
1374
|
+
)
|
|
1375
|
+
)
|
|
1376
|
+
# 添加悬停和焦点效果
|
|
1377
|
+
search_box._dom_classes = ['hover:border-indigo-500',
|
|
1378
|
+
'focus:ring-2', 'focus:ring-indigo-500', 'focus:border-indigo-500']
|
|
1379
|
+
|
|
1380
|
+
# 创建结果显示区域,添加固定高度和滚动条
|
|
1381
|
+
result_area = widgets.Output(
|
|
1382
|
+
layout=widgets.Layout(
|
|
1383
|
+
width='100%',
|
|
1384
|
+
height='500px', # 固定高度
|
|
1385
|
+
# border='1px solid #ddd',
|
|
1386
|
+
padding='5px',
|
|
1387
|
+
overflow_y='auto' # 添加垂直滚动条
|
|
1388
|
+
)
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1391
|
+
# 保存到实例变量中
|
|
1392
|
+
self.widgets['result_area'] = result_area
|
|
1393
|
+
|
|
1394
|
+
# 绑定事件处理函数
|
|
1395
|
+
search_box.on_submit(self.on_search_submit)
|
|
1396
|
+
|
|
1397
|
+
# 创建标题
|
|
1398
|
+
title = widgets.HTML(
|
|
1399
|
+
value='<h3 style="margin:0 0 2px 0;">Model QA Assistant</h3>'
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
# 组装右侧面板 - 修改这部分代码
|
|
1403
|
+
right_panel.children = [
|
|
1404
|
+
title,
|
|
1405
|
+
search_box,
|
|
1406
|
+
result_area
|
|
1407
|
+
]
|
|
1408
|
+
|
|
1409
|
+
# 将原有的main_container放入左侧面板
|
|
1410
|
+
left_panel.children = [main_container]
|
|
1411
|
+
|
|
1412
|
+
# 组装分栏容器
|
|
1413
|
+
split_container.children = [left_panel, right_panel]
|
|
1414
|
+
|
|
1415
|
+
# 定义切换QA Panel的函数
|
|
1416
|
+
qa_panel_visible = [True] # 初始状态为显示
|
|
1417
|
+
|
|
1418
|
+
def toggle_qa_panel(button=None):
|
|
1419
|
+
if qa_panel_visible[0]:
|
|
1420
|
+
# 隐藏QA Panel
|
|
1421
|
+
split_container.children = [left_panel]
|
|
1422
|
+
left_panel.layout.width = '100%'
|
|
1423
|
+
qa_panel_visible[0] = False
|
|
1424
|
+
print("QA Panel hidden") # 调试信息
|
|
1425
|
+
else:
|
|
1426
|
+
# 显示QA Panel
|
|
1427
|
+
split_container.children = [left_panel, right_panel]
|
|
1428
|
+
left_panel.layout.width = '60%'
|
|
1429
|
+
qa_panel_visible[0] = True
|
|
1430
|
+
print("QA Panel shown") # 调试信息
|
|
1431
|
+
|
|
1432
|
+
# 直接绑定问号按钮的点击事件
|
|
1433
|
+
qa_toggle_button.on_click(toggle_qa_panel)
|
|
1434
|
+
|
|
1435
|
+
# 添加CSS样式来美化按钮
|
|
1436
|
+
display, HTML, _ = _lazy_import_ipython_display()
|
|
1437
|
+
button_css = HTML("""
|
|
1438
|
+
<style>
|
|
1439
|
+
/* 美化问号按钮 - 使用原有配色风格 */
|
|
1440
|
+
.widget-hbox .widget-button .btn {
|
|
1441
|
+
border-radius: 50% !important;
|
|
1442
|
+
border: 1px solid #e2e8f0 !important;
|
|
1443
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
|
|
1444
|
+
transition: all 0.2s ease !important;
|
|
1445
|
+
font-size: 16px !important;
|
|
1446
|
+
font-weight: bold !important;
|
|
1447
|
+
min-width: 28px !important;
|
|
1448
|
+
padding: 0 !important;
|
|
1449
|
+
}
|
|
1450
|
+
.widget-hbox .widget-button .btn:hover {
|
|
1451
|
+
transform: scale(1.05) !important;
|
|
1452
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15) !important;
|
|
1453
|
+
border-color: #cbd5e1 !important;
|
|
1454
|
+
background-color: #ffffff !important;
|
|
1455
|
+
}
|
|
1456
|
+
.widget-hbox .widget-button .btn:active {
|
|
1457
|
+
transform: scale(0.95) !important;
|
|
1458
|
+
}
|
|
1459
|
+
</style>
|
|
1460
|
+
""")
|
|
1461
|
+
display(button_css)
|
|
1462
|
+
|
|
1463
|
+
return split_container
|
|
1464
|
+
|
|
1465
|
+
def show_model(self, model_name):
|
|
1466
|
+
"""显示指定模型的交互界面(invoke_model的别名,保持向后兼容)"""
|
|
1467
|
+
return self.invoke_model(model_name)
|
|
1468
|
+
|
|
1469
|
+
def _on_run_button_clicked(self, b):
|
|
1470
|
+
"""处理运行按钮点击事件"""
|
|
1471
|
+
# 导入requests模块
|
|
1472
|
+
requests = _lazy_import_requests()
|
|
1473
|
+
|
|
1474
|
+
with self.widgets['output_area']:
|
|
1475
|
+
self.widgets['output_area'].clear_output()
|
|
1476
|
+
|
|
1477
|
+
missing_required_fields = []
|
|
1478
|
+
input_files = {}
|
|
1479
|
+
|
|
1480
|
+
for state in self.current_model.states:
|
|
1481
|
+
state_name = state.get('name')
|
|
1482
|
+
input_files[state_name] = {}
|
|
1483
|
+
|
|
1484
|
+
for event in state.get('event', []):
|
|
1485
|
+
if event.get('eventType') == 'response':
|
|
1486
|
+
event_name = event.get('eventName', '')
|
|
1487
|
+
is_required = not event.get('optional', False)
|
|
1488
|
+
|
|
1489
|
+
# 检查是否有nodes数据
|
|
1490
|
+
has_nodes = False
|
|
1491
|
+
nodes_data = []
|
|
1492
|
+
for data_item in event.get('data', []):
|
|
1493
|
+
if 'nodes' in data_item:
|
|
1494
|
+
has_nodes = True
|
|
1495
|
+
nodes_data = data_item['nodes']
|
|
1496
|
+
|
|
1497
|
+
if has_nodes:
|
|
1498
|
+
# 直接收集节点参数值,不转XML
|
|
1499
|
+
for node in nodes_data:
|
|
1500
|
+
widget = self.widgets.get(
|
|
1501
|
+
f'node-{event_name}-{node.get("text")}')
|
|
1502
|
+
if widget:
|
|
1503
|
+
value = widget.value
|
|
1504
|
+
if value:
|
|
1505
|
+
kernel_type = node.get(
|
|
1506
|
+
'kernelType', 'string')
|
|
1507
|
+
node_name = node.get("text")
|
|
1508
|
+
|
|
1509
|
+
# 根据kernelType转换数据类型
|
|
1510
|
+
try:
|
|
1511
|
+
if kernel_type == 'int':
|
|
1512
|
+
converted_value = int(value)
|
|
1513
|
+
elif kernel_type in ['double', 'float']:
|
|
1514
|
+
converted_value = float(value)
|
|
1515
|
+
elif kernel_type == 'boolean':
|
|
1516
|
+
converted_value = str(value).lower() in [
|
|
1517
|
+
'true', '1', 'yes']
|
|
1518
|
+
else: # string or default
|
|
1519
|
+
converted_value = str(value)
|
|
1520
|
+
|
|
1521
|
+
# 直接存储到input_files中
|
|
1522
|
+
input_files[state_name][node_name] = converted_value
|
|
1523
|
+
|
|
1524
|
+
except (ValueError, TypeError) as e:
|
|
1525
|
+
print(
|
|
1526
|
+
f"❌ Error: Invalid value for {node_name}: {value}")
|
|
1527
|
+
return
|
|
1528
|
+
elif is_required:
|
|
1529
|
+
missing_required_fields.append(
|
|
1530
|
+
f"'{node.get('text')}'")
|
|
1531
|
+
elif is_required:
|
|
1532
|
+
missing_required_fields.append(
|
|
1533
|
+
f"'{node.get('text')}'")
|
|
1534
|
+
else:
|
|
1535
|
+
# 处理文件输入
|
|
1536
|
+
file_chooser = self.widgets.get(
|
|
1537
|
+
f'file_chooser_{event_name}')
|
|
1538
|
+
if file_chooser:
|
|
1539
|
+
if file_chooser.selected:
|
|
1540
|
+
input_files[state_name][event_name] = file_chooser.selected
|
|
1541
|
+
elif is_required:
|
|
1542
|
+
missing_required_fields.append(
|
|
1543
|
+
f"'{event_name}'")
|
|
1544
|
+
|
|
1545
|
+
if missing_required_fields:
|
|
1546
|
+
print(
|
|
1547
|
+
f"❌ Error: The following required fields are missing: {', '.join(missing_required_fields)}")
|
|
1548
|
+
return
|
|
1549
|
+
|
|
1550
|
+
try:
|
|
1551
|
+
print(input_files)
|
|
1552
|
+
# 继续执行模型
|
|
1553
|
+
# 导入openModel模块
|
|
1554
|
+
openModel = _lazy_import_openmodel()
|
|
1555
|
+
taskServer = openModel.OGMSAccess(
|
|
1556
|
+
modelName=self.current_model.name,
|
|
1557
|
+
token="6U3O1Sy5696I5ryJFaYCYVjcIV7rhd1MKK0QGX9A7zafogi8xTdvejl6ISUP1lEs"
|
|
1558
|
+
)
|
|
1559
|
+
# 静默运行,不打印控制台日志
|
|
1560
|
+
import contextlib
|
|
1561
|
+
import io
|
|
1562
|
+
_b1, _b2 = io.StringIO(), io.StringIO()
|
|
1563
|
+
with contextlib.redirect_stdout(_b1), contextlib.redirect_stderr(_b2):
|
|
1564
|
+
result = taskServer.createTask(params=input_files)
|
|
1565
|
+
# print(result)
|
|
1566
|
+
|
|
1567
|
+
# 在UI中展示结果的下载链接(不自动下载到本地)
|
|
1568
|
+
display, HTML, _ = _lazy_import_ipython_display()
|
|
1569
|
+
rows = []
|
|
1570
|
+
for output in result:
|
|
1571
|
+
url = output.get('url')
|
|
1572
|
+
tag = output.get('tag', '')
|
|
1573
|
+
suffix = output.get('suffix', '')
|
|
1574
|
+
statename = output.get('statename', '')
|
|
1575
|
+
event = output.get('event', '')
|
|
1576
|
+
filename = f"{tag}.{suffix}" if tag and suffix else (
|
|
1577
|
+
tag or '')
|
|
1578
|
+
if url:
|
|
1579
|
+
rows.append(f"""
|
|
1580
|
+
<tr>
|
|
1581
|
+
<td style=\"padding:8px;border-bottom:1px solid #e5e7eb;text-align:left;\">{statename}</td>
|
|
1582
|
+
<td style=\"padding:8px;border-bottom:1px solid #e5e7eb;text-align:left;\">{event}</td>
|
|
1583
|
+
<td style=\"padding:8px;border-bottom:1px solid #e5e7eb;text-align:left;\">{filename}</td>
|
|
1584
|
+
<td style=\"padding:8px;border-bottom:1px solid #e5e7eb;text-align:left;\"><a href=\"{url}\" target=\"_blank\">Download</a></td>
|
|
1585
|
+
</tr>
|
|
1586
|
+
""")
|
|
1587
|
+
|
|
1588
|
+
# 预生成表格行HTML,避免在f-string表达式中包含反斜杠
|
|
1589
|
+
rows_html = ''.join(
|
|
1590
|
+
rows) if rows else '<tr><td colspan="4" style="padding:8px;color:#64748b;">No outputs</td></tr>'
|
|
1591
|
+
|
|
1592
|
+
table_html = f"""
|
|
1593
|
+
<div style=\"margin:10px 0;\">
|
|
1594
|
+
<div style=\"font-weight:600;margin-bottom:6px;\">Model outputs</div>
|
|
1595
|
+
<table style=\"width:100%;border-collapse:collapse;font-size:14px;\">
|
|
1596
|
+
<thead>
|
|
1597
|
+
<tr style=\"background:#f8fafc;\">
|
|
1598
|
+
<th style=\"text-align:left;padding:8px;border-bottom:1px solid #e5e7eb;\">State</th>
|
|
1599
|
+
<th style=\"text-align:left;padding:8px;border-bottom:1px solid #e5e7eb;\">Event</th>
|
|
1600
|
+
<th style=\"text-align:left;padding:8px;border-bottom:1px solid #e5e7eb;\">File</th>
|
|
1601
|
+
<th style=\"text-align:left;padding:8px;border-bottom:1px solid #e5e7eb;\">Link</th>
|
|
1602
|
+
</tr>
|
|
1603
|
+
</thead>
|
|
1604
|
+
<tbody>
|
|
1605
|
+
{rows_html}
|
|
1606
|
+
</tbody>
|
|
1607
|
+
</table>
|
|
1608
|
+
</div>
|
|
1609
|
+
"""
|
|
1610
|
+
display(HTML(table_html))
|
|
1611
|
+
|
|
1612
|
+
except Exception as e:
|
|
1613
|
+
print(f"❌ Error: Model run failed - {str(e)}")
|
|
1614
|
+
|
|
1615
|
+
def _upload_to_server(self, xml_content, event_name):
|
|
1616
|
+
"""上传XML据到中转服务器并获取下载链接"""
|
|
1617
|
+
# 导入requests模块
|
|
1618
|
+
requests = _lazy_import_requests()
|
|
1619
|
+
from io import StringIO
|
|
1620
|
+
|
|
1621
|
+
try:
|
|
1622
|
+
# 务器地址
|
|
1623
|
+
upload_url = 'http://221.224.35.86:38083/data'
|
|
1624
|
+
|
|
1625
|
+
# 使用event_name作为文件名
|
|
1626
|
+
filename = f"{event_name}"
|
|
1627
|
+
|
|
1628
|
+
# 创建表单数据
|
|
1629
|
+
files = {
|
|
1630
|
+
'datafile': (filename, StringIO(xml_content), 'application/xml')
|
|
1631
|
+
}
|
|
1632
|
+
data = {
|
|
1633
|
+
'name': filename # 使用相同的文件名
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
# 发送POST请求
|
|
1637
|
+
response = requests.post(upload_url, files=files, data=data)
|
|
1638
|
+
|
|
1639
|
+
# 检查响应状态
|
|
1640
|
+
if response.status_code == 200:
|
|
1641
|
+
response_data = response.json()
|
|
1642
|
+
# 构造下载链接
|
|
1643
|
+
download_url = f"{upload_url}/{response_data['data']['id']}"
|
|
1644
|
+
return download_url
|
|
1645
|
+
else:
|
|
1646
|
+
raise Exception(
|
|
1647
|
+
f"Server returned error status code: {response.status_code}")
|
|
1648
|
+
|
|
1649
|
+
except Exception as e:
|
|
1650
|
+
raise Exception(f"Failed to upload data to server: {str(e)}")
|
|
1651
|
+
|
|
1652
|
+
async def _rewrite_user_query(self, original_query: str) -> str:
|
|
1653
|
+
"""
|
|
1654
|
+
使用LLM对用户查询进行改写,基于当前模型上下文和用户建模历史
|
|
1655
|
+
"""
|
|
1656
|
+
# 导入IPython模块
|
|
1657
|
+
get_ipython = _lazy_import_ipython()
|
|
1658
|
+
|
|
1659
|
+
# 导入OpenAI模块
|
|
1660
|
+
OpenAI = _lazy_import_openai()
|
|
1661
|
+
|
|
1662
|
+
# 只收集模型名称和描述
|
|
1663
|
+
model_info = {
|
|
1664
|
+
"name": self.current_model.name,
|
|
1665
|
+
"description": self.current_model.description
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
# 获取Jupyter历史上下文
|
|
1669
|
+
ip = get_ipython()
|
|
1670
|
+
history_context = ""
|
|
1671
|
+
if ip is not None:
|
|
1672
|
+
history = []
|
|
1673
|
+
for session, line_num, input in ip.history_manager.get_range():
|
|
1674
|
+
history.append(input)
|
|
1675
|
+
history_context = "\n".join(history[-10:]) # 只取最近10条指令
|
|
1676
|
+
|
|
1677
|
+
# 构建上下文增强提示
|
|
1678
|
+
prompt = f"""
|
|
1679
|
+
你是一个专业的地理建模系统助手,你的任务是理解用户对模型的问题并进行智能改写,使其更加明确和全面,以便更好地回答用户真正的需求。
|
|
1680
|
+
|
|
1681
|
+
### 当前上下文:
|
|
1682
|
+
1. 用户正在使用名为"{model_info['name']}"的地理模型
|
|
1683
|
+
2. 模型描述: {model_info['description']}
|
|
1684
|
+
3. 用户最近的Jupyter代码历史:
|
|
1685
|
+
```
|
|
1686
|
+
{history_context}
|
|
1687
|
+
```
|
|
1688
|
+
|
|
1689
|
+
### 原始用户查询:
|
|
1690
|
+
"{original_query}"
|
|
1691
|
+
|
|
1692
|
+
### 你的任务:
|
|
1693
|
+
1. 分析用户原始查询,考虑查询是否具体明确
|
|
1694
|
+
2. 如果用户查询过于宽泛或模糊,请根据上下文将其具体化和明确化
|
|
1695
|
+
3. 如果用户查询是关于模型参数,请确保改写后的查询包括该参数的具体角色、作用、推荐值范围等内容
|
|
1696
|
+
4. 如果用户查询是关于模型整体的,请考虑将查询扩展到包含模型的理论基础、应用场景、实际案例等
|
|
1697
|
+
5. 如果用户查询是关于模型与其他模型比较的,请明确比较的具体方面(如精度、速度、适用场景等)
|
|
1698
|
+
|
|
1699
|
+
### 输出格式:
|
|
1700
|
+
只输出改写后的查询,不要包含任何解释或前缀,直接返回改写后的查询文本。如果原始查询已经足够明确和全面,可以保持不变或进行微调。改写后的问题要短小精炼,不要冗余。问题要限制在200个英文字符以内。
|
|
1701
|
+
"""
|
|
1702
|
+
|
|
1703
|
+
# 调用OpenAI API进行查询改写
|
|
1704
|
+
client = OpenAI(api_key="sk-4bp5a1DcdLSHCiw1401270055f47424b9eA58cAd587266A3",
|
|
1705
|
+
base_url="https://aihubmix.com/v1")
|
|
1706
|
+
try:
|
|
1707
|
+
response = client.chat.completions.create(
|
|
1708
|
+
model="gpt-4.1-nano",
|
|
1709
|
+
messages=[
|
|
1710
|
+
{"role": "system", "content": prompt},
|
|
1711
|
+
{"role": "user", "content": original_query}
|
|
1712
|
+
],
|
|
1713
|
+
temperature=0.3, # 使用较低温度以获得更确定性的输出
|
|
1714
|
+
max_tokens=15000
|
|
1715
|
+
)
|
|
1716
|
+
rewritten_query = response.choices[0].message.content.strip()
|
|
1717
|
+
return rewritten_query
|
|
1718
|
+
except Exception as e:
|
|
1719
|
+
print(f"查询改写出错: {str(e)}")
|
|
1720
|
+
return original_query # 如果出错,返回原始查询
|
|
1721
|
+
|
|
1722
|
+
async def _get_search_result(self, query: str) -> str:
|
|
1723
|
+
"""
|
|
1724
|
+
调用学术查询服务和自建知识库获取结果
|
|
1725
|
+
"""
|
|
1726
|
+
# 导入IPython模块
|
|
1727
|
+
get_ipython = _lazy_import_ipython()
|
|
1728
|
+
|
|
1729
|
+
# 首先进行查询改写
|
|
1730
|
+
rewritten_query = await self._rewrite_user_query(query)
|
|
1731
|
+
|
|
1732
|
+
# 获取历史上下文
|
|
1733
|
+
ip = get_ipython()
|
|
1734
|
+
history_context = ""
|
|
1735
|
+
if ip is not None:
|
|
1736
|
+
history = []
|
|
1737
|
+
for session, line_num, input in ip.history_manager.get_range():
|
|
1738
|
+
history.append(input)
|
|
1739
|
+
history_context = "\n".join(history)
|
|
1740
|
+
|
|
1741
|
+
# 构建建模上下文
|
|
1742
|
+
modeling_context = f"""
|
|
1743
|
+
当前模型: {self.current_model.name}
|
|
1744
|
+
模型描述: {self.current_model.description}
|
|
1745
|
+
历史记录:
|
|
1746
|
+
{history_context}
|
|
1747
|
+
"""
|
|
1748
|
+
|
|
1749
|
+
try:
|
|
1750
|
+
# 并行查询两个数据源
|
|
1751
|
+
tasks = []
|
|
1752
|
+
|
|
1753
|
+
# 任务1: 查询学术论文API
|
|
1754
|
+
academic_task = asyncio.create_task(
|
|
1755
|
+
self._query_academic_api(rewritten_query))
|
|
1756
|
+
tasks.append(academic_task)
|
|
1757
|
+
|
|
1758
|
+
# 任务2: 查询本地知识库
|
|
1759
|
+
# 直接查询本地模型数据,不需要外部ID
|
|
1760
|
+
kb_task = asyncio.create_task(
|
|
1761
|
+
self._query_knowledge_base(rewritten_query))
|
|
1762
|
+
tasks.append(kb_task)
|
|
1763
|
+
|
|
1764
|
+
# 等待所有查询完成
|
|
1765
|
+
results = await asyncio.gather(*tasks)
|
|
1766
|
+
|
|
1767
|
+
# 收集结果
|
|
1768
|
+
academic_result = results[0] if results else {}
|
|
1769
|
+
kb_records = results[1] if len(results) > 1 else []
|
|
1770
|
+
|
|
1771
|
+
# 如果自建知识库有结果,合并到最终结果中
|
|
1772
|
+
if kb_records:
|
|
1773
|
+
# 处理知识库结果
|
|
1774
|
+
kb_contents = []
|
|
1775
|
+
for record in kb_records:
|
|
1776
|
+
segment = record.get("segment", {})
|
|
1777
|
+
kb_contents.append(segment.get("content", ""))
|
|
1778
|
+
|
|
1779
|
+
# 使用OpenAI合成最终回答
|
|
1780
|
+
final_answer = await self._synthesize_final_answer(
|
|
1781
|
+
academic_result.get("answer", ""),
|
|
1782
|
+
kb_contents,
|
|
1783
|
+
rewritten_query
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1786
|
+
# 构建包含自建知识库的新结果
|
|
1787
|
+
enhanced_result = {
|
|
1788
|
+
"question": academic_result.get("question", rewritten_query),
|
|
1789
|
+
"answer": final_answer,
|
|
1790
|
+
"paperList": academic_result.get("paperList", []),
|
|
1791
|
+
"knowledgeBase": kb_records
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
return enhanced_result
|
|
1795
|
+
else:
|
|
1796
|
+
# 如果没有知识库结果,直接返回学术结果
|
|
1797
|
+
return academic_result
|
|
1798
|
+
|
|
1799
|
+
except Exception as e:
|
|
1800
|
+
print(f"获取搜索结果时出错: {str(e)}")
|
|
1801
|
+
return {"answer": "网络异常请稍后重试", "paperList": []}
|
|
1802
|
+
|
|
1803
|
+
async def _query_academic_api(self, query: str) -> dict:
|
|
1804
|
+
"""
|
|
1805
|
+
查询学术API获取论文和答案
|
|
1806
|
+
"""
|
|
1807
|
+
# 导入学术查询服务
|
|
1808
|
+
AcademicQueryService = _lazy_import_academic_service()
|
|
1809
|
+
|
|
1810
|
+
try:
|
|
1811
|
+
service = AcademicQueryService()
|
|
1812
|
+
full_query = f"Tell me about {self.current_model.name} model's {query}"
|
|
1813
|
+
result = await service.get_academic_question_answer(full_query)
|
|
1814
|
+
return result
|
|
1815
|
+
except Exception as e:
|
|
1816
|
+
print(f"查询学术API时出错: {str(e)}")
|
|
1817
|
+
return {"answer": "学术查询服务暂时不可用", "paperList": []}
|
|
1818
|
+
|
|
1819
|
+
async def _synthesize_final_answer(self, academic_answer: str, kb_contents: list, query: str) -> str:
|
|
1820
|
+
"""
|
|
1821
|
+
使用OpenAI合成最终答案,整合学术答案和知识库内容
|
|
1822
|
+
"""
|
|
1823
|
+
# 导入OpenAI模块
|
|
1824
|
+
OpenAI = _lazy_import_openai()
|
|
1825
|
+
|
|
1826
|
+
try:
|
|
1827
|
+
# 准备知识库内容
|
|
1828
|
+
kb_content_text = "\n---\n".join(kb_contents)
|
|
1829
|
+
|
|
1830
|
+
# 构建提示
|
|
1831
|
+
prompt = f"""
|
|
1832
|
+
作为地理建模领域的专家助手,你的任务是基于以下两个来源的信息,为用户提供最全面、最准确的回答:
|
|
1833
|
+
|
|
1834
|
+
1. 来自学术论文的答案:
|
|
1835
|
+
{academic_answer}
|
|
1836
|
+
|
|
1837
|
+
2. 来自模型知识库的内容:
|
|
1838
|
+
{kb_content_text}
|
|
1839
|
+
|
|
1840
|
+
用户的问题是: "{query}"
|
|
1841
|
+
|
|
1842
|
+
请综合分析这两个来源的信息,给出一个完整的回答,满足以下要求:
|
|
1843
|
+
1. 合并这两个来源的关键信息,避免重复
|
|
1844
|
+
2. 如果学术来源和知识库来源有冲突,请说明这种差异
|
|
1845
|
+
3. 优先引用知识库的具体参数值、配置建议和使用方法
|
|
1846
|
+
4. 以清晰的结构组织回答,必要时使用小标题和列表
|
|
1847
|
+
5. 如果知识库内容包含具体的模型参数或配置指南,请着重强调这些实用信息
|
|
1848
|
+
|
|
1849
|
+
你的回答应当既满足科学严谨性,又具有实操指导价值。请直接给出回答,不需要解释或总结你的分析过程。
|
|
1850
|
+
Please output in English.
|
|
1851
|
+
"""
|
|
1852
|
+
|
|
1853
|
+
# 调用OpenAI API
|
|
1854
|
+
client = OpenAI(api_key="sk-4bp5a1DcdLSHCiw1401270055f47424b9eA58cAd587266A3",
|
|
1855
|
+
base_url="https://aihubmix.com/v1")
|
|
1856
|
+
response = client.chat.completions.create(
|
|
1857
|
+
model="gpt-4.1-nano",
|
|
1858
|
+
messages=[
|
|
1859
|
+
{"role": "system", "content": prompt}
|
|
1860
|
+
],
|
|
1861
|
+
temperature=0.3,
|
|
1862
|
+
max_tokens=1500
|
|
1863
|
+
)
|
|
1864
|
+
|
|
1865
|
+
return response.choices[0].message.content.strip()
|
|
1866
|
+
|
|
1867
|
+
except Exception as e:
|
|
1868
|
+
print(f"合成最终答案时出错: {str(e)}")
|
|
1869
|
+
return f"{academic_answer}\n\n[注: 知识库内容集成失败]"
|
|
1870
|
+
|
|
1871
|
+
async def _get_knowledge_base_model_id(self, model_name: str) -> str:
|
|
1872
|
+
"""
|
|
1873
|
+
查询MongoDB获取模型ID
|
|
1874
|
+
"""
|
|
1875
|
+
try:
|
|
1876
|
+
# 模型ID映射表 - 实际应用中可以从MongoDB查询
|
|
1877
|
+
# 以下是示例映射,实际使用时应替换为真实的数据库查询
|
|
1878
|
+
model_id_mapping = {
|
|
1879
|
+
"SWAT_Model": "67eaa67e713cad3b0e31b438",
|
|
1880
|
+
# 其他模型映射...
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
# 查找当前模型ID
|
|
1884
|
+
model_id = model_id_mapping.get(model_name)
|
|
1885
|
+
if not model_id:
|
|
1886
|
+
print(f"警告: 未找到模型 '{model_name}' 的知识库ID")
|
|
1887
|
+
return None
|
|
1888
|
+
|
|
1889
|
+
return model_id
|
|
1890
|
+
except Exception as e:
|
|
1891
|
+
print(f"获取模型知识库ID时出错: {str(e)}")
|
|
1892
|
+
return None
|
|
1893
|
+
|
|
1894
|
+
async def _query_knowledge_base(self, query: str, top_k: int = 3) -> list:
|
|
1895
|
+
"""
|
|
1896
|
+
查询本地模型知识库(基于computeModel.json)
|
|
1897
|
+
"""
|
|
1898
|
+
try:
|
|
1899
|
+
import json
|
|
1900
|
+
import os
|
|
1901
|
+
|
|
1902
|
+
# 加载本地模型数据
|
|
1903
|
+
model_data_path = os.path.join(os.path.dirname(
|
|
1904
|
+
__file__), 'data', 'computeModel.json')
|
|
1905
|
+
|
|
1906
|
+
if not os.path.exists(model_data_path):
|
|
1907
|
+
return []
|
|
1908
|
+
|
|
1909
|
+
with open(model_data_path, 'r', encoding='utf-8') as f:
|
|
1910
|
+
all_models = json.load(f)
|
|
1911
|
+
|
|
1912
|
+
# 获取当前模型信息
|
|
1913
|
+
current_model_name = self.current_model.name
|
|
1914
|
+
if current_model_name not in all_models:
|
|
1915
|
+
return []
|
|
1916
|
+
|
|
1917
|
+
model_info = all_models[current_model_name]
|
|
1918
|
+
|
|
1919
|
+
# 构建知识库内容
|
|
1920
|
+
kb_contents = []
|
|
1921
|
+
|
|
1922
|
+
# 1. 模型描述
|
|
1923
|
+
if 'description' in model_info:
|
|
1924
|
+
kb_contents.append({
|
|
1925
|
+
"type": "model_description",
|
|
1926
|
+
"content": f"模型描述: {model_info['description']}",
|
|
1927
|
+
"relevance": 0.9
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
# 2. 模型标签
|
|
1931
|
+
if 'normalTags' in model_info:
|
|
1932
|
+
tags = model_info['normalTags']
|
|
1933
|
+
kb_contents.append({
|
|
1934
|
+
"type": "model_tags",
|
|
1935
|
+
"content": f"应用领域: {', '.join(tags)}",
|
|
1936
|
+
"relevance": 0.7
|
|
1937
|
+
})
|
|
1938
|
+
|
|
1939
|
+
# 3. 参数信息
|
|
1940
|
+
if 'mdlJson' in model_info and 'mdl' in model_info['mdlJson']:
|
|
1941
|
+
mdl = model_info['mdlJson']['mdl']
|
|
1942
|
+
|
|
1943
|
+
# 提取事件和参数信息
|
|
1944
|
+
if 'events' in mdl:
|
|
1945
|
+
for event in mdl['events']:
|
|
1946
|
+
event_desc = event.get('eventDesc', '')
|
|
1947
|
+
if event_desc and any(keyword in event_desc.lower() for keyword in query.lower().split()):
|
|
1948
|
+
kb_contents.append({
|
|
1949
|
+
"type": "event_description",
|
|
1950
|
+
"content": f"操作步骤: {event_desc}",
|
|
1951
|
+
"relevance": 0.8
|
|
1952
|
+
})
|
|
1953
|
+
|
|
1954
|
+
# 提取参数信息
|
|
1955
|
+
if 'data' in event:
|
|
1956
|
+
for param in event['data']:
|
|
1957
|
+
param_text = param.get('text', '')
|
|
1958
|
+
param_desc = param.get('desc', '')
|
|
1959
|
+
param_type = param.get('dataType', '')
|
|
1960
|
+
|
|
1961
|
+
if param_text and any(keyword in param_text.lower() for keyword in query.lower().split()):
|
|
1962
|
+
kb_contents.append({
|
|
1963
|
+
"type": "parameter_info",
|
|
1964
|
+
"content": f"参数 '{param_text}': {param_desc} (类型: {param_type})",
|
|
1965
|
+
"relevance": 0.9
|
|
1966
|
+
})
|
|
1967
|
+
|
|
1968
|
+
# 4. 按相关性排序并返回前top_k个结果
|
|
1969
|
+
kb_contents.sort(key=lambda x: x['relevance'], reverse=True)
|
|
1970
|
+
|
|
1971
|
+
# 转换为与原来格式兼容的结构
|
|
1972
|
+
formatted_results = []
|
|
1973
|
+
for item in kb_contents[:top_k]:
|
|
1974
|
+
formatted_results.append({
|
|
1975
|
+
"segment": {
|
|
1976
|
+
"content": item["content"],
|
|
1977
|
+
"type": item["type"],
|
|
1978
|
+
"relevance": item["relevance"]
|
|
1979
|
+
}
|
|
1980
|
+
})
|
|
1981
|
+
|
|
1982
|
+
return formatted_results
|
|
1983
|
+
|
|
1984
|
+
except Exception as e:
|
|
1985
|
+
return []
|
|
1986
|
+
|
|
1987
|
+
def on_search_submit(self, widget):
|
|
1988
|
+
"""处理搜索提交"""
|
|
1989
|
+
# 导入IPython display模块
|
|
1990
|
+
_lazy_import_ipython_display()
|
|
1991
|
+
|
|
1992
|
+
query = widget.value.strip()
|
|
1993
|
+
with self.widgets['result_area']:
|
|
1994
|
+
self.widgets['result_area'].clear_output()
|
|
1995
|
+
if query:
|
|
1996
|
+
# 显示加载动画
|
|
1997
|
+
loading_html = """
|
|
1998
|
+
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 0;">
|
|
1999
|
+
<div class="loading-spinner"></div>
|
|
2000
|
+
<p style="margin-top: 16px; color: #6b7280; font-size: 14px;">Please wait while we process your request...</p>
|
|
2001
|
+
<style>
|
|
2002
|
+
.loading-spinner {
|
|
2003
|
+
width: 50px;
|
|
2004
|
+
height: 50px;
|
|
2005
|
+
border: 5px solid rgba(79, 70, 229, 0.2);
|
|
2006
|
+
border-radius: 50%;
|
|
2007
|
+
border-top-color: #4f46e5;
|
|
2008
|
+
animation: spin 1s linear infinite;
|
|
2009
|
+
}
|
|
2010
|
+
@keyframes spin {
|
|
2011
|
+
to { transform: rotate(360deg); }
|
|
2012
|
+
}
|
|
2013
|
+
</style>
|
|
2014
|
+
</div>
|
|
2015
|
+
"""
|
|
2016
|
+
display(HTML(loading_html))
|
|
2017
|
+
|
|
2018
|
+
# 获取当前运行的事件循环
|
|
2019
|
+
loop = asyncio.get_event_loop()
|
|
2020
|
+
try:
|
|
2021
|
+
# 清除之前的输出,包括加载动画
|
|
2022
|
+
self.widgets['result_area'].clear_output(wait=True)
|
|
2023
|
+
|
|
2024
|
+
# 执行查询
|
|
2025
|
+
result = loop.run_until_complete(
|
|
2026
|
+
self._get_search_result(query))
|
|
2027
|
+
if isinstance(result, dict):
|
|
2028
|
+
# 将答案转换为markdown格式
|
|
2029
|
+
markdown_func, _ = _lazy_import_markdown()
|
|
2030
|
+
answer_html = markdown_func(
|
|
2031
|
+
result['answer'], extensions=['extra'])
|
|
2032
|
+
# 包装在div中显示
|
|
2033
|
+
answer_wrapper = f"""
|
|
2034
|
+
<style>
|
|
2035
|
+
.answer-box {{
|
|
2036
|
+
margin: 0;
|
|
2037
|
+
padding: 0;
|
|
2038
|
+
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
2039
|
+
}}
|
|
2040
|
+
.answer-box h1 {{
|
|
2041
|
+
font-size: 1.5rem;
|
|
2042
|
+
font-weight: 600;
|
|
2043
|
+
margin: 1.2rem 0 0.8rem 0;
|
|
2044
|
+
border-bottom: 2px solid #dbeafe;
|
|
2045
|
+
padding-bottom: 0.3rem;
|
|
2046
|
+
}}
|
|
2047
|
+
.answer-box h2 {{
|
|
2048
|
+
font-size: 1.3rem;
|
|
2049
|
+
font-weight: 600;
|
|
2050
|
+
margin: 1.1rem 0 0.7rem 0;
|
|
2051
|
+
}}
|
|
2052
|
+
.answer-box h3 {{
|
|
2053
|
+
font-size: 1.15rem;
|
|
2054
|
+
font-weight: 600;
|
|
2055
|
+
margin: 1rem 0 0.6rem 0;
|
|
2056
|
+
}}
|
|
2057
|
+
.answer-box p {{
|
|
2058
|
+
margin: 0.8rem 0;
|
|
2059
|
+
line-height: 1.6;
|
|
2060
|
+
text-align: justify;
|
|
2061
|
+
}}
|
|
2062
|
+
.answer-box ul, .answer-box ol {{
|
|
2063
|
+
margin: 0.8rem 0;
|
|
2064
|
+
padding-left: 1.5rem;
|
|
2065
|
+
}}
|
|
2066
|
+
.answer-box li {{
|
|
2067
|
+
margin: 0.4rem 0;
|
|
2068
|
+
line-height: 1.6;
|
|
2069
|
+
}}
|
|
2070
|
+
.answer-box strong {{
|
|
2071
|
+
font-weight: 600;
|
|
2072
|
+
}}
|
|
2073
|
+
.answer-box code {{
|
|
2074
|
+
background-color: #f1f5f9;
|
|
2075
|
+
color: #ef4444;
|
|
2076
|
+
padding: 0.1rem 0.3rem;
|
|
2077
|
+
border-radius: 0.2rem;
|
|
2078
|
+
font-family: Menlo, Monaco, Consolas, monospace;
|
|
2079
|
+
font-size: 0.9em;
|
|
2080
|
+
}}
|
|
2081
|
+
</style>
|
|
2082
|
+
<div style="background: linear-gradient(to bottom, #ffffff, #f8fafc); border-radius: 0.75rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); margin: 0.5rem 0; overflow: hidden;">
|
|
2083
|
+
<div style="height: 0.3rem; background: linear-gradient(90deg, #3b82f6, #2563eb);"></div>
|
|
2084
|
+
<div class="answer-box" style="padding: 1.25rem; font-size: 15px; line-height: 1.6; color: #374151;">
|
|
2085
|
+
{answer_html}
|
|
2086
|
+
</div>
|
|
2087
|
+
</div>
|
|
2088
|
+
"""
|
|
2089
|
+
display(HTML(answer_wrapper))
|
|
2090
|
+
|
|
2091
|
+
# 创建选项卡的HTML内容
|
|
2092
|
+
has_kb = 'knowledgeBase' in result and result['knowledgeBase']
|
|
2093
|
+
has_papers = 'paperList' in result and result['paperList']
|
|
2094
|
+
|
|
2095
|
+
if has_papers:
|
|
2096
|
+
# 只显示Related Resources选项卡
|
|
2097
|
+
tab_buttons = []
|
|
2098
|
+
active_tab = "papers"
|
|
2099
|
+
|
|
2100
|
+
papers_active = 'active'
|
|
2101
|
+
tab_buttons.append(
|
|
2102
|
+
f"""<button class="tab-button {papers_active}" onclick="switchTab(event, 'papers-content')">Related Resources ({len(result['paperList'])})</button>""")
|
|
2103
|
+
|
|
2104
|
+
# 构建论文内容
|
|
2105
|
+
papers_content = ""
|
|
2106
|
+
if has_papers:
|
|
2107
|
+
papers_display = "block"
|
|
2108
|
+
paper_items = []
|
|
2109
|
+
for paper in result['paperList']:
|
|
2110
|
+
authors = paper.get('authors', [])
|
|
2111
|
+
if len(authors) > 3:
|
|
2112
|
+
author_text = f"{authors[0]} et al."
|
|
2113
|
+
else:
|
|
2114
|
+
author_text = " · ".join(authors)
|
|
2115
|
+
|
|
2116
|
+
markdown_func, _ = _lazy_import_markdown()
|
|
2117
|
+
title_html = markdown_func(
|
|
2118
|
+
paper['title'], extensions=['extra'])
|
|
2119
|
+
display_text_html = markdown_func(
|
|
2120
|
+
paper.get('display_text', ''), extensions=['extra'])
|
|
2121
|
+
|
|
2122
|
+
paper_item = f"""
|
|
2123
|
+
<div style="margin: 8px 0; padding: 12px; background: white; border: 1px solid #e5e7eb; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
2124
|
+
<h4 style="margin: 0 0 8px 0; padding: 0; font-size: 14px; font-weight: 600; color: #111827; line-height: 1.4; text-align: justify;">{title_html}</h4>
|
|
2125
|
+
<p style="margin: 0 0 8px 0; padding: 0; color: #4b5563; font-size: 13px; line-height: 1.5; text-align: justify;">{display_text_html}</p>
|
|
2126
|
+
<div style="display: flex; gap: 10px; align-items: center; font-size: 11px; color: #6b7280;">
|
|
2127
|
+
<span style="padding: 2px 8px; background: #f3f4f6; border-radius: 9999px;">{paper.get('year', 'N/A')}</span>
|
|
2128
|
+
<span>{paper.get('citation_count', 0)} Citations</span>
|
|
2129
|
+
<span>{author_text}</span>
|
|
2130
|
+
<span style="color: #9ca3af;">{paper.get('journal', 'N/A')}</span>
|
|
2131
|
+
</div>
|
|
2132
|
+
</div>
|
|
2133
|
+
"""
|
|
2134
|
+
paper_items.append(paper_item)
|
|
2135
|
+
|
|
2136
|
+
papers_content = f"""<div id="papers-content" class="tab-content" style="display: {papers_display};">{''.join(paper_items)}</div>"""
|
|
2137
|
+
|
|
2138
|
+
# 组合选项卡
|
|
2139
|
+
tab_style = """
|
|
2140
|
+
<style>
|
|
2141
|
+
.tab-container {
|
|
2142
|
+
margin-top: 16px;
|
|
2143
|
+
border-radius: 8px;
|
|
2144
|
+
overflow: hidden;
|
|
2145
|
+
border: 1px solid #e5e7eb;
|
|
2146
|
+
}
|
|
2147
|
+
.tab-buttons {
|
|
2148
|
+
display: flex;
|
|
2149
|
+
background: #f3f4f6;
|
|
2150
|
+
border-bottom: 1px solid #e5e7eb;
|
|
2151
|
+
}
|
|
2152
|
+
.tab-button {
|
|
2153
|
+
padding: 10px 16px;
|
|
2154
|
+
border: none;
|
|
2155
|
+
background: none;
|
|
2156
|
+
cursor: pointer;
|
|
2157
|
+
font-size: 14px;
|
|
2158
|
+
font-weight: 500;
|
|
2159
|
+
color: #6b7280;
|
|
2160
|
+
transition: all 0.2s;
|
|
2161
|
+
}
|
|
2162
|
+
.tab-button:hover {
|
|
2163
|
+
background: rgba(255, 255, 255, 0.5);
|
|
2164
|
+
}
|
|
2165
|
+
.tab-button.active {
|
|
2166
|
+
color: #4f46e5;
|
|
2167
|
+
background: white;
|
|
2168
|
+
border-bottom: 2px solid #4f46e5;
|
|
2169
|
+
}
|
|
2170
|
+
.tab-content {
|
|
2171
|
+
padding: 16px;
|
|
2172
|
+
background: white;
|
|
2173
|
+
max-height: 500px;
|
|
2174
|
+
overflow-y: auto;
|
|
2175
|
+
}
|
|
2176
|
+
</style>
|
|
2177
|
+
"""
|
|
2178
|
+
|
|
2179
|
+
tab_script = """
|
|
2180
|
+
<script>
|
|
2181
|
+
function switchTab(evt, tabName) {
|
|
2182
|
+
var i, tabContent, tabButtons;
|
|
2183
|
+
|
|
2184
|
+
// 隐藏所有标签内容
|
|
2185
|
+
tabContent = document.getElementsByClassName("tab-content");
|
|
2186
|
+
for (i = 0; i < tabContent.length; i++) {
|
|
2187
|
+
tabContent[i].style.display = "none";
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// 移除所有按钮的活动状态
|
|
2191
|
+
tabButtons = document.getElementsByClassName("tab-button");
|
|
2192
|
+
for (i = 0; i < tabButtons.length; i++) {
|
|
2193
|
+
tabButtons[i].className = tabButtons[i].className.replace(" active", "");
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// 显示当前标签并添加活动状态
|
|
2197
|
+
document.getElementById(tabName).style.display = "block";
|
|
2198
|
+
evt.currentTarget.className += " active";
|
|
2199
|
+
}
|
|
2200
|
+
</script>
|
|
2201
|
+
"""
|
|
2202
|
+
|
|
2203
|
+
tabs_html = f"""
|
|
2204
|
+
{tab_style}
|
|
2205
|
+
<div class="tab-container">
|
|
2206
|
+
<div class="tab-buttons">
|
|
2207
|
+
{''.join(tab_buttons)}
|
|
2208
|
+
</div>
|
|
2209
|
+
{papers_content}
|
|
2210
|
+
</div>
|
|
2211
|
+
{tab_script}
|
|
2212
|
+
"""
|
|
2213
|
+
|
|
2214
|
+
display(HTML(tabs_html))
|
|
2215
|
+
else:
|
|
2216
|
+
print(result)
|
|
2217
|
+
except Exception as e:
|
|
2218
|
+
print(f"发生错误: {str(e)}")
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
class NotebookContext:
|
|
2222
|
+
"""用于收集和处理Notebook上下文信息"""
|
|
2223
|
+
|
|
2224
|
+
def __init__(self):
|
|
2225
|
+
self.data_context = self._get_data_context()
|
|
2226
|
+
self.model_context = self._get_model_context()
|
|
2227
|
+
self.history_context = self._get_modeling_history_context()
|
|
2228
|
+
|
|
2229
|
+
def to_dict(self):
|
|
2230
|
+
"""将上下文信息转换为字典格式"""
|
|
2231
|
+
return {
|
|
2232
|
+
"data_context": self.data_context,
|
|
2233
|
+
"model_context": self.model_context,
|
|
2234
|
+
"history_context": self.history_context
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
def _get_data_context(self):
|
|
2238
|
+
"""获取数据仓库上下文信息"""
|
|
2239
|
+
try:
|
|
2240
|
+
# 获取IPython shell实例
|
|
2241
|
+
get_ipython = _lazy_import_ipython()
|
|
2242
|
+
ipython = get_ipython()
|
|
2243
|
+
if ipython is None:
|
|
2244
|
+
raise RuntimeError(
|
|
2245
|
+
"This function must be run in an IPython environment")
|
|
2246
|
+
|
|
2247
|
+
# 获取当前工作录
|
|
2248
|
+
notebook_dir = os.getcwd()
|
|
2249
|
+
|
|
2250
|
+
# 定义要排除的目录和文件模式
|
|
2251
|
+
exclude_dirs = {
|
|
2252
|
+
'.git',
|
|
2253
|
+
'__pycache__',
|
|
2254
|
+
'.ipynb_checkpoints',
|
|
2255
|
+
'node_modules',
|
|
2256
|
+
'.idea',
|
|
2257
|
+
'.vscode'
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
# 定义要排除的扩展名
|
|
2261
|
+
exclude_extensions = {
|
|
2262
|
+
'.pyc',
|
|
2263
|
+
'.pyo',
|
|
2264
|
+
'.pyd',
|
|
2265
|
+
'.so',
|
|
2266
|
+
'.git',
|
|
2267
|
+
'.DS_Store',
|
|
2268
|
+
'.gitignore',
|
|
2269
|
+
'.py',
|
|
2270
|
+
'.c',
|
|
2271
|
+
'.md',
|
|
2272
|
+
'.txt'
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
# 创建数据文件列表
|
|
2276
|
+
data_files = []
|
|
2277
|
+
|
|
2278
|
+
# 遍历目录树
|
|
2279
|
+
for root, dirs, files in os.walk(notebook_dir):
|
|
2280
|
+
# 过滤掉不需要的目录
|
|
2281
|
+
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
2282
|
+
|
|
2283
|
+
# 过滤并处理文件
|
|
2284
|
+
for file in files:
|
|
2285
|
+
# 检查文件扩展名
|
|
2286
|
+
_, ext = os.path.splitext(file)
|
|
2287
|
+
if ext not in exclude_extensions and not file.startswith('.'):
|
|
2288
|
+
# 获取相对路径
|
|
2289
|
+
rel_path = os.path.relpath(
|
|
2290
|
+
os.path.join(root, file), notebook_dir)
|
|
2291
|
+
data_files.append(
|
|
2292
|
+
f"- A {ext[1:]} file named '{file}' located at '{rel_path}'")
|
|
2293
|
+
|
|
2294
|
+
# 构建自然语描述
|
|
2295
|
+
if not data_files:
|
|
2296
|
+
context_description = "No relevant data files found in the current directory."
|
|
2297
|
+
else:
|
|
2298
|
+
context_description = "The following data files are available in the current working directory:\n"
|
|
2299
|
+
context_description += "\n".join(data_files)
|
|
2300
|
+
context_description += "\n\nThese files might be useful as input data for model operations."
|
|
2301
|
+
|
|
2302
|
+
return context_description
|
|
2303
|
+
|
|
2304
|
+
except Exception as e:
|
|
2305
|
+
print(f"Error getting data context: {str(e)}")
|
|
2306
|
+
return "Failed to analyze data context due to an error."
|
|
2307
|
+
|
|
2308
|
+
def _get_model_context(self):
|
|
2309
|
+
"""获取模型仓库上下文信息"""
|
|
2310
|
+
try:
|
|
2311
|
+
# 获取当前文件所在目录
|
|
2312
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
2313
|
+
# 构建JSON文件路径
|
|
2314
|
+
json_path = os.path.join(current_dir, "data", "computeModel.json")
|
|
2315
|
+
|
|
2316
|
+
# 取模型配置文件
|
|
2317
|
+
with open(json_path, encoding='utf-8') as f:
|
|
2318
|
+
models_data = json.load(f)
|
|
2319
|
+
|
|
2320
|
+
# 如果没有模型数据,返回相应描述
|
|
2321
|
+
if not models_data:
|
|
2322
|
+
return "No models are currently available in the model repository."
|
|
2323
|
+
|
|
2324
|
+
# 构建模型描述表
|
|
2325
|
+
model_descriptions = [
|
|
2326
|
+
"The following models are available in the model repository:"]
|
|
2327
|
+
|
|
2328
|
+
for model_name, model_data in models_data.items():
|
|
2329
|
+
# 模型数据中提取信息
|
|
2330
|
+
mdl_json = model_data.get("mdlJson", {})
|
|
2331
|
+
mdl = mdl_json.get("mdl", {})
|
|
2332
|
+
|
|
2333
|
+
description = model_data.get(
|
|
2334
|
+
"description", "No description available")
|
|
2335
|
+
author = model_data.get("author", "Unknown")
|
|
2336
|
+
tags = model_data.get("normalTags", [])
|
|
2337
|
+
states = mdl.get("states", [])
|
|
2338
|
+
|
|
2339
|
+
# 构建该模型的描述
|
|
2340
|
+
model_desc = [f"\n- Model: {model_name}"]
|
|
2341
|
+
model_desc.append(f" Description: {description}")
|
|
2342
|
+
model_desc.append(f" Author: {author}")
|
|
2343
|
+
|
|
2344
|
+
if tags:
|
|
2345
|
+
model_desc.append(f" Tags: {', '.join(tags)}")
|
|
2346
|
+
|
|
2347
|
+
# 收集所有输入输出事件
|
|
2348
|
+
all_inputs = []
|
|
2349
|
+
all_outputs = []
|
|
2350
|
+
|
|
2351
|
+
for state in states:
|
|
2352
|
+
state_events = state.get("event", [])
|
|
2353
|
+
all_inputs.extend(
|
|
2354
|
+
[e for e in state_events if e.get("eventType") == "response"])
|
|
2355
|
+
all_outputs.extend(
|
|
2356
|
+
[e for e in state_events if e.get("eventType") == "noresponse"])
|
|
2357
|
+
|
|
2358
|
+
# 描述输入需求
|
|
2359
|
+
if all_inputs:
|
|
2360
|
+
model_desc.append(" Input Requirements:")
|
|
2361
|
+
for event in all_inputs:
|
|
2362
|
+
event_name = event.get("eventName", "Unnamed input")
|
|
2363
|
+
event_desc = event.get("eventDesc", "No description")
|
|
2364
|
+
event_optional = "Optional" if event.get(
|
|
2365
|
+
"optional", False) else "Required"
|
|
2366
|
+
|
|
2367
|
+
model_desc.append(
|
|
2368
|
+
f" - {event_name} ({event_optional})")
|
|
2369
|
+
model_desc.append(f" Description: {event_desc}")
|
|
2370
|
+
|
|
2371
|
+
# 描述输出数据
|
|
2372
|
+
if all_outputs:
|
|
2373
|
+
model_desc.append(" Generated Outputs:")
|
|
2374
|
+
for event in all_outputs:
|
|
2375
|
+
event_name = event.get("eventName", "Unnamed output")
|
|
2376
|
+
event_desc = event.get("eventDesc", "No description")
|
|
2377
|
+
|
|
2378
|
+
model_desc.append(f" - {event_name}")
|
|
2379
|
+
model_desc.append(f" Description: {event_desc}")
|
|
2380
|
+
|
|
2381
|
+
# 将该模型的描述添加到总描述中
|
|
2382
|
+
model_descriptions.extend(model_desc)
|
|
2383
|
+
|
|
2384
|
+
# 添加总结性述
|
|
2385
|
+
model_descriptions.append(
|
|
2386
|
+
"\nThese models can be used for various computational tasks based on their specific purposes and requirements.")
|
|
2387
|
+
model_descriptions.append(
|
|
2388
|
+
"Each model has specific input requirements and generates corresponding outputs.")
|
|
2389
|
+
|
|
2390
|
+
# 将所有描述组合成一个字符串
|
|
2391
|
+
return "\n".join(model_descriptions)
|
|
2392
|
+
|
|
2393
|
+
except Exception as e:
|
|
2394
|
+
print(f"Error getting model context: {str(e)}")
|
|
2395
|
+
return "Failed to analyze model repository context due to an error."
|
|
2396
|
+
|
|
2397
|
+
def _get_modeling_history_context(self):
|
|
2398
|
+
"""获取建模历史上下文信息,包括代码和Markdown内容"""
|
|
2399
|
+
try:
|
|
2400
|
+
# 获取IPython shell实例
|
|
2401
|
+
get_ipython = _lazy_import_ipython()
|
|
2402
|
+
ipython = get_ipython()
|
|
2403
|
+
if ipython is None:
|
|
2404
|
+
raise RuntimeError(
|
|
2405
|
+
"This function must be run in an IPython environment")
|
|
2406
|
+
|
|
2407
|
+
# 获取当前工作目录
|
|
2408
|
+
current_dir = os.getcwd()
|
|
2409
|
+
|
|
2410
|
+
# 查找最新的ipynb文件
|
|
2411
|
+
notebook_path = None
|
|
2412
|
+
latest_time = 0
|
|
2413
|
+
for root, dirs, files in os.walk(current_dir):
|
|
2414
|
+
for file in files:
|
|
2415
|
+
if file.endswith('.ipynb') and not file.endswith('-checkpoint.ipynb'):
|
|
2416
|
+
file_path = os.path.join(root, file)
|
|
2417
|
+
mod_time = os.path.getmtime(file_path)
|
|
2418
|
+
if mod_time > latest_time:
|
|
2419
|
+
latest_time = mod_time
|
|
2420
|
+
notebook_path = file_path
|
|
2421
|
+
|
|
2422
|
+
# 记录所有内容
|
|
2423
|
+
history_desc = []
|
|
2424
|
+
|
|
2425
|
+
# 如果找到notebook文件
|
|
2426
|
+
if notebook_path:
|
|
2427
|
+
try:
|
|
2428
|
+
import nbformat
|
|
2429
|
+
notebook = nbformat.read(notebook_path, as_version=4)
|
|
2430
|
+
|
|
2431
|
+
for cell in notebook.cells:
|
|
2432
|
+
if cell.cell_type == 'code':
|
|
2433
|
+
if cell.source.strip(): # 忽略空单元格
|
|
2434
|
+
history_desc.append(
|
|
2435
|
+
f"Code Cell:\n{cell.source}")
|
|
2436
|
+
elif cell.cell_type == 'markdown':
|
|
2437
|
+
if cell.source.strip(): # 忽略空单元格
|
|
2438
|
+
history_desc.append(
|
|
2439
|
+
f"Markdown Cell:\n{cell.source}")
|
|
2440
|
+
except Exception as e:
|
|
2441
|
+
print(
|
|
2442
|
+
f"Warning: Could not read notebook content: {str(e)}")
|
|
2443
|
+
|
|
2444
|
+
# 获取命令历史
|
|
2445
|
+
code_history = list(
|
|
2446
|
+
ipython.history_manager.get_range(output=False))
|
|
2447
|
+
for session, line_number, code in code_history:
|
|
2448
|
+
if code.strip(): # 忽略空行
|
|
2449
|
+
history_desc.append(f"In [{line_number}]: {code}")
|
|
2450
|
+
|
|
2451
|
+
# 将所有描述组合成一个字符串
|
|
2452
|
+
return "\n\n".join(history_desc)
|
|
2453
|
+
|
|
2454
|
+
except Exception as e:
|
|
2455
|
+
print(f"Error getting modeling history: {str(e)}")
|
|
2456
|
+
return "Failed to analyze modeling history due to an error."
|
|
2457
|
+
|
|
2458
|
+
|
|
2459
|
+
# 向后兼容别名
|
|
2460
|
+
ModelGUI = GeoModeler
|