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:
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