Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ docker-compose.override.yml
_docker_production_test
.devcontainer
.agents
skills-lock.json
skills-lock.json
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
# Install dev.command.sh
COPY --chmod=755 --chown=www-data:www-data docker/app/dev.command.sh /usr/bin/dev.command.sh

# -----------------------------------------------------
# APP - TEST
# -----------------------------------------------------
FROM app_root AS app_test

COPY --chown=www-data:www-data . .
COPY --from=node_builder --chown=www-data:www-data /var/www/html/public/build /var/www/html/public/build

RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lib,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get upgrade -y && apt-get install -y \
tmux \
&& pecl install xdebug-3.5.0 \
&& docker-php-ext-enable xdebug

RUN mkdir -p /var/www/html/storage/framework/cache \
&& mkdir -p /var/www/html/storage/framework/sessions \
&& mkdir -p /var/www/html/storage/framework/views \
&& mkdir -p /var/www/html/storage/logs \
&& mkdir -p /var/www/html/storage/app/public \
&& chown -R www-data:www-data /var/www/html/storage \
&& composer install --no-cache --no-progress --no-interaction --verbose \
&& composer clear-cache

ENV XDEBUG_MODE=coverage
# -----------------------------------------------------
# APP - PROD
# -----------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions _changelog/next-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

