Skip to content

Commit c456929

Browse files
author
M1Screw
committed
feat: anthropic llm service
1 parent 5b730ba commit c456929

11 files changed

+397
-225
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ SSPanel UIM is a multi-purpose proxy service sales management system designed fo
3838
- 重构的商店系统,支持包括但不限于包年包月,按量计费,接入类型计费等计费模式
3939
- 重构的定时任务系统,一个命令即可自动完成所有定时任务
4040
- 深度集成大型语言模型(Large Language Model),支持工单智能回复,文档生成等功能
41-
- 一键对接 OpenAI,Gemini Pro,Hugging Face Hosted APICloudflare Workers AI 等人工智能服务
41+
- 一键对接 OpenAI,Google AI,Vertex AI, Hugging Face Hosted API, Cloudflare Workers AI 和 Anthropic 等大型语言模型服务
4242

4343
- Integrate multiple payment systems such as Alipay F2F, PayPal, Stripe, etc.
4444
- Support multiple mail services, built-in mail queue function, no third-party components are required to use
@@ -49,7 +49,7 @@ SSPanel UIM is a multi-purpose proxy service sales management system designed fo
4949
- Refactored store system, support billing modes including but not limited to annual/monthly, pay-as-you-go, access type billing, etc.
5050
- Refactored scheduled task system, one command can automatically complete all scheduled tasks
5151
- Deep integration of large language models, support intelligent reply to ticket, document generation and other functions
52-
- One-click access to OpenAI, Gemini Pro, Hugging Face Hosted API, Cloudflare Workers AI and other artificial intelligence services
52+
- One-click access to OpenAI, Google AI, Vertex AI, Hugging Face Hosted API, Cloudflare Workers AI and Anthropic and other large language model services
5353

5454
## 安装 / Installation
5555

config/.config.example.php

+6-3
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,17 @@
106106
$_ENV['geoip_locale'] = 'en';
107107

108108
// Large language model powered ticket reply and more
109-
$_ENV['llm_backend'] = 'openai'; // openai/google-ai/huggingface/cf-workers-ai
109+
$_ENV['llm_backend'] = 'openai'; // openai/google-ai/huggingface/cf-workers-ai/anthropic
110110
// OpenAI ChatGPT
111111
$_ENV['openai_api_key'] = '';
112112
$_ENV['openai_model'] = 'gpt-4-turbo-preview';
113113
// Google AI API
114114
$_ENV['google_ai_api_key'] = '';
115-
$_ENV['google_ai_model_id'] = 'gemini-pro';
115+
$_ENV['google_ai_model_id'] = 'gemini-1.5-pro-latest';
116116
// Vertex AI API
117117
$_ENV['vertex_ai_access_token'] = '';
118118
$_ENV['vertex_ai_location'] = 'us-central1';
119-
$_ENV['vertex_ai_model_id'] = 'gemini-pro';
119+
$_ENV['vertex_ai_model_id'] = 'gemini-1.0-pro';
120120
$_ENV['vertex_ai_project_id'] = '';
121121
// Hugging Face Inference API
122122
$_ENV['huggingface_api_key'] = '';
@@ -125,6 +125,9 @@
125125
$_ENV['cf_workers_ai_account_id'] = '';
126126
$_ENV['cf_workers_ai_api_token'] = '';
127127
$_ENV['cf_workers_ai_model_id'] = '@cf/meta/llama-2-7b-chat-int8';
128+
// Anthropic
129+
$_ENV['anthropic_api_key'] = '';
130+
$_ENV['anthropic_model_id'] = 'claude-3-opus-20240229';
128131

129132
// ClientDownload 命令解决 API 访问频率高而被限制使用的 Github access token
130133
$_ENV['github_access_token'] = '';

src/Services/LLM.php

+23-218
Original file line numberDiff line numberDiff line change
@@ -4,241 +4,46 @@
44

55
namespace App\Services;
66

7-
use GuzzleHttp\Client;
8-
use GuzzleHttp\Exception\GuzzleException;
9-
use OpenAI;
10-
use function json_decode;
7+
use App\Services\LLM\Anthropic;
8+
use App\Services\LLM\CloudflareWorkersAI;
9+
use App\Services\LLM\GoogleAI;
10+
use App\Services\LLM\HuggingFace;
11+
use App\Services\LLM\OpenAI;
12+
use App\Services\LLM\VertexAI;
1113

