├── src ├── AgentBase.php ├── Task.php ├── AutoPHPInternalTool.php ├── AutoPHPCLIOutputUtils.php ├── PrioritizationTaskAgent.php ├── TaskManager.php ├── CreationTaskAgent.php ├── ExecutionTaskAgent.php └── AutoPHP.php ├── simple-test.php ├── LICENSE.md └── composer.json /src/AgentBase.php: -------------------------------------------------------------------------------- 1 | run(); 12 | 13 | echo $answer; 14 | -------------------------------------------------------------------------------- /src/Task.php: -------------------------------------------------------------------------------- 1 | $objectiveCompleted, 'answer' => $answer]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/AutoPHPCLIOutputUtils.php: -------------------------------------------------------------------------------- 1 | name} ({$task->description})".PHP_EOL; 19 | 20 | continue; 21 | } 22 | 23 | if (is_null($task->result)) { 24 | $liItems .= "\t⚪️ - {$task->name} ({$task->description})".PHP_EOL; 25 | 26 | continue; 27 | } 28 | 29 | $result = self::truncateString($verbose, $task->result, $task->name); 30 | 31 | if ($task->wasSuccessful) { 32 | $liItems .= "\t🟢 - {$task->name} ({$task->description}) - {$result}".PHP_EOL; 33 | } else { 34 | $liItems .= "\t🔴 - {$task->name} ({$task->description})".PHP_EOL; 35 | } 36 | } 37 | $liItems .= $separator; 38 | 39 | $this->render($liItems, $verbose); 40 | } 41 | 42 | private static function truncateString(bool $verbose, string $message, ?string $title = null): string 43 | { 44 | $maxSize = 250; 45 | if ($title) { 46 | $maxSize -= strlen($title); 47 | } 48 | 49 | if (! $verbose) { 50 | $message = str_replace('\n', '', $message); 51 | $message = str_replace('\r', '', $message); 52 | if (strlen($message) > $maxSize) { 53 | $message = substr($message, 0, $maxSize).'...'; 54 | } 55 | } 56 | 57 | return $message; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llphant/autophp", 3 | "description": "AutoPHP is a librairy to help you build Generative AI applications agents", 4 | "keywords": ["php", "openai", "GPT-4", "api", "language", "LLM", "vectorstore", "ollama", "anthropic", "mistral"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Maxime Thoonsen" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.1.31", 13 | "theodo-group/llphant": "^0.9.1" 14 | 15 | }, 16 | "require-dev": { 17 | "laravel/pint": "v1.15.3", 18 | "mockery/mockery": "^1.6.12", 19 | "pestphp/pest": "^v2.36.0", 20 | "pestphp/pest-plugin-arch": "^2.7.0", 21 | "pestphp/pest-plugin-type-coverage": "2.8.0", 22 | "phpstan/phpstan": "1.10.55", 23 | "rector/rector": "^0.16.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "AutoPHP\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Tests\\": "tests/" 33 | } 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true, 37 | "config": { 38 | "sort-packages": true, 39 | "preferred-install": "dist", 40 | "allow-plugins": { 41 | "pestphp/pest-plugin": true, 42 | "php-http/discovery": false 43 | } 44 | }, 45 | "scripts": { 46 | "lint": "pint -v", 47 | "fix-lint": "pint -v --repair", 48 | "refactor": "rector --debug", 49 | "test:lint": "pint --test -v", 50 | "test:refactor": "rector --dry-run", 51 | "test:types": "phpstan analyse --ansi", 52 | "test:type-coverage": "pest ./tests --type-coverage --min=100", 53 | "test:unit": "pest ./tests/Unit --colors=always", 54 | "test:int": "pest ./tests/Integration --colors=always", 55 | "test": [ 56 | "@test:lint", 57 | "@test:refactor", 58 | "@test:types", 59 | "@test:type-coverage", 60 | "@test:unit" 61 | ] 62 | }, 63 | "archive": { 64 | "exclude": ["examples"] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/PrioritizationTaskAgent.php: -------------------------------------------------------------------------------- 1 | taskManager->getUnachievedTasks()) <= 1) { 19 | return $this->taskManager->getNextTask(); 20 | } 21 | if ($this->taskManager->getAchievedTasks() === []) { 22 | return $this->taskManager->getNextTask(); 23 | } 24 | 25 | $unachievedTasks = ''; 26 | foreach ($this->taskManager->getUnachievedTasks() as $key => $task) { 27 | $unachievedTasks .= "id:{$key} name: {$task->name}."; 28 | } 29 | $achievedTasks = $this->taskManager->getAchievedTasksNameAndResult(); 30 | $prompt = "Consider the ultimate objective of your team: {$objective}. 31 | You are a tasks prioritization AI tasked with prioritizing the following tasks: {$unachievedTasks}." 32 | ." To help you the previous tasks are: {$achievedTasks}." 33 | .' Return the id of the task that we should do next'; 34 | 35 | $this->outputAgent->renderTitleAndMessageGreen('🤖 PrioritizationTaskAgent.', 'Prompt: '.$prompt, $this->verbose); 36 | 37 | $response = $this->openAIChat->generateText($prompt); 38 | 39 | $this->outputAgent->renderTitleAndMessageGreen('🤖 PrioritizationTaskAgent.', 'Response: '.$response, $this->verbose); 40 | 41 | // Look for the first number in the response 42 | if (preg_match('/\d+/', $response, $matches)) { 43 | $firstNumber = $matches[0]; 44 | if (isset($this->taskManager->getUnachievedTasks()[$firstNumber])) { 45 | 46 | return $this->taskManager->getUnachievedTasks()[$firstNumber]; 47 | } 48 | } 49 | 50 | return $this->taskManager->getNextTask(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/TaskManager.php: -------------------------------------------------------------------------------- 1 | tasks = array_merge($this->tasks, $tasksObject); 31 | } 32 | 33 | public function getNextTask(): ?Task 34 | { 35 | foreach ($this->tasks as $task) { 36 | if ($task->result === null) { 37 | return $task; 38 | } 39 | } 40 | 41 | return null; 42 | } 43 | 44 | /** 45 | * @return Task[] 46 | */ 47 | public function getAchievedTasks(): array 48 | { 49 | $achievedTasks = []; 50 | foreach ($this->tasks as $task) { 51 | if ($task->result !== null) { 52 | $achievedTasks[] = $task; 53 | } 54 | } 55 | 56 | return $achievedTasks; 57 | } 58 | 59 | /** 60 | * @return Task[] 61 | */ 62 | public function getUnachievedTasks(): array 63 | { 64 | $unachievedTasks = []; 65 | foreach ($this->tasks as $task) { 66 | if ($task->result === null) { 67 | $unachievedTasks[] = $task; 68 | } 69 | } 70 | 71 | return $unachievedTasks; 72 | } 73 | 74 | public function getAchievedTasksNameAndResult(): string 75 | { 76 | $previousCompletedTask = ''; 77 | foreach ($this->getAchievedTasks() as $task) { 78 | $previousCompletedTask .= "Task: {$task->name}. Result: {$task->result} \n"; 79 | } 80 | 81 | return $previousCompletedTask; 82 | } 83 | 84 | public function getUnachievedTasksNameAndResult(): string 85 | { 86 | $unachievedTasks = ''; 87 | foreach ($this->getUnachievedTasks() as $task) { 88 | $unachievedTasks .= "Task: {$task->name}."; 89 | } 90 | 91 | return $unachievedTasks; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/CreationTaskAgent.php: -------------------------------------------------------------------------------- 1 | taskManager, 30 | 'add tasks to the list of tasks to be completed', [$param], [$param]); 31 | $this->openAIChat->addTool($addTasksFunction); 32 | foreach ($tools as $tool) { 33 | $this->openAIChat->addTool($tool); 34 | } 35 | $this->openAIChat->requiredFunction = $addTasksFunction; 36 | } 37 | 38 | /** 39 | * Generates new tasks using OpenAI API based on previous tasks' results. 40 | * 41 | * @param FunctionInfo[] $tools 42 | */ 43 | public function createTasks(string $objective, array $tools): void 44 | { 45 | if (empty($this->taskManager->getAchievedTasks())) { 46 | $prompt = 'You are a task creation AI. ' 47 | ."The objective is: {$objective}." 48 | .'You need to create tasks to do the objective.'; 49 | 50 | } else { 51 | // Join the task list into a string for the prompt 52 | $unachievedTasks = implode(', ', array_column($this->taskManager->getUnachievedTasks(), 'name')); 53 | $achievedTasks = $this->taskManager->getAchievedTasksNameAndResult(); 54 | $prompt = 'You are a task creation AI that uses the result of an execution agent' 55 | ."Your objective is: {$objective}," 56 | ." The previous tasks are: {$achievedTasks}." 57 | ." These are incomplete tasks: {$unachievedTasks}." 58 | .' Based on the result of previous tasks, create new tasks to do the objective but ONLY if needed.' 59 | .' You MUST avoid create duplicated tasks.'; 60 | } 61 | 62 | // We don't handle the response because the function will be executed 63 | $this->openAIChat->generateText($prompt); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ExecutionTaskAgent.php: -------------------------------------------------------------------------------- 1 | openAIChat->setTools($functions); 31 | } 32 | 33 | public function run( 34 | string $objective, 35 | Task $task, 36 | string $additionalContext = '', 37 | ): string { 38 | $prompt = "You are part of a big project. You need to perform the following task: {$task->description} 39 | {$additionalContext} 40 | If you have enough information or if you know that the task has been done, answer with only the relevant information related to the task. 41 | Your answer:"; 42 | $this->outputAgent->renderTitleAndMessageGreen('🤖 ExecutionTaskAgent.', 'Prompt: '.$prompt, $this->verbose); 43 | 44 | // Send prompt to OpenAI API and retrieve the result 45 | try { 46 | $stringOrFunctionInfo = $this->openAIChat->generateTextOrReturnFunctionCalled($prompt); 47 | if ($stringOrFunctionInfo instanceof FunctionInfo) { 48 | // $toolResponse can be a very long string 49 | $toolResponse = FunctionRunner::run($stringOrFunctionInfo); 50 | $refinedData = is_string($toolResponse) ? $this->refineData($objective, $task, 51 | $toolResponse) : 'no data returned'; 52 | 53 | $message = "The tool {$stringOrFunctionInfo->name} was used and this is the result: 54 | (data from tool) {$refinedData} (end of data from tool)"; 55 | $newContext = $additionalContext.$message; 56 | 57 | $prompt = "You are part of a big project. You are performing the following task: {$task->description}. {$newContext}. 58 | If you have enough information from using the tool or if you know that the task has been done, answer with only the relevant information related to the task. 59 | Your answer:"; 60 | 61 | $stringOrFunctionInfo = (new OpenAIChat())->generateText($prompt); 62 | } 63 | $task->wasSuccessful = true; 64 | 65 | return $stringOrFunctionInfo; 66 | } catch (\Exception $e) { 67 | var_dump('error'.$e->getMessage()); 68 | $task->wasSuccessful = false; 69 | 70 | return 'Task failed'; 71 | } 72 | } 73 | 74 | private function refineData( 75 | string $objective, 76 | Task $task, 77 | ?string $dataToRefine, 78 | int $counter = 0 79 | ): string { 80 | if (is_null($dataToRefine)) { 81 | return ''; 82 | } 83 | 84 | // Naive approach: if the data is not too long, we don't refine it 85 | if (strlen($dataToRefine) <= self::MAX_REFINEMENT_REQUEST_LENGTH) { 86 | return $dataToRefine; 87 | } 88 | if ($counter >= $this->refinementIterations) { 89 | return $dataToRefine; 90 | } 91 | $document = new Document(); 92 | $document->content = $dataToRefine; 93 | $splittedDocuments = DocumentSplitter::splitDocument($document, self::MAX_REFINEMENT_REQUEST_LENGTH); 94 | 95 | $refinedData = ''; 96 | 97 | $gpt = new OpenAIChat(); 98 | 99 | $splittedDocumentsTotal = count($splittedDocuments); 100 | $splittedDocumentsCounter = 0; 101 | foreach ($splittedDocuments as $splittedDocument) { 102 | $splittedDocumentsCounter++; 103 | $this->outputAgent->render('📄Refining data: '.$splittedDocumentsCounter.' / '.$splittedDocumentsTotal, 104 | $this->verbose); 105 | //TODO: we should ignore part of the data that is not relevant to the task 106 | $prompt = "You are part of a big project. The main objective is {$objective}. You need to perform the following task: {$task->description}. 107 | You MUST be very concise and only extract information that can help for the task and objective. 108 | If you can't find any useful information from the given data, you MUST answer with 'NULL'. 109 | The data you must use: (start of the data){$splittedDocument->content}(end of the data)."; 110 | $refinedData .= $gpt->generateText($prompt).' '; 111 | } 112 | 113 | if ($this->verbose) { 114 | $this->outputAgent->renderTitleAndMessageOrange('Refined data: ', $refinedData, $this->verbose); 115 | } 116 | 117 | return $this->refineData($objective, $task, $refinedData, $counter + 1); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/AutoPHP.php: -------------------------------------------------------------------------------- 1 | taskManager = new TaskManager(); 35 | $this->creationTaskAgent = new CreationTaskAgent($this->taskManager, new OpenAIChat(), $tools, $verbose, 36 | $this->outputAgent); 37 | $this->prioritizationTaskAgent = new PrioritizationTaskAgent($this->taskManager, new OpenAIChat(), $verbose, 38 | $this->outputAgent); 39 | $this->defaultModelName = OpenAIChatModel::Gpt4Turbo->value; 40 | } 41 | 42 | public function run(int $maxIteration = 10): string 43 | { 44 | $this->outputAgent->renderTitle('🐘 AutoPHP 🐘', '🎯 Objective: '.$this->objective, $this->verbose); 45 | $this->creationTaskAgent->createTasks($this->objective, $this->tools); 46 | $this->outputAgent->printTasks($this->verbose, $this->taskManager->tasks); 47 | $currentTask = $this->prioritizationTaskAgent->prioritizeTask($this->objective); 48 | $iteration = 1; 49 | while ($currentTask instanceof Task && $maxIteration >= $iteration) { 50 | $this->outputAgent->render('Iteration '.$iteration, false); 51 | $this->outputAgent->printTasks($this->verbose, $this->taskManager->tasks, $currentTask); 52 | 53 | // TODO: add a mechanism to retrieve short-term / long-term memory 54 | $previousCompletedTask = $this->taskManager->getAchievedTasksNameAndResult(); 55 | $context = "Previous tasks status: {$previousCompletedTask}"; 56 | $this->checkForCancellation(); 57 | 58 | // TODO: add a mechanism to get the best tool for a given Task 59 | $executionAgent = new ExecutionTaskAgent($this->tools, new OpenAIChat(), $this->verbose); 60 | $currentTask->result = $executionAgent->run($this->objective, $currentTask, $context); 61 | 62 | $this->outputAgent->printTasks($this->verbose, $this->taskManager->tasks); 63 | if ($finalResult = $this->getObjectiveResult()) { 64 | $this->outputAgent->renderResult($finalResult); 65 | 66 | return $finalResult; 67 | } 68 | $this->checkForCancellation(); 69 | 70 | if (count($this->taskManager->getUnachievedTasks()) <= 0) { 71 | $this->creationTaskAgent->createTasks($this->objective, $this->tools); 72 | } 73 | 74 | $currentTask = $this->prioritizationTaskAgent->prioritizeTask($this->objective); 75 | $this->checkForCancellation(); 76 | $iteration++; 77 | } 78 | 79 | return "failed to achieve objective in {$iteration} iterations"; 80 | } 81 | 82 | private function getObjectiveResult(): ?string 83 | { 84 | $config = new OpenAIConfig(); 85 | $config->model = $this->defaultModelName; 86 | $model = new OpenAIChat($config); 87 | $autoPHPInternalTool = new AutoPHPInternalTool(); 88 | $enoughDataToFinishFunction = FunctionBuilder::buildFunctionInfo($autoPHPInternalTool, 'objectiveStatus'); 89 | $model->setTools([$enoughDataToFinishFunction]); 90 | $model->requiredFunction = $enoughDataToFinishFunction; 91 | 92 | $achievedTasks = $this->taskManager->getAchievedTasksNameAndResult(); 93 | $unachievedTasks = $this->taskManager->getUnachievedTasksNameAndResult(); 94 | 95 | $prompt = "Consider the ultimate objective of your team: {$this->objective}." 96 | .'Based on the result from previous tasks, you need to determine if the objective has been achieved.' 97 | ."The previous tasks are: {$achievedTasks}." 98 | ."Remaining tasks: {$unachievedTasks}." 99 | ."If the objective has been completed, give the exact answer to the objective {$this->objective}."; 100 | 101 | $stringOrFunctionInfo = $model->generateTextOrReturnFunctionCalled($prompt); 102 | if (!is_array($stringOrFunctionInfo)) { 103 | return null; 104 | } 105 | $functionCalled = $stringOrFunctionInfo[0]; 106 | 107 | 108 | if (! $functionCalled instanceof FunctionInfo) { 109 | // Shouldn't be null as OPENAI should call the function 110 | return null; 111 | } 112 | 113 | $objectiveData = FunctionRunner::run($functionCalled); 114 | if (! is_array($objectiveData)) { 115 | // The wrong function has probably been called, shouldn't happen 116 | return null; 117 | } 118 | 119 | if ($objectiveData['objectiveCompleted']) { 120 | return $objectiveData['answer']; 121 | } 122 | 123 | return null; 124 | } 125 | 126 | private function checkForCancellation(): void 127 | { 128 | 129 | // You can uncomment this and add a CONTROL_FILE_PATH const to have a mean of controlling the execution of stopping AutoPHP in the background 130 | // without killing the process 131 | 132 | // if (file_exists(self::CONTROL_FILE_PATH)) { 133 | // $content = file_get_contents(self::CONTROL_FILE_PATH); 134 | // if (! $content) { 135 | // echo json_encode(['end' => 'control file empty or not readable']); 136 | // exit(); 137 | // } 138 | // if (trim($content) !== 'ok') { 139 | // echo json_encode(['end' => 'control file not ok']); 140 | // exit(); 141 | // } 142 | // } 143 | // 144 | // echo json_encode(['end' => 'end']); 145 | // exit(); 146 | } 147 | } 148 | --------------------------------------------------------------------------------