diff --git a/json/llm.json b/json/llm.json index 118dfc2..ead126d 100644 --- a/json/llm.json +++ b/json/llm.json @@ -48,8 +48,8 @@ "subtables":[ { "field":"llmid", - "title":"模型类型", - "subtable":"llm_catelog_rel" + "title":"能力映射", + "subtable":"llm_api_map" } ] } diff --git a/json/llm_api_map.json b/json/llm_api_map.json index 12a24d9..e246f95 100644 --- a/json/llm_api_map.json +++ b/json/llm_api_map.json @@ -1,17 +1,27 @@ { - "summary": [{"name": "llm_api_map", "title": "模型能力映射表", "primary": ["id"], "catelog": "relation"}], - "fields": [ - {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"}, - {"name": "llmid", "title": "模型ID", "type": "str", "length": 32, "nullable": "no"}, - {"name": "llmcatelogid", "title": "类目ID", "type": "str", "length": 32, "nullable": "no"}, - {"name": "apiname", "title": "API接口名", "type": "str", "length": 100, "nullable": "no"}, - {"name": "query_apiname", "title": "结果查询API名", "type": "str", "length": 100}, - {"name": "query_period", "title": "查询间隔(秒)", "type": "int"}, - {"name": "ppid", "title": "定价项目ID", "type": "str", "length": 32} - ], - "indexes": [ - {"name": "idx_llm_api_llm", "idxtype": "index", "idxfields": ["llmid"]}, - {"name": "idx_llm_api_catelog", "idxtype": "unique", "idxfields": ["llmid", "llmcatelogid"]} - ], - "codes": [] + "tblname": "llm_api_map", + "title": "模型能力映射", + "params": { + "browserfields": { + "exclouded": ["id", "llmid"], + "alters": { + "llmcatelogid": { + "dataurl": "{{entire_url('../api/llm_catelog_options.dspy')}}", + "textField": "name", + "valueField": "id" + }, + "apiname": { + "dataurl": "{{entire_url('../api/uapi_options.dspy')}}", + "textField": "name", + "valueField": "name" + }, + "ppid": { + "dataurl": "{{entire_url('/pricing/get_all_pricing_programs.dspy')}}", + "textField": "name", + "valueField": "id" + } + } + }, + "editexclouded": ["id", "llmid"] + } } diff --git a/models/llm_api_map.json b/models/llm_api_map.json new file mode 100644 index 0000000..23e13d8 --- /dev/null +++ b/models/llm_api_map.json @@ -0,0 +1,99 @@ +{ + "table_name": "llm_api_map", + "summary": [ + { + "name": "llm_api_map", + "title": "模型API映射表", + "primary": "id", + "catelog": "relation" + } + ], + "fields": [ + { + "name": "id", + "type": "varchar(32)", + "not_null": true, + "comment": "主键ID" + }, + { + "name": "llmid", + "type": "varchar(32)", + "not_null": true, + "comment": "模型ID,关联llm表" + }, + { + "name": "llmcatelogid", + "type": "varchar(32)", + "not_null": true, + "comment": "模型分类ID,关联llmcatelog表" + }, + { + "name": "apiname", + "type": "varchar(100)", + "not_null": true, + "comment": "推理接口名称,关联uapi表name字段" + }, + { + "name": "query_apiname", + "type": "varchar(100)", + "comment": "异步任务结果查询接口名称,可逗号分隔多个" + }, + { + "name": "query_period", + "type": "bigint", + "default": 30, + "comment": "异步任务查询轮询间隔(秒),默认30" + }, + { + "name": "ppid", + "type": "varchar(32)", + "comment": "计费程序ID,关联pricing_program表" + } + ], + "indexes": [ + { + "name": "idx_api_map_llmid", + "type": "normal", + "idxfields": ["llmid"], + "idxtype": "index" + }, + { + "name": "idx_api_map_catelog", + "type": "normal", + "idxfields": ["llmcatelogid"], + "idxtype": "index" + }, + { + "name": "idx_api_map_apiname", + "type": "normal", + "idxfields": ["apiname"], + "idxtype": "index" + }, + { + "name": "uk_llmid_apiname", + "type": "unique", + "idxfields": ["llmid", "apiname"], + "idxtype": "unique" + } + ], + "codes": [ + { + "field": "llmid", + "table": "llm", + "valuefield": "id", + "textfield": "name" + }, + { + "field": "llmcatelogid", + "table": "llmcatelog", + "valuefield": "id", + "textfield": "name" + }, + { + "field": "ppid", + "table": "pricing_program", + "valuefield": "id", + "textfield": "name" + } + ] +} diff --git a/wwwroot/api/llm_api_map_create.dspy b/wwwroot/api/llm_api_map_create.dspy new file mode 100644 index 0000000..46a5739 --- /dev/null +++ b/wwwroot/api/llm_api_map_create.dspy @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import json + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}} + +try: + dbname = get_module_dbname('llmage') + llmid = params_kw.get('llmid', '') + catelogid = params_kw.get('llmcatelogid', '') + apiname = params_kw.get('apiname', '') + + if not llmid or not catelogid or not apiname: + result['options'] = {'title': 'Error', 'message': '模型、分类和API接口为必填项', 'type': 'error'} + else: + user_orgid = await get_userorgid() + from appPublic.uniqueID import getID + new_id = getID() + + async with DBPools().sqlorContext(dbname) as sor: + # 验证模型属于当前用户的机构 + check_llm = await sor.sqlExe( + "select id from llm where id = ${llmid}$ and ownerid = ${ownerid}$", + {'llmid': llmid, 'ownerid': user_orgid} + ) + if not check_llm: + result['options'] = {'title': 'Error', 'message': '无权操作该模型', 'type': 'error'} + else: + # 检查是否已存在 + check_sql = """select id from llm_api_map + where llmid = ${llmid}$ and apiname = ${apiname}$""" + exists = await sor.sqlExe(check_sql, {'llmid': llmid, 'apiname': apiname}) + + if exists: + result['options'] = {'title': '提示', 'message': '该模型的此API映射已存在', 'type': 'warning'} + else: + data = { + 'id': new_id, + 'llmid': llmid, + 'llmcatelogid': catelogid, + 'apiname': apiname + } + query_apiname = params_kw.get('query_apiname', '').strip() + if query_apiname: + data['query_apiname'] = query_apiname + query_period = params_kw.get('query_period', '').strip() + if query_period: + try: + data['query_period'] = int(query_period) + except ValueError: + data['query_period'] = 30 + else: + data['query_period'] = 30 + ppid = params_kw.get('ppid', '').strip() + if ppid: + data['ppid'] = ppid + + await sor.C('llm_api_map', data) + result['options'] = {'title': 'Success', 'message': '添加成功', 'type': 'success'} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'添加失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/llm_api_map_delete.dspy b/wwwroot/api/llm_api_map_delete.dspy new file mode 100644 index 0000000..66b8ace --- /dev/null +++ b/wwwroot/api/llm_api_map_delete.dspy @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import json + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}} + +try: + dbname = get_module_dbname('llmage') + map_id = params_kw.get('id', '') + + if not map_id: + result['options'] = {'title': 'Error', 'message': '缺少ID参数', 'type': 'error'} + else: + user_orgid = await get_userorgid() + async with DBPools().sqlorContext(dbname) as sor: + # 验证映射记录对应的模型属于当前用户的机构 + check = await sor.sqlExe( + """select m.id from llm_api_map m + join llm l on m.llmid = l.id + where m.id = ${id}$ and l.ownerid = ${ownerid}$""", + {'id': map_id, 'ownerid': user_orgid} + ) + if not check: + result['options'] = {'title': 'Error', 'message': '无权删除该映射', 'type': 'error'} + else: + await sor.sqlExe("delete from llm_api_map where id = ${id}$", {'id': map_id}) + result['options'] = {'title': 'Success', 'message': '删除成功', 'type': 'success'} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/llm_api_map_list.dspy b/wwwroot/api/llm_api_map_list.dspy new file mode 100644 index 0000000..e43129e --- /dev/null +++ b/wwwroot/api/llm_api_map_list.dspy @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +import json + +result = {'success': False, 'rows': [], 'total': 0} + +try: + dbname = get_module_dbname('llmage') + user_orgid = await get_userorgid() + + sql = """ + select m.id, m.llmid, l.name as llm_name, + m.llmcatelogid, c.name as catelog_name, + m.apiname, m.query_apiname, m.query_period, + m.ppid, p.name as ppid_name + from llm_api_map m + join llm l on m.llmid = l.id + join llmcatelog c on m.llmcatelogid = c.id + left join pricing_program p on m.ppid = p.id + where l.ownerid = ${ownerid}$ + order by l.name, c.name, m.apiname + """ + + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe(sql, {'ownerid': user_orgid}) + result['rows'] = [dict(r) for r in (rows or [])] + result['total'] = len(result['rows']) + result['success'] = True + +except Exception as e: + result['error'] = str(e) + +return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/api/llm_api_map_options.dspy b/wwwroot/api/llm_api_map_options.dspy new file mode 100644 index 0000000..f5b3209 --- /dev/null +++ b/wwwroot/api/llm_api_map_options.dspy @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import json + +result = {'success': False, 'data': {'llms': [], 'catelogs': [], 'apis': [], 'ppids': []}} + +try: + dbname = get_module_dbname('llmage') + user_orgid = await get_userorgid() + + async with DBPools().sqlorContext(dbname) as sor: + # Active LLMs for user's organization + today = await get_business_date(sor) + llms_sql = """select id, name from llm + where enabled_date <= ${today}$ + and expired_date > ${today}$ + and ownerid = ${ownerid}$ + order by name""" + llms = await sor.sqlExe(llms_sql, {'today': today, 'ownerid': user_orgid}) + result['data']['llms'] = [{'id': r['id'], 'text': r['name']} for r in (llms or [])] + + # All catalogs + catelogs = await sor.sqlExe("select id, name from llmcatelog order by name", {}) + result['data']['catelogs'] = [{'id': r['id'], 'text': r['name']} for r in (catelogs or [])] + + # uapi list (for apiname selection) + apis = await sor.sqlExe("select name, path from uapi order by name", {}) + result['data']['apis'] = [{'id': r['name'], 'text': f"{r['name']} ({r['path']})"} for r in (apis or [])] + + # Pricing programs + ppids = await sor.sqlExe("select id, name from pricing_program order by name", {}) + result['data']['ppids'] = [{'id': r['id'], 'text': r['name']} for r in (ppids or [])] + + result['success'] = True + +except Exception as e: + result['error'] = str(e) + +return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/api/uapi_options.dspy b/wwwroot/api/uapi_options.dspy new file mode 100644 index 0000000..04cbb66 --- /dev/null +++ b/wwwroot/api/uapi_options.dspy @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import json + +result = {'success': False, 'data': []} + +try: + dbname = get_module_dbname('llmage') + + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe("select name, path from uapi order by name", {}) + result['data'] = [{'id': r['name'], 'text': f"{r['name']} ({r['path']})"} for r in (rows or [])] + result['success'] = True + +except Exception as e: + result['error'] = str(e) + +return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/llm_api_map_manage.ui b/wwwroot/llm_api_map_manage.ui new file mode 100644 index 0000000..e111f7b --- /dev/null +++ b/wwwroot/llm_api_map_manage.ui @@ -0,0 +1,181 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "spacing": 16 + }, + "subwidgets": [ + { + "widgettype": "Title2", + "options": { + "text": "模型能力映射管理", + "halign": "left" + } + }, + { + "widgettype": "VBox", + "options": { + "width": "calc(100% - 40px)", + "margin": "0 20px", + "padding": "16px", + "bgcolor": "#f5f5f5", + "spacing": 12 + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "添加新映射", + "fontSize": "16px", + "fontWeight": "bold" + } + }, + { + "widgettype": "Form", + "id": "add_form", + "options": { + "layout": "horizontal", + "cols": 3, + "fields": [ + { + "name": "llmid", + "label": "选择模型", + "uitype": "select", + "dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}", + "data_field": "llms", + "placeholder": "请选择模型" + }, + { + "name": "llmcatelogid", + "label": "选择分类", + "uitype": "select", + "dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}", + "data_field": "catelogs", + "placeholder": "请选择分类" + }, + { + "name": "apiname", + "label": "API接口", + "uitype": "select", + "dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}", + "data_field": "apis", + "placeholder": "请选择API接口" + }, + { + "name": "query_apiname", + "label": "查询API", + "uitype": "text", + "placeholder": "异步查询API名,多个用逗号分隔" + }, + { + "name": "query_period", + "label": "查询间隔(秒)", + "uitype": "number", + "placeholder": "默认30" + }, + { + "name": "ppid", + "label": "计费项目", + "uitype": "select", + "dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}", + "data_field": "ppids", + "placeholder": "请选择计费项目" + } + ], + "buttons": [ + { + "name": "add_btn", + "label": "添加映射", + "variant": "primary" + } + ] + }, + "binds": [ + { + "wid": "add_btn", + "event": "click", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": {"archor": "cc", "width": "30%", "height": "20%"}, + "options": { + "url": "{{entire_url('./api/llm_api_map_create.dspy')}}", + "params": { + "llmid": "$[add_form.llmid]$", + "llmcatelogid": "$[add_form.llmcatelogid]$", + "apiname": "$[add_form.apiname]$", + "query_apiname": "$[add_form.query_apiname]$", + "query_period": "$[add_form.query_period]$", + "ppid": "$[add_form.ppid]$" + } + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "options": { + "width": "calc(100% - 40px)", + "margin": "0 20px", + "spacing": 12 + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "当前映射列表", + "fontSize": "16px", + "fontWeight": "bold" + } + }, + { + "widgettype": "Tabular", + "id": "map_table", + "options": { + "width": "100%", + "height": "400px", + "data_url": "{{entire_url('./api/llm_api_map_list.dspy')}}", + "data_method": "GET", + "page_rows": 20, + "row_options": { + "fields": [ + {"name": "llm_name", "title": "模型名称", "width": 180}, + {"name": "catelog_name", "title": "分类", "width": 120}, + {"name": "apiname", "title": "API接口", "width": 150}, + {"name": "query_apiname", "title": "查询API", "width": 180}, + {"name": "query_period", "title": "间隔(秒)", "width": 80}, + {"name": "ppid_name", "title": "计费项目", "width": 150}, + { + "name": "actions", + "title": "操作", + "width": 100, + "uitype": "button", + "data": [ + {"text": "删除", "event": "delete_map"} + ] + } + ] + } + }, + "binds": [ + { + "wid": "self", + "event": "delete_map", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": {"archor": "cc", "width": "30%", "height": "20%"}, + "options": { + "url": "{{entire_url('./api/llm_api_map_delete.dspy')}}", + "params": { + "id": "$[event.params.id]$" + } + } + } + ] + } + ] + } + ] +}