Skip to content

Commit 019334f

Browse files
authored
Initial Release
Initial Release 1.0
1 parent 4bc1223 commit 019334f

34 files changed

+1134
-0
lines changed

Api/CompletionRequestInterface.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
namespace Magecomp\Chatgptaicontent\Api;
3+
4+
use Psr\Http\Message\StreamInterface;
5+
6+
interface CompletionRequestInterface
7+
{
8+
public function getApiPayload(string $text): array;
9+
public function convertToResponse(StreamInterface $stream): string;
10+
public function getJsConfig(): ?array;
11+
public function query(string $prompt): string;
12+
public function getType(): string;
13+
public function getQuery(array $params): string;
14+
}

Api/Data/QueryAttributeInterface.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Magecomp\Chatgptaicontent\Api\Data;
4+
5+
interface QueryAttributeInterface
6+
{
7+
public function getValue(): string;
8+
public function getName(): string;
9+
}

Block/Adminhtml/Product/Helper.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
namespace Magecomp\Chatgptaicontent\Block\Adminhtml\Product;
3+
4+
use Magento\Catalog\Model\Locator\LocatorInterface;
5+
use Magento\Framework\View\Element\Template;
6+
use Magento\Store\Api\StoreRepositoryInterface;
7+
use Magecomp\Chatgptaicontent\Model\Config;
8+
use Magento\Framework\Serialize\Serializer\Json;
9+
10+
class Helper extends Template
11+
{
12+
private Config $config;
13+
private StoreRepositoryInterface $storeRepository;
14+
private LocatorInterface $locator;
15+
private Json $json;
16+
17+
public function __construct(
18+
Template\Context $context,
19+
Config $config,
20+
StoreRepositoryInterface $storeRepository,
21+
LocatorInterface $locator,
22+
Json $json,
23+
array $data = []
24+
) {
25+
parent::__construct($context, $data);
26+
$this->config = $config;
27+
$this->storeRepository = $storeRepository;
28+
$this->locator = $locator;
29+
$this->json = $json;
30+
}
31+
32+
public function getComponentJsonConfig(): string
33+
{
34+
$config = [
35+
// 'component' => 'Magecomp_Chatgptaicontent/js/view/helper',
36+
'serviceUrl' => $this->getUrl('Magecomp_Chatgptaicontent/helper/validate'),
37+
'sku' => $this->locator->getProduct()->getSku(),
38+
'storeId' => $this->locator->getStore()->getId(),
39+
'stores' => $this->getStores()
40+
];
41+
return $this->json->serialize($config);
42+
}
43+
44+
public function getStores(): array
45+
{
46+
$selectedStoreId = (int) $this->locator->getStore()->getId();
47+
$storeIds = $this->config->getEnabledStoreIds();
48+
49+
$results = [];
50+
$first = null;
51+
foreach ($storeIds as $storeId) {
52+
$store = $this->storeRepository->getById($storeId);
53+
if ($selectedStoreId === $storeId) {
54+
$first = $store;
55+
continue;
56+
}
57+
$results[] = [
58+
'label' => $storeId === 0 ? __('Default scope') : $store->getName(),
59+
'store_id' => $storeId,
60+
'selected' => false
61+
];
62+
}
63+
64+
if ($first) {
65+
array_unshift($results, [
66+
'label' => __('Current scope'),
67+
'store_id' => $first->getId(),
68+
'selected' => true
69+
]);
70+
}
71+
72+
return $results;
73+
}
74+
75+
public function toHtml(): string
76+
{
77+
$enabled = $this->config->getValue(Config::XML_PATH_ENABLED);
78+
if (!$enabled) {
79+
return '';
80+
}
81+
return parent::toHtml();
82+
}
83+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace Magecomp\Chatgptaicontent\Controller\Adminhtml\Generate;
4+
5+
use Magecomp\Chatgptaicontent\Model\OpenAI\OpenAiException;
6+
use InvalidArgumentException;
7+
use Magento\Backend\App\Action;
8+
use Magento\Backend\App\Action\Context;
9+
use Magento\Framework\App\Action\HttpPostActionInterface;
10+
use Magento\Framework\Controller\Result\JsonFactory;
11+
use Magecomp\Chatgptaicontent\Model\CompletionConfig;
12+
use Magento\Framework\Exception\LocalizedException;
13+
use Magento\Catalog\Api\ProductRepositoryInterface;
14+
use Magento\Framework\App\RequestInterface;
15+
16+
17+
class Index extends Action implements HttpPostActionInterface
18+
{
19+
public const ADMIN_RESOURCE = 'Magecomp_Chatgptaicontent::generate';
20+
21+
private JsonFactory $jsonFactory;
22+
private CompletionConfig $completionConfig;
23+
protected $productRepository;
24+
protected $request;
25+
26+
public function __construct(
27+
Context $context,
28+
JsonFactory $jsonFactory,
29+
CompletionConfig $completionConfig,
30+
ProductRepositoryInterface $productRepository,
31+
RequestInterface $request
32+
) {
33+
parent::__construct($context);
34+
$this->jsonFactory = $jsonFactory;
35+
$this->completionConfig = $completionConfig;
36+
$this->productRepository = $productRepository;
37+
$this->request = $request;
38+
}
39+
40+
/**
41+
* @throws LocalizedException
42+
*/
43+
public function execute()
44+
{
45+
$resultPage = $this->jsonFactory->create();
46+
47+
$type = $this->completionConfig->getByType(
48+
$this->getRequest()->getParam('type')
49+
);
50+
51+
if ($type === null) {
52+
throw new LocalizedException(__('Invalid request parameters'));
53+
}
54+
55+
try {
56+
$prompt = $this->getRequest()->getParam('prompt');
57+
$result = $type->query($prompt);
58+
59+
} catch (OpenAiException | InvalidArgumentException $e) {
60+
$resultPage->setData([
61+
'error' => $e->getMessage()
62+
]);
63+
return $resultPage;
64+
}
65+
66+
$resultPage->setData([
67+
'result' => $result,'type' => $this->getRequest()->getParam('type')
68+
]);
69+
70+
return $resultPage;
71+
}
72+
/**
73+
* @inheritDoc
74+
*/
75+
protected function _isAllowed()
76+
{
77+
return $this->_authorization->isAllowed('Magecomp_Chatgptaicontent::generate');
78+
}
79+
}

Model/CompletionConfig.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace Magecomp\Chatgptaicontent\Model;
4+
5+
use Magecomp\Chatgptaicontent\Api\CompletionRequestInterface;
6+
use Magento\Framework\App\RequestInterface;
7+
8+
class CompletionConfig
9+
{
10+
/**
11+
* @var CompletionRequestInterface[]
12+
*/
13+
private array $pool;
14+
private Config $config;
15+
private RequestInterface $request;
16+
17+
public function __construct(
18+
array $pool,
19+
Config $config,
20+
RequestInterface $request
21+
) {
22+
$this->pool = $pool;
23+
$this->config = $config;
24+
$this->request = $request;
25+
}
26+
27+
public function getConfig(): array
28+
{
29+
if (!$this->config->getValue(Config::XML_PATH_ENABLED)) {
30+
return [
31+
'targets' => []
32+
];
33+
}
34+
35+
$allowedStores = $this->config->getEnabledStoreIds();
36+
$storeId = (int) $this->request->getParam('store', '0');
37+
if (!in_array($storeId, $allowedStores)) {
38+
return [
39+
'targets' => []
40+
];
41+
}
42+
43+
$targets = [];
44+
45+
foreach ($this->pool as $config) {
46+
$targets[$config->getType()] = $config->getJsConfig();
47+
}
48+
49+
$targets = array_filter($targets);
50+
51+
return [
52+
'targets' => $targets
53+
];
54+
}
55+
56+
public function getByType(string $type): ?CompletionRequestInterface
57+
{
58+
foreach ($this->pool as $config) {
59+
60+
if ($config->getType() === $type) {
61+
return $config;
62+
}
63+
}
64+
return null;
65+
}
66+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
namespace Magecomp\Chatgptaicontent\Model\CompletionRequest;
4+
5+
use Magecomp\Chatgptaicontent\Model\Config;
6+
use Magecomp\Chatgptaicontent\Model\OpenAI\ApiClient;
7+
use Magecomp\Chatgptaicontent\Model\OpenAI\OpenAiException;
8+
use InvalidArgumentException;
9+
use Laminas\Json\Decoder;
10+
use Laminas\Json\Json;
11+
use Magento\Framework\App\Config\ScopeConfigInterface;
12+
use Psr\Http\Message\ResponseInterface;
13+
use Psr\Http\Message\StreamInterface;
14+
use Magecomp\Chatgptaicontent\Model\Normalizer;
15+
16+
abstract class AbstractCompletion
17+
{
18+
public const TYPE = '';
19+
protected const CUT_RESULT_PREFIX = '';
20+
protected ScopeConfigInterface $scopeConfig;
21+
protected ?ApiClient $apiClient = null;
22+
23+
public function __construct(
24+
ScopeConfigInterface $scopeConfig
25+
) {
26+
$this->scopeConfig = $scopeConfig;
27+
}
28+
29+
abstract public function getApiPayload(string $text): array;
30+
31+
private function getClient(): ApiClient
32+
{
33+
$token = $this->scopeConfig->getValue(Config::XML_PATH_TOKEN);
34+
if (empty($token)) {
35+
throw new InvalidArgumentException('API token is missing');
36+
}
37+
if ($this->apiClient === null) {
38+
$this->apiClient = new ApiClient(
39+
$this->scopeConfig->getValue(Config::XML_PATH_BASE_URL),
40+
$this->scopeConfig->getValue(Config::XML_PATH_TOKEN)
41+
);
42+
}
43+
return $this->apiClient;
44+
}
45+
46+
public function getQuery(array $params): string
47+
{
48+
return $params['prompt'] ?? '';
49+
}
50+
51+
/**
52+
* @throws OpenAiException
53+
*/
54+
public function query(string $prompt): string
55+
{
56+
$payload = $this->getApiPayload(
57+
Normalizer::htmlToPlainText($prompt)
58+
);
59+
60+
$result = $this->getClient()->post(
61+
'/v1/completions',
62+
$payload
63+
);
64+
65+
$this->validateResponse($result);
66+
67+
return $this->convertToResponse($result->getBody());
68+
}
69+
70+
protected function validateRequest(string $prompt): void
71+
{
72+
if (empty($prompt) || strlen($prompt) < 10) {
73+
throw new InvalidArgumentException('Invalid query (must be at least 10 characters)');
74+
}
75+
}
76+
77+
/**
78+
* @throws OpenAiException
79+
*/
80+
protected function validateResponse(ResponseInterface $result): void
81+
{
82+
if ($result->getStatusCode() === 401) {
83+
throw new OpenAiException(__('API unauthorized. Token could be invalid.'));
84+
}
85+
86+
if ($result->getStatusCode() >= 500) {
87+
throw new OpenAiException(__('Server error: %1', $result->getReasonPhrase()));
88+
}
89+
90+
$data = Decoder::decode($result->getBody(), Json::TYPE_ARRAY);
91+
92+
if (isset($data['error'])) {
93+
throw new OpenAiException(__(
94+
'%1: %2',
95+
$data['error']['type'] ?? 'unknown',
96+
$data['error']['message'] ?? 'unknown'
97+
));
98+
}
99+
100+
if (!isset($data['choices'])) {
101+
throw new OpenAiException(__('No results were returned by the server'));
102+
}
103+
}
104+
105+
public function convertToResponse(StreamInterface $stream): string
106+
{
107+
$streamText = (string) $stream;
108+
$data = Decoder::decode($streamText, Json::TYPE_ARRAY);
109+
110+
$choices = $data['choices'] ?? [];
111+
$textData = reset($choices);
112+
113+
$text = $textData['text'] ?? '';
114+
$text = trim($text);
115+
$text = trim($text, '"');
116+
117+
if (substr($text, 0, strlen(static::CUT_RESULT_PREFIX)) == static::CUT_RESULT_PREFIX) {
118+
$text = substr($text, strlen(static::CUT_RESULT_PREFIX));
119+
}
120+
121+
return $text;
122+
}
123+
124+
public function getType(): string
125+
{
126+
return static::TYPE;
127+
}
128+
}

0 commit comments

Comments
 (0)