diff --git a/llmage/utils.py b/llmage/utils.py index 78a8cb0..61db282 100644 --- a/llmage/utils.py +++ b/llmage/utils.py @@ -141,29 +141,37 @@ async def get_llmcatelogs(): return [] -async def get_llms_by_catelog(): +async def get_llms_by_catelog(catelogid=None): env = ServerEnv() async with get_sor_context(env, 'llmage') as sor: today = curDateString() - sql = """select a.*, b.name as catelogname from llm a, llmcatelog b -where a.llmcatelogid = b.id - and enabled_date <= ${today}$ - and expired_date > ${today}$ - order by a.llmcatelogid, a.id - """ - recs = await sor.sqlExe(sql, {'today': today}) + # Join with llm_catalog_rel to support multiple catalogs per LLM + sql = """select a.*, b.name as catelogname, rel.llmcatelogid as catelog_id + from llm a + join llm_catalog_rel rel on a.id = rel.llmid + join llmcatelog b on rel.llmcatelogid = b.id + where a.enabled_date <= ${today}$ + and a.expired_date > ${today}$""" + params = {'today': today} + if catelogid: + sql += " and rel.llmcatelogid = ${catelogid}$" + params['catelogid'] = catelogid + + sql += " order by rel.llmcatelogid, a.id" + + recs = await sor.sqlExe(sql, params) d = [] cid = '' x = None for r in recs: - if cid != r.llmcatelogid: + if cid != r.catelog_id: x = { - 'catelogid': r.llmcatelogid, + 'catelogid': r.catelog_id, 'catelogname': r.catelogname, 'llms': [r] } d.append(x) - cid = r.llmcatelogid + cid = r.catelog_id else: x['llms'].append(r) return d diff --git a/models/llm.xlsx b/models/llm.xlsx index 5b849ab..68bcc0d 100644 Binary files a/models/llm.xlsx and b/models/llm.xlsx differ diff --git a/models/llm_catalog_rel.xlsx b/models/llm_catalog_rel.xlsx new file mode 100644 index 0000000..35f3884 --- /dev/null +++ b/models/llm_catalog_rel.xlsx @@ -0,0 +1,11 @@ +fields: +name|title|type|length:int|dec:int|nullable|default|comments +id|id|str|32|None|None|None|None +llmid|模型id|str|32|None|None|None|None +llmcatelogid|模型类型id|str|32|None|None|None|None + +indexes: +name|fields|unique +idx_llm|llmid|None +idx_catelog|llmcatelogid|None +uq_llm_catelog|llmid,llmcatelogid|1 \ No newline at end of file diff --git a/wwwroot/api/llm_catalog_rel_create.dspy b/wwwroot/api/llm_catalog_rel_create.dspy new file mode 100644 index 0000000..a8351a8 --- /dev/null +++ b/wwwroot/api/llm_catalog_rel_create.dspy @@ -0,0 +1,32 @@ +#!/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', '') + + if not llmid or not catelogid: + result['options'] = {'title': 'Error', 'message': '请选择模型和目录', 'type': 'error'} + else: + from appPublic.uniqueID import getID + new_id = getID() + + async with DBPools().sqlorContext(dbname) as sor: + # 检查是否已存在 + check_sql = "select id from llm_catalog_rel where llmid = ${llmid}$ and llmcatelogid = ${catelogid}$" + exists = await sor.sqlExe(check_sql, {'llmid': llmid, 'catelogid': catelogid}) + + if exists: + result['options'] = {'title': '提示', 'message': '该关联已存在', 'type': 'warning'} + else: + data = {'id': new_id, 'llmid': llmid, 'llmcatelogid': catelogid} + await sor.C('llm_catalog_rel', 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_catalog_rel_delete.dspy b/wwwroot/api/llm_catalog_rel_delete.dspy new file mode 100644 index 0000000..5a81d1d --- /dev/null +++ b/wwwroot/api/llm_catalog_rel_delete.dspy @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import json + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}} + +try: + dbname = get_module_dbname('llmage') + rel_id = params_kw.get('id', '') + + if not rel_id: + result['options'] = {'title': 'Error', 'message': '缺少ID参数', 'type': 'error'} + else: + async with DBPools().sqlorContext(dbname) as sor: + await sor.sqlExe("delete from llm_catalog_rel where id = ${id}$", {'id': rel_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_catalog_rel_list.dspy b/wwwroot/api/llm_catalog_rel_list.dspy new file mode 100644 index 0000000..30ed9d5 --- /dev/null +++ b/wwwroot/api/llm_catalog_rel_list.dspy @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +import json + +result = {'success': False, 'rows': [], 'total': 0} + +try: + dbname = get_module_dbname('llmage') + sql = """ + select r.id, r.llmid, l.name as llm_name, r.llmcatelogid, c.name as catelog_name + from llm_catalog_rel r + join llm l on r.llmid = l.id + join llmcatelog c on r.llmcatelogid = c.id + order by l.name, c.name + """ + + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe(sql, {}) + 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_catelog_options.dspy b/wwwroot/api/llm_catelog_options.dspy new file mode 100644 index 0000000..7703a7c --- /dev/null +++ b/wwwroot/api/llm_catelog_options.dspy @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import json + +result = {'success': False, 'data': {'llms': [], 'catelogs': []}} + +try: + dbname = get_module_dbname('llmage') + user_orgid = await get_userorgid() + + async with DBPools().sqlorContext(dbname) as sor: + # Get all active LLMs belonging to the 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 [])] + + # Get all catalogs (assuming catalogs are global or filtered similarly if needed) + 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 [])] + + result['success'] = True + +except Exception as e: + result['error'] = str(e) + +return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/get_type_llms.dspy b/wwwroot/get_type_llms.dspy index b92c861..c368fc2 100644 --- a/wwwroot/get_type_llms.dspy +++ b/wwwroot/get_type_llms.dspy @@ -3,16 +3,16 @@ lt = '文生视频' if params_kw.type in ['文生视频', '参考生视频', '图生视频']: lt = params_kw.type async with get_sor_context(request._run_ns, 'llmage') as sor: - sql = '''select a.*, e.input_fields from llm a, llmcatelog b, upapp c, uapi d, uapiio e -where a.llmcatelogid = b.id - and a.upappid = c.id - and c.apisetid = d.apisetid - and a.apiname = d.name - and d.ioid = e.id + sql = '''select a.*, e.input_fields from llm a +join llm_catalog_rel rel on a.id = rel.llmid +join llmcatelog b on rel.llmcatelogid = b.id +join upapp c on a.upappid = c.id +join uapi d on c.apisetid = d.apisetid and a.apiname = d.name +join uapiio e on d.ioid = e.id +where b.name=${lt}$ and a.enabled_date <= ${biz_date}$ and ${biz_date}$ < a.expired_date - and ppid is not NULL - and b.name=${lt}$''' + and ppid is not NULL''' biz_date = await get_business_date(sor) recs = await sor.sqlExe(sql, { 'biz_date': biz_date, diff --git a/wwwroot/list_catelog_models.dspy b/wwwroot/list_catelog_models.dspy index e07f5f4..9365041 100644 --- a/wwwroot/list_catelog_models.dspy +++ b/wwwroot/list_catelog_models.dspy @@ -1,7 +1,9 @@ dbname = get_module_dbname('llmage') db = DBPools() async with db.sqlorContext(dbname) as sor: - sql = """select * from llm where llmcatelogid = ${llmcatelogid}$ and id != ${llmid}$""" + sql = """select * from llm a +join llm_catalog_rel rel on a.id = rel.llmid +where rel.llmcatelogid = ${llmcatelogid}$ and a.id != ${llmid}$""" ns = params_kw.copy() recs = await sor.sqlExe(sql, ns) for r in recs.get('rows', []): diff --git a/wwwroot/list_paging_catelog_llms.dspy b/wwwroot/list_paging_catelog_llms.dspy index 6298cbc..8c3f106 100644 --- a/wwwroot/list_paging_catelog_llms.dspy +++ b/wwwroot/list_paging_catelog_llms.dspy @@ -13,15 +13,15 @@ y.user_message, y.assisant_message from ( select a.*, b.hfid, e.ioid, e.stream -from llm a, llmcatelog b,upapp c, uapiset d, uapi e -where a.llmcatelogid = b.id - and a.upappid = c.id - and c.apisetid = d.id - and e.apisetid = d.id - and a.apiname = e.name +from llm a +join llm_catalog_rel rel on a.id = rel.llmid +join llmcatelog b on rel.llmcatelogid = b.id +join upapp c on a.upappid = c.id +join uapiset d on c.apisetid = d.id +join uapi e on e.apisetid = d.id and a.apiname = e.name ) x left join historyformat y on x.hfid = y.id left join uapiio z on x.ioid = z.id -where llmcatelogid = ${llmcatelogid}$ +where rel.llmcatelogid = ${llmcatelogid}$ and x.id != ${llmid}$ """ ns = params_kw.copy() diff --git a/wwwroot/llm_catalog_rel_manage.ui b/wwwroot/llm_catalog_rel_manage.ui new file mode 100644 index 0000000..bdbd36e --- /dev/null +++ b/wwwroot/llm_catalog_rel_manage.ui @@ -0,0 +1,145 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "spacing": 16 + }, + "subwidgets": [ + { + "widgettype": "Title2", + "options": { + "text": "LLM 目录关联管理", + "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_catelog_options.dspy')}}", + "data_field": "llms", + "placeholder": "请选择模型" + }, + { + "name": "llmcatelogid", + "label": "选择目录", + "uitype": "select", + "dataurl": "{{entire_url('./api/llm_catelog_options.dspy')}}", + "data_field": "catelogs", + "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_catalog_rel_create.dspy')}}", + "params": { + "llmid": "$[add_form.llmid]$", + "llmcatelogid": "$[add_form.llmcatelogid]$" + } + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "options": { + "width": "calc(100% - 40px)", + "margin": "0 20px", + "spacing": 12 + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "当前关联列表", + "fontSize": "16px", + "fontWeight": "bold" + } + }, + { + "widgettype": "Tabular", + "id": "rel_table", + "options": { + "width": "100%", + "height": "400px", + "data_url": "{{entire_url('./api/llm_catalog_rel_list.dspy')}}", + "data_method": "GET", + "page_rows": 20, + "row_options": { + "fields": [ + {"name": "llm_name", "title": "模型名称", "width": 200}, + {"name": "catelog_name", "title": "目录名称", "width": 150}, + { + "name": "actions", + "title": "操作", + "width": 100, + "uitype": "button", + "data": [ + {"text": "删除", "event": "delete_rel"} + ] + } + ] + } + }, + "binds": [ + { + "wid": "self", + "event": "delete_rel", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": {"archor": "cc", "width": "30%", "height": "20%"}, + "options": { + "url": "{{entire_url('./api/llm_catalog_rel_delete.dspy')}}", + "params": { + "id": "$[event.params.id]$" + } + } + } + ] + } + ] + } + ] +} diff --git a/wwwroot/llm_dialog.ui b/wwwroot/llm_dialog.ui index c2efdcd..fc9e007 100644 --- a/wwwroot/llm_dialog.ui +++ b/wwwroot/llm_dialog.ui @@ -22,7 +22,6 @@ "models":[ { "llmid":"{{llm.id}}", - "llmcatelogid":"{{llm.llmcatelogid}}", "response_mode": "{{llm.stream}}", "icon":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}", "url":"{{entire_url('/llmage/llminference.dspy')}}", diff --git a/wwwroot/t2t/index.dspy b/wwwroot/t2t/index.dspy index 428d7e0..8adc2fd 100644 --- a/wwwroot/t2t/index.dspy +++ b/wwwroot/t2t/index.dspy @@ -17,10 +17,11 @@ if not params_kw.prompt: return json_response(d, status=400) env = request._run_ns async with get_sor_context(env, 'llmage') as sor: - sql = """select a.* from llm a, llmcatelog b -where a.llmcatelogid=b.id - and a.model=${model}$ - and b.name = ${lctype}$""" + sql = """select a.* from llm a +join llm_catalog_rel rel on a.id = rel.llmid +join llmcatelog b on rel.llmcatelogid = b.id +where b.name = ${lctype}$ + and a.model=${model}$""" recs = await sor.sqlExe(sql, { 'lctype': lctype, 'model': params_kw.model or 'qwen3-max' diff --git a/wwwroot/v1/chat/completions/index.dspy b/wwwroot/v1/chat/completions/index.dspy index e806caa..d247ca0 100644 --- a/wwwroot/v1/chat/completions/index.dspy +++ b/wwwroot/v1/chat/completions/index.dspy @@ -30,10 +30,11 @@ if not params_kw.prompt and not params_kw.messages: return json_response(d, status=400) env = request._run_ns async with get_sor_context(env, 'llmage') as sor: - sql = """select a.* from llm a, llmcatelog b -where a.llmcatelogid=b.id - and a.model=${model}$ - and b.name = ${lctype}$""" + sql = """select a.* from llm a +join llm_catalog_rel rel on a.id = rel.llmid +join llmcatelog b on rel.llmcatelogid = b.id +where b.name = ${lctype}$ + and a.model=${model}$""" recs = await sor.sqlExe(sql, { 'lctype': lctype, 'model': params_kw.model or 'qwen3-max'