diff --git a/.cursorrules b/.cursorrules
index 6bce2eb..2083d81 100644
--- a/.cursorrules
+++ b/.cursorrules
@@ -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.
\ No newline at end of file
+You carefully provide accurate, factual thoughtfull answers and are a genius at reasoning.
+
+Dont use make: to create or edit entities
\ No newline at end of file
diff --git a/.env b/.env
index bc233da..0e9d502 100644
--- a/.env
+++ b/.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 ###
diff --git a/config/services.yaml b/config/services.yaml
index d76d87d..de3bd97 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -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
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..f6a5550
--- /dev/null
+++ b/docker-compose.yml
@@ -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:
\ No newline at end of file
diff --git a/migrations/Version20250317180455.php b/migrations/Version20250317180455.php
new file mode 100644
index 0000000..59d03dc
--- /dev/null
+++ b/migrations/Version20250317180455.php
@@ -0,0 +1,31 @@
+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');
+ }
+}
diff --git a/migrations/Version20250317182942.php b/migrations/Version20250317182942.php
new file mode 100644
index 0000000..44a1a8a
--- /dev/null
+++ b/migrations/Version20250317182942.php
@@ -0,0 +1,31 @@
+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');
+ }
+}
diff --git a/migrations/Version20250317183016.php b/migrations/Version20250317183016.php
new file mode 100644
index 0000000..4bf7b69
--- /dev/null
+++ b/migrations/Version20250317183016.php
@@ -0,0 +1,31 @@
+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');
+ }
+}
diff --git a/src/Command/ReadCalendarCommand.php b/src/Command/ReadCalendarCommand.php
index e36b162..196c50a 100644
--- a/src/Command/ReadCalendarCommand.php
+++ b/src/Command/ReadCalendarCommand.php
@@ -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);
}
diff --git a/src/Command/RunAgentCommand.php b/src/Command/RunAgentCommand.php
index abf9ea5..b66454f 100644
--- a/src/Command/RunAgentCommand.php
+++ b/src/Command/RunAgentCommand.php
@@ -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(''.$e->getMessage().'');
+ $io->error($e->getMessage());
return Command::FAILURE;
}
diff --git a/src/Controller/AiActionController.php b/src/Controller/AiActionController.php
new file mode 100644
index 0000000..123b612
--- /dev/null
+++ b/src/Controller/AiActionController.php
@@ -0,0 +1,41 @@
+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,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Actions/ActionInterface.php b/src/Core/Actions/ActionInterface.php
new file mode 100644
index 0000000..65e714a
--- /dev/null
+++ b/src/Core/Actions/ActionInterface.php
@@ -0,0 +1,22 @@
+
+ */
+ public function getParameters(): array;
+
+ public function execute(array $parameters): void;
+}
diff --git a/src/Core/Actions/ActionsProvider.php b/src/Core/Actions/ActionsProvider.php
new file mode 100644
index 0000000..852fa93
--- /dev/null
+++ b/src/Core/Actions/ActionsProvider.php
@@ -0,0 +1,24 @@
+
+ */
+ public function getActions(): array
+ {
+ return iterator_to_array($this->actions);
+ }
+}
diff --git a/src/Core/Actions/BrainStorageAction.php b/src/Core/Actions/BrainStorageAction.php
new file mode 100644
index 0000000..d5c3295
--- /dev/null
+++ b/src/Core/Actions/BrainStorageAction.php
@@ -0,0 +1,51 @@
+ [
+ '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();
+ }
+}
+
diff --git a/src/Core/Actions/ImAliveAction.php b/src/Core/Actions/ImAliveAction.php
new file mode 100644
index 0000000..bd9b3df
--- /dev/null
+++ b/src/Core/Actions/ImAliveAction.php
@@ -0,0 +1,30 @@
+ [
+ '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
+ {
+ }
+}
+
diff --git a/src/Core/Actions/SendNotificationAction.php b/src/Core/Actions/SendNotificationAction.php
new file mode 100644
index 0000000..62f0b70
--- /dev/null
+++ b/src/Core/Actions/SendNotificationAction.php
@@ -0,0 +1,58 @@
+ [
+ '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
+ {
+
+ }
+}
diff --git a/src/Core/Agent/Agent.php b/src/Core/Agent/Agent.php
index 5a94cd1..ae3c312 100644
--- a/src/Core/Agent/Agent.php
+++ b/src/Core/Agent/Agent.php
@@ -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;
}
}
diff --git a/src/Core/Agent/PromptProvider.php b/src/Core/Agent/PromptProvider.php
index 3354c92..bd90e8f 100644
--- a/src/Core/Agent/PromptProvider.php
+++ b/src/Core/Agent/PromptProvider.php
@@ -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;
}
}
diff --git a/src/Core/Services/Calendar/Calendar.php b/src/Core/Services/Calendar/Calendar.php
index b078add..eef46e2 100644
--- a/src/Core/Services/Calendar/Calendar.php
+++ b/src/Core/Services/Calendar/Calendar.php
@@ -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;
diff --git a/src/Core/Services/Calendar/CalendarService.php b/src/Core/Services/Calendar/CalendarService.php
index 634f0c8..429ffa0 100644
--- a/src/Core/Services/Calendar/CalendarService.php
+++ b/src/Core/Services/Calendar/CalendarService.php
@@ -17,6 +17,9 @@ class CalendarService
) {
}
+ /**
+ * @return Calendar[]
+ */
public function getCalendars(): array
{
$allCalendars = [];
diff --git a/src/Entity/AiAction.php b/src/Entity/AiAction.php
new file mode 100644
index 0000000..2a630c4
--- /dev/null
+++ b/src/Entity/AiAction.php
@@ -0,0 +1,98 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/BrainStorageItem.php b/src/Entity/BrainStorageItem.php
new file mode 100644
index 0000000..0eb0033
--- /dev/null
+++ b/src/Entity/BrainStorageItem.php
@@ -0,0 +1,79 @@
+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;
+ }
+}
diff --git a/src/Repository/AiActionRepository.php b/src/Repository/AiActionRepository.php
new file mode 100644
index 0000000..6b21fc5
--- /dev/null
+++ b/src/Repository/AiActionRepository.php
@@ -0,0 +1,53 @@
+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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Repository/BrainStorageItemRepository.php b/src/Repository/BrainStorageItemRepository.php
new file mode 100644
index 0000000..919757d
--- /dev/null
+++ b/src/Repository/BrainStorageItemRepository.php
@@ -0,0 +1,18 @@
+
+ */
+class BrainStorageItemRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, BrainStorageItem::class);
+ }
+}
diff --git a/src/Service/AiActionLogger.php b/src/Service/AiActionLogger.php
new file mode 100644
index 0000000..1b7af48
--- /dev/null
+++ b/src/Service/AiActionLogger.php
@@ -0,0 +1,39 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/src/Services/Calendar/IcsCalendarProvider.php b/src/Services/Calendar/IcsCalendarProvider.php
index 1821551..7131125 100644
--- a/src/Services/Calendar/IcsCalendarProvider.php
+++ b/src/Services/Calendar/IcsCalendarProvider.php
@@ -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;
diff --git a/src/Services/HomeAssistant/HomeAssistantHomeService.php b/src/Services/HomeAssistant/HomeAssistantHomeService.php
index d8ec898..13998ae 100644
--- a/src/Services/HomeAssistant/HomeAssistantHomeService.php
+++ b/src/Services/HomeAssistant/HomeAssistantHomeService.php
@@ -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
diff --git a/src/Services/OpenAI/OpenAIClient.php b/src/Services/OpenAI/OpenAIClient.php
index 9cbf5c5..d2062b5 100644
--- a/src/Services/OpenAI/OpenAIClient.php
+++ b/src/Services/OpenAI/OpenAIClient.php
@@ -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();
diff --git a/templates/_menu.html.twig b/templates/_menu.html.twig
new file mode 100644
index 0000000..15d5449
--- /dev/null
+++ b/templates/_menu.html.twig
@@ -0,0 +1,68 @@
+{% set current_route = app.request.get('_route') %}
+
+
+
+
\ No newline at end of file
diff --git a/templates/ai_action/agent.html.twig b/templates/ai_action/agent.html.twig
new file mode 100644
index 0000000..6abd441
--- /dev/null
+++ b/templates/ai_action/agent.html.twig
@@ -0,0 +1,39 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}AI Actions by {{ agent }}{% endblock %}
+
+{% block body %}
+
+
+
+
+ {% for action in actions %}
+
+
+
+
{{ action.action }}
+
{{ action.description }}
+
+
+ {{ action.createdAt|date('Y-m-d H:i:s') }}
+
+
+ {% if action.context %}
+
+
{{ action.context|json_encode(constant('JSON_PRETTY_PRINT')) }}
+
+ {% endif %}
+
+ {% else %}
+
+ No actions found for this agent
+
+ {% endfor %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/ai_action/index.html.twig b/templates/ai_action/index.html.twig
new file mode 100644
index 0000000..e177363
--- /dev/null
+++ b/templates/ai_action/index.html.twig
@@ -0,0 +1,40 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}AI Actions{% endblock %}
+
+{% block body %}
+
+
Recent AI Actions
+
+
+ {% for action in actions %}
+
+
+
+
+ {{ action.createdAt|date('Y-m-d H:i:s') }}
+
+
+ {% if action.context %}
+
+
{{ action.context|json_encode(constant('JSON_PRETTY_PRINT')) }}
+
+ {% endif %}
+
+ {% else %}
+
+ No actions found
+
+ {% endfor %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/base.html.twig b/templates/base.html.twig
index 4b648c2..b14362c 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -10,17 +10,17 @@
{% block stylesheets %}{% endblock %}
-
-
+
+ {% include '_menu.html.twig' %}
+
+
+ {% block body %}{% endblock %}
+
- {% block body %}{% endblock %}
-
-