Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ee438c3
Add mcp server
karlomikus Nov 17, 2025
47525fd
Merge branch 'develop' into mcp-server
karlomikus Nov 30, 2025
8ab432d
Add auth
karlomikus Nov 30, 2025
fb979ae
Merge branch 'develop' into mcp-server
karlomikus Dec 6, 2025
398a07e
Add more tools
karlomikus Dec 6, 2025
6fa26b5
Remove unused
karlomikus Dec 6, 2025
005b9e0
update deps
karlomikus Jan 14, 2026
f295513
Update datapack fetching
karlomikus Jan 15, 2026
a13dd89
add kanidm sso provider
p-fruck Jan 19, 2026
2058916
Merge pull request #564 from p-fruck/feat/kanidm-sso
karlomikus Jan 21, 2026
7a1a9c6
update deps
karlomikus Jan 21, 2026
8711397
Add gen endpoints
karlomikus Jan 22, 2026
ff8d2cc
Update openapi spec
karlomikus Jan 22, 2026
44a1b96
Add recipe from text
karlomikus Jan 23, 2026
eaf013d
Update prompt
karlomikus Jan 23, 2026
0ed1b50
Add requests
karlomikus Jan 23, 2026
36b28f4
Update prompts
karlomikus Jan 23, 2026
2b237c7
Add a feature flag
karlomikus Jan 24, 2026
10d67e5
Merge branch 'prism' into mcp-server
karlomikus Jan 24, 2026
db177d5
Merge pull request #548 from karlomikus/mcp-server
karlomikus Jan 24, 2026
15ce6ea
Add tool
karlomikus Jan 24, 2026
e51df50
Update tool
karlomikus Jan 24, 2026
5a737fb
Use scopes
karlomikus Jan 24, 2026
88bfcfc
Update deps
karlomikus Jan 24, 2026
a05696b
csfix
karlomikus Jan 24, 2026
a489aa8
Merge pull request #566 from karlomikus/prism
karlomikus Jan 25, 2026
0db4af4
Update deps
karlomikus Jan 25, 2026
7db531d
php84
karlomikus Jan 25, 2026
62675f5
Add missing indexes
karlomikus Jan 25, 2026
e863c6d
Remove feeds
karlomikus Jan 25, 2026
806cf36
rector fixes
karlomikus Jan 25, 2026
676fb9a
update rector paths
karlomikus Jan 25, 2026
55f089e
remove route
karlomikus Jan 25, 2026
2ad598a
move logic to middleware
karlomikus Jan 25, 2026
7ab1390
update changelog
karlomikus Jan 25, 2026
74c0ba3
docs openapi
karlomikus Jan 25, 2026
eb4b87c
csfix
karlomikus Jan 25, 2026
24faca6
update readme
karlomikus Jan 25, 2026
7946dcf
Remove timestamps from shopping list
karlomikus Jan 27, 2026
3ebd006
Remove timestamps from cocktail subs
karlomikus Jan 27, 2026
0f21894
csfix
karlomikus Jan 27, 2026
6f5495f
deps
karlomikus Jan 28, 2026
a8be92d
Cleanup old image
karlomikus Feb 1, 2026
33e9540
deps
karlomikus Feb 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
# v5.13.0
## New
- Added `kanidm` SSO provider
- Added generative AI endpoints
- Support is added via [PrismPHP library](https://prismphp.com/). So you can use any provider supported by this library.
- Added `/generate/ingredient`, used to complete ingredient data based on the name
- Added `/generate/cocktail-tags`, used to suggest cocktail tags
- Added `/generate/cocktail-recipe-from-text`, used to generate structured recipe output from raw text
- Here's an example of env configuration:
```
# To use with ollama:
GEN_AI_PROVIDER=ollama
GEN_AI_MODEL=ministral-3
OLLAMA_URL= #Optional, will use http://localhost:11434 by default

# To use with openai:
GEN_AI_PROVIDER=openai
GEN_AI_MODEL=gpt-4
OPENAI_API_KEY=sk-proj-your-key
OPENAI_ORGANIZATION= #Optional
```

## Fixes
- Correctly clean up old image when new one is uploaded

## Changes
- Removed feeds
- Improved bar delete query plan
- Updated default recipes data

# v5.12.0
## New
- Added `filters` property to `meta` property in public cocktails index
Expand Down
12 changes: 1 addition & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
FROM alpine:latest AS datapack

RUN apk add --no-cache git

WORKDIR /app/data

RUN git clone --depth 1 --branch v5 https://github.com/bar-assistant/data.git .

RUN rm -r .git

FROM serversideup/php:8.4-fpm-nginx AS php-base

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

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

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

RUN composer install --optimize-autoloader --no-dev \
&& sed -i "s/{{VERSION}}/$BAR_ASSISTANT_VERSION/g" ./docs/openapi-generated.yaml \
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ Here's a list of interesting environment variables you can set to configure Bar
|MAIL_ENCRYPTION||The mail encryption method.|
|MAIL_USERNAME||The mail username.|
|MAIL_PASSWORD||The mail password.|
|GEN_AI_PROVIDER||Identifier of LLM provider. Supports any provider that PrismPHP supports.|
|GEN_AI_MODEL||Specific model to use from the provider.|
|GEN_AI_TIMEOUT|60|Timeout for LLM provider requests in seconds.|

## Managed instance

Expand Down
2 changes: 2 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
#[\Override]
protected function schedule(Schedule $schedule)
{
$schedule->command('sanctum:prune-expired --hours=24')->daily();
Expand All @@ -24,6 +25,7 @@ protected function schedule(Schedule $schedule)
*
* @return void
*/
#[\Override]
protected function commands()
{
$this->load(__DIR__ . '/Commands');
Expand Down
1 change: 1 addition & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Handler extends ExceptionHandler
*
* @return void
*/
#[\Override]
public function register()
{
$this->reportable(function (Throwable $e) {
Expand Down
2 changes: 1 addition & 1 deletion app/External/Model/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static function fromDraft2Array(array $source): self
{
return new self(
Cocktail::fromDraft2Array($source['recipe']),
array_map(fn ($ingredient) => Ingredient::fromDraft2Array($ingredient), $source['ingredients']),
array_map(Ingredient::fromDraft2Array(...), $source['ingredients']),
);
}

Expand Down
25 changes: 0 additions & 25 deletions app/Http/Controllers/FeedsController.php

This file was deleted.

251 changes: 251 additions & 0 deletions app/Http/Controllers/GenerateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<?php

declare(strict_types=1);

namespace Kami\Cocktail\Http\Controllers;

use Kami\Cocktail\Models\Tag;
use OpenApi\Attributes as OAT;
use Prism\Prism\Facades\Prism;
use Prism\Prism\Enums\Provider;
use Kami\Cocktail\OpenAPI as BAO;
use Kami\Cocktail\Models\Cocktail;
use Illuminate\Support\Facades\Log;
use Prism\Prism\Schema\ArraySchema;
use Prism\Prism\Schema\NumberSchema;
use Prism\Prism\Schema\ObjectSchema;
use Prism\Prism\Schema\StringSchema;
use Kami\Cocktail\External\Model\Schema;
use Kami\Cocktail\Http\Requests\CompleteIngredientRequest;
use Kami\Cocktail\Http\Requests\CompleteCocktailTagsRequest;
use Kami\Cocktail\Http\Requests\CocktailRecipeFromTextRequest;
use Kami\Cocktail\Http\Resources\Generated\GeneratedIngredientResource;
use Kami\Cocktail\Http\Resources\Generated\GeneratedCocktailTagsResource;
use Kami\Cocktail\Http\Resources\Generated\GeneratedCocktailFromTextResource;

class GenerateController extends Controller
{
#[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: [
new BAO\Parameters\BarIdHeaderParameter(),
], requestBody: new OAT\RequestBody(
required: true,
content: [
new OAT\JsonContent(
type: 'object',
properties: [
new OAT\Property(property: 'name', type: 'string', example: 'Vodka', description: 'Name of the ingredient to complete information for'),
],
required: ['name'],
),
]
))]
#[BAO\SuccessfulResponse(content: [
new BAO\WrapObjectWithData(GeneratedIngredientResource::class),
])]
public function completeIngredient(CompleteIngredientRequest $request): GeneratedIngredientResource
{
$provider = Provider::tryFrom(config('bar-assistant.ai.provider'));
$model = config('bar-assistant.ai.model');
$timeout = config('bar-assistant.ai.timeout');
$ingredientName = $request->input('name');

$schema = new ObjectSchema(
name: 'cocktail_ingredient',
description: 'Basic information about cocktail ingredient',
properties: [
new StringSchema('name', 'The name of the ingredient'),
new StringSchema('description', 'Helpful description of ingredient (1-2 short paragraphs)'),
new NumberSchema('strength', 'Alcohol by volume percentage', true),
new StringSchema('color', 'The predominant color of the ingredient as hex value', true),
new StringSchema('distillery', 'Name of the distillery producing the ingredient', true),
new StringSchema('origin', 'The geographical origin of the ingredient', true),
],
requiredFields: ['name', 'description', 'color', 'distillery', 'origin', 'strength']
);

$prompt = <<<PROMPT
You are a cocktail and spirits expert. Provide detailed information about the following ingredient used in cocktails.

Ingredient: {$ingredientName}

Instructions:
- Keep the description informative and concise (1-2 short paragraphs)
- For strength (ABV), provide the typical alcohol by volume percentage as a number (e.g., 40 for 40% ABV). Use null if non-alcoholic.
- For color, provide a hex color code (e.g., #FF5733) representing the ingredient's predominant color
- For distillery, provide the name of a well-known producer if applicable, otherwise use null
- For origin, specify the country or region where this ingredient typically originates
PROMPT;

Log::info("[LLM] Generating ingredient information for: {$ingredientName}");

$response = Prism::structured()
->using($provider, $model)
->withSchema($schema)
->withPrompt(trim($prompt))
->withClientOptions(['timeout' => $timeout])
->asStructured();

return new GeneratedIngredientResource($response->structured);
}

#[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: [
new BAO\Parameters\BarIdHeaderParameter(),
], requestBody: new OAT\RequestBody(
required: true,
content: [
new OAT\JsonContent(
type: 'object',
properties: [
new OAT\Property(property: 'cocktail_id', type: 'string', example: 'Vodka', description: 'Id or slug of the ingredient'),
],
required: ['cocktail_id'],
),
]
))]
#[BAO\SuccessfulResponse(content: [
new BAO\WrapObjectWithData(GeneratedCocktailTagsResource::class),
])]
public function completeCocktailTags(CompleteCocktailTagsRequest $request): GeneratedCocktailTagsResource
{
$cocktailId = $request->input('cocktail_id');
$cocktail = Cocktail::where('slug', $cocktailId)
->orWhere('id', $cocktailId)
->firstOrFail();

$provider = Provider::tryFrom(config('bar-assistant.ai.provider'));
$model = config('bar-assistant.ai.model');
$timeout = config('bar-assistant.ai.timeout');

// Limit to top 50 most used tags to reduce token usage
$existingTags = Tag::where('bar_id', $cocktail->bar_id)
->withCount('cocktails')
->orderByDesc('cocktails_count')
->limit(50)
->pluck('name')
->implode(', ');

$schema = new ObjectSchema(
name: 'cocktail_tags',
description: 'Cocktail tags',
properties: [
new ArraySchema('tags', 'List of recommended cocktail tags', new StringSchema('tag', 'A single cocktail tag')),
],
requiredFields: ['tags']
);

$cocktailMarkdown = Schema::fromCocktailModel($cocktail)->toMarkdown();

$existingTagsContext = !empty($existingTags)
? "Here are the most commonly used tags in this bar: {$existingTags}"
: "This bar has no existing tags yet.";

$prompt = <<<PROMPT
You are a cocktail expert assistant. Analyze the following cocktail recipe and suggest relevant tags.

{$existingTagsContext}

Instructions:
- Generate 5-6 relevant tags (no more than 6)
- Strongly prefer existing tags when they match the cocktail's characteristics
- Only create new tags if existing ones don't adequately describe the cocktail
- Tags should describe: flavor profile, occasion, difficulty, alcohol content, season, or ingredients
- Keep tags concise (1-3 words each)
- Use lowercase for consistency

Cocktail recipe:
{$cocktailMarkdown}
PROMPT;

Log::info("[LLM] Generating cocktail tags for cocktail ID: {$cocktailId}");

$response = Prism::structured()
->using($provider, $model)
->withSchema($schema)
->withPrompt(trim($prompt))
->withClientOptions(['timeout' => $timeout])
->asStructured();

return new GeneratedCocktailTagsResource($response->structured);
}

