ai_upgrade_assistant-0.2.0-alpha2/src/Service/HuggingFaceService.php
src/Service/HuggingFaceService.php
<?php
namespace Drupal\ai_upgrade_assistant\Service;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\State\StateInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* Service for interacting with HuggingFace's Hub.
*/
class HuggingFaceService {
use DependencySerializationTrait;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The logger factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The subscription manager.
*
* @var \Drupal\ai_upgrade_assistant\Service\SubscriptionManager
*/
protected $subscriptionManager;
/**
* The HuggingFace API base URL.
*
* @var string
*/
protected const API_BASE_URL = 'https://huggingface.co/api';
/**
* The inference API base URL.
*
* @var string
*/
protected const INFERENCE_API_URL = 'https://api-inference.huggingface.co/models';
/**
* The organization name.
*
* @var string
*/
protected const ORGANIZATION = 'thronedigital';
/**
* The dataset repository name.
*
* @var string
*/
protected const DATASET_REPO = 'thronedigital/drupal-upgrade-patterns';
/**
* Available code models.
*
* @var array
*/
protected const CODE_MODELS = [
'Salesforce/codet5-base' => [
'name' => 'CodeT5',
'description' => 'Open source code-aware encoder-decoder model',
'type' => 'encoder-decoder',
'license' => 'BSD-3-Clause',
],
'bigcode/starcoder' => [
'name' => 'StarCoder',
'description' => 'Open source code model trained on public code',
'type' => 'decoder',
'license' => 'Apache-2.0',
],
'codellama/codellama-7b' => [
'name' => 'Code Llama',
'description' => 'Meta\'s open source code model',
'type' => 'decoder',
'license' => 'LLAMA 2',
],
];
/**
* Constructs a new HuggingFaceService.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\ai_upgrade_assistant\Service\SubscriptionManager $subscription_manager
* The subscription manager.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
ClientInterface $http_client,
LoggerChannelFactoryInterface $logger_factory,
StateInterface $state,
SubscriptionManager $subscription_manager
) {
$this->configFactory = $config_factory;
$this->httpClient = $http_client;
$this->loggerFactory = $logger_factory->get('ai_upgrade_assistant');
$this->state = $state;
$this->subscriptionManager = $subscription_manager;
}
/**
* Gets the HuggingFace API token from state.
*
* @return string|null
* The API token if configured, NULL otherwise.
*/
protected function getApiToken() {
return $this->state->get('ai_upgrade_assistant.huggingface_token');
}
/**
* Makes an authenticated request to the HuggingFace API.
*
* @param string $endpoint
* The API endpoint.
* @param array $options
* Request options.
*
* @return \Psr\Http\Message\ResponseInterface
* The response.
*/
protected function makeAuthenticatedRequest($endpoint, array $options = []) {
$options['headers'] = ($options['headers'] ?? []) + [
'Authorization' => 'Bearer ' . $this->getApiToken(),
'X-Organization' => static::ORGANIZATION,
];
try {
return $this->httpClient->request(
$options['method'] ?? 'GET',
static::API_BASE_URL . '/' . ltrim($endpoint, '/'),
$options
);
}
catch (GuzzleException $e) {
$this->loggerFactory->error('HuggingFace API request failed: @error', [
'@error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* Shares an upgrade pattern to the public dataset.
*
* @param array $pattern
* The upgrade pattern to share.
*
* @return bool
* TRUE if the pattern was shared successfully, FALSE otherwise.
*/
public function sharePattern(array $pattern) {
try {
$token = $this->getApiToken();
if (empty($token)) {
$this->loggerFactory->warning('Cannot share pattern: HuggingFace API token not configured.');
return FALSE;
}
$response = $this->makeAuthenticatedRequest('datasets/' . static::DATASET_REPO . '/push', [
'method' => 'POST',
'json' => [
'commit_message' => 'Add new upgrade pattern',
'content' => $pattern,
],
]);
if ($response->getStatusCode() === 200) {
$this->loggerFactory->info('Successfully shared upgrade pattern to HuggingFace dataset.');
return TRUE;
}
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Error sharing pattern to HuggingFace: @error', ['@error' => $e->getMessage()]);
}
return FALSE;
}
/**
* Gets statistics about the public dataset.
*
* @return array
* Array containing dataset statistics:
* - total_patterns: Total number of patterns
* - contributors: Number of unique contributors
* - last_update: Timestamp of last update
* - success_rate: Pattern success rate percentage
*/
public function getDatasetStats() {
try {
$response = $this->makeAuthenticatedRequest('datasets/' . static::DATASET_REPO . '/stats');
if ($response->getStatusCode() === 200) {
$stats = json_decode($response->getBody(), TRUE);
// Cache the stats in state for 1 hour
$this->state->set('ai_upgrade_assistant.dataset_stats', [
'data' => $stats,
'timestamp' => \Drupal::time()->getRequestTime(),
]);
return $stats;
}
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Error fetching HuggingFace dataset stats: @error', ['@error' => $e->getMessage()]);
// Return cached stats if available
$cached = $this->state->get('ai_upgrade_assistant.dataset_stats');
if ($cached && ($cached['timestamp'] > (\Drupal::time()->getRequestTime() - 3600))) {
return $cached['data'];
}
}
return [];
}
/**
* Gets a preview of patterns that will be shared.
*
* @param int $limit
* Maximum number of patterns to return.
*
* @return array
* Array of patterns ready for sharing.
*/
public function getPatternPreview($limit = 5) {
try {
$response = $this->makeAuthenticatedRequest('datasets/' . static::DATASET_REPO . '/preview', [
'query' => ['limit' => $limit],
]);
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody(), TRUE);
}
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Error fetching pattern preview: @error', ['@error' => $e->getMessage()]);
}
return [];
}
/**
* Downloads the latest patterns from the dataset.
*
* @return array
* Array of downloaded patterns.
*/
public function downloadPatterns() {
try {
$response = $this->makeAuthenticatedRequest('datasets/' . static::DATASET_REPO . '/patterns');
if ($response->getStatusCode() === 200) {
$patterns = json_decode($response->getBody(), TRUE);
// Cache the patterns
$this->state->set('ai_upgrade_assistant.patterns', [
'data' => $patterns,
'timestamp' => \Drupal::time()->getRequestTime(),
]);
return $patterns;
}
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Error downloading patterns: @error', ['@error' => $e->getMessage()]);
// Return cached patterns if available
$cached = $this->state->get('ai_upgrade_assistant.patterns');
if ($cached && ($cached['timestamp'] > (\Drupal::time()->getRequestTime() - 86400))) {
return $cached['data'];
}
}
return [];
}
/**
* Creates a new dataset repository if it doesn't exist.
*
* @return bool
* TRUE if successful, FALSE otherwise.
*/
public function initializeDataset() {
try {
// Check if dataset exists
try {
$this->makeAuthenticatedRequest('datasets/' . static::DATASET_REPO);
$this->loggerFactory->info('Dataset repository already exists');
return TRUE;
}
catch (GuzzleException $e) {
if ($e->getCode() !== 404) {
throw $e;
}
}
// Create dataset repository
$response = $this->makeAuthenticatedRequest('datasets/create', [
'method' => 'POST',
'json' => [
'name' => 'drupal-upgrade-patterns',
'organization' => static::ORGANIZATION,
'private' => FALSE,
'type' => 'code',
'description' => 'Collection of Drupal upgrade patterns for automated code updates',
'tags' => ['drupal', 'code-upgrade', 'migration'],
],
]);
$this->loggerFactory->info('Created new dataset repository');
return $response->getStatusCode() === 200;
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Failed to initialize dataset: @error', [
'@error' => $e->getMessage(),
]);
return FALSE;
}
}
/**
* Validates a HuggingFace API token.
*
* @param string $token
* The token to validate.
*
* @return bool
* TRUE if the token is valid, FALSE otherwise.
*/
public function validateToken($token) {
try {
$response = $this->makeAuthenticatedRequest('whoami', [
'headers' => [
'Authorization' => 'Bearer ' . $token,
],
]);
return $response->getStatusCode() === 200;
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Error validating HuggingFace token: @error', ['@error' => $e->getMessage()]);
return FALSE;
}
}
/**
* Analyzes code using a specified model.
*
* @param string $code
* The code to analyze.
* @param string $model_id
* The model ID to use (default: Salesforce/codet5-base).
*
* @return array
* The analysis results.
*
* @throws \Exception
* If the user doesn't have access to AI features.
*/
public function analyzeCode($code, $model_id = 'Salesforce/codet5-base') {
// Check subscription
if (!$this->subscriptionManager->hasFeature('ai_code_analysis')) {
throw new \Exception('AI code analysis requires a Pro or Enterprise subscription.');
}
if ($this->subscriptionManager->hasExceededLimits()) {
throw new \Exception('API usage limit exceeded for your subscription tier.');
}
if (!isset(static::CODE_MODELS[$model_id])) {
throw new \InvalidArgumentException("Invalid model ID: $model_id");
}
try {
$response = $this->makeAuthenticatedRequest('models/' . $model_id, [
'method' => 'POST',
'json' => [
'inputs' => $code,
'options' => [
'wait_for_model' => true,
'use_cache' => true,
],
],
]);
// Track usage
$this->subscriptionManager->trackUsage('code_analysis');
return json_decode((string) $response->getBody(), TRUE);
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Code analysis failed: @error', [
'@error' => $e->getMessage(),
]);
return NULL;
}
}
/**
* Creates or updates a pattern in the dataset.
*
* @param array $pattern
* The upgrade pattern data.
* @param string $version
* The Drupal version this pattern applies to.
* @param array $metadata
* Additional metadata about the pattern.
*
* @return bool
* TRUE if successful, FALSE otherwise.
*
* @throws \Exception
* If the user doesn't have access to pattern creation.
*/
public function createPattern(array $pattern, $version, array $metadata = []) {
// Basic pattern creation is allowed for all tiers
$canUseAI = $this->subscriptionManager->hasFeature('ai_code_analysis');
try {
$data = [
'pattern' => $pattern,
'drupal_version' => $version,
'metadata' => $metadata + [
'timestamp' => time(),
'validated' => FALSE,
'success_count' => 0,
'failure_count' => 0,
'organization' => static::ORGANIZATION,
'tier' => $this->subscriptionManager->getCurrentTier(),
],
];
// Add AI analysis only for pro/enterprise tiers
if ($canUseAI && !empty($pattern['before_code'])) {
$data['analysis'] = [
'before' => $this->analyzeCode($pattern['before_code']),
'after' => $this->analyzeCode($pattern['after_code']),
];
}
$response = $this->makeAuthenticatedRequest('datasets/' . static::DATASET_REPO . '/push', [
'method' => 'POST',
'json' => $data,
]);
// Track pattern creation
$this->subscriptionManager->trackUsage('pattern_created');
return $response->getStatusCode() === 200;
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Failed to create pattern: @error', [
'@error' => $e->getMessage(),
]);
return FALSE;
}
}
/**
* Validates a pattern against existing code samples.
*
* @param array $pattern
* The pattern to validate.
*
* @return array
* Validation results with confidence scores.
*/
public function validatePattern(array $pattern) {
try {
// Use CodeBERT to analyze pattern similarity
$similarity = $this->analyzeCode(
$pattern['before_code'] . "\n" . $pattern['after_code'],
'Salesforce/codet5-base'
);
// Use GraphCodeBERT for structure analysis
$structure = $this->analyzeCode(
$pattern['before_code'] . "\n" . $pattern['after_code'],
'bigcode/starcoder'
);
return [
'similarity_score' => $similarity['score'] ?? 0,
'structure_score' => $structure['score'] ?? 0,
'is_valid' => ($similarity['score'] ?? 0) > 0.8,
'suggestions' => $this->generateSuggestions($similarity, $structure),
];
}
catch (\Exception $e) {
$this->loggerFactory->error('Pattern validation failed: @error', [
'@error' => $e->getMessage(),
]);
return [
'similarity_score' => 0,
'structure_score' => 0,
'is_valid' => FALSE,
'error' => $e->getMessage(),
];
}
}
/**
* Generates improvement suggestions based on analysis results.
*
* @param array $similarity
* Similarity analysis results.
* @param array $structure
* Structure analysis results.
*
* @return array
* List of suggestions.
*/
protected function generateSuggestions(array $similarity, array $structure) {
$suggestions = [];
if (($similarity['score'] ?? 0) < 0.8) {
$suggestions[] = 'Pattern may need more specific code context';
}
if (($structure['score'] ?? 0) < 0.8) {
$suggestions[] = 'Consider preserving more of the original code structure';
}
return $suggestions;
}
/**
* Updates the organization profile on HuggingFace.
*
* @param array $settings
* The organization settings.
*
* @return bool
* TRUE if successful, FALSE otherwise.
*/
public function updateOrganizationProfile(array $settings) {
try {
$data = [
'username' => $settings['username'],
'fullName' => $settings['full_name'],
'type' => $settings['type'],
];
// Add optional fields if they exist
if (!empty($settings['homepage'])) {
$data['websiteUrl'] = $settings['homepage'];
}
if (!empty($settings['github_username'])) {
$data['githubUsername'] = $settings['github_username'];
}
if (!empty($settings['twitter_username'])) {
$data['twitterUsername'] = $settings['twitter_username'];
}
if (!empty($settings['ai_interests'])) {
$data['interests'] = $settings['ai_interests'];
}
// Handle logo upload if present
if (!empty($settings['logo'])) {
$file = \Drupal\file\Entity\File::load($settings['logo'][0]);
if ($file) {
$data['logoUrl'] = $file->createFileUrl(FALSE);
}
}
$response = $this->makeAuthenticatedRequest('organizations/' . static::ORGANIZATION, [
'method' => 'PUT',
'json' => $data,
]);
$this->loggerFactory->info('Updated organization profile on HuggingFace');
return $response->getStatusCode() === 200;
}
catch (GuzzleException $e) {
$this->loggerFactory->error('Failed to update organization profile: @error', [
'@error' => $e->getMessage(),
]);
return FALSE;
}
}
}
