xref: /plugin/dokullm/LlmClient.php (revision 40986135d739ccf5c15ff2ed7d324236e1d1e2f5)
159036814SCostin Stroie<?php
259036814SCostin Stroienamespace dokuwiki\plugin\dokullm;
359036814SCostin Stroie
4e2481ee1SCostin Stroie (aider)use Exception;
5e2481ee1SCostin Stroie (aider)
659036814SCostin Stroie/**
759036814SCostin Stroie * LLM Client for the dokullm plugin
859036814SCostin Stroie *
959036814SCostin Stroie * This class provides methods to interact with an LLM API for various
1059036814SCostin Stroie * text processing tasks such as completion, rewriting, grammar correction,
1159036814SCostin Stroie * summarization, conclusion creation, text analysis, and custom prompts.
1259036814SCostin Stroie *
1359036814SCostin Stroie * The client handles:
1459036814SCostin Stroie * - API configuration and authentication
1559036814SCostin Stroie * - Prompt template loading and processing
1659036814SCostin Stroie * - Context-aware requests with metadata
1759036814SCostin Stroie * - DokuWiki page content retrieval
1859036814SCostin Stroie */
1959036814SCostin Stroie
2059036814SCostin Stroie// must be run within Dokuwiki
2159036814SCostin Stroieif (!defined('DOKU_INC')) {
2259036814SCostin Stroie    die();
2359036814SCostin Stroie}
2459036814SCostin Stroie
2559036814SCostin Stroie/**
2659036814SCostin Stroie * LLM Client class for handling API communications
2759036814SCostin Stroie *
2859036814SCostin Stroie * Manages configuration settings and provides methods for various
2959036814SCostin Stroie * text processing operations through an LLM API.
3059036814SCostin Stroie * Implements caching for tool calls to avoid duplicate processing.
3159036814SCostin Stroie */
3259036814SCostin Stroieclass LlmClient
3359036814SCostin Stroie{
3459036814SCostin Stroie    /** @var string The API endpoint URL */
3559036814SCostin Stroie    private $api_url;
3659036814SCostin Stroie
3759036814SCostin Stroie    /** @var array Cache for tool call results */
3859036814SCostin Stroie    private $toolCallCache = [];
3959036814SCostin Stroie
4059036814SCostin Stroie    /** @var string Current text for tool usage */
4159036814SCostin Stroie    private $currentText = '';
4259036814SCostin Stroie
4359036814SCostin Stroie    /** @var array Track tool call counts to prevent infinite loops */
4459036814SCostin Stroie    private $toolCallCounts = [];
4559036814SCostin Stroie
4659036814SCostin Stroie    /** @var string The API authentication key */
4759036814SCostin Stroie    private $api_key;
4859036814SCostin Stroie
4959036814SCostin Stroie    /** @var string The model identifier to use */
5059036814SCostin Stroie    private $model;
5159036814SCostin Stroie
5259036814SCostin Stroie    /** @var int The request timeout in seconds */
5359036814SCostin Stroie    private $timeout;
5459036814SCostin Stroie
5559036814SCostin Stroie    /** @var float The temperature setting for response randomness */
5659036814SCostin Stroie    private $temperature;
5759036814SCostin Stroie
5859036814SCostin Stroie    /** @var float The top-p setting for nucleus sampling */
5959036814SCostin Stroie    private $top_p;
6059036814SCostin Stroie
6159036814SCostin Stroie    /** @var int The top-k setting for token selection */
6259036814SCostin Stroie    private $top_k;
6359036814SCostin Stroie
6459036814SCostin Stroie    /** @var float The min-p setting for minimum probability threshold */
6559036814SCostin Stroie    private $min_p;
6659036814SCostin Stroie
672de95678SCostin Stroie (aider)    /** @var bool Whether to enable thinking in LLM responses */
6859036814SCostin Stroie    private $think;
6959036814SCostin Stroie
702de95678SCostin Stroie (aider)    /** @var object|null ChromaDB client instance */
712de95678SCostin Stroie (aider)    private $chromaClient;
722de95678SCostin Stroie (aider)
7372d9a73bSCostin Stroie (aider)    /** @var string|null Page ID */
7472d9a73bSCostin Stroie (aider)    private $pageId;
7572d9a73bSCostin Stroie (aider)
7659036814SCostin Stroie    /**
7759036814SCostin Stroie     * Initialize the LLM client with configuration settings
7859036814SCostin Stroie     *
7959036814SCostin Stroie     * Retrieves configuration values from DokuWiki's configuration system
8059036814SCostin Stroie     * for API URL, key, model, timeout, and LLM sampling parameters.
8159036814SCostin Stroie     *
8259036814SCostin Stroie     * Configuration values:
8359036814SCostin Stroie     * - api_url: The LLM API endpoint URL
8459036814SCostin Stroie     * - api_key: Authentication key for the API (optional)
8559036814SCostin Stroie     * - model: The model identifier to use for requests
8659036814SCostin Stroie     * - timeout: Request timeout in seconds
87bb1c2789SCostin Stroie     * - profile: Profile for prompt templates
8859036814SCostin Stroie     * - temperature: Temperature setting for response randomness (0.0-1.0)
8959036814SCostin Stroie     * - top_p: Top-p (nucleus sampling) setting (0.0-1.0)
9059036814SCostin Stroie     * - top_k: Top-k setting (integer >= 1)
9159036814SCostin Stroie     * - min_p: Minimum probability threshold (0.0-1.0)
9259036814SCostin Stroie     * - think: Whether to enable thinking in LLM responses (boolean)
932de95678SCostin Stroie (aider)     * - chromaClient: ChromaDB client instance (optional)
9472d9a73bSCostin Stroie (aider)     * - pageId: Page ID (optional)
9559036814SCostin Stroie     */
962adac073SCostin 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, $profile = null, $chromaClient = null, $pageId = null)
9759036814SCostin Stroie    {
9815cead4bSCostin Stroie        $this->api_url = $api_url;
9915cead4bSCostin Stroie        $this->api_key = $api_key;
10015cead4bSCostin Stroie        $this->model = $model;
10115cead4bSCostin Stroie        $this->timeout = $timeout;
10215cead4bSCostin Stroie        $this->temperature = $temperature;
10315cead4bSCostin Stroie        $this->top_p = $top_p;
10415cead4bSCostin Stroie        $this->top_k = $top_k;
10515cead4bSCostin Stroie        $this->min_p = $min_p;
10615cead4bSCostin Stroie        $this->think = $think;
107bb1c2789SCostin Stroie        $this->profile = $profile;
1082de95678SCostin Stroie (aider)        $this->chromaClient = $chromaClient;
10972d9a73bSCostin Stroie (aider)        $this->pageId = $pageId;
1108dbd6d13SCostin Stroie (aider)    }
1118dbd6d13SCostin Stroie (aider)
11259036814SCostin Stroie
11359036814SCostin Stroie
114852801abSCostin Stroie (aider)    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
14229c165f3SCostin Stroie (aider)        $prompt = $this->loadPrompt($action, $metadata);
14359036814SCostin Stroie
14429c165f3SCostin Stroie (aider)        return $this->callAPI($action, $prompt, $metadata, $useContext);
14559036814SCostin Stroie    }
14659036814SCostin Stroie
14759036814SCostin Stroie    /**
14859036814SCostin Stroie     * Process text with a custom user prompt
14959036814SCostin Stroie     *
15059036814SCostin Stroie     * Sends a custom prompt to the LLM along with the provided text.
15159036814SCostin Stroie     *
15259036814SCostin Stroie     * @param string $text The text to process
15359036814SCostin Stroie     * @param string $customPrompt The custom prompt to use
15459036814SCostin Stroie     * @param array $metadata Optional metadata containing template and examples
15559036814SCostin Stroie     * @param bool $useContext Whether to include template and examples in the context (default: true)
15659036814SCostin Stroie     * @return string The processed text
15759036814SCostin Stroie     */
15859036814SCostin Stroie
15959036814SCostin Stroie    /**
16059036814SCostin Stroie     * Get the list of available tools for the LLM
16159036814SCostin Stroie     *
16259036814SCostin Stroie     * Defines the tools that can be used by the LLM during processing.
16359036814SCostin Stroie     *
16459036814SCostin Stroie     * @return array List of tool definitions
16559036814SCostin Stroie     */
16659036814SCostin Stroie    private function getAvailableTools()
16759036814SCostin Stroie    {
16859036814SCostin Stroie        return [
16959036814SCostin Stroie            [
17059036814SCostin Stroie                'type' => 'function',
17159036814SCostin Stroie                'function' => [
17259036814SCostin Stroie                    'name' => 'get_document',
17359036814SCostin 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.',
17459036814SCostin Stroie                    'parameters' => [
17559036814SCostin Stroie                        'type' => 'object',
17659036814SCostin Stroie                        'properties' => [
17759036814SCostin Stroie                            'id' => [
17859036814SCostin Stroie                                'type' => 'string',
17959036814SCostin Stroie                                'description' => 'The unique identifier of the document to retrieve. This should be a valid document ID that exists in the system.'
18059036814SCostin Stroie                            ]
18159036814SCostin Stroie                        ],
18259036814SCostin Stroie                        'required' => ['id']
18359036814SCostin Stroie                    ]
18459036814SCostin Stroie                ]
18559036814SCostin Stroie            ],
18659036814SCostin Stroie            [
18759036814SCostin Stroie                'type' => 'function',
18859036814SCostin Stroie                'function' => [
18959036814SCostin Stroie                    'name' => 'get_template',
19059036814SCostin 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.',
19159036814SCostin Stroie                    'parameters' => [
19259036814SCostin Stroie                        'type' => 'object',
19359036814SCostin Stroie                        'properties' => [
194bb1c2789SCostin Stroie                            'type' => [
19559036814SCostin Stroie                                'type' => 'string',
196bb1c2789SCostin Stroie                                'description' => 'The type of the template (e.g., "mri" for MRI reports, "daily" for daily reports).',
197bb1c2789SCostin Stroie                                'default' => ''
19859036814SCostin Stroie                            ]
19959036814SCostin Stroie                        ]
20059036814SCostin Stroie                    ]
20159036814SCostin Stroie                ]
20259036814SCostin Stroie            ],
20359036814SCostin Stroie            [
20459036814SCostin Stroie                'type' => 'function',
20559036814SCostin Stroie                'function' => [
20659036814SCostin Stroie                    'name' => 'get_examples',
20759036814SCostin 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.',
20859036814SCostin Stroie                    'parameters' => [
20959036814SCostin Stroie                        'type' => 'object',
21059036814SCostin Stroie                        'properties' => [
21159036814SCostin Stroie                            'count' => [
21259036814SCostin Stroie                                'type' => 'integer',
21359036814SCostin 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.',
21459036814SCostin Stroie                                'default' => 5
21559036814SCostin Stroie                            ]
21659036814SCostin Stroie                        ]
21759036814SCostin Stroie                    ]
21859036814SCostin Stroie                ]
21959036814SCostin Stroie            ]
22059036814SCostin Stroie        ];
22159036814SCostin Stroie    }
22259036814SCostin Stroie
22359036814SCostin Stroie    /**
22459036814SCostin Stroie     * Call the LLM API with the specified prompt
22559036814SCostin Stroie     *
22659036814SCostin Stroie     * Makes an HTTP POST request to the configured API endpoint with
22759036814SCostin Stroie     * the prompt and other parameters. Handles authentication if an
22859036814SCostin Stroie     * API key is configured.
22959036814SCostin Stroie     *
23059036814SCostin Stroie     * The method constructs a conversation with system and user messages,
23159036814SCostin Stroie     * including context information from metadata when available.
23259036814SCostin Stroie     *
23359036814SCostin Stroie     * Complex logic includes:
23459036814SCostin Stroie     * 1. Loading and enhancing the system prompt with metadata context
23559036814SCostin Stroie     * 2. Building the API request with model parameters
23659036814SCostin Stroie     * 3. Handling authentication with API key if configured
23759036814SCostin Stroie     * 4. Making the HTTP request with proper error handling
23859036814SCostin Stroie     * 5. Parsing and validating the API response
23959036814SCostin Stroie     * 6. Supporting tool usage with automatic tool calling when enabled
24059036814SCostin Stroie     * 7. Implementing context enhancement with templates, examples, and snippets
24159036814SCostin Stroie     *
24259036814SCostin Stroie     * The context information includes:
24359036814SCostin Stroie     * - Template content: Used as a starting point for the response
24459036814SCostin Stroie     * - Example pages: Full content of specified example pages
24559036814SCostin Stroie     * - Text snippets: Relevant text examples from ChromaDB
24659036814SCostin Stroie     *
24759036814SCostin Stroie     * When tools are enabled, the method supports automatic tool calling:
24859036814SCostin Stroie     * - Tools can retrieve documents, templates, and examples as needed
24959036814SCostin Stroie     * - Tool responses are cached to avoid duplicate calls with identical parameters
25059036814SCostin Stroie     * - Infinite loop protection prevents excessive tool calls
25159036814SCostin Stroie     *
25259036814SCostin Stroie     * @param string $command The command name for loading command-specific system prompts
25359036814SCostin Stroie     * @param string $prompt The prompt to send to the LLM as user message
25459036814SCostin Stroie     * @param array $metadata Optional metadata containing template, examples, and snippets
25559036814SCostin Stroie     * @param bool $useContext Whether to include template and examples in the context (default: true)
25659036814SCostin Stroie     * @return string The response content from the LLM
25759036814SCostin Stroie     * @throws Exception If the API request fails or returns unexpected format
25859036814SCostin Stroie     */
25959036814SCostin Stroie
260f135ebabSCostin Stroie (aider)    private function callAPI($command, $prompt, $metadata = [], $useContext = true, $useTools = false)
26159036814SCostin Stroie    {
26259036814SCostin Stroie        // Load system prompt which provides general instructions to the LLM
26329c165f3SCostin Stroie (aider)        $systemPrompt = $this->loadSystemPrompt($command, []);
26459036814SCostin Stroie
26559036814SCostin Stroie        // Enhance the prompt with context information from metadata
26659036814SCostin Stroie        // This provides the LLM with additional context about templates and examples
26759036814SCostin Stroie        if ($useContext && !empty($metadata) && (!empty($metadata['template']) || !empty($metadata['examples']) || !empty($metadata['snippets']))) {
26859036814SCostin Stroie            $contextInfo = "\n\n<context>\n";
26959036814SCostin Stroie
27059036814SCostin Stroie            // Add template content if specified in metadata
27159036814SCostin Stroie            if (!empty($metadata['template'])) {
27259036814SCostin Stroie                $templateContent = $this->getPageContent($metadata['template']);
27359036814SCostin Stroie                if ($templateContent !== false) {
27459036814SCostin Stroie                    $contextInfo .= "\n\n<template>\nPornește de la acest template (" . $metadata['template'] . "):\n" . $templateContent . "\n</template>\n";
27559036814SCostin Stroie                }
27659036814SCostin Stroie            }
27759036814SCostin Stroie
27859036814SCostin Stroie            // Add example pages content if specified in metadata
27959036814SCostin Stroie            if (!empty($metadata['examples'])) {
28059036814SCostin Stroie                $examplesContent = [];
28159036814SCostin Stroie                foreach ($metadata['examples'] as $example) {
28259036814SCostin Stroie                    $content = $this->getPageContent($example);
28359036814SCostin Stroie                    if ($content !== false) {
28459036814SCostin Stroie                        $examplesContent[] = "\n<example_page source=\"" . $example . "\">\n" . $content . "\n</example_page>\n";
28559036814SCostin Stroie                    }
28659036814SCostin Stroie                }
28759036814SCostin Stroie                if (!empty($examplesContent)) {
28859036814SCostin Stroie                    $contextInfo .= "\n<style_examples>\nAcestea sunt rapoarte complete anterioare - studiază stilul meu de redactare:\n" . implode("\n", $examplesContent) . "\n</style_examples>\n";
28959036814SCostin Stroie                }
29059036814SCostin Stroie            }
29159036814SCostin Stroie
29259036814SCostin Stroie            // Add text snippets if specified in metadata
29359036814SCostin Stroie            if (!empty($metadata['snippets'])) {
29459036814SCostin Stroie                $snippetsContent = [];
29559036814SCostin Stroie                foreach ($metadata['snippets'] as $index => $snippet) {
29659036814SCostin Stroie                    // These are text snippets from ChromaDB
29759036814SCostin Stroie                    $snippetsContent[] = "\n<example id=\"" . ($index + 1) . "\">\n" . $snippet . "\n</example>\n";
29859036814SCostin Stroie                }
29959036814SCostin Stroie                if (!empty($snippetsContent)) {
30059036814SCostin 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";
30159036814SCostin Stroie                }
30259036814SCostin Stroie            }
30359036814SCostin Stroie
30459036814SCostin Stroie            $contextInfo .= "\n</context>\n";
30559036814SCostin Stroie
30659036814SCostin Stroie            // Append context information to system prompt
30759036814SCostin Stroie            $prompt = $contextInfo . "\n\n" . $prompt;
30859036814SCostin Stroie        }
30959036814SCostin Stroie
31059036814SCostin Stroie        // Prepare API request data with model parameters
31159036814SCostin Stroie        $data = [
31259036814SCostin Stroie            'model' => $this->model,
31359036814SCostin Stroie            'messages' => [
31459036814SCostin Stroie                ['role' => 'system', 'content' => $systemPrompt],
31559036814SCostin Stroie                ['role' => 'user', 'content' => $prompt]
31659036814SCostin Stroie            ],
31759036814SCostin Stroie            'max_tokens' => 6144,
31859036814SCostin Stroie            'stream' => false,
31959036814SCostin Stroie            'keep_alive' => '30m',
32059036814SCostin Stroie            'think' => true
32159036814SCostin Stroie        ];
32259036814SCostin Stroie
32359036814SCostin Stroie        // Add tools to the request only if useTools is true
32459036814SCostin Stroie        if ($useTools) {
32559036814SCostin Stroie            // Define available tools
32659036814SCostin Stroie            $data['tools'] = $this->getAvailableTools();
32759036814SCostin Stroie            $data['tool_choice'] = 'auto';
32859036814SCostin Stroie            $data['parallel_tool_calls'] = false;
32959036814SCostin Stroie        }
33059036814SCostin Stroie
33159036814SCostin Stroie        // Only add parameters if they are defined and not null
33259036814SCostin Stroie        if ($this->temperature !== null) {
33359036814SCostin Stroie            $data['temperature'] = $this->temperature;
33459036814SCostin Stroie        }
33559036814SCostin Stroie        if ($this->top_p !== null) {
33659036814SCostin Stroie            $data['top_p'] = $this->top_p;
33759036814SCostin Stroie        }
33859036814SCostin Stroie        if ($this->top_k !== null) {
33959036814SCostin Stroie            $data['top_k'] = $this->top_k;
34059036814SCostin Stroie        }
34159036814SCostin Stroie        if ($this->min_p !== null) {
34259036814SCostin Stroie            $data['min_p'] = $this->min_p;
34359036814SCostin Stroie        }
34459036814SCostin Stroie
34559036814SCostin Stroie        // Make an API call with tool responses
34659036814SCostin Stroie        return $this->callAPIWithTools($data, false);
34759036814SCostin Stroie    }
34859036814SCostin Stroie
34959036814SCostin Stroie    /**
35059036814SCostin Stroie     * Handle tool calls from the LLM
35159036814SCostin Stroie     *
35259036814SCostin Stroie     * Processes tool calls made by the LLM and returns appropriate responses.
35359036814SCostin Stroie     * Implements caching to avoid duplicate calls with identical parameters.
35459036814SCostin Stroie     *
35559036814SCostin Stroie     * @param array $toolCall The tool call data from the LLM
35659036814SCostin Stroie     * @return array The tool response message
35759036814SCostin Stroie     */
35859036814SCostin Stroie    private function handleToolCall($toolCall)
35959036814SCostin Stroie    {
36059036814SCostin Stroie        $toolName = $toolCall['function']['name'];
36159036814SCostin Stroie        $arguments = json_decode($toolCall['function']['arguments'], true);
36259036814SCostin Stroie
36359036814SCostin Stroie        // Create a cache key from the tool name and arguments
36459036814SCostin Stroie        $cacheKey = md5($toolName . serialize($arguments));
36559036814SCostin Stroie
36659036814SCostin Stroie        // Check if we have a cached result for this tool call
36759036814SCostin Stroie        if (isset($this->toolCallCache[$cacheKey])) {
36859036814SCostin Stroie            // Return cached result and indicate it was found in cache
36959036814SCostin Stroie            $toolResponse = $this->toolCallCache[$cacheKey];
37059036814SCostin Stroie            // Update with current tool call ID
37159036814SCostin Stroie            $toolResponse['tool_call_id'] = $toolCall['id'];
37259036814SCostin Stroie            $toolResponse['cached'] = true; // Indicate this response was cached
37359036814SCostin Stroie            return $toolResponse;
37459036814SCostin Stroie        }
37559036814SCostin Stroie
37659036814SCostin Stroie        $toolResponse = [
37759036814SCostin Stroie            'role' => 'tool',
37859036814SCostin Stroie            'tool_call_id' => $toolCall['id'],
37959036814SCostin Stroie            'cached' => false // Indicate this is a fresh response
38059036814SCostin Stroie        ];
38159036814SCostin Stroie
38259036814SCostin Stroie        switch ($toolName) {
38359036814SCostin Stroie            case 'get_document':
38459036814SCostin Stroie                $documentId = $arguments['id'];
38559036814SCostin Stroie                $content = $this->getPageContent($documentId);
38659036814SCostin Stroie                if ($content === false) {
38759036814SCostin Stroie                    $toolResponse['content'] = 'Document not found: ' . $documentId;
38859036814SCostin Stroie                } else {
38959036814SCostin Stroie                    $toolResponse['content'] = $content;
39059036814SCostin Stroie                }
39159036814SCostin Stroie                break;
39259036814SCostin Stroie
39359036814SCostin Stroie            case 'get_template':
39459036814SCostin Stroie                // Get template content using the convenience function
39559036814SCostin Stroie                $toolResponse['content'] = $this->getTemplateContent();
39659036814SCostin Stroie                break;
39759036814SCostin Stroie
39859036814SCostin Stroie            case 'get_examples':
39959036814SCostin Stroie                // Get examples content using the convenience function
40059036814SCostin Stroie                $count = isset($arguments['count']) ? (int)$arguments['count'] : 5;
40159036814SCostin Stroie                $toolResponse['content'] = '<examples>\n' . $this->getSnippets($count) . '\n</examples>';
40259036814SCostin Stroie                break;
40359036814SCostin Stroie
40459036814SCostin Stroie            default:
40559036814SCostin Stroie                $toolResponse['content'] = 'Unknown tool: ' . $toolName;
40659036814SCostin Stroie        }
40759036814SCostin Stroie
40859036814SCostin Stroie        // Cache the result for future calls with the same parameters
40959036814SCostin Stroie        $cacheEntry = $toolResponse;
41059036814SCostin Stroie        // Remove tool_call_id and cached flag from cache as they change per call
41159036814SCostin Stroie        unset($cacheEntry['tool_call_id']);
41259036814SCostin Stroie        unset($cacheEntry['cached']);
41359036814SCostin Stroie        $this->toolCallCache[$cacheKey] = $cacheEntry;
41459036814SCostin Stroie
41559036814SCostin Stroie        return $toolResponse;
41659036814SCostin Stroie    }
41759036814SCostin Stroie
41859036814SCostin Stroie    /**
41959036814SCostin Stroie     * Make an API call with tool responses
42059036814SCostin Stroie     *
42159036814SCostin Stroie     * Sends a follow-up request to the LLM with tool responses.
42259036814SCostin Stroie     * Implements complex logic for handling tool calls with caching and loop protection.
42359036814SCostin Stroie     *
42459036814SCostin Stroie     * Complex logic includes:
42559036814SCostin Stroie     * 1. Making HTTP requests with proper authentication and error handling
42659036814SCostin Stroie     * 2. Processing tool calls from the LLM response
42759036814SCostin Stroie     * 3. Caching tool responses to avoid duplicate calls with identical parameters
42859036814SCostin Stroie     * 4. Tracking tool call counts to prevent infinite loops
42959036814SCostin Stroie     * 5. Implementing loop protection with call count limits
43059036814SCostin Stroie     * 6. Handling recursive tool calls until final content is generated
43159036814SCostin Stroie     *
43259036814SCostin Stroie     * Loop protection works by:
43359036814SCostin Stroie     * - Tracking individual tool call counts (max 3 per tool)
43459036814SCostin Stroie     * - Tracking total tool calls (max 10 total)
43559036814SCostin Stroie     * - Disabling tools when limits are exceeded to break potential loops
43659036814SCostin Stroie     *
43759036814SCostin Stroie     * @param array $data The API request data including messages with tool responses
43859036814SCostin Stroie     * @param bool $toolsCalled Whether tools have already been called (used for loop protection)
43959036814SCostin Stroie     * @param bool $useTools Whether to process tool calls (used for loop protection)
44059036814SCostin Stroie     * @return string The final response content
44159036814SCostin Stroie     */
44259036814SCostin Stroie    private function callAPIWithTools($data, $toolsCalled = false, $useTools = true)
44359036814SCostin Stroie    {
44459036814SCostin Stroie        // Set up HTTP headers, including authentication if API key is configured
44559036814SCostin Stroie        $headers = [
44659036814SCostin Stroie            'Content-Type: application/json'
44759036814SCostin Stroie        ];
44859036814SCostin Stroie
44959036814SCostin Stroie        if (!empty($this->api_key)) {
45059036814SCostin Stroie            $headers[] = 'Authorization: Bearer ' . $this->api_key;
45159036814SCostin Stroie        }
45259036814SCostin Stroie
45359036814SCostin Stroie       // If tools have already been called, remove tools and tool_choice from data to prevent infinite loops
45459036814SCostin Stroie        if ($toolsCalled) {
45559036814SCostin Stroie            unset($data['tools']);
45659036814SCostin Stroie            unset($data['tool_choice']);
45759036814SCostin Stroie        }
45859036814SCostin Stroie
45959036814SCostin Stroie        // Initialize and configure cURL for the API request
46059036814SCostin Stroie        $ch = curl_init();
46159036814SCostin Stroie        curl_setopt($ch, CURLOPT_URL, $this->api_url);
46259036814SCostin Stroie        curl_setopt($ch, CURLOPT_POST, true);
46359036814SCostin Stroie        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
46459036814SCostin Stroie        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
46559036814SCostin Stroie        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
46659036814SCostin Stroie        curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
46759036814SCostin Stroie        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
46859036814SCostin Stroie
46959036814SCostin Stroie        // Execute the API request
47059036814SCostin Stroie        $response = curl_exec($ch);
47159036814SCostin Stroie        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
47259036814SCostin Stroie        $error = curl_error($ch);
47359036814SCostin Stroie        curl_close($ch);
47459036814SCostin Stroie
47559036814SCostin Stroie        // Handle cURL errors
47659036814SCostin Stroie        if ($error) {
47759036814SCostin Stroie            throw new Exception('API request failed: ' . $error);
47859036814SCostin Stroie        }
47959036814SCostin Stroie
48059036814SCostin Stroie        // Handle HTTP errors
48159036814SCostin Stroie        if ($httpCode !== 200) {
48259036814SCostin Stroie            throw new Exception('API request failed with HTTP code: ' . $httpCode);
48359036814SCostin Stroie        }
48459036814SCostin Stroie
48559036814SCostin Stroie        // Parse and validate the JSON response
48659036814SCostin Stroie        $result = json_decode($response, true);
48759036814SCostin Stroie
48859036814SCostin Stroie        // Extract the content from the response if available
48959036814SCostin Stroie        if (isset($result['choices'][0]['message']['content'])) {
49059036814SCostin Stroie            $content = trim($result['choices'][0]['message']['content']);
49159036814SCostin Stroie            // Reset tool call counts when we get final content
49259036814SCostin Stroie            $this->toolCallCounts = [];
49359036814SCostin Stroie            return $content;
49459036814SCostin Stroie        }
49559036814SCostin Stroie
49659036814SCostin Stroie        // Handle tool calls if present
49759036814SCostin Stroie        if ($useTools && isset($result['choices'][0]['message']['tool_calls'])) {
49859036814SCostin Stroie            $toolCalls = $result['choices'][0]['message']['tool_calls'];
49959036814SCostin Stroie            // Start with original messages
50059036814SCostin Stroie            $messages = $data['messages'];
50159036814SCostin Stroie            // Add assistant's message with tool calls, keeping all original fields except for content (which is null)
50259036814SCostin Stroie            $assistantMessage = [];
50359036814SCostin Stroie            foreach ($result['choices'][0]['message'] as $key => $value) {
50459036814SCostin Stroie                if ($key !== 'content') {
50559036814SCostin Stroie                    $assistantMessage[$key] = $value;
50659036814SCostin Stroie                }
50759036814SCostin Stroie            }
50859036814SCostin Stroie            // Add assistant's message with tool calls
50959036814SCostin Stroie            $messages[] = $assistantMessage;
51059036814SCostin Stroie
51159036814SCostin Stroie            // Process each tool call and track counts to prevent infinite loops
51259036814SCostin Stroie            foreach ($toolCalls as $toolCall) {
51359036814SCostin Stroie                $toolName = $toolCall['function']['name'];
51459036814SCostin Stroie                // Increment tool call count
51559036814SCostin Stroie                if (!isset($this->toolCallCounts[$toolName])) {
51659036814SCostin Stroie                    $this->toolCallCounts[$toolName] = 0;
51759036814SCostin Stroie                }
51859036814SCostin Stroie                $this->toolCallCounts[$toolName]++;
51959036814SCostin Stroie
52059036814SCostin Stroie                $toolResponse = $this->handleToolCall($toolCall);
52159036814SCostin Stroie                $messages[] = $toolResponse;
52259036814SCostin Stroie            }
52359036814SCostin Stroie
52459036814SCostin Stroie            // Check if any tool has been called more than 3 times
52559036814SCostin Stroie            $toolsCalledCount = 0;
52659036814SCostin Stroie            foreach ($this->toolCallCounts as $count) {
52759036814SCostin Stroie                if ($count > 3) {
52859036814SCostin Stroie                    // If any tool called more than 3 times, disable tools to break loop
52959036814SCostin Stroie                    $toolsCalled = true;
53059036814SCostin Stroie                    break;
53159036814SCostin Stroie                }
53259036814SCostin Stroie                $toolsCalledCount += $count;
53359036814SCostin Stroie            }
53459036814SCostin Stroie
53559036814SCostin Stroie            // If total tool calls exceed 10, also disable tools
53659036814SCostin Stroie            if ($toolsCalledCount > 10) {
53759036814SCostin Stroie                $toolsCalled = true;
53859036814SCostin Stroie            }
53959036814SCostin Stroie
54059036814SCostin Stroie            // Make another API call with tool responses
54159036814SCostin Stroie            $data['messages'] = $messages;
54259036814SCostin Stroie            return $this->callAPIWithTools($data, $toolsCalled, $useTools);
54359036814SCostin Stroie        }
54459036814SCostin Stroie
54559036814SCostin Stroie        // Throw exception for unexpected response format
54659036814SCostin Stroie        throw new Exception('Unexpected API response format');
54759036814SCostin Stroie    }
54859036814SCostin Stroie
54959036814SCostin Stroie    /**
55059036814SCostin Stroie     * Load a prompt template from a DokuWiki page and replace placeholders
55159036814SCostin Stroie     *
55259036814SCostin Stroie     * Loads prompt templates from DokuWiki pages with IDs in the format
553bb1c2789SCostin Stroie     * dokullm:profiles:PROFILE:PROMPT_NAME
55459036814SCostin Stroie     *
555bb1c2789SCostin Stroie     * The method implements a profile fallback mechanism:
556bb1c2789SCostin Stroie     * 1. First tries to load the prompt from the configured profile
557bb1c2789SCostin Stroie     * 2. If not found, falls back to default prompts
55859036814SCostin Stroie     * 3. Throws an exception if neither is available
55959036814SCostin Stroie     *
56059036814SCostin Stroie     * After loading the prompt, it scans for placeholders and automatically
56159036814SCostin Stroie     * adds missing ones with appropriate values before replacing all placeholders.
56259036814SCostin Stroie     *
56359036814SCostin Stroie     * @param string $promptName The name of the prompt (e.g., 'create', 'rewrite')
56459036814SCostin Stroie     * @param array $variables Associative array of placeholder => value pairs
56559036814SCostin Stroie     * @return string The processed prompt with placeholders replaced
566bb1c2789SCostin Stroie     * @throws Exception If the prompt page cannot be loaded from any profile
56759036814SCostin Stroie     */
568f135ebabSCostin Stroie (aider)    private function loadPrompt($promptName, $variables = [])
56959036814SCostin Stroie    {
570bb1c2789SCostin Stroie        // Default to 'default' if profile is not set
571bb1c2789SCostin Stroie        if (empty($this->profile)) {
572bb1c2789SCostin Stroie            $this->profile = 'default';
57359036814SCostin Stroie        }
57459036814SCostin Stroie
575bb1c2789SCostin Stroie        // Construct the page ID for the prompt in the configured profile
576bb1c2789SCostin Stroie        $promptPageId = 'dokullm:profiles:' . $this->profile . ':' . $promptName;
57759036814SCostin Stroie
578bb1c2789SCostin Stroie        // Try to get the content of the prompt page in the configured profile
57959036814SCostin Stroie        $prompt = $this->getPageContent($promptPageId);
58059036814SCostin Stroie
581bb1c2789SCostin Stroie        // If the profile-specific prompt doesn't exist, try default as fallback
582bb1c2789SCostin Stroie        if ($prompt === false && $this->profile !== 'default') {
583bb1c2789SCostin Stroie            $promptPageId = 'dokullm:profile:default:' . $promptName;
58459036814SCostin Stroie            $prompt = $this->getPageContent($promptPageId);
58559036814SCostin Stroie        }
58659036814SCostin Stroie
58759036814SCostin Stroie        // If still no prompt found, throw an exception
58859036814SCostin Stroie        if ($prompt === false) {
58959036814SCostin Stroie            throw new Exception('Prompt page not found: ' . $promptPageId);
59059036814SCostin Stroie        }
59159036814SCostin Stroie
59259036814SCostin Stroie        // Find placeholders in the prompt
59359036814SCostin Stroie        $placeholders = $this->findPlaceholders($prompt);
59459036814SCostin Stroie
59559036814SCostin Stroie        // Add missing placeholders with appropriate values
59659036814SCostin Stroie        foreach ($placeholders as $placeholder) {
59759036814SCostin Stroie            // Skip if already provided in variables
59859036814SCostin Stroie            if (isset($variables[$placeholder])) {
59959036814SCostin Stroie                continue;
60059036814SCostin Stroie            }
60159036814SCostin Stroie
60259036814SCostin Stroie            // Add appropriate values for specific placeholders
60359036814SCostin Stroie            switch ($placeholder) {
60459036814SCostin Stroie                case 'template':
60559036814SCostin Stroie                    // If we have a page_template in variables, use it
60659036814SCostin Stroie                    $variables[$placeholder] = $this->getTemplateContent($variables['page_template']);
60759036814SCostin Stroie                    break;
60859036814SCostin Stroie
60959036814SCostin Stroie                case 'snippets':
6102adac073SCostin Stroie (aider)                    $variables[$placeholder] = $this->chromaClient !== null ? $this->getSnippets(10) : '( no examples )';
61159036814SCostin Stroie                    break;
61259036814SCostin Stroie
61359036814SCostin Stroie                case 'examples':
61459036814SCostin Stroie                    // If we have example page IDs in metadata, add examples content
61559036814SCostin Stroie                    $variables[$placeholder] = $this->getExamplesContent($variables['page_examples']);
61659036814SCostin Stroie                    break;
61759036814SCostin Stroie
61859036814SCostin Stroie                case 'previous':
61959036814SCostin Stroie                    // If we have a previous report page ID in metadata, add previous content
62059036814SCostin Stroie                    $variables[$placeholder] = $this->getPreviousContent($variables['page_previous']);
62159036814SCostin Stroie
62259036814SCostin Stroie                    // Add current and previous dates to metadata
623889c09bdSCostin Stroie (aider)                    $variables['current_date'] = $this->getPageDate($this->pageId);
62459036814SCostin Stroie                    $variables['previous_date'] = !empty($variables['page_previous']) ?
62559036814SCostin Stroie                                                $this->getPageDate($variables['page_previous']) :
62659036814SCostin Stroie                                                '';
62759036814SCostin Stroie                    break;
62859036814SCostin Stroie
6292c175338SCostin Stroie (aider)                case 'prompt':
6302c175338SCostin Stroie (aider)                    // Add the custom prompt value
6312c175338SCostin Stroie (aider)                    $variables[$placeholder] = isset($variables['prompt']) ? $variables['prompt'] : '';
6322c175338SCostin Stroie (aider)                    break;
6332c175338SCostin Stroie (aider)
63459036814SCostin Stroie                default:
63559036814SCostin Stroie                    // For other placeholders, leave them empty or set a default value
63659036814SCostin Stroie                    $variables[$placeholder] = '';
63759036814SCostin Stroie                    break;
63859036814SCostin Stroie            }
63959036814SCostin Stroie        }
64059036814SCostin Stroie
64159036814SCostin Stroie        // Replace placeholders with actual values
64259036814SCostin Stroie        // Placeholders are in the format {placeholder_name}
64359036814SCostin Stroie        foreach ($variables as $placeholder => $value) {
64459036814SCostin Stroie            $prompt = str_replace('{' . $placeholder . '}', $value, $prompt);
64559036814SCostin Stroie        }
64659036814SCostin Stroie
64759036814SCostin Stroie        // Return the processed prompt
64859036814SCostin Stroie        return $prompt;
64959036814SCostin Stroie    }
65059036814SCostin Stroie
65159036814SCostin Stroie    /**
65259036814SCostin Stroie     * Load system prompt with optional command-specific appendage
65359036814SCostin Stroie     *
65459036814SCostin Stroie     * Loads the main system prompt and appends any command-specific system prompt
65559036814SCostin Stroie     * if available.
65659036814SCostin Stroie     *
65759036814SCostin Stroie     * @param string $action The action/command name
65859036814SCostin Stroie     * @param array $variables Associative array of placeholder => value pairs
65959036814SCostin Stroie     * @return string The combined system prompt
66059036814SCostin Stroie     */
661f135ebabSCostin Stroie (aider)    private function loadSystemPrompt($action, $variables = [])
66259036814SCostin Stroie    {
66359036814SCostin Stroie        // Load system prompt which provides general instructions to the LLM
664f135ebabSCostin Stroie (aider)        $systemPrompt = $this->loadPrompt('system', $variables);
66559036814SCostin Stroie
66659036814SCostin Stroie        // Check if there's a command-specific system prompt appendage
66759036814SCostin Stroie        if (!empty($action)) {
66859036814SCostin Stroie            try {
669f135ebabSCostin Stroie (aider)                $commandSystemPrompt = $this->loadPrompt($action . ':system', $variables);
67059036814SCostin Stroie                if ($commandSystemPrompt !== false) {
67159036814SCostin Stroie                    $systemPrompt .= "\n" . $commandSystemPrompt;
67259036814SCostin Stroie                }
67359036814SCostin Stroie            } catch (Exception $e) {
67459036814SCostin Stroie                // Ignore exceptions when loading command-specific system prompt
67559036814SCostin Stroie                // This allows the main system prompt to still be used
67659036814SCostin Stroie            }
67759036814SCostin Stroie        }
67859036814SCostin Stroie
67959036814SCostin Stroie        return $systemPrompt;
68059036814SCostin Stroie    }
68159036814SCostin Stroie
68259036814SCostin Stroie    /**
68359036814SCostin Stroie     * Get the content of a DokuWiki page
68459036814SCostin Stroie     *
68559036814SCostin Stroie     * Retrieves the raw content of a DokuWiki page by its ID.
68659036814SCostin Stroie     * Used for loading template and example page content for context.
68759036814SCostin Stroie     *
68859036814SCostin Stroie     * @param string $pageId The page ID to retrieve
68959036814SCostin Stroie     * @return string|false The page content or false if not found/readable
690*40986135SCostin Stroie (aider)     * @throws Exception If access is denied
69159036814SCostin Stroie     */
69259036814SCostin Stroie    public function getPageContent($pageId)
69359036814SCostin Stroie    {
694*40986135SCostin Stroie (aider)        // Clean the ID and check ACL
695*40986135SCostin Stroie (aider)        $cleanId = cleanID($pageId);
696*40986135SCostin Stroie (aider)        if (auth_quickaclcheck($cleanId) < AUTH_READ) {
697*40986135SCostin Stroie (aider)            throw new Exception('You are not allowed to read this file');
698*40986135SCostin Stroie (aider)        }
699*40986135SCostin Stroie (aider)
70059036814SCostin Stroie        // Convert page ID to file path
701*40986135SCostin Stroie (aider)        $pageFile = wikiFN($cleanId);
70259036814SCostin Stroie
70359036814SCostin Stroie        // Check if file exists and is readable
70459036814SCostin Stroie        if (file_exists($pageFile) && is_readable($pageFile)) {
70559036814SCostin Stroie            return file_get_contents($pageFile);
70659036814SCostin Stroie        }
70759036814SCostin Stroie
70859036814SCostin Stroie        return false;
70959036814SCostin Stroie    }
71059036814SCostin Stroie
71159036814SCostin Stroie    /**
71259036814SCostin Stroie     * Extract date from page ID or file timestamp
71359036814SCostin Stroie     *
71459036814SCostin Stroie     * Attempts to extract a date in YYmmdd format from the page ID.
71559036814SCostin Stroie     * If not found, uses the file's last modification timestamp.
71659036814SCostin Stroie     *
71759036814SCostin Stroie     * @param string $pageId Optional page ID to extract date from (defaults to current page)
71859036814SCostin Stroie     * @return string Formatted date string (YYYY-MM-DD)
71959036814SCostin Stroie     */
72059036814SCostin Stroie    private function getPageDate($pageId = null)
72159036814SCostin Stroie    {
72259036814SCostin Stroie        // Use provided page ID or current page ID
723889c09bdSCostin Stroie (aider)        $targetPageId = $pageId ?: $this->pageId;
72459036814SCostin Stroie
72559036814SCostin Stroie        // Try to extract date from page ID (looking for YYmmdd pattern)
72659036814SCostin Stroie        if (preg_match('/(\d{2})(\d{2})(\d{2})/', $targetPageId, $matches)) {
72759036814SCostin Stroie            // Convert YYmmdd to YYYY-MM-DD
72859036814SCostin Stroie            $year = $matches[1];
72959036814SCostin Stroie            $month = $matches[2];
73059036814SCostin Stroie            $day = $matches[3];
73159036814SCostin Stroie
73259036814SCostin Stroie            // Assume 20xx for years 00-69, 19xx for years 70-99
73359036814SCostin Stroie            $fullYear = intval($year) <= 69 ? '20' . $year : '19' . $year;
73459036814SCostin Stroie
73559036814SCostin Stroie            return $fullYear . '-' . $month . '-' . $day;
73659036814SCostin Stroie        }
73759036814SCostin Stroie
73859036814SCostin Stroie        // Fallback to file timestamp
73959036814SCostin Stroie        $pageFile = wikiFN($targetPageId);
74059036814SCostin Stroie        if (file_exists($pageFile)) {
74159036814SCostin Stroie            $timestamp = filemtime($pageFile);
74259036814SCostin Stroie            return date('Y-m-d', $timestamp);
74359036814SCostin Stroie        }
74459036814SCostin Stroie
74559036814SCostin Stroie        // Return empty string if no date can be determined
74659036814SCostin Stroie        return '';
74759036814SCostin Stroie    }
74859036814SCostin Stroie
74959036814SCostin Stroie    /**
75059036814SCostin Stroie     * Get current text
75159036814SCostin Stroie     *
75259036814SCostin Stroie     * Retrieves the current text stored from the process function.
75359036814SCostin Stroie     *
75459036814SCostin Stroie     * @return string The current text
75559036814SCostin Stroie     */
75659036814SCostin Stroie    private function getCurrentText()
75759036814SCostin Stroie    {
75859036814SCostin Stroie        return $this->currentText;
75959036814SCostin Stroie    }
76059036814SCostin Stroie
76159036814SCostin Stroie    /**
76259036814SCostin Stroie     * Scan text for placeholders
76359036814SCostin Stroie     *
76459036814SCostin Stroie     * Finds all placeholders in the format {placeholder_name} in the provided text
76559036814SCostin Stroie     * and returns an array of unique placeholder names.
76659036814SCostin Stroie     *
76759036814SCostin Stroie     * @param string $text The text to scan for placeholders
76859036814SCostin Stroie     * @return array List of unique placeholder names found in the text
76959036814SCostin Stroie     */
77059036814SCostin Stroie    public function findPlaceholders($text)
77159036814SCostin Stroie    {
77259036814SCostin Stroie        $placeholders = [];
77359036814SCostin Stroie        $pattern = '/\{([^}]+)\}/';
77459036814SCostin Stroie
77559036814SCostin Stroie        if (preg_match_all($pattern, $text, $matches)) {
77659036814SCostin Stroie            // Get unique placeholder names
77759036814SCostin Stroie            $placeholders = array_unique($matches[1]);
77859036814SCostin Stroie        }
77959036814SCostin Stroie
78059036814SCostin Stroie        return $placeholders;
78159036814SCostin Stroie    }
78259036814SCostin Stroie
78359036814SCostin Stroie    /**
78459036814SCostin Stroie     * Get template content for the current text
78559036814SCostin Stroie     *
78659036814SCostin Stroie     * Convenience function to retrieve template content. If a pageId is provided,
78759036814SCostin Stroie     * retrieves content directly from that page. Otherwise, queries ChromaDB for
78859036814SCostin Stroie     * a relevant template based on the current text.
78959036814SCostin Stroie     *
79059036814SCostin Stroie     * @param string|null $pageId Optional page ID to retrieve template from directly
79159036814SCostin Stroie     * @return string The template content or empty string if not found
79259036814SCostin Stroie     */
79359036814SCostin Stroie    private function getTemplateContent($pageId = null)
79459036814SCostin Stroie    {
79559036814SCostin Stroie        // If pageId is provided, use it directly
79659036814SCostin Stroie        if ($pageId !== null) {
79759036814SCostin Stroie            $templateContent = $this->getPageContent($pageId);
79859036814SCostin Stroie            if ($templateContent !== false) {
79959036814SCostin Stroie                return $templateContent;
80059036814SCostin Stroie            }
80159036814SCostin Stroie        }
80259036814SCostin Stroie
803a8c74011SCostin Stroie (aider)        // If ChromaDB is disabled, return empty template
8042adac073SCostin Stroie (aider)        if ($this->chromaClient === null) {
805a8c74011SCostin Stroie (aider)            return '( no template )';
806a8c74011SCostin Stroie (aider)        }
807a8c74011SCostin Stroie (aider)
80859036814SCostin Stroie        // Otherwise, get template suggestion for the current text
80959036814SCostin Stroie        $pageId = $this->queryChromaDBTemplate($this->getCurrentText());
81059036814SCostin Stroie        if (!empty($pageId)) {
81159036814SCostin Stroie            $templateContent = $this->getPageContent($pageId[0]);
81259036814SCostin Stroie            if ($templateContent !== false) {
81359036814SCostin Stroie                return $templateContent;
81459036814SCostin Stroie            }
81559036814SCostin Stroie        }
81659036814SCostin Stroie        return '( no template )';
81759036814SCostin Stroie    }
81859036814SCostin Stroie
81959036814SCostin Stroie    /**
82059036814SCostin Stroie     * Get snippets content for the current text
82159036814SCostin Stroie     *
82259036814SCostin Stroie     * Convenience function to retrieve relevant snippets for the current text.
82359036814SCostin Stroie     * Queries ChromaDB for relevant snippets and returns them formatted.
82459036814SCostin Stroie     *
82559036814SCostin Stroie     * @param int $count Number of snippets to retrieve (default: 10)
82659036814SCostin Stroie     * @return string Formatted snippets content or empty string if not found
82759036814SCostin Stroie     */
82859036814SCostin Stroie    private function getSnippets($count = 10)
82959036814SCostin Stroie    {
830a8c74011SCostin Stroie (aider)        // If ChromaDB is disabled, return empty snippets
8312adac073SCostin Stroie (aider)        if ($this->chromaClient === null) {
832a8c74011SCostin Stroie (aider)            return '( no examples )';
833a8c74011SCostin Stroie (aider)        }
834a8c74011SCostin Stroie (aider)
83559036814SCostin Stroie        // Get example snippets for the current text
83659036814SCostin Stroie        $snippets = $this->queryChromaDBSnippets($this->getCurrentText(), $count);
83759036814SCostin Stroie        if (!empty($snippets)) {
83859036814SCostin Stroie            $formattedSnippets = [];
83959036814SCostin Stroie            foreach ($snippets as $index => $snippet) {
84059036814SCostin Stroie                $formattedSnippets[] = '<example id="' . ($index + 1) . '">\n' . $snippet . '\n</example>';
84159036814SCostin Stroie            }
84259036814SCostin Stroie            return implode("\n", $formattedSnippets);
84359036814SCostin Stroie        }
84459036814SCostin Stroie        return '( no examples )';
84559036814SCostin Stroie    }
84659036814SCostin Stroie
84759036814SCostin Stroie    /**
84859036814SCostin Stroie     * Get examples content from example page IDs
84959036814SCostin Stroie     *
85059036814SCostin Stroie     * Convenience function to retrieve content from example pages.
85159036814SCostin Stroie     * Returns the content of each page packed in XML elements.
85259036814SCostin Stroie     *
85359036814SCostin Stroie     * @param array $exampleIds List of example page IDs
85459036814SCostin Stroie     * @return string Formatted examples content or empty string if not found
85559036814SCostin Stroie     */
85659036814SCostin Stroie    private function getExamplesContent($exampleIds = [])
85759036814SCostin Stroie    {
85859036814SCostin Stroie        if (empty($exampleIds) || !is_array($exampleIds)) {
85959036814SCostin Stroie            return '( no examples )';
86059036814SCostin Stroie        }
86159036814SCostin Stroie
86259036814SCostin Stroie        $examplesContent = [];
86359036814SCostin Stroie        foreach ($exampleIds as $index => $exampleId) {
86459036814SCostin Stroie            $content = $this->getPageContent($exampleId);
86559036814SCostin Stroie            if ($content !== false) {
86659036814SCostin Stroie                $examplesContent[] = '<example_page source="' . $exampleId . '">\n' . $content . '\n</example_page>';
86759036814SCostin Stroie            }
86859036814SCostin Stroie        }
86959036814SCostin Stroie
87059036814SCostin Stroie        return implode("\n", $examplesContent);
87159036814SCostin Stroie    }
87259036814SCostin Stroie
87359036814SCostin Stroie    /**
87459036814SCostin Stroie     * Get previous report content from previous page ID
87559036814SCostin Stroie     *
87659036814SCostin Stroie     * Convenience function to retrieve content from a previous report page.
87759036814SCostin Stroie     * Returns the content of the previous page or a default message if not found.
87859036814SCostin Stroie     *
87959036814SCostin Stroie     * @param string $previousId Previous page ID
88059036814SCostin Stroie     * @return string Previous report content or default message if not found
88159036814SCostin Stroie     */
88259036814SCostin Stroie    private function getPreviousContent($previousId = '')
88359036814SCostin Stroie    {
88459036814SCostin Stroie        if (empty($previousId)) {
88559036814SCostin Stroie            return '( no previous report )';
88659036814SCostin Stroie        }
88759036814SCostin Stroie
88859036814SCostin Stroie        $content = $this->getPageContent($previousId);
88959036814SCostin Stroie        if ($content !== false) {
89059036814SCostin Stroie            return $content;
89159036814SCostin Stroie        }
89259036814SCostin Stroie
89359036814SCostin Stroie        return '( previous report not found )';
89459036814SCostin Stroie    }
89559036814SCostin Stroie
89659036814SCostin Stroie    /**
89759036814SCostin Stroie     * Get ChromaDB client with configuration
89859036814SCostin Stroie     *
8992de95678SCostin Stroie (aider)     * Returns the ChromaDB client and collection name.
9002de95678SCostin Stroie (aider)     * If a client was passed in the constructor, use it. Otherwise, this method
9012de95678SCostin Stroie (aider)     * should not be called as it depends on getConf() which is not available.
90259036814SCostin Stroie     *
90359036814SCostin Stroie     * @return array Array containing the ChromaDB client and collection name
9042de95678SCostin Stroie (aider)     * @throws Exception If no ChromaDB client is available
90559036814SCostin Stroie     */
90659036814SCostin Stroie    private function getChromaDBClient()
90759036814SCostin Stroie    {
9082de95678SCostin Stroie (aider)        // If we have a ChromaDB client passed in constructor, use it
9092de95678SCostin Stroie (aider)        if ($this->chromaClient !== null) {
91072d9a73bSCostin Stroie (aider)            // Get the collection name based on the page ID
911b1d13019SCostin Stroie	    // FIXME
912340ecab5SCostin Stroie            $chromaCollection = 'reports';
913b1d13019SCostin Stroie            $pageId = $pageId;
91459036814SCostin Stroie
915b1d13019SCostin Stroie            if (!empty($this->pageId)) {
91659036814SCostin Stroie                // Split the page ID by ':' and take the first part as collection name
917b1d13019SCostin Stroie                $parts = explode(':', $this->pageId);
91859036814SCostin Stroie                if (isset($parts[0]) && !empty($parts[0])) {
91959036814SCostin Stroie                    // If the first part is 'playground', use the default collection
92059036814SCostin Stroie                    // Otherwise, use the first part as the collection name
92159036814SCostin Stroie                    if ($parts[0] === 'playground') {
922b1d13019SCostin Stroie                        $chromaCollection = '';
92359036814SCostin Stroie                    } else {
92459036814SCostin Stroie                        $chromaCollection = $parts[0];
92559036814SCostin Stroie                    }
92659036814SCostin Stroie                }
92759036814SCostin Stroie            }
92859036814SCostin Stroie
9292de95678SCostin Stroie (aider)            return [$this->chromaClient, $chromaCollection];
9302de95678SCostin Stroie (aider)        }
93159036814SCostin Stroie
9322de95678SCostin Stroie (aider)        // If we don't have a ChromaDB client, we can't create one here
9332de95678SCostin Stroie (aider)        // because getConf() is not available in this context
9342de95678SCostin Stroie (aider)        throw new Exception('No ChromaDB client available');
93559036814SCostin Stroie    }
93659036814SCostin Stroie
93759036814SCostin Stroie    /**
93859036814SCostin Stroie     * Query ChromaDB for relevant documents
93959036814SCostin Stroie     *
94059036814SCostin Stroie     * Generates embeddings for the input text and queries ChromaDB for similar documents.
94159036814SCostin Stroie     * Extracts modality from the current page ID to use as the collection name.
94259036814SCostin Stroie     *
94359036814SCostin Stroie     * @param string $text The text to find similar documents for
94459036814SCostin Stroie     * @param int $limit Maximum number of documents to retrieve (default: 5)
94559036814SCostin Stroie     * @param array|null $where Optional filter conditions for metadata
94659036814SCostin Stroie     * @return array List of document IDs
94759036814SCostin Stroie     */
94859036814SCostin Stroie    private function queryChromaDB($text, $limit = 5, $where = null)
94959036814SCostin Stroie    {
95059036814SCostin Stroie        try {
95159036814SCostin Stroie            // Get ChromaDB client and collection name
95259036814SCostin Stroie            list($chromaClient, $chromaCollection) = $this->getChromaDBClient();
95359036814SCostin Stroie            // Query for similar documents
95459036814SCostin Stroie            $results = $chromaClient->queryCollection($chromaCollection, [$text], $limit, $where);
95559036814SCostin Stroie
95659036814SCostin Stroie            // Extract document IDs from results
95759036814SCostin Stroie            $documentIds = [];
95859036814SCostin Stroie            if (isset($results['ids'][0]) && is_array($results['ids'][0])) {
95959036814SCostin Stroie                foreach ($results['ids'][0] as $id) {
96059036814SCostin Stroie                    // Use the ChromaDB ID directly without conversion
96159036814SCostin Stroie                    $documentIds[] = $id;
96259036814SCostin Stroie                }
96359036814SCostin Stroie            }
96459036814SCostin Stroie
96559036814SCostin Stroie            return $documentIds;
96659036814SCostin Stroie        } catch (Exception $e) {
96759036814SCostin Stroie            // Log error but don't fail the operation
96859036814SCostin Stroie            error_log('ChromaDB query failed: ' . $e->getMessage());
96959036814SCostin Stroie            return [];
97059036814SCostin Stroie        }
97159036814SCostin Stroie    }
97259036814SCostin Stroie
97359036814SCostin Stroie    /**
97459036814SCostin Stroie     * Query ChromaDB for relevant documents and return text snippets
97559036814SCostin Stroie     *
97659036814SCostin Stroie     * Generates embeddings for the input text and queries ChromaDB for similar documents.
97759036814SCostin Stroie     * Returns the actual text snippets instead of document IDs.
97859036814SCostin Stroie     *
97959036814SCostin Stroie     * @param string $text The text to find similar documents for
98059036814SCostin Stroie     * @param int $limit Maximum number of documents to retrieve (default: 10)
98159036814SCostin Stroie     * @param array|null $where Optional filter conditions for metadata
98259036814SCostin Stroie     * @return array List of text snippets
98359036814SCostin Stroie     */
98459036814SCostin Stroie    private function queryChromaDBSnippets($text, $limit = 10, $where = null)
98559036814SCostin Stroie    {
98659036814SCostin Stroie        try {
98759036814SCostin Stroie            // Get ChromaDB client and collection name
98859036814SCostin Stroie            list($chromaClient, $chromaCollection) = $this->getChromaDBClient();
98959036814SCostin Stroie            // Query for similar documents
99059036814SCostin Stroie            $results = $chromaClient->queryCollection($chromaCollection, [$text], $limit, $where);
99159036814SCostin Stroie
99259036814SCostin Stroie            // Extract document texts from results
99359036814SCostin Stroie            $snippets = [];
99459036814SCostin Stroie            if (isset($results['documents'][0]) && is_array($results['documents'][0])) {
99559036814SCostin Stroie                foreach ($results['documents'][0] as $document) {
99659036814SCostin Stroie                    $snippets[] = $document;
99759036814SCostin Stroie                }
99859036814SCostin Stroie            }
99959036814SCostin Stroie
100059036814SCostin Stroie            return $snippets;
100159036814SCostin Stroie        } catch (Exception $e) {
100259036814SCostin Stroie            // Log error but don't fail the operation
100359036814SCostin Stroie            error_log('ChromaDB query failed: ' . $e->getMessage());
100459036814SCostin Stroie            return [];
100559036814SCostin Stroie        }
100659036814SCostin Stroie    }
100759036814SCostin Stroie
100859036814SCostin Stroie    /**
100959036814SCostin Stroie     * Query ChromaDB for a template document
101059036814SCostin Stroie     *
101159036814SCostin Stroie     * Generates embeddings for the input text and queries ChromaDB for a template document
101259036814SCostin Stroie     * by filtering with metadata 'template=true'.
101359036814SCostin Stroie     *
101459036814SCostin Stroie     * @param string $text The text to find a template for
101559036814SCostin Stroie     * @return array List of template document IDs (maximum 1)
101659036814SCostin Stroie     */
101759036814SCostin Stroie    public function queryChromaDBTemplate($text)
101859036814SCostin Stroie    {
101959036814SCostin Stroie        $templateIds = $this->queryChromaDB($text, 1, ['type' => 'template']);
102059036814SCostin Stroie
102159036814SCostin Stroie        // Remove chunk number (e.g., "@2") from the ID to get the base document ID
102259036814SCostin Stroie        if (!empty($templateIds)) {
102359036814SCostin Stroie            $templateIds[0] = preg_replace('/@\\d+$/', '', $templateIds[0]);
102459036814SCostin Stroie        }
102559036814SCostin Stroie
102659036814SCostin Stroie        return $templateIds;
102759036814SCostin Stroie    }
102859036814SCostin Stroie
102959036814SCostin Stroie}
1030