mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-04 05:39:00 -05:00
added AI step sorter
This commit is contained in:
@@ -33,12 +33,14 @@ class AiCallbackHandler(CustomLogger):
|
|||||||
space = None
|
space = None
|
||||||
user = None
|
user = None
|
||||||
ai_provider = None
|
ai_provider = None
|
||||||
|
function = None
|
||||||
|
|
||||||
def __init__(self, space, user, ai_provider):
|
def __init__(self, space, user, ai_provider, function):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.space = space
|
self.space = space
|
||||||
self.user = user
|
self.user = user
|
||||||
self.ai_provider = ai_provider
|
self.ai_provider = ai_provider
|
||||||
|
self.function = function
|
||||||
|
|
||||||
def log_pre_api_call(self, model, messages, kwargs):
|
def log_pre_api_call(self, model, messages, kwargs):
|
||||||
pass
|
pass
|
||||||
@@ -77,7 +79,7 @@ class AiCallbackHandler(CustomLogger):
|
|||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
input_tokens=response_obj['usage']['prompt_tokens'],
|
input_tokens=response_obj['usage']['prompt_tokens'],
|
||||||
output_tokens=response_obj['usage']['completion_tokens'],
|
output_tokens=response_obj['usage']['completion_tokens'],
|
||||||
function=AiLog.F_FILE_IMPORT,
|
function=self.function,
|
||||||
credit_cost=credit_cost,
|
credit_cost=credit_cost,
|
||||||
credits_from_balance=credits_from_balance,
|
credits_from_balance=credits_from_balance,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ class AiProvider(models.Model):
|
|||||||
|
|
||||||
class AiLog(models.Model, PermissionModelMixin):
|
class AiLog(models.Model, PermissionModelMixin):
|
||||||
F_FILE_IMPORT = 'FILE_IMPORT'
|
F_FILE_IMPORT = 'FILE_IMPORT'
|
||||||
|
F_STEP_SORT = 'STEP_SORT'
|
||||||
|
|
||||||
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
|
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
|
||||||
function = models.CharField(max_length=64)
|
function = models.CharField(max_length=64)
|
||||||
@@ -447,7 +448,7 @@ class AiLog(models.Model, PermissionModelMixin):
|
|||||||
return f"{self.function} {self.ai_provider.name} {self.created_at}"
|
return f"{self.function} {self.ai_provider.name} {self.created_at}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('id',)
|
ordering = ('-created_at',)
|
||||||
|
|
||||||
|
|
||||||
class ConnectorConfig(models.Model, PermissionModelMixin):
|
class ConnectorConfig(models.Model, PermissionModelMixin):
|
||||||
|
|||||||
@@ -151,19 +151,22 @@ class CustomOnHandField(serializers.Field):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
if not self.context["request"].user.is_authenticated:
|
try:
|
||||||
|
if not self.context["request"].user.is_authenticated:
|
||||||
|
return []
|
||||||
|
shared_users = []
|
||||||
|
if c := caches['default'].get(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
|
||||||
|
shared_users = c
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||||
|
caches['default'].set(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', shared_users, timeout=5 * 60)
|
||||||
|
# TODO ugly hack that improves API performance significantly, should be done properly
|
||||||
|
except AttributeError: # Anonymous users (using share links) don't have shared users
|
||||||
|
pass
|
||||||
|
return obj.onhand_users.filter(id__in=shared_users).exists()
|
||||||
|
except AttributeError:
|
||||||
return []
|
return []
|
||||||
shared_users = []
|
|
||||||
if c := caches['default'].get(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
|
|
||||||
shared_users = c
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
|
||||||
caches['default'].set(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', shared_users, timeout=5 * 60)
|
|
||||||
# TODO ugly hack that improves API performance significantly, should be done properly
|
|
||||||
except AttributeError: # Anonymous users (using share links) don't have shared users
|
|
||||||
pass
|
|
||||||
return obj.onhand_users.filter(id__in=shared_users).exists()
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
return data
|
return data
|
||||||
@@ -843,28 +846,31 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
|
|
||||||
@extend_schema_field(bool)
|
@extend_schema_field(bool)
|
||||||
def get_substitute_onhand(self, obj):
|
def get_substitute_onhand(self, obj):
|
||||||
if not self.context["request"].user.is_authenticated:
|
try:
|
||||||
|
if not self.context["request"].user.is_authenticated:
|
||||||
|
return []
|
||||||
|
shared_users = []
|
||||||
|
if c := caches['default'].get(
|
||||||
|
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
|
||||||
|
shared_users = c
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
||||||
|
self.context['request'].user.id]
|
||||||
|
caches['default'].set(
|
||||||
|
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
|
||||||
|
shared_users, timeout=5 * 60)
|
||||||
|
# TODO ugly hack that improves API performance significantly, should be done properly
|
||||||
|
except AttributeError: # Anonymous users (using share links) don't have shared users
|
||||||
|
pass
|
||||||
|
filter = Q(id__in=obj.substitute.all())
|
||||||
|
if obj.substitute_siblings:
|
||||||
|
filter |= Q(path__startswith=obj.path[:Food.steplen * (obj.depth - 1)], depth=obj.depth)
|
||||||
|
if obj.substitute_children:
|
||||||
|
filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
|
||||||
|
return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exists()
|
||||||
|
except AttributeError:
|
||||||
return []
|
return []
|
||||||
shared_users = []
|
|
||||||
if c := caches['default'].get(
|
|
||||||
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
|
|
||||||
shared_users = c
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
|
||||||
self.context['request'].user.id]
|
|
||||||
caches['default'].set(
|
|
||||||
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
|
|
||||||
shared_users, timeout=5 * 60)
|
|
||||||
# TODO ugly hack that improves API performance significantly, should be done properly
|
|
||||||
except AttributeError: # Anonymous users (using share links) don't have shared users
|
|
||||||
pass
|
|
||||||
filter = Q(id__in=obj.substitute.all())
|
|
||||||
if obj.substitute_siblings:
|
|
||||||
filter |= Q(path__startswith=obj.path[:Food.steplen * (obj.depth - 1)], depth=obj.depth)
|
|
||||||
if obj.substitute_children:
|
|
||||||
filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
|
|
||||||
return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exists()
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
name = validated_data['name'].strip()
|
name = validated_data['name'].strip()
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ urlpatterns = [
|
|||||||
path('api/sync_all/', api.sync_all, name='api_sync'),
|
path('api/sync_all/', api.sync_all, name='api_sync'),
|
||||||
path('api/recipe-from-source/', api.RecipeUrlImportView.as_view(), name='api_recipe_from_source'),
|
path('api/recipe-from-source/', api.RecipeUrlImportView.as_view(), name='api_recipe_from_source'),
|
||||||
path('api/ai-import/', api.AiImportView.as_view(), name='api_ai_import'),
|
path('api/ai-import/', api.AiImportView.as_view(), name='api_ai_import'),
|
||||||
|
path('api/ai-step-sort/', api.AiStepSortView.as_view(), name='api_ai_step_sort'),
|
||||||
path('api/import-open-data/', api.ImportOpenData.as_view(), name='api_import_open_data'),
|
path('api/import-open-data/', api.ImportOpenData.as_view(), name='api_import_open_data'),
|
||||||
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
||||||
path('api/fdc-search/', api.FdcSearchView.as_view(), name='api_fdc_search'),
|
path('api/fdc-search/', api.FdcSearchView.as_view(), name='api_fdc_search'),
|
||||||
|
|||||||
@@ -2296,7 +2296,7 @@ class AiImportView(APIView):
|
|||||||
|
|
||||||
ai_provider = AiProvider.objects.filter(pk=serializer.validated_data['ai_provider_id']).filter(Q(space=request.space) | Q(space__isnull=True)).first()
|
ai_provider = AiProvider.objects.filter(pk=serializer.validated_data['ai_provider_id']).filter(Q(space=request.space) | Q(space__isnull=True)).first()
|
||||||
|
|
||||||
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider)]
|
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider, AiLog.F_FILE_IMPORT)]
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
uploaded_file = serializer.validated_data['file']
|
uploaded_file = serializer.validated_data['file']
|
||||||
@@ -2413,6 +2413,80 @@ class AiImportView(APIView):
|
|||||||
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
|
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class AiStepSortView(APIView):
|
||||||
|
throttle_classes = [AiEndpointThrottle]
|
||||||
|
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||||
|
|
||||||
|
@extend_schema(request=RecipeSerializer(many=False), responses=RecipeSerializer(many=False),
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(name='provider', description='ID of the AI provider that should be used for this AI request', type=int),
|
||||||
|
])
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
given an image or PDF file convert its content to a structured recipe using AI and the scraping system
|
||||||
|
"""
|
||||||
|
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(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(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)
|
||||||
|
|
||||||
|
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_STEP_SORT)]
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "You are given data for a recipe formatted as json. You cannot under any circumstance change the value of any of the fields. You are only allowed to split the instructions into multiple steps and to sort the ingredients to their appropriate step. Your goal is to properly structure the recipe by splitting large instructions into multiple coherent steps and putting the ingredients that belong to this step into the ingredients list. Generally an ingredient of a cooking recipe should occur in the first step where its needed. Please sort the ingredients to the appropriate steps without changing any of the actual field values. Return the recipe in the same format you were given as json. Do not change any field value like strings or numbers, or change the sorting, also do not change the language."
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": json.dumps(request.data)
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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
|
||||||
|
# TODO validate by loading/dumping using serializer ?
|
||||||
|
|
||||||
|
return Response(json.loads(response_text), status=status.HTTP_200_OK)
|
||||||
|
except BadRequestError as err:
|
||||||
|
response = {
|
||||||
|
'error': True,
|
||||||
|
'msg': 'The AI could not process your request. \n\n' + err.message,
|
||||||
|
}
|
||||||
|
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class AppImportView(APIView):
|
class AppImportView(APIView):
|
||||||
parser_classes = [MultiPartParser]
|
parser_classes = [MultiPartParser]
|
||||||
throttle_classes = [RecipeImportThrottle]
|
throttle_classes = [RecipeImportThrottle]
|
||||||
|
|||||||
65
vue3/src/components/buttons/AiActionButton.vue
Normal file
65
vue3/src/components/buttons/AiActionButton.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<v-btn v-bind="props" :color="props.color" :variant="props.variant" :density="props.density" :icon="props.icon" :prepend-icon="props.prependIcon" :loading="props.loading">
|
||||||
|
{{ props.text }}
|
||||||
|
|
||||||
|
<v-menu activator="parent">
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
v-for="provider in aiProviders"
|
||||||
|
:key="provider.id!"
|
||||||
|
@click="emit('selected', provider.id!)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ provider.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {AiProvider, ApiApi} from "@/openapi";
|
||||||
|
import {onMounted, PropType, ref} from "vue";
|
||||||
|
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits(['selected'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: {type: String, default: 'AI'},
|
||||||
|
color: {type: String, default: ''},
|
||||||
|
variant: {type: undefined, default: undefined},
|
||||||
|
density: {type: undefined, default: undefined},
|
||||||
|
icon: {type: String, default: undefined},
|
||||||
|
prependIcon: {type: String, default: undefined},
|
||||||
|
loading: {type: Boolean, default: false},
|
||||||
|
})
|
||||||
|
|
||||||
|
const aiProviders = ref([] as AiProvider[])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadAiProviders()
|
||||||
|
})
|
||||||
|
|
||||||
|
function loadAiProviders() {
|
||||||
|
let api = new ApiApi()
|
||||||
|
|
||||||
|
api.apiAiProviderList().then(r => {
|
||||||
|
aiProviders.value = r.results
|
||||||
|
}).catch(err => {
|
||||||
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
v-if="!mobile">{{ $t('Split') }}</span></v-btn>
|
v-if="!mobile">{{ $t('Split') }}</span></v-btn>
|
||||||
<v-btn prepend-icon="fa-solid fa-minimize" @click="handleMergeAllSteps" :disabled="editingObj.steps.length < 2"><span
|
<v-btn prepend-icon="fa-solid fa-minimize" @click="handleMergeAllSteps" :disabled="editingObj.steps.length < 2"><span
|
||||||
v-if="!mobile">{{ $t('Merge') }}</span></v-btn>
|
v-if="!mobile">{{ $t('Merge') }}</span></v-btn>
|
||||||
|
<ai-action-button :text="$t('Auto_Sort')" prepend-icon="$ai" :loading="aiStepSortLoading" @selected="aiStepSort"></ai-action-button>
|
||||||
</v-btn-group>
|
</v-btn-group>
|
||||||
|
|
||||||
|
|
||||||
@@ -173,6 +174,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
|||||||
import {mergeAllSteps, splitAllSteps} from "@/utils/step_utils.ts";
|
import {mergeAllSteps, splitAllSteps} from "@/utils/step_utils.ts";
|
||||||
import DeleteConfirmDialog from "@/components/dialogs/DeleteConfirmDialog.vue";
|
import DeleteConfirmDialog from "@/components/dialogs/DeleteConfirmDialog.vue";
|
||||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
||||||
|
import AiActionButton from "@/components/buttons/AiActionButton.vue";
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -202,6 +204,8 @@ const dialogStepManager = ref(false)
|
|||||||
const {fileApiLoading, updateRecipeImage} = useFileApi()
|
const {fileApiLoading, updateRecipeImage} = useFileApi()
|
||||||
const file = shallowRef<File | null>(null)
|
const file = shallowRef<File | null>(null)
|
||||||
|
|
||||||
|
const aiStepSortLoading = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initializeEditor()
|
initializeEditor()
|
||||||
})
|
})
|
||||||
@@ -297,6 +301,22 @@ function deleteExternalFile() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sort steps and ingredients using UI and update recipe with result
|
||||||
|
* @param providerId provider to use for request
|
||||||
|
*/
|
||||||
|
function aiStepSort(providerId: number){
|
||||||
|
let api = new ApiApi()
|
||||||
|
aiStepSortLoading.value = true
|
||||||
|
api.apiAiStepSortCreate({recipe: editingObj.value, provider: providerId}).then(r => {
|
||||||
|
editingObj.value = r
|
||||||
|
}).catch(err => {
|
||||||
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
|
}).finally(() => {
|
||||||
|
aiStepSortLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -606,6 +606,11 @@ export interface ApiAiProviderUpdateRequest {
|
|||||||
aiProvider: Omit<AiProvider, 'createdAt'|'updatedAt'>;
|
aiProvider: Omit<AiProvider, 'createdAt'|'updatedAt'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiAiStepSortCreateRequest {
|
||||||
|
recipe: Omit<Recipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
|
||||||
|
provider?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiAutoPlanCreateRequest {
|
export interface ApiAutoPlanCreateRequest {
|
||||||
autoMealPlan: AutoMealPlan;
|
autoMealPlan: AutoMealPlan;
|
||||||
}
|
}
|
||||||
@@ -3327,6 +3332,50 @@ export class ApiApi extends runtime.BaseAPI {
|
|||||||
return await response.value();
|
return await response.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* given an image or PDF file convert its content to a structured recipe using AI and the scraping system
|
||||||
|
*/
|
||||||
|
async apiAiStepSortCreateRaw(requestParameters: ApiAiStepSortCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Recipe>> {
|
||||||
|
if (requestParameters['recipe'] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
'recipe',
|
||||||
|
'Required parameter "recipe" was null or undefined when calling apiAiStepSortCreate().'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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/ai-step-sort/`,
|
||||||
|
method: 'POST',
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
body: RecipeToJSON(requestParameters['recipe']),
|
||||||
|
}, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* given an image or PDF file convert its content to a structured recipe using AI and the scraping system
|
||||||
|
*/
|
||||||
|
async apiAiStepSortCreate(requestParameters: ApiAiStepSortCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Recipe> {
|
||||||
|
const response = await this.apiAiStepSortCreateRaw(requestParameters, initOverrides);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* logs request counts to redis cache total/per user/
|
* logs request counts to redis cache total/per user/
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user