- 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.
250 lines
8.1 KiB
Python
250 lines
8.1 KiB
Python
"""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}
|