diff --git a/cookbook/models.py b/cookbook/models.py index e89398d71..09c0f0333 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -427,6 +427,7 @@ class AiProvider(models.Model): class AiLog(models.Model, PermissionModelMixin): F_FILE_IMPORT = 'FILE_IMPORT' F_STEP_SORT = 'STEP_SORT' + F_FOOD_PROPERTIES = 'FOOD_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 b7162e72b..eee52f0b5 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -1088,6 +1088,82 @@ class FoodViewSet(LoggingMixin, TreeMixin, DeleteRelationMixing): return JsonResponse({'msg': 'there was an error parsing the FDC data, please check the server logs'}, status=500, json_dumps_params={'indent': 4}) + @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_FOOD_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 food and the following different types of properties please update the food 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 for the amount specified in the properties_food_amount attribute of the food, which is given in the properties_food_unit." + "property_amount is a decimal number. Please try to keep a percision 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." + "Only return values if you are sure they are meant for the food given. 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) + def destroy(self, *args, **kwargs): try: return (super().destroy(self, *args, **kwargs)) @@ -2433,14 +2509,14 @@ class AiStepSortView(APIView): 'error': True, 'msg': _('You must select an AI provider to perform your request.'), } - return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_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(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST) + 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() diff --git a/vue3/src/components/buttons/AiActionButton.vue b/vue3/src/components/buttons/AiActionButton.vue index c707caab7..51706d11d 100644 --- a/vue3/src/components/buttons/AiActionButton.vue +++ b/vue3/src/components/buttons/AiActionButton.vue @@ -1,10 +1,15 @@