From 7be3614f96153e2d1a3e71be3aa8a206ee2b1433 Mon Sep 17 00:00:00 2001 From: Dillon Ngo Date: Sun, 25 Jan 2026 15:31:00 +0800 Subject: [PATCH] New anthropic claude service to replace openai chatgpt --- .../Services/CreatesClaudeResponse.php | 66 ++++++++++++++++++ .../CreatesClaudeResponseWithFiles.php | 67 +++++++++++++++++++ .../Anthropic/Services/UploadsClaudeFiles.php | 57 ++++++++++++++++ .../FetchByTrakingNoYdPortalV2Processor.php | 48 +++++++++---- config/anthropic.php | 8 +++ 5 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 app/Classes/Modules/Anthropic/Services/CreatesClaudeResponse.php create mode 100644 app/Classes/Modules/Anthropic/Services/CreatesClaudeResponseWithFiles.php create mode 100644 app/Classes/Modules/Anthropic/Services/UploadsClaudeFiles.php create mode 100644 config/anthropic.php diff --git a/app/Classes/Modules/Anthropic/Services/CreatesClaudeResponse.php b/app/Classes/Modules/Anthropic/Services/CreatesClaudeResponse.php new file mode 100644 index 00000000..de4136f4 --- /dev/null +++ b/app/Classes/Modules/Anthropic/Services/CreatesClaudeResponse.php @@ -0,0 +1,66 @@ + 'text', + 'text' => $userPrompt, + ]; + + $response = Http::withHeaders([ + 'x-api-key' => config('anthropic.api_key'), + 'anthropic-version' => config('anthropic.api_version', '2023-06-01'), + 'Content-Type' => 'application/json', + ])->post(config('anthropic.base_url') . '/v1/messages', [ + 'model' => config('anthropic.default_model', 'claude-sonnet-4-5-20250929'), + 'max_tokens' => 8192, + 'messages' => [ + [ + 'role' => 'user', + 'content' => $content, + ], + ], + ]); + + if ($response->successful()) { + return $response->json(); + } else { + Log::info('CreatesClaudeResponse error: ' . $response->body()); + return null; + } + } + catch (\Illuminate\Http\Client\ConnectionException $exception) { + Log::info('ConnectionException' . json_encode($exception)); + throw new ConnectionErrorException( + 'Failed to connect to Claude API', + $exception->getMessage(), + $userPrompt, + $exception->getTraceAsString() + ); + } + catch (\Exception $exception) { + Log::info('Exception' . json_encode($exception)); + throw new MalformedRequestException( + 'Unable to get correct response from Claude API: ' . $exception->getMessage() + ); + } + } +} diff --git a/app/Classes/Modules/Anthropic/Services/CreatesClaudeResponseWithFiles.php b/app/Classes/Modules/Anthropic/Services/CreatesClaudeResponseWithFiles.php new file mode 100644 index 00000000..0acd806c --- /dev/null +++ b/app/Classes/Modules/Anthropic/Services/CreatesClaudeResponseWithFiles.php @@ -0,0 +1,67 @@ + 'text', + 'text' => $userPrompt, + ]; + + // Attach files + foreach ($fileIds as $fileId) { + $content[] = [ + 'type' => 'document', + 'source' => [ + 'type' => 'file', + 'file_id' => $fileId, + ], + ]; + } + + $response = Http::withHeaders([ + 'x-api-key' => config('anthropic.api_key'), + 'anthropic-version' => config('anthropic.api_version', '2023-06-01'), + 'anthropic-beta' => 'files-api-2025-04-14', + 'Content-Type' => 'application/json', + ])->post(config('anthropic.base_url') . '/v1/messages', [ + 'model' => config('anthropic.default_model', 'claude-sonnet-4-5-20250929'), + 'max_tokens' => 16384, + 'messages' => [ + [ + 'role' => 'user', + 'content' => $content, + ], + ], + ]); + + return $response->json(); + } + catch (\Illuminate\Http\Client\ConnectionException $exception) { + $error = 'Failed to connect to Claude API'; + throw new ConnectionErrorException( + $error, + $exception->getMessage(), + $userPrompt, + $exception->getTraceAsString() + ); + } + catch (\Exception $exception) { + throw new MalformedRequestException( + 'Unable to get correct response from Claude API: ' . $exception->getMessage() + ); + } + } +} diff --git a/app/Classes/Modules/Anthropic/Services/UploadsClaudeFiles.php b/app/Classes/Modules/Anthropic/Services/UploadsClaudeFiles.php new file mode 100644 index 00000000..995ff325 --- /dev/null +++ b/app/Classes/Modules/Anthropic/Services/UploadsClaudeFiles.php @@ -0,0 +1,57 @@ + config('anthropic.api_key'), + 'anthropic-version' => '2023-06-01', + 'anthropic-beta' => 'files-api-2025-04-14', + ])->attach( + 'file', + fopen($path, 'r'), + basename($path) + )->post(config('anthropic.base_url') . '/v1/files'); + + $data = $response->json(); + + if ($response->successful() && isset($data['id'])) { + $ids[] = $data['id']; + } else { + Log::error('Claude file upload failed', [ + 'path' => $path, + 'response' => $data, + ]); + } + } + + return $ids; + } + catch (\Illuminate\Http\Client\ConnectionException $exception) { + $error = 'Failed to connect to Claude API'; + throw new ConnectionErrorException( + $error, + $exception->getMessage(), + $exception->getTraceAsString() + ); + } + catch (\Exception $exception) { + throw new MalformedRequestException( + 'Unable to get correct response from Claude API: ' . $exception->getMessage() + ); + } + } +} diff --git a/app/Classes/Modules/PackingLists/Processors/V2/FetchByTrakingNoYdPortalV2Processor.php b/app/Classes/Modules/PackingLists/Processors/V2/FetchByTrakingNoYdPortalV2Processor.php index ac8e34ae..b50520a0 100644 --- a/app/Classes/Modules/PackingLists/Processors/V2/FetchByTrakingNoYdPortalV2Processor.php +++ b/app/Classes/Modules/PackingLists/Processors/V2/FetchByTrakingNoYdPortalV2Processor.php @@ -3,6 +3,7 @@ namespace App\Classes\Modules\PackingLists\Processors\V2; use App\Classes\Exceptions\MalformedRequestException; +use App\Classes\Modules\Anthropic\Services\CreatesClaudeResponse; use App\Classes\Modules\Orders\Services\FetchesDataFromYDPortal; use App\Classes\Modules\OpenAI\Services\CreatesChatGPTResponse; use App\Models\PackingList; @@ -19,14 +20,19 @@ class FetchByTrakingNoYdPortalV2Processor /** @var CreatesChatGPTResponse */ private $createsChatGPTResponse; + /** @var CreatesClaudeResponse */ + private $createsClaudeResponse; + /** * @param FetchesDataFromYDPortal $fetchesDataFRomYDPortal * @param CreatesChatGPTResponse $createsChatGPTResponse + * @param CreatesClaudeResponse $createsClaudeResponse */ - public function __construct(FetchesDataFromYDPortal $fetchesDataFRomYDPortal, CreatesChatGPTResponse $createsChatGPTResponse) + public function __construct(FetchesDataFromYDPortal $fetchesDataFRomYDPortal, CreatesChatGPTResponse $createsChatGPTResponse, CreatesClaudeResponse $createsClaudeResponse) { $this->fetchesDataFRomYDPortal = $fetchesDataFRomYDPortal; $this->createsChatGPTResponse = $createsChatGPTResponse; + $this->createsClaudeResponse = $createsClaudeResponse; } @@ -129,9 +135,9 @@ class FetchByTrakingNoYdPortalV2Processor else{ $isUpdated = false; if($item['tracking']){ - $isTranslatedPreviouly = $this->checkIfTranslationAlreadyExist(!empty($item['tracking']) ? $item['tracking'] : ""); - $result = $isTranslatedPreviouly; - if (!$isTranslatedPreviouly){ + $isTranslatedPreviously = $this->checkIfTranslationAlreadyExist(!empty($item['tracking']) ? $item['tracking'] : ""); + $result = $isTranslatedPreviously; + if (!$isTranslatedPreviously){ $result = $this->translateTracking($item['tracking']); } if ($result && $existingRecord->tracking_english !== $result) { @@ -143,9 +149,9 @@ class FetchByTrakingNoYdPortalV2Processor } if($item['remark']) { - $isTranslatedPreviouly = $this->checkIfTranslationAlreadyExist(!empty($item['remark']) ? $item['remark'] : ""); - $result = $isTranslatedPreviouly; - if (!$isTranslatedPreviouly){ + $isTranslatedPreviously = $this->checkIfTranslationAlreadyExist(!empty($item['remark']) ? $item['remark'] : ""); + $result = $isTranslatedPreviously; + if (!$isTranslatedPreviously){ $result = $this->translateTracking($item['remark']); } @@ -189,7 +195,7 @@ class FetchByTrakingNoYdPortalV2Processor $trackingDate = Carbon::parse($trackingtime); $trackingYear = $trackingDate->year; - if($remark && preg_match('/\b\d{1,2}[-\/.]\d{1,2}\b/', $remark) && $eta === null && $etd === null){ + if($remark && $eta === null && $etd === null){ $existingRecord = YDOrderTracking::where('remark', $remark)->where(function ($query) { $query->whereNotNull('eta') ->orWhereNotNull('etd'); @@ -203,14 +209,27 @@ class FetchByTrakingNoYdPortalV2Processor ]; } - $userPrompt = "Show me the answer in json string without escape, e.g. {'ETA': '', 'ETD': ''}. ".$remark." What is the ETA and ETD?"; + $userPrompt = "You must respond with raw JSON only. + No explanations. + No markdown. + No code fences. + No comments. + Only this JSON structure: + {\"ETA\":\"\", \"ETD\":\"\"} + Dates must be strings in YYYY-MM-DD format. + + {$remark} + What are the ETA and ETD?"; Log::info('prompt: '.$userPrompt); - $result = $this->createsChatGPTResponse->execute($userPrompt); + $result = $this->createsClaudeResponse->execute($userPrompt); Log::info('result (json_encoded): '.json_encode($result)); - $content = $result['choices'][0]['message']['content'] ?? null; + // $content = $result['choices'][0]['message']['content'] ?? null; + $content = $result['content'][0]['text'] ?? '[]'; Log::info('content (json_encoded):'. json_encode($content)); if ($content) { - $parsedContent = json_decode(str_replace("'", '"', $content), true); + $parsedContent = json_decode($content, true); + + // $parsedContent = json_decode(str_replace("'", '"', $content), true); if (is_array($parsedContent) && isset($parsedContent['ETA'], $parsedContent['ETD'])) { Log::info('parsedContent ETA:'. $parsedContent['ETA']); Log::info('parsedContent ETD:'. $parsedContent['ETD']); @@ -310,9 +329,10 @@ class FetchByTrakingNoYdPortalV2Processor { $userPrompt = "Please translate mandarin to english and return result in this format, e.g. {'mandarin': '.$textToTranslate.', 'english': ''}. ".$textToTranslate." "; Log::info('prompt: '.$userPrompt); - $result = $this->createsChatGPTResponse->execute($userPrompt); + $result = $this->createsClaudeResponse->execute($userPrompt); Log::info('result (json_encoded): '.json_encode($result)); - $content = $result['choices'][0]['message']['content'] ?? null; + // $content = $result['choices'][0]['message']['content'] ?? null; + $content = $result['content'][0]['text'] ?? '[]'; Log::info('content (json_encoded):'. json_encode($content)); $parsedContent = json_decode(str_replace("'", '"', $content), true); diff --git a/config/anthropic.php b/config/anthropic.php new file mode 100644 index 00000000..19a929de --- /dev/null +++ b/config/anthropic.php @@ -0,0 +1,8 @@ + env('ANTHROPIC_BASE_URL', 'https://api.anthropic.com'), + 'api_key' => env('ANTHROPIC_API_KEY', ''), + 'api_version' => env('ANTHROPIC_API_VERSION', '2023-06-01'), + // 'is_enabled' => env('OPENAI_IS_ENABLED', 'true'), +];