How to write the Custom AI Adapter in P4 Code Review to support an in-house AI model

Use this guide to write and integrate a Custom AI Adapter for your in-house or on-premise AI vendor with P4 Code Review.

Currently, the Custom AI Adapter has been tested with the following AI vendors and models:

  • lmstudio-community/gemma-3-1B-it-qat-GGUF (/v1/completions)

  • granite-embedding-278m-multilingual-GGUF (/v1/embeddings)

If you are developing a Custom AI Adapter for a different vendor or model, additional configuration may be required. Refer to the vendor's documentation for specific configuration details.

We strongly recommend implementing and testing it first in a sandbox environment, test account, or a new instance of P4 Code Review before integrating it into your production environment.

To write a Custom AI Adapter in P4 Code Review, complete these three stages:

Copy and modify GenericAIAdapter

  1. Go to the folder module/AiAnalysis/src/Service/ and create the copy of file GenericAIAdapter.php within the same folder.

  2. Name the copied file CustomAIAdapter.php. Below is an example of GenericAIAdapter.php.

    Copy
    <?php
    /**
     * Perforce Swarm
     *
     * @copyright   2024 Perforce Software. All rights reserved.
     * @license     Please see LICENSE.txt in top-level folder of this distribution.
     * @version     <release>/<patch>
     */

    namespace AiAnalysis\Service;

    use AiAnalysis\Filter\IAiAnalysis;
    use AiAnalysis\Helper\IAiAnalysisHelper;
    use Application\Config\ConfigException;
    use Application\Config\ConfigManager;
    use Application\Config\IConfigDefinition;
    use Application\Log\SwarmLogger;
    use Laminas\Http\Response;
    use OpenAI;

    class GenericAIAdapter extends AbstractAiAdapter implements AiServiceInterface
    {

        const LOG_PREFIX = GenericAIAdapter::class;

        /**
         * performAI, note this uses the content body and ignores any data provided
         * Login to Swarm using the credentials provided
         *
         * @param mixed $data
         * @return array
         */
        public function executeAIRequest(mixed $data): array
        {
            $logger = $this->services->get(SwarmLogger::SERVICE);
            try {
                $fileName        = ($data[IAiAnalysis::FILE_NAME]) ?
                    'of the file: ' . $data[IAiAnalysis::FILE_NAME] . ' ' : ' ';
                $content         = $data[IConfigDefinition::AI_PACKAGE_TYPE] . $fileName .
                    $data[IAiAnalysisHelper::CONTENT_TO_ANALYZE];
                $args['content'] = $content;
                $result          = $this->request($this->getApiUrl(), false, $args);

                if ($this->validateResult($result)) {
                    $logger->trace(sprintf("[%s]: Content generation successful ", self::LOG_PREFIX));
                    return [IAiAnalysisHelper::CONTENT_GENERATED => $result->choices[0]->message->content,
                        self::ERROR => null,
                        self::CODE => Response::STATUS_CODE_200];
                } else {
                    $logger->trace(sprintf("[%s]: Failed at content generation ", self::LOG_PREFIX));
                    return [IAiAnalysisHelper::CONTENT_GENERATED => '',
                        self::ERROR => is_object($result) && property_exists($result, 'error') ? $result->error :
                            'Response not received from AI Vendor',
                        self::CODE => is_object($result) && property_exists($result, 'code') ? $result->code :
                            Response::STATUS_CODE_500];
                }
            } catch (\Exception | \Throwable $exception) {
                $logger->debug(
                    sprintf(
                        "[%s]: Error occurred at Generic AI Adapter %s",
                        self::LOG_PREFIX,
                        $exception->getMessage()
                    )
                );
                $statusCode = $exception->getCode();
                return [
                    IAiAnalysisHelper::CONTENT_GENERATED => null,
                    'error' => $exception->getMessage(),
                    'code' => $statusCode === 0 ? Response::STATUS_CODE_500 : $statusCode,
                ];
            }
        }

        /**
         * This method will fetch the API key from config, required to execute the generic AI request
         * @throws ConfigException
         * @return string
         */
        private function getApiUrl() : string
        {
            $config = $this->services->get(IConfigDefinition::CONFIG);
            return ConfigManager::getValue($config, IConfigDefinition::AI_REVIEW_AI_VENDORS_AI_MODEL1_API_END_POINT);
        }
    }
  3. In CustomAIAdapter.php, change the class from GenericAIAdapter to CustomAIAdapter.

  4. In CustomAIAdapter.php, within the executeAIRequest function, replace $arg with the request format used by your chosen AI model. By default, this is $args['content'] = $content; however, below is an example of a different $arg request format.

    Copy
    $args = [
         'messages' => [
            [
                'role' => 'user',
                'content' => $content
            ],
        ],
    ];
    //Note here $content is prompt ( $data[IConfigDefinition::AI_PACKAGE_TYPE] ) + filename with its extension + diff from review 
  5. Save the changes to in CustomAIAdapter.php and build the request. The following line of code in CustomAIAdapter.php sends the request to the API endpoint (api_end_point) specified in config.php file.

    Copy
    $result = $this->request($this->getApiUrl(), false, $args);

Validate the response

  1. Go to the folder module/AiAnalysis/src/Service/, open the file AbstractAiAdapter.php and copy the function validateResult.

  2. Open CustomAIAdapter.php and overwrite validateResult with the copied function.

  3. Modify the copied function in CustomAIAdapter.php with your AI model’s response format. An example of an unmodified validateResult function is as follows:

    Copy
    protected function validateResult(object $result): bool
    {
        if (!property_exists($result, 'error') &&
            property_exists($result, 'choices') &&
            isset($result->choices[0]) && is_object($result->choices[0]) &&
            property_exists($result->choices[0], 'message') &&
            is_object($result->choices[0]->message) &&
            property_exists($result->choices[0]->message, 'content')) {
            return true;
        }
        return false;
    }

    If your AI model response is not a object, you need to modify the validateResult function in CustomAIAdapter.php so that is can handle the response.

    For example, if the AI vendor returns the response as an array, update the validateResult function to parse the response. An example of an updated validateResult function that can handle the response as an array is shown below.

    Copy
    protected function validateAIResponse(array $result): bool
    {
        if (is_array($result) && isset($result[0]) && is_object($result[0]) &&
            property_exists($result[0], 'embedding'))
        {
            return true;
        }
        return false;
    }
  4. If your AI model’s response format is different from the example above, modify the format accordingly. You will also need to update IAiAnalysisHelper within the CustomAIAdapter.php file. An example of different AI response format is shown below:

    Copy
    {
      "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT",
      "choices": [
        {
          "index": 0,
          "message": {
            "role": "assistant",
            "content": "Hello! How can I assist you today?",
            "refusal": null,
            "annotations": []
          },
        }
      ],
    }
  5. Update the Services.php file located in module/Application/src/Config/ to include an entry for CustomAIAdapter alongside the other AI adapters. Below is an example for the AI adapter entries in this file:

    Copy
    const LM_STUDIO_AI = 'lmStudioAI';
    const CUSTOM_AI = 'customAI';
  6. Update the AiServiceFactory.php file located in module/AiAnalysis/src/Factory/ to include an entry for CustomAIAdapter alongside the other AI adapters. Below is an example for the AI adapter entries in this file:

    Copy
        const OPENAIADAPTER     = 'openAI';
        const GENERICAIADAPTER  = 'genericAI';
        const LMSTUDIOAIADAPTER = 'lmStudioAI';
        const CUSTOMAIADAPTER = 'customAI';

Add custom AI adapter to config files

Two config files require updating with entries for the custom AI adapter. These files are:

  • The module.config.php file found in module/AiAnalysis/config/.

  • The config.php file found in SWARM_ROOT/data/.

To update the files, follow these steps:

  1. Open the module.config.php file found in module/AiAnalysis/config/.

  2. At the top of the module.config.php file, add a use statement for the CustomAIAdapter above the current use statement for the GenericAIAdapter. Below is an example of these use statements:

    Copy
    use AiAnalysis\Service\CustomAiAdapter;
    use AiAnalysis\Service\GenericAiAdapter;
  3. Inside the same module.config.php file, find the service_manager array and add the following entries.

    In the aliases array, add:

    • AiServiceFactory::CUSTOMAIADAPTER => CustomAIAdapter::class

    In the factories array, add:

    • CustomAIAdapter::class => AiServiceFactory::class
    • Services::CUSTOM_AI => CustomAIAdapter::class

    Below is an example of the updated service_manager array in the module.config.php file.

    Copy
    'service_manager' => [
        'aliases' => [
            IDao::AI_ANALYSIS_DAO  => AiAnalysisDAO::class,
            AiServiceFactory::OPENAIADAPTER => OpenAIAdapter::class,
            AiServiceFactory::GENERICAIADAPTER => GenericAIAdapter::class,
            AiServiceFactory::LMSTUDIOAIADAPTER => LMStudioAIAdapter::class,
            IAiAnalysis::NAME => AiAnalysis::class,
            IAiAnalysis::DISCARD_ANALYSIS_FILTER => DiscardAnalysis::class,
            AiServiceFactory::CUSTOMAIADAPTER => CustomAIAdapter::class
        ],
        'factories' => [
                AiAnalysisDAO::class => InvokableServiceFactory::class,
                OpenAIAdapter::class => AiServiceFactory::class,
                Services::OPEN_AI => OpenAIAdapter::class,
                GenericAIAdapter::class => AiServiceFactory::class,
                Services::GENERIC_AI => GenericAIAdapter::class,
                LMStudioAIAdapter::class => AiServiceFactory::class,
                Services::LM_STUDIO_AI => LMStudioAIAdapter::class,
                AiAnalysis::class => InvokableServiceFactory::class,
                AiAnalysisChecker::class => InvokableServiceFactory::class,
                AiAnalysisCharLimitChecker::class => InvokableServiceFactory::class,
                AiAnalysisDataRetentionLifetimeChecker::class => InvokableServiceFactory::class,
                DiscardAnalysis::class => InvokableServiceFactory::class,
                CustomAIAdapter::class => AiServiceFactory::class,
                Services::CUSTOM_AI => CustomAIAdapter::class,
        ],
    ],
  4. Open config.php file found in SWARM_ROOT/data/.

  5. Update the ai_review module with the values for the custom AI Adapter.

    Make sure you update api_end_point with your AI vendor end point .

    Below is an example of a modified ai_review module:

    Copy
    'ai_review' => array(
        // Please read through the https://www.perforce.com/generative-ai-policy before you enable this feature
        'enabled'    => true,
        'data_retention_lifetime' => '150 days',
        'timeout' => 500,
        'ai_vendors' => array(
            'ai_model1' => array(
                'ai_vendor' => 'customAI',
                'ai_package_id' => '1', // id should be one & should not modify it
                'ai_package_key' => 'CustomAIPackage', // This is for future purposes when we will be supporting multiple models & will have an AI configuration page on UI
                'ai_package_value' => 'Custom AI', // This is used for displaying the model type on the AI vendor response summary
                'ai_package_type' => "Explain the following code ", // This is prompt type
                'api_end_point' => 'http://myAIVendorAPI/chat/completion',
                'ai_min_char_limit' => 10,
                'ai_max_char_limit' => 70000
            ),
        ),
    ),

After completing these steps, a custom AI adapter has been added to P4 Code Review that supports your in-house AI model.