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 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 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 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
|
# 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="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="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 ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
###> OpenAI API ###
|
###> OpenAI API ###
|
||||||
OPENAI_MODEL=gpt-4o
|
|
||||||
OPENAI_API_URL=https://api.openai.com/v1
|
OPENAI_API_URL=https://api.openai.com/v1
|
||||||
###< OpenAI API ###
|
###< OpenAI API ###
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,16 @@
|
|||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
# 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
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
# Calendar configuration
|
|
||||||
app.calendars.ics:
|
app.calendars.ics:
|
||||||
tim_calendar: 'webcal://p101-caldav.icloud.com/published/2/MTIwODk2NzA4MjIxMjA4OX8U9-11KVNdAw-HVVfEeHJioeELY1BwErQansnsIRnd'
|
tim_calendar:
|
||||||
household_calendar: 'webcal://p101-caldav.icloud.com/published/2/MTIwODk2NzA4MjIxMjA4OX8U9-11KVNdAw-HVVfEeHJDG4lEVQV-T3I5sEk0H6vfdGGP0X9Mpef_3zp3JNiiYvbAqzkgkukXO0nsKSxY1FA'
|
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:
|
services:
|
||||||
# default configuration for services in *this* file
|
# 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);
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
$days = 7;
|
$days = 14;
|
||||||
|
|
||||||
$from = new DateTime();
|
$from = new DateTime();
|
||||||
$to = (new DateTime())->modify("+$days days");
|
$to = (new DateTime())->modify("+$days days");
|
||||||
@ -36,7 +36,7 @@ class ReadCalendarCommand extends Command
|
|||||||
$calendars = $this->calendarService->getCalendars();
|
$calendars = $this->calendarService->getCalendars();
|
||||||
|
|
||||||
foreach ($calendars as $calendar) {
|
foreach ($calendars as $calendar) {
|
||||||
$events = $calendar->getEvents($from, $to);
|
$events = $calendar->getEventsByDateRange($from, $to);
|
||||||
$io->section($calendar->getName());
|
$io->section($calendar->getName());
|
||||||
$this->displayEvents($io, $events);
|
$this->displayEvents($io, $events);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,9 @@ use Exception;
|
|||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
#[AsCommand(
|
#[AsCommand(
|
||||||
name: 'app:run-agent',
|
name: 'app:run-agent',
|
||||||
@ -23,16 +25,43 @@ class RunAgentCommand extends Command
|
|||||||
parent::__construct();
|
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
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$isDryRun = $input->getOption('dry-run');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($isDryRun) {
|
||||||
|
$prompt = $this->agent->getPrompt();
|
||||||
|
$io->section('Prompt Preview');
|
||||||
|
$io->text($prompt);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
$result = $this->agent->run();
|
$result = $this->agent->run();
|
||||||
$output->writeln($result['prompt']);
|
$io->section('Prompt');
|
||||||
$output->writeln($result['response']);
|
$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;
|
return Command::SUCCESS;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$output->writeln('<error>'.$e->getMessage().'</error>');
|
$io->error($e->getMessage());
|
||||||
|
|
||||||
return Command::FAILURE;
|
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;
|
namespace App\Core\Agent;
|
||||||
|
|
||||||
|
use App\Core\Actions\ActionsProvider;
|
||||||
use App\Core\Services\AI\ChatServiceInterface;
|
use App\Core\Services\AI\ChatServiceInterface;
|
||||||
use App\Core\Services\Calendar\CalendarService;
|
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 DateTimeImmutable;
|
||||||
|
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
@ -16,7 +21,12 @@ class Agent
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PromptProvider $promptProvider,
|
private readonly PromptProvider $promptProvider,
|
||||||
private readonly CalendarService $calendarService,
|
private readonly CalendarService $calendarService,
|
||||||
|
private readonly HomeServiceInterface $homeService,
|
||||||
private readonly ChatServiceInterface $chatGPTService,
|
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();
|
$prompt = $this->getPrompt();
|
||||||
$response = $this->chatGPTService->sendMessage($prompt);
|
$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 [
|
return [
|
||||||
'prompt' => $prompt,
|
'prompt' => $prompt,
|
||||||
@ -40,17 +70,95 @@ class Agent
|
|||||||
$calendars = $this->calendarService->getCalendars();
|
$calendars = $this->calendarService->getCalendars();
|
||||||
$calendarEventsText = '';
|
$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) {
|
foreach ($calendars as $calendar) {
|
||||||
$calendarEventsText .= sprintf(
|
$calendarEventsText .= sprintf(
|
||||||
"- %s: %s\n",
|
"- %s: %s\n",
|
||||||
$calendar->getName(),
|
$calendar->getName(),
|
||||||
$calendar->getDescription(),
|
$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,
|
'{calendar_events}' => $calendarEventsText,
|
||||||
|
'{smart_home_devices}' => $smartHomeDevicesText,
|
||||||
'{current_time}' => $now->format('Y-m-d H:i:s'),
|
'{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
|
public function getPromptTemplate(): string
|
||||||
{
|
{
|
||||||
return <<<'EOT'
|
return <<<'EOT'
|
||||||
You are a high level smart home assistant.
|
You are a high level smart home assistant.
|
||||||
You can answer questions and help with tasks.
|
You can answer questions and help with tasks.
|
||||||
You can also control the smart home devices.
|
You can also control the smart home devices.
|
||||||
You can also control the calendar.
|
You can also control the calendar.
|
||||||
Tim and Cara are both members of the household.
|
Tim and Cara are both members of the household.
|
||||||
You have access to the following calendars:
|
You have access to the following calendars with the events from the next 14 days:
|
||||||
- Tim's calendar
|
{calendar_events}
|
||||||
- Cara's calendar
|
|
||||||
- Household calendar
|
You have also access to the states of the following smart home devices:
|
||||||
These are the events from the calendars:
|
{smart_home_devices}
|
||||||
{calendar_events}
|
|
||||||
|
The following information is stored yourself in the past:
|
||||||
Its currently {current_time}
|
{brain_storage}
|
||||||
I will ask you every 5 minutes to perform actions in the smart home.
|
|
||||||
Sometimes you have to do something, but sometimes you dont.
|
Its currently {current_time}
|
||||||
So its your turn. What actions to you want to perform?
|
I will ask you every 60 minutes to perform actions.
|
||||||
Answer with a JSON array of 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;
|
EOT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Core\Services\Calendar;
|
namespace App\Core\Services\Calendar;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
class Calendar
|
class Calendar
|
||||||
{
|
{
|
||||||
/** @var CalendarEvent[] */
|
/** @var CalendarEvent[] */
|
||||||
@ -51,6 +53,20 @@ class Calendar
|
|||||||
return $this->events;
|
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
|
public function addEvent(CalendarEvent $event): void
|
||||||
{
|
{
|
||||||
$this->events[] = $event;
|
$this->events[] = $event;
|
||||||
|
|||||||
@ -17,6 +17,9 @@ class CalendarService
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Calendar[]
|
||||||
|
*/
|
||||||
public function getCalendars(): array
|
public function getCalendars(): array
|
||||||
{
|
{
|
||||||
$allCalendars = [];
|
$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 = [];
|
$calendars = [];
|
||||||
|
|
||||||
foreach ($this->icsCalendars as $name => $url) {
|
foreach ($this->icsCalendars as $calendarConfig) {
|
||||||
$calendar = new Calendar(
|
$calendar = new Calendar(
|
||||||
name: $name,
|
name: $calendarConfig['name'],
|
||||||
description: sprintf('ICS Calendar: %s', $name),
|
description: $calendarConfig['description'],
|
||||||
url: $url,
|
url: $calendarConfig['url'],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
$content = $this->icsClient->fetchCalendarContent($url);
|
$content = $this->icsClient->fetchCalendarContent($calendarConfig['url']);
|
||||||
$this->icsParser->parseEvents($calendar, $content);
|
$this->icsParser->parseEvents($calendar, $content);
|
||||||
|
|
||||||
$calendars[] = $calendar;
|
$calendars[] = $calendar;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Services\HomeAssistant;
|
namespace App\Services\HomeAssistant;
|
||||||
|
|
||||||
use App\Core\Services\Home\HomeEntityInterface;
|
use App\Core\Services\Home\HomeEntityInterface;
|
||||||
|
use App\Core\Services\Home\HomeEntityType;
|
||||||
use App\Core\Services\Home\HomeServiceInterface;
|
use App\Core\Services\Home\HomeServiceInterface;
|
||||||
|
|
||||||
use function array_filter;
|
use function array_filter;
|
||||||
@ -35,10 +36,18 @@ final readonly class HomeAssistantHomeService implements HomeServiceInterface
|
|||||||
throw new HomeAssistantException('No entities found');
|
throw new HomeAssistantException('No entities found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_map(
|
$entities = [];
|
||||||
fn (EntityState $state) => new HomeAssistantEntity($state),
|
foreach ($states as $state) {
|
||||||
$states,
|
$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
|
public function callService(string $service, array $data = []): array
|
||||||
|
|||||||
@ -15,19 +15,16 @@ class OpenAIClient
|
|||||||
#[Autowire('%env(OPENAI_API_KEY)%')]
|
#[Autowire('%env(OPENAI_API_KEY)%')]
|
||||||
private readonly string $apiKey,
|
private readonly string $apiKey,
|
||||||
#[Autowire('%env(OPENAI_API_URL)%')]
|
#[Autowire('%env(OPENAI_API_URL)%')]
|
||||||
private readonly string $apiUrl,
|
private readonly string $apiUrl
|
||||||
#[Autowire('%env(OPENAI_MODEL)%')]
|
|
||||||
private readonly string $model,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function chat(array $messages, float $temperature = 0.7, int $maxTokens = 2048): array
|
public function chat(array $messages, float $temperature = 0.7, int $maxTokens = 2048): array
|
||||||
{
|
{
|
||||||
$response = $this->sendRequest('/chat/completions', [
|
$response = $this->sendRequest('/chat/completions', [
|
||||||
'model' => $this->model,
|
'model' => 'o3-mini',
|
||||||
'messages' => $messages,
|
'messages' => $messages,
|
||||||
'temperature' => $temperature,
|
'reasoning_effort' => 'low',
|
||||||
'max_tokens' => $maxTokens,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $response->toArray();
|
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 %}
|
{% block stylesheets %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50">
|
<body class="bg-gray-50">
|
||||||
<div class="container py-4">
|
<div class="min-h-screen flex flex-col">
|
||||||
<header class="pb-3 mb-4 border-bottom">
|
{% include '_menu.html.twig' %}
|
||||||
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
|
|
||||||
<span class="fs-4">Tars AI</span>
|
<main class="flex-grow container mx-auto px-4 py-8">
|
||||||
</a>
|
{% block body %}{% endblock %}
|
||||||
</header>
|
</main>
|
||||||
|
|
||||||
{% block body %}{% endblock %}
|
<footer class="bg-white border-t mt-auto">
|
||||||
|
<div class="container mx-auto px-4 py-6">
|
||||||
<footer class="pt-3 mt-4 text-muted border-top">
|
<p class="text-center text-gray-500">© {{ 'now'|date('Y') }} Tars AI</p>
|
||||||
© {{ 'now'|date('Y') }} Tars AI
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<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