Added database and implemented action logging
This commit is contained in:
parent
9590992c08
commit
0509b1be52
@ -2,4 +2,6 @@ You are an expert AI programming assistant that primarily focuses on producing c
|
||||
You are also an expert in Software architect and you provide very decoupled code with come abstractions.
|
||||
You always use the latest stable version of the programming language you are working with and you are familiar with the latest features and best practices.
|
||||
You are a full stack developer with expert knowledge Symfony and Docker.
|
||||
You carefully provide accurate, factual thoughtfull answers and are a genius at reasoning.
|
||||
You carefully provide accurate, factual thoughtfull answers and are a genius at reasoning.
|
||||
|
||||
Dont use make: to create or edit entities
|
||||
4
.env
4
.env
@ -24,13 +24,11 @@ APP_SECRET=
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||
DATABASE_URL="mysql://tars:tars@127.0.0.1:3306/tars?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> OpenAI API ###
|
||||
OPENAI_MODEL=gpt-4o
|
||||
OPENAI_API_URL=https://api.openai.com/v1
|
||||
###< OpenAI API ###
|
||||
|
||||
|
||||
@ -4,10 +4,16 @@
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
# Calendar configuration
|
||||
app.calendars.ics:
|
||||
tim_calendar: 'webcal://p101-caldav.icloud.com/published/2/MTIwODk2NzA4MjIxMjA4OX8U9-11KVNdAw-HVVfEeHJioeELY1BwErQansnsIRnd'
|
||||
household_calendar: 'webcal://p101-caldav.icloud.com/published/2/MTIwODk2NzA4MjIxMjA4OX8U9-11KVNdAw-HVVfEeHJDG4lEVQV-T3I5sEk0H6vfdGGP0X9Mpef_3zp3JNiiYvbAqzkgkukXO0nsKSxY1FA'
|
||||
tim_calendar:
|
||||
url: 'webcal://p101-caldav.icloud.com/published/2/MTIwODk2NzA4MjIxMjA4OX8U9-11KVNdAw-HVVfEeHJioeELY1BwErQansnsIRnd'
|
||||
name: 'Tims calendar'
|
||||
description: 'Tims personal calendar for individual events and appointments'
|
||||
|
||||
household_calendar:
|
||||
url: 'webcal://p101-caldav.icloud.com/published/2/MTIwODk2NzA4MjIxMjA4OX8U9-11KVNdAw-HVVfEeHJDG4lEVQV-T3I5sEk0H6vfdGGP0X9Mpef_3zp3JNiiYvbAqzkgkukXO0nsKSxY1FA'
|
||||
name: 'Household calendar'
|
||||
description: 'Shared household calendar for family events and coordination'
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
|
||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: tars
|
||||
MYSQL_USER: tars
|
||||
MYSQL_PASSWORD: tars
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_unicode_ci
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
31
migrations/Version20250317180455.php
Normal file
31
migrations/Version20250317180455.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250317180455 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE brain_storage_item (id INT AUTO_INCREMENT NOT NULL, description LONGTEXT NOT NULL, content LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE brain_storage_item');
|
||||
}
|
||||
}
|
||||
31
migrations/Version20250317182942.php
Normal file
31
migrations/Version20250317182942.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250317182942 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE ai_action (id INT AUTO_INCREMENT NOT NULL, action VARCHAR(255) NOT NULL, description LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', agent VARCHAR(255) DEFAULT NULL, context JSON DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE ai_action');
|
||||
}
|
||||
}
|
||||
31
migrations/Version20250317183016.php
Normal file
31
migrations/Version20250317183016.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250317183016 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE ai_action CHANGE description description LONGTEXT DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE ai_action CHANGE description description LONGTEXT NOT NULL');
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ class ReadCalendarCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$days = 7;
|
||||
$days = 14;
|
||||
|
||||
$from = new DateTime();
|
||||
$to = (new DateTime())->modify("+$days days");
|
||||
@ -36,7 +36,7 @@ class ReadCalendarCommand extends Command
|
||||
$calendars = $this->calendarService->getCalendars();
|
||||
|
||||
foreach ($calendars as $calendar) {
|
||||
$events = $calendar->getEvents($from, $to);
|
||||
$events = $calendar->getEventsByDateRange($from, $to);
|
||||
$io->section($calendar->getName());
|
||||
$this->displayEvents($io, $events);
|
||||
}
|
||||
|
||||
@ -9,7 +9,9 @@ use Exception;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:run-agent',
|
||||
@ -23,16 +25,43 @@ class RunAgentCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
'dry-run',
|
||||
'd',
|
||||
InputOption::VALUE_NONE,
|
||||
'Only show the prompt without sending it to the AI service'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$isDryRun = $input->getOption('dry-run');
|
||||
|
||||
try {
|
||||
if ($isDryRun) {
|
||||
$prompt = $this->agent->getPrompt();
|
||||
$io->section('Prompt Preview');
|
||||
$io->text($prompt);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$result = $this->agent->run();
|
||||
$output->writeln($result['prompt']);
|
||||
$output->writeln($result['response']);
|
||||
$io->section('Prompt');
|
||||
$io->text($result['prompt']);
|
||||
$io->section('Response');
|
||||
if (is_array($result['response'])) {
|
||||
$io->writeln(json_encode($result['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
} else {
|
||||
$io->writeln($result['response']);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$output->writeln('<error>'.$e->getMessage().'</error>');
|
||||
$io->error($e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
41
src/Controller/AiActionController.php
Normal file
41
src/Controller/AiActionController.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Service\AiActionLogger;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/ai-actions')]
|
||||
class AiActionController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AiActionLogger $aiActionLogger
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/', name: 'app_ai_actions_index')]
|
||||
public function index(): Response
|
||||
{
|
||||
$actions = $this->aiActionLogger->getRecentActions();
|
||||
|
||||
return $this->render('ai_action/index.html.twig', [
|
||||
'actions' => $actions,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/agent/{agent}', name: 'app_ai_actions_agent')]
|
||||
public function agentActions(string $agent): Response
|
||||
{
|
||||
$since = new \DateTimeImmutable('-7 weeks');
|
||||
$actions = $this->aiActionLogger->getAgentActions($agent, $since);
|
||||
|
||||
return $this->render('ai_action/agent.html.twig', [
|
||||
'actions' => $actions,
|
||||
'agent' => $agent,
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
src/Core/Actions/ActionInterface.php
Normal file
22
src/Core/Actions/ActionInterface.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Actions;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
|
||||
#[Autoconfigure(tags: ['app.action'])]
|
||||
interface ActionInterface
|
||||
{
|
||||
public function getName(): string;
|
||||
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, array{type: string, description: string}>
|
||||
*/
|
||||
public function getParameters(): array;
|
||||
|
||||
public function execute(array $parameters): void;
|
||||
}
|
||||
24
src/Core/Actions/ActionsProvider.php
Normal file
24
src/Core/Actions/ActionsProvider.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Actions;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
|
||||
|
||||
final readonly class ActionsProvider
|
||||
{
|
||||
public function __construct(
|
||||
#[TaggedIterator('app.action')]
|
||||
private iterable $actions,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<ActionInterface>
|
||||
*/
|
||||
public function getActions(): array
|
||||
{
|
||||
return iterator_to_array($this->actions);
|
||||
}
|
||||
}
|
||||
51
src/Core/Actions/BrainStorageAction.php
Normal file
51
src/Core/Actions/BrainStorageAction.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Actions;
|
||||
|
||||
use App\Entity\BrainStorageItem;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final readonly class BrainStorageAction implements ActionInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'brain_storage';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Stores information for yourself for future use. You can store any information you want to remember.';
|
||||
}
|
||||
|
||||
public function getParameters(): array
|
||||
{
|
||||
return [
|
||||
'description' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The description of the information to be stored',
|
||||
],
|
||||
'content' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The content of the information to be stored',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function execute(array $parameters): void
|
||||
{
|
||||
$item = new BrainStorageItem();
|
||||
$item->setDescription($parameters['description']);
|
||||
$item->setContent($parameters['content']);
|
||||
|
||||
$this->entityManager->persist($item);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
30
src/Core/Actions/ImAliveAction.php
Normal file
30
src/Core/Actions/ImAliveAction.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Actions;
|
||||
|
||||
|
||||
final readonly class ImAliveAction implements ActionInterface
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'im_alive';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Return this action in your response to indicate that you are alive and well.';
|
||||
}
|
||||
|
||||
public function getParameters(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
public function execute(array $parameters): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
42
src/Core/Actions/PlanAutomationAction.php
Normal file
42
src/Core/Actions/PlanAutomationAction.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Actions;
|
||||
|
||||
|
||||
final readonly class PlanAutomationAction implements ActionInterface
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plan_automation';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Plans an automation based on provided parameters';
|
||||
}
|
||||
|
||||
public function getParameters(): array
|
||||
{
|
||||
return [
|
||||
'description' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The description of the automation',
|
||||
],
|
||||
'datetime' => [
|
||||
'type' => 'YYYY-MM-DD HH:MM:SS',
|
||||
'description' => 'The datetime when the automation should be executed',
|
||||
],
|
||||
'actions' => [
|
||||
'type' => 'array',
|
||||
'description' => 'The actions to be performed in the automation. Each action is an array with the following keys: "action" ("turn_on_light", "turn_off_light"), "parameters" (array of parameters for the action).',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function execute(array $parameters): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
58
src/Core/Actions/SendNotificationAction.php
Normal file
58
src/Core/Actions/SendNotificationAction.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Actions;
|
||||
|
||||
|
||||
final class SendNotificationAction implements ActionInterface
|
||||
{
|
||||
public function __construct(
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Send a notification to the user';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'send_notification';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return [
|
||||
'recipient' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The recipient of the notification (e.g. "tim", "cara", "household")',
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The title of the notification',
|
||||
],
|
||||
'message' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The message to send to the user',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute(array $parameters): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Agent;
|
||||
|
||||
use App\Core\Actions\ActionsProvider;
|
||||
use App\Core\Services\AI\ChatServiceInterface;
|
||||
use App\Core\Services\Calendar\CalendarService;
|
||||
use App\Core\Services\Home\HomeServiceInterface;
|
||||
use App\Repository\BrainStorageItemRepository;
|
||||
use App\Repository\AiActionRepository;
|
||||
use App\Service\AiActionLogger;
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function sprintf;
|
||||
@ -16,7 +21,12 @@ class Agent
|
||||
public function __construct(
|
||||
private readonly PromptProvider $promptProvider,
|
||||
private readonly CalendarService $calendarService,
|
||||
private readonly HomeServiceInterface $homeService,
|
||||
private readonly ChatServiceInterface $chatGPTService,
|
||||
private readonly ActionsProvider $actionsProvider,
|
||||
private readonly BrainStorageItemRepository $brainStorageItemRepository,
|
||||
private readonly AiActionLogger $aiActionLogger,
|
||||
private readonly AiActionRepository $aiActionRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -24,6 +34,26 @@ class Agent
|
||||
{
|
||||
$prompt = $this->getPrompt();
|
||||
$response = $this->chatGPTService->sendMessage($prompt);
|
||||
$response = trim($response);
|
||||
|
||||
$response = json_decode($response, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \RuntimeException('Invalid JSON response: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
if (!is_array($response)) {
|
||||
throw new \RuntimeException('Invalid JSON response: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
foreach ($response as $action) {
|
||||
$this->aiActionLogger->logAction(
|
||||
$action['action'],
|
||||
$action['description'] ?? null,
|
||||
'agent',
|
||||
$action['parameters'] ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'prompt' => $prompt,
|
||||
@ -40,17 +70,95 @@ class Agent
|
||||
$calendars = $this->calendarService->getCalendars();
|
||||
$calendarEventsText = '';
|
||||
|
||||
$smartHomeDevices = $this->homeService->findAllEntities();
|
||||
$smartHomeDevicesText = '';
|
||||
|
||||
foreach ($smartHomeDevices as $smartHomeDevice) {
|
||||
$smartHomeDevicesText .= sprintf(
|
||||
"- %s: %s (%s)\n",
|
||||
$smartHomeDevice->getId(),
|
||||
$smartHomeDevice->getName(),
|
||||
$smartHomeDevice->getState(),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($calendars as $calendar) {
|
||||
$calendarEventsText .= sprintf(
|
||||
"- %s: %s\n",
|
||||
$calendar->getName(),
|
||||
$calendar->getDescription(),
|
||||
);
|
||||
|
||||
$events = $calendar->getEventsByDateRange($from, $to);
|
||||
foreach ($events as $event) {
|
||||
$calendarEventsText .= sprintf(
|
||||
" * %s - %s: %s%s\n",
|
||||
$event->getStart()->format('Y-m-d H:i'),
|
||||
$event->getEnd()->format('H:i'),
|
||||
$event->getTitle(),
|
||||
$event->getLocation() ? ' @ ' . $event->getLocation() : ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return strtr($this->promptProvider->getPromptTemplate(), [
|
||||
$brainStorageItems = $this->brainStorageItemRepository->findAll();
|
||||
$brainStorageText = '';
|
||||
foreach ($brainStorageItems as $brainStorageItem) {
|
||||
$brainStorageText .= sprintf("- %s: %s\n", $brainStorageItem->getDescription(), $brainStorageItem->getContent());
|
||||
}
|
||||
|
||||
if (empty($brainStorageText)) {
|
||||
$brainStorageText = 'No information stored in your brain.';
|
||||
}
|
||||
|
||||
$availableActions = '';
|
||||
foreach ($this->actionsProvider->getActions() as $action) {
|
||||
$availableActions .= sprintf("- %s: %s\n", $action->getName(), $action->getDescription());
|
||||
|
||||
$parameters = $action->getParameters();
|
||||
if (!empty($parameters)) {
|
||||
$availableActions .= " Parameters:\n";
|
||||
foreach ($parameters as $paramName => $paramDetails) {
|
||||
$availableActions .= sprintf(
|
||||
" * %s: %s (%s)\n",
|
||||
$paramName,
|
||||
$paramDetails['type'],
|
||||
$paramDetails['description']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$since = new DateTimeImmutable('-7 days');
|
||||
$performedActions = $this->aiActionRepository->findRecentActions(100);
|
||||
$performedActionsText = '';
|
||||
|
||||
if (empty($performedActions)) {
|
||||
$performedActionsText = 'No actions performed in the last 7 days.';
|
||||
} else {
|
||||
foreach ($performedActions as $performedAction) {
|
||||
if ($performedAction->getAction() === 'im_alive') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$performedActionsText .= sprintf(
|
||||
"- %s (%s): %s \n%s\n",
|
||||
$performedAction->getAction(),
|
||||
$performedAction->getCreatedAt()->format('Y-m-d H:i'),
|
||||
$performedAction->getDescription(),
|
||||
json_encode($performedAction->getContext(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$response = trim(strtr($this->promptProvider->getPromptTemplate(), [
|
||||
'{calendar_events}' => $calendarEventsText,
|
||||
'{smart_home_devices}' => $smartHomeDevicesText,
|
||||
'{current_time}' => $now->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
'{available_actions}' => $availableActions,
|
||||
'{brain_storage}' => $brainStorageText,
|
||||
'{performed_actions}' => $performedActionsText,
|
||||
]));
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,23 +9,30 @@ class PromptProvider
|
||||
public function getPromptTemplate(): string
|
||||
{
|
||||
return <<<'EOT'
|
||||
You are a high level smart home assistant.
|
||||
You can answer questions and help with tasks.
|
||||
You can also control the smart home devices.
|
||||
You can also control the calendar.
|
||||
Tim and Cara are both members of the household.
|
||||
You have access to the following calendars:
|
||||
- Tim's calendar
|
||||
- Cara's calendar
|
||||
- Household calendar
|
||||
These are the events from the calendars:
|
||||
{calendar_events}
|
||||
|
||||
Its currently {current_time}
|
||||
I will ask you every 5 minutes to perform actions in the smart home.
|
||||
Sometimes you have to do something, but sometimes you dont.
|
||||
So its your turn. What actions to you want to perform?
|
||||
Answer with a JSON array of actions.
|
||||
You are a high level smart home assistant.
|
||||
You can answer questions and help with tasks.
|
||||
You can also control the smart home devices.
|
||||
You can also control the calendar.
|
||||
Tim and Cara are both members of the household.
|
||||
You have access to the following calendars with the events from the next 14 days:
|
||||
{calendar_events}
|
||||
|
||||
You have also access to the states of the following smart home devices:
|
||||
{smart_home_devices}
|
||||
|
||||
The following information is stored yourself in the past:
|
||||
{brain_storage}
|
||||
|
||||
Its currently {current_time}
|
||||
I will ask you every 60 minutes to perform actions.
|
||||
Sometimes you have to do something, but sometimes you dont.
|
||||
So its your turn. What actions to you want to perform?
|
||||
Answer with a JSON array of actions.
|
||||
You can use the following actions:
|
||||
{available_actions}
|
||||
If you dont want to perform any actions, just answer with an empty array.
|
||||
These are the actions you have already performed in the past:
|
||||
{performed_actions}
|
||||
EOT;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Services\Calendar;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
class Calendar
|
||||
{
|
||||
/** @var CalendarEvent[] */
|
||||
@ -51,6 +53,20 @@ class Calendar
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
public function getEventsByDate(DateTimeInterface $date): array
|
||||
{
|
||||
return array_filter($this->events, function (CalendarEvent $event) use ($date) {
|
||||
return $event->getStart()->format('Y-m-d') === $date->format('Y-m-d');
|
||||
});
|
||||
}
|
||||
|
||||
public function getEventsByDateRange(DateTimeInterface $start, DateTimeInterface $end): array
|
||||
{
|
||||
return array_filter($this->events, function (CalendarEvent $event) use ($start, $end) {
|
||||
return $event->getStart() >= $start && $event->getStart() <= $end;
|
||||
});
|
||||
}
|
||||
|
||||
public function addEvent(CalendarEvent $event): void
|
||||
{
|
||||
$this->events[] = $event;
|
||||
|
||||
@ -17,6 +17,9 @@ class CalendarService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Calendar[]
|
||||
*/
|
||||
public function getCalendars(): array
|
||||
{
|
||||
$allCalendars = [];
|
||||
|
||||
98
src/Entity/AiAction.php
Normal file
98
src/Entity/AiAction.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\AiActionRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AiActionRepository::class)]
|
||||
class AiAction
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $action = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $agent = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
private ?array $context = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getAction(): ?string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function setAction(string $action): static
|
||||
{
|
||||
$this->action = $action;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeImmutable $createdAt): static
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAgent(): ?string
|
||||
{
|
||||
return $this->agent;
|
||||
}
|
||||
|
||||
public function setAgent(?string $agent): static
|
||||
{
|
||||
$this->agent = $agent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContext(): ?array
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function setContext(?array $context): static
|
||||
{
|
||||
$this->context = $context;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
79
src/Entity/BrainStorageItem.php
Normal file
79
src/Entity/BrainStorageItem.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'brain_storage_item')]
|
||||
class BrainStorageItem
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $description;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $content;
|
||||
|
||||
#[ORM\Column]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column]
|
||||
private \DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->updatedAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): self
|
||||
{
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(\DateTimeImmutable $updatedAt): self
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
53
src/Repository/AiActionRepository.php
Normal file
53
src/Repository/AiActionRepository.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AiAction;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class AiActionRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AiAction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AiAction[]
|
||||
*/
|
||||
public function findRecentActions(int $limit = 100): array
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->orderBy('a.createdAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AiAction[]
|
||||
*/
|
||||
public function findActionsByAgent(string $agent, \DateTimeImmutable $since): array
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->andWhere('a.agent = :agent')
|
||||
->andWhere('a.createdAt >= :since')
|
||||
->setParameter('agent', $agent)
|
||||
->setParameter('since', $since)
|
||||
->orderBy('a.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function save(AiAction $entity, bool $flush = false): void
|
||||
{
|
||||
$this->getEntityManager()->persist($entity);
|
||||
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Repository/BrainStorageItemRepository.php
Normal file
18
src/Repository/BrainStorageItemRepository.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\BrainStorageItem;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<BrainStorageItem>
|
||||
*/
|
||||
class BrainStorageItemRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, BrainStorageItem::class);
|
||||
}
|
||||
}
|
||||
39
src/Service/AiActionLogger.php
Normal file
39
src/Service/AiActionLogger.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\AiAction;
|
||||
use App\Repository\AiActionRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class AiActionLogger
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly AiActionRepository $aiActionRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function logAction(string $action, ?string $description = null, ?string $agent = null, ?array $context = null): void
|
||||
{
|
||||
$aiAction = new AiAction();
|
||||
$aiAction->setAction($action);
|
||||
$aiAction->setDescription($description);
|
||||
$aiAction->setAgent($agent);
|
||||
$aiAction->setContext($context);
|
||||
|
||||
$this->aiActionRepository->save($aiAction, true);
|
||||
}
|
||||
|
||||
public function getRecentActions(int $limit = 100): array
|
||||
{
|
||||
return $this->aiActionRepository->findRecentActions($limit);
|
||||
}
|
||||
|
||||
public function getAgentActions(string $agent, \DateTimeImmutable $since): array
|
||||
{
|
||||
return $this->aiActionRepository->findActionsByAgent($agent, $since);
|
||||
}
|
||||
}
|
||||
@ -28,16 +28,16 @@ class IcsCalendarProvider implements CalendarProviderInterface
|
||||
{
|
||||
$calendars = [];
|
||||
|
||||
foreach ($this->icsCalendars as $name => $url) {
|
||||
foreach ($this->icsCalendars as $calendarConfig) {
|
||||
$calendar = new Calendar(
|
||||
name: $name,
|
||||
description: sprintf('ICS Calendar: %s', $name),
|
||||
url: $url,
|
||||
name: $calendarConfig['name'],
|
||||
description: $calendarConfig['description'],
|
||||
url: $calendarConfig['url'],
|
||||
enabled: true,
|
||||
isDefault: false,
|
||||
);
|
||||
|
||||
$content = $this->icsClient->fetchCalendarContent($url);
|
||||
$content = $this->icsClient->fetchCalendarContent($calendarConfig['url']);
|
||||
$this->icsParser->parseEvents($calendar, $content);
|
||||
|
||||
$calendars[] = $calendar;
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Services\HomeAssistant;
|
||||
|
||||
use App\Core\Services\Home\HomeEntityInterface;
|
||||
use App\Core\Services\Home\HomeEntityType;
|
||||
use App\Core\Services\Home\HomeServiceInterface;
|
||||
|
||||
use function array_filter;
|
||||
@ -35,10 +36,18 @@ final readonly class HomeAssistantHomeService implements HomeServiceInterface
|
||||
throw new HomeAssistantException('No entities found');
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn (EntityState $state) => new HomeAssistantEntity($state),
|
||||
$states,
|
||||
);
|
||||
$entities = [];
|
||||
foreach ($states as $state) {
|
||||
$type = HomeEntityType::tryFrom($state->getDomain());
|
||||
|
||||
if ($type === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entities[] = new HomeAssistantEntity($state);
|
||||
}
|
||||
|
||||
return array_filter($entities, fn (HomeEntityInterface $entity) => $entity->getType() === HomeEntityType::LIGHT);
|
||||
}
|
||||
|
||||
public function callService(string $service, array $data = []): array
|
||||
|
||||
@ -15,19 +15,16 @@ class OpenAIClient
|
||||
#[Autowire('%env(OPENAI_API_KEY)%')]
|
||||
private readonly string $apiKey,
|
||||
#[Autowire('%env(OPENAI_API_URL)%')]
|
||||
private readonly string $apiUrl,
|
||||
#[Autowire('%env(OPENAI_MODEL)%')]
|
||||
private readonly string $model,
|
||||
private readonly string $apiUrl
|
||||
) {
|
||||
}
|
||||
|
||||
public function chat(array $messages, float $temperature = 0.7, int $maxTokens = 2048): array
|
||||
{
|
||||
$response = $this->sendRequest('/chat/completions', [
|
||||
'model' => $this->model,
|
||||
'model' => 'o3-mini',
|
||||
'messages' => $messages,
|
||||
'temperature' => $temperature,
|
||||
'max_tokens' => $maxTokens,
|
||||
'reasoning_effort' => 'low',
|
||||
]);
|
||||
|
||||
return $response->toArray();
|
||||
|
||||
68
templates/_menu.html.twig
Normal file
68
templates/_menu.html.twig
Normal file
@ -0,0 +1,68 @@
|
||||
{% set current_route = app.request.get('_route') %}
|
||||
|
||||
<nav class="bg-white shadow-sm rounded-lg mb-6">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<a href="{{ path('app_home') }}" class="text-xl font-bold text-gray-800">
|
||||
Tars AI
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
<a href="{{ path('app_home') }}"
|
||||
class="{% if current_route == 'app_home' %}border-indigo-500 text-gray-900{% else %}border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700{% endif %} inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
|
||||
Chat
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
<a href="{{ path('app_ai_actions_index') }}"
|
||||
class="{% if current_route == 'app_ai_actions_index' %}border-indigo-500 text-gray-900{% else %}border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700{% endif %} inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
|
||||
Actions
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden sm:ml-6 sm:flex sm:items-center">
|
||||
<div class="ml-3 relative">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ path('app_home') }}" class="text-gray-500 hover:text-gray-700">
|
||||
<span class="sr-only">Settings</span>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mr-2 flex items-center sm:hidden">
|
||||
<button type="button" class="mobile-menu-button inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sm:hidden mobile-menu hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<a href="{{ path('app_home') }}"
|
||||
class="{% if current_route == 'app_home' %}bg-indigo-50 border-indigo-500 text-indigo-700{% else %}border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700{% endif %} block pl-3 pr-4 py-2 border-l-4 text-base font-medium">
|
||||
Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileMenuButton = document.querySelector('.mobile-menu-button');
|
||||
const mobileMenu = document.querySelector('.mobile-menu');
|
||||
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
39
templates/ai_action/agent.html.twig
Normal file
39
templates/ai_action/agent.html.twig
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}AI Actions by {{ agent }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-3xl font-bold">Actions by {{ agent }}</h1>
|
||||
<a href="{{ path('app_ai_actions_index') }}" class="text-blue-600 hover:text-blue-800">
|
||||
← Back to all actions
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4">
|
||||
{% for action in actions %}
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">{{ action.action }}</h2>
|
||||
<p class="text-gray-600 mt-2">{{ action.description }}</p>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ action.createdAt|date('Y-m-d H:i:s') }}
|
||||
</div>
|
||||
</div>
|
||||
{% if action.context %}
|
||||
<div class="mt-4">
|
||||
<pre class="bg-gray-100 p-4 rounded text-sm overflow-x-auto">{{ action.context|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
No actions found for this agent
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
templates/ai_action/index.html.twig
Normal file
40
templates/ai_action/index.html.twig
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}AI Actions{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-6">Recent AI Actions</h1>
|
||||
|
||||
<div class="grid gap-4">
|
||||
{% for action in actions %}
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">{{ action.action }}</h2>
|
||||
<p class="text-gray-600 mt-2">{{ action.description }}</p>
|
||||
{% if action.agent %}
|
||||
<a href="{{ path('app_ai_actions_agent', {'agent': action.agent}) }}"
|
||||
class="text-blue-600 hover:text-blue-800 mt-2 inline-block">
|
||||
View all actions by {{ action.agent }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ action.createdAt|date('Y-m-d H:i:s') }}
|
||||
</div>
|
||||
</div>
|
||||
{% if action.context %}
|
||||
<div class="mt-4">
|
||||
<pre class="bg-gray-100 p-4 rounded text-sm overflow-x-auto">{{ action.context|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
No actions found
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -10,17 +10,17 @@
|
||||
{% block stylesheets %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container py-4">
|
||||
<header class="pb-3 mb-4 border-bottom">
|
||||
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
|
||||
<span class="fs-4">Tars AI</span>
|
||||
</a>
|
||||
</header>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
{% include '_menu.html.twig' %}
|
||||
|
||||
<main class="flex-grow container mx-auto px-4 py-8">
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
<footer class="pt-3 mt-4 text-muted border-top">
|
||||
© {{ 'now'|date('Y') }} Tars AI
|
||||
<footer class="bg-white border-t mt-auto">
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<p class="text-center text-gray-500">© {{ 'now'|date('Y') }} Tars AI</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user