mirror of
https://github.com/typemill/typemill.git
synced 2025-07-31 03:10:19 +02:00
Add claude ai service
This commit is contained in:
2
cache/timer.yaml
vendored
2
cache/timer.yaml
vendored
@@ -1 +1 @@
|
||||
licenseupdate: 1743349357
|
||||
licenseupdate: 1743531622
|
||||
|
@@ -15,6 +15,19 @@ class ControllerApiKixote extends Controller
|
||||
{
|
||||
private $error = false;
|
||||
|
||||
private function getSystemMessage()
|
||||
{
|
||||
$system = 'You are a content editor and writing assistant.'
|
||||
. ' If the user prompt does not explicitly specify otherwise,'
|
||||
. ' apply the prompt to the provided article and return only the updated article in Markdown syntax,'
|
||||
. ' without any extra comments or explanations.'
|
||||
. ' If you find the tag <focus></focus>,'
|
||||
. ' modify only the content inside these tags and leave everything else unchanged.'
|
||||
. ' Always return the full article.';
|
||||
|
||||
return $system;
|
||||
}
|
||||
|
||||
public function getKixoteSettings(Request $request, Response $response)
|
||||
{
|
||||
$settingsModel = new Settings();
|
||||
@@ -35,7 +48,6 @@ class ControllerApiKixote extends Controller
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
|
||||
}
|
||||
|
||||
public function updateKixoteSettings(Request $request, Response $response)
|
||||
@@ -247,6 +259,199 @@ class ControllerApiKixote extends Controller
|
||||
return $jwt;
|
||||
}
|
||||
|
||||
public function prompt(Request $request, Response $response)
|
||||
{
|
||||
$params = $request->getParsedBody();
|
||||
|
||||
if (empty($params['prompt']) || !is_string($params['prompt']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Prompt is missing or invalid.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if (empty($params['article']) || !is_string($params['article']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Article is missing or invalid.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$promptname = $params['name'] ?? '';
|
||||
$prompt = $params['prompt'] ?? '';
|
||||
$article = $params['article'] ?? '';
|
||||
$tone = $params['tone'] ?? '';
|
||||
|
||||
$aiservice = $this->settings['aiservice'] ?? false;
|
||||
if(!$aiservice)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'No ai service is selected.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
switch ($aiservice) {
|
||||
case 'chatgpt':
|
||||
$answer = $this->promptChatGPT($promptname, $prompt, $article, $tone);
|
||||
break;
|
||||
|
||||
case 'claude':
|
||||
$answer = $this->promptClaude($promptname, $prompt, $article, $tone);
|
||||
break;
|
||||
|
||||
default:
|
||||
$answer = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if(!isset($answer) or !$answer)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => $this->error
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Success',
|
||||
'answer' => $answer,
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
|
||||
public function promptChatGPT($promptname, $prompt, $article, $tone)
|
||||
{
|
||||
# check if user has accepted
|
||||
|
||||
$settingsModel = new Settings();
|
||||
$model = $this->settings['chatgptModel'] ?? false;
|
||||
$apikey = $settingsModel->getSecret('chatgptKey');
|
||||
|
||||
if (!$model || !$apikey)
|
||||
{
|
||||
$this->error = 'Model or api key for chatgpt is missing, please add it in the system settings.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = 'https://api.openai.com/v1/chat/completions';
|
||||
$authHeader = "Authorization: Bearer $apikey";
|
||||
|
||||
$postdata = [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => $this->getSystemMessage(),
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $prompt . "\n" . $article
|
||||
],
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 8000,
|
||||
];
|
||||
|
||||
$apiservice = new ApiCalls();
|
||||
$apiResponse = $apiservice->makePostCall($url, $postdata, $authHeader);
|
||||
|
||||
if (!$apiResponse)
|
||||
{
|
||||
$this->error = 'Failed to communicate with ChatGPT: ' . $apiservice->getError();
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode($apiResponse, true);
|
||||
|
||||
if(isset($data['error']))
|
||||
{
|
||||
$this->error = 'ChatGPT returned and error';
|
||||
if(isset($data['error']['message']))
|
||||
{
|
||||
$this->error = $data['error']['message'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($data['choices'][0]['message']['content']) || !is_string($data['choices'][0]['message']['content']))
|
||||
{
|
||||
$this->error = 'ChatGPT did not return a valid answer.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$answer = trim($data['choices'][0]['message']['content']);
|
||||
|
||||
return $answer;
|
||||
}
|
||||
|
||||
public function promptClaude($promptname, $prompt, $article, $tone)
|
||||
{
|
||||
# Check if user has accepted
|
||||
$settingsModel = new Settings();
|
||||
$model = $this->settings['claudeModel'] ?? false;
|
||||
$apikey = $settingsModel->getSecret('claudeKey');
|
||||
|
||||
if (!$model || !$apikey)
|
||||
{
|
||||
$this->error = 'Model or API key for Claude is missing, please add it in the system settings.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = 'https://api.anthropic.com/v1/messages';
|
||||
$headers = [
|
||||
"x-api-key: $apikey",
|
||||
"anthropic-version: 2023-06-01"
|
||||
];
|
||||
|
||||
$postdata = [
|
||||
'model' => $model,
|
||||
'system' => $this->getSystemMessage(),
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $prompt . "\n" . $article
|
||||
],
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 8000,
|
||||
];
|
||||
|
||||
$apiservice = new ApiCalls();
|
||||
$apiResponse = $apiservice->makePostCall($url, $postdata, $headers);
|
||||
|
||||
if (!$apiResponse) {
|
||||
$this->error = 'Failed to communicate with Claude: ' . $apiservice->getError();
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode($apiResponse, true);
|
||||
|
||||
if (isset($data['error']))
|
||||
{
|
||||
$this->error = 'Claude API returned an error';
|
||||
if (isset($data['error']['message']))
|
||||
{
|
||||
$this->error = $data['error']['message'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($data['content'][0]['text']) || !is_string($data['content'][0]['text']))
|
||||
{
|
||||
$this->error = 'Claude did not return a valid answer.';
|
||||
return false;
|
||||
}
|
||||
|
||||
return trim($data['content'][0]['text']);
|
||||
}
|
||||
|
||||
# NOT READY YET
|
||||
public function promptKixote(Request $request, Response $response)
|
||||
{
|
||||
$jwt = $this->getKixoteJWT();
|
||||
@@ -296,84 +501,4 @@ class ControllerApiKixote extends Controller
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
|
||||
public function promptChatGPT(Request $request, Response $response): Response
|
||||
{
|
||||
# check if user has accepted
|
||||
|
||||
$params = $request->getParsedBody();
|
||||
|
||||
$params['name'] = $params['name'] ?? '';
|
||||
$params['prompt'] = $params['prompt'] ?? '';
|
||||
$params['article'] = $params['article'] ?? '';
|
||||
$params['tone'] = $params['tone'] ?? '';
|
||||
|
||||
$settingsModel = new Settings();
|
||||
$model = $this->settings['chatgptModel'] ?? false;
|
||||
$apikey = $settingsModel->getSecret('chatgptKey');
|
||||
|
||||
if (empty($params['prompt']) || !is_string($params['prompt']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Prompt is missing or invalid.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if (empty($params['article']) || !is_string($params['article']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Article is missing or invalid.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if (!$model || !$apikey)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Model or api key for chatgpt is missing, please add it in the system settings.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$url = 'https://api.openai.com/v1/chat/completions';
|
||||
$authHeader = "Authorization: Bearer $apikey";
|
||||
|
||||
$postdata = [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => 'You are a content editor and writing assistant. If the user prompt does not explicitly specify otherwise, apply the prompt to the provided article and return only the updated article in Markdown syntax, without any extra comments or explanations. If you find the tag <focus></focus>, modify only the content inside these tags and leave everything else unchanged. Always return the full article.'
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $params['prompt'] . "\n" . $params['article']
|
||||
],
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 2000,
|
||||
];
|
||||
|
||||
$apiservice = new ApiCalls();
|
||||
$apiResponse = $apiservice->makePostCall($url, $postdata, $authHeader);
|
||||
|
||||
if (!$apiResponse)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Failed to communicate with ChatGPT',
|
||||
'error' => $apiservice->getError()
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$data = json_decode($apiResponse, true);
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Success',
|
||||
'data' => $data,
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
}
|
@@ -31,7 +31,7 @@ class ApiCalls
|
||||
return $this->makeFileGetContentsCall($url, 'GET', null, $authHeader);
|
||||
}
|
||||
|
||||
private function makeCurlCall($url, $method, $data = false, $authHeader = '')
|
||||
private function makeCurlCall($url, $method, $data = false, $customHeader = '')
|
||||
{
|
||||
$this->error = null;
|
||||
|
||||
@@ -39,10 +39,7 @@ class ApiCalls
|
||||
"Content-Type: application/json",
|
||||
];
|
||||
|
||||
if (!empty($authHeader))
|
||||
{
|
||||
$headers[] = $authHeader;
|
||||
}
|
||||
$headers = $this->addCustomHeader($customHeader);
|
||||
|
||||
$curl = curl_init($url);
|
||||
if (defined('CURLSSLOPT_NATIVE_CA') && version_compare(curl_version()['version'], '7.71', '>='))
|
||||
@@ -78,7 +75,7 @@ class ApiCalls
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function makeFileGetContentsCall($url, $method, $data = null, $authHeader = '')
|
||||
private function makeFileGetContentsCall($url, $method, $data = null, $customHeader = '')
|
||||
{
|
||||
$this->error = null;
|
||||
|
||||
@@ -86,10 +83,7 @@ class ApiCalls
|
||||
"Content-Type: application/json"
|
||||
];
|
||||
|
||||
if (!empty($authHeader))
|
||||
{
|
||||
$headers[] = $authHeader;
|
||||
}
|
||||
$headers = $this->addCustomHeader($headers, $customHeader);
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
@@ -133,4 +127,24 @@ class ApiCalls
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
private function addCustomHeader(array $header, $customHeader = '')
|
||||
{
|
||||
if(!empty($customHeader))
|
||||
{
|
||||
if(is_array($customHeader))
|
||||
{
|
||||
foreach($customHeader as $cHeader)
|
||||
{
|
||||
$header[] = $cHeader;
|
||||
}
|
||||
}
|
||||
elseif(is_string($customHeader))
|
||||
{
|
||||
$header[] = $customHeader;
|
||||
}
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
}
|
@@ -332,6 +332,7 @@ kixote.component('tab-generate', {
|
||||
activeversion: 0,
|
||||
versions: [],
|
||||
prompt: '',
|
||||
promptError: false,
|
||||
showFocusButton: false,
|
||||
buttonPosition: { top: 0, left: 0 },
|
||||
selection: { start: 0, end: 0, text: '' },
|
||||
@@ -456,6 +457,7 @@ kixote.component('tab-generate', {
|
||||
</div>
|
||||
|
||||
<!-- PROMPT INPUT -->
|
||||
<div v-if="promptError" class="w-full px-8 py-1 bg-rose-500 text-white">{{ promptError }}</div>
|
||||
<div class="w-full bg-stone-600 px-8 py-2">
|
||||
<div class="flex items-start">
|
||||
<span class="text-teal-300 mr-1">Ki></span>
|
||||
@@ -643,7 +645,6 @@ kixote.component('tab-generate', {
|
||||
currentTab(newTab, oldTab) {
|
||||
if (newTab === 'article')
|
||||
{
|
||||
console.info('article');
|
||||
this.$nextTick(() => {
|
||||
this.initAutosize(); // Trigger the resizing when switching back to the article tab
|
||||
});
|
||||
@@ -710,12 +711,12 @@ kixote.component('tab-generate', {
|
||||
{
|
||||
eventBus.$emit('agreetoservice');
|
||||
self.$nextTick(() => {
|
||||
self.resizeAiEditor();
|
||||
self.initAutosize();
|
||||
});
|
||||
})
|
||||
},
|
||||
setCurrentTab(tabValue)
|
||||
{
|
||||
{
|
||||
this.currentTab = tabValue;
|
||||
|
||||
if(tabValue == 'article')
|
||||
@@ -768,10 +769,12 @@ kixote.component('tab-generate', {
|
||||
},
|
||||
submitPrompt()
|
||||
{
|
||||
this.promptError = false;
|
||||
|
||||
var self = this;
|
||||
eventBus.$emit('switchLoading');
|
||||
|
||||
tmaxios.post('/api/v1/chatgpt',{
|
||||
tmaxios.post('/api/v1/prompt',{
|
||||
'prompt': this.prompt,
|
||||
'article': this.versions[this.activeversion]
|
||||
})
|
||||
@@ -780,7 +783,7 @@ kixote.component('tab-generate', {
|
||||
eventBus.$emit('switchLoading');
|
||||
if (response.data.message === 'Success')
|
||||
{
|
||||
let answer = response.data.data.choices[0].message.content;
|
||||
let answer = response.data.answer;
|
||||
answer = answer.replace(/<\/?focus>/g, '');
|
||||
self.versions.push(answer);
|
||||
self.activeversion = self.versions.length-1;
|
||||
@@ -795,12 +798,11 @@ kixote.component('tab-generate', {
|
||||
if(error.response)
|
||||
{
|
||||
self.disabled = false;
|
||||
self.message = handleErrorMessage(error);
|
||||
self.messageClass = 'bg-rose-500';
|
||||
self.promptError = handleErrorMessage(error);
|
||||
self.licensemessage = error.response.data.message;
|
||||
if(error.response.data.errors !== undefined)
|
||||
{
|
||||
self.errors = error.response.data.errors;
|
||||
self.promptError = error.response.data.errors;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -99,7 +99,7 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) {
|
||||
# KIXOTE Remote Services
|
||||
$group->get('/tokenstats', ControllerApiKixote::class . ':getTokenStats')->setName('api.kixote.tokenstats')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
$group->post('/agreetoaiservice', ControllerApiKixote::class . ':agreeToAiService')->setName('api.kixote.serviceagreement')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
$group->post('/chatgpt', ControllerApiKixote::class . ':promptChatGPT')->setName('api.kixote.chatgpt')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
$group->post('/prompt', ControllerApiKixote::class . ':prompt')->setName('api.kixote.prompt')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
|
||||
# API USED ONLY EXTERNALLY
|
||||
$group->get('/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member
|
||||
|
@@ -325,23 +325,35 @@ fieldsetai:
|
||||
none: 'None'
|
||||
# kixote: 'Kixote (not available yet)'
|
||||
chatgpt: 'ChatGPT'
|
||||
claude: 'Claude'
|
||||
chatgptModel:
|
||||
type: select
|
||||
label: 'ChatGPT model'
|
||||
description: "Select the AI model you want to use. Pricing may vary over time, so please check the latest details on [OpenAI's pricing page](https://platform.openai.com/docs/pricing)"
|
||||
options:
|
||||
gpt-4o-mini: 'gpt-4o-mini ($0.6 / 1M output tokens)'
|
||||
gpt-3.5-turbo-0125: 'gpt-3.5-turbo-0125 ($1.50 / 1M output tokens)'
|
||||
gpt-4o: 'gpt-4o ($10.00 / 1M output tokens)'
|
||||
o1-mini: 'o1-mini ($12.00 / 1M output tokens)'
|
||||
o1: 'o1 ($60.00 / 1M output tokens)'
|
||||
gpt-4o-mini: 'gpt-4o-mini ($0.15 / $0.6)'
|
||||
gpt-4o: 'gpt-4o ($2.50 / $10)'
|
||||
gpt-3.5-turbo-0125: 'gpt-3.5-turbo ($0.50 / $1.50)'
|
||||
o1-mini: 'o1-mini ($1.10 / 4.40)'
|
||||
o1: 'o1 ($15 / $60.00)'
|
||||
chatgptKey:
|
||||
type: password
|
||||
autocomplete: new-password
|
||||
generator: false
|
||||
label: 'ChatGPT Api Key'
|
||||
description: "Enter your ChatGPT API key here. You can generate a new key on [OpenAI's platform](https://platform.openai.com/docs/pricing). For security reasons, your API key is secret and will not be visible again after you leave this page."
|
||||
customsmall:
|
||||
type: customfields
|
||||
label: 'Customfield Small'
|
||||
description: "This is a small standard customfield."
|
||||
claudeModel:
|
||||
type: select
|
||||
label: 'Claude model'
|
||||
description: "Select the AI model you want to use. Pricing may vary over time, so please check the latest details on [Claude's pricing page](https://docs.anthropic.com/en/docs/about-claude/models/all-models)"
|
||||
options:
|
||||
claude-3-7-sonnet-20250219: 'Claude 3.7 Sonnet ($3 / $15)'
|
||||
claude-3-5-haiku-20241022: 'Claude 3.5 Haiku ($0.80 / $4)'
|
||||
claude-3-opus-20240229: 'Claude 3 Opus ($15.00 / $75)'
|
||||
claude-3-haiku-20240307: 'Claude 3 Haiku ($0.25 / $1.25)'
|
||||
claudeKey:
|
||||
type: password
|
||||
autocomplete: new-password
|
||||
generator: false
|
||||
label: 'Claude Api Key'
|
||||
description: "Enter your Claude API key here. You can generate a new key on [Claude's console](https://console.anthropic.com). For security reasons, your API key is secret and will not be visible again after you leave this page."
|
||||
|
Reference in New Issue
Block a user