From bfe1dad207fb91787653cd077dc36d4b892d08f2 Mon Sep 17 00:00:00 2001 From: Sascha Greuel Date: Thu, 19 Oct 2023 06:16:00 +0100 Subject: [PATCH] [Release] Initial commit --- .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/bug.yml | 63 ++++++ .github/ISSUE_TEMPLATE/documentation.yml | 30 +++ .github/ISSUE_TEMPLATE/feature.yml | 37 ++++ .github/PULL_REQUEST_TEMPLATE.md | 22 ++ .github/workflows/create-release.yml | 142 +++++++++++++ .github/workflows/update-contributors.yml | 26 +++ .github/workflows/validate-pr.yml | 105 ++++++++++ .gitignore | 5 + .php-cs-fixer.dist.php | 122 +++++++++++ CHANGELOG.md | 14 ++ CODE_OF_CONDUCT.md | 43 ++++ LICENSE.md | 10 + README.md | 127 ++++++++++++ composer.json | 61 ++++++ examples/PerplexityAIFactory.php | 65 ++++++ examples/chat/createChatCompletion.php | 34 ++++ examples/completions/createCompletion.php | 25 +++ phpunit.xml.dist | 27 +++ src/Exception/PerplexityAIException.php | 68 +++++++ src/PerplexityAI.php | 237 ++++++++++++++++++++++ src/PerplexityAIURLBuilder.php | 107 ++++++++++ tests/PerplexityAITest.php | 229 +++++++++++++++++++++ tests/TestHelper.php | 57 ++++++ tests/responses/chatCompletion.json | 25 +++ tests/responses/completion.json | 18 ++ 26 files changed, 1701 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/update-contributors.yml create mode 100644 .github/workflows/validate-pr.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.dist.php create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 examples/PerplexityAIFactory.php create mode 100644 examples/chat/createChatCompletion.php create mode 100644 examples/completions/createCompletion.php create mode 100644 phpunit.xml.dist create mode 100644 src/Exception/PerplexityAIException.php create mode 100644 src/PerplexityAI.php create mode 100644 src/PerplexityAIURLBuilder.php create mode 100644 tests/PerplexityAITest.php create mode 100644 tests/TestHelper.php create mode 100644 tests/responses/chatCompletion.json create mode 100644 tests/responses/completion.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5183c50 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: softcreatr +custom: ['https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4'] diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..9573b70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,63 @@ +name: 🐛 Bug Report +description: Submit a bug report to help us improve. +labels: ["bug"] +body: + - type: markdown + attributes: + value: "## 🐛 Bug Report" + + - type: textarea + id: bug-description + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: checkboxes + id: previous-research + attributes: + label: Have you spent some time to check if this issue has been raised before? + options: + - label: I have googled for a similar issue or checked our older issues for a similar bug + required: true + + - type: checkboxes + id: code-of-conduct + attributes: + label: Have you read the Code of Conduct? + options: + - label: I have read the [Code of Conduct](https://github.com/SoftCreatR/php-perplexity-ai-sdk/blob/main/CODE_OF_CONDUCT.md) + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: To Reproduce + description: Write your steps here + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: Write down what you thought would happen. + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: Actual Behavior + description: Write what happened. Add screenshots, if applicable. + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Your Environment + description: Include as many relevant details about the environment you experienced the bug in (e.g., Environment, Operating system and version, etc.) + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..3d0385a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,30 @@ +name: 📚 Documentation +description: Report an issue related to documentation. +labels: ["documentation"] + +body: + - type: markdown + attributes: + value: "## 📚 Documentation" + + - type: textarea + id: issue-description + attributes: + label: Description + description: A clear and concise description of what the issue is. + placeholder: Enter the issue details here. + validations: + required: true + + - type: markdown + attributes: + value: "### Have you read the [Code of Conduct](https://github.com/SoftCreatR/php-perplexity-ai-sdk/blob/main/CODE_OF_CONDUCT.md)?" + + - type: checkboxes + id: code-of-conduct + attributes: + label: Code of Conduct + description: Please confirm that you have read the Code of Conduct. + options: + - label: I have read the Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..adac7b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,37 @@ +name: 💡 Feature / Idea +description: Submit a proposal for a new feature. +labels: ["feature"] +body: + - type: markdown + attributes: + value: "## 💡 Feature / Idea" + - type: textarea + id: feature-description + attributes: + label: "A clear and concise description of what the feature is." + placeholder: "Describe the feature here..." + validations: + required: true + - type: checkboxes + id: checked-previous-issues + attributes: + label: "Have you spent some time to check if this issue has been raised before?" + description: "Please check if a similar issue has been raised before." + options: + - label: "I have googled for a similar issue or checked our older issues for a similar idea" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: "Have you read the [Code of Conduct](https://github.com/SoftCreatR/php-perplexity-ai-sdk/blob/main/CODE_OF_CONDUCT.md)?" + options: + - label: "I have read the Code of Conduct" + required: true + - type: textarea + id: pitch + attributes: + label: "Pitch" + description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable." + placeholder: "Explain the reasoning behind the feature and provide examples..." + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8672e5e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + +# 🔀 Pull Request + +## What does this PR do? + +(Provide a description of what this PR does.) + +## Test Plan + +(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.) + +## Related PRs and Issues + +(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..151f892 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,142 @@ +name: Create Release + +on: + push: + branches: + - main + +jobs: + syntax-check: + runs-on: ubuntu-latest + if: startsWith(github.event.head_commit.message, '[Release]') + + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2' ] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Install Composer dependencies + run: composer install --no-dev + + - name: Check PHP syntax + run: find {src,tests} -type f -name "*.php" -exec php -l {} \; + + analyze-code: + needs: syntax-check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: php-cs-fixer, phpstan + extensions: mbstring + + - name: Install Composer dependencies + run: composer install + + - name: Run PHP-CS-Fixer + run: php-cs-fixer fix --dry-run --diff + + - name: Run PHPStan + run: phpstan analyse src tests --xdebug + + check-dependencies: + needs: analyze-code + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + + - name: Install Composer dependencies + run: composer install + + - name: Run security checker + run: composer require --dev enlightn/security-checker && vendor/bin/security-checker security:check + + run-tests: + needs: check-dependencies + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: pcov + ini-values: zend.assertions=1 + + - name: Install Composer dependencies + run: composer install + + - name: Run PHPUnit + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: Upload coverage report to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage.xml + fail_ci_if_error: true + + - name: Upload coverage report to Code Climate + uses: paambaati/codeclimate-action@v3.2.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageLocations: ${{github.workspace}}/coverage.xml:clover + + create-release: + needs: run-tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + + - name: Install Composer dependencies + run: composer install --no-dev + + - name: Get current version + id: current-version + run: | + VERSION=$(composer config version --quiet) + echo "Current version: $VERSION" + echo "::set-output name=version::$VERSION" + + - name: Create Release + id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.current-version.outputs.version }} + release_name: Release v${{ steps.current-version.outputs.version }} + body: 'New release for version ${{ steps.current-version.outputs.version }}' + draft: false + prerelease: false diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml new file mode 100644 index 0000000..2a52029 --- /dev/null +++ b/.github/workflows/update-contributors.yml @@ -0,0 +1,26 @@ +name: Update Contributors + +on: + pull_request: + types: [closed] + branches: + - main + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' + workflow_dispatch: + +jobs: + update-contributors: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Add contributors + uses: BobAnkh/add-contributors@v0.2.2 + with: + CONTRIBUTOR: '## Contributors ✨' + ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IGNORED_CONTRIBUTORS: 'Sascha Greuel' diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 0000000..20ee3df --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,105 @@ +name: Validate PR + +on: + pull_request: + paths: + - 'composer.json' + - '**.php' + +jobs: + syntax-check: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2' ] + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Install Composer dependencies + run: composer install --no-dev + + - name: Check PHP syntax + run: find {src,tests} -type f -name "*.php" -exec php -l {} \; + + analyze-code: + needs: syntax-check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: php-cs-fixer, phpstan + extensions: mbstring + + - name: Install Composer dependencies + run: composer install + + - name: Run PHP-CS-Fixer + run: php-cs-fixer fix --dry-run --diff + + - name: Run PHPStan + run: phpstan analyse src tests --xdebug + + check-dependencies: + needs: analyze-code + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + + - name: Install Composer dependencies + run: composer install + + - name: Run security checker + run: composer require --dev enlightn/security-checker && vendor/bin/security-checker security:check + + run-tests: + needs: check-dependencies + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: pcov + ini-values: zend.assertions=1 + + - name: Install Composer dependencies + run: composer install + + - name: Run PHPUnit + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: Upload coverage report to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage.xml + fail_ci_if_error: true + + - name: Upload coverage report to Code Climate + uses: paambaati/codeclimate-action@v3.2.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageLocations: ${{github.workspace}}/coverage.xml:clover diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a078fdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/composer.lock +vendor/ +/*.cache +.idea/ +/coverage.xml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..1dca607 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,122 @@ +exclude('*/vendor/*') + ->exclude('node_modules') + ->in(__DIR__) + ->notPath('lib/system/api'); + +return (new PhpCsFixer\Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PSR1' => true, + '@PSR2' => true, + '@PSR12' => true, + '@PER' => true, + + 'array_push' => true, + 'backtick_to_shell_exec' => true, + 'no_alias_language_construct_call' => true, + 'no_mixed_echo_print' => true, + 'pow_to_exponentiation' => true, + 'random_api_migration' => true, + + 'array_syntax' => ['syntax' => 'short'], + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_whitespace_before_comma_in_array' => true, + 'normalize_index_brace' => true, + 'whitespace_after_comma_in_array' => true, + + 'non_printable_character' => ['use_escape_sequences_in_strings' => true], + + 'magic_constant_casing' => true, + 'magic_method_casing' => true, + 'native_function_casing' => true, + 'native_function_type_declaration_casing' => true, + + 'cast_spaces' => ['space' => 'none'], + 'no_unset_cast' => true, + + 'class_attributes_separation' => true, + 'no_null_property_initialization' => true, + 'self_accessor' => true, + 'single_class_element_per_statement' => true, + + 'no_empty_comment' => true, + 'single_line_comment_style' => ['comment_types' => ['hash']], + + 'native_constant_invocation' => ['strict' => false], + + 'no_alternative_syntax' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield', 'yield_from']], + 'no_unneeded_curly_braces' => ['namespaces' => true], + 'switch_continue_to_break' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + + 'function_typehint_space' => true, + 'lambda_not_used_import' => true, + 'native_function_invocation' => ['include' => ['@internal']], + 'no_unreachable_default_argument_value' => true, + 'nullable_type_declaration_for_default_null_value' => true, + 'static_lambda' => true, + + 'fully_qualified_strict_types' => true, + 'no_unused_imports' => true, + + 'dir_constant' => true, + 'explicit_indirect_variable' => true, + 'function_to_constant' => true, + 'is_null' => true, + 'no_unset_on_property' => true, + + 'list_syntax' => ['syntax' => 'short'], + + 'clean_namespace' => true, + 'no_leading_namespace_whitespace' => true, + + 'no_homoglyph_names' => true, + + 'binary_operator_spaces' => true, + 'concat_space' => ['spacing' => 'one'], + 'increment_style' => ['style' => 'post'], + 'logical_operators' => true, + 'object_operator_without_whitespace' => true, + 'operator_linebreak' => true, + 'standardize_increment' => true, + 'standardize_not_equals' => true, + 'ternary_to_elvis_operator' => true, + 'ternary_to_null_coalescing' => true, + 'unary_operator_spaces' => true, + + 'no_useless_return' => true, + 'return_assignment' => true, + + 'multiline_whitespace_before_semicolons' => true, + 'no_empty_statement' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'space_after_semicolon' => ['remove_in_empty_for_expressions' => true], + + 'escape_implicit_backslashes' => true, + 'explicit_string_variable' => true, + 'heredoc_to_nowdoc' => true, + 'no_binary_string' => true, + 'simple_to_complex_string_variable' => true, + + 'array_indentation' => true, + 'blank_line_before_statement' => ['statements' => ['return', 'exit']], + 'method_chaining_indentation' => true, + 'no_extra_blank_lines' => ['tokens' => ['case', 'continue', 'curly_brace_block', 'default', 'extra', 'parenthesis_brace_block', 'square_brace_block', 'switch', 'throw', 'use']], + 'no_spaces_around_offset' => true, + + // SoftCreatR style + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => true, + 'import_functions' => false, + ], + 'ordered_imports' => [ + 'imports_order' => ['class', 'function', 'const'], + ], + ]) + ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc8579b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2023-10-19 + +### Added + +- Initial release of the Perplexity AI PHP library. +- Basic implementation for making API calls to the pplx API. +- Unit tests for the initial implementation. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..deebbde --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,43 @@ +# Code of Conduct + +## Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project maintainer is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4c74543 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +Copyright (c) 2023, Sascha Greuel and Contributors + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, +provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7731479 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# PerplexityAI API Wrapper for PHP + +[![Build](https://img.shields.io/github/actions/workflow/status/SoftCreatR/php-perplexity-ai-sdk/.github/workflows/create-release.yml?branch=main)](https://github.com/SoftCreatR/php-perplexity-ai-sdk/actions/workflows/create-release.yml) [![Latest Release](https://img.shields.io/packagist/v/SoftCreatR/php-perplexity-ai-sdk?color=blue&label=Latest%20Release)](https://packagist.org/packages/softcreatr/php-perplexity-ai-sdk) [![ISC licensed](https://img.shields.io/badge/license-ISC-blue.svg)](./LICENSE.md) [![Plant Tree](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Plant%20Tree&query=%24.total&url=https%3A%2F%2Fpublic.offset.earth%2Fusers%2Fsoftcreatr%2Ftrees)](https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4) [![Codecov branch](https://img.shields.io/codecov/c/github/SoftCreatR/php-perplexity-ai-sdk)](https://codecov.io/gh/SoftCreatR/php-perplexity-ai-sdk) [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability-percentage/SoftCreatR/php-perplexity-ai-sdk)](https://codeclimate.com/github/SoftCreatR/php-perplexity-ai-sdk) + +This PHP library provides a simple wrapper for the PerplexityAI API, allowing you to easily integrate the PerplexityAI API into your PHP projects. + + +## Features + +- Easy integration with PerplexityAI API +- Supports all PerplexityAI API endpoints +- Utilizes PSR-17 and PSR-18 compliant HTTP clients, and factories for making API requests + +## Requirements + +- PHP 7.4 or higher +- A PSR-17 HTTP Factory implementation (e.g., [guzzle/psr7](https://github.com/guzzle/psr7) or [nyholm/psr7](https://github.com/Nyholm/psr7)) +- A PSR-18 HTTP Client implementation (e.g., [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) or [symfony/http-client](https://github.com/symfony/http-client)) + +## Installation + +You can install the library via [Composer](https://getcomposer.org/): + +```bash +composer require softcreatr/php-perplexity-ai-sdk +``` + +## Usage + +First, include the library in your project: + +```php +createChatCompletion([ + 'model' => 'mistral-7b-instruct', + 'messages' => [ + [ + 'role' => 'system', + 'content' => 'Be precise and concise.' + ], + [ + 'role' => 'user', + 'content' => 'How many stars are there in our galaxy?' + ] + ], +]); + +// Process the API response +if ($response->getStatusCode() === 200) { + $responseObj = json_decode($response->getBody()->getContents(), true); + + print_r($responseObj); +} else { + echo "Error: " . $response->getStatusCode(); +} +``` + +For more details on how to use each endpoint, refer to the [PerplexityAI API documentation](https://docs.perplexity.ai/reference), and the [examples](https://github.com/SoftCreatR/php-perplexity-ai-sdk/tree/main/examples) provided in the repository. + +## Supported Methods + +### Completions +- [Create Completion](https://docs.perplexity.ai/reference/post_text_completions) - [Example](https://github.com/SoftCreatR/php-perplexity-ai-sdk/blob/main/examples/completions/createCompletion.php) + - `createCompletion(array $options = [])` + +### Chat Completions +- [Create Chat Completion](https://docs.perplexity.ai/reference/post_chat_completions) - [Example](https://github.com/SoftCreatR/php-perplexity-ai-sdk/blob/main/examples/chat/createChatCompletion.php) + - `createChatCompletion(array $options = [])` + +## Changelog + +For a detailed list of changes and updates, please refer to the [CHANGELOG.md](https://github.com/SoftCreatR/php-perplexity-ai-sdk/blob/main/CHANGELOG.md) file. We adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and document notable changes for each release. + +## Known Problems and limitations + +### Streaming Support +Currently, streaming is not supported in the `createCompletion` and `createChatCompletion` methods. It's planned to address this limitation asap. For now, please be aware that these methods cannot be used for streaming purposes. + +If you require streaming functionality, consider using an alternative implementation or keep an eye out for future updates to this library. + +## License + +This library is licensed under the ISC License. See the [LICENSE](https://github.com/SoftCreatR/php-perplexity-ai-sdk/blob/main/LICENSE.md) file for more information. + +## Maintainers 🛠️ + + + + + +
+ + Sascha Greuel +
+ Sascha Greuel +
+
+ +## Contributors ✨ + + + + +
diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1067421 --- /dev/null +++ b/composer.json @@ -0,0 +1,61 @@ +{ + "name": "softcreatr/php-perplexity-ai-sdk", + "description": "A powerful and easy-to-use PHP SDK for the pplx API, allowing seamless integration of advanced AI-powered features into your PHP projects.", + "license": "ISC", + "type": "library", + "version": "1.0.0", + "keywords": [ + "perplexity", + "ai", + "llama", + "mistral", + "replit", + "gpt", + "gpt-3", + "gpt-4", + "artificial-intelligence", + "machine-learning", + "natural-language-processing", + "nlp", + "php", + "sdk", + "api-wrapper" + ], + "authors": [ + { + "name": "Sascha Greuel", + "email": "hello@1-2.dev" + } + ], + "require": { + "php": ">=7.4", + "ext-fileinfo": "*", + "ext-json": "*", + "ext-mbstring": "*", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7", + "phpunit/phpunit": "^9" + }, + "minimum-stability": "stable", + "autoload": { + "psr-4": { + "SoftCreatR\\PerplexityAI\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "SoftCreatR\\PerplexityAI\\Tests\\": "tests/" + } + }, + "config": { + "sort-packages": true + }, + "scripts": { + "phpcsf": "php-cs-fixer fix", + "test": "phpunit" + } +} diff --git a/examples/PerplexityAIFactory.php b/examples/PerplexityAIFactory.php new file mode 100644 index 0000000..0d690b8 --- /dev/null +++ b/examples/PerplexityAIFactory.php @@ -0,0 +1,65 @@ +{$method}($args); + + // Decode the response body + $result = \json_decode( + $response->getBody()->getContents(), + true, + 512, + \JSON_THROW_ON_ERROR + ); + + // Print the result information as a JSON string + echo "============\n| Response |\n============\n\n"; + echo \json_encode($result, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); + echo "\n\n============\n"; + } catch (Exception $e) { + // Handle any exceptions during the API call + echo "Error: {$e->getMessage()}\n"; + } + } +} diff --git a/examples/chat/createChatCompletion.php b/examples/chat/createChatCompletion.php new file mode 100644 index 0000000..625763b --- /dev/null +++ b/examples/chat/createChatCompletion.php @@ -0,0 +1,34 @@ + 'mistral-7b-instruct', + 'messages' => [ + [ + 'role' => 'system', + 'content' => 'Be precise and concise.', + ], + [ + 'role' => 'user', + 'content' => 'How many stars are there in our galaxy?', + ], + ], +]); diff --git a/examples/completions/createCompletion.php b/examples/completions/createCompletion.php new file mode 100644 index 0000000..b0645fd --- /dev/null +++ b/examples/completions/createCompletion.php @@ -0,0 +1,25 @@ + 'replit-code-v1.5-3b', + 'prompt' => 'def fibonnaci(n): ', +]); diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..766495c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/src/Exception/PerplexityAIException.php b/src/Exception/PerplexityAIException.php new file mode 100644 index 0000000..4eac5b5 --- /dev/null +++ b/src/Exception/PerplexityAIException.php @@ -0,0 +1,68 @@ +extractErrorMessageFromJson($message), $code, $previous); + } + + /** + * Extracts the error message from a JSON-encoded string, if available. + * + * @param string $errorMessage The error message, encoded as a JSON string. + * + * @return string The extracted error message, or the original error message if JSON decoding failed, + * or the error message does not contain the expected structure. + */ + private function extractErrorMessageFromJson(string $errorMessage): string + { + try { + $decoded = \json_decode($errorMessage, true, 512, JSON_THROW_ON_ERROR); + + if (\is_array($decoded) && isset($decoded['error']['message'])) { + return $decoded['error']['message']; + } + } catch (Exception $e) { + // ignore + } + + return $errorMessage; + } +} diff --git a/src/PerplexityAI.php b/src/PerplexityAI.php new file mode 100644 index 0000000..081cc88 --- /dev/null +++ b/src/PerplexityAI.php @@ -0,0 +1,237 @@ +requestFactory = $requestFactory; + $this->streamFactory = $streamFactory; + $this->uriFactory = $uriFactory; + $this->httpClient = $httpClient; + $this->apiKey = $apiKey; + $this->origin = $origin; + } + + /** + * Magic method to call the PerplexityAI API endpoints. + * + * @param string $key The endpoint method. + * @param array $args The arguments for the endpoint method. + * + * @return ResponseInterface The API response. + * + * @throws PerplexityAIException If the API returns an error (HTTP status code >= 400). + */ + public function __call(string $key, array $args): ResponseInterface + { + $endpoint = PerplexityAIURLBuilder::getEndpoint($key); + $httpMethod = $endpoint['method']; + + [$parameter, $opts] = $this->extractCallArguments($args); + + return $this->callAPI($httpMethod, $key, $parameter, $opts); + } + + /** + * Extracts the arguments from the input array. + * + * @param array $args The input arguments. + * + * @return array An array containing the extracted parameter and options. + */ + private function extractCallArguments(array $args): array + { + $parameter = null; + $opts = []; + + if (!isset($args[0])) { + return [$parameter, $opts]; + } + + if (\is_string($args[0])) { + $parameter = $args[0]; + + if (isset($args[1]) && \is_array($args[1])) { + $opts = $args[1]; + } + } elseif (\is_array($args[0])) { + $opts = $args[0]; + } + + return [$parameter, $opts]; + } + + /** + * Calls the PerplexityAI API with the provided method, key, parameter, and options. + * + * @param string $method The HTTP method for the request. + * @param string $key The API endpoint key. + * @param string|null $parameter An optional parameter for the request. + * @param array $opts The options for the request. + * + * @return ResponseInterface The API response. + * + * @throws PerplexityAIException If the API returns an error (HTTP status code >= 400). + */ + private function callAPI(string $method, string $key, ?string $parameter = null, array $opts = []): ResponseInterface + { + return $this->sendRequest( + PerplexityAIURLBuilder::createUrl($this->uriFactory, $key, $parameter, $this->origin), + $method, + $opts + ); + } + + /** + * Sends an HTTP request to the PerplexityAI API and returns the response. + * + * @param UriInterface $uri The URL to send the request to. + * @param string $method The HTTP method to use (e.g., 'GET', 'POST', etc.). + * @param array $params An associative array of parameters to send with the request (optional). + * + * @return ResponseInterface The response from the PerplexityAI API. + * + * @throws PerplexityAIException If the API returns an error (HTTP status code >= 400). + * @throws Exception + */ + private function sendRequest(UriInterface $uri, string $method, array $params = []): ResponseInterface + { + $request = $this->requestFactory->createRequest($method, $uri); + + $headers = $this->createHeaders(); + $request = $this->applyHeaders($request, $headers); + + $body = $this->createJsonBody($params); + + if (!empty($body)) { + $request = $request->withBody($this->streamFactory->createStream($body)); + } + + try { + $response = $this->httpClient->sendRequest($request); + + // Check if the response has a non-200 status code (error) + if ($response->getStatusCode() >= 400) { + throw new PerplexityAIException($response->getBody()->getContents(), $response->getStatusCode()); + } + } catch (ClientExceptionInterface $e) { + throw new PerplexityAIException($e->getMessage(), $e->getCode(), $e->getPrevious()); + } + + return $response; + } + + /** + * Creates the headers for an API request. + * + * @return array An associative array of headers. + */ + private function createHeaders(): array + { + return [ + 'authorization' => 'Bearer ' . $this->apiKey, + 'content-type' => 'application/json', + ]; + } + + /** + * Applies the headers to the given request. + * + * @param RequestInterface $request The request to apply headers to. + * @param array $headers An associative array of headers to apply. + * + * @return RequestInterface The request with headers applied. + */ + private function applyHeaders(RequestInterface $request, array $headers): RequestInterface + { + foreach ($headers as $key => $value) { + $request = $request->withHeader($key, $value); + } + + return $request; + } + + /** + * Creates a JSON encoded body string from the given parameters. + * + * @param array $params An associative array of parameters to encode as JSON. + * + * @return string The JSON encoded body string, or an empty string if encoding fails. + */ + private function createJsonBody(array $params): string + { + try { + return !empty($params) ? \json_encode($params, JSON_THROW_ON_ERROR) : ''; + } catch (JsonException $e) { + // Fallback to an empty string if encoding fails + return ''; + } + } +} diff --git a/src/PerplexityAIURLBuilder.php b/src/PerplexityAIURLBuilder.php new file mode 100644 index 0000000..effaf4c --- /dev/null +++ b/src/PerplexityAIURLBuilder.php @@ -0,0 +1,107 @@ +> PerplexityAI API endpoints configuration. + */ + private static array $urlEndpoints = [ + // Completions: https://docs.perplexity.ai/reference/post_text_completions + 'createCompletion' => ['method' => self::HTTP_METHOD_POST, 'path' => '/completions'], + + // Chat Completions: https://docs.perplexity.ai/reference/post_chat_completions + 'createChatCompletion' => ['method' => self::HTTP_METHOD_POST, 'path' => '/chat/completions'], + ]; + + /** + * Gets the PerplexityAI API endpoint configuration. + * + * @param string $key The endpoint key. + * + * @return array The endpoint configuration. + * + * @throws InvalidArgumentException If the provided key is invalid. + */ + public static function getEndpoint(string $key): array + { + if (!isset(self::$urlEndpoints[$key])) { + throw new InvalidArgumentException('Invalid Perplexity AI URL key "' . $key . '".'); + } + + return self::$urlEndpoints[$key]; + } + + /** + * Creates a URL for the specified PerplexityAI API endpoint. + * + * @param UriFactoryInterface $uriFactory The PSR-17 URI factory instance used for creating URIs. + * @param string $key The key representing the API endpoint. + * @param string|null $parameter Optional parameter to replace in the endpoint path. + * @param string $origin Custom origin (Hostname), if needed. + * + * @return UriInterface The fully constructed URL for the API endpoint. + * + * @throws InvalidArgumentException If the provided key is invalid. + */ + public static function createUrl( + UriFactoryInterface $uriFactory, + string $key, + ?string $parameter = null, + string $origin = '' + ): UriInterface { + $endpoint = self::getEndpoint($key); + $path = self::replacePathParameters($endpoint['path'], $parameter); + + return $uriFactory + ->createUri() + ->withScheme('https') + ->withHost($origin ?: self::ORIGIN) + ->withPath($path); + } + + /** + * Replaces path parameters in the given path with provided parameter value. + * + * @param string $path The path containing the parameter placeholder. + * @param string|null $parameter The parameter value to replace the placeholder with. + * + * @return string The path with replaced parameter value. + */ + private static function replacePathParameters(string $path, ?string $parameter = null): string + { + if ($parameter !== null) { + return \sprintf($path, $parameter); + } + + return $path; + } +} diff --git a/tests/PerplexityAITest.php b/tests/PerplexityAITest.php new file mode 100644 index 0000000..d98ea3a --- /dev/null +++ b/tests/PerplexityAITest.php @@ -0,0 +1,229 @@ +mockedClient = $this->createMock(ClientInterface::class); + + $this->pplx = new PerplexityAI( + $psr17Factory, + $psr17Factory, + $psr17Factory, + $this->mockedClient, + $this->apiKey, + $this->origin + ); + } + + /** + * Test that PerplexityAI::completion method can handle API calls correctly. + */ + public function testCreateCompletion(): void + { + $this->testApiCall( + fn() => $this->pplx->createCompletion([ + 'model' => 'replit-code-v1.5-3b', + 'prompt' => 'def fibonnaci(n): ', + ]), + 'completion.json' + ); + } + + /** + * Test that PerplexityAI::chat method can handle API calls correctly. + */ + public function testCreateChatCompletion(): void + { + $this->testApiCall( + fn() => $this->pplx->createChatCompletion([ + 'model' => 'mistral-7b-instruct', + 'messages' => [ + [ + 'role' => 'system', + 'content' => 'Be precise and concise.', + ], + [ + 'role' => 'user', + 'content' => 'How many stars are there in our galaxy?', + ], + ], + ]), + 'chatCompletion.json' + ); + } + + /** + * Test the 'extractCallArguments' method with various input scenarios. + * + * This test ensures that the 'extractCallArguments' method correctly extracts + * the parameter and options from the provided arguments array for different cases: + * - String parameter and options array + * - Only string parameter + * - Only options array + * - Empty array + * + * @throws ReflectionException + */ + public function testExtractCallArguments(): void + { + // Invoke the protected method 'extractCallArguments' via reflection + $reflectionMethod = TestHelper::getPrivateMethod($this->pplx, 'extractCallArguments'); + + $testCases = [ + [['stringParam', ['key' => 'value']], ['stringParam', ['key' => 'value']]], + [['stringParam'], ['stringParam', []]], + [[['key' => 'value']], [null, ['key' => 'value']]], + [[], [null, []]], + ]; + + foreach ($testCases as $testCase) { + [$args, $expected] = $testCase; + $result = $reflectionMethod->invoke($this->pplx, $args); + $this->assertEquals($expected, $result); + } + } + + /** + * Test that PerplexityAI::callAPI handles JSON encoding errors correctly. + * + * This test ensures that when the JSON encoding fails due to an invalid value, + * the method catches the JsonException and sets the request body to an empty string. + */ + public function testCallAPIJsonEncodingException(): void + { + $this->sendRequestMock(static function (RequestInterface $request) { + $fakeResponse = new Response(200, [], ''); + // Check if the request body is empty + self::assertEquals('', (string)$request->getBody()); + + return $fakeResponse; + }); + + $invalidValue = \tmpfile(); // create an invalid value that cannot be JSON encoded + $response = null; + + try { + $response = $this->pplx->createCompletion([ + 'model' => 'replit-code-v1.5-3b', + 'prompt' => 'Say this is a test', + 'max_tokens' => 500, + 'invalid' => $invalidValue, // pass the invalid value + ]); + } catch (Exception $e) { + // ignore + } + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals('', (string)$response->getBody()); + } + + /** + * Test an API call using a callable and a response file. + * This method mocks the HTTP client to return a predefined response loaded from a file, + * and checks if the status code and the response body match the expected values. + * + * @param callable $apiCall The API call to test, wrapped in a callable function. + * @param string $responseFile The path to the file containing the expected response. + */ + private function testApiCall(callable $apiCall, string $responseFile): void + { + $response = null; + $fakeResponseBody = TestHelper::loadResponseFromFile($responseFile); + $fakeResponse = new Response(200, [], $fakeResponseBody); + + $this->sendRequestMock(static function () use ($fakeResponse) { + return $fakeResponse; + }); + + try { + $response = $apiCall(); + } catch (Exception $e) { + // ignore + } + + self::assertEquals($this->apiKey, $this->pplx->apiKey); + self::assertEquals($this->origin, $this->pplx->origin); + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals($fakeResponseBody, (string)$response->getBody()); + } + + /** + * Sets up a mock for the sendRequest method of the mocked client. + * + * This helper method is used to reduce code duplication when configuring + * the sendRequest mock in multiple test cases. It accepts a callable, which + * will be used as the return value or exception thrown by the sendRequest mock. + * + * @param callable $responseCallback A callable that returns a response or throws an exception + */ + private function sendRequestMock(callable $responseCallback): void + { + $this->mockedClient + ->expects(self::once()) + ->method('sendRequest') + ->willReturnCallback($responseCallback); + } +} diff --git a/tests/TestHelper.php b/tests/TestHelper.php new file mode 100644 index 0000000..3d91717 --- /dev/null +++ b/tests/TestHelper.php @@ -0,0 +1,57 @@ +getMethod($methodName); + $method->setAccessible(true); + + return $method; + } + + /** + * Load the content of a response file for testing. + * + * @param string $filename The name of the response file. + * + * @return string The content of the response file. + */ + public static function loadResponseFromFile(string $filename): string + { + return \file_get_contents(__DIR__ . '/responses/' . $filename); + } +} diff --git a/tests/responses/chatCompletion.json b/tests/responses/chatCompletion.json new file mode 100644 index 0000000..ae41a26 --- /dev/null +++ b/tests/responses/chatCompletion.json @@ -0,0 +1,25 @@ +{ + "id": "ccfafd43-ef5b-4518-a560-1cde45cd5153", + "model": "mistral-7b-instruct", + "created": 2889453, + "usage": { + "prompt_tokens": 10, + "completion_tokens": 10, + "total_tokens": 20 + }, + "object": "chat.completion", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + "delta": { + "role": "assistant", + "content": "" + } + } + ] +} diff --git a/tests/responses/completion.json b/tests/responses/completion.json new file mode 100644 index 0000000..9a239be --- /dev/null +++ b/tests/responses/completion.json @@ -0,0 +1,18 @@ +{ + "id": "9a4701a7-5150-4a96-badf-b98e115e0345", + "model": "replit-code-v1.5-3b", + "created": 3759728, + "usage": { + "prompt_tokens": 8, + "completion_tokens": 93, + "total_tokens": 101 + }, + "object": "text_completion", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "text": " #yield den retur labeli sto fibonaci exei to 2 to exoume gestouryma\n a = [0, 1]\n for i in range(n):\n yield a[i]\n a.append(a[i] + a[i + 1])\nG = (fibonnaci(9))\nprint(*G, sep='\\n')\n\n\nprint(*a1, sep='\\n')" + } + ] +}