feat: support multi-catalog for LLMs
- Create llm_catalog_rel model for one-to-many relationship - Remove llmcatelogid from llm model - Update SQL queries in utils.py and dspy files to use join - Add maintenance UI (llm_catalog_rel_manage.ui) and API endpoints - Filter options by user's orgid
This commit is contained in:
parent
6a33c5e9aa
commit
6cc3986a2d
@ -141,29 +141,37 @@ async def get_llmcatelogs():
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def get_llms_by_catelog():
|
async def get_llms_by_catelog(catelogid=None):
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
async with get_sor_context(env, 'llmage') as sor:
|
async with get_sor_context(env, 'llmage') as sor:
|
||||||
today = curDateString()
|
today = curDateString()
|
||||||
sql = """select a.*, b.name as catelogname from llm a, llmcatelog b
|
# Join with llm_catalog_rel to support multiple catalogs per LLM
|
||||||
where a.llmcatelogid = b.id
|
sql = """select a.*, b.name as catelogname, rel.llmcatelogid as catelog_id
|
||||||
and enabled_date <= ${today}$
|
from llm a
|
||||||
and expired_date > ${today}$
|
join llm_catalog_rel rel on a.id = rel.llmid
|
||||||
order by a.llmcatelogid, a.id
|
join llmcatelog b on rel.llmcatelogid = b.id
|
||||||
"""
|
where a.enabled_date <= ${today}$
|
||||||
recs = await sor.sqlExe(sql, {'today': 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 = []
|
d = []
|
||||||
cid = ''
|
cid = ''
|
||||||
x = None
|
x = None
|
||||||
for r in recs:
|
for r in recs:
|
||||||
if cid != r.llmcatelogid:
|
if cid != r.catelog_id:
|
||||||
x = {
|
x = {
|
||||||
'catelogid': r.llmcatelogid,
|
'catelogid': r.catelog_id,
|
||||||
'catelogname': r.catelogname,
|
'catelogname': r.catelogname,
|
||||||
'llms': [r]
|
'llms': [r]
|
||||||
}
|
}
|
||||||
d.append(x)
|
d.append(x)
|
||||||
cid = r.llmcatelogid
|
cid = r.catelog_id
|
||||||
else:
|
else:
|
||||||
x['llms'].append(r)
|
x['llms'].append(r)
|
||||||
return d
|
return d
|
||||||
|
|||||||
BIN
models/llm.xlsx
BIN
models/llm.xlsx
Binary file not shown.
11
models/llm_catalog_rel.xlsx
Normal file
11
models/llm_catalog_rel.xlsx
Normal file
@ -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
|
||||||
32
wwwroot/api/llm_catalog_rel_create.dspy
Normal file
32
wwwroot/api/llm_catalog_rel_create.dspy
Normal file
@ -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)
|
||||||
20
wwwroot/api/llm_catalog_rel_delete.dspy
Normal file
20
wwwroot/api/llm_catalog_rel_delete.dspy
Normal file
@ -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)
|
||||||
25
wwwroot/api/llm_catalog_rel_list.dspy
Normal file
25
wwwroot/api/llm_catalog_rel_list.dspy
Normal file
@ -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)
|
||||||
30
wwwroot/api/llm_catelog_options.dspy
Normal file
30
wwwroot/api/llm_catelog_options.dspy
Normal file
@ -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)
|
||||||
@ -3,16 +3,16 @@ lt = '文生视频'
|
|||||||
if params_kw.type in ['文生视频', '参考生视频', '图生视频']:
|
if params_kw.type in ['文生视频', '参考生视频', '图生视频']:
|
||||||
lt = params_kw.type
|
lt = params_kw.type
|
||||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
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
|
sql = '''select a.*, e.input_fields from llm a
|
||||||
where a.llmcatelogid = b.id
|
join llm_catalog_rel rel on a.id = rel.llmid
|
||||||
and a.upappid = c.id
|
join llmcatelog b on rel.llmcatelogid = b.id
|
||||||
and c.apisetid = d.apisetid
|
join upapp c on a.upappid = c.id
|
||||||
and a.apiname = d.name
|
join uapi d on c.apisetid = d.apisetid and a.apiname = d.name
|
||||||
and d.ioid = e.id
|
join uapiio e on d.ioid = e.id
|
||||||
|
where b.name=${lt}$
|
||||||
and a.enabled_date <= ${biz_date}$
|
and a.enabled_date <= ${biz_date}$
|
||||||
and ${biz_date}$ < a.expired_date
|
and ${biz_date}$ < a.expired_date
|
||||||
and ppid is not NULL
|
and ppid is not NULL'''
|
||||||
and b.name=${lt}$'''
|
|
||||||
biz_date = await get_business_date(sor)
|
biz_date = await get_business_date(sor)
|
||||||
recs = await sor.sqlExe(sql, {
|
recs = await sor.sqlExe(sql, {
|
||||||
'biz_date': biz_date,
|
'biz_date': biz_date,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
dbname = get_module_dbname('llmage')
|
dbname = get_module_dbname('llmage')
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
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()
|
ns = params_kw.copy()
|
||||||
recs = await sor.sqlExe(sql, ns)
|
recs = await sor.sqlExe(sql, ns)
|
||||||
for r in recs.get('rows', []):
|
for r in recs.get('rows', []):
|
||||||
|
|||||||
@ -13,15 +13,15 @@ y.user_message,
|
|||||||
y.assisant_message
|
y.assisant_message
|
||||||
from (
|
from (
|
||||||
select a.*, b.hfid, e.ioid, e.stream
|
select a.*, b.hfid, e.ioid, e.stream
|
||||||
from llm a, llmcatelog b,upapp c, uapiset d, uapi e
|
from llm a
|
||||||
where a.llmcatelogid = b.id
|
join llm_catalog_rel rel on a.id = rel.llmid
|
||||||
and a.upappid = c.id
|
join llmcatelog b on rel.llmcatelogid = b.id
|
||||||
and c.apisetid = d.id
|
join upapp c on a.upappid = c.id
|
||||||
and e.apisetid = d.id
|
join uapiset d on c.apisetid = d.id
|
||||||
and a.apiname = e.name
|
join uapi e on e.apisetid = d.id and a.apiname = e.name
|
||||||
) x left join historyformat y on x.hfid = y.id
|
) x left join historyformat y on x.hfid = y.id
|
||||||
left join uapiio z on x.ioid = z.id
|
left join uapiio z on x.ioid = z.id
|
||||||
where llmcatelogid = ${llmcatelogid}$
|
where rel.llmcatelogid = ${llmcatelogid}$
|
||||||
and x.id != ${llmid}$
|
and x.id != ${llmid}$
|
||||||
"""
|
"""
|
||||||
ns = params_kw.copy()
|
ns = params_kw.copy()
|
||||||
|
|||||||
145
wwwroot/llm_catalog_rel_manage.ui
Normal file
145
wwwroot/llm_catalog_rel_manage.ui
Normal file
@ -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]$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -22,7 +22,6 @@
|
|||||||
"models":[
|
"models":[
|
||||||
{
|
{
|
||||||
"llmid":"{{llm.id}}",
|
"llmid":"{{llm.id}}",
|
||||||
"llmcatelogid":"{{llm.llmcatelogid}}",
|
|
||||||
"response_mode": "{{llm.stream}}",
|
"response_mode": "{{llm.stream}}",
|
||||||
"icon":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}",
|
"icon":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}",
|
||||||
"url":"{{entire_url('/llmage/llminference.dspy')}}",
|
"url":"{{entire_url('/llmage/llminference.dspy')}}",
|
||||||
|
|||||||
@ -17,10 +17,11 @@ if not params_kw.prompt:
|
|||||||
return json_response(d, status=400)
|
return json_response(d, status=400)
|
||||||
env = request._run_ns
|
env = request._run_ns
|
||||||
async with get_sor_context(env, 'llmage') as sor:
|
async with get_sor_context(env, 'llmage') as sor:
|
||||||
sql = """select a.* from llm a, llmcatelog b
|
sql = """select a.* from llm a
|
||||||
where a.llmcatelogid=b.id
|
join llm_catalog_rel rel on a.id = rel.llmid
|
||||||
and a.model=${model}$
|
join llmcatelog b on rel.llmcatelogid = b.id
|
||||||
and b.name = ${lctype}$"""
|
where b.name = ${lctype}$
|
||||||
|
and a.model=${model}$"""
|
||||||
recs = await sor.sqlExe(sql, {
|
recs = await sor.sqlExe(sql, {
|
||||||
'lctype': lctype,
|
'lctype': lctype,
|
||||||
'model': params_kw.model or 'qwen3-max'
|
'model': params_kw.model or 'qwen3-max'
|
||||||
|
|||||||
@ -30,10 +30,11 @@ if not params_kw.prompt and not params_kw.messages:
|
|||||||
return json_response(d, status=400)
|
return json_response(d, status=400)
|
||||||
env = request._run_ns
|
env = request._run_ns
|
||||||
async with get_sor_context(env, 'llmage') as sor:
|
async with get_sor_context(env, 'llmage') as sor:
|
||||||
sql = """select a.* from llm a, llmcatelog b
|
sql = """select a.* from llm a
|
||||||
where a.llmcatelogid=b.id
|
join llm_catalog_rel rel on a.id = rel.llmid
|
||||||
and a.model=${model}$
|
join llmcatelog b on rel.llmcatelogid = b.id
|
||||||
and b.name = ${lctype}$"""
|
where b.name = ${lctype}$
|
||||||
|
and a.model=${model}$"""
|
||||||
recs = await sor.sqlExe(sql, {
|
recs = await sor.sqlExe(sql, {
|
||||||
'lctype': lctype,
|
'lctype': lctype,
|
||||||
'model': params_kw.model or 'qwen3-max'
|
'model': params_kw.model or 'qwen3-max'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user