1214
final class LLM
1315
{
14-
/**
15-
* @throws GuzzleException
16-
*/
17-
public static function genTextResponse(string $q): string
16+
public static function getBackend(): VertexAI|CloudflareWorkersAI|GoogleAI|HuggingFace|OpenAI|Anthropic
1817
{
19-
if ($q === '') {
20-
return 'No question provided';
21-
}
22-
2318
return match ($_ENV['llm_backend']) {
24-
'openai' => self::textPromptGPT($q),
25-
'google-ai' => self::textPromptGoogleAI($q),
26-
'vertex-ai' => self::textPromptVertexAI($q),
27-
'huggingface' => self::textPromptHF($q),
28-
'cf-workers-ai' => self::textPromptCF($q),
29-
default => 'No LLM backend configured',
19+
'google-ai' => new GoogleAI(),
20+
'vertex-ai' => new VertexAI(),
21+
'huggingface' => new HuggingFace(),
22+
'cf-workers-ai' => new CloudflareWorkersAI(),
23+
'anthropic' => new Anthropic(),
24+
default => new OpenAI(),
3025
};
3126
}
3227

33-
public static function textPromptGPT(string $q): string
34-
{
35-
if ($_ENV['openai_api_key'] === '') {
36-
return 'OpenAI API key not set';
37-
}
38-
39-
$client = OpenAI::client($_ENV['openai_api_key']);
40-
41-
$response = $client->chat()->create([
42-
'model' => $_ENV['openai_model'],
43-
'messages' => [
44-
[
45-
'role' => 'user',
46-
'content' => $q,
47-
],
48-
],
49-
]);
50-
51-
return $response->choices[0]->message->content;
52-
}
53-
54-
/**
55-
* @throws GuzzleException
56-
*/
57-
public static function textPromptGoogleAI(string $q): string
58-
{
59-
if ($_ENV['google_ai_api_key'] === '') {
60-
return 'Google AI API key not set';
61-
}
62-
63-
$client = new Client();
64-
65-
$api_url = 'https://generativelanguage.googleapis.com/v1/models/' .
66-
$_ENV['google_ai_model_id'] . ':generateContent?key=' . $_ENV['google_ai_api_key'];
67-
68-
$headers = [
69-
'Content-Type' => 'application/json',
70-
];
71-
72-
$data = [
73-
'contents' => [
74-
'parts' => [
75-
[
76-
'text' => $q,
77-
],
78-
],
79-
],
80-
'generationConfig' => [
81-
'temperature' => 1,
82-
'topK' => 1,
83-
'topP' => 1,
84-
'candidateCount' => 1,
85-
'maxOutputTokens' => 2048,
86-
'stopSequences' => [],
87-
],
88-
'safetySettings' => [
89-
[
90-
'category' => 'HARM_CATEGORY_HARASSMENT',
91-
'threshold' => 'BLOCK_NONE',
92-
],
93-
[
94-
'category' => 'HARM_CATEGORY_HATE_SPEECH',
95-
'threshold' => 'BLOCK_NONE',
96-
],
97-
[
98-
'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
99-
'threshold' => 'BLOCK_NONE',
100-
],
101-
[
102-
'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
103-
'threshold' => 'BLOCK_NONE',
104-
],
105-
],
106-
];
107-
108-
$response = json_decode($client->post($api_url, [
109-
'headers' => $headers,
110-
'json' => $data,
111-
'timeout' => 10,
112-
])->getBody()->getContents());
113-
114-
return $response->candidates[0]->content->parts[0]->text;
115-
}
116-
117-
/**
118-
* @throws GuzzleException
119-
*/
120-
public static function textPromptVertexAI(string $q): string
28+
public static function genTextResponse(string $q): string
12129
{
122-
if ($_ENV['vertex_ai_access_token'] === '') {
123-
return 'Vertex AI API key not set';
30+
if ($q === '') {
31+
return 'No question provided';
12432
}
12533

126-
$client = new Client();
127-
128-
$api_url = 'https://' . $_ENV['vertex_ai_location'] .'-aiplatform.googleapis.com/v1/projects/' .
129-
$_ENV['vertex_ai_project_id'] . '/locations/' . $_ENV['vertex_ai_location'] . '/publishers/google/models/' .
130-
$_ENV['vertex_ai_model_id'] . ':streamGenerateContent';
131-
132-
$headers = [
133-
'Authorization' => 'Bearer ' . $_ENV['vertex_ai_access_token'],
134-
'Content-Type' => 'application/json',
135-
];
136-
137-
$data = [
138-
'contents' => [
139-
'parts' => [
140-
[
141-
'text' => $q,
142-
],
143-
],
144-
],
145-
'generationConfig' => [
146-
'temperature' => 1,
147-
'topK' => 1,
148-
'topP' => 1,
149-
'candidateCount' => 1,
150-
'maxOutputTokens' => 2048,
151-
'stopSequences' => [],
152-
],
153-
'safetySettings' => [
154-
[
155-
'category' => 'HARM_CATEGORY_HARASSMENT',
156-
'threshold' => 'BLOCK_NONE',
157-
],
158-
[
159-
'category' => 'HARM_CATEGORY_HATE_SPEECH',
160-
'threshold' => 'BLOCK_NONE',
161-
],
162-
[
163-
'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
164-
'threshold' => 'BLOCK_NONE',
165-
],
166-
[
167-
'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
168-
'threshold' => 'BLOCK_NONE',
169-
],
170-
],
171-
];
172-
173-
$response = json_decode($client->post($api_url, [
174-
'headers' => $headers,
175-
'json' => $data,
176-
'timeout' => 10,
177-
])->getBody()->getContents());
178-
179-
return $response->candidates[0]->content->parts[0]->text;
34+
return self::getClient()->textPrompt($q);
18035
}
18136

182-
/**
183-
* @throws GuzzleException
184-
*/
185-
public static function textPromptHF(string $q): string
37+
public static function genTextResponseWithContext(string $q, array $context = []): string
18638
{
187-
if ($_ENV['huggingface_api_key'] === '' || $_ENV['huggingface_endpoint_url'] === '') {
188-
return 'Hugging Face API key or Endpoint URL not set';
39+
if ($q === '') {
40+
return 'No question provided';
18941
}
19042

191-
$client = new Client();
192-
193-
$headers = [
194-
'Authorization' => 'Bearer ' . $_ENV['huggingface_api_key'],
195-
'Content-Type' => 'application/json',
196-
];
197-
198-
$data = [
199-
'inputs' => [
200-
'question' => $q,
201-
],
202-
];
203-
204-
$response = json_decode($client->post($_ENV['huggingface_endpoint_url'], [
205-
'headers' => $headers,
206-
'json' => $data,
207-
'timeout' => 10,
208-
])->getBody()->getContents());
209-
210-
return $response->answer;
211-
}
212-
213-
/**
214-
* @throws GuzzleException
215-
*/
216-
public static function textPromptCF(string $q): string
217-
{
218-
if ($_ENV['cf_workers_ai_account_id'] === '' || $_ENV['cf_workers_ai_api_token'] === '') {
219-
return 'Cloudflare Workers AI Account ID or API Token not set';
43+
if ($context === []) {
44+
return self::getClient()->textPrompt($q);
22045
}
22146

222-
$client = new Client();
223-
224-
$api_url = 'https://api.cloudflare.com/client/v4/accounts/' .
225-
$_ENV['cf_workers_ai_account_id'] . '/ai/run/' . $_ENV['cf_workers_ai_model_id'];
226-
227-
$headers = [
228-
'Authorization' => 'Bearer ' . $_ENV['cf_workers_ai_api_token'],
229-
'Content-Type' => 'application/json',
230-
];
231-
232-
$data = [
233-
'prompt' => $q,
234-
];
235-
236-
$response = json_decode($client->post($api_url, [
237-
'headers' => $headers,
238-
'json' => $data,
239-
'timeout' => 10,
240-
])->getBody()->getContents());
241-
242-
return $response->result->response;
47+
return self::getClient()->textPromptWithContext($q, $context);
24348
}
24449
}

src/Services/LLM/Anthropic.php

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Services\LLM;
6+
7+
use GuzzleHttp\Client;
8+
use GuzzleHttp\Exception\GuzzleException;
9+
use function json_decode;
10+
11+
final class Anthropic extends Base
12+
{
13+
/**
14+
* @throws GuzzleException
15+
*/
16+
public function textPrompt(string $q): string
17+
{
18+
if ($_ENV['anthropic_api_key'] === '') {
19+
return 'Anthropic API key not set';
20+
}
21+
22+
$client = new Client();
23+
24+
$api_url = 'https://api.anthropic.com/v1/messages';
25+
26+
$headers = [
27+
'x-api-key' => $_ENV['anthropic_api_key'],
28+
'anthropic-version' => '2023-06-01',
29+
'content-type' => 'application/json',
30+
];
31+
32+
$data = [
33+
'model' => $_ENV['anthropic_model_id'],
34+
'max_tokens' => 1024,
35+
'temperature' => 1,
36+
'messages' => [
37+
[
38+
'role' => 'user',
39+
'content' => $q,
40+
],
41+
],
42+
];
43+
44+
$response = json_decode($client->post($api_url, [
45+
'headers' => $headers,
46+
'json' => $data,
47+
'timeout' => 10,
48+
])->getBody()->getContents());
49+
50+
return $response->content[0]->text;
51+
}
52+
53+
public function textPromptWithContext(string $q, array $context): string
54+
{
55+
return '';
56+
}
57+
}

src/Services/LLM/Base.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Services\LLM;
6+
7+
abstract class Base
8+
{
9+
abstract public function textPrompt(string $q): string;
10+
11+
abstract public function textPromptWithContext(string $q, array $context): string;
12+
}

0 commit comments

Comments
 (0)