[//]: # (Describe what the user needs to do.)

- Run the migrations to add the assistent model

## Notes

[//]: # (Any additional warnings or tips for administrators performing the upgrade.)
2 changes: 2 additions & 0 deletions _changelog/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

[//]: # (- The main new features and changes in this version.)

- The assistent model is introduced

### Quality of Life

[//]: # (- Improvements and enhancements that improve the user experience.)
Expand Down
2 changes: 1 addition & 1 deletion app/Console/Commands/Ai/CheckModelStatusCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function handle(): void
$models = $this->aiService->getAvailableModels()->models;
foreach ($models as $model) {
$this->output->write("Checking model: {$model->getId()}");
$status = $model->getClient()->getStatus();
$status = $model->getClient()->getStatus($model);
$this->output->writeln(" is " . $status->value);
$this->modelStatusDb->setModelStatus($model, $status);
}
Expand Down
12 changes: 12 additions & 0 deletions app/Events/AssistantCreated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace App\Events;

use App\Models\Assistants\Assistant;

class AssistantCreated
{
public function __construct(
public readonly Assistant $assistant,
) {}
}
15 changes: 15 additions & 0 deletions app/Events/AssistantTriggerReleaseStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace App\Events;

use App\Models\Assistants\Assistant;
use App\Services\Assistant\Values\ReleaseStage;

class AssistantTriggerReleaseStatus
{
public function __construct(
public readonly Assistant $assistant,
public readonly ReleaseStage $oldStage,
public readonly ReleaseStage $newStage,
) {}
}
14 changes: 14 additions & 0 deletions app/Events/AssistantUpdated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace App\Events;

use App\Models\Assistants\Assistant;

class AssistantUpdated
{
public function __construct(
public readonly Assistant $assistant,
public readonly ?string $versionText = null,
public readonly array $changedKeys = [],
) {}
}
13 changes: 13 additions & 0 deletions app/Http/Controllers/AiModelController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use LaravelJsonApi\Laravel\Http\Controllers\Actions;

class AiModelController extends Controller
{
use Actions\FetchMany;
use Actions\FetchOne;
}
13 changes: 13 additions & 0 deletions app/Http/Controllers/AiProviderController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use LaravelJsonApi\Laravel\Http\Controllers\Actions;

class AiProviderController extends Controller
{
use Actions\FetchMany;
use Actions\FetchOne;
}
13 changes: 13 additions & 0 deletions app/Http/Controllers/AiToolController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use LaravelJsonApi\Laravel\Http\Controllers\Actions;

class AiToolController extends Controller
{
use Actions\FetchMany;
use Actions\FetchOne;
}
217 changes: 217 additions & 0 deletions app/Http/Controllers/AssistantController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Events\AssistantCreated;
use App\Events\AssistantUpdated;
use App\JsonApi\V1\Assistants\AssistantQuery;
use App\JsonApi\V1\Assistants\AssistantRequest;
use App\JsonApi\V1\Assistants\AssistantSchema;
use App\JsonApi\V1\Assistants\ChatTestAssistantRequest;
use App\JsonApi\V1\Assistants\FavoriteAssistantRequest;
use App\JsonApi\V1\Assistants\FeedbackAssistantRequest;
use App\JsonApi\V1\Assistants\ReleaseAssistantRequest;
use App\Models\Assistants\Assistant;
use App\Services\Assistant\AssistantService;
use App\Services\Assistant\Chat\AssistantChatRunnerInterface;
use App\Services\Assistant\Values\ReleaseStage;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use LaravelJsonApi\Core\Responses\DataResponse;
use LaravelJsonApi\Laravel\Http\Controllers\Actions;
use Symfony\Component\HttpFoundation\StreamedResponse;

class AssistantController extends Controller
{
use Actions\Destroy;
use Actions\FetchMany;
use Actions\FetchOne;
use Actions\Store;
use Actions\Update;

public function __construct(
private readonly AssistantService $assistantService,
) {
$this->authorizeResource(Assistant::class, 'assistant');
}

public function created(Assistant $assistant, AssistantRequest $request, AssistantQuery $query): void
{
Event::dispatch(new AssistantCreated($assistant));
}

public function updated(Assistant $assistant, AssistantRequest $request, AssistantQuery $query): void
{
$changedKeys = array_values(array_filter(
array_keys($assistant->getChanges()),
fn (string $key) => $key !== 'updated_at',
));

$validated = $request->validated();
if (isset($validated['tags'])) {
$changedKeys[] = 'tags';
}
if (isset($validated['ai_tools'])) {
$changedKeys[] = 'ai_tools';
}
if (isset($validated['user_prompts'])) {
$changedKeys[] = 'user_prompts';
}

if ($changedKeys !== []) {
Event::dispatch(new AssistantUpdated(
$assistant,
$validated['version_text'] ?? null,
$changedKeys,
));
}
}

public function remix(AssistantSchema $schema, AssistantQuery $query, Assistant $assistant): Responsable
{
$this->authorize('remix', $assistant);

$remixed = $this->assistantService->remix($assistant, request()->user());

return DataResponse::make($remixed)
->withQueryParameters($query)
->didCreate();
}

public function feedback(
FeedbackAssistantRequest $request,
AssistantSchema $schema,
AssistantQuery $query,
Assistant $assistant,
): Responsable {
$this->authorize('view', $assistant);

$this->assistantService->feedback(
$assistant,
$request->user(),
$request->input('data.attributes.text'),
);

return DataResponse::make($assistant)
->withQueryParameters($query);
}

public function release(ReleaseAssistantRequest $request, AssistantSchema $schema, AssistantQuery $query, Assistant $assistant): Responsable
{
$this->authorize('release', $assistant);

$releaseStage = ReleaseStage::from($request->input('data.attributes.release_stage'));

$assistant = $this->assistantService->release($assistant, $releaseStage);

return DataResponse::make($assistant)
->withQueryParameters($query);
}

public function favorite(
FavoriteAssistantRequest $request,
AssistantSchema $schema,
AssistantQuery $query,
Assistant $assistant,
): Responsable {
$this->authorize('favorite', $assistant);

$this->assistantService->setFavorite(
$assistant,
$request->user(),
$request->boolean('data.attributes.is_favorite'),
);

return DataResponse::make($assistant->fresh())
->withQueryParameters($query);
}

public function chatTest(
ChatTestAssistantRequest $request,
AssistantSchema $schema,
AssistantQuery $query,
Assistant $assistant,
): StreamedResponse {
$this->authorize('view', $assistant);

$attrs = $request->input('data.attributes');
$runner = app(AssistantChatRunnerInterface::class);
$messages = $this->buildMessages($attrs['messages'], $assistant);

return response()->stream(function () use ($runner, $messages, $assistant, $attrs) {
$this->writeEvent('stream_start', ['model' => $assistant->model]);

$fullContent = '';

try {
$generator = $runner->stream(
systemPrompt: $assistant->system_prompt ?? '',
messages: $messages,
model: $assistant->model,
tools: $attrs['tools'] ?? [],
params: $attrs['params'] ?? [],
);

foreach ($generator as $chunk) {
$this->writeEvent($chunk['type'], $chunk['content']);

if ($chunk['type'] === 'text_delta') {
$fullContent .= $chunk['content'];
}
}

$this->writeEvent('message', [
'type' => 'messages',
'id' => (string) Str::uuid(),
'attributes' => [
'content' => $fullContent,
'model' => $assistant->model,
'status' => 'completed',
],
]);

$this->writeEvent('stream_end', ['reason' => 'stop']);
} catch (\Throwable $e) {
$this->writeEvent('stream_failed', ['message' => $e->getMessage()]);
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
'Connection' => 'keep-alive',
]);
}

private function writeEvent(string $event, mixed $data): void
{
echo "event: {$event}\ndata: ".json_encode($data)."\n\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}

private function buildMessages(array $messages, Assistant $assistant): array
{
$payload = [];

if (! empty($assistant->system_prompt)) {
$payload[] = [
'role' => 'system',
'content' => ['text' => $assistant->system_prompt],
];
}

foreach ($messages as $msg) {
$payload[] = [
'role' => $msg['role'],
'content' => $msg['content'] ?? ['text' => ''],
];
}

return $payload;
}
}
13 changes: 13 additions & 0 deletions app/Http/Controllers/AssistantLanguageController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use LaravelJsonApi\Laravel\Http\Controllers\Actions;

class AssistantLanguageController extends Controller
{
use Actions\FetchMany;
use Actions\FetchOne;
}
13 changes: 13 additions & 0 deletions app/Http/Controllers/CategoryController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use LaravelJsonApi\Laravel\Http\Controllers\Actions;

class CategoryController extends Controller
{
use Actions\FetchMany;
use Actions\FetchOne;
}
Loading