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.
To write a Custom AI Adapter in P4 Code Review, complete these three stages:
Copy and modify GenericAIAdapter
-
Go to the folder
module/AiAnalysis/src/Service/and create the copy of fileGenericAIAdapter.phpwithin the same folder. -
Name the copied file
CustomAIAdapter.php. Below is an example ofGenericAIAdapter.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);
}
} -
In
CustomAIAdapter.php, change theclassfromGenericAIAdaptertoCustomAIAdapter. -
In
CustomAIAdapter.php, within theexecuteAIRequestfunction, replace$argwith the request format used by your chosen AI model. By default, this is$args['content'] = $content;however, below is an example of a different$argrequest 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 -
Save the changes to in
CustomAIAdapter.phpand build the request. The following line of code inCustomAIAdapter.phpsends the request to the API endpoint (api_end_point) specified inconfig.phpfile.Copy$result = $this->request($this->getApiUrl(), false, $args);
Validate the response
-
Go to the folder
module/AiAnalysis/src/Service/, open the fileAbstractAiAdapter.phpand copy the functionvalidateResult. -
Open
CustomAIAdapter.phpand overwritevalidateResultwith the copied function. -
Modify the copied function in
CustomAIAdapter.phpwith your AI model’s response format. An example of an unmodifiedvalidateResultfunction is as follows:Copyprotected 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
validateResultfunction inCustomAIAdapter.phpso that is can handle the response.For example, if the AI vendor returns the response as an array, update the
validateResultfunction to parse the response. An example of an updatedvalidateResultfunction that can handle the response as an array is shown below.Copyprotected 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;
} -
If your AI model’s response format is different from the example above, modify the format accordingly. You will also need to update
IAiAnalysisHelperwithin theCustomAIAdapter.phpfile. 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": []
},
}
],
} -
Update the
Services.phpfile located inmodule/Application/src/Config/to include an entry forCustomAIAdapteralongside the other AI adapters. Below is an example for the AI adapter entries in this file:Copyconst LM_STUDIO_AI = 'lmStudioAI';
const CUSTOM_AI = 'customAI'; -
Update the
AiServiceFactory.phpfile located inmodule/AiAnalysis/src/Factory/to include an entry forCustomAIAdapteralongside the other AI adapters. Below is an example for the AI adapter entries in this file:Copyconst 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.phpfile found in module/AiAnalysis/config/. -
The
config.phpfile found inSWARM_ROOT/data/.
To update the files, follow these steps:
-
Open the
module.config.phpfile found in module/AiAnalysis/config/. -
At the top of the
module.config.phpfile, add ausestatement for theCustomAIAdapterabove the currentusestatement for theGenericAIAdapter. Below is an example of theseusestatements:Copyuse AiAnalysis\Service\CustomAiAdapter;
use AiAnalysis\Service\GenericAiAdapter; -
Inside the same
module.config.phpfile, find theservice_managerarray and add the following entries.In the
aliasesarray, add:AiServiceFactory::CUSTOMAIADAPTER => CustomAIAdapter::class
In the
factoriesarray, add:CustomAIAdapter::class => AiServiceFactory::classServices::CUSTOM_AI => CustomAIAdapter::class
Below is an example of the updated
service_managerarray in themodule.config.phpfile.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,
],
], -
Open
config.phpfile found inSWARM_ROOT/data/. -
Update the
ai_reviewmodule with the values for the custom AI Adapter.Make sure you updateapi_end_pointwith your AI vendor end point .Below is an example of a modified
ai_reviewmodule: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.