From f7713a43a7f6910496935aef71e9b429d27d401a Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sun, 5 Oct 2025 12:55:44 +0200 Subject: [PATCH] fxied recipe properties editor and added recipe AI properties --- cookbook/models.py | 1 + cookbook/views/api.py | 76 ++++++++++++ .../components/inputs/PropertiesEditor.vue | 30 +++-- .../components/model_editors/RecipeEditor.vue | 4 +- vue3/src/openapi/apis/ApiApi.ts | 114 ++++++++++++++++++ 5 files changed, 214 insertions(+), 11 deletions(-) diff --git a/cookbook/models.py b/cookbook/models.py index f34a80331..afb591993 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -431,6 +431,7 @@ class AiLog(models.Model, PermissionModelMixin): F_FILE_IMPORT = 'FILE_IMPORT' F_STEP_SORT = 'STEP_SORT' F_FOOD_PROPERTIES = 'FOOD_PROPERTIES' + F_RECIPE_PROPERTIES = 'RECIPE_PROPERTIES' ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True) function = models.CharField(max_length=64) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index bc944730d..c7214612f 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -1805,6 +1805,82 @@ class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet, DeleteRelationMixing): return Response(serializer.errors, 400) + @extend_schema( + parameters=[ + OpenApiParameter(name='provider', description='ID of the AI provider that should be used for this AI request', type=int), + ] + ) + @decorators.action(detail=True, methods=['POST'], ) + def aiproperties(self, request, pk): + serializer = RecipeSerializer(data=request.data, partial=True, context={'request': request}) + if serializer.is_valid(): + + if not request.query_params.get('provider', None) or not re.match(r'^(\d)+$', request.query_params.get('provider', None)): + response = { + 'error': True, + 'msg': _('You must select an AI provider to perform your request.'), + } + return Response(response, status=status.HTTP_400_BAD_REQUEST) + + if not can_perform_ai_request(request.space): + response = { + 'error': True, + 'msg': _("You don't have any credits remaining to use AI or AI features are not enabled for your space."), + } + return Response(response, status=status.HTTP_400_BAD_REQUEST) + + ai_provider = AiProvider.objects.filter(pk=request.query_params.get('provider')).filter(Q(space=request.space) | Q(space__isnull=True)).first() + + litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider, AiLog.F_RECIPE_PROPERTIES)] + + property_type_list = list(PropertyType.objects.filter(space=request.space).values('id', 'name', 'description', 'unit', 'category', 'fdc_id')) + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Given the following recipe and the following different types of properties please update the recipe so that the properties attribute contains a list with all property types in the following format [{property_amount: , property_type: {id: , name: }}]." + "The property values should be in the unit given in the property type and calculated based on the total quantity of the foods used for the recipe." + "property_amount is a decimal number. Please try to keep a precision of two decimal places if given in your source data." + "Do not make up any data. If there is no data available for the given property type that is ok, just return null as a property_amount for that property type. Do not change anything else!" + "Most property types are likely going to be nutritional values. Please do not make up any values, only return values you can find in the sources available to you." + "Under no circumstance are you allowed to change any other value of the given food or change the structure in any way or form." + }, + { + "type": "text", + "text": json.dumps(request.data) + }, + { + "type": "text", + "text": json.dumps(property_type_list) + }, + ] + }, + ] + + try: + ai_request = { + 'api_key': ai_provider.api_key, + 'model': ai_provider.model_name, + 'response_format': {"type": "json_object"}, + 'messages': messages, + } + if ai_provider.url: + ai_request['api_base'] = ai_provider.url + ai_response = completion(**ai_request) + + response_text = ai_response.choices[0].message.content + + return Response(json.loads(response_text), status=status.HTTP_200_OK) + except BadRequestError as err: + pass + response = { + 'error': True, + 'msg': 'The AI could not process your request. \n\n' + err.message, + } + return Response(response, status=status.HTTP_400_BAD_REQUEST) + @extend_schema(responses=RecipeSerializer(many=False)) @decorators.action(detail=True, pagination_class=None, methods=['PATCH'], serializer_class=RecipeSerializer) def delete_external(self, request, pk): diff --git a/vue3/src/components/inputs/PropertiesEditor.vue b/vue3/src/components/inputs/PropertiesEditor.vue index 4e12d1097..87f98ec21 100644 --- a/vue3/src/components/inputs/PropertiesEditor.vue +++ b/vue3/src/components/inputs/PropertiesEditor.vue @@ -2,7 +2,7 @@ {{ $t('Add') }} {{ $t('AddAll') }} - {{ $t('AI') }} + {{ $t('AI') }} @@ -56,7 +56,7 @@ const isFood = computed(() => { return !('steps' in editingObj.value) }) -const editingObj = defineModel({required: true}) +const editingObj = defineModel({required: true}) const aiLoading = ref(false) @@ -94,13 +94,25 @@ function addAllProperties() { function propertiesFromAi(providerId: number) { const api = new ApiApi() aiLoading.value = true - api.apiFoodAipropertiesCreate({id: editingObj.value.id!, food: editingObj.value, provider: providerId}).then(r => { - editingObj.value = r - }).catch(err => { - useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) - }).finally(() => { - aiLoading.value = false - }) + + if (isFood.value) { + api.apiFoodAipropertiesCreate({id: editingObj.value.id!, food: editingObj.value, provider: providerId}).then(r => { + editingObj.value = r + }).catch(err => { + useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) + }).finally(() => { + aiLoading.value = false + }) + } else { + api.apiRecipeAipropertiesCreate({id: editingObj.value.id!, recipe: editingObj.value, provider: providerId}).then(r => { + editingObj.value = r + }).catch(err => { + useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) + }).finally(() => { + aiLoading.value = false + }) + } + } diff --git a/vue3/src/components/model_editors/RecipeEditor.vue b/vue3/src/components/model_editors/RecipeEditor.vue index 9ef3ebf05..49327382d 100644 --- a/vue3/src/components/model_editors/RecipeEditor.vue +++ b/vue3/src/components/model_editors/RecipeEditor.vue @@ -15,8 +15,8 @@ {{ $t('Recipe') }} {{ $t('Steps') }} - {{ $t('Properties') }} - {{ $t('Miscellaneous') }} + {{ $t('Properties') }} + {{ $t('Miscellaneous') }} diff --git a/vue3/src/openapi/apis/ApiApi.ts b/vue3/src/openapi/apis/ApiApi.ts index 9add2648c..52b484d83 100644 --- a/vue3/src/openapi/apis/ApiApi.ts +++ b/vue3/src/openapi/apis/ApiApi.ts @@ -877,6 +877,12 @@ export interface ApiEnterpriseSocialKeywordUpdateRequest { keyword: Omit; } +export interface ApiEnterpriseSocialRecipeAipropertiesCreateRequest { + id: number; + recipe: Omit; + provider?: number; +} + export interface ApiEnterpriseSocialRecipeBatchUpdateUpdateRequest { recipeBatchUpdate: RecipeBatchUpdate; } @@ -1689,6 +1695,12 @@ export interface ApiPropertyUpdateRequest { property: Property; } +export interface ApiRecipeAipropertiesCreateRequest { + id: number; + recipe: Omit; + provider?: number; +} + export interface ApiRecipeBatchUpdateUpdateRequest { recipeBatchUpdate: RecipeBatchUpdate; } @@ -5574,6 +5586,57 @@ export class ApiApi extends runtime.BaseAPI { return await response.value(); } + /** + * logs request counts to redis cache total/per user/ + */ + async apiEnterpriseSocialRecipeAipropertiesCreateRaw(requestParameters: ApiEnterpriseSocialRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiEnterpriseSocialRecipeAipropertiesCreate().' + ); + } + + if (requestParameters['recipe'] == null) { + throw new runtime.RequiredError( + 'recipe', + 'Required parameter "recipe" was null or undefined when calling apiEnterpriseSocialRecipeAipropertiesCreate().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['provider'] != null) { + queryParameters['provider'] = requestParameters['provider']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/enterprise-social-recipe/{id}/aiproperties/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: RecipeToJSON(requestParameters['recipe']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiEnterpriseSocialRecipeAipropertiesCreate(requestParameters: ApiEnterpriseSocialRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiEnterpriseSocialRecipeAipropertiesCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * logs request counts to redis cache total/per user/ */ @@ -12351,6 +12414,57 @@ export class ApiApi extends runtime.BaseAPI { return await response.value(); } + /** + * logs request counts to redis cache total/per user/ + */ + async apiRecipeAipropertiesCreateRaw(requestParameters: ApiRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiRecipeAipropertiesCreate().' + ); + } + + if (requestParameters['recipe'] == null) { + throw new runtime.RequiredError( + 'recipe', + 'Required parameter "recipe" was null or undefined when calling apiRecipeAipropertiesCreate().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['provider'] != null) { + queryParameters['provider'] = requestParameters['provider']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/recipe/{id}/aiproperties/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: RecipeToJSON(requestParameters['recipe']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiRecipeAipropertiesCreate(requestParameters: ApiRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiRecipeAipropertiesCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * logs request counts to redis cache total/per user/ */