diff --git a/.github/actions/poetry_setup/action.yml b/.github/actions/poetry_setup/action.yml new file mode 100644 index 00000000..75965f14 --- /dev/null +++ b/.github/actions/poetry_setup/action.yml @@ -0,0 +1,93 @@ +# An action for setting up poetry install with caching. +# Using a custom action since the default action does not +# take poetry install groups into account. +# Action code from: +# https://github.com/actions/setup-python/issues/505#issuecomment-1273013236 +name: poetry-install-with-caching +description: Poetry install with support for caching of dependency groups. + +inputs: + python-version: + description: Python version, supporting MAJOR.MINOR only + required: true + + poetry-version: + description: Poetry version + required: true + + cache-key: + description: Cache key to use for manual handling of caching + required: true + + working-directory: + description: Directory whose poetry.lock file should be cached + required: true + +runs: + using: composite + steps: + - uses: actions/setup-python@v5 + name: Setup python ${{ inputs.python-version }} + id: setup-python + with: + python-version: ${{ inputs.python-version }} + + - uses: actions/cache@v4 + id: cache-bin-poetry + name: Cache Poetry binary - Python ${{ inputs.python-version }} + env: + SEGMENT_DOWNLOAD_TIMEOUT_MIN: "1" + with: + path: | + /opt/pipx/venvs/poetry + # This step caches the poetry installation, so make sure it's keyed on the poetry version as well. + key: bin-poetry-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-${{ inputs.poetry-version }} + + - name: Refresh shell hashtable and fixup softlinks + if: steps.cache-bin-poetry.outputs.cache-hit == 'true' + shell: bash + env: + POETRY_VERSION: ${{ inputs.poetry-version }} + PYTHON_VERSION: ${{ inputs.python-version }} + run: | + set -eux + + # Refresh the shell hashtable, to ensure correct `which` output. + hash -r + + # `actions/cache@v3` doesn't always seem able to correctly unpack softlinks. + # Delete and recreate the softlinks pipx expects to have. + rm /opt/pipx/venvs/poetry/bin/python + cd /opt/pipx/venvs/poetry/bin + ln -s "$(which "python$PYTHON_VERSION")" python + chmod +x python + cd /opt/pipx_bin/ + ln -s /opt/pipx/venvs/poetry/bin/poetry poetry + chmod +x poetry + + # Ensure everything got set up correctly. + /opt/pipx/venvs/poetry/bin/python --version + /opt/pipx_bin/poetry --version + + - name: Install poetry + if: steps.cache-bin-poetry.outputs.cache-hit != 'true' + shell: bash + env: + POETRY_VERSION: ${{ inputs.poetry-version }} + PYTHON_VERSION: ${{ inputs.python-version }} + # Install poetry using the python version installed by setup-python step. + run: pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose + + - name: Restore pip and poetry cached dependencies + uses: actions/cache@v4 + env: + SEGMENT_DOWNLOAD_TIMEOUT_MIN: "4" + WORKDIR: ${{ inputs.working-directory == '' && '.' || inputs.working-directory }} + with: + path: | + ~/.cache/pip + ~/.cache/pypoetry/virtualenvs + ~/.cache/pypoetry/cache + ~/.cache/pypoetry/artifacts + ${{ env.WORKDIR }}/.venv + key: py-deps-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-poetry-${{ inputs.poetry-version }}-${{ inputs.cache-key }}-${{ hashFiles(format('{0}/**/poetry.lock', env.WORKDIR)) }} \ No newline at end of file diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml new file mode 100644 index 00000000..291c6972 --- /dev/null +++ b/.github/workflows/_release.yml @@ -0,0 +1,264 @@ +name: release +run-name: Release ${{ inputs.working-directory }} by @${{ github.actor }} +on: + workflow_call: + branches: + - dev + inputs: + working-directory: + required: true + type: string + description: "From which folder this pipeline executes" + workflow_dispatch: + branches: + - dev + inputs: + working-directory: + required: true + type: string + default: './chatchat-server' + description: "From which folder this pipeline executes" +env: + PYTHON_VERSION: "3.8" + POETRY_VERSION: "1.7.1" + +jobs: + build: + if: github.ref == 'refs/heads/master' + environment: Scheduled testing + runs-on: ubuntu-latest + + outputs: + pkg-name: ${{ steps.check-version.outputs.pkg-name }} + version: ${{ steps.check-version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + Poetry ${{ env.POETRY_VERSION }} + uses: "./.github/actions/poetry_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + poetry-version: ${{ env.POETRY_VERSION }} + working-directory: ${{ inputs.working-directory }} + cache-key: release + + # We want to keep this build stage *separate* from the release stage, + # so that there's no sharing of permissions between them. + # The release stage has trusted publishing and GitHub repo contents write access, + # and we want to keep the scope of that access limited just to the release job. + # Otherwise, a malicious `build` step (e.g. via a compromised dependency) + # could get access to our GitHub or PyPI credentials. + # + # Per the trusted publishing GitHub Action: + # > It is strongly advised to separate jobs for building [...] + # > from the publish job. + # https://github.com/pypa/gh-action-pypi-publish#non-goals + - name: Build project for distribution + run: poetry build + working-directory: ${{ inputs.working-directory }} + + - name: Upload build + uses: actions/upload-artifact@v4 + with: + name: dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Check Version + id: check-version + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + echo pkg-name="$(poetry version | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT + echo version="$(poetry version --short)" >> $GITHUB_OUTPUT + + test-pypi-publish: + needs: + - build + uses: + ./.github/workflows/_test_release.yml + with: + working-directory: ${{ inputs.working-directory }} + secrets: inherit + + pre-release-checks: + needs: + - build + - test-pypi-publish + environment: Scheduled testing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # We explicitly *don't* set up caching here. This ensures our tests are + # maximally sensitive to catching breakage. + # + # For example, here's a way that caching can cause a falsely-passing test: + # - Make the langchain package manifest no longer list a dependency package + # as a requirement. This means it won't be installed by `pip install`, + # and attempting to use it would cause a crash. + # - That dependency used to be required, so it may have been cached. + # When restoring the venv packages from cache, that dependency gets included. + # - Tests pass, because the dependency is present even though it wasn't specified. + # - The package is published, and it breaks on the missing dependency when + # used in the real world. + + - name: Set up Python + Poetry ${{ env.POETRY_VERSION }} + uses: "./.github/actions/poetry_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + poetry-version: ${{ env.POETRY_VERSION }} + working-directory: ${{ inputs.working-directory }} + + - name: Import published package + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PKG_NAME: ${{ needs.build.outputs.pkg-name }} + VERSION: ${{ needs.build.outputs.version }} + # Here we use: + # - The default regular PyPI index as the *primary* index, meaning + # that it takes priority (https://pypi.org/simple) + # - The test PyPI index as an extra index, so that any dependencies that + # are not found on test PyPI can be resolved and installed anyway. + # (https://test.pypi.org/simple). This will include the PKG_NAME==VERSION + # package because VERSION will not have been uploaded to regular PyPI yet. + # - attempt install again after 5 seconds if it fails because there is + # sometimes a delay in availability on test pypi + run: | + poetry run pip install \ + --extra-index-url https://test.pypi.org/simple/ \ + "$PKG_NAME==$VERSION" || \ + ( \ + sleep 5 && \ + poetry run pip install \ + --extra-index-url https://test.pypi.org/simple/ \ + "$PKG_NAME==$VERSION" \ + ) + + # Replace all dashes in the package name with underscores, + # since that's how Python imports packages with dashes in the name. + IMPORT_NAME="$(echo "$PKG_NAME" | sed s/-/_/g)" + + poetry run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))" + + - name: Import test dependencies + run: poetry install --with test + working-directory: ${{ inputs.working-directory }} + + # Overwrite the local version of the package with the test PyPI version. + - name: Import published package (again) + working-directory: ${{ inputs.working-directory }} + shell: bash + env: + PKG_NAME: ${{ needs.build.outputs.pkg-name }} + VERSION: ${{ needs.build.outputs.version }} + run: | + poetry run pip install \ + --extra-index-url https://test.pypi.org/simple/ \ + "$PKG_NAME==$VERSION" + + - name: Run unit tests + run: make tests + env: + ZHIPUAI_API_KEY: ${{ secrets.ZHIPUAI_API_KEY }} + ZHIPUAI_BASE_URL: ${{ secrets.ZHIPUAI_BASE_URL }} + working-directory: ${{ inputs.working-directory }} + +# - name: Run integration tests +# env: +# ZHIPUAI_API_KEY: ${{ secrets.ZHIPUAI_API_KEY }} +# ZHIPUAI_BASE_URL: ${{ secrets.ZHIPUAI_BASE_URL }} +# run: make integration_tests +# working-directory: ${{ inputs.working-directory }} + + publish: + needs: + - build + - test-pypi-publish + - pre-release-checks + environment: Scheduled testing + runs-on: ubuntu-latest + permissions: + # This permission is used for trusted publishing: + # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ + # + # Trusted publishing has to also be configured on PyPI for each package: + # https://docs.pypi.org/trusted-publishers/adding-a-publisher/ + id-token: write + + defaults: + run: + working-directory: ${{ inputs.working-directory }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + Poetry ${{ env.POETRY_VERSION }} + uses: "./.github/actions/poetry_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + poetry-version: ${{ env.POETRY_VERSION }} + working-directory: ${{ inputs.working-directory }} + cache-key: release + + - uses: actions/download-artifact@v4 + with: + name: dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ${{ inputs.working-directory }}/dist/ + verbose: true + print-hash: true + password: ${{ secrets.PYPI_API_TOKEN }} + # We overwrite any existing distributions with the same name and version. + # This is *only for CI use* and is *extremely dangerous* otherwise! + # https://github.com/pypa/gh-action-pypi-publish#tolerating-release-package-file-duplicates + skip-existing: true + + mark-release: + needs: + - build + - test-pypi-publish + - pre-release-checks + - publish + environment: Scheduled testing + runs-on: ubuntu-latest + permissions: + # This permission is needed by `ncipollo/release-action` to + # create the GitHub release. + contents: write + + defaults: + run: + working-directory: ${{ inputs.working-directory }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + Poetry ${{ env.POETRY_VERSION }} + uses: "./.github/actions/poetry_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + poetry-version: ${{ env.POETRY_VERSION }} + working-directory: ${{ inputs.working-directory }} + cache-key: release + + - uses: actions/download-artifact@v4 + with: + name: dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Create Release + uses: ncipollo/release-action@v1 + if: ${{ inputs.working-directory == './chatchat-server' }} + with: + artifacts: "dist/*" + token: ${{ secrets.GITHUB_TOKEN }} + draft: false + generateReleaseNotes: true + tag: v${{ needs.build.outputs.version }} + commit: main \ No newline at end of file diff --git a/.github/workflows/_test_release.yml b/.github/workflows/_test_release.yml new file mode 100644 index 00000000..efeb289b --- /dev/null +++ b/.github/workflows/_test_release.yml @@ -0,0 +1,97 @@ +name: test-release + +on: + workflow_call: + branches: + - dev + inputs: + working-directory: + required: true + type: string + description: "From which folder this pipeline executes" + +env: + POETRY_VERSION: "1.7.1" + PYTHON_VERSION: "3.8" + +jobs: + build: + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + + outputs: + pkg-name: ${{ steps.check-version.outputs.pkg-name }} + version: ${{ steps.check-version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + Poetry ${{ env.POETRY_VERSION }} + uses: "./.github/actions/poetry_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + poetry-version: ${{ env.POETRY_VERSION }} + working-directory: ${{ inputs.working-directory }} + cache-key: release + + # We want to keep this build stage *separate* from the release stage, + # so that there's no sharing of permissions between them. + # The release stage has trusted publishing and GitHub repo contents write access, + # and we want to keep the scope of that access limited just to the release job. + # Otherwise, a malicious `build` step (e.g. via a compromised dependency) + # could get access to our GitHub or PyPI credentials. + # + # Per the trusted publishing GitHub Action: + # > It is strongly advised to separate jobs for building [...] + # > from the publish job. + # https://github.com/pypa/gh-action-pypi-publish#non-goals + - name: Build project for distribution + run: poetry build + working-directory: ${{ inputs.working-directory }} + + - name: Upload build + uses: actions/upload-artifact@v4 + with: + name: test-dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Check Version + id: check-version + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + echo pkg-name="$(poetry version | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT + echo version="$(poetry version --short)" >> $GITHUB_OUTPUT + + publish: + needs: + - build + runs-on: ubuntu-latest + permissions: + # This permission is used for trusted publishing: + # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ + # + # Trusted publishing has to also be configured on PyPI for each package: + # https://docs.pypi.org/trusted-publishers/adding-a-publisher/ + id-token: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: test-dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Publish to test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ${{ inputs.working-directory }}/dist/ + verbose: true + print-hash: true + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # We overwrite any existing distributions with the same name and version. + # This is *only for CI use* and is *extremely dangerous* otherwise! + # https://github.com/pypa/gh-action-pypi-publish#tolerating-release-package-file-duplicates + skip-existing: true \ No newline at end of file diff --git a/model-providers/pyproject.toml b/model-providers/pyproject.toml index 8a4db8a0..67994dc9 100644 --- a/model-providers/pyproject.toml +++ b/model-providers/pyproject.toml @@ -21,21 +21,25 @@ openai = "1.13.3" tiktoken = "0.5.2" pydub = "0.25.1" boto3 = "1.28.17" + [tool.poetry.group.test.dependencies] # The only dependencies that should be added are # dependencies used for running tests (e.g., pytest, freezegun, response). # Any dependencies that do not meet that criteria will be removed. pytest = "^7.3.0" +pytest-cov = "^4.0.0" +pytest-dotenv = "^0.5.2" +duckdb-engine = "^0.9.2" +pytest-watcher = "^0.2.6" freezegun = "^1.2.2" -pytest-mock = "^3.10.0" +responses = "^0.22.0" +pytest-asyncio = "^0.23.2" +lark = "^1.1.5" +pandas = "^2.0.0" +pytest-mock = "^3.10.0" +pytest-socket = "^0.6.0" syrupy = "^4.0.2" -pytest-watcher = "^0.3.4" -pytest-asyncio = "^0.21.1" -grandalf = "^0.8" -pytest-profiling = "^1.7.0" -responses = "^0.25.0" -langchain = "0.1.5" -langchain-openai = "0.0.5" +requests-mock = "^1.11.0" [tool.poetry.group.lint] optional = true diff --git a/model-providers/tests/unit_test/conftest.py b/model-providers/tests/unit_test/conftest.py deleted file mode 100644 index eea02a65..00000000 --- a/model-providers/tests/unit_test/conftest.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Configuration for unit tests.""" -import logging -from importlib import util -from typing import Dict, List, Sequence - -import pytest -from pytest import Config, Function, Parser - -from model_providers.core.utils.utils import ( - get_config_dict, - get_log_file, - get_timestamp_ms, -) - - -def pytest_addoption(parser: Parser) -> None: - """Add custom command line options to pytest.""" - parser.addoption( - "--only-extended", - action="store_true", - help="Only run extended tests. Does not allow skipping any extended tests.", - ) - parser.addoption( - "--only-core", - action="store_true", - help="Only run core tests. Never runs any extended tests.", - ) - - -def pytest_collection_modifyitems(config: Config, items: Sequence[Function]) -> None: - """Add implementations for handling custom markers. - - At the moment, this adds support for a custom `requires` marker. - - The `requires` marker is used to denote tests that require one or more packages - to be installed to run. If the package is not installed, the test is skipped. - - The `requires` marker syntax is: - - .. code-block:: python - - @pytest.mark.requires("package1", "package2") - def test_something(): - ... - """ - # Mapping from the name of a package to whether it is installed or not. - # Used to avoid repeated calls to `util.find_spec` - required_pkgs_info: Dict[str, bool] = {} - - only_extended = config.getoption("--only-extended") or False - only_core = config.getoption("--only-core") or False - - if only_extended and only_core: - raise ValueError("Cannot specify both `--only-extended` and `--only-core`.") - - for item in items: - requires_marker = item.get_closest_marker("requires") - if requires_marker is not None: - if only_core: - item.add_marker(pytest.mark.skip(reason="Skipping not a core test.")) - continue - - # Iterate through the list of required packages - required_pkgs = requires_marker.args - for pkg in required_pkgs: - # If we haven't yet checked whether the pkg is installed - # let's check it and store the result. - if pkg not in required_pkgs_info: - try: - installed = util.find_spec(pkg) is not None - except Exception: - installed = False - required_pkgs_info[pkg] = installed - - if not required_pkgs_info[pkg]: - if only_extended: - pytest.fail( - f"Package `{pkg}` is not installed but is required for " - f"extended tests. Please install the given package and " - f"try again.", - ) - - else: - # If the package is not installed, we immediately break - # and mark the test as skipped. - item.add_marker( - pytest.mark.skip(reason=f"Requires pkg: `{pkg}`") - ) - break - else: - if only_extended: - item.add_marker( - pytest.mark.skip(reason="Skipping not an extended test.") - ) - - -@pytest.fixture -def logging_conf() -> dict: - return get_config_dict( - "DEBUG", - get_log_file(log_path="logs", sub_dir=f"local_{get_timestamp_ms()}"), - 122, - 111, - ) diff --git a/model-providers/tests/unit_tests/model_providers.yaml b/model-providers/tests/unit_tests/model_providers.yaml new file mode 100644 index 00000000..908883c7 --- /dev/null +++ b/model-providers/tests/unit_tests/model_providers.yaml @@ -0,0 +1,33 @@ +openai: + model_credential: + - model: 'gpt-3.5-turbo' + model_type: 'llm' + model_credentials: + openai_api_key: 'sk-' + openai_organization: '' + openai_api_base: '' + - model: 'gpt-4' + model_type: 'llm' + model_credentials: + openai_api_key: 'sk-' + openai_organization: '' + openai_api_base: '' + + provider_credential: + openai_api_key: 'sk-' + openai_organization: '' + openai_api_base: '' + +xinference: + model_credential: + - model: 'chatglm3-6b' + model_type: 'llm' + model_credentials: + server_url: 'http://127.0.0.1:9997/' + model_uid: 'chatglm3-6b' + + +zhipuai: + + provider_credential: + api_key: 'd4fa0690b6dfa205204cae2e12aa6fb6.1' \ No newline at end of file diff --git a/model-providers/tests/unit_test/test_provider_manager_models.py b/model-providers/tests/unit_tests/test_provider_manager_models.py similarity index 89% rename from model-providers/tests/unit_test/test_provider_manager_models.py rename to model-providers/tests/unit_tests/test_provider_manager_models.py index 023c48ec..9416fb9f 100644 --- a/model-providers/tests/unit_test/test_provider_manager_models.py +++ b/model-providers/tests/unit_tests/test_provider_manager_models.py @@ -12,12 +12,11 @@ from model_providers.core.provider_manager import ProviderManager logger = logging.getLogger(__name__) -def test_provider_manager_models(logging_conf: dict) -> None: +def test_provider_manager_models(logging_conf: dict, providers_file: str) -> None: logging.config.dictConfig(logging_conf) # type: ignore # 读取配置文件 cfg = OmegaConf.load( - "/media/gpt4-pdf-chatbot-langchain/langchain-ChatGLM/model-providers" - "/model_providers.yaml" + providers_file ) # 转换配置文件 (