xref: /plugin/dokullm/LlmClient.php (revision 85ad8ea9e52c070d54b8d3a99d1f7e363f474c2a)
159036814SCostin Stroie<?php
259036814SCostin Stroienamespace dokuwiki\plugin\dokullm;
359036814SCostin Stroie
459036814SCostin Stroie/**
559036814SCostin Stroie * LLM Client for the dokullm plugin
659036814SCostin Stroie *
759036814SCostin Stroie * This class provides methods to interact with an LLM API for various
859036814SCostin Stroie * text processing tasks such as completion, rewriting, grammar correction,
959036814SCostin Stroie * summarization, conclusion creation, text analysis, and custom prompts.
1059036814SCostin Stroie *
1159036814SCostin Stroie * The client handles:
1259036814SCostin Stroie * - API configuration and authentication
1359036814SCostin Stroie * - Prompt template loading and processing
1459036814SCostin Stroie * - Context-aware requests with metadata
1559036814SCostin Stroie * - DokuWiki page content retrieval
1659036814SCostin Stroie */
1759036814SCostin Stroie
1859036814SCostin Stroie// must be run within Dokuwiki
1959036814SCostin Stroieif (!defined('DOKU_INC')) {
2059036814SCostin Stroie    die();
2159036814SCostin Stroie}
2259036814SCostin Stroie
239b704e62SCostin Stroie (aider)/**
249b704e62SCostin Stroie (aider) * Get configuration value for the dokullm plugin
259b704e62SCostin Stroie (aider) *
269b704e62SCostin Stroie (aider) * @param string $key Configuration key
279b704e62SCostin Stroie (aider) * @param mixed $default Default value if key not found
289b704e62SCostin Stroie (aider) * @return mixed Configuration value
299b704e62SCostin Stroie (aider) */
309b704e62SCostin Stroie (aider)function getConf($key, $default = null) {
319b704e62SCostin Stroie (aider)    global $conf;
329b704e62SCostin Stroie (aider)    return isset($conf['plugin']['dokullm'][$key]) ? $conf['plugin']['dokullm'][$key] : $default;
339b704e62SCostin Stroie (aider)}
349b704e62SCostin Stroie (aider)
3559036814SCostin Stroie
3659036814SCostin Stroie/**
3759036814SCostin Stroie * LLM Client class for handling API communications
3859036814SCostin Stroie *
3959036814SCostin Stroie * Manages configuration settings and provides methods for various
4059036814SCostin Stroie * text processing operations through an LLM API.
4159036814SCostin Stroie * Implements caching for tool calls to avoid duplicate processing.
4259036814SCostin Stroie */
4359036814SCostin Stroieclass LlmClient
4459036814SCostin Stroie{
4559036814SCostin Stroie    /** @var string The API endpoint URL */
4659036814SCostin Stroie    private $api_url;
4759036814SCostin Stroie
4859036814SCostin Stroie    /** @var array Cache for tool call results */
4959036814SCostin Stroie    private $toolCallCache = [];
5059036814SCostin Stroie
5159036814SCostin Stroie    /** @var string Current text for tool usage */
5259036814SCostin Stroie    private $currentText = '';
5359036814SCostin Stroie
5459036814SCostin Stroie    /** @var array Track tool call counts to prevent infinite loops */
5559036814SCostin Stroie    private $toolCallCounts = [];
5659036814SCostin Stroie
5759036814SCostin Stroie    /** @var string The API authentication key */
5859036814SCostin Stroie    private $api_key;
5959036814SCostin Stroie
6059036814SCostin Stroie    /** @var string The model identifier to use */
6159036814SCostin Stroie    private $model;
6259036814SCostin Stroie
6359036814SCostin Stroie    /** @var int The request timeout in seconds */
6459036814SCostin Stroie    private $timeout;
6559036814SCostin Stroie
6659036814SCostin Stroie    /** @var float The temperature setting for response randomness */
6759036814SCostin Stroie    private $temperature;
6859036814SCostin Stroie
6959036814SCostin Stroie    /** @var float The top-p setting for nucleus sampling */
7059036814SCostin Stroie    private $top_p;
7159036814SCostin Stroie
7259036814SCostin Stroie    /** @var int The top-k setting for token selection */
7359036814SCostin Stroie    private $top_k;
7459036814SCostin Stroie
7559036814SCostin Stroie    /** @var float The min-p setting for minimum probability threshold */
7659036814SCostin Stroie    private $min_p;
7759036814SCostin Stroie
7859036814SCostin Stroie    /** @var bool Whether to enable thinking in the LLM responses */
7959036814SCostin Stroie    private $think;
8059036814SCostin Stroie
8159036814SCostin Stroie    /**
8259036814SCostin Stroie     * Initialize the LLM client with configuration settings
8359036814SCostin Stroie     *
8459036814SCostin Stroie     * Retrieves configuration values from DokuWiki's configuration system
8559036814SCostin Stroie     * for API URL, key, model, timeout, and LLM sampling parameters.
8659036814SCostin Stroie     *
8759036814SCostin Stroie     * Configuration values:
8859036814SCostin Stroie     * - api_url: The LLM API endpoint URL
8959036814SCostin Stroie     * - api_key: Authentication key for the API (optional)
9059036814SCostin Stroie     * - model: The model identifier to use for requests
9159036814SCostin Stroie     * - timeout: Request timeout in seconds
9259036814SCostin Stroie     * - language: Language code for prompt templates
9359036814SCostin Stroie     * - temperature: Temperature setting for response randomness (0.0-1.0)
9459036814SCostin Stroie     * - top_p: Top-p (nucleus sampling) setting (0.0-1.0)
9559036814SCostin Stroie     * - top_k: Top-k setting (integer >= 1)
9659036814SCostin Stroie     * - min_p: Minimum probability threshold (0.0-1.0)
9759036814SCostin Stroie     * - think: Whether to enable thinking in LLM responses (boolean)
9859036814SCostin Stroie     */
99*85ad8ea9SCostin Stroie (aider)    public function __construct($api_url = null, $api_key = null, $model = null, $timeout = null, $temperature = null, $top_p = null, $top_k = null, $min_p = null, $think = null)
10059036814SCostin Stroie    {
101*85ad8ea9SCostin Stroie (aider)        $this->api_url = $api_url ?? $this->getConf('api_url');
102*85ad8ea9SCostin Stroie (aider)        $this->api_key = $api_key ?? $this->getConf('api_key');
103*85ad8ea9SCostin Stroie (aider)        $this->model = $model ?? $this->getConf('model');
104*85ad8ea9SCostin Stroie (aider)        $this->timeout = $timeout ?? $this->getConf('timeout');
105*85ad8ea9SCostin Stroie (aider)        $this->temperature = $temperature ?? $this->getConf('temperature');
106*85ad8ea9SCostin Stroie (aider)        $this->top_p = $top_p ?? $this->getConf('top_p');
107*85ad8ea9SCostin Stroie (aider)        $this->top_k = $top_k ?? $this->getConf('top_k');
108*85ad8ea9SCostin Stroie (aider)        $this->min_p = $min_p ?? $this->getConf('min_p');
109*85ad8ea9SCostin Stroie (aider)        $this->think = $think ?? $this->getConf('think', false);
11059036814SCostin Stroie    }
11159036814SCostin Stroie
11259036814SCostin Stroie
11359036814SCostin Stroie
11459036814SCostin Stroie    public function process($action, $text, $metadata = [], $useContext = true)
11559036814SCostin Stroie    {
11659036814SCostin Stroie        // Store the current text for tool usage
11759036814SCostin Stroie        $this->currentText = $text;
11859036814SCostin Stroie
11959036814SCostin Stroie        // Add text, think and action to metadata
12059036814SCostin Stroie        $metadata['text'] = $text;
12159036814SCostin Stroie        $metadata['think'] = $this->think ? '/think' : '/no_think';
12259036814SCostin Stroie        $metadata['action'] = $action;
12359036814SCostin Stroie
12459036814SCostin Stroie        // If we have 'template' in metadata, move it to 'page_template'
12559036814SCostin Stroie        if (isset($metadata['template'])) {
12659036814SCostin Stroie            $metadata['page_template'] = $metadata['template'];
12759036814SCostin Stroie            unset($metadata['template']);
12859036814SCostin Stroie        }
12959036814SCostin Stroie
13059036814SCostin Stroie        // If we have 'examples' in metadata, move it to 'page_examples'
13159036814SCostin Stroie        if (isset($metadata['examples'])) {
13259036814SCostin Stroie            $metadata['page_examples'] = $metadata['examples'];
13359036814SCostin Stroie            unset($metadata['examples']);
13459036814SCostin Stroie        }
13559036814SCostin Stroie
13659036814SCostin Stroie        // If we have 'previous' in metadata, move it to 'page_previous'
13759036814SCostin Stroie        if (isset($metadata['previous'])) {
13859036814SCostin Stroie            $metadata['page_previous'] = $metadata['previous'];
13959036814SCostin Stroie            unset($metadata['previous']);
14059036814SCostin Stroie        }
14159036814SCostin Stroie
14259036814SCostin Stroie        $prompt = $this->loadPrompt($action, $metadata);
14359036814SCostin Stroie
14459036814SCostin Stroie        return $this->callAPI($action, $prompt, $metadata, $useContext);
14559036814SCostin Stroie    }
14659036814SCostin Stroie
14759036814SCostin Stroie
14859036814SCostin Stroie
14959036814SCostin Stroie    /**
15059036814SCostin Stroie     * Create the provided text using the LLM
15159036814SCostin Stroie     *
15259036814SCostin Stroie     * Sends a prompt to the LLM asking it to create the given text.
15359036814SCostin Stroie     * First queries ChromaDB for relevant documents to include as examples.
15459036814SCostin Stroie     * If no template is defined, queries ChromaDB for a template.
15559036814SCostin Stroie     *
15659036814SCostin Stroie     * @param string $text The text to create
15759036814SCostin Stroie     * @param array $metadata Optional metadata containing template, examples, and snippets
15859036814SCostin Stroie     * @param bool $useContext Whether to include template and examples in the context (default: true)
15959036814SCostin Stroie     * @return string The created text
16059036814SCostin Stroie     */
16159036814SCostin Stroie    public function createReport($text, $metadata = [], $useContext = true)
16259036814SCostin Stroie    {
16359036814SCostin Stroie        // Store the current text for tool usage
16459036814SCostin Stroie        $this->currentText = $text;
16559036814SCostin Stroie
16659036814SCostin Stroie        // Check if tools should be used based on configuration
1679b704e62SCostin Stroie (aider)        $useTools = $this->getConf('use_tools', false);
16859036814SCostin Stroie
16959036814SCostin Stroie        // Only try to find template and add snippets if tools are not enabled
17059036814SCostin Stroie        // When tools are enabled, the LLM will call get_template and get_examples as needed
17159036814SCostin Stroie        if (!$useTools) {
17259036814SCostin Stroie            // If no template is defined, try to find one using ChromaDB
17359036814SCostin Stroie            if (empty($metadata['template'])) {
17459036814SCostin Stroie                $templateResult = $this->queryChromaDBTemplate($text);
17559036814SCostin Stroie                if (!empty($templateResult)) {
17659036814SCostin Stroie                    // Use the first result as template
17759036814SCostin Stroie                    $metadata['template'] = $templateResult[0];
17859036814SCostin Stroie                }
17959036814SCostin Stroie            }
18059036814SCostin Stroie
18159036814SCostin Stroie            // Query ChromaDB for relevant documents to use as examples
18259036814SCostin Stroie            $chromaResults = $this->queryChromaDBSnippets($text, 10);
18359036814SCostin Stroie
18459036814SCostin Stroie            // Add ChromaDB results to metadata as snippets
18559036814SCostin Stroie            if (!empty($chromaResults)) {
18659036814SCostin Stroie                // Merge with existing snippets
18759036814SCostin Stroie                $metadata['snippets'] = array_merge(
18859036814SCostin Stroie                    isset($metadata['snippets']) ? $metadata['snippets'] : [],
18959036814SCostin Stroie                    $chromaResults
19059036814SCostin Stroie                );
19159036814SCostin Stroie            }
19259036814SCostin Stroie        }
19359036814SCostin Stroie
19459036814SCostin Stroie        $think = $this->think ? '/think' : '/no_think';
19559036814SCostin Stroie        $prompt = $this->loadPrompt('create', ['text' => $text, 'think' => $think]);
19659036814SCostin Stroie
19759036814SCostin Stroie        return $this->callAPI('create', $prompt, $metadata, $useContext);
19859036814SCostin Stroie    }
19959036814SCostin Stroie
20059036814SCostin Stroie    /**
20159036814SCostin Stroie     * Compare two texts and highlight differences
20259036814SCostin Stroie     *
20359036814SCostin Stroie     * Sends a prompt to the LLM asking it to compare two texts and
20459036814SCostin Stroie     * highlight their similarities and differences.
20559036814SCostin Stroie     *
20659036814SCostin Stroie     * @param string $text The current text to compare
20759036814SCostin Stroie     * @param array $metadata Optional metadata containing template, examples, and previous report reference
20859036814SCostin Stroie     * @return string The comparison results
20959036814SCostin Stroie     */
21059036814SCostin Stroie    public function compareText($text, $metadata = [], $useContext = false)
21159036814SCostin Stroie    {
21259036814SCostin Stroie        // Store the current text for tool usage
21359036814SCostin Stroie        $this->currentText = $text;
21459036814SCostin Stroie
21559036814SCostin Stroie        // Load previous report from metadata if specified
21659036814SCostin Stroie        $previousText = '';
21759036814SCostin Stroie        if (!empty($metadata['previous_report_page'])) {
21859036814SCostin Stroie            $previousText = $this->getPageContent($metadata['previous_report_page']);
21959036814SCostin Stroie            if ($previousText === false) {
22059036814SCostin Stroie                $previousText = '';
22159036814SCostin Stroie            }
22259036814SCostin Stroie        }
22359036814SCostin Stroie
22459036814SCostin Stroie        // Extract dates for placeholders
22559036814SCostin Stroie        $currentDate = $this->getPageDate();
22659036814SCostin Stroie        $previousDate = !empty($metadata['previous_report_page']) ?
22759036814SCostin Stroie                        $this->getPageDate($metadata['previous_report_page']) :
22859036814SCostin Stroie                        '';
22959036814SCostin Stroie
23059036814SCostin Stroie        $think = $this->think ? '/think' : '/no_think';
23159036814SCostin Stroie        $prompt = $this->loadPrompt('compare', [
23259036814SCostin Stroie            'text' => $text,
23359036814SCostin Stroie            'previous_text' => $previousText,
23459036814SCostin Stroie            'current_date' => $currentDate,
23559036814SCostin Stroie            'previous_date' => $previousDate,
23659036814SCostin Stroie            'think' => $think
23759036814SCostin Stroie        ]);
23859036814SCostin Stroie
23959036814SCostin Stroie        return $this->callAPI('compare', $prompt, $metadata, $useContext);
24059036814SCostin Stroie    }
24159036814SCostin Stroie
24259036814SCostin Stroie    /**
24359036814SCostin Stroie     * Process text with a custom user prompt
24459036814SCostin Stroie     *
24559036814SCostin Stroie     * Sends a custom prompt to the LLM along with the provided text.
24659036814SCostin Stroie     *
24759036814SCostin Stroie     * @param string $text The text to process
24859036814SCostin Stroie     * @param string $customPrompt The custom prompt to use
24959036814SCostin Stroie     * @param array $metadata Optional metadata containing template and examples
25059036814SCostin Stroie     * @param bool $useContext Whether to include template and examples in the context (default: true)
25159036814SCostin Stroie     * @return string The processed text
25259036814SCostin Stroie     */
25359036814SCostin Stroie    public function processCustomPrompt($text, $metadata = [], $useContext = true)
25459036814SCostin Stroie    {
25559036814SCostin Stroie        // Store the current text for tool usage
25659036814SCostin Stroie        $this->currentText = $text;
25759036814SCostin Stroie
25859036814SCostin Stroie        // Format the prompt with the text and custom prompt
25959036814SCostin Stroie        $prompt = $metadata['prompt'] . "\n\nText to process:\n" . $text;
26059036814SCostin Stroie
26159036814SCostin Stroie        return $this->callAPI('custom', $prompt, $metadata, $useContext);
26259036814SCostin Stroie    }
26359036814SCostin Stroie
26459036814SCostin Stroie    /**
26559036814SCostin Stroie     * Get the list of available tools for the LLM
26659036814SCostin Stroie     *
26759036814SCostin Stroie     * Defines the tools that can be used by the LLM during processing.
26859036814SCostin Stroie     *
26959036814SCostin Stroie     * @return array List of tool definitions
27059036814SCostin Stroie     */
27159036814SCostin Stroie    private function getAvailableTools()
27259036814SCostin Stroie    {
27359036814SCostin Stroie        return [
27459036814SCostin Stroie            [
27559036814SCostin Stroie                'type' => 'function',
27659036814SCostin Stroie                'function' => [
27759036814SCostin Stroie                    'name' => 'get_document',
27859036814SCostin Stroie                    'description' => 'Retrieve the full content of a specific document by providing its unique document ID. Use this when you need to access the complete text of a particular document for reference or analysis.',
27959036814SCostin Stroie                    'parameters' => [
28059036814SCostin Stroie                        'type' => 'object',
28159036814SCostin Stroie                        'properties' => [
28259036814SCostin Stroie                            'id' => [
28359036814SCostin Stroie                                'type' => 'string',
28459036814SCostin Stroie                                'description' => 'The unique identifier of the document to retrieve. This should be a valid document ID that exists in the system.'
28559036814SCostin Stroie                            ]
28659036814SCostin Stroie                        ],
28759036814SCostin Stroie                        'required' => ['id']
28859036814SCostin Stroie                    ]
28959036814SCostin Stroie                ]
29059036814SCostin Stroie            ],
29159036814SCostin Stroie            [
29259036814SCostin Stroie                'type' => 'function',
29359036814SCostin Stroie                'function' => [
29459036814SCostin Stroie                    'name' => 'get_template',
29559036814SCostin Stroie                    'description' => 'Retrieve a relevant template document that matches the current context and content. Use this when you need a structural template or format example to base your response on, particularly for creating consistent reports or documents.',
29659036814SCostin Stroie                    'parameters' => [
29759036814SCostin Stroie                        'type' => 'object',
29859036814SCostin Stroie                        'properties' => [
29959036814SCostin Stroie                            'language' => [
30059036814SCostin Stroie                                'type' => 'string',
30159036814SCostin Stroie                                'description' => 'The language the template should be written in (e.g., "ro" for Romanian, "en" for English).',
30259036814SCostin Stroie                                'default' => 'ro'
30359036814SCostin Stroie                            ]
30459036814SCostin Stroie                        ]
30559036814SCostin Stroie                    ]
30659036814SCostin Stroie                ]
30759036814SCostin Stroie            ],
30859036814SCostin Stroie            [
30959036814SCostin Stroie                'type' => 'function',
31059036814SCostin Stroie                'function' => [
31159036814SCostin Stroie                    'name' => 'get_examples',
31259036814SCostin Stroie                    'description' => 'Retrieve relevant example snippets from previous reports that are similar to the current context. Use this when you need to see how similar content was previously handled, to maintain consistency in style, terminology, and structure.',
31359036814SCostin Stroie                    'parameters' => [
31459036814SCostin Stroie                        'type' => 'object',
31559036814SCostin Stroie                        'properties' => [
31659036814SCostin Stroie                            'count' => [
31759036814SCostin Stroie                                'type' => 'integer',
31859036814SCostin Stroie                                'description' => 'The number of examples to retrieve (1-20). Use more examples when you need comprehensive reference material, fewer when you need just a quick reminder of the style.',
31959036814SCostin Stroie                                'default' => 5
32059036814SCostin Stroie                            ]
32159036814SCostin Stroie                        ]
32259036814SCostin Stroie                    ]
32359036814SCostin Stroie                ]
32459036814SCostin Stroie            ]
32559036814SCostin Stroie        ];
32659036814SCostin Stroie    }
32759036814SCostin Stroie
32859036814SCostin Stroie    /**
32959036814SCostin Stroie     * Call the LLM API with the specified prompt
33059036814SCostin Stroie     *
33159036814SCostin Stroie     * Makes an HTTP POST request to the configured API endpoint with
33259036814SCostin Stroie     * the prompt and other parameters. Handles authentication if an
33359036814SCostin Stroie     * API key is configured.
33459036814SCostin Stroie     *
33559036814SCostin Stroie     * The method constructs a conversation with system and user messages,
33659036814SCostin Stroie     * including context information from metadata when available.
33759036814SCostin Stroie     *
33859036814SCostin Stroie     * Complex logic includes:
33959036814SCostin Stroie     * 1. Loading and enhancing the system prompt with metadata context
34059036814SCostin Stroie     * 2. Building the API request with model parameters
34159036814SCostin Stroie     * 3. Handling authentication with API key if configured
34259036814SCostin Stroie     * 4. Making the HTTP request with proper error handling
34359036814SCostin Stroie     * 5. Parsing and validating the API response
34459036814SCostin Stroie     * 6. Supporting tool usage with automatic tool calling when enabled
34559036814SCostin Stroie     * 7. Implementing context enhancement with templates, examples, and snippets
34659036814SCostin Stroie     *
34759036814SCostin Stroie     * The context information includes:
34859036814SCostin Stroie     * - Template content: Used as a starting point for the response
34959036814SCostin Stroie     * - Example pages: Full content of specified example pages
35059036814SCostin Stroie     * - Text snippets: Relevant text examples from ChromaDB
35159036814SCostin Stroie     *
35259036814SCostin Stroie     * When tools are enabled, the method supports automatic tool calling:
35359036814SCostin Stroie     * - Tools can retrieve documents, templates, and examples as needed
35459036814SCostin Stroie     * - Tool responses are cached to avoid duplicate calls with identical parameters
35559036814SCostin Stroie     * - Infinite loop protection prevents excessive tool calls
35659036814SCostin Stroie     *
35759036814SCostin Stroie     * @param string $command The command name for loading command-specific system prompts
35859036814SCostin Stroie     * @param string $prompt The prompt to send to the LLM as user message
35959036814SCostin Stroie     * @param array $metadata Optional metadata containing template, examples, and snippets
36059036814SCostin Stroie     * @param bool $useContext Whether to include template and examples in the context (default: true)
36159036814SCostin Stroie     * @return string The response content from the LLM
36259036814SCostin Stroie     * @throws Exception If the API request fails or returns unexpected format
36359036814SCostin Stroie     */
36459036814SCostin Stroie
36559036814SCostin Stroie    private function callAPI($command, $prompt, $metadata = [], $useContext = true)
36659036814SCostin Stroie    {
36759036814SCostin Stroie        // Load system prompt which provides general instructions to the LLM
36859036814SCostin Stroie        $systemPrompt = $this->loadSystemPrompt($command, []);
36959036814SCostin Stroie
37059036814SCostin Stroie        // Enhance the prompt with context information from metadata
37159036814SCostin Stroie        // This provides the LLM with additional context about templates and examples
37259036814SCostin Stroie        if ($useContext && !empty($metadata) && (!empty($metadata['template']) || !empty($metadata['examples']) || !empty($metadata['snippets']))) {
37359036814SCostin Stroie            $contextInfo = "\n\n<context>\n";
37459036814SCostin Stroie
37559036814SCostin Stroie            // Add template content if specified in metadata
37659036814SCostin Stroie            if (!empty($metadata['template'])) {
37759036814SCostin Stroie                $templateContent = $this->getPageContent($metadata['template']);
37859036814SCostin Stroie                if ($templateContent !== false) {
37959036814SCostin Stroie                    $contextInfo .= "\n\n<template>\nPornește de la acest template (" . $metadata['template'] . "):\n" . $templateContent . "\n</template>\n";
38059036814SCostin Stroie                }
38159036814SCostin Stroie            }
38259036814SCostin Stroie
38359036814SCostin Stroie            // Add example pages content if specified in metadata
38459036814SCostin Stroie            if (!empty($metadata['examples'])) {
38559036814SCostin Stroie                $examplesContent = [];
38659036814SCostin Stroie                foreach ($metadata['examples'] as $example) {
38759036814SCostin Stroie                    $content = $this->getPageContent($example);
38859036814SCostin Stroie                    if ($content !== false) {
38959036814SCostin Stroie                        $examplesContent[] = "\n<example_page source=\"" . $example . "\">\n" . $content . "\n</example_page>\n";
39059036814SCostin Stroie                    }
39159036814SCostin Stroie                }
39259036814SCostin Stroie                if (!empty($examplesContent)) {
39359036814SCostin Stroie                    $contextInfo .= "\n<style_examples>\nAcestea sunt rapoarte complete anterioare - studiază stilul meu de redactare:\n" . implode("\n", $examplesContent) . "\n</style_examples>\n";
39459036814SCostin Stroie                }
39559036814SCostin Stroie            }
39659036814SCostin Stroie
39759036814SCostin Stroie            // Add text snippets if specified in metadata
39859036814SCostin Stroie            if (!empty($metadata['snippets'])) {
39959036814SCostin Stroie                $snippetsContent = [];
40059036814SCostin Stroie                foreach ($metadata['snippets'] as $index => $snippet) {
40159036814SCostin Stroie                    // These are text snippets from ChromaDB
40259036814SCostin Stroie                    $snippetsContent[] = "\n<example id=\"" . ($index + 1) . "\">\n" . $snippet . "\n</example>\n";
40359036814SCostin Stroie                }
40459036814SCostin Stroie                if (!empty($snippetsContent)) {
40559036814SCostin Stroie                    $contextInfo .= "\n\n<style_examples>\nAcestea sunt exemple din rapoartele mele anterioare - studiază stilul de redactare, terminologia și structura frazelor:\n" . implode("\n", $snippetsContent) . "\n</style_examples>\n";
40659036814SCostin Stroie                }
40759036814SCostin Stroie            }
40859036814SCostin Stroie
40959036814SCostin Stroie            $contextInfo .= "\n</context>\n";
41059036814SCostin Stroie
41159036814SCostin Stroie            // Append context information to system prompt
41259036814SCostin Stroie            $prompt = $contextInfo . "\n\n" . $prompt;
41359036814SCostin Stroie        }
41459036814SCostin Stroie
41559036814SCostin Stroie        // Check if tools should be used based on configuration
4169b704e62SCostin Stroie (aider)        $useTools = $this->getConf('use_tools', false);
41759036814SCostin Stroie
41859036814SCostin Stroie        // Prepare API request data with model parameters
41959036814SCostin Stroie        $data = [
42059036814SCostin Stroie            'model' => $this->model,
42159036814SCostin Stroie            'messages' => [
42259036814SCostin Stroie                ['role' => 'system', 'content' => $systemPrompt],
42359036814SCostin Stroie                ['role' => 'user', 'content' => $prompt]
42459036814SCostin Stroie            ],
42559036814SCostin Stroie            'max_tokens' => 6144,
42659036814SCostin Stroie            'stream' => false,
42759036814SCostin Stroie            'keep_alive' => '30m',
42859036814SCostin Stroie            'think' => true
42959036814SCostin Stroie        ];
43059036814SCostin Stroie
43159036814SCostin Stroie        // Add tools to the request only if useTools is true
43259036814SCostin Stroie        if ($useTools) {
43359036814SCostin Stroie            // Define available tools
43459036814SCostin Stroie            $data['tools'] = $this->getAvailableTools();
43559036814SCostin Stroie            $data['tool_choice'] = 'auto';
43659036814SCostin Stroie            $data['parallel_tool_calls'] = false;
43759036814SCostin Stroie        }
43859036814SCostin Stroie
43959036814SCostin Stroie        // Only add parameters if they are defined and not null
44059036814SCostin Stroie        if ($this->temperature !== null) {
44159036814SCostin Stroie            $data['temperature'] = $this->temperature;
44259036814SCostin Stroie        }
44359036814SCostin Stroie        if ($this->top_p !== null) {
44459036814SCostin Stroie            $data['top_p'] = $this->top_p;
44559036814SCostin Stroie        }
44659036814SCostin Stroie        if ($this->top_k !== null) {
44759036814SCostin Stroie            $data['top_k'] = $this->top_k;
44859036814SCostin Stroie        }
44959036814SCostin Stroie        if ($this->min_p !== null) {
45059036814SCostin Stroie            $data['min_p'] = $this->min_p;
45159036814SCostin Stroie        }
45259036814SCostin Stroie
45359036814SCostin Stroie        // Make an API call with tool responses
45459036814SCostin Stroie        return $this->callAPIWithTools($data, false);
45559036814SCostin Stroie    }
45659036814SCostin Stroie
45759036814SCostin Stroie    /**
45859036814SCostin Stroie     * Handle tool calls from the LLM
45959036814SCostin Stroie     *
46059036814SCostin Stroie     * Processes tool calls made by the LLM and returns appropriate responses.
46159036814SCostin Stroie     * Implements caching to avoid duplicate calls with identical parameters.
46259036814SCostin Stroie     *
46359036814SCostin Stroie     * @param array $toolCall The tool call data from the LLM
46459036814SCostin Stroie     * @return array The tool response message
46559036814SCostin Stroie     */
46659036814SCostin Stroie    private function handleToolCall($toolCall)
46759036814SCostin Stroie    {
46859036814SCostin Stroie        $toolName = $toolCall['function']['name'];
46959036814SCostin Stroie        $arguments = json_decode($toolCall['function']['arguments'], true);
47059036814SCostin Stroie
47159036814SCostin Stroie        // Create a cache key from the tool name and arguments
47259036814SCostin Stroie        $cacheKey = md5($toolName . serialize($arguments));
47359036814SCostin Stroie
47459036814SCostin Stroie        // Check if we have a cached result for this tool call
47559036814SCostin Stroie        if (isset($this->toolCallCache[$cacheKey])) {
47659036814SCostin Stroie            // Return cached result and indicate it was found in cache
47759036814SCostin Stroie            $toolResponse = $this->toolCallCache[$cacheKey];
47859036814SCostin Stroie            // Update with current tool call ID
47959036814SCostin Stroie            $toolResponse['tool_call_id'] = $toolCall['id'];
48059036814SCostin Stroie            $toolResponse['cached'] = true; // Indicate this response was cached
48159036814SCostin Stroie            return $toolResponse;
48259036814SCostin Stroie        }
48359036814SCostin Stroie
48459036814SCostin Stroie        $toolResponse = [
48559036814SCostin Stroie            'role' => 'tool',
48659036814SCostin Stroie            'tool_call_id' => $toolCall['id'],
48759036814SCostin Stroie            'cached' => false // Indicate this is a fresh response
48859036814SCostin Stroie        ];
48959036814SCostin Stroie
49059036814SCostin Stroie        switch ($toolName) {
49159036814SCostin Stroie            case 'get_document':
49259036814SCostin Stroie                $documentId = $arguments['id'];
49359036814SCostin Stroie                $content = $this->getPageContent($documentId);
49459036814SCostin Stroie                if ($content === false) {
49559036814SCostin Stroie                    $toolResponse['content'] = 'Document not found: ' . $documentId;
49659036814SCostin Stroie                } else {
49759036814SCostin Stroie                    $toolResponse['content'] = $content;
49859036814SCostin Stroie                }
49959036814SCostin Stroie                break;
50059036814SCostin Stroie
50159036814SCostin Stroie            case 'get_template':
50259036814SCostin Stroie                // Get template content using the convenience function
50359036814SCostin Stroie                $toolResponse['content'] = $this->getTemplateContent();
50459036814SCostin Stroie                break;
50559036814SCostin Stroie
50659036814SCostin Stroie            case 'get_examples':
50759036814SCostin Stroie                // Get examples content using the convenience function
50859036814SCostin Stroie                $count = isset($arguments['count']) ? (int)$arguments['count'] : 5;
50959036814SCostin Stroie                $toolResponse['content'] = '<examples>\n' . $this->getSnippets($count) . '\n</examples>';
51059036814SCostin Stroie                break;
51159036814SCostin Stroie
51259036814SCostin Stroie            default:
51359036814SCostin Stroie                $toolResponse['content'] = 'Unknown tool: ' . $toolName;
51459036814SCostin Stroie        }
51559036814SCostin Stroie
51659036814SCostin Stroie        // Cache the result for future calls with the same parameters
51759036814SCostin Stroie        $cacheEntry = $toolResponse;
51859036814SCostin Stroie        // Remove tool_call_id and cached flag from cache as they change per call
51959036814SCostin Stroie        unset($cacheEntry['tool_call_id']);
52059036814SCostin Stroie        unset($cacheEntry['cached']);
52159036814SCostin Stroie        $this->toolCallCache[$cacheKey] = $cacheEntry;
52259036814SCostin Stroie
52359036814SCostin Stroie        return $toolResponse;
52459036814SCostin Stroie    }
52559036814SCostin Stroie
52659036814SCostin Stroie    /**
52759036814SCostin Stroie     * Make an API call with tool responses
52859036814SCostin Stroie     *
52959036814SCostin Stroie     * Sends a follow-up request to the LLM with tool responses.
53059036814SCostin Stroie     * Implements complex logic for handling tool calls with caching and loop protection.
53159036814SCostin Stroie     *
53259036814SCostin Stroie     * Complex logic includes:
53359036814SCostin Stroie     * 1. Making HTTP requests with proper authentication and error handling
53459036814SCostin Stroie     * 2. Processing tool calls from the LLM response
53559036814SCostin Stroie     * 3. Caching tool responses to avoid duplicate calls with identical parameters
53659036814SCostin Stroie     * 4. Tracking tool call counts to prevent infinite loops
53759036814SCostin Stroie     * 5. Implementing loop protection with call count limits
53859036814SCostin Stroie     * 6. Handling recursive tool calls until final content is generated
53959036814SCostin Stroie     *
54059036814SCostin Stroie     * Loop protection works by:
54159036814SCostin Stroie     * - Tracking individual tool call counts (max 3 per tool)
54259036814SCostin Stroie     * - Tracking total tool calls (max 10 total)
54359036814SCostin Stroie     * - Disabling tools when limits are exceeded to break potential loops
54459036814SCostin Stroie     *
54559036814SCostin Stroie     * @param array $data The API request data including messages with tool responses
54659036814SCostin Stroie     * @param bool $toolsCalled Whether tools have already been called (used for loop protection)
54759036814SCostin Stroie     * @param bool $useTools Whether to process tool calls (used for loop protection)
54859036814SCostin Stroie     * @return string The final response content
54959036814SCostin Stroie     */
55059036814SCostin Stroie    private function callAPIWithTools($data, $toolsCalled = false, $useTools = true)
55159036814SCostin Stroie    {
55259036814SCostin Stroie        // Set up HTTP headers, including authentication if API key is configured
55359036814SCostin Stroie        $headers = [
55459036814SCostin Stroie            'Content-Type: application/json'
55559036814SCostin Stroie        ];
55659036814SCostin Stroie
55759036814SCostin Stroie        if (!empty($this->api_key)) {
55859036814SCostin Stroie            $headers[] = 'Authorization: Bearer ' . $this->api_key;
55959036814SCostin Stroie        }
56059036814SCostin Stroie
56159036814SCostin Stroie       // If tools have already been called, remove tools and tool_choice from data to prevent infinite loops
56259036814SCostin Stroie        if ($toolsCalled) {
56359036814SCostin Stroie            unset($data['tools']);
56459036814SCostin Stroie            unset($data['tool_choice']);
56559036814SCostin Stroie        }
56659036814SCostin Stroie
56759036814SCostin Stroie        // Initialize and configure cURL for the API request
56859036814SCostin Stroie        $ch = curl_init();
56959036814SCostin Stroie        curl_setopt($ch, CURLOPT_URL, $this->api_url);
57059036814SCostin Stroie        curl_setopt($ch, CURLOPT_POST, true);
57159036814SCostin Stroie        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
57259036814SCostin Stroie        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
57359036814SCostin Stroie        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
57459036814SCostin Stroie        curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
57559036814SCostin Stroie        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
57659036814SCostin Stroie
57759036814SCostin Stroie        // Execute the API request
57859036814SCostin Stroie        $response = curl_exec($ch);
57959036814SCostin Stroie        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
58059036814SCostin Stroie        $error = curl_error($ch);
58159036814SCostin Stroie        curl_close($ch);
58259036814SCostin Stroie
58359036814SCostin Stroie        // Handle cURL errors
58459036814SCostin Stroie        if ($error) {
58559036814SCostin Stroie            throw new Exception('API request failed: ' . $error);
58659036814SCostin Stroie        }
58759036814SCostin Stroie
58859036814SCostin Stroie        // Handle HTTP errors
58959036814SCostin Stroie        if ($httpCode !== 200) {
59059036814SCostin Stroie            throw new Exception('API request failed with HTTP code: ' . $httpCode);
59159036814SCostin Stroie        }
59259036814SCostin Stroie
59359036814SCostin Stroie        // Parse and validate the JSON response
59459036814SCostin Stroie        $result = json_decode($response, true);
59559036814SCostin Stroie
59659036814SCostin Stroie        // Extract the content from the response if available
59759036814SCostin Stroie        if (isset($result['choices'][0]['message']['content'])) {
59859036814SCostin Stroie            $content = trim($result['choices'][0]['message']['content']);
59959036814SCostin Stroie            // Reset tool call counts when we get final content
60059036814SCostin Stroie            $this->toolCallCounts = [];
60159036814SCostin Stroie            return $content;
60259036814SCostin Stroie        }
60359036814SCostin Stroie
60459036814SCostin Stroie        // Handle tool calls if present
60559036814SCostin Stroie        if ($useTools && isset($result['choices'][0]['message']['tool_calls'])) {
60659036814SCostin Stroie            $toolCalls = $result['choices'][0]['message']['tool_calls'];
60759036814SCostin Stroie            // Start with original messages
60859036814SCostin Stroie            $messages = $data['messages'];
60959036814SCostin Stroie            // Add assistant's message with tool calls, keeping all original fields except for content (which is null)
61059036814SCostin Stroie            $assistantMessage = [];
61159036814SCostin Stroie            foreach ($result['choices'][0]['message'] as $key => $value) {
61259036814SCostin Stroie                if ($key !== 'content') {
61359036814SCostin Stroie                    $assistantMessage[$key] = $value;
61459036814SCostin Stroie                }
61559036814SCostin Stroie            }
61659036814SCostin Stroie            // Add assistant's message with tool calls
61759036814SCostin Stroie            $messages[] = $assistantMessage;
61859036814SCostin Stroie
61959036814SCostin Stroie            // Process each tool call and track counts to prevent infinite loops
62059036814SCostin Stroie            foreach ($toolCalls as $toolCall) {
62159036814SCostin Stroie                $toolName = $toolCall['function']['name'];
62259036814SCostin Stroie                // Increment tool call count
62359036814SCostin Stroie                if (!isset($this->toolCallCounts[$toolName])) {
62459036814SCostin Stroie                    $this->toolCallCounts[$toolName] = 0;
62559036814SCostin Stroie                }
62659036814SCostin Stroie                $this->toolCallCounts[$toolName]++;
62759036814SCostin Stroie
62859036814SCostin Stroie                $toolResponse = $this->handleToolCall($toolCall);
62959036814SCostin Stroie                $messages[] = $toolResponse;
63059036814SCostin Stroie            }
63159036814SCostin Stroie
63259036814SCostin Stroie            // Check if any tool has been called more than 3 times
63359036814SCostin Stroie            $toolsCalledCount = 0;
63459036814SCostin Stroie            foreach ($this->toolCallCounts as $count) {
63559036814SCostin Stroie                if ($count > 3) {
63659036814SCostin Stroie                    // If any tool called more than 3 times, disable tools to break loop
63759036814SCostin Stroie                    $toolsCalled = true;
63859036814SCostin Stroie                    break;
63959036814SCostin Stroie                }
64059036814SCostin Stroie                $toolsCalledCount += $count;
64159036814SCostin Stroie            }
64259036814SCostin Stroie
64359036814SCostin Stroie            // If total tool calls exceed 10, also disable tools
64459036814SCostin Stroie            if ($toolsCalledCount > 10) {
64559036814SCostin Stroie                $toolsCalled = true;
64659036814SCostin Stroie            }
64759036814SCostin Stroie
64859036814SCostin Stroie            // Make another API call with tool responses
64959036814SCostin Stroie            $data['messages'] = $messages;
65059036814SCostin Stroie            return $this->callAPIWithTools($data, $toolsCalled, $useTools);
65159036814SCostin Stroie        }
65259036814SCostin Stroie
65359036814SCostin Stroie        // Throw exception for unexpected response format
65459036814SCostin Stroie        throw new Exception('Unexpected API response format');
65559036814SCostin Stroie    }
65659036814SCostin Stroie
65759036814SCostin Stroie    /**
65859036814SCostin Stroie     * Load a prompt template from a DokuWiki page and replace placeholders
65959036814SCostin Stroie     *
66059036814SCostin Stroie     * Loads prompt templates from DokuWiki pages with IDs in the format
66159036814SCostin Stroie     * dokullm:prompts:LANGUAGE:PROMPT_NAME
66259036814SCostin Stroie     *
66359036814SCostin Stroie     * The method implements a language fallback mechanism:
66459036814SCostin Stroie     * 1. First tries to load the prompt in the configured language
66559036814SCostin Stroie     * 2. If not found, falls back to English prompts
66659036814SCostin Stroie     * 3. Throws an exception if neither is available
66759036814SCostin Stroie     *
66859036814SCostin Stroie     * After loading the prompt, it scans for placeholders and automatically
66959036814SCostin Stroie     * adds missing ones with appropriate values before replacing all placeholders.
67059036814SCostin Stroie     *
67159036814SCostin Stroie     * @param string $promptName The name of the prompt (e.g., 'create', 'rewrite')
67259036814SCostin Stroie     * @param array $variables Associative array of placeholder => value pairs
67359036814SCostin Stroie     * @return string The processed prompt with placeholders replaced
67459036814SCostin Stroie     * @throws Exception If the prompt page cannot be loaded in any language
67559036814SCostin Stroie     */
67659036814SCostin Stroie    private function loadPrompt($promptName, $variables = [])
67759036814SCostin Stroie    {
6789b704e62SCostin Stroie (aider)        $language = $this->getConf('language');
67959036814SCostin Stroie
68059036814SCostin Stroie        // Default to 'en' if language is 'default' or not set
68159036814SCostin Stroie        if ($language === 'default' || empty($language)) {
68259036814SCostin Stroie            $language = 'en';
68359036814SCostin Stroie        }
68459036814SCostin Stroie
68559036814SCostin Stroie        // Construct the page ID for the prompt in the configured language
68659036814SCostin Stroie        $promptPageId = 'dokullm:prompts:' . $language . ':' . $promptName;
68759036814SCostin Stroie
68859036814SCostin Stroie        // Try to get the content of the prompt page in the configured language
68959036814SCostin Stroie        $prompt = $this->getPageContent($promptPageId);
69059036814SCostin Stroie
69159036814SCostin Stroie        // If the language-specific prompt doesn't exist, try English as fallback
69259036814SCostin Stroie        if ($prompt === false && $language !== 'en') {
69359036814SCostin Stroie            $promptPageId = 'dokullm:prompts:en:' . $promptName;
69459036814SCostin Stroie            $prompt = $this->getPageContent($promptPageId);
69559036814SCostin Stroie        }
69659036814SCostin Stroie
69759036814SCostin Stroie        // If still no prompt found, throw an exception
69859036814SCostin Stroie        if ($prompt === false) {
69959036814SCostin Stroie            throw new Exception('Prompt page not found: ' . $promptPageId);
70059036814SCostin Stroie        }
70159036814SCostin Stroie
70259036814SCostin Stroie        // Find placeholders in the prompt
70359036814SCostin Stroie        $placeholders = $this->findPlaceholders($prompt);
70459036814SCostin Stroie
70559036814SCostin Stroie        // Add missing placeholders with appropriate values
70659036814SCostin Stroie        foreach ($placeholders as $placeholder) {
70759036814SCostin Stroie            // Skip if already provided in variables
70859036814SCostin Stroie            if (isset($variables[$placeholder])) {
70959036814SCostin Stroie                continue;
71059036814SCostin Stroie            }
71159036814SCostin Stroie
71259036814SCostin Stroie            // Add appropriate values for specific placeholders
71359036814SCostin Stroie            switch ($placeholder) {
71459036814SCostin Stroie                case 'template':
71559036814SCostin Stroie                    // If we have a page_template in variables, use it
71659036814SCostin Stroie                    $variables[$placeholder] = $this->getTemplateContent($variables['page_template']);
71759036814SCostin Stroie                    break;
71859036814SCostin Stroie
71959036814SCostin Stroie                case 'snippets':
72059036814SCostin Stroie                    $variables[$placeholder] = $this->getSnippets(10);
72159036814SCostin Stroie                    break;
72259036814SCostin Stroie
72359036814SCostin Stroie                case 'examples':
72459036814SCostin Stroie                    // If we have example page IDs in metadata, add examples content
72559036814SCostin Stroie                    $variables[$placeholder] = $this->getExamplesContent($variables['page_examples']);
72659036814SCostin Stroie                    break;
72759036814SCostin Stroie
72859036814SCostin Stroie                case 'previous':
72959036814SCostin Stroie                    // If we have a previous report page ID in metadata, add previous content
73059036814SCostin Stroie                    $variables[$placeholder] = $this->getPreviousContent($variables['page_previous']);
73159036814SCostin Stroie
73259036814SCostin Stroie                    // Add current and previous dates to metadata
73359036814SCostin Stroie                    $variables['current_date'] = $this->getPageDate();
73459036814SCostin Stroie                    $variables['previous_date'] = !empty($variables['page_previous']) ?
73559036814SCostin Stroie                                                $this->getPageDate($variables['page_previous']) :
73659036814SCostin Stroie                                                '';
73759036814SCostin Stroie                    break;
73859036814SCostin Stroie
73959036814SCostin Stroie                default:
74059036814SCostin Stroie                    // For other placeholders, leave them empty or set a default value
74159036814SCostin Stroie                    $variables[$placeholder] = '';
74259036814SCostin Stroie                    break;
74359036814SCostin Stroie            }
74459036814SCostin Stroie        }
74559036814SCostin Stroie
74659036814SCostin Stroie        // Replace placeholders with actual values
74759036814SCostin Stroie        // Placeholders are in the format {placeholder_name}
74859036814SCostin Stroie        foreach ($variables as $placeholder => $value) {
74959036814SCostin Stroie            $prompt = str_replace('{' . $placeholder . '}', $value, $prompt);
75059036814SCostin Stroie        }
75159036814SCostin Stroie
75259036814SCostin Stroie        // Return the processed prompt
75359036814SCostin Stroie        return $prompt;
75459036814SCostin Stroie    }
75559036814SCostin Stroie
75659036814SCostin Stroie    /**
75759036814SCostin Stroie     * Load system prompt with optional command-specific appendage
75859036814SCostin Stroie     *
75959036814SCostin Stroie     * Loads the main system prompt and appends any command-specific system prompt
76059036814SCostin Stroie     * if available.
76159036814SCostin Stroie     *
76259036814SCostin Stroie     * @param string $action The action/command name
76359036814SCostin Stroie     * @param array $variables Associative array of placeholder => value pairs
76459036814SCostin Stroie     * @return string The combined system prompt
76559036814SCostin Stroie     */
76659036814SCostin Stroie    private function loadSystemPrompt($action, $variables = [])
76759036814SCostin Stroie    {
76859036814SCostin Stroie        // Load system prompt which provides general instructions to the LLM
76959036814SCostin Stroie        $systemPrompt = $this->loadPrompt('system', $variables);
77059036814SCostin Stroie
77159036814SCostin Stroie        // Check if there's a command-specific system prompt appendage
77259036814SCostin Stroie        if (!empty($action)) {
77359036814SCostin Stroie            try {
77459036814SCostin Stroie                $commandSystemPrompt = $this->loadPrompt($action . ':system', $variables);
77559036814SCostin Stroie                if ($commandSystemPrompt !== false) {
77659036814SCostin Stroie                    $systemPrompt .= "\n" . $commandSystemPrompt;
77759036814SCostin Stroie                }
77859036814SCostin Stroie            } catch (Exception $e) {
77959036814SCostin Stroie                // Ignore exceptions when loading command-specific system prompt
78059036814SCostin Stroie                // This allows the main system prompt to still be used
78159036814SCostin Stroie            }
78259036814SCostin Stroie        }
78359036814SCostin Stroie
78459036814SCostin Stroie        return $systemPrompt;
78559036814SCostin Stroie    }
78659036814SCostin Stroie
78759036814SCostin Stroie    /**
78859036814SCostin Stroie     * Get the content of a DokuWiki page
78959036814SCostin Stroie     *
79059036814SCostin Stroie     * Retrieves the raw content of a DokuWiki page by its ID.
79159036814SCostin Stroie     * Used for loading template and example page content for context.
79259036814SCostin Stroie     *
79359036814SCostin Stroie     * @param string $pageId The page ID to retrieve
79459036814SCostin Stroie     * @return string|false The page content or false if not found/readable
79559036814SCostin Stroie     */
79659036814SCostin Stroie    public function getPageContent($pageId)
79759036814SCostin Stroie    {
79859036814SCostin Stroie        // Convert page ID to file path
79959036814SCostin Stroie        $pageFile = wikiFN($pageId);
80059036814SCostin Stroie
80159036814SCostin Stroie        // Check if file exists and is readable
80259036814SCostin Stroie        if (file_exists($pageFile) && is_readable($pageFile)) {
80359036814SCostin Stroie            return file_get_contents($pageFile);
80459036814SCostin Stroie        }
80559036814SCostin Stroie
80659036814SCostin Stroie        return false;
80759036814SCostin Stroie    }
80859036814SCostin Stroie
80959036814SCostin Stroie    /**
81059036814SCostin Stroie     * Extract date from page ID or file timestamp
81159036814SCostin Stroie     *
81259036814SCostin Stroie     * Attempts to extract a date in YYmmdd format from the page ID.
81359036814SCostin Stroie     * If not found, uses the file's last modification timestamp.
81459036814SCostin Stroie     *
81559036814SCostin Stroie     * @param string $pageId Optional page ID to extract date from (defaults to current page)
81659036814SCostin Stroie     * @return string Formatted date string (YYYY-MM-DD)
81759036814SCostin Stroie     */
81859036814SCostin Stroie    private function getPageDate($pageId = null)
81959036814SCostin Stroie    {
82059036814SCostin Stroie        global $ID;
82159036814SCostin Stroie
82259036814SCostin Stroie        // Use provided page ID or current page ID
82359036814SCostin Stroie        $targetPageId = $pageId ?: $ID;
82459036814SCostin Stroie
82559036814SCostin Stroie        // Try to extract date from page ID (looking for YYmmdd pattern)
82659036814SCostin Stroie        if (preg_match('/(\d{2})(\d{2})(\d{2})/', $targetPageId, $matches)) {
82759036814SCostin Stroie            // Convert YYmmdd to YYYY-MM-DD
82859036814SCostin Stroie            $year = $matches[1];
82959036814SCostin Stroie            $month = $matches[2];
83059036814SCostin Stroie            $day = $matches[3];
83159036814SCostin Stroie
83259036814SCostin Stroie            // Assume 20xx for years 00-69, 19xx for years 70-99
83359036814SCostin Stroie            $fullYear = intval($year) <= 69 ? '20' . $year : '19' . $year;
83459036814SCostin Stroie
83559036814SCostin Stroie            return $fullYear . '-' . $month . '-' . $day;
83659036814SCostin Stroie        }
83759036814SCostin Stroie
83859036814SCostin Stroie        // Fallback to file timestamp
83959036814SCostin Stroie        $pageFile = wikiFN($targetPageId);
84059036814SCostin Stroie        if (file_exists($pageFile)) {
84159036814SCostin Stroie            $timestamp = filemtime($pageFile);
84259036814SCostin Stroie            return date('Y-m-d', $timestamp);
84359036814SCostin Stroie        }
84459036814SCostin Stroie
84559036814SCostin Stroie        // Return empty string if no date can be determined
84659036814SCostin Stroie        return '';
84759036814SCostin Stroie    }
84859036814SCostin Stroie
84959036814SCostin Stroie    /**
85059036814SCostin Stroie     * Get current text
85159036814SCostin Stroie     *
85259036814SCostin Stroie     * Retrieves the current text stored from the process function.
85359036814SCostin Stroie     *
85459036814SCostin Stroie     * @return string The current text
85559036814SCostin Stroie     */
85659036814SCostin Stroie    private function getCurrentText()
85759036814SCostin Stroie    {
85859036814SCostin Stroie        return $this->currentText;
85959036814SCostin Stroie    }
86059036814SCostin Stroie
86159036814SCostin Stroie    /**
86259036814SCostin Stroie     * Scan text for placeholders
86359036814SCostin Stroie     *
86459036814SCostin Stroie     * Finds all placeholders in the format {placeholder_name} in the provided text
86559036814SCostin Stroie     * and returns an array of unique placeholder names.
86659036814SCostin Stroie     *
86759036814SCostin Stroie     * @param string $text The text to scan for placeholders
86859036814SCostin Stroie     * @return array List of unique placeholder names found in the text
86959036814SCostin Stroie     */
87059036814SCostin Stroie    public function findPlaceholders($text)
87159036814SCostin Stroie    {
87259036814SCostin Stroie        $placeholders = [];
87359036814SCostin Stroie        $pattern = '/\{([^}]+)\}/';
87459036814SCostin Stroie
87559036814SCostin Stroie        if (preg_match_all($pattern, $text, $matches)) {
87659036814SCostin Stroie            // Get unique placeholder names
87759036814SCostin Stroie            $placeholders = array_unique($matches[1]);
87859036814SCostin Stroie        }
87959036814SCostin Stroie
88059036814SCostin Stroie        return $placeholders;
88159036814SCostin Stroie    }
88259036814SCostin Stroie
88359036814SCostin Stroie    /**
88459036814SCostin Stroie     * Get template content for the current text
88559036814SCostin Stroie     *
88659036814SCostin Stroie     * Convenience function to retrieve template content. If a pageId is provided,
88759036814SCostin Stroie     * retrieves content directly from that page. Otherwise, queries ChromaDB for
88859036814SCostin Stroie     * a relevant template based on the current text.
88959036814SCostin Stroie     *
89059036814SCostin Stroie     * @param string|null $pageId Optional page ID to retrieve template from directly
89159036814SCostin Stroie     * @return string The template content or empty string if not found
89259036814SCostin Stroie     */
89359036814SCostin Stroie    private function getTemplateContent($pageId = null)
89459036814SCostin Stroie    {
89559036814SCostin Stroie        // If pageId is provided, use it directly
89659036814SCostin Stroie        if ($pageId !== null) {
89759036814SCostin Stroie            $templateContent = $this->getPageContent($pageId);
89859036814SCostin Stroie            if ($templateContent !== false) {
89959036814SCostin Stroie                return $templateContent;
90059036814SCostin Stroie            }
90159036814SCostin Stroie        }
90259036814SCostin Stroie
90359036814SCostin Stroie        // Otherwise, get template suggestion for the current text
90459036814SCostin Stroie        $pageId = $this->queryChromaDBTemplate($this->getCurrentText());
90559036814SCostin Stroie        if (!empty($pageId)) {
90659036814SCostin Stroie            $templateContent = $this->getPageContent($pageId[0]);
90759036814SCostin Stroie            if ($templateContent !== false) {
90859036814SCostin Stroie                return $templateContent;
90959036814SCostin Stroie            }
91059036814SCostin Stroie        }
91159036814SCostin Stroie        return '( no template )';
91259036814SCostin Stroie    }
91359036814SCostin Stroie
91459036814SCostin Stroie    /**
91559036814SCostin Stroie     * Get snippets content for the current text
91659036814SCostin Stroie     *
91759036814SCostin Stroie     * Convenience function to retrieve relevant snippets for the current text.
91859036814SCostin Stroie     * Queries ChromaDB for relevant snippets and returns them formatted.
91959036814SCostin Stroie     *
92059036814SCostin Stroie     * @param int $count Number of snippets to retrieve (default: 10)
92159036814SCostin Stroie     * @return string Formatted snippets content or empty string if not found
92259036814SCostin Stroie     */
92359036814SCostin Stroie    private function getSnippets($count = 10)
92459036814SCostin Stroie    {
92559036814SCostin Stroie        // Get example snippets for the current text
92659036814SCostin Stroie        $snippets = $this->queryChromaDBSnippets($this->getCurrentText(), $count);
92759036814SCostin Stroie        if (!empty($snippets)) {
92859036814SCostin Stroie            $formattedSnippets = [];
92959036814SCostin Stroie            foreach ($snippets as $index => $snippet) {
93059036814SCostin Stroie                $formattedSnippets[] = '<example id="' . ($index + 1) . '">\n' . $snippet . '\n</example>';
93159036814SCostin Stroie            }
93259036814SCostin Stroie            return implode("\n", $formattedSnippets);
93359036814SCostin Stroie        }
93459036814SCostin Stroie        return '( no examples )';
93559036814SCostin Stroie    }
93659036814SCostin Stroie
93759036814SCostin Stroie    /**
93859036814SCostin Stroie     * Get examples content from example page IDs
93959036814SCostin Stroie     *
94059036814SCostin Stroie     * Convenience function to retrieve content from example pages.
94159036814SCostin Stroie     * Returns the content of each page packed in XML elements.
94259036814SCostin Stroie     *
94359036814SCostin Stroie     * @param array $exampleIds List of example page IDs
94459036814SCostin Stroie     * @return string Formatted examples content or empty string if not found
94559036814SCostin Stroie     */
94659036814SCostin Stroie    private function getExamplesContent($exampleIds = [])
94759036814SCostin Stroie    {
94859036814SCostin Stroie        if (empty($exampleIds) || !is_array($exampleIds)) {
94959036814SCostin Stroie            return '( no examples )';
95059036814SCostin Stroie        }
95159036814SCostin Stroie
95259036814SCostin Stroie        $examplesContent = [];
95359036814SCostin Stroie        foreach ($exampleIds as $index => $exampleId) {
95459036814SCostin Stroie            $content = $this->getPageContent($exampleId);
95559036814SCostin Stroie            if ($content !== false) {
95659036814SCostin Stroie                $examplesContent[] = '<example_page source="' . $exampleId . '">\n' . $content . '\n</example_page>';
95759036814SCostin Stroie            }
95859036814SCostin Stroie        }
95959036814SCostin Stroie
96059036814SCostin Stroie        return implode("\n", $examplesContent);
96159036814SCostin Stroie    }
96259036814SCostin Stroie
96359036814SCostin Stroie    /**
96459036814SCostin Stroie     * Get previous report content from previous page ID
96559036814SCostin Stroie     *
96659036814SCostin Stroie     * Convenience function to retrieve content from a previous report page.
96759036814SCostin Stroie     * Returns the content of the previous page or a default message if not found.
96859036814SCostin Stroie     *
96959036814SCostin Stroie     * @param string $previousId Previous page ID
97059036814SCostin Stroie     * @return string Previous report content or default message if not found
97159036814SCostin Stroie     */
97259036814SCostin Stroie    private function getPreviousContent($previousId = '')
97359036814SCostin Stroie    {
97459036814SCostin Stroie        if (empty($previousId)) {
97559036814SCostin Stroie            return '( no previous report )';
97659036814SCostin Stroie        }
97759036814SCostin Stroie
97859036814SCostin Stroie        $content = $this->getPageContent($previousId);
97959036814SCostin Stroie        if ($content !== false) {
98059036814SCostin Stroie            return $content;
98159036814SCostin Stroie        }
98259036814SCostin Stroie
98359036814SCostin Stroie        return '( previous report not found )';
98459036814SCostin Stroie    }
98559036814SCostin Stroie
98659036814SCostin Stroie    /**
98759036814SCostin Stroie     * Get ChromaDB client with configuration
98859036814SCostin Stroie     *
98959036814SCostin Stroie     * Creates and returns a ChromaDB client with the appropriate configuration.
99059036814SCostin Stroie     * Extracts modality from the current page ID to use as the collection name.
99159036814SCostin Stroie     *
99259036814SCostin Stroie     * @return array Array containing the ChromaDB client and collection name
99359036814SCostin Stroie     */
99459036814SCostin Stroie    private function getChromaDBClient()
99559036814SCostin Stroie    {
9966c51c388SCostin Stroie (aider)        // Get ChromaDB configuration from DokuWiki plugin configuration
9979b704e62SCostin Stroie (aider)        $chromaHost = $this->getConf('chroma_host', 'localhost');
9989b704e62SCostin Stroie (aider)        $chromaPort = $this->getConf('chroma_port', 8000);
9999b704e62SCostin Stroie (aider)        $chromaTenant = $this->getConf('chroma_tenant', 'dokullm');
10009b704e62SCostin Stroie (aider)        $chromaDatabase = $this->getConf('chroma_database', 'dokullm');
10019b704e62SCostin Stroie (aider)        $chromaDefaultCollection = $this->getConf('chroma_collection', 'documents');
10029b704e62SCostin Stroie (aider)        $ollamaHost = $this->getConf('ollama_host', 'localhost');
10039b704e62SCostin Stroie (aider)        $ollamaPort = $this->getConf('ollama_port', 11434);
10049b704e62SCostin Stroie (aider)        $ollamaModel = $this->getConf('ollama_embeddings_model', 'nomic-embed-text');
100559036814SCostin Stroie
100659036814SCostin Stroie        // Use the first part of the current page ID as collection name, fallback to default
100759036814SCostin Stroie        global $ID;
100859036814SCostin Stroie        $chromaCollection = $chromaDefaultCollection; // Default collection name
100959036814SCostin Stroie
101059036814SCostin Stroie        if (!empty($ID)) {
101159036814SCostin Stroie            // Split the page ID by ':' and take the first part as collection name
101259036814SCostin Stroie            $parts = explode(':', $ID);
101359036814SCostin Stroie            if (isset($parts[0]) && !empty($parts[0])) {
101459036814SCostin Stroie                // If the first part is 'playground', use the default collection
101559036814SCostin Stroie                // Otherwise, use the first part as the collection name
101659036814SCostin Stroie                if ($parts[0] === 'playground') {
101759036814SCostin Stroie                    $chromaCollection = $chromaDefaultCollection;
101859036814SCostin Stroie                } else {
101959036814SCostin Stroie                    $chromaCollection = $parts[0];
102059036814SCostin Stroie                }
102159036814SCostin Stroie            }
102259036814SCostin Stroie        }
102359036814SCostin Stroie
10246c51c388SCostin Stroie (aider)        // Create ChromaDB client with all required parameters
10256c51c388SCostin Stroie (aider)        $chromaClient = new \dokuwiki\plugin\dokullm\ChromaDBClient(
10266c51c388SCostin Stroie (aider)            $chromaHost,
10276c51c388SCostin Stroie (aider)            $chromaPort,
10286c51c388SCostin Stroie (aider)            $chromaTenant,
10296c51c388SCostin Stroie (aider)            $chromaDatabase,
10306c51c388SCostin Stroie (aider)            $ollamaHost,
10316c51c388SCostin Stroie (aider)            $ollamaPort,
10326c51c388SCostin Stroie (aider)            $ollamaModel
10336c51c388SCostin Stroie (aider)        );
103459036814SCostin Stroie
103559036814SCostin Stroie
103659036814SCostin Stroie        return [$chromaClient, $chromaCollection];
103759036814SCostin Stroie    }
103859036814SCostin Stroie
103959036814SCostin Stroie    /**
104059036814SCostin Stroie     * Query ChromaDB for relevant documents
104159036814SCostin Stroie     *
104259036814SCostin Stroie     * Generates embeddings for the input text and queries ChromaDB for similar documents.
104359036814SCostin Stroie     * Extracts modality from the current page ID to use as the collection name.
104459036814SCostin Stroie     *
104559036814SCostin Stroie     * @param string $text The text to find similar documents for
104659036814SCostin Stroie     * @param int $limit Maximum number of documents to retrieve (default: 5)
104759036814SCostin Stroie     * @param array|null $where Optional filter conditions for metadata
104859036814SCostin Stroie     * @return array List of document IDs
104959036814SCostin Stroie     */
105059036814SCostin Stroie    private function queryChromaDB($text, $limit = 5, $where = null)
105159036814SCostin Stroie    {
105259036814SCostin Stroie        try {
105359036814SCostin Stroie            // Get ChromaDB client and collection name
105459036814SCostin Stroie            list($chromaClient, $chromaCollection) = $this->getChromaDBClient();
105559036814SCostin Stroie            // Query for similar documents
105659036814SCostin Stroie            $results = $chromaClient->queryCollection($chromaCollection, [$text], $limit, $where);
105759036814SCostin Stroie
105859036814SCostin Stroie            // Extract document IDs from results
105959036814SCostin Stroie            $documentIds = [];
106059036814SCostin Stroie            if (isset($results['ids'][0]) && is_array($results['ids'][0])) {
106159036814SCostin Stroie                foreach ($results['ids'][0] as $id) {
106259036814SCostin Stroie                    // Use the ChromaDB ID directly without conversion
106359036814SCostin Stroie                    $documentIds[] = $id;
106459036814SCostin Stroie                }
106559036814SCostin Stroie            }
106659036814SCostin Stroie
106759036814SCostin Stroie            return $documentIds;
106859036814SCostin Stroie        } catch (Exception $e) {
106959036814SCostin Stroie            // Log error but don't fail the operation
107059036814SCostin Stroie            error_log('ChromaDB query failed: ' . $e->getMessage());
107159036814SCostin Stroie            return [];
107259036814SCostin Stroie        }
107359036814SCostin Stroie    }
107459036814SCostin Stroie
107559036814SCostin Stroie    /**
107659036814SCostin Stroie     * Query ChromaDB for relevant documents and return text snippets
107759036814SCostin Stroie     *
107859036814SCostin Stroie     * Generates embeddings for the input text and queries ChromaDB for similar documents.
107959036814SCostin Stroie     * Returns the actual text snippets instead of document IDs.
108059036814SCostin Stroie     *
108159036814SCostin Stroie     * @param string $text The text to find similar documents for
108259036814SCostin Stroie     * @param int $limit Maximum number of documents to retrieve (default: 10)
108359036814SCostin Stroie     * @param array|null $where Optional filter conditions for metadata
108459036814SCostin Stroie     * @return array List of text snippets
108559036814SCostin Stroie     */
108659036814SCostin Stroie    private function queryChromaDBSnippets($text, $limit = 10, $where = null)
108759036814SCostin Stroie    {
108859036814SCostin Stroie        try {
108959036814SCostin Stroie            // Get ChromaDB client and collection name
109059036814SCostin Stroie            list($chromaClient, $chromaCollection) = $this->getChromaDBClient();
109159036814SCostin Stroie            // Query for similar documents
109259036814SCostin Stroie            $results = $chromaClient->queryCollection($chromaCollection, [$text], $limit, $where);
109359036814SCostin Stroie
109459036814SCostin Stroie            // Extract document texts from results
109559036814SCostin Stroie            $snippets = [];
109659036814SCostin Stroie            if (isset($results['documents'][0]) && is_array($results['documents'][0])) {
109759036814SCostin Stroie                foreach ($results['documents'][0] as $document) {
109859036814SCostin Stroie                    $snippets[] = $document;
109959036814SCostin Stroie                }
110059036814SCostin Stroie            }
110159036814SCostin Stroie
110259036814SCostin Stroie            return $snippets;
110359036814SCostin Stroie        } catch (Exception $e) {
110459036814SCostin Stroie            // Log error but don't fail the operation
110559036814SCostin Stroie            error_log('ChromaDB query failed: ' . $e->getMessage());
110659036814SCostin Stroie            return [];
110759036814SCostin Stroie        }
110859036814SCostin Stroie    }
110959036814SCostin Stroie
111059036814SCostin Stroie    /**
111159036814SCostin Stroie     * Query ChromaDB for a template document
111259036814SCostin Stroie     *
111359036814SCostin Stroie     * Generates embeddings for the input text and queries ChromaDB for a template document
111459036814SCostin Stroie     * by filtering with metadata 'template=true'.
111559036814SCostin Stroie     *
111659036814SCostin Stroie     * @param string $text The text to find a template for
111759036814SCostin Stroie     * @return array List of template document IDs (maximum 1)
111859036814SCostin Stroie     */
111959036814SCostin Stroie    public function queryChromaDBTemplate($text)
112059036814SCostin Stroie    {
112159036814SCostin Stroie        $templateIds = $this->queryChromaDB($text, 1, ['type' => 'template']);
112259036814SCostin Stroie
112359036814SCostin Stroie        // Remove chunk number (e.g., "@2") from the ID to get the base document ID
112459036814SCostin Stroie        if (!empty($templateIds)) {
112559036814SCostin Stroie            $templateIds[0] = preg_replace('/@\\d+$/', '', $templateIds[0]);
112659036814SCostin Stroie        }
112759036814SCostin Stroie
112859036814SCostin Stroie        return $templateIds;
112959036814SCostin Stroie    }
113059036814SCostin Stroie
113159036814SCostin Stroie}
1132