├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config ├── skeleton.php └── taskallama.php ├── configure.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ ├── create_skeleton_table.php.stub │ └── create_taskallama_table.php.stub ├── resources └── views │ └── .gitkeep └── src ├── Commands ├── SkeletonCommand.php └── TaskallamaCommand.php ├── Facades ├── Skeleton.php └── Taskallama.php ├── Services └── TaskallamaService.php ├── Skeleton.php ├── SkeletonServiceProvider.php ├── Taskallama.php ├── TaskallamaServiceProvider.php └── Traits ├── MakesHttpRequest.php └── StreamHelper.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `taskallama` will be documented in this file. 4 | 5 | ## Initial release - 2024-11-17 6 | 7 | This is initial release. 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) CodingWisely 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taskallama: Laravel Integration with Ollama LLM API 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/codingwisely/taskallama.svg?style=for-the-badge&logo=packagist)](https://packagist.org/packages/codingwisely/taskallama) 4 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/codingwisely/taskallama/fix-php-code-style-issues.yml?branch=main&label=Code%20Style&style=for-the-badge&logo=github)](https://github.com/coding-wisely/taskallama/actions/workflows/fix-php-code-style-issues.yml?branch=main) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/codingwisely/taskallama.svg?style=for-the-badge&logo=packagist)](https://packagist.org/packages/codingwisely/taskallama) 6 | 7 | **Taskallama** is a Laravel package that provides seamless integration with Ollama's LLM API. 8 | It simplifies generating AI-powered content, from professional task writing to conversational agents, with minimal effort. Whether you're building a task management system, an HR assistant for job posts, or blog content generation, Taskallama has you covered. 9 | 10 | **Why i built it?** Simple reasons - i want to implement a ai helper on or Project and task management system at [Taskavel.com](https://taskavel.com) to help me quickly scaffold the task. We gonna use it also on our another SaaS project, Advanced ATS system at [Bagel.blue](https://bagel.blue) to make it easy to create a Job Postings. 11 | 12 | --- 13 | 14 | ## Features 15 | 16 | - Simple API for generating AI responses via the Ollama LLM. 17 | - Supports task creation, conversational AI, embeddings, and more. 18 | - Customizable agent personalities for tailored responses. 19 | - Integration with Laravel Livewire for real-time interactions. 20 | - Configurable options like streaming, model selection, and temperature. 21 | 22 | --- 23 | 24 | --- 25 | 26 | ## Prerequisites 27 | 28 | 1. **Ollama Installation** 29 | 30 | - Taskallama requires [Ollama](https://ollama.com/) to be installed and running locally on your machine. You can download and install Ollama from their official website: 31 | - [Ollama Installation Guide](https://ollama.com/) 32 | 33 | 2. **Ollama Configuration** 34 | 35 | - By default, Taskallama connects to Ollama at `http://127.0.0.1:11434`. Ensure that Ollama is running and accessible at this address. You can update the `OLLAMA_URL` in the config file if it's hosted elsewhere. 36 | 37 | 3. **System Requirements** 38 | - PHP `^8.3` or higher. 39 | - Laravel `^11.0` or higher. 40 | 41 | --- 42 | 43 | ## Installation 44 | 45 | You can install the package via composer: 46 | 47 | ```bash 48 | composer require codingwisely/taskallama 49 | ``` 50 | 51 | Next, you should publish the package's configuration file: 52 | 53 | ```bash 54 | php artisan vendor:publish --tag="taskallama-config" 55 | ``` 56 | 57 | This will publish a `taskallama.php` file in your `config` directory where you can configure your Ollama API key and other settings. 58 | 59 | ```php 60 | return [ 61 | 'model' => env('OLLAMA_MODEL', 'llama3.2'), 62 | 'default_format' => 'json', 63 | 'url' => env('OLLAMA_URL', 'http://127.0.0.1:11434'), 64 | 'default_prompt' => env('OLLAMA_DEFAULT_PROMPT', 'Hello Taskavelian, how can I assist you today?'), 65 | 'connection' => [ 66 | 'timeout' => env('OLLAMA_CONNECTION_TIMEOUT', 300), 67 | ], 68 | ]; 69 | ``` 70 | 71 | ### Usage 72 | 73 | #### Basic Example (non-stream) 74 | 75 | Generate a response using a prompt: 76 | 77 | ```php 78 | use CodingWisely\Taskallama\Facades\Taskallama; 79 | 80 | $response = Taskallama::agent('You are a professional task creator...') 81 | ->prompt('Write a task for implementing a new feature in a SaaS app.') 82 | ->model('llama3.2') 83 | ->options(['temperature' => 0.5]) 84 | ->stream(false) 85 | ->ask(); 86 | 87 | return $response['response']; 88 | ``` 89 | 90 | #### Basic Example (stream) 91 | 92 | Generate a stream response using a prompt: 93 | 94 | ```php 95 | use CodingWisely\Taskallama\Facades\Taskallama; 96 | 97 | return response()->stream(function () use () { 98 | Taskallama::agent('You are a professional task creator...') 99 | ->prompt('Write a task for implementing a new feature in a SaaS app.') 100 | ->model('llama3.2') 101 | ->options(['temperature' => 0.5]) 102 | ->stream(true) 103 | ->ask(); 104 | }, 200, [ 105 | 'Cache-Control' => 'no-cache', 106 | 'X-Accel-Buffering' => 'no', 107 | 'Content-Type' => 'text/event-stream', 108 | ]); 109 | ``` 110 | 111 | #### Chat Example 112 | 113 | Create a conversational agent: 114 | 115 | ```php 116 | use CodingWisely\Taskallama\Facades\Taskallama; 117 | $messages = [ 118 | ['role' => 'user', 'content' => 'Tell me about Laravel'], 119 | ['role' => 'assistant', 'content' => 'Laravel is a PHP framework for web development.'], 120 | ['role' => 'user', 'content' => 'Why is it so popular?'], 121 | ]; 122 | 123 | $response = Taskallama::agent('You are a Laravel expert.') 124 | ->model('llama3.2') 125 | ->options(['temperature' => 0.7]) 126 | ->chat($messages); 127 | ``` 128 | 129 | #### Livewire Integration Example 130 | 131 | Integrate Taskallama into a Livewire component for real-time task generation: 132 | 133 | ```php 134 | namespace App\Livewire; 135 | 136 | use CodingWisely\Taskallama\Taskallama; 137 | use Livewire\Component; 138 | 139 | class AskTaskallama extends Component 140 | { 141 | public $question = ''; 142 | public $response = ''; 143 | 144 | public function ask() 145 | { 146 | if (empty(trim($this->question))) { 147 | $this->response = "Please provide a valid question."; 148 | return; 149 | } 150 | 151 | try { 152 | $this->response = Taskallama::agent('You are a task-writing assistant.') 153 | ->prompt($this->question) 154 | ->model('llama3.2') 155 | ->options(['temperature' => 0]) 156 | ->stream(false) 157 | ->ask()['response'] ?? "No response received."; 158 | } catch (\Exception $e) { 159 | $this->response = "Error: " . $e->getMessage(); 160 | } 161 | } 162 | 163 | public function render() 164 | { 165 | return view('livewire.ask-taskallama'); 166 | } 167 | } 168 | ``` 169 | 170 | #### Embeddings Example 171 | 172 | Generate embeddings for advanced search or semantic analysis: 173 | 174 | ```php 175 | $embeddings = Taskallama::agent('Embedding Assistant') 176 | ->model('llama3.2') 177 | ->options(['temperature' => 0.5]) 178 | ->ask(); 179 | 180 | print_r($embeddings); 181 | ``` 182 | 183 | ### Additional Methods 184 | 185 | #### List Local Models 186 | 187 | ```php 188 | $models = Taskallama::getInstance()->listLocalModels(); 189 | print_r($models); 190 | ``` 191 | 192 | #### Retrieve Model Information 193 | 194 | ```php 195 | $modelInfo = Taskallama::getInstance()->getModelInfo('llama3.2'); 196 | print_r($modelInfo); 197 | ``` 198 | 199 | #### Retrieve Model Settings 200 | 201 | ```php 202 | $modelSettings = Taskallama::getInstance()->getModelSettings('llama3.2'); 203 | print_r($modelSettings); 204 | ``` 205 | 206 | #### Pull or Delete a Model 207 | 208 | If you're pulling model, make sure you set this a background job, as it may take a while to download the model. 209 | 210 | ```php 211 | $pullModel = Taskallama::getInstance()->pull('mistral'); 212 | $deleteModel = Taskallama::getInstance()->delete('mistral'); 213 | ``` 214 | 215 | ### Testing 216 | 217 | Run the tests with: 218 | 219 | ```bash 220 | composer test 221 | ``` 222 | 223 | ### License 224 | 225 | This package is open-source software licensed under the MIT License. Please see the [LICENSE.md](LICENSE.md) file for more information. 226 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codingwisely/taskallama", 3 | "description": "Taskallama is a Laravel package that seamlessly integrates with Ollama’s LLM API to empower your applications with AI-driven text generation, task management assistance, and more. Designed for simplicity and scalability, Taskallama brings the power of language models to your Laravel projects.", 4 | "keywords": [ 5 | "CodingWisely", 6 | "laravel", 7 | "taskallama", 8 | "Ollama LLM", 9 | "larevel ai" 10 | ], 11 | "homepage": "https://github.com/codingwisely/taskallama", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Vladimir Nikolic", 16 | "email": "vladimir@codingwisely.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.3", 22 | "spatie/laravel-package-tools": "^1.16", 23 | "illuminate/contracts": "^10.0||^11.0" 24 | }, 25 | "require-dev": { 26 | "laravel/pint": "^1.14", 27 | "nunomaduro/collision": "^8.1.1||^7.10.0", 28 | "orchestra/testbench": "^9.0.0||^8.22.0", 29 | "pestphp/pest": "^2.34", 30 | "pestphp/pest-plugin-arch": "^2.7", 31 | "pestphp/pest-plugin-laravel": "^2.3", 32 | "phpstan/phpstan": "^2.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "CodingWisely\\Taskallama\\": "src/", 37 | "CodingWisely\\Taskallama\\Database\\Factories\\": "database/factories/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "CodingWisely\\Taskallama\\Tests\\": "tests/", 43 | "Workbench\\App\\": "workbench/app/" 44 | } 45 | }, 46 | "scripts": { 47 | "post-autoload-dump": "@composer run prepare", 48 | "clear": "@php vendor/bin/testbench package:purge-taskallama --ansi", 49 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 50 | "build": [ 51 | "@composer run prepare", 52 | "@php vendor/bin/testbench workbench:build --ansi" 53 | ], 54 | "start": [ 55 | "Composer\\Config::disableProcessTimeout", 56 | "@composer run build", 57 | "@php vendor/bin/testbench serve" 58 | ], 59 | "analyse": "vendor/bin/phpstan analyse", 60 | "test": "vendor/bin/pest", 61 | "test-coverage": "vendor/bin/pest --coverage", 62 | "format": "vendor/bin/pint" 63 | }, 64 | "config": { 65 | "sort-packages": true, 66 | "allow-plugins": { 67 | "pestphp/pest-plugin": true, 68 | "phpstan/extension-installer": true 69 | } 70 | }, 71 | "extra": { 72 | "laravel": { 73 | "providers": [ 74 | "CodingWisely\\Taskallama\\TaskallamaServiceProvider" 75 | ], 76 | "aliases": { 77 | "Taskallama": "CodingWisely\\Taskallama\\Facades\\Taskallama" 78 | } 79 | } 80 | }, 81 | "minimum-stability": "dev", 82 | "prefer-stable": true 83 | } 84 | -------------------------------------------------------------------------------- /config/skeleton.php: -------------------------------------------------------------------------------- 1 | env('OLLAMA_MODEL', 'llama3.2'), 6 | 'default_format' => 'json', 7 | 'url' => env('OLLAMA_URL', 'http://127.0.0.1:11434'), 8 | 'default_prompt' => env('OLLAMA_DEFAULT_PROMPT', 'Hello Taskavelian, how can I assist you today?'), 9 | 'connection' => [ 10 | 'timeout' => env('OLLAMA_CONNECTION_TIMEOUT', 300), 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /configure.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $version) { 90 | if (in_array($name, $names, true)) { 91 | unset($data['require-dev'][$name]); 92 | } 93 | } 94 | 95 | file_put_contents(__DIR__.'/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); 96 | } 97 | 98 | function remove_composer_script($scriptName) 99 | { 100 | $data = json_decode(file_get_contents(__DIR__.'/composer.json'), true); 101 | 102 | foreach ($data['scripts'] as $name => $script) { 103 | if ($scriptName === $name) { 104 | unset($data['scripts'][$name]); 105 | break; 106 | } 107 | } 108 | 109 | file_put_contents(__DIR__.'/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); 110 | } 111 | 112 | function remove_readme_paragraphs(string $file): void 113 | { 114 | $contents = file_get_contents($file); 115 | 116 | file_put_contents( 117 | $file, 118 | preg_replace('/.*/s', '', $contents) ?: $contents 119 | ); 120 | } 121 | 122 | function safeUnlink(string $filename) 123 | { 124 | if (file_exists($filename) && is_file($filename)) { 125 | unlink($filename); 126 | } 127 | } 128 | 129 | function determineSeparator(string $path): string 130 | { 131 | return str_replace('/', DIRECTORY_SEPARATOR, $path); 132 | } 133 | 134 | function replaceForWindows(): array 135 | { 136 | return preg_split('/\\r\\n|\\r|\\n/', run('dir /S /B * | findstr /v /i .git\ | findstr /v /i vendor | findstr /v /i '.basename(__FILE__).' | findstr /r /i /M /F:/ ":author :vendor :package VendorName skeleton migration_table_name vendor_name vendor_slug author@domain.com"')); 137 | } 138 | 139 | function replaceForAllOtherOSes(): array 140 | { 141 | return explode(PHP_EOL, run('grep -E -r -l -i ":author|:vendor|:package|VendorName|skeleton|migration_table_name|vendor_name|vendor_slug|author@domain.com" --exclude-dir=vendor ./* ./.github/* | grep -v '.basename(__FILE__))); 142 | } 143 | 144 | function getGitHubApiEndpoint(string $endpoint): ?stdClass 145 | { 146 | try { 147 | $curl = curl_init("https://api.github.com/{$endpoint}"); 148 | curl_setopt_array($curl, [ 149 | CURLOPT_RETURNTRANSFER => true, 150 | CURLOPT_FOLLOWLOCATION => true, 151 | CURLOPT_HTTPGET => true, 152 | CURLOPT_HTTPHEADER => [ 153 | 'User-Agent: spatie-configure-script/1.0', 154 | ], 155 | ]); 156 | 157 | $response = curl_exec($curl); 158 | $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); 159 | 160 | curl_close($curl); 161 | 162 | if ($statusCode === 200) { 163 | return json_decode($response); 164 | } 165 | } catch (Exception $e) { 166 | // ignore 167 | } 168 | 169 | return null; 170 | } 171 | 172 | function searchCommitsForGitHubUsername(): string 173 | { 174 | $authorName = strtolower(trim(shell_exec('git config user.name'))); 175 | 176 | $committersRaw = shell_exec("git log --author='@users.noreply.github.com' --pretty='%an:%ae' --reverse"); 177 | $committersLines = explode("\n", $committersRaw ?? ''); 178 | $committers = array_filter(array_map(function ($line) use ($authorName) { 179 | $line = trim($line); 180 | [$name, $email] = explode(':', $line) + [null, null]; 181 | 182 | return [ 183 | 'name' => $name, 184 | 'email' => $email, 185 | 'isMatch' => strtolower($name) === $authorName && ! str_contains($name, '[bot]'), 186 | ]; 187 | }, $committersLines), fn ($item) => $item['isMatch']); 188 | 189 | if (empty($committers)) { 190 | return ''; 191 | } 192 | 193 | $firstCommitter = reset($committers); 194 | 195 | return explode('@', $firstCommitter['email'])[0] ?? ''; 196 | } 197 | 198 | function guessGitHubUsernameUsingCli() 199 | { 200 | try { 201 | if (preg_match('/ogged in to github\.com as ([a-zA-Z-_]+).+/', shell_exec('gh auth status -h github.com 2>&1'), $matches)) { 202 | return $matches[1]; 203 | } 204 | } catch (Exception $e) { 205 | // ignore 206 | } 207 | 208 | return ''; 209 | } 210 | 211 | function guessGitHubUsername(): string 212 | { 213 | $username = searchCommitsForGitHubUsername(); 214 | if (! empty($username)) { 215 | return $username; 216 | } 217 | 218 | $username = guessGitHubUsernameUsingCli(); 219 | if (! empty($username)) { 220 | return $username; 221 | } 222 | 223 | // fall back to using the username from the git remote 224 | $remoteUrl = shell_exec('git config remote.origin.url'); 225 | $remoteUrlParts = explode('/', str_replace(':', '/', trim($remoteUrl))); 226 | 227 | return $remoteUrlParts[1] ?? ''; 228 | } 229 | 230 | function guessGitHubVendorInfo($authorName, $username): array 231 | { 232 | $remoteUrl = shell_exec('git config remote.origin.url'); 233 | $remoteUrlParts = explode('/', str_replace(':', '/', trim($remoteUrl))); 234 | 235 | $response = getGitHubApiEndpoint("orgs/{$remoteUrlParts[1]}"); 236 | 237 | if ($response === null) { 238 | return [$authorName, $username]; 239 | } 240 | 241 | return [$response->name ?? $authorName, $response->login ?? $username]; 242 | } 243 | 244 | $gitName = run('git config user.name'); 245 | $authorName = ask('Author name', $gitName); 246 | 247 | $gitEmail = run('git config user.email'); 248 | $authorEmail = ask('Author email', $gitEmail); 249 | $authorUsername = ask('Author username', guessGitHubUsername()); 250 | 251 | $guessGitHubVendorInfo = guessGitHubVendorInfo($authorName, $authorUsername); 252 | 253 | $vendorName = ask('Vendor name', $guessGitHubVendorInfo[0]); 254 | $vendorUsername = ask('Vendor username', $guessGitHubVendorInfo[1] ?? slugify($vendorName)); 255 | $vendorSlug = slugify($vendorUsername); 256 | 257 | $vendorNamespace = str_replace('-', '', ucwords($vendorName)); 258 | $vendorNamespace = ask('Vendor namespace', $vendorNamespace); 259 | 260 | $currentDirectory = getcwd(); 261 | $folderName = basename($currentDirectory); 262 | 263 | $packageName = ask('Package name', $folderName); 264 | $packageSlug = slugify($packageName); 265 | $packageSlugWithoutPrefix = remove_prefix('laravel-', $packageSlug); 266 | 267 | $className = title_case($packageName); 268 | $className = ask('Class name', $className); 269 | $variableName = lcfirst($className); 270 | $description = ask('Package description', "This is my package {$packageSlug}"); 271 | 272 | $usePhpStan = confirm('Enable PhpStan?', true); 273 | $useLaravelPint = confirm('Enable Laravel Pint?', true); 274 | $useDependabot = confirm('Enable Dependabot?', true); 275 | $useLaravelRay = confirm('Use Ray for debugging?', true); 276 | $useUpdateChangelogWorkflow = confirm('Use automatic changelog updater workflow?', true); 277 | 278 | writeln('------'); 279 | writeln("Author : {$authorName} ({$authorUsername}, {$authorEmail})"); 280 | writeln("Vendor : {$vendorName} ({$vendorSlug})"); 281 | writeln("Package : {$packageSlug} <{$description}>"); 282 | writeln("Namespace : {$vendorNamespace}\\{$className}"); 283 | writeln("Class name : {$className}"); 284 | writeln('---'); 285 | writeln('Packages & Utilities'); 286 | writeln('Use Laravel/Pint : '.($useLaravelPint ? 'yes' : 'no')); 287 | writeln('Use Larastan/PhpStan : '.($usePhpStan ? 'yes' : 'no')); 288 | writeln('Use Dependabot : '.($useDependabot ? 'yes' : 'no')); 289 | writeln('Use Ray App : '.($useLaravelRay ? 'yes' : 'no')); 290 | writeln('Use Auto-Changelog : '.($useUpdateChangelogWorkflow ? 'yes' : 'no')); 291 | writeln('------'); 292 | 293 | writeln('This script will replace the above values in all relevant files in the project directory.'); 294 | 295 | if (! confirm('Modify files?', true)) { 296 | exit(1); 297 | } 298 | 299 | $files = (str_starts_with(strtoupper(PHP_OS), 'WIN') ? replaceForWindows() : replaceForAllOtherOSes()); 300 | 301 | foreach ($files as $file) { 302 | replace_in_file($file, [ 303 | ':author_name' => $authorName, 304 | ':author_username' => $authorUsername, 305 | 'author@domain.com' => $authorEmail, 306 | ':vendor_name' => $vendorName, 307 | ':vendor_slug' => $vendorSlug, 308 | 'VendorName' => $vendorNamespace, 309 | ':package_name' => $packageName, 310 | ':package_slug' => $packageSlug, 311 | ':package_slug_without_prefix' => $packageSlugWithoutPrefix, 312 | 'Skeleton' => $className, 313 | 'skeleton' => $packageSlug, 314 | 'migration_table_name' => title_snake($packageSlug), 315 | 'variable' => $variableName, 316 | ':package_description' => $description, 317 | ]); 318 | 319 | match (true) { 320 | str_contains($file, determineSeparator('src/Skeleton.php')) => rename($file, determineSeparator('./src/'.$className.'.php')), 321 | str_contains($file, determineSeparator('src/SkeletonServiceProvider.php')) => rename($file, determineSeparator('./src/'.$className.'ServiceProvider.php')), 322 | str_contains($file, determineSeparator('src/Facades/Skeleton.php')) => rename($file, determineSeparator('./src/Facades/'.$className.'.php')), 323 | str_contains($file, determineSeparator('src/Commands/SkeletonCommand.php')) => rename($file, determineSeparator('./src/Commands/'.$className.'Command.php')), 324 | str_contains($file, determineSeparator('database/migrations/create_skeleton_table.php.stub')) => rename($file, determineSeparator('./database/migrations/create_'.title_snake($packageSlugWithoutPrefix).'_table.php.stub')), 325 | str_contains($file, determineSeparator('config/skeleton.php')) => rename($file, determineSeparator('./config/'.$packageSlugWithoutPrefix.'.php')), 326 | str_contains($file, 'README.md') => remove_readme_paragraphs($file), 327 | default => [], 328 | }; 329 | } 330 | 331 | if (! $useLaravelPint) { 332 | safeUnlink(__DIR__.'/.github/workflows/fix-php-code-style-issues.yml'); 333 | safeUnlink(__DIR__.'/pint.json'); 334 | } 335 | 336 | if (! $usePhpStan) { 337 | safeUnlink(__DIR__.'/phpstan.neon.dist'); 338 | safeUnlink(__DIR__.'/phpstan-baseline.neon'); 339 | safeUnlink(__DIR__.'/.github/workflows/phpstan.yml'); 340 | 341 | remove_composer_deps([ 342 | 'phpstan/extension-installer', 343 | 'phpstan/phpstan-deprecation-rules', 344 | 'phpstan/phpstan-phpunit', 345 | 'larastan/larastan', 346 | ]); 347 | 348 | remove_composer_script('phpstan'); 349 | } 350 | 351 | if (! $useDependabot) { 352 | safeUnlink(__DIR__.'/.github/dependabot.yml'); 353 | safeUnlink(__DIR__.'/.github/workflows/dependabot-auto-merge.yml'); 354 | } 355 | 356 | if (! $useLaravelRay) { 357 | remove_composer_deps(['spatie/laravel-ray']); 358 | } 359 | 360 | if (! $useUpdateChangelogWorkflow) { 361 | safeUnlink(__DIR__.'/.github/workflows/update-changelog.yml'); 362 | } 363 | 364 | confirm('Execute `composer install` and run tests?') && run('composer install && composer test'); 365 | 366 | confirm('Let this script delete itself?', true) && unlink(__FILE__); 367 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /database/migrations/create_taskallama_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-wisely/taskallama/d6e81495d8485759f5262fa06796acdc3de4a7b8/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Commands/SkeletonCommand.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | 17 | return self::SUCCESS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Commands/TaskallamaCommand.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | 17 | return self::SUCCESS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Facades/Skeleton.php: -------------------------------------------------------------------------------- 1 | baseUrl = config('taskallama.url'); 16 | } 17 | 18 | public function listLocalModels() 19 | { 20 | return $this->sendRequest('/api/tags', [], 'get'); 21 | } 22 | 23 | public function showModelInformation(string $modelName) 24 | { 25 | return $this->sendRequest('/api/show', ['name' => $modelName]); 26 | } 27 | 28 | public function copyModel(string $source, string $destination) 29 | { 30 | return $this->sendRequest('/api/copy', [ 31 | 'source' => $source, 32 | 'destination' => $destination, 33 | ]); 34 | } 35 | 36 | public function deleteModel(string $modelName) 37 | { 38 | return $this->sendRequest('/api/delete', ['name' => $modelName], 'delete'); 39 | } 40 | 41 | public function pullModel(string $modelName) 42 | { 43 | return $this->sendRequest('/api/pull', ['name' => $modelName]); 44 | } 45 | 46 | public function generateEmbeddings(string $modelName, string $prompt) 47 | { 48 | return $this->sendRequest('/api/embeddings', [ 49 | 'model' => $modelName, 50 | 'prompt' => $prompt, 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Skeleton.php: -------------------------------------------------------------------------------- 1 | name('skeleton') 20 | ->hasConfigFile() 21 | ->hasViews() 22 | ->hasMigration('create_migration_table_name_table') 23 | ->hasCommand(SkeletonCommand::class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Taskallama.php: -------------------------------------------------------------------------------- 1 | modelService = $modelService; 43 | } 44 | 45 | // Method to set raw property 46 | public static function raw($raw): static 47 | { 48 | $instance = self::getInstance(); 49 | $instance->raw = $raw; 50 | 51 | return $instance; 52 | } 53 | 54 | public static function getInstance(): static 55 | { 56 | if (! self::$instance) { 57 | self::$instance = app(self::class); 58 | } 59 | 60 | return self::$instance; 61 | } 62 | 63 | public static function agent(string $agent): static 64 | { 65 | $instance = self::getInstance(); 66 | $instance->agent = $agent; 67 | 68 | return $instance; 69 | } 70 | 71 | public static function prompt(string $prompt): static 72 | { 73 | $instance = self::getInstance(); 74 | $instance->prompt = $prompt; 75 | 76 | return $instance; 77 | } 78 | 79 | public static function model(string $model): static 80 | { 81 | $instance = self::getInstance(); 82 | $instance->selectedModel = $model; 83 | $instance->model = $model; 84 | 85 | return $instance; 86 | } 87 | 88 | public static function format(string $format): static 89 | { 90 | $instance = self::getInstance(); 91 | $instance->format = $format; 92 | 93 | return $instance; 94 | } 95 | 96 | public static function options(array $options = []): static 97 | { 98 | $instance = self::getInstance(); 99 | $instance->options = array_merge($instance->options, $options); 100 | 101 | return $instance; 102 | } 103 | 104 | public static function stream(bool $stream = false): static 105 | { 106 | $instance = self::getInstance(); 107 | $instance->stream = $stream; 108 | 109 | return $instance; 110 | } 111 | 112 | public static function ask(): array|Response 113 | { 114 | $instance = self::getInstance(); 115 | $requestData = [ 116 | 'model' => $instance->model, 117 | 'prompt' => $instance->prompt, 118 | 'options' => $instance->options, 119 | 'stream' => $instance->stream, 120 | 'raw' => $instance->raw, 121 | 'keep_alive' => $instance->keepAlive, 122 | ]; 123 | 124 | //for testing 'raw mode' 125 | if ($instance->raw) { 126 | unset($requestData['system'], $requestData['format']); 127 | } else { 128 | $requestData['system'] = $instance->agent; 129 | $requestData['format'] = $instance->format; 130 | } 131 | 132 | if ($instance->image) { 133 | $requestData['images'] = [$instance->image]; 134 | } 135 | 136 | $response = $instance->sendRequest('/api/generate', $requestData); 137 | 138 | if ($instance->stream && $response instanceof ResponseInterface) { 139 | return self::doProcessStream($response, function ($chunk) { 140 | try { 141 | logger()->info('taskallama stream chunk:', [$chunk]); 142 | echo $chunk; 143 | ob_flush(); 144 | flush(); 145 | } catch (\Exception $e) { 146 | logger()->error('Error processing stream chunk:', [ 147 | 'error' => $e->getMessage(), 148 | 'chunk' => $chunk, 149 | ]); 150 | } 151 | }); 152 | } 153 | 154 | return $response; 155 | } 156 | 157 | public static function chat(array $conversation): array 158 | { 159 | $instance = self::getInstance(); 160 | 161 | $response = $instance->sendRequest('/api/chat', [ 162 | 'model' => $instance->model, 163 | 'messages' => $conversation, 164 | 'format' => $instance->format, 165 | 'options' => $instance->options ?: (object) [], 166 | 'stream' => $instance->stream, 167 | ]); 168 | 169 | // Handle streaming 170 | if ($instance->stream && $response instanceof ResponseInterface) { 171 | return self::doProcessStream($response, function ($chunk) { 172 | try { 173 | logger()->info('taskallama stream chunk:', [$chunk]); 174 | echo $chunk; 175 | ob_flush(); 176 | flush(); 177 | } catch (\Exception $e) { 178 | logger()->error('Error processing stream chunk:', [ 179 | 'error' => $e->getMessage(), 180 | 'chunk' => $chunk, 181 | ]); 182 | } 183 | }); 184 | } 185 | 186 | return $response; 187 | } 188 | 189 | public static function embeddings(string $prompt): array 190 | { 191 | $instance = self::getInstance(); 192 | 193 | return $instance->sendRequest('/api/embeddings', [ 194 | 'model' => $instance->model, 195 | 'prompt' => $prompt, 196 | ]); 197 | } 198 | 199 | public static function listLocalModels(): array 200 | { 201 | $instance = self::getInstance(); 202 | 203 | return $instance->modelService->listLocalModels(); 204 | } 205 | 206 | public static function getModelInfo(string $model): array 207 | { 208 | $instance = self::getInstance(); 209 | 210 | return $instance->modelService->showModelInformation($model); 211 | } 212 | 213 | public static function pull(string $model): array 214 | { 215 | $instance = self::getInstance(); 216 | 217 | return $instance->modelService->pullModel($model); 218 | } 219 | 220 | public static function delete(string $model): array 221 | { 222 | $instance = self::getInstance(); 223 | 224 | return $instance->modelService->deleteModel($model); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/TaskallamaServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('taskallama') 19 | ->hasConfigFile(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/MakesHttpRequest.php: -------------------------------------------------------------------------------- 1 | request($method, $url, [ 22 | 'json' => $data, 23 | 'stream' => true, 24 | 'timeout' => config('taskallama.connection.timeout'), 25 | ]); 26 | 27 | return $response; 28 | } else { 29 | $response = Http::timeout(config('taskallama.connection.timeout'))->$method($url, $data); 30 | 31 | return $response->json(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Traits/StreamHelper.php: -------------------------------------------------------------------------------- 1 | getBody(); 12 | // Use a buffer to hold incomplete JSON object parts 13 | $buffer = ''; 14 | 15 | $jsonObjects = []; 16 | 17 | while (! $body->eof()) { 18 | $chunk = $body->read(256); 19 | $buffer .= $chunk; 20 | 21 | // Split the buffer by newline as a delimiter 22 | while (($pos = strpos($buffer, "\n")) !== false) { 23 | $json = substr($buffer, 0, $pos); 24 | $buffer = substr($buffer, $pos + 1); 25 | 26 | // Attempt to decode the JSON object 27 | $data = json_decode($json, true); 28 | $data = $data['response'] ?? $data; 29 | 30 | // Check if JSON decoding was successful 31 | // if so, pass the object to the handler 32 | if ($data !== null) { 33 | $handleJsonObject($data); 34 | $jsonObjects[] = $data; 35 | 36 | // Ensure real-time output for streams 37 | ob_flush(); 38 | flush(); 39 | } else { 40 | // If JSON decoding fails, it means this is an incomplete object, 41 | // So, we append this part back to the buffer to be processed with the next chunk 42 | $buffer = $json."\n".$buffer; 43 | break; 44 | } 45 | } 46 | } 47 | 48 | // Process any remaining data in the buffer 49 | if (! empty($buffer)) { 50 | $data = json_decode($buffer, true); 51 | $data = $data['response'] ?? $data; 52 | 53 | if ($data !== null) { 54 | $handleJsonObject($data); 55 | $jsonObjects[] = $data; 56 | 57 | // Ensure final real-time output 58 | ob_flush(); 59 | flush(); 60 | } else { 61 | // we shouldn't hit this, except when ollama is unexpectedly killed 62 | throw new \Exception('Incomplete JSON object remaining: '.$buffer); 63 | } 64 | } 65 | 66 | return $jsonObjects; 67 | } 68 | } 69 | --------------------------------------------------------------------------------