diff --git a/Modules/LlmDriver/app/BaseClient.php b/Modules/LlmDriver/app/BaseClient.php index d3942120..bd2b2477 100644 --- a/Modules/LlmDriver/app/BaseClient.php +++ b/Modules/LlmDriver/app/BaseClient.php @@ -175,6 +175,11 @@ public function getFunctions(): array { $functions = LlmDriverFacade::getFunctions(); + return $this->remapFunctions($functions); + } + + public function remapFunctions(array $functions): array + { return collect($functions)->map(function ($function) { $function = $function->toArray(); $properties = []; diff --git a/Modules/LlmDriver/app/OpenAiClient.php b/Modules/LlmDriver/app/OpenAiClient.php index b04e9b74..a418cbba 100644 --- a/Modules/LlmDriver/app/OpenAiClient.php +++ b/Modules/LlmDriver/app/OpenAiClient.php @@ -4,8 +4,11 @@ use App\Models\Setting; use Illuminate\Http\Client\Pool; +use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Laravel\Pennant\Feature; +use LlmLaraHub\LlmDriver\Functions\FunctionDto; use LlmLaraHub\LlmDriver\Requests\MessageInDto; use LlmLaraHub\LlmDriver\Responses\CompletionResponse; use LlmLaraHub\LlmDriver\Responses\EmbeddingsResponseDto; @@ -22,19 +25,52 @@ class OpenAiClient extends BaseClient */ public function chat(array $messages): CompletionResponse { + $token = Setting::getSecret('openai', 'api_key'); - $response = OpenAI::chat()->create([ + if (is_null($token)) { + throw new \Exception('Missing open ai api key'); + } + + $payload = [ 'model' => $this->getConfig('openai')['models']['chat_model'], 'messages' => $this->messagesToArray($messages), - ]); + ]; - $results = null; + $payload = $this->modifyPayload($payload); - foreach ($response->choices as $result) { - $results = $result->message->content; + $response = Http::withHeaders([ + 'Content-type' => 'application/json', + ]) + ->withToken($token) + ->baseUrl($this->baseUrl) + ->timeout(240) + ->retry(3, function (int $attempt, \Exception $exception) { + Log::info('OpenAi API Error going to retry', [ + 'attempt' => $attempt, + 'error' => $exception->getMessage(), + ]); + + return 60000; + }) + ->post('/chat/completions', $payload); + + if ($response->failed()) { + Log::error('OpenAi API Error ', [ + 'error' => $response->body(), + ]); + + throw new \Exception('OpenAi API Error Chat'); } - return new CompletionResponse($results); + [$data, $tool_used, $stop_reason] = $this->getContentAndToolTypeFromResults($response); + + return CompletionResponse::from([ + 'content' => $data, + 'tool_used' => $tool_used, + 'stop_reason' => $stop_reason, + 'input_tokens' => data_get($response, 'usage.prompt_tokens', null), + 'output_tokens' => data_get($response, 'usage.completion_tokens', null), + ]); } public function embedData(string $data): EmbeddingsResponseDto @@ -72,6 +108,15 @@ public function completionPool(array $prompts, int $temperature = 0): array $responses = Http::pool(function (Pool $pool) use ($prompts, $token) { foreach ($prompts as $prompt) { + $payload = [ + 'model' => $this->getConfig('openai')['models']['completion_model'], + 'messages' => [ + ['role' => 'user', 'content' => $prompt], + ], + ]; + + $payload = $this->modifyPayload($payload); + $pool->withHeaders([ 'content-type' => 'application/json', 'Authorization' => 'Bearer '.$token, @@ -86,12 +131,7 @@ public function completionPool(array $prompts, int $temperature = 0): array return 60000; }) - ->post('/chat/completions', [ - 'model' => $this->getConfig('openai')['models']['completion_model'], - 'messages' => [ - ['role' => 'user', 'content' => $prompt], - ], - ]); + ->post('/chat/completions', $payload); } }); @@ -100,13 +140,14 @@ public function completionPool(array $prompts, int $temperature = 0): array foreach ($responses as $index => $response) { if ($response->ok()) { - $response = $response->json(); - foreach (data_get($response, 'choices', []) as $result) { - $result = data_get($result, 'message.content', ''); - $results[] = CompletionResponse::from([ - 'content' => $result, - ]); - } + [$data, $tool_used, $stop_reason] = $this->getContentAndToolTypeFromResults($response); + $results[] = CompletionResponse::from([ + 'content' => $data, + 'tool_used' => $tool_used, + 'stop_reason' => $stop_reason, + 'input_tokens' => data_get($response, 'usage.prompt_tokens', null), + 'output_tokens' => data_get($response, 'usage.completion_tokens', null), + ]); } else { Log::error('OpenAi API Error ', [ 'index' => $index, @@ -133,6 +174,8 @@ public function completion(string $prompt, int $temperature = 0): CompletionResp ], ]; + $payload = $this->modifyPayload($payload); + $response = Http::withHeaders([ 'Content-type' => 'application/json', ]) @@ -149,15 +192,84 @@ public function completion(string $prompt, int $temperature = 0): CompletionResp }) ->post('/chat/completions', $payload); - $results = null; + if ($response->failed()) { + Log::error('OpenAi API Error ', [ + 'error' => $response->body(), + ]); - $response = $response->json(); + throw new \Exception('OpenAi API Error Chat'); + } + + [$data, $tool_used, $stop_reason] = $this->getContentAndToolTypeFromResults($response); + + return CompletionResponse::from([ + 'content' => $data, + 'tool_used' => $tool_used, + 'stop_reason' => $stop_reason, + 'input_tokens' => data_get($response, 'usage.prompt_tokens', null), + 'output_tokens' => data_get($response, 'usage.completion_tokens', null), + ]); + } + + public function getContentAndToolTypeFromResults(Response $results): array + { + $results = $results->json(); + $tool_used = null; + $stop_reason = data_get($results, 'choices.0.finish_reason', 'stop'); + $tool_calls = data_get($results, 'choices.0.message.tool_calls', []); + + if ($stop_reason === 'tool_calls' || ! empty($tool_calls)) { + /** + * @TOOD + * The tool should be used here to get the + * output since it might be different + * for each tool + * Right now it assumes the JSON one is being used + */ + foreach ($results['choices'] as $content) { + $tool_used = data_get($content, 'message.tool_calls.0.function.name'); + $data = json_encode(data_get($content, 'message.tool_calls.0.function.arguments', []), JSON_THROW_ON_ERROR); + } + } else { + foreach (data_get($results, 'choices', []) as $result) { + $data = data_get($result, 'message.content', ''); + } + } + + return [$data, $tool_used, $stop_reason]; + } - foreach (data_get($response, 'choices', []) as $result) { - $results = data_get($result, 'message.content', ''); + public function modifyPayload(array $payload): array + { + Log::info('LlmDriver::OpenAi::modifyPayload', [ + 'payload' => $payload, + 'forceTool' => $this->forceTool, + ]); + + if (! empty($this->forceTool)) { + $function = [$this->forceTool]; + $function = $this->remapFunctions($function); + + $payload['tools'] = $function; + $payload['tool_choice'] = [ + 'type' => 'function', + 'function' => [ + 'name' => $this->forceTool->name, + ], + ]; + } else { + //I should add all the tools here? + if (Feature::active('all_tools')) { + $payload['tools'] = $this->getFunctions(); + $payload['tool_choice'] = 'auto'; + } else { + //$payload['tool_choice'] = 'none'; + } } - return new CompletionResponse($results); + $payload = $this->addJsonFormat($payload); + + return $payload; } /** @@ -215,11 +327,22 @@ public function getFunctions(): array { $functions = LlmDriverFacade::getFunctions(); + return $this->remapFunctions($functions); + + } + + /** + * @param FunctionDto[] $functions + */ + public function remapFunctions(array $functions): array + { return collect($functions)->map(function ($function) { $function = $function->toArray(); $properties = []; $required = []; + $type = data_get($function, 'parameters.type', 'object'); + foreach (data_get($function, 'parameters.properties', []) as $property) { $name = data_get($property, 'name'); @@ -230,8 +353,21 @@ public function getFunctions(): array $properties[$name] = [ 'description' => data_get($property, 'description', null), 'type' => data_get($property, 'type', 'string'), - 'enum' => data_get($property, 'enum', []), - 'default' => data_get($property, 'default', null), + ]; + } + + $itemsOrProperties = $properties; + + if ($type === 'array') { + $itemsOrProperties = [ + 'results' => [ + 'type' => 'array', + 'description' => 'The results of prompt', + 'items' => [ + 'type' => 'object', + 'properties' => $properties, + ], + ], ]; } @@ -242,9 +378,8 @@ public function getFunctions(): array 'description' => data_get($function, 'description'), 'parameters' => [ 'type' => 'object', - 'properties' => $properties, + 'properties' => $itemsOrProperties, ], - 'required' => $required, ], ]; })->toArray(); diff --git a/Modules/LlmDriver/tests/Feature/ClaudeClientTest.php b/Modules/LlmDriver/tests/Feature/ClaudeClientTest.php index 17bb14d2..87537fc6 100644 --- a/Modules/LlmDriver/tests/Feature/ClaudeClientTest.php +++ b/Modules/LlmDriver/tests/Feature/ClaudeClientTest.php @@ -247,7 +247,6 @@ public function test_remap_array(): void $results = (new ClaudeClient)->remapFunctions([$dto]); - $this->assertEquals( $shouldBe, $results diff --git a/Modules/LlmDriver/tests/Feature/OpenAiClientTest.php b/Modules/LlmDriver/tests/Feature/OpenAiClientTest.php index e5719d6b..35ab2302 100644 --- a/Modules/LlmDriver/tests/Feature/OpenAiClientTest.php +++ b/Modules/LlmDriver/tests/Feature/OpenAiClientTest.php @@ -4,6 +4,9 @@ use App\Models\Setting; use Illuminate\Support\Facades\Http; +use LlmLaraHub\LlmDriver\Functions\FunctionDto; +use LlmLaraHub\LlmDriver\Functions\ParametersDto; +use LlmLaraHub\LlmDriver\Functions\PropertyDto; use LlmLaraHub\LlmDriver\Requests\MessageInDto; use LlmLaraHub\LlmDriver\Responses\CompletionResponse; use LlmLaraHub\LlmDriver\Responses\EmbeddingsResponseDto; @@ -70,20 +73,52 @@ public function test_completion(): void $this->assertInstanceOf(CompletionResponse::class, $response); } + public function test_remap_array(): void + { + $dto = FunctionDto::from([ + 'name' => 'reporting_json', + 'description' => 'JSON Summary of the report', + 'parameters' => ParametersDto::from([ + 'type' => 'array', + 'properties' => [ + PropertyDto::from([ + 'name' => 'title', + 'description' => 'The title of the section', + 'type' => 'string', + 'required' => true, + ]), + PropertyDto::from([ + 'name' => 'content', + 'description' => 'The content of the section', + 'type' => 'string', + 'required' => true, + ]), + ], + ]), + ]); + + $openaiClient = new \LlmLaraHub\LlmDriver\OpenAiClient(); + $response = $openaiClient->remapFunctions([$dto]); + $shouldBe = get_fixture('openai_payload_modified.json'); + $shouldBe = data_get($shouldBe, 'tools', []); + $this->assertEquals($shouldBe, $response); + + } + public function test_chat(): void { - OpenAI::fake([ - ChatCreateResponse::fake([ + Http::fake([ + 'api.openai.com/*' => Http::response([ 'choices' => [ - [ - 'message' => [ - 'content' => 'awesome!', - ], + 'messages' => [ + 'content' => 'Foo bar', ], ], ]), ]); + Http::preventStrayRequests(); + $openaiClient = new \LlmLaraHub\LlmDriver\OpenAiClient(); $response = $openaiClient->chat([ MessageInDto::from([ @@ -107,9 +142,6 @@ public function test_functions_prompt(): void 'choices' => data_get($data, 'choices', []), ]; - // OpenAI::fake([ - // ChatCreateResponse::fake($response) - // ]); OpenAI::fake([ ChatCreateResponse::fake([ 'choices' => [ diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index cc07e4c1..dad53398 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -45,6 +45,14 @@ public function boot(): void return config('llmdriver.features.reference_collection'); //just not ready yet }); + Feature::define('all_tools', function (User $user) { + if (config('llmdriver.features.all_tools')) { + return true; + } + + return false; + }); + Feature::define('verification_prompt_tags', function (User $user) { return false; }); diff --git a/config/llmdriver.php b/config/llmdriver.php index 91c1fa67..2479e17c 100644 --- a/config/llmdriver.php +++ b/config/llmdriver.php @@ -71,6 +71,7 @@ 'chatv2' => true, 'reference_collection' => env('FEATURE_REFERENCE_COLLECTION', false), 'date_range' => env('FEATURE_DATE_RANGE', true), + 'all_tools' => env('FEATURE_ALL_TOOLS', false), ], 'sources' => [ 'search_driver' => env('LARALAMMA_SEARCH_SOURCE', 'mock'), diff --git a/tests/Feature/ReportingToolTest.php b/tests/Feature/ReportingToolTest.php index dfdc0cf5..2060a824 100644 --- a/tests/Feature/ReportingToolTest.php +++ b/tests/Feature/ReportingToolTest.php @@ -83,7 +83,7 @@ public function test_asks() ]', ]); - LlmDriverFacade::shouldReceive('driver->completionPool') + LlmDriverFacade::shouldReceive('driver->setForceTool->completionPool') ->times(5) ->andReturn([ $dto1, diff --git a/tests/fixtures/claude_payload_all_functions.json b/tests/fixtures/claude_payload_all_functions.json new file mode 100644 index 00000000..8aadad74 --- /dev/null +++ b/tests/fixtures/claude_payload_all_functions.json @@ -0,0 +1,74 @@ +{ + "model": "claude-3-haiku-20240307", + "max_tokens": 4096, + "messages": [ + { + "role": "user", + "content": "### ROLE ###\nRole You are a JSON-only report builder. Your task is to analyze this content and extract requirements for this RFP, outputting the results in a specific JSON format, later we will use these requirements for other steps in the process.\n\n### TASK ###\nTask Analyze the CONTEXT provided, which represents a few pages of many in a report. Extract and summarize each requirement without losing important details. Make sure to break them up as \"small\" as possible so each item is a REQUIREMENT of the document\n\n### FORMAT ###\nOutput in JSON format as an Array of Objects with keys: title (string), content (string).\nNO SURROUNDING TEXT JUST VALID JSON! START WITH [ and END WITH ]\n\n### REPORT BELOW ###\n\n### 4.1 API Issue Resolution\n- Diagnose the current API integration problems\n- Implement necessary fixes to ensure smooth data flow between frontend and backend\n- Optimize API calls for improved performance\n- Ensure proper error handling and logging\n- Update API documentation as needed\n### 4.2 VueJS Banner Enhancement\n- Investigate the cause of low-quality images in the banner\n- Implement image optimization techniques\n- Ensure responsive design across various devices and screen sizes\n- Improve loading times for banner images\n- Implement lazy loading if appropriate\n### 4.3 General Improvements\n- Conduct a brief audit of the existing codebase\n- Suggest and implement any critical optimizations\n- Ensure cross-browser compatibility\n- Improve overall website performance" + } + ], + "tools": [ + { + "name": "summarize_collection", + "description": "NOT FOR SEARCH, This is used when the prompt wants to summarize the entire collection of documents", + "input_schema": { + "type": "object", + "properties": { + "prompt": { + "description": "The prompt the user is using the search for.", + "type": "string" + } + }, + "required": [ + "prompt" + ] + } + }, + { + "name": "search_and_summarize", + "description": "Used to embed users prompt, search database and return summarized results.", + "input_schema": { + "type": "object", + "properties": { + "prompt": { + "description": "This is the prompt the user is using to search the database and may or may not assist the results.", + "type": "string" + } + }, + "required": [] + } + }, + { + "name": "standards_checker", + "description": "Checks the prompt data follows the standards of the documents in the collection", + "input_schema": { + "type": "object", + "properties": { + "prompt": { + "description": "The prompt the user is using to check standards.", + "type": "string" + } + }, + "required": [ + "prompt" + ] + } + }, + { + "name": "reporting_tool", + "description": "Uses Reference collection to generate a report", + "input_schema": { + "type": "object", + "properties": { + "prompt": { + "description": "The prompt the user is using to use as solutions to the report", + "type": "string" + } + }, + "required": [ + "prompt" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/openai_payload_all_functions.json b/tests/fixtures/openai_payload_all_functions.json new file mode 100644 index 00000000..210652e5 --- /dev/null +++ b/tests/fixtures/openai_payload_all_functions.json @@ -0,0 +1,85 @@ +{ + "model": "gpt-3.5-turbo-0125", + "messages": [ + { + "role": "user", + "content": "### ROLE ###\nRole You are a JSON-only report builder. Your task is to analyze this content and extract requirements for this RFP, outputting the results in a specific JSON format, later we will use these requirements for other steps in the process.\n\n### TASK ###\nTask Analyze the CONTEXT provided, which represents a few pages of many in a report. Extract and summarize each requirement without losing important details. Make sure to break them up as \"small\" as possible so each item is a REQUIREMENT of the document\n\n### FORMAT ###\nOutput in JSON format as an Array of Objects with keys: title (string), content (string).\nNO SURROUNDING TEXT JUST VALID JSON! START WITH [ and END WITH ]\n\n### REPORT BELOW ###\n\n### 4.1 API Issue Resolution\n- Diagnose the current API integration problems\n- Implement necessary fixes to ensure smooth data flow between frontend and backend\n- Optimize API calls for improved performance\n- Ensure proper error handling and logging\n- Update API documentation as needed\n### 4.2 VueJS Banner Enhancement\n- Investigate the cause of low-quality images in the banner\n- Implement image optimization techniques\n- Ensure responsive design across various devices and screen sizes\n- Improve loading times for banner images\n- Implement lazy loading if appropriate\n### 4.3 General Improvements\n- Conduct a brief audit of the existing codebase\n- Suggest and implement any critical optimizations\n- Ensure cross-browser compatibility\n- Improve overall website performance" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "summarize_collection", + "description": "NOT FOR SEARCH, This is used when the prompt wants to summarize the entire collection of documents", + "parameters": { + "type": "object", + "properties": { + "prompt": { + "description": "The prompt the user is using the search for.", + "type": "string" + } + }, + "required": [ + "prompt" + ] + } + } + }, + { + "type": "function", + "function": { + "name": "search_and_summarize", + "description": "Used to embed users prompt, search database and return summarized results.", + "parameters": { + "type": "object", + "properties": { + "prompt": { + "description": "This is the prompt the user is using to search the database and may or may not assist the results.", + "type": "string" + } + }, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "standards_checker", + "description": "Checks the prompt data follows the standards of the documents in the collection", + "parameters": { + "type": "object", + "properties": { + "prompt": { + "description": "The prompt the user is using to check standards.", + "type": "string" + } + }, + "required": [ + "prompt" + ] + } + } + }, + { + "type": "function", + "function": { + "name": "reporting_tool", + "description": "Uses Reference collection to generate a report", + "parameters": { + "type": "object", + "properties": { + "prompt": { + "description": "The prompt the user is using to use as solutions to the report", + "type": "string" + } + }, + "required": [ + "prompt" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/openai_payload_modified.json b/tests/fixtures/openai_payload_modified.json new file mode 100644 index 00000000..3ad675a2 --- /dev/null +++ b/tests/fixtures/openai_payload_modified.json @@ -0,0 +1,46 @@ +{ + "model": "gpt-3.5-turbo-0125", + "messages": [ + { + "role": "user", + "content": "### ROLE ###\nRole You are a JSON-only report builder. Your task is to analyze this content and extract requirements for this RFP, outputting the results in a specific JSON format, later we will use these requirements for other steps in the process.\n\n### TASK ###\nTask Analyze the CONTEXT provided, which represents a few pages of many in a report. Extract and summarize each requirement without losing important details. Make sure to break them up as \"small\" as possible so each item is a REQUIREMENT of the document\n\n### FORMAT ###\nOutput in JSON format as an Array of Objects with keys: title (string), content (string).\nNO SURROUNDING TEXT JUST VALID JSON! START WITH [ and END WITH ]\n\n### REPORT BELOW ###\n\n### 4.1 API Issue Resolution\n- Diagnose the current API integration problems\n- Implement necessary fixes to ensure smooth data flow between frontend and backend\n- Optimize API calls for improved performance\n- Ensure proper error handling and logging\n- Update API documentation as needed\n### 4.2 VueJS Banner Enhancement\n- Investigate the cause of low-quality images in the banner\n- Implement image optimization techniques\n- Ensure responsive design across various devices and screen sizes\n- Improve loading times for banner images\n- Implement lazy loading if appropriate\n### 4.3 General Improvements\n- Conduct a brief audit of the existing codebase\n- Suggest and implement any critical optimizations\n- Ensure cross-browser compatibility\n- Improve overall website performance" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "reporting_json", + "description": "JSON Summary of the report", + "parameters": { + "type": "object", + "properties": { + "results": { + "type": "array", + "description": "The results of prompt", + "items": { + "type": "object", + "properties": { + "title": { + "description": "The title of the section", + "type": "string" + }, + "content": { + "description": "The content of the section", + "type": "string" + } + } + } + } + } + } + } + } + ], + "tool_choice": { + "type": "function", + "function": { + "name": "reporting_json" + } + } +} diff --git a/tests/fixtures/openai_payload_modified_001.json b/tests/fixtures/openai_payload_modified_001.json new file mode 100644 index 00000000..8fdc581e --- /dev/null +++ b/tests/fixtures/openai_payload_modified_001.json @@ -0,0 +1,31 @@ +[ + { + "type": "function", + "function": { + "name": "reporting_json", + "description": "JSON Summary of the report", + "parameters": { + "type": "object", + "properties": { + "results": { + "type": "array", + "description": "The results of prompt", + "items": { + "type": "object", + "properties": { + "title": { + "description": "The title of the section", + "type": "string" + }, + "content": { + "description": "The content of the section", + "type": "string" + } + } + } + } + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/results_from_json_tool.json b/tests/fixtures/results_from_json_tool.json new file mode 100644 index 00000000..df4a2d51 --- /dev/null +++ b/tests/fixtures/results_from_json_tool.json @@ -0,0 +1,60 @@ +{ + "results": [ + { + "title": "API Issue Resolution", + "content": "Diagnose the current API integration problems" + }, + { + "title": "API Issue Resolution", + "content": "Implement necessary fixes to ensure smooth data flow between frontend and backend" + }, + { + "title": "API Issue Resolution", + "content": "Optimize API calls for improved performance" + }, + { + "title": "API Issue Resolution", + "content": "Ensure proper error handling and logging" + }, + { + "title": "API Issue Resolution", + "content": "Update API documentation as needed" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Investigate the cause of low-quality images in the banner" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Implement image optimization techniques" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Ensure responsive design across various devices and screen sizes" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Improve loading times for banner images" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Implement lazy loading if appropriate" + }, + { + "title": "General Improvements", + "content": "Conduct a brief audit of the existing codebase" + }, + { + "title": "General Improvements", + "content": "Suggest and implement any critical optimizations" + }, + { + "title": "General Improvements", + "content": "Ensure cross-browser compatibility" + }, + { + "title": "General Improvements", + "content": "Improve overall website performance" + } + ] +} diff --git a/tests/fixtures/results_tinker.json b/tests/fixtures/results_tinker.json new file mode 100644 index 00000000..defffee6 --- /dev/null +++ b/tests/fixtures/results_tinker.json @@ -0,0 +1,58 @@ +[ + { + "title": "API Issue Resolution", + "content": "Diagnose the current API integration problems" + }, + { + "title": "API Issue Resolution", + "content": "Implement necessary fixes to ensure smooth data flow between frontend and backend" + }, + { + "title": "API Issue Resolution", + "content": "Optimize API calls for improved performance" + }, + { + "title": "API Issue Resolution", + "content": "Ensure proper error handling and logging" + }, + { + "title": "API Issue Resolution", + "content": "Update API documentation as needed" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Investigate the cause of low-quality images in the banner" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Implement image optimization techniques" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Ensure responsive design across various devices and screen sizes" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Improve loading times for banner images" + }, + { + "title": "VueJS Banner Enhancement", + "content": "Implement lazy loading if appropriate" + }, + { + "title": "General Improvements", + "content": "Conduct a brief audit of the existing codebase" + }, + { + "title": "General Improvements", + "content": "Suggest and implement any critical optimizations" + }, + { + "title": "General Improvements", + "content": "Ensure cross-browser compatibility" + }, + { + "title": "General Improvements", + "content": "Improve overall website performance" + } +] \ No newline at end of file