#[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: [
new BAO\Parameters\BarIdHeaderParameter(),
], requestBody: new OAT\RequestBody(
required: true,
content: [
new OAT\JsonContent(
type: 'object',
properties: [
new OAT\Property(property: 'recipe', type: 'string', example: 'My recipe details are here...', description: 'The raw text of the cocktail recipe to parse')
],
required: ['recipe'],
),
]
))]
#[BAO\SuccessfulResponse(content: [
new BAO\WrapObjectWithData(GeneratedCocktailFromTextResource::class),
])]
public function cocktailRecipeFromText(CocktailRecipeFromTextRequest $request): GeneratedCocktailFromTextResource
{
$provider = Provider::tryFrom(config('bar-assistant.ai.provider'));
$model = config('bar-assistant.ai.model');
$timeout = config('bar-assistant.ai.timeout');

$textRecipe = trim((string) $request->input('recipe'));

$schema = new ObjectSchema(
name: 'cocktail_recipe',
description: 'Cocktail recipe parsed by LLM',
properties: [
new StringSchema('name', 'Title of the recipe'),
new StringSchema('instructions', 'Step by step instructions to prepare the cocktail'),
new StringSchema('garnish', 'Recommended garnish for the cocktail', true),
new StringSchema('method', 'Cocktail preperation method.', true),
new StringSchema('description', 'Helpful description of the cocktail (1-2 short paragraphs)', true),
new ArraySchema('ingredients', 'List of ingredients', new ObjectSchema(
name: 'cocktail_recipe_ingredient',
description: 'Cocktail ingredient',
properties: [
new StringSchema('name', 'Name of the ingredient'),
new NumberSchema('amount', 'The min amount of the ingredient'),
new NumberSchema('amount_max', 'The max amount of the ingredient', true),
new StringSchema('units', 'Units of the ingredient amount'),
new StringSchema('note', 'Additional note about the ingredient', true),
],
requiredFields: ['name', 'amount', 'units']
)),
],
requiredFields: ['name', 'instructions', 'ingredients', 'description', 'garnish']
);

$prompt = <<<PROMPT
You are a cocktail recipe parser. Extract structured information from the following cocktail recipe text.

Instructions:
- Extract the recipe name, ingredients, and preparation instructions
- 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.
- For method, choose ONLY one of: "Shake", "Stir", "Build", "Blend", "Muddle", "Layer"
- Standardize ingredient units to: "ml", "cl", "oz", "dash", "tsp", "tbsp", "cup", "part", "slice", "wedge", "piece", "whole"
- For ingredient amounts:
* If a range is given (e.g., "2-3 oz"), use amount for minimum and amount_max for maximum
* If a fraction is given (e.g., "1/2 oz"), convert to decimal (0.5)
* If no amount is specified, use a sensible default or estimation
- If the recipe text is incomplete or ambiguous, extract what you can and make reasonable assumptions
- Provide a brief, engaging description (1-2 paragraphs) if one isn't included in the text

COCKTAIL RECIPE:
{$textRecipe}
PROMPT;

Log::info("[LLM] Generating cocktail recipe from text.");

$response = Prism::structured()
->using($provider, $model)
->withSchema($schema)
->withPrompt(trim($prompt))
->withClientOptions(['timeout' => $timeout])
->asStructured();

return new GeneratedCocktailFromTextResource($response->structured);
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/ServerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public function version(VersionCheckService $versionCheckService): JsonResponse
'type' => config('app.env'),
'search_host' => $searchHost,
'search_version' => $searchVersion,
'is_feeds_enabled' => (bool) config('bar-assistant.enable_feeds') === true,
'is_password_login_enabled' => config('bar-assistant.enable_password_login') === true,
'is_ai_enabled' => config('bar-assistant.ai.provider') !== null,
]
]);
}
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Filters/FilterNameSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function __invoke(Builder $query, mixed $value, string $property)
}, $value);

// Filter out empty values
$value = array_filter($value, fn ($val) => strlen($val) > 0);
$value = array_filter($value, fn ($val) => strlen((string) $val) > 0);

if (count($value) === 0) {
return $query;
Expand Down
Loading