Skip to content

Commit

Permalink
feat: OpenAI Custom Headers/Params and Debug Page (mealie-recipes#4227)
Browse files Browse the repository at this point in the history
Co-authored-by: Kuchenpirat <[email protected]>
  • Loading branch information
2 people authored and boc-the-git committed Sep 28, 2024
1 parent 9695873 commit ad0f0be
Show file tree
Hide file tree
Showing 20 changed files with 277 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/overrides/api.html

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions frontend/components/Layout/LayoutParts/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down
6 changes: 5 additions & 1 deletion frontend/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}!",
Expand Down
19 changes: 16 additions & 3 deletions frontend/layouts/admin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
]
},
];
Expand Down
21 changes: 21 additions & 0 deletions frontend/lib/api/admin/admin-debug.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions frontend/lib/api/client-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/api/types/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ export interface CustomPageOut {
categories?: RecipeCategoryResponse[];
id: number;
}
export interface DebugResponse {
success: boolean;
response?: string | null;
}
export interface EmailReady {
ready: boolean;
}
Expand Down
127 changes: 127 additions & 0 deletions frontend/pages/admin/debug/openai.vue
Original file line number Diff line number Diff line change
@@ -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>
File renamed without changes.
6 changes: 5 additions & 1 deletion mealie/core/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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
Expand Down Expand Up @@ -305,6 +305,10 @@ def OIDC_READY(self) -> bool:
"""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
Expand Down
2 changes: 2 additions & 0 deletions mealie/routes/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from . import (
admin_about,
admin_backups,
admin_debug,
admin_email,
admin_maintenance,
admin_management_groups,
Expand All @@ -19,3 +20,4 @@
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 changes: 52 additions & 0 deletions mealie/routes/admin/admin_debug.py
Original file line number Diff line number Diff line change
@@ -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}"',
)
2 changes: 2 additions & 0 deletions mealie/schema/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -49,4 +50,5 @@
"EmailReady",
"EmailSuccess",
"EmailTest",
"DebugResponse",
]
6 changes: 6 additions & 0 deletions mealie/schema/admin/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from mealie.schema._mealie import MealieModel


class DebugResponse(MealieModel):
success: bool
response: str | None = None
7 changes: 4 additions & 3 deletions mealie/services/openai/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ def __init__(self) -> None:
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__()
Expand Down Expand Up @@ -176,6 +178,5 @@ async def get_response(
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 change: 1 addition & 0 deletions mealie/services/openai/prompts/debug.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You are a simple chatbot being used for debugging purposes.
Loading

0 comments on commit ad0f0be

Please sign in to comment.