├── assets ├── mcp-logo-wide.png └── mcp-settings.css ├── .gitignore ├── src └── Convo │ └── Gpt │ ├── ContextLengthExceededException.php │ ├── RefuseFunctionCallException.php │ ├── Pckg │ ├── GptPackageDefinition.json │ ├── Help │ │ ├── actions-prompt-element.html │ │ ├── conversation-messages-element.md │ │ ├── embeddings-element.md │ │ ├── chat-completion-element.md │ │ ├── group-system-messages-element.md │ │ ├── system-message-element.md │ │ ├── moderation-api-element.md │ │ └── simple-message-limiter-element.md │ ├── ConversationMessagesElement.php │ ├── SystemMessageElement.php │ ├── ModerationApiElement.php │ ├── MessagesLimiterElement.php │ ├── EmbeddingsElement.php │ ├── SimpleMcpPromptTemplate.php │ ├── ChatCompletionElement.php │ ├── mcp-server-project.template.json │ ├── SystemMessageGroupElement.php │ ├── ExternalChatFunctionElement.php │ ├── ChatFunctionElement.php │ ├── SimpleMessagesLimiterElement.php │ └── WpRestProxyFunction.php │ ├── IMessages.php │ ├── IChatFunctionContainer.php │ ├── GptApiFactory.php │ ├── IChatFunction.php │ ├── Tools │ ├── AbstractRestFunctions.php │ ├── SettingsRestFunctions.php │ ├── TaxonomyRestFunctions.php │ ├── PluginRestFunctions.php │ ├── MediaRestFunctions.php │ ├── PagesRestFunctions.php │ ├── CommentRestFunctions.php │ ├── UserRestFunctions.php │ └── PostRestFunctions.php │ ├── Mcp │ ├── McpSessionManagerFactory.php │ ├── McpServerPublisher.php │ ├── StreamWriter.php │ ├── SseResponse.php │ ├── McpServerPlatform.php │ ├── IMcpSessionStoreInterface.php │ ├── StreamHandler.php │ ├── McpSessionManager.php │ ├── McpServerCommandRequest.php │ ├── McpFilesystemSessionStore.php │ └── CommandDispatcher.php │ ├── FunctionResultTooLargeException.php │ ├── GptPlugin.php │ ├── Admin │ ├── SettingsViewModel.php │ ├── SettingsProcessor.php │ └── McpConvoworksManager.php │ ├── GptApi.php │ └── PluginContext.php ├── phpunit.xml ├── update.json ├── package.json ├── .editorconfig ├── .vscode └── settings.json ├── phpstan.neon ├── composer.json ├── sync-version.js ├── phpstan-bootstrap.php ├── phpstan-stubs └── convoworks-wp.php ├── .github └── workflows │ ├── release.yml │ └── update-json.yml ├── tests ├── SummarizeMessagesTest.php └── ProcessJsonWithConstantsTest.php ├── convoworks-gpt.php ├── .phpstan-setup.md ├── .cursorrules └── CHANGELOG.md /assets/mcp-logo-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zef-dev/convoworks-gpt/HEAD/assets/mcp-logo-wide.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /dist/ 3 | /composer.lock 4 | /.phpunit.result.cache 5 | /.workspace/ 6 | /node_modules/ 7 | /package-lock.json 8 | -------------------------------------------------------------------------------- /src/Convo/Gpt/ContextLengthExceededException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /update.json: -------------------------------------------------------------------------------- 1 | { 2 | "convoworks-gpt/convoworks-gpt.php": { 3 | "version": "0.16.1", 4 | "package": "https://github.com/zef-dev/convoworks-gpt/releases/download/v0.16.1/convoworks-gpt-v0.16.1.zip", 5 | "requires": "5.0", 6 | "tested": "6.6", 7 | "last_updated": "2025-12-04" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/GptPackageDefinition.json: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "convo-gpt", 3 | "author": "ZEF Development", 4 | "email": "tihomir@convoworks.com", 5 | "source_for": [ 6 | "templates", "elements" 7 | ], 8 | "description": "Contains components for GPT", 9 | "stability": "stable" 10 | } 11 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/actions-prompt-element.html: -------------------------------------------------------------------------------- 1 |
2 |

Actions Prompt Generator

3 | 4 |

5 | This element will collect all defined actions and insert their prompts. 6 |

7 | 8 |

9 | You can use a local prompt title and content to improve specification intro. 10 |

11 | 12 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.16.1", 4 | "scripts": { 5 | "build" : "node sync-version.js && node build.js", 6 | "sync-version": "node sync-version.js" 7 | }, 8 | "devDependencies": { 9 | "archiver": "^5.3.2", 10 | "fs-extra": "^11.2.0", 11 | "yargs": "^17.0.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Convo/Gpt/IMessages.php: -------------------------------------------------------------------------------- 1 | =7.2.5" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Convo\\Gpt\\": "src/Convo/Gpt" 19 | } 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^8", 23 | "zef-dev/convoworks-core": "^0.22", 24 | "phpstan/phpstan": "*", 25 | "php-stubs/wordpress-stubs": "^6.9" 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Convo\\": "tests/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Convo/Gpt/GptApiFactory.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 26 | $this->_httpFactory = $httpFactory; 27 | } 28 | 29 | public function getApi($apiKey, $baseUrl = null) 30 | { 31 | return new GptApi($this->_logger, $this->_httpFactory, $apiKey, $baseUrl); 32 | } 33 | 34 | 35 | // UTIL 36 | public function __toString() 37 | { 38 | return get_class($this) . '[]'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Convo/Gpt/IChatFunction.php: -------------------------------------------------------------------------------- 1 | $val) { 17 | $request->set_param($key, $val); 18 | } 19 | } else { 20 | $request->set_body_params($data); 21 | } 22 | $response = rest_do_request($request); 23 | return json_encode($response->get_data()); 24 | }; 25 | } 26 | 27 | 28 | // UTIL 29 | public function __toString() 30 | { 31 | return get_class($this) . '[]'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sync-version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | // Read the version from package.json 4 | const packageJson = require('./package.json'); 5 | const version = packageJson.version; 6 | 7 | // Update composer.json 8 | const composerJsonPath = './composer.json'; 9 | const composerJson = require(composerJsonPath); 10 | composerJson.version = version; 11 | fs.writeFileSync(composerJsonPath, JSON.stringify(composerJson, null, 4), 'utf8'); 12 | 13 | // Update PHP file where the version is defined 14 | const phpFilePath = './convoworks-gpt.php'; 15 | let phpContent = fs.readFileSync(phpFilePath, 'utf8'); 16 | 17 | // Replace the version in the define() statement 18 | phpContent = phpContent.replace(/define\( 'CONVO_GPT_VERSION', '.*' \);/, `define( 'CONVO_GPT_VERSION', '${version}' );`); 19 | 20 | // Replace the version in the plugin header comment 21 | phpContent = phpContent.replace(/(\* Version:\s*)[0-9\.]+/, `$1${version}`); 22 | 23 | fs.writeFileSync(phpFilePath, phpContent, 'utf8'); 24 | 25 | console.log(`Version synchronized to ${version}`); 26 | -------------------------------------------------------------------------------- /assets/mcp-settings.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Convoworks MCP Server Settings Page Styles 3 | */ 4 | 5 | .settings-label { 6 | vertical-align: top; 7 | font-weight: bold; 8 | min-width: 100px; 9 | max-width: 200px; 10 | } 11 | 12 | .settings-input input.setting-text { 13 | width: 100%; 14 | } 15 | 16 | #special_role { 17 | width: 200px; 18 | display: block; 19 | margin-bottom: 10px; 20 | } 21 | 22 | #wrapbox { 23 | margin-right: 25px; 24 | } 25 | 26 | .settings-spacer { 27 | height: 7px; 28 | } 29 | 30 | .form-invalid.form-required textarea { 31 | border-color: #d63638 !important; 32 | box-shadow: 0 0 2px rgba(214, 54, 56, .8); 33 | } 34 | 35 | .settings-row .settings-input textarea, 36 | .settings-row .settings-input select { 37 | width: 100%; 38 | max-width: 100%; 39 | } 40 | 41 | .settings-helper { 42 | vertical-align: top; 43 | font-style: italic; 44 | min-width: 200px; 45 | max-width: 500px; 46 | line-height: 16px; 47 | } 48 | 49 | .settings-input { 50 | vertical-align: top; 51 | min-width: 100px; 52 | max-width: 300px; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /phpstan-bootstrap.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 27 | $this->_basePath = $basePath; 28 | } 29 | 30 | public function getSessionManager($serviceId): McpSessionManager 31 | { 32 | if (!isset($this->_instances[$serviceId])) { 33 | $this->_logger->debug("Creating new McpSessionManager for service: $serviceId"); 34 | $mcp_store = new McpFilesystemSessionStore($this->_logger, $this->_basePath, $serviceId); 35 | $this->_instances[$serviceId] = new McpSessionManager( 36 | $this->_logger, 37 | $mcp_store 38 | ); 39 | } 40 | return $this->_instances[$serviceId]; 41 | } 42 | 43 | // UTIL 44 | public function __toString() 45 | { 46 | return get_class($this) . '[]'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/McpServerPublisher.php: -------------------------------------------------------------------------------- 1 | _checkEnabled(); 32 | 33 | $this->_serviceReleaseManager->initDevelopmentRelease($this->_user, $this->_serviceId, $this->getPlatformId(), 'a'); 34 | } 35 | 36 | public function delete(array &$report) 37 | { 38 | $this->_serviceReleaseManager->withdrawPlatform($this->_user, $this->_serviceId, McpServerPlatform::PLATFORM_ID); 39 | } 40 | 41 | public function getStatus() 42 | { 43 | return ['status' => IPlatformPublisher::SERVICE_PROPAGATION_STATUS_FINISHED]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/ConversationMessagesElement.php: -------------------------------------------------------------------------------- 1 | _messages = $properties['messages']; 22 | } 23 | 24 | public function read(IConvoRequest $request, IConvoResponse $response) 25 | { 26 | /** @var \Convo\Gpt\IMessages $container */ 27 | $container = $this->findAncestor('\Convo\Gpt\IMessages'); 28 | 29 | $messages = $this->evaluateString($this->_messages); 30 | 31 | if (is_array($messages)) { 32 | $this->_logger->debug('Got loaded messages [' . count($messages) . ']'); 33 | foreach ($messages as $message) { 34 | $container->registerMessage($message); 35 | } 36 | } 37 | } 38 | 39 | // UTIL 40 | public function __toString() 41 | { 42 | return parent::__toString() . '[' . $this->_messages . ']'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/SettingsRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 15 | return $functions; 16 | }, 10, 2); 17 | } 18 | 19 | private function _buildFunctions() 20 | { 21 | return [ 22 | [ 23 | 'name' => 'get_settings', 24 | 'description' => 'Retrieve WordPress settings.', 25 | 'parameters' => [], 26 | 'defaults' => [], 27 | 'required' => [], 28 | 'execute' => $this->_makeRestFn('GET', 'settings') 29 | ], 30 | [ 31 | 'name' => 'update_settings', 32 | 'description' => 'Update WordPress settings.', 33 | 'parameters' => [ 34 | 'settings' => ['type' => 'array', 'description' => 'Settings key-value pairs'] 35 | ], 36 | 'defaults' => [], 37 | 'required' => ['settings'], 38 | 'execute' => function ($data) { 39 | return $this->_makeRestFn('POST', 'settings')($data['settings']); 40 | } 41 | ] 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/TaxonomyRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 15 | return $functions; 16 | }, 10, 2); 17 | } 18 | 19 | private function _buildFunctions() 20 | { 21 | return [ 22 | [ 23 | 'name' => 'list_taxonomies', 24 | 'description' => 'List WordPress taxonomies.', 25 | 'parameters' => [], 26 | 'defaults' => [], 27 | 'required' => [], 28 | 'execute' => $this->_makeRestFn('GET', 'taxonomies') 29 | ], 30 | [ 31 | 'name' => 'get_taxonomy', 32 | 'description' => 'Retrieve a taxonomy by slug.', 33 | 'parameters' => [ 34 | 'slug' => ['type' => 'string', 'description' => 'Taxonomy slug'] 35 | ], 36 | 'defaults' => [], 37 | 'required' => ['slug'], 38 | 'execute' => function ($data) { 39 | $fn = $this->_makeRestFn('GET', "taxonomies/{$data['slug']}"); 40 | return $fn($data); 41 | } 42 | ] 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /phpstan-stubs/convoworks-wp.php: -------------------------------------------------------------------------------- 1 | _content = $properties['content']; 23 | $this->_disableEval = $properties['disable_eval'] ?? ''; 24 | } 25 | 26 | public function read(IConvoRequest $request, IConvoResponse $response) 27 | { 28 | /** @var \Convo\Gpt\IMessages $container */ 29 | $container = $this->findAncestor('\Convo\Gpt\IMessages'); 30 | 31 | $disabled = boolval($this->evaluateString($this->_disableEval)); 32 | 33 | if ($disabled) { 34 | $container->registerMessage([ 35 | 'role' => 'system', 36 | 'transient' => true, 37 | 'content' => $this->_content 38 | ]); 39 | } else { 40 | $container->registerMessage([ 41 | 'role' => 'system', 42 | 'transient' => true, 43 | 'content' => $this->evaluateString($this->_content) 44 | ]); 45 | } 46 | } 47 | 48 | // UTIL 49 | public function __toString() 50 | { 51 | return parent::__toString() . '[' . $this->_content . ']'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Triggers the workflow when pushing a tag that starts with 'v' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: "14" 20 | 21 | - name: Install Node.js dependencies 22 | run: npm install 23 | 24 | - name: Sync version across files 25 | run: npm run sync-version 26 | 27 | - name: Set up PHP 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: "7.4" 31 | tools: composer 32 | 33 | - name: Install PHP dependencies 34 | run: composer install --no-dev --prefer-dist 35 | 36 | - name: Run build script 37 | run: npm run build 38 | 39 | - name: Debug - List files in build directory 40 | run: ls -al ./build 41 | 42 | - name: Debug - Check file existence 43 | run: | 44 | if [ ! -f "./build/convoworks-gpt-${{ github.ref_name }}.zip" ]; then 45 | echo "Error: File not found!" 46 | exit 1 47 | fi 48 | 49 | - name: Create GitHub Release 50 | uses: softprops/action-gh-release@v1 51 | with: 52 | files: ./build/convoworks-gpt-${{ github.ref_name }}.zip 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} # Use PAT_TOKEN instead of GITHUB_TOKEN 55 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/StreamWriter.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 23 | } 24 | 25 | /** 26 | * Sends HTTP headers for the stream. 27 | * 28 | * @param array $headers Associative array of header name => value. 29 | */ 30 | public function sendHeaders(array $headers): void 31 | { 32 | foreach ($headers as $name => $value) { 33 | header("$name: $value"); 34 | } 35 | flush(); 36 | $this->_logger->debug('Sent headers: ' . json_encode($headers)); 37 | } 38 | 39 | /** 40 | * Sends an SSE event. 41 | * 42 | * @param string $event Event name (e.g., 'message'). 43 | * @param string $data JSON-encoded data. 44 | */ 45 | public function sendEvent(string $event, string $data): void 46 | { 47 | echo "event: $event\n"; 48 | echo "data: $data\n\n"; 49 | flush(); 50 | $this->_logger->debug("Sent SSE event [$event]: $data"); 51 | } 52 | 53 | /** 54 | * Sends a raw message line for bidirectional streaming. 55 | * 56 | * @param string $json JSON-encoded message. 57 | */ 58 | public function sendMessage(string $json): void 59 | { 60 | echo $json . "\n"; 61 | flush(); 62 | $this->_logger->debug("Sent message: $json"); 63 | } 64 | 65 | /** 66 | * Sends a ping message. 67 | */ 68 | public function sendPing(): void 69 | { 70 | $ping = json_encode(['jsonrpc' => '2.0', 'method' => 'ping']); 71 | $this->sendMessage($ping); 72 | $this->_logger->debug('Sent ping'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/PluginRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 15 | return $functions; 16 | }, 10, 2); 17 | } 18 | 19 | private function _buildFunctions() 20 | { 21 | return [ 22 | [ 23 | 'name' => 'list_plugins', 24 | 'description' => 'List installed WordPress plugins.', 25 | 'parameters' => [], 26 | 'defaults' => [], 27 | 'required' => [], 28 | 'execute' => $this->_makeRestFn('GET', 'plugins') 29 | ], 30 | [ 31 | 'name' => 'activate_plugin', 32 | 'description' => 'Activate a plugin by slug.', 33 | 'parameters' => [ 34 | 'slug' => ['type' => 'string', 'description' => 'Plugin slug'] 35 | ], 36 | 'defaults' => [], 37 | 'required' => ['slug'], 38 | 'execute' => function ($data) { 39 | return $this->_makeRestFn('POST', "plugins/{$data['slug']}/activate")([]); 40 | } 41 | ], 42 | [ 43 | 'name' => 'deactivate_plugin', 44 | 'description' => 'Deactivate a plugin by slug.', 45 | 'parameters' => [ 46 | 'slug' => ['type' => 'string', 'description' => 'Plugin slug'] 47 | ], 48 | 'defaults' => [], 49 | 'required' => ['slug'], 50 | 'execute' => function ($data) { 51 | return $this->_makeRestFn('POST', "plugins/{$data['slug']}/deactivate")([]); 52 | } 53 | ] 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Convo/Gpt/FunctionResultTooLargeException.php: -------------------------------------------------------------------------------- 1 | _result = $result; 15 | } 16 | 17 | /** 18 | * Returns structured response including error message and result info: 19 | * - If JSON: includes 'structure' key with scanned structure 20 | * - Otherwise: includes 'preview' key with first 250 chars 21 | * 22 | * @return array 23 | */ 24 | public function getResponse() 25 | { 26 | if (!$this->_responseGenerated) { 27 | $this->_generateResponse(); 28 | } 29 | 30 | $response = [ 31 | 'error' => $this->getMessage() 32 | ]; 33 | 34 | if ($this->_structure !== null) { 35 | $response['structure'] = $this->_structure; 36 | } else { 37 | $response['preview'] = substr($this->_result, 0, 250); 38 | } 39 | 40 | return $response; 41 | } 42 | 43 | private function _generateResponse() 44 | { 45 | $this->_responseGenerated = true; 46 | 47 | // Try to parse as JSON 48 | $result_data = json_decode($this->_result, true); 49 | 50 | if (json_last_error() === JSON_ERROR_NONE) { 51 | // Successfully parsed as JSON - scan structure 52 | $this->_structure = Util::scanStructure($result_data); 53 | } 54 | // Otherwise structure stays null and getResponse() returns substring 55 | } 56 | 57 | /** 58 | * @deprecated Use getResponse() instead 59 | */ 60 | public function getStructure() : ?array 61 | { 62 | if (!$this->_responseGenerated) { 63 | $this->_generateResponse(); 64 | } 65 | return $this->_structure; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/SseResponse.php: -------------------------------------------------------------------------------- 1 | _sessionId = $sessionId; 25 | $this->_logger = new NullLogger(); 26 | } 27 | 28 | 29 | public function setPlatformResponse($data) 30 | { 31 | $this->_platformResponse = $data; 32 | } 33 | 34 | public function getPlatformResponse() 35 | { 36 | return is_array($this->_platformResponse) && empty($this->_platformResponse) 37 | ? new \stdClass() 38 | : $this->_platformResponse; 39 | } 40 | 41 | 42 | public function addText($text, $append = false): void {} 43 | 44 | 45 | public function getText() {} 46 | 47 | 48 | public function setShouldEndSession($shouldEndSession) 49 | { 50 | throw new \RuntimeException('Not implemented'); 51 | } 52 | 53 | public function shouldEndSession(): bool 54 | { 55 | throw new \RuntimeException('Not implemented'); 56 | } 57 | 58 | public function isEmpty(): bool 59 | { 60 | throw new \RuntimeException('Not implemented'); 61 | } 62 | 63 | public function isSsml(): bool 64 | { 65 | throw new \RuntimeException('Not implemented'); 66 | } 67 | 68 | public function addRepromptText($text, $append = false) {} 69 | 70 | public function getRepromptText() {} 71 | 72 | public function getTextSsml() 73 | { 74 | throw new \RuntimeException('Not implemented'); 75 | } 76 | 77 | public function getRepromptTextSsml() 78 | { 79 | throw new \RuntimeException('Not implemented'); 80 | } 81 | 82 | 83 | public function setLogger(LoggerInterface $logger) 84 | { 85 | $this->_logger = $logger; 86 | } 87 | 88 | // UTIL 89 | public function __toString() 90 | { 91 | return get_class($this) . '[' . $this->_sessionId . ']'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/McpServerPlatform.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 47 | $this->_publicHandler = $publicHandler; 48 | $this->_convoServiceDataProvider = $serviceDataProvider; 49 | $this->_serviceReleaseManager = $serviceReleaseManager; 50 | } 51 | 52 | public function getPlatformId() 53 | { 54 | return self::PLATFORM_ID; 55 | } 56 | 57 | public function getPublicRestHandler() 58 | { 59 | return $this->_publicHandler; 60 | } 61 | 62 | public function getPlatformPublisher(IAdminUser $user, $serviceId) 63 | { 64 | if (!isset($this->_platformPublisher)) { 65 | $this->_platformPublisher = new McpServerPublisher( 66 | $this->_logger, 67 | $user, 68 | $serviceId, 69 | $this->_convoServiceDataProvider, 70 | $this->_serviceReleaseManager 71 | ); 72 | } 73 | 74 | return $this->_platformPublisher; 75 | } 76 | 77 | // UTIL 78 | public function __toString() 79 | { 80 | return get_class($this) . '[' . $this->getPlatformId() . ']'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/ModerationApiElement.php: -------------------------------------------------------------------------------- 1 | _gptApiFactory = $gptApiFactory; 32 | 33 | foreach ($properties['ok'] as $element) { 34 | $this->_ok[] = $element; 35 | $this->addChild($element); 36 | } 37 | } 38 | 39 | public function read(IConvoRequest $request, IConvoResponse $response) 40 | { 41 | $input = $this->evaluateString($this->_properties['input']); 42 | $api_key = $this->evaluateString($this->_properties['api_key']); 43 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null); 44 | 45 | $api = $this->_gptApiFactory->getApi($api_key, $base_url); 46 | $http_response = $api->moderations($this->_buildApiOptions($input)); 47 | 48 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this); 49 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), $http_response); 50 | 51 | foreach ($this->_ok as $elem) { 52 | $elem->read($request, $response); 53 | } 54 | } 55 | 56 | private function _buildApiOptions($input) 57 | { 58 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this); 59 | $options['input'] = $input; 60 | return $options; 61 | } 62 | 63 | // UTIL 64 | public function __toString() 65 | { 66 | return parent::__toString() . '[]'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/MessagesLimiterElement.php: -------------------------------------------------------------------------------- 1 | _gptApiFactory = $gptApiFactory; 22 | } 23 | 24 | protected function _truncateMessages($allMessages) 25 | { 26 | $messages = parent::_truncateMessages($allMessages); 27 | $truncated = Util::getTruncatedPart($allMessages, $messages); 28 | 29 | if ($truncated) { 30 | $this->_logger->debug('Truncated messages [' . print_r($truncated, true) . ']'); 31 | $summarized = $this->_sumarize($truncated); 32 | $messages = array_merge([['role' => 'system', 'content' => $summarized]], $messages); 33 | } 34 | return $messages; 35 | } 36 | 37 | private function _sumarize($conversation) 38 | { 39 | $messages = [[ 40 | 'role' => 'system', 41 | 'content' => $this->evaluateString($this->_properties['system_message']) 42 | ]]; 43 | 44 | $messages[] = [ 45 | 'role' => 'user', 46 | 'content' => Util::serializeGptMessages($conversation, true) 47 | ]; 48 | 49 | $api_key = $this->evaluateString($this->_properties['api_key']); 50 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null); 51 | 52 | $api = $this->_gptApiFactory->getApi($api_key, $base_url); 53 | 54 | $http_response = $api->chatCompletion($this->_buildApiOptions($messages)); 55 | 56 | return $http_response['choices'][0]['message']['content']; 57 | } 58 | 59 | private function _buildApiOptions($messages) 60 | { 61 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this); 62 | 63 | $options['messages'] = $messages; 64 | 65 | return $options; 66 | } 67 | 68 | // UTIL 69 | public function __toString() 70 | { 71 | return parent::__toString() . '[]'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/update-json.yml: -------------------------------------------------------------------------------- 1 | name: Update Plugin JSON and Sync Versions 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | update-json-and-sync-versions: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout main branch 13 | uses: actions/checkout@v3 14 | with: 15 | ref: main # Explicitly check out the main branch 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: "14" 21 | 22 | - name: Set up PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "7.4" 26 | 27 | - name: Install dependencies 28 | run: | 29 | npm install 30 | composer install --no-dev --prefer-dist 31 | 32 | - name: Sync version in files 33 | run: npm run sync-version # Assuming you have a script that syncs versions 34 | 35 | - name: Update JSON file 36 | run: | 37 | version="${{ github.event.release.tag_name }}" 38 | version="${version#v}" 39 | 40 | download_url="https://github.com/${{ github.repository }}/releases/download/${{ github.event.release.tag_name }}/convoworks-gpt-v${version}.zip" 41 | updated_date=$(date -u +"%Y-%m-%d") 42 | 43 | cat > update.json <_gptApiFactory = $gptApiFactory; 32 | 33 | foreach ($properties['ok'] as $element) { 34 | $this->_ok[] = $element; 35 | $this->addChild($element); 36 | } 37 | } 38 | 39 | public function read(IConvoRequest $request, IConvoResponse $response) 40 | { 41 | $input = $this->evaluateString($this->_properties['input']); 42 | 43 | $api_key = $this->evaluateString($this->_properties['api_key']); 44 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null); 45 | $api = $this->_gptApiFactory->getApi($api_key, $base_url); 46 | 47 | $this->_logger->debug('Got input ============'); 48 | $this->_logger->debug("\n" . print_r($input, true)); 49 | $this->_logger->debug('============'); 50 | 51 | $http_response = $api->embeddings($this->_buildApiOptions($input)); 52 | 53 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this); 54 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), $http_response); 55 | 56 | foreach ($this->_ok as $elem) { 57 | $elem->read($request, $response); 58 | } 59 | } 60 | 61 | private function _buildApiOptions($input) 62 | { 63 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this); 64 | $options['input'] = $input; 65 | return $options; 66 | } 67 | 68 | // UTIL 69 | public function __toString() 70 | { 71 | return parent::__toString() . '[]'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/SummarizeMessagesTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $result, "Failed asserting that serialized messages are as expected."); 16 | } 17 | 18 | public function shouldHandleWHenContentIsNull() 19 | { 20 | return [ 21 | [ 22 | 'messages' => [ 23 | [ 24 | 'role' => 'assistant', 25 | 'content' => null, 26 | 'tool_calls' => [ 27 | [ 28 | 'id' => 'call_glNresze0Hjxix79HKUOKTPi', 29 | 'type' => 'function', 30 | 'function' => [ 31 | 'name' => 'email_verify', 32 | 'arguments' => '{"email":"tole@zefdev.com","code":7921}', 33 | ], 34 | ], 35 | ], 36 | 'refusal' => '', 37 | ], 38 | [ 39 | 'role' => 'tool', 40 | 'tool_call_id' => 'call_glNresze0Hjxix79HKUOKTPi', 41 | 'content' => '{"verified" : true}', 42 | ], 43 | [ 44 | 'role' => 'assistant', 45 | 'content' => 'Your email tole@zefdev.com has been successfully verified. Now, let\'s schedule your demo. Could you please tell me your preferred date and time for the demo? Remember, demos can be scheduled between 9:00 AM and 4:30 PM during the workweek, and we need to avoid same-day or next-day bookings.', 46 | 'refusal' => '', 47 | ], 48 | ], 49 | 'expected' => "Assistant: Your email tole@zefdev.com has been successfully verified. Now, let's schedule your demo. Could you please tell me your preferred date and time for the demo? Remember, demos can be scheduled between 9:00 AM and 4:30 PM during the workweek, and we need to avoid same-day or next-day bookings.", 50 | ], 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/SimpleMcpPromptTemplate.php: -------------------------------------------------------------------------------- 1 | _name = $properties['name']; 26 | $this->_description = $properties['description']; 27 | $this->_prompt = $properties['prompt']; 28 | $this->_arguments = $properties['arguments']; 29 | } 30 | 31 | public function read(IConvoRequest $request, IConvoResponse $response) 32 | { 33 | try { 34 | /** @var McpServerProcessor $container */ 35 | $container = $this->findAncestor('\Convo\Gpt\Pckg\McpServerProcessor'); 36 | $container->registerPrompt([ 37 | 'name' => $this->evaluateString($this->_name), 38 | 'description' => $this->evaluateString($this->_description), 39 | 'arguments' => $this->getService()->evaluateArgs($this->_arguments, $this), 40 | 'template' => $this->_prompt 41 | ]); 42 | } catch (DataItemNotFoundException $e) { 43 | $this->_logger->warning('Failed to find ancestor McpServerProcessor: ' . $e->getMessage()); 44 | } 45 | 46 | // act as reguklar system message 47 | try { 48 | /** @var \Convo\Gpt\IMessages $container */ 49 | $container = $this->findAncestor('\Convo\Gpt\IMessages'); 50 | 51 | $container->registerMessage([ 52 | 'role' => 'system', 53 | 'transient' => true, 54 | 'content' => $this->evaluateString($this->_prompt) 55 | ]); 56 | } catch (DataItemNotFoundException $e) { 57 | $this->_logger->warning('Failed to find ancestor IMessages: ' . $e->getMessage()); 58 | } 59 | } 60 | 61 | // UTIL 62 | public function __toString() 63 | { 64 | return parent::__toString() . '[' . $this->_name . '][' . $this->_description . ']'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/IMcpSessionStoreInterface.php: -------------------------------------------------------------------------------- 1 | ..., 'data' => ...] 58 | * @return void 59 | */ 60 | public function queueEvent(string $sessionId, array $data): void; 61 | 62 | /** 63 | * Persists multiple events/messages for the session. 64 | * 65 | * @param string $sessionId 66 | * @param array $events 67 | * @return void 68 | */ 69 | public function queueEvents(string $sessionId, array $events): void; 70 | 71 | /** 72 | * Retrieves and removes the next queued message for the session. 73 | * 74 | * @param string $sessionId 75 | * @return array|null 76 | */ 77 | public function nextEvent(string $sessionId): ?array; 78 | 79 | /** 80 | * Deletes session files and folders that have been inactive for the given time (in seconds). 81 | * 82 | * @param int $inactiveTime Seconds of inactivity before deletion. 83 | * @return int Number of deleted sessions. 84 | */ 85 | public function cleanupInactiveSessions(int $inactiveTime): int; 86 | } 87 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/ChatCompletionElement.php: -------------------------------------------------------------------------------- 1 | _gptApiFactory = $gptApiFactory; 32 | 33 | foreach ($properties['ok'] as $element) { 34 | $this->_ok[] = $element; 35 | $this->addChild($element); 36 | } 37 | } 38 | 39 | public function read(IConvoRequest $request, IConvoResponse $response) 40 | { 41 | $system_message = $this->evaluateString($this->_properties['system_message']); 42 | $messages = $this->evaluateString($this->_properties['messages']); 43 | 44 | $messages = array_merge( 45 | [['role' => 'system', 'content' => $system_message]], 46 | $messages 47 | ); 48 | 49 | $api_key = $this->evaluateString($this->_properties['api_key']); 50 | $base_url = $this->evaluateString($this->_properties['base_url'] ?? null); 51 | $api = $this->_gptApiFactory->getApi($api_key, $base_url); 52 | 53 | $this->_logger->debug('Got messages ============'); 54 | $this->_logger->debug("\n" . json_encode($messages, JSON_PRETTY_PRINT)); 55 | $this->_logger->debug('============'); 56 | 57 | $http_response = $api->chatCompletion($this->_buildApiOptions($messages)); 58 | 59 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this); 60 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), $http_response); 61 | 62 | foreach ($this->_ok as $elem) { 63 | $elem->read($request, $response); 64 | } 65 | } 66 | 67 | private function _buildApiOptions($messages) 68 | { 69 | $options = $this->getService()->evaluateArgs($this->_properties['apiOptions'], $this); 70 | $options['messages'] = $messages; 71 | return $options; 72 | } 73 | 74 | // UTIL 75 | public function __toString() 76 | { 77 | return parent::__toString() . '[]'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/mcp-server-project.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_id": "mcp-server-project", 3 | "name": "MCP Server Project", 4 | "description": "Blank, MCP server starter template.", 5 | "service": { 6 | "convo_service_version": 40, 7 | "packages": [ 8 | "convo-core", 9 | "convo-gpt" 10 | ], 11 | "contexts": [], 12 | "variables": [], 13 | "preview_variables": [], 14 | "entities": [], 15 | "intents": [], 16 | "blocks": [ 17 | { 18 | "class": "\\Convo\\Pckg\\Core\\Elements\\SpecialRoleProcessorBlock", 19 | "namespace": "convo-core", 20 | "properties": { 21 | "block_id": "MCP_Server", 22 | "name": "MCP Server", 23 | "role": "mcp-server", 24 | "processors": [ 25 | { 26 | "class": "\\Convo\\Gpt\\Pckg\\McpServerProcessor", 27 | "namespace": "convo-gpt", 28 | "properties": { 29 | "name": "WP MCP Server", 30 | "version": "1.0", 31 | "tools": [], 32 | "_component_id": "6p0uigl8-lbqp-8if5-diuw-xwwvijqgfbtf" 33 | } 34 | } 35 | ], 36 | "failback": [], 37 | "_component_id": "b9th0mg2-ymeo-hhkt-orif-ozjcafiqs2a0" 38 | } 39 | }, 40 | { 41 | "class": "\\Convo\\Pckg\\Core\\Elements\\ConversationBlock", 42 | "namespace": "convo-core", 43 | "properties": { 44 | "role": "error_handler", 45 | "block_id": "Error_handler", 46 | "name": "Error handler", 47 | "pre_dispatch": [], 48 | "elements": [ 49 | { 50 | "class": "\\Convo\\Pckg\\Core\\Elements\\LogElement", 51 | "namespace": "convo-core", 52 | "properties": { 53 | "log_message": "${error.getMessage()}", 54 | "_component_id": "pqamtg9p-fdnj-tvdq-1ckm-3ynp5zznvg2n" 55 | } 56 | } 57 | ], 58 | "processors": [], 59 | "fallback": [], 60 | "_component_id": "701jr3fq-gior-sdop-w9or-cdgglgnvp1k8" 61 | } 62 | } 63 | ], 64 | "fragments": [], 65 | "properties": [], 66 | "configurations": [] 67 | } 68 | } -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/SystemMessageGroupElement.php: -------------------------------------------------------------------------------- 1 | _trimChildren = $properties['trim_children']; 28 | foreach ( $properties['message_provider'] as $element) { 29 | $this->_messagesDefinition[] = $element; 30 | $this->addChild($element); 31 | } 32 | } 33 | 34 | public function read( IConvoRequest $request, IConvoResponse $response) 35 | { 36 | 37 | // collect inner messages first 38 | $this->_messages = []; 39 | 40 | foreach ( $this->_messagesDefinition as $elem) { 41 | $elem->read( $request, $response); 42 | } 43 | 44 | /** @var \Convo\Gpt\IMessages $container */ 45 | $container = $this->findAncestor( '\Convo\Gpt\IMessages'); 46 | 47 | $container->registerMessage( $this->_getCompleteMessage()); 48 | } 49 | 50 | /** 51 | * @param array $message 52 | */ 53 | public function registerMessage( $message) { 54 | $this->_messages[] = $message; 55 | } 56 | 57 | /** 58 | * Returns all messages 59 | * @return array 60 | */ 61 | public function getMessages() { 62 | return [$this->_getCompleteMessage()]; 63 | } 64 | 65 | private function _getCompleteMessage() { 66 | return [ 67 | 'role' => 'system', 68 | 'transient' => true, 69 | 'content' => $this->_getCompleteContent() 70 | ]; 71 | } 72 | 73 | 74 | private function _getCompleteContent() { 75 | $content = ''; 76 | $trim = $this->evaluateString( $this->_trimChildren); 77 | if ( $trim) { 78 | foreach ( $this->_messages as $message) { 79 | $content .= trim( $message['content']); 80 | } 81 | } else { 82 | foreach ( $this->_messages as $message) { 83 | $content .= "\n\n".$message['content']; 84 | } 85 | } 86 | 87 | return $content; 88 | } 89 | 90 | 91 | // UTIL 92 | public function __toString() 93 | { 94 | return parent::__toString().'['.$this->_trimChildren.']['.count( $this->_messagesDefinition).']'; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /convoworks-gpt.php: -------------------------------------------------------------------------------- 1 | register(); 54 | } 55 | run_convoworks_gpt_plugin(); 56 | 57 | function convoworks_gpt_check_for_updates($update, $plugin_data, $plugin_file) 58 | { 59 | static $response = false; 60 | 61 | if (empty($plugin_data['UpdateURI']) || !empty($update)) { 62 | return $update; 63 | } 64 | 65 | if ($response === false) { 66 | $response = wp_remote_get($plugin_data['UpdateURI']); 67 | } 68 | 69 | if (is_a($response, 'WP_Error')) { 70 | /** @var WP_Error $response */ 71 | error_log('Error updating plugin [Convoworks GPT]: ' . implode("\n", $response->get_error_messages())); 72 | return $update; 73 | } 74 | 75 | if (empty($response['body'])) { 76 | return $update; 77 | } 78 | 79 | $custom_plugins_data = json_decode($response['body'], true); 80 | 81 | if (!empty($custom_plugins_data[$plugin_file])) { 82 | $custom_data = $custom_plugins_data[$plugin_file]; 83 | $custom_data['slug'] = $plugin_file; // Add slug property here 84 | return (object) $custom_data; 85 | } else { 86 | return $update; 87 | } 88 | } 89 | add_filter('update_plugins_raw.githubusercontent.com', 'convoworks_gpt_check_for_updates', 10, 3); 90 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/MediaRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 15 | return $functions; 16 | }, 10, 2); 17 | } 18 | 19 | private function _buildFunctions() 20 | { 21 | return [ 22 | [ 23 | 'name' => 'list_media', 24 | 'description' => 'List WordPress media items with filters.', 25 | 'parameters' => [ 26 | 'per_page' => ['type' => 'number', 'description' => 'Media items per page', 'default' => 10], 27 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1] 28 | ], 29 | 'defaults' => ['per_page' => 10, 'page' => 1], 30 | 'required' => [], 31 | 'execute' => $this->_makeRestFn('GET', 'media') 32 | ], 33 | [ 34 | 'name' => 'get_media', 35 | 'description' => 'Retrieve a media item by ID.', 36 | 'parameters' => [ 37 | 'id' => ['type' => 'number', 'description' => 'Media ID'] 38 | ], 39 | 'defaults' => [], 40 | 'required' => ['id'], 41 | 'execute' => function ($data) { 42 | $fn = $this->_makeRestFn('GET', "media/{$data['id']}"); 43 | return $fn($data); 44 | } 45 | ], 46 | [ 47 | 'name' => 'create_media', 48 | 'description' => 'Upload a new media item.', 49 | 'parameters' => [ 50 | 'file' => ['type' => 'string', 'description' => 'File path or URL'], 51 | 'title' => ['type' => 'string', 'description' => 'Media title'] 52 | ], 53 | 'defaults' => [], 54 | 'required' => ['file'], 55 | 'execute' => $this->_makeRestFn('POST', 'media') 56 | ], 57 | [ 58 | 'name' => 'delete_media', 59 | 'description' => 'Delete a media item.', 60 | 'parameters' => [ 61 | 'id' => ['type' => 'number', 'description' => 'Media ID'], 62 | 'force' => ['type' => 'boolean', 'default' => true] 63 | ], 64 | 'defaults' => ['force' => true], 65 | 'required' => ['id'], 66 | 'execute' => function ($data) { 67 | return $this->_makeRestFn('DELETE', "media/{$data['id']}")(['force' => $data['force']]); 68 | } 69 | ] 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/StreamHandler.php: -------------------------------------------------------------------------------- 1 | _streamWriter = $streamWriter; 29 | $this->_logger = $logger; 30 | } 31 | 32 | /** 33 | * Starts and manages the SSE stream. 34 | * 35 | * @param string $sessionId 36 | * @param McpSessionManager $manager 37 | */ 38 | public function startSse(string $sessionId, McpSessionManager $manager): void 39 | { 40 | set_time_limit(0); 41 | ignore_user_abort(true); 42 | 43 | if (PHP_VERSION_ID >= 80000) { 44 | ob_implicit_flush(true); 45 | } else { 46 | ob_implicit_flush(1); 47 | } 48 | 49 | while (ob_get_level()) ob_end_clean(); 50 | 51 | $headers = [ 52 | 'Content-Type' => 'text/event-stream; charset=utf-8', 53 | 'Cache-Control' => 'no-cache', 54 | 'mcp-session-id' => $sessionId, 55 | 'MCP-Protocol-Version' => '2025-06-18' 56 | ]; 57 | $this->_streamWriter->sendHeaders($headers); 58 | 59 | $this->_streamWriter->sendEvent('message', ': connected'); 60 | 61 | $lastPing = time(); 62 | $lastSessionCheck = time(); 63 | 64 | while (!connection_aborted()) { 65 | if ((time() - $lastSessionCheck) >= 1) { 66 | try { 67 | $manager->getActiveSession($sessionId, true); 68 | } catch (DataItemNotFoundException $e) { 69 | $this->_logger->warning("Session check failed [$sessionId]: " . $e->getMessage()); 70 | break; 71 | } 72 | $lastSessionCheck = time(); 73 | } 74 | 75 | if ($message = $manager->getSessionStore()->nextEvent($sessionId)) { 76 | $data = is_string($message['data']) ? $message['data'] : json_encode($message['data']); 77 | $this->_streamWriter->sendEvent('message', $data); 78 | } 79 | 80 | if (CONVO_GPT_MCP_PING_INTERVAL && (time() - $lastPing) >= CONVO_GPT_MCP_PING_INTERVAL) { 81 | $this->_streamWriter->sendPing(); 82 | $manager->getSessionStore()->pingSession($sessionId); 83 | $lastPing = time(); 84 | } 85 | 86 | usleep(CONVO_GPT_MCP_LISTEN_USLEEP); 87 | } 88 | 89 | $this->_logger->info("SSE disconnected for session [$sessionId]"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/McpSessionManager.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 25 | $this->_sessionStore = $sessionStore; 26 | } 27 | 28 | // check if valid session 29 | public function getActiveSession($sessionId, $allowNew = false): array 30 | { 31 | $session = $this->_sessionStore->getSession($sessionId); 32 | $status_ok = [ 33 | IMcpSessionStoreInterface::SESSION_STATUS_INITIALISED, 34 | ]; 35 | if ($allowNew) { 36 | $status_ok[] = IMcpSessionStoreInterface::SESSION_STATUS_NEW; 37 | } 38 | if (!in_array($session['status'], $status_ok)) { 39 | throw new DataItemNotFoundException('No active session found [' . $allowNew . ']: ' . $sessionId); 40 | } 41 | if ($session['last_active'] < time() - CONVO_GPT_MCP_SESSION_TIMEOUT) { 42 | throw new DataItemNotFoundException('Session expired: ' . $sessionId); 43 | } 44 | return $session; 45 | } 46 | 47 | // creates new session and sets up stream headers 48 | public function startSession($clientName): string 49 | { 50 | $session_id = $this->_sessionStore->createSession($clientName); 51 | 52 | $this->_logger->info("New session started: $session_id"); 53 | 54 | return $session_id; 55 | } 56 | 57 | public function activateSession($sessionId): array 58 | { 59 | $session = $this->_sessionStore->getSession($sessionId); 60 | if ($session['status'] !== IMcpSessionStoreInterface::SESSION_STATUS_NEW) { 61 | throw new DataItemNotFoundException('No NEW session found: ' . $sessionId); 62 | } 63 | 64 | $this->_sessionStore->initialiseSession($sessionId); 65 | 66 | return $session; 67 | } 68 | 69 | public function terminateSession($sessionId): void 70 | { 71 | $session = $this->_sessionStore->getSession($sessionId); 72 | $session['status'] = IMcpSessionStoreInterface::SESSION_STATUS_TERMINATED; 73 | $session['last_active'] = time(); 74 | $this->_sessionStore->saveSession($session); 75 | $this->_logger->info("Session terminated: $sessionId"); 76 | } 77 | 78 | // queues the full JSON-RPC message 79 | public function enqueueMessage($sessionId, $message): void 80 | { 81 | $this->_sessionStore->queueEvent($sessionId, $message); 82 | } 83 | 84 | public function getSessionStore(): IMcpSessionStoreInterface 85 | { 86 | return $this->_sessionStore; 87 | } 88 | 89 | // UTIL 90 | public function __toString() 91 | { 92 | return get_class($this) . '[]'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.phpstan-setup.md: -------------------------------------------------------------------------------- 1 | # PHPStan WordPress Integration - Setup Complete ✅ 2 | 3 | ## What Was Fixed 4 | 5 | ### 1. WordPress Classes and Functions 6 | **Problem**: PHPStan didn't recognize WordPress classes like `WP_REST_Request`, `WP_REST_Response`, etc. 7 | 8 | **Solution**: Installed official WordPress stubs 9 | ```bash 10 | composer require --dev php-stubs/wordpress-stubs 11 | ``` 12 | 13 | ### 2. Plugin Constants 14 | **Problem**: PHPStan reported constants like `CONVO_GPT_URL`, `CONVO_GPT_MCP_SESSION_TIMEOUT` as not found. 15 | 16 | **Solution**: Created `phpstan-bootstrap.php` defining all plugin constants. 17 | 18 | ### 3. Convoworks WP Classes 19 | **Problem**: PHPStan didn't know about `Convo\Wp\AdminUser` class from convoworks-wp plugin. 20 | 21 | **Solution**: Created stub file `phpstan-stubs/convoworks-wp.php` with minimal interface implementation. 22 | 23 | ### 4. Wrong PHPDoc Type 24 | **Problem**: `WpRestProxyFunction.php` had incorrect namespace in PHPDoc (`Convo\Gpt\Pckg\WP_REST_Response`). 25 | 26 | **Solution**: Fixed to use global namespace `\WP_REST_Response`. 27 | 28 | ## Results 29 | 30 | - **Before**: 78 errors (many WordPress-related false positives) 31 | - **After**: 60 errors (real code issues to address) 32 | - **Improvement**: Eliminated 18 WordPress/infrastructure errors ✨ 33 | 34 | ## Remaining Errors 35 | 36 | The 60 remaining errors are legitimate code issues: 37 | - Missing return statements 38 | - Type mismatches 39 | - Dead code 40 | - Unused properties 41 | - Always-true/false conditions 42 | 43 | These are **real code quality issues** worth fixing, not false positives! 44 | 45 | ## Configuration Files 46 | 47 | ### `phpstan.neon` 48 | ```yaml 49 | parameters: 50 | level: 5 51 | paths: 52 | - src 53 | bootstrapFiles: 54 | - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php 55 | - phpstan-bootstrap.php 56 | - phpstan-stubs/convoworks-wp.php 57 | ``` 58 | 59 | ### `phpstan-bootstrap.php` 60 | Defines plugin constants for static analysis. 61 | 62 | ### `phpstan-stubs/convoworks-wp.php` 63 | Minimal stub for `Convo\Wp\AdminUser` class. 64 | 65 | ## Usage 66 | 67 | ```bash 68 | # Run PHPStan analysis 69 | vendor/bin/phpstan analyse 70 | 71 | # Run on specific file 72 | vendor/bin/phpstan analyse src/Convo/Gpt/Admin/ 73 | 74 | # With error baseline (to ignore existing errors) 75 | vendor/bin/phpstan analyse --generate-baseline 76 | ``` 77 | 78 | ## For Other WordPress Plugins 79 | 80 | If you have similar WordPress plugin projects, use the same approach: 81 | 82 | 1. **Install WordPress stubs**: 83 | ```bash 84 | composer require --dev php-stubs/wordpress-stubs 85 | ``` 86 | 87 | 2. **Add to phpstan.neon**: 88 | ```yaml 89 | parameters: 90 | bootstrapFiles: 91 | - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php 92 | ``` 93 | 94 | 3. **Create bootstrap file** for plugin constants 95 | 96 | 4. **Create stub files** for external dependencies (like convoworks-core, convoworks-wp) 97 | 98 | ## Next Steps 99 | 100 | To get to **zero errors**, consider: 101 | 102 | 1. Fix missing return statements 103 | 2. Add proper type hints where PHPStan detects issues 104 | 3. Remove dead code 105 | 4. Fix type mismatches 106 | 5. Consider using PHPStan baseline if some errors are intentional: 107 | ```bash 108 | vendor/bin/phpstan analyse --generate-baseline 109 | ``` 110 | 111 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/McpServerCommandRequest.php: -------------------------------------------------------------------------------- 1 | _serviceId = $serviceId; 25 | $this->_sessionId = $sessionId; 26 | $this->_requestId = $requestId; 27 | $this->_data = $data; 28 | $this->_role = $role; 29 | } 30 | 31 | public function getId() 32 | { 33 | return $this->_data['id'] ?? null; 34 | } 35 | 36 | public function getMethod() 37 | { 38 | return $this->_data['method'] ?? null; 39 | } 40 | 41 | public function getSpecialRole() 42 | { 43 | return $this->_role; 44 | } 45 | 46 | public function getServiceId() 47 | { 48 | return $this->_serviceId; 49 | } 50 | 51 | public function getPlatformId() 52 | { 53 | return McpServerPlatform::PLATFORM_ID; 54 | } 55 | 56 | public function getApplicationId() 57 | { 58 | return $this->getServiceId(); 59 | } 60 | 61 | public function getPlatformData() 62 | { 63 | return $this->_data; 64 | } 65 | 66 | public function getMediaTypeRequest(): string 67 | { 68 | return ''; 69 | } 70 | 71 | public function getText() 72 | { 73 | return ''; 74 | } 75 | 76 | public function isEmpty() 77 | { 78 | return false; 79 | } 80 | 81 | public function getInstallationId() 82 | { 83 | return $this->getServiceId(); 84 | } 85 | 86 | public function isMediaRequest() 87 | { 88 | return false; 89 | } 90 | 91 | public function isHealthCheck() 92 | { 93 | return false; 94 | } 95 | 96 | public function getIsCrossSessionCapable() 97 | { 98 | return true; 99 | } 100 | 101 | public function getAccessToken(): string 102 | { 103 | return ''; 104 | } 105 | 106 | public function isLaunchRequest() 107 | { 108 | return $this->isSessionStart(); 109 | } 110 | 111 | public function getDeviceId() 112 | { 113 | return $this->getServiceId(); 114 | } 115 | 116 | public function isSalesRequest() 117 | { 118 | return false; 119 | } 120 | 121 | public function isSessionEndRequest(): bool 122 | { 123 | return false; 124 | } 125 | 126 | public function isSessionStart() 127 | { 128 | return false; 129 | } 130 | 131 | public function getSessionId() 132 | { 133 | return $this->_sessionId; 134 | } 135 | 136 | public function getRequestId() 137 | { 138 | return $this->_requestId; 139 | } 140 | 141 | 142 | 143 | // UTIL 144 | public function __toString() 145 | { 146 | return get_class($this) . '[' . $this->getText() . '][' . $this->getServiceId() . '][' . $this->getRequestId() . '][' . $this->getSessionId() . '][' . $this->getSpecialRole() . ']'; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/PagesRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 15 | return $functions; 16 | }, 10, 2); 17 | } 18 | 19 | private function _buildFunctions() 20 | { 21 | return [ 22 | [ 23 | 'name' => 'list_pages', 24 | 'description' => 'List WordPress pages with filters.', 25 | 'parameters' => [ 26 | 'status' => ['type' => 'string', 'description' => 'Page status', 'default' => 'publish'], 27 | 'per_page' => ['type' => 'number', 'description' => 'Pages per page', 'default' => 10], 28 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1] 29 | ], 30 | 'defaults' => ['status' => 'publish', 'per_page' => 10, 'page' => 1], 31 | 'required' => [], 32 | 'execute' => $this->_makeRestFn('GET', 'pages') 33 | ], 34 | [ 35 | 'name' => 'get_page', 36 | 'description' => 'Retrieve a page by ID.', 37 | 'parameters' => [ 38 | 'id' => ['type' => 'number', 'description' => 'Page ID'] 39 | ], 40 | 'defaults' => [], 41 | 'required' => ['id'], 42 | 'execute' => function ($data) { 43 | return $this->_makeRestFn('GET', "pages/{$data['id']}")($data); 44 | } 45 | ], 46 | [ 47 | 'name' => 'create_page', 48 | 'description' => 'Create a new page.', 49 | 'parameters' => [ 50 | 'title' => ['type' => 'string', 'description' => 'Page title'], 51 | 'content' => ['type' => 'string', 'description' => 'Page content'], 52 | 'status' => ['type' => 'string', 'enum' => ['publish', 'draft'], 'default' => 'draft'] 53 | ], 54 | 'defaults' => ['status' => 'draft'], 55 | 'required' => ['title', 'content'], 56 | 'execute' => $this->_makeRestFn('POST', 'pages') 57 | ], 58 | [ 59 | 'name' => 'update_page', 60 | 'description' => 'Update an existing page.', 61 | 'parameters' => [ 62 | 'id' => ['type' => 'number', 'description' => 'Page ID'], 63 | 'title' => ['type' => 'string'], 64 | 'content' => ['type' => 'string'] 65 | ], 66 | 'defaults' => [], 67 | 'required' => ['id'], 68 | 'execute' => function ($data) { 69 | $id = $data['id']; 70 | unset($data['id']); 71 | return $this->_makeRestFn('POST', "pages/{$id}")($data); 72 | } 73 | ], 74 | [ 75 | 'name' => 'delete_page', 76 | 'description' => 'Delete a page.', 77 | 'parameters' => [ 78 | 'id' => ['type' => 'number', 'description' => 'Page ID'], 79 | 'force' => ['type' => 'boolean', 'default' => true] 80 | ], 81 | 'defaults' => ['force' => true], 82 | 'required' => ['id'], 83 | 'execute' => function ($data) { 84 | return $this->_makeRestFn('DELETE', "pages/{$data['id']}")(['force' => $data['force']]); 85 | } 86 | ] 87 | ]; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/CommentRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 15 | return $functions; 16 | }, 10, 2); 17 | } 18 | 19 | private function _buildFunctions() 20 | { 21 | return [ 22 | [ 23 | 'name' => 'list_comments', 24 | 'description' => 'List WordPress comments with filters.', 25 | 'parameters' => [ 26 | 'post' => ['type' => 'number', 'description' => 'Post ID'], 27 | 'per_page' => ['type' => 'number', 'description' => 'Comments per page', 'default' => 10], 28 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1] 29 | ], 30 | 'defaults' => ['per_page' => 10, 'page' => 1], 31 | 'required' => [], 32 | 'execute' => $this->_makeRestFn('GET', 'comments') 33 | ], 34 | [ 35 | 'name' => 'get_comment', 36 | 'description' => 'Retrieve a comment by ID.', 37 | 'parameters' => [ 38 | 'id' => ['type' => 'number', 'description' => 'Comment ID'] 39 | ], 40 | 'defaults' => [], 41 | 'required' => ['id'], 42 | 'execute' => function ($data) { 43 | $fn = $this->_makeRestFn('GET', "comments/{$data['id']}"); 44 | return $fn($data); 45 | } 46 | ], 47 | [ 48 | 'name' => 'create_comment', 49 | 'description' => 'Create a new comment.', 50 | 'parameters' => [ 51 | 'post' => ['type' => 'number', 'description' => 'Post ID'], 52 | 'content' => ['type' => 'string', 'description' => 'Comment content'], 53 | 'author' => ['type' => 'string', 'description' => 'Author name'], 54 | 'author_email' => ['type' => 'string', 'description' => 'Author email'] 55 | ], 56 | 'defaults' => [], 57 | 'required' => ['post', 'content', 'author', 'author_email'], 58 | 'execute' => $this->_makeRestFn('POST', 'comments') 59 | ], 60 | [ 61 | 'name' => 'update_comment', 62 | 'description' => 'Update an existing comment.', 63 | 'parameters' => [ 64 | 'id' => ['type' => 'number', 'description' => 'Comment ID'], 65 | 'content' => ['type' => 'string'] 66 | ], 67 | 'defaults' => [], 68 | 'required' => ['id'], 69 | 'execute' => function ($data) { 70 | $id = $data['id']; 71 | unset($data['id']); 72 | $fn = $this->_makeRestFn('POST', "comments/{$id}"); 73 | return $fn($data); 74 | } 75 | ], 76 | [ 77 | 'name' => 'delete_comment', 78 | 'description' => 'Delete a comment.', 79 | 'parameters' => [ 80 | 'id' => ['type' => 'number', 'description' => 'Comment ID'], 81 | 'force' => ['type' => 'boolean', 'default' => true] 82 | ], 83 | 'defaults' => ['force' => true], 84 | 'required' => ['id'], 85 | 'execute' => function ($data) { 86 | return $this->_makeRestFn('DELETE', "comments/{$data['id']}")(['force' => $data['force']]); 87 | } 88 | ] 89 | ]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/UserRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 15 | return $functions; 16 | }, 10, 2); 17 | } 18 | 19 | private function _buildFunctions() 20 | { 21 | return [ 22 | [ 23 | 'name' => 'list_users', 24 | 'description' => 'List WordPress users with filters.', 25 | 'parameters' => [ 26 | 'role' => ['type' => 'string', 'description' => 'User role', 'default' => 'subscriber'], 27 | 'per_page' => ['type' => 'number', 'description' => 'Users per page', 'default' => 10], 28 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1] 29 | ], 30 | 'defaults' => ['role' => 'subscriber', 'per_page' => 10, 'page' => 1], 31 | 'required' => [], 32 | 'execute' => $this->_makeRestFn('GET', 'users') 33 | ], 34 | [ 35 | 'name' => 'get_user', 36 | 'description' => 'Retrieve a user by ID.', 37 | 'parameters' => [ 38 | 'id' => ['type' => 'number', 'description' => 'User ID'] 39 | ], 40 | 'defaults' => [], 41 | 'required' => ['id'], 42 | 'execute' => function ($data) { 43 | $fn = $this->_makeRestFn('GET', "users/{$data['id']}"); 44 | return $fn($data); 45 | } 46 | ], 47 | [ 48 | 'name' => 'create_user', 49 | 'description' => 'Create a new user.', 50 | 'parameters' => [ 51 | 'username' => ['type' => 'string', 'description' => 'Username'], 52 | 'email' => ['type' => 'string', 'description' => 'User email'], 53 | 'password' => ['type' => 'string', 'description' => 'User password'], 54 | 'role' => ['type' => 'string', 'default' => 'subscriber'] 55 | ], 56 | 'defaults' => ['role' => 'subscriber'], 57 | 'required' => ['username', 'email', 'password'], 58 | 'execute' => $this->_makeRestFn('POST', 'users') 59 | ], 60 | [ 61 | 'name' => 'update_user', 62 | 'description' => 'Update an existing user.', 63 | 'parameters' => [ 64 | 'id' => ['type' => 'number', 'description' => 'User ID'], 65 | 'email' => ['type' => 'string'], 66 | 'role' => ['type' => 'string'] 67 | ], 68 | 'defaults' => [], 69 | 'required' => ['id'], 70 | 'execute' => function ($data) { 71 | $id = $data['id']; 72 | unset($data['id']); 73 | $fn = $this->_makeRestFn('POST', "users/{$id}"); 74 | return $fn($data); 75 | } 76 | ], 77 | [ 78 | 'name' => 'delete_user', 79 | 'description' => 'Delete a user.', 80 | 'parameters' => [ 81 | 'id' => ['type' => 'number', 'description' => 'User ID'], 82 | 'reassign' => ['type' => 'number', 'description' => 'Reassign posts to this user ID'] 83 | ], 84 | 'defaults' => [], 85 | 'required' => ['id'], 86 | 'execute' => function ($data) { 87 | return $this->_makeRestFn('DELETE', "users/{$data['id']}")(['reassign' => $data['reassign']]); 88 | } 89 | ] 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Convo/Gpt/GptPlugin.php: -------------------------------------------------------------------------------- 1 | _pluginContext)) { 33 | throw new \Exception('Not properly iinitilaized'); 34 | } 35 | return $this->_pluginContext; 36 | } 37 | 38 | public function register() 39 | { 40 | add_action('init', [$this, 'init']); 41 | } 42 | 43 | public function init() 44 | { 45 | if (!defined('CONVOWP_VERSION')) { 46 | error_log('GPT: Convoworks WP is not present. Exiting ...'); 47 | 48 | add_action('admin_notices', function () { 49 | echo '
50 |

Convoworks GPT requires Convoworks WP plugin to be installed and activated

51 |
'; 52 | }); 53 | return; 54 | } 55 | 56 | add_action('admin_init', [$this, 'adminInit']); 57 | 58 | add_action('admin_menu', function () { 59 | $context = $this->getPluginContext(); 60 | $settings_view = new SettingsView( 61 | $context->getLogger(), 62 | $context->getSettingsViewModel(), 63 | $context->getMcpConvoworksManager() 64 | ); 65 | $settings_view->register(); 66 | }, 20); 67 | $this->_pluginContext = new PluginContext(); 68 | $this->_pluginContext->init(); 69 | 70 | add_action('register_convoworks_package', [$this, 'gptPackageRegister'], 10, 2); 71 | 72 | $posts = new PostRestFunctions(); 73 | $posts->register(); 74 | 75 | $pages = new PagesRestFunctions(); 76 | $pages->register(); 77 | 78 | $comments = new CommentRestFunctions(); 79 | $comments->register(); 80 | 81 | $users = new UserRestFunctions(); 82 | $users->register(); 83 | 84 | $media = new MediaRestFunctions(); 85 | $media->register(); 86 | 87 | $plugins = new PluginRestFunctions(); 88 | $plugins->register(); 89 | 90 | $taxonomies = new TaxonomyRestFunctions(); 91 | $taxonomies->register(); 92 | 93 | $settings = new SettingsRestFunctions(); 94 | $settings->register(); 95 | } 96 | 97 | 98 | public function adminInit() 99 | { 100 | $context = $this->getPluginContext(); 101 | $logger = $context->getLogger(); 102 | 103 | // $logger->debug('Admin init ...'); 104 | 105 | if (!empty($_POST) && isset($_REQUEST['action'])) { 106 | $processor = new SettingsProcessor($logger, $context->getSettingsViewModel(), $context->getMcpConvoworksManager()); 107 | if ($processor->accepts()) { 108 | $logger->debug('Processing settings ...'); 109 | $processor->run(); 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * @param PackageProviderFactory $packageProviderFactory 116 | * @param ContainerInterface $container 117 | */ 118 | public function gptPackageRegister($packageProviderFactory, $container) 119 | { 120 | $packageProviderFactory->registerPackage($this->getPluginContext()->getMcpServerPackage()); 121 | } 122 | 123 | // UTIL 124 | public function __toString() 125 | { 126 | return get_class($this); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/ExternalChatFunctionElement.php: -------------------------------------------------------------------------------- 1 | _name = $properties['name']; 30 | $this->_description = $properties['description']; 31 | $this->_parameters = $properties['parameters']; 32 | $this->_defaults = $properties['defaults'] ?? []; 33 | $this->_required = $properties['required']; 34 | $this->_execute = $properties['execute']; 35 | } 36 | 37 | public function read(IConvoRequest $request, IConvoResponse $response) 38 | { 39 | /** @var \Convo\Gpt\IChatFunctionContainer $container */ 40 | $container = $this->findAncestor('\Convo\Gpt\IChatFunctionContainer'); 41 | 42 | $data = [ 43 | 'name' => $this->evaluateString($this->_name), 44 | 'description' => $this->evaluateString($this->_description), 45 | 'parameters' => $this->evaluateString($this->_parameters), 46 | 'defaults' => $this->evaluateString($this->_defaults), 47 | 'required' => $this->evaluateString($this->_required), 48 | 'execute' => $this->evaluateString($this->_execute), 49 | ]; 50 | 51 | $chat_function = new class($data, $this->getService()) extends AbstractScopedFunction implements IChatFunction { 52 | 53 | /** 54 | * @var ConvoServiceInstance 55 | */ 56 | private $_convoServiceInstance; 57 | private $_functionData; 58 | public function __construct($functionData, $service) 59 | { 60 | $this->_functionData = $functionData; 61 | $this->_convoServiceInstance = $service; 62 | } 63 | 64 | public function accepts($functionName) 65 | { 66 | return $functionName === $this->getName(); 67 | } 68 | 69 | public function getName() 70 | { 71 | return $this->_functionData['name']; 72 | } 73 | 74 | public function getDefinition() 75 | { 76 | // Return the definition of the function, as an array 77 | 78 | return [ 79 | 'name' => $this->getName(), 80 | 'description' => $this->_functionData['description'], 81 | 'parameters' => [ 82 | 'type' => 'object', 83 | 'properties' => empty($this->_functionData['parameters']) ? new \stdClass : $this->_functionData['parameters'], 84 | 'required' => $this->_functionData['required'] ?? [], 85 | ], 86 | ]; 87 | } 88 | 89 | public function execute(IConvoRequest $request, IConvoResponse $response, array $data) 90 | { 91 | 92 | // $data = json_decode($data, true); 93 | $data = array_merge($this->_functionData['defaults'], $data); 94 | $result = $this->_functionData['execute']($data); 95 | 96 | if (is_string($result)) { 97 | return $result; 98 | } 99 | return json_encode($result); 100 | } 101 | }; 102 | 103 | $container->registerFunction($chat_function); 104 | } 105 | 106 | 107 | // UTIL 108 | public function __toString() 109 | { 110 | return parent::__toString() . '[' . $this->_name . ']'; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/ProcessJsonWithConstantsTest.php: -------------------------------------------------------------------------------- 1 | \n Order allow,deny\n Deny from all\n\n", "FILE_APPEND"]', 50 | '["\/home\/fohovotaha8776\/web\/zealous-zebra-1ckjj.instawp.xyz\/public_html\/.htaccess","# Prevent access to debug.log\n\n Order allow,deny\n Deny from all\n<\/Files>\n",8]' 51 | ], 52 | [ 53 | '[\n \"C:\\\\xampp\\\\htdocs\\\\wp-test\\\\.htaccess\",\n \"# BEGIN WordPress\\n# The directives (lines) between \\\"BEGIN WordPress\\\" and \\\"END WordPress\\\" are\\n# dynamically generated, and should only be modified via WordPress filters.\\n# Any changes to the directives between these markers will be overwritten.\\n\\nRewriteEngine On\\nRewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\\nRewriteBase \/wp-test\/\\nRewriteRule ^index\\\\.php$ - [L]\\nRewriteCond %{REQUEST_FILENAME} !-f\\nRewriteCond %{REQUEST_FILENAME} !-d\\nRewriteRule . \/wp-test\/index.php [L]\\n<\/IfModule>\\n# END WordPress\\n\\n# BEGIN Restrict access to debug.log\\n\\nOrder allow,deny\\nDeny from all\\n<\/Files>\\n# END Restrict access to debug.log\\n\"\n ]', 54 | '[\n \"C:\\\\xampp\\\\htdocs\\\\wp-test\\\\.htaccess\",\n \"# BEGIN WordPress\\n# The directives (lines) between \\\"BEGIN WordPress\\\" and \\\"END WordPress\\\" are\\n# dynamically generated, and should only be modified via WordPress filters.\\n# Any changes to the directives between these markers will be overwritten.\\n\\nRewriteEngine On\\nRewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\\nRewriteBase \/wp-test\/\\nRewriteRule ^index\\\\.php$ - [L]\\nRewriteCond %{REQUEST_FILENAME} !-f\\nRewriteCond %{REQUEST_FILENAME} !-d\\nRewriteRule . \/wp-test\/index.php [L]\\n<\/IfModule>\\n# END WordPress\\n\\n# BEGIN Restrict access to debug.log\\n\\nOrder allow,deny\\nDeny from all\\n<\/Files>\\n# END Restrict access to debug.log\\n\"\n ]' 55 | ] 56 | ]; 57 | } 58 | 59 | /** 60 | * @dataProvider processDataProvider 61 | */ 62 | public function testProcessJson($inputJson, $expectedJson) 63 | { 64 | $result = Util::processJsonWithConstants($inputJson); 65 | 66 | $this->assertEquals($expectedJson, $result, "Failed asserting that two objects are equal."); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Admin/SettingsViewModel.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 32 | $this->_convoServiceDataProvider = $convoServiceDataProvider; 33 | } 34 | 35 | public function init() 36 | { 37 | $page = isset($_REQUEST['page']) ? sanitize_text_field(wp_unslash($_REQUEST['page'])) : ''; 38 | if ($page !== SettingsView::ID) { 39 | // $this->_logger->debug('Not convoworks mcp server call. Exiting ...'); 40 | return; 41 | } 42 | 43 | $this->_logger->info('Initializing convoworks mcp settings view model'); 44 | 45 | // Load basicAuth from config 46 | try { 47 | $svcId = $this->getSelectedServiceId(); 48 | $user = new \Convo\Wp\AdminUser(wp_get_current_user()); 49 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig( 50 | $user, 51 | $svcId, 52 | \Convo\Core\Publish\IPlatformPublisher::MAPPING_TYPE_DEVELOP 53 | ); 54 | $platformId = \Convo\Gpt\Mcp\McpServerPlatform::PLATFORM_ID; 55 | if (isset($config[$platformId]['basic_auth'])) { 56 | $this->_basicAuth = $config[$platformId]['basic_auth'] ? true : false; 57 | } 58 | } catch (\Throwable $e) { 59 | /** @phpstan-ignore-next-line */ 60 | $this->_logger->error($e); 61 | } 62 | } 63 | 64 | public function getSelectedServiceId() 65 | { 66 | if (isset($_REQUEST['service_id'])) { 67 | return sanitize_text_field(wp_unslash($_REQUEST['service_id'])); 68 | } 69 | throw new DataItemNotFoundException('No selected service found'); 70 | } 71 | 72 | public function hasServiceSelection() 73 | { 74 | try { 75 | $this->getSelectedServiceId(); 76 | return true; 77 | } catch (DataItemNotFoundException $e) { 78 | } 79 | return false; 80 | } 81 | 82 | public function getPageId() 83 | { 84 | return SettingsView::ID; 85 | } 86 | 87 | public function getBaseUrl($serviceId = null) 88 | { 89 | $args = [ 90 | 'page' => $this->getPageId(), 91 | ]; 92 | if ($serviceId) { 93 | $args['service_id'] = $serviceId; 94 | } 95 | $url = add_query_arg($args, admin_url('admin.php')); 96 | return $url; 97 | } 98 | 99 | public function getActionUrl($action, $params = []) 100 | { 101 | $url = add_query_arg(array_merge([ 102 | 'page' => $this->getPageId(), 103 | 'action' => $action, 104 | ], $params), admin_url('admin-post.php')); 105 | $url = wp_nonce_url($url, self::NONCE_ACTION, self::NONCE_NAME); 106 | return $url; 107 | } 108 | 109 | public function getBackToConvoworksUrl() 110 | { 111 | $url = add_query_arg([ 112 | 'page' => 'convo-plugin', 113 | ], admin_url('admin.php')); 114 | $url .= '#!/convoworks-editor/' . $this->getSelectedServiceId() . '/configuration/platforms'; 115 | return $url; 116 | } 117 | 118 | public function getFormUrl($params = []) 119 | { 120 | $url = add_query_arg(array_merge([ 121 | 'page' => $this->getPageId(), 122 | ], $params), admin_url('admin-post.php')); 123 | return $url; 124 | } 125 | 126 | public function getBasicAuth() 127 | { 128 | return $this->_basicAuth; 129 | } 130 | 131 | public function setBasicAuth($value) 132 | { 133 | $this->_basicAuth = $value ? true : false; 134 | } 135 | 136 | // UTIL 137 | public function __toString() 138 | { 139 | return get_class($this); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Tools/PostRestFunctions.php: -------------------------------------------------------------------------------- 1 | _buildFunctions()); 16 | return $functions; 17 | }, 10, 2); 18 | } 19 | 20 | private function _buildFunctions() 21 | { 22 | return [ 23 | [ 24 | 'name' => 'list_posts', 25 | 'description' => 'List WordPress posts with filters.', 26 | 'parameters' => [ 27 | 'status' => ['type' => 'string', 'description' => 'Post status', 'default' => 'publish'], 28 | 'per_page' => ['type' => 'number', 'description' => 'Posts per page', 'default' => 10], 29 | 'page' => ['type' => 'number', 'description' => 'Page number', 'default' => 1] 30 | ], 31 | 'defaults' => ['status' => 'publish', 'per_page' => 10, 'page' => 1], 32 | 'required' => [], 33 | 'execute' => $this->_makeRestFn('GET', 'posts') 34 | ], 35 | [ 36 | 'name' => 'get_post', 37 | 'description' => 'Retrieve a post by ID.', 38 | 'parameters' => [ 39 | 'id' => ['type' => 'number', 'description' => 'Post ID'] 40 | ], 41 | 'defaults' => [], 42 | 'required' => ['id'], 43 | 'execute' => function ($data) { 44 | $fn = $this->_makeRestFn('GET', "posts/{$data['id']}"); 45 | return $fn($data); 46 | } 47 | ], 48 | [ 49 | 'name' => 'create_post', 50 | 'description' => 'Create a new post.', 51 | 'parameters' => [ 52 | 'title' => ['type' => 'string', 'description' => 'Post title'], 53 | 'content' => ['type' => 'string', 'description' => 'Post content'], 54 | 'status' => ['type' => 'string', 'enum' => ['publish', 'draft'], 'default' => 'draft'] 55 | ], 56 | 'defaults' => ['status' => 'draft'], 57 | 'required' => ['title', 'content'], 58 | 'execute' => $this->_makeRestFn('POST', 'posts') 59 | ], 60 | [ 61 | 'name' => 'update_post', 62 | 'description' => 'Update an existing post.', 63 | 'parameters' => [ 64 | 'id' => ['type' => 'number', 'description' => 'Post ID'], 65 | 'title' => ['type' => 'string'], 66 | 'content' => ['type' => 'string'] 67 | ], 68 | 'defaults' => [], 69 | 'required' => ['id'], 70 | 'execute' => function ($data) { 71 | $id = $data['id']; 72 | unset($data['id']); 73 | $fn = $this->_makeRestFn('GET', "posts/{$id}"); 74 | return $fn($data); 75 | } 76 | ], 77 | [ 78 | 'name' => 'delete_post', 79 | 'description' => 'Delete a post.', 80 | 'parameters' => [ 81 | 'id' => ['type' => 'number', 'description' => 'Post ID'], 82 | 'force' => ['type' => 'boolean', 'default' => true] 83 | ], 84 | 'defaults' => ['force' => true], 85 | 'required' => ['id'], 86 | 'execute' => function ($data) { 87 | return $this->_makeRestFn('DELETE', "posts/{$data['id']}")(['force' => $data['force']]); 88 | } 89 | ], 90 | [ 91 | 'name' => 'search_posts', 92 | 'description' => 'Search posts by title or content.', 93 | 'parameters' => [ 94 | 'search' => ['type' => 'string', 'description' => 'Search query'], 95 | 'per_page' => ['type' => 'number', 'default' => 5] 96 | ], 97 | 'defaults' => ['per_page' => 5], 98 | 'required' => ['search'], 99 | 'execute' => $this->_makeRestFn('GET', 'posts') 100 | ] 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/ChatFunctionElement.php: -------------------------------------------------------------------------------- 1 | _name = $properties['name']; 35 | $this->_description = $properties['description']; 36 | $this->_parameters = $properties['parameters']; 37 | $this->_defaults = $properties['defaults'] ?? []; 38 | $this->_required = $properties['required']; 39 | $this->_requestData = $properties['request_data']; 40 | $this->_resultData = $properties['result_data']; 41 | 42 | foreach ($properties['ok'] as $element) { 43 | $this->_ok[] = $element; 44 | $this->addChild($element); 45 | } 46 | } 47 | 48 | public function read(IConvoRequest $request, IConvoResponse $response) 49 | { 50 | /** @var \Convo\Gpt\IChatFunctionContainer $container */ 51 | $container = $this->findAncestor('\Convo\Gpt\IChatFunctionContainer'); 52 | $container->registerFunction($this); 53 | } 54 | 55 | /** 56 | * @param IConvoRequest $request 57 | * @param IConvoResponse $response 58 | * @param array $data 59 | * @return string 60 | */ 61 | public function execute(IConvoRequest $request, IConvoResponse $response, array $data) 62 | { 63 | // $data = json_decode($data, true); 64 | // $error = json_last_error(); 65 | // if ($error !== JSON_ERROR_NONE) { 66 | // throw new \Exception('JSON parsing error: ' . json_last_error_msg()); 67 | // } 68 | $this->_logger->debug('Got data decoded [' . print_r($data, true) . ']'); 69 | $data = array_merge($this->_getDefaults(), $data); 70 | $this->_logger->info('Got data with defaults [' . print_r($data, true) . ']'); 71 | 72 | $params = $this->getFunctionParams(); 73 | $data_var = $this->evaluateString($this->_requestData); 74 | $params->setServiceParam($data_var, $data); 75 | 76 | $this->_logger->info('Executing function [' . $this->getName() . ']. Arguments available as [' . $data_var . ']'); 77 | 78 | foreach ($this->_ok as $elem) { 79 | $elem->read($request, $response); 80 | } 81 | 82 | // $params = $this->getService()->getServiceParams(IServiceParamsScope::SCOPE_TYPE_REQUEST); 83 | $result = $this->evaluateString($this->_resultData); 84 | if (is_callable($result)) { 85 | $this->_logger->info('Executing callable result'); 86 | $result = $result(); 87 | } 88 | 89 | if (\is_string($result)) { 90 | return $result; 91 | } 92 | return json_encode($result); 93 | } 94 | 95 | private function _getDefaults() 96 | { 97 | $defaults = $this->evaluateString($this->_defaults); 98 | if (\is_array($defaults)) { 99 | return $defaults; 100 | } 101 | return []; 102 | } 103 | 104 | public function accepts($functionName) 105 | { 106 | return $this->getName() === $functionName; 107 | } 108 | 109 | public function getName() 110 | { 111 | return $this->evaluateString($this->_name); 112 | } 113 | 114 | public function getDefinition() 115 | { 116 | $parameters = $this->getService()->evaluateArgs($this->_parameters, $this); 117 | if (empty($parameters)) { 118 | $parameters = new \stdClass(); 119 | } 120 | return [ 121 | 'name' => $this->getName(), 122 | 'description' => $this->evaluateString($this->_description), 123 | 'parameters' => [ 124 | 'type' => 'object', 125 | 'properties' => $parameters, 126 | 'required' => $this->evaluateString($this->_required), 127 | ], 128 | ]; 129 | } 130 | 131 | // UTIL 132 | public function __toString() 133 | { 134 | return parent::__toString() . '[' . $this->_name . ']'; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Convo/Gpt/GptApi.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 31 | $this->_httpFactory = $httpFactory; 32 | $this->_apiKey = $apiKey; 33 | $this->_baseUrl = empty($baseUrl) ? self::DEFAULT_BASE_URL : $baseUrl; 34 | $this->_logger->debug('GptApi created [' . $this->_baseUrl . ']'); 35 | } 36 | 37 | public function chatCompletion($data) 38 | { 39 | $apiUrl = $this->_baseUrl . '/chat/completions'; 40 | 41 | $this->_logger->info('Performing request to [' . $apiUrl . ']'); 42 | 43 | $headers = [ 44 | 'Authorization' => 'Bearer ' . $this->_apiKey, 45 | ]; 46 | 47 | $this->_logger->debug('Http request data [' . print_r($data, true) . ']'); 48 | 49 | $config = []; 50 | 51 | $client = $this->_httpFactory->getHttpClient($config); 52 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $apiUrl, $headers, $data); 53 | 54 | try { 55 | $response = $client->sendRequest($request); 56 | } catch (HttpClientException $e) { 57 | if ($e->getCode() === 400) { 58 | $response_data = json_decode($e->getMessage(), true); 59 | if (isset($response_data['error']['code']) && $response_data['error']['code'] === 'context_length_exceeded') { 60 | throw new ContextLengthExceededException($response_data['error']['message'], 0, $e); 61 | } 62 | } 63 | throw $e; 64 | } 65 | 66 | $response_data = json_decode($response->getBody()->getContents(), true); 67 | 68 | $this->_logger->debug('Http response data [' . print_r($response_data, true) . ']'); 69 | 70 | return $response_data; 71 | } 72 | 73 | 74 | public function embeddings($data) 75 | { 76 | $headers = [ 77 | 'Authorization' => 'Bearer ' . $this->_apiKey, 78 | ]; 79 | 80 | $this->_logger->debug('Http request data [' . print_r($data, true) . ']'); 81 | 82 | $config = []; 83 | 84 | $client = $this->_httpFactory->getHttpClient($config); 85 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $this->_baseUrl . '/embeddings', $headers, $data); 86 | 87 | $response = $client->sendRequest($request); 88 | 89 | $response_data = json_decode($response->getBody()->getContents(), true); 90 | 91 | $this->_logger->debug('Http response data [' . print_r($response_data, true) . ']'); 92 | 93 | return $response_data; 94 | } 95 | 96 | public function moderations($data) 97 | { 98 | $headers = [ 99 | 'Authorization' => 'Bearer ' . $this->_apiKey, 100 | ]; 101 | 102 | $this->_logger->debug('Http request data [' . print_r($data, true) . ']'); 103 | 104 | $config = []; 105 | 106 | $client = $this->_httpFactory->getHttpClient($config); 107 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $this->_baseUrl . '/moderations', $headers, $data); 108 | 109 | $response = $client->sendRequest($request); 110 | 111 | $response_data = json_decode($response->getBody()->getContents(), true); 112 | 113 | $this->_logger->debug('Http response data [' . print_r($response_data, true) . ']'); 114 | 115 | return $response_data; 116 | } 117 | 118 | public function createImage($data) 119 | { 120 | $headers = [ 121 | 'Authorization' => 'Bearer ' . $this->_apiKey, 122 | ]; 123 | 124 | $this->_logger->debug('Performing request'); 125 | 126 | $config = []; 127 | 128 | $client = $this->_httpFactory->getHttpClient($config); 129 | $request = $this->_httpFactory->buildRequest(IHttpFactory::METHOD_POST, $this->_baseUrl . '/images/generations', $headers, $data); 130 | 131 | $response = $client->sendRequest($request); 132 | 133 | $response_data = json_decode($response->getBody()->getContents(), true); 134 | 135 | $this->_logger->debug('Http response [' . $response->getStatusCode() . ']'); 136 | 137 | return $response_data; 138 | } 139 | 140 | // UTIL 141 | public function __toString() 142 | { 143 | return \get_class($this) . '[' . $this->_baseUrl . ']'; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/SimpleMessagesLimiterElement.php: -------------------------------------------------------------------------------- 1 | _messagesDefinition[] = $element; 38 | $this->addChild($element); 39 | } 40 | } 41 | 42 | if (isset($properties['truncated_flow'])) { 43 | foreach ($properties['truncated_flow'] as $element) { 44 | $this->_truncatedFlow[] = $element; 45 | $this->addChild($element); 46 | } 47 | } 48 | } 49 | 50 | public function registerMessage($message) 51 | { 52 | $this->_messages[] = $message; 53 | } 54 | 55 | public function getMessages() 56 | { 57 | return $this->_messages; 58 | } 59 | 60 | public function read(IConvoRequest $request, IConvoResponse $response) 61 | { 62 | // REGISTER MESSAGES 63 | $this->_messages = []; 64 | foreach ($this->_messagesDefinition as $elem) { 65 | $elem->read($request, $response); 66 | } 67 | 68 | // TRUNCATE 69 | $all_messages = $this->getMessages(); 70 | $this->_logger->debug('Checking messages count [' . count($all_messages) . ']'); 71 | 72 | $messages = $this->_truncateMessages($all_messages); 73 | $this->_logger->debug('Messages after truncation [' . count($messages) . ']'); 74 | 75 | $truncated = Util::getTruncatedPart($all_messages, $messages); 76 | 77 | // TRUNCATED FLOW 78 | if (\count($truncated)) { 79 | $this->_logger->debug('Got messages after truncation [' . print_r($messages, true) . ']. Executing truncated flow'); 80 | $params = $this->getService()->getComponentParams(IServiceParamsScope::SCOPE_TYPE_REQUEST, $this); 81 | $var_name = $this->evaluateString($this->_properties['result_var']); 82 | if ($var_name) { 83 | $params->setServiceParam($this->evaluateString($this->_properties['result_var']), [ 84 | 'messages' => $messages, 85 | 'truncated' => $truncated, 86 | ]); 87 | } 88 | 89 | foreach ($this->_truncatedFlow as $elem) { 90 | $elem->read($request, $response); 91 | } 92 | } else { 93 | $this->_logger->debug('No need tp truncate messages count [' . count($messages) . ']'); 94 | } 95 | 96 | // PASS MESSAGES TO THE UPPER LAYER 97 | /** @var \Convo\Gpt\IMessages $container */ 98 | $container = $this->findAncestor('\Convo\Gpt\IMessages'); 99 | 100 | foreach ($messages as $message) { 101 | $container->registerMessage($message); 102 | } 103 | } 104 | 105 | protected function _truncateMessages($messages) 106 | { 107 | if (isset($this->_properties['max_tokens']) && !empty($this->_properties['max_tokens'])) { 108 | $this->_logger->warning('Checking messages size in tokens [' . count($messages) . '][' . Util::estimateTokensForMessages($messages) . ']'); 109 | $messages = Util::truncateByTokens( 110 | $messages, 111 | intval($this->evaluateString($this->_properties['max_tokens'])), 112 | intval($this->evaluateString($this->_properties['truncate_to_tokens'])) 113 | ); 114 | } else { 115 | $this->_logger->warning('Compatibility mode. No max_tokens set, using max_count and truncate_to'); 116 | $messages = Util::truncate( 117 | $messages, 118 | intval($this->evaluateString($this->_properties['max_count'])), 119 | intval($this->evaluateString($this->_properties['truncate_to'])) 120 | ); 121 | } 122 | 123 | return $messages; 124 | } 125 | 126 | // UTIL 127 | public function __toString() 128 | { 129 | return parent::__toString() . '[]'; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Admin/SettingsProcessor.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 30 | $this->_viewModel = $viewModel; 31 | $this->_mcpManager = $mcpManager; 32 | } 33 | 34 | public function accepts() 35 | { 36 | $action = $this->getAction(); 37 | return in_array($action, [ 38 | self::ACTION_ENABLE, 39 | self::ACTION_UPDATE, // if you ever want to support updates 40 | self::ACTION_DISABLE, // ← now accepted 41 | ], true); 42 | } 43 | 44 | public function getAction() 45 | { 46 | if (isset($_REQUEST['action'])) { 47 | return sanitize_text_field(wp_unslash($_REQUEST['action'])); 48 | } 49 | return ''; 50 | } 51 | 52 | public function run() 53 | { 54 | // Verify user has permission 55 | if (!current_user_can('manage_convoworks')) { 56 | wp_die( 57 | esc_html__('You do not have sufficient permissions to access this page.', 'convoworks-gpt'), 58 | esc_html__('Permission Denied', 'convoworks-gpt'), 59 | ['response' => 403] 60 | ); 61 | } 62 | 63 | // Verify nonce 64 | if (!check_admin_referer(self::NONCE_ACTION, self::NONCE_NAME)) { 65 | wp_die( 66 | esc_html__('Security check failed. Please try again.', 'convoworks-gpt'), 67 | esc_html__('Security Error', 'convoworks-gpt'), 68 | ['response' => 403] 69 | ); 70 | } 71 | 72 | $svc = $this->_viewModel->getSelectedServiceId(); 73 | $this->_logger->info("Processing action [{$this->getAction()}] for service [{$svc}]"); 74 | 75 | try { 76 | switch ($this->getAction()) { 77 | case self::ACTION_ENABLE: 78 | $this->_processCreate($svc); 79 | $msg = __('MCP service configuration has been successfully created!', 'convoworks-gpt'); 80 | $type = 'success'; 81 | break; 82 | case self::ACTION_DISABLE: 83 | $this->_processDisable($svc); 84 | $msg = __('MCP service configuration has been successfully disabled!', 'convoworks-gpt'); 85 | $type = 'success'; 86 | break; 87 | case self::ACTION_UPDATE: 88 | $this->_processUpdate($svc); 89 | $msg = __('MCP service configuration has been successfully updated!', 'convoworks-gpt'); 90 | $type = 'success'; 91 | break; 92 | default: 93 | throw new \Exception('Unexpected action [' . $this->getAction() . ']'); 94 | } 95 | add_settings_error('convo_mcp_settings', 'convo_mcp_settings', $msg, $type); 96 | } catch (\Exception $e) { 97 | /** @phpstan-ignore-next-line */ 98 | $this->_logger->error($e); 99 | add_settings_error('convo_mcp_settings', 'convo_mcp_settings', $e->getMessage(), 'error'); 100 | } 101 | 102 | set_transient('convo_mcp_settings_errors', get_settings_errors('convo_mcp_settings'), 30); 103 | wp_safe_redirect($this->_viewModel->getBaseUrl($svc)); 104 | exit; 105 | } 106 | 107 | private function _processCreate($serviceId) 108 | { 109 | $this->_logger->info('Creating mcp service for [' . $serviceId . ']'); 110 | $basicAuth = isset($_POST['basic_auth']) && sanitize_text_field($_POST['basic_auth']) === '1'; 111 | $this->_mcpManager->enableMcp($serviceId, $basicAuth); 112 | } 113 | 114 | private function _processDisable($serviceId) 115 | { 116 | // update amazon config 117 | $this->_logger->info('Disabling mcp service for [' . $serviceId . ']'); 118 | $this->_mcpManager->disableMcp($serviceId); 119 | } 120 | 121 | private function _processUpdate($serviceId) 122 | { 123 | $this->_logger->info('Updating mcp service for [' . $serviceId . ']'); 124 | $basicAuth = isset($_POST['basic_auth']) && sanitize_text_field($_POST['basic_auth']) === '1'; 125 | $this->_mcpManager->updateMcp($serviceId, $basicAuth); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | # Convoworks GPT - Cursor Rules 2 | 3 | Always include the following documentation files in chat context when working on this project: 4 | 5 | - AGENTS.md (project overview and navigation) 6 | - README.md (user-facing documentation, build instructions, features) 7 | - CHANGELOG.md (version history and recent changes) 8 | 9 | When relevant to PHP framework work, also reference documentation from the parent Convoworks WP plugin: 10 | - ../convoworks-wp/AGENTS.md (overview) 11 | - ../convoworks-wp/src/AGENTS_CONVOWORKS_FWRK.md (PHP framework guide) 12 | 13 | ## Project-Specific Guidelines 14 | 15 | ### PHP Development 16 | 17 | - This plugin extends Convoworks WP – always verify the parent plugin is available 18 | - Main namespace is `Convo\Gpt` – all plugin code lives in `src/Convo/Gpt/` 19 | - Follow Convoworks framework conventions (see parent plugin docs) 20 | - Components must implement appropriate workflow interfaces from `Convo\Core\Workflow` 21 | - Use dependency injection – services are wired in Convoworks WP's DI container 22 | 23 | ### Component Help Files 24 | 25 | - Prefer Markdown (`.md`) for all new help files 26 | - Place help files in `src/Convo/Gpt/Pckg/Help/` 27 | - Follow naming convention: `component-name.html` or `component-name.md` 28 | - Reference Convoworks WP's component help guidelines for structure 29 | 30 | ### Version Management 31 | 32 | - Version is managed in `package.json` (single source of truth) 33 | - Run `npm run sync-version` after version changes to update `composer.json` and `convoworks-gpt.php` 34 | - Update `CHANGELOG.md` for all user-facing changes 35 | - Follow semantic versioning (major.minor.patch) 36 | 37 | ### Build & Distribution 38 | 39 | - No frontend bundles – this plugin has no JavaScript UI 40 | - **Local development**: No build needed, just `composer install` and activate 41 | - **Release builds**: Automated via GitHub Actions when tags are pushed 42 | - **Manual build** (if needed): `npm run build` creates distributable zip 43 | - Test built packages in a clean WordPress install with Convoworks WP 44 | - **GitHub Actions**: Push a tag (`git tag v0.16.2 && git push --tags`) to trigger automated release 45 | 46 | ### MCP Server Development 47 | 48 | - MCP-related code lives in `src/Convo/Gpt/Mcp/` 49 | - Session storage is currently filesystem-based (may migrate to DB later) 50 | - Streamable HTTP implementation follows MCP 2024-11-05 spec 51 | - WordPress REST API tools are registered via filters for lazy loading 52 | 53 | ### Testing & Code Quality 54 | 55 | - **PHPStan**: Run `vendor/bin/phpstan analyse` before committing to catch type errors 56 | - Level 5 configured in `phpstan.neon` 57 | - Ignores WordPress symbols (they're not available during analysis) 58 | - If too many errors, consider generating a baseline: `vendor/bin/phpstan analyse --generate-baseline` 59 | - **PHPUnit**: Run `vendor/bin/phpunit` to execute unit tests 60 | - Tests in `tests/` focus on utility functions (serialization, truncation, etc.) 61 | - Add tests for new utility functions 62 | - **Manual testing**: Use one of the provided service templates 63 | - Always test with Convoworks WP activated first 64 | - Test MCP endpoints with compatible clients (cLine, Claude Desktop with proxy, etc.) 65 | 66 | ### Code Style & Conventions 67 | 68 | - Follow PSR-4 autoloading (already configured in composer.json) 69 | - Use type hints where possible (PHP 7.2+ compatible) 70 | - Document public methods with PHPDoc blocks 71 | - Keep component classes focused – one responsibility per class 72 | - Use descriptive variable names (no single-letter vars except in loops) 73 | 74 | ## Common Pitfalls 75 | 76 | - Don't forget to register new components in `GptPackageDefinition::_initDefintions()` 77 | - Don't modify Convoworks core classes – extend and compose instead 78 | - Don't add frontend build steps – this plugin has no UI bundle 79 | - Don't bundle heavy PHP dependencies – keep the plugin lightweight 80 | - Don't forget to update help files when adding/changing component properties 81 | 82 | ## Quick Reference 83 | 84 | ### Key Files 85 | 86 | - `convoworks-gpt.php` – Plugin entry point, constants, WordPress hooks 87 | - `src/Convo/Gpt/GptPlugin.php` – Main plugin registration 88 | - `src/Convo/Gpt/Pckg/GptPackageDefinition.php` – Component package definition 89 | - `src/Convo/Gpt/Mcp/McpServerPlatform.php` – MCP platform adapter 90 | - `build.js` – Build script (creates distributable zip) 91 | - `sync-version.js` – Version synchronization script 92 | 93 | ### Common npm Commands 94 | 95 | - `npm install` – Install packaging dependencies 96 | - `npm run build` – Create distributable zip (usually done by GitHub Actions) 97 | - `npm run sync-version` – Sync version from package.json to other files 98 | - `npm run clean` – Remove all build artifacts 99 | 100 | ### Common Composer Commands 101 | 102 | - `composer install` – Install dev dependencies (for local testing) 103 | - `composer update` – Update dependencies 104 | - `vendor/bin/phpunit` – Run unit tests 105 | - `vendor/bin/phpstan analyse` – Run static analysis 106 | 107 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/conversation-messages-element.md: -------------------------------------------------------------------------------- 1 | ### Conversation Messages 2 | 3 | Manages and provides the entire conversation history to the **GPT Chat Completion API v2**, ensuring consistent context throughout the interaction. This element loads a stored conversation array and registers each message with the parent Chat Completion component. 4 | 5 | ### When to use 6 | 7 | Use **Conversation Messages** when you need to: 8 | 9 | - Load a conversation history stored in session or database 10 | - Provide the full message thread to GPT Chat Completion API v2 11 | - Maintain conversation context across multiple turns 12 | - Integrate with **GPT Chat Completion API v2**'s message provider flow 13 | 14 | This element must be used inside the **message_provider** flow of **GPT Chat Completion API v2**. 15 | 16 | ### Properties 17 | 18 | #### Messages 19 | 20 | Expression that evaluates to an array of conversation messages. Each message should be an object with `role` and `content` properties following the OpenAI chat format. 21 | 22 | Example values: 23 | 24 | - `${conversation}` – Load from session variable 25 | - `${get_conversation(user_id)}` – Load from custom function 26 | - `${[]}` – Start with empty conversation 27 | 28 | **Message format**: 29 | 30 | ```text 31 | ${[ 32 | {"role": "system", "content": "You are a helpful assistant."}, 33 | {"role": "user", "content": "Hello!"}, 34 | {"role": "assistant", "content": "Hi! How can I help you?"} 35 | ]} 36 | ``` 37 | 38 | ### Runtime behavior 39 | 40 | When the element executes: 41 | 42 | 1. The messages expression is evaluated 43 | 2. If the result is an array, each message is registered with the parent `IMessages` container (typically **GPT Chat Completion API v2**) 44 | 3. The registered messages become part of the conversation context sent to the API 45 | 46 | **Important**: This element does not modify or store messages – it only loads and registers them. Message persistence is handled separately (typically in the **New Message Flow** of Chat Completion v2). 47 | 48 | ### Integration with Chat Completion API v2 49 | 50 | **Conversation Messages** is typically used inside the **Messages** (message_provider) flow of **GPT Chat Completion API v2**: 51 | 52 | **Chat Completion v2 configuration**: 53 | 54 | - **Messages flow**: 55 | 1. **System Message**: `You are a helpful assistant for ${site_name}.` 56 | 2. **Conversation Messages**: `${conversation}` ← Loads conversation history 57 | 3. *[Optional]* **Messages Limiter** or **Simple Messages Limiter** to manage context length 58 | 59 | - **New Message Flow**: 60 | 1. **Set Param** (session scope): 61 | - Name: `conversation` 62 | - Value: `${array_merge(conversation, [status.last_message])}` ← Saves new messages 63 | 64 | This pattern ensures: 65 | - Messages are loaded before each API call 66 | - New messages (both from GPT and function calls) are appended to conversation 67 | - Full conversation history is maintained across turns 68 | 69 | ### Example 70 | 71 | **Basic conversation management**: 72 | 73 | **Service Variables** (Configuration → Variables): 74 | 75 | - `conversation` (default: `${[]}`) 76 | 77 | **Chat Completion v2 configuration**: 78 | 79 | **Messages flow**: 80 | 81 | 1. **System Message**: 82 | - Content: `You are a friendly customer support assistant. Today is ${date("l, F j, Y")}.` 83 | 84 | 2. **Conversation Messages**: 85 | - Messages: `${conversation}` 86 | 87 | 3. **Simple Messages Limiter**: 88 | - Max Tokens: `${8000}` 89 | - Truncate to Tokens: `${4000}` 90 | - Messages: (nested) **Conversation Messages** with `${conversation}` 91 | 92 | **New Message Flow**: 93 | 94 | 1. **Set Param**: 95 | - Scope: `session` 96 | - Name: `conversation` 97 | - Value: `${array_merge(conversation, [status.last_message])}` 98 | 99 | **OK Flow**: 100 | 101 | 1. **Text Response**: `${status.last_message.content}` 102 | 103 | ### Example: Database-backed conversation 104 | 105 | **Load from custom storage**: 106 | 107 | **Messages flow**: 108 | 109 | 1. **PHP Delegate**: 110 | - Code: Load conversation from database 111 | - Result variable: `db_conversation` 112 | 113 | 2. **Conversation Messages**: 114 | - Messages: `${db_conversation}` 115 | 116 | **New Message Flow**: 117 | 118 | 1. **PHP Delegate**: 119 | - Code: Save new message to database 120 | - Parameters: `${status.last_message}` 121 | 122 | ### Tips 123 | 124 | - Always initialize the conversation variable with an empty array `${[]}` in service configuration 125 | - Use session scope for the conversation variable to persist across requests 126 | - Wrap **Conversation Messages** inside a **Messages Limiter** or **Simple Messages Limiter** to prevent context length overflow 127 | - The messages array can come from session variables, database queries, API responses, or any expression that evaluates to an array 128 | - Each message object should have at minimum `role` and `content` properties 129 | - Optionally, messages can include `name` (for function calls), `tool_calls`, `tool_call_id`, or `transient` flags 130 | - If the expression evaluates to null or non-array, no messages are registered (no error) 131 | - Don't confuse with **System Message** – use **Conversation Messages** for loading multi-turn history, use **System Message** for single static/dynamic instructions 132 | - For debugging, log the messages count: `${count(conversation)}` to monitor conversation growth 133 | 134 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/embeddings-element.md: -------------------------------------------------------------------------------- 1 | ### GPT Embeddings API 2 | 3 | Wrapper for the OpenAI Embeddings API, enabling you to generate vector representations (embeddings) for text input. Embeddings are useful for semantic search, clustering, recommendations, and similarity comparisons. 4 | 5 | ### When to use 6 | 7 | Use **GPT Embeddings API** when you need to: 8 | 9 | - Create vector embeddings for semantic search in your content 10 | - Build recommendation systems based on text similarity 11 | - Cluster or categorize text content automatically 12 | - Compare semantic similarity between documents 13 | - Store vector representations for RAG (Retrieval-Augmented Generation) systems 14 | 15 | ### Properties 16 | 17 | #### Input 18 | 19 | The text input for which you want to create an embedding. Can be a string or an array of strings (depending on the model). 20 | 21 | **Preprocessing recommendation**: Clean up raw text using the `tokenize_string()` function to remove stop words and improve embedding quality: 22 | 23 | ```text 24 | ${tokenize_string(your_raw_text)} 25 | ``` 26 | 27 | Example values: 28 | 29 | - `${page_content}` – Content from a WordPress page 30 | - `${tokenize_string(post.content)}` – Tokenized post content 31 | - `The quick brown fox jumps over the lazy dog` – Literal string 32 | 33 | #### API key 34 | 35 | Your OpenAI API key. Store this in a service variable for security. 36 | 37 | Example: `${GPT_API_KEY}` 38 | 39 | #### Base URL 40 | 41 | Optional. Base URL for the API endpoint. If left blank, defaults to `https://api.openai.com/v1`. 42 | 43 | Use this to point to custom endpoints or OpenAI-compatible APIs. 44 | 45 | #### API options 46 | 47 | Configuration options for the Embeddings API: 48 | 49 | - **model** – The embedding model to use (e.g., `text-embedding-ada-002`, `text-embedding-3-small`, `text-embedding-3-large`) 50 | 51 | Default value: 52 | 53 | ```text 54 | model: text-embedding-ada-002 55 | ``` 56 | 57 | For all available options and models, see the [OpenAI Embeddings API Reference](https://platform.openai.com/docs/api-reference/embeddings). 58 | 59 | #### Result Variable Name 60 | 61 | The name of the variable that will store the complete embeddings API response. Default is `status`. 62 | 63 | #### OK flow 64 | 65 | Sub-flow executed after the API call completes successfully. The result variable will be available for use in this flow. 66 | 67 | ### Runtime behavior 68 | 69 | When the element executes: 70 | 71 | 1. The input text is evaluated 72 | 2. An API call is made to OpenAI Embeddings endpoint with the input and options 73 | 3. The complete response is stored in the result variable (request scope) 74 | 4. The OK flow is executed with the result variable available 75 | 76 | ### Accessing the embedding 77 | 78 | Inside the OK flow, access the embedding vector like this: 79 | 80 | ```text 81 | ${status.data[0].embedding} 82 | ``` 83 | 84 | The result variable contains: 85 | 86 | - `${status.data}` – Array of embedding objects 87 | - `${status.data[0].embedding}` – The actual embedding vector (array of floats) 88 | - `${status.data[0].index}` – Index of the embedding in the input array 89 | - `${status.usage}` – Token usage statistics 90 | 91 | The embedding vector is a numerical array (typically 1536 dimensions for `text-embedding-ada-002`, or configurable dimensions for newer models). 92 | 93 | ### Example 94 | 95 | **Building a semantic search index**: 96 | 97 | **Configuration**: 98 | 99 | - **Input**: `${tokenize_string(post.post_content)}` 100 | - **API key**: `${GPT_API_KEY}` 101 | - **API options**: 102 | - model: `text-embedding-3-small` 103 | - **Result Variable Name**: `embedding_result` 104 | 105 | **In the OK flow**, store the embedding: 106 | 107 | 1. Add **HTTP Request** to store in your vector database: 108 | - URL: `https://your-vector-db.com/api/embeddings` 109 | - Method: `POST` 110 | - Body: `${{"post_id": post.ID, "embedding": embedding_result.data[0].embedding}}` 111 | 112 | Or store in WordPress post meta: 113 | 114 | 1. Add **Set Meta** element: 115 | - Post ID: `${post.ID}` 116 | - Meta key: `_embedding_vector` 117 | - Meta value: `${json_encode(embedding_result.data[0].embedding)}` 118 | 119 | ### Example: Semantic similarity search 120 | 121 | **Find similar posts**: 122 | 123 | 1. Get embedding for search query using **GPT Embeddings API** 124 | 2. In OK flow, compare with stored embeddings using cosine similarity 125 | 3. Rank posts by similarity score 126 | 127 | ### Tips 128 | 129 | - Use `tokenize_string()` to preprocess text – removes stop words and improves embedding quality 130 | - For long documents, split into chunks and embed each chunk separately (embeddings have token limits) 131 | - Use `text-embedding-3-small` for cost-effective embeddings with good quality 132 | - Use `text-embedding-3-large` for highest quality when accuracy is critical 133 | - Store embeddings in a vector database (Pinecone, Weaviate, pgvector) for production semantic search 134 | - For WordPress-based vector search, store embeddings in post meta or a custom table 135 | - Calculate cosine similarity between embeddings to measure semantic similarity (dot product if vectors are normalized) 136 | - Batch multiple inputs in a single API call when possible to reduce costs 137 | - Monitor token usage through `${status.usage.total_tokens}` – embeddings are charged per token 138 | - Cache embeddings – they don't change unless the input text changes 139 | 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## Convoworks GPT WordPress plugin 3 | 4 | ### 0.16.1 - 2025-12-04 5 | 6 | * Added the ability to disable expression language evaluation for system prompts. 7 | * Updated the method for accessing the Convoworks WP plugin. 8 | 9 | ### 0.16.0 - 2025-08-23 - MCP 2025-06-18 10 | 11 | * Updated to MCP specification 2025-06-18 and Streamable HTTP. 12 | * Added support for basic authentication in MCP server, using WordPress application passwords. 13 | 14 | ### 0.15.0 - 2025-06-22 - Token-based limiters 15 | 16 | * Message limiter elements now use token-based limits. 17 | * You can now limit function call result size (in tokens). 18 | * Removed old, deprecated components. 19 | * Added `estimate_tokens(content)` function. 20 | 21 | ### 0.14.0 - 2025-04-25 - Session ping 22 | 23 | * Improved keep alive mechanism and check 24 | * Added missing MCP logo 25 | 26 | ### 0.13.2 - 2025-04-23 - Function scope fix 27 | 28 | * Initialize properly scoped function call 29 | 30 | ### 0.13.1 - 2025-04-22 - Menu fix 31 | 32 | * Remove MCP settings page from menu 33 | 34 | ### 0.13.0 - 2025-04-22 - MCP Server Support 35 | 36 | * Added MCP Server platform. 37 | * Added **MCP Server Example** service template. 38 | * Added MCP Processor for routing MCP requests. 39 | * Added WP REST Proxy Function for forwarding REST API calls. 40 | * Added Simple MCP Prompt Template. 41 | 42 | 43 | ### 0.12.2 - 2025-03-27 - Deep Research template 44 | 45 | * Added new service template - Deep Research Assistant. 46 | 47 | 48 | ### 0.12.1 - 2025-02-23 - Chat Functions Handling 49 | 50 | * Improved chat function execution with better error handling—now catches `Throwable`. 51 | * Used scoped functions as the base for all chat functions, enabling local function scope. 52 | 53 | 54 | ### 0.12.0 Preview fixes 55 | 56 | * Added function scope support to the Chat Function element 57 | 58 | ### 0.11.2 Preview fixes 59 | 60 | * Fixed not displayed help for the Conversation Messages Element. 61 | * Long System GPT messages are now trimmed (12 lines) when displayed in editor. 62 | * The Chat Function Element now has description displayed in editor. 63 | * Fixed message serialization when message content is null (tool call). 64 | 65 | ### 0.11.1 Truncate messages 66 | 67 | * Fixed the truncate messages function - grouping 68 | 69 | ### 0.11.0 Truncate messages 70 | 71 | * Fixed truncate messages functions to preserve message grouping 72 | * Message limiter elements now have truncated flow 73 | 74 | ### 0.10.0 Catchup with OpenAI API changes - tools 75 | 76 | * GPT API components now support base url parameter, enabling using other AI providers 77 | * Chat compčletion element now uses functions as part of tools definition 78 | * Added `unserialize_gpt_messages()` function for unserializing stringified conversation into associative array. 79 | 80 | ### 0.9.2 Updated Service Templates 81 | 82 | - Updated both service templates to take advantage of new features. 83 | - Revised documentation and help files. 84 | 85 | ### 0.9.1 Serialize messages function 86 | 87 | * Added `serialize_gpt_messages()` function for serialisation messages into readable string. 88 | 89 | ### 0.9.0 External gpt functions element 90 | 91 | * Added `ExternalChatFunctionElement` which allows registering GPT functions from 3rd party plugins 92 | 93 | ### 0.8.0 94 | 95 | * Added simple messages limiter 96 | * Chat completion now rebuilds context even adter function calls 97 | * Chat completion now has on a new message flow to more transparent messages handling 98 | * Added `split_text_into_chunks()` el function 99 | * Added `SystemMessageGroupElement` for grouping prompt parts into the single system message. 100 | 101 | ### 0.7.3 Regenerate context 102 | 103 | * In function call scenarios, prompt (and functions) are now rebuilt for each GPT API call. 104 | 105 | ### 0.7.2 JSON schema fix 106 | 107 | * Fixed `args` argument definition for the `call_user_func_array` in `gpt-site-assistant.template.json` 108 | 109 | ### 0.7.1 Arry syntax fix 110 | 111 | * Corrected extra comma issue in `ChatCompletionV2Element` which caused error on the PHP 7.2 112 | 113 | ### 0.7.0 Include Function Results in Summarization 114 | 115 | * Added the ability to include function results in the summarization process. 116 | * Introduced `${conversation}`, a conversation summary that can be utilized within the query generator prompt. 117 | 118 | ### 0.6.0 Improved Chat Functions Execution 119 | 120 | * Enhanced the execution of chat functions: Included fixes for JSON parsing, handled endless loops better, and improved error handling. 121 | * Optimized the trimming of conversation messages. 122 | 123 | ### 0.5.0 Chat functions support 124 | 125 | * New Chat GPT API component with support for functions 126 | * Added System Message and Message Limiter elements 127 | * Added Embeddings and Moderation API elements 128 | 129 | ### 0.4.0 Auto activated actions 130 | 131 | * Add ability to mark actions as auto activated (e.g. logged user info) 132 | * Add ability to use just main Chat app prompt - child prompts will be ignored. Useful when testing and tuning prompts. 133 | 134 | ### 0.3.0 Turbo Chat App 135 | 136 | * Add chat app which uses GPT-3.5-turbo API 137 | * Update service template with the Turbochat app example 138 | 139 | ### 0.2.0 Refactor Chat App 140 | 141 | * Refactor prompt & action interfaces 142 | * Add validation error element 143 | * Remove actions prompt element 144 | * Update service template with appointment scheduling 145 | 146 | ### 0.1.0 Initial version 147 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/chat-completion-element.md: -------------------------------------------------------------------------------- 1 | ### GPT Chat Completion API 2 | 3 | Allows you to execute OpenAI Chat Completion API calls and get chat completion responses. This is a simple wrapper element for making chat API calls with a system message and conversation history. 4 | 5 | ### When to use 6 | 7 | Use **GPT Chat Completion API** when you need to: 8 | 9 | - Make straightforward chat completion API calls with OpenAI 10 | - Quickly test GPT responses with a simple system prompt 11 | - Build basic conversational interfaces without function calling 12 | - Get chat completions without complex message management 13 | 14 | For more advanced scenarios with function calling, dynamic message providers, and better control over the conversation flow, consider using **GPT Chat Completion API v2** instead. 15 | 16 | ### Properties 17 | 18 | #### System message 19 | 20 | The main system prompt that will be automatically prepended to the conversation. This sets the behavior and context for the AI assistant. 21 | 22 | Example values: 23 | 24 | - `The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly. Today is ${date("l, F j, Y")}.` 25 | - `You are a customer support assistant for our e-commerce platform. Be professional and helpful.` 26 | 27 | The system message is evaluated as an expression, so you can use dynamic values and functions. 28 | 29 | #### Messages 30 | 31 | An array of message objects in the OpenAI chat format. Each message should have a `role` (user, system, or assistant) and `content` (the message text). 32 | 33 | Example format: 34 | 35 | ```text 36 | ${[ 37 | {"role": "user", "content": "Hello!"}, 38 | {"role": "assistant", "content": "Hi! How can I help you today?"}, 39 | {"role": "user", "content": "Tell me a joke."} 40 | ]} 41 | ``` 42 | 43 | **Important**: This element does not automatically manage message persistence through the session. You are responsible for storing and retrieving the conversation history from session variables. 44 | 45 | #### API key 46 | 47 | Your OpenAI API key. This should typically be stored in a service variable for security. 48 | 49 | Example: `${GPT_API_KEY}` 50 | 51 | #### Base URL 52 | 53 | Optional. Base URL for the API endpoint. If left blank, the default OpenAI API endpoint is used: `https://api.openai.com/v1` 54 | 55 | Use this to point to custom endpoints, proxy servers, or OpenAI-compatible APIs. 56 | 57 | #### API options 58 | 59 | Configuration parameters for the Chat Completion API. Common options include: 60 | 61 | - **model** – The model to use (e.g., `gpt-4o`, `gpt-4`, `gpt-3.5-turbo`) 62 | - **temperature** – Controls randomness (0.0 to 2.0). Lower values make output more focused and deterministic 63 | - **max_tokens** – Maximum number of tokens in the response 64 | 65 | Default values: 66 | 67 | ```text 68 | model: gpt-4o 69 | temperature: ${0.7} 70 | max_tokens: ${4096} 71 | ``` 72 | 73 | For all available options, see the [OpenAI Chat Completion API Reference](https://platform.openai.com/docs/api-reference/chat). 74 | 75 | #### Result Variable Name 76 | 77 | The name of the variable that will store the complete Chat Completion API response. Default is `status`. 78 | 79 | The response contains the full API response object, including choices, usage statistics, and metadata. 80 | 81 | #### OK flow 82 | 83 | Sub-flow executed after the API call completes successfully. The result variable will be available for use in this flow. 84 | 85 | ### Runtime behavior 86 | 87 | When the element executes: 88 | 89 | 1. The system message is evaluated and prepended to the messages array 90 | 2. An API call is made to OpenAI with the combined messages and options 91 | 3. The complete response is stored in the result variable (request scope) 92 | 4. The OK flow is executed with the result variable available 93 | 94 | ### Accessing the response 95 | 96 | Inside the OK flow, access the completion text like this: 97 | 98 | ```text 99 | ${status.choices[0]["message"]["content"]} 100 | ``` 101 | 102 | The result variable contains the complete API response structure: 103 | 104 | - `${status.choices}` – Array of completion choices 105 | - `${status.choices[0].message}` – The assistant's message object 106 | - `${status.choices[0].message.content}` – The actual response text 107 | - `${status.usage}` – Token usage statistics 108 | 109 | ### Example 110 | 111 | **Configuration**: 112 | 113 | - **System message**: `You are a helpful travel assistant. Provide concise information about destinations.` 114 | - **Messages**: `${conversation}` 115 | - **API key**: `${GPT_API_KEY}` 116 | - **API options**: 117 | - model: `gpt-4o` 118 | - temperature: `${0.5}` 119 | - max_tokens: `${1000}` 120 | - **Result Variable Name**: `gpt_response` 121 | 122 | **In the OK flow**, add a Text Response element: 123 | 124 | > ${gpt_response.choices[0]["message"]["content"]} 125 | 126 | ### Tips 127 | 128 | - Store your API key in a service variable (Configuration → Variables) instead of hardcoding it 129 | - Use lower temperature values (0.1-0.3) for more consistent, focused responses 130 | - Use higher temperature values (0.7-1.0) for more creative, varied responses 131 | - Always handle the response inside the OK flow to ensure the API call completed successfully 132 | - For production use with ongoing conversations, store the messages array in session scope and append to it with each turn 133 | - Consider using **GPT Chat Completion API v2** if you need function calling or more flexible message management 134 | - Monitor your token usage through `${status.usage.total_tokens}` to optimize costs 135 | 136 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Admin/McpConvoworksManager.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 39 | $this->_package = $package; 40 | $this->_convoServiceDataProvider = $convoServiceDataProvider; 41 | } 42 | 43 | public function isServiceEnabled($serviceId) 44 | { 45 | $user = new AdminUser(wp_get_current_user()); 46 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig( 47 | $user, 48 | $serviceId, 49 | IPlatformPublisher::MAPPING_TYPE_DEVELOP 50 | ); 51 | return isset($config[McpServerPlatform::PLATFORM_ID]); 52 | } 53 | 54 | public function getServiceName($serviceId) 55 | { 56 | $user = new AdminUser(wp_get_current_user()); 57 | $service = $this->_convoServiceDataProvider->getServiceData( 58 | $user, 59 | $serviceId, 60 | IPlatformPublisher::MAPPING_TYPE_DEVELOP 61 | ); 62 | return $service['name']; 63 | } 64 | 65 | public function enableMcp($serviceId, $basicAuth = false) 66 | { 67 | $this->_logger->info('Enabling service [' . $serviceId . ']'); 68 | 69 | $user = new AdminUser(wp_get_current_user()); 70 | $publisher = $this->_getMcpServerPublisher($user, $serviceId); 71 | 72 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig( 73 | $user, 74 | $serviceId, 75 | IPlatformPublisher::MAPPING_TYPE_DEVELOP 76 | ); 77 | 78 | $config[McpServerPlatform::PLATFORM_ID] = []; 79 | $config[McpServerPlatform::PLATFORM_ID]['time_created'] = time(); 80 | $config[McpServerPlatform::PLATFORM_ID]['time_updated'] = time(); 81 | $config[McpServerPlatform::PLATFORM_ID]['basic_auth'] = $basicAuth; 82 | $this->_convoServiceDataProvider->updateServicePlatformConfig($user, $serviceId, $config); 83 | $publisher->enable(); 84 | } 85 | 86 | public function updateMcp($serviceId, $basicAuth = false) 87 | { 88 | $this->_logger->info('Updating MCP config for service [' . $serviceId . ']'); 89 | $user = new AdminUser(wp_get_current_user()); 90 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig( 91 | $user, 92 | $serviceId, 93 | IPlatformPublisher::MAPPING_TYPE_DEVELOP 94 | ); 95 | $platformId = McpServerPlatform::PLATFORM_ID; 96 | if (!isset($config[$platformId])) { 97 | throw new NotFoundException('Service [' . $serviceId . '] config [' . $platformId . '] not found'); 98 | } 99 | $config[$platformId]['basic_auth'] = $basicAuth; 100 | $config[$platformId]['time_updated'] = time(); 101 | $this->_convoServiceDataProvider->updateServicePlatformConfig($user, $serviceId, $config); 102 | } 103 | 104 | public function disableMcp($serviceId) 105 | { 106 | $platformId = McpServerPlatform::PLATFORM_ID; 107 | $user = new AdminUser(wp_get_current_user()); 108 | $config = $this->_convoServiceDataProvider->getServicePlatformConfig( 109 | $user, 110 | $serviceId, 111 | IPlatformPublisher::MAPPING_TYPE_DEVELOP 112 | ); 113 | 114 | if (!isset($config[$platformId])) { 115 | throw new NotFoundException('Service [' . $serviceId . '] config [' . $platformId . '] not found'); 116 | } 117 | 118 | // RELEASES ? 119 | // PUBLISHER 120 | $publisher = $this->_getMcpServerPublisher($user, $serviceId); 121 | $report = []; 122 | $publisher->delete($report); 123 | $this->_logger->info('Platform delete report [' . print_r($report, true) . ']'); 124 | 125 | // CONFIG 126 | $this->_logger->info('Deleting configuration [' . $platformId . '] for service [' . $serviceId . ']'); 127 | unset($config[$platformId]); 128 | $this->_convoServiceDataProvider->updateServicePlatformConfig($user, $serviceId, $config); 129 | } 130 | 131 | /** 132 | * @param IAdminUser $user 133 | * @param string $serviceId 134 | * @return IPlatformPublisher 135 | */ 136 | private function _getMcpServerPublisher($user, $serviceId) 137 | { 138 | /** @var IPlatformProvider $package */ 139 | $package = $this->_package->getPackageInstance(); 140 | $platform = $package->getPlatform(McpServerPlatform::PLATFORM_ID); 141 | return $platform->getPlatformPublisher($user, $serviceId); 142 | } 143 | 144 | // UTIL 145 | public function __toString() 146 | { 147 | return get_class($this); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/group-system-messages-element.md: -------------------------------------------------------------------------------- 1 | ### Group System Messages 2 | 3 | Groups multiple child **System Message** elements into a single combined system message. This helps organize complex prompts and manage multiple system instructions more effectively. 4 | 5 | ### When to use 6 | 7 | Use **Group System Messages** when you need to: 8 | 9 | - Combine multiple system messages into one 10 | - Organize complex prompts with conditional or dynamic parts 11 | - Reduce the number of separate system messages in the conversation 12 | - Build system prompts from modular components 13 | - Control formatting of combined system instructions 14 | 15 | This element is typically used in the **Messages** (message_provider) flow of **GPT Chat Completion API v2**. 16 | 17 | ### Properties 18 | 19 | #### Trim Child Prompts (trim_children) 20 | 21 | Controls how child messages are joined together: 22 | 23 | - **false** (default): Child messages are separated with double newlines (`\n\n`) 24 | - **true**: Child messages are joined inline without spacing (trimmed) 25 | 26 | Use `true` for compact, inline formatting. Use `false` for better readability with paragraph breaks. 27 | 28 | #### Messages (message_provider) 29 | 30 | A sub-flow containing **System Message** elements that will be grouped. The sub-flow executes, collecting all child system messages, then combines them into a single system message. 31 | 32 | ### Runtime behavior 33 | 34 | When the element executes: 35 | 36 | 1. The message_provider sub-flow runs, collecting all **System Message** elements 37 | 2. The content of each child message is extracted 38 | 3. If `trim_children` is true, contents are joined inline (no spacing) 39 | 4. If `trim_children` is false, contents are separated with `\n\n` 40 | 5. A single combined system message is registered with the parent `IMessages` container 41 | 6. The combined message is marked as `transient: true` 42 | 43 | ### Message structure 44 | 45 | The registered message has this structure: 46 | 47 | ```text 48 | { 49 | "role": "system", 50 | "transient": true, 51 | "content": "" 52 | } 53 | ``` 54 | 55 | ### Example 56 | 57 | **Grouping role and instructions**: 58 | 59 | **Group System Messages**: 60 | 61 | - **Trim Child Prompts**: `false` 62 | - **Messages** sub-flow: 63 | 1. **System Message**: `You are a helpful WordPress site administrator assistant.` 64 | 2. **System Message**: `You have access to the WordPress REST API for managing posts, pages, users, and settings.` 65 | 3. **System Message**: `Always confirm before making destructive changes like deleting content or deactivating plugins.` 66 | 67 | **Result**: A single system message with content: 68 | 69 | ```text 70 | You are a helpful WordPress site administrator assistant. 71 | 72 | You have access to the WordPress REST API for managing posts, pages, users, and settings. 73 | 74 | Always confirm before making destructive changes like deleting content or deactivating plugins. 75 | ``` 76 | 77 | ### Example: Inline formatting 78 | 79 | **Group System Messages**: 80 | 81 | - **Trim Child Prompts**: `true` 82 | - **Messages** sub-flow: 83 | 1. **System Message**: `Current date: ${date("Y-m-d")}.` 84 | 2. **System Message**: ` User: ${current_user.display_name}.` 85 | 3. **System Message**: ` Site: ${site_name}.` 86 | 87 | **Result**: A single compact system message: 88 | 89 | ```text 90 | Current date: 2025-12-04. User: John Doe. Site: My WordPress Site. 91 | ``` 92 | 93 | ### Example: Conditional system instructions 94 | 95 | **Group System Messages**: 96 | 97 | - **Trim Child Prompts**: `false` 98 | - **Messages** sub-flow: 99 | 1. **System Message**: `You are a customer support assistant for ${company_name}.` 100 | 2. **If** element: `${user_is_premium}` 101 | - **True flow**: **System Message**: `This is a premium customer. Provide priority support and offer advanced features.` 102 | 3. **If** element: `${hour(date("H")) >= 18}` 103 | - **True flow**: **System Message**: `It's currently after business hours. Inform the user that live chat support resumes at 9 AM.` 104 | 105 | **Result**: A dynamically composed system message based on user status and time. 106 | 107 | ### Example: Modular prompt components 108 | 109 | **Organize complex prompts into logical sections**: 110 | 111 | **Group System Messages**: 112 | 113 | - **Messages** sub-flow: 114 | 1. **Group System Messages** (Role definition): 115 | - **System Message**: `You are an AI assistant specialized in technical support.` 116 | 2. **Group System Messages** (Available tools): 117 | - **System Message**: `You have access to the following tools: search_knowledge_base, create_support_ticket, check_server_status.` 118 | 3. **Group System Messages** (Rules and guidelines): 119 | - **System Message**: `Never share sensitive information. Always verify user identity before accessing account details.` 120 | 121 | ### Tips 122 | 123 | - Use `trim_children: false` for better readability – it's easier for GPT to parse separate paragraphs 124 | - Use `trim_children: true` when building compact inline metadata or lists 125 | - Group related instructions together – don't mix unrelated concepts in one group 126 | - Combine with conditional elements (**If**) to dynamically include/exclude sections based on context 127 | - Nest **Group System Messages** elements to create hierarchical prompt structures 128 | - Use **Group System Messages** to separate static prompts from dynamic ones 129 | - Remember that the grouped message still counts as a single system message in the API request 130 | - Grouped messages are marked `transient: true` – they won't appear in `getConversation()` results 131 | - This element is particularly useful when building prompts from template files or database-stored instructions 132 | - For very long prompts, consider using **Messages Limiter** to keep total context under control 133 | 134 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/WpRestProxyFunction.php: -------------------------------------------------------------------------------- 1 | _name = $properties['name']; 28 | $this->_description = $properties['description']; 29 | $this->_parameters = $properties['parameters']; 30 | $this->_defaults = $properties['defaults']; 31 | $this->_required = $properties['required']; 32 | $this->_method = $properties['method']; 33 | $this->_endpoint = $properties['endpoint']; 34 | $this->_pagination = $properties['pagination']; 35 | } 36 | 37 | 38 | // element 39 | public function read(IConvoRequest $request, IConvoResponse $response) 40 | { 41 | /** @var \Convo\Gpt\IChatFunctionContainer $container */ 42 | $container = $this->findAncestor('\Convo\Gpt\IChatFunctionContainer'); 43 | $container->registerFunction($this); 44 | } 45 | 46 | public function execute(IConvoRequest $request, IConvoResponse $response, array $data) 47 | { 48 | $this->_logger->debug('Got data decoded [' . $this->getName() . '] - [' . print_r($data, true) . ']'); 49 | $data = array_merge($this->_getDefaults(), $data); 50 | $this->_logger->info('Got data with defaults [' . $this->getName() . '] - [' . print_r($data, true) . ']'); 51 | 52 | $method = strtoupper($this->evaluateString($this->_method)); 53 | $endpoint = ltrim($this->evaluateString($this->_endpoint), '/'); 54 | $pagination = $this->evaluateString($this->_pagination); 55 | 56 | // Handle pagination via cursor 57 | if ($pagination && isset($data['cursor'])) { 58 | $cursorData = json_decode(base64_decode($data['cursor']), true); 59 | if (is_array($cursorData)) { 60 | unset($data['cursor']); 61 | $data = array_merge($data, $cursorData); 62 | } 63 | } 64 | 65 | $route = "/wp/v2/{$endpoint}"; 66 | $this->_logger->debug('Executing [' . $method . '] - [' . $route . ']'); 67 | $restRequest = new \WP_REST_Request($method, $route); 68 | 69 | if ($method === 'GET') { 70 | foreach ($data as $key => $val) { 71 | $restRequest->set_param($key, $val); 72 | } 73 | } else { 74 | $restRequest->set_body_params($data); 75 | } 76 | 77 | /** @var \WP_REST_Response $restResponse */ 78 | $restResponse = rest_do_request($restRequest); 79 | $responseData = $restResponse->get_data(); 80 | 81 | // Handle pagination response wrapping 82 | if ($pagination && is_array($responseData)) { 83 | $headers = $restResponse->get_headers(); 84 | $page = isset($data['page']) ? intval($data['page']) : 1; 85 | $perPage = isset($data['per_page']) ? intval($data['per_page']) : 10; 86 | $totalPages = isset($headers['X-WP-TotalPages']) ? intval($headers['X-WP-TotalPages']) : null; 87 | 88 | if ($totalPages && $page < $totalPages) { 89 | $nextCursor = base64_encode(json_encode([ 90 | 'page' => $page + 1, 91 | 'per_page' => $perPage 92 | ])); 93 | 94 | $responseData = [ 95 | 'results' => $responseData, 96 | 'nextCursor' => $nextCursor 97 | ]; 98 | } 99 | } 100 | 101 | if (is_string($responseData)) { 102 | return $responseData; 103 | } 104 | 105 | return json_encode($responseData); 106 | } 107 | 108 | 109 | private function _getDefaults() 110 | { 111 | $defaults = $this->evaluateString($this->_defaults); 112 | if (\is_array($defaults)) { 113 | return $defaults; 114 | } 115 | return []; 116 | } 117 | 118 | public function accepts($functionName) 119 | { 120 | return $this->getName() === $functionName; 121 | } 122 | 123 | public function getName() 124 | { 125 | return $this->evaluateString($this->_name); 126 | } 127 | 128 | public function getDefinition() 129 | { 130 | $parameters = $this->getService()->evaluateArgs($this->_parameters, $this); 131 | if (empty($parameters)) { 132 | $parameters = new \stdClass(); 133 | } 134 | return [ 135 | 'name' => $this->getName(), 136 | 'description' => $this->evaluateString($this->_description), 137 | 'parameters' => [ 138 | 'type' => 'object', 139 | 'properties' => $parameters, 140 | 'required' => $this->evaluateString($this->_required), 141 | ], 142 | ]; 143 | } 144 | 145 | 146 | // TODO: refactor separate chat from scoped function 147 | public function initParams() { 148 | return ''; 149 | } 150 | 151 | public function restoreParams($executionId) {} 152 | 153 | public function getFunctionParams() 154 | { 155 | return new SimpleParams(); 156 | } 157 | 158 | 159 | // UTIL 160 | public function __toString() 161 | { 162 | return parent::__toString() . '[' . $this->_name . ']'; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Convo/Gpt/PluginContext.php: -------------------------------------------------------------------------------- 1 | _logger = $this->getContainer()->get('logger'); 39 | 40 | // $this->_logger->debug('-------------------------------------------------------------'); 41 | // $this->_logger->debug('Initializing Convoworks MCP Plugin Context'); 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | private static function getConvoWPPluginClass() 48 | { 49 | if (!isset(self::$_convoWPPluginClass)) { 50 | if (class_exists('Convo\Wp\Providers\ConvoWPPlugin')) { 51 | self::$_convoWPPluginClass = 'Convo\Wp\Providers\ConvoWPPlugin'; 52 | } elseif (class_exists('Convo\Providers\ConvoWPPlugin')) { 53 | self::$_convoWPPluginClass = 'Convo\Providers\ConvoWPPlugin'; 54 | } else { 55 | throw new \Exception('ConvoWPPlugin class not found. Neither Convo\Wp\Providers\ConvoWPPlugin nor Convo\Providers\ConvoWPPlugin is available.'); 56 | } 57 | } 58 | return self::$_convoWPPluginClass; 59 | } 60 | 61 | /** 62 | * @return \Psr\Container\ContainerInterface 63 | */ 64 | public function getContainer() 65 | { 66 | $convoWPPluginClass = self::getConvoWPPluginClass(); 67 | 68 | if (is_admin()) { 69 | return $convoWPPluginClass::getAdminDiContainer(); 70 | } 71 | 72 | // if ( wp_doing_cron()) { 73 | // $container = $convoWPPluginClass::getPublicDiContainer(); 74 | // } else { 75 | // $container = $convoWPPluginClass::getPublicDiContainer(); 76 | // } 77 | 78 | return $convoWPPluginClass::getPublicDiContainer(); 79 | } 80 | 81 | /** 82 | * @return \Psr\Log\LoggerInterface 83 | */ 84 | public function getLogger() 85 | { 86 | return $this->_logger; 87 | } 88 | 89 | public function getSettingsViewModel() 90 | { 91 | if (!isset($this->_cache['settingsViewModel'])) { 92 | $this->_cache['settingsViewModel'] = new SettingsViewModel($this->getLogger(), $this->getContainer()->get('convoServiceDataProvider')); 93 | $this->_cache['settingsViewModel']->init(); 94 | } 95 | 96 | return $this->_cache['settingsViewModel']; 97 | } 98 | 99 | /** 100 | * @return McpConvoworksManager 101 | */ 102 | public function getMcpConvoworksManager() 103 | { 104 | if (!isset($this->_cache['mcpConvoworksManager'])) { 105 | $this->_cache['mcpConvoworksManager'] = new McpConvoworksManager( 106 | $this->getLogger(), 107 | $this->getMcpServerPackage(), 108 | $this->getContainer()->get('convoServiceDataProvider') 109 | ); 110 | } 111 | 112 | return $this->_cache['mcpConvoworksManager']; 113 | } 114 | 115 | /** 116 | * @return IPackageDescriptor 117 | */ 118 | public function getMcpServerPackage() 119 | { 120 | if (!isset($this->_cache['mcpPackage'])) { 121 | $container = $this->getContainer(); 122 | $this->_cache['mcpPackage'] = new \Convo\Core\Factory\FunctionPackageDescriptor( 123 | '\Convo\Gpt\Pckg\GptPackageDefinition', 124 | function () use ($container) { 125 | $logger = $container->get('logger'); 126 | $api_factory = new GptApiFactory($logger, $container->get('httpFactory')); 127 | $stream_writer = new StreamWriter($logger); 128 | $stream_handler = new StreamHandler($stream_writer, $logger); 129 | 130 | $mcp_manager_factory = new McpSessionManagerFactory( 131 | $logger, 132 | CONVO_GPT_MCP_SESSION_STORAGE_PATH 133 | ); 134 | 135 | $command_dispatcher = new CommandDispatcher( 136 | $container->get('convoServiceFactory'), 137 | $container->get('convoServiceParamsFactory'), 138 | $logger, 139 | $mcp_manager_factory 140 | ); 141 | 142 | $handler = new StreamableRestHandler( 143 | $logger, 144 | $container->get('httpFactory'), 145 | $container->get('convoServiceFactory'), 146 | $container->get('convoServiceDataProvider'), 147 | $mcp_manager_factory, 148 | $command_dispatcher, 149 | $stream_handler 150 | ); 151 | $mcp_platform = new McpServerPlatform( 152 | $logger, 153 | $container->get('convoServiceDataProvider'), 154 | $container->get('serviceReleaseManager'), 155 | $handler 156 | ); 157 | $logger->debug('Registering package [' . GptPackageDefinition::NAMESPACE . ']'); 158 | return new GptPackageDefinition( 159 | $logger, 160 | $api_factory, 161 | $mcp_platform, 162 | $mcp_manager_factory 163 | ); 164 | } 165 | ); 166 | } 167 | 168 | return $this->_cache['mcpPackage']; 169 | } 170 | 171 | // UTIL 172 | public function __toString() 173 | { 174 | return get_class($this); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/system-message-element.md: -------------------------------------------------------------------------------- 1 | ### System Message 2 | 3 | Defines a system-generated message within the chat context for **GPT Chat Completion API v2**. System messages set the behavior, personality, and context for the AI assistant and are crucial for consistent AI responses. 4 | 5 | ### When to use 6 | 7 | Use **System Message** when you need to: 8 | 9 | - Provide instructions or context to the AI assistant 10 | - Set the AI's personality, tone, or role 11 | - Define the AI's capabilities or limitations 12 | - Include dynamic context (date, user info, site settings) in the prompt 13 | - Add transient instructions that shouldn't be stored in conversation history 14 | 15 | This element is typically used in the **Messages** (message_provider) flow of **GPT Chat Completion API v2**. 16 | 17 | ### Properties 18 | 19 | #### Message content (content) 20 | 21 | The text content of the system message. This can be static text or dynamic content using expression language. 22 | 23 | Example values: 24 | 25 | - `You are a helpful customer support assistant for ${site_name}.` 26 | - `Today is ${date("l, F j, Y")}. Answer questions about our products and services.` 27 | - `You are an expert in ${topic}. Provide detailed, accurate information.` 28 | 29 | The content is evaluated as an expression by default, allowing you to inject dynamic values. 30 | 31 | #### Disable evaluation (disable_eval) 32 | 33 | When enabled, prevents expression language evaluation in the content field. The content is used as literal text. 34 | 35 | Default: `false` 36 | 37 | Use this when: 38 | 39 | - You want to include `${...}` syntax as literal text (for demonstrating expression language) 40 | - You have pre-formatted content that shouldn't be evaluated 41 | - You want to avoid accidental expression evaluation in user-provided content 42 | 43 | ### Runtime behavior 44 | 45 | When the element executes: 46 | 47 | 1. The content is evaluated (unless `disable_eval` is true) 48 | 2. A system message object is registered with the parent `IMessages` container 49 | 3. The message is marked as `transient: true` by default 50 | 51 | **Transient flag**: System messages created by this element are marked as transient, meaning they are excluded from the conversation history returned by `getConversation()`. They are still sent to the API but don't appear in the persistent conversation array. 52 | 53 | ### Message structure 54 | 55 | The registered message has this structure: 56 | 57 | ```text 58 | { 59 | "role": "system", 60 | "transient": true, 61 | "content": "" 62 | } 63 | ``` 64 | 65 | ### Integration with Chat Completion API v2 66 | 67 | **System Message** is typically used at the beginning of the **Messages** (message_provider) flow in **GPT Chat Completion API v2**: 68 | 69 | **Messages flow**: 70 | 71 | 1. **System Message**: Main instructions for the AI 72 | 2. **Conversation Messages**: Load conversation history 73 | 3. *[Optional]* Additional **System Message** elements for context-specific instructions 74 | 75 | **Why multiple system messages?** 76 | 77 | You can add multiple **System Message** elements to organize your prompts: 78 | 79 | - First system message: Core AI personality and role 80 | - Second system message: Dynamic context (date, user profile, current page) 81 | - Third system message: Task-specific instructions 82 | 83 | GPT will consider all system messages when generating responses. 84 | 85 | ### Example 86 | 87 | **Basic system message**: 88 | 89 | - **Message content**: `You are a friendly travel assistant. Provide concise information about destinations, hotels, and activities.` 90 | - **Disable evaluation**: `false` 91 | 92 | **Dynamic system message with date**: 93 | 94 | - **Message content**: `You are a helpful assistant. Today is ${date("l, F j, Y")}. The current time is ${date("H:i")}. Use this information when relevant to the user's query.` 95 | - **Disable evaluation**: `false` 96 | 97 | **User-personalized system message**: 98 | 99 | - **Message content**: `You are assisting ${user_name}, a ${user_role} at ${company_name}. Tailor your responses to their professional context.` 100 | - **Disable evaluation**: `false` 101 | 102 | ### Example: Multi-part system prompts 103 | 104 | **Messages flow in Chat Completion v2**: 105 | 106 | 1. **System Message**: 107 | - Content: `You are a WordPress site administrator assistant with access to the WordPress REST API.` 108 | 109 | 2. **System Message**: 110 | - Content: `Current site: ${site_url}. Site name: ${site_name}. Admin user: ${current_user.display_name}.` 111 | 112 | 3. **System Message**: 113 | - Content: `You can create, read, update, and delete posts, pages, and users. Always confirm destructive actions before executing them.` 114 | 115 | 4. **Conversation Messages**: 116 | - Messages: `${conversation}` 117 | 118 | ### Example: Literal expression syntax 119 | 120 | **When you need to show `${...}` as literal text**: 121 | 122 | - **Message content**: `To access variables in Convoworks, use this syntax: \${variable_name}` 123 | - **Disable evaluation**: `true` 124 | 125 | This will send the literal text with `${variable_name}` to GPT without evaluation. 126 | 127 | ### Example: Conditional system message 128 | 129 | **Add context only when needed**: 130 | 131 | **Messages flow**: 132 | 133 | 1. **System Message**: 134 | - Content: `You are a helpful assistant.` 135 | 136 | 2. **If** element: `${request.platform == "amazon"}` 137 | - **True flow**: 138 | - **System Message**: 139 | - Content: `You are running on Amazon Alexa. Keep responses concise and voice-friendly.` 140 | 141 | 3. **Conversation Messages**: `${conversation}` 142 | 143 | ### Tips 144 | 145 | - Keep system messages clear, specific, and concise – avoid overly long instructions 146 | - Place the most important instructions in the first system message 147 | - Use multiple system messages to separate concerns (role, context, rules) 148 | - Include the current date/time when temporal context matters: `${date("Y-m-d H:i:s")}` 149 | - System messages with `transient: true` won't be stored in the conversation array returned by Chat Completion v2 150 | - System messages are processed by GPT on every API call, so dynamic values (like date) are always fresh 151 | - For production chatbots, include usage guidelines (e.g., "Don't provide medical advice", "Don't share personal data") 152 | - Test different system prompts – small wording changes can significantly impact AI behavior 153 | - Avoid contradictory instructions across multiple system messages 154 | - For showing expression syntax as examples, use `disable_eval: true` 155 | - System messages appear as separate messages in the API request, so they count toward context length 156 | 157 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/McpFilesystemSessionStore.php: -------------------------------------------------------------------------------- 1 | _logger = $logger; 25 | $this->_basePath = $basePath; 26 | $this->_serviceId = $serviceId; 27 | } 28 | 29 | // create new session 30 | public function createSession($clientName): string 31 | { 32 | $session_id = StrUtil::uuidV4(); 33 | $path = $this->_getServicePath() . $session_id; 34 | if (false === wp_mkdir_p($path)) { 35 | throw new \RuntimeException('Failed to create session directory: ' . $path); 36 | } 37 | $this->_logger->debug('Created new session directory: ' . $path); 38 | 39 | $session = [ 40 | 'session_id' => $session_id, 41 | 'status' => IMcpSessionStoreInterface::SESSION_STATUS_NEW, 42 | 'clientName' => $clientName, 43 | 'created_at' => time(), 44 | 'last_active' => time(), 45 | ]; 46 | $this->_saveSession($session); 47 | 48 | return $session_id; 49 | } 50 | 51 | public function saveSession($session): void 52 | { 53 | $this->_saveSession($session); 54 | } 55 | 56 | 57 | 58 | // COMMANDS 59 | public function initialiseSession($sessionId): void 60 | { 61 | $session = $this->getSession($sessionId); 62 | 63 | if ($session['status'] !== IMcpSessionStoreInterface::SESSION_STATUS_NEW) { 64 | throw new DataItemNotFoundException('No NEW session found: ' . $sessionId); 65 | } 66 | 67 | $session['status'] = IMcpSessionStoreInterface::SESSION_STATUS_INITIALISED; 68 | $session['last_active'] = time(); 69 | 70 | $this->_saveSession($session); 71 | } 72 | 73 | // queues the notification (now queues full JSON-RPC message) 74 | public function queueEvent(string $sessionId, array $data): void 75 | { 76 | $queueFile = $this->_getQueueFile($sessionId); 77 | file_put_contents($queueFile, json_encode($data) . PHP_EOL, FILE_APPEND | LOCK_EX); 78 | $this->pingSession($sessionId); 79 | } 80 | 81 | public function queueEvents(string $sessionId, array $events): void 82 | { 83 | $queueFile = $this->_getQueueFile($sessionId); 84 | $lines = array_map(function ($e) { 85 | return json_encode($e); 86 | }, $events); 87 | file_put_contents($queueFile, implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND | LOCK_EX); 88 | $this->pingSession($sessionId); 89 | } 90 | 91 | // read next message (deletes it) 92 | public function nextEvent($sessionId): ?array 93 | { 94 | $queueFile = $this->_getQueueFile($sessionId); 95 | if (!file_exists($queueFile) || filesize($queueFile) === 0) return null; 96 | $handle = fopen($queueFile, 'r+'); 97 | flock($handle, LOCK_EX); 98 | $event = json_decode(fgets($handle), true); 99 | $remaining = fread($handle, filesize($queueFile)); 100 | rewind($handle); 101 | ftruncate($handle, 0); 102 | fwrite($handle, $remaining); 103 | flock($handle, LOCK_UN); 104 | fclose($handle); 105 | return $event; 106 | } 107 | 108 | private function _getQueueFile(string $sessionId): string 109 | { 110 | return $this->_getServicePath() . $sessionId . '/queue.jsonl'; 111 | } 112 | 113 | public function pingSession($sessionId): void 114 | { 115 | $session = $this->getSession($sessionId); 116 | $session['last_active'] = time(); 117 | $this->_saveSession($session); 118 | } 119 | 120 | private function _getServicePath(): string 121 | { 122 | return $this->_basePath . $this->_serviceId . '/'; 123 | } 124 | 125 | // PERSISTENCE 126 | public function getSession($sessionId): array 127 | { 128 | $path = $this->_getServicePath() . $sessionId . '.json'; 129 | if (!is_file($path)) { 130 | throw new DataItemNotFoundException('Session file [' . $path . '] not found'); 131 | } 132 | 133 | $session = json_decode(file_get_contents($path), true); 134 | if (empty($session)) { 135 | throw new \RuntimeException('Failed to decode session file: ' . $path); 136 | } 137 | 138 | return $session; 139 | } 140 | 141 | private function _saveSession($session) 142 | { 143 | $path = $this->_getServicePath() . $session['session_id'] . '.json'; 144 | $jsonData = json_encode($session, JSON_PRETTY_PRINT); 145 | file_put_contents($path, $jsonData); 146 | } 147 | 148 | 149 | // UTIL 150 | 151 | 152 | /** 153 | * Deletes session files and folders that have been inactive for the given time (in seconds). 154 | * 155 | * @param int $inactiveTime Seconds of inactivity before deletion. 156 | * @return int Number of deleted sessions. 157 | */ 158 | public function cleanupInactiveSessions(int $inactiveTime): int 159 | { 160 | $deleted = 0; 161 | $files = glob($this->_getServicePath() . '*.json'); 162 | foreach ($files as $file) { 163 | $session = json_decode(file_get_contents($file), true); 164 | if (!$session || !isset($session['last_active'])) { 165 | continue; 166 | } 167 | if ($session['last_active'] < time() - $inactiveTime) { 168 | $sessionId = $session['session_id']; 169 | // Delete session file 170 | @unlink($file); 171 | // Delete queue file 172 | $queueFile = $this->_getQueueFile($sessionId); 173 | if (is_file($queueFile)) { 174 | @unlink($queueFile); 175 | } 176 | // Delete session folder 177 | $sessionFolder = $this->_getServicePath() . $sessionId; 178 | if (is_dir($sessionFolder)) { 179 | // Remove all files in folder 180 | $folderFiles = glob($sessionFolder . '/*'); 181 | foreach ($folderFiles as $f) { 182 | @unlink($f); 183 | } 184 | @rmdir($sessionFolder); 185 | } 186 | $deleted++; 187 | $this->_logger->info("Deleted inactive session: $sessionId"); 188 | } 189 | } 190 | return $deleted; 191 | } 192 | 193 | public function __toString() 194 | { 195 | return get_class($this) . '[]'; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/moderation-api-element.md: -------------------------------------------------------------------------------- 1 | ### GPT Moderation API 2 | 3 | Validates input content using the OpenAI Moderation API. This element helps ensure that user-generated content or AI responses comply with content policies by detecting potentially harmful, offensive, or policy-violating content. 4 | 5 | ### When to use 6 | 7 | Use **GPT Moderation API** when you need to: 8 | 9 | - Screen user input before processing it with GPT 10 | - Validate AI-generated content before displaying it to users 11 | - Implement content filtering and safety controls 12 | - Detect harmful, offensive, or inappropriate content 13 | - Comply with content moderation policies and regulations 14 | - Flag content for human review based on moderation scores 15 | 16 | ### Properties 17 | 18 | #### Input 19 | 20 | The text content to be moderated. This can be user input, AI-generated content, or any text you want to validate. 21 | 22 | Example values: 23 | 24 | - `${request.text}` – User's message 25 | - `${gpt_response.choices[0].message.content}` – AI response 26 | - `Any text string you want to check` 27 | 28 | #### API key 29 | 30 | Your OpenAI API key. Store this in a service variable for security. 31 | 32 | Example: `${GPT_API_KEY}` 33 | 34 | #### Base URL 35 | 36 | Optional. Base URL for the API endpoint. If left blank, defaults to `https://api.openai.com/v1`. 37 | 38 | Use this to point to custom endpoints if needed. 39 | 40 | #### API options 41 | 42 | Configuration options for the Moderation API: 43 | 44 | - **model** – The moderation model to use (e.g., `text-moderation-latest`, `text-moderation-stable`) 45 | 46 | Default value: 47 | 48 | ```text 49 | model: text-moderation-latest 50 | ``` 51 | 52 | The `text-moderation-latest` model is automatically updated to use the most recent moderation model, while `text-moderation-stable` provides a stable version that only updates with advance notice. 53 | 54 | For details, see the [OpenAI Moderation API Reference](https://platform.openai.com/docs/api-reference/moderations). 55 | 56 | #### Result Variable Name 57 | 58 | The name of the variable that will store the moderation API response. Default is `status`. 59 | 60 | #### OK flow 61 | 62 | Sub-flow executed after the moderation check completes. The result variable will be available for use in this flow. 63 | 64 | Use this flow to: 65 | 66 | - Check moderation flags and take appropriate action 67 | - Block content that violates policies 68 | - Log flagged content for review 69 | - Provide user feedback about content violations 70 | 71 | ### Runtime behavior 72 | 73 | When the element executes: 74 | 75 | 1. The input text is evaluated 76 | 2. An API call is made to OpenAI Moderation endpoint 77 | 3. The moderation response is stored in the result variable (request scope) 78 | 4. The OK flow is executed with the result variable available 79 | 80 | ### Response structure 81 | 82 | The result variable contains moderation analysis with: 83 | 84 | - `${status.results[0].flagged}` – Boolean indicating if content was flagged 85 | - `${status.results[0].categories}` – Object with category flags (true/false for each category) 86 | - `${status.results[0].category_scores}` – Object with confidence scores (0.0-1.0) for each category 87 | 88 | **Moderation categories**: 89 | 90 | - `hate` – Content promoting hate based on identity 91 | - `hate/threatening` – Hateful content with violence or harm 92 | - `harassment` – Harassing, bullying, or abusive content 93 | - `harassment/threatening` – Harassment with threats of harm 94 | - `self-harm` – Content promoting self-harm 95 | - `self-harm/intent` – Intent to engage in self-harm 96 | - `self-harm/instructions` – Instructions for self-harm 97 | - `sexual` – Sexual content 98 | - `sexual/minors` – Sexual content involving minors 99 | - `violence` – Content promoting violence 100 | - `violence/graphic` – Graphic violent content 101 | 102 | ### Example 103 | 104 | **Moderate user input before sending to GPT**: 105 | 106 | **Configuration**: 107 | 108 | - **Input**: `${request.text}` 109 | - **API key**: `${GPT_API_KEY}` 110 | - **Result Variable Name**: `moderation` 111 | 112 | **In the OK flow**, add conditional logic: 113 | 114 | 1. Add **If** element with condition: `${moderation.results[0].flagged}` 115 | - **True flow**: Add **Text Response**: `I'm sorry, I can't process that request. Please rephrase your message to comply with our content policy.` 116 | - Add **Stop** element to prevent further processing 117 | - **False flow**: Continue with normal GPT processing 118 | 119 | ### Example: Log flagged content 120 | 121 | **In the OK flow**, add logging for flagged content: 122 | 123 | 1. Add **If** element: `${moderation.results[0].flagged}` 124 | - **True flow**: 125 | - Add **Log Message**: `Flagged content detected: ${request.text}` 126 | - Add **Set Param** (session scope): 127 | - Name: `flagged_messages` 128 | - Value: `${array_merge(flagged_messages, [{"text": request.text, "categories": moderation.results[0].categories, "timestamp": date("Y-m-d H:i:s")}])}` 129 | 130 | ### Example: Category-specific handling 131 | 132 | **Handle different categories differently**: 133 | 134 | 1. Add **If** element: `${moderation.results[0].categories.harassment}` 135 | - **True flow**: Block and log as harassment 136 | 2. Add **Else If** element: `${moderation.results[0].category_scores.violence > 0.8}` 137 | - **True flow**: Flag for manual review (high violence score) 138 | 3. Add **Else** flow: Continue normally 139 | 140 | ### Example: Two-way moderation 141 | 142 | **Moderate both user input and AI responses**: 143 | 144 | **Step 1 – Moderate user input**: 145 | 146 | - Use **GPT Moderation API** on `${request.text}` 147 | - Block flagged input 148 | 149 | **Step 2 – Process with GPT** (if input is clean) 150 | 151 | **Step 3 – Moderate GPT response**: 152 | 153 | - Use another **GPT Moderation API** on `${gpt_response.choices[0].message.content}` 154 | - If flagged, regenerate with adjusted parameters or use a fallback response 155 | 156 | ### Tips 157 | 158 | - Always moderate user input before processing expensive GPT API calls – prevents wasting tokens on policy-violating content 159 | - Consider moderating AI-generated content as well – even with safety settings, GPT can occasionally produce borderline content 160 | - Use `category_scores` for fine-grained control – you can set your own thresholds stricter than the default `flagged` boolean 161 | - Log all flagged content with timestamps and user IDs for compliance and improvement 162 | - Provide clear, helpful error messages to users when content is blocked 163 | - For chatbots, implement progressive warnings – first warning, then temporary restrictions, then account restrictions for repeated violations 164 | - Use `text-moderation-latest` in development, `text-moderation-stable` in production if you need predictable behavior 165 | - The Moderation API is free to use (as of current OpenAI pricing) – don't hesitate to use it liberally 166 | - Combine with application-level filtering for comprehensive content safety 167 | - Test edge cases – the API may not catch all policy violations, and it may occasionally flag acceptable content (false positives) 168 | 169 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Pckg/Help/simple-message-limiter-element.md: -------------------------------------------------------------------------------- 1 | ### Simple Messages Limiter 2 | 3 | Limits the size of the conversation by trimming old messages based on token count. Unlike **GPT Messages Limiter**, this element does not summarize – it simply removes the oldest messages when the conversation exceeds the specified limit. 4 | 5 | ### When to use 6 | 7 | Use **Simple Messages Limiter** when you need to: 8 | 9 | - Keep conversation context within model token limits 10 | - Remove old messages without summarization (faster, no API cost) 11 | - Implement a sliding window conversation approach 12 | - Prevent context length overflow errors 13 | - Manage long-running conversations efficiently 14 | 15 | For conversations where context from old messages is important, consider using **GPT Messages Limiter** instead, which summarizes removed messages. 16 | 17 | ### Properties 18 | 19 | #### Max Tokens to Keep 20 | 21 | The maximum estimated token count allowed in the conversation before trimming. When the total token count of messages exceeds this limit, old messages are removed. 22 | 23 | Example: `${8192}` 24 | 25 | Tokens are estimated using a simple calculation (not exact tokenization). 26 | 27 | #### Truncate to Tokens 28 | 29 | The estimated token count to retain after trimming. This should be lower than **Max Tokens to Keep** to provide a buffer before the next truncation. 30 | 31 | Example: `${4096}` 32 | 33 | When truncation is triggered, messages are removed from the beginning until the total is below this value. 34 | 35 | #### Max messages to keep (Deprecated) 36 | 37 | Legacy parameter for message count-based truncation. Use **Max Tokens to Keep** instead for better control. 38 | 39 | #### Truncate message count (Deprecated) 40 | 41 | Legacy parameter for count-based truncation. Use **Truncate to Tokens** instead. 42 | 43 | #### Result Variable Name 44 | 45 | The name of the variable available in the **Truncated Flow**. Default is `status`. 46 | 47 | The variable contains: 48 | 49 | - `${status.messages}` – Array of remaining messages after truncation 50 | - `${status.truncated}` – Array of messages that were removed 51 | 52 | #### Messages (message_provider) 53 | 54 | A sub-flow that provides the conversation messages to be checked and potentially truncated. Typically contains **Conversation Messages** or **System Message** elements. 55 | 56 | #### Truncated Flow 57 | 58 | Flow executed when messages are truncated. This flow runs **after** truncation but **before** messages are passed to the parent container. 59 | 60 | Use this flow to: 61 | 62 | - Log truncation events 63 | - Update conversation metadata 64 | - Notify the user about removed messages 65 | - Store truncated messages for later reference 66 | 67 | ### Runtime behavior 68 | 69 | When the element executes: 70 | 71 | 1. The message_provider sub-flow runs, collecting messages 72 | 2. Total token count is estimated for all messages 73 | 3. If total exceeds **Max Tokens to Keep**: 74 | - Messages are removed from the beginning (oldest first) 75 | - Removal continues until total is below **Truncate to Tokens** 76 | - Truncated Flow executes with result variable containing remaining and removed messages 77 | 4. Remaining messages are registered with the parent `IMessages` container 78 | 79 | **Truncation strategy**: Messages are removed from the start of the array (oldest messages first). System messages and recent messages are preserved. 80 | 81 | ### Token estimation 82 | 83 | Token count is estimated using a simplified formula: 84 | 85 | - Text is split by whitespace and punctuation 86 | - Each word/token is counted 87 | - Overhead for message structure is added 88 | 89 | This is not exact GPT tokenization but provides a reasonable approximation for context management. 90 | 91 | ### Example 92 | 93 | **Basic conversation limiting**: 94 | 95 | **Simple Messages Limiter**: 96 | 97 | - **Max Tokens to Keep**: `${8000}` 98 | - **Truncate to Tokens**: `${4000}` 99 | - **Result Variable Name**: `status` 100 | - **Messages** sub-flow: 101 | - **Conversation Messages**: `${conversation}` 102 | - **Truncated Flow**: 103 | - **Log Message**: `Truncated ${count(status.truncated)} messages from conversation` 104 | 105 | **How it works**: 106 | 107 | 1. Conversation starts with 0 messages 108 | 2. Messages accumulate in `${conversation}` session variable 109 | 3. When total exceeds 8000 estimated tokens, oldest messages are removed 110 | 4. Truncation continues until total is below 4000 tokens 111 | 5. Log message records truncation event 112 | 6. Remaining messages are sent to GPT 113 | 114 | ### Example: User notification 115 | 116 | **Notify user when truncation occurs**: 117 | 118 | **Truncated Flow**: 119 | 120 | 1. **Set Param** (request scope): 121 | - Name: `show_truncation_notice` 122 | - Value: `${true}` 123 | 124 | 2. **Log Message**: `Removed ${count(status.truncated)} old messages (estimated ${estimate_tokens(serialize_gpt_messages(status.truncated))} tokens)` 125 | 126 | **OK Flow** (in Chat Completion v2): 127 | 128 | 1. **If** element: `${show_truncation_notice}` 129 | - **True flow**: **Text Response**: `Note: I've cleared some older messages from our conversation to free up space for new information.` 130 | 131 | ### Example: Nested with system messages 132 | 133 | **Include system messages in truncation check**: 134 | 135 | **Messages flow in Chat Completion v2**: 136 | 137 | 1. **Simple Messages Limiter**: 138 | - **Max Tokens to Keep**: `${10000}` 139 | - **Truncate to Tokens**: `${5000}` 140 | - **Messages** sub-flow: 141 | 1. **System Message**: `You are a helpful assistant. Today is ${date("Y-m-d")}.` 142 | 2. **Conversation Messages**: `${conversation}` 143 | 144 | **Result**: System messages are included in token count but typically preserved since they're at the end of the array. 145 | 146 | ### Comparison: Simple vs. GPT Messages Limiter 147 | 148 | | Feature | Simple Messages Limiter | GPT Messages Limiter | 149 | |---------|------------------------|---------------------| 150 | | **Method** | Removes old messages | Summarizes old messages | 151 | | **API cost** | Free (no API calls) | Costs tokens (uses Chat Completion API) | 152 | | **Speed** | Fast | Slower (waits for API) | 153 | | **Context preservation** | None (messages lost) | Good (summary preserved) | 154 | | **Best for** | Less important conversations, cost optimization | Important conversations requiring context | 155 | 156 | ### Tips 157 | 158 | - Set **Max Tokens to Keep** to about 75% of your model's context window 159 | - Set **Truncate to Tokens** to about 50% to provide headroom before next truncation 160 | - For GPT-4o (128k context), you might use max: 90000, truncate: 60000 161 | - For GPT-4 (8k context), use max: 6000, truncate: 3000 162 | - Always keep at least a few recent messages – don't truncate too aggressively 163 | - Use **Simple Messages Limiter** for cost-sensitive applications 164 | - Use **GPT Messages Limiter** when conversation context is critical 165 | - Monitor `${count(status.truncated)}` in the truncated flow to track truncation frequency 166 | - Consider storing `${status.truncated}` in a log or database for conversation history analysis 167 | - System messages added after this element are not included in truncation (they're added later) 168 | - Nested multiple limiters to implement tiered truncation strategies (e.g., remove then summarize) 169 | 170 | -------------------------------------------------------------------------------- /src/Convo/Gpt/Mcp/CommandDispatcher.php: -------------------------------------------------------------------------------- 1 | _convoServiceFactory = $convoServiceFactory; 47 | $this->_convoServiceParamsFactory = $convoServiceParamsFactory; 48 | $this->_logger = $logger; 49 | $this->_mcpSessionManagerFactory = $mcpSessionManagerFactory; 50 | } 51 | 52 | /** 53 | * Processes incoming data (single or batch) and returns responses. 54 | * 55 | * @param array $data 56 | * @param string $sessionId 57 | * @param string $variant 58 | * @param string $serviceId 59 | * @return array 60 | */ 61 | public function processIncoming(array $data, string $sessionId, string $variant, string $serviceId): array 62 | { 63 | $this->_mcpSessionManagerFactory->getSessionManager($serviceId)->getActiveSession($sessionId, true); 64 | 65 | if (array_keys($data) === range(0, \count($data) - 1)) { 66 | $this->_logger->warning('Batching not supported in 2025-06-18; processing first message only'); 67 | $data = $data[0]; 68 | } 69 | 70 | // Handle notifications (no 'id') 71 | if (!isset($data['id'])) { 72 | $this->_logger->debug('Processing notification [' . $data['method'] . ']; no response needed'); 73 | $owner = new RestSystemUser(); 74 | $role = McpServerCommandRequest::SPECIAL_ROLE_MCP; 75 | $version_id = $this->_convoServiceFactory->getVariantVersion($owner, $serviceId, McpServerPlatform::PLATFORM_ID, $variant); 76 | $service = $this->_convoServiceFactory->getService($owner, $serviceId, $version_id, $this->_convoServiceParamsFactory); 77 | 78 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $data, $role); 79 | $text_response = new SseResponse($sessionId); 80 | $text_response->setLogger($this->_logger); 81 | 82 | try { 83 | $this->_logger->info('Running service instance [' . $service->getId() . '] for notification [' . $data['method'] . ']'); 84 | $service->run($text_request, $text_response); 85 | } catch (Throwable $e) { 86 | $this->_logger->error('Error processing notification [' . $data['method'] . ']: ' . $e->getMessage()); 87 | } 88 | 89 | return []; // No response body for notifications 90 | } 91 | 92 | $owner = new RestSystemUser(); 93 | $role = McpServerCommandRequest::SPECIAL_ROLE_MCP; 94 | $version_id = $this->_convoServiceFactory->getVariantVersion($owner, $serviceId, McpServerPlatform::PLATFORM_ID, $variant); 95 | $service = $this->_convoServiceFactory->getService($owner, $serviceId, $version_id, $this->_convoServiceParamsFactory); 96 | 97 | $req_id = $data['id']; 98 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $data, $role); 99 | $text_response = new SseResponse($sessionId); 100 | $text_response->setLogger($this->_logger); 101 | 102 | try { 103 | $this->_logger->info('Running service instance [' . $service->getId() . '] in MCP POST Handler.'); 104 | $service->run($text_request, $text_response); 105 | } catch (Throwable $e) { 106 | /** @phpstan-ignore-next-line */ 107 | $this->_logger->error($e); 108 | return ['jsonrpc' => '2.0', 'id' => $req_id, 'error' => ['code' => -32603, 'message' => $e->getMessage()]]; 109 | } 110 | 111 | $result = $text_response->getPlatformResponse(); 112 | if (\is_array($result) && empty($result)) { 113 | $this->_logger->warning('Empty result array detected; converting to empty object'); 114 | $result = new \stdClass(); 115 | } 116 | 117 | return ['jsonrpc' => '2.0', 'id' => $req_id, 'result' => $result]; 118 | } 119 | 120 | /** 121 | * Processes a single incoming message (for bidirectional streaming). 122 | * 123 | * @param array $message 124 | * @param string $sessionId 125 | * @param string $variant 126 | * @param string $serviceId 127 | */ 128 | public function processMessage( 129 | array $message, 130 | string $sessionId, 131 | string $variant, 132 | string $serviceId 133 | ): void { 134 | $owner = new RestSystemUser(); 135 | $role = McpServerCommandRequest::SPECIAL_ROLE_MCP; 136 | $version_id = $this->_convoServiceFactory->getVariantVersion($owner, $serviceId, McpServerPlatform::PLATFORM_ID, $variant); 137 | $service = $this->_convoServiceFactory->getService($owner, $serviceId, $version_id, $this->_convoServiceParamsFactory); 138 | 139 | $manager = $this->_mcpSessionManagerFactory->getSessionManager($serviceId); 140 | 141 | if (array_keys($message) === range(0, \count($message) - 1)) { 142 | $responses = []; 143 | foreach ($message as $single_msg) { 144 | $req_id = $single_msg['id']; 145 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $single_msg, $role); 146 | $text_response = new SseResponse($sessionId); 147 | $text_response->setLogger($this->_logger); 148 | try { 149 | $service->run($text_request, $text_response); 150 | } catch (Throwable $e) { 151 | $responses[] = ['jsonrpc' => '2.0', 'id' => $req_id, 'error' => ['code' => -32603, 'message' => $e->getMessage()]]; 152 | } 153 | } 154 | if (!empty($responses)) { 155 | $manager->enqueueMessage($sessionId, $responses); 156 | } 157 | } else { 158 | $req_id = $message['id']; 159 | $text_request = new McpServerCommandRequest($serviceId, $sessionId, StrUtil::uuidV4(), $message, $role); 160 | $text_response = new SseResponse($sessionId); 161 | $text_response->setLogger($this->_logger); 162 | try { 163 | $service->run($text_request, $text_response); 164 | } catch (Throwable $e) { 165 | $error = ['jsonrpc' => '2.0', 'id' => $req_id, 'error' => ['code' => -32603, 'message' => $e->getMessage()]]; 166 | $manager->enqueueMessage($sessionId, $error); 167 | } 168 | } 169 | } 170 | } 171 | --------------------------------------------------------------------------------