Refactor backend to support interface-definition driven model resources
- Introduce a new model structure for managing interface definitions and model resources, enhancing the backend's capability to handle various service integrations. - Update the Makefile to reflect changes in database seeding and resource management commands. - Remove the deprecated credentials management routes and replace them with a unified model registry API. - Modify existing routes and schemas to align with the new model structure, ensuring seamless integration with the frontend. - Enhance database seeding scripts to populate new model resources and their configurations. - Update README documentation to reflect the new architecture and usage instructions for model resources and interface definitions.
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
"""助手 CRUD。前端「助手列表 / 创建 / 编辑」对接这里。
|
||||
|
||||
模型/KB 以 FK 引用注册表;瘦类型字段直接是真列。外部类型(dify/fastgpt/opencode)的
|
||||
api_key 是私有密钥,读时打码、写时哨兵(列级,复用 services/masking,与凭证表一致)。
|
||||
"""
|
||||
"""Assistant CRUD backed by capability-to-model-resource bindings."""
|
||||
|
||||
import uuid
|
||||
|
||||
from db.models import Assistant
|
||||
from db.models import Assistant, AssistantModelBinding, ModelResource
|
||||
from db.session import get_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import AssistantOut, AssistantUpsert
|
||||
@@ -15,27 +11,62 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/api/assistants", tags=["assistants"])
|
||||
CAPABILITIES = ("LLM", "ASR", "TTS", "Realtime", "Embedding")
|
||||
|
||||
|
||||
def _to_out(a: Assistant) -> AssistantOut:
|
||||
async def _sync_bindings(
|
||||
session: AsyncSession, assistant_id: str, resource_ids: dict[str, str]
|
||||
) -> None:
|
||||
for capability in CAPABILITIES:
|
||||
resource_id = resource_ids.get(capability)
|
||||
binding = await session.get(AssistantModelBinding, (assistant_id, capability))
|
||||
if not resource_id:
|
||||
if binding:
|
||||
await session.delete(binding)
|
||||
continue
|
||||
resource = await session.get(ModelResource, resource_id)
|
||||
if not resource or resource.capability != capability:
|
||||
raise HTTPException(400, f"{capability} 绑定必须引用同能力的模型资源")
|
||||
if binding:
|
||||
binding.model_resource_id = resource_id
|
||||
else:
|
||||
session.add(
|
||||
AssistantModelBinding(
|
||||
assistant_id=assistant_id,
|
||||
capability=capability,
|
||||
model_resource_id=resource_id,
|
||||
config={},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _resource_ids(session: AsyncSession, assistant_id: str) -> dict[str, str]:
|
||||
bindings = (
|
||||
await session.execute(
|
||||
select(AssistantModelBinding).where(
|
||||
AssistantModelBinding.assistant_id == assistant_id
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
return {binding.capability: binding.model_resource_id for binding in bindings}
|
||||
|
||||
|
||||
async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
||||
return AssistantOut(
|
||||
id=a.id,
|
||||
name=a.name,
|
||||
type=a.type, # type: ignore[arg-type]
|
||||
runtime_mode=a.runtime_mode, # type: ignore[arg-type]
|
||||
greeting=a.greeting,
|
||||
enable_interrupt=a.enable_interrupt,
|
||||
llm_credential_id=a.llm_credential_id,
|
||||
asr_credential_id=a.asr_credential_id,
|
||||
tts_credential_id=a.tts_credential_id,
|
||||
realtime_credential_id=a.realtime_credential_id,
|
||||
knowledge_base_id=a.knowledge_base_id,
|
||||
prompt=a.prompt,
|
||||
api_url=a.api_url,
|
||||
api_key=mask(a.api_key), # 仅外部类型有值;空串 mask 仍是空串
|
||||
app_id=a.app_id,
|
||||
graph=a.graph or {},
|
||||
updated_at=a.updated_at.isoformat() if a.updated_at else None,
|
||||
id=assistant.id,
|
||||
name=assistant.name,
|
||||
type=assistant.type, # type: ignore[arg-type]
|
||||
runtime_mode=assistant.runtime_mode, # type: ignore[arg-type]
|
||||
greeting=assistant.greeting,
|
||||
enable_interrupt=assistant.enable_interrupt,
|
||||
model_resource_ids=await _resource_ids(session, assistant.id),
|
||||
knowledge_base_id=assistant.knowledge_base_id,
|
||||
prompt=assistant.prompt,
|
||||
api_url=assistant.api_url,
|
||||
api_key=mask(assistant.api_key),
|
||||
app_id=assistant.app_id,
|
||||
graph=assistant.graph or {},
|
||||
updated_at=assistant.updated_at.isoformat() if assistant.updated_at else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,60 +75,61 @@ async def list_assistants(session: AsyncSession = Depends(get_session)):
|
||||
rows = (
|
||||
await session.execute(select(Assistant).order_by(Assistant.updated_at.desc()))
|
||||
).scalars().all()
|
||||
return [_to_out(a) for a in rows]
|
||||
return [await _to_out(session, assistant) for assistant in rows]
|
||||
|
||||
|
||||
@router.post("", response_model=AssistantOut)
|
||||
async def create_assistant(
|
||||
body: AssistantUpsert, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
a = Assistant(id=f"asst_{uuid.uuid4().hex[:12]}", **body.model_dump())
|
||||
session.add(a)
|
||||
data = body.model_dump()
|
||||
resource_ids = data.pop("model_resource_ids")
|
||||
assistant = Assistant(id=f"asst_{uuid.uuid4().hex[:12]}", **data)
|
||||
session.add(assistant)
|
||||
await session.flush()
|
||||
await _sync_bindings(session, assistant.id, resource_ids)
|
||||
await session.commit()
|
||||
await session.refresh(a)
|
||||
return _to_out(a)
|
||||
await session.refresh(assistant)
|
||||
return await _to_out(session, assistant)
|
||||
|
||||
|
||||
@router.get("/{assistant_id}", response_model=AssistantOut)
|
||||
async def get_assistant(
|
||||
assistant_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
a = await session.get(Assistant, assistant_id)
|
||||
if not a:
|
||||
assistant = await session.get(Assistant, assistant_id)
|
||||
if not assistant:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
return _to_out(a)
|
||||
return await _to_out(session, assistant)
|
||||
|
||||
|
||||
@router.post("/{assistant_id}/duplicate", response_model=AssistantOut)
|
||||
async def duplicate_assistant(
|
||||
assistant_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
"""服务端整行复制:含真实 api_key,DB→DB,密钥不经过浏览器,副本可直接用。"""
|
||||
src = await session.get(Assistant, assistant_id)
|
||||
if not src:
|
||||
source = await session.get(Assistant, assistant_id)
|
||||
if not source:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
a = Assistant(
|
||||
assistant = Assistant(
|
||||
id=f"asst_{uuid.uuid4().hex[:12]}",
|
||||
name=f"{src.name} 副本",
|
||||
type=src.type,
|
||||
runtime_mode=src.runtime_mode,
|
||||
greeting=src.greeting,
|
||||
enable_interrupt=src.enable_interrupt,
|
||||
llm_credential_id=src.llm_credential_id,
|
||||
asr_credential_id=src.asr_credential_id,
|
||||
tts_credential_id=src.tts_credential_id,
|
||||
realtime_credential_id=src.realtime_credential_id,
|
||||
knowledge_base_id=src.knowledge_base_id,
|
||||
prompt=src.prompt,
|
||||
api_url=src.api_url,
|
||||
api_key=src.api_key, # 真 key,DB→DB
|
||||
app_id=src.app_id,
|
||||
graph=dict(src.graph or {}), # 浅拷贝,避免与源行共享同一 dict
|
||||
name=f"{source.name} 副本",
|
||||
type=source.type,
|
||||
runtime_mode=source.runtime_mode,
|
||||
greeting=source.greeting,
|
||||
enable_interrupt=source.enable_interrupt,
|
||||
knowledge_base_id=source.knowledge_base_id,
|
||||
prompt=source.prompt,
|
||||
api_url=source.api_url,
|
||||
api_key=source.api_key,
|
||||
app_id=source.app_id,
|
||||
graph=dict(source.graph or {}),
|
||||
)
|
||||
session.add(a)
|
||||
session.add(assistant)
|
||||
await session.flush()
|
||||
await _sync_bindings(session, assistant.id, await _resource_ids(session, source.id))
|
||||
await session.commit()
|
||||
await session.refresh(a)
|
||||
return _to_out(a)
|
||||
await session.refresh(assistant)
|
||||
return await _to_out(session, assistant)
|
||||
|
||||
|
||||
@router.put("/{assistant_id}", response_model=AssistantOut)
|
||||
@@ -106,26 +138,27 @@ async def update_assistant(
|
||||
body: AssistantUpsert,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
a = await session.get(Assistant, assistant_id)
|
||||
if not a:
|
||||
assistant = await session.get(Assistant, assistant_id)
|
||||
if not assistant:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
data = body.model_dump()
|
||||
# 写时哨兵(列级):回传打码/空 api_key → 保留旧 key
|
||||
data["api_key"] = resolve_incoming_key(data["api_key"], a.api_key)
|
||||
for k, v in data.items():
|
||||
setattr(a, k, v)
|
||||
resource_ids = data.pop("model_resource_ids")
|
||||
data["api_key"] = resolve_incoming_key(data["api_key"], assistant.api_key)
|
||||
for key, value in data.items():
|
||||
setattr(assistant, key, value)
|
||||
await _sync_bindings(session, assistant.id, resource_ids)
|
||||
await session.commit()
|
||||
await session.refresh(a)
|
||||
return _to_out(a)
|
||||
await session.refresh(assistant)
|
||||
return await _to_out(session, assistant)
|
||||
|
||||
|
||||
@router.delete("/{assistant_id}")
|
||||
async def delete_assistant(
|
||||
assistant_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
a = await session.get(Assistant, assistant_id)
|
||||
if not a:
|
||||
assistant = await session.get(Assistant, assistant_id)
|
||||
if not assistant:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
await session.delete(a)
|
||||
await session.delete(assistant)
|
||||
await session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
"""模型资源凭证 CRUD。前端 ComponentsModelsPage 对接这里。
|
||||
|
||||
字段对齐前端 ModelResource。读:api_key 打码;写:占位符表示不改(写时哨兵)。
|
||||
设默认时清掉同 type 其它默认。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from db.models import ProviderCredential
|
||||
from db.session import get_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import (
|
||||
CredentialOut,
|
||||
CredentialTestRequest,
|
||||
CredentialTestResult,
|
||||
CredentialUpsert,
|
||||
)
|
||||
from services.credential_tester import test_openai_credential, test_xfyun_credential
|
||||
from services.masking import mask, resolve_incoming_key
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/api/credentials", tags=["credentials"])
|
||||
|
||||
|
||||
def _to_out(c: ProviderCredential) -> CredentialOut:
|
||||
return CredentialOut(
|
||||
id=c.id,
|
||||
name=c.name,
|
||||
model_id=c.model_id,
|
||||
type=c.type,
|
||||
interface_type=c.interface_type,
|
||||
api_url=c.api_url,
|
||||
api_key=mask(c.api_key), # 永远打码
|
||||
voice=c.voice,
|
||||
speed=c.speed,
|
||||
language=c.language,
|
||||
is_default=c.is_default,
|
||||
)
|
||||
|
||||
|
||||
async def _clear_other_defaults(session: AsyncSession, type_: str, keep_id: str):
|
||||
await session.execute(
|
||||
update(ProviderCredential)
|
||||
.where(ProviderCredential.type == type_, ProviderCredential.id != keep_id)
|
||||
.values(is_default=False)
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[CredentialOut])
|
||||
async def list_credentials(session: AsyncSession = Depends(get_session)):
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(ProviderCredential).order_by(ProviderCredential.type)
|
||||
)
|
||||
).scalars().all()
|
||||
return [_to_out(c) for c in rows]
|
||||
|
||||
|
||||
@router.post("", response_model=CredentialOut)
|
||||
async def create_credential(
|
||||
body: CredentialUpsert, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
c = ProviderCredential(
|
||||
id=f"model_{uuid.uuid4().hex[:12]}",
|
||||
name=body.name,
|
||||
model_id=body.model_id,
|
||||
type=body.type,
|
||||
interface_type=body.interface_type,
|
||||
api_url=body.api_url,
|
||||
api_key=resolve_incoming_key(body.api_key, ""),
|
||||
voice=body.voice,
|
||||
speed=body.speed,
|
||||
language=body.language,
|
||||
is_default=body.is_default,
|
||||
)
|
||||
session.add(c)
|
||||
if c.is_default:
|
||||
await _clear_other_defaults(session, c.type, c.id)
|
||||
await session.commit()
|
||||
await session.refresh(c)
|
||||
return _to_out(c)
|
||||
|
||||
|
||||
@router.post("/test", response_model=CredentialTestResult)
|
||||
async def test_new_credential(body: CredentialTestRequest):
|
||||
if body.interface_type == "xfyun":
|
||||
return test_xfyun_credential(body)
|
||||
if body.interface_type != "openai":
|
||||
return CredentialTestResult(
|
||||
ok=False,
|
||||
message="暂不支持该接口类型",
|
||||
detail="当前仅支持 OpenAI 兼容接口测试",
|
||||
)
|
||||
if not body.api_key:
|
||||
return CredentialTestResult(
|
||||
ok=False,
|
||||
message="缺少 API Key",
|
||||
detail="测试新配置时需要输入 API Key",
|
||||
)
|
||||
return await test_openai_credential(body)
|
||||
|
||||
|
||||
@router.post("/{cred_id}/test", response_model=CredentialTestResult)
|
||||
async def test_saved_credential(
|
||||
cred_id: str,
|
||||
body: CredentialTestRequest,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
c = await session.get(ProviderCredential, cred_id)
|
||||
if not c:
|
||||
raise HTTPException(404, "凭证不存在")
|
||||
config = body.model_copy(
|
||||
update={"api_key": resolve_incoming_key(body.api_key, c.api_key)}
|
||||
)
|
||||
if config.interface_type == "xfyun":
|
||||
return test_xfyun_credential(config)
|
||||
if config.interface_type != "openai":
|
||||
return CredentialTestResult(
|
||||
ok=False,
|
||||
message="暂不支持该接口类型",
|
||||
detail="当前仅支持 OpenAI 兼容接口测试",
|
||||
)
|
||||
return await test_openai_credential(config)
|
||||
|
||||
|
||||
@router.post("/{cred_id}/duplicate", response_model=CredentialOut)
|
||||
async def duplicate_credential(
|
||||
cred_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
"""服务端整行复制:含真实 api_key,DB→DB,密钥不经浏览器。副本不继承默认标记。"""
|
||||
src = await session.get(ProviderCredential, cred_id)
|
||||
if not src:
|
||||
raise HTTPException(404, "凭证不存在")
|
||||
c = ProviderCredential(
|
||||
id=f"model_{uuid.uuid4().hex[:12]}",
|
||||
name=f"{src.name} 副本",
|
||||
model_id=src.model_id,
|
||||
type=src.type,
|
||||
interface_type=src.interface_type,
|
||||
api_url=src.api_url,
|
||||
api_key=src.api_key, # 真 key,DB→DB
|
||||
voice=src.voice,
|
||||
speed=src.speed,
|
||||
language=src.language,
|
||||
is_default=False, # 副本不继承默认,避免抢走源的默认标记
|
||||
)
|
||||
session.add(c)
|
||||
await session.commit()
|
||||
await session.refresh(c)
|
||||
return _to_out(c)
|
||||
|
||||
|
||||
@router.put("/{cred_id}", response_model=CredentialOut)
|
||||
async def update_credential(
|
||||
cred_id: str,
|
||||
body: CredentialUpsert,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
c = await session.get(ProviderCredential, cred_id)
|
||||
if not c:
|
||||
raise HTTPException(404, "凭证不存在")
|
||||
c.name = body.name
|
||||
c.model_id = body.model_id
|
||||
c.type = body.type
|
||||
c.interface_type = body.interface_type
|
||||
c.api_url = body.api_url
|
||||
c.voice = body.voice
|
||||
c.speed = body.speed
|
||||
c.language = body.language
|
||||
c.is_default = body.is_default
|
||||
# 写时哨兵:打码占位符 → 保留旧 key
|
||||
c.api_key = resolve_incoming_key(body.api_key, c.api_key)
|
||||
if c.is_default:
|
||||
await _clear_other_defaults(session, c.type, c.id)
|
||||
await session.commit()
|
||||
await session.refresh(c)
|
||||
return _to_out(c)
|
||||
|
||||
|
||||
@router.delete("/{cred_id}")
|
||||
async def delete_credential(
|
||||
cred_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
c = await session.get(ProviderCredential, cred_id)
|
||||
if not c:
|
||||
raise HTTPException(404, "凭证不存在")
|
||||
await session.delete(c)
|
||||
await session.commit()
|
||||
return {"ok": True}
|
||||
@@ -1,12 +1,12 @@
|
||||
"""知识库 CRUD。前端助手编辑页的"知识库"下拉对接这里。
|
||||
|
||||
KB 自身引用一个 Embedding 凭证(embeddingCredentialId)。被助手引用时禁止删除
|
||||
KB 自身引用一个 Embedding 模型资源。被助手引用时禁止删除
|
||||
(DB 层 ON DELETE RESTRICT),这里把外键冲突翻译成 409。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from db.models import KnowledgeBase
|
||||
from db.models import KnowledgeBase, ModelResource
|
||||
from db.session import get_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import KnowledgeBaseOut, KnowledgeBaseUpsert
|
||||
@@ -17,12 +17,22 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
router = APIRouter(prefix="/api/knowledge-bases", tags=["knowledge-bases"])
|
||||
|
||||
|
||||
async def _validate_embedding_resource(
|
||||
session: AsyncSession, resource_id: str | None
|
||||
) -> None:
|
||||
if not resource_id:
|
||||
return
|
||||
resource = await session.get(ModelResource, resource_id)
|
||||
if not resource or resource.capability != "Embedding":
|
||||
raise HTTPException(400, "知识库必须引用 Embedding 模型资源")
|
||||
|
||||
|
||||
def _to_out(kb: KnowledgeBase) -> KnowledgeBaseOut:
|
||||
return KnowledgeBaseOut(
|
||||
id=kb.id,
|
||||
name=kb.name,
|
||||
description=kb.description,
|
||||
embedding_credential_id=kb.embedding_credential_id,
|
||||
embedding_model_resource_id=kb.embedding_model_resource_id,
|
||||
status=kb.status,
|
||||
updated_at=kb.updated_at.isoformat() if kb.updated_at else None,
|
||||
)
|
||||
@@ -40,6 +50,7 @@ async def list_knowledge_bases(session: AsyncSession = Depends(get_session)):
|
||||
async def create_knowledge_base(
|
||||
body: KnowledgeBaseUpsert, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
await _validate_embedding_resource(session, body.embedding_model_resource_id)
|
||||
kb = KnowledgeBase(id=f"kb_{uuid.uuid4().hex[:12]}", **body.model_dump())
|
||||
session.add(kb)
|
||||
await session.commit()
|
||||
@@ -66,6 +77,7 @@ async def update_knowledge_base(
|
||||
kb = await session.get(KnowledgeBase, kb_id)
|
||||
if not kb:
|
||||
raise HTTPException(404, "知识库不存在")
|
||||
await _validate_embedding_resource(session, body.embedding_model_resource_id)
|
||||
for k, v in body.model_dump().items():
|
||||
setattr(kb, k, v)
|
||||
await session.commit()
|
||||
|
||||
249
backend/routes/model_registry.py
Normal file
249
backend/routes/model_registry.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""Interface-definition driven model resource registry APIs."""
|
||||
|
||||
import uuid
|
||||
|
||||
from db.models import (
|
||||
AssistantModelBinding,
|
||||
InterfaceDefinition,
|
||||
KnowledgeBase,
|
||||
ModelResource,
|
||||
)
|
||||
from db.session import get_session
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from schemas import (
|
||||
InterfaceDefinitionOut,
|
||||
ModelResourceOut,
|
||||
ModelResourceTestResult,
|
||||
ModelResourceUpsert,
|
||||
)
|
||||
from services.interface_catalog import validate_fields
|
||||
from services.masking import mask_secrets, merge_secrets
|
||||
from services.model_resource_tester import test_model_resource
|
||||
from sqlalchemy import delete, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["model-registry"])
|
||||
|
||||
|
||||
def _definition_dict(row: InterfaceDefinition) -> dict:
|
||||
return {
|
||||
"interface_type": row.interface_type,
|
||||
"name": row.name,
|
||||
"capability": row.capability,
|
||||
"fields": (row.field_schema or {}).get("fields", []),
|
||||
}
|
||||
|
||||
|
||||
def _definition_out(row: InterfaceDefinition) -> InterfaceDefinitionOut:
|
||||
return InterfaceDefinitionOut(
|
||||
interface_type=row.interface_type,
|
||||
name=row.name,
|
||||
capability=row.capability, # type: ignore[arg-type]
|
||||
field_schema=row.field_schema or {},
|
||||
enabled=row.enabled,
|
||||
version=row.version,
|
||||
)
|
||||
|
||||
|
||||
def _resource_out(row: ModelResource) -> ModelResourceOut:
|
||||
return ModelResourceOut(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
capability=row.capability, # type: ignore[arg-type]
|
||||
interface_type=row.interface_type,
|
||||
values=row.values or {},
|
||||
secrets=mask_secrets(row.secrets or {}),
|
||||
enabled=row.enabled,
|
||||
is_default=row.is_default,
|
||||
updated_at=row.updated_at.isoformat() if row.updated_at else None,
|
||||
)
|
||||
|
||||
|
||||
async def _definition(
|
||||
session: AsyncSession, interface_type: str
|
||||
) -> InterfaceDefinition:
|
||||
row = await session.get(InterfaceDefinition, interface_type)
|
||||
if not row or not row.enabled:
|
||||
raise HTTPException(400, f"接口类型不可用: {interface_type}")
|
||||
return row
|
||||
|
||||
|
||||
async def _validate(
|
||||
session: AsyncSession,
|
||||
body: ModelResourceUpsert,
|
||||
stored_secrets: dict | None = None,
|
||||
) -> tuple[InterfaceDefinition, dict]:
|
||||
definition = await _definition(session, body.interface_type)
|
||||
secrets = merge_secrets(body.secrets, stored_secrets or {})
|
||||
try:
|
||||
validate_fields(_definition_dict(definition), body.values, secrets)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, str(exc)) from exc
|
||||
return definition, secrets
|
||||
|
||||
|
||||
async def _clear_incompatible_references(
|
||||
session: AsyncSession, resource: ModelResource, capability: str
|
||||
) -> None:
|
||||
if capability == resource.capability:
|
||||
return
|
||||
await session.execute(
|
||||
delete(AssistantModelBinding).where(
|
||||
AssistantModelBinding.model_resource_id == resource.id
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
update(KnowledgeBase)
|
||||
.where(KnowledgeBase.embedding_model_resource_id == resource.id)
|
||||
.values(embedding_model_resource_id=None)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/interface-definitions", response_model=list[InterfaceDefinitionOut])
|
||||
async def list_interface_definitions(
|
||||
capability: str | None = Query(default=None),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
stmt = select(InterfaceDefinition).where(InterfaceDefinition.enabled.is_(True))
|
||||
if capability:
|
||||
stmt = stmt.where(InterfaceDefinition.capability == capability)
|
||||
rows = (await session.execute(stmt.order_by(InterfaceDefinition.capability))).scalars().all()
|
||||
return [_definition_out(row) for row in rows]
|
||||
|
||||
|
||||
@router.get("/model-resources", response_model=list[ModelResourceOut])
|
||||
async def list_model_resources(session: AsyncSession = Depends(get_session)):
|
||||
rows = (
|
||||
await session.execute(select(ModelResource).order_by(ModelResource.capability))
|
||||
).scalars().all()
|
||||
return [_resource_out(row) for row in rows]
|
||||
|
||||
|
||||
@router.post("/model-resources", response_model=ModelResourceOut)
|
||||
async def create_model_resource(
|
||||
body: ModelResourceUpsert, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
definition, secrets = await _validate(session, body)
|
||||
row = ModelResource(
|
||||
id=f"model_{uuid.uuid4().hex[:12]}",
|
||||
name=body.name,
|
||||
capability=definition.capability,
|
||||
interface_type=definition.interface_type,
|
||||
values=body.values,
|
||||
secrets=secrets,
|
||||
enabled=body.enabled,
|
||||
is_default=body.is_default,
|
||||
)
|
||||
session.add(row)
|
||||
if row.is_default:
|
||||
await session.execute(
|
||||
update(ModelResource)
|
||||
.where(ModelResource.capability == row.capability, ModelResource.id != row.id)
|
||||
.values(is_default=False)
|
||||
)
|
||||
await session.commit()
|
||||
return _resource_out(row)
|
||||
|
||||
|
||||
@router.post("/model-resources/test", response_model=ModelResourceTestResult)
|
||||
async def test_new_model_resource(
|
||||
body: ModelResourceUpsert, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
definition, secrets = await _validate(session, body)
|
||||
return await test_model_resource(
|
||||
definition.interface_type,
|
||||
definition.capability,
|
||||
body.values,
|
||||
secrets,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model-resources/{resource_id}/test", response_model=ModelResourceTestResult
|
||||
)
|
||||
async def test_saved_model_resource(
|
||||
resource_id: str,
|
||||
body: ModelResourceUpsert,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
row = await session.get(ModelResource, resource_id)
|
||||
if not row:
|
||||
raise HTTPException(404, "模型资源不存在")
|
||||
definition, secrets = await _validate(session, body, row.secrets or {})
|
||||
return await test_model_resource(
|
||||
definition.interface_type,
|
||||
definition.capability,
|
||||
body.values,
|
||||
secrets,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/model-resources/{resource_id}/duplicate", response_model=ModelResourceOut)
|
||||
async def duplicate_model_resource(
|
||||
resource_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
source = await session.get(ModelResource, resource_id)
|
||||
if not source:
|
||||
raise HTTPException(404, "模型资源不存在")
|
||||
row = ModelResource(
|
||||
id=f"model_{uuid.uuid4().hex[:12]}",
|
||||
name=f"{source.name} 副本",
|
||||
capability=source.capability,
|
||||
interface_type=source.interface_type,
|
||||
values=dict(source.values or {}),
|
||||
secrets=dict(source.secrets or {}),
|
||||
enabled=source.enabled,
|
||||
is_default=False,
|
||||
)
|
||||
session.add(row)
|
||||
await session.commit()
|
||||
return _resource_out(row)
|
||||
|
||||
|
||||
@router.put("/model-resources/{resource_id}", response_model=ModelResourceOut)
|
||||
async def update_model_resource(
|
||||
resource_id: str,
|
||||
body: ModelResourceUpsert,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
row = await session.get(ModelResource, resource_id)
|
||||
if not row:
|
||||
raise HTTPException(404, "模型资源不存在")
|
||||
definition, secrets = await _validate(session, body, row.secrets or {})
|
||||
await _clear_incompatible_references(session, row, definition.capability)
|
||||
row.name = body.name
|
||||
row.capability = definition.capability
|
||||
row.interface_type = definition.interface_type
|
||||
row.values = body.values
|
||||
row.secrets = secrets
|
||||
row.enabled = body.enabled
|
||||
row.is_default = body.is_default
|
||||
if row.is_default:
|
||||
await session.execute(
|
||||
update(ModelResource)
|
||||
.where(ModelResource.capability == row.capability, ModelResource.id != row.id)
|
||||
.values(is_default=False)
|
||||
)
|
||||
await session.commit()
|
||||
return _resource_out(row)
|
||||
|
||||
|
||||
@router.delete("/model-resources/{resource_id}")
|
||||
async def delete_model_resource(
|
||||
resource_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
row = await session.get(ModelResource, resource_id)
|
||||
if not row:
|
||||
raise HTTPException(404, "模型资源不存在")
|
||||
in_use = (
|
||||
await session.execute(
|
||||
select(AssistantModelBinding.assistant_id)
|
||||
.where(AssistantModelBinding.model_resource_id == resource_id)
|
||||
.limit(1)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if in_use:
|
||||
raise HTTPException(409, "该模型资源仍被助手引用")
|
||||
await session.delete(row)
|
||||
await session.commit()
|
||||
return {"ok": True}
|
||||
Reference in New Issue
Block a user