diff --git a/internal/api/docs/openapi.yaml b/internal/api/docs/openapi.yaml index 0b767d6f..f50969e6 100644 --- a/internal/api/docs/openapi.yaml +++ b/internal/api/docs/openapi.yaml @@ -1319,15 +1319,15 @@ components: $ref: '#/components/schemas/CodeExample' nullable: true type: array - description: - type: string - id: - type: string - models: + compatible_models: items: $ref: '#/components/schemas/AIModel' nullable: true type: array + description: + type: string + id: + type: string name: type: string readme: @@ -1350,6 +1350,11 @@ components: type: string category: type: string + compatible_models: + items: + $ref: '#/components/schemas/AIModel' + nullable: true + type: array config_variables: items: $ref: '#/components/schemas/BrickConfigVariable' diff --git a/internal/e2e/client/client.gen.go b/internal/e2e/client/client.gen.go index 2325f388..1f3e6bbd 100644 --- a/internal/e2e/client/client.gen.go +++ b/internal/e2e/client/client.gen.go @@ -142,29 +142,30 @@ type BrickCreateUpdateRequest struct { // BrickDetailsResult defines model for BrickDetailsResult. type BrickDetailsResult struct { - ApiDocsPath *string `json:"api_docs_path,omitempty"` - Author *string `json:"author,omitempty"` - Category *string `json:"category,omitempty"` - CodeExamples *[]CodeExample `json:"code_examples"` - Description *string `json:"description,omitempty"` - Id *string `json:"id,omitempty"` - Models *[]AIModel `json:"models"` - Name *string `json:"name,omitempty"` - Readme *string `json:"readme,omitempty"` - Status *string `json:"status,omitempty"` - UsedByApps *[]AppReference `json:"used_by_apps"` - Variables *map[string]BrickVariable `json:"variables,omitempty"` + ApiDocsPath *string `json:"api_docs_path,omitempty"` + Author *string `json:"author,omitempty"` + Category *string `json:"category,omitempty"` + CodeExamples *[]CodeExample `json:"code_examples"` + CompatibleModels *[]AIModel `json:"compatible_models"` + Description *string `json:"description,omitempty"` + Id *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Readme *string `json:"readme,omitempty"` + Status *string `json:"status,omitempty"` + UsedByApps *[]AppReference `json:"used_by_apps"` + Variables *map[string]BrickVariable `json:"variables,omitempty"` } // BrickInstance defines model for BrickInstance. type BrickInstance struct { - Author *string `json:"author,omitempty"` - Category *string `json:"category,omitempty"` - ConfigVariables *[]BrickConfigVariable `json:"config_variables,omitempty"` - Id *string `json:"id,omitempty"` - Model *string `json:"model,omitempty"` - Name *string `json:"name,omitempty"` - Status *string `json:"status,omitempty"` + Author *string `json:"author,omitempty"` + Category *string `json:"category,omitempty"` + CompatibleModels *[]AIModel `json:"compatible_models"` + ConfigVariables *[]BrickConfigVariable `json:"config_variables,omitempty"` + Id *string `json:"id,omitempty"` + Model *string `json:"model,omitempty"` + Name *string `json:"name,omitempty"` + Status *string `json:"status,omitempty"` // Variables Deprecated: use config_variables instead. This field is kept for backward compatibility. Variables *map[string]string `json:"variables,omitempty"` diff --git a/internal/e2e/daemon/brick_test.go b/internal/e2e/daemon/brick_test.go index 488a670a..b5f2b6d8 100644 --- a/internal/e2e/daemon/brick_test.go +++ b/internal/e2e/daemon/brick_test.go @@ -144,7 +144,7 @@ func TestBricksDetails(t *testing.T) { require.NotEmpty(t, *response.JSON200.Readme) require.NotNil(t, response.JSON200.UsedByApps, "UsedByApps should not be nil") require.Equal(t, expectedUsedByApps, *(response.JSON200.UsedByApps)) - require.NotNil(t, response.JSON200.Models, "Models should not be nil") - require.Equal(t, expectedModelLiteInfo, *(response.JSON200.Models)) + require.NotNil(t, response.JSON200.CompatibleModels, "Models should not be nil") + require.Equal(t, expectedModelLiteInfo, *(response.JSON200.CompatibleModels)) }) } diff --git a/internal/e2e/daemon/instance_bricks_test.go b/internal/e2e/daemon/bricks_instance_test.go similarity index 93% rename from internal/e2e/daemon/instance_bricks_test.go rename to internal/e2e/daemon/bricks_instance_test.go index c210a9e6..0a6375bc 100644 --- a/internal/e2e/daemon/instance_bricks_test.go +++ b/internal/e2e/daemon/bricks_instance_test.go @@ -51,6 +51,18 @@ var ( Value: f.Ptr("/models/ootb/ei/mobilenet-v2-224px.eim"), }, } + + expectedModelInfo = []client.AIModel{ + { + Id: f.Ptr("mobilenet-image-classification"), + Name: f.Ptr("General purpose image classification"), + Description: f.Ptr("General purpose image classification model based on MobileNetV2. This model is trained on the ImageNet dataset and can classify images into 1000 categories."), + }, + { + Id: f.Ptr("person-classification"), + Name: f.Ptr("Person classification"), + Description: f.Ptr("Person classification model based on WakeVision dataset. This model is trained to classify images into two categories: person and not-person."), + }} ) func setupTestApp(t *testing.T) (*client.CreateAppResp, *client.ClientWithResponses) { @@ -78,7 +90,6 @@ func setupTestApp(t *testing.T) (*client.CreateAppResp, *client.ClientWithRespon ) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode()) - return createResp, httpClient } @@ -135,6 +146,20 @@ func TestGetAppBrickInstanceById(t *testing.T) { require.NotEmpty(t, brickInstance.JSON200) require.Equal(t, ImageClassifactionBrickID, *brickInstance.JSON200.Id) require.Equal(t, expectedConfigVariables, (*brickInstance.JSON200.ConfigVariables)) + require.NotNil(t, brickInstance.JSON200.CompatibleModels) + require.Equal(t, expectedModelInfo, *(brickInstance.JSON200.CompatibleModels)) + }) + t.Run("GetAppBrickInstanceByBrickIDWithCompatibleModels_Success", func(t *testing.T) { + brickInstance, err := httpClient.GetAppBrickInstanceByBrickIDWithResponse( + t.Context(), + *createResp.JSON201.Id, + ImageClassifactionBrickID, + func(ctx context.Context, req *http.Request) error { return nil }) + require.NoError(t, err) + require.NotEmpty(t, brickInstance.JSON200) + require.Equal(t, ImageClassifactionBrickID, *brickInstance.JSON200.Id) + require.NotNil(t, brickInstance.JSON200.CompatibleModels) + require.Equal(t, expectedModelInfo, *(brickInstance.JSON200.CompatibleModels)) }) t.Run("GetAppBrickInstanceByBrickID_InvalidAppID_Fails", func(t *testing.T) { diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index bcc4f016..3b722b3d 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -121,6 +121,13 @@ func (s *Service) AppBrickInstanceDetails(a *app.ArduinoApp, brickID string) (Br Variables: variables, ConfigVariables: configVariables, ModelID: modelID, + CompatibleModels: f.Map(s.modelsIndex.GetModelsByBrick(brick.ID), func(m modelsindex.AIModel) AIModel { + return AIModel{ + ID: m.ID, + Name: m.Name, + Description: m.ModuleDescription, + } + }), }, nil } @@ -202,7 +209,7 @@ func (s *Service) BricksDetails(id string, idProvider *app.IDProvider, ApiDocsPath: apiDocsPath, CodeExamples: codeExamples, UsedByApps: usedByApps, - Models: f.Map(s.modelsIndex.GetModelsByBrick(brick.ID), func(m modelsindex.AIModel) AIModel { + CompatibleModels: f.Map(s.modelsIndex.GetModelsByBrick(brick.ID), func(m modelsindex.AIModel) AIModel { return AIModel{ ID: m.ID, Name: m.Name, diff --git a/internal/orchestrator/bricks/bricks_test.go b/internal/orchestrator/bricks/bricks_test.go index 8f378e20..f9804b25 100644 --- a/internal/orchestrator/bricks/bricks_test.go +++ b/internal/orchestrator/bricks/bricks_test.go @@ -418,13 +418,13 @@ func TestBricksDetails(t *testing.T) { require.Len(t, res.UsedByApps, 1) require.Equal(t, "My App", res.UsedByApps[0].Name) require.NotEmpty(t, res.UsedByApps[0].ID) - require.Len(t, res.Models, 2) - require.Equal(t, "yolox-object-detection", res.Models[0].ID) - require.Equal(t, "General purpose object detection - YoloX", res.Models[0].Name) - require.Equal(t, "General purpose object detection...", res.Models[0].Description) - require.Equal(t, "face-detection", res.Models[1].ID) - require.Equal(t, "Lightweight-Face-Detection", res.Models[1].Name) - require.Equal(t, "", res.Models[1].Description) + require.Len(t, res.CompatibleModels, 2) + require.Equal(t, "yolox-object-detection", res.CompatibleModels[0].ID) + require.Equal(t, "General purpose object detection - YoloX", res.CompatibleModels[0].Name) + require.Equal(t, "General purpose object detection...", res.CompatibleModels[0].Description) + require.Equal(t, "face-detection", res.CompatibleModels[1].ID) + require.Equal(t, "Lightweight-Face-Detection", res.CompatibleModels[1].Name) + require.Equal(t, "", res.CompatibleModels[1].Description) }) t.Run("Success - Full Details - no models", func(t *testing.T) { @@ -443,7 +443,7 @@ func TestBricksDetails(t *testing.T) { require.Len(t, res.UsedByApps, 1) require.Equal(t, "My App", res.UsedByApps[0].Name) require.NotEmpty(t, res.UsedByApps[0].ID) - require.Len(t, res.Models, 0) + require.Len(t, res.CompatibleModels, 0) }) t.Run("Success - Full Details - one model", func(t *testing.T) { @@ -452,10 +452,10 @@ func TestBricksDetails(t *testing.T) { require.Equal(t, "arduino:one_model_brick", res.ID) require.Equal(t, "one model brick", res.Name) - require.Len(t, res.Models, 1) - require.Equal(t, "face-detection", res.Models[0].ID) - require.Equal(t, "Lightweight-Face-Detection", res.Models[0].Name) - require.Equal(t, "", res.Models[0].Description) + require.Len(t, res.CompatibleModels, 1) + require.Equal(t, "face-detection", res.CompatibleModels[0].ID) + require.Equal(t, "Lightweight-Face-Detection", res.CompatibleModels[0].Name) + require.Equal(t, "", res.CompatibleModels[0].Description) }) } @@ -489,3 +489,153 @@ bricks: require.NoError(t, os.MkdirAll(pythonDir, 0755)) require.NoError(t, os.WriteFile(filepath.Join(pythonDir, "main.py"), []byte("print('hello')"), 0600)) } + +func TestAppBrickInstanceModelsDetails(t *testing.T) { + + bIndex := &bricksindex.BricksIndex{ + Bricks: []bricksindex.Brick{ + { + ID: "arduino:object_detection", + Name: "Object Detection", + Category: "video", + ModelName: "yolox-object-detection", // Default model + Variables: []bricksindex.BrickVariable{ + {Name: "EI_OBJ_DETECTION_MODEL", DefaultValue: "default_path", Description: "path to the model file"}, + {Name: "CUSTOM_MODEL_PATH", DefaultValue: "/home/arduino/.arduino-bricks/ei-models", Description: "path to the custom model directory"}, + }, + }, + { + ID: "arduino:weather_forecast", + Name: "Weather Forecast", + Category: "miscellaneous", + ModelName: "", + }, + }, + } + + mIndex := &modelsindex.ModelsIndex{ + Models: []modelsindex.AIModel{ + + { + ID: "yolox-object-detection", + Name: "General purpose object detection - YoloX", + ModuleDescription: "General purpose object detection...", + Bricks: []string{"arduino:object_detection", "arduino:video_object_detection"}, + }, + { + ID: "face-detection", + Name: "Lightweight-Face-Detection", + Bricks: []string{"arduino:object_detection", "arduino:video_object_detection"}, + }, + }} + + svc := &Service{ + bricksIndex: bIndex, + modelsIndex: mIndex, + } + + tests := []struct { + name string + app *app.ArduinoApp + brickID string + expectedError string + validate func(*testing.T, BrickInstance) + }{ + { + name: "Brick not found in global Index", + brickID: "arduino:non_existent_brick", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{Bricks: []app.Brick{}}, + }, + expectedError: "brick not found", + }, + { + name: "Brick found in Index but not added to App", + brickID: "arduino:object_detection", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + {ID: "arduino:weather_forecast"}, + }, + }, + }, + expectedError: "brick arduino:object_detection not added in the app", + }, + { + name: "Success - Standard Brick without Model", + brickID: "arduino:weather_forecast", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + {ID: "arduino:weather_forecast"}, + }, + }, + }, + validate: func(t *testing.T, res BrickInstance) { + require.Equal(t, "arduino:weather_forecast", res.ID) + require.Equal(t, "Weather Forecast", res.Name) + require.Equal(t, "installed", res.Status) + require.Empty(t, res.ModelID) + require.Empty(t, res.CompatibleModels) + }, + }, + { + name: "Success - Brick with Default Model", + brickID: "arduino:object_detection", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + { + ID: "arduino:object_detection", + }, + }, + }, + }, + validate: func(t *testing.T, res BrickInstance) { + require.Equal(t, "arduino:object_detection", res.ID) + require.Equal(t, "yolox-object-detection", res.ModelID) + require.Len(t, res.CompatibleModels, 2) + require.Equal(t, "yolox-object-detection", res.CompatibleModels[0].ID) + require.Equal(t, "face-detection", res.CompatibleModels[1].ID) + }, + }, + { + name: "Success - Brick with Overridden Model in App", + brickID: "arduino:object_detection", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + { + ID: "arduino:object_detection", + Model: "face-detection", + }, + }, + }, + }, + validate: func(t *testing.T, res BrickInstance) { + require.Equal(t, "arduino:object_detection", res.ID) + require.Equal(t, "face-detection", res.ModelID) + require.Len(t, res.CompatibleModels, 2) + require.Equal(t, "yolox-object-detection", res.CompatibleModels[0].ID) + require.Equal(t, "face-detection", res.CompatibleModels[1].ID) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := svc.AppBrickInstanceDetails(tt.app, tt.brickID) + + if tt.expectedError != "" { + require.Error(t, err) + require.Equal(t, err.Error(), tt.expectedError) + return + } + + require.NoError(t, err) + if tt.validate != nil { + tt.validate(t, result) + } + }) + } +} diff --git a/internal/orchestrator/bricks/types.go b/internal/orchestrator/bricks/types.go index f27b0652..782ec2f2 100644 --- a/internal/orchestrator/bricks/types.go +++ b/internal/orchestrator/bricks/types.go @@ -33,16 +33,22 @@ type AppBrickInstancesResult struct { } type BrickInstance struct { - ID string `json:"id"` - Name string `json:"name"` - Author string `json:"author"` - Category string `json:"category"` - Status string `json:"status"` - Variables map[string]string `json:"variables,omitempty" description:"Deprecated: use config_variables instead. This field is kept for backward compatibility."` - ConfigVariables []BrickConfigVariable `json:"config_variables,omitempty"` - ModelID string `json:"model,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Author string `json:"author"` + Category string `json:"category"` + Status string `json:"status"` + Variables map[string]string `json:"variables,omitempty" description:"Deprecated: use config_variables instead. This field is kept for backward compatibility."` + ConfigVariables []BrickConfigVariable `json:"config_variables,omitempty"` + ModelID string `json:"model,omitempty"` + CompatibleModels []AIModel `json:"compatible_models"` } +type AIModel struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} type BrickConfigVariable struct { Name string `json:"name"` Value string `json:"value"` @@ -66,22 +72,16 @@ type AppReference struct { } type BrickDetailsResult struct { - ID string `json:"id"` - Name string `json:"name"` - Author string `json:"author"` - Description string `json:"description"` - Category string `json:"category"` - Status string `json:"status"` - Variables map[string]BrickVariable `json:"variables,omitempty"` - Readme string `json:"readme"` - ApiDocsPath string `json:"api_docs_path"` - CodeExamples []CodeExample `json:"code_examples"` - UsedByApps []AppReference `json:"used_by_apps"` - Models []AIModel `json:"models"` -} - -type AIModel struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` + ID string `json:"id"` + Name string `json:"name"` + Author string `json:"author"` + Description string `json:"description"` + Category string `json:"category"` + Status string `json:"status"` + Variables map[string]BrickVariable `json:"variables,omitempty"` + Readme string `json:"readme"` + ApiDocsPath string `json:"api_docs_path"` + CodeExamples []CodeExample `json:"code_examples"` + UsedByApps []AppReference `json:"used_by_apps"` + CompatibleModels []AIModel `json:"compatible_models"` }