From 3a0a8d4c86dd5683fa22216f1b9f5b6bd82895c6 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 4 Jun 2026 18:11:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=B8=8A=E7=BA=BF=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 llm_launch_check_api.dspy:执行完整的上线前检查 * 检查模型记录、日期、状态 * 检查上位系统(upapp)关联 * 检查API映射(uapi) * 检查IO定义(uapiio) * 检查能力映射(llm_api_map) * 检查定价项目(pricing_program) * 检查定价数据(pricingdata) * 支持体验测试(action=inference) - 新增 llm_launch_check.ui:检查结果展示界面 - 修改 llm.json:将'体验'按钮改为'上线检查' - 更新 load_path.py:注册新路径 --- json/llm.json | 14 +-- scripts/load_path.py | 2 + wwwroot/api/llm_launch_check_api.dspy | 158 ++++++++++++++++++++++++++ wwwroot/llm_launch_check.ui | 61 ++++++++++ 4 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 wwwroot/api/llm_launch_check_api.dspy create mode 100644 wwwroot/llm_launch_check.ui diff --git a/json/llm.json b/json/llm.json index 1f38025..732b444 100644 --- a/json/llm.json +++ b/json/llm.json @@ -49,8 +49,8 @@ "toolbar": { "tools":[ { - "name":"test", - "label":"体验", + "name":"launch_check", + "label":"上线检查", "selected_row":true }, { @@ -68,16 +68,16 @@ "binds":[ { "wid":"self", - "event":"test", + "event":"launch_check", "actiontype":"urlwidget", "target":"PopupWindow", "popup_options":{ - "title":"model Test", - "cwidth":22, - "height":"75%" + "title":"上线检查", + "cwidth":25, + "cheight":20 }, "options":{ - "url":"{{entire_url('./llm_dialog.ui')}}", + "url":"{{entire_url('./llm_launch_check.ui')}}", "params":{ "id":"${id}" } diff --git a/scripts/load_path.py b/scripts/load_path.py index 5a53b0a..ac829d3 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -56,6 +56,7 @@ PATHS_LOGINED = [ f"/{MOD}/api_doc.ui", f"/{MOD}/api_doc.md", f"/{MOD}/llm_dialog.ui", + f"/{MOD}/llm_launch_check.ui", f"/{MOD}/show_same_catelog_llm.ui", f"/{MOD}/show_llms.ui", f"/{MOD}/show_llms_by_providers.ui", @@ -91,6 +92,7 @@ PATHS_LOGINED = [ f"/{MOD}/api/get_search_providerid.dspy", f"/{MOD}/api/get_search_upappid.dspy", f"/{MOD}/api/get_upapps.dspy", + f"/{MOD}/api/llm_launch_check_api.dspy", f"/{MOD}/api/llm_api_map_create.dspy", f"/{MOD}/api/llm_api_map_delete.dspy", f"/{MOD}/api/llm_api_map_list.dspy", diff --git a/wwwroot/api/llm_launch_check_api.dspy b/wwwroot/api/llm_launch_check_api.dspy new file mode 100644 index 0000000..e30e61c --- /dev/null +++ b/wwwroot/api/llm_launch_check_api.dspy @@ -0,0 +1,158 @@ +import json +from sqlor.dbpools import get_sor_context +from ahserver.serverenv import ServerEnv + +llmid = params_kw.get('llmid', '') +action = params_kw.get('action', 'check') + +if not llmid: + return json.dumps({'error': 'missing llmid'}, ensure_ascii=False) + +if action == 'check': + # Return check results + from datetime import date + today = date.today().isoformat() + env = request._run_ns + result = {'llmid': llmid, 'checks': [], 'all_passed': True} + + async def add_check(name, passed, detail=''): + result['checks'].append({'name': name, 'passed': passed, 'detail': detail}) + if not passed: + result['all_passed'] = False + + async with get_sor_context(env, 'llmage') as sor: + # 1. llm record + recs = await sor.sqlExe( + "select * from llm where id=${llmid}$", {'llmid': llmid}) + if not recs: + await add_check('模型记录', False, f'llm id={llmid} 不存在') + return json.dumps(result, ensure_ascii=False) + llm = recs[0] + await add_check('模型记录', True, f'{llm.name} ({llm.model})') + + # 2. dates & status + date_ok = llm.enabled_date and llm.expired_date + status_ok = llm.status == 'published' + await add_check('日期与状态', + date_ok and status_ok, + f"启用:{llm.enabled_date} 失效:{llm.expired_date} 状态:{llm.status}") + + # 3. upapp + recs = await sor.sqlExe( + "select a.* from llm a, upapp b where a.id=${llmid}$ and a.upappid=b.id", + {'llmid': llmid}) + if recs: + await add_check('上位系统(upapp)', True, f'upappid={llm.upappid}') + else: + await add_check('上位系统(upapp)', False, f'upappid={llm.upappid} 未找到关联') + + # 4. uapi + recs = await sor.sqlExe(""" + select a.*, e.ioid, e.stream + from llm a + join upapp c on a.upappid = c.id + join uapi e on c.apisetid = e.apisetid and a.apiname = e.name + where a.id=${llmid}$""", {'llmid': llmid}) + if recs: + await add_check('API映射(uapi)', True, f'ioid={recs[0].ioid}, stream={recs[0].stream}') + else: + await add_check('API映射(uapi)', False, f'apiname={getattr(llm, "apiname", "N/A")} 在upapp中未找到') + + # 5. uapiio + if recs: + ioid = recs[0].ioid + recs2 = await sor.sqlExe( + "select * from uapiio where id=${ioid}$", {'ioid': ioid}) + if recs2: + await add_check('IO定义(uapiio)', True, f'uapiio id={ioid}') + else: + await add_check('IO定义(uapiio)', False, f'ioid={ioid} 未找到') + else: + await add_check('IO定义(uapiio)', False, '依赖 uapi 未通过') + + # 6. llm_api_map + maps = await sor.sqlExe( + "select * from llm_api_map where llmid=${llmid}$", {'llmid': llmid}) + if maps: + ppids = [m.ppid for m in maps if m.ppid] + await add_check('能力映射(llm_api_map)', True, + f'{len(maps)}条记录, {len(ppids)}个有定价') + else: + await add_check('能力映射(llm_api_map)', False, '无映射记录') + ppids = [] + + # 7. pricing_program + if ppids: + ppid = ppids[0] + async with get_sor_context(env, 'pricing') as psor: + pregs = await psor.sqlExe( + "select * from pricing_program where id=${ppid}$", {'ppid': ppid}) + if pregs: + await add_check('定价项目(pricing_program)', True, + f'{pregs[0].name} (id={ppid})') + # 8. pricingdata + datas = await psor.sqlExe( + "select count(*) as cnt from pricingdata where ppid=${ppid}$", + {'ppid': ppid}) + cnt = datas[0].cnt if datas else 0 + await add_check('定价数据(pricingdata)', cnt > 0, + f'{cnt}条记录' if cnt > 0 else '无定价数据') + else: + await add_check('定价项目(pricing_program)', False, + f'ppid={ppid} 未找到') + await add_check('定价数据(pricingdata)', False, '依赖定价项目未通过') + else: + await add_check('定价项目(pricing_program)', False, 'llm_api_map中无ppid') + await add_check('定价数据(pricingdata)', False, '无定价项目') + + return json.dumps(result, ensure_ascii=False) + +elif action == 'inference': + # Perform test inference + # Get llm info + async with get_sor_context(request._run_ns, 'llmage') as sor: + recs = await sor.sqlExe( + "select * from llm where id=${llmid}$", {'llmid': llmid}) + if not recs: + return json.dumps({'error': 'llm not found'}, ensure_ascii=False) + llm = recs[0] + + # Get API mapping + maps = await sor.sqlExe( + "select * from llm_api_map where llmid=${llmid}$ and isdefaultcatelog='1'", + {'llmid': llmid}) + if not maps: + return json.dumps({'error': 'no default api map'}, ensure_ascii=False) + + api_map = maps[0] + + # Call llminference logic + try: + from llmage.llmclient import llm_inference + userid = await get_user() + userorgid = await get_userorgid() + + # Simple test message + test_input = {'messages': [{'role': 'user', 'content': '你好,这是一条测试消息'}]} + + result = await llm_inference( + llmid=llmid, + apiname=api_map.apiname, + user_input=test_input, + userid=userid, + userorgid=userorgid, + stream=False + ) + + return json.dumps({ + 'success': True, + 'response': result.get('response', ''), + 'usage': result.get('usage', {}) + }, ensure_ascii=False) + except Exception as e: + return json.dumps({ + 'success': False, + 'error': str(e) + }, ensure_ascii=False) + +return json.dumps({'error': 'invalid action'}, ensure_ascii=False) diff --git a/wwwroot/llm_launch_check.ui b/wwwroot/llm_launch_check.ui new file mode 100644 index 0000000..ed08708 --- /dev/null +++ b/wwwroot/llm_launch_check.ui @@ -0,0 +1,61 @@ +{% if params_kw.id %} +{% set llmid = params_kw.id %} +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "spacing": 10, + "children": [ + { + "widgettype": "Title", + "options": { + "text": "模型上线检查", + "level": 2 + } + }, + { + "widgettype": "Text", + "options": { + "name": "check_status", + "text": "检查中...", + "i18n": false + } + }, + { + "widgettype": "VBox", + "options": { + "name": "checks_list", + "spacing": 5 + } + }, + { + "widgettype": "Button", + "options": { + "name": "test_btn", + "text": "体验一次", + "i18n": false, + "disabled": true + } + }, + { + "widgettype": "Text", + "options": { + "name": "test_result", + "text": "", + "i18n": false + } + } + ], + "init": "async function(self) {\n const llmid = '{{llmid}}';\n const statusEl = self.children.check_status;\n const listEl = self.children.checks_list;\n const testBtn = self.children.test_btn;\n const testResult = self.children.test_result;\n \n async function runCheck() {\n statusEl.setText('检查中...');\n listEl.clear();\n testBtn.setDisabled(true);\n \n try {\n const url = bricks.absurl('../api/llm_launch_check_api.dspy?llmid=' + llmid, self);\n const resp = await fetch(url);\n const data = await resp.json();\n \n if (data.error) {\n statusEl.setText('错误: ' + data.error);\n return;\n }\n \n statusEl.setText(data.all_passed ? '✓ 所有检查通过' : '✗ 存在未通过项');\n statusEl.setStyle({color: data.all_passed ? '#4CAF50' : '#F44336', fontSize: '16px', fontWeight: 'bold'});\n \n data.checks.forEach(check => {\n const row = bricks.createWidget('HBox', {\n spacing: 10,\n children: [\n {widgettype: 'Text', options: {text: check.passed ? '✓' : '✗', style: {color: check.passed ? '#4CAF50' : '#F44336', fontSize: '18px', width: '30px'}}},\n {widgettype: 'Text', options: {text: check.name, style: {fontWeight: 'bold', width: '200px'}}},\n {widgettype: 'Text', options: {text: check.detail, style: {color: '#666'}}}\n ]\n });\n listEl.addChild(row);\n });\n \n if (data.all_passed) {\n testBtn.setDisabled(false);\n }\n } catch (e) {\n statusEl.setText('请求失败: ' + e.message);\n }\n }\n \n testBtn.on('click', async () => {\n testResult.setText('体验中...');\n try {\n const url = bricks.absurl('../api/llm_launch_check_api.dspy?llmid=' + llmid + '&action=inference', self);\n const resp = await fetch(url);\n const data = await resp.json();\n \n if (data.success) {\n testResult.setText('响应: ' + (data.response || JSON.stringify(data.usage)));\n testResult.setStyle({color: '#4CAF50'});\n } else {\n testResult.setText('失败: ' + data.error);\n testResult.setStyle({color: '#F44336'});\n }\n } catch (e) {\n testResult.setText('请求失败: ' + e.message);\n testResult.setStyle({color: '#F44336'});\n }\n });\n \n await runCheck();\n}" + } +} +{% else %} +{ + "widgettype": "Text", + "options": { + "text": "缺少模型ID参数", + "i18n": true + } +} +{% endif %}