Skip to content

Commit f169370

Browse files
authored
Merge pull request #568 from karlomikus/develop
Minor release
2 parents 4e536cf + 33e9540 commit f169370

File tree

131 files changed

+2359
-1102
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

131 files changed

+2359
-1102
lines changed

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
# v5.13.0
2+
## New
3+
- Added `kanidm` SSO provider
4+
- Added generative AI endpoints
5+
- Support is added via [PrismPHP library](https://prismphp.com/). So you can use any provider supported by this library.
6+
- Added `/generate/ingredient`, used to complete ingredient data based on the name
7+
- Added `/generate/cocktail-tags`, used to suggest cocktail tags
8+
- Added `/generate/cocktail-recipe-from-text`, used to generate structured recipe output from raw text
9+
- Here's an example of env configuration:
10+
```
11+
# To use with ollama:
12+
GEN_AI_PROVIDER=ollama
13+
GEN_AI_MODEL=ministral-3
14+
OLLAMA_URL= #Optional, will use http://localhost:11434 by default
15+
16+
# To use with openai:
17+
GEN_AI_PROVIDER=openai
18+
GEN_AI_MODEL=gpt-4
19+
OPENAI_API_KEY=sk-proj-your-key
20+
OPENAI_ORGANIZATION= #Optional
21+
```
22+
23+
## Fixes
24+
- Correctly clean up old image when new one is uploaded
25+
26+
## Changes
27+
- Removed feeds
28+
- Improved bar delete query plan
29+
- Updated default recipes data
30+
131
# v5.12.0
232
## New
333
- Added `filters` property to `meta` property in public cocktails index

Dockerfile

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
FROM alpine:latest AS datapack
2-
3-
RUN apk add --no-cache git
4-
5-
WORKDIR /app/data
6-
7-
RUN git clone --depth 1 --branch v5 https://github.com/bar-assistant/data.git .
8-
9-
RUN rm -r .git
10-
111
FROM serversideup/php:8.4-fpm-nginx AS php-base
122

133
LABEL org.opencontainers.image.source="https://github.com/karlomikus/bar-assistant"
@@ -59,7 +49,7 @@ COPY ./resources/docker/dist/php.ini /usr/local/etc/php/conf.d/zzz-bass-php.ini
5949

6050
COPY --chown=www-data:www-data . .
6151

62-
COPY --from=datapack --chown=www-data:www-data /app/data ./resources/data
52+
ADD --chown=www-data:www-data "https://github.com/bar-assistant/data.git" ./resources/data
6353

6454
RUN composer install --optimize-autoloader --no-dev \
6555
&& sed -i "s/{{VERSION}}/$BAR_ASSISTANT_VERSION/g" ./docs/openapi-generated.yaml \

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ Here's a list of interesting environment variables you can set to configure Bar
9191
|MAIL_ENCRYPTION||The mail encryption method.|
9292
|MAIL_USERNAME||The mail username.|
9393
|MAIL_PASSWORD||The mail password.|
94+
|GEN_AI_PROVIDER||Identifier of LLM provider. Supports any provider that PrismPHP supports.|
95+
|GEN_AI_MODEL||Specific model to use from the provider.|
96+
|GEN_AI_TIMEOUT|60|Timeout for LLM provider requests in seconds.|
9497

9598
## Managed instance
9699

app/Console/Kernel.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
1313
* @param \Illuminate\Console\Scheduling\Schedule $schedule
1414
* @return void
1515
*/
16+
#[\Override]
1617
protected function schedule(Schedule $schedule)
1718
{
1819
$schedule->command('sanctum:prune-expired --hours=24')->daily();
@@ -24,6 +25,7 @@ protected function schedule(Schedule $schedule)
2425
*
2526
* @return void
2627
*/
28+
#[\Override]
2729
protected function commands()
2830
{
2931
$this->load(__DIR__ . '/Commands');

app/Exceptions/Handler.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class Handler extends ExceptionHandler
4545
*
4646
* @return void
4747
*/
48+
#[\Override]
4849
public function register()
4950
{
5051
$this->reportable(function (Throwable $e) {

app/External/Model/Schema.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static function fromDraft2Array(array $source): self
4747
{
4848
return new self(
4949
Cocktail::fromDraft2Array($source['recipe']),
50-
array_map(fn ($ingredient) => Ingredient::fromDraft2Array($ingredient), $source['ingredients']),
50+
array_map(Ingredient::fromDraft2Array(...), $source['ingredients']),
5151
);
5252
}
5353

app/Http/Controllers/FeedsController.php

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kami\Cocktail\Http\Controllers;
6+
7+
use Kami\Cocktail\Models\Tag;
8+
use OpenApi\Attributes as OAT;
9+
use Prism\Prism\Facades\Prism;
10+
use Prism\Prism\Enums\Provider;
11+
use Kami\Cocktail\OpenAPI as BAO;
12+
use Kami\Cocktail\Models\Cocktail;
13+
use Illuminate\Support\Facades\Log;
14+
use Prism\Prism\Schema\ArraySchema;
15+
use Prism\Prism\Schema\NumberSchema;
16+
use Prism\Prism\Schema\ObjectSchema;
17+
use Prism\Prism\Schema\StringSchema;
18+
use Kami\Cocktail\External\Model\Schema;
19+
use Kami\Cocktail\Http\Requests\CompleteIngredientRequest;
20+
use Kami\Cocktail\Http\Requests\CompleteCocktailTagsRequest;
21+
use Kami\Cocktail\Http\Requests\CocktailRecipeFromTextRequest;
22+
use Kami\Cocktail\Http\Resources\Generated\GeneratedIngredientResource;
23+
use Kami\Cocktail\Http\Resources\Generated\GeneratedCocktailTagsResource;
24+
use Kami\Cocktail\Http\Resources\Generated\GeneratedCocktailFromTextResource;
25+
26+
class GenerateController extends Controller
27+
{
28+
#[OAT\Post(path: '/generate/ingredient', tags: ['Generation'], operationId: 'completeIngredient', description: 'Suggested ingredient information based on it\'s name, generated by an LLM.', summary: 'Complete ingredient', parameters: [
29+
new BAO\Parameters\BarIdHeaderParameter(),
30+
], requestBody: new OAT\RequestBody(
31+
required: true,
32+
content: [
33+
new OAT\JsonContent(
34+
type: 'object',
35+
properties: [
36+
new OAT\Property(property: 'name', type: 'string', example: 'Vodka', description: 'Name of the ingredient to complete information for'),
37+
],
38+
required: ['name'],
39+
),
40+
]
41+
))]
42+
#[BAO\SuccessfulResponse(content: [
43+
new BAO\WrapObjectWithData(GeneratedIngredientResource::class),
44+
])]
45+
public function completeIngredient(CompleteIngredientRequest $request): GeneratedIngredientResource
46+
{
47+
$provider = Provider::tryFrom(config('bar-assistant.ai.provider'));
48+
$model = config('bar-assistant.ai.model');
49+
$timeout = config('bar-assistant.ai.timeout');
50+
$ingredientName = $request->input('name');
51+
52+
$schema = new ObjectSchema(
53+
name: 'cocktail_ingredient',
54+
description: 'Basic information about cocktail ingredient',
55+
properties: [
56+
new StringSchema('name', 'The name of the ingredient'),
57+
new StringSchema('description', 'Helpful description of ingredient (1-2 short paragraphs)'),
58+
new NumberSchema('strength', 'Alcohol by volume percentage', true),
59+
new StringSchema('color', 'The predominant color of the ingredient as hex value', true),
60+
new StringSchema('distillery', 'Name of the distillery producing the ingredient', true),
61+
new StringSchema('origin', 'The geographical origin of the ingredient', true),
62+
],
63+
requiredFields: ['name', 'description', 'color', 'distillery', 'origin', 'strength']
64+
);
65+
66+
$prompt = <<<PROMPT
67+
You are a cocktail and spirits expert. Provide detailed information about the following ingredient used in cocktails.
68+
69+
Ingredient: {$ingredientName}
70+
71+
Instructions:
72+
- Keep the description informative and concise (1-2 short paragraphs)
73+
- For strength (ABV), provide the typical alcohol by volume percentage as a number (e.g., 40 for 40% ABV). Use null if non-alcoholic.
74+
- For color, provide a hex color code (e.g., #FF5733) representing the ingredient's predominant color
75+
- For distillery, provide the name of a well-known producer if applicable, otherwise use null
76+
- For origin, specify the country or region where this ingredient typically originates
77+
PROMPT;
78+
79+
Log::info("[LLM] Generating ingredient information for: {$ingredientName}");
80+
81+
$response = Prism::structured()
82+
->using($provider, $model)
83+
->withSchema($schema)
84+
->withPrompt(trim($prompt))
85+
->withClientOptions(['timeout' => $timeout])
86+
->asStructured();
87+
88+
return new GeneratedIngredientResource($response->structured);
89+
}
90+
91+
#[OAT\Post(path: '/generate/cocktail-tags', tags: ['Generation'], operationId: 'completeCocktailTags', description: 'Suggested cocktail tags, based on existing cocktail information, generated by an LLM.', summary: 'Cocktail tags', parameters: [
92+
new BAO\Parameters\BarIdHeaderParameter(),
93+
], requestBody: new OAT\RequestBody(
94+
required: true,
95+
content: [
96+
new OAT\JsonContent(
97+
type: 'object',
98+
properties: [
99+
new OAT\Property(property: 'cocktail_id', type: 'string', example: 'Vodka', description: 'Id or slug of the ingredient'),
100+
],
101+
required: ['cocktail_id'],
102+
),
103+
]
104+
))]
105+
#[BAO\SuccessfulResponse(content: [
106+
new BAO\WrapObjectWithData(GeneratedCocktailTagsResource::class),
107+
])]
108+
public function completeCocktailTags(CompleteCocktailTagsRequest $request): GeneratedCocktailTagsResource
109+
{
110+
$cocktailId = $request->input('cocktail_id');
111+
$cocktail = Cocktail::where('slug', $cocktailId)
112+
->orWhere('id', $cocktailId)
113+
->firstOrFail();
114+
115+
$provider = Provider::tryFrom(config('bar-assistant.ai.provider'));
116+
$model = config('bar-assistant.ai.model');
117+
$timeout = config('bar-assistant.ai.timeout');
118+
119+
// Limit to top 50 most used tags to reduce token usage
120+
$existingTags = Tag::where('bar_id', $cocktail->bar_id)
121+
->withCount('cocktails')
122+
->orderByDesc('cocktails_count')
123+
->limit(50)
124+
->pluck('name')
125+
->implode(', ');
126+
127+
$schema = new ObjectSchema(
128+
name: 'cocktail_tags',
129+
description: 'Cocktail tags',
130+
properties: [
131+
new ArraySchema('tags', 'List of recommended cocktail tags', new StringSchema('tag', 'A single cocktail tag')),
132+
],
133+
requiredFields: ['tags']
134+
);
135+
136+
$cocktailMarkdown = Schema::fromCocktailModel($cocktail)->toMarkdown();
137+
138+
$existingTagsContext = !empty($existingTags)
139+
? "Here are the most commonly used tags in this bar: {$existingTags}"
140+
: "This bar has no existing tags yet.";
141+
142+
$prompt = <<<PROMPT
143+
You are a cocktail expert assistant. Analyze the following cocktail recipe and suggest relevant tags.
144+
145+
{$existingTagsContext}
146+
147+
Instructions:
148+
- Generate 5-6 relevant tags (no more than 6)
149+
- Strongly prefer existing tags when they match the cocktail's characteristics
150+
- Only create new tags if existing ones don't adequately describe the cocktail
151+
- Tags should describe: flavor profile, occasion, difficulty, alcohol content, season, or ingredients
152+
- Keep tags concise (1-3 words each)
153+
- Use lowercase for consistency
154+
155+
Cocktail recipe:
156+
{$cocktailMarkdown}
157+
PROMPT;
158+
159+
Log::info("[LLM] Generating cocktail tags for cocktail ID: {$cocktailId}");
160+
161+
$response = Prism::structured()
162+
->using($provider, $model)
163+
->withSchema($schema)
164+
->withPrompt(trim($prompt))
165+
->withClientOptions(['timeout' => $timeout])
166+
->asStructured();
167+
168+
return new GeneratedCocktailTagsResource($response->structured);
169+
}
170+
171+
#[OAT\Post(path: '/generate/cocktail-recipe-from-text', tags: ['Generation'], operationId: 'cocktailRecipeFromText', description: 'Generate a parseable json recipe structure from raw text.', summary: 'Recipe from text', parameters: [
172+
new BAO\Parameters\BarIdHeaderParameter(),
173+
], requestBody: new OAT\RequestBody(
174+
required: true,
175+
content: [
176+
new OAT\JsonContent(
177+
type: 'object',
178+
properties: [
179+
new OAT\Property(property: 'recipe', type: 'string', example: 'My recipe details are here...', description: 'The raw text of the cocktail recipe to parse')
180+
],
181+
required: ['recipe'],
182+
),
183+
]
184+
))]
185+
#[BAO\SuccessfulResponse(content: [
186+
new BAO\WrapObjectWithData(GeneratedCocktailFromTextResource::class),
187+
])]
188+
public function cocktailRecipeFromText(CocktailRecipeFromTextRequest $request): GeneratedCocktailFromTextResource
189+
{
190+
$provider = Provider::tryFrom(config('bar-assistant.ai.provider'));
191+
$model = config('bar-assistant.ai.model');
192+
$timeout = config('bar-assistant.ai.timeout');
193+
194+
$textRecipe = trim((string) $request->input('recipe'));
195+
196+
$schema = new ObjectSchema(
197+
name: 'cocktail_recipe',
198+
description: 'Cocktail recipe parsed by LLM',
199+
properties: [
200+
new StringSchema('name', 'Title of the recipe'),
201+
new StringSchema('instructions', 'Step by step instructions to prepare the cocktail'),
202+
new StringSchema('garnish', 'Recommended garnish for the cocktail', true),
203+
new StringSchema('method', 'Cocktail preperation method.', true),
204+
new StringSchema('description', 'Helpful description of the cocktail (1-2 short paragraphs)', true),
205+
new ArraySchema('ingredients', 'List of ingredients', new ObjectSchema(
206+
name: 'cocktail_recipe_ingredient',
207+
description: 'Cocktail ingredient',
208+
properties: [
209+
new StringSchema('name', 'Name of the ingredient'),
210+
new NumberSchema('amount', 'The min amount of the ingredient'),
211+
new NumberSchema('amount_max', 'The max amount of the ingredient', true),
212+
new StringSchema('units', 'Units of the ingredient amount'),
213+
new StringSchema('note', 'Additional note about the ingredient', true),
214+
],
215+
requiredFields: ['name', 'amount', 'units']
216+
)),
217+
],
218+
requiredFields: ['name', 'instructions', 'ingredients', 'description', 'garnish']
219+
);
220+
221+
$prompt = <<<PROMPT
222+
You are a cocktail recipe parser. Extract structured information from the following cocktail recipe text.
223+
224+
Instructions:
225+
- Extract the recipe name, ingredients, and preparation instructions
226+
- Format instructions as a numbered markdown list (e.g., "1. Step one\n2. Step two"). Do NOT include markdown headings. Do NOT include ingredient amounts in instructions.
227+
- For method, choose ONLY one of: "Shake", "Stir", "Build", "Blend", "Muddle", "Layer"
228+
- Standardize ingredient units to: "ml", "cl", "oz", "dash", "tsp", "tbsp", "cup", "part", "slice", "wedge", "piece", "whole"
229+
- For ingredient amounts:
230+
* If a range is given (e.g., "2-3 oz"), use amount for minimum and amount_max for maximum
231+
* If a fraction is given (e.g., "1/2 oz"), convert to decimal (0.5)
232+
* If no amount is specified, use a sensible default or estimation
233+
- If the recipe text is incomplete or ambiguous, extract what you can and make reasonable assumptions
234+
- Provide a brief, engaging description (1-2 paragraphs) if one isn't included in the text
235+
236+
COCKTAIL RECIPE:
237+
{$textRecipe}
238+
PROMPT;
239+
240+
Log::info("[LLM] Generating cocktail recipe from text.");
241+
242+
$response = Prism::structured()
243+
->using($provider, $model)
244+
->withSchema($schema)
245+
->withPrompt(trim($prompt))
246+
->withClientOptions(['timeout' => $timeout])
247+
->asStructured();
248+
249+
return new GeneratedCocktailFromTextResource($response->structured);
250+
}
251+
}

app/Http/Controllers/ServerController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ public function version(VersionCheckService $versionCheckService): JsonResponse
4040
'type' => config('app.env'),
4141
'search_host' => $searchHost,
4242
'search_version' => $searchVersion,
43-
'is_feeds_enabled' => (bool) config('bar-assistant.enable_feeds') === true,
4443
'is_password_login_enabled' => config('bar-assistant.enable_password_login') === true,
44+
'is_ai_enabled' => config('bar-assistant.ai.provider') !== null,
4545
]
4646
]);
4747
}

app/Http/Filters/FilterNameSearch.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function __invoke(Builder $query, mixed $value, string $property)
3434
}, $value);
3535

3636
// Filter out empty values
37-
$value = array_filter($value, fn ($val) => strlen($val) > 0);
37+
$value = array_filter($value, fn ($val) => strlen((string) $val) > 0);
3838

3939
if (count($value) === 0) {
4040
return $query;

0 commit comments

Comments
 (0)