1
0
Aleksandr Trofimov 6 сар өмнө
parent
commit
665f961ca6

+ 2 - 1
composer.json

@@ -107,7 +107,8 @@
         "html2text/html2text": "^4.3",
         "knplabs/github-api": "^3.0",
         "guzzlehttp/guzzle": "^7.0.1",
-        "http-interop/http-factory-guzzle": "^1.0"
+        "http-interop/http-factory-guzzle": "^1.0",
+        "psr/log": "^3.0"
     },
     "scripts": {
         "post-update-cmd": [

+ 1 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "bca2e3a30fbd906066387ec4f852af2e",
+    "content-hash": "8f564e30ede1cd007042f79796804f51",
     "packages": [
         {
             "name": "achingbrain/php5-akismet",

+ 2 - 2
inc/classes/BxDolAIToolContentChange.php

@@ -19,7 +19,7 @@ class BxDolAIToolContentChange extends Tool
     {
         parent::__construct(
             'content_change',
-            'Use this tool to add, update or delete content. Never pass empty values for data field for "update" and "add" actions, always pass all required fields. Always user "content_structure" tool if it\'s available to get knowledge about content modules fields for add and update actions. Available modules: ' . self::getModules() . '.',
+            'Use this tool to add, update or delete content. Always use "content_structure" tool if it\'s available to get knowledge about content modules fields for "add" and "update" actions. ALL module fields MUST be placed inside data - no exceptions, flat fields are invalid. Available modules: ' . self::getModules() . '.',
         );
     }
     
@@ -53,7 +53,7 @@ class BxDolAIToolContentChange extends Tool
             new ToolProperty(
                 name: 'data',
                 type: PropertyType::OBJECT,
-                description: 'The data to update the content item with, as key-value pairs, where key is the field name and value is the value. Skip this parameter for delete action, for create and update actions this is required.',
+                description: 'The data to update the content item with, as key-value pairs, where key is the field name and value is the value. Skip this parameter for delete action, for "create" and "update" actions this is mandatory.',
                 required: false
             ),
             new ToolProperty(

+ 6 - 1
inc/classes/BxDolAiAgent.php

@@ -9,6 +9,7 @@
 
 use NeuronAI\RAG\RAG;
 use NeuronAI\Observability\InspectorObserver;
+use NeuronAI\Observability\LogObserver;
 
 class BxDolAiAgent extends RAG
 {
@@ -21,7 +22,11 @@ class BxDolAiAgent extends RAG
             $this->observe(InspectorObserver::instance(
                 key: $sKey,
                 autoFlush: true,
-            ));
+            ));            
+        }
+        else {
+            $logger = new BxDolLoggerDb('sys_agents_' . $this->aAgent['id']);
+            $this->observe(new LogObserver($logger));
         }
     }
 

+ 111 - 0
inc/classes/BxDolLoggerDb.php

@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+use Psr\Log\AbstractLogger;
+use Psr\Log\LogLevel;
+
+final class BxDolLoggerDb extends AbstractLogger
+{
+    private const LEVELS = [
+        LogLevel::DEBUG     => 0,
+        LogLevel::INFO      => 1,
+        LogLevel::NOTICE    => 2,
+        LogLevel::WARNING   => 3,
+        LogLevel::ERROR     => 4,
+        LogLevel::CRITICAL  => 5,
+        LogLevel::ALERT     => 6,
+        LogLevel::EMERGENCY => 7,
+    ];
+
+    private \PDO $pdo;
+    private \PDOStatement $stmt;
+    private int $minimumLevel;
+    private string $channel;
+
+    public function __construct(string $channel = 'system')
+    {
+        $this->channel      = $channel;
+        $this->minimumLevel = self::LEVELS[LogLevel::INFO];
+
+        $this->pdo = BxDolDb::getInstance()->getLink();
+
+        $this->stmt = $this->pdo->prepare(
+            'INSERT INTO `sys_logger` (level, message, context, channel, created_at)
+             VALUES (:level, :message, :context, :channel, :created_at)'
+        );
+    }
+
+    public function log(mixed $level, string|Stringable $message, array $context = []): void
+    {
+        $level = (string) $level;
+
+        if (!isset(self::LEVELS[$level])) {
+            throw new \Psr\Log\InvalidArgumentException("Unknown log level: {$level}");
+        }
+
+        if (self::LEVELS[$level] < $this->minimumLevel) {
+            return;
+        }
+
+        try {
+            $this->stmt->execute([
+                ':level'      => $level,
+                ':message'    => $this->interpolate((string) $message, $context),
+                ':context'    => $this->contextToString($context),
+                ':channel'    => $this->channel,
+                ':created_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s.v'),
+            ]);
+        } catch (Throwable) {
+            // Swallow — logging must never crash the application
+        }
+    }
+
+    private function contextToString(array $context): ?string
+    {
+        if (empty($context)) {
+            return null;
+        }
+
+        $parts = [];
+
+        if (isset($context['exception']) && $context['exception'] instanceof Throwable) {
+            $e = $context['exception'];
+            $parts[] = sprintf(
+                "[exception] %s(%d): %s\nFile: %s:%d\nTrace:\n%s",
+                $e::class, $e->getCode(), $e->getMessage(),
+                $e->getFile(), $e->getLine(), $e->getTraceAsString()
+            );
+            unset($context['exception']);
+        }
+
+        foreach ($context as $key => $value) {
+            $parts[] = sprintf('[%s] %s', $key, match (true) {
+                is_null($value)                       => 'null',
+                is_bool($value)                       => $value ? 'true' : 'false',
+                is_scalar($value)                     => (string) $value,
+                $value instanceof Stringable          => (string) $value,
+                is_array($value) || is_object($value) => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
+                default                               => gettype($value),
+            });
+        }
+
+        return implode("\n", $parts);
+    }
+
+    private function interpolate(string $message, array $context): string
+    {
+        if (!str_contains($message, '{')) {
+            return $message;
+        }
+
+        $replace = [];
+        foreach ($context as $key => $value) {
+            if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) {
+                $replace['{' . $key . '}'] = (string) $value;
+            }
+        }
+
+        return strtr($message, $replace);
+    }
+}

+ 16 - 0
install/sql/system.sql

@@ -7736,3 +7736,19 @@ INSERT INTO `sys_std_pages_widgets`(`page_id`, `widget_id`, `order`) VALUES(@iId
 INSERT INTO `sys_std_widgets`(`page_id`, `module`, `type`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
 (@iIdAgents, 'system', 'configuration', '{url_studio}agents.php', '', 'wi-agents.svg', '_adm_wgt_cpt_agents', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
 INSERT INTO `sys_std_pages_widgets`(`page_id`, `widget_id`, `order`) VALUES(@iIdHome, LAST_INSERT_ID(), 15);
+
+-- Logger 
+
+CREATE TABLE `sys_logger` (
+    `id`         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    `level`      VARCHAR(16)     NOT NULL,
+    `message`    TEXT            NOT NULL,
+    `context`    TEXT            NULL,
+    `channel`    VARCHAR(64)     NOT NULL DEFAULT 'system',
+    `created_at` DATETIME(3)     NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+
+    PRIMARY KEY (`id`),
+    INDEX `idx_level`      (`level`),
+    INDEX `idx_channel`    (`channel`),
+    INDEX `idx_created_at` (`created_at`)
+);