diff --git a/amt/api/deps.py b/amt/api/deps.py index 3bc6fec8..ee6b0751 100644 --- a/amt/api/deps.py +++ b/amt/api/deps.py @@ -28,6 +28,7 @@ time_ago, ) from amt.schema.localized_value_item import LocalizedValueItem +from amt.schema.shared import IterMixin T = TypeVar("T", bound=Enum | LocalizableEnum) @@ -135,6 +136,19 @@ def Redirect(self, request: Request, url: str) -> HTMLResponse: return self.TemplateResponse(request, "redirect.html.j2", headers=headers) +def instance(obj, type_string): + match type_string: + case "str": + return isinstance(obj, str) + case "list": + return isinstance(obj, list) + case "IterMixin": + return isinstance(obj, IterMixin) + case "dict": + return isinstance(obj, dict) + case _: + raise TypeError("Unsupported type: " + type_string) + templates = LocaleJinja2Templates( directory="amt/site/templates/", context_processors=[custom_context_processor], undefined=get_undefined_behaviour() ) @@ -146,3 +160,4 @@ def Redirect(self, request: Request, url: str) -> HTMLResponse: templates.env.globals.update(is_nested_enum=is_nested_enum) # pyright: ignore [reportUnknownMemberType] templates.env.globals.update(nested_enum=nested_enum) # pyright: ignore [reportUnknownMemberType] templates.env.globals.update(nested_enum_value=nested_enum_value) # pyright: ignore [reportUnknownMemberType] +templates.env.globals.update(isinstance=instance) # pyright: ignore [reportUnknownMemberType] diff --git a/amt/api/routes/project.py b/amt/api/routes/project.py index 58a5d5b4..b1330db7 100644 --- a/amt/api/routes/project.py +++ b/amt/api/routes/project.py @@ -1,8 +1,6 @@ import asyncio -import functools import logging from collections.abc import Sequence -from pathlib import Path from typing import Annotated, Any, cast from fastapi import APIRouter, Depends, Request @@ -28,26 +26,17 @@ from amt.services import task_registry from amt.services.instruments_and_requirements_state import InstrumentStateService, RequirementsStateService from amt.services.projects import ProjectsService -from amt.services.storage import StorageFactory from amt.services.task_registry import fetch_measures, fetch_requirements from amt.services.tasks import TasksService -from amt.utils.storage import get_include_content router = APIRouter() logger = logging.getLogger(__name__) -def get_system_card_data() -> SystemCard: - # TODO: This now loads an example system card independent of the project ID. - path = Path("example_system_card/system_card.yaml") - system_card: Any = StorageFactory.init(storage_type="file", location=path.parent, filename=path.name).read() - return SystemCard(**system_card) - - -@functools.lru_cache -def get_instrument_state() -> dict[str, Any]: - system_card_data = get_system_card_data() - instrument_state = InstrumentStateService(system_card_data) +# TODO: does this need LRU cache? +# @functools.lru_cache() +def get_instrument_state(system_card: SystemCard) -> dict[str, Any]: + instrument_state = InstrumentStateService(system_card) instrument_states = instrument_state.get_state_per_instrument() return { "instrument_states": instrument_states, @@ -181,14 +170,12 @@ async def get_project_context( project_id: int, projects_service: ProjectsService, request: Request ) -> tuple[Project, dict[str, Any]]: project = await get_project_or_error(project_id, projects_service, request) - system_card_data = get_system_card_data() - instrument_state = get_instrument_state() + instrument_state = get_instrument_state(project.system_card) requirements_state = get_requirements_state(project.system_card) tab_items = get_project_details_tabs(request) - project.system_card = system_card_data return project, { "last_edited": project.last_edited, - "system_card": system_card_data, + "system_card": project.system_card, "instrument_state": instrument_state, "requirements_state": requirements_state, "project": project, @@ -313,15 +300,8 @@ async def get_system_card( request, ) - # TODO: This now loads an example system card independent of the project ID. - filepath = Path("example_system_card/system_card.yaml") - file_system_storage_service = StorageFactory.init( - storage_type="file", location=filepath.parent, filename=filepath.name - ) - system_card_data = file_system_storage_service.read() - context = { - "system_card": system_card_data, + "system_card": project.system_card, "instrument_state": instrument_state, "requirements_state": requirements_state, "last_edited": project.last_edited, @@ -351,15 +331,14 @@ async def get_project_inference( request, ) - system_card_data = get_system_card_data() - instrument_state = get_instrument_state() + instrument_state = get_instrument_state(project.system_card) requirements_state = get_requirements_state(project.system_card) tab_items = get_project_details_tabs(request) context = { "last_edited": project.last_edited, - "system_card": system_card_data, + "system_card": project.system_card, "instrument_state": instrument_state, "requirements_state": requirements_state, "project": project, @@ -383,7 +362,7 @@ async def get_system_card_requirements( projects_service: Annotated[ProjectsService, Depends(ProjectsService)], ) -> HTMLResponse: project = await get_project_or_error(project_id, projects_service, request) - instrument_state = get_instrument_state() + instrument_state = get_instrument_state(project.system_card) requirements_state = get_requirements_state(project.system_card) # TODO: This tab is fairly slow, fix in later releases tab_items = get_project_details_tabs(request) @@ -546,7 +525,7 @@ async def get_system_card_data_page( projects_service: Annotated[ProjectsService, Depends(ProjectsService)], ) -> HTMLResponse: project = await get_project_or_error(project_id, projects_service, request) - instrument_state = get_instrument_state() + instrument_state = get_instrument_state(project.system_card) requirements_state = get_requirements_state(project.system_card) tab_items = get_project_details_tabs(request) @@ -586,7 +565,7 @@ async def get_system_card_instruments( projects_service: Annotated[ProjectsService, Depends(ProjectsService)], ) -> HTMLResponse: project = await get_project_or_error(project_id, projects_service, request) - instrument_state = get_instrument_state() + instrument_state = get_instrument_state(project.system_card) requirements_state = get_requirements_state(project.system_card) tab_items = get_project_details_tabs(request) @@ -614,11 +593,6 @@ async def get_system_card_instruments( return templates.TemplateResponse(request, "projects/details_instruments.html.j2", context) -# !!! -# Implementation of this endpoint is for now independent of the project ID, meaning -# that the same system card is rendered for all project ID's. This is due to the fact -# that the logical process flow of a system card is not complete. -# !!! @router.get("/{project_id}/details/system_card/assessments/{assessment_card}") async def get_assessment_card( request: Request, @@ -627,7 +601,7 @@ async def get_assessment_card( projects_service: Annotated[ProjectsService, Depends(ProjectsService)], ) -> HTMLResponse: project = await get_project_or_error(project_id, projects_service, request) - instrument_state = get_instrument_state() + instrument_state = get_instrument_state(project.system_card) requirements_state = get_requirements_state(project.system_card) request.state.path_variables.update({"assessment_card": assessment_card}) @@ -645,9 +619,7 @@ async def get_assessment_card( request, ) - # TODO: This now loads an example system card independent of the project ID. - filepath = Path("example_system_card/system_card.yaml") - assessment_card_data = get_include_content(filepath.parent, filepath.name, "assessments", assessment_card) + assessment_card_data = next((assessment for assessment in project.system_card.assessments if assessment.name.lower() == assessment_card), None) if not assessment_card_data: logger.warning("assessment card not found") @@ -678,13 +650,9 @@ async def get_model_card( projects_service: Annotated[ProjectsService, Depends(ProjectsService)], ) -> HTMLResponse: project = await get_project_or_error(project_id, projects_service, request) - instrument_state = get_instrument_state() - requirements_state = get_requirements_state(project.system_card) - - # TODO: This now loads an example system card independent of the project ID. - filepath = Path("example_system_card/system_card.yaml") - model_card_data = get_include_content(filepath.parent, filepath.name, "models", model_card) request.state.path_variables.update({"model_card": model_card}) + instrument_state = get_instrument_state(project.system_card) + requirements_state = get_requirements_state(project.system_card) tab_items = get_project_details_tabs(request) @@ -699,6 +667,8 @@ async def get_model_card( request, ) + model_card_data = next((model for model in project.system_card.models if model.name.lower() == model_card), None) + if not model_card_data: logger.warning("model card not found") raise AMTNotFound() diff --git a/amt/schema/assessment_card.py b/amt/schema/assessment_card.py index 1b102508..95ca7b6e 100644 --- a/amt/schema/assessment_card.py +++ b/amt/schema/assessment_card.py @@ -5,12 +5,14 @@ Field, # pyright: ignore [reportUnknownMemberType] ) +from amt.schema.shared import IterMixin -class AssessmentAuthor(BaseModel): + +class AssessmentAuthor(BaseModel, IterMixin): name: str = Field(default=None) -class AssessmentContent(BaseModel): +class AssessmentContent(BaseModel, IterMixin): question: str = Field(default=None) urn: str = Field(default=None) answer: str = Field(default=None) @@ -19,7 +21,7 @@ class AssessmentContent(BaseModel): timestamp: datetime | None = Field(default=None) -class AssessmentCard(BaseModel): +class AssessmentCard(BaseModel, IterMixin): name: str = Field(default=None) urn: str = Field(default=None) date: datetime = Field(default=None) diff --git a/amt/schema/instrument.py b/amt/schema/instrument.py index 8b79bea0..d01f7b10 100644 --- a/amt/schema/instrument.py +++ b/amt/schema/instrument.py @@ -1,7 +1,9 @@ from pydantic import BaseModel, Field +from amt.schema.shared import IterMixin -class Owner(BaseModel): + +class Owner(BaseModel, IterMixin): organization: str name: str email: str @@ -15,7 +17,7 @@ class InstrumentTask(BaseModel): lifecycle: list[str] -class InstrumentBase(BaseModel): +class InstrumentBase(BaseModel, IterMixin): urn: str diff --git a/amt/schema/measure.py b/amt/schema/measure.py index 05eb2cef..f015981f 100644 --- a/amt/schema/measure.py +++ b/amt/schema/measure.py @@ -1,7 +1,9 @@ from pydantic import BaseModel, Field +from amt.schema.shared import IterMixin -class MeasureBase(BaseModel): + +class MeasureBase(BaseModel, IterMixin): urn: str diff --git a/amt/schema/model_card.py b/amt/schema/model_card.py new file mode 100644 index 00000000..2e4add01 --- /dev/null +++ b/amt/schema/model_card.py @@ -0,0 +1,185 @@ +# generated by datamodel-codegen: +# filename: modelcard_schema.json +# timestamp: 2024-10-28T07:45:38+00:00 + +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel, Field + + +class Provenance(BaseModel): + git_commit_hash: str | None = Field( + None, + description='Git commit hash of the commit which contains the transformation file used to create this card', + ) + timestamp: datetime | None = Field( + None, + description='A timestamp of the date, time and timezone of generation of this Model Card in ISO 8601 format', + ) + uri: str | None = Field( + None, description='URI to the tool that was used to perform the transformations' + ) + author: str | None = Field( + None, description='Name of person that initiated the transformations' + ) + + +class License(BaseModel): + license_name: str = Field( + ..., description='Any license from the open source license list' + ) + license_link: str | None = Field( + None, + description='A link to a file of that name inside the repo, or a URL to a remote file containing the license contents', + ) + + +class Owner(BaseModel): + oin: str | None = Field( + None, description='If applicable the Organisatie-identificatienummer (OIN)' + ) + organization: str | None = Field( + None, + description='Name of the organization that owns the model. If ion is NOT provided this field is REQUIRED', + ) + name: str | None = Field( + None, description='Name of a contact person within the organization' + ) + email: str | None = Field( + None, description='Email address of the contact person or organization' + ) + role: str | None = Field( + None, + description='Role of the contact person. This field should only be set when the name field is set', + ) + + +class Artifact(BaseModel): + uri: str | None = Field(None, description='The URI of the model artifact') + content_type: str | None = Field( + None, + alias='content-type', + description="The content type of the model artifact. Recognized values are 'application/onnx', to refer to an ONNX representation of the model", + ) + md5_checksum: str | None = Field( + None, + alias='md5-checksum', + description='Optional checksum for the content of the file', + ) + + +class Label(BaseModel): + name: str = Field(..., description='The name of the label.') + dtype: str = Field(..., description='The data type of the label.') + value: str = Field(..., description='The value of the label.') + + +class Parameter(BaseModel): + name: str = Field(..., description='The name of the parameter.') + dtype: str | None = Field(None, description='The data type of the parameter.') + value: str | None = Field(None, description='The value of the parameter.') + labels: list[Label] | None = None + + +class TaskItem(BaseModel): + type: str = Field(..., description='The task of the model') + name: str | None = Field( + None, description='The (pretty) name for the model tasks' + ) + + +class Dataset(BaseModel): + type: str = Field(..., description='The type of the dataset') + name: str = Field(..., description='The name of the dataset') + split: str | None = Field( + None, description='The dataset split (e.g., train, test, validation).' + ) + features: list[str] | None = None + revision: str | None = Field(None, description='Version of the dataset') + + +class Label1(BaseModel): + name: str | None = Field(None, description='The name of the feature') + type: str | None = Field(None, description='The type of the label') + dtype: str | None = Field(None, description='The data type of the feature') + value: str | None = Field(None, description='The value of the feature.') + + +class Metric(BaseModel): + type: str = Field(..., description='Metric-id from Hugging Face Metrics') + name: str = Field(..., description='A descriptive name of the metric.') + dtype: str = Field(..., description='The data type of the metric') + value: str | int | float = Field(..., description='The value of the metric') + labels: list[Label1] | None = Field( + None, description='This field allows to store meta information about a metric' + ) + + +class Result1(BaseModel): + name: str = Field(..., description='The name of the bar') + value: float = Field(..., description='The value of the corresponding bar') + + +class BarPlot(BaseModel): + type: str = Field(..., description='The type of the bar plot') + name: str | None = Field(None, description='A (pretty) name for the plot') + results: list[Result1] + + +class Datum(BaseModel): + x_value: float = Field(..., description='The x-value of the graph') + y_value: float = Field(..., description='The y-value of the graph') + + +class Result2(BaseModel): + class_: str | int | float | bool | None = Field( + None, + alias='class', + description='The output class name that the graph corresponds to', + ) + feature: str = Field(..., description='The feature the graph corresponds to') + data: list[Datum] + + +class GraphPlot(BaseModel): + type: str = Field(..., description='The type of the graph plot') + name: str | None = Field(None, description='A (pretty) name of the graph') + results: list[Result2] + + +class Measurements(BaseModel): + bar_plots: list[BarPlot] | None = None + graph_plots: list[GraphPlot] | None = None + + +class Result(BaseModel): + task: list[TaskItem] + datasets: list[Dataset] + metrics: list[Metric] + measurements: Measurements + + +class ModelIndexItem(BaseModel): + name: str | None = Field(None, description='The name of the model') + model: str | None = Field( + None, description='A URI pointing to a repository containing the model file' + ) + artifacts: list[Artifact] | None = None + parameters: list[Parameter] | None = None + results: list[Result] + + +class ModelCardSchema(BaseModel): + provenance: Provenance | None = None + name: str | None = Field(None, description='Name of the model') + language: list[str] | None = Field( + None, description='The natural languages the model supports in ISO 639' + ) + license: License + tags: list[str] | None = Field( + None, description='Tags with keywords to describe the project' + ) + owners: list[Owner] | None = None + model_index: list[ModelIndexItem] = Field(alias="model-index") diff --git a/amt/schema/requirement.py b/amt/schema/requirement.py index 1e13351d..d6d2e314 100644 --- a/amt/schema/requirement.py +++ b/amt/schema/requirement.py @@ -1,7 +1,9 @@ from pydantic import BaseModel, Field +from amt.schema.shared import IterMixin -class RequirementBase(BaseModel): + +class RequirementBase(BaseModel, IterMixin): urn: str diff --git a/amt/schema/shared.py b/amt/schema/shared.py new file mode 100644 index 00000000..ceb8b94b --- /dev/null +++ b/amt/schema/shared.py @@ -0,0 +1,4 @@ +class IterMixin: + def __iter__(self): + for attr, value in self.__dict__.items(): + yield attr, value diff --git a/amt/schema/system_card.py b/amt/schema/system_card.py index f72312b0..bcefc7a8 100644 --- a/amt/schema/system_card.py +++ b/amt/schema/system_card.py @@ -9,15 +9,17 @@ from amt.schema.assessment_card import AssessmentCard from amt.schema.instrument import InstrumentBase from amt.schema.measure import MeasureTask +from amt.schema.model_card import ModelCardSchema from amt.schema.requirement import RequirementTask +from amt.schema.shared import IterMixin -class Reference(BaseModel): +class Reference(BaseModel, IterMixin): name: str = Field(default=None) link: str = Field(default=None) -class SystemCard(BaseModel): +class SystemCard(BaseModel, IterMixin): schema_version: str = Field(default="0.1a10") name: str = Field(default=None) ai_act_profile: AiActProfile = Field(default=None) @@ -30,3 +32,4 @@ class SystemCard(BaseModel): measures: list[MeasureTask] = Field(default=[]) assessments: list[AssessmentCard] = Field(default=[]) references: list[Reference] = Field(default=[]) + models: list[ModelCardSchema] = Field(default=[]) diff --git a/amt/site/templates/macros/cards.html.j2 b/amt/site/templates/macros/cards.html.j2 index 9cfdf0fe..b8e53135 100644 --- a/amt/site/templates/macros/cards.html.j2 +++ b/amt/site/templates/macros/cards.html.j2 @@ -5,16 +5,19 @@ {{ attribute.capitalize().replace("_", " ") }} {%- endmacro %} {% macro render_value(key, value, depth) -%} - {% if value.__class__.__name__ == 'dict' %} + {% if isinstance(value, 'IterMixin') or isinstance(value, 'dict') %}
{% trans %}Value{% endtrans %} | - {% for key, value in model.items() %} + {% for key, value in model %} {% if key != "name" %}
---|
{% trans %}Value{% endtrans %} |