mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-03-12 04:35:35 -07:00
feat: OpenAI Custom Headers/Params and Debug Page (#4227)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
parent
7c274de778
commit
ea1f727a8b
docs/docs
frontend
components/Layout/LayoutParts
lang/messages
layouts
lib/api
pages/admin/debug
mealie
core/settings
routes/admin
schema/admin
services
tests/utils/api_routes
@ -105,18 +105,21 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc.md)
|
||||
:octicons-tag-24: v1.7.0
|
||||
|
||||
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md).
|
||||
For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`)
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ---------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| OPENAI_BASE_URL | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
|
||||
### Themeing
|
||||
### Theming
|
||||
|
||||
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -84,13 +84,12 @@
|
||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</template>
|
||||
|
||||
<v-list-item v-for="child in nav.children" :key="child.key || child.title" exact :to="child.to">
|
||||
<v-list-item v-for="child in nav.children" :key="child.key || child.title" exact :to="child.to" class="ml-2">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ child.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ child.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="mb-4"></v-divider>
|
||||
</v-list-group>
|
||||
|
||||
<!-- Single Item -->
|
||||
|
@ -1246,7 +1246,11 @@
|
||||
"here-are-a-few-things-to-help-you-get-started": "Here are a few things to help you get started with Mealie",
|
||||
"restore-from-v1-backup": "Have a backup from a previous instance of Mealie v1? You can restore it here.",
|
||||
"manage-profile-or-get-invite-link": "Manage your own profile, or grab an invite link to share with others."
|
||||
}
|
||||
},
|
||||
"debug-openai-services": "Debug OpenAI Services",
|
||||
"debug-openai-services-description": "Use this page to debug OpenAI services. You can test your OpenAI connection and see the results here. If you have image services enabled, you can also provide an image.",
|
||||
"run-test": "Run Test",
|
||||
"test-results": "Test Results"
|
||||
},
|
||||
"profile": {
|
||||
"welcome-user": "👋 Welcome, {0}!",
|
||||
|
@ -92,10 +92,23 @@ export default defineComponent({
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.slotMachine,
|
||||
to: "/admin/parser",
|
||||
title: i18n.tc("sidebar.parser"),
|
||||
icon: $globals.icons.robot,
|
||||
title: i18n.tc("recipe.debug"),
|
||||
restricted: true,
|
||||
children: [
|
||||
{
|
||||
icon: $globals.icons.robot,
|
||||
to: "/admin/debug/openai",
|
||||
title: i18n.tc("admin.openai"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.slotMachine,
|
||||
to: "/admin/debug/parser",
|
||||
title: i18n.tc("sidebar.parser"),
|
||||
restricted: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
|
21
frontend/lib/api/admin/admin-debug.ts
Normal file
21
frontend/lib/api/admin/admin-debug.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { BaseAPI } from "../base/base-clients";
|
||||
import { DebugResponse } from "~/lib/api/types/admin";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
openai: `${prefix}/admin/debug/openai`,
|
||||
};
|
||||
|
||||
export class AdminDebugAPI extends BaseAPI {
|
||||
async debugOpenAI(fileObject: Blob | File | undefined = undefined, fileName = "") {
|
||||
let formData: FormData | null = null;
|
||||
if (fileObject) {
|
||||
formData = new FormData();
|
||||
formData.append("image", fileObject);
|
||||
formData.append("extension", fileName.split(".").pop() ?? "");
|
||||
}
|
||||
|
||||
return await this.requests.post<DebugResponse>(routes.openai, formData);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import { AdminGroupsApi } from "./admin/admin-groups";
|
||||
import { AdminBackupsApi } from "./admin/admin-backups";
|
||||
import { AdminMaintenanceApi } from "./admin/admin-maintenance";
|
||||
import { AdminAnalyticsApi } from "./admin/admin-analytics";
|
||||
import { AdminDebugAPI } from "./admin/admin-debug";
|
||||
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
||||
|
||||
export class AdminAPI {
|
||||
@ -15,6 +16,7 @@ export class AdminAPI {
|
||||
public backups: AdminBackupsApi;
|
||||
public maintenance: AdminMaintenanceApi;
|
||||
public analytics: AdminAnalyticsApi;
|
||||
public debug: AdminDebugAPI;
|
||||
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
this.about = new AdminAboutAPI(requests);
|
||||
@ -24,6 +26,7 @@ export class AdminAPI {
|
||||
this.backups = new AdminBackupsApi(requests);
|
||||
this.maintenance = new AdminMaintenanceApi(requests);
|
||||
this.analytics = new AdminAnalyticsApi(requests);
|
||||
this.debug = new AdminDebugAPI(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
@ -173,6 +173,10 @@ export interface CustomPageOut {
|
||||
categories?: RecipeCategoryResponse[];
|
||||
id: number;
|
||||
}
|
||||
export interface DebugResponse {
|
||||
success: boolean;
|
||||
response?: string | null;
|
||||
}
|
||||
export interface EmailReady {
|
||||
ready: boolean;
|
||||
}
|
||||
|
127
frontend/pages/admin/debug/openai.vue
Normal file
127
frontend/pages/admin/debug/openai.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<v-container class="pa-0">
|
||||
<v-container>
|
||||
<BaseCardSectionTitle :title="$tc('admin.debug-openai-services')">
|
||||
{{ $t('admin.debug-openai-services-description') }}
|
||||
<br />
|
||||
<DocLink class="mt-2" link="/documentation/getting-started/installation/open-ai" />
|
||||
</BaseCardSectionTitle>
|
||||
</v-container>
|
||||
<v-form ref="uploadForm" @submit.prevent="testOpenAI">
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-container class="pa-0">
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center">
|
||||
<AppButtonUpload
|
||||
v-if="!uploadedImage"
|
||||
class="ml-auto"
|
||||
url="none"
|
||||
file-name="image"
|
||||
accept="image/*"
|
||||
:text="$i18n.tc('recipe.upload-image')"
|
||||
:text-btn="false"
|
||||
:post="false"
|
||||
@uploaded="uploadImage"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="!!uploadedImage"
|
||||
color="error"
|
||||
@click="clearImage"
|
||||
>
|
||||
<v-icon left>{{ $globals.icons.close }}</v-icon>
|
||||
{{ $i18n.tc("recipe.remove-image") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
</v-row>
|
||||
<v-row v-if="uploadedImage && uploadedImagePreviewUrl" style="max-width: 25%;">
|
||||
<v-spacer />
|
||||
<v-col cols="12">
|
||||
<v-img :src="uploadedImagePreviewUrl" />
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
:text="$i18n.tc('admin.run-test')"
|
||||
:icon="$globals.icons.check"
|
||||
:loading="loading"
|
||||
class="ml-auto"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</v-form>
|
||||
<v-divider v-if="response" class="mt-4" />
|
||||
<v-container v-if="response" class="ma-0 pa-0">
|
||||
<v-card-title> {{ $t('admin.test-results') }} </v-card-title>
|
||||
<v-card-text> {{ response }} </v-card-text>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const api = useAdminApi();
|
||||
const loading = ref(false);
|
||||
const response = ref("");
|
||||
|
||||
const uploadForm = ref<VForm | null>(null);
|
||||
const uploadedImage = ref<Blob | File>();
|
||||
const uploadedImageName = ref<string>("");
|
||||
const uploadedImagePreviewUrl = ref<string>();
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
uploadedImage.value = fileObject;
|
||||
uploadedImageName.value = fileObject.name;
|
||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
}
|
||||
|
||||
function clearImage() {
|
||||
uploadedImage.value = undefined;
|
||||
uploadedImageName.value = "";
|
||||
uploadedImagePreviewUrl.value = undefined;
|
||||
}
|
||||
|
||||
async function testOpenAI() {
|
||||
response.value = "";
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.debug.debugOpenAI(uploadedImage.value);
|
||||
loading.value = false;
|
||||
|
||||
if (!data) {
|
||||
alert.error("Unable to test OpenAI services");
|
||||
} else {
|
||||
response.value = data.response || (data.success ? "Test Successful" : "Test Failed");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
response,
|
||||
uploadForm,
|
||||
uploadedImage,
|
||||
uploadedImagePreviewUrl,
|
||||
uploadImage,
|
||||
clearImage,
|
||||
testOpenAI,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("admin.debug-openai-services"),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@ -3,7 +3,7 @@ import os
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from dateutil.tz import tzlocal
|
||||
from pydantic import field_validator
|
||||
@ -305,6 +305,10 @@ class AppSettings(AppLoggingSettings):
|
||||
"""Your OpenAI API key. Required to enable OpenAI features"""
|
||||
OPENAI_MODEL: str = "gpt-4o"
|
||||
"""Which OpenAI model to send requests to. Leave this unset for most usecases"""
|
||||
OPENAI_CUSTOM_HEADERS: dict[str, str] = {}
|
||||
"""Custom HTTP headers to send with each OpenAI request"""
|
||||
OPENAI_CUSTOM_PARAMS: dict[str, Any] = {}
|
||||
"""Custom HTTP parameters to send with each OpenAI request"""
|
||||
OPENAI_ENABLE_IMAGE_SERVICES: bool = True
|
||||
"""Whether to enable image-related features in OpenAI"""
|
||||
OPENAI_WORKERS: int = 2
|
||||
|
@ -3,6 +3,7 @@ from mealie.routes._base.routers import AdminAPIRouter
|
||||
from . import (
|
||||
admin_about,
|
||||
admin_backups,
|
||||
admin_debug,
|
||||
admin_email,
|
||||
admin_maintenance,
|
||||
admin_management_groups,
|
||||
@ -19,3 +20,4 @@ router.include_router(admin_management_groups.router, tags=["Admin: Manage Group
|
||||
router.include_router(admin_email.router, tags=["Admin: Email"])
|
||||
router.include_router(admin_backups.router, tags=["Admin: Backups"])
|
||||
router.include_router(admin_maintenance.router, tags=["Admin: Maintenance"])
|
||||
router.include_router(admin_debug.router, tags=["Admin: Debug"])
|
||||
|
52
mealie/routes/admin/admin_debug.py
Normal file
52
mealie/routes/admin/admin_debug.py
Normal file
@ -0,0 +1,52 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, File, UploadFile
|
||||
|
||||
from mealie.core.dependencies.dependencies import get_temporary_path
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.schema.admin.debug import DebugResponse
|
||||
from mealie.services.openai import OpenAILocalImage, OpenAIService
|
||||
|
||||
router = APIRouter(prefix="/debug")
|
||||
|
||||
|
||||
@controller(router)
|
||||
class AdminDebugController(BaseAdminController):
|
||||
@router.post("/openai", response_model=DebugResponse)
|
||||
async def debug_openai(self, image: UploadFile | None = File(None)):
|
||||
if not self.settings.OPENAI_ENABLED:
|
||||
return DebugResponse(success=False, response="OpenAI is not enabled")
|
||||
if image and not self.settings.OPENAI_ENABLE_IMAGE_SERVICES:
|
||||
return DebugResponse(
|
||||
success=False, response="Image was provided, but OpenAI image services are not enabled"
|
||||
)
|
||||
|
||||
with get_temporary_path() as temp_path:
|
||||
if image:
|
||||
with temp_path.joinpath(image.filename).open("wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
local_image_path = temp_path.joinpath(image.filename)
|
||||
local_images = [OpenAILocalImage(filename=os.path.basename(local_image_path), path=local_image_path)]
|
||||
else:
|
||||
local_images = None
|
||||
|
||||
try:
|
||||
openai_service = OpenAIService()
|
||||
prompt = openai_service.get_prompt("debug")
|
||||
|
||||
message = "Hello, checking to see if I can reach you."
|
||||
if local_images:
|
||||
message = f"{message} Here is an image to test with:"
|
||||
|
||||
response = await openai_service.get_response(
|
||||
prompt, message, images=local_images, force_json_response=False
|
||||
)
|
||||
return DebugResponse(success=True, response=f'OpenAI is working. Response: "{response}"')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
return DebugResponse(
|
||||
success=False,
|
||||
response=f'OpenAI request failed. Full error has been logged. {e.__class__.__name__}: "{e}"',
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
# This file is auto-generated by gen_schema_exports.py
|
||||
from .about import AdminAboutInfo, AppInfo, AppStartupInfo, AppStatistics, AppTheme, CheckAppConfig, OIDCInfo
|
||||
from .backup import AllBackups, BackupFile, BackupOptions, CreateBackup, ImportJob
|
||||
from .debug import DebugResponse
|
||||
from .email import EmailReady, EmailSuccess, EmailTest
|
||||
from .maintenance import MaintenanceLogs, MaintenanceStorageDetails, MaintenanceSummary
|
||||
from .migration import ChowdownURL, MigrationFile, MigrationImport, Migrations
|
||||
@ -49,4 +50,5 @@ __all__ = [
|
||||
"EmailReady",
|
||||
"EmailSuccess",
|
||||
"EmailTest",
|
||||
"DebugResponse",
|
||||
]
|
||||
|
6
mealie/schema/admin/debug.py
Normal file
6
mealie/schema/admin/debug.py
Normal file
@ -0,0 +1,6 @@
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
|
||||
class DebugResponse(MealieModel):
|
||||
success: bool
|
||||
response: str | None = None
|
@ -90,6 +90,8 @@ class OpenAIService(BaseService):
|
||||
base_url=settings.OPENAI_BASE_URL,
|
||||
api_key=settings.OPENAI_API_KEY,
|
||||
timeout=settings.OPENAI_REQUEST_TIMEOUT,
|
||||
default_headers=settings.OPENAI_CUSTOM_HEADERS,
|
||||
default_query=settings.OPENAI_CUSTOM_PARAMS,
|
||||
)
|
||||
|
||||
super().__init__()
|
||||
@ -176,6 +178,5 @@ class OpenAIService(BaseService):
|
||||
if not response.choices:
|
||||
return None
|
||||
return response.choices[0].message.content
|
||||
except Exception:
|
||||
self.logger.exception("OpenAI Request Failed")
|
||||
return None
|
||||
except Exception as e:
|
||||
raise Exception(f"OpenAI Request Failed. {e.__class__.__name__}: {e}") from e
|
||||
|
1
mealie/services/openai/prompts/debug.txt
Normal file
1
mealie/services/openai/prompts/debug.txt
Normal file
@ -0,0 +1 @@
|
||||
You are a simple chatbot being used for debugging purposes.
|
@ -80,10 +80,20 @@ class OpenAIParser(ABCIngredientParser):
|
||||
tasks.append(service.get_response(prompt, message, force_json_response=True))
|
||||
|
||||
# re-combine chunks into one response
|
||||
responses_json = await asyncio.gather(*tasks)
|
||||
responses = [
|
||||
OpenAIIngredients.parse_openai_response(response_json) for response_json in responses_json if responses_json
|
||||
]
|
||||
try:
|
||||
responses_json = await asyncio.gather(*tasks)
|
||||
except Exception as e:
|
||||
raise Exception("Failed to call OpenAI services") from e
|
||||
|
||||
try:
|
||||
responses = [
|
||||
OpenAIIngredients.parse_openai_response(response_json)
|
||||
for response_json in responses_json
|
||||
if responses_json
|
||||
]
|
||||
except Exception as e:
|
||||
raise Exception("Failed to parse OpenAI response") from e
|
||||
|
||||
if not responses:
|
||||
raise Exception("No response from OpenAI")
|
||||
|
||||
|
@ -487,7 +487,13 @@ class OpenAIRecipeService(RecipeServiceBase):
|
||||
if translate_language:
|
||||
message += f" Please translate the recipe to {translate_language}."
|
||||
|
||||
response = await openai_service.get_response(prompt, message, images=openai_images, force_json_response=True)
|
||||
try:
|
||||
response = await openai_service.get_response(
|
||||
prompt, message, images=openai_images, force_json_response=True
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception("Failed to call OpenAI services") from e
|
||||
|
||||
try:
|
||||
openai_recipe = OpenAIRecipe.parse_openai_response(response)
|
||||
recipe = self._convert_recipe(openai_recipe)
|
||||
|
@ -11,6 +11,8 @@ admin_backups = "/api/admin/backups"
|
||||
"""`/api/admin/backups`"""
|
||||
admin_backups_upload = "/api/admin/backups/upload"
|
||||
"""`/api/admin/backups/upload`"""
|
||||
admin_debug_openai = "/api/admin/debug/openai"
|
||||
"""`/api/admin/debug/openai`"""
|
||||
admin_email = "/api/admin/email"
|
||||
"""`/api/admin/email`"""
|
||||
admin_groups = "/api/admin/groups"
|
||||
|
Loading…
x
Reference in New Issue
Block a user