diff --git a/.github/labeler.yml b/.github/labeler.yml index e71752dc39..a7e32e9489 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,80 +1,78 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + # Add 'root' label to any root file changes # Quotation marks are required for the leading asterisk root: - changed-files: - - any-glob-to-any-file: '*' + - any-glob-to-any-file: "*" docs: - changed-files: - any-glob-to-any-file: - - '**/*.md' - - '**/*.rst' - - 'LICENSE' - - 'docs-go/**' - - 'docs/**' - - 'py/engdocs/**' + - "**/*.md" + - "**/*.rst" + - "LICENSE" + - "docs-go/**" + - "docs/**" + - "py/engdocs/**" go: - changed-files: - any-glob-to-any-file: - - '**/*.go' - - '**/go.mod' - - '**/go.sum' - - 'go/**' + - "**/*.go" + - "**/go.mod" + - "**/go.sum" + - "go/**" python: - changed-files: - any-glob-to-any-file: - - '**/*.py' - - '**/pyproject.toml' - - 'py/**' + - "**/*.py" + - "**/pyproject.toml" + - "py/**" js: - changed-files: - any-glob-to-any-file: - - '**/*.js' - - '**/*.jsx' - - '**/*.ts' - - '**/*.tsx' - - '**/package.json' - - 'js/**' + - "**/*.js" + - "**/*.jsx" + - "**/*.ts" + - "**/*.tsx" + - "**/package.json" + - "js/**" tooling: - changed-files: - any-glob-to-any-file: - - 'genkit-tools/**' + - "genkit-tools/**" config: - changed-files: - any-glob-to-any-file: - - '**/*.toml' - - '**/*.yaml' - - '**/*.yml' - - '**/.editorconfig' - - '**/.github/**' - - '**/.gitignore' - - '**/.npmignore' - - '**/.npmrc' - - '**/.prettierignore' - - '**/package.json' - - '**/tsconfig.*.json' - - '**/tsconfig.json' - - '**/typedoc.json' + - "**/*.toml" + - "**/*.yaml" + - "**/*.yml" + - "**/.editorconfig" + - "**/.github/**" + - "**/.gitignore" + - "**/.npmignore" + - "**/.npmrc" + - "**/.prettierignore" + - "**/package.json" + - "**/tsconfig.*.json" + - "**/tsconfig.json" + - "**/typedoc.json" sample: - changed-files: - any-glob-to-any-file: - - 'samples/**' + - "samples/**" dotprompt: - changed-files: - any-glob-to-any-file: - - '**/dotprompt/**' - -handlebarz: - - changed-files: - - any-glob-to-any-file: - - '**/handlebarz/**' + - "**/dotprompt/**" # Automatically add labels to any PR also based on branch naming conventions. build: diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 09cf4ca172..a960b4a70d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,3 +1,6 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + name: "Pull Request Labeler" on: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 2c8b96fcbb..0d17facdf0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,3 +1,6 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + name: Python Checks on: pull_request @@ -11,9 +14,6 @@ jobs: matrix: python-version: - "3.12" - defaults: - run: - working-directory: py steps: - uses: actions/checkout@v4 @@ -28,16 +28,19 @@ jobs: python-version: ${{ matrix.python-version }} - name: Format check - run: uv run ruff format --check . + run: uv run --directory py ruff format --check . - name: Lint with ruff - run: uv run ruff check . + run: uv run --directory py ruff check --select I . + + - name: Check licenses + run: ./bin/check_license - name: Run tests - run: ./bin/run_tests + run: ./py/bin/run_python_tests - name: Build documentation - run: uv run mkdocs build --strict + run: uv run --directory py mkdocs build --strict - name: Build distributions - run: ./bin/build_dists + run: ./py/bin/build_dists diff --git a/py/.hooks/commit-message-format-pre-push b/.hooks/commit-message-format-pre-push similarity index 100% rename from py/.hooks/commit-message-format-pre-push rename to .hooks/commit-message-format-pre-push diff --git a/.hooks/conventional-commit-msg b/.hooks/conventional-commit-msg new file mode 100755 index 0000000000..f04673cfea --- /dev/null +++ b/.hooks/conventional-commit-msg @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +cat "$1" | convco check --from-stdin diff --git a/py/.hooks/no-commits-on-branches b/.hooks/no-commits-on-branches similarity index 100% rename from py/.hooks/no-commits-on-branches rename to .hooks/no-commits-on-branches diff --git a/.prettierignore b/.prettierignore index fedcb43fe2..d11d1c8a5d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ go/ pnpm-lock.yaml public/ py/* +README.md diff --git a/COMMIT_MESSAGE_TEMPLATE b/COMMIT_MESSAGE_TEMPLATE new file mode 100644 index 0000000000..2f2453be0a --- /dev/null +++ b/COMMIT_MESSAGE_TEMPLATE @@ -0,0 +1,45 @@ +feat: + +ISSUE: + +CHANGELOG: + - [] + +## COMMIT MESSAGE FULL EXAMPLE +# +# feat(py/plugins/vertexai): Implement two-factor authentication +# +# This commit introduces two-factor authentication for enhanced security. +# It uses TOTP and requires users to configure an authenticator app. +# +# ISSUE: #123 +# +# CHANGELOG: +# - [ ] Add support for two-factor authentication +# - [ ] Update user login endpoint to require two-factor authentication +# +# BREAKING CHANGE: The API endpoint for user login has been modified. + +## CONVENTIONAL COMMIT TEMPLATE +# +# Subject line (required, max 50 characters, use imperative mood): +# (): +# Example: feat(user-authentication): Implement two-factor authentication +# +# Body (optional, wrap at 72 characters, explain the change in more detail, mention why and what): +# + +## TYPES OF CHANGE (choose one): +# - feat: A new feature +# - fix: A bug fix +# - docs: Documentation changes +# - style: Code style changes (formatting, etc.) +# - refactor: Code refactoring (no new features or bug fixes) +# - perf: Performance improvements +# - test: Adding or modifying tests +# - build: Changes that affect the build system or external dependencies +# - ci: Changes to CI configuration files and scripts +# - chore: Routine tasks, build process changes, etc. +# - revert: Revert a previous commit +# +## SCOPE (optional, specify the affected area, e.g., component, module): diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9af808872..660e72aac5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,16 +34,56 @@ information on using pull requests. ## Setup -### Environment +Genkit supports JavaScript, Go, and Python. Before contributing in any of these languages, complete these prerequisites: -1. [Install node v20](https://nodejs.org/en/download) -2. Run `corepack enable pnpm` to enable pnpm. +1. Install Node.js 20 or later using [nvm](https://nodejs.org/en/download) -Note: We recommend using Node v20 or greater when compiling and running Genkit. -Any older versions of Node may not work properly. + > **Note:** Node.js v20 or greater is required. Earlier versions may not work properly. + +2. Install the Genkit CLI globally: + ```bash + npm install -g genkit-cli + ``` + +After completing these prerequisites, follow the language-specific setup instructions below. + +## Go Guide + +1. Install Go 1.24 or later + Follow the [official Go installation guide](https://golang.org/doc/install). + +2. Configure your AI model + Most samples use Google's Gemini model. You'll need to generate an API key at [Google AI Studio](https://aistudio.google.com/app/apikey). + + Once you have your key, set it in your environment: + + ```bash + export GOOGLE_GENAI_API_KEY= + ``` + +3. Run a sample application + + ```bash + cd go/samples # Navigate to samples directory + cd # Choose a sample to run + go mod tidy # Install Go dependencies + genkit start -- go run . # Start the Genkit server and run the application + ``` + + Once running, visit http://localhost:4000 to access the Developer UI. + +4. Run tests + ```bash + cd # Navigate to test directory + go test . # Run tests in current directory + ``` + +## JS Guide ### Install dependencies +Run `corepack enable pnpm` to enable pnpm. + ``` pnpm i pnpm run setup @@ -160,7 +200,7 @@ cd js && pnpm build && pnpm typedoc-html && open api-refs-js/index.html ## Send it -Once done coding you will want to send a PR. Always do things in a separate branch (by convention name the branch `your_name-feature-something`). +Once done coding you will want to send a PR. Always do things in a separate branch (by convention name the branch `your-name/feature-something`). Before sending the PR, always run: diff --git a/bin/add_license b/bin/add_license new file mode 100755 index 0000000000..e8fdb9a16a --- /dev/null +++ b/bin/add_license @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +# +# Adds a license header to all files that don't already have it. + +# set -x # Uncomment to enable tracing. +set -euo pipefail + +TOP_DIR=$(git rev-parse --show-toplevel) + +if ! command -v addlicense &>/dev/null; then + if ! command -v go &>/dev/null; then + echo "Please install go" + exit 1 + fi + echo "Installing addlicense..." + go install github.com/google/addlicense@latest +fi + +# NOTE: If you edit the ignore patterns, make sure to update the ignore patterns +# in the corresponding check_license script. +$HOME/go/bin/addlicense \ + -c "Google LLC" \ + -s=only \ + -l apache \ + -ignore '**/.dist/**/*' \ + -ignore '**/.eggs/**/*' \ + -ignore '**/.idea/**/*' \ + -ignore '**/.mypy_cache/**/*' \ + -ignore '**/.next/**/*' \ + -ignore '**/.output/**/*' \ + -ignore '**/.pytest_cache/**/*' \ + -ignore '**/.ruff_cache/**/*' \ + -ignore '**/.venv/**/*' \ + -ignore '**/.wxt/**/*' \ + -ignore '**/__pycache__/**/*' \ + -ignore '**/bazel-*/**/*' \ + -ignore '**/coverage/**/*' \ + -ignore '**/develop-eggs/**/*' \ + -ignore '**/dist/**/*' \ + -ignore '**/node_modules/**/*' \ + -ignore '**/pnpm-lock.yaml' \ + -ignore '.nx/**/*' \ + -ignore '.trunk/**/*' \ + "$TOP_DIR" diff --git a/py/bin/check-licenses b/bin/check-licenses similarity index 100% rename from py/bin/check-licenses rename to bin/check-licenses diff --git a/bin/check_license b/bin/check_license new file mode 100755 index 0000000000..c48621876f --- /dev/null +++ b/bin/check_license @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +# +# Checks that all files have a license header. + +# set -x # Uncomment to enable tracing. +set -euo pipefail + +TOP_DIR=$(git rev-parse --show-toplevel) + +if ! command -v addlicense &>/dev/null; then + if ! command -v go &>/dev/null; then + echo "Please install go" + exit 1 + fi + echo "Installing addlicense..." + go install github.com/google/addlicense@latest +fi + +export PATH=$(go env GOPATH):$PATH + +# NOTE: If you edit the ignore patterns, make sure to update the ignore patterns +# in the corresponding add_license script. +$HOME/go/bin/addlicense \ + -check \ + -c "Google LLC" \ + -s=only \ + -l apache \ + -ignore '**/.dist/**/*' \ + -ignore '**/.eggs/**/*' \ + -ignore '**/.idea/**/*' \ + -ignore '**/.mypy_cache/**/*' \ + -ignore '**/.next/**/*' \ + -ignore '**/.output/**/*' \ + -ignore '**/.pytest_cache/**/*' \ + -ignore '**/.ruff_cache/**/*' \ + -ignore '**/.venv/**/*' \ + -ignore '**/.wxt/**/*' \ + -ignore '**/__pycache__/**/*' \ + -ignore '**/bazel-*/**/*' \ + -ignore '**/coverage/**/*' \ + -ignore '**/develop-eggs/**/*' \ + -ignore '**/dist/**/*' \ + -ignore '**/node_modules/**/*' \ + -ignore '**/pnpm-lock.yaml' \ + -ignore '.nx/**/*' \ + -ignore '.trunk/**/*' \ + "$TOP_DIR" diff --git a/py/bin/fmt b/bin/fmt similarity index 50% rename from py/bin/fmt rename to bin/fmt index fda0432d25..aa5f9f578f 100755 --- a/py/bin/fmt +++ b/bin/fmt @@ -15,25 +15,18 @@ fi TOP_DIR=$(git rev-parse --show-toplevel) -addlicense \ - -c "Google LLC" \ - -s=only \ - -ignore '**/.github/**/*' \ - -ignore '**/.mypy_cache/**/*' \ - -ignore '**/bazel-*/**/*' \ - -ignore '**/docs/**/*' \ - -ignore '**/node_modules/**/*' \ - -ignore '**/pnpm-lock.yaml' \ - "$TOP_DIR" +# Add license header to all files that don't already have it. +"${TOP_DIR}/bin/add_license" # Format all TOML files. -"${TOP_DIR}/py/bin/format_toml_files" +"${TOP_DIR}/bin/format_toml_files" if [[ $? -ne 0 ]]; then exit 1 fi -# Format all Python code. -uvx ruff format "${TOP_DIR}/py" +# Format all Python code while organizing imports. +uv run --directory "${TOP_DIR}/py" ruff check --select I --fix --preview --unsafe-fixes . +uv run --directory "${TOP_DIR}/py" ruff format . if [[ $? -ne 0 ]]; then exit 1 fi @@ -47,9 +40,13 @@ fi popd # Format all TypeScript code. -pushd ${TOP_DIR} -pnpm run format -if [[ $? -ne 0 ]]; then - exit 1 -fi -popd +# +# TODO: Re-enable once we have biome configured and enabled because that is +# several times faster and compatible. +# +#pushd ${TOP_DIR} +#pnpm run format +#if [[ $? -ne 0 ]]; then +# exit 1 +#fi +#popd diff --git a/bin/format_toml_files b/bin/format_toml_files new file mode 100755 index 0000000000..90e1689cc9 --- /dev/null +++ b/bin/format_toml_files @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Format all TOML files in the project. +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +TOP_DIR=$(git rev-parse --show-toplevel) + +if command -v taplo >/dev/null 2>&1; then + if [ ! -f "${TOP_DIR}/taplo.toml" ]; then + echo "error: config file not found at ${TOP_DIR}/taplo.toml" + exit 1 + fi + + FORMATTER_COMMAND="taplo format --config ${TOP_DIR}/taplo.toml" + if command -v rust-parallel >/dev/null 2>&1; then + FORMATTER_COMMAND="rust-parallel -j4 ${FORMATTER_COMMAND}" + else + echo "warning: it is recommended to install https://crates.io/crates/rust-parallel for faster formatting" + fi + + pushd "${TOP_DIR}" + if command -v fd >/dev/null 2>&1; then + echo "Using fd" + fd -e toml \ + --exclude '**/*.egg-info/**' \ + --exclude '**/.dist/**' \ + --exclude '**/.next/**' \ + --exclude '**/.output/**' \ + --exclude '**/.pytest_cache/**' \ + --exclude '**/.venv/**' \ + --exclude '**/__pycache__/**' \ + --exclude '**/bazel-*/**' \ + --exclude '**/build/**' \ + --exclude '**/develop-eggs/**' \ + --exclude '**/dist/**' \ + --exclude '**/eggs/**' \ + --exclude '**/node_modules/**' \ + --exclude '**/sdist/**' \ + --exclude '**/site/**' \ + --exclude '**/target/**' \ + --exclude '**/venv/**' \ + --exclude '**/wheels/**' | + ${FORMATTER_COMMAND} + else + echo "Please install https://github.com/sharkdp/fd to find files to format." + fi + popd +else + echo "Please install https://github.com/tamasfe/taplo to format TOML files." +fi diff --git a/bin/golang b/bin/golang new file mode 100755 index 0000000000..5d7b51ae59 --- /dev/null +++ b/bin/golang @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +# +# Libraries usually need to support a sliding window of versions of Go. See +# https://go.dev/dl/ for the list of available versions. + +#set -x # To enable tracing. +set -euo pipefail + +# Check if a Go version is provided as the first argument +if [ -z "$1" ]; then + echo "Usage: $0 [arguments...]" + exit 1 +fi + +go_version="$1" +shift + +# Construct the go tool name (e.g., go1.18, go1.20) +go_tool="go${go_version}" + +# Check if the specified Go version is already installed. If not, install it. +if ! command -v "$go_tool" &>/dev/null; then + echo "Installing Go version $go_version..." + GOBIN="$HOME/go/bin" # or wherever you want your go binaries + export GOBIN + GOPATH="$HOME/go" # or wherever your go path is set + export GOPATH + go install "golang.org/dl/${go_tool}@latest" + if [ $? -ne 0 ]; then + echo "Failed to install Go version $go_version" + exit 1 + fi + "$go_tool" download + # Add GOBIN to your PATH if it's not already there. This is crucial. + if [[ ":$PATH:" != *":$GOBIN:"* ]]; then + export PATH="$GOBIN:$PATH" + echo "Added $GOBIN to PATH. You may need to source your profile for this to take effect in future sessions." + fi +fi + +# Execute the command with the specified Go version +"$HOME/go/bin/$go_tool" "$@" + +exit $? diff --git a/bin/run_go_tests b/bin/run_go_tests new file mode 100755 index 0000000000..e37b6e064c --- /dev/null +++ b/bin/run_go_tests @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Run tests for all supported Go versions +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +TOP_DIR=$(git rev-parse --show-toplevel) + +# We're concerned about only the release versions of Go, not "tip", but it has +# been included as an example in case it is needed in the future. +GO_VERSIONS=( + "1.22.12" + "1.23.6" + "1.24.0" + #"tip" # Fetches and builds the latest version of go from source and is slow. +) + +cd "${TOP_DIR}/go" + +for VERSION in "${GO_VERSIONS[@]}"; do + echo "Running tests with Go ${VERSION}..." + pushd "${TOP_DIR}/go" &>/dev/null + "${TOP_DIR}/bin/golang" "${VERSION}" test ./... || true # TODO: Skip failures temporarily. + popd &>/dev/null +done diff --git a/py/bin/setup b/bin/setup similarity index 85% rename from py/bin/setup rename to bin/setup index 77180dc103..1dbbef0028 100755 --- a/py/bin/setup +++ b/bin/setup @@ -5,6 +5,15 @@ # Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 +# NOTE: This script is not specific to any particular runtime. It is intended to +# be used as a convenience script for eng so that all the runtimes are set up in +# a consistent manner so that pre-commit hooks run properly and the environment +# is consistent. + +# TODO: This script is nowhere close to perfect. At a later date, we can replace +# this with something like nix to have a reproducible environment. For now this +# is a convenience script just to get eng started as quickly as possible. + if ((EUID == 0)) && [[ -z ${DANGEROUSLY_RUN_AS_ROOT+x} ]]; then echo "Please do not run as root unless DANGEROUSLY_RUN_AS_ROOT is set." exit 1 @@ -87,14 +96,16 @@ function genkit::install_prerequisites() { if [[ ${OS_NAME} == "Darwin" && -x "$(command -v brew)" ]]; then # Darwin-based systems. brew install \ + cmake \ curl \ fd \ gh \ go \ - node \ python3 \ ripgrep elif [[ -x "$(command -v apt)" ]]; then + sudo apt update -y && sudo apt upgrade -y + # Check if the OS is Ubuntu 22.04 (or a derivative) since some of our eng # use it. if lsb_release -a | grep -q "Description:.*Ubuntu 22.04"; then @@ -105,26 +116,30 @@ function genkit::install_prerequisites() { sudo apt install -y golang fi + if lsb_release -a | grep -q "Description:.*Ubuntu"; then + sudo apt install -y build-essential + fi + # Debian-based systems. sudo apt install -y \ + cmake \ curl \ fd-find \ gh \ - nodejs \ python3 \ ripgrep elif [[ -x "$(command -v dnf)" ]]; then # Fedora-based systems. sudo dnf install -y \ + cmake \ curl \ fd-find \ gh \ go \ - node \ python3 \ ripgrep else - echo "Unsupported OS. Please install protoc manually." + echo "Unsupported OS. Please install tools manually." fi genkit::install_rust @@ -172,11 +187,10 @@ function genkit::install_google_cloud_sdk() { if command -v gcloud &>/dev/null; then gcloud config set disable_usage_reporting true gcloud components update - return 0 + else + curl https://sdk.cloud.google.com | bash -s -- --disable-prompts + gcloud config set disable_usage_reporting true fi - - curl https://sdk.cloud.google.com | bash -s -- --disable-prompts - gcloud config set disable_usage_reporting true } # Install all the required tools that have been written in Go. @@ -202,6 +216,7 @@ function genkit::install_go_cli_tools_eng() { function genkit::install_cargo_cli_tools_eng() { cargo install --locked \ convco \ + pylyzer \ rust-parallel \ taplo-cli } @@ -240,9 +255,17 @@ function genkit::install_docs_cli_tools() { --with mkdocstrings[python] } +# Configure the commit message template. +function genkit::configure_commit_template() { + echo "Setting up commit message template..." + ln -sf "${TOP_DIR}/COMMIT_MESSAGE_TEMPLATE" "${TOP_DIR}/.git/COMMIT_MESSAGE_TEMPLATE" + git config commit.template "${TOP_DIR}/.git/COMMIT_MESSAGE_TEMPLATE" +} + # Install pre-commit hooks. function genkit::install_pre_commit_hooks() { - captainhook install -f -c "${TOP_DIR}/py/captainhook.json" + genkit::configure_commit_template + captainhook install -f -c "${TOP_DIR}/captainhook.json" } # Setup genkit. @@ -273,8 +296,8 @@ function genkit::install_eng_packages() { genkit::install_common_packages genkit::install_go_cli_tools_eng genkit::install_cargo_cli_tools_eng - genkit::install_google_cloud_sdk genkit::install_pre_commit_hooks + genkit::install_google_cloud_sdk genkit::setup_genkit } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000000..c89309e043 --- /dev/null +++ b/biome.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "files": { + "ignoreUnknown": true, + "ignore": [ + "**/.dist/**", + "**/.eggs/**", + "**/.idea/**", + "**/.mypy_cache/**", + "**/.next/**", + "**/.output/**", + "**/.pytest_cache/**", + "**/.ruff_cache/**", + "**/.venv/**", + "**/.wxt/**", + "**/__pycache__/**", + "**/coverage/**", + "**/develop-eggs/**", + "**/dist/**", + "**/manifest.*.json", + "**/node_modules/**", + ".nx/**", + ".trunk/**", + "bazel-*/**", + "node_modules/**", + "third_party/**" + ] + }, + "formatter": { + "useEditorconfig": true + }, + "vcs": { + "clientKind": "git", + "defaultBranch": "main", + "enabled": true, + "useIgnoreFile": true + }, + "javascript": { + "formatter": { + "arrowParentheses": "asNeeded", + "attributePosition": "auto", + "bracketSpacing": true, + "jsxQuoteStyle": "double", + "lineEnding": "lf", + "lineWidth": 80, + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "es5" + } + }, + "json": { + "formatter": { + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80, + "trailingCommas": "none" + } + } +} diff --git a/py/captainhook.json b/captainhook.json similarity index 58% rename from py/captainhook.json rename to captainhook.json index 56bbad8147..a2c9217489 100644 --- a/py/captainhook.json +++ b/captainhook.json @@ -6,14 +6,14 @@ "commit-msg": { "actions": [ { - "run": "convco check -n 1" + "run": ".hooks/conventional-commit-msg {$MESSAGE_FILE}" } ] }, "pre-commit": { "actions": [ { - "run": "py/.hooks/no-commits-on-branches main" + "run": ".hooks/no-commits-on-branches main" }, { "run": "CaptainHook::File.MaxSize", @@ -34,16 +34,13 @@ "run": "pnpm i --frozen-lockfile" }, { - "run": "py/bin/generate_schema_types" + "run": "py/bin/generate_schema_typing" }, { - "run": "py/bin/fmt" + "run": "bin/fmt" }, { - "run": "uvx --directory py ruff check --fix ." - }, - { - "run": "py/bin/run_tests" + "run": "py/bin/run_python_tests" }, { "run": "uv run --directory py mkdocs build" @@ -52,18 +49,7 @@ "run": "py/bin/build_dists" }, { - "run": "go test go/..." - }, - { - "run": "govulncheck -C go ./...", - "conditions": [ - { - "run": "CaptainHook::FileChanged.Any", - "options": { - "files": ["go/go.mod", "go.sum", "*.go"] - } - } - ] + "run": "bin/run_go_tests" } ] }, @@ -85,16 +71,13 @@ "run": "pnpm i --frozen-lockfile" }, { - "run": "py/bin/generate_schema_types" + "run": "py/bin/generate_schema_typing" }, { - "run": "py/bin/fmt" + "run": "bin/fmt" }, { - "run": "uvx --directory py ruff check --fix ." - }, - { - "run": "py/bin/run_tests" + "run": "py/bin/run_python_tests" }, { "run": "uv run --directory py mkdocs build" @@ -103,21 +86,10 @@ "run": "py/bin/build_dists" }, { - "run": "go test go/..." - }, - { - "run": "govulncheck -C go ./...", - "conditions": [ - { - "run": "CaptainHook::FileChanged.Any", - "options": { - "files": ["go/go.mod", "go.sum", "*.go"] - } - } - ] + "run": "bin/run_go_tests" }, { - "run": "py/.hooks/commit-message-format-pre-push" + "run": ".hooks/commit-message-format-pre-push" } ] } diff --git a/docs/_guides.yaml b/docs/_guides.yaml index 783cf124c2..a4a69e7c38 100644 --- a/docs/_guides.yaml +++ b/docs/_guides.yaml @@ -17,34 +17,50 @@ toc: path: /docs/genkit/ - title: Get started path: /docs/genkit/get-started + - title: Migrate from Genkit 0.9 to 1.0 + path: /docs/genkit/migrating-from-0.9 + - title: API Stability Channels + path: /docs/genkit/api-stability.md - title: Developer tools path: /docs/genkit/devtools + - heading: API reference + - title: Genkit JS API reference + path: https://js.api.genkit.dev/ + - heading: Codelabs - title: Chat with a PDF file path: /docs/genkit/codelabs/codelab-chat-with-a-pdf - - - heading: Build AI workflows + + - heading: Building AI workflows - title: Generating content path: /docs/genkit/models + - title: Passing information through context + path: /docs/genkit/context - title: Creating flows path: /docs/genkit/flows - title: Managing prompts with Dotprompt path: /docs/genkit/dotprompt - title: Persistent chat sessions path: /docs/genkit/chat - - title: Tool calling - path: /docs/genkit/tool-calling + - title: Tools + section: + - title: Tool calling + path: /docs/genkit/tool-calling + - title: Pause generation using interrupts + path: /docs/genkit/interrupts - title: Retrieval-augmented generation (RAG) path: /docs/genkit/rag - title: Multi-agent systems path: /docs/genkit/multi-agent - title: Evaluation path: /docs/genkit/evaluation - - title: Local Observability + - title: Observe local metrics path: /docs/genkit/local-observability - - - heading: Deploy AI workflows + - title: Error Types + path: /docs/genkit/errors/types.md + + - heading: Deploying AI workflows - title: Deploy with Firebase path: /docs/genkit/firebase - title: Deploy with Cloud Run @@ -54,18 +70,24 @@ toc: - title: Authorization and integrity path: /docs/genkit/auth - - heading: Observe AI workflows + - heading: Observing AI workflows - title: Getting Started path: /docs/genkit/observability/getting-started + - title: Authentication + path: /docs/genkit/observability/authentication + - title: Advanced Configuration + path: /docs/genkit/observability/advanced-configuration + - title: Telemetry Collection + path: /docs/genkit/observability/telemetry-collection - title: Troubleshooting path: /docs/genkit/observability/troubleshooting - - - heading: Write plugins + + - heading: Writing plugins - title: Overview path: /docs/genkit/plugin-authoring - title: Writing an Evaluator Plugin path: /docs/genkit/plugin-authoring-evaluator - + - heading: Official plugins - title: Google AI for Developers path: /docs/genkit/plugins/google-genai @@ -81,21 +103,15 @@ toc: path: /docs/genkit/templates/pgvector - title: Firebase path: /docs/genkit/plugins/firebase - - title: Google Cloud - path: /docs/genkit/plugins/google-cloud - break: true - title: Using Genkit with Next.js path: /docs/genkit/nextjs - break: true - - title: Migrate from Genkit 0.5 + - title: Migrate from Genkit 0.5 to 0.9 path: /docs/genkit/migrating-from-0.5 - break: true - title: Connect with us path: /docs/genkit/feedback - -# reference: -# - title: Resource summary -# path: /docs/genkit/reference/coming-soon diff --git a/docs/api-stability.md b/docs/api-stability.md index 4be9eec74f..8ecc374fa8 100644 --- a/docs/api-stability.md +++ b/docs/api-stability.md @@ -12,7 +12,7 @@ been declared stable. The beta channel may include breaking changes on ## Using the Stable Channel -To use the stable channel of Genkit, import from the standard `"genkit"` +To use the stable channel of Genkit, import from the standard `"genkit"` entrypoint: ```ts @@ -51,4 +51,4 @@ for beta features. You can modify your existing dependency string by changing along with persistent sessions that store both conversation history and an arbitrary state object. - **[Interrupts](interrupts):** special tools that can pause generation for - human-in-the-loop feedback, out-of-band processing, and more. \ No newline at end of file + human-in-the-loop feedback, out-of-band processing, and more. diff --git a/docs/auth.md b/docs/auth.md index 1b31a1debb..af08c4ce35 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -9,7 +9,7 @@ only by verified client applications. Firebase Genkit provides mechanisms for managing authorization policies and contexts. Flows running on Firebase can use an auth policy callback (or helper). Alternatively, Firebase also provides auth context into the flow where it can -do its own checks. For non-Functions flows, auth can be managed and set +do its own checks. For non-Functions flows, auth can be managed and set through middleware. ## Authorize within a Flow {:# authorize-within-flow} diff --git a/docs/chat.md b/docs/chat.md index 08916ae382..5ab2699d6c 100644 --- a/docs/chat.md +++ b/docs/chat.md @@ -1,3 +1,7 @@ +Beta: This feature of Genkit is in **Beta,** which means it is not yet +part of Genkit's stable API. APIs of beta features may change in minor +version releases. + # Creating persistent chat sessions Many of your users will have interacted with large language models for the first @@ -28,16 +32,16 @@ Here is a minimal, console-based, chatbot application: ```ts import { genkit } from "genkit/beta"; -import { googleAI, gemini15Flash } from "@genkit-ai/googleai"; +import { googleAI, gemini20Flash } from "@genkit-ai/googleai"; import { createInterface } from "node:readline/promises"; const ai = genkit({ plugins: [googleAI()], - model: gemini15Flash, + model: gemini20Flash, }); -(async () => { +async function main() { const chat = ai.chat(); console.log("You're chatting with Gemini. Ctrl-C to quit.\n"); const readline = createInterface(process.stdin, process.stdout); @@ -46,7 +50,9 @@ const ai = genkit({ const { text } = await chat.send(userInput); console.log(text); } -})(); +} + +main(); ``` A chat session with this program looks something like the following example: diff --git a/docs/cloud-run.md b/docs/cloud-run.md index 14c650041b..34fa601c0e 100644 --- a/docs/cloud-run.md +++ b/docs/cloud-run.md @@ -1,8 +1,8 @@ # Deploy flows using Cloud Run You can deploy Genkit flows as HTTPS endpoints using Cloud Run. Cloud Run has -several deployment options, including container based deployment; this page will -explain how to deploy your flows directly from code. +several deployment options, including container based deployment; this page +explains how to deploy your flows directly from code. ## Before you begin @@ -33,7 +33,7 @@ If you don't already have a Google Cloud project set up, follow these steps: For your flows to be deployable, you will need to make some small changes to your project code: -### Add start and build scripts to package.json +### Add start and build scripts to package.json When deploying a Node.js project to Cloud Run, the deployment tools expect your project to have a `start` script and, optionally, a `build` script. For a @@ -54,7 +54,8 @@ endpoints. When you make the call, specify the flows you want to serve: -There is also +There is also: + ```ts import { startFlowServer } from '@genkit-ai/express'; @@ -127,7 +128,7 @@ See Refer to [express plugin documentation](https://js.api.genkit.dev/modules/_genkit-ai_express.html) for more details. -### Make API credentials available to deployed flows +### Make API credentials available to deployed flows Once deployed, your flows need some way to authenticate with any remote services they rely on. Most flows will at a minimum need credentials for accessing the @@ -138,35 +139,35 @@ chose: - {Gemini (Google AI)} - 1. Make sure Google AI is - [available in your region](https://ai.google.dev/available_regions). + 1. Make sure Google AI is + [available in your region](https://ai.google.dev/available_regions). - 1. [Generate an API key](https://aistudio.google.com/app/apikey) for the - Gemini API using Google AI Studio. + 1. [Generate an API key](https://aistudio.google.com/app/apikey) for the + Gemini API using Google AI Studio. - 1. Make the API key available in the Cloud Run environment: + 1. Make the API key available in the Cloud Run environment: - 1. In the Cloud console, enable the - [Secret Manager API](https://console.cloud.google.com/apis/library/secretmanager.googleapis.com?project=_). - 1. On the [Secret Manager](https://console.cloud.google.com/security/secret-manager?project=_) - page, create a new secret containing your API key. - 1. After you create the secret, on the same page, grant your default - compute service account access to the secret with the **Secret - Manager Secret Accessor** role. (You can look up the name of the - default compute service account on the IAM page.) + 1. In the Cloud console, enable the + [Secret Manager API](https://console.cloud.google.com/apis/library/secretmanager.googleapis.com?project=_). + 1. On the [Secret Manager](https://console.cloud.google.com/security/secret-manager?project=_) + page, create a new secret containing your API key. + 1. After you create the secret, on the same page, grant your default + compute service account access to the secret with the **Secret + Manager Secret Accessor** role. (You can look up the name of the + default compute service account on the IAM page.) - In a later step, when you deploy your service, you will need to - reference the name of this secret. + In a later step, when you deploy your service, you will need to + reference the name of this secret. - {Gemini (Vertex AI)} - 1. In the Cloud console, - [Enable the Vertex AI API](https://console.cloud.google.com/apis/library/aiplatform.googleapis.com?project=_) - for your project. + 1. In the Cloud console, + [Enable the Vertex AI API](https://console.cloud.google.com/apis/library/aiplatform.googleapis.com?project=_) + for your project. - 1. On the [IAM](https://console.cloud.google.com/iam-admin/iam?project=_) - page, ensure that the **Default compute service account** is granted the - **Vertex AI User** role. + 1. On the [IAM](https://console.cloud.google.com/iam-admin/iam?project=_) + page, ensure that the **Default compute service account** is granted the + **Vertex AI User** role. The only secret you need to set up for this tutorial is for the model provider, but in general, you must do something similar for each service your flow uses. @@ -205,4 +206,4 @@ it with `curl`: curl -X POST https:///menuSuggestionFlow \ -H "Authorization: Bearer $(gcloud auth print-identity-token)" \ -H "Content-Type: application/json" -d '{"data": "banana"}' -``` +``` \ No newline at end of file diff --git a/docs/codelabs/codelab-chat-with-a-pdf.md b/docs/codelabs/codelab-chat-with-a-pdf.md index e3159b424f..ea823e98b6 100644 --- a/docs/codelabs/codelab-chat-with-a-pdf.md +++ b/docs/codelabs/codelab-chat-with-a-pdf.md @@ -1,7 +1,7 @@ # Chat with a PDF file -You can use Genkit to build an app that lets its user chat with a PDF file. -To do this, follow these steps: +This codelab demonstrates how to build a conversational application that +allows users to extract information from PDF documents using natural language. 1. [Set up your project](#setup-project) 1. [Import the required dependencies](#import-dependencies) @@ -12,19 +12,16 @@ To do this, follow these steps: 1. [Implement the chat loop](#implement-the-chat-loop) 1. [Run the app](#run-the-app) -This guide explains how to perform each of these tasks. +## Prerequisites {:#prerequisites} -## Dependencies {:#dependencies} - -Before starting work, you should have these dependencies set up: +Before starting work, you should have these prerequisites set up: * [Node.js v20+](https://nodejs.org/en/download) * [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) -## Tasks {:#tasks} +## Implementation Steps {:#implementation-steps} -After setting up your dependencies, you can build the -project, itself. +After setting up your dependencies, you can build the project. ### 1. Set up your project {:#setup-project} @@ -33,8 +30,8 @@ your source code. ```shell $ mkdir -p chat-with-a-pdf/src && \ - cd chat-with-a-pdf/src && \ - touch index.ts + cd chat-with-a-pdf && \ + touch src/index.ts ``` 1. Initialize a new TypeScript project. @@ -43,137 +40,139 @@ your source code. $ npm init -y ``` -1. Install the pdf-parse module: +1. Install the pdf-parse module. ```shell - $ npm i pdf-parse + $ npm i pdf-parse && npm i -D @types/pdf-parse ``` 1. Install the following Genkit dependencies to use Genkit in your project: ```shell - $ npm install genkit @genkit-ai/googleai + $ npm i genkit @genkit-ai/googleai ``` -* `genkit` provides Genkit core capabilities. -* `@genkit-ai/googleai` provides access to the Google AI Gemini models. - -      5. Get and configure -your model API key {:#configure-your-model-api-key} - -
    - -To use the Gemini API, which this codelab uses, you must first -configure an API key. If you don't already have one, -create a -key in Google AI Studio. + * `genkit` provides Genkit core capabilities. + * `@genkit-ai/googleai` provides access to the Google AI Gemini models. -The Gemini API provides a generous free-of-charge tier and does not require a -credit card to get started. +1. Get and configure your model API key {:#configure-your-model-api-key} -After creating your API key, set the GOOGLE_GENAI_API_KEY` -environment variable to your key with the following command: + To use the Gemini API, which this codelab uses, you must first + configure an API key. If you don't already have one, + create a + key in Google AI Studio. -
    -$ export GOOGLE_GENAI_API_KEY=<your API key>
    -
    + The Gemini API provides a generous free-of-charge tier and does not require a + credit card to get started. -
-
+ After creating your API key, set the `GOOGLE_GENAI_API_KEY` environment + variable to your key with the following command: -**Note:** Although this tutorial uses the Gemini API from AI Studio, Genkit -supports a wide variety of model providers, including: + ```shell + $ export GOOGLE_GENAI_API_KEY= + ``` -* [Gemini from Vertex AI](https://firebase.google.com/docs/genkit/plugins/vertex-ai#generative_ai_models). -* Anthropic's Claude 3 models and Llama 3.1 through the -[Vertex AI Model Garden](https://firebase.google.com/docs/genkit/plugins/vertex-ai#anthropic_claude_3_on_vertex_ai_model_garden), -as well as community plugins. -* Open source models through -[Ollama](https://firebase.google.com/docs/genkit/plugins/ollama). -* [Community-supported providers](https://firebase.google.com/docs/genkit/models#models-supported) such as OpenAI and Cohere. +> **Note:** Although this tutorial uses the Gemini API from AI Studio, Genkit +> supports a wide variety of model providers, including: +> +> * [Gemini from Vertex AI](https://firebase.google.com/docs/genkit/plugins/vertex-ai#generative_ai_models). +> * Anthropic's Claude 3 models and Llama 3.1 through the +> [Vertex AI Model Garden](https://firebase.google.com/docs/genkit/plugins/vertex-ai#anthropic_claude_3_on_vertex_ai_model_garden), +> as well as community plugins. +> * Open source models through +> [Ollama](https://firebase.google.com/docs/genkit/plugins/ollama). +> * [Community-supported providers](https://firebase.google.com/docs/genkit/models#models-supported) such as OpenAI and Cohere. ### 2. Import the required dependencies {:#import-dependencies} In the `index.ts` file that you created, add the following lines to import the dependencies required for this project: - ```typescript - import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; - import { genkit } from 'genkit'; - import pdf from 'pdf-parse'; - import fs from 'fs'; - import { createInterface } from "node:readline/promises"; - ``` -
    -
  • The first two lines import Genkit and the Google AI plugin.
  • -
  • The second two lines are for the pdf parser.
  • -
  • The fifth line is for implementing your UI.
  • -
+```typescript +import { gemini20Flash, googleAI } from '@genkit-ai/googleai'; +import { genkit } from 'genkit/beta'; // chat is a beta feature +import pdf from 'pdf-parse'; +import fs from 'fs'; +import { createInterface } from "node:readline/promises"; +``` + +* The first line imports the `gemini20Flash` model and the `googleAI` + plugin from the `@genkit-ai/googleai` package, enabling access to + Google's Gemini models. +* The next two lines import the `pdf-parse` library for parsing PDF files + and the `fs` module for file system operations. +* The final line imports the `createInterface` function from the + `node:readline/promises` module, which is used to create a command-line + interface for user interaction. ### 3. Configure Genkit and the default model {:#configure-genkit} -Add the following lines to configure Genkit and set Gemini 1.5 Flash as the +Add the following lines to configure Genkit and set Gemini 2.0 Flash as the default model. - ```typescript - const ai = genkit({ - plugins: [googleAI()], - model: gemini15Flash, - }); - ``` +```typescript +const ai = genkit({ + plugins: [googleAI()], + model: gemini20Flash, +}); +``` You can then add a skeleton for the code and error-handling. - ```typescript - (async () => { - try { - // Step 1: get command line arguments +```typescript +(async () => { + try { + // Step 1: get command line arguments + + // Step 2: load PDF file - // Step 2: load PDF file + // Step 3: construct prompt - // Step 3: construct prompt + // Step 4: start chat - // Step 4: start chat + // Step 5: chat loop - Step 5: chat loop + } catch (error) { + console.error("Error parsing PDF or interacting with Genkit:", error); + } +})(); // <-- don't forget the trailing parentheses to call the function! +``` - } catch (error) { - console.error("Error parsing PDF or interacting with Genkit:", error); - } - })(); // <-- don't forget the trailing parentheses to call the function! - ``` ### 4. Load and parse the PDF {:#load-and-parse} -1. Under Step 1, add code to read the PDF filename that was passed +1. Add code to read the PDF filename that was passed in from the command line. - ```typescript - const filename = process.argv[2]; - if (!filename) { - console.error("Please provide a filename as a command line argument."); - process.exit(1); - } - ``` + ```typescript + // Step 1: get command line arguments + const filename = process.argv[2]; + if (!filename) { + console.error("Please provide a filename as a command line argument."); + process.exit(1); + } + ``` -1. Under Step 2, add code to load the contents of the PDF file. +1. Add code to load the contents of the PDF file. - ```typescript - let dataBuffer = fs.readFileSync(filename); - const { text } = await pdf(dataBuffer); - ``` + ```typescript + // Step 2: load PDF file + let dataBuffer = fs.readFileSync(filename); + const { text } = await pdf(dataBuffer); + ``` ### 5. Set up the prompt {:#set-up-the-prompt} -Under Step 3, add code to set up the prompt: +Add code to set up the prompt: - ```typescript - const prefix = process.argv[3] || "Sample prompt: Answer the user's questions about the contents of this PDF file."; - const prompt = ` - ${prefix} - Context: - ${text} - ` - ``` +```typescript + // Step 3: construct prompt + const prefix = process.argv[3] || "Sample prompt: Answer the user's questions about the contents of this PDF file."; + const prompt = ` + ${prefix} + Context: + ${text} + `; +``` * The first `const` declaration defines a default prompt if the user doesn't pass in one of their own from the command line. @@ -182,14 +181,15 @@ text of the PDF file into the prompt for the model. ### 6. Implement the UI {:#implement-the-interface} -Under Step 4, add the following code to start the chat and +Add the following code to start the chat and implement the UI: - ```typescript - const chat = ai.chat({ system: prompt }) - const readline = createInterface(process.stdin, process.stdout); - console.log("You're chatting with Gemini. Ctrl-C to quit.\n"); - ``` +```typescript + // Step 4: start chat + const chat = ai.chat({ system: prompt }); + const readline = createInterface(process.stdin, process.stdout); + console.log("You're chatting with Gemini. Ctrl-C to quit.\n"); +``` The first `const` declaration starts the chat with the model by calling the `chat` method, passing the prompt (which includes @@ -202,17 +202,18 @@ Under Step 5, add code to receive user input and send that input to the model using `chat.send`. This part of the app loops until the user presses _CTRL + C_. - ```typescript - while (true) { - const userInput = await readline.question("> "); - const {text} = await chat.send(userInput); - console.log(text); - } - ``` +```typescript + // Step 5: chat loop + while (true) { + const userInput = await readline.question("> "); + const { text } = await chat.send(userInput); + console.log(text); + } +``` ### 8. Run the app {:#run-the-app} -Run the app from your terminal. Open the terminal in the root +To run the app, open the terminal in the root folder of your project, then run the following command: ```typescript diff --git a/docs/deploy-node.md b/docs/deploy-node.md index 29d89b5c2a..03ed12fbb8 100644 --- a/docs/deploy-node.md +++ b/docs/deploy-node.md @@ -47,7 +47,7 @@ sample flow. 1. **Set up a sample flow and server:** - In `src/index.ts`, define a sample flow and configure the flow server: +In `src/index.ts`, define a sample flow and configure the flow server: ```typescript import { genkit } from 'genkit'; @@ -76,33 +76,33 @@ sample flow. }); ``` - There are also some optional parameters for `startFlowServer` you can specify: +There are also some optional parameters for `startFlowServer` you can specify: - - `port`: the network port to listen on. If unspecified, the server listens on +- `port`: the network port to listen on. If unspecified, the server listens on the port defined in the PORT environment variable, and if PORT is not set, defaults to 3400. - - `cors`: the flow server's +- `cors`: the flow server's [CORS policy](https://www.npmjs.com/package/cors#configuration-options). If you will be accessing these endpoints from a web application, you likely need to specify this. - - `pathPrefix`: an optional path prefix to add before your flow endpoints. - - `jsonParserOptions`: options to pass to Express's +- `pathPrefix`: an optional path prefix to add before your flow endpoints. +- `jsonParserOptions`: options to pass to Express's [JSON body parser](https://www.npmjs.com/package/body-parser#bodyparserjsonoptions) 1. **Set up model provider credentials:** - Configure the required environment variables for your model provider. In this guide, we'll use the Gemini API from Google AI Studio as an example. +Configure the required environment variables for your model provider. In this guide, we'll use the Gemini API from Google AI Studio as an example. - [Get an API key from Google AI Studio](https://makersuite.google.com/app/apikey) +[Get an API key from Google AI Studio](https://makersuite.google.com/app/apikey) - After you’ve created an API key, set the `GOOGLE_GENAI_API_KEY` environment - variable to your key with the following command: +After you’ve created an API key, set the `GOOGLE_GENAI_API_KEY` environment +variable to your key with the following command: ```posix-terminal export GOOGLE_GENAI_API_KEY= ``` - Different providers for deployment will have different ways of securing your API key in their environment. For security, ensure that your API key is not publicly exposed. +Different providers for deployment will have different ways of securing your API key in their environment. For security, ensure that your API key is not publicly exposed. ## 3. Prepare your Node.js project for deployment diff --git a/docs/get-started.md b/docs/get-started.md index 61a1317d88..19e08804eb 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -55,20 +55,22 @@ Get started with Genkit in just a few lines of simple code. ```ts // import the Genkit and Google AI plugin libraries -import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { gemini20Flash, googleAI } from '@genkit-ai/googleai'; import { genkit } from 'genkit'; // configure a Genkit instance const ai = genkit({ plugins: [googleAI()], - model: gemini15Flash, // set default model + model: gemini20Flash, // set default model }); -(async () => { +async function main() { // make a generation request const { text } = await ai.generate('Hello, Gemini!'); console.log(text); -})(); +} + +main(); ``` ## Next steps diff --git a/docs/index.md b/docs/index.md index 091e63409f..559d200000 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,7 @@ {% setvar custom_project %}/docs/genkit/_project.yaml{% endsetvar %} {% setvar supportsNode %}true{% endsetvar %} {% setvar supportsGolang %}true{% endsetvar %} + {% setvar youtubeID %}3p1P5grjXIQ{% endsetvar %} {% endblock variables %} {% block extraMeta %} diff --git a/docs/interrupts.md b/docs/interrupts.md index a9a4413ce3..4f389e2e3d 100644 --- a/docs/interrupts.md +++ b/docs/interrupts.md @@ -1,3 +1,7 @@ +Beta: This feature of Genkit is in **Beta,** which means it is not yet +part of Genkit's stable API. APIs of beta features may change in minor +version releases. + # Pause generation using interrupts _Interrupts_ are a special kind of [tool](tool-calling) that can pause the diff --git a/docs/local-observability.md b/docs/local-observability.md index 8992231b63..2e1d6ec55b 100644 --- a/docs/local-observability.md +++ b/docs/local-observability.md @@ -1,14 +1,28 @@ -# Observability +# Observe local metrics -Genkit provides a robust set of built-in observability features, including tracing and metrics collection powered by [OpenTelemetry](https://opentelemetry.io/). For local observability (e.g. during the development phase), the Genkit Developer UI provides detailed trace viewing and debugging capabilities. For production observability, we provide Genkit Monitoring in the Firebase console via the Firebase plugin. Alternatively, you can export your OpenTelemetry data to the observability tooling of your choice. +Genkit provides a robust set of built-in observability features, including +tracing and metrics collection powered by +[OpenTelemetry](https://opentelemetry.io/). For local observability, such as +during the development phase, the Genkit Developer UI provides detailed trace +viewing and debugging capabilities. For production observability, we provide +Genkit Monitoring in the Firebase console via the Firebase plugin. +Alternatively, you can export your OpenTelemetry data to the observability +tooling of your choice. -## Tracing & Metrics +## Tracing & Metrics {:#tracing-and-metrics} -Genkit automatically collects traces and metrics without requiring explicit configuration, allowing you to observe and debug your Genkit code's behavior in the Developer UI. These traces are stored locally, enabling you to analyze your Genkit flows step-by-step with detailed input/output logging and statistics. In production, traces and metrics can be exported to Firebase Genkit Monitoring for further analysis. +Genkit automatically collects traces and metrics without requiring explicit configuration, allowing you to observe and debug your Genkit code's behavior +in the Developer UI. Genkit stores these traces, enabling you to analyze +your Genkit flows step-by-step with detailed input/output logging and +statistics. In production, Genkit can export traces and metrics to Firebase +Genkit Monitoring for further analysis. -## Logging +## Log and export events {:#log-and-export} -Genkit also provides a centralized logging system that can be configured using the logging module. One advantage of using the Genkit provided logger is that logs will automatically be exported to Genkit Monitoring when the Firebase Telemetry plugin is enabled. +Genkit provides a centralized logging system that you can configure using +the logging module. One advantage of using the Genkit-provided logger is that +it automatically exports logs to Genkit Monitoring when the Firebase +Telemetry plugin is enabled. ```typescript import { logger } from 'genkit/logging'; @@ -17,6 +31,12 @@ import { logger } from 'genkit/logging'; logger.setLogLevel('debug'); ``` -## Production Observability +## Production Observability {:#production-observability} -The [Genkit Monitoring](https://console.firebase.google.com/project/_/genai_monitoring) dashboard helps you to understand the overall health of your Genkit features, as well as debug stability and content issues that may point to problems with your LLM prompts and/or Genkit Flows. See the [Getting Started](./observability/getting-started.md) guide for more details. +The +[Genkit Monitoring](https://console.firebase.google.com/project/_/genai_monitoring) +dashboard helps you understand the overall health of your Genkit features. It +is also useful for debugging stability and content issues that may +indicate problems with your LLM prompts and/or Genkit Flows. See the +[Getting Started](/docs/genkit/observability/getting-started) guide for +more details. \ No newline at end of file diff --git a/docs/multi-agent.md b/docs/multi-agent.md index 6ae92cdbbb..a9d8f0277a 100644 --- a/docs/multi-agent.md +++ b/docs/multi-agent.md @@ -1,3 +1,7 @@ +Beta: This feature of Genkit is in **Beta,** which means it is not yet +part of Genkit's stable API. APIs of beta features may change in minor +version releases. + # Building multi-agent systems A powerful application of large language models are LLM-powered agents. An agent diff --git a/docs/observability/advanced-configuration.md b/docs/observability/advanced-configuration.md new file mode 100644 index 0000000000..0b259812b4 --- /dev/null +++ b/docs/observability/advanced-configuration.md @@ -0,0 +1,142 @@ +# Advanced Configuration {: #advanced-configuration } + +This guide focuses on advanced configuration options for deployed features using +the Firebase telemetry plugin. Detailed descriptions of each configuration +option can be found in our +[JS API reference documentation](https://js.api.genkit.dev/interfaces/_genkit-ai_google-cloud.GcpTelemetryConfigOptions.html). + +This documentation will describe how to fine-tune which telemetry is collected, +how often, and from what environments. + +## Default Configuration {: #default-configuration } + +The Firebase telemetry plugin provides default options, out of the box, to get +you up and running quickly. These are the provided defaults: + +```typescript +{ + autoInstrumentation: true, + autoInstrumentationConfig: { + '@opentelemetry/instrumentation-dns': { enabled: false }, + } + disableMetrics: false, + disableTraces: false, + disableLoggingInputAndOutput: false, + forceDevExport: false, + // 5 minutes + metricExportIntervalMillis: 300_000, + // 5 minutes + metricExportTimeoutMillis: 300_000, + // See https://js.api.genkit.dev/interfaces/_genkit-ai_google-cloud.GcpTelemetryConfigOptions.html#sampler + sampler: AlwaysOnSampler() +} +``` + +## Export local telemetry {: #export-local-telemetry } + +To export telemetry when running locally set the `forceDevExport` option to +`true`. + +```typescript +import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; + +enableFirebaseTelemetry({forceDevExport: true}); +``` + +During development and testing, you can decrease latency by adjusting the export +interval and timeout. + +Note: Shipping to production with a frequent export interval may +increase the cost for exported telemetry. + +```typescript +import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; + +enableFirebaseTelemetry({ + forceDevExport: true, + metricExportIntervalMillis: 10_000, // 10 seconds + metricExportTimeoutMillis: 10_000 // 10 seconds +}); +``` + +## Adjust auto instrumentation {: #adjust-auto-instrumentation } + +The Firebase telemetry plugin will automatically collect traces and metrics for +popular frameworks using OpenTelemetry [zero-code instrumentation](https://opentelemetry.io/docs/zero-code/js/). + +A full list of available instrumentations can be found in the +[auto-instrumentations-node](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/metapackages/auto-instrumentations-node/README.md#supported-instrumentations) +documentation. + +To selectively disable or enable instrumentations that are eligible for auto +instrumentation, update the `autoInstrumentationConfig` field: + +```typescript +import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; + +enableFirebaseTelemetry({ + autoInstrumentationConfig: { + '@opentelemetry/instrumentation-fs': { enabled: false }, + '@opentelemetry/instrumentation-dns': { enabled: false }, + '@opentelemetry/instrumentation-net': { enabled: false }, + } +}); +``` + +## Disable telemetry {: #disable-telemetry } + +Firebase Genkit Monitoring leverages a combination of logging, tracing, and +metrics to capture a holistic view of your Genkit interactions, however, you can +also disable each of these elements independently if needed. + +### Disable input and output logging {: #disable-input-output-logging } + +By default, the Firebase telemetry plugin will capture inputs and outputs for +each Genkit feature or step. + +To help you control how customer data is stored, you can disable the logging of +input and output by adding the following to your configuration: + +```typescript +import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; + +enableFirebaseTelemetry({ + disableLoggingInputAndOutput: true +}); +``` + +With this option set, input and output attributes will be redacted +in the Firebase Genkit Monitoring trace viewer and will be missing +from Google Cloud logging. + +### Disable metrics {: #disable-metrics } + +To disable metrics collection, add the following to your configuration: + +```typescript +import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; + +enableFirebaseTelemetry({ + disableMetrics: true +}); +``` + +With this option set, you will no longer see stability metrics in the +Firebase Genkit Monitoring dashboard and will be missing from Google Cloud +Metrics. + +### Disable traces {: #disable-traces } + +To disable trace collection, add the following to your configuration: + +```typescript +import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; + +enableFirebaseTelemetry({ + disableTraces: true +}); +``` + +With this option set, you will no longer see traces in the Firebase Genkit +Monitoring feature page, have access to the trace viewer, or see traces +present in Google Cloud Tracing. \ No newline at end of file diff --git a/docs/observability/authentication.md b/docs/observability/authentication.md new file mode 100644 index 0000000000..8b2a28aeef --- /dev/null +++ b/docs/observability/authentication.md @@ -0,0 +1,158 @@ +# Authentication and authorization {: #authentication } + +The Firebase telemetry plugin requires a Google Cloud or Firebase project ID +and application credentials. + +If you don't have a Google Cloud project and account, you can set one up in the +[Firebase Console](https://console.firebase.google.com/) or in the +[Google Cloud Console](https://cloud.google.com). All Firebase project IDs are +Google Cloud project IDs. + +## Enable APIs {: #enable-apis } + +Prior to adding the plugin, make sure the following APIs are enabled for +your project: + +- [Cloud Logging API](https://console.cloud.google.com/apis/library/logging.googleapis.com) +- [Cloud Trace API](https://console.cloud.google.com/apis/library/cloudtrace.googleapis.com) +- [Cloud Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) + +These APIs should be listed in the +[API dashboard](https://console.cloud.google.com/apis/dashboard) for your +project. +Click to learn more about how to [enable and disable APIs](https://support.google.com/googleapi/answer/6158841). + +## User Authentication {: #user-authentication } + +To export telemetry from your local development environment to Firebase Genkit +Monitoring, you will need to authenticate yourself with Google Cloud. + +The easiest way to authenticate as yourself is using the gcloud CLI, which will +automatically make your credentials available to the framework through +[Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials). + +If you don't have the gcloud CLI installed, first follow the [installation instructions](https://cloud.google.com/sdk/docs/install#installation_instructions). + +1. Authenticate using the `gcloud` CLI: + + ```posix-terminal + gcloud auth application-default login + ``` + +2. Set your project ID + + ```posix-terminal + gcloud config set project PROJECT_ID + ``` + +## Deploy to Google Cloud {: #deploy-to-cloud } + +If deploying your code to a Google Cloud or Firebase environment (Cloud +Functions, Cloud Run, App Hosting, etc), the project ID and credentials will be +discovered automatically with +[Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc). + +You will need to apply the following roles to the service account that is +running your code (i.e. 'attached service account') using the +[IAM Console](https://console.cloud.google.com/iam-admin/iam): + +- `roles/monitoring.metricWriter` +- `roles/cloudtrace.agent` +- `roles/logging.logWriter` + +Not sure which service account is the right one? See the +[Find or create your service account](#find-or-create-your-service-account) +section. + +## Deploy outside of Google Cloud (with ADC) {: #deploy-to-cloud-with-adc } + +If possible, use +[Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc) +to make credentials available to the plugin. + +Typically this involves generating a service account key and deploying +those credentials to your production environment. + +1. Follow the instructions to set up a + [service account key](https://cloud.google.com/iam/docs/keys-create-delete#creating). + +2. Ensure the service account has the following roles: + - `roles/monitoring.metricWriter` + - `roles/cloudtrace.agent` + - `roles/logging.logWriter` + +3. Deploy the credential file to production (**do not** check into source code) + +4. Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable as the path to + the credential file. + + ```posix-terminal + GOOGLE_APPLICATION_CREDENTIALS = "path/to/your/key/file" + ``` + +Not sure which service account is the right one? See the +[Find or create your service account](#find-or-create-your-service-account) +section. + +## Deploy outside of Google Cloud (without ADC) {: #deploy-to-cloud-without-adc } + +In some serverless environments, you may not be able to deploy a credential +file. + +1. Follow the instructions to set up a +[service account key](https://cloud.google.com/iam/docs/keys-create-delete#creating). + +2. Ensure the service account has the following roles: + - `roles/monitoring.metricWriter` + - `roles/cloudtrace.agent` + - `roles/logging.logWriter` + +3. Download the credential file. + +4. Assign the contents of the credential file to the +`GCLOUD_SERVICE_ACCOUNT_CREDS` environment variable as follows: + +```posix-terminal +GCLOUD_SERVICE_ACCOUNT_CREDS='{ + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "your-private-key-id", + "private_key": "your-private-key", + "client_email": "your-client-email", + "client_id": "your-client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "your-cert-url" +}' +``` + +Not sure which service account is the right one? See the +[Find or create your service account](#find-or-create-your-service-account) +section. + +## Find or create your service account {: #find-or-create-your-service-account } + +To find the appropriate service account: + +1. Navigate to the [service accounts page](https://console.cloud.google.com/iam-admin/serviceaccounts) + in the Google Cloud Console +2. Select your project +3. Find the appropriate service account. Common default service accounts are as follows: + +- Firebase functions & Cloud Run + + PROJECT ID-compute@developer.gserviceaccount.com + +- App Engine + + PROJECT ID@appspot.gserviceaccount.com + +- App Hosting + + firebase-app-hosting-compute@PROJECT ID.iam.gserviceaccount.com + +If you are deploying outside of the Google ecosystem or don't want to use a +default service account, you can +[create a service account](https://cloud.google.com/iam/docs/service-accounts-create#creating) +in the Google Cloud console. diff --git a/docs/observability/getting-started.md b/docs/observability/getting-started.md index 6207e585d0..85d325388a 100644 --- a/docs/observability/getting-started.md +++ b/docs/observability/getting-started.md @@ -1,41 +1,56 @@ -# Get started with Genkit Monitoring +# Get started with Genkit Monitoring {: #get-started } -This quickstart guide describes how to set up Firebase Genkit Monitoring for your deployed Genkit features, so that you can collect and view real-time telemetry data. With Firebase Genkit Monitoring, you get visibility into how your Genkit features are performing in production. +This quickstart guide describes how to set up Firebase Genkit Monitoring for +your deployed Genkit features, so that you can collect and view real-time +telemetry data. With Firebase Genkit Monitoring, you get visibility into how +your Genkit features are performing in production. -Key capabilities include: +Key capabilities of Firebase Genkit Monitoring include: -* Viewing quantitative metrics like Genkit feature latency, errors, and token usage -* Inspecting traces to see your Genkit's feature steps, inputs, and outputs, to help with debugging and quality improvement -* Exporting production traces to run evals within Genkit +* Viewing quantitative metrics like Genkit feature latency, errors, and + token usage. +* Inspecting traces to see your Genkit's feature steps, inputs, and outputs, + to help with debugging and quality improvement. +* Exporting production traces to run evals within Genkit. -Setting up Genkit Monitoring requires completing tasks in both your codebase and on the Google Cloud Console. +Setting up Genkit Monitoring requires completing tasks in both your codebase +and on the Google Cloud Console. -## Before you begin +## Before you begin {: #before-you-begin } -1. If you haven't already, create a Firebase project. +1. If you haven't already, create a Firebase project. - In the [Firebase console](https://console.firebase.google.com), click **Add a project**, then follow the on-screen instructions. You can create a new project or add Firebase services to an already existing Google Cloud project. + In the [Firebase console](https://console.firebase.google.com), click + **Add a project**, then follow the on-screen instructions. You can + create a new project or add Firebase services to an already-existing + Google Cloud project. -2. Ensure your project is on the [Blaze pricing plan](https://firebase.google.com/pricing). +2. Ensure your project is on the + [Blaze pricing plan](https://firebase.google.com/pricing). - Genkit Monitoring relies on telemetry data written to Google Cloud Logging, Metrics, and Trace which are paid services. View the [Google Cloud Observability](http://cloud/stackdriver/pricing#cloud-monitoring-pricing) page for pricing details and to learn about available free tier limits. + Genkit Monitoring relies on telemetry data written to Google Cloud + Logging, Metrics, and Trace, which are paid services. View the + [Google Cloud Observability pricing](https://cloud.google.com/stackdriver/pricing) + page for pricing details and to learn about free-of-charge tier limits. -3. Write a Genkit feature by following the [Get Started Guide](https://firebase.google.com/docs/genkit/get-started) and prepare your code for deployment by using one of the following guides: +3. Write a Genkit feature by following the + [Get Started Guide](https://firebase.google.com/docs/genkit/get-started), and + prepare your code for deployment by using one of the following guides: - 1. [Deploy with Firebase](../firebase.md) - 2. [Deploy with Cloud Run](../cloud-run.md) - 3. [Deploy to any Node.js platform](../deploy-node.md) + 1. [Deploy flows using Cloud Functions for Firebase](../firebase) + 2. [Deploy flows using Cloud Run](../cloud-run) + 3. [Deploy flows to any Node.js platform](../deploy-node) - -## Step 1. Add the Firebase plugin +## Step 1. Add the Firebase plugin {: #add-plugin } Install the `@genkit-ai/firebase` plugin in your project: -``` +```posix-terminal npm i –save @genkit-ai/firebase ``` -Import `enableFirebaseTelemetry` into your Genkit configuration file (i.e. where `genkit(...)` is initalized) and call it: +Import `enableFirebaseTelemetry` into your Genkit configuration file (the +file where `genkit(...)` is initalized), and call it: ```typescript import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; @@ -43,54 +58,79 @@ import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; enableFirebaseTelemetry(); ``` -## Step 2. Enable the required APIs +## Step 2. Enable the required APIs {: #enable-apis } -Make sure that the following APIs are enabled for your GCP project: +Make sure that the following APIs are enabled for your Google Cloud project: * [Cloud Logging API](https://console.cloud.google.com/apis/library/logging.googleapis.com) * [Cloud Trace API](https://console.cloud.google.com/apis/library/cloudtrace.googleapis.com) * [Cloud Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) -These APIs should be listed in the [API dashboard](https://console.cloud.google.com/apis/dashboard) for your project. +These APIs should be listed in the +[API dashboard](https://console.cloud.google.com/apis/dashboard) for your +project. -## Step 3. Set up permissions +## Step 3. Set up permissions {: #set-up-permissions } -The Firebase plugin needs to authenticate with Google Cloud Logging, Metrics, and Trace services using a _service account_. +The Firebase plugin needs to use a _service account_ to authenticate with +Google Cloud Logging, Metrics, and Trace services. -Grant the following roles to whichever service account is configured to run your code within the [Google Cloud IAM Console](https://console.cloud.google.com/iam-admin/iam). For Firebase Functions and/or Cloud Run, this is typically the “default compute service account”. +Grant the following roles to whichever service account is configured to run +your code within the +[Google Cloud IAM Console](https://console.cloud.google.com/iam-admin/iam). +For Cloud Functions for Firebase and Cloud Run, that's typically the default +compute service account. * **Monitoring Metric Writer** (`roles/monitoring.metricWriter`) * **Cloud Trace Agent** (`roles/cloudtrace.agent`) * **Logs Writer** (`roles/logging.logWriter`) -## Step 4. (Optional) Test your configuration locally +## Step 4. (Optional) Test your configuration locally {: #test-locally } -Before deploying, you can run your Genkit code locally to confirm that telemetry data is being collected and viewable in the Genkit Monitoring dashboard. +Before deploying, you can run your Genkit code locally to confirm that +telemetry data is being collected, and is viewable in the Genkit Monitoring +dashboard. -1. In your Genkit code, set `forceDevExport` to `true` to send telemetry from your local environment. +1. In your Genkit code, set `forceDevExport` to `true` to send telemetry from + your local environment. 2. Use your service account to authenticate and test your configuration. - > [!TIP] - > In order to impersonate the service account, you will need to have the `roles/iam.serviceAccountTokenCreator` [IAM role](https://console.cloud.google.com/iam-admin/iam) applied to your user account. + Tip: In order to impersonate the service account, you will need to have + the `roles/iam.serviceAccountTokenCreator` + [IAM role](https://console.cloud.google.com/iam-admin/iam) applied to your + user account. - With the [Google Cloud CLI tool](https://cloud.google.com/sdk/docs/install?authuser=0), authenticate using the service account: + With the + [Google Cloud CLI tool](https://cloud.google.com/sdk/docs/install?authuser=0), + authenticate using the service account: + ```posix-terminal + gcloud auth application-default login --impersonate-service-account SERVICE_ACCT_EMAIL ``` - gcloud auth application-default login --impersonate-service-account - ``` -3. Run and invoke your Genkit feature, and then view metrics on the [Genkit Monitoring dashboard](https://firebase.google.com/project/_/genai_monitoring). Allow for up to 5 minutes to collect the first metric. This delay can be reduced by setting `metricExportIntervalMillis` in the telemetry configuration. -4. If metrics are not appearing in the Genkit Monitoring dashboard, view the [Troubleshooting](./troubleshooting.md) guide for steps to debug. +3. Run and invoke your Genkit feature, and then view metrics on the + [Genkit Monitoring dashboard](https://console.firebase.google.com/project/_/genai_monitoring). + Allow for up to 5 minutes to collect the first metric. You can reduce this + delay by setting `metricExportIntervalMillis` in the telemetry configuration. + +4. If metrics are not appearing in the Genkit Monitoring dashboard, view the + [Troubleshooting](/docs/genkit/observability/troubleshooting) guide for steps + to debug. -## Step 5. Re-build and deploy code +## Step 5. Re-build and deploy code {: #build-and-deploy } -Re-build, deploy, and invoke your Genkit feature to start collecting data. Once Genkit Monitoring receives your metrics, they can be viewed by visiting the [Genkit Monitoring dashboard](https://firebase.google.com/project/_/genai_monitoring) +Re-build, deploy, and invoke your Genkit feature to start collecting data. +After Genkit Monitoring receives your metrics, you can view them by +visiting the +[Genkit Monitoring dashboard](https://console.firebase.google.com/project/_/genai_monitoring) -Note: It may take up to 5 minutes to collect the first metric (based on the default `metricExportIntervalMillis` setting in the telemetry configuration). +Note: It may take up to 5 minutes to collect the first metric (based on the +default `metricExportIntervalMillis` setting in the telemetry configuration). \ No newline at end of file diff --git a/docs/observability/telemetry-collection.md b/docs/observability/telemetry-collection.md new file mode 100644 index 0000000000..1170c31fe0 --- /dev/null +++ b/docs/observability/telemetry-collection.md @@ -0,0 +1,288 @@ +# Telemetry Collection {: #telemetry-collection } + +The Firebase telemetry plugin exports a combination of metrics, traces, and +logs to Google Cloud Observability. This document details which metrics, trace +attributes, and logs will be collected and what you can expect in terms of +latency, quotas, and cost. + +## Telemetry delay {: #telemetry-delay } + +There may be a slight delay before telemetry from a given invocation is +available in Firebase. This is dependent on your export interval (5 minutes +by default). + +## Quotas and limits {: #quotas-and-limits } + +There are several quotas that are important to keep in mind: + +- [Cloud Trace Quotas](http://cloud.google.com/trace/docs/quotas) +- [Cloud Logging Quotas](http://cloud.google.com/logging/quotas) +- [Cloud Monitoring Quotas](http://cloud.google.com/monitoring/quotas) + +## Cost {: #cost } + +Cloud Logging, Cloud Trace, and Cloud Monitoring have generous free-of-charge +tiers. Specific pricing can be found at the following links: + +- [Cloud Logging Pricing](http://cloud.google.com/stackdriver/pricing#google-cloud-observability-pricing) +- [Cloud Trace Pricing](https://cloud.google.com/trace#pricing) +- [Cloud Monitoring Pricing](https://cloud.google.com/stackdriver/pricing#monitoring-pricing-summary) + +## Metrics {: #metrics } + +The Firebase telemetry plugin collects a number of different metrics to support +the various Genkit action types detailed in the following sections. + +### Feature metrics {: #feature-metrics } + +Features are the top-level entry-point to your Genkit code. In most cases, this +will be a flow. Otherwise, this will be the top-most span in a trace. + +| Name | Type | Description | +| ----------------------- | --------- | ----------------------- | +| genkit/feature/requests | Counter | Number of requests | +| genkit/feature/latency | Histogram | Execution latency in ms | + +Each feature metric contains the following dimensions: + +| Name | Description | +| ------------- | -------------------------------------------------------------------------------- | +| name | The name of the feature. In most cases, this is the top-level Genkit flow | +| status | 'success' or 'failure' depending on whether or not the feature request succeeded | +| error | Only set when `status=failure`. Contains the error type that caused the failure | +| source | The Genkit source language. Eg. 'ts' | +| sourceVersion | The Genkit framework version | + +### Action metrics {: #action-metrics } + +Actions represent a generic step of execution within Genkit. Each of these steps +will have the following metrics tracked: + +| Name | Type | Description | +| ----------------------- | --------- | --------------------------------------------- | +| genkit/action/requests | Counter | Number of times this action has been executed | +| genkit/action/latency | Histogram | Execution latency in ms | + +Each action metric contains the following dimensions: + +| Name | Description | +| ------------- | ---------------------------------------------------------------------------------------------------- | +| name | The name of the action | +| featureName | The name of the parent feature being executed | +| path | The path of execution from the feature root to this action. eg. '/myFeature/parentAction/thisAction' | +| status | 'success' or 'failure' depending on whether or not the action succeeded | +| error | Only set when `status=failure`. Contains the error type that caused the failure | +| source | The Genkit source language. Eg. 'ts' | +| sourceVersion | The Genkit framework version | + +### Generate metrics {: #generate-metrics } + +These are special action metrics relating to actions that interact with a model. +In addition to requests and latency, input and output are also tracked, with +model specific dimensions that make debugging and configuration tuning easier. + +| Name | Type | Description | +| ------------------------------------ | --------- | ------------------------------------------ | +| genkit/ai/generate/requests | Counter | Number of times this model has been called | +| genkit/ai/generate/latency | Histogram | Execution latency in ms | +| genkit/ai/generate/input/tokens | Counter | Input tokens | +| genkit/ai/generate/output/tokens | Counter | Output tokens | +| genkit/ai/generate/input/characters | Counter | Input characters | +| genkit/ai/generate/output/characters | Counter | Output characters | +| genkit/ai/generate/input/images | Counter | Input images | +| genkit/ai/generate/output/images | Counter | Output images | +| genkit/ai/generate/input/audio | Counter | Input audio files | +| genkit/ai/generate/output/audio | Counter | Output audio files | + +Each generate metric contains the following dimensions: + +| Name | Description | +| --------------- | ---------------------------------------------------------------------------------------------------- | +| modelName | The name of the model | +| featureName | The name of the parent feature being executed | +| path | The path of execution from the feature root to this action. eg. '/myFeature/parentAction/thisAction' | +| latencyMs | The response time taken by the model | +| status | 'success' or 'failure' depending on whether or not the feature request succeeded | +| error | Only set when `status=failure`. Contains the error type that caused the failure | +| source | The Genkit source language. Eg. 'ts' | +| sourceVersion | The Genkit framework version | + +## Traces {: #traces } + +All Genkit actions are automatically instrumented to provide detailed traces for +your AI features. Locally, traces are visible in the Developer UI. For deployed +apps enable Firebase Genkit Monitoring to get the same level of visibility. + +The following sections describe what trace attributes you can expect based on +the Genkit action type for a particular span in the trace. + +### Root Spans {: #root-spans } + +Root spans have special attributes to help disambiguate the state attributes for +the whole trace versus an individual span. + +| Attribute name | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| genkit/feature | The name of the parent feature being executed | +| genkit/isRoot | Marked true if this span is the root span | +| genkit/rootState | The state of the overall execution as `success` or `error`. This does not indicate that this step failed in particular. | + +### Flow {: #flow } + +| Attribute name | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| genkit/input | The input to the flow. This will always be `` because of trace attribute size limits. | +| genkit/metadata/subtype | The type of Genkit action. For flows it will be `flow`. | +| genkit/name | The name of this Genkit action. In this case the name of the flow | +| genkit/output | The output generated in the flow. This will always be `` because of trace attribute size limits. | +| genkit/path | The fully qualified execution path that lead to this step in the trace, including type information. | +| genkit/state | The state of this span's execution as `success` or `error`. | +| genkit/type | The type of Genkit primitive that corresponds to this span. For flows, this will be `action`. | + +### Util {: #util } + +| Attribute name | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| genkit/input | The input to the util. This will always be `` because of trace attribute size limits. | +| genkit/name | The name of this Genkit action. In this case the name of the flow | +| genkit/output | The output generated in the util. This will always be `` because of trace attribute size limits. | +| genkit/path | The fully qualified execution path that lead to this step in the trace, including type information. | +| genkit/state | The state of this span's execution as `success` or `error`. | +| genkit/type | The type of Genkit primitive that corresponds to this span. For flows, this will be `util`. | + +### Model {: #model } + +| Attribute name | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| genkit/input | The input to the model. This will always be `` because of trace attribute size limits. | +| genkit/metadata/subtype | The type of Genkit action. For models it will be `model`. | +| genkit/model | The name of the model. | +| genkit/name | The name of this Genkit action. In this case the name of the model. | +| genkit/output | The output generated by the model. This will always be `` because of trace attribute size limits. | +| genkit/path | The fully qualified execution path that lead to this step in the trace, including type information. | +| genkit/state | The state of this span's execution as `success` or `error`. | +| genkit/type | The type of Genkit primitive that corresponds to this span. For flows, this will be `action`. | + +### Tool {: #tool } + +| Attribute name | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| genkit/input | The input to the model. This will always be `` because of trace attribute size limits. | +| genkit/metadata/subtype | The type of Genkit action. For tools it will be `tool`. | +| genkit/name | The name of this Genkit action. In this case the name of the model. | +| genkit/output | The output generated by the model. This will always be `` because of trace attribute size limits. | +| genkit/path | The fully qualified execution path that lead to this step in the trace, including type information. | +| genkit/state | The state of this span's execution as `success` or `error`. | +| genkit/type | The type of Genkit primitive that corresponds to this span. For flows, this will be `action`. | + +## Logs {: #logs } + +For deployed apps with Firebase Genkit Monitoring, logs are used to capture +input, output, and configuration metadata that provides rich detail about +each step in your AI feature. + +All logs will include the following shared metadata fields: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| insertId | Unique id for the log entry | +| jsonPayload | Container for variable information that is unique to each log type | +| labels | `{module: genkit}` | +| logName | `projects/weather-gen-test-next/logs/genkit_log` | +| receivedTimestamp | Time the log was received by Cloud | +| resource | Information about the source of the log including deployment information region, and projectId | +| severity | The log level written. See Cloud's [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity) | +| spanId | Identifier for the span that created this log | +| timestamp | Time that the client logged a message | +| trace | Identifier for the trace of the format `projects//traces/` | +| traceSampled | Boolean representing whether the trace was sampled. Logs are not sampled. | + +Each log type will have a different json payload described in each section. + +### Input {: #input } + +JSON payload: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| message | `[genkit] Input[, ]` including `(message X of N)` for multi-part messages | +| metadata | Additional context including the input message sent to the action | + +Metadata: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| content | The input message content sent to this Genkit action | +| featureName | The name of the Genkit flow, action, tool, util, or helper. | +| messageIndex * | Index indicating the order of messages for inputs that contain multiple messages. For single messages, this will always be 0. | +| model * | Model name. | +| path | The execution path that generated this log of the format `step1 > step2 > step3` | +| partIndex * | Index indicating the order of parts within a message for multi-part messages. This is typical when combining text and images in a single input. | +| qualifiedPath | The execution path that generated this log, including type information of the format: `/{flow1,t:flow}/{generate,t:util}/{modelProvider/model,t:action,s:model` | +| totalMessages * | The total number of messages for this input. For single messages, this will always be 1. | +| totalParts * | Total number of parts for this message. For single-part messages, this will always be 1. | + +(*) Starred items are only present on Input logs for model interactions. + +### Output {: #output } + +JSON payload: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| message | `[genkit] Output[, ]` including `(message X of N)` for multi-part messages | +| metadata | Additional context including the input message sent to the action | + +Metadata: + +| Field name | Description | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| candidateIndex * (deprecated) | Index indicating the order of candidates for outputs that contain multiple candidates. For logs with single candidates, this will always be 0. | +| content | The output message generated by the Genkit action | +| featureName | The name of the Genkit flow, action, tool, util, or helper. | +| messageIndex * | Index indicating the order of messages for inputs that contain multiple messages. For single messages, this will always be 0. | +| model * | Model name. | +| path | The execution path that generated this log of the format `step1 > step2 > step3 | +| partIndex * | Index indicating the order of parts within a message for multi-part messages. This is typical when combining text and images in a single output. | +| qualifiedPath | The execution path that generated this log, including type information of the format: `/{flow1,t:flow}/{generate,t:util}/{modelProvider/model,t:action,s:model` | +| totalCandidates * (deprecated) | Total number of candidates generated as output. For single-candidate messages, this will always be 1. | +| totalParts * | Total number of parts for this message. For single-part messages, this will always be 1. | + +(*) Starred items are only present on Output logs for model interactions. + +### Config {: #config } + +JSON payload: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| message | `[genkit] Config[, ]` | +| metadata | Additional context including the input message sent to the action | + +Metadata: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| featureName | The name of the Genkit flow, action, tool, util, or helper. | +| model | Model name. | +| path | The execution path that generated this log of the format `step1 > step2 > step3 | +| qualifiedPath | The execution path that generated this log, including type information of the format: `/{flow1,t:flow}/{generate,t:util}/{modelProvider/model,t:action,s:model` | +| source | The Genkit library language used. This will always be set to 'ts' as it is the only supported language. | +| sourceVersion | The Genkit library version. | +| temperature | Model temperature used. | + +### Paths {: #paths } + +JSON payload: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| message | `[genkit] Paths[, ]` | +| metadata | Additional context including the input message sent to the action | + +Metadata: + +| Field name | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| flowName | The name of the Genkit flow, action, tool, util, or helper. | +| paths | An array containing all execution paths for the collected spans. | \ No newline at end of file diff --git a/docs/observability/troubleshooting.md b/docs/observability/troubleshooting.md index e130baf58c..c5f61ffa39 100644 --- a/docs/observability/troubleshooting.md +++ b/docs/observability/troubleshooting.md @@ -1,19 +1,31 @@ -# Genkit Monitoring - Troubleshooting - -## I can’t see traces and/or metrics in Firebase Genkit Monitoring - -1. Ensure that the following APIs are enabled for your underlying GCP project: - * [Cloud Logging API](https://console.cloud.google.com/apis/library/logging.googleapis.com) - * [Cloud Trace API](https://console.cloud.google.com/apis/library/cloudtrace.googleapis.com) - * [Cloud Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) -2. Ensure that the following roles are applied to the service account that is running your code (or service account that has been configured as part of the plugin options) in [Cloud IAM](https://console.cloud.google.com/iam-admin/iam). - * **Monitoring Metric Writer** (`roles/monitoring.metricWriter`) - * **Cloud Trace Agent** (`roles/cloudtrace.agent`) - * **Logs Writer** (`roles/logging.logWriter`) -3. Inspect the application logs for errors writing to Cloud Logging, Cloud Trace, and/or Cloud Monitoring. On GCP infrastructure (e.g. Firebase Functions, Cloud Run, etc), even when telemetry is misconfigured, logs to stdout/stderr are automatically ingested by the Cloud Logging Agent, allowing you to diagnose issues in the in the [Cloud Logging Console](https://console.cloud.google.com/logs). +# Genkit Monitoring: Troubleshooting {: #troubleshooting } + +The following sections detail solutions to common issues that developers run +into when using Firebase Genkit Monitoring. + +## I can't see traces or metrics in Firebase Genkit Monitoring {: #missing-metrics } + +1. Ensure that the following APIs are enabled for your underlying Google Cloud + project: + * [Cloud Logging API](https://console.cloud.google.com/apis/library/logging.googleapis.com) + * [Cloud Trace API](https://console.cloud.google.com/apis/library/cloudtrace.googleapis.com) + * [Cloud Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) +2. Ensure that the following roles are applied to the service account that + is running your code (or service account that has been configured as part of + the plugin options) in [Cloud IAM](https://console.cloud.google.com/iam-admin/iam). + * **Monitoring Metric Writer** (`roles/monitoring.metricWriter`) + * **Cloud Trace Agent** (`roles/cloudtrace.agent`) + * **Logs Writer** (`roles/logging.logWriter`) +3. Inspect the application logs for errors writing to Cloud Logging, Cloud + Trace, and Cloud Monitoring. On Google Cloud infrastructure such as Firebase + Functions and Cloud Run, even when telemetry is misconfigured, logs to + `stdout/stderr` are automatically ingested by the Cloud Logging Agent, + allowing you to diagnose issues in the in the + [Cloud Logging Console](https://console.cloud.google.com/logs). + 4. Debug locally: - Enable dev export: + Enable dev export: ```typescript enableFirebaseTelemetry({ @@ -21,20 +33,32 @@ }); ``` - To test with your personal user credentials, authenticate with Google Cloud via the [gcloud CLI](https://cloud.google.com/sdk/docs/install). This can help diagnose enabled/disabled APIs, but will not test the production service account permissions: + To test with your personal user credentials, use the + [gcloud CLI](https://cloud.google.com/sdk/docs/install) to authenticate with + Google Cloud. Doing so can help diagnose enabled or disabled APIs, but does + not test the gcloud auth application-default login. - ``` - gcloud auth application-default login - ``` - - Alternatively, impersonating the service account will allow you to test production-like access. You will need to have the `roles/iam.serviceAccountTokenCreator` IAM role applied to your user account in order to impersonate service accounts: + Alternatively, impersonating the service account lets you test + production-like access. You must have the + `roles/iam. serviceAccountTokenCreator` IAM role applied to your user account + in order to impersonate service accounts: - ``` + ```posix-terminal gcloud auth application-default login --impersonate-service-account ``` - See the [ADC](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment) documentation for more information. + See the + [ADC](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment) + documentation for more information. + +### Telemetry upload reliability in Firebase Functions / Cloud Run {: #telemetry-reliability } -### Telemetry upload reliability in Firebase Functions / Cloud Run +When Genkit is hosted in Google Cloud Run (including Cloud Functions for +Firebase), telemetry-data upload may be less reliable as the container switches +to the "idle" +[lifecycle state](https://cloud.google.com/blog/topics/developers-practitioners/lifecycle-container-cloud-run). +If higher reliability is important to you, consider changing +[CPU allocation](https://cloud.google.com/run/docs/configuring/cpu-allocation) +to **always allocated** in the Google Cloud Console. -When Genkit is hosted in Google Cloud Run (including Firebase Functions), telemetry data upload may be less reliable as the container switches to the "idle" [lifecycle state](https://cloud.google.com/blog/topics/developers-practitioners/lifecycle-container-cloud-run). If higher reliability is important to you, consider changing [CPU allocation](https://cloud.google.com/run/docs/configuring/cpu-allocation) to "always allocated" in the Google Cloud Console. Note that this impacts pricing. \ No newline at end of file +Note: The **always allocated** setting impacts pricing. \ No newline at end of file diff --git a/docs/plugins/firebase.md b/docs/plugins/firebase.md index 8833f8f0cb..ba6876a69c 100644 --- a/docs/plugins/firebase.md +++ b/docs/plugins/firebase.md @@ -9,8 +9,8 @@ build intelligent and scalable AI applications. Key features include: - **Firestore Vector Store**: Use Firestore for indexing and retrieval with vector embeddings. - **Telemetry**: Export telemetry to -[Google's Cloud operations suite](https://cloud.google.com/products/operations) that powers the Firebase Genkit -Monitoring console. +[Google's Cloud operations suite](https://cloud.google.com/products/operations) +that powers the Firebase Genkit Monitoring console. ## Installation @@ -78,37 +78,27 @@ Application Default Credentials. To specify your credentials: page of the Firebase console. 1. Set the environment variable `GOOGLE_APPLICATION_CREDENTIALS` to the file path of the JSON file that contains your service account key, or you can set - the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDS` to the content of the JSON file. + the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDS` to the content of the + JSON file. ## Features and usage ### Telemetry -Firebase Genkit Monitoring is powered by Google's Cloud operation suite. This -requires telemetry related API's to be enabled for your project. Please refer -to the [Google Cloud plugin](google-cloud.md#set-up-a-google-cloud-account) -documentation for more details. +The Firebase plugin provides a telemetry implementation for sending metrics, +traces, and logs to Firebase Genkit Monitoring. -Grant the following roles to the **"Default compute service account"** within -the [Google Cloud IAM Console](https://console.cloud.google.com/iam-admin/iam): +To get started, visit the [Getting started guide](../observability/getting-started.md) +for installation and configuration instructions. -- **Monitoring Metric Writer** (roles/monitoring.metricWriter) -- **Cloud Trace Agent** (roles/cloudtrace.agent) -- **Logs Writer** (roles/logging.logWriter) +See the [Authentication and authorization guide](../observability/authentication.md) +to authenticate with Google Cloud. -To enable telemetry export call `enableFirebaseTelemetry()`: +See the [Advanced configuration guide](../observability/advanced-configuration.md) +for configuration options. - - -```js -import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; - -enableFirebaseTelemetry({ - forceDevExport: false, // Set this to true to export telemetry for local runs -}); -``` - -This plugin shares [configuration options](google-cloud.md#plugin-configuration) with the [Google Cloud plugin](google-cloud.md). +See the [Telemetry collection](../observability/telemetry-collection.md) for +details on which Genkit metrics, traces, and logs collected. ### Cloud Firestore vector search @@ -291,7 +281,7 @@ section of the Firestore docs. The command looks like the following: - ``` + ```posix-terminal gcloud alpha firestore indexes composite create --project=your-project-id \ --collection-group=yourCollectionName --query-scope=COLLECTION \ --field-config=vector-config='{"dimension":"768","flat": "{}"}',field-path=yourEmbeddingField @@ -300,7 +290,6 @@ section of the Firestore docs. However, the correct indexing configuration depends on the queries you make and the embedding model you're using. - - Alternatively, call `ai.retrieve()` and Firestore will throw an error with the correct command to create the index. @@ -308,8 +297,8 @@ section of the Firestore docs. - See the [Retrieval-augmented generation](http://../rag.md) page for a general discussion on indexers and retrievers in Genkit. -- See [Search with vector embeddings](https://firebase.google.com/docs/firestore/vector-search) in the Cloud Firestore docs for more -on the vector search feature. +- See [Search with vector embeddings](https://firebase.google.com/docs/firestore/vector-search) +in the Cloud Firestore docs for more on the vector search feature. ### Deploy flows as Cloud Functions @@ -343,6 +332,6 @@ export const example = onCallGenkit({ secrets: [apiKey] }, exampleFlow); Deploy your flow using the Firebase CLI: -``` +```posix-terminal firebase deploy --only functions -``` +``` \ No newline at end of file diff --git a/docs/plugins/google-cloud.md b/docs/plugins/google-cloud.md deleted file mode 100644 index ad3eab996e..0000000000 --- a/docs/plugins/google-cloud.md +++ /dev/null @@ -1,412 +0,0 @@ -# Google Cloud plugin - -The Google Cloud plugin exports Firebase Genkit telemetry and logging data to the [Cloud Observability](https://cloud.google.com/products/operations) suite. - -## Installation - -```posix-terminal -npm i --save @genkit-ai/google-cloud -``` - -When running Genkit code locally that includes this plugin, you will also need -the [Google Cloud CLI tool](https://cloud.google.com/sdk/docs/install) -installed. - -## Set up a Google Cloud account - -This plugin requires a Google Cloud account/project. All Firebase projects -include one by default ([GCP Console](https://console.cloud.google.com)), -or you can sign up at https://cloud.google.com. - -Prior to adding the plugin, make sure that the following APIs are enabled for -your GCP project: - -- [Cloud Logging API](https://console.cloud.google.com/apis/library/logging.googleapis.com) -- [Cloud Trace API](https://console.cloud.google.com/apis/library/cloudtrace.googleapis.com) -- [Cloud Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com) - -These APIs should be listed in the -[API dashboard](https://console.cloud.google.com/apis/dashboard) for your -project. - -Click [here](https://support.google.com/googleapi/answer/6158841) to learn more -about enabling and disabling APIs. - -## Genkit configuration - -To enable Cloud Tracing, Logging, and Monitoring (metrics), simply call -`enableGoogleCloudTelemetry()`: - -```ts -import { enableGoogleCloudTelemetry } from '@genkit-ai/google-cloud'; - -enableGoogleCloudTelemetry(); -``` - -When running in production, telemetry will be exported automatically. - -### Authentication and authorization - -The plugin requires a Google Cloud project ID and application credentials. - -#### Google Cloud - -If deploying your code to a Google Cloud environment (Cloud -Functions, Cloud Run, etc), the project ID and credentials will be discovered -automatically via -[Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc). - -You will need to apply the following roles to the service account that is -running your code (i.e. 'attached service account') via the -[IAM Console](https://console.cloud.google.com/iam-admin/iam): - -- `roles/monitoring.metricWriter` -- `roles/cloudtrace.agent` -- `roles/logging.logWriter` - -#### Local Development - -When doing local development, in order for your user credentials to be available -to the plugin, additional steps are required. - -1. Set the `GCLOUD_PROJECT` environment variable to your Google Cloud project. - -2. Authenticate using the `gcloud` CLI: - - ```posix-terminal - gcloud auth application-default login - ``` - -#### Production environments outside of Google Cloud - -If possible, it is still recommended to leverage the -[Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc) -process to make credentials available to the plugin. - -Typically this involves generating a service account key/pair and deploying -those credentials to your production environment. - -1. Follow the instructions to set up a -[service account key](https://cloud.google.com/iam/docs/keys-create-delete#creating). - -2. Ensure the service account has the following roles: - - `roles/monitoring.metricWriter` - - `roles/cloudtrace.agent` - - `roles/logging.logWriter` - -3. Deploy the credential file to production (**do not** check into source code) - -4. Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable as the path to -the credential file. - - ``` - GOOGLE_APPLICATION_CREDENTIALS = "path/to/your/key/file" - ``` - -In some serverless environments, you may not be able to deploy a credential -file. In this case, as an alternative to steps 3 & 4 above, you can set the -`GCLOUD_SERVICE_ACCOUNT_CREDS` environment variable with the contents of the -credential file as follows: - -``` -GCLOUD_SERVICE_ACCOUNT_CREDS='{ - "type": "service_account", - "project_id": "your-project-id", - "private_key_id": "your-private-key-id", - "private_key": "your-private-key", - "client_email": "your-client-email", - "client_id": "your-client-id", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "your-cert-url" -}' -``` - -## Plugin configuration - -The `enableGoogleCloudTelemetry()` function takes an optional configuration -object which configures the -[OpenTelemetry NodeSDK](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_sdk_node.NodeSDK.html) -instance. - -```ts -import { AlwaysOnSampler } from '@opentelemetry/sdk-trace-base'; - -enableGoogleCloudTelemetry({ - forceDevExport: false, // Set this to true to export telemetry for local runs - sampler: new AlwaysOnSampler(), - autoInstrumentation: true, - autoInstrumentationConfig: { - '@opentelemetry/instrumentation-fs': { enabled: false }, - '@opentelemetry/instrumentation-dns': { enabled: false }, - '@opentelemetry/instrumentation-net': { enabled: false }, - }, - metricExportIntervalMillis: 5_000, -}); -``` -The configuration objects allows fine grained control over various aspects of -the telemetry export outlined below. - -#### credentials -Allows specifying credentials directly using -[JWTInput](http://cloud/nodejs/docs/reference/google-auth-library/latest/google-auth-library/jwtinput) -from the google-auth library. - -#### sampler - -For cases where exporting all traces isn't practical, OpenTelemetry allows trace -[sampling](https://opentelemetry.io/docs/languages/java/instrumentation/#sampler). - -There are four preconfigured samplers: - -- [AlwaysOnSampler](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/AlwaysOnSampler.ts) - samples all traces -- [AlwaysOffSampler](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/AlwaysOffSampler.ts) - samples no traces -- [ParentBased](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/ParentBasedSampler.ts) - samples based on parent span -- [TraceIdRatioBased](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/TraceIdRatioBasedSampler.ts) - samples a configurable percentage of traces - -#### autoInstrumentation & autoInstrumentationConfig - -Enabling -[automatic instrumentation](https://opentelemetry.io/docs/languages/js/automatic/) -allows OpenTelemetry to capture telemetry data from -[third-party libraries](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/metapackages/auto-instrumentations-node/src/utils.ts) -without the need to modify code. - -#### metricExportIntervalMillis - -This field specifies the metrics export interval in milliseconds. - -> Note: The minimum export interval for Google Cloud Monitoring is 5000ms. - -#### metricExportTimeoutMillis - -This field specifies the timeout for the metrics export in milliseconds. - -#### disableMetrics - -Provides an override that disables metrics export while still exporting traces -and logs. - -#### disableTraces - -Provides an override that disables exporting traces while still exprting metrics -and logs. - -#### disableLoggingInputAndOutput - -Provides an override that disables collecting input and output logs. - -#### forceDevExport - -This option will force Genkit to export telemetry and log data when running in -the `dev` environment (e.g. locally). - -> Note: When running locally, internal telemetry buffers may not fully flush -prior to the process exiting, resulting in an incomplete telemetry export. - -## Test your integration - -When configuring the plugin, use `forceDevExport: true` to enable telemetry -export for local runs. Navigate to the Google Cloud Logs, Metrics, or Trace -Explorer to view telemetry. - -# Google Cloud Observability suite - -Once your code (e.g. flow) is deployed, navigate to the -[Cloud Monitoring](https://console.cloud.google.com/monitoring/) dashboard and -select your project. From here, you can easily navigate between the Logs, -Metrics and Trace explorers for production monitoring. - - - -## Logs and traces - -From the left hand side menu, click 'Logs explorer' under the 'Explore' heading. - - - -Here, you will see all logs that are associated with your deployed Genkit code, -including `console.log()`. Any log which has the prefix `[genkit]` is a -Genkit-internal log that contains information that may be interesting for -debugging purposes. For example, Genkit logs in the format `Config[...]` contain -metadata such as the temperature and topK values for specific LLM inferences. -Logs in the format `Output[...]` contain LLM responses while `Input[...]` logs -contain the prompts. Cloud Logging has robust ACLs that allow fine grained -control over access to sensitive logs. - -> Note: Prompts and LLM responses are redacted from trace attributes in Cloud -Trace. For specific log lines, it is possible to navigate to their respective -traces by clicking on the extended menu - icon and -selecting "View in trace details". - - - -This will bring up a trace preview pane providing a quick glance of the details -of the trace. To get to the full details, click the "View in Trace" link at the -top right of the pane. - - - -The most prominent navigation element in Cloud Trace is the trace scatter plot. -It contains all collected traces in a given time span. - - - -Clicking on each data point will show its details below the scatter plot. - - - -The detailed view contains the flow shape, including all steps, and important -timing information. Cloud Trace has the ability to interleave all logs -associated with a given trace within this view. Select the "Show expanded" -option in the "Logs & events" drop down. - - - -The resultant view allows detailed examination of logs in the context of the -trace, including prompts and LLM responses. - - - -## Metrics - -Viewing all metrics exported by Genkit can be done by clicking on -'Metrics management' under the 'Configure' heading in the left hand side menu. - - - -The metrics management console contains a tabular view of all collected metrics, -including those that pertain to Cloud Run and its surrounding environment. -Clicking on the 'Workload' option will reveal a list that includes -Genkit-collected metrics. Any metric with the `genkit` prefix constitutes an -internal Genkit metric. - - - -Genkit collects several categories of metrics including: feature, action, and -generate. Each metric has several useful dimensions facilitating robust -filtering and grouping. - -Common dimensions include: - -- `flow_name` - the top-level name of the flow. -- `flow_path` - the span and its parent span chain up to the root span. -- `error_code` - in case of an error, the corresponding error code. -- `error_message` - in case of an error, the corresponding error message. -- `model` - the name of the model. - -### Feature metrics - -Features are the top-level entry-point to your Genkit code. In most cases, this -will be a flow. Otherwise, this will be the top-most span in a trace. - -| Name | Type | Description | -| ----------------------- | --------- | ----------------------- | -| genkit/feature/requests | Counter | Number of requests | -| genkit/feature/latency | Histogram | Execution latency in ms | - -Each feature metric contains the following dimensions: - -| Name | Description | -| ------------- | -------------------------------------------------------------------------------- | -| name | The name of the feature. In most cases, this is the top-level Genkit flow | -| status | 'success' or 'failure' depending on whether or not the feature request succeeded | -| error | Only set when `status=failure`. Contains the error type that caused the failure | -| source | The Genkit source language. Eg. 'ts' | -| sourceVersion | The Genkit framework version | - - -### Action metrics - -Actions represent a generic step of execution within Genkit. Each of these steps -will have the following metrics tracked: - -| Name | Type | Description | -| ----------------------- | --------- | --------------------------------------------- | -| genkit/action/requests | Counter | Number of times this action has been executed | -| genkit/action/latency | Histogram | Execution latency in ms | - -Each action metric contains the following dimensions: - -| Name | Description | -| ------------- | ---------------------------------------------------------------------------------------------------- | -| name | The name of the action | -| featureName | The name of the parent feature being executed | -| path | The path of execution from the feature root to this action. eg. '/myFeature/parentAction/thisAction' | -| status | 'success' or 'failure' depending on whether or not the action succeeded | -| error | Only set when `status=failure`. Contains the error type that caused the failure | -| source | The Genkit source language. Eg. 'ts' | -| sourceVersion | The Genkit framework version | - -### Generate metrics - -These are special action metrics relating to actions that interact with a model. -In addition to requests and latency, input and output are also tracked, with -model specific dimensions that make debugging and configuration tuning easier. - -| Name | Type | Description | -| ------------------------------------ | --------- | ------------------------------------------ | -| genkit/ai/generate/requests | Counter | Number of times this model has been called | -| genkit/ai/generate/latency | Histogram | Execution latency in ms | -| genkit/ai/generate/input/tokens | Counter | Input tokens | -| genkit/ai/generate/output/tokens | Counter | Output tokens | -| genkit/ai/generate/input/characters | Counter | Input characters | -| genkit/ai/generate/output/characters | Counter | Output characters | -| genkit/ai/generate/input/images | Counter | Input images | -| genkit/ai/generate/output/images | Counter | Output images | -| genkit/ai/generate/input/audio | Counter | Input audio files | -| genkit/ai/generate/output/audio | Counter | Output audio files | - -Each generate metric contains the following dimensions: - -| Name | Description | -| --------------- | ---------------------------------------------------------------------------------------------------- | -| modelName | The name of the model | -| featureName | The name of the parent feature being executed | -| path | The path of execution from the feature root to this action. eg. '/myFeature/parentAction/thisAction' | -| latencyMs | The response time taken by the model | -| status | 'success' or 'failure' depending on whether or not the feature request succeeded | -| error | Only set when `status=failure`. Contains the error type that caused the failure | -| source | The Genkit source language. Eg. 'ts' | -| sourceVersion | The Genkit framework version | - -Visualizing metrics can be done through the Metrics Explorer. Using the left -hand side menu, click 'Metrics explorer' under the 'Explore' heading. - - - -Select a metrics by clicking on the 'Select a metric' dropdown, selecting -'Generic Node', 'Genkit', and a metric. - - - -The visualization of the metric will depend on its type (counter, histogram, -etc). The Metrics Explorer provides robust aggregation and querying facilities -to help graph metrics by their various dimensions. - - - -## Telemetry delay - -There may be a slight delay before telemetry for a particular execution of a -flow is displayed in Cloud's operations suite. In most cases, this delay is -under 1 minute. - -## Quotas and limits - -There are several quotas that are important to keep in mind: - -- [Cloud Trace Quotas](http://cloud.google.com/trace/docs/quotas) -- [Cloud Logging Quotas](http://cloud.google.com/logging/quotas) -- [Cloud Monitoring Quotas](http://cloud.google.com/monitoring/quotas) - -## Cost - -Cloud Logging, Cloud Trace, and Cloud Monitoring have generous free tiers. -Specific pricing can be found at the following links: - -- [Cloud Logging Pricing](http://cloud.google.com/stackdriver/pricing#google-cloud-observability-pricing) -- [Cloud Trace Pricing](https://cloud.google.com/trace#pricing) -- [Cloud Monitoring Pricing](https://cloud.google.com/stackdriver/pricing#monitoring-pricing-summary) diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index 733a30a947..5e5faacc05 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -1,6 +1,6 @@ { "name": "genkit-cli", - "version": "1.0.4", + "version": "1.0.5", "description": "CLI for interacting with the Google Genkit AI framework", "license": "Apache-2.0", "keywords": [ diff --git a/genkit-tools/cli/src/commands/start.ts b/genkit-tools/cli/src/commands/start.ts index 4cf4e67f82..43a3be8fb6 100644 --- a/genkit-tools/cli/src/commands/start.ts +++ b/genkit-tools/cli/src/commands/start.ts @@ -64,9 +64,8 @@ export const start = new Command('start') }); async function startRuntime(telemetryServerUrl?: string) { - let runtimePromise = Promise.resolve(); if (start.args.length > 0) { - runtimePromise = new Promise((urlResolver, reject) => { + return new Promise((urlResolver, reject) => { const appProcess = spawn(start.args[0], start.args.slice(1), { env: { ...process.env, @@ -96,5 +95,5 @@ async function startRuntime(telemetryServerUrl?: string) { }); }); } - return runtimePromise; + return new Promise(() => {}); // no runtime, return a hanging promise. } diff --git a/genkit-tools/common/package.json b/genkit-tools/common/package.json index 6688eedd16..c37a74bebb 100644 --- a/genkit-tools/common/package.json +++ b/genkit-tools/common/package.json @@ -1,6 +1,6 @@ { "name": "@genkit-ai/tools-common", - "version": "1.0.4", + "version": "1.0.5", "scripts": { "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", "build:clean": "rimraf ./lib", diff --git a/genkit-tools/common/src/server/server.ts b/genkit-tools/common/src/server/server.ts index 100ca061ea..70d00d5bbc 100644 --- a/genkit-tools/common/src/server/server.ts +++ b/genkit-tools/common/src/server/server.ts @@ -87,36 +87,6 @@ export function startServer(manager: RuntimeManager, port: number) { res.end(); }); - // General purpose endpoint for Server Side Events to the Developer UI. - // Currently only event type "current-time" is supported, which notifies the - // subsriber of the currently selected Genkit Runtime (typically most recent). - app.get('/api/sse', async (_, res) => { - res.writeHead(200, { - 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'no-cache', - 'Content-Type': 'text/event-stream', - Connection: 'keep-alive', - }); - - // On connection, immediately send the "current" runtime (i.e. most recent) - const runtimeInfo = JSON.stringify(manager.getMostRecentRuntime() ?? {}); - res.write('event: current-runtime\n'); - res.write(`data: ${runtimeInfo}\n\n`); - - // When runtimes are added or removed, notify the Dev UI which runtime - // is considered "current" (i.e. most recent). In the future, we could send - // updates and let the developer decide which to use. - manager.onRuntimeEvent(() => { - const runtimeInfo = JSON.stringify(manager.getMostRecentRuntime() ?? {}); - res.write('event: current-runtime\n'); - res.write(`data: ${runtimeInfo}\n\n`); - }); - - res.on('close', () => { - res.end(); - }); - }); - app.get('/api/__health', (_, res) => { res.status(200).send(''); }); diff --git a/genkit-tools/common/src/types/model.ts b/genkit-tools/common/src/types/model.ts index cd92b7a25f..757c0b4d31 100644 --- a/genkit-tools/common/src/types/model.ts +++ b/genkit-tools/common/src/types/model.ts @@ -20,7 +20,7 @@ import { DocumentDataSchema } from './document'; // IMPORTANT: Keep this file in sync with genkit/ai/src/model.ts! // -const EmptyPartSchema = z.object({ +export const EmptyPartSchema = z.object({ text: z.never().optional(), media: z.never().optional(), toolRequest: z.never().optional(), @@ -96,7 +96,7 @@ export const MessageSchema = z.object({ }); export type MessageData = z.infer; -const OutputFormatSchema = z.enum(['json', 'text', 'media']); +export const OutputFormatSchema = z.enum(['json', 'text', 'media']); export const ModelInfoSchema = z.object({ /** Acceptable names for this model (e.g. different versions). */ @@ -157,9 +157,11 @@ export const GenerationCommonConfigSchema = z.object({ }); export type GenerationCommonConfig = typeof GenerationCommonConfigSchema; -const OutputConfigSchema = z.object({ +export const OutputConfigSchema = z.object({ format: OutputFormatSchema.optional(), schema: z.record(z.any()).optional(), + constrained: z.boolean().optional(), + contentType: z.string().optional(), }); export type OutputConfig = z.infer; diff --git a/genkit-tools/genkit-schema.json b/genkit-tools/genkit-schema.json index 8927aaedc7..092b0c5a0d 100644 --- a/genkit-tools/genkit-schema.json +++ b/genkit-tools/genkit-schema.json @@ -334,6 +334,28 @@ }, "additionalProperties": false }, + "EmptyPart": { + "type": "object", + "properties": { + "text": { + "$ref": "#/$defs/DataPart/properties/text" + }, + "media": { + "$ref": "#/$defs/DataPart/properties/media" + }, + "toolRequest": { + "$ref": "#/$defs/DataPart/properties/toolRequest" + }, + "toolResponse": { + "$ref": "#/$defs/DataPart/properties/toolResponse" + }, + "data": {}, + "metadata": { + "$ref": "#/$defs/DataPart/properties/metadata" + } + }, + "additionalProperties": false + }, "FinishReason": { "type": "string", "enum": [ @@ -497,22 +519,7 @@ ] }, "output": { - "type": "object", - "properties": { - "format": { - "type": "string", - "enum": [ - "json", - "text", - "media" - ] - }, - "schema": { - "type": "object", - "additionalProperties": {} - } - }, - "additionalProperties": false + "$ref": "#/$defs/OutputConfig" }, "context": { "type": "array", @@ -684,7 +691,9 @@ "toolResponse": { "$ref": "#/$defs/DataPart/properties/toolResponse" }, - "data": {}, + "data": { + "$ref": "#/$defs/EmptyPart/properties/data" + }, "metadata": { "$ref": "#/$defs/DataPart/properties/metadata" } @@ -860,6 +869,33 @@ ], "additionalProperties": false }, + "OutputConfig": { + "type": "object", + "properties": { + "format": { + "$ref": "#/$defs/OutputFormat" + }, + "schema": { + "type": "object", + "additionalProperties": {} + }, + "constrained": { + "type": "boolean" + }, + "contentType": { + "type": "string" + } + }, + "additionalProperties": false + }, + "OutputFormat": { + "type": "string", + "enum": [ + "json", + "text", + "media" + ] + }, "Part": { "anyOf": [ { @@ -904,7 +940,7 @@ "$ref": "#/$defs/DataPart/properties/toolResponse" }, "data": { - "$ref": "#/$defs/MediaPart/properties/data" + "$ref": "#/$defs/EmptyPart/properties/data" }, "metadata": { "$ref": "#/$defs/DataPart/properties/metadata" @@ -976,7 +1012,7 @@ "$ref": "#/$defs/DataPart/properties/toolResponse" }, "data": { - "$ref": "#/$defs/MediaPart/properties/data" + "$ref": "#/$defs/EmptyPart/properties/data" }, "metadata": { "$ref": "#/$defs/DataPart/properties/metadata" @@ -1016,7 +1052,7 @@ "additionalProperties": false }, "data": { - "$ref": "#/$defs/MediaPart/properties/data" + "$ref": "#/$defs/EmptyPart/properties/data" }, "metadata": { "$ref": "#/$defs/DataPart/properties/metadata" diff --git a/genkit-tools/telemetry-server/package.json b/genkit-tools/telemetry-server/package.json index eff3f18e0d..5c17174d24 100644 --- a/genkit-tools/telemetry-server/package.json +++ b/genkit-tools/telemetry-server/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", diff --git a/go/core/action.go b/go/core/action.go index 89d360a82e..4c1a87f083 100644 --- a/go/core/action.go +++ b/go/core/action.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "reflect" "time" @@ -20,16 +21,11 @@ import ( "github.com/invopop/jsonschema" ) -// Func is the type of function that Actions and Flows execute. -// It takes an input of type Int and returns an output of type Out, optionally -// streaming values of type Stream incrementally by invoking a callback. -// If the StreamingCallback is non-nil and the function supports streaming, it should -// stream the results by invoking the callback periodically, ultimately returning -// with a final return value. Otherwise, it should ignore the StreamingCallback and -// just return a result. -type Func[In, Out, Stream any] func(context.Context, In, func(context.Context, Stream) error) (Out, error) +// Func is an alias for non-streaming functions with input of type In and output of type Out. +type Func[In, Out any] = func(context.Context, In) (Out, error) -// TODO: use a generic type alias for the above when they become available? +// StreamingFunc is an alias for streaming functions with input of type In, output of type Out, and streaming chunk of type Stream. +type StreamingFunc[In, Out, Stream any] = func(context.Context, In, func(context.Context, Stream) error) (Out, error) // An Action is a named, observable operation. // It consists of a function that takes an input of type I and returns an output @@ -40,30 +36,27 @@ type Func[In, Out, Stream any] func(context.Context, In, func(context.Context, S // Each time an Action is run, it results in a new trace span. type Action[In, Out, Stream any] struct { name string + description string atype atype.ActionType - fn Func[In, Out, Stream] + fn StreamingFunc[In, Out, Stream] tstate *tracing.State inputSchema *jsonschema.Schema outputSchema *jsonschema.Schema - // optional - description string - metadata map[string]any + metadata map[string]any } type noStream = func(context.Context, struct{}) error -// See js/core/src/action.ts - // DefineAction creates a new non-streaming Action and registers it. func DefineAction[In, Out any]( r *registry.Registry, provider, name string, atype atype.ActionType, metadata map[string]any, - fn func(context.Context, In) (Out, error), + fn Func[In, Out], ) *Action[In, Out, struct{}] { return defineAction(r, provider, name, atype, metadata, nil, - func(ctx context.Context, in In, _ noStream) (Out, error) { + func(ctx context.Context, in In, cb noStream) (Out, error) { return fn(ctx, in) }) } @@ -74,21 +67,11 @@ func DefineStreamingAction[In, Out, Stream any]( provider, name string, atype atype.ActionType, metadata map[string]any, - fn Func[In, Out, Stream], + fn StreamingFunc[In, Out, Stream], ) *Action[In, Out, Stream] { return defineAction(r, provider, name, atype, metadata, nil, fn) } -// DefineCustomAction defines a streaming action with type Custom. -func DefineCustomAction[In, Out, Stream any]( - r *registry.Registry, - provider, name string, - metadata map[string]any, - fn Func[In, Out, Stream], -) *Action[In, Out, Stream] { - return DefineStreamingAction(r, provider, name, atype.Custom, metadata, fn) -} - // DefineActionWithInputSchema creates a new Action and registers it. // This differs from DefineAction in that the input schema is // defined dynamically; the static input type is "any". @@ -99,7 +82,7 @@ func DefineActionWithInputSchema[Out any]( atype atype.ActionType, metadata map[string]any, inputSchema *jsonschema.Schema, - fn func(context.Context, any) (Out, error), + fn Func[any, Out], ) *Action[any, Out, struct{}] { return defineAction(r, provider, name, atype, metadata, inputSchema, func(ctx context.Context, in any, _ noStream) (Out, error) { @@ -114,13 +97,13 @@ func defineAction[In, Out, Stream any]( atype atype.ActionType, metadata map[string]any, inputSchema *jsonschema.Schema, - fn Func[In, Out, Stream], + fn StreamingFunc[In, Out, Stream], ) *Action[In, Out, Stream] { fullName := name if provider != "" { fullName = provider + "/" + name } - a := newAction(fullName, atype, metadata, inputSchema, fn) + a := newAction(r, fullName, atype, metadata, inputSchema, fn) r.RegisterAction(atype, a) return a } @@ -128,11 +111,12 @@ func defineAction[In, Out, Stream any]( // newAction creates a new Action with the given name and arguments. // If inputSchema is nil, it is inferred from In. func newAction[In, Out, Stream any]( + r *registry.Registry, name string, atype atype.ActionType, metadata map[string]any, inputSchema *jsonschema.Schema, - fn Func[In, Out, Stream], + fn StreamingFunc[In, Out, Stream], ) *Action[In, Out, Stream] { var i In var o Out @@ -146,8 +130,9 @@ func newAction[In, Out, Stream any]( outputSchema = base.InferJSONSchema(o) } return &Action[In, Out, Stream]{ - name: name, - atype: atype, + name: name, + atype: atype, + tstate: r.TracingState(), fn: func(ctx context.Context, input In, sc func(context.Context, Stream) error) (Out, error) { tracing.SetCustomMetadataAttr(ctx, "subtype", string(atype)) return fn(ctx, input, sc) @@ -161,9 +146,6 @@ func newAction[In, Out, Stream any]( // Name returns the Action's Name. func (a *Action[In, Out, Stream]) Name() string { return a.name } -// setTracingState sets the action's tracing.State. -func (a *Action[In, Out, Stream]) SetTracingState(tstate *tracing.State) { a.tstate = tstate } - // Run executes the Action's function in a new trace span. func (a *Action[In, Out, Stream]) Run(ctx context.Context, input In, cb func(context.Context, Stream) error) (output Out, err error) { logger.FromContext(ctx).Debug("Action.Run", @@ -205,11 +187,13 @@ func (a *Action[In, Out, Stream]) Run(ctx context.Context, input In, cb func(con func (a *Action[In, Out, Stream]) RunJSON(ctx context.Context, input json.RawMessage, cb func(context.Context, json.RawMessage) error) (json.RawMessage, error) { // Validate input before unmarshaling it because invalid or unknown fields will be discarded in the process. if err := base.ValidateJSON(input, a.inputSchema); err != nil { - return nil, err + return nil, &base.HTTPError{Code: http.StatusBadRequest, Err: err} } var in In - if err := json.Unmarshal(input, &in); err != nil { - return nil, err + if input != nil { + if err := json.Unmarshal(input, &in); err != nil { + return nil, err + } } var callback func(context.Context, Stream) error if cb != nil { @@ -233,7 +217,7 @@ func (a *Action[In, Out, Stream]) RunJSON(ctx context.Context, input json.RawMes } // Desc returns a description of the action. -func (a *Action[I, O, S]) Desc() action.Desc { +func (a *Action[In, Out, Stream]) Desc() action.Desc { ad := action.Desc{ Name: a.name, Description: a.description, @@ -262,20 +246,3 @@ func LookupActionFor[In, Out, Stream any](r *registry.Registry, typ atype.Action } return a.(*Action[In, Out, Stream]) } - -var actionContextKey = base.NewContextKey[int]() - -// WithActionContext returns a new context with action runtime context (side channel data) -// value set. -func WithActionContext(ctx context.Context, actionContext map[string]any) context.Context { - return context.WithValue(ctx, actionContextKey, actionContext) -} - -// ActionContext returns the action runtime context (side channel data) from ctx. -func ActionContext(ctx context.Context) map[string]any { - val := ctx.Value(actionContextKey) - if val == nil { - return nil - } - return val.(map[string]any) -} diff --git a/go/core/context.go b/go/core/context.go new file mode 100644 index 0000000000..79b9e60485 --- /dev/null +++ b/go/core/context.go @@ -0,0 +1,42 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "encoding/json" + + "github.com/firebase/genkit/go/internal/base" +) + +var actionCtxKey = base.NewContextKey[int]() + +// WithActionContext returns a new Context with Action runtime context (side channel data) value set. +func WithActionContext(ctx context.Context, actionCtx ActionContext) context.Context { + return context.WithValue(ctx, actionCtxKey, actionCtx) +} + +// FromContext returns the Action runtime context (side channel data) from context. +func FromContext(ctx context.Context) ActionContext { + val := ctx.Value(actionCtxKey) + if val == nil { + return nil + } + return val.(ActionContext) +} + +// ActionContext is the runtime context for an Action. +type ActionContext = map[string]any + +// RequestData is the data associated with a request. +// It is used to provide additional context to the Action. +type RequestData struct { + Method string // Method is the HTTP method of the request (e.g. "GET", "POST", etc.) + Headers map[string]string // Headers is the headers of the request. The keys are the header names in lowercase. + Input json.RawMessage // Input is the body of the request. +} + +// ContextProvider is a function that returns an ActionContext for a given request. +// It is used to provide additional context to the Action. +type ContextProvider = func(ctx context.Context, req RequestData) (ActionContext, error) diff --git a/go/core/file_flow_state_store.go b/go/core/file_flow_state_store.go deleted file mode 100644 index 132860ed6a..0000000000 --- a/go/core/file_flow_state_store.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 Google LLC -// SPDX-License-Identifier: Apache-2.0 - - -package core - -import ( - "context" - "os" - "path/filepath" - - "github.com/firebase/genkit/go/internal/base" -) - -// A FileFlowStateStore is a FlowStateStore that writes flowStates to files. -type FileFlowStateStore struct { - dir string -} - -// NewFileFlowStateStore creates a FileFlowStateStore that writes traces to the given -// directory. The directory is created if it does not exist. -func NewFileFlowStateStore(dir string) (*FileFlowStateStore, error) { - if err := os.MkdirAll(dir, 0o755); err != nil { - return nil, err - } - return &FileFlowStateStore{dir: dir}, nil -} - -func (s *FileFlowStateStore) Save(ctx context.Context, id string, fs base.FlowStater) error { - data, err := fs.ToJSON() - if err != nil { - return err - } - return os.WriteFile(filepath.Join(s.dir, base.Clean(id)), data, 0666) -} - -func (s *FileFlowStateStore) Load(ctx context.Context, id string, pfs any) error { - return base.ReadJSONFile(filepath.Join(s.dir, base.Clean(id)), pfs) -} diff --git a/go/core/flow.go b/go/core/flow.go new file mode 100644 index 0000000000..1dfe31ad41 --- /dev/null +++ b/go/core/flow.go @@ -0,0 +1,145 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/firebase/genkit/go/core/tracing" + "github.com/firebase/genkit/go/internal/atype" + "github.com/firebase/genkit/go/internal/base" + "github.com/firebase/genkit/go/internal/registry" +) + +// A Flow is a user-defined Action. A Flow[In, Out, Stream] represents a function from In to Out. The Stream parameter is for flows that support streaming: providing their results incrementally. +type Flow[In, Out, Stream any] struct { + action *Action[In, Out, Stream] +} + +// StreamFlowValue is either a streamed value or a final output of a flow. +type StreamFlowValue[Out, Stream any] struct { + Done bool + Output Out // valid if Done is true + Stream Stream // valid if Done is false +} + +// flowContextKey is a context key that indicates whether the current context is a flow context. +var flowContextKey = base.NewContextKey[*flowContext]() + +// flowContext is a context that contains the tracing state for a flow. +type flowContext struct { + tracingState *tracing.State +} + +// DefineFlow creates a Flow that runs fn, and registers it as an action. fn takes an input of type In and returns an output of type Out. +func DefineFlow[In, Out any]( + r *registry.Registry, + name string, + fn Func[In, Out], +) *Flow[In, Out, struct{}] { + a := DefineAction(r, "", name, atype.Flow, nil, func(ctx context.Context, input In) (Out, error) { + fc := &flowContext{tracingState: r.TracingState()} + ctx = flowContextKey.NewContext(ctx, fc) + return fn(ctx, input) + }) + return &Flow[In, Out, struct{}]{action: a} +} + +// DefineStreamingFlow creates a streaming Flow that runs fn, and registers it as an action. +// +// fn takes an input of type In and returns an output of type Out, optionally +// streaming values of type Stream incrementally by invoking a callback. +// +// If the function supports streaming and the callback is non-nil, it should +// stream the results by invoking the callback periodically, ultimately returning +// with a final return value that includes all the streamed data. +// Otherwise, it should ignore the callback and just return a result. +func DefineStreamingFlow[In, Out, Stream any]( + r *registry.Registry, + name string, + fn StreamingFunc[In, Out, Stream], +) *Flow[In, Out, Stream] { + a := DefineStreamingAction(r, "", name, atype.Flow, nil, func(ctx context.Context, input In, cb func(context.Context, Stream) error) (Out, error) { + fc := &flowContext{tracingState: r.TracingState()} + ctx = flowContextKey.NewContext(ctx, fc) + return fn(ctx, input, cb) + }) + return &Flow[In, Out, Stream]{action: a} +} + +// Run runs the function f in the context of the current flow +// and returns what f returns. +// It returns an error if no flow is active. +// +// Each call to Run results in a new step in the flow. +// A step has its own span in the trace, and its result is cached so that if the flow +// is restarted, f will not be called a second time. +func Run[Out any](ctx context.Context, name string, fn func() (Out, error)) (Out, error) { + fc := flowContextKey.FromContext(ctx) + if fc == nil { + var z Out + return z, fmt.Errorf("flow.Run(%q): must be called from a flow", name) + } + return tracing.RunInNewSpan(ctx, fc.tracingState, name, "flowStep", false, nil, func(ctx context.Context, _ any) (Out, error) { + tracing.SetCustomMetadataAttr(ctx, "genkit:name", name) + tracing.SetCustomMetadataAttr(ctx, "genkit:type", "flowStep") + o, err := fn() + if err != nil { + return base.Zero[Out](), err + } + return o, nil + }) +} + +// Name returns the name of the flow. +func (f *Flow[In, Out, Stream]) Name() string { + return f.action.Name() +} + +// RunJSON runs the flow with JSON input and streaming callback and returns the output as JSON. +func (f *Flow[In, Out, Stream]) RunJSON(ctx context.Context, input json.RawMessage, cb func(context.Context, json.RawMessage) error) (json.RawMessage, error) { + return f.action.RunJSON(ctx, input, cb) +} + +// Run runs the flow in the context of another flow. +func (f *Flow[In, Out, Stream]) Run(ctx context.Context, input In) (Out, error) { + return f.action.Run(ctx, input, nil) +} + +// Stream runs the flow in the context of another flow and streams the output. +// It returns a function whose argument function (the "yield function") will be repeatedly +// called with the results. +// +// If the yield function is passed a non-nil error, the flow has failed with that +// error; the yield function will not be called again. +// +// If the yield function's [StreamFlowValue] argument has Done == true, the value's +// Output field contains the final output; the yield function will not be called +// again. +// +// Otherwise the Stream field of the passed [StreamFlowValue] holds a streamed result. +func (f *Flow[In, Out, Stream]) Stream(ctx context.Context, input In) func(func(*StreamFlowValue[Out, Stream], error) bool) { + return func(yield func(*StreamFlowValue[Out, Stream], error) bool) { + cb := func(ctx context.Context, s Stream) error { + if ctx.Err() != nil { + return ctx.Err() + } + if !yield(&StreamFlowValue[Out, Stream]{Stream: s}, nil) { + return errStop + } + return nil + } + output, err := f.action.Run(ctx, input, cb) + if err != nil { + yield(nil, err) + } else { + yield(&StreamFlowValue[Out, Stream]{Done: true, Output: output}, nil) + } + } +} + +var errStop = errors.New("stop") diff --git a/go/core/flow_state_store.go b/go/core/flow_state_store.go deleted file mode 100644 index 055c93e80a..0000000000 --- a/go/core/flow_state_store.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2024 Google LLC -// SPDX-License-Identifier: Apache-2.0 - - -package core - -import ( - "context" - - "github.com/firebase/genkit/go/internal/base" -) - -// A FlowStateStore stores flow states. -// Every flow state has a unique string identifier. -// A durable FlowStateStore is necessary for durable flows. -type FlowStateStore interface { - // Save saves the FlowState to the store, overwriting an existing one. - Save(ctx context.Context, id string, fs base.FlowStater) error - // Load reads the FlowState with the given ID from the store. - // It returns an error that is fs.ErrNotExist if there isn't one. - // pfs must be a pointer to a flowState[I, O] of the correct type. - Load(ctx context.Context, id string, pfs any) error -} - -// nopFlowStateStore is a FlowStateStore that does nothing. -type nopFlowStateStore struct{} - -func (nopFlowStateStore) Save(ctx context.Context, id string, fs base.FlowStater) error { return nil } -func (nopFlowStateStore) Load(ctx context.Context, id string, pfs any) error { return nil } diff --git a/go/core/flow_test.go b/go/core/flow_test.go new file mode 100644 index 0000000000..29f6fc412e --- /dev/null +++ b/go/core/flow_test.go @@ -0,0 +1,61 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "slices" + "testing" + + "github.com/firebase/genkit/go/internal/registry" +) + +func TestRunInFlow(t *testing.T) { + r, err := registry.New() + if err != nil { + t.Fatal(err) + } + n := 0 + stepf := func() (int, error) { + n++ + return n, nil + } + + flow := DefineFlow(r, "run", func(ctx context.Context, _ any) ([]int, error) { + g1, err := Run(ctx, "s1", stepf) + if err != nil { + return nil, err + } + g2, err := Run(ctx, "s2", stepf) + if err != nil { + return nil, err + } + return []int{g1, g2}, nil + }) + got, err := flow.Run(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + want := []int{1, 2} + if !slices.Equal(got, want) { + t.Errorf("got %v, want %v", got, want) + } +} + +func TestRunFlow(t *testing.T) { + r, err := registry.New() + if err != nil { + t.Fatal(err) + } + f := DefineFlow(r, "inc", func(ctx context.Context, i int) (int, error) { + return i + 1, nil + }) + got, err := f.Run(context.Background(), 2) + if err != nil { + t.Fatal(err) + } + if want := 3; got != want { + t.Errorf("got %d, want %d", got, want) + } +} diff --git a/go/genkit/conformance_test.go b/go/genkit/conformance_test.go deleted file mode 100644 index 42e7dc550c..0000000000 --- a/go/genkit/conformance_test.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2024 Google LLC -// SPDX-License-Identifier: Apache-2.0 - - -package genkit - -import ( - "cmp" - "context" - "encoding/json" - "errors" - "fmt" - "path/filepath" - "reflect" - "slices" - "strings" - "testing" - "time" - - "github.com/firebase/genkit/go/core" - "github.com/firebase/genkit/go/core/tracing" - "github.com/firebase/genkit/go/internal/base" - "github.com/firebase/genkit/go/internal/registry" - "golang.org/x/exp/maps" -) - -// conformanceTest describes a JSON format for language-independent testing -// of genkit flows ("conformance testing" for lack of a better term). -// -// All flows are functions from string to string. -type conformanceTest struct { - // Flow definition - Name string // name of the flow - Commands []command // a list of commands comprising the body of the flow - - // Action input - // This is the input field in the body of the /api/runAction route. - // The key field is constructed from the flow name. - Input json.RawMessage - - // Expected output - // These will unmarshal into untyped JSON (map[string]any, etc.), which - // facilitates comparing them in a general way. See compareJSON, below. - Result any - Trace any -} - -// A command is one function to run as part of a flow. -type command struct { - // Append appends its value to the input. - Append *string - // Run calls [Run] with the given name and a function whose body executes the - // given command. - Run *struct { - Name string - Command *command - } -} - -func (c *command) run(ctx context.Context, input string) (string, error) { - switch { - case c.Append != nil: - return input + *c.Append, nil - case c.Run != nil: - return Run(ctx, c.Run.Name, func() (string, error) { - return c.Run.Command.run(ctx, input) - }) - default: - return "", errors.New("unknown command") - } -} - -func TestFlowConformance(t *testing.T) { - testFiles, err := filepath.Glob(filepath.FromSlash("testdata/conformance/*.json")) - if err != nil { - t.Fatal(err) - } - if len(testFiles) == 0 { - t.Fatal("did not find any test files") - } - for _, filename := range testFiles { - t.Run(strings.TrimSuffix(filepath.Base(filename), ".json"), func(t *testing.T) { - var test conformanceTest - if err := base.ReadJSONFile(filename, &test); err != nil { - t.Fatal(err) - } - // Each test uses its own registry to avoid interference. - r, err := registry.New() - if err != nil { - t.Fatal(err) - } - tc := tracing.NewTestOnlyTelemetryClient() - r.TracingState().WriteTelemetryImmediate(tc) - _ = defineFlow(r, test.Name, flowFunction(test.Commands)) - key := fmt.Sprintf("/flow/%s", test.Name) - resp, err := runAction(context.Background(), r, key, test.Input, nil, nil) - if err != nil { - t.Fatal(err) - } - var result any - if err := json.Unmarshal(resp.Result, &result); err != nil { - t.Fatal(err) - } - if diff := compareJSON(result, test.Result); diff != "" { - t.Errorf("result:\n%s", diff) - } - - if test.Trace == nil { - return - } - gotTrace := tc.Traces[resp.Telemetry.TraceID] - var gotTraceAny map[string]any - gotTraceBytes, err := json.Marshal(gotTrace) - if err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(gotTraceBytes, &gotTraceAny); err != nil { - t.Fatal(err) - } - renameSpans(t, gotTraceAny) - renameSpans(t, test.Trace) - if diff := compareJSON(gotTraceAny, test.Trace); diff != "" { - t.Errorf("trace:\n%s", diff) - } - }) - } -} - -// flowFunction returns a function that runs the list of commands. -func flowFunction(commands []command) core.Func[string, string, struct{}] { - return func(ctx context.Context, input string, cb noStream) (string, error) { - result := input - var err error - for i, cmd := range commands { - if i > 0 { - // Pause between commands to ensure the trace start times are different. - // See renameSpans for why this is necessary. - time.Sleep(5 * time.Millisecond) - } - result, err = cmd.run(ctx, result) - if err != nil { - return "", err - } - } - return result, nil - } -} - -// renameSpans is given a trace, one of whose fields is a map from span ID to span. -// It changes the span map keys to s0, s1, ... in order of the span start time, -// as well as references to those IDs within the spans. -// This makes it possible to compare two span maps with different span IDs. -func renameSpans(t *testing.T, trace any) { - spans := trace.(map[string]any)["spans"].(map[string]any) - type item struct { - id string - t float64 - } - var items []item - startTimes := map[float64]bool{} - for id, span := range spans { - m := span.(map[string]any) - startTime := m["startTime"].(float64) - if startTimes[startTime] { - t.Fatal("duplicate start times") - } - startTimes[startTime] = true - // Delete startTimes because we don't want to compare them. - delete(m, "startTime") - items = append(items, item{id, startTime}) - } - slices.SortFunc(items, func(i1, i2 item) int { - return cmp.Compare(i1.t, i2.t) - }) - oldIDToNew := map[string]string{} - for i, item := range items { - oldIDToNew[item.id] = fmt.Sprintf("s%03d", i) - } - // Change old spanIDs to new. - // We cannot range over the map itself, because we change its keys in the loop. - for _, oldID := range maps.Keys(spans) { - span := spans[oldID].(map[string]any) - newID := oldIDToNew[oldID] - if newID == "" { - t.Fatalf("missing id: %q", oldID) - } - spans[newID] = span - delete(spans, oldID) - // A span references it own span ID and possibly its parent's. - span["spanId"] = oldIDToNew[span["spanId"].(string)] - if pid, ok := span["parentSpanId"]; ok { - span["parentSpanId"] = oldIDToNew[pid.(string)] - } - } -} - -// compareJSON compares two unmarshaled JSON values. -// Each must be nil or of type string, float64, bool, []any or map[string]any; -// these are the types used by json.Unmarshal when there is no type information -// (that is, when unmarshaling into a value of type any). -// For maps, only keys in the "want" map are examined; any extra keys in the "got" -// map are ignored. -// If the "want" value is the string "$ANYTHING", then the corresponding "got" value can -// be any string. -// If a "want" map key is "_comment", no comparison is done. -func compareJSON(got, want any) string { - var problems []string - - add := func(prefix, format string, args ...any) { - problems = append(problems, prefix+": "+fmt.Sprintf(format, args...)) - } - - var compareJSON1 func(prefix string, got, want any) - compareJSON1 = func(prefix string, got, want any) { - if want == nil { - if got != nil { - add(prefix, "got %v, want nil", got) - } - return - } - if got == nil { - add(prefix, "got nil, want %v", want) - return - } - if gt, wt := reflect.TypeOf(got), reflect.TypeOf(want); gt != wt { - add(prefix, "got type %s, want %s", gt, wt) - return - } - switch want := want.(type) { - case string, float64, bool: - if got != want && want != "$ANYTHING" { - add(prefix, "\ngot %v\nwant %v", got, want) - } - case []any: - got := got.([]any) - if len(got) != len(want) { - add(prefix, "lengths differ") - return - } - for i, g := range got { - compareJSON1(fmt.Sprintf("%s[%d]", prefix, i), g, want[i]) - } - - case map[string]any: - got := got.(map[string]any) - for k, wv := range want { - if k == "_comment" { - continue - } - gv, ok := got[k] - if !ok { - add(prefix, "missing key: %q", k) - } else { - compareJSON1(prefix+"."+k, gv, wv) - } - } - default: - add(prefix, "unknown type %T", want) - } - } - - compareJSON1("", got, want) - return strings.Join(problems, "\n") -} diff --git a/go/genkit/flow.go b/go/genkit/flow.go deleted file mode 100644 index b0a5d39fc9..0000000000 --- a/go/genkit/flow.go +++ /dev/null @@ -1,672 +0,0 @@ -// Copyright 2024 Google LLC -// SPDX-License-Identifier: Apache-2.0 - - -package genkit - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "log" - "net/http" - "strconv" - "sync" - "time" - - "github.com/firebase/genkit/go/core" - "github.com/firebase/genkit/go/core/logger" - "github.com/firebase/genkit/go/core/tracing" - "github.com/firebase/genkit/go/internal/atype" - "github.com/firebase/genkit/go/internal/base" - "github.com/firebase/genkit/go/internal/metrics" - "github.com/firebase/genkit/go/internal/registry" - "github.com/google/uuid" - "github.com/invopop/jsonschema" - otrace "go.opentelemetry.io/otel/trace" -) - -// TODO: support auth -// TODO: provide a way to start a Flow from user code. - -// A Flow is a kind of Action that can be interrupted and resumed. -// (Resumption is an experimental feature in the Javascript implementation, -// and not yet supported in Go.) -// -// A Flow[In, Out, Stream] represents a function from I to O (the S parameter is for streaming, -// described below). But the function may run in pieces, with interruptions and resumptions. -// (The interruptions discussed here are a part of the flow mechanism, not hardware -// interrupts.) The actual Go function for the flow may be executed multiple times, -// each time making more progress, until finally it completes with a value of type -// O or an error. The mechanism used to achieve this is explained below. -// -// To treat a flow as an action, which is an uninterrupted function execution, we -// use different input and output types to capture the additional behavior. The input -// to a flow action is an instruction about what to do: start running on the input, -// resume after being suspended, and others. This is the type flowInstruction[I] -// (called FlowInvokeEnvelopeMessage in the javascript code). -// -// The output of a flow action may contain the final output of type O if the flow -// finishes, but in general contains the state of the flow, including an ID to retrieve -// it later, what caused it to block, and so on. -// -// A flow consists of ordinary code, and can be interrupted on one machine and resumed -// on another, even if the underlying system has no support for process migration. -// To accomplish this, flowStates include the original input, and resuming a flow -// involves loading its flowState from storage and re-running its Go function from -// the beginning. To avoid repeating expensive work, parts of the flow, called steps, -// are cached in the flowState. The programmer marks these steps manually, by calling -// genkit.Run. -// -// A flow computation consists of one or more flow executions. (The flowExecution -// type records information about these; a flowState holds a slice of flowExecutions.) -// The computation begins with a "start" instruction. If the function is not interrupted, -// it will run to completion and the final state will contain its result. If it is -// interrupted, state will contain information about how and when it can be resumed. -// A "resume" instruction will run the Go function again using the information in -// the saved state. -// -// Another way to start a flow is to schedule it for some time in the future. The -// "schedule" instruction accomplishes this; the flow is finally started at a later -// time by the "runScheduled" instruction. -// -// Some flows can "stream" their results, providing them incrementally. To do so, -// the flow invokes a callback repeatedly. When streaming is complete, the flow -// returns a final result in the usual way. -// -// Streaming is only supported for the "start" flow instruction. Currently there is -// no way to schedule or resume a flow with streaming. - -// A Flow is an Action with additional support for observability and introspection. -// A Flow[In, Out, Stream] represents a function from In to Out. The Stream parameter is for -// flows that support streaming: providing their results incrementally. -type Flow[In, Out, Stream any] struct { - name string // The last component of the flow's key in the registry. - fn core.Func[In, Out, Stream] // The function to run. - stateStore core.FlowStateStore // Where FlowStates are stored, to support resumption. - tstate *tracing.State // set from the action when the flow is defined - inputSchema *jsonschema.Schema // Schema of the input to the flow - outputSchema *jsonschema.Schema // Schema of the output out of the flow - auth FlowAuth // Auth provider and policy checker for the flow. - // TODO: scheduler - // TODO: experimentalDurable - // TODO: middleware -} - -// runOptions configures a single flow run. -type runOptions struct { - authContext AuthContext // Auth context to pass to auth policy checker when calling a flow directly. -} - -// flowOptions configures a flow. -type flowOptions struct { - auth FlowAuth // Auth provider and policy checker for the flow. -} - -type noStream = func(context.Context, struct{}) error - -// AuthContext is the type of the auth context passed to the auth policy checker. -type AuthContext map[string]any - -// FlowAuth configures an auth context provider and an auth policy check for a flow. -type FlowAuth interface { - // ProvideAuthContext sets the auth context on the given context by parsing an auth header. - // The parsing logic is provided by the auth provider. - ProvideAuthContext(ctx context.Context, authHeader string) (context.Context, error) - - // NewContext sets the auth context on the given context. This is used when - // the auth context is provided by the user, rather than by the auth provider. - NewContext(ctx context.Context, authContext AuthContext) context.Context - - // FromContext retrieves the auth context from the given context. - FromContext(ctx context.Context) AuthContext - - // CheckAuthPolicy checks the auth context against policy. - CheckAuthPolicy(ctx context.Context, input any) error -} - -// streamingCallback is the type of streaming callbacks. -type streamingCallback[Stream any] func(context.Context, Stream) error - -// FlowOption modifies the flow with the provided option. -type FlowOption func(opts *flowOptions) - -// FlowRunOption modifies a flow run with the provided option. -type FlowRunOption func(opts *runOptions) - -// WithFlowAuth sets an auth provider and policy checker for the flow. -func WithFlowAuth(auth FlowAuth) FlowOption { - return func(f *flowOptions) { - if f.auth != nil { - log.Panic("auth already set in flow") - } - f.auth = auth - } -} - -// WithLocalAuth configures an option to run or stream a flow with a local auth value. -func WithLocalAuth(authContext AuthContext) FlowRunOption { - return func(opts *runOptions) { - if opts.authContext != nil { - log.Panic("authContext already set in runOptions") - } - opts.authContext = authContext - } -} - -// DefineFlow creates a Flow that runs fn, and registers it as an action. -// -// fn takes an input of type In and returns an output of type Out. -func DefineFlow[In, Out any]( - g *Genkit, - name string, - fn func(ctx context.Context, input In) (Out, error), - opts ...FlowOption, -) *Flow[In, Out, struct{}] { - return defineFlow(g.reg, name, core.Func[In, Out, struct{}]( - func(ctx context.Context, input In, cb func(ctx context.Context, _ struct{}) error) (Out, error) { - return fn(ctx, input) - }), opts...) -} - -// DefineStreamingFlow creates a streaming Flow that runs fn, and registers it as an action. -// -// fn takes an input of type In and returns an output of type Out, optionally -// streaming values of type Stream incrementally by invoking a callback. -// -// If the function supports streaming and the callback is non-nil, it should -// stream the results by invoking the callback periodically, ultimately returning -// with a final return value that includes all the streamed data. -// Otherwise, it should ignore the callback and just return a result. -func DefineStreamingFlow[In, Out, Stream any]( - g *Genkit, - name string, - fn func(ctx context.Context, input In, callback func(context.Context, Stream) error) (Out, error), - opts ...FlowOption, -) *Flow[In, Out, Stream] { - return defineFlow(g.reg, name, core.Func[In, Out, Stream](fn), opts...) -} - -func defineFlow[In, Out, Stream any](r *registry.Registry, name string, fn core.Func[In, Out, Stream], opts ...FlowOption) *Flow[In, Out, Stream] { - var i In - var o Out - f := &Flow[In, Out, Stream]{ - name: name, - fn: fn, - inputSchema: base.InferJSONSchema(i), - outputSchema: base.InferJSONSchema(o), - } - flowOpts := &flowOptions{} - for _, opt := range opts { - opt(flowOpts) - } - f.auth = flowOpts.auth - metadata := map[string]any{ - "requiresAuth": f.auth != nil, - } - afunc := func(ctx context.Context, input In, cb func(context.Context, Stream) error) (*Out, error) { - tracing.SetCustomMetadataAttr(ctx, "flow:wrapperAction", "true") - runtimeContext := core.ActionContext(ctx) - if f.auth != nil { - ctx = f.auth.NewContext(ctx, runtimeContext) - if err := f.checkAuthPolicy(ctx, any(input)); err != nil { - return nil, err - } - } - var opts []FlowRunOption - if runtimeContext != nil { - opts = append(opts, WithLocalAuth(runtimeContext)) - } - result, err := f.run(ctx, input, streamingCallback[Stream](cb), opts...) - if err != nil { - return nil, err - } - return &result, err - } - core.DefineStreamingAction(r, "", f.name, atype.Flow, metadata, afunc) - f.tstate = r.TracingState() - r.RegisterFlow(f) - return f -} - -// A flowState is a persistent representation of a flow that may be in the middle of running. -// It contains all the information needed to resume a flow, including the original input -// and a cache of all completed steps. -type flowState[In, Out any] struct { - FlowID string `json:"flowId,omitempty"` - FlowName string `json:"name,omitempty"` - // start time in milliseconds since the epoch - StartTime tracing.Milliseconds `json:"startTime,omitempty"` - Input In `json:"input,omitempty"` - mu sync.Mutex - Cache map[string]json.RawMessage `json:"cache,omitempty"` - EventsTriggered map[string]any `json:"eventsTriggered,omitempty"` - Executions []*flowExecution `json:"executions,omitempty"` - // The operation is the user-visible part of the state. - Operation *operation[Out] `json:"operation,omitempty"` - TraceContext string `json:"traceContext,omitempty"` -} - -func newFlowState[In, Out any](id, name string, input In) *flowState[In, Out] { - return &flowState[In, Out]{ - FlowID: id, - FlowName: name, - Input: input, - StartTime: tracing.ToMilliseconds(time.Now()), - Cache: map[string]json.RawMessage{}, - Operation: &operation[Out]{ - FlowID: id, - Done: false, - }, - } -} - -// flowState implements base.FlowStater. -func (fs *flowState[In, Out]) IsFlowState() {} - -func (fs *flowState[In, Out]) ToJSON() ([]byte, error) { - var buf bytes.Buffer - enc := json.NewEncoder(&buf) - enc.SetIndent("", " ") // make the value easy to read for debugging - if err := enc.Encode(fs); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func (fs *flowState[In, Out]) CacheAt(key string) json.RawMessage { - fs.mu.Lock() - defer fs.mu.Unlock() - return fs.Cache[key] -} - -func (fs *flowState[In, Out]) CacheSet(key string, val json.RawMessage) { - fs.mu.Lock() - defer fs.mu.Unlock() - fs.Cache[key] = val -} - -// An operation describes the state of a Flow that may still be in progress. -type operation[Out any] struct { - FlowID string `json:"name,omitempty"` - // The step that the flow is blocked on, if any. - BlockedOnStep *struct { - Name string `json:"name"` - Schema string `json:"schema"` - } `json:"blockedOnStep,omitempty"` - // Whether the operation is completed. - // If true Result will be non-nil. - Done bool `json:"done,omitempty"` - // Service-specific metadata associated with the operation. It typically contains progress information and common metadata such as create time. - Metadata any `json:"metadata,omitempty"` - Result *FlowResult[Out] `json:"result,omitempty"` -} - -// A FlowResult is the result of a flow: either success, in which case Response is -// the return value of the flow's function; or failure, in which case Error is the -// non-empty error string. -type FlowResult[Out any] struct { - Response Out `json:"response,omitempty"` - Error string `json:"error,omitempty"` - // The Error field above is not used in the code, but it gets marshaled - // into JSON. - // TODO: replace with a type that implements error and json.Marshaler. - err error - StackTrace string `json:"stacktrace,omitempty"` -} - -// The following methods make Flow[I, O, S] implement the flow interface, define in servers.go. - -// Name returns the name that the flow was defined with. -func (f *Flow[In, Out, Stream]) Name() string { return f.name } - -func (f *Flow[In, Out, Stream]) runJSON(ctx context.Context, authHeader string, input json.RawMessage, cb streamingCallback[json.RawMessage]) (json.RawMessage, error) { - // Validate input before unmarshaling it because invalid or unknown fields will be discarded in the process. - if err := base.ValidateJSON(input, f.inputSchema); err != nil { - return nil, &base.HTTPError{Code: http.StatusBadRequest, Err: err} - } - var in In - if err := json.Unmarshal(input, &in); err != nil { - return nil, &base.HTTPError{Code: http.StatusBadRequest, Err: err} - } - newCtx, err := f.provideAuthContext(ctx, authHeader) - if err != nil { - return nil, &base.HTTPError{Code: http.StatusUnauthorized, Err: err} - } - if err := f.checkAuthPolicy(newCtx, in); err != nil { - return nil, &base.HTTPError{Code: http.StatusForbidden, Err: err} - } - // If there is a callback, wrap it to turn an S into a json.RawMessage. - var callback streamingCallback[Stream] - if cb != nil { - callback = func(ctx context.Context, s Stream) error { - bytes, err := json.Marshal(s) - if err != nil { - return err - } - return cb(ctx, json.RawMessage(bytes)) - } - } - fstate, err := f.start(ctx, in, callback) - if err != nil { - return nil, err - } - if fstate.Operation == nil { - return nil, errors.New("nil operation") - } - res := fstate.Operation.Result - if res == nil { - return nil, errors.New("nil result") - } - if res.err != nil { - return nil, res.err - } - return json.Marshal(res.Response) -} - -// provideAuthContext provides auth context for the given auth header if flow auth is configured. -func (f *Flow[In, Out, Stream]) provideAuthContext(ctx context.Context, authHeader string) (context.Context, error) { - if f.auth != nil { - newCtx, err := f.auth.ProvideAuthContext(ctx, authHeader) - if err != nil { - return nil, fmt.Errorf("unauthorized: %w", err) - } - return newCtx, nil - } - return ctx, nil -} - -// checkAuthPolicy checks auth context against the policy if flow auth is configured. -func (f *Flow[In, Out, Stream]) checkAuthPolicy(ctx context.Context, input any) error { - if f.auth != nil { - if err := f.auth.CheckAuthPolicy(ctx, input); err != nil { - return fmt.Errorf("permission denied for resource: %w", err) - } - } - return nil -} - -// start starts executing the flow with the given input. -func (f *Flow[In, Out, Stream]) start(ctx context.Context, input In, cb streamingCallback[Stream]) (_ *flowState[In, Out], err error) { - flowID, err := generateFlowID() - if err != nil { - return nil, err - } - state := newFlowState[In, Out](flowID, f.name, input) - f.execute(ctx, state, "start", cb) - return state, nil -} - -// execute performs one flow execution. -// Using its flowState argument as a starting point, it runs the flow function until -// it finishes or is interrupted. -// It updates the passed flowState to reflect the new state of the flow compuation. -// -// This function corresponds to Flow.executeSteps in the js, but does more: -// it creates the flowContext and saves the state. -func (f *Flow[In, Out, Stream]) execute(ctx context.Context, state *flowState[In, Out], dispatchType string, cb streamingCallback[Stream]) { - fctx := newFlowContext(state, f.stateStore, f.tstate) - defer func() { - if err := fctx.finish(ctx); err != nil { - // TODO: do something more with this error? - logger.FromContext(ctx).Error("flowContext.finish", "err", err.Error()) - } - }() - ctx = flowContextKey.NewContext(ctx, fctx) - exec := &flowExecution{ - StartTime: tracing.ToMilliseconds(time.Now()), - } - state.mu.Lock() - state.Executions = append(state.Executions, exec) - state.mu.Unlock() - // TODO: retrieve the JSON-marshaled SpanContext from state.traceContext. - // TODO: add a span link to the context. - output, err := tracing.RunInNewSpan(ctx, fctx.tracingState(), f.name, "flow", true, state.Input, func(ctx context.Context, input In) (Out, error) { - tracing.SetCustomMetadataAttr(ctx, "flow:execution", strconv.Itoa(len(state.Executions)-1)) - // TODO: put labels into span metadata. - tracing.SetCustomMetadataAttr(ctx, "flow:name", f.name) - tracing.SetCustomMetadataAttr(ctx, "flow:id", state.FlowID) - tracing.SetCustomMetadataAttr(ctx, "flow:dispatchType", dispatchType) - rootSpanContext := otrace.SpanContextFromContext(ctx) - traceID := rootSpanContext.TraceID().String() - exec.TraceIDs = append(exec.TraceIDs, traceID) - // TODO: Save rootSpanContext in the state. - // TODO: If input is missing, get it from state.input and overwrite metadata.input. - start := time.Now() - var err error - if err = base.ValidateValue(input, f.inputSchema); err != nil { - err = fmt.Errorf("invalid input: %w", err) - } - var output Out - if err == nil { - output, err = f.fn(ctx, input, cb) - if err == nil { - if err = base.ValidateValue(output, f.outputSchema); err != nil { - err = fmt.Errorf("invalid output: %w", err) - } - } - } - latency := time.Since(start) - if err != nil { - // TODO: handle InterruptError - logger.FromContext(ctx).Error("flow failed", - "path", tracing.SpanPath(ctx), - "err", err.Error(), - ) - metrics.WriteFlowFailure(ctx, f.name, latency, err) - tracing.SetCustomMetadataAttr(ctx, "flow:state", "error") - } else { - logger.FromContext(ctx).Info("flow succeeded", "path", tracing.SpanPath(ctx)) - metrics.WriteFlowSuccess(ctx, f.name, latency) - tracing.SetCustomMetadataAttr(ctx, "flow:state", "done") - - } - // TODO: telemetry - return output, err - }) - // TODO: perhaps this should be in a defer, to handle panics? - state.mu.Lock() - defer state.mu.Unlock() - state.Operation.Done = true - if err != nil { - state.Operation.Result = &FlowResult[Out]{ - err: err, - Error: err.Error(), - // TODO: stack trace? - } - } else { - state.Operation.Result = &FlowResult[Out]{Response: output} - } -} - -// generateFlowID returns a unique ID for identifying a flow execution. -func generateFlowID() (string, error) { - // v4 UUID, as in the js code. - id, err := uuid.NewRandom() - if err != nil { - return "", err - } - return id.String(), nil -} - -// A flowContext holds dynamically accessible information about a flow. -// A flowContext is created when a flow starts running, and is stored -// in a context.Context so it can be accessed from within the currrently active flow. -type flowContext[I, O any] struct { - state *flowState[I, O] - stateStore core.FlowStateStore - tstate *tracing.State - mu sync.Mutex - seenSteps map[string]int // number of times each name appears, to avoid duplicate names - // TODO: auth -} - -// flowContexter is the type of all flowContext[I, O]. -type flowContexter interface { - uniqueStepName(string) string - stater() base.FlowStater - tracingState() *tracing.State -} - -func newFlowContext[I, O any](state *flowState[I, O], store core.FlowStateStore, tstate *tracing.State) *flowContext[I, O] { - return &flowContext[I, O]{ - state: state, - stateStore: store, - tstate: tstate, - seenSteps: map[string]int{}, - } -} -func (fc *flowContext[I, O]) stater() base.FlowStater { return fc.state } -func (fc *flowContext[I, O]) tracingState() *tracing.State { return fc.tstate } - -// finish is called at the end of a flow execution. -func (fc *flowContext[I, O]) finish(ctx context.Context) error { - if fc.stateStore == nil { - return nil - } - // TODO: In the js, start saves the state only under certain conditions. Duplicate? - return fc.stateStore.Save(ctx, fc.state.FlowID, fc.state) -} - -// uniqueStepName returns a name that is unique for this flow execution. -func (fc *flowContext[I, O]) uniqueStepName(name string) string { - fc.mu.Lock() - defer fc.mu.Unlock() - n := fc.seenSteps[name] - fc.seenSteps[name] = n + 1 - if n == 0 { - return name - } - return fmt.Sprintf("%s-%d", name, n) -} - -var flowContextKey = base.NewContextKey[flowContexter]() - -// Run runs the function f in the context of the current flow -// and returns what f returns. -// It returns an error if no flow is active. -// -// Each call to Run results in a new step in the flow. -// A step has its own span in the trace, and its result is cached so that if the flow -// is restarted, f will not be called a second time. -func Run[Out any](ctx context.Context, name string, f func() (Out, error)) (Out, error) { - // from js/flow/src/steps.ts - fc := flowContextKey.FromContext(ctx) - if fc == nil { - var z Out - return z, fmt.Errorf("genkit.Run(%q): must be called from a flow", name) - } - // TODO: The input here is irrelevant. Perhaps runInNewSpan should have only a result type param, - // as in the js. - return tracing.RunInNewSpan(ctx, fc.tracingState(), name, "flowStep", false, 0, func(ctx context.Context, _ int) (Out, error) { - uName := fc.uniqueStepName(name) - tracing.SetCustomMetadataAttr(ctx, "flow:stepType", "run") - tracing.SetCustomMetadataAttr(ctx, "flow:stepName", name) - tracing.SetCustomMetadataAttr(ctx, "flow:resolvedStepName", uName) - // Memoize the function call, using the cache in the flowState. - // The locking here prevents corruption of the cache from concurrent access, but doesn't - // prevent two goroutines racing to check the cache and call f. However, that shouldn't - // happen because every step has a unique cache key. - // TODO: don't memoize a nested flow (see context.ts) - fs := fc.stater() - j := fs.CacheAt(uName) - if j != nil { - var t Out - if err := json.Unmarshal(j, &t); err != nil { - return base.Zero[Out](), err - } - tracing.SetCustomMetadataAttr(ctx, "flow:state", "cached") - return t, nil - } - t, err := f() - if err != nil { - return base.Zero[Out](), err - } - bytes, err := json.Marshal(t) - if err != nil { - return base.Zero[Out](), err - } - fs.CacheSet(uName, json.RawMessage(bytes)) - tracing.SetCustomMetadataAttr(ctx, "flow:state", "run") - return t, nil - }) -} - -// Run runs the flow in the context of another flow. The flow must run to completion when started -// (that is, it must not have interrupts). -func (f *Flow[In, Out, Stream]) Run(ctx context.Context, input In, opts ...FlowRunOption) (Out, error) { - return f.run(ctx, input, nil, opts...) -} - -func (f *Flow[In, Out, Stream]) run(ctx context.Context, input In, cb func(context.Context, Stream) error, opts ...FlowRunOption) (Out, error) { - runOpts := &runOptions{} - for _, opt := range opts { - opt(runOpts) - } - if runOpts.authContext != nil && f.auth != nil { - ctx = f.auth.NewContext(ctx, runOpts.authContext) - } - if err := f.checkAuthPolicy(ctx, input); err != nil { - return base.Zero[Out](), err - } - state, err := f.start(ctx, input, cb) - if err != nil { - return base.Zero[Out](), err - } - return finishedOpResponse(state.Operation) -} - -// StreamFlowValue is either a streamed value or a final output of a flow. -type StreamFlowValue[Out, Stream any] struct { - Done bool - Output Out // valid if Done is true - Stream Stream // valid if Done is false -} - -// Stream runs the flow on input and delivers both the streamed values and the final output. -// It returns a function whose argument function (the "yield function") will be repeatedly -// called with the results. -// -// If the yield function is passed a non-nil error, the flow has failed with that -// error; the yield function will not be called again. An error is also passed if -// the flow fails to complete (that is, it has an interrupt). -// Genkit Go does not yet support interrupts. -// -// If the yield function's [StreamFlowValue] argument has Done == true, the value's -// Output field contains the final output; the yield function will not be called -// again. -// -// Otherwise the Stream field of the passed [StreamFlowValue] holds a streamed result. -func (f *Flow[In, Out, Stream]) Stream(ctx context.Context, input In, opts ...FlowRunOption) func(func(*StreamFlowValue[Out, Stream], error) bool) { - return func(yield func(*StreamFlowValue[Out, Stream], error) bool) { - cb := func(ctx context.Context, s Stream) error { - if ctx.Err() != nil { - return ctx.Err() - } - if !yield(&StreamFlowValue[Out, Stream]{Stream: s}, nil) { - return errStop - } - return nil - } - output, err := f.run(ctx, input, cb, opts...) - if err != nil { - yield(nil, err) - } else { - yield(&StreamFlowValue[Out, Stream]{Done: true, Output: output}, nil) - } - } -} - -var errStop = errors.New("stop") - -func finishedOpResponse[O any](op *operation[O]) (O, error) { - if !op.Done { - return base.Zero[O](), fmt.Errorf("flow %s did not finish execution", op.FlowID) - } - if op.Result.err != nil { - return base.Zero[O](), fmt.Errorf("flow %s: %w", op.FlowID, op.Result.err) - } - return op.Result.Response, nil -} diff --git a/go/genkit/flow_test.go b/go/genkit/flow_test.go deleted file mode 100644 index 9fa573a61d..0000000000 --- a/go/genkit/flow_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2024 Google LLC -// SPDX-License-Identifier: Apache-2.0 - - -package genkit - -import ( - "context" - "encoding/json" - "errors" - "slices" - "testing" - - "github.com/firebase/genkit/go/core" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -func incFlow(_ context.Context, i int, _ noStream) (int, error) { - return i + 1, nil -} - -func TestFlowStart(t *testing.T) { - ai, err := New(nil) - if err != nil { - t.Fatal(err) - } - f := DefineStreamingFlow(ai, "inc", incFlow) - ss, err := core.NewFileFlowStateStore(t.TempDir()) - if err != nil { - t.Fatal(err) - } - f.stateStore = ss - state, err := f.start(context.Background(), 1, nil) - if err != nil { - t.Fatal(err) - } - got := state.Operation - want := &operation[int]{ - Done: true, - Result: &FlowResult[int]{ - Response: 2, - }, - } - diff := cmp.Diff(want, got, - cmpopts.IgnoreFields(operation[int]{}, "FlowID"), - cmpopts.IgnoreUnexported(FlowResult[int]{}, flowState[int, int]{})) - if diff != "" { - t.Errorf("mismatch (-want, +got):\n%s", diff) - } -} - -func TestFlowRun(t *testing.T) { - ai, err := New(nil) - if err != nil { - t.Fatal(err) - } - n := 0 - stepf := func() (int, error) { - n++ - return n, nil - } - - flow := DefineFlow(ai, "run", func(ctx context.Context, s string) ([]int, error) { - g1, err := Run(ctx, "s1", stepf) - if err != nil { - return nil, err - } - g2, err := Run(ctx, "s2", stepf) - if err != nil { - return nil, err - } - return []int{g1, g2}, nil - }) - state, err := flow.start(context.Background(), "", nil) - if err != nil { - t.Fatal(err) - } - op := state.Operation - if !op.Done { - t.Fatal("not done") - } - got := op.Result.Response - want := []int{1, 2} - if !slices.Equal(got, want) { - t.Errorf("got %v, want %v", got, want) - } -} - -func TestRunFlow(t *testing.T) { - ai, err := New(nil) - if err != nil { - t.Fatal(err) - } - f := defineFlow(ai.reg, "inc", incFlow) - got, err := f.Run(context.Background(), 2) - if err != nil { - t.Fatal(err) - } - if want := 3; got != want { - t.Errorf("got %d, want %d", got, want) - } -} - -func TestFlowState(t *testing.T) { - // A flowState is an action output, so it must support JSON marshaling. - // Verify that a fully populated flowState can round-trip via JSON. - - fs := &flowState[int, int]{ - FlowID: "id", - FlowName: "name", - StartTime: 1, - Input: 2, - Cache: map[string]json.RawMessage{"x": json.RawMessage([]byte("3"))}, - EventsTriggered: map[string]any{"a": "b"}, - Executions: []*flowExecution{{StartTime: 4, EndTime: 5, TraceIDs: []string{"c"}}}, - Operation: &operation[int]{ - FlowID: "id", - BlockedOnStep: &struct { - Name string `json:"name"` - Schema string `json:"schema"` - }{Name: "bos", Schema: "s"}, - Done: true, - Metadata: "meta", - Result: &FlowResult[int]{ - Response: 6, - err: errors.New("err"), - Error: "err", - StackTrace: "st", - }, - }, - TraceContext: "tc", - } - data, err := json.Marshal(fs) - if err != nil { - t.Fatal(err) - } - var got *flowState[int, int] - if err := json.Unmarshal(data, &got); err != nil { - t.Fatal(err) - } - diff := cmp.Diff(fs, got, cmpopts.IgnoreUnexported(flowState[int, int]{}, FlowResult[int]{})) - if diff != "" { - t.Errorf("mismatch (-want, +got):\n%s", diff) - } -} diff --git a/go/genkit/gen.go b/go/genkit/gen.go deleted file mode 100644 index 8168e18f25..0000000000 --- a/go/genkit/gen.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024 Google LLC -// SPDX-License-Identifier: Apache-2.0 - - -// This file was generated by jsonschemagen. DO NOT EDIT. - -package genkit - -import "github.com/firebase/genkit/go/core/tracing" - -type flowError struct { - Error string `json:"error,omitempty"` - Stacktrace string `json:"stacktrace,omitempty"` -} - -type flowExecution struct { - // end time in milliseconds since the epoch - EndTime tracing.Milliseconds `json:"endTime,omitempty"` - // start time in milliseconds since the epoch - StartTime tracing.Milliseconds `json:"startTime,omitempty"` - TraceIDs []string `json:"traceIds,omitempty"` -} diff --git a/go/genkit/genkit.go b/go/genkit/genkit.go index 73e7c92419..365e7bc13d 100644 --- a/go/genkit/genkit.go +++ b/go/genkit/genkit.go @@ -6,151 +6,171 @@ package genkit import ( "context" + "encoding/json" + "errors" "fmt" "log/slog" - "net/http" "os" "os/signal" "strings" - "sync" "syscall" "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/core" + "github.com/firebase/genkit/go/internal/atype" "github.com/firebase/genkit/go/internal/registry" "github.com/invopop/jsonschema" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) +// Action is the interface that all Genkit actions (e.g. flows, models, tools, etc) have in common. +type Action interface { + // Name returns the name of the action. + Name() string + + // RunJSON runs the action with the given JSON input and streaming callback and returns the output as JSON. + RunJSON(ctx context.Context, input json.RawMessage, cb func(context.Context, json.RawMessage) error) (json.RawMessage, error) +} + // Genkit encapsulates a Genkit instance including the registry and configuration. type Genkit struct { - // The registry for this instance. + // Registry for all actions contained in this instance. reg *registry.Registry - // Options to configure the instance. - Opts *Options + // Params to configure calls using this instance. + Params *GenkitParams } -type Options struct { - // The default model to use if no model is specified. - DefaultModel string - // Directory where dotprompts are stored. - PromptDir string -} +type genkitOption = func(params *GenkitParams) error -// StartOptions are options to [Start]. -type StartOptions struct { - // If "-", do not start a FlowServer. - // Otherwise, start a FlowServer on the given address, or the - // default of ":3400" if empty. - FlowAddr string - // The names of flows to serve. - // If empty, all registered flows are served. - Flows []string +type GenkitParams struct { + DefaultModel string // The default model to use if no model is specified. + PromptDir string // Directory where dotprompts are stored. } -// New creates a new Genkit instance. -func New(opts *Options) (*Genkit, error) { - r, err := registry.New() - if err != nil { - return nil, err - } - if opts == nil { - opts = &Options{} +// WithDefaultModel sets the default model to use if no model is specified. +func WithDefaultModel(model string) genkitOption { + return func(params *GenkitParams) error { + if params.DefaultModel != "" { + return errors.New("genkit.WithDefaultModel: cannot set DefaultModel more than once") + } + params.DefaultModel = model + return nil } - if opts.DefaultModel != "" { - parts := strings.Split(opts.DefaultModel, "/") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid default model format %q, expected provider/name", opts.DefaultModel) +} + +// WithPromptDir sets the directory where dotprompts are stored. Defaults to "prompts" at project root. +func WithPromptDir(dir string) genkitOption { + return func(params *GenkitParams) error { + if params.PromptDir != "" { + return errors.New("genkit.WithPromptDir: cannot set PromptDir more than once") } + params.PromptDir = dir + return nil } - return &Genkit{ - reg: r, - Opts: opts, - }, nil } -// Start initializes Genkit. -// After it is called, no further actions can be defined. +// Init creates a new Genkit instance. // -// Start starts servers depending on the value of the GENKIT_ENV -// environment variable and the provided options. -// -// If GENKIT_ENV = "dev", a development server is started -// in a separate goroutine at the address in opts.DevAddr, or the default -// of ":3100" if empty. -// -// If opts.FlowAddr is a value other than "-", a flow server is started -// and the call to Start waits for the server to shut down. -// If opts.FlowAddr == "-", no flow server is started and Start returns immediately. -// -// Thus Start(nil) will start a dev server in the "dev" environment, will always start -// a flow server, and will pause execution until the flow server terminates. -func (g *Genkit) Start(ctx context.Context, opts *StartOptions) error { - ai.DefineGenerateAction(ctx, g.reg) +// During local development (GENKIT_ENV=dev), it starts the Reflection API server (default :3100) as a side effect. +func Init(ctx context.Context, opts ...genkitOption) (*Genkit, error) { + ctx, _ = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) - if opts == nil { - opts = &StartOptions{} + r, err := registry.New() + if err != nil { + return nil, err } - g.reg.Freeze() - var mu sync.Mutex - var servers []*http.Server - var wg sync.WaitGroup - errCh := make(chan error, 2) + params := &GenkitParams{} + for _, opt := range opts { + if err := opt(params); err != nil { + return nil, err + } + } - if registry.CurrentEnvironment() == registry.EnvironmentDev { - wg.Add(1) - go func() { - defer wg.Done() - s := startReflectionServer(ctx, g.reg, errCh) - mu.Lock() - servers = append(servers, s) - mu.Unlock() - }() + if params.DefaultModel != "" { + _, err := modelRefParts(params.DefaultModel) + if err != nil { + return nil, err + } } - if opts.FlowAddr != "-" { - wg.Add(1) + if registry.CurrentEnvironment() == registry.EnvironmentDev { + errCh := make(chan error, 1) + serverStartCh := make(chan struct{}) + go func() { - defer wg.Done() - s := startFlowServer(g, opts.FlowAddr, opts.Flows, errCh) - mu.Lock() - servers = append(servers, s) - mu.Unlock() + if s := startReflectionServer(ctx, r, errCh, serverStartCh); s == nil { + return + } + if err := <-errCh; err != nil { + slog.Error("reflection server error", "err", err) + } }() - } - serverStartCh := make(chan struct{}) - go func() { - wg.Wait() - close(serverStartCh) - }() - - // It will block here until either all servers start up or there is an error in starting one. - select { - case <-serverStartCh: - slog.Info("all servers started successfully") - case err := <-errCh: - return fmt.Errorf("failed to start servers: %w", err) - case <-ctx.Done(): - return ctx.Err() + select { + case err := <-errCh: + return nil, fmt.Errorf("reflection server startup failed: %w", err) + case <-serverStartCh: + slog.Debug("reflection server started successfully") + case <-ctx.Done(): + return nil, ctx.Err() + } } - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - // It will block here (i.e. servers will run) until we get an interrupt signal. - select { - case sig := <-sigCh: - slog.Info("received signal, initiating shutdown", "signal", sig) - case err := <-errCh: - slog.Error("server error", "err", err) - return err - case <-ctx.Done(): - slog.Info("context cancelled, initiating shutdown") - } + return &Genkit{ + reg: r, + Params: params, + }, nil +} + +// DefineFlow creates a Flow that runs fn, and registers it as an action. fn takes an input of type In and returns an output of type Out. +func DefineFlow[In, Out any]( + g *Genkit, + name string, + fn core.Func[In, Out], +) *core.Flow[In, Out, struct{}] { + return core.DefineFlow(g.reg, name, fn) +} + +// DefineStreamingFlow creates a streaming Flow that runs fn, and registers it as an action. +// +// fn takes an input of type In and returns an output of type Out, optionally +// streaming values of type Stream incrementally by invoking a callback. +// +// If the function supports streaming and the callback is non-nil, it should +// stream the results by invoking the callback periodically, ultimately returning +// with a final return value that includes all the streamed data. +// Otherwise, it should ignore the callback and just return a result. +func DefineStreamingFlow[In, Out, Stream any]( + g *Genkit, + name string, + fn core.StreamingFunc[In, Out, Stream], +) *core.Flow[In, Out, Stream] { + return core.DefineStreamingFlow(g.reg, name, fn) +} + +// Run runs the function f in the context of the current flow +// and returns what f returns. +// It returns an error if no flow is active. +// +// Each call to Run results in a new step in the flow. +// A step has its own span in the trace, and its result is cached so that if the flow +// is restarted, f will not be called a second time. +func Run[Out any](ctx context.Context, name string, f func() (Out, error)) (Out, error) { + return core.Run(ctx, name, f) +} - return shutdownServers(servers) +// ListFlows returns all flows registered in the Genkit instance. +func ListFlows(g *Genkit) []Action { + acts := g.reg.ListActions() + flows := []Action{} + for _, act := range acts { + if strings.HasPrefix(act.Key, "/"+string(atype.Flow)+"/") { + flows = append(flows, g.reg.LookupAction(act.Key)) + } + } + return flows } // DefineModel registers the given generate function as an action, and returns a @@ -301,16 +321,25 @@ func RegisterSpanProcessor(g *Genkit, sp sdktrace.SpanProcessor) { // optsWithDefaults prepends defaults to the options so that they can be overridden by the caller. func optsWithDefaults(g *Genkit, opts []ai.GenerateOption) ([]ai.GenerateOption, error) { - if g.Opts.DefaultModel != "" { - parts := strings.Split(g.Opts.DefaultModel, "/") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid default model format %q, expected provider/name", g.Opts.DefaultModel) + if g.Params.DefaultModel != "" { + parts, err := modelRefParts(g.Params.DefaultModel) + if err != nil { + return nil, err } model := LookupModel(g, parts[0], parts[1]) if model == nil { - return nil, fmt.Errorf("default model %q not found", g.Opts.DefaultModel) + return nil, fmt.Errorf("default model %q not found", g.Params.DefaultModel) } opts = append([]ai.GenerateOption{ai.WithModel(model)}, opts...) } return opts, nil } + +// modelRefParts parses a model string into a provider and name. +func modelRefParts(model string) ([]string, error) { + parts := strings.Split(model, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid model format %q, expected provider/name", model) + } + return parts, nil +} diff --git a/go/genkit/genkit_test.go b/go/genkit/genkit_test.go index db4fdfb332..e0c040beb5 100644 --- a/go/genkit/genkit_test.go +++ b/go/genkit/genkit_test.go @@ -1,23 +1,24 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package genkit import ( "context" "testing" + + "github.com/firebase/genkit/go/core" ) func TestStreamFlow(t *testing.T) { - ai, err := New(nil) + g, err := Init(context.Background()) if err != nil { t.Fatal(err) } - f := DefineStreamingFlow(ai, "count", count) + f := DefineStreamingFlow(g, "count", count) iter := f.Stream(context.Background(), 2) want := 0 - iter(func(val *StreamFlowValue[int, int], err error) bool { + iter(func(val *core.StreamFlowValue[int, int], err error) bool { if err != nil { t.Fatal(err) } diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go new file mode 100644 index 0000000000..0b8674a46c --- /dev/null +++ b/go/genkit/reflection.go @@ -0,0 +1,364 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: Apache-2.0 + +package genkit + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/firebase/genkit/go/core" + "github.com/firebase/genkit/go/core/logger" + "github.com/firebase/genkit/go/core/tracing" + "github.com/firebase/genkit/go/internal" + "github.com/firebase/genkit/go/internal/action" + "github.com/firebase/genkit/go/internal/base" + "github.com/firebase/genkit/go/internal/registry" + "go.opentelemetry.io/otel/trace" +) + +type streamingCallback[Stream any] = func(context.Context, Stream) error + +// runtimeFileData is the data written to the file describing this runtime. +type runtimeFileData struct { + ID string `json:"id"` + PID int `json:"pid"` + ReflectionServerURL string `json:"reflectionServerUrl"` + Timestamp string `json:"timestamp"` + GenkitVersion string `json:"genkitVersion"` + ReflectionApiSpecVersion int `json:"reflectionApiSpecVersion"` +} + +// reflectionServer encapsulates everything needed to serve the Reflection API. +type reflectionServer struct { + *http.Server + Reg *registry.Registry // Registry from which the server gets its actions. + RuntimeFilePath string // Path to the runtime file that was written at startup. +} + +// startReflectionServer starts the Reflection API server listening at the +// value of the environment variable GENKIT_REFLECTION_PORT for the port, +// or ":3100" if it is empty. +func startReflectionServer(ctx context.Context, r *registry.Registry, errCh chan<- error, serverStartCh chan<- struct{}) *reflectionServer { + if r == nil { + errCh <- fmt.Errorf("nil registry provided") + return nil + } + + addr := "127.0.0.1:3100" + if os.Getenv("GENKIT_REFLECTION_PORT") != "" { + addr = "127.0.0.1:" + os.Getenv("GENKIT_REFLECTION_PORT") + } + + s := &reflectionServer{ + Server: &http.Server{ + Addr: addr, + Handler: serveMux(r), + }, + Reg: r, + } + + slog.Debug("starting reflection server", "addr", s.Addr) + + if err := s.writeRuntimeFile(s.Addr); err != nil { + errCh <- fmt.Errorf("failed to write runtime file: %w", err) + return nil + } + + serverCtx, cancel := context.WithCancel(context.Background()) + + go func() { + // First check that the port is available before signaling a server start success. + listener, err := net.Listen("tcp", s.Addr) + if err != nil { + errCh <- fmt.Errorf("failed to create listener: %w", err) + return + } + + close(serverStartCh) + + if err := s.Serve(listener); err != nil && err != http.ErrServerClosed { + errCh <- err + } + // If the server shuts down unexpectedly, this will trigger the cleanup. + cancel() + }() + + go func() { + // Blocks here until the context is done or the server crashes. + select { + case <-ctx.Done(): + case <-serverCtx.Done(): + return + } + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := s.Shutdown(shutdownCtx); err != nil { + slog.Error("reflection server shutdown error", "error", err) + } + + if err := s.cleanupRuntimeFile(); err != nil { + slog.Error("failed to cleanup runtime file", "error", err) + } + }() + + return s +} + +// writeRuntimeFile writes a file describing the runtime to the project root. +func (s *reflectionServer) writeRuntimeFile(url string) error { + projectRoot, err := findProjectRoot() + if err != nil { + return fmt.Errorf("failed to find project root: %w", err) + } + + runtimesDir := filepath.Join(projectRoot, ".genkit", "runtimes") + if err := os.MkdirAll(runtimesDir, 0755); err != nil { + return fmt.Errorf("failed to create runtimes directory: %w", err) + } + + runtimeID := os.Getenv("GENKIT_RUNTIME_ID") + if runtimeID == "" { + runtimeID = strconv.Itoa(os.Getpid()) + } + + timestamp := time.Now().UTC().Format(time.RFC3339) + s.RuntimeFilePath = filepath.Join(runtimesDir, fmt.Sprintf("%d-%s.json", os.Getpid(), timestamp)) + + data := runtimeFileData{ + ID: runtimeID, + PID: os.Getpid(), + ReflectionServerURL: fmt.Sprintf("http://%s", url), + Timestamp: timestamp, + GenkitVersion: "go/" + internal.Version, + ReflectionApiSpecVersion: internal.GENKIT_REFLECTION_API_SPEC_VERSION, + } + + fileContent, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal runtime data: %w", err) + } + + if err := os.WriteFile(s.RuntimeFilePath, fileContent, 0644); err != nil { + return fmt.Errorf("failed to write runtime file: %w", err) + } + + slog.Debug("runtime file written", "path", s.RuntimeFilePath) + return nil +} + +// cleanupRuntimeFile removes the runtime file associated with the dev server. +func (s *reflectionServer) cleanupRuntimeFile() error { + if s.RuntimeFilePath == "" { + return nil + } + + content, err := os.ReadFile(s.RuntimeFilePath) + if err != nil { + return fmt.Errorf("failed to read runtime file: %w", err) + } + + var data runtimeFileData + if err := json.Unmarshal(content, &data); err != nil { + return fmt.Errorf("failed to unmarshal runtime data: %w", err) + } + + if data.PID == os.Getpid() { + if err := os.Remove(s.RuntimeFilePath); err != nil { + return fmt.Errorf("failed to remove runtime file: %w", err) + } + slog.Debug("runtime file cleaned up", "path", s.RuntimeFilePath) + } + + return nil +} + +// findProjectRoot finds the project root by looking for a go.mod file. +func findProjectRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + + parent := filepath.Dir(dir) + if parent == dir { + slog.Warn("could not find project root (go.mod not found)") + return os.Getwd() + } + dir = parent + } +} + +// serveMux returns a new ServeMux configured for the required Reflection API endpoints. +func serveMux(r *registry.Registry) *http.ServeMux { + mux := http.NewServeMux() + // Skip wrapHandler here to avoid logging constant polling requests. + mux.HandleFunc("GET /api/__health", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + mux.HandleFunc("GET /api/actions", wrapHandler(handleListActions(r))) + mux.HandleFunc("POST /api/runAction", wrapHandler(handleRunAction(r))) + mux.HandleFunc("POST /api/notify", wrapHandler(handleNotify(r))) + return mux +} + +// handleRunAction looks up an action by name in the registry, runs it with the +// provided JSON input, and writes back the JSON-marshaled request. +func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http.Request) error { + return func(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + + var body struct { + Key string `json:"key"` + Input json.RawMessage `json:"input"` + Context json.RawMessage `json:"context"` + } + defer r.Body.Close() + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + return &base.HTTPError{Code: http.StatusBadRequest, Err: err} + } + + stream, err := parseBoolQueryParam(r, "stream") + if err != nil { + return err + } + + logger.FromContext(ctx).Debug("running action", "key", body.Key, "stream", stream) + + var cb streamingCallback[json.RawMessage] + if stream { + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Transfer-Encoding", "chunked") + // Stream results are newline-separated JSON. + cb = func(ctx context.Context, msg json.RawMessage) error { + _, err := fmt.Fprintf(w, "%s\n", msg) + if err != nil { + return err + } + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + return nil + } + } + + var contextMap core.ActionContext = nil + if body.Context != nil { + json.Unmarshal(body.Context, &contextMap) + } + + resp, err := runAction(ctx, reg, body.Key, body.Input, cb, contextMap) + if err != nil { + return err + } + + return writeJSON(ctx, w, resp) + } +} + +// handleNotify configures the telemetry server URL from the request. +func handleNotify(reg *registry.Registry) func(w http.ResponseWriter, r *http.Request) error { + return func(w http.ResponseWriter, r *http.Request) error { + var body struct { + TelemetryServerURL string `json:"telemetryServerUrl"` + ReflectionApiSpecVersion int `json:"reflectionApiSpecVersion"` + } + + defer r.Body.Close() + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + return &base.HTTPError{Code: http.StatusBadRequest, Err: err} + } + + if os.Getenv("GENKIT_TELEMETRY_SERVER") == "" && body.TelemetryServerURL != "" { + reg.TracingState().WriteTelemetryImmediate(tracing.NewHTTPTelemetryClient(body.TelemetryServerURL)) + slog.Debug("connected to telemetry server", "url", body.TelemetryServerURL) + } + + if body.ReflectionApiSpecVersion != internal.GENKIT_REFLECTION_API_SPEC_VERSION { + slog.Error("Genkit CLI version is not compatible with runtime library. Please use `genkit-cli` version compatible with runtime library version.") + } + + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("OK")) + return err + } +} + +// handleListActions lists all the registered actions. +func handleListActions(reg *registry.Registry) func(w http.ResponseWriter, r *http.Request) error { + return func(w http.ResponseWriter, r *http.Request) error { + descs := reg.ListActions() + descMap := map[string]action.Desc{} + for _, d := range descs { + descMap[d.Key] = d + } + return writeJSON(r.Context(), w, descMap) + } +} + +// TODO: Pull these from common types in genkit-tools. + +type runActionResponse struct { + Result json.RawMessage `json:"result"` + Telemetry telemetry `json:"telemetry"` +} + +type telemetry struct { + TraceID string `json:"traceId"` +} + +func runAction(ctx context.Context, reg *registry.Registry, key string, input json.RawMessage, cb streamingCallback[json.RawMessage], runtimeContext map[string]any) (*runActionResponse, error) { + action := reg.LookupAction(key) + if action == nil { + return nil, &base.HTTPError{Code: http.StatusNotFound, Err: fmt.Errorf("no action with key %q", key)} + } + if runtimeContext != nil { + ctx = core.WithActionContext(ctx, runtimeContext) + } + + var traceID string + output, err := tracing.RunInNewSpan(ctx, reg.TracingState(), "dev-run-action-wrapper", "", true, input, func(ctx context.Context, input json.RawMessage) (json.RawMessage, error) { + tracing.SetCustomMetadataAttr(ctx, "genkit-dev-internal", "true") + traceID = trace.SpanContextFromContext(ctx).TraceID().String() + return action.RunJSON(ctx, input, cb) + }) + if err != nil { + return nil, err + } + + return &runActionResponse{ + Result: output, + Telemetry: telemetry{TraceID: traceID}, + }, nil +} + +// writeJSON writes a JSON-marshaled value to the response writer. +func writeJSON(ctx context.Context, w http.ResponseWriter, value any) error { + data, err := json.Marshal(value) + if err != nil { + return err + } + _, err = w.Write(data) + if err != nil { + logger.FromContext(ctx).Error("writing output", "err", err) + } + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + return nil +} diff --git a/go/genkit/reflection_test.go b/go/genkit/reflection_test.go new file mode 100644 index 0000000000..de4d744c17 --- /dev/null +++ b/go/genkit/reflection_test.go @@ -0,0 +1,255 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: Apache-2.0 + +package genkit + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/firebase/genkit/go/core" + "github.com/firebase/genkit/go/core/tracing" + "github.com/firebase/genkit/go/internal/action" + "github.com/firebase/genkit/go/internal/atype" + "github.com/firebase/genkit/go/internal/registry" +) + +func inc(_ context.Context, x int) (int, error) { + return x + 1, nil +} + +func dec(_ context.Context, x int) (int, error) { + return x - 1, nil +} + +func TestReflectionServer(t *testing.T) { + t.Run("server startup and shutdown", func(t *testing.T) { + r, err := registry.New() + if err != nil { + t.Fatal(err) + } + tc := tracing.NewTestOnlyTelemetryClient() + r.TracingState().WriteTelemetryImmediate(tc) + + errCh := make(chan error, 1) + serverStartCh := make(chan struct{}) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + srv := startReflectionServer(ctx, r, errCh, serverStartCh) + if srv == nil { + t.Fatal("failed to start reflection server") + } + + select { + case err := <-errCh: + t.Fatalf("server failed to start: %v", err) + case <-serverStartCh: + // Server started successfully + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for server to start") + } + + if _, err := os.Stat(srv.RuntimeFilePath); err != nil { + t.Errorf("runtime file not created: %v", err) + } + + cancel() + time.Sleep(100 * time.Millisecond) + + if _, err := os.Stat(srv.RuntimeFilePath); !os.IsNotExist(err) { + t.Error("runtime file was not cleaned up") + } + }) +} + +func TestServeMux(t *testing.T) { + r, err := registry.New() + if err != nil { + t.Fatal(err) + } + tc := tracing.NewTestOnlyTelemetryClient() + r.TracingState().WriteTelemetryImmediate(tc) + + core.DefineAction(r, "test", "inc", "custom", nil, inc) + core.DefineAction(r, "test", "dec", "custom", nil, dec) + + ts := httptest.NewServer(serveMux(r)) + defer ts.Close() + + t.Parallel() + + t.Run("health check", func(t *testing.T) { + res, err := http.Get(ts.URL + "/api/__health") + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + t.Errorf("health check failed: got status %d, want %d", res.StatusCode, http.StatusOK) + } + }) + + t.Run("list actions", func(t *testing.T) { + res, err := http.Get(ts.URL + "/api/actions") + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + + var actions map[string]action.Desc + if err := json.NewDecoder(res.Body).Decode(&actions); err != nil { + t.Fatal(err) + } + + expectedKeys := []string{"/custom/test/inc", "/custom/test/dec"} + for _, key := range expectedKeys { + if _, ok := actions[key]; !ok { + t.Errorf("action %q not found in response", key) + } + } + }) + + t.Run("run action", func(t *testing.T) { + tests := []struct { + name string + body string + wantStatus int + wantResult string + }{ + { + name: "valid increment", + body: `{"key": "/custom/test/inc", "input": 3}`, + wantStatus: http.StatusOK, + wantResult: "4", + }, + { + name: "valid decrement", + body: `{"key": "/custom/test/dec", "input": 3}`, + wantStatus: http.StatusOK, + wantResult: "2", + }, + { + name: "invalid action key", + body: `{"key": "/custom/test/invalid", "input": 3}`, + wantStatus: http.StatusNotFound, + }, + { + name: "invalid input type", + body: `{"key": "/custom/test/inc", "input": "not a number"}`, + wantStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := http.Post(ts.URL+"/api/runAction", "application/json", strings.NewReader(tt.body)) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + + if res.StatusCode != tt.wantStatus { + t.Errorf("got status %d, want %d", res.StatusCode, tt.wantStatus) + return + } + + if tt.wantResult != "" { + var resp runActionResponse + if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { + t.Fatal(err) + } + if g := string(resp.Result); g != tt.wantResult { + t.Errorf("got result %q, want %q", g, tt.wantResult) + } + if resp.Telemetry.TraceID == "" { + t.Error("expected non-empty trace ID") + } + } + }) + } + }) + + t.Run("streaming action", func(t *testing.T) { + streamingInc := func(_ context.Context, x int, cb streamingCallback[json.RawMessage]) (int, error) { + for i := 0; i < x; i++ { + msg, _ := json.Marshal(i) + if err := cb(context.Background(), msg); err != nil { + return 0, err + } + } + return x, nil + } + core.DefineStreamingAction(r, "test", "streaming", atype.Custom, nil, streamingInc) + + body := `{"key": "/custom/test/streaming", "input": 3}` + req, err := http.NewRequest("POST", ts.URL+"/api/runAction?stream=true", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + + scanner := bufio.NewScanner(res.Body) + + for i := 0; i < 3; i++ { + if !scanner.Scan() { + t.Fatalf("expected streaming chunk %d", i) + } + got := scanner.Text() + want := fmt.Sprintf("%d", i) + if got != want { + t.Errorf("chunk %d: got %q, want %q", i, got, want) + } + } + + if !scanner.Scan() { + t.Fatal("expected final response") + } + var resp runActionResponse + if err := json.Unmarshal([]byte(scanner.Text()), &resp); err != nil { + t.Fatal(err) + } + if g := string(resp.Result); g != "3" { + t.Errorf("got final result %q, want %q", g, "3") + } + if resp.Telemetry.TraceID == "" { + t.Error("expected non-empty trace ID") + } + + if scanner.Scan() { + t.Errorf("unexpected additional data: %q", scanner.Text()) + } + if err := scanner.Err(); err != nil { + t.Errorf("scanner error: %v", err) + } + }) + + t.Run("notify endpoint", func(t *testing.T) { + body := `{ + "telemetryServerURL": "http://localhost:9999", + "reflectionApiSpecVersion": 1 + }` + res, err := http.Post(ts.URL+"/api/notify", "application/json", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Errorf("got status %d, want %d", res.StatusCode, http.StatusOK) + } + }) +} diff --git a/go/genkit/servers.go b/go/genkit/servers.go index c094654ce0..3285293b75 100644 --- a/go/genkit/servers.go +++ b/go/genkit/servers.go @@ -1,15 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - -// This file implements production and development servers. -// -// The genkit CLI sends requests to the development server. -// See js/common/src/reflectionApi.ts. -// -// The production server has a route for each flow. It -// is intended for production deployments. - package genkit import ( @@ -19,381 +10,97 @@ import ( "fmt" "log/slog" "net/http" - "os" - "path/filepath" "strconv" - "sync" + "strings" "sync/atomic" - "time" "github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/core/logger" - "github.com/firebase/genkit/go/core/tracing" - "github.com/firebase/genkit/go/internal" - "github.com/firebase/genkit/go/internal/action" "github.com/firebase/genkit/go/internal/base" - "github.com/firebase/genkit/go/internal/registry" - "go.opentelemetry.io/otel/trace" ) -type runtimeFileData struct { - ID string `json:"id"` - PID int `json:"pid"` - ReflectionServerURL string `json:"reflectionServerUrl"` - Timestamp string `json:"timestamp"` - GenkitVersion string `json:"genkitVersion"` - ReflectionApiSpecVersion int `json:"reflectionApiSpecVersion"` -} +type HandlerOption = func(params *handlerParams) -type devServer struct { - reg *registry.Registry - runtimeFilePath string +type handlerParams struct { + ContextProviders []core.ContextProvider } -// startReflectionServer starts the Reflection API server listening at the -// value of the environment variable GENKIT_REFLECTION_PORT for the port, -// or ":3100" if it is empty. -func startReflectionServer(ctx context.Context, r *registry.Registry, errCh chan<- error) *http.Server { - slog.Debug("starting reflection server") - addr := serverAddress("", "GENKIT_REFLECTION_PORT", "127.0.0.1:3100") - s := &devServer{reg: r} - if err := s.writeRuntimeFile(addr); err != nil { - slog.Error("failed to write runtime file", "error", err) - } - mux := newDevServeMux(s) - server := startServer(addr, mux, errCh) - go func() { - <-ctx.Done() - if err := s.cleanupRuntimeFile(); err != nil { - slog.Error("failed to cleanup runtime file", "error", err) - } - }() - return server -} - -// writeRuntimeFile writes a file describing the runtime to the project root. -func (s *devServer) writeRuntimeFile(url string) error { - projectRoot, err := findProjectRoot() - if err != nil { - return fmt.Errorf("failed to find project root: %w", err) - } - runtimesDir := filepath.Join(projectRoot, ".genkit", "runtimes") - if err := os.MkdirAll(runtimesDir, 0755); err != nil { - return fmt.Errorf("failed to create runtimes directory: %w", err) - } - runtimeID := os.Getenv("GENKIT_RUNTIME_ID") - if runtimeID == "" { - runtimeID = strconv.Itoa(os.Getpid()) - } - timestamp := time.Now().UTC().Format(time.RFC3339) - s.runtimeFilePath = filepath.Join(runtimesDir, fmt.Sprintf("%d-%s.json", os.Getpid(), timestamp)) - data := runtimeFileData{ - ID: runtimeID, - PID: os.Getpid(), - ReflectionServerURL: fmt.Sprintf("http://%s", url), - Timestamp: timestamp, - GenkitVersion: "go/" + internal.Version, - ReflectionApiSpecVersion: internal.GENKIT_REFLECTION_API_SPEC_VERSION, - } - fileContent, err := json.MarshalIndent(data, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal runtime data: %w", err) - } - if err := os.WriteFile(s.runtimeFilePath, fileContent, 0644); err != nil { - return fmt.Errorf("failed to write runtime file: %w", err) - } - slog.Debug("runtime file written", "path", s.runtimeFilePath) - return nil -} - -// cleanupRuntimeFile removes the runtime file associated with the dev server. -func (s *devServer) cleanupRuntimeFile() error { - if s.runtimeFilePath == "" { - return nil - } - content, err := os.ReadFile(s.runtimeFilePath) - if err != nil { - return fmt.Errorf("failed to read runtime file: %w", err) - } - var data runtimeFileData - if err := json.Unmarshal(content, &data); err != nil { - return fmt.Errorf("failed to unmarshal runtime data: %w", err) - } - if data.PID == os.Getpid() { - if err := os.Remove(s.runtimeFilePath); err != nil { - return fmt.Errorf("failed to remove runtime file: %w", err) - } - slog.Debug("runtime file cleaned up", "path", s.runtimeFilePath) - } - return nil -} +// requestID is a unique ID for each request. +var requestID atomic.Int64 -// findProjectRoot finds the project root by looking for a go.mod file. -func findProjectRoot() (string, error) { - dir, err := os.Getwd() - if err != nil { - return "", err - } - for { - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { - return dir, nil - } - parent := filepath.Dir(dir) - if parent == dir { - return "", fmt.Errorf("could not find project root (go.mod not found)") +// WithContextProviders adds providers for action context that may be used during runtime. +// They are called in the order added and may overwrite previous context. +func WithContextProviders(ctxProviders ...core.ContextProvider) HandlerOption { + return func(params *handlerParams) { + if params.ContextProviders != nil { + panic("genkit.WithContextProviders: cannot set ContextProviders more than once") } - dir = parent + params.ContextProviders = ctxProviders } } -// startFlowServer starts a production server listening at the given address. -// The Server has a route for each defined flow. -// If addr is "", it uses the value of the environment variable PORT -// for the port, and if that is empty it uses ":3400". -// -// To construct a server with additional routes, use [NewFlowServeMux]. -func startFlowServer(g *Genkit, addr string, flows []string, errCh chan<- error) *http.Server { - slog.Debug("starting flow server") - addr = serverAddress(addr, "PORT", "127.0.0.1:3400") - mux := NewFlowServeMux(g, flows) - return startServer(addr, mux, errCh) -} - -// flow is the type that all Flow[In, Out, Stream] have in common. -type flow interface { - Name() string - - // runJSON uses encoding/json to unmarshal the input, - // calls Flow.start, then returns the marshaled result. - runJSON(ctx context.Context, authHeader string, input json.RawMessage, cb streamingCallback[json.RawMessage]) (json.RawMessage, error) -} - -// startServer starts an HTTP server listening on the address. -// It returns the server an -func startServer(addr string, handler http.Handler, errCh chan<- error) *http.Server { - server := &http.Server{ - Addr: addr, - Handler: handler, +// Handler returns an HTTP handler function that serves the action with the provided options. +func Handler(a Action, opts ...HandlerOption) http.HandlerFunc { + params := &handlerParams{} + for _, opt := range opts { + opt(params) } - - go func() { - slog.Debug("server listening", "addr", addr) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errCh <- fmt.Errorf("server error on %s: %w", addr, err) - } - }() - - return server + return wrapHandler(handler(a, params)) } -// shutdownServers initiates shutdown of the servers and waits for the shutdown to complete. -// After 5 seconds, it will timeout. -func shutdownServers(servers []*http.Server) error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() +// wrapHandler wraps an HTTP handler function with common logging and error handling. +func wrapHandler(h func(http.ResponseWriter, *http.Request) error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log := slog.Default().With("reqID", requestID.Add(1)) + log.Debug("request start", "method", r.Method, "path", r.URL.Path) - var wg sync.WaitGroup - for _, server := range servers { - wg.Add(1) - go func(srv *http.Server) { - defer wg.Done() - if err := srv.Shutdown(ctx); err != nil { - slog.Error("server shutdown failed", "addr", srv.Addr, "err", err) + var err error + defer func() { + if err != nil { + log.Error("request end", "err", err) } else { - slog.Debug("server shutdown successfully", "addr", srv.Addr) + log.Debug("request end") } - }(server) - } - - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() - - select { - case <-done: - slog.Info("all servers shut down successfully") - case <-ctx.Done(): - return errors.New("server shutdown timed out") - } - - return nil -} - -func newDevServeMux(s *devServer) *http.ServeMux { - mux := http.NewServeMux() - handle(mux, "GET /api/__health", func(w http.ResponseWriter, _ *http.Request) error { - return nil - }) - handle(mux, "POST /api/runAction", s.handleRunAction) - handle(mux, "GET /api/actions", s.handleListActions) - handle(mux, "POST /api/notify", s.handleNotify) - return mux -} + }() -// handleRunAction looks up an action by name in the registry, runs it with the -// provided JSON input, and writes back the JSON-marshaled request. -func (s *devServer) handleRunAction(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - var body struct { - Key string `json:"key"` - Input json.RawMessage `json:"input"` - Context json.RawMessage `json:"context"` - } - defer r.Body.Close() - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &base.HTTPError{Code: http.StatusBadRequest, Err: err} - } - stream, err := parseBoolQueryParam(r, "stream") - if err != nil { - return err - } - logger.FromContext(ctx).Debug("running action", - "key", body.Key, - "stream", stream) - var callback streamingCallback[json.RawMessage] - if stream { - w.Header().Set("Content-Type", "text/plain") - w.Header().Set("Transfer-Encoding", "chunked") - // Stream results are newline-separated JSON. - callback = func(ctx context.Context, msg json.RawMessage) error { - _, err := fmt.Fprintf(w, "%s\n", msg) - if err != nil { - return err - } - if f, ok := w.(http.Flusher); ok { - f.Flush() + if err = h(w, r); err != nil { + var herr *base.HTTPError + if errors.As(err, &herr) { + http.Error(w, herr.Error(), herr.Code) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) } - return nil } } - var contextMap map[string]any = nil - if body.Context != nil { - json.Unmarshal(body.Context, &contextMap) - } - resp, err := runAction(ctx, s.reg, body.Key, body.Input, callback, contextMap) - if err != nil { - return err - } - return writeJSON(ctx, w, resp) -} - -// handleNotify configures the telemetry server URL from the request. -func (s *devServer) handleNotify(w http.ResponseWriter, r *http.Request) error { - var body struct { - TelemetryServerURL string `json:"telemetryServerUrl"` - ReflectionApiSpecVersion int `json:"reflectionApiSpecVersion"` - } - defer r.Body.Close() - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &base.HTTPError{Code: http.StatusBadRequest, Err: err} - } - if body.TelemetryServerURL != "" { - s.reg.TracingState().WriteTelemetryImmediate(tracing.NewHTTPTelemetryClient(body.TelemetryServerURL)) - slog.Debug("connected to telemetry server", "url", body.TelemetryServerURL) - } - if body.ReflectionApiSpecVersion != internal.GENKIT_REFLECTION_API_SPEC_VERSION { - slog.Error("Genkit CLI version is not compatible with runtime library. Please use `genkit-cli` version compatible with runtime library version.") - } - w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte("OK")) - return err -} - -type runActionResponse struct { - Result json.RawMessage `json:"result"` - Telemetry telemetry `json:"telemetry"` -} - -type telemetry struct { - TraceID string `json:"traceId"` -} - -func runAction(ctx context.Context, reg *registry.Registry, key string, input json.RawMessage, cb streamingCallback[json.RawMessage], runtimeContext map[string]any) (*runActionResponse, error) { - action := reg.LookupAction(key) - if action == nil { - return nil, &base.HTTPError{Code: http.StatusNotFound, Err: fmt.Errorf("no action with key %q", key)} - } - if runtimeContext != nil { - ctx = core.WithActionContext(ctx, runtimeContext) - } - - var traceID string - output, err := tracing.RunInNewSpan(ctx, reg.TracingState(), "dev-run-action-wrapper", "", true, input, func(ctx context.Context, input json.RawMessage) (json.RawMessage, error) { - tracing.SetCustomMetadataAttr(ctx, "genkit-dev-internal", "true") - traceID = trace.SpanContextFromContext(ctx).TraceID().String() - return action.RunJSON(ctx, input, cb) - }) - if err != nil { - return nil, err - } - return &runActionResponse{ - Result: output, - Telemetry: telemetry{TraceID: traceID}, - }, nil -} - -// handleListActions lists all the registered actions. -func (s *devServer) handleListActions(w http.ResponseWriter, r *http.Request) error { - descs := s.reg.ListActions() - descMap := map[string]action.Desc{} - for _, d := range descs { - descMap[d.Key] = d - } - return writeJSON(r.Context(), w, descMap) -} - -// NewFlowServeMux constructs a [net/http.ServeMux]. -// If flows is non-empty, the each of the named flows is registered as a route. -// Otherwise, all defined flows are registered. -// -// All routes take a single query parameter, "stream", which if true will stream the -// flow's results back to the client. (Not all flows support streaming, however.) -// -// To use the returned ServeMux as part of a server with other routes, either add routes -// to it, or install it as part of another ServeMux, like so: -// -// mainMux := http.NewServeMux() -// mainMux.Handle("POST /flow/", http.StripPrefix("/flow/", NewFlowServeMux())) -func NewFlowServeMux(g *Genkit, flows []string) *http.ServeMux { - return newFlowServeMux(g.reg, flows) } -func newFlowServeMux(r *registry.Registry, flows []string) *http.ServeMux { - mux := http.NewServeMux() - m := map[string]bool{} - for _, f := range flows { - m[f] = true - } - for _, f := range r.ListFlows() { - f := f.(flow) - if len(flows) == 0 || m[f.Name()] { - handle(mux, "POST /"+f.Name(), nonDurableFlowHandler(f)) +// handler returns an HTTP handler function that serves the action with the provided params. Responses are written in server-sent events (SSE) format. +func handler(a Action, params *handlerParams) func(http.ResponseWriter, *http.Request) error { + return func(w http.ResponseWriter, r *http.Request) error { + if a == nil { + return errors.New("action is nil; cannot serve") } - } - return mux -} -func nonDurableFlowHandler(f flow) func(http.ResponseWriter, *http.Request) error { - return func(w http.ResponseWriter, r *http.Request) error { var body struct { Data json.RawMessage `json:"data"` } - defer r.Body.Close() - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &base.HTTPError{Code: http.StatusBadRequest, Err: err} + if r.Body != nil && r.ContentLength > 0 { + defer r.Body.Close() + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + return &base.HTTPError{Code: http.StatusBadRequest, Err: err} + } } + stream, err := parseBoolQueryParam(r, "stream") if err != nil { return err } + stream = stream || r.Header.Get("Accept") == "text/event-stream" + var callback streamingCallback[json.RawMessage] - if r.Header.Get("Accept") == "text/event-stream" || stream { + if stream { w.Header().Set("Content-Type", "text/plain") w.Header().Set("Transfer-Encoding", "chunked") - // Event Stream results are in JSON format separated by two newline escape sequences - // including the `data` and `message` labels callback = func(ctx context.Context, msg json.RawMessage) error { _, err := fmt.Fprintf(w, "data: {\"message\": %s}\n\n", msg) if err != nil { @@ -405,77 +112,53 @@ func nonDurableFlowHandler(f flow) func(http.ResponseWriter, *http.Request) erro return nil } } - // TODO: telemetry - out, err := f.runJSON(r.Context(), r.Header.Get("Authorization"), body.Data, callback) + + ctx := r.Context() + if params.ContextProviders != nil { + for _, ctxProvider := range params.ContextProviders { + headers := make(map[string]string, len(r.Header)) + for k, v := range r.Header { + headers[strings.ToLower(k)] = strings.Join(v, " ") + } + + actionCtx, err := ctxProvider(ctx, core.RequestData{ + Method: r.Method, + Headers: headers, + Input: body.Data, + }) + if err != nil { + logger.FromContext(ctx).Error("error providing action context from request", "err", err) + return &base.HTTPError{Code: http.StatusUnauthorized, Err: err} + } + + if existing := core.FromContext(ctx); existing != nil { + for k, v := range actionCtx { + existing[k] = v + } + actionCtx = existing + } + ctx = core.WithActionContext(ctx, actionCtx) + } + } + + out, err := a.RunJSON(ctx, body.Data, callback) if err != nil { - if r.Header.Get("Accept") == "text/event-stream" || stream { + if stream { _, err = fmt.Fprintf(w, "data: {\"error\": {\"status\": \"INTERNAL\", \"message\": \"stream flow error\", \"details\": \"%v\"}}\n\n", err) return err } return err } - // Responses for streaming, non-durable flows should be prefixed - // with "data" - if r.Header.Get("Accept") == "text/event-stream" || stream { + if stream { _, err = fmt.Fprintf(w, "data: {\"result\": %s}\n\n", out) return err } - // Responses for non-streaming, non-durable flows are passed back - // with the flow result stored in a field called "result." _, err = fmt.Fprintf(w, `{"result": %s}\n`, out) return err } } -// serverAddress determines a server address. -func serverAddress(arg, envVar, defaultValue string) string { - if arg != "" { - return arg - } - if port := os.Getenv(envVar); port != "" { - return "127.0.0.1:" + port - } - return defaultValue -} - -// requestID is a unique ID for each request. -var requestID atomic.Int64 - -// handle registers pattern on mux with an http.Handler that calls f. -// If f returns a non-nil error, the handler calls http.Error. -// If the error is an httpError, the code it contains is used as the status code; -// otherwise a 500 status is used. -func handle(mux *http.ServeMux, pattern string, f func(w http.ResponseWriter, r *http.Request) error) { - mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { - id := requestID.Add(1) - // Create a logger that always outputs the requestID, and store it in the request context. - log := slog.Default().With("reqID", id) - log.Info("request start", - "method", r.Method, - "path", r.URL.Path) - var err error - defer func() { - if err != nil { - log.Error("request end", "err", err) - } else { - log.Info("request end") - } - }() - err = f(w, r) - if err != nil { - // If the error is an httpError, serve the status code it contains. - // Otherwise, assume this is an unexpected error and serve a 500. - var herr *base.HTTPError - if errors.As(err, &herr) { - http.Error(w, herr.Error(), herr.Code) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } - }) -} - func parseBoolQueryParam(r *http.Request, name string) (bool, error) { b := false if s := r.FormValue(name); s != "" { @@ -487,18 +170,3 @@ func parseBoolQueryParam(r *http.Request, name string) (bool, error) { } return b, nil } - -func writeJSON(ctx context.Context, w http.ResponseWriter, value any) error { - data, err := json.Marshal(value) - if err != nil { - return err - } - _, err = w.Write(data) - if err != nil { - logger.FromContext(ctx).Error("writing output", "err", err) - } - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - return nil -} diff --git a/go/genkit/servers_test.go b/go/genkit/servers_test.go index 6ee102013d..cab5ca36a0 100644 --- a/go/genkit/servers_test.go +++ b/go/genkit/servers_test.go @@ -1,13 +1,12 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package genkit import ( - "bytes" "context" - "encoding/json" + "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -15,209 +14,246 @@ import ( "testing" "github.com/firebase/genkit/go/core" - "github.com/firebase/genkit/go/core/tracing" - "github.com/firebase/genkit/go/internal/action" - "github.com/firebase/genkit/go/internal/atype" - "github.com/firebase/genkit/go/internal/registry" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/invopop/jsonschema" ) -func inc(_ context.Context, x int) (int, error) { - return x + 1, nil -} - -func dec(_ context.Context, x int) (int, error) { - return x - 1, nil +func FakeContextProvider(ctx context.Context, req core.RequestData) (core.ActionContext, error) { + return core.ActionContext{ + "test": "action-context-value", + }, nil } -func TestDevServer(t *testing.T) { - r, err := registry.New() +func TestHandler(t *testing.T) { + g, err := Init(context.Background()) if err != nil { - t.Fatal(err) + t.Fatalf("failed to initialize Genkit: %v", err) } - tc := tracing.NewTestOnlyTelemetryClient() - r.TracingState().WriteTelemetryImmediate(tc) - - core.DefineAction(r, "devServer", "inc", atype.Custom, map[string]any{ - "foo": "bar", - }, inc) - core.DefineAction(r, "devServer", "dec", atype.Custom, map[string]any{ - "bar": "baz", - }, dec) - srv := httptest.NewServer(newDevServeMux(&devServer{reg: r})) - defer srv.Close() - - t.Run("runAction", func(t *testing.T) { - body := `{"key": "/custom/devServer/inc", "input": 3}` - res, err := http.Post(srv.URL+"/api/runAction", "application/json", strings.NewReader(body)) - if err != nil { - t.Fatal(err) - } - defer res.Body.Close() - if res.StatusCode != 200 { - t.Fatalf("got status %d, wanted 200", res.StatusCode) - } - got, err := readJSON[runActionResponse](res.Body) - if err != nil { - t.Fatal(err) - } - if g, w := string(got.Result), "4"; g != w { - t.Errorf("got %q, want %q", g, w) - } - tid := got.Telemetry.TraceID - if len(tid) != 32 { - t.Errorf("trace ID is %q, wanted 32-byte string", tid) - } - checkActionTrace(t, tc, tid, "inc") + + echoFlow := DefineFlow(g, "echo", func(ctx context.Context, input string) (string, error) { + return input, nil }) - t.Run("list actions", func(t *testing.T) { - res, err := http.Get(srv.URL + "/api/actions") - if err != nil { - t.Fatal(err) - } - defer res.Body.Close() - if res.StatusCode != 200 { - t.Fatalf("got status %d, wanted 200", res.StatusCode) - } - got, err := readJSON[map[string]action.Desc](res.Body) - if err != nil { - t.Fatal(err) - } - want := map[string]action.Desc{ - "/custom/devServer/inc": { - Key: "/custom/devServer/inc", - Name: "devServer/inc", - InputSchema: &jsonschema.Schema{Type: "integer"}, - OutputSchema: &jsonschema.Schema{Type: "integer"}, - Metadata: map[string]any{"foo": "bar"}, - }, - "/custom/devServer/dec": { - Key: "/custom/devServer/dec", - InputSchema: &jsonschema.Schema{Type: "integer"}, - OutputSchema: &jsonschema.Schema{Type: "integer"}, - Name: "devServer/dec", - Metadata: map[string]any{"bar": "baz"}, - }, + + errorFlow := DefineFlow(g, "error", func(ctx context.Context, input string) (string, error) { + return "", errors.New("flow error") + }) + + contextReaderFlow := DefineFlow(g, "contextReader", func(ctx context.Context, input []string) (string, error) { + actionCtx := core.FromContext(ctx) + if actionCtx == nil { + return "", errors.New("no action context") } - diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(jsonschema.Schema{})) - if diff != "" { - t.Errorf("mismatch (-want, +got):\n%s", diff) + + if len(input) == 0 { + return "", nil } - }) -} -func TestProdServer(t *testing.T) { - r, err := registry.New() - if err != nil { - t.Fatal(err) - } - tc := tracing.NewTestOnlyTelemetryClient() - r.TracingState().WriteTelemetryImmediate(tc) + var values []string + for _, key := range input { + value, ok := actionCtx[key] + if !ok { + return "", fmt.Errorf("action context key %q not found", key) + } - defineFlow(r, "inc", func(_ context.Context, i int, _ noStream) (int, error) { - return i + 1, nil + strValue, ok := value.(string) + if !ok { + return "", fmt.Errorf("action context value for key %q is not a string", key) + } + + values = append(values, strValue) + } + + return strings.Join(values, ","), nil }) - srv := httptest.NewServer(newFlowServeMux(r, nil)) - defer srv.Close() - check := func(t *testing.T, input string, wantStatus, wantResult int) { - type body struct { - Data json.RawMessage `json:"data"` + t.Run("basic handler", func(t *testing.T) { + handler := Handler(echoFlow) + + req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":"test-input"}`)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Errorf("want status code %d, got %d", http.StatusOK, resp.StatusCode) } - payload := body{ - Data: json.RawMessage([]byte(input)), + + if !strings.Contains(string(body), `"test-input"`) { + t.Errorf("want response to contain test-input, got %q", string(body)) } - jsonPayload, err := json.Marshal(payload) - if err != nil { - t.Fatal(err) + }) + + t.Run("action error", func(t *testing.T) { + handler := Handler(errorFlow) + + req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":"test-input"}`)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusInternalServerError { + t.Errorf("want status code %d, got %d", http.StatusInternalServerError, resp.StatusCode) } - res, err := http.Post(srv.URL+"/inc", "application/json", bytes.NewBuffer(jsonPayload)) - if err != nil { - t.Fatal(err) + + expected := "flow error\n" + if string(body) != expected { + t.Errorf("want response to contain flow error, got %q", string(body)) } - defer res.Body.Close() - if g, w := res.StatusCode, wantStatus; g != w { - t.Fatalf("status: got %d, want %d", g, w) + }) + + t.Run("invalid JSON", func(t *testing.T) { + handler := Handler(echoFlow) + + req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":invalid-json}`)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("want status code %d, got %d", http.StatusBadRequest, resp.StatusCode) } - if res.StatusCode != 200 { - return + + if !strings.Contains(string(body), "invalid character") { + t.Errorf("want error about invalid JSON, got %q", string(body)) } - type resultType struct { - Result int + }) + + t.Run("with context provider", func(t *testing.T) { + handler := Handler(contextReaderFlow, WithContextProviders(FakeContextProvider)) + + req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":["test"]}`)) + w := httptest.NewRecorder() + + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Errorf("want status code %d, got %d", http.StatusOK, resp.StatusCode) } - got, err := readJSON[resultType](res.Body) - if err != nil { - t.Fatal(err) + + if !strings.Contains(string(body), "action-context-value") { + t.Errorf("want response to containaction-context-value, got %q", string(body)) } - if g, w := got.Result, wantResult; g != w { - t.Errorf("result: got %d, want %d", g, w) + }) + + t.Run("multiple context providers", func(t *testing.T) { + handler := Handler(contextReaderFlow, WithContextProviders( + func(ctx context.Context, req core.RequestData) (core.ActionContext, error) { + return core.ActionContext{"provider1": "value1"}, nil + }, + func(ctx context.Context, req core.RequestData) (core.ActionContext, error) { + return core.ActionContext{"provider2": "value2"}, nil + }, + )) + + req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":["provider1","provider2"]}`)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Errorf("want status code %d, got %d", http.StatusOK, resp.StatusCode) } - } - t.Run("ok", func(t *testing.T) { check(t, "2", 200, 3) }) - t.Run("bad", func(t *testing.T) { check(t, "true", 400, 0) }) + if !strings.Contains(string(body), "value1,value2") { + t.Errorf("want response to contain value1,value2, got %q", string(body)) + } + }) } -func checkActionTrace(t *testing.T, tc *tracing.TestOnlyTelemetryClient, tid, name string) { - td := tc.Traces[tid] - if td == nil { - t.Fatalf("trace %q not found", tid) - } - rootSpan := findRootSpan(t, td.Spans) - want := &tracing.SpanData{ - TraceID: tid, - DisplayName: "dev-run-action-wrapper", - SpanKind: "INTERNAL", - SameProcessAsParentSpan: tracing.BoolValue{Value: true}, - Status: tracing.Status{Code: 0}, - InstrumentationLibrary: tracing.InstrumentationLibrary{ - Name: "genkit-tracer", - Version: "v1", - }, - Attributes: map[string]any{ - "genkit:name": "dev-run-action-wrapper", - "genkit:input": "3", - "genkit:isRoot": true, - "genkit:path": "/dev-run-action-wrapper", - "genkit:output": "4", - "genkit:metadata:genkit-dev-internal": "true", - "genkit:state": "success", - }, - } - diff := cmp.Diff(want, rootSpan, cmpopts.IgnoreFields(tracing.SpanData{}, "SpanID", "StartTime", "EndTime")) - if diff != "" { - t.Errorf("mismatch (-want, +got):\n%s", diff) +func TestStreamingHandler(t *testing.T) { + g, err := Init(context.Background()) + if err != nil { + t.Fatalf("failed to initialize Genkit: %v", err) } -} -// findRootSpan finds the root span in spans. -// It also verifies that it is unique. -func findRootSpan(t *testing.T, spans map[string]*tracing.SpanData) *tracing.SpanData { - t.Helper() - var root *tracing.SpanData - for _, sd := range spans { - if sd.ParentSpanID == "" { - if root != nil { - t.Fatal("more than one root span") - } - if g, w := sd.Attributes["genkit:isRoot"], true; g != w { - t.Errorf("root span genkit:isRoot attr = %v, want %v", g, w) + streamingFlow := DefineStreamingFlow(g, "streaming", + func(ctx context.Context, input string, cb func(context.Context, string) error) (string, error) { + for _, c := range input { + if err := cb(ctx, string(c)); err != nil { + return "", err + } } - root = sd + return input + "-end", nil + }) + + errorStreamingFlow := DefineStreamingFlow(g, "errorStreaming", + func(ctx context.Context, input string, cb func(context.Context, string) error) (string, error) { + return "", errors.New("streaming error") + }) + + t.Run("streaming response", func(t *testing.T) { + handler := Handler(streamingFlow) + + req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":"hello"}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "text/event-stream") + w := httptest.NewRecorder() + + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Errorf("want status code %d, got %d", http.StatusOK, resp.StatusCode) } - } - if root == nil { - t.Fatal("no root span") - } - return root -} -func readJSON[T any](r io.Reader) (T, error) { - var x T - if err := json.NewDecoder(r).Decode(&x); err != nil { - return x, err - } - return x, nil + expected := `data: {"message": "h"} + +data: {"message": "e"} + +data: {"message": "l"} + +data: {"message": "l"} + +data: {"message": "o"} + +data: {"result": "hello-end"} + +` + if string(body) != expected { + t.Errorf("want streaming body:\n%q\n\nGot:\n%q", expected, string(body)) + } + }) + + t.Run("streaming error", func(t *testing.T) { + handler := Handler(errorStreamingFlow) + + req := httptest.NewRequest("POST", "/?stream=true", strings.NewReader(`{"data":"test"}`)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { // Note: SSE errors are sent as part of the stream + t.Errorf("want status code %d, got %d", http.StatusOK, resp.StatusCode) + } + + expected := `data: {"error": {"status": "INTERNAL", "message": "stream flow error", "details": "streaming error"}} + +` + if string(body) != expected { + t.Errorf("want error body:\n%q\n\nGot:\n%q", expected, string(body)) + } + }) } diff --git a/go/go.mod b/go/go.mod index 9e1fb4f0e1..41193141b5 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,6 +1,6 @@ module github.com/firebase/genkit/go -go 1.22.0 +go 1.24.0 retract ( v0.1.4 // Retraction only. @@ -9,7 +9,8 @@ retract ( require ( cloud.google.com/go/aiplatform v1.68.0 - cloud.google.com/go/logging v1.10.0 + cloud.google.com/go/firestore v1.16.0 + cloud.google.com/go/logging v1.11.0 cloud.google.com/go/vertexai v0.12.1-0.20240711230438-265963bd5b91 firebase.google.com/go/v4 v4.14.1 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.46.0 @@ -17,7 +18,6 @@ require ( github.com/aymerick/raymond v2.0.2+incompatible github.com/google/generative-ai-go v0.16.1-0.20240711222609-09946422abc6 github.com/google/go-cmp v0.6.0 - github.com/google/uuid v1.6.0 github.com/invopop/jsonschema v0.12.0 github.com/jba/slog v0.2.0 github.com/lib/pq v1.10.9 @@ -26,31 +26,30 @@ require ( github.com/weaviate/weaviate-go-client/v4 v4.15.0 github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/xeipuuv/gojsonschema v1.2.0 - go.opentelemetry.io/otel v1.26.0 - go.opentelemetry.io/otel/metric v1.26.0 - go.opentelemetry.io/otel/sdk v1.26.0 - go.opentelemetry.io/otel/sdk/metric v1.26.0 - go.opentelemetry.io/otel/trace v1.26.0 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/metric v1.29.0 + go.opentelemetry.io/otel/sdk v1.29.0 + go.opentelemetry.io/otel/sdk/metric v1.29.0 + go.opentelemetry.io/otel/trace v1.29.0 golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 golang.org/x/tools v0.23.0 - google.golang.org/api v0.188.0 + google.golang.org/api v0.196.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/ai v0.8.1-0.20240711230438-265963bd5b91 // indirect - cloud.google.com/go/auth v0.7.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.4.0 // indirect - cloud.google.com/go/firestore v1.15.0 // indirect - cloud.google.com/go/iam v1.1.10 // indirect - cloud.google.com/go/longrunning v0.5.9 // indirect - cloud.google.com/go/monitoring v1.20.1 // indirect - cloud.google.com/go/storage v1.41.0 // indirect - cloud.google.com/go/trace v1.10.9 // indirect + cloud.google.com/go/auth v0.9.3 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect + cloud.google.com/go/iam v1.2.0 // indirect + cloud.google.com/go/longrunning v0.6.0 // indirect + cloud.google.com/go/monitoring v1.21.0 // indirect + cloud.google.com/go/storage v1.43.0 // indirect + cloud.google.com/go/trace v1.11.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.46.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect @@ -59,7 +58,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.2 // indirect github.com/go-openapi/errors v0.22.0 // indirect @@ -73,9 +72,10 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -85,18 +85,18 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.6.0 // indirect google.golang.org/appengine/v2 v2.0.2 // indirect - google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect - google.golang.org/grpc v1.65.0 // indirect + google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.0 // indirect ) diff --git a/go/go.sum b/go/go.sum index fbd0eb794b..cc53089587 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,30 +1,30 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= +cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= cloud.google.com/go/ai v0.8.1-0.20240711230438-265963bd5b91 h1:VA80iXvWirtF1jQK5BQd7MPHvHOE+UZ2v4AJCcChHqk= cloud.google.com/go/ai v0.8.1-0.20240711230438-265963bd5b91/go.mod h1:rVgd6oDdCDlN3mYqXqgE2nnzUblrwM/khbqLUXOJLeM= cloud.google.com/go/aiplatform v1.68.0 h1:EPPqgHDJpBZKRvv+OsB3cr0jYz3EL2pZ+802rBPcG8U= cloud.google.com/go/aiplatform v1.68.0/go.mod h1:105MFA3svHjC3Oazl7yjXAmIR89LKhRAeNdnDKJczME= -cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts= -cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c= -cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M= -cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= -cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= -cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI= -cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps= -cloud.google.com/go/logging v1.10.0 h1:f+ZXMqyrSJ5vZ5pE/zr0xC8y/M9BLNzQeLBwfeZ+wY4= -cloud.google.com/go/logging v1.10.0/go.mod h1:EHOwcxlltJrYGqMGfghSet736KR3hX1MAj614mrMk9I= -cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k= -cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c= -cloud.google.com/go/monitoring v1.20.1 h1:XmM6uk4+mI2ZhWdI2n/2GNhJdpeQN+1VdG2UWEDhX48= -cloud.google.com/go/monitoring v1.20.1/go.mod h1:FYSe/brgfuaXiEzOQFhTjsEsJv+WePyK71X7Y8qo6uQ= -cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= -cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= -cloud.google.com/go/trace v1.10.9 h1:Cy6D1Zdz8up4mIPUWModTuIGDr3fh7AZaCnR+uyxpgA= -cloud.google.com/go/trace v1.10.9/go.mod h1:vtWRnvEh+d8h2xljwxVwsdxxpoWZkxcNYnJF3FuJUV8= +cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= +cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/firestore v1.16.0 h1:YwmDHcyrxVRErWcgxunzEaZxtNbc8QoFYA/JOEwDPgc= +cloud.google.com/go/firestore v1.16.0/go.mod h1:+22v/7p+WNBSQwdSwP57vz47aZiY+HrDkrOsJNhk7rg= +cloud.google.com/go/iam v1.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8= +cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q= +cloud.google.com/go/logging v1.11.0 h1:v3ktVzXMV7CwHq1MBF65wcqLMA7i+z3YxbUsoK7mOKs= +cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A= +cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI= +cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts= +cloud.google.com/go/monitoring v1.21.0 h1:EMc0tB+d3lUewT2NzKC/hr8cSR9WsUieVywzIHetGro= +cloud.google.com/go/monitoring v1.21.0/go.mod h1:tuJ+KNDdJbetSsbSGTqnaBvbauS5kr3Q/koy3Up6r+4= +cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= +cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= +cloud.google.com/go/trace v1.11.0 h1:UHX6cOJm45Zw/KIbqHe4kII8PupLt/V5tscZUkeiJVI= +cloud.google.com/go/trace v1.11.0/go.mod h1:Aiemdi52635dBR7o3zuc9lLjXo3BwGaChEjCa3tJNmM= cloud.google.com/go/vertexai v0.12.1-0.20240711230438-265963bd5b91 h1:JwSkFKQ/yI97gCjMMnaEOZAigRpN53yiH6gJzik/OYA= cloud.google.com/go/vertexai v0.12.1-0.20240711230438-265963bd5b91/go.mod h1:KrfEQtFq2gqyHt4kZ+k1kIo5oy9Jw90yEHxgPsyl1bw= entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE= @@ -71,8 +71,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU= @@ -164,16 +164,16 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= -github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/googleapis/enterprise-certificate-proxy v0.3.3 h1:QRje2j5GZimBzlbhGA2V2QlGNgL8G6e+wGo/+/2bWI0= +github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= @@ -298,27 +298,27 @@ go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= -go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= -go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= -go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= @@ -333,19 +333,19 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -359,8 +359,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -368,10 +368,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -384,10 +384,8 @@ golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw= -google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= +google.golang.org/api v0.196.0 h1:k/RafYqebaIJBO3+SMnfEGtFVlvp5vSgqTUF54UN/zg= +google.golang.org/api v0.196.0/go.mod h1:g9IL21uGkYgvQ5BZg6BAtoGJQIm8r6EgaAbpNey5wBE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= @@ -395,19 +393,19 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b h1:dSTjko30weBaMj3eERKc0ZVXW4GudCswM3m+P++ukU0= -google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/go/internal/action/action.go b/go/internal/action/action.go index 0600a4a9d1..e72fbd2790 100644 --- a/go/internal/action/action.go +++ b/go/internal/action/action.go @@ -1,18 +1,16 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package action import ( "context" "encoding/json" - "github.com/firebase/genkit/go/core/tracing" "github.com/invopop/jsonschema" ) -// Action is the type that all Action[I, O, S] have in common. +// Action is the type that all Action[I, O, S] have in common. Internal version. type Action interface { Name() string @@ -24,9 +22,6 @@ type Action interface { // It should set all fields of actionDesc except Key, which // the registry will set. Desc() Desc - - // SetTracingState set's the action's tracing.State. - SetTracingState(*tracing.State) } // A Desc is a description of an Action. diff --git a/go/internal/base/misc.go b/go/internal/base/misc.go index ef2290791f..837215ed97 100644 --- a/go/internal/base/misc.go +++ b/go/internal/base/misc.go @@ -1,11 +1,9 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package base import ( - "encoding/json" "fmt" "net/http" "net/url" @@ -39,11 +37,3 @@ type HTTPError struct { func (e *HTTPError) Error() string { return fmt.Sprintf("%s: %s", http.StatusText(e.Code), e.Err) } - -// FlowStater is the common type of all flowState[I, O] types. -type FlowStater interface { - IsFlowState() - ToJSON() ([]byte, error) - CacheAt(key string) json.RawMessage - CacheSet(key string, val json.RawMessage) -} diff --git a/go/internal/doc-snippets/dotprompt.go b/go/internal/doc-snippets/dotprompt.go index c374e5931e..1da11aaf5e 100644 --- a/go/internal/doc-snippets/dotprompt.go +++ b/go/internal/doc-snippets/dotprompt.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -18,10 +17,9 @@ import ( ) func dot01() error { + ctx := context.Background() // [START dot01_1] - g, err := genkit.New(&genkit.Options{ - PromptDir: "prompts", - }) + g, err := genkit.Init(ctx, genkit.WithPromptDir("prompts")) if err != nil { log.Fatal(err) } @@ -29,7 +27,7 @@ func dot01() error { // [END dot01_1] // [START dot01_2] - ctx := context.Background() + ctx = context.Background() // Default to the project in GCLOUD_PROJECT and the location "us-central1". vertexai.Init(ctx, g, nil) @@ -78,7 +76,8 @@ func dot01() error { } func dot02() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -116,9 +115,8 @@ func dot02() { func dot03() error { // [START dot03] - g, err := genkit.New(&genkit.Options{ - PromptDir: "prompts", - }) + ctx := context.Background() + g, err := genkit.Init(ctx, genkit.WithPromptDir("prompts")) if err != nil { log.Fatal(err) } @@ -151,7 +149,8 @@ func dot03() error { } func dot04() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -164,9 +163,8 @@ func dot04() { } func dot05() { - g, err := genkit.New(&genkit.Options{ - PromptDir: "prompts", - }) + ctx := context.Background() + g, err := genkit.Init(ctx, genkit.WithPromptDir("prompts")) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/flows.go b/go/internal/doc-snippets/flows.go index 98e32e4692..4ed37f044e 100644 --- a/go/internal/doc-snippets/flows.go +++ b/go/internal/doc-snippets/flows.go @@ -1,23 +1,22 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( "context" - "errors" "fmt" "log" "net/http" "strings" + "github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/firebase" ) func f1() { - g, _ := genkit.New(nil) + ctx := context.Background() + g, _ := genkit.Init(ctx) // [START flow1] menuSuggestionFlow := genkit.DefineFlow( @@ -44,7 +43,8 @@ type MenuSuggestion struct { func makeMenuItemSuggestion(string) string { return "" } func f2() { - g, _ := genkit.New(nil) + ctx := context.Background() + g, _ := genkit.Init(ctx) // [START flow2] menuSuggestionFlow := genkit.DefineFlow( @@ -72,7 +72,8 @@ type StreamType string // [END streaming-types] func f3() { - g, _ := genkit.New(nil) + ctx := context.Background() + g, _ := genkit.Init(ctx) // [START streaming] menuSuggestionFlow := genkit.DefineStreamingFlow( @@ -105,7 +106,7 @@ func f3() { menuSuggestionFlow.Stream( context.Background(), "French", - )(func(sfv *genkit.StreamFlowValue[OutputType, StreamType], err error) bool { + )(func(sfv *core.StreamFlowValue[OutputType, StreamType], err error) bool { if err != nil { // handle err return false @@ -129,7 +130,8 @@ func makeFullMenuSuggestion(restaurantTheme InputType, menuChunks chan StreamTyp // [START main] func main() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -141,22 +143,25 @@ func main() { return "", nil }, ) - if err := g.Start(context.Background(), nil); err != nil { - log.Fatal(err) - } + <-ctx.Done() } // [END main] func f4() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } + myFlow := genkit.DefineFlow(g, "myFlow", func(ctx context.Context, restaurantTheme string) (string, error) { + return "", nil + }) + // [START mux] mainMux := http.NewServeMux() - mainMux.Handle("POST /flow/", http.StripPrefix("/flow/", genkit.NewFlowServeMux(g, nil))) + mainMux.Handle("POST /flow/myFlow", genkit.Handler(myFlow)) // [END mux] // [START run] genkit.DefineFlow( @@ -176,75 +181,44 @@ func f4() { } func deploy(ctx context.Context) { - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } + _ = g // [START init] - if err := g.Start(ctx, - &genkit.StartOptions{FlowAddr: ":3400"}, // Add this parameter. - ); err != nil { - log.Fatal(err) - } + // TODO: Replace code snippet. // [END init] } func f5() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } // [START auth] - ctx := context.Background() - // Define an auth policy and create a Firebase auth provider - firebaseAuth, err := firebase.NewAuth(ctx, func(authContext genkit.AuthContext, input any) error { - // The type must match the input type of the flow. - userID := input.(string) - if authContext == nil || authContext["UID"] != userID { - return errors.New("user ID does not match") - } - return nil - }, true) - if err != nil { - log.Fatalf("failed to set up Firebase auth: %v", err) - } - // Define a flow with authentication - authenticatedFlow := genkit.DefineFlow( - g, - "authenticated-flow", - func(ctx context.Context, userID string) (string, error) { - return fmt.Sprintf("Secure data for user %s", userID), nil - }, - genkit.WithFlowAuth(firebaseAuth), - ) + // TODO: Replace code snippet. // [END auth] - _ = authenticatedFlow + _ = g } func f6() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } - ctx := context.Background() - var policy func(authContext genkit.AuthContext, input any) error - required := true + _ = g // [START auth-create] - firebaseAuth, err := firebase.NewAuth(ctx, policy, required) + // TODO: Replace code snippet. // [END auth-create] - _ = firebaseAuth _ = err - userDataFunc := func(ctx context.Context, userID string) (string, error) { - return fmt.Sprintf("Secure data for user %s", userID), nil - } // [START auth-define] - genkit.DefineFlow(g, "secureUserFlow", userDataFunc, genkit.WithFlowAuth(firebaseAuth)) + // TODO: Replace code snippet. // [END auth-define] - authenticatedFlow := genkit.DefineFlow(g, "your-flow", userDataFunc, genkit.WithFlowAuth(firebaseAuth)) // [START auth-run] - response, err := authenticatedFlow.Run(ctx, "user123", - genkit.WithLocalAuth(map[string]any{"UID": "user123"})) + // TODO: Replace code snippet. // [END auth-run] - _ = response _ = err } diff --git a/go/internal/doc-snippets/gcp.go b/go/internal/doc-snippets/gcp.go index 44f7c0a64a..32ee27a323 100644 --- a/go/internal/doc-snippets/gcp.go +++ b/go/internal/doc-snippets/gcp.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -14,7 +13,7 @@ import ( ) func gcpEx(ctx context.Context) error { - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/googleai.go b/go/internal/doc-snippets/googleai.go index 1f9c45d03e..b31ea80965 100644 --- a/go/internal/doc-snippets/googleai.go +++ b/go/internal/doc-snippets/googleai.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -14,7 +13,7 @@ import ( ) func googleaiEx(ctx context.Context) error { - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/init/main.go b/go/internal/doc-snippets/init/main.go index 44d9c3bc79..c033fe2a0a 100644 --- a/go/internal/doc-snippets/init/main.go +++ b/go/internal/doc-snippets/init/main.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - // [START main] package main @@ -20,7 +19,7 @@ import ( func main() { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -59,13 +58,7 @@ func main() { return text, nil }) - // Initialize Genkit and start a flow server. This call must come last, - // after all of your plug-in configuration and flow definitions. When you - // pass a nil configuration to Init, Genkit starts a local flow server, - // which you can interact with using the developer UI. - if err := g.Start(ctx, nil); err != nil { - log.Fatal(err) - } + <-ctx.Done() } // [END main] diff --git a/go/internal/doc-snippets/modelplugin/modelplugin.go b/go/internal/doc-snippets/modelplugin/modelplugin.go index 669ef9f8b4..a092ddc0a2 100644 --- a/go/internal/doc-snippets/modelplugin/modelplugin.go +++ b/go/internal/doc-snippets/modelplugin/modelplugin.go @@ -23,7 +23,8 @@ type MyModelConfig struct { // [END cfg] func Init() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { return err } diff --git a/go/internal/doc-snippets/models.go b/go/internal/doc-snippets/models.go index 219765b30c..9515daa17f 100644 --- a/go/internal/doc-snippets/models.go +++ b/go/internal/doc-snippets/models.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -24,7 +23,8 @@ var ctx = context.Background() var gemini15pro ai.Model func m1() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -53,7 +53,8 @@ func m1() error { } func opts() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -78,7 +79,8 @@ func opts() error { } func streaming() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -104,7 +106,8 @@ func streaming() error { } func multi() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -131,7 +134,8 @@ func multi() error { } func tools() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -155,7 +159,8 @@ func tools() error { } func history() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/ollama.go b/go/internal/doc-snippets/ollama.go index 148fe87355..b69d46100c 100644 --- a/go/internal/doc-snippets/ollama.go +++ b/go/internal/doc-snippets/ollama.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -14,7 +13,7 @@ import ( ) func ollamaEx(ctx context.Context) error { - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/pinecone.go b/go/internal/doc-snippets/pinecone.go index 2de2c07e8e..306c10761c 100644 --- a/go/internal/doc-snippets/pinecone.go +++ b/go/internal/doc-snippets/pinecone.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -15,7 +14,7 @@ import ( ) func pineconeEx(ctx context.Context) error { - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/prompts.go b/go/internal/doc-snippets/prompts.go index d396365998..70b55ffa07 100644 --- a/go/internal/doc-snippets/prompts.go +++ b/go/internal/doc-snippets/prompts.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -16,7 +15,8 @@ import ( ) func pr01() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -39,7 +39,8 @@ func helloPrompt(name string) *ai.Part { // [END hello] func pr02() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -58,7 +59,8 @@ func pr02() { } func pr03() error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/rag/main.go b/go/internal/doc-snippets/rag/main.go index aac27c025a..bd9934f80d 100644 --- a/go/internal/doc-snippets/rag/main.go +++ b/go/internal/doc-snippets/rag/main.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package rag import ( @@ -24,7 +23,7 @@ func main() { // [START vec] ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -94,10 +93,7 @@ func main() { ) // [END indexflow] - err = g.Start(ctx, nil) - if err != nil { - log.Fatal(err) - } + <-ctx.Done() } // [START readpdf] @@ -130,7 +126,7 @@ func menuQA() { // [START retrieve] ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -192,7 +188,8 @@ make up an answer. Do not add or change items on the menu.`), } func customret() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/doc-snippets/telemetryplugin/telemetryplugin.go b/go/internal/doc-snippets/telemetryplugin/telemetryplugin.go index d230eaf664..3639a50997 100644 --- a/go/internal/doc-snippets/telemetryplugin/telemetryplugin.go +++ b/go/internal/doc-snippets/telemetryplugin/telemetryplugin.go @@ -1,10 +1,10 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package telemetryplugin import ( + "context" "log/slog" "os" "time" @@ -38,7 +38,8 @@ type Config struct { // [END config] func Init(cfg Config) error { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { return err } diff --git a/go/internal/doc-snippets/vertexai.go b/go/internal/doc-snippets/vertexai.go index d98c8314c6..16c51d04ec 100644 --- a/go/internal/doc-snippets/vertexai.go +++ b/go/internal/doc-snippets/vertexai.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package snippets import ( @@ -14,7 +13,7 @@ import ( ) func vertexaiEx(ctx context.Context) error { - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } diff --git a/go/internal/registry/registry.go b/go/internal/registry/registry.go index 7077e7ae45..1edc0e0a44 100644 --- a/go/internal/registry/registry.go +++ b/go/internal/registry/registry.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package registry import ( @@ -25,7 +24,6 @@ type Registry struct { mu sync.Mutex frozen bool // when true, no more additions actions map[string]action.Action - flows []Flow } func New() (*Registry, error) { @@ -33,6 +31,9 @@ func New() (*Registry, error) { actions: map[string]action.Action{}, } r.tstate = tracing.NewState() + if os.Getenv("GENKIT_TELEMETRY_SERVER") != "" { + r.tstate.WriteTelemetryImmediate(tracing.NewHTTPTelemetryClient(os.Getenv("GENKIT_TELEMETRY_SERVER"))) + } return r, nil } @@ -51,7 +52,6 @@ func (r *Registry) RegisterAction(typ atype.ActionType, a action.Action) { if _, ok := r.actions[key]; ok { panic(fmt.Sprintf("action %q is already registered", key)) } - a.SetTracingState(r.tstate) r.actions[key] = a slog.Debug("RegisterAction", "type", typ, @@ -88,25 +88,6 @@ func (r *Registry) ListActions() []action.Desc { return ads } -// Flow is the type for the flows stored in a registry. -// Since a registry just remembers flows and returns them, -// this interface is empty. -type Flow interface{} - -// RegisterFlow stores the flow for use by the production server (see [NewFlowServeMux]). -// It doesn't check for duplicates because registerAction will do that. -func (r *Registry) RegisterFlow(f Flow) { - r.mu.Lock() - defer r.mu.Unlock() - r.flows = append(r.flows, f) -} - -func (r *Registry) ListFlows() []Flow { - r.mu.Lock() - defer r.mu.Unlock() - return r.flows -} - func (r *Registry) RegisterSpanProcessor(sp sdktrace.SpanProcessor) { r.tstate.RegisterSpanProcessor(sp) } diff --git a/go/plugins/dotprompt/dotprompt.go b/go/plugins/dotprompt/dotprompt.go index 243e7a7017..a031d940e0 100644 --- a/go/plugins/dotprompt/dotprompt.go +++ b/go/plugins/dotprompt/dotprompt.go @@ -99,7 +99,7 @@ func Open(g *genkit.Genkit, name string) (*Prompt, error) { // OpenVariant opens a parses a dotprompt file with a variant. // If the variant does not exist, the non-variant version is tried. func OpenVariant(g *genkit.Genkit, name, variant string) (*Prompt, error) { - if g.Opts.PromptDir == "" { + if g.Params.PromptDir == "" { // The TypeScript code defaults to ./prompts, // but that makes the program change behavior // depending on where it is run. @@ -111,7 +111,7 @@ func OpenVariant(g *genkit.Genkit, name, variant string) (*Prompt, error) { vname = name + "." + variant } - fileName := filepath.Join(g.Opts.PromptDir, vname+".prompt") + fileName := filepath.Join(g.Params.PromptDir, vname+".prompt") data, err := os.ReadFile(fileName) if err != nil { @@ -280,7 +280,7 @@ func Define(g *genkit.Genkit, name, templateText string, opts ...PromptOption) ( // fallback to default model name if no model was specified if p.Config.ModelName == "" && p.Config.Model == nil { - p.Config.ModelName = g.Opts.DefaultModel + p.Config.ModelName = g.Params.DefaultModel } p.Register(g) diff --git a/go/plugins/dotprompt/dotprompt_test.go b/go/plugins/dotprompt/dotprompt_test.go index 44a45d8a51..675761cdf3 100644 --- a/go/plugins/dotprompt/dotprompt_test.go +++ b/go/plugins/dotprompt/dotprompt_test.go @@ -1,10 +1,10 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package dotprompt import ( + "context" "encoding/json" "log" "testing" @@ -29,16 +29,12 @@ func testTool(g *genkit.Genkit, name string) *ai.ToolDef[struct{ Test string }, ) } -var g, _ = genkit.New(&genkit.Options{ - PromptDir: "testdata", -}) +var g, _ = genkit.Init(context.Background(), genkit.WithPromptDir("testdata")) var testModel = genkit.DefineModel(g, "defineoptions", "test", nil, testGenerate) func TestPrompts(t *testing.T) { - g, err := genkit.New(&genkit.Options{ - PromptDir: "testdata", - }) + g, err := genkit.Init(context.Background(), genkit.WithPromptDir("testdata")) if err != nil { log.Fatal(err) } diff --git a/go/plugins/dotprompt/genkit_test.go b/go/plugins/dotprompt/genkit_test.go index f6aa2488b5..005d6b2656 100644 --- a/go/plugins/dotprompt/genkit_test.go +++ b/go/plugins/dotprompt/genkit_test.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package dotprompt import ( @@ -41,7 +40,7 @@ func testGenerate(ctx context.Context, req *ai.ModelRequest, cb func(context.Con } func TestExecute(t *testing.T) { - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { log.Fatal(err) } @@ -96,7 +95,7 @@ func TestExecute(t *testing.T) { } func TestOptionsPatternGenerate(t *testing.T) { - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { log.Fatal(err) } @@ -155,7 +154,7 @@ func TestOptionsPatternGenerate(t *testing.T) { } func TestGenerateOptions(t *testing.T) { - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { log.Fatal(err) } diff --git a/go/plugins/dotprompt/render_test.go b/go/plugins/dotprompt/render_test.go index dd6fa354d7..03fd613fa7 100644 --- a/go/plugins/dotprompt/render_test.go +++ b/go/plugins/dotprompt/render_test.go @@ -1,10 +1,10 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package dotprompt import ( + "context" "fmt" "log" "testing" @@ -16,9 +16,7 @@ import ( // TestRender is some of the tests from prompt_test.ts. func TestRender(t *testing.T) { - g, err := genkit.New(&genkit.Options{ - PromptDir: "testdata", - }) + g, err := genkit.Init(context.Background(), genkit.WithPromptDir("testdata")) if err != nil { log.Fatal(err) } @@ -85,9 +83,7 @@ This is the rest of the prompt`, // TestRenderMessages is some of the tests from template_test.ts. func TestRenderMessages(t *testing.T) { - g, err := genkit.New(&genkit.Options{ - PromptDir: "testdata", - }) + g, err := genkit.Init(context.Background(), genkit.WithPromptDir("testdata")) if err != nil { log.Fatal(err) } diff --git a/go/plugins/firebase/auth.go b/go/plugins/firebase/auth.go index 1e1226ebe4..c995ad0f0f 100644 --- a/go/plugins/firebase/auth.go +++ b/go/plugins/firebase/auth.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package firebase import ( @@ -12,93 +11,58 @@ import ( "strings" "firebase.google.com/go/v4/auth" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/internal/base" + "github.com/firebase/genkit/go/core" ) -var authContextKey = base.NewContextKey[map[string]any]() +// AuthContext is the context of an authenticated request. +type AuthContext = *auth.Token + +// AuthPolicy is a function that validates an incoming request. +type AuthPolicy = func(context.Context, AuthContext, json.RawMessage) error +// AuthClient is a client for the Firebase Auth service. type AuthClient interface { VerifyIDToken(context.Context, string) (*auth.Token, error) } -// firebaseAuth is a Firebase auth provider. -type firebaseAuth struct { - client AuthClient // Auth client for verifying ID tokens. - policy func(genkit.AuthContext, any) error // Auth policy for checking auth context. - required bool // Whether auth is required for direct calls. -} - -// NewAuth creates a Firebase auth check. -func NewAuth(ctx context.Context, policy func(genkit.AuthContext, any) error, required bool) (genkit.FlowAuth, error) { +// ContextProvider creates a Firebase context provider for Genkit actions. +func ContextProvider(ctx context.Context, policy AuthPolicy) (core.ContextProvider, error) { app, err := App(ctx) if err != nil { return nil, err } + client, err := app.Auth(ctx) if err != nil { return nil, err } - auth := &firebaseAuth{ - client: client, - policy: policy, - required: required, - } - return auth, nil -} -// ProvideAuthContext provides auth context from an auth header and sets it on the context. -func (f *firebaseAuth) ProvideAuthContext(ctx context.Context, authHeader string) (context.Context, error) { - if authHeader == "" { - if f.required { + return func(ctx context.Context, input core.RequestData) (core.ActionContext, error) { + authHeader, ok := input.Headers["authorization"] + if !ok { return nil, errors.New("authorization header is required but not provided") } - return ctx, nil - } - const bearerPrefix = "bearer " - if !strings.HasPrefix(strings.ToLower(authHeader), bearerPrefix) { - return nil, errors.New("invalid authorization header format") - } - token := authHeader[len(bearerPrefix):] - authToken, err := f.client.VerifyIDToken(ctx, token) - if err != nil { - return nil, fmt.Errorf("error verifying ID token: %v", err) - } - authBytes, err := json.Marshal(authToken) - if err != nil { - return nil, err - } - var authContext genkit.AuthContext - if err = json.Unmarshal(authBytes, &authContext); err != nil { - return nil, err - } - return f.NewContext(ctx, authContext), nil -} -// NewContext sets the auth context on the given context. -func (f *firebaseAuth) NewContext(ctx context.Context, authContext genkit.AuthContext) context.Context { - if ctx == nil { - return nil - } - return authContextKey.NewContext(ctx, authContext) -} + const bearerPrefix = "bearer " -// FromContext retrieves the auth context from the given context. -func (*firebaseAuth) FromContext(ctx context.Context) genkit.AuthContext { - if ctx == nil { - return nil - } - return authContextKey.FromContext(ctx) -} + if !strings.HasPrefix(strings.ToLower(authHeader), bearerPrefix) { + return nil, errors.New("invalid authorization header format") + } -// CheckAuthPolicy checks auth context against policy. -func (f *firebaseAuth) CheckAuthPolicy(ctx context.Context, input any) error { - authContext := f.FromContext(ctx) - if authContext == nil { - if f.required { - return errors.New("auth is required") + token := authHeader[len(bearerPrefix):] + authCtx, err := client.VerifyIDToken(ctx, token) + if err != nil { + return nil, fmt.Errorf("error verifying ID token: %v", err) } - return nil - } - return f.policy(authContext, input) + + if policy != nil { + if err := policy(ctx, authCtx, input.Input); err != nil { + return nil, err + } + } + + return core.ActionContext{ + "auth": authCtx, + }, nil + }, nil } diff --git a/go/plugins/firebase/auth_test.go b/go/plugins/firebase/auth_test.go deleted file mode 100644 index bfe24178a9..0000000000 --- a/go/plugins/firebase/auth_test.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2024 Google LLC -// SPDX-License-Identifier: Apache-2.0 - - -package firebase - -import ( - "context" - "errors" - "testing" - - "firebase.google.com/go/v4/auth" - "github.com/firebase/genkit/go/genkit" -) - -type mockAuthClient struct { - verifyIDTokenFunc func(context.Context, string) (*auth.Token, error) -} - -func (m *mockAuthClient) VerifyIDToken(ctx context.Context, token string) (*auth.Token, error) { - return m.verifyIDTokenFunc(ctx, token) -} - -func TestProvideAuthContext(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - tests := []struct { - name string - authHeader string - required bool - mockToken *auth.Token - mockError error - expectedUID string - expectedError string - }{ - { - name: "Valid token", - authHeader: "Bearer validtoken", - required: true, - mockToken: &auth.Token{ - UID: "user123", - Firebase: auth.FirebaseInfo{ - SignInProvider: "custom", - }, - }, - mockError: nil, - expectedUID: "user123", - expectedError: "", - }, - { - name: "Missing header when required", - authHeader: "", - required: true, - expectedUID: "", - expectedError: "authorization header is required but not provided", - }, - { - name: "Missing header when not required", - authHeader: "", - required: false, - expectedUID: "", - expectedError: "", - }, - { - name: "Invalid header format", - authHeader: "InvalidBearer token", - required: true, - expectedUID: "", - expectedError: "invalid authorization header format", - }, - { - name: "Token verification error", - authHeader: "Bearer invalidtoken", - required: true, - mockToken: nil, - mockError: errors.New("invalid token"), - expectedUID: "", - expectedError: "error verifying ID token: invalid token", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := &mockAuthClient{ - verifyIDTokenFunc: func(ctx context.Context, token string) (*auth.Token, error) { - if token == "validtoken" { - return tt.mockToken, tt.mockError - } - return nil, tt.mockError - }, - } - - auth := &firebaseAuth{ - client: mockClient, - required: tt.required, - } - - newCtx, err := auth.ProvideAuthContext(ctx, tt.authHeader) - - if tt.expectedError != "" { - if err == nil || err.Error() != tt.expectedError { - t.Errorf("Expected error %q, got %v", tt.expectedError, err) - } - } else if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if tt.expectedUID != "" { - authContext := auth.FromContext(newCtx) - if authContext == nil { - t.Errorf("Expected non-nil auth context") - } else { - uid, ok := authContext["uid"].(string) - if !ok { - t.Errorf("Expected 'uid' to be a string, got %T", authContext["uid"]) - } else if uid != tt.expectedUID { - t.Errorf("Expected UID %q, got %q", tt.expectedUID, uid) - } - } - } else if auth.FromContext(newCtx) != nil && tt.authHeader != "" { - t.Errorf("Expected nil auth context, but got non-nil") - } - }) - } -} - -func TestCheckAuthPolicy(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - authContext genkit.AuthContext - input any - required bool - policy func(genkit.AuthContext, any) error - expectedError string - }{ - { - name: "Valid auth context and policy", - authContext: map[string]any{"uid": "user123"}, - input: "test input", - required: true, - policy: func(authContext genkit.AuthContext, in any) error { - return nil - }, - expectedError: "", - }, - { - name: "Policy error", - authContext: map[string]any{"uid": "user123"}, - input: "test input", - required: true, - policy: func(authContext genkit.AuthContext, in any) error { - return errors.New("policy error") - }, - expectedError: "policy error", - }, - { - name: "Missing auth context when required", - authContext: nil, - input: "test input", - required: true, - policy: nil, - expectedError: "auth is required", - }, - { - name: "Missing auth context when not required", - authContext: nil, - input: "test input", - required: false, - policy: nil, - expectedError: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - auth := &firebaseAuth{ - required: tt.required, - policy: tt.policy, - } - - ctx := context.Background() - if tt.authContext != nil { - ctx = auth.NewContext(ctx, tt.authContext) - } - - err := auth.CheckAuthPolicy(ctx, tt.input) - - if tt.expectedError != "" { - if err == nil || err.Error() != tt.expectedError { - t.Errorf("Expected error %q, got %v", tt.expectedError, err) - } - } else if err != nil { - t.Errorf("Unexpected error: %v", err) - } - }) - } -} diff --git a/go/plugins/firebase/firebase.go b/go/plugins/firebase/firebase.go index bade89f8fe..66b3a802d8 100644 --- a/go/plugins/firebase/firebase.go +++ b/go/plugins/firebase/firebase.go @@ -1,36 +1,95 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package firebase import ( "context" + "fmt" + "log" "sync" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/auth" + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" ) +var state struct { + mu sync.Mutex // Ensures thread-safe access to state + initted bool // Tracks if the plugin has been initialized + app *firebase.App // Holds the Firebase app instance + retrievers []ai.Retriever // Holds the list of initialized retrievers +} + +// FirebaseApp is an interface to represent the Firebase App object type FirebaseApp interface { Auth(ctx context.Context) (*auth.Client, error) } -var ( - app *firebase.App - mutex sync.Mutex -) +// FirebasePluginConfig is the configuration for the Firebase plugin. +type FirebasePluginConfig struct { + App *firebase.App // Pre-initialized Firebase app + Retrievers []RetrieverOptions // Array of retriever options +} + +// Init initializes the plugin with the provided configuration. +func Init(ctx context.Context, g *genkit.Genkit, cfg *FirebasePluginConfig) error { + state.mu.Lock() + defer state.mu.Unlock() + + if state.initted { + log.Println("firebase.Init: plugin already initialized, returning without reinitializing") + return nil + } + + if cfg.App == nil { + return fmt.Errorf("firebase.Init: no Firebase app provided") + } + + state.app = cfg.App -// app returns a cached Firebase app. -func App(ctx context.Context) (FirebaseApp, error) { - mutex.Lock() - defer mutex.Unlock() - if app == nil { - newApp, err := firebase.NewApp(ctx, nil) + var retrievers []ai.Retriever + for _, retrieverCfg := range cfg.Retrievers { + retriever, err := DefineFirestoreRetriever(g, retrieverCfg) if err != nil { - return nil, err + return fmt.Errorf("firebase.Init: failed to initialize retriever %s: %v", retrieverCfg.Name, err) } - app = newApp + retrievers = append(retrievers, retriever) + } + + state.retrievers = retrievers + state.initted = true + return nil +} + +// unInit clears the initialized plugin state. +func unInit() { + state.mu.Lock() + defer state.mu.Unlock() + state.initted = false + state.app = nil + state.retrievers = nil +} + +// App returns the cached Firebase app. +func App(ctx context.Context) (*firebase.App, error) { + state.mu.Lock() + defer state.mu.Unlock() + + if !state.initted { + return nil, fmt.Errorf("firebase.App: Firebase app not initialized. Call Init first") + } + return state.app, nil +} + +// Retrievers returns the cached list of retrievers. +func Retrievers(ctx context.Context) ([]ai.Retriever, error) { + state.mu.Lock() + defer state.mu.Unlock() + + if !state.initted { + return nil, fmt.Errorf("firebase.Retrievers: Plugin not initialized. Call Init first") } - return app, nil + return state.retrievers, nil } diff --git a/go/plugins/firebase/firebase_init_test.go b/go/plugins/firebase/firebase_init_test.go new file mode 100644 index 0000000000..1a396a2276 --- /dev/null +++ b/go/plugins/firebase/firebase_init_test.go @@ -0,0 +1,94 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firebase + +import ( + "context" + "testing" + + firebase "firebase.google.com/go/v4" + "github.com/firebase/genkit/go/genkit" +) + +func TestInit(t *testing.T) { + t.Parallel() + + ctx := context.Background() + g, err := genkit.Init(ctx) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + config *FirebasePluginConfig + expectedError string + setup func() error + }{ + { + name: "Successful initialization", + config: &FirebasePluginConfig{ + App: &firebase.App{}, // Mock Firebase app + }, + expectedError: "", + setup: func() error { + return nil // No setup required, first call should succeed + }, + }, + { + name: "Initialization when already initialized", + config: &FirebasePluginConfig{ + App: &firebase.App{}, // Mock Firebase app + }, + expectedError: "", + setup: func() error { + // Initialize once + return Init(ctx, g, &FirebasePluginConfig{ + App: &firebase.App{}, // Mock Firebase app + }) + }, + }, + { + name: "Initialization with missing App", + config: &FirebasePluginConfig{ + App: nil, // No app provided + }, + expectedError: "firebase.Init: no Firebase app provided", // Expecting an error when no app is passed + setup: func() error { + return nil // No setup required + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer unInit() + + if err := tt.setup(); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + err := Init(ctx, g, tt.config) + + if tt.expectedError != "" { + if err == nil || err.Error() != tt.expectedError { + t.Errorf("Expected error %q, got %v", tt.expectedError, err) + } + } else if err != nil { + t.Errorf("Unexpected error: %v", err) + } + }) + } +} diff --git a/go/plugins/firebase/retriever.go b/go/plugins/firebase/retriever.go new file mode 100644 index 0000000000..9741f33529 --- /dev/null +++ b/go/plugins/firebase/retriever.go @@ -0,0 +1,125 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firebase + +import ( + "context" + "fmt" + + "cloud.google.com/go/firestore" + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" +) + +type VectorType int + +const ( + Vector64 VectorType = iota +) + +const provider = "firebase" + +type RetrieverOptions struct { + Name string + Label string + Client *firestore.Client + Collection string + Embedder ai.Embedder + VectorField string + MetadataFields []string + ContentField string + Limit int + DistanceMeasure firestore.DistanceMeasure + VectorType VectorType +} + +func DefineFirestoreRetriever(g *genkit.Genkit, cfg RetrieverOptions) (ai.Retriever, error) { + if cfg.VectorType != Vector64 { + return nil, fmt.Errorf("DefineFirestoreRetriever: only Vector64 is supported") + } + if cfg.Client == nil { + return nil, fmt.Errorf("DefineFirestoreRetriever: Firestore client is not provided") + } + + Retrieve := func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) { + if req.Document == nil { + return nil, fmt.Errorf("DefineFirestoreRetriever: Request document is nil") + } + + // Generate query embedding using the Embedder + embedRequest := &ai.EmbedRequest{Documents: []*ai.Document{req.Document}} + embedResponse, err := cfg.Embedder.Embed(ctx, embedRequest) + if err != nil { + return nil, fmt.Errorf("DefineFirestoreRetriever: Embedding failed: %v", err) + } + + if len(embedResponse.Embeddings) == 0 { + return nil, fmt.Errorf("DefineFirestoreRetriever: No embeddings returned") + } + + queryEmbedding := embedResponse.Embeddings[0].Embedding + if len(queryEmbedding) == 0 { + return nil, fmt.Errorf("DefineFirestoreRetriever: Generated embedding is empty") + } + + // Convert to []float64 + queryEmbedding64 := make([]float64, len(queryEmbedding)) + for i, val := range queryEmbedding { + queryEmbedding64[i] = float64(val) + } + // Perform the FindNearest query + vectorQuery := cfg.Client.Collection(cfg.Collection).FindNearest( + cfg.VectorField, + firestore.Vector64(queryEmbedding64), + cfg.Limit, + cfg.DistanceMeasure, + nil, + ) + iter := vectorQuery.Documents(ctx) + + results, err := iter.GetAll() + if err != nil { + return nil, fmt.Errorf("DefineFirestoreRetriever: FindNearest query failed: %v", err) + } + + // Prepare the documents to return in the response + var documents []*ai.Document + for _, result := range results { + data := result.Data() + + // Ensure content field exists and is of type string + content, ok := data[cfg.ContentField].(string) + if !ok { + fmt.Printf("Content field %s missing or not a string in document %s", cfg.ContentField, result.Ref.ID) + continue + } + + // Extract metadata fields + metadata := make(map[string]interface{}) + for _, field := range cfg.MetadataFields { + if value, ok := data[field]; ok { + metadata[field] = value + } + } + + doc := ai.DocumentFromText(content, metadata) + documents = append(documents, doc) + } + + return &ai.RetrieverResponse{Documents: documents}, nil + } + + return genkit.DefineRetriever(g, provider, cfg.Name, Retrieve), nil +} diff --git a/go/plugins/firebase/retriever_test.go b/go/plugins/firebase/retriever_test.go new file mode 100644 index 0000000000..4f5e6d8d71 --- /dev/null +++ b/go/plugins/firebase/retriever_test.go @@ -0,0 +1,296 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firebase + +import ( + "context" + "flag" + "testing" + + "cloud.google.com/go/firestore" + firebasev4 "firebase.google.com/go/v4" + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" + "google.golang.org/api/iterator" +) + +var ( + testProjectID = flag.String("test-project-id", "", "GCP Project ID to use for tests") + testCollection = flag.String("test-collection", "testR2", "Firestore collection to use for tests") + testVectorField = flag.String("test-vector-field", "embedding", "Field name for vector embeddings") +) + +/* + * Pre-requisites to run this test: + * + * 1. **Set Up Firebase Project and Firestore:** + * You must create a Firebase project and ensure Firestore is enabled for that project. To do so: + * + * - Visit the Firebase Console: https://console.firebase.google.com/ + * - Create a new project (or use an existing one). + * - Enable Firestore in your project from the "Build" section > "Firestore Database". + * + * 2. **Create a Firestore Collection and Composite Index:** + * This test assumes you have a Firestore collection set up for storing documents with vector embeddings. + * Additionally, you need to create a vector index for the embedding field. You can do this via the Firestore API with the following `curl` command: + * + * ```bash + * curl -X POST \ + * "https://firestore.googleapis.com/v1/projects//databases/(default)/collectionGroups//indexes" \ + * -H "Authorization: Bearer $(gcloud auth print-access-token)" \ + * -H "Content-Type: application/json" \ + * -d '{ + * "fields": [ + * { + * "fieldPath": "embedding", + * "vectorConfig": { + * "dimension": 3, + * "flat": {} + * } + * } + * ], + * "queryScope": "COLLECTION" + * }' + * ``` + * Replace `` and `` with your project and collection names. + * + * 3. **Authentication & Credentials:** + * Ensure you have access to the project and Firestore using Google Cloud CLI. You can authenticate using the following commands: + * + * ```bash + * gcloud auth login + * gcloud config set project + * gcloud auth application-default login + * ``` + * + * This authenticates your local environment with your GCP project and ensures the Go SDK can access Firestore. + * + * 4. **Running the Test:** + * After setting up Firestore and the index, you can run the test by passing in the required flags for the project, collection, and vector field: + * + * ```bash + * go test \ + * -test-project-id= \ + * -test-collection= \ + * -test-vector-field=embedding + * ``` + */ + +// MockEmbedder implements the Embedder interface for testing purposes +type MockEmbedder struct{} + +func (e *MockEmbedder) Name() string { + return "MockEmbedder" +} + +func (e *MockEmbedder) Embed(ctx context.Context, req *ai.EmbedRequest) (*ai.EmbedResponse, error) { + var embeddings []*ai.DocumentEmbedding + for _, doc := range req.Documents { + var embedding []float32 + switch doc.Content[0].Text { + case "This is document one": + // Embedding for document one is the closest to the query + embedding = []float32{0.9, 0.1, 0.0} + case "This is document two": + // Embedding for document two is less close to the query + embedding = []float32{0.7, 0.2, 0.1} + case "This is document three": + // Embedding for document three is even further from the query + embedding = []float32{0.4, 0.3, 0.3} + case "This is input query": + // Embedding for the input query + embedding = []float32{0.9, 0.1, 0.0} + default: + // Default embedding for any other documents + embedding = []float32{0.0, 0.0, 0.0} + } + + embeddings = append(embeddings, &ai.DocumentEmbedding{Embedding: embedding}) + } + return &ai.EmbedResponse{Embeddings: embeddings}, nil +} + +// To run this test you must have a Firestore database initialized in a GCP project, with a vector indexed collection (of dimension 3). +// Warning: This test will delete all documents in the collection in cleanup. + +func TestFirestoreRetriever(t *testing.T) { + // skip if flags aren't defined + if *testProjectID == "" { + t.Skip("Skipping test due to missing flags") + } + if *testCollection == "" { + t.Skip("Skipping test due to missing flags") + } + if *testVectorField == "" { + t.Skip("Skipping test due to missing flags") + } + + ctx := context.Background() + g, err := genkit.Init(ctx) + if err != nil { + t.Fatal(err) + } + + // Initialize Firebase app + conf := &firebasev4.Config{ProjectID: *testProjectID} + app, err := firebasev4.NewApp(ctx, conf) + if err != nil { + t.Fatalf("Failed to create Firebase app: %v", err) + } + + // Initialize Firestore client + client, err := app.Firestore(ctx) + if err != nil { + t.Fatalf("Failed to create Firestore client: %v", err) + } + defer client.Close() + + // Clean up the collection before the test + defer deleteCollection(ctx, client, *testCollection, t) + + // Initialize the embedder + embedder := &MockEmbedder{} + + // Insert test documents with embeddings generated by the embedder + testDocs := []struct { + ID string + Text string + Data map[string]interface{} + }{ + {"doc1", "This is document one", map[string]interface{}{"metadata": "meta1"}}, + {"doc2", "This is document two", map[string]interface{}{"metadata": "meta2"}}, + {"doc3", "This is document three", map[string]interface{}{"metadata": "meta3"}}, + } + + // Expected document text content in order of relevance for the query + expectedTexts := []string{ + "This is document one", + "This is document two", + } + + for _, doc := range testDocs { + // Create an ai.Document + aiDoc := ai.DocumentFromText(doc.Text, doc.Data) + + // Generate embedding + embedRequest := &ai.EmbedRequest{Documents: []*ai.Document{aiDoc}} + embedResponse, err := embedder.Embed(ctx, embedRequest) + if err != nil { + t.Fatalf("Failed to generate embedding for document %s: %v", doc.ID, err) + } + + if len(embedResponse.Embeddings) == 0 { + t.Fatalf("No embeddings returned for document %s", doc.ID) + } + + embedding := embedResponse.Embeddings[0].Embedding + if len(embedding) == 0 { + t.Fatalf("Generated embedding is empty for document %s", doc.ID) + } + + // Convert to []float64 + embedding64 := make([]float64, len(embedding)) + for i, val := range embedding { + embedding64[i] = float64(val) + } + + // Store in Firestore + _, err = client.Collection(*testCollection).Doc(doc.ID).Set(ctx, map[string]interface{}{ + "text": doc.Text, + "metadata": doc.Data["metadata"], + *testVectorField: firestore.Vector64(embedding64), + }) + if err != nil { + t.Fatalf("Failed to insert document %s: %v", doc.ID, err) + } + t.Logf("Inserted document: %s with embedding: %v", doc.ID, embedding64) + } + + // Define retriever options + retrieverOptions := RetrieverOptions{ + Name: "test-retriever", + Label: "Test Retriever", + Client: client, + Collection: *testCollection, + Embedder: embedder, + VectorField: *testVectorField, + MetadataFields: []string{"metadata"}, + ContentField: "text", + Limit: 2, + DistanceMeasure: firestore.DistanceMeasureEuclidean, + VectorType: Vector64, + } + + // Define the retriever + retriever, err := DefineFirestoreRetriever(g, retrieverOptions) + if err != nil { + t.Fatalf("Failed to define retriever: %v", err) + } + + // Create a retriever request with the input document + queryText := "This is input query" + inputDocument := ai.DocumentFromText(queryText, nil) + + req := &ai.RetrieverRequest{ + Document: inputDocument, + } + + // Perform the retrieval + resp, err := retriever.Retrieve(ctx, req) + if err != nil { + t.Fatalf("Retriever failed: %v", err) + } + + // Check the retrieved documents + if len(resp.Documents) == 0 { + t.Fatalf("No documents retrieved") + } + + // Verify the content of all retrieved documents against the expected list + for i, doc := range resp.Documents { + if i >= len(expectedTexts) { + t.Errorf("More documents retrieved than expected. Retrieved: %d, Expected: %d", len(resp.Documents), len(expectedTexts)) + break + } + + if doc.Content[0].Text != expectedTexts[i] { + t.Errorf("Mismatch in document %d content. Expected: '%s', Got: '%s'", i+1, expectedTexts[i], doc.Content[0].Text) + } else { + t.Logf("Retrieved Document %d matches expected content: '%s'", i+1, expectedTexts[i]) + } + } +} + +func deleteCollection(ctx context.Context, client *firestore.Client, collectionName string, t *testing.T) { + // Get all documents in the collection + iter := client.Collection(collectionName).Documents(ctx) + for { + doc, err := iter.Next() + if err == iterator.Done { + break // No more documents + } + if err != nil { + t.Fatalf("Failed to iterate documents for deletion: %v", err) + } + + // Delete each document + _, err = doc.Ref.Delete(ctx) + if err != nil { + t.Errorf("Failed to delete document %s: %v", doc.Ref.ID, err) + } else { + t.Logf("Deleted document: %s", doc.Ref.ID) + } + } +} diff --git a/go/plugins/googleai/googleai.go b/go/plugins/googleai/googleai.go index 6b3ecf8ffc..e33e439487 100644 --- a/go/plugins/googleai/googleai.go +++ b/go/plugins/googleai/googleai.go @@ -61,6 +61,10 @@ var ( Versions: []string{}, Supports: &gemini.Multimodal, }, + "gemini-2.0-pro-exp-02-05": { + Versions: []string{}, + Supports: &gemini.Multimodal, + }, } knownEmbedders = []string{"text-embedding-004", "embedding-001"} diff --git a/go/plugins/googleai/googleai_test.go b/go/plugins/googleai/googleai_test.go index d168f05c09..b87116139c 100644 --- a/go/plugins/googleai/googleai_test.go +++ b/go/plugins/googleai/googleai_test.go @@ -38,9 +38,7 @@ func TestLive(t *testing.T) { if *testAll { t.Skip("-all provided") } - g, err := genkit.New(&genkit.Options{ - DefaultModel: "googleai/gemini-1.5-flash", - }) + g, err := genkit.Init(context.Background(), genkit.WithDefaultModel("googleai/gemini-1.5-flash")) if err != nil { log.Fatal(err) } @@ -147,9 +145,7 @@ func TestLive(t *testing.T) { } func TestHeader(t *testing.T) { - g, err := genkit.New(&genkit.Options{ - DefaultModel: "googleai/gemini-1.5-flash", - }) + g, err := genkit.Init(context.Background(), genkit.WithDefaultModel("googleai/gemini-1.5-flash")) if err != nil { log.Fatal(err) } diff --git a/go/plugins/localvec/localvec.go b/go/plugins/localvec/localvec.go index 357316498f..14f9478947 100644 --- a/go/plugins/localvec/localvec.go +++ b/go/plugins/localvec/localvec.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - // Package localvec is a local vector database for development and testing. // The database is stored in a file in the local file system. // Production code should use a real vector database. diff --git a/go/plugins/localvec/localvec_test.go b/go/plugins/localvec/localvec_test.go index be080de09b..afbdf5836e 100644 --- a/go/plugins/localvec/localvec_test.go +++ b/go/plugins/localvec/localvec_test.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package localvec import ( @@ -18,7 +17,7 @@ import ( func TestLocalVec(t *testing.T) { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { t.Fatal(err) } @@ -88,7 +87,7 @@ func TestLocalVec(t *testing.T) { func TestPersistentIndexing(t *testing.T) { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { t.Fatal(err) } @@ -189,7 +188,7 @@ func TestSimilarity(t *testing.T) { } func TestInit(t *testing.T) { - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { t.Fatal(err) } diff --git a/go/plugins/ollama/ollama_live_test.go b/go/plugins/ollama/ollama_live_test.go index a7e11fd506..9b3fe8d28e 100644 --- a/go/plugins/ollama/ollama_live_test.go +++ b/go/plugins/ollama/ollama_live_test.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package ollama_test import ( @@ -30,7 +29,7 @@ func TestLive(t *testing.T) { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { t.Fatal(err) } diff --git a/go/plugins/pinecone/genkit_test.go b/go/plugins/pinecone/genkit_test.go index 00d5a0f8b8..360db2a1c9 100644 --- a/go/plugins/pinecone/genkit_test.go +++ b/go/plugins/pinecone/genkit_test.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package pinecone import ( @@ -27,7 +26,7 @@ func TestGenkit(t *testing.T) { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { t.Fatal(err) } diff --git a/go/plugins/server/server.go b/go/plugins/server/server.go new file mode 100644 index 0000000000..894595f3f8 --- /dev/null +++ b/go/plugins/server/server.go @@ -0,0 +1,45 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" +) + +// Start starts a new HTTP server and manages its lifecycle. +// This is a convenience function since Go does not manage interrupt signals directly. +func Start(ctx context.Context, addr string, mux *http.ServeMux) error { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer cancel() + + srv := &http.Server{ + Addr: addr, + Handler: mux, + } + + errChan := make(chan error, 1) + + go func() { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + errChan <- fmt.Errorf("server error: %w", err) + } + cancel() + }() + + select { + case err := <-errChan: + return err + case <-ctx.Done(): + if err := srv.Shutdown(ctx); err != nil { + return fmt.Errorf("failed to shutdown server: %w", err) + } + } + return nil +} diff --git a/go/plugins/vertexai/vertexai_test.go b/go/plugins/vertexai/vertexai_test.go index 4610b192aa..ec66c031a7 100644 --- a/go/plugins/vertexai/vertexai_test.go +++ b/go/plugins/vertexai/vertexai_test.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package vertexai_test import ( @@ -27,9 +26,7 @@ func TestLive(t *testing.T) { t.Skipf("no -projectid provided") } ctx := context.Background() - g, err := genkit.New(&genkit.Options{ - DefaultModel: "vertexai/gemini-1.5-flash", - }) + g, err := genkit.Init(context.Background(), genkit.WithDefaultModel("vertexai/gemini-1.5-flash")) if err != nil { t.Fatal(err) } diff --git a/go/plugins/weaviate/weaviate_test.go b/go/plugins/weaviate/weaviate_test.go index 46da356d19..a5f9af2bc3 100644 --- a/go/plugins/weaviate/weaviate_test.go +++ b/go/plugins/weaviate/weaviate_test.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package weaviate import ( @@ -32,7 +31,7 @@ func TestGenkit(t *testing.T) { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(context.Background()) if err != nil { t.Fatal(err) } diff --git a/go/samples/basic-gemini/main.go b/go/samples/basic-gemini/main.go index daf2a67083..7b597ec353 100644 --- a/go/samples/basic-gemini/main.go +++ b/go/samples/basic-gemini/main.go @@ -28,7 +28,7 @@ import ( func main() { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -63,7 +63,5 @@ func main() { return text, nil }) - if err := g.Start(ctx, nil); err != nil { - log.Fatal(err) - } + <-ctx.Done() } diff --git a/go/samples/coffee-shop/main.go b/go/samples/coffee-shop/main.go index df22299a09..2de78b04dc 100755 --- a/go/samples/coffee-shop/main.go +++ b/go/samples/coffee-shop/main.go @@ -26,11 +26,13 @@ package main import ( "context" "log" + "net/http" "github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/dotprompt" "github.com/firebase/genkit/go/plugins/googleai" + "github.com/firebase/genkit/go/plugins/server" ) const simpleGreetingPromptTemplate = ` @@ -83,9 +85,8 @@ type testAllCoffeeFlowsOutput struct { } func main() { - g, err := genkit.New(&genkit.Options{ - DefaultModel: "googleai/gemini-1.5-flash", - }) + ctx := context.Background() + g, err := genkit.Init(ctx, genkit.WithDefaultModel("googleai/gemini-1.5-flash")) if err != nil { log.Fatalf("failed to create Genkit: %v", err) } @@ -201,7 +202,9 @@ func main() { return out, nil }) - if err := g.Start(context.Background(), nil); err != nil { - log.Fatal(err) + mux := http.NewServeMux() + for _, a := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+a.Name(), genkit.Handler(a)) } + log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) } diff --git a/go/samples/firebase-auth/main.go b/go/samples/firebase-auth/main.go index bd9af13f89..8374a1cb6c 100644 --- a/go/samples/firebase-auth/main.go +++ b/go/samples/firebase-auth/main.go @@ -1,67 +1,39 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package main import ( "context" - "errors" "fmt" "log" + "net/http" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/firebase" + "github.com/firebase/genkit/go/plugins/server" ) func main() { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatalf("failed to create Genkit: %v", err) } - policy := func(authContext genkit.AuthContext, input any) error { - user := input.(string) - if authContext == nil || authContext["UID"] != user { - return errors.New("user ID does not match") - } - return nil - } - firebaseAuth, err := firebase.NewAuth(ctx, policy, true) - if err != nil { - log.Fatalf("failed to set up Firebase auth: %v", err) - } - - flowWithRequiredAuth := genkit.DefineFlow(g, "flow-with-required-auth", func(ctx context.Context, user string) (string, error) { - return fmt.Sprintf("info about user %q", user), nil - }, genkit.WithFlowAuth(firebaseAuth)) + genkit.DefineFlow(g, "flow-with-auth", func(ctx context.Context, user string) (string, error) { + return fmt.Sprintf("authenticated info about user %q", user), nil + }) - firebaseAuth, err = firebase.NewAuth(ctx, policy, false) + ctxProvider, err := firebase.ContextProvider(ctx, nil) if err != nil { - log.Fatalf("failed to set up Firebase auth: %v", err) + log.Fatalf("failed to create Firebase context provider: %v", err) } - flowWithAuth := genkit.DefineFlow(g, "flow-with-auth", func(ctx context.Context, user string) (string, error) { - return fmt.Sprintf("info about user %q", user), nil - }, genkit.WithFlowAuth(firebaseAuth)) - - genkit.DefineFlow(g, "super-caller", func(ctx context.Context, _ struct{}) (string, error) { - // Auth is required so we have to pass local credentials. - resp1, err := flowWithRequiredAuth.Run(ctx, "admin-user", genkit.WithLocalAuth(map[string]any{"UID": "admin-user"})) - if err != nil { - return "", fmt.Errorf("flowWithRequiredAuth: %w", err) - } - // Auth is not required so we can just run the flow. - resp2, err := flowWithAuth.Run(ctx, "admin-user-2") - if err != nil { - return "", fmt.Errorf("flowWithAuth: %w", err) - } - return resp1 + ", " + resp2, nil - }) - - if err := g.Start(ctx, nil); err != nil { - log.Fatal(err) + mux := http.NewServeMux() + for _, a := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+a.Name(), genkit.Handler(a, genkit.WithContextProviders(ctxProvider))) } + log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) } diff --git a/go/samples/firebase-retrievers/README.md b/go/samples/firebase-retrievers/README.md new file mode 100644 index 0000000000..2edba6cf16 --- /dev/null +++ b/go/samples/firebase-retrievers/README.md @@ -0,0 +1,101 @@ + +# Genkit Firestore Example + +This sample demonstrates how to index and retrieve documents using Firestore and Genkit. The documents contain text about famous films, and users can query the indexed documents to retrieve information based on their input. + +Currently the sample uses a mock embedder for simplicity. In your applications you will want to use an actual embedder from genkit. + +## Prerequisites + +Before running the sample, ensure you have the following: + +1. **Google Cloud Project**: You must have a Google Cloud project with Firestore enabled. +2. **Genkit**: Installed and set up in your local environment. +3. **Authentication & Credentials**: Ensure you are authenticated with your Google Cloud project using the following command: + ```bash + gcloud auth application-default login + ``` +4. **Firestore Composite Index**: You need to create a composite vector index in Firestore for the `embedding` field. You can do this by running the following `curl` command: + + ```bash + curl -X POST "https://firestore.googleapis.com/v1/projects//databases/(default)/collectionGroups//indexes" -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json" -d '{ + "fields": [ + { + "fieldPath": "embedding", + "vectorConfig": { + "dimension": 3, + "flat": {} + } + } + ], + "queryScope": "COLLECTION" + }' + ``` + + Replace `` and `` with your actual project and collection names. + +## Environment Variables + +You need to set the following environment variables before running the project: + +- `FIREBASE_PROJECT_ID`: The ID of your Google Cloud project. +- `FIRESTORE_COLLECTION`: The name of the Firestore collection to use for storing and retrieving documents. + +You can set these variables by running: + +```bash +export FIREBASE_PROJECT_ID=your-project-id +export FIRESTORE_COLLECTION=your-collection-name +``` + +## Running the Project + +Once the environment is set up, follow these steps: + +1. **Start Genkit**: + Run the following command to start the Genkit server: + + ```bash + genkit start + ``` + +2. **Index Documents**: + To index the 10 documents with text about famous films, run the following Genkit flow: + + ```bash + curl -X POST http://localhost:4000/api/runAction -H "Content-Type: application/json" -d '{"key":"/flow/flow-index-documents"}' + ``` + + This will insert 10 documents into the Firestore collection. + +3. **Retrieve Documents**: + To query the indexed documents, run the following Genkit flow and pass your query as input: + + ```bash + curl -X POST http://localhost:4000/api/runAction -H "Content-Type: application/json" -d '{"key":"/flow/flow-retrieve-documents", "input": "crime film"}' + ``` + + You can replace `"crime film"` with any other query related to the indexed film documents. + +## Troubleshooting + +1. **Firestore Composite Index**: Ensure the Firestore composite index for the `embedding` field is correctly set up, otherwise queries may fail. +2. **Environment Variables**: Make sure that the `FIREBASE_PROJECT_ID` and `FIRESTORE_COLLECTION` environment variables are correctly exported. + +## License + +``` +Copyright 2024 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` \ No newline at end of file diff --git a/go/samples/firebase-retrievers/main.go b/go/samples/firebase-retrievers/main.go new file mode 100644 index 0000000000..df046cfab2 --- /dev/null +++ b/go/samples/firebase-retrievers/main.go @@ -0,0 +1,180 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log" + "os" + + "cloud.google.com/go/firestore" + firebasev4 "firebase.google.com/go/v4" + + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/firebase" + "google.golang.org/api/option" +) + +func main() { + ctx := context.Background() + + // Load project ID and Firestore collection from environment variables + projectID := os.Getenv("FIREBASE_PROJECT_ID") + if projectID == "" { + log.Fatal("Environment variable FIREBASE_PROJECT_ID is not set") + } + + collectionName := os.Getenv("FIRESTORE_COLLECTION") + if collectionName == "" { + log.Fatal("Environment variable FIRESTORE_COLLECTION is not set") + } + + // Initialize Firestore client + firestoreClient, err := firestore.NewClient(ctx, projectID, option.WithCredentialsFile("")) + if err != nil { + log.Fatalf("Error creating Firestore client: %v", err) + } + defer firestoreClient.Close() + + // Firebase app configuration and initialization + firebaseApp, err := firebasev4.NewApp(ctx, nil) + if err != nil { + log.Fatalf("Error initializing Firebase app: %v", err) + } + + // Firebase configuration using the initialized app + firebaseConfig := &firebase.FirebasePluginConfig{ + App: firebaseApp, // Pass the pre-initialized Firebase app + } + + g, err := genkit.Init(ctx) + if err != nil { + log.Fatal(err) + } + + // Initialize Firebase plugin + if err := firebase.Init(ctx, g, firebaseConfig); err != nil { + log.Fatalf("Error initializing Firebase: %v", err) + } + + // Mock embedder + embedder := &MockEmbedder{} + + // Famous films text + films := []string{ + "The Godfather is a 1972 crime film directed by Francis Ford Coppola.", + "The Dark Knight is a 2008 superhero film directed by Christopher Nolan.", + "Pulp Fiction is a 1994 crime film directed by Quentin Tarantino.", + "Schindler's List is a 1993 historical drama directed by Steven Spielberg.", + "Inception is a 2010 sci-fi film directed by Christopher Nolan.", + "The Matrix is a 1999 sci-fi film directed by the Wachowskis.", + "Fight Club is a 1999 film directed by David Fincher.", + "Forrest Gump is a 1994 drama directed by Robert Zemeckis.", + "Star Wars is a 1977 sci-fi film directed by George Lucas.", + "The Shawshank Redemption is a 1994 drama directed by Frank Darabont.", + } + + // Define the index flow: Insert 10 documents about famous films + genkit.DefineFlow(g, "flow-index-documents", func(ctx context.Context, _ struct{}) (string, error) { + for i, filmText := range films { + docID := fmt.Sprintf("doc-%d", i+1) + embedding := []float64{float64(i+1) * 0.1, float64(i+1) * 0.2, float64(i+1) * 0.3} + + _, err := firestoreClient.Collection(collectionName).Doc(docID).Set(ctx, map[string]interface{}{ + "text": filmText, + "embedding": firestore.Vector64(embedding), + "metadata": fmt.Sprintf("metadata for doc %d", i+1), + }) + if err != nil { + return "", fmt.Errorf("failed to index document %d: %w", i+1, err) + } + log.Printf("Indexed document %d with text: %s", i+1, filmText) + } + return "10 film documents indexed successfully", nil + }) + + // Firestore Retriever Configuration + retrieverOptions := firebase.RetrieverOptions{ + Name: "example-retriever", + Client: firestoreClient, + Collection: collectionName, + Embedder: embedder, + VectorField: "embedding", + ContentField: "text", + MetadataFields: []string{"metadata"}, + Limit: 10, + DistanceMeasure: firestore.DistanceMeasureEuclidean, + VectorType: firebase.Vector64, + } + + // Define Firestore Retriever + retriever, err := firebase.DefineFirestoreRetriever(g, retrieverOptions) + if err != nil { + log.Fatalf("Error defining Firestore retriever: %v", err) + } + + // Define the retrieval flow: Retrieve documents based on user query + genkit.DefineFlow(g, "flow-retrieve-documents", func(ctx context.Context, query string) (string, error) { + // Perform Firestore retrieval based on user input + req := &ai.RetrieverRequest{ + Document: ai.DocumentFromText(query, nil), + } + log.Println("Starting retrieval with query:", query) + resp, err := retriever.Retrieve(ctx, req) + if err != nil { + return "", fmt.Errorf("retriever error: %w", err) + } + + // Check if documents were retrieved + if len(resp.Documents) == 0 { + log.Println("No documents retrieved, response:", resp) + return "", fmt.Errorf("no documents retrieved") + } + + // Log the retrieved documents for debugging + for _, doc := range resp.Documents { + log.Printf("Retrieved document: %s", doc.Content[0].Text) + } + + return fmt.Sprintf("Retrieved document: %s", resp.Documents[0].Content[0].Text), nil + }) + + <-ctx.Done() +} + +// MockEmbedder is used to simulate an AI embedder for testing purposes. +type MockEmbedder struct{} + +func (e *MockEmbedder) Name() string { + return "MockEmbedder" +} + +func (e *MockEmbedder) Embed(ctx context.Context, req *ai.EmbedRequest) (*ai.EmbedResponse, error) { + var embeddings []*ai.DocumentEmbedding + + // Generate a simple uniform embedding for each document + for _, doc := range req.Documents { + // Example: Use the length of the document text to generate embeddings + embedding := []float32{ + float32(len(doc.Content[0].Text)) * 0.1, // Scale based on text length + 0.5, // Static value + 0.3, // Static value + } + embeddings = append(embeddings, &ai.DocumentEmbedding{Embedding: embedding}) + } + return &ai.EmbedResponse{Embeddings: embeddings}, nil +} diff --git a/go/samples/flow-sample1/main.go b/go/samples/flow-sample1/main.go index 327b8aa722..f6fc68a165 100644 --- a/go/samples/flow-sample1/main.go +++ b/go/samples/flow-sample1/main.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - // This program can be manually tested like so: // Start the server listening on port 3100: // @@ -24,17 +23,20 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "log" + "net/http" "strconv" + "github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/server" ) func main() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatalf("failed to create Genkit: %v", err) } @@ -47,18 +49,7 @@ func main() { return genkit.Run(ctx, "call-llm", func() (string, error) { return "foo: " + foo, nil }) }) - auth := &testAuth{} - - genkit.DefineFlow(g, "withContext", func(ctx context.Context, subject string) (string, error) { - authJson, err := json.Marshal(auth.FromContext(ctx)) - if err != nil { - return "", err - } - - return "subject=" + subject + ",auth=" + string(authJson), nil - }, genkit.WithFlowAuth(auth)) - - genkit.DefineFlow(g, "parent", func(ctx context.Context, _ struct{}) (string, error) { + genkit.DefineFlow(g, "parent", func(ctx context.Context, _ any) (string, error) { return basic.Run(ctx, "foo") }) @@ -68,7 +59,7 @@ func main() { } genkit.DefineFlow(g, "complex", func(ctx context.Context, c complex) (string, error) { - foo, err := genkit.Run(ctx, "call-llm", func() (string, error) { return c.Key + ": " + strconv.Itoa(c.Value), nil }) + foo, err := core.Run(ctx, "call-llm", func() (string, error) { return c.Key + ": " + strconv.Itoa(c.Value), nil }) if err != nil { return "", err } @@ -110,51 +101,9 @@ func main() { return fmt.Sprintf("done: %d, streamed: %d times", count, i), nil }) - if err := g.Start(context.Background(), nil); err != nil { - log.Fatal(err) - } -} - -type testAuth struct { - genkit.FlowAuth -} - -const authKey = "testAuth" - -// ProvideAuthContext provides auth context from an auth header and sets it on the context. -func (f *testAuth) ProvideAuthContext(ctx context.Context, authHeader string) (context.Context, error) { - var context genkit.AuthContext - context = map[string]any{ - "username": authHeader, - } - return f.NewContext(ctx, context), nil -} - -// NewContext sets the auth context on the given context. -func (f *testAuth) NewContext(ctx context.Context, authContext genkit.AuthContext) context.Context { - return context.WithValue(ctx, authKey, authContext) -} - -// FromContext retrieves the auth context from the given context. -func (*testAuth) FromContext(ctx context.Context) genkit.AuthContext { - if ctx == nil { - return nil - } - val := ctx.Value(authKey) - if val == nil { - return nil - } - return val.(genkit.AuthContext) -} - -// CheckAuthPolicy checks auth context against policy. -func (f *testAuth) CheckAuthPolicy(ctx context.Context, input any) error { - authContext := f.FromContext(ctx) - if authContext == nil { - return errors.New("auth is required") - } - if authContext["username"] != "authorized" { - return errors.New("unauthorized") + mux := http.NewServeMux() + for _, a := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+a.Name(), genkit.Handler(a)) } - return nil + log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) } diff --git a/go/samples/menu/main.go b/go/samples/menu/main.go index b6489127b9..259426da9b 100644 --- a/go/samples/menu/main.go +++ b/go/samples/menu/main.go @@ -56,7 +56,7 @@ var textMenuQuestionInputSchema = jsonschema.Reflect(textMenuQuestionInput{}) func main() { ctx := context.Background() - g, err := genkit.New(nil) + g, err := genkit.Init(ctx) if err != nil { log.Fatalf("failed to create Genkit: %v", err) } @@ -96,7 +96,5 @@ func main() { log.Fatal(err) } - if err := g.Start(ctx, nil); err != nil { - log.Fatal(err) - } + <-ctx.Done() } diff --git a/go/samples/pgvector/main.go b/go/samples/pgvector/main.go index 08a05a74dc..d4f34d8d79 100644 --- a/go/samples/pgvector/main.go +++ b/go/samples/pgvector/main.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - // This program shows how to use Postgres's pgvector extension with Genkit. // This program can be manually tested like so: @@ -39,7 +38,8 @@ var ( func main() { flag.Parse() - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -101,7 +101,8 @@ func run(g *genkit.Genkit) error { }) // [END use-retr] - return g.Start(ctx, nil) + <-ctx.Done() + return nil } const provider = "pgvector" diff --git a/go/samples/rag/main.go b/go/samples/rag/main.go index 8fa0addb99..ad98bfc609 100644 --- a/go/samples/rag/main.go +++ b/go/samples/rag/main.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - // This program can be manually tested like so: // // In development mode (with the environment variable GENKIT_ENV="dev"): @@ -56,7 +55,8 @@ type simpleQaPromptInput struct { } func main() { - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -121,7 +121,5 @@ func main() { return resp.Text(), nil }) - if err := g.Start(context.Background(), nil); err != nil { - log.Fatal(err) - } + <-ctx.Done() } diff --git a/go/tests/api_test.go b/go/tests/api_test.go index b1a3f271cf..7a9ef2c801 100644 --- a/go/tests/api_test.go +++ b/go/tests/api_test.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - package api_test import ( @@ -134,6 +133,8 @@ func startGenkitApp(ctx context.Context, dir string) (func() error, error) { cmd = exec.CommandContext(ctx, "./"+dir) cmd.Dir = tmp cmd.Env = append(os.Environ(), "GENKIT_ENV=dev") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr cmd.WaitDelay = time.Second if err := cmd.Start(); err != nil { return nil, err diff --git a/go/tests/test_app/main.go b/go/tests/test_app/main.go index 55d13a7f90..c65bf20361 100644 --- a/go/tests/test_app/main.go +++ b/go/tests/test_app/main.go @@ -1,7 +1,6 @@ // Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 - // This program doesn't do anything interesting. // It is used by go/tests/api_test.go. package main @@ -11,22 +10,21 @@ import ( "encoding/json" "fmt" "log" + "net/http" "github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/server" ) func main() { - opts := genkit.StartOptions{ - FlowAddr: "127.0.0.1:3400", - } - // used for streamed flows type chunk struct { Count int `json:"count"` } - g, err := genkit.New(nil) + ctx := context.Background() + g, err := genkit.Init(ctx) if err != nil { log.Fatal(err) } @@ -52,9 +50,11 @@ func main() { return fmt.Sprintf("done %d, streamed: %d times", count, i), nil }) - if err := g.Start(context.Background(), &opts); err != nil { - log.Fatal(err) + mux := http.NewServeMux() + for _, a := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+a.Name(), genkit.Handler(a)) } + log.Fatal(server.Start(ctx, "127.0.0.1:3500", mux)) } func echo(ctx context.Context, req *ai.ModelRequest, cb func(context.Context, *ai.ModelResponseChunk) error) (*ai.ModelResponse, error) { diff --git a/js/ai/package.json b/js/ai/package.json index a532dc1742..48bf47d021 100644 --- a/js/ai/package.json +++ b/js/ai/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/ai/src/formats/json.ts b/js/ai/src/formats/json.ts index cc4beecd0a..5d7465546a 100644 --- a/js/ai/src/formats/json.ts +++ b/js/ai/src/formats/json.ts @@ -23,8 +23,19 @@ export const jsonFormatter: Formatter = { format: 'json', contentType: 'application/json', constrained: true, + defaultInstructions: false, }, - handler: () => { + handler: (schema) => { + let instructions: string | undefined; + + if (schema) { + instructions = `Output should be in JSON format and conform to the following schema: + +\`\`\` +${JSON.stringify(schema)} +\`\`\` +`; + } return { parseChunk: (chunk) => { return extractJson(chunk.accumulatedText); @@ -33,6 +44,8 @@ export const jsonFormatter: Formatter = { parseMessage: (message) => { return extractJson(message.text); }, + + instructions, }; }, }; diff --git a/js/ai/src/formats/types.ts b/js/ai/src/formats/types.ts index 7f0c9fbd5a..3cd17e3d3a 100644 --- a/js/ai/src/formats/types.ts +++ b/js/ai/src/formats/types.ts @@ -23,7 +23,9 @@ export type OutputContentTypes = 'application/json' | 'text/plain'; export interface Formatter { name: string; - config: ModelRequest['output']; + config: ModelRequest['output'] & { + defaultInstructions?: false; + }; handler: (schema?: JSONSchema) => { parseMessage(message: Message): O; parseChunk?: (chunk: GenerateResponseChunk) => CO; diff --git a/js/ai/src/generate.ts b/js/ai/src/generate.ts index d0b3ebef14..c609b4860f 100755 --- a/js/ai/src/generate.ts +++ b/js/ai/src/generate.ts @@ -33,7 +33,10 @@ import { resolveFormat, resolveInstructions, } from './formats/index.js'; -import { generateHelper } from './generate/action.js'; +import { + generateHelper, + shouldInjectFormatInstructions, +} from './generate/action.js'; import { GenerateResponseChunk } from './generate/chunk.js'; import { GenerateResponse } from './generate/response.js'; import { Message } from './message.js'; @@ -211,14 +214,19 @@ export async function toGenerateRequest( ); const out = { - messages: injectInstructions(messages, instructions), + messages: shouldInjectFormatInstructions( + resolvedFormat?.config, + options.output + ) + ? injectInstructions(messages, instructions) + : messages, config: options.config, docs: options.docs, tools: tools?.map(toToolDefinition) || [], output: { ...(resolvedFormat?.config || {}), - schema: resolvedSchema, ...options.output, + schema: resolvedSchema, }, } as GenerateRequest; if (!out?.output?.schema) delete out?.output?.schema; @@ -343,16 +351,11 @@ export async function generate< resolvedOptions.output.format = 'json'; } const resolvedFormat = await resolveFormat(registry, resolvedOptions.output); - const instructions = resolveInstructions( - resolvedFormat, - resolvedSchema, - resolvedOptions?.output?.instructions - ); const params: GenerateActionOptions = { model: resolvedModel.modelAction.__action.name, docs: resolvedOptions.docs, - messages: injectInstructions(messages, instructions), + messages: messages, tools, toolChoice: resolvedOptions.toolChoice, config: { @@ -374,6 +377,10 @@ export async function generate< returnToolRequests: resolvedOptions.returnToolRequests, maxTurns: resolvedOptions.maxTurns, }; + // if config is empty and it was not explicitly passed in, we delete it, don't want {} + if (Object.keys(params.config).length === 0 && !resolvedOptions.config) { + delete params.config; + } return await runWithStreamingCallback( registry, diff --git a/js/ai/src/generate/action.ts b/js/ai/src/generate/action.ts index 74f69ad072..7e8065c423 100644 --- a/js/ai/src/generate/action.ts +++ b/js/ai/src/generate/action.ts @@ -41,6 +41,7 @@ import { import { GenerateActionOptions, GenerateActionOptionsSchema, + GenerateActionOutputConfig, GenerateRequest, GenerateRequestSchema, GenerateResponseChunkData, @@ -172,7 +173,14 @@ function applyFormat( ); if (resolvedFormat) { - outRequest.messages = injectInstructions(outRequest.messages, instructions); + if ( + shouldInjectFormatInstructions(resolvedFormat.config, rawRequest?.output) + ) { + outRequest.messages = injectInstructions( + outRequest.messages, + instructions + ); + } outRequest.output = { // use output config from the format ...resolvedFormat.config, @@ -184,6 +192,16 @@ function applyFormat( return outRequest; } +export function shouldInjectFormatInstructions( + formatConfig?: Formatter['config'], + rawRequestConfig?: z.infer +) { + return ( + formatConfig?.defaultInstructions !== false || + rawRequestConfig?.instructions + ); +} + function applyTransferPreamble( rawRequest: GenerateActionOptions, transferPreamble?: GenerateActionOptions @@ -200,6 +218,7 @@ function applyTransferPreamble( ], toolChoice: transferPreamble.toolChoice || rawRequest.toolChoice, tools: transferPreamble.tools || rawRequest.tools, + config: transferPreamble.config || rawRequest.config, }); } diff --git a/js/ai/src/model.ts b/js/ai/src/model.ts index 3f7e2ca576..368d48a24e 100644 --- a/js/ai/src/model.ts +++ b/js/ai/src/model.ts @@ -699,6 +699,14 @@ export async function resolveModel( return out; } +export const GenerateActionOutputConfig = z.object({ + format: z.string().optional(), + contentType: z.string().optional(), + instructions: z.union([z.boolean(), z.string()]).optional(), + jsonSchema: z.any().optional(), + constrained: z.boolean().optional(), +}); + export const GenerateActionOptionsSchema = z.object({ /** A model name (e.g. `vertexai/gemini-1.0-pro`). */ model: z.string(), @@ -713,15 +721,7 @@ export const GenerateActionOptionsSchema = z.object({ /** Configuration for the generation request. */ config: z.any().optional(), /** Configuration for the desired output of the request. Defaults to the model's default output if unspecified. */ - output: z - .object({ - format: z.string().optional(), - contentType: z.string().optional(), - instructions: z.union([z.boolean(), z.string()]).optional(), - jsonSchema: z.any().optional(), - constrained: z.boolean().optional(), - }) - .optional(), + output: GenerateActionOutputConfig.optional(), /** Options for resuming an interrupted generation. */ resume: z .object({ diff --git a/js/ai/src/prompt.ts b/js/ai/src/prompt.ts index 58efc1ad72..7dc1160bd3 100644 --- a/js/ai/src/prompt.ts +++ b/js/ai/src/prompt.ts @@ -293,7 +293,7 @@ function definePromptAsync< docs = resolvedOptions.docs; } - return stripUndefinedProps({ + const opts: GenerateOptions = stripUndefinedProps({ model: resolvedOptions.model, maxTurns: resolvedOptions.maxTurns, messages, @@ -310,6 +310,11 @@ function definePromptAsync< ...renderOptions?.config, }, }); + // if config is empty and it was not explicitly passed in, we delete it, don't want {} + if (Object.keys(opts.config).length === 0 && !renderOptions?.config) { + delete opts.config; + } + return opts; }; const rendererActionConfig = lazy(() => optionsPromise.then((options: PromptConfig) => { @@ -394,7 +399,7 @@ function promptMetadata(options: PromptConfig) { input: { schema: options.input ? toJsonSchema(options.input) : undefined, }, - name: options.name, + name: `${options.name}${options.variant ? `.${options.variant}` : ''}`, model: modelName(options.model), }, type: 'prompt', diff --git a/js/ai/tests/model/middleware_test.ts b/js/ai/tests/model/middleware_test.ts index 49cd624fd3..cdd70d1caa 100644 --- a/js/ai/tests/model/middleware_test.ts +++ b/js/ai/tests/model/middleware_test.ts @@ -396,7 +396,7 @@ describe('augmentWithContext', () => { }); }); -describe('simulateConstrainedGeneration', () => { +describe.only('simulateConstrainedGeneration', () => { let registry: Registry; beforeEach(() => { @@ -555,4 +555,70 @@ describe('simulateConstrainedGeneration', () => { tools: [], }); }); + + it('uses format instructions when instructions is explicitly set to true', async () => { + let pm = defineProgrammableModel(registry, { + supports: { constrained: 'all' }, + }); + pm.handleResponse = async (req, sc) => { + return { + message: { + role: 'model', + content: [{ text: '```\n{"foo": "bar"}\n```' }], + }, + }; + }; + + const { output } = await generate(registry, { + model: 'programmableModel', + prompt: 'generate json', + output: { + instructions: true, + constrained: false, + schema: z.object({ + foo: z.string(), + }), + }, + }); + assert.deepEqual(output, { foo: 'bar' }); + assert.deepStrictEqual(pm.lastRequest, { + config: {}, + messages: [ + { + role: 'user', + content: [ + { text: 'generate json' }, + { + metadata: { + purpose: 'output', + }, + text: + 'Output should be in JSON format and conform to the following schema:\n' + + '\n' + + '```\n' + + '{"type":"object","properties":{"foo":{"type":"string"}},"required":["foo"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"}\n' + + '```\n', + }, + ], + }, + ], + output: { + constrained: false, + contentType: 'application/json', + format: 'json', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: true, + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + type: 'object', + }, + }, + tools: [], + }); + }); }); diff --git a/js/core/package.json b/js/core/package.json index f6ebcf9564..7d155de261 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/core/src/action.ts b/js/core/src/action.ts index 4fee1628cd..c7fa15d454 100644 --- a/js/core/src/action.ts +++ b/js/core/src/action.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; +import { type JSONSchema7 } from 'json-schema'; import * as z from 'zod'; import { lazy } from './async.js'; import { ActionContext, getContext, runWithContext } from './context.js'; @@ -26,7 +26,7 @@ import { setCustomMetadataAttributes, } from './tracing.js'; -export { Status, StatusCodes, StatusSchema } from './statusTypes.js'; +export { StatusCodes, StatusSchema, type Status } from './statusTypes.js'; export { JSONSchema7 }; /** @@ -273,6 +273,7 @@ export function action< inputJsonSchema: config.inputJsonSchema, outputSchema: config.outputSchema, outputJsonSchema: config.outputJsonSchema, + streamSchema: config.streamSchema, metadata: config.metadata, } as ActionMetadata; actionFn.run = async ( diff --git a/js/core/src/error.ts b/js/core/src/error.ts index bda27d595f..305220a48e 100644 --- a/js/core/src/error.ts +++ b/js/core/src/error.ts @@ -15,7 +15,7 @@ */ import { Registry } from './registry.js'; -import { httpStatusCode, StatusName } from './statusTypes.js'; +import { httpStatusCode, type StatusName } from './statusTypes.js'; export { StatusName }; diff --git a/js/core/src/reflection.ts b/js/core/src/reflection.ts index 5cbb4ae612..feb1f39a6b 100644 --- a/js/core/src/reflection.ts +++ b/js/core/src/reflection.ts @@ -20,7 +20,11 @@ import getPort, { makeRange } from 'get-port'; import { Server } from 'http'; import path from 'path'; import * as z from 'zod'; -import { Status, StatusCodes, runWithStreamingCallback } from './action.js'; +import { + StatusCodes, + runWithStreamingCallback, + type Status, +} from './action.js'; import { GENKIT_REFLECTION_API_SPEC_VERSION, GENKIT_VERSION } from './index.js'; import { logger } from './logging.js'; import { Registry } from './registry.js'; diff --git a/js/core/src/utils.ts b/js/core/src/utils.ts index 292b10320d..a15ab4c02d 100644 --- a/js/core/src/utils.ts +++ b/js/core/src/utils.ts @@ -36,6 +36,7 @@ export function deleteUndefinedProps(obj: any) { export function stripUndefinedProps(input: T): T { if ( input === undefined || + input === null || Array.isArray(input) || typeof input !== 'object' ) { diff --git a/js/doc-snippets/src/dotprompt/minimal.ts b/js/doc-snippets/src/dotprompt/minimal.ts index e035fc815e..eb30135df9 100644 --- a/js/doc-snippets/src/dotprompt/minimal.ts +++ b/js/doc-snippets/src/dotprompt/minimal.ts @@ -24,7 +24,7 @@ const ai = genkit({ // Initialize and configure the model plugins. plugins: [ googleAI({ - apiKey: 'your-api-key', // Or (preferred): export GOOGLE_GENAI_API_KEY=... + apiKey: 'your-api-key', // Or (preferred): export GEMINI_API_KEY=... }), ], }); diff --git a/js/doc-snippets/src/index.ts b/js/doc-snippets/src/index.ts index 91ec4f02c7..39adb45faa 100644 --- a/js/doc-snippets/src/index.ts +++ b/js/doc-snippets/src/index.ts @@ -14,15 +14,18 @@ * limitations under the License. */ -import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { gemini20Flash, googleAI } from '@genkit-ai/googleai'; import { genkit } from 'genkit'; const ai = genkit({ plugins: [googleAI()], - model: gemini15Flash, + model: gemini20Flash, }); -(async () => { - const { text } = await ai.generate('hi'); +async function main() { + // make a generation request + const { text } = await ai.generate('Hello, Gemini!'); console.log(text); -})(); +} + +main(); diff --git a/js/doc-snippets/src/models/imagen.ts b/js/doc-snippets/src/models/imagen.ts index c96c9fbb45..2494975b55 100644 --- a/js/doc-snippets/src/models/imagen.ts +++ b/js/doc-snippets/src/models/imagen.ts @@ -25,7 +25,7 @@ const ai = genkit({ plugins: [vertexAI({ location: 'us-central1' })], }); -(async () => { +async function main() { const { media } = await ai.generate({ model: imagen3Fast, prompt: 'photo of a meal fit for a pirate', @@ -38,5 +38,7 @@ const ai = genkit({ if (data === null) throw new Error('Invalid "data:" URL.'); await writeFile(`output.${data.mimeType.subtype}`, data.body); -})(); +} + +main(); // [END imagen] diff --git a/js/doc-snippets/src/models/minimal.ts b/js/doc-snippets/src/models/minimal.ts index cd7190ad79..1945ea4be7 100644 --- a/js/doc-snippets/src/models/minimal.ts +++ b/js/doc-snippets/src/models/minimal.ts @@ -15,18 +15,20 @@ */ // [START minimal] -import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { gemini20Flash, googleAI } from '@genkit-ai/googleai'; import { genkit } from 'genkit'; const ai = genkit({ plugins: [googleAI()], - model: gemini15Flash, + model: gemini20Flash, // Changed to gemini20Flash to match import }); -(async () => { +async function main() { const { text } = await ai.generate( 'Invent a menu item for a pirate themed restaurant.' ); console.log(text); -})(); +} + +main(); // [END minimal] diff --git a/js/genkit/package.json b/js/genkit/package.json index ff73a38faa..7a946fee93 100644 --- a/js/genkit/package.json +++ b/js/genkit/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "main": "./lib/cjs/index.js", "scripts": { diff --git a/js/genkit/src/client/client.ts b/js/genkit/src/client/client.ts index dd42cabb84..e8408c5922 100644 --- a/js/genkit/src/client/client.ts +++ b/js/genkit/src/client/client.ts @@ -41,8 +41,11 @@ export function streamFlow({ input, headers, }: { + /** URL of the deployed flow. */ url: string; + /** Flow input. */ input?: any; + /** A map of HTTP headers to be added to the HTTP call. */ headers?: Record; }): { readonly output: Promise; @@ -139,7 +142,7 @@ async function __flowRunEnvelope({ * For example: * * ```js - * import { runFlow } from '@genkit-ai/core/flow-client'; + * import { runFlow } from 'genkit/beta/client'; * * const response = await runFlow({ * url: 'https://my-flow-deployed-url', @@ -153,8 +156,11 @@ export async function runFlow({ input, headers, }: { + /** URL of the deployed flow. */ url: string; + /** Flow input. */ input?: any; + /** A map of HTTP headers to be added to the HTTP call. */ headers?: Record; }): Promise { const response = await fetch(url, { diff --git a/js/genkit/src/client/index.ts b/js/genkit/src/client/index.ts index 154de16044..82bbc06aa5 100644 --- a/js/genkit/src/client/index.ts +++ b/js/genkit/src/client/index.ts @@ -1,4 +1,6 @@ /** + * @license + * * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,4 +16,27 @@ * limitations under the License. */ +/** + * A simple, browser-safe client library for remotely runnning/streaming deployed Genkit flows. + * + * ```ts + * import { runFlow, streamFlow } from 'genkit/beta/client'; + * + * const response = await runFlow({ + * url: 'https://my-flow-deployed-url', + * input: 'foo', + * }); + * + * const response = streamFlow({ + * url: 'https://my-flow-deployed-url', + * input: 'foo', + * }); + * for await (const chunk of response.stream) { + * console.log(chunk); + * } + * console.log(await response.output); + * ``` + * + * @module beta/client + */ export { runFlow, streamFlow } from './client.js'; diff --git a/js/genkit/src/genkit.ts b/js/genkit/src/genkit.ts index 4b5795372d..4a0c608f71 100644 --- a/js/genkit/src/genkit.ts +++ b/js/genkit/src/genkit.ts @@ -172,9 +172,7 @@ export class Genkit implements HasRegistry { } /** - * Defines and registers a non-streaming flow. - * - * @todo TODO: Improve this documentation (show snippets, etc). + * Defines and registers a flow function. */ defineFlow< I extends z.ZodTypeAny = z.ZodTypeAny, @@ -233,9 +231,8 @@ export class Genkit implements HasRegistry { } /** - * Looks up a prompt by `name` and optional `variant`. - * - * @todo TODO: Show an example of a name and variant. + * Looks up a prompt by `name` (and optionally `variant`). Can be used to lookup + * .prompt files or prompts previously defined with {@link Genkit.definePrompt} */ prompt< I extends z.ZodTypeAny = z.ZodTypeAny, diff --git a/js/genkit/src/registry.ts b/js/genkit/src/registry.ts index 8c45c10d46..367d697b5d 100644 --- a/js/genkit/src/registry.ts +++ b/js/genkit/src/registry.ts @@ -15,8 +15,8 @@ */ export { - ActionType, - AsyncProvider, Registry, - Schema, + type ActionType, + type AsyncProvider, + type Schema, } from '@genkit-ai/core/registry'; diff --git a/js/genkit/tests/chat_test.ts b/js/genkit/tests/chat_test.ts index 501ede1b3a..7be76ded7a 100644 --- a/js/genkit/tests/chat_test.ts +++ b/js/genkit/tests/chat_test.ts @@ -290,8 +290,7 @@ describe('preamble', () => { assert.deepStrictEqual(text, 'hi from agent b (toolChoice: required)'); assert.deepStrictEqual(pm.lastRequest, { config: { - // TODO: figure out if config should be swapped out as well... - temperature: 2, + temperature: 1, }, messages: [ { diff --git a/js/genkit/tests/formats_test.ts b/js/genkit/tests/formats_test.ts index 774bbd47e4..e62f101dc7 100644 --- a/js/genkit/tests/formats_test.ts +++ b/js/genkit/tests/formats_test.ts @@ -86,14 +86,13 @@ describe('formats', () => { output: { constrained: true, format: 'banana', - schema: {}, }, tools: [], }); }); it('lets you define and use a custom output format with simulated constrained generation', async () => { - defineEchoModel(ai, { supports: { constrained: false } }); + defineEchoModel(ai, { supports: { constrained: 'none' } }); const { output } = await ai.generate({ model: 'echoModel', @@ -101,14 +100,7 @@ describe('formats', () => { output: { format: 'banana' }, }); - assert.strictEqual( - output, - 'banana: Echo: hi,Output should be in JSON format and conform to the following schema:\n' + - '\n' + - '```\n' + - '{}\n' + - '```\n' - ); + assert.strictEqual(output, 'banana: Echo: hi'); const { response, stream } = await ai.generateStream({ model: 'echoModel', @@ -120,14 +112,7 @@ describe('formats', () => { chunks.push(`${chunk.output}`); } assert.deepStrictEqual(chunks, ['banana: 3', 'banana: 2', 'banana: 1']); - assert.strictEqual( - (await response).output, - 'banana: Echo: hi,Output should be in JSON format and conform to the following schema:\n' + - '\n' + - '```\n' + - '{}\n' + - '```\n' - ); + assert.strictEqual((await response).output, 'banana: Echo: hi'); assert.deepStrictEqual(stripUndefinedProps((await response).request), { config: {}, messages: [ @@ -139,7 +124,6 @@ describe('formats', () => { output: { constrained: true, format: 'banana', - schema: {}, }, tools: [], }); diff --git a/js/genkit/tests/prompts_test.ts b/js/genkit/tests/prompts_test.ts index 14d547c814..14f4dbf83e 100644 --- a/js/genkit/tests/prompts_test.ts +++ b/js/genkit/tests/prompts_test.ts @@ -173,7 +173,7 @@ describe('definePrompt', () => { }); }); -describe.only('definePrompt', () => { +describe('definePrompt', () => { describe('default model', () => { let ai: GenkitBeta; @@ -310,7 +310,7 @@ describe.only('definePrompt', () => { }); }); - describe.only('default model ref', () => { + describe('default model ref', () => { let ai: GenkitBeta; beforeEach(() => { @@ -676,7 +676,6 @@ describe.only('definePrompt', () => { const response = await hi.render({ name: 'Genkit' }); delete response.model; // ignore assert.deepStrictEqual(response, { - config: {}, messages: [{ content: [{ text: 'hi Genkit' }], role: 'user' }], }); }); @@ -978,7 +977,6 @@ describe('definePrompt', () => { const response = await hi.render({ name: 'Genkit' }); delete response.model; // ignore assert.deepStrictEqual(response, { - config: {}, messages: [ { content: [ @@ -1369,8 +1367,8 @@ describe('asTool', () => { assert.deepStrictEqual(text, 'hi from agent b'); assert.deepStrictEqual(pm.lastRequest, { + // Original config, toolPrompt has no config. config: { - // TODO: figure out if config should be swapped out as well... temperature: 2, }, messages: [ diff --git a/js/plugins/checks/package.json b/js/plugins/checks/package.json index 822d94e141..c811008e49 100644 --- a/js/plugins/checks/package.json +++ b/js/plugins/checks/package.json @@ -13,7 +13,7 @@ "google checks", "guardrails" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/chroma/package.json b/js/plugins/chroma/package.json index 23de5913b2..e82e0cc226 100644 --- a/js/plugins/chroma/package.json +++ b/js/plugins/chroma/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/dev-local-vectorstore/README.md b/js/plugins/dev-local-vectorstore/README.md index 7dd9a1ade7..ff8cdfd305 100644 --- a/js/plugins/dev-local-vectorstore/README.md +++ b/js/plugins/dev-local-vectorstore/README.md @@ -16,7 +16,7 @@ npm i --save @genkit-ai/dev-local-vectorstore import { Document, genkit } from 'genkit'; import { googleAI, - gemini15Flash, + gemini20Flash, // Replaced gemini15Flash with gemini20Flash textEmbeddingGecko001, } from '@genkit-ai/googleai'; import { @@ -35,14 +35,14 @@ const ai = genkit({ }, ]), ], - model: gemini15Flash, + model: gemini20Flash, // Use gemini20Flash }); // Reference to a local vector database storing Genkit documentation const indexer = devLocalIndexerRef('BobFacts'); const retriever = devLocalRetrieverRef('BobFacts'); -(async () => { +async function main() { // Add documents to the index. Only do it once. await ai.index({ indexer: indexer, @@ -68,7 +68,9 @@ const retriever = devLocalRetrieverRef('BobFacts'); }); console.log(result.text); -})(); +} + +main(); ``` The sources for this package are in the main [Genkit](https://github.com/firebase/genkit) repo. Please file issues and pull requests against that repo. diff --git a/js/plugins/dev-local-vectorstore/package.json b/js/plugins/dev-local-vectorstore/package.json index 7e856055a8..09c3f86f48 100644 --- a/js/plugins/dev-local-vectorstore/package.json +++ b/js/plugins/dev-local-vectorstore/package.json @@ -10,7 +10,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/evaluators/package.json b/js/plugins/evaluators/package.json index 02543ee9a2..71b0499921 100644 --- a/js/plugins/evaluators/package.json +++ b/js/plugins/evaluators/package.json @@ -11,7 +11,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/evaluators/src/index.ts b/js/plugins/evaluators/src/index.ts index 41973ca2e3..f4fc32e1a9 100644 --- a/js/plugins/evaluators/src/index.ts +++ b/js/plugins/evaluators/src/index.ts @@ -102,7 +102,6 @@ export function genkitEvaluators< return metrics.map((metric) => { switch (metric) { case GenkitMetric.ANSWER_RELEVANCY: { - ai.defineIndexer; return ai.defineEvaluator( { name: `${PLUGIN_NAME}/${metric.toLocaleLowerCase()}`, diff --git a/js/plugins/express/package.json b/js/plugins/express/package.json index 53772a5c40..299d6653e9 100644 --- a/js/plugins/express/package.json +++ b/js/plugins/express/package.json @@ -9,7 +9,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/express/src/index.ts b/js/plugins/express/src/index.ts index 2b1f66513a..aa29ce9fc2 100644 --- a/js/plugins/express/src/index.ts +++ b/js/plugins/express/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import * as bodyParser from 'body-parser'; +import bodyParser from 'body-parser'; import cors, { CorsOptions } from 'cors'; import express from 'express'; import { diff --git a/js/plugins/firebase/package.json b/js/plugins/firebase/package.json index 84a709ae76..be5e6e0830 100644 --- a/js/plugins/firebase/package.json +++ b/js/plugins/firebase/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/firebase/src/index.ts b/js/plugins/firebase/src/index.ts index 889783eba0..4f2ac18739 100644 --- a/js/plugins/firebase/src/index.ts +++ b/js/plugins/firebase/src/index.ts @@ -26,8 +26,18 @@ import { } from '@genkit-ai/google-cloud'; export { defineFirestoreRetriever } from './firestoreRetriever.js'; +export interface FirebaseTelemetryOptions extends GcpTelemetryConfigOptions { + // future: firebase specific telemetry options +} + +/** + * Enables telemetry export to Firebase Genkit Monitoring, backed by the + * Google Cloud Observability suite. + * + * @param options configuration options + */ export async function enableFirebaseTelemetry( - options?: GcpTelemetryConfigOptions + options?: FirebaseTelemetryOptions | GcpTelemetryConfigOptions ) { await enableGoogleCloudTelemetry(options); } diff --git a/js/plugins/google-cloud/README.md b/js/plugins/google-cloud/README.md index e8b4c25f4b..aee7992e26 100644 --- a/js/plugins/google-cloud/README.md +++ b/js/plugins/google-cloud/README.md @@ -1,25 +1,8 @@ # Google Cloud plugin for Genkit -## Installing the plugin +The Google Cloud plugin is a utility plugin to export telemetry and logs to Cloud Observability. This functionality is exposed through the Firebase plugin for customer use. -```bash -npm i --save @genkit-ai/google-cloud -``` - -## Using the plugin - -```ts -import { genkit } from 'genkit'; -import { enableGoogleCloudTelemetry } from '@genkit-ai/google-cloud'; - -enableGoogleCloudTelemetry(); - -const ai = genkit({ - plugins: [ - // ... - ], -}); -``` +Visit the [Getting started](https://firebase.google.com/docs/genkit/observability/getting-started) docs to set up Firebase Genkit Monitoring. The sources for this package are in the main [Genkit](https://github.com/firebase/genkit) repo. Please file issues and pull requests against that repo. diff --git a/js/plugins/google-cloud/package.json b/js/plugins/google-cloud/package.json index b50dd64985..8367896fc5 100644 --- a/js/plugins/google-cloud/package.json +++ b/js/plugins/google-cloud/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/google-cloud/src/gcpLogger.ts b/js/plugins/google-cloud/src/gcpLogger.ts index ff95b821b9..2684772eab 100644 --- a/js/plugins/google-cloud/src/gcpLogger.ts +++ b/js/plugins/google-cloud/src/gcpLogger.ts @@ -55,10 +55,10 @@ export class GcpLogger { transports.push( this.shouldExport(env) ? new LoggingWinston({ - projectId: this.config.projectId, labels: { module: 'genkit' }, prefix: 'genkit', logName: 'genkit_log', + projectId: this.config.projectId, credentials: this.config.credentials, autoRetry: true, defaultCallback: await this.getErrorHandler(), diff --git a/js/plugins/google-cloud/src/gcpOpenTelemetry.ts b/js/plugins/google-cloud/src/gcpOpenTelemetry.ts index 5b641fbda0..39d199042f 100644 --- a/js/plugins/google-cloud/src/gcpOpenTelemetry.ts +++ b/js/plugins/google-cloud/src/gcpOpenTelemetry.ts @@ -123,8 +123,9 @@ export class GcpOpenTelemetry { spanExporter = new AdjustingTraceExporter( this.shouldExportTraces() ? new TraceExporter({ - // Creds for non-GCP environments; otherwise credentials will be - // automatically detected via ADC + // provided projectId should take precedence over env vars, etc + projectId: this.config.projectId, + // creds for non-GCP environments, in lieu of using ADC. credentials: this.config.credentials, }) : new InMemorySpanExporter(), @@ -154,12 +155,17 @@ export class GcpOpenTelemetry { /** Gets all open telemetry instrumentations as configured by the plugin. */ private getInstrumentations() { + let instrumentations: Instrumentation[] = []; + if (this.config.autoInstrumentation) { - return getNodeAutoInstrumentations( + instrumentations = getNodeAutoInstrumentations( this.config.autoInstrumentationConfig - ).concat(this.getDefaultLoggingInstrumentations()); + ); } - return this.getDefaultLoggingInstrumentations(); + + return instrumentations + .concat(this.getDefaultLoggingInstrumentations()) + .concat(this.config.instrumentations ?? []); } private shouldExportTraces(): boolean { @@ -186,8 +192,9 @@ export class GcpOpenTelemetry { product: 'genkit', version: GENKIT_VERSION, }, - // Creds for non-GCP environments; otherwise credentials will be - // automatically detected via ADC + // provided projectId should take precedence over env vars, etc + projectId: this.config.projectId, + // creds for non-GCP environments, in lieu of using ADC. credentials: this.config.credentials, }, getErrorHandler( diff --git a/js/plugins/google-cloud/src/index.ts b/js/plugins/google-cloud/src/index.ts index 856cbd188a..14c24cd4c9 100644 --- a/js/plugins/google-cloud/src/index.ts +++ b/js/plugins/google-cloud/src/index.ts @@ -23,6 +23,11 @@ import { GcpOpenTelemetry } from './gcpOpenTelemetry.js'; import { TelemetryConfigs } from './telemetry/defaults.js'; import { GcpTelemetryConfig, GcpTelemetryConfigOptions } from './types.js'; +/** + * Enables telemetry export to the Google Cloud Observability suite. + * + * @param options configuration options + */ export function enableGoogleCloudTelemetry( options?: GcpTelemetryConfigOptions ) { @@ -36,7 +41,7 @@ export function enableGoogleCloudTelemetry( /** * Create a configuration object for the plugin. - * Not normally needed, but exposed for use by the firebase plugin. + * Not normally needed, but exposed for use by the Firebase plugin. */ async function configureGcpPlugin( options?: GcpTelemetryConfigOptions diff --git a/js/plugins/google-cloud/src/telemetry/generate.ts b/js/plugins/google-cloud/src/telemetry/generate.ts index ee81594ded..66f8cfd6a8 100644 --- a/js/plugins/google-cloud/src/telemetry/generate.ts +++ b/js/plugins/google-cloud/src/telemetry/generate.ts @@ -251,7 +251,7 @@ class GenerateTelemetry implements Telemetry { topK: input.config?.topK, topP: input.config?.topP, maxOutputTokens: input.config?.maxOutputTokens, - stopSequences: truncate(input.config?.stopSequences, 1024), + stopSequences: input.config?.stopSequences, // array source: 'ts', sourceVersion: GENKIT_VERSION, }); diff --git a/js/plugins/google-cloud/src/types.ts b/js/plugins/google-cloud/src/types.ts index 8c63a04733..1b8c0ff702 100644 --- a/js/plugins/google-cloud/src/types.ts +++ b/js/plugins/google-cloud/src/types.ts @@ -21,40 +21,96 @@ import { JWTInput } from 'google-auth-library'; /** Configuration options for the Google Cloud plugin. */ export interface GcpTelemetryConfigOptions { - /** Cloud projectId is required, either passed here, through GCLOUD_PROJECT or application default credentials. */ + /** + * Google Cloud Project ID. If provided, will take precedence over the + * projectId inferred from the application credential and/or environment. + * Required when providing an external credential (e.g. Workload Identity + * Federation.) + */ projectId?: string; - /** Credentials must be provided to export telemetry, if not available through the environment. */ + /** + * Credentials for authenticating with Google Cloud. Primarily intended for + * use in environments outside of GCP. On GCP credentials will typically be + * inferred from the environment via Application Default Credentials (ADC). + */ credentials?: JWTInput; - /** Trace sampler, defaults to always on which exports all traces. */ + /** + * OpenTelemetry sampler; controls the number of traces collected and exported + * to Google Cloud. Defaults to AlwaysOnSampler, which will collect and export + * all traces. + * + * There are four built-in samplers to choose from: + * + * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/AlwaysOnSampler.ts | AlwaysOnSampler} - samples all traces + * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/AlwaysOffSampler.ts | AlwaysOffSampler} - samples no traces + * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/ParentBasedSampler.ts | ParentBasedSampler} - samples based on parent span + * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/TraceIdRatioBasedSampler.ts | TraceIdRatioBasedSampler} - samples a configurable percentage of traces + */ sampler?: Sampler; - /** Include OpenTelemetry autoInstrumentation. Defaults to true. */ + /** + * Enabled by default, OpenTelemetry will automatically collect telemetry for + * popular libraries via auto instrumentations without any additional code + * or configuration. All available instrumentations will be collected, unless + * otherwise specified via {@link autoInstrumentationConfig}. + * + * @see https://opentelemetry.io/docs/zero-code/js/ + */ autoInstrumentation?: boolean; + + /** + * Map of auto instrumentations and their configuration options. Available + * options will vary by instrumentation. + * + * @see https://opentelemetry.io/docs/zero-code/js/ + */ autoInstrumentationConfig?: InstrumentationConfigMap; + + /** + * Additional OpenTelemetry instrumentations to include, beyond those + * provided by auto instrumentations. + */ instrumentations?: Instrumentation[]; - /** Metric export intervals, minimum is 5000ms. */ + /** + * Metrics export interval in milliseconds; Google Cloud requires a minimum + * value of 5000ms. + */ metricExportIntervalMillis?: number; + + /** + * Timeout for the metrics export in milliseconds. + */ metricExportTimeoutMillis?: number; - /** When true, metrics are not exported. */ + /** + * If set to true, metrics will not be exported to Google Cloud. Traces and + * logs may still be exported. + */ disableMetrics?: boolean; - /** When true, traces are not exported. */ + /** + * If set to true, traces will not be exported to Google Cloud. Metrics and + * logs may still be exported. + */ disableTraces?: boolean; - /** When true, inputs and outputs are not logged to GCP */ + /** + * If set to true, input and output logs will not be collected. + */ disableLoggingInputAndOutput?: boolean; - /** When true, telemetry data will be exported, even for local runs. Defaults to not exporting development traces. */ + /** + * If set to true, telemetry data will be exported in the Genkit `dev` + * environment. Useful for local testing and troubleshooting; default is + * false. + */ forceDevExport?: boolean; } -/** - * Internal telemetry configuration. - */ +/** Internal telemetry configuration. */ export interface GcpTelemetryConfig { projectId?: string; credentials?: JWTInput; diff --git a/js/plugins/googleai/package.json b/js/plugins/googleai/package.json index eb2357f50f..94cd245775 100644 --- a/js/plugins/googleai/package.json +++ b/js/plugins/googleai/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/googleai/src/common.ts b/js/plugins/googleai/src/common.ts new file mode 100644 index 0000000000..4ce5bc0a05 --- /dev/null +++ b/js/plugins/googleai/src/common.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import process from 'process'; + +export function getApiKeyFromEnvVar(): string | undefined { + return ( + process.env.GEMINI_API_KEY || + process.env.GOOGLE_API_KEY || + process.env.GOOGLE_GENAI_API_KEY + ); +} diff --git a/js/plugins/googleai/src/embedder.ts b/js/plugins/googleai/src/embedder.ts index 756d0a8b57..df807f13a5 100644 --- a/js/plugins/googleai/src/embedder.ts +++ b/js/plugins/googleai/src/embedder.ts @@ -17,6 +17,7 @@ import { EmbedContentRequest, GoogleGenerativeAI } from '@google/generative-ai'; import { EmbedderAction, EmbedderReference, Genkit, z } from 'genkit'; import { embedderRef } from 'genkit/embedder'; +import { getApiKeyFromEnvVar } from './common.js'; import { PluginOptions } from './index.js'; export const TaskTypeSchema = z.enum([ @@ -82,13 +83,10 @@ export function defineGoogleAIEmbedder( name: string, options: PluginOptions ): EmbedderAction { - let apiKey = - options?.apiKey || - process.env.GOOGLE_GENAI_API_KEY || - process.env.GOOGLE_API_KEY; + let apiKey = options?.apiKey || getApiKeyFromEnvVar(); if (!apiKey) throw new Error( - 'Please pass in the API key or set either GOOGLE_GENAI_API_KEY or GOOGLE_API_KEY environment variable.\n' + + 'Please pass in the API key or set either GEMINI_API_KEY or GOOGLE_API_KEY environment variable.\n' + 'For more details see https://firebase.google.com/docs/genkit/plugins/google-genai' ); const embedder: EmbedderReference = diff --git a/js/plugins/googleai/src/gemini.ts b/js/plugins/googleai/src/gemini.ts index 4bdfcc4efa..9672a94839 100644 --- a/js/plugins/googleai/src/gemini.ts +++ b/js/plugins/googleai/src/gemini.ts @@ -15,6 +15,7 @@ */ import { + EnhancedGenerateContentResponse, FileDataPart, FunctionCallingMode, FunctionCallPart, @@ -29,6 +30,7 @@ import { GoogleGenerativeAI, InlineDataPart, RequestOptions, + Schema, SchemaType, StartChatParams, Tool, @@ -61,7 +63,8 @@ import { downloadRequestMedia, simulateSystemPrompt, } from 'genkit/model/middleware'; -import process from 'process'; +import { runInNewSpan } from 'genkit/tracing'; +import { getApiKeyFromEnvVar } from './common'; import { handleCacheIfNeeded } from './context-caching'; import { extractCacheConfig } from './context-caching/utils'; @@ -189,6 +192,23 @@ export const gemini20Flash = modelRef({ configSchema: GeminiConfigSchema, }); +export const gemini20ProExp0205 = modelRef({ + name: 'googleai/gemini-2.0-pro-exp-02-05', + info: { + label: 'Google AI - Gemini 2.0 Pro Exp 02-05', + versions: [], + supports: { + multiturn: true, + media: true, + tools: true, + toolChoice: true, + systemRole: true, + constrained: 'no-tools', + }, + }, + configSchema: GeminiConfigSchema, +}); + export const SUPPORTED_V1_MODELS = { 'gemini-1.0-pro': gemini10Pro, }; @@ -198,6 +218,7 @@ export const SUPPORTED_V15_MODELS = { 'gemini-1.5-flash': gemini15Flash, 'gemini-1.5-flash-8b': gemini15Flash8b, 'gemini-2.0-flash': gemini20Flash, + 'gemini-2.0-pro-exp-02-05': gemini20ProExp0205, }; export const GENERIC_GEMINI_MODEL = modelRef({ @@ -312,31 +333,64 @@ function toGeminiRole( function convertSchemaProperty(property) { if (!property || !property.type) { - return null; + return undefined; + } + const baseSchema = {} as Schema; + if (property.description) { + baseSchema.description = property.description; } - if (property.type === 'object') { + if (property.enum) { + baseSchema.enum = property.enum; + } + if (property.nullable) { + baseSchema.nullable = property.nullable; + } + let propertyType; + // nullable schema can ALSO be defined as, for example, type=['string','null'] + if (Array.isArray(property.type)) { + const types = property.type as string[]; + if (types.includes('null')) { + baseSchema.nullable = true; + } + // grab the type that's not `null` + propertyType = types.find((t) => t !== 'null'); + } else { + propertyType = property.type; + } + if (propertyType === 'object') { const nestedProperties = {}; Object.keys(property.properties).forEach((key) => { nestedProperties[key] = convertSchemaProperty(property.properties[key]); }); return { + ...baseSchema, type: SchemaType.OBJECT, properties: nestedProperties, required: property.required, }; - } else if (property.type === 'array') { + } else if (propertyType === 'array') { return { + ...baseSchema, type: SchemaType.ARRAY, items: convertSchemaProperty(property.items), }; } else { + const schemaType = SchemaType[propertyType.toUpperCase()] as SchemaType; + if (!schemaType) { + throw new GenkitError({ + status: 'INVALID_ARGUMENT', + message: `Unsupported property type ${propertyType.toUpperCase()}`, + }); + } return { - type: SchemaType[property.type.toUpperCase()], + ...baseSchema, + type: schemaType, }; } } -function toGeminiTool( +/** @hidden */ +export function toGeminiTool( tool: z.infer ): FunctionDeclaration { const declaration: FunctionDeclaration = { @@ -581,21 +635,31 @@ export function cleanSchema(schema: JSONSchema): JSONSchema { /** * Defines a new GoogleAI model. */ -export function defineGoogleAIModel( - ai: Genkit, - name: string, - apiKey?: string, - apiVersion?: string, - baseUrl?: string, - info?: ModelInfo, - defaultConfig?: GeminiConfig -): ModelAction { +export function defineGoogleAIModel({ + ai, + name, + apiKey, + apiVersion, + baseUrl, + info, + defaultConfig, + debugTraces, +}: { + ai: Genkit; + name: string; + apiKey?: string; + apiVersion?: string; + baseUrl?: string; + info?: ModelInfo; + defaultConfig?: GeminiConfig; + debugTraces?: boolean; +}): ModelAction { if (!apiKey) { - apiKey = process.env.GOOGLE_GENAI_API_KEY || process.env.GOOGLE_API_KEY; + apiKey = getApiKeyFromEnvVar(); } if (!apiKey) { throw new Error( - 'Please pass in the API key or set the GOOGLE_GENAI_API_KEY or GOOGLE_API_KEY environment variable.\n' + + 'Please pass in the API key or set the GEMINI_API_KEY or GOOGLE_API_KEY environment variable.\n' + 'For more details see https://firebase.google.com/docs/genkit/plugins/google-genai' ); } @@ -780,20 +844,35 @@ export function defineGoogleAIModel( ); } - if (sendChunk) { - const result = await genModel - .startChat(updatedChatRequest) - .sendMessageStream(msg.parts, options); - for await (const item of result.stream) { - (item as GenerateContentResponse).candidates?.forEach((candidate) => { - const c = fromJSONModeScopedGeminiCandidate(candidate); - sendChunk({ - index: c.index, - content: c.message.content, - }); - }); + const callGemini = async () => { + let response: EnhancedGenerateContentResponse; + + if (sendChunk) { + const result = await genModel + .startChat(updatedChatRequest) + .sendMessageStream(msg.parts, options); + + for await (const item of result.stream) { + (item as GenerateContentResponse).candidates?.forEach( + (candidate) => { + const c = fromJSONModeScopedGeminiCandidate(candidate); + sendChunk({ + index: c.index, + content: c.message.content, + }); + } + ); + } + + response = await result.response; + } else { + const result = await genModel + .startChat(updatedChatRequest) + .sendMessage(msg.parts, options); + + response = result.response; } - const response = await result.response; + const candidates = response.candidates || []; if (response.candidates?.['undefined']) { candidates.push(response.candidates['undefined']); @@ -804,30 +883,47 @@ export function defineGoogleAIModel( message: 'No valid candidates returned.', }); } + + const candidateData = + candidates.map(fromJSONModeScopedGeminiCandidate) || []; + return { - candidates: candidates.map(fromJSONModeScopedGeminiCandidate) || [], + candidates: candidateData, custom: response, - }; - } else { - const result = await genModel - .startChat(updatedChatRequest) - .sendMessage(msg.parts, options); - if (!result.response.candidates?.length) - throw new Error('No valid candidates returned.'); - const responseCandidates = - result.response.candidates.map(fromJSONModeScopedGeminiCandidate) || - []; - return { - candidates: responseCandidates, - custom: result.response, usage: { - ...getBasicUsageStats(request.messages, responseCandidates), - inputTokens: result.response.usageMetadata?.promptTokenCount, - outputTokens: result.response.usageMetadata?.candidatesTokenCount, - totalTokens: result.response.usageMetadata?.totalTokenCount, + ...getBasicUsageStats(request.messages, candidateData), + inputTokens: response.usageMetadata?.promptTokenCount, + outputTokens: response.usageMetadata?.candidatesTokenCount, + totalTokens: response.usageMetadata?.totalTokenCount, }, }; - } + }; + + // If debugTraces is enable, we wrap the actual model call with a span, add raw + // API params as for input. + return debugTraces + ? await runInNewSpan( + ai.registry, + { + metadata: { + name: sendChunk ? 'sendMessageStream' : 'sendMessage', + }, + }, + async (metadata) => { + metadata.input = { + sdk: '@google/generative-ai', + cache: cache, + model: genModel.model, + chatOptions: updatedChatRequest, + parts: msg.parts, + options, + }; + const response = await callGemini(); + metadata.output = response.custom; + return response; + } + ) + : await callGemini(); } ); } diff --git a/js/plugins/googleai/src/index.ts b/js/plugins/googleai/src/index.ts index b2fe4dd975..4a1264f90a 100644 --- a/js/plugins/googleai/src/index.ts +++ b/js/plugins/googleai/src/index.ts @@ -33,6 +33,7 @@ import { gemini15Flash8b, gemini15Pro, gemini20Flash, + gemini20ProExp0205, type GeminiConfig, type GeminiVersionString, } from './gemini.js'; @@ -43,6 +44,7 @@ export { gemini15Flash8b, gemini15Pro, gemini20Flash, + gemini20ProExp0205, textEmbedding004, textEmbeddingGecko001, type GeminiConfig, @@ -57,6 +59,7 @@ export interface PluginOptions { | ModelReference | string )[]; + experimental_debugTraces?: boolean; } /** @@ -76,33 +79,36 @@ export function googleAI(options?: PluginOptions): GenkitPlugin { if (apiVersions.includes('v1beta')) { Object.keys(SUPPORTED_V15_MODELS).forEach((name) => - defineGoogleAIModel( + defineGoogleAIModel({ ai, name, - options?.apiKey, - 'v1beta', - options?.baseUrl - ) + apiKey: options?.apiKey, + apiVersion: 'v1beta', + baseUrl: options?.baseUrl, + debugTraces: options?.experimental_debugTraces, + }) ); } if (apiVersions.includes('v1')) { Object.keys(SUPPORTED_V1_MODELS).forEach((name) => - defineGoogleAIModel( + defineGoogleAIModel({ ai, name, - options?.apiKey, - undefined, - options?.baseUrl - ) + apiKey: options?.apiKey, + apiVersion: undefined, + baseUrl: options?.baseUrl, + debugTraces: options?.experimental_debugTraces, + }) ); Object.keys(SUPPORTED_V15_MODELS).forEach((name) => - defineGoogleAIModel( + defineGoogleAIModel({ ai, name, - options?.apiKey, - undefined, - options?.baseUrl - ) + apiKey: options?.apiKey, + apiVersion: undefined, + baseUrl: options?.baseUrl, + debugTraces: options?.experimental_debugTraces, + }) ); Object.keys(EMBEDDER_MODELS).forEach((name) => defineGoogleAIEmbedder(ai, name, { apiKey: options?.apiKey }) @@ -118,17 +124,17 @@ export function googleAI(options?: PluginOptions): GenkitPlugin { modelOrRef.name.split('/')[1]; const modelRef = typeof modelOrRef === 'string' ? gemini(modelOrRef) : modelOrRef; - defineGoogleAIModel( + defineGoogleAIModel({ ai, - modelName, - options?.apiKey, - undefined, - options?.baseUrl, - { + name: modelName, + apiKey: options?.apiKey, + baseUrl: options?.baseUrl, + info: { ...modelRef.info, label: `Google AI - ${modelName}`, - } - ); + }, + debugTraces: options?.experimental_debugTraces, + }); } } }); diff --git a/js/plugins/googleai/tests/gemini_test.ts b/js/plugins/googleai/tests/gemini_test.ts index 0ce663c973..88b7944358 100644 --- a/js/plugins/googleai/tests/gemini_test.ts +++ b/js/plugins/googleai/tests/gemini_test.ts @@ -16,8 +16,9 @@ import { GenerateContentCandidate } from '@google/generative-ai'; import * as assert from 'assert'; -import { genkit } from 'genkit'; +import { genkit, z } from 'genkit'; import { MessageData, ModelInfo } from 'genkit/model'; +import { toJsonSchema } from 'genkit/schema'; import { afterEach, beforeEach, describe, it } from 'node:test'; import { GENERIC_GEMINI_MODEL, @@ -28,6 +29,7 @@ import { gemini15Pro, toGeminiMessage, toGeminiSystemInstruction, + toGeminiTool, } from '../src/gemini.js'; import { googleAI } from '../src/index.js'; @@ -501,6 +503,64 @@ describe('plugin', () => { }); }); +describe('toGeminiTool', () => { + it('', async () => { + const got = toGeminiTool({ + name: 'foo', + description: 'tool foo', + inputSchema: toJsonSchema({ + schema: z.object({ + simpleString: z.string().describe('a string').nullable(), + simpleNumber: z.number().describe('a number'), + simpleBoolean: z.boolean().describe('a boolean').optional(), + simpleArray: z.array(z.string()).describe('an array').optional(), + simpleEnum: z + .enum(['choice_a', 'choice_b']) + .describe('an enum') + .optional(), + }), + }), + }); + + const want = { + description: 'tool foo', + name: 'foo', + parameters: { + properties: { + simpleArray: { + description: 'an array', + items: { + type: 'string', + }, + type: 'array', + }, + simpleBoolean: { + description: 'a boolean', + type: 'boolean', + }, + simpleEnum: { + description: 'an enum', + enum: ['choice_a', 'choice_b'], + type: 'string', + }, + simpleNumber: { + description: 'a number', + type: 'number', + }, + simpleString: { + description: 'a string', + nullable: true, + type: 'string', + }, + }, + required: ['simpleString', 'simpleNumber'], + type: 'object', + }, + }; + assert.deepStrictEqual(got, want); + }); +}); + function assertEqualModelInfo( modelAction: ModelInfo, expectedLabel: string, diff --git a/js/plugins/langchain/package.json b/js/plugins/langchain/package.json index 54a96b2afc..7f530fe9b4 100644 --- a/js/plugins/langchain/package.json +++ b/js/plugins/langchain/package.json @@ -9,7 +9,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/mcp/package.json b/js/plugins/mcp/package.json index 838e6ca85e..5d14baeb89 100644 --- a/js/plugins/mcp/package.json +++ b/js/plugins/mcp/package.json @@ -9,7 +9,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "description": "A Genkit plugin that provides interoperability between Genkit and Model Context Protocol (MCP). Both client and server use cases are supported.", "main": "dist/index.js", "types": "./lib/index.d.ts", diff --git a/js/plugins/next/package.json b/js/plugins/next/package.json index 6d39ab3287..993bc921c0 100644 --- a/js/plugins/next/package.json +++ b/js/plugins/next/package.json @@ -14,7 +14,7 @@ "next.js", "react" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "main": "lib/index.js", "scripts": { diff --git a/js/plugins/ollama/README.md b/js/plugins/ollama/README.md index 2b173ef992..16917a15ba 100644 --- a/js/plugins/ollama/README.md +++ b/js/plugins/ollama/README.md @@ -21,10 +21,15 @@ const ai = genkit({ ], }); -(async () => { - const { text } = ai.generate({prompt: 'hi Gemini!', model: 'ollama/gemma'); +async function main() { + const { text } = await ai.generate({ + prompt: 'hi Gemini!', + model: 'ollama/gemma', + }); console.log(text); -}); +} + +main(); ``` The sources for this package are in the main [Genkit](https://github.com/firebase/genkit) repo. Please file issues and pull requests against that repo. diff --git a/js/plugins/ollama/package.json b/js/plugins/ollama/package.json index 9ca4f01f8d..6c09b1b51b 100644 --- a/js/plugins/ollama/package.json +++ b/js/plugins/ollama/package.json @@ -10,7 +10,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/pinecone/package.json b/js/plugins/pinecone/package.json index 5e63ff0e75..6317d92984 100644 --- a/js/plugins/pinecone/package.json +++ b/js/plugins/pinecone/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/vertexai/package.json b/js/plugins/vertexai/package.json index d9ff39189f..2e3ca5e4a2 100644 --- a/js/plugins/vertexai/package.json +++ b/js/plugins/vertexai/package.json @@ -17,7 +17,7 @@ "genai", "generative-ai" ], - "version": "1.0.4", + "version": "1.0.5", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/vertexai/src/common/types.ts b/js/plugins/vertexai/src/common/types.ts index 642f5980a3..70af9dc9f4 100644 --- a/js/plugins/vertexai/src/common/types.ts +++ b/js/plugins/vertexai/src/common/types.ts @@ -26,6 +26,8 @@ export interface CommonPluginOptions { location: string; /** Provide custom authentication configuration for connecting to Vertex AI. */ googleAuth?: GoogleAuthOptions; + /** Enables additional debug traces (e.g. raw model API call details). */ + experimental_debugTraces?: boolean; } /** Combined plugin options, extending common options with subplugin-specific options */ diff --git a/js/plugins/vertexai/src/gemini.ts b/js/plugins/vertexai/src/gemini.ts index 3a5938329d..303366f30c 100644 --- a/js/plugins/vertexai/src/gemini.ts +++ b/js/plugins/vertexai/src/gemini.ts @@ -25,13 +25,20 @@ import { GenerativeModelPreview, HarmBlockThreshold, HarmCategory, + Schema, StartChatParams, ToolConfig, VertexAI, type GoogleSearchRetrieval, } from '@google-cloud/vertexai'; import { ApiClient } from '@google-cloud/vertexai/build/src/resources/index.js'; -import { GENKIT_CLIENT_HEADER, Genkit, JSONSchema, z } from 'genkit'; +import { + GENKIT_CLIENT_HEADER, + Genkit, + GenkitError, + JSONSchema, + z, +} from 'genkit'; import { CandidateData, GenerateRequest, @@ -51,6 +58,7 @@ import { downloadRequestMedia, simulateSystemPrompt, } from 'genkit/model/middleware'; +import { runInNewSpan } from 'genkit/tracing'; import { GoogleAuth } from 'google-auth-library'; import { PluginOptions } from './common/types.js'; import { handleCacheIfNeeded } from './context-caching/index.js'; @@ -424,7 +432,8 @@ function toGeminiRole( } } -const toGeminiTool = ( +/** @hidden */ +export const toGeminiTool = ( tool: z.infer ): FunctionDeclaration => { const declaration: FunctionDeclaration = { @@ -645,31 +654,65 @@ export function fromGeminiCandidate( // Translate JSON schema to Vertex AI's format. Specifically, the type field needs be mapped. // Since JSON schemas can include nested arrays/objects, we have to recursively map the type field // in all nested fields. -const convertSchemaProperty = (property) => { +function convertSchemaProperty(property) { if (!property || !property.type) { - return null; + return undefined; + } + const baseSchema = {} as Schema; + if (property.description) { + baseSchema.description = property.description; + } + if (property.enum) { + baseSchema.enum = property.enum; + } + if (property.nullable) { + baseSchema.nullable = property.nullable; } - if (property.type === 'object') { + let propertyType; + // nullable schema can ALSO be defined as, for example, type=['string','null'] + if (Array.isArray(property.type)) { + const types = property.type as string[]; + if (types.includes('null')) { + baseSchema.nullable = true; + } + // grab the type that's not `null` + propertyType = types.find((t) => t !== 'null'); + } else { + propertyType = property.type; + } + if (propertyType === 'object') { const nestedProperties = {}; Object.keys(property.properties).forEach((key) => { nestedProperties[key] = convertSchemaProperty(property.properties[key]); }); return { + ...baseSchema, type: FunctionDeclarationSchemaType.OBJECT, properties: nestedProperties, required: property.required, }; - } else if (property.type === 'array') { + } else if (propertyType === 'array') { return { + ...baseSchema, type: FunctionDeclarationSchemaType.ARRAY, items: convertSchemaProperty(property.items), }; } else { + const schemaType = FunctionDeclarationSchemaType[ + propertyType.toUpperCase() + ] as FunctionDeclarationSchemaType; + if (!schemaType) { + throw new GenkitError({ + status: 'INVALID_ARGUMENT', + message: `Unsupported property type ${propertyType.toUpperCase()}`, + }); + } return { - type: FunctionDeclarationSchemaType[property.type.toUpperCase()], + ...baseSchema, + type: schemaType, }; } -}; +} export function cleanSchema(schema: JSONSchema): JSONSchema { const out = structuredClone(schema); @@ -700,36 +743,47 @@ export function defineGeminiKnownModel( vertexClientFactory: ( request: GenerateRequest ) => VertexAI, - options: PluginOptions + options: PluginOptions, + debugTraces?: boolean ): ModelAction { const modelName = `vertexai/${name}`; const model: ModelReference = SUPPORTED_GEMINI_MODELS[name]; if (!model) throw new Error(`Unsupported model: ${name}`); - return defineGeminiModel( + return defineGeminiModel({ ai, modelName, - name, - model?.info, + version: name, + modelInfo: model?.info, vertexClientFactory, - options - ); + options, + debugTraces, + }); } /** * Define a Vertex AI Gemini model. */ -export function defineGeminiModel( - ai: Genkit, - modelName: string, - version: string, - modelInfo: ModelInfo | undefined, +export function defineGeminiModel({ + ai, + modelName, + version, + modelInfo, + vertexClientFactory, + options, + debugTraces, +}: { + ai: Genkit; + modelName: string; + version: string; + modelInfo: ModelInfo | undefined; vertexClientFactory: ( request: GenerateRequest - ) => VertexAI, - options: PluginOptions -): ModelAction { + ) => VertexAI; + options: PluginOptions; + debugTraces?: boolean; +}): ModelAction { const middlewares: ModelMiddleware[] = []; if (SUPPORTED_V1_MODELS[version]) { middlewares.push(simulateSystemPrompt()); @@ -746,7 +800,7 @@ export function defineGeminiModel( configSchema: GeminiConfigSchema, use: middlewares, }, - async (request, streamingCallback) => { + async (request, sendChunk) => { const vertex = vertexClientFactory(request); // Make a copy of messages to avoid side-effects @@ -884,57 +938,84 @@ export function defineGeminiModel( ); } - // Handle streaming and non-streaming responses - if (streamingCallback) { - const result = await genModel - .startChat(updatedChatRequest) - .sendMessageStream(msg.parts); - - for await (const item of result.stream) { - (item as GenerateContentResponse).candidates?.forEach((candidate) => { - const c = fromGeminiCandidate(candidate, jsonMode); - streamingCallback({ - index: c.index, - content: c.message.content, - }); - }); - } + const callGemini = async () => { + let response: GenerateContentResponse; + + // Handle streaming and non-streaming responses + if (sendChunk) { + const result = await genModel + .startChat(updatedChatRequest) + .sendMessageStream(msg.parts); + + for await (const item of result.stream) { + (item as GenerateContentResponse).candidates?.forEach( + (candidate) => { + const c = fromGeminiCandidate(candidate, jsonMode); + sendChunk({ + index: c.index, + content: c.message.content, + }); + } + ); + } - const response = await result.response; - if (!response.candidates?.length) { - throw new Error('No valid candidates returned.'); - } + response = await result.response; + } else { + const result = await genModel + .startChat(updatedChatRequest) + .sendMessage(msg.parts); - return { - candidates: response.candidates.map((c) => - fromGeminiCandidate(c, jsonMode) - ), - custom: response, - }; - } else { - const result = await genModel - .startChat(updatedChatRequest) - .sendMessage(msg.parts); + response = result.response; + } - if (!result?.response.candidates?.length) { - throw new Error('No valid candidates returned.'); + if (!response.candidates?.length) { + throw new GenkitError({ + status: 'FAILED_PRECONDITION', + message: 'No valid candidates returned.', + }); } - const responseCandidates = result.response.candidates.map((c) => + const candidateData = response.candidates.map((c) => fromGeminiCandidate(c, jsonMode) ); return { - candidates: responseCandidates, - custom: result.response, + candidates: candidateData, + custom: response, usage: { - ...getBasicUsageStats(request.messages, responseCandidates), - inputTokens: result.response.usageMetadata?.promptTokenCount, - outputTokens: result.response.usageMetadata?.candidatesTokenCount, - totalTokens: result.response.usageMetadata?.totalTokenCount, + ...getBasicUsageStats(request.messages, candidateData), + inputTokens: response.usageMetadata?.promptTokenCount, + outputTokens: response.usageMetadata?.candidatesTokenCount, + totalTokens: response.usageMetadata?.totalTokenCount, }, }; - } + }; + + // If debugTraces is enable, we wrap the actual model call with a span, add raw + // API params as for input. + return debugTraces + ? await runInNewSpan( + ai.registry, + { + metadata: { + name: sendChunk ? 'sendMessageStream' : 'sendMessage', + }, + }, + async (metadata) => { + metadata.input = { + sdk: '@google-cloud/vertexai', + cache: cache, + model: genModel.getModelName(), + chatOptions: updatedChatRequest, + parts: msg.parts, + options, + }; + const response = await callGemini(); + metadata.output = response.custom; + return response; + } + ) + : await callGemini(); } ); } diff --git a/js/plugins/vertexai/src/index.ts b/js/plugins/vertexai/src/index.ts index bfd1c74c94..eda1df60ae 100644 --- a/js/plugins/vertexai/src/index.ts +++ b/js/plugins/vertexai/src/index.ts @@ -87,10 +87,16 @@ export function vertexAI(options?: PluginOptions): GenkitPlugin { imagenModel(ai, name, authClient, { projectId, location }) ); Object.keys(SUPPORTED_GEMINI_MODELS).map((name) => - defineGeminiKnownModel(ai, name, vertexClientFactory, { - projectId, - location, - }) + defineGeminiKnownModel( + ai, + name, + vertexClientFactory, + { + projectId, + location, + }, + options?.experimental_debugTraces + ) ); if (options?.models) { for (const modelOrRef of options?.models) { @@ -101,17 +107,18 @@ export function vertexAI(options?: PluginOptions): GenkitPlugin { modelOrRef.name.split('/')[1]; const modelRef = typeof modelOrRef === 'string' ? gemini(modelOrRef) : modelOrRef; - defineGeminiModel( + defineGeminiModel({ ai, - modelRef.name, - modelName, - modelRef.info, + modelName: modelRef.name, + version: modelName, + modelInfo: modelRef.info, vertexClientFactory, - { + options: { projectId, location, - } - ); + }, + debugTraces: options.experimental_debugTraces, + }); } } diff --git a/js/plugins/vertexai/src/vectorsearch/index.ts b/js/plugins/vertexai/src/vectorsearch/index.ts index 1a8ddca75a..bbd9d47ba9 100644 --- a/js/plugins/vertexai/src/vectorsearch/index.ts +++ b/js/plugins/vertexai/src/vectorsearch/index.ts @@ -19,12 +19,8 @@ import { GenkitPlugin, genkitPlugin } from 'genkit/plugin'; import { getDerivedParams } from '../common/index.js'; import { PluginOptions } from './types.js'; import { vertexAiIndexers, vertexAiRetrievers } from './vector_search/index.js'; -export { PluginOptions } from '../common/types.js'; +export { type PluginOptions } from '../common/types.js'; export { - DocumentIndexer, - DocumentRetriever, - Neighbor, - VectorSearchOptions, getBigQueryDocumentIndexer, getBigQueryDocumentRetriever, getFirestoreDocumentIndexer, @@ -33,6 +29,10 @@ export { vertexAiIndexers, vertexAiRetrieverRef, vertexAiRetrievers, + type DocumentIndexer, + type DocumentRetriever, + type Neighbor, + type VectorSearchOptions, } from './vector_search/index.js'; /** * Add Google Cloud Vertex AI to Genkit. Includes Gemini and Imagen models and text embedder. diff --git a/js/plugins/vertexai/src/vectorsearch/vector_search/index.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/index.ts index 638ba1abc2..cc4d065e1c 100644 --- a/js/plugins/vertexai/src/vectorsearch/vector_search/index.ts +++ b/js/plugins/vertexai/src/vectorsearch/vector_search/index.ts @@ -17,20 +17,20 @@ export { getBigQueryDocumentIndexer, getBigQueryDocumentRetriever, -} from './bigquery'; +} from './bigquery.js'; export { getFirestoreDocumentIndexer, getFirestoreDocumentRetriever, -} from './firestore'; -export { vertexAiIndexerRef, vertexAiIndexers } from './indexers'; -export { vertexAiRetrieverRef, vertexAiRetrievers } from './retrievers'; +} from './firestore.js'; +export { vertexAiIndexerRef, vertexAiIndexers } from './indexers.js'; +export { vertexAiRetrieverRef, vertexAiRetrievers } from './retrievers.js'; export { - DocumentIndexer, - DocumentRetriever, - Neighbor, - VectorSearchOptions, - VertexAIVectorIndexerOptions, VertexAIVectorIndexerOptionsSchema, - VertexAIVectorRetrieverOptions, VertexAIVectorRetrieverOptionsSchema, -} from './types'; + type DocumentIndexer, + type DocumentRetriever, + type Neighbor, + type VectorSearchOptions, + type VertexAIVectorIndexerOptions, + type VertexAIVectorRetrieverOptions, +} from './types.js'; diff --git a/js/plugins/vertexai/tests/gemini_test.ts b/js/plugins/vertexai/tests/gemini_test.ts index aeb388d377..4df3a71319 100644 --- a/js/plugins/vertexai/tests/gemini_test.ts +++ b/js/plugins/vertexai/tests/gemini_test.ts @@ -16,13 +16,15 @@ import { GenerateContentCandidate } from '@google-cloud/vertexai'; import * as assert from 'assert'; -import { MessageData } from 'genkit'; +import { MessageData, z } from 'genkit'; +import { toJsonSchema } from 'genkit/schema'; import { describe, it } from 'node:test'; import { cleanSchema, fromGeminiCandidate, toGeminiMessage, toGeminiSystemInstruction, + toGeminiTool, } from '../src/gemini.js'; describe('toGeminiMessages', () => { @@ -381,3 +383,61 @@ describe('cleanSchema', () => { }); }); }); + +describe('toGeminiTool', () => { + it('', async () => { + const got = toGeminiTool({ + name: 'foo', + description: 'tool foo', + inputSchema: toJsonSchema({ + schema: z.object({ + simpleString: z.string().describe('a string').nullable(), + simpleNumber: z.number().describe('a number'), + simpleBoolean: z.boolean().describe('a boolean').optional(), + simpleArray: z.array(z.string()).describe('an array').optional(), + simpleEnum: z + .enum(['choice_a', 'choice_b']) + .describe('an enum') + .optional(), + }), + }), + }); + + const want = { + description: 'tool foo', + name: 'foo', + parameters: { + properties: { + simpleArray: { + description: 'an array', + items: { + type: 'STRING', + }, + type: 'ARRAY', + }, + simpleBoolean: { + description: 'a boolean', + type: 'BOOLEAN', + }, + simpleEnum: { + description: 'an enum', + enum: ['choice_a', 'choice_b'], + type: 'STRING', + }, + simpleNumber: { + description: 'a number', + type: 'NUMBER', + }, + simpleString: { + description: 'a string', + nullable: true, + type: 'STRING', + }, + }, + required: ['simpleString', 'simpleNumber'], + type: 'OBJECT', + }, + }; + assert.deepStrictEqual(got, want); + }); +}); diff --git a/js/testapps/context-caching/src/index.ts b/js/testapps/context-caching/src/index.ts index af5ab6f6f1..8c54c378a4 100644 --- a/js/testapps/context-caching/src/index.ts +++ b/js/testapps/context-caching/src/index.ts @@ -24,7 +24,10 @@ import { genkit, z } from 'genkit'; // Import Genkit framework and Zod for schem import { logger } from 'genkit/logging'; // Import logging utility from Genkit const ai = genkit({ - plugins: [vertexAI(), googleAI()], // Initialize Genkit with the Google AI plugin + plugins: [ + vertexAI({ experimental_debugTraces: true, location: 'us-central1' }), + googleAI({ experimental_debugTraces: true }), + ], // Initialize Genkit with the Google AI plugin }); logger.setLogLevel('debug'); // Set the logging level to debug for detailed output @@ -38,7 +41,7 @@ export const lotrFlowVertex = ai.defineFlow( }), outputSchema: z.string(), // Define the expected output as a string }, - async ({ query, textFilePath }) => { + async ({ query, textFilePath }, { sendChunk }) => { const defaultQuery = 'What is the text i provided you with?'; // Default query to use if none is provided // Read the content from the file if the path is provided @@ -69,6 +72,7 @@ export const lotrFlowVertex = ai.defineFlow( }, model: gemini15Flash, // Specify the model (gemini15Flash) to use for generation prompt: query || defaultQuery, // Use the provided query or fall back to the default query + onChunk: sendChunk, }); return llmResponse.text; // Return the generated text from the model @@ -84,7 +88,7 @@ export const lotrFlowGoogleAI = ai.defineFlow( }), outputSchema: z.string(), // Define the expected output as a string }, - async ({ query, textFilePath }) => { + async ({ query, textFilePath }, { sendChunk }) => { const defaultQuery = 'What is the text i provided you with?'; // Default query to use if none is provided // Read the content from the file if the path is provided @@ -115,6 +119,7 @@ export const lotrFlowGoogleAI = ai.defineFlow( }, model: gemini15FlashGoogleAI, // Specify the model (gemini15Flash) to use for generation prompt: query || defaultQuery, // Use the provided query or fall back to the default query + onChunk: sendChunk, }); return llmResponse.text; // Return the generated text from the model diff --git a/js/testapps/dev-ui-gallery/src/genkit.ts b/js/testapps/dev-ui-gallery/src/genkit.ts index e61aa99201..dba03fc5f3 100644 --- a/js/testapps/dev-ui-gallery/src/genkit.ts +++ b/js/testapps/dev-ui-gallery/src/genkit.ts @@ -80,6 +80,7 @@ export const PERMISSIVE_SAFETY_SETTINGS: any = { }; export const ai = genkit({ + model: gemini15Flash, // load at least one plugin representing each action type plugins: [ // model providers diff --git a/js/testapps/dev-ui-gallery/src/main/flows-firebase-functions.ts b/js/testapps/dev-ui-gallery/src/main/flows-firebase-functions.ts index 01153ec244..83d17b39ca 100644 --- a/js/testapps/dev-ui-gallery/src/main/flows-firebase-functions.ts +++ b/js/testapps/dev-ui-gallery/src/main/flows-firebase-functions.ts @@ -27,9 +27,11 @@ const greetFlow = ai.defineFlow( inputSchema: z.string(), outputSchema: z.string(), }, - async (langauge: string) => { - const { output } = await ai.generate(`Say hello in language ${langauge}`); - return output; + async (language: string) => { + const { text } = await ai.generate({ + prompt: `Say hello in language ${language}`, + }); + return text; } ); diff --git a/js/testapps/docs-menu-rag/src/genkit.ts b/js/testapps/docs-menu-rag/src/genkit.ts new file mode 100644 index 0000000000..dc39ac0220 --- /dev/null +++ b/js/testapps/docs-menu-rag/src/genkit.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { devLocalVectorstore } from '@genkit-ai/dev-local-vectorstore'; +import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; +import { genkit } from 'genkit'; + +export const ai = genkit({ + plugins: [ + vertexAI(), + devLocalVectorstore([ + { + indexName: 'menuQA', + embedder: textEmbedding004, + }, + ]), + ], +}); diff --git a/js/testapps/docs-menu-rag/src/index.ts b/js/testapps/docs-menu-rag/src/index.ts index 377bdf110f..84a3ec0ba1 100644 --- a/js/testapps/docs-menu-rag/src/index.ts +++ b/js/testapps/docs-menu-rag/src/index.ts @@ -14,23 +14,10 @@ * limitations under the License. */ -import { devLocalVectorstore } from '@genkit-ai/dev-local-vectorstore'; -import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; -import { genkit, z } from 'genkit'; +import { z } from 'genkit'; +import { ai } from './genkit.js'; import { indexMenu } from './indexer'; -export const ai = genkit({ - plugins: [ - vertexAI(), - devLocalVectorstore([ - { - indexName: 'menuQA', - embedder: textEmbedding004, - }, - ]), - ], -}); - const menus = ['./docs/GenkitGrubPub.pdf']; // genkit flow:run setup diff --git a/js/testapps/docs-menu-rag/src/indexer.ts b/js/testapps/docs-menu-rag/src/indexer.ts index 725c88deaf..fbca147c72 100644 --- a/js/testapps/docs-menu-rag/src/indexer.ts +++ b/js/testapps/docs-menu-rag/src/indexer.ts @@ -21,7 +21,7 @@ import { Document } from 'genkit/retriever'; import { chunk } from 'llm-chunk'; import path from 'path'; import pdf from 'pdf-parse'; -import { ai } from './index.js'; +import { ai } from './genkit.js'; // Create a reference to the configured local indexer. export const menuPdfIndexer = devLocalIndexerRef('menuQA'); diff --git a/js/testapps/docs-menu-rag/src/menuQA.ts b/js/testapps/docs-menu-rag/src/menuQA.ts index 364409d420..cc942f2684 100644 --- a/js/testapps/docs-menu-rag/src/menuQA.ts +++ b/js/testapps/docs-menu-rag/src/menuQA.ts @@ -17,7 +17,7 @@ import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore'; import { gemini15Flash } from '@genkit-ai/vertexai'; import { z } from 'genkit'; -import { ai } from './index.js'; +import { ai } from './genkit.js'; // Define the retriever reference export const menuRetriever = devLocalRetrieverRef('menuQA'); diff --git a/js/testapps/flow-sample1/src/index.ts b/js/testapps/flow-sample1/src/index.ts index d2ac1a21ef..25cb6a2b44 100644 --- a/js/testapps/flow-sample1/src/index.ts +++ b/js/testapps/flow-sample1/src/index.ts @@ -80,7 +80,7 @@ export const streamy = ai.defineFlow( } ); -// genkit flow:run streamy 5 -s +// genkit flow:run streamyThrowy 5 -s export const streamyThrowy = ai.defineFlow( { name: 'streamyThrowy', diff --git a/js/testapps/flow-simple-ai/src/index.ts b/js/testapps/flow-simple-ai/src/index.ts index 79da26e118..ab550425f0 100644 --- a/js/testapps/flow-simple-ai/src/index.ts +++ b/js/testapps/flow-simple-ai/src/index.ts @@ -21,12 +21,16 @@ import { googleAI, gemini10Pro as googleGemini10Pro, } from '@genkit-ai/googleai'; -import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; +import { + textEmbedding004, + vertexAI, + gemini15Flash as vertexGemini15Flash, +} from '@genkit-ai/vertexai'; import { GoogleAIFileManager } from '@google/generative-ai/server'; import { AlwaysOnSampler } from '@opentelemetry/sdk-trace-base'; import { initializeApp } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore'; -import { GenerateResponseData, MessageSchema, genkit, z } from 'genkit'; +import { GenerateResponseData, MessageSchema, genkit, z } from 'genkit/beta'; import { logger } from 'genkit/logging'; import { ModelMiddleware, simulateConstrainedGeneration } from 'genkit/model'; import { PluginProvider } from 'genkit/plugin'; @@ -52,7 +56,10 @@ enableGoogleCloudTelemetry({ }); const ai = genkit({ - plugins: [googleAI(), vertexAI()], + plugins: [ + googleAI({ experimental_debugTraces: true }), + vertexAI({ location: 'us-central1', experimental_debugTraces: true }), + ], }); const math: PluginProvider = { @@ -386,7 +393,11 @@ const gablorkenTool = ai.defineTool( { name: 'gablorkenTool', inputSchema: z.object({ - value: z.number(), + value: z + .number() + .describe( + 'always add 1 to the value (it is 1 based, but upstream it is zero based)' + ), }), description: 'can be used to calculate gablorken value', }, @@ -395,6 +406,23 @@ const gablorkenTool = ai.defineTool( } ); +const characterGenerator = ai.defineTool( + { + name: 'characterGenerator', + inputSchema: z.object({ + age: z.number().describe('must be between 23 and 27'), + type: z.enum(['archer', 'banana']), + name: z.string().describe('can only be Bob or John'), + surname: z.string(), + }), + description: + 'can be used to generate a character. Seed it with some input.', + }, + async (input) => { + return input; + } +); + export const toolCaller = ai.defineFlow( { name: 'toolCaller', @@ -427,8 +455,8 @@ const exitTool = ai.defineTool( }), description: 'call this tool when you have the final answer', }, - async (input) => { - throw new Error(`Answer: ${input.answer}`); + async (input, { interrupt }) => { + interrupt(); } ); @@ -436,12 +464,11 @@ export const forcedToolCaller = ai.defineFlow( { name: 'forcedToolCaller', inputSchema: z.number(), - outputSchema: z.string(), streamSchema: z.any(), }, async (input, { sendChunk }) => { const { response, stream } = ai.generateStream({ - model: gemini15Flash, + model: vertexGemini15Flash, config: { temperature: 1, }, @@ -454,7 +481,32 @@ export const forcedToolCaller = ai.defineFlow( sendChunk(chunk); } - return (await response).text; + return await response; + } +); + +export const toolCallerCharacterGenerator = ai.defineFlow( + { + name: 'toolCallerCharacterGenerator', + inputSchema: z.number(), + streamSchema: z.any(), + }, + async (input, { sendChunk }) => { + const { response, stream } = ai.generateStream({ + model: vertexGemini15Flash, + config: { + temperature: 1, + }, + tools: [characterGenerator, exitTool], + toolChoice: 'required', + prompt: `generate an archer character`, + }); + + for await (const chunk of stream) { + sendChunk(chunk); + } + + return await response; } ); @@ -654,3 +706,87 @@ ai.defineFlow('blockingMiddleware', async () => { }); return text; }); + +ai.defineFlow('formatJson', async (input, { sendChunk }) => { + const { output, text } = await ai.generate({ + model: gemini15Flash, + prompt: `generate an RPG game character of type ${input || 'archer'}`, + output: { + constrained: true, + instructions: true, + schema: z + .object({ + name: z.string(), + weapon: z.string(), + }) + .strict(), + }, + onChunk: (c) => sendChunk(c.output), + }); + return { output, text }; +}); + +ai.defineFlow('formatJsonManualSchema', async (input, { sendChunk }) => { + const { output, text } = await ai.generate({ + model: gemini15Flash, + prompt: `generate one RPG game character of type ${input || 'archer'} and generated JSON must match this interface + + \`\`\`typescript + interface Character { + name: string; + weapon: string; + } + \`\`\` + `, + output: { + constrained: true, + instructions: false, + schema: z + .object({ + name: z.string(), + weapon: z.string(), + }) + .strict(), + }, + onChunk: (c) => sendChunk(c.output), + }); + return { output, text }; +}); + +ai.defineFlow('testArray', async (input, { sendChunk }) => { + const { output } = await ai.generate({ + prompt: `10 different weapons for ${input}`, + output: { + format: 'array', + schema: z.array(z.string()), + }, + onChunk: (c) => sendChunk(c.output), + }); + return output; +}); + +ai.defineFlow('formatEnum', async (input, { sendChunk }) => { + const { output } = await ai.generate({ + prompt: `classify the denger level of sky diving`, + output: { + format: 'enum', + schema: z.enum(['safe', 'dangerous', 'medium']), + }, + onChunk: (c) => sendChunk(c.output), + }); + return output; +}); + +ai.defineFlow('formatJsonl', async (input, { sendChunk }) => { + const { output } = await ai.generate({ + prompt: `generate 5 randon persons`, + output: { + format: 'jsonl', + schema: z.array( + z.object({ name: z.string(), surname: z.string() }).strict() + ), + }, + onChunk: (c) => sendChunk(c.output), + }); + return output; +}); diff --git a/js/testapps/format-tester/src/index.ts b/js/testapps/format-tester/src/index.ts index 84dcfb5905..59716c1629 100644 --- a/js/testapps/format-tester/src/index.ts +++ b/js/testapps/format-tester/src/index.ts @@ -154,6 +154,7 @@ if (!models.length) { 'vertexai/gemini-1.5-flash', 'googleai/gemini-1.5-pro', 'googleai/gemini-1.5-flash', + 'googleai/gemini-2.0-flash', ]; } diff --git a/package.json b/package.json index 31b5f16af1..fb14278ec8 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "setup": "npm-run-all pnpm-install-js pnpm-install-genkit-tools build link-genkit-cli", "format": "(prettier . --write) && (tsx scripts/copyright.ts)", "format:check": "(prettier . --check) && (tsx scripts/copyright.ts --check)", + "format:todo": "pnpm dlx @biomejs/biome format --write .", "build": "pnpm build:js && pnpm build:genkit-tools", "build:js": "cd js && pnpm i && pnpm build", "build:genkit-tools": "cd genkit-tools && pnpm i && pnpm build", diff --git a/py/bin/build_dists b/py/bin/build_dists index 504136680c..6a6d3a361a 100755 --- a/py/bin/build_dists +++ b/py/bin/build_dists @@ -15,9 +15,7 @@ fi TOP_DIR=$(git rev-parse --show-toplevel) PROJECT_DIRS=( - "packages/dotprompt" "packages/genkit" - "packages/handlebarz" "plugins/chroma" "plugins/firebase" "plugins/google-ai" diff --git a/py/bin/format_toml_files b/py/bin/format_toml_files deleted file mode 100755 index eef13cf5fa..0000000000 --- a/py/bin/format_toml_files +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -# -# Format all TOML files in the project. -# -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - -set -euo pipefail - -GIT_ROOT=$(git rev-parse --show-toplevel) - -if command -v rust-parallel >/dev/null 2>&1; then - if command -v fd >/dev/null 2>&1; then - fd -e toml \ - --exclude 'py/**/*.egg-info/**' \ - --exclude 'py/**/.dist/**' \ - --exclude 'py/**/.next/**' \ - --exclude 'py/**/.output/**' \ - --exclude 'py/**/.pytest_cache/**' \ - --exclude 'py/**/.venv/**' \ - --exclude 'py/**/__pycache__/**' \ - --exclude 'py/**/build/**' \ - --exclude 'py/**/develop-eggs/**' \ - --exclude 'py/**/dist/**' \ - --exclude 'py/**/eggs/**' \ - --exclude 'py/**/node_modules/**' \ - --exclude 'py/**/sdist/**' \ - --exclude 'py/**/site/**' \ - --exclude 'py/**/target/**' \ - --exclude 'py/**/venv/**' \ - --exclude 'py/**/wheels/**' | - rust-parallel -j4 \ - taplo format --config "${GIT_ROOT}/py/taplo.toml" - else - echo "Using find" - find "${GIT_ROOT}" -name "*.toml" \ - ! -path 'py/**/*.egg-info/**' \ - ! -path 'py/**/.dist/**' \ - ! -path 'py/**/.next/**' \ - ! -path 'py/**/.output/**' \ - ! -path 'py/**/.pytest_cache/**' \ - ! -path 'py/**/.venv/**' \ - ! -path 'py/**/__pycache__/**' \ - ! -path 'py/**/build/**' \ - ! -path 'py/**/develop-eggs/**' \ - ! -path 'py/**/dist/**' \ - ! -path 'py/**/eggs/**' \ - ! -path 'py/**/node_modules/**' \ - ! -path 'py/**/sdist/**' \ - ! -path 'py/**/site/**' \ - ! -path 'py/**/target/**' \ - ! -path 'py/**/venv/**' \ - ! -path 'py/**/wheels/**' \ - -print0 | - rust-parallel -j4 \ - taplo format --config "${GIT_ROOT}/py/taplo.toml" - fi -else - echo "Please install GNU parallel to use this script" -fi diff --git a/py/bin/generate_schema_types b/py/bin/generate_schema_types deleted file mode 100755 index 03ff2be788..0000000000 --- a/py/bin/generate_schema_types +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - -set -euo pipefail - -TOP_DIR=$(git rev-parse --show-toplevel) -SCHEMA_FILE="$TOP_DIR/py/packages/genkit/src/genkit/core/schemas.py" - -# Generate types using configuration from pyproject.toml -uv run --directory "$TOP_DIR/py" datamodel-codegen - -# This isn't causing runtime errors at the moment so letting it be. -#sed -i '' '/^class Model(RootModel\[Any\]):$/,/^ root: Any$/d' "$SCHEMA_FILE" - -# Sanitize the generated schema. -python3 "${TOP_DIR}/py/bin/sanitize_schemas.py" "$SCHEMA_FILE" - -# Add a generated by `generate_schema_types` comment. -sed -i '' '1i\ -# DO NOT EDIT: Generated by `generate_schema_types` from `genkit-schemas.json`. -' "$SCHEMA_FILE" - -# Add license header. -addlicense \ - -c "Google LLC" \ - -s=only \ - "$SCHEMA_FILE" - -# Checks and formatting. -uv run --directory "$TOP_DIR/py" \ - ruff check --fix "$SCHEMA_FILE" -uv run --directory "$TOP_DIR/py" \ - ruff format "$SCHEMA_FILE" diff --git a/py/bin/generate_schema_typing b/py/bin/generate_schema_typing new file mode 100755 index 0000000000..2d7ffb26fd --- /dev/null +++ b/py/bin/generate_schema_typing @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +# +# The convention in Python used by several libraries for names of modules +# containing type hints is `typing`. For example, the ASGI ref module from +# Django called `asgiref` also uses this convention. + +set -euo pipefail + +TOP_DIR=$(git rev-parse --show-toplevel) +TYPING_FILE="${TOP_DIR}/py/packages/genkit/src/genkit/core/typing.py" + +# Generate types using configuration from pyproject.toml +uv run --directory "${TOP_DIR}/py" datamodel-codegen + +# This isn't causing runtime errors at the moment so letting it be. +#sed -i '' '/^class Model(RootModel\[Any\]):$/,/^ root: Any$/d' "${TYPING_FILE}" + +# Sanitize the generated schema. +python3 "${TOP_DIR}/py/bin/sanitize_schema_typing.py" "${TYPING_FILE}" + +# Checks and formatting. +uv run --directory "${TOP_DIR}/py" \ + ruff format "${TOP_DIR}" +uv run --directory "${TOP_DIR}/py" \ + ruff check --fix "${TYPING_FILE}" +uv run --directory "${TOP_DIR}/py" \ + ruff format "${TOP_DIR}" diff --git a/py/bin/run_tests b/py/bin/run_python_tests similarity index 96% rename from py/bin/run_tests rename to py/bin/run_python_tests index 7fe1339e79..2fefe34ce4 100755 --- a/py/bin/run_tests +++ b/py/bin/run_python_tests @@ -15,5 +15,5 @@ PYTHON_VERSIONS=( for VERSION in "${PYTHON_VERSIONS[@]}"; do echo "Running tests with Python ${VERSION}..." - uv run --python "python${VERSION}" --directory "${TOP_DIR}/py" pytest . + uv run --python "python${VERSION}" --directory "${TOP_DIR}/py" pytest -vv . done diff --git a/py/bin/sanitize_schema_typing.py b/py/bin/sanitize_schema_typing.py new file mode 100644 index 0000000000..c738670a72 --- /dev/null +++ b/py/bin/sanitize_schema_typing.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + + +"""Standalone convenience script used to massage the typing.py. + +The `py/packages/genkit/src/genkit/core/schema_types.py` file is generated by +datamodel-codegen. However, since the tool doesn't currently provide options to +generate exactly the kind of code we need, we use this convenience script to +parse the Python source code, walk the AST, modify it to include the bits we +need and regenerate the code for eventual use within our codebase. + +Transformations applied: +- We remove the model_config attribute from classes that ineherit from + RootModel. +- We add the `populate_by_name=True` parameter to ensure serialization uses + camelCase for attributes since the JS implementation uses camelCase and Python + uses snake_case. The codegen pass is configured to generate snake_case for a + Pythonic API but serialize to camelCase in order to be compatible with + runtimes. +- We add a license header +- We add a header indicating that this file has been generated by a code + generator pass. +- We add the ability to use forward references. +- Add docstrings if missing. +""" + +import ast +import sys +from datetime import datetime +from pathlib import Path + + +class ModelConfigRemover(ast.NodeTransformer): + """AST transformer to remove model_config from RootModel classes. + + This class traverses the AST and removes model_config assignments from + RootModel classes while preserving them in other classes. + """ + + def __init__(self) -> None: + """Initialize the ModelConfigRemover.""" + self.modified = False + + def is_rootmodel_class(self, node: ast.ClassDef) -> bool: + """Check if a class definition is a RootModel class. + + Args: + node: The AST node representing a class definition. + + Returns: + True if the class inherits from RootModel, False otherwise. + """ + for base in node.bases: + if isinstance(base, ast.Name) and base.id == 'RootModel': + return True + elif isinstance(base, ast.Subscript): + value = base.value + if isinstance(value, ast.Name) and value.id == 'RootModel': + return True + return False + + def create_model_config( + self, extra_forbid: bool = True, populate_by_name: bool = True + ) -> ast.Assign: + """Create a model_config assignment with the specified options.""" + keywords = [] + if extra_forbid: + keywords.append( + ast.keyword(arg='extra', value=ast.Constant(value='forbid')) + ) + if populate_by_name: + keywords.append( + ast.keyword( + arg='populate_by_name', value=ast.Constant(value=True) + ) + ) + + return ast.Assign( + targets=[ast.Name(id='model_config')], + value=ast.Call( + func=ast.Name(id='ConfigDict'), args=[], keywords=keywords + ), + ) + + def has_model_config(self, node: ast.ClassDef) -> bool: + """Check if a class already has a model_config assignment.""" + for item in node.body: + if isinstance(item, ast.Assign): + targets = item.targets + if len(targets) == 1 and isinstance(targets[0], ast.Name): + if targets[0].id == 'model_config': + return True + return False + + def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: # noqa: N802 + """Visit class definitions and handle model_config based on class type. + + This method removes model_config assignments from RootModel classes + while preserving them in other classes. + + Args: + node: The AST node representing a class definition. + + Returns: + The modified class definition node. + """ + if self.is_rootmodel_class(node): + # Filter out model_config assignments for RootModel classes + node.body = [ + stmt + for stmt in node.body + if not ( + isinstance(stmt, ast.Assign) + and any( + isinstance(target, ast.Name) + and target.id == 'model_config' + for target in stmt.targets + ) + ) + ] + self.modified = True + else: + # For non-RootModel classes that inherit from BaseModel + if any( + isinstance(base, ast.Name) and base.id == 'BaseModel' + for base in node.bases + ): + if not self.has_model_config(node): + # Add model_config with populate_by_name=True + node.body.insert(0, self.create_model_config()) + self.modified = True + else: + # Update existing model_config to include + # populate_by_name=True + new_body = [] + for item in node.body: + if isinstance(item, ast.Assign): + targets = item.targets + if len(targets) == 1 and isinstance( + targets[0], ast.Name + ): + if targets[0].id == 'model_config': + new_body.append(self.create_model_config()) + self.modified = True + continue + new_body.append(item) + node.body = new_body + + return node + + +class ClassTransformer(ast.NodeTransformer): + """AST transformer that modifies class definitions.""" + + def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: # noqa: N802 + """Visit and transform a class definition node. + + Args: + node: The ClassDef AST node to transform. + + Returns: + The transformed ClassDef node. + """ + # First apply base class transformations + node = super().generic_visit(node) + + # Check if class has a docstring + has_docstring = False + if node.body and isinstance(node.body[0], ast.Expr): + if isinstance(node.body[0].value, ast.Str): + has_docstring = True + + # Add docstring if missing + if not has_docstring: + # Generate a more descriptive docstring based on class type + if any( + base.id == 'RootModel' + for base in node.bases + if isinstance(base, ast.Name) + ): + docstring = ( + f'Root model for {node.name.lower().replace("_", " ")}.' + ) + elif any( + base.id == 'BaseModel' + for base in node.bases + if isinstance(base, ast.Name) + ): + docstring = ( + f'Model for {node.name.lower().replace("_", " ")} data.' + ) + elif any( + base.id == 'Enum' + for base in node.bases + if isinstance(base, ast.Name) + ): + n = node.name.lower().replace('_', ' ') + docstring = f'Enumeration of {n} values.' + else: + docstring = f'{node.name} data type class.' + + node.body.insert(0, ast.Expr(value=ast.Str(s=docstring))) + + # Remove model_config from RootModel classes + if any( + base.id == 'RootModel' + for base in node.bases + if isinstance(base, ast.Name) + ): + node.body = [ + stmt + for stmt in node.body + if not ( + isinstance(stmt, ast.AnnAssign) + and isinstance(stmt.target, ast.Name) + and stmt.target.id == 'model_config' + ) + ] + + return node + + +def add_header(content: str) -> str: + """Add the generated header to the content.""" + header = '''# Copyright {year} Google LLC +# SPDX-License-Identifier: Apache-2.0 +# +# DO NOT EDIT: Generated by `generate_schema_typing` from `genkit-schemas.json`. + +"""Schema types module defining the core data models for Genkit. + +This module contains Pydantic models that define the structure and validation +for various data types used throughout the Genkit framework, including messages, +actions, tools, and configuration options. +""" + +from __future__ import annotations +''' + return header.format(year=datetime.now().year) + content + + +def process_file(filename: str) -> None: + """Process a Python file to remove model_config from RootModel classes. + + This function reads a Python file, processes its AST to remove model_config + from RootModel classes, and writes the modified code back to the file. + + Args: + filename: Path to the Python file to process. + + Raises: + FileNotFoundError: If the input file does not exist. + SyntaxError: If the input file contains invalid Python syntax. + """ + path = Path(filename) + if not path.is_file(): + print(f'Error: File not found: {filename}') + sys.exit(1) + + try: + with open(path, encoding='utf-8') as f: + source = f.read() + + tree = ast.parse(source) + model_transformer = ModelConfigRemover() + modified_tree = model_transformer.visit(tree) + class_transformer = ClassTransformer() + modified_tree = class_transformer.visit(modified_tree) + + if ( + hasattr(model_transformer, 'modified') + and model_transformer.modified + ) or ( + hasattr(class_transformer, 'modified') + and class_transformer.modified + ): + # Format the modified code + ast.fix_missing_locations(modified_tree) + modified_source = ast.unparse(modified_tree) + src = add_header(modified_source) + with open(path, 'w', encoding='utf-8') as f: + f.write(src) + print(f'Successfully processed {filename}') + else: + # Even if no AST modifications, still write back to add the header. + path.write_text(add_header(source), encoding='utf-8') + print(f'Added header to {filename}') + + except SyntaxError as e: + print(f'Error: Invalid Python syntax in {filename}: {e}') + sys.exit(1) + + +def main() -> None: + """Main entry point for the script. + + This function processes command line arguments and calls the appropriate + functions to process the schema types file. + + Usage: + python script.py + """ + if len(sys.argv) != 2: + print('Usage: python script.py ') + sys.exit(1) + + process_file(sys.argv[1]) + + +if __name__ == '__main__': + main() diff --git a/py/bin/sanitize_schemas.py b/py/bin/sanitize_schemas.py deleted file mode 100644 index 198a969487..0000000000 --- a/py/bin/sanitize_schemas.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - -import ast -import sys - - -class ModelConfigRemover(ast.NodeTransformer): - def __init__(self) -> None: - self.modified = False - - def is_rootmodel_class(self, node: ast.ClassDef) -> bool: - """Check if the class inherits from RootModel.""" - for base in node.bases: - if isinstance(base, ast.Name) and base.id == 'RootModel': - return True - elif isinstance(base, ast.Subscript): - if ( - isinstance(base.value, ast.Name) - and base.value.id == 'RootModel' - ): - return True - return False - - def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: - """Visit class definitions and remove model_config if class inherits from RootModel.""" - if self.is_rootmodel_class(node): - # Filter out model_config assignments - new_body = [] - for item in node.body: - if isinstance(item, ast.Assign): - targets = item.targets - if len(targets) == 1 and isinstance(targets[0], ast.Name): - if targets[0].id != 'model_config': - new_body.append(item) - else: - new_body.append(item) - - if len(new_body) != len(node.body): - self.modified = True - - node.body = new_body - - return node - - -def process_file(filename: str) -> None: - """Process a Python file to remove model_config from RootModel classes.""" - with open(filename, 'r') as f: - source = f.read() - - tree = ast.parse(source) - - transformer = ModelConfigRemover() - modified_tree = transformer.visit(tree) - - if transformer.modified: - ast.fix_missing_locations(modified_tree) - modified_source = ast.unparse(modified_tree) - with open(filename, 'w') as f: - f.write(modified_source) - print( - f'Modified {filename}: Removed model_config from RootModel classes' - ) - else: - print(f'No modifications needed in {filename}') - - -def main() -> None: - if len(sys.argv) != 2: - print('Usage: python script.py ') - sys.exit(1) - - filename = sys.argv[1] - try: - process_file(filename) - except Exception as e: - print(f'Error processing {filename}: {str(e)}') - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/py/engdoc/ROADMAP.org b/py/engdoc/ROADMAP.org new file mode 100644 index 0000000000..7c0994eaf2 --- /dev/null +++ b/py/engdoc/ROADMAP.org @@ -0,0 +1,225 @@ +#+title: SDK Roadmap +#+description: An org document that enlists the milestones and objectives of our SDK roadmap. + +* SDK Roadmap [0/0] +** Objectives [0/4] +- [ ] The Python SDK needs to be at feature parity with the JavaScript SDK. +- [ ] The Go SDK needs to be at feature parity with the JavaScript SDK. +- [ ] The Python Dotprompt library needs to be at feature parity with the JavaScript Dotprompt implementation. +- [ ] The Go Dotprompt library needs to be at feature parity with the JavaScript Dotprompt implementation. +** Specifications and Schemas [0/4] +- [ ] dotprompt + - [ ] helpers (based on yaml spec) + - [ ] json + - [ ] Go + - [ ] Python + - [ ] media + - [ ] Go + - [ ] Python + - [ ] role + - [ ] Go + - [ ] Python + - [ ] history + - [ ] Create the spec yaml + - [ ] Go + - [ ] Python + - [ ] section + - [ ] Create the spec yaml + - [ ] Go + - [ ] Python + - [ ] metadata.yaml + - [ ] Go + - [ ] Python + - [ ] partials.yaml + - [ ] Go + - [ ] Python + - [ ] picoschema.yaml + - [ ] Go + - [ ] Python + - [ ] variables.yaml + - [ ] Go + - [ ] Python +- [ ] genkit-schema converter + - [ ] schema.py and tests + - [ ] Candidate + - [ ] CandidateError + - [ ] DataPart + - [ ] DocumentData + - [ ] FinishReason + - [ ] GenerateActionOptions + - [ ] GenerateCommonConfig + - [ ] GenerateRequest + - [ ] GenerateResponse + - [ ] GenerateResponseChunk + - [ ] GenerationUsage + - [ ] InstrmentationLibrary + - [ ] Link + - [ ] MediaPart + - [ ] Message + - [ ] ModelInfo + - [ ] ModelRequest + - [ ] ModelResponse + - [ ] ModelResponseChunk + - [ ] Part + - [ ] Role + - [ ] SpanContext + - [ ] SpanData + - [ ] SpanMetadata + - [ ] SpanStatus + - [ ] TextPart + - [ ] TimeEvent + - [ ] ToolDefinition + - [ ] ToolRequest + - [ ] ToolRequestPart + - [ ] ToolResponse + - [ ] ToolResponsePart + - [ ] TraceData +- [ ] reflection API [0/7] + + See: `reflectionApi.yaml` + + - [ ] GET /api/actions: Retrieves all runnable actions. + - [ ] POST /api/runAction: Runs an action and returns the result. + - [ ] GET /api/envs/{env}/traces: Retrieves all traces for a given environment (e.g. dev or prod) + - [ ] GET /api/envs/{env}/traces/{traceId}: Retrieves traces for the given environment + - [ ] GET /api/envs/{env}/flowStates: Retrieves all flow states for a given environment (e.g. dev or prod) + - [ ] GET /api/envs/{env}/flowStates/{flowId}: Retrieves a flow state for the given ID + - [ ] GET /api/__health: health check +- [ ] generate API + - [ ] +** Plugins [0/9] +- [ ] Design [0/2] + - [ ] Proposal with example API + - [ ] Design review +- [ ] Chroma [0/4] + - [ ] Plugin + - [ ] Documentation + - [ ] Tests + - [ ] Sample +- [ ] Dotprompt [0/0] +- [ ] Firebase [0/4] + - [ ] Plugin + - [ ] Documentation + - [ ] Tests + - [ ] Sample +- [ ] GoogleAI [0/4] + - [ ] Plugin + - [ ] Documentation + - [ ] Tests + - [ ] Sample +- [ ] Ollama [0/4] + - [ ] Plugin + - [ ] Documentation + - [ ] Tests + - [ ] Sample +- [ ] OpenAI [0/4] + - [ ] Plugin + - [ ] Documentation + - [ ] Tests + - [ ] Sample +- [ ] Pinecone [0/4] + - [ ] Plugin + - [ ] Documentation + - [ ] Tests + - [ ] Sample +- [ ] VertexAI [0/4] + - [ ] Plugin + - [ ] Documentation + - [ ] Tests + - [ ] Sample +** Samples +- [ ] Hello world +- [ ] Basic Gemini +- [ ] Context caching +- [ ] Context caching2 +- [ ] Custom evaluators +- [ ] Docs Menu Basic +- [ ] Docs Menu RAG +- [ ] Flow sample 1 +- [ ] Flow sample 2 +- [ ] Prompt file +- [ ] RAG +- [ ] Vertex AI model garden +- [ ] Vertex AI reranker +- [ ] Vertex AI Vector Search +** Server implementations [/] +- [ ] multiprocessing server cluster [0/2] + - [ ] reflection server in dev mode + - [ ] production flows server +** CI/CD/Dev workflow [2/6] +- [-] Unit testing library + - [ ] Go testify + - [X] Python pytest +- [X] Unit testing watcher + - [X] pytest-watcher +- [-] Coverage analysis + - [X] pytest-cov + - [ ] Go test coverage tool +- [ ] Vulnerability analysis + - [ ] Python + - [ ] Go +- [ ] License compatibility checks + - [ ] Python + - [ ] Go +- [X] Automated license header check + - [X] Python + - [X] Go +** Git Hooks [0/1] +- [-] Pre-commit and pre-push hooks + - [-] Build Code + - [-] go build + - [X] Genkit + - [ ] Dotprompt + - [X] build python distribution + - [X] Genkit + - [X] Dotprompt + - [X] Distribution + - [X] Genkit + - [X] Dotprompt + - [-] Documentation + - [-] godoc + - [X] Genkit + - [ ] Dotprompt + - [X] engdoc using mkdocs + - [X] Genkit + - [X] Dotprompt + - [ ] Python API doc using mkdocstrings + - [ ] Genkit + - [ ] Dotprompt + - [-] Test + - [-] go test + - [X] Genkit + - [ ] Dotprompt + - [X] pytest with coverage threshold + - [X] Genkit + - [X] Dotprompt + - [X] Format + - [-] Lint + - [-] Python + - [-] mypy static type checks + - [X] dotprompt + - [ ] genkit +** Dependencies +- [X] Handlebars + - [X] handlebars-py (MIT License; feasibility test done) + - [X] pybars3 (LGPL 3.0 License; cannot use) +- [ ] JSON Schema + - [ ] Go: + - [ ] https://github.com/swaggest/jsonschema-go + - [ ] https://github.com/xeipuuv/gojsonschema + - [ ] https://github.com/santhosh-tekuri/jsonschema + - [ ] https://github.com/qri-io/jsonschema +- [ ] Picoschema + - [ ] Go + - [ ] https://github.com/jumonapp/picoschema +** Release management +- [ ] Semantic Versioning and tagging +- [ ] PyPi project for dotprompt https://pypi.org/project/dotprompt/ + - [ ] Acquire from current owner +- [ ] PyPi project for genkit https://pypi.org/project/genkit/ + - [ ] Acquire from current owner + +** Integration Tests [/] + - [ ] Go + - [ ] Python + - [X] JS diff --git a/py/packages/dotprompt/README.md b/py/packages/dotprompt/README.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/py/packages/dotprompt/src/dotprompt/__init__.py b/py/packages/dotprompt/src/dotprompt/__init__.py deleted file mode 100644 index e3d2291826..0000000000 --- a/py/packages/dotprompt/src/dotprompt/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - - -def hello() -> str: - return 'Hello from dotprompt!' diff --git a/py/packages/dotprompt/src/dotprompt/py.typed b/py/packages/dotprompt/src/dotprompt/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/py/packages/genkit/src/genkit/ai/__init__.py b/py/packages/genkit/src/genkit/ai/__init__.py index c0494838ae..fe0e3baf20 100644 --- a/py/packages/genkit/src/genkit/ai/__init__.py +++ b/py/packages/genkit/src/genkit/ai/__init__.py @@ -2,12 +2,26 @@ # SPDX-License-Identifier: Apache-2.0 -""" -AI Foundations for Genkit +"""AI foundations for the Genkit framework. + +This package provides the artificial intelligence and machine learning +capabilities of the Genkit framework. It includes: + + - Model interfaces for various AI models + - Prompt management and templating + - AI-specific utilities and helpers + +The AI package enables seamless integration of AI models and capabilities +into applications built with Genkit. """ def package_name() -> str: + """Get the fully qualified package name. + + Returns: + The string 'genkit.ai', which is the fully qualified package name. + """ return 'genkit.ai' diff --git a/py/packages/genkit/src/genkit/ai/embedding.py b/py/packages/genkit/src/genkit/ai/embedding.py new file mode 100644 index 0000000000..2e4baa7b3a --- /dev/null +++ b/py/packages/genkit/src/genkit/ai/embedding.py @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +from collections.abc import Callable + +from pydantic import BaseModel + + +class EmbedRequest(BaseModel): + """Request for embedding documents. + + Attributes: + documents: The list of documents to embed. + """ + + documents: list[str] + + +class EmbedResponse(BaseModel): + """Response for embedding documents. + + Attributes: + embeddings: The list of embeddings for the documents. + """ + + embeddings: list[list[float]] + + +type EmbedderFn = Callable[[EmbedRequest], EmbedResponse] diff --git a/py/packages/genkit/src/genkit/ai/generate.py b/py/packages/genkit/src/genkit/ai/generate.py new file mode 100644 index 0000000000..ef334b630c --- /dev/null +++ b/py/packages/genkit/src/genkit/ai/generate.py @@ -0,0 +1,278 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +import copy +import logging +from collections.abc import Callable +from typing import Any + +from genkit.ai.model import ( + GenerateResponseChunkWrapper, + GenerateResponseWrapper, +) +from genkit.core.codec import dump_dict +from genkit.core.registry import Action, ActionKind, Registry +from genkit.core.typing import ( + GenerateActionOptions, + GenerateRequest, + GenerateResponse, + GenerateResponseChunk, + Message, + OutputConfig, + Role, + ToolDefinition, + ToolResponse1, + ToolResponsePart, +) + +logger = logging.getLogger(__name__) + +StreamingCallback = Callable[[GenerateResponseChunkWrapper], None] + +DEFAULT_MAX_TURNS = 5 + + +async def generate_action( + registry: Registry, + raw_request: GenerateActionOptions, + on_chunk: StreamingCallback | None = None, + message_index: int = 0, + current_turn: int = 0, +) -> GenerateResponseWrapper: + # TODO: formats + # TODO: middleware + + model, tools = resolve_parameters(registry, raw_request) + + assert_valid_tool_names(tools) + + # TODO: interrupts + + request = await action_to_generate_request(raw_request, tools, model) + + prev_chunks: list[GenerateResponseChunk] = [] + + chunk_role: Role = Role.MODEL + + def make_chunk( + role: Role, chunk: GenerateResponseChunk + ) -> GenerateResponseChunk: + """convenience method to create a full chunk from role and data, append + the chunk to the previousChunks array, and increment the message index + as needed""" + nonlocal chunk_role, message_index + + if role != chunk_role and len(prev_chunks.length) > 0: + message_index += 1 + + chunk_role = role + + prev_to_send = copy.copy(prev_chunks) + prev_chunks.append(chunk) + + return GenerateResponseChunkWrapper( + chunk, index=message_index, previous_chunks=prev_to_send + ) + + model_response = ( + await model.arun(input=request, on_chunk=on_chunk) + ).response + response = GenerateResponseWrapper(model_response, request) + + response.assert_valid() + generated_msg = response.message + + tool_requests = [x for x in response.message.content if x.root.tool_request] + + if raw_request.return_tool_requests or len(tool_requests) == 0: + if len(tool_requests) == 0: + response.assert_valid_schema() + return response + + max_iters = ( + raw_request.max_turns if raw_request.max_turns else DEFAULT_MAX_TURNS + ) + + if current_turn + 1 > max_iters: + raise GenerationResponseError( + response=response, + message=f'Exceeded maximum tool call iterations ({max_iters})', + status='ABORTED', + details={'request': request}, + ) + + ( + revised_model_msg, + tool_msg, + transfer_preamble, + ) = await resolve_tool_requests(registry, raw_request, generated_msg) + + # if an interrupt message is returned, stop the tool loop and return a response + if revised_model_msg: + interrupted_resp = GenerateResponseWrapper(response, request) + interrupted_resp.finish_reason = 'interrupted' + interrupted_resp.finish_message = ( + 'One or more tool calls resulted in interrupts.' + ) + interrupted_resp.message = revised_model_msg + return interrupted_resp + + # if the loop will continue, stream out the tool response message... + if on_chunk: + on_chunk( + make_chunk('tool', GenerateResponseChunk(content=tool_msg.content)) + ) + + next_request = copy.copy(raw_request) + nextMessages = copy.copy(raw_request.messages) + nextMessages.append(generated_msg) + nextMessages.append(tool_msg) + next_request.messages = nextMessages + next_request = apply_transfer_preamble(next_request, transfer_preamble) + + # then recursively call for another loop + return await generate_action( + registry, + raw_request=next_request, + # middleware: middleware, + current_turn=current_turn + 1, + message_index=message_index + 1, + ) + + +def apply_transfer_preamble( + next_request: GenerateActionOptions, preamble: GenerateActionOptions +) -> GenerateActionOptions: + # TODO: implement me + return next_request + + +def assert_valid_tool_names(raw_request: GenerateActionOptions): + # TODO: implement me + pass + + +def resolve_parameters( + registry: Registry, request: GenerateActionOptions +) -> tuple[Action, list[Action]]: + model = ( + request.model if request.model is not None else registry.default_model + ) + if not model: + raise Exception('No model configured.') + + model_action = registry.lookup_action(ActionKind.MODEL, model) + if model_action is None: + raise Exception(f'Failed to to resolve model {model}') + + tools: list[Action] = [] + if request.tools: + for tool_name in request.tools: + tool_action = registry.lookup_action(ActionKind.TOOL, tool_name) + if tool_action is None: + raise Exception(f'Unable to resolve tool {tool_name}') + tools.append(tool_action) + + return (model_action, tools) + + +async def action_to_generate_request( + options: GenerateActionOptions, resolvedTools: list[Action], model: Action +) -> GenerateRequest: + # TODO: add warning when tools are not supported in ModelInfo + # TODO: add warning when toolChoice is not supported in ModelInfo + + tool_defs = ( + [to_tool_definition(tool) for tool in resolvedTools] + if resolvedTools + else [] + ) + return GenerateRequest( + messages=options.messages, + config=options.config if options.config is not None else {}, + context=options.docs, + tools=tool_defs, + tool_choice=options.tool_choice, + output=OutputConfig( + content_type=options.output.content_type + if options.output + else None, + format=options.output.format if options.output else None, + schema_=options.output.json_schema if options.output else None, + constrained=options.output.constrained if options.output else None, + ), + ) + + +def to_tool_definition(tool: Action) -> ToolDefinition: + original_name: str = tool.name + name: str = original_name + + if '/' in original_name: + name = original_name[original_name.rfind('/') + 1 :] + + metadata = None + if original_name != name: + metadata = {'originalName': original_name} + + tdef = ToolDefinition( + name=name, + description=tool.description, + inputSchema=tool.input_schema, + outputSchema=tool.output_schema, + metadata=metadata, + ) + return tdef + + +async def resolve_tool_requests( + registry: Registry, request: GenerateActionOptions, message: Message +) -> tuple[Message, Message, GenerateActionOptions]: + # TODO: interrupts + # TODO: prompt transfer + + tool_requests = [ + x.root.tool_request for x in message.content if x.root.tool_request + ] + tool_dict: dict[str, Action] = {} + for tool_name in request.tools: + tool_dict[tool_name] = resolve_tool(registry, tool_name) + + response_parts: list[ToolResponsePart] = [] + for tool_request in tool_requests: + if tool_request.name not in tool_dict: + raise RuntimeError(f'failed {tool_request.name} not found') + tool = tool_dict[tool_request.name] + tool_response = (await tool.arun_raw(tool_request.input)).response + # TODO: figure out why pydantic generates ToolResponse1 + response_parts.append( + ToolResponsePart( + toolResponse=ToolResponse1( + name=tool_request.name, + ref=tool_request.ref, + output=dump_dict(tool_response), + ) + ) + ) + + return (None, Message(role=Role.TOOL, content=response_parts), None) + + +def resolve_tool(registry: Registry, tool_name: str): + return registry.lookup_action(kind=ActionKind.TOOL, name=tool_name) + + +# TODO: extend GenkitError +class GenerationResponseError(Exception): + # TODO: use status enum + def __init__( + self, + response: GenerateResponse, + message: str, + status: str, + details: dict[str, Any], + ): + self.response = response + self.message = message + self.status = status + self.details = details diff --git a/py/packages/genkit/src/genkit/ai/generate_test.py b/py/packages/genkit/src/genkit/ai/generate_test.py new file mode 100644 index 0000000000..446af60c4f --- /dev/null +++ b/py/packages/genkit/src/genkit/ai/generate_test.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the action module.""" + +import pathlib + +import pytest +import yaml +from genkit.ai.generate import generate_action +from genkit.core.action import ActionRunContext +from genkit.core.codec import dump_dict, dump_json +from genkit.core.typing import ( + FinishReason, + GenerateActionOptions, + GenerateRequest, + GenerateResponse, + GenerateResponseChunk, + Message, + Role, + TextPart, +) +from genkit.veneer.veneer import Genkit +from pydantic import TypeAdapter + + +class ProgrammableModel: + request_idx = 0 + responses: list[GenerateResponse] = [] + chunks: list[list[GenerateResponseChunk]] = None + last_request: GenerateResponse = None + + +@pytest.fixture +def setup_test(): + ai = Genkit() + + pm = ProgrammableModel() + + def programmableModel(request: GenerateRequest, ctx: ActionRunContext): + pm.last_request = request + response = pm.responses[pm.request_idx] + if pm.chunks is not None: + for chunk in pm.chunks[pm.request_idx]: + ctx.send_chunk(chunk) + pm.request_idx += 1 + return response + + ai.define_model(name='programmableModel', fn=programmableModel) + + @ai.tool('the tool') + def testTool(): + return 'abc' + + return (ai, pm) + + +@pytest.mark.asyncio +async def test_simple_text_generate_request(setup_test) -> None: + ai, pm = setup_test + + pm.responses.append( + GenerateResponse( + finishReason=FinishReason.STOP, + message=Message(role=Role.MODEL, content=[TextPart(text='bye')]), + ) + ) + + response = await generate_action( + ai.registry, + GenerateActionOptions( + model='programmableModel', + messages=[ + Message( + role=Role.USER, + content=[TextPart(text='hi')], + ), + ], + ), + ) + + assert response.text() == 'bye' + + +########################################################################## +# run tests from /tests/specs/generate.yaml +########################################################################## + +specs = [] +with open( + pathlib.Path(__file__) + .parent.joinpath('../../../../../../tests/specs/generate.yaml') + .resolve() +) as stream: + testsSpec = yaml.safe_load(stream) + specs = testsSpec['tests'] + specs = [x for x in testsSpec['tests'] if x['name'] == 'calls tools'] + + +@pytest.mark.parametrize( + 'spec', + specs, +) +@pytest.mark.asyncio +async def test_generate_action_spec(spec) -> None: + ai = Genkit() + + pm = ProgrammableModel() + + def programmableModel(request: GenerateRequest, ctx: ActionRunContext): + pm.last_request = request + response = pm.responses[pm.request_idx] + if pm.chunks is not None: + for chunk in pm.chunks[pm.request_idx]: + ctx.send_chunk(chunk) + pm.request_idx += 1 + return response + + ai.define_model(name='programmableModel', fn=programmableModel) + + @ai.tool('description') + def testTool(): + return 'tool called' + + if 'modelResponses' in spec: + pm.responses = [ + TypeAdapter(GenerateResponse).validate_python(resp) + for resp in spec['modelResponses'] + ] + + if 'streamChunks' in spec: + pm.chunks = [] + for chunks in spec['streamChunks']: + converted = [] + for chunk in chunks: + converted.append( + TypeAdapter(GenerateResponseChunk).validate_python(chunk) + ) + pm.chunks.append(converted) + + response = None + chunks = None + if 'stream' in spec and spec['stream']: + chunks = [] + + def on_chunk(chunk): + chunks.append(chunk) + + response = await generate_action( + ai.registry, + TypeAdapter(GenerateActionOptions).validate_python(spec['input']), + on_chunk=on_chunk, + ) + else: + response = await generate_action( + ai.registry, + TypeAdapter(GenerateActionOptions).validate_python(spec['input']), + ) + + if 'expectChunks' in spec: + got = clean_schema(chunks) + want = clean_schema(spec['expectChunks']) + if not is_equal_lists(got, want): + raise AssertionError( + f'{dump_json(got, indent=2)}\n\nis not equal to expected:\n\n{dump_json(want, indent=2)}' + ) + + if 'expectResponse' in spec: + got = clean_schema(dump_dict(response)) + want = clean_schema(spec['expectResponse']) + if got != want: + raise AssertionError( + f'{dump_json(got, indent=2)}\n\nis not equal to expected:\n\n{dump_json(want, indent=2)}' + ) + + +def is_equal_lists(a, b): + if len(a) != len(b): + return False + + for i in range(len(a)): + if dump_dict(a[i]) != dump_dict(b[i]): + return False + + return True + + +primitives = (bool, str, int, float, type(None)) + + +def is_primitive(obj): + return isinstance(obj, primitives) + + +def clean_schema(d): + if is_primitive(d): + return d + if isinstance(d, dict): + out = {} + for key in d: + if key != '$schema': + out[key] = clean_schema(d[key]) + return out + elif hasattr(d, '__len__'): + return [clean_schema(i) for i in d] + else: + return d diff --git a/py/packages/genkit/src/genkit/ai/model.py b/py/packages/genkit/src/genkit/ai/model.py index b052cfbfc7..a094847503 100644 --- a/py/packages/genkit/src/genkit/ai/model.py +++ b/py/packages/genkit/src/genkit/ai/model.py @@ -1,7 +1,84 @@ # Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 -from typing import Callable -from genkit.core.schemas import GenerateRequest, GenerateResponse +"""Model type definitions for the Genkit framework. +This module defines the type interfaces for AI models in the Genkit framework. +These types ensure consistent interaction with different AI models and provide +type safety when working with model inputs and outputs. + +Example: + def my_model(request: GenerateRequest) -> GenerateResponse: + # Model implementation + return GenerateResponse(...) + + model_fn: ModelFn = my_model +""" + +from collections.abc import Callable + +from genkit.core.typing import ( + GenerateRequest, + GenerateResponse, + GenerateResponseChunk, + GenerationUsage, +) + +# Type alias for a function that takes a GenerateRequest and returns +# a GenerateResponse ModelFn = Callable[[GenerateRequest], GenerateResponse] + + +class GenerateResponseWrapper(GenerateResponse): + def __init__(self, response: GenerateResponse, request: GenerateRequest): + super().__init__( + message=response.message, + finish_reason=response.finish_reason, + finish_message=response.finish_message, + latency_ms=response.latency_ms, + usage=response.usage + if response.usage is not None + else GenerationUsage(), + custom=response.custom if response.custom is not None else {}, + request=request, + candidates=response.candidates, + ) + + def assert_valid(self): + # TODO: implement + pass + + def assert_valid_schema(self): + # TODO: implement + pass + + def text(self): + return ''.join([ + p.root.text if p.root.text is not None else '' + for p in self.message.content + ]) + + def output(self): + # TODO: implement + pass + + +class GenerateResponseChunkWrapper(GenerateResponseChunk): + def __init__( + self, + chunk: GenerateResponseChunk, + index: int, + previous_chunks: list[GenerateResponseChunk], + ): + super().__init__( + role=chunk.role, + index=chunk.index, + content=chunk.content, + custom=chunk.custom, + aggregated=chunk.aggregated, + ) + + def text(self): + return ''.join([ + p.root.text if p.root.text is not None else '' for p in self.content + ]) diff --git a/py/packages/genkit/src/genkit/ai/prompt.py b/py/packages/genkit/src/genkit/ai/prompt.py index 438492c88f..9fff93184e 100644 --- a/py/packages/genkit/src/genkit/ai/prompt.py +++ b/py/packages/genkit/src/genkit/ai/prompt.py @@ -2,8 +2,18 @@ # SPDX-License-Identifier: Apache-2.0 -from typing import Callable, Optional, Any -from genkit.core.schemas import GenerateRequest +"""Prompt management and templating for the Genkit framework. +This module provides types and utilities for managing prompts and templates +used with AI models in the Genkit framework. It enables consistent prompt +generation and management across different parts of the application. +""" -PromptFn = Callable[[Optional[Any]], GenerateRequest] +from collections.abc import Callable +from typing import Any + +from genkit.core.typing import GenerateRequest + +# Type alias for a function that takes optional context and returns +# a GenerateRequest +PromptFn = Callable[[Any | None], GenerateRequest] diff --git a/py/packages/genkit/src/genkit/core/__init__.py b/py/packages/genkit/src/genkit/core/__init__.py index 2a74de4b3f..aab681d867 100644 --- a/py/packages/genkit/src/genkit/core/__init__.py +++ b/py/packages/genkit/src/genkit/core/__init__.py @@ -2,12 +2,25 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Core Foundations for Genkit +"""Core foundations for the Genkit framework. + +This package provides the fundamental building blocks and abstractions used +throughout the Genkit framework. It includes: + + - Action system for defining and managing callable functions + - Plugin architecture for extending framework functionality + - Registry for managing resources and actions + - Tracing and telemetry for monitoring and debugging + - Schema types for data validation and serialization """ def package_name() -> str: + """Get the fully qualified package name. + + Returns: + The string 'genkit.core', which is the fully qualified package name. + """ return 'genkit.core' diff --git a/py/packages/genkit/src/genkit/core/action.py b/py/packages/genkit/src/genkit/core/action.py index 4c4f6a19d3..d5157a2ae0 100644 --- a/py/packages/genkit/src/genkit/core/action.py +++ b/py/packages/genkit/src/genkit/core/action.py @@ -1,92 +1,432 @@ # Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2. -import inspect -import json +# SPDX-License-Identifier: Apache-2.0 + +"""Action module for defining and managing RPC-over-HTTP functions. + +This module provides the core functionality for creating and managing actions in +the Genkit framework. Actions are strongly-typed, named, observable, +uninterrupted operations that can operate in streaming or non-streaming mode. +""" -from typing import Dict, Optional, Callable, Any -from pydantic import ConfigDict, BaseModel, TypeAdapter +import asyncio +import inspect +from collections.abc import Callable +from enum import StrEnum +from functools import cached_property +from typing import Any +from genkit.core.codec import dump_json from genkit.core.tracing import tracer +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter + +# TODO: add typing, generics +StreamingCallback = Callable[[Any], None] + + +class ActionKind(StrEnum): + """Enumerates all the types of action that can be registered. + + This enum defines the various types of actions supported by the framework, + including chat models, embedders, evaluators, and other utility functions. + """ + + CHATLLM = 'chat-llm' + CUSTOM = 'custom' + EMBEDDER = 'embedder' + EVALUATOR = 'evaluator' + FLOW = 'flow' + INDEXER = 'indexer' + MODEL = 'model' + PROMPT = 'prompt' + RETRIEVER = 'retriever' + TEXTLLM = 'text-llm' + TOOL = 'tool' + UTIL = 'util' class ActionResponse(BaseModel): - model_config = ConfigDict(extra='forbid') + """The response from an action. + + Attributes: + response: The actual response data from the action execution. + trace_id: A unique identifier for tracing the action execution. + """ + + model_config = ConfigDict(extra='forbid', populate_by_name=True) response: Any - traceId: str + trace_id: str = Field(alias='traceId') -class Action: +class ActionMetadataKey(StrEnum): + """Enumerates all the keys of the action metadata. + + Attributes: + INPUT_KEY: Key for the input schema metadata. + OUTPUT_KEY: Key for the output schema metadata. + RETURN: Key for the return type metadata. + """ + INPUT_KEY = 'inputSchema' OUTPUT_KEY = 'outputSchema' RETURN = 'return' + +def parse_action_key(key: str) -> tuple[ActionKind, str]: + """Parse an action key into its kind and name components. + + Args: + key: The action key to parse, in the format "/kind/name". + + Returns: + A tuple containing the ActionKind and name. + + Raises: + ValueError: If the key format is invalid or if the kind is not a valid + ActionKind. + """ + tokens = key.split('/') + if len(tokens) < 3 or not tokens[1] or not tokens[2]: + msg = ( + f'Invalid action key format: `{key}`.' + 'Expected format: `//`' + ) + raise ValueError(msg) + + kind_str = tokens[1] + name = tokens[2] + try: + kind = ActionKind(kind_str) + except ValueError as e: + msg = f'Invalid action kind: `{kind_str}`' + raise ValueError(msg) from e + return kind, name + + +def create_action_key(kind: ActionKind, name: str) -> str: + """Create an action key from its kind and name components. + + Args: + kind: The kind of action. + name: The name of the action. + + Returns: + The action key in the format `//`. + """ + return f'/{kind}/{name}' + + +def noop_streaming_callback(chunk: Any) -> None: + """A no-op streaming callback. + + This callback does nothing and is used when no streaming is desired. + """ + pass + + +class ActionRunContext: + """Context for an action execution.""" + def __init__( self, - action_type: str, - name: str, - fn: Callable, - description: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - span_metadata: Optional[Dict[str, str]] = None, + on_chunk: StreamingCallback | None = None, + context: Any | None = None, ): - # TODO(Tatsiana Havina): separate a long constructor into methods. - self.type = action_type + """Initialize an ActionRunContext. + + Args: + on_chunk: The callback to invoke when a chunk is received. + context: The context to pass to the action. + """ + self._on_chunk = ( + on_chunk if on_chunk is not None else noop_streaming_callback + ) + self.context = context if context is not None else {} + + @cached_property + def is_streaming(self) -> bool: + """Returns true if context contains on chunk callback, False otherwise""" + return self._on_chunk != noop_streaming_callback + + def send_chunk(self, chunk: Any): + """Send a chunk to from the action to the client. + + Args: + chunk: The chunk to send to the client. + """ + self._on_chunk(chunk) + + +class Action: + """An action is a Typed JSON-based RPC-over-HTTP remote-callable function. + + Actions support metadata, streaming, reflection and discovery. They are + strongly-typed, named, observable, uninterrupted operations that can operate + in streaming or non-streaming mode. An action wraps a function that takes an + input and returns an output, optionally streaming values incrementally by + invoking a streaming callback. + """ + + def __init__( + self, + kind: ActionKind, + name: str, + fn: Callable[..., Any], + description: str | None = None, + metadata: dict[str, Any] | None = None, + span_metadata: dict[str, Any] | None = None, + ) -> None: + """Initialize an Action. + + Args: + kind: The kind of action (e.g., TOOL, MODEL, etc.). + name: Unique name identifier for this action. + fn: The function to call when the action is executed. + description: Optional human-readable description of the action. + metadata: Optional dictionary of metadata about the action. + span_metadata: Optional dictionary of tracing span metadata. + """ + self.kind = kind self.name = name - def fn_to_call(*args, **kwargs): + input_spec = inspect.getfullargspec(fn) + action_args = [ + k for k in input_spec.annotations if k != ActionMetadataKey.RETURN + ] + + afn = ensure_async(fn) + self.is_async = asyncio.iscoroutinefunction(fn) + + async def async_tracing_wrapper( + input: Any | None, ctx: ActionRunContext + ) -> ActionResponse: + """Wrap the function in an async tracing wrapper. + + Args: + input: The input to the action. + ctx: The context to pass to the action. + + Returns: + The action response. + """ with tracer.start_as_current_span(name) as span: trace_id = str(span.get_span_context().trace_id) - span.set_attribute('genkit:type', action_type) - span.set_attribute('genkit:name', name) + record_input_metadata( + span=span, + kind=kind, + name=name, + span_metadata=span_metadata, + input=input, + ) + + match len(action_args): + case 0: + output = await afn() + case 1: + output = await afn(input) + case 2: + output = await afn(input, ctx) + case _: + raise ValueError('action fn must have 0-2 args...') - if span_metadata is not None: - for meta_key in span_metadata: - span.set_attribute(meta_key, span_metadata[meta_key]) + record_output_metadata(span, output=output) + return ActionResponse(response=output, trace_id=trace_id) - if len(args) > 0: - if isinstance(args[0], BaseModel): - span.set_attribute( - 'genkit:input', args[0].model_dump_json() - ) - else: - span.set_attribute('genkit:input', json.dumps(args[0])) + def sync_tracing_wrapper( + input: Any | None, ctx: ActionRunContext + ) -> ActionResponse: + """Wrap the function in a sync tracing wrapper. - output = fn(*args, **kwargs) + Args: + input: The input to the action. + ctx: The context to pass to the action. - span.set_attribute('genkit:state', 'success') + Returns: + The action response. + """ + with tracer.start_as_current_span(name) as span: + trace_id = str(span.get_span_context().trace_id) + record_input_metadata( + span=span, + kind=kind, + name=name, + span_metadata=span_metadata, + input=input, + ) - if isinstance(output, BaseModel): - span.set_attribute( - 'genkit:output', output.model_dump_json() - ) - else: - span.set_attribute('genkit:output', json.dumps(output)) + match len(action_args): + case 0: + output = fn() + case 1: + output = fn(input) + case 2: + output = fn(input, ctx) + case _: + raise ValueError('action fn must have 0-2 args...') - return ActionResponse(response=output, traceId=trace_id) + record_output_metadata(span, output=output) + return ActionResponse(response=output, trace_id=trace_id) - self.fn = fn_to_call + self.__fn = sync_tracing_wrapper + self.__afn = async_tracing_wrapper self.description = description self.metadata = metadata if metadata else {} - input_spec = inspect.getfullargspec(fn) - action_args = [k for k in input_spec.annotations if k != self.RETURN] - - if len(action_args) > 1: + if len(action_args) > 2: raise Exception('can only have one arg') if len(action_args) > 0: type_adapter = TypeAdapter(input_spec.annotations[action_args[0]]) self.input_schema = type_adapter.json_schema() self.input_type = type_adapter - self.metadata[self.INPUT_KEY] = self.input_schema + self.metadata[ActionMetadataKey.INPUT_KEY] = self.input_schema else: self.input_schema = TypeAdapter(Any).json_schema() - self.metadata[self.INPUT_KEY] = self.input_schema + self.input_type = None + self.metadata[ActionMetadataKey.INPUT_KEY] = self.input_schema - if self.RETURN in input_spec.annotations: - type_adapter = TypeAdapter(input_spec.annotations[self.RETURN]) + if ActionMetadataKey.RETURN in input_spec.annotations: + type_adapter = TypeAdapter( + input_spec.annotations[ActionMetadataKey.RETURN] + ) self.output_schema = type_adapter.json_schema() - self.metadata[self.OUTPUT_KEY] = self.output_schema + self.metadata[ActionMetadataKey.OUTPUT_KEY] = self.output_schema else: self.output_schema = TypeAdapter(Any).json_schema() - self.metadata[self.OUTPUT_KEY] = self.output_schema + self.metadata[ActionMetadataKey.OUTPUT_KEY] = self.output_schema + + def run( + self, + input: Any = None, + on_chunk: StreamingCallback | None = None, + context: dict[str, Any] | None = None, + telemetry_labels: dict[str, Any] | None = None, + ) -> ActionResponse: + """Run the action with input. + + Args: + input: The input to the action. + on_chunk: The callback to invoke when a chunk is received. + context: The context to pass to the action. + telemetry_labels: The telemetry labels to pass to the action. + + Returns: + The action response. + """ + # TODO: handle telemetry_labels + # TODO: propagate context down the callstack via contextvars + return self.__fn( + input, ActionRunContext(on_chunk=on_chunk, context=context) + ) + + async def arun( + self, + input: Any = None, + on_chunk: StreamingCallback | None = None, + context: dict[str, Any] | None = None, + telemetry_labels: dict[str, Any] | None = None, + ) -> ActionResponse: + """Run the action with raw input. + + Args: + input: The input to the action. + on_chunk: The callback to invoke when a chunk is received. + context: The context to pass to the action. + telemetry_labels: The telemetry labels to pass to the action. + + Returns: + The action response. + """ + # TODO: handle telemetry_labels + # TODO: propagate context down the callstack via contextvars + return await self.__afn( + input, ActionRunContext(on_chunk=on_chunk, context=context) + ) + + async def arun_raw( + self, + raw_input: Any, + on_chunk: StreamingCallback | None = None, + context: dict[str, Any] | None = None, + telemetry_labels: dict[str, Any] | None = None, + ): + """Run the action with raw input. + + Args: + raw_input: The raw input to the action. + on_chunk: The callback to invoke when a chunk is received. + context: The context to pass to the action. + telemetry_labels: The telemetry labels to pass to the action. + + Returns: + The action response. + """ + input_action = ( + self.input_type.validate_python(raw_input) + if self.input_type != None + else None + ) + return await self.arun( + input=input_action, + on_chunk=on_chunk, + context=context, + telemetry_labels=telemetry_labels, + ) + + +def record_input_metadata(span, kind, name, span_metadata, input): + """Record the input metadata for the action. + + Args: + span: The span to record the metadata for. + kind: The kind of action. + name: The name of the action. + span_metadata: The span metadata to record. + input: The input to the action. + """ + span.set_attribute('genkit:type', kind) + span.set_attribute('genkit:name', name) + if input is not None: + span.set_attribute('genkit:input', dump_json(input)) + + if span_metadata is not None: + for meta_key in span_metadata: + span.set_attribute(meta_key, span_metadata[meta_key]) + + +def record_output_metadata(span, output) -> None: + """Record the output metadata for the action. + + Args: + span: The span to record the metadata for. + output: The output to the action. + """ + span.set_attribute('genkit:state', 'success') + span.set_attribute('genkit:output', dump_json(output)) + + +def ensure_async(fn: Callable) -> Callable: + """Ensure the function is async. + + Args: + fn: The function to ensure is async. + + Returns: + The async function. + """ + is_async = asyncio.iscoroutinefunction(fn) + if is_async: + return fn + + async def async_wrapper(*args, **kwargs): + """Wrap the function in an async function. + + Args: + *args: The arguments to the function. + **kwargs: The keyword arguments to the function. + + Returns: + The result of the function. + """ + return fn(*args, **kwargs) + + return async_wrapper diff --git a/py/packages/genkit/src/genkit/core/action_test.py b/py/packages/genkit/src/genkit/core/action_test.py new file mode 100644 index 0000000000..3c075db100 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/action_test.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the action module.""" + +import pytest +from genkit.core.action import ( + Action, + ActionKind, + ActionRunContext, + create_action_key, + parse_action_key, +) + + +def test_action_enum_behaves_like_str() -> None: + """Ensure the ActionType behaves like a string. + + This test verifies that the ActionType enum values can be compared + directly with strings and that the correct variants are used. + """ + assert ActionKind.CHATLLM == 'chat-llm' + assert ActionKind.CUSTOM == 'custom' + assert ActionKind.EMBEDDER == 'embedder' + assert ActionKind.EVALUATOR == 'evaluator' + assert ActionKind.FLOW == 'flow' + assert ActionKind.INDEXER == 'indexer' + assert ActionKind.MODEL == 'model' + assert ActionKind.PROMPT == 'prompt' + assert ActionKind.RETRIEVER == 'retriever' + assert ActionKind.TEXTLLM == 'text-llm' + assert ActionKind.TOOL == 'tool' + assert ActionKind.UTIL == 'util' + + +def test_parse_action_key_valid() -> None: + """Test valid inputs.""" + test_cases = [ + ('/prompt/my-prompt', (ActionKind.PROMPT, 'my-prompt')), + ('/model/gpt-4', (ActionKind.MODEL, 'gpt-4')), + ('/custom/test-action', (ActionKind.CUSTOM, 'test-action')), + ('/flow/my-flow', (ActionKind.FLOW, 'my-flow')), + ] + + for key, expected in test_cases: + kind, name = parse_action_key(key) + assert kind == expected[0] + assert name == expected[1] + + +def test_parse_action_key_invalid_format() -> None: + """Test invalid formats.""" + invalid_keys = [ + 'invalid_key', # Missing separator + '/missing-kind', # Missing kind + 'missing-name/', # Missing name + '', # Empty string + '/', # Just separator + ] + + for key in invalid_keys: + with pytest.raises(ValueError, match='Invalid action key format'): + parse_action_key(key) + + +def test_create_action_key() -> None: + """Test that create_action_key returns the correct action key.""" + assert create_action_key(ActionKind.CUSTOM, 'foo') == '/custom/foo' + assert create_action_key(ActionKind.MODEL, 'foo') == '/model/foo' + assert create_action_key(ActionKind.PROMPT, 'foo') == '/prompt/foo' + assert create_action_key(ActionKind.RETRIEVER, 'foo') == '/retriever/foo' + assert create_action_key(ActionKind.TEXTLLM, 'foo') == '/text-llm/foo' + assert create_action_key(ActionKind.TOOL, 'foo') == '/tool/foo' + assert create_action_key(ActionKind.UTIL, 'foo') == '/util/foo' + + +@pytest.mark.asyncio +async def test_define_sync_action() -> None: + """Test that a sync action can be defined and run.""" + + def syncFoo(): + """A sync action that returns 'syncFoo'.""" + return 'syncFoo' + + syncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=syncFoo) + + assert (await syncFooAction.arun()).response == 'syncFoo' + assert syncFoo() == 'syncFoo' + + +@pytest.mark.asyncio +@pytest.mark.skip('bug, action ignores args without type annotation') +async def test_define_sync_action_with_input_without_type_annotation() -> None: + """Test that a sync action can be defined and run with an input without a type annotation.""" + + def syncFoo(input): + """A sync action that returns 'syncFoo' with an input.""" + return f'syncFoo {input}' + + syncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=syncFoo) + + assert (await syncFooAction.arun('foo')).response == 'syncFoo foo' + assert syncFoo('foo') == 'syncFoo foo' + + +@pytest.mark.asyncio +async def test_define_sync_action_with_input() -> None: + """Test that a sync action can be defined and run with an input.""" + + def syncFoo(input: str): + """A sync action that returns 'syncFoo' with an input.""" + return f'syncFoo {input}' + + syncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=syncFoo) + + assert (await syncFooAction.arun('foo')).response == 'syncFoo foo' + assert syncFoo('foo') == 'syncFoo foo' + + +@pytest.mark.asyncio +async def test_define_sync_action_with_input_and_context() -> None: + """Test that a sync action can be defined and run with an input and context.""" + + def syncFoo(input: str, ctx: ActionRunContext): + """A sync action that returns 'syncFoo' with an input and context.""" + return f'syncFoo {input} {ctx.context["foo"]}' + + syncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=syncFoo) + + assert ( + await syncFooAction.arun('foo', context={'foo': 'bar'}) + ).response == 'syncFoo foo bar' + assert ( + syncFoo('foo', ActionRunContext(context={'foo': 'bar'})) + == 'syncFoo foo bar' + ) + + +@pytest.mark.asyncio +async def test_define_sync_streaming_action() -> None: + """Test that a sync action can be defined and run with streaming output.""" + + def syncFoo(input: str, ctx: ActionRunContext): + """A sync action that returns 'syncFoo' with streaming output.""" + ctx.send_chunk('1') + ctx.send_chunk('2') + return 3 + + syncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=syncFoo) + + chunks = [] + + def on_chunk(c): + chunks.append(c) + + assert ( + await syncFooAction.arun( + 'foo', context={'foo': 'bar'}, on_chunk=on_chunk + ) + ).response == 3 + assert chunks == ['1', '2'] + + +@pytest.mark.asyncio +async def test_define_async_action() -> None: + """Test that an async action can be defined and run.""" + + async def asyncFoo(): + """An async action that returns 'asyncFoo'.""" + return 'asyncFoo' + + asyncFooAction = Action( + name='asyncFoo', kind=ActionKind.CUSTOM, fn=asyncFoo + ) + + assert (await asyncFooAction.arun()).response == 'asyncFoo' + assert (await asyncFoo()) == 'asyncFoo' + + +@pytest.mark.asyncio +async def test_define_async_action_with_input() -> None: + """Test that an async action can be defined and run with an input.""" + + async def asyncFoo(input: str): + """An async action that returns 'asyncFoo' with an input.""" + return f'syncFoo {input}' + + asyncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=asyncFoo) + + assert (await asyncFooAction.arun('foo')).response == 'syncFoo foo' + assert (await asyncFoo('foo')) == 'syncFoo foo' + + +@pytest.mark.asyncio +async def test_define_async_action_with_input_and_context() -> None: + """Test that an async action can be defined and run with an input and context.""" + + async def asyncFoo(input: str, ctx: ActionRunContext): + """An async action that returns 'syncFoo' with an input and context.""" + return f'syncFoo {input} {ctx.context["foo"]}' + + asyncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=asyncFoo) + + assert ( + await asyncFooAction.arun('foo', context={'foo': 'bar'}) + ).response == 'syncFoo foo bar' + assert ( + await asyncFoo('foo', ActionRunContext(context={'foo': 'bar'})) + ) == 'syncFoo foo bar' + + +@pytest.mark.asyncio +async def test_define_async_streaming_action() -> None: + """Test that an async action can be defined and run with streaming output.""" + + async def asyncFoo(input: str, ctx: ActionRunContext): + """An async action that returns 'syncFoo' with streaming output.""" + ctx.send_chunk('1') + ctx.send_chunk('2') + return 3 + + asyncFooAction = Action(name='syncFoo', kind=ActionKind.CUSTOM, fn=asyncFoo) + + chunks = [] + + def on_chunk(c): + chunks.append(c) + + assert ( + await asyncFooAction.arun( + 'foo', context={'foo': 'bar'}, on_chunk=on_chunk + ) + ).response == 3 + assert chunks == ['1', '2'] diff --git a/py/packages/genkit/src/genkit/core/codec.py b/py/packages/genkit/src/genkit/core/codec.py new file mode 100644 index 0000000000..5b0765079c --- /dev/null +++ b/py/packages/genkit/src/genkit/core/codec.py @@ -0,0 +1,45 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2. + +import json +from typing import Any + +from pydantic import BaseModel + + +def dump_dict(obj: Any): + """Converts an object to a dictionary, handling Pydantic BaseModel instances specially. + + If the input object is a Pydantic BaseModel, it returns a dictionary representation + of the model, excluding fields with `None` values and using aliases for field names. + For any other object type, it returns the object unchanged. + + Args: + obj: The object to potentially convert to a dictionary. + + Returns: + A dictionary if the input is a Pydantic BaseModel, otherwise the original object. + """ + if isinstance(obj, BaseModel): + return obj.model_dump(exclude_none=True, by_alias=True) + else: + return obj + + +def dump_json(obj: Any, indent=None) -> str: + """Dumps an object to a JSON string. + + If the object is a Pydantic BaseModel, it will be dumped using the + model_dump_json method using the by_alias flag set to True. Otherwise, the + object will be dumped using the json.dumps method. + + Args: + obj: The object to dump. + + Returns: + A JSON string. + """ + if isinstance(obj, BaseModel): + return obj.model_dump_json(by_alias=True, indent=indent) + else: + return json.dumps(obj) diff --git a/py/packages/genkit/src/genkit/core/codec_test.py b/py/packages/genkit/src/genkit/core/codec_test.py new file mode 100644 index 0000000000..06d766e8d3 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/codec_test.py @@ -0,0 +1,55 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the codec module.""" + +import pytest +from genkit.core.codec import dump_json +from pydantic import BaseModel + + +def test_dump_json_basic(): + """Test basic JSON serialization.""" + # Test dictionary + assert dump_json({'a': 1, 'b': 'test'}) == '{"a": 1, "b": "test"}' + + # Test list + assert dump_json([1, 2, 3]) == '[1, 2, 3]' + + # Test nested structures + assert ( + dump_json({'a': [1, 2], 'b': {'c': 3}}) + == '{"a": [1, 2], "b": {"c": 3}}' + ) + + +def test_dump_json_special_types(): + """Test JSON serialization of special Python types.""" + # Test None + assert dump_json(None) == 'null' + + # Test boolean + assert dump_json(True) == 'true' + assert dump_json(False) == 'false' + + +def test_dump_json_numbers(): + """Test JSON serialization of different number types.""" + # Test integers + assert dump_json(42) == '42' + + # Test floats + assert dump_json(3.14) == '3.14' + + # Test scientific notation + assert dump_json(1e-10) == '1e-10' + + +def test_dump_json_pydantic(): + """Test JSON serialization of Pydantic models.""" + + class MyModel(BaseModel): + a: int + b: str + + assert dump_json(MyModel(a=1, b='test')) == '{"a":1,"b":"test"}' diff --git a/py/packages/genkit/src/genkit/core/constants.py b/py/packages/genkit/src/genkit/core/constants.py new file mode 100644 index 0000000000..6af285d619 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/constants.py @@ -0,0 +1,7 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Module containing various core constants.""" + +# The version of Genkit sent over HTTP in the headers. +DEFAULT_GENKIT_VERSION = '1.0.5' diff --git a/py/packages/genkit/src/genkit/core/environment.py b/py/packages/genkit/src/genkit/core/environment.py new file mode 100644 index 0000000000..c3ef1b8dd3 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/environment.py @@ -0,0 +1,53 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Convenience functionality to determine the running environment.""" + +import os +from enum import StrEnum + + +class EnvVar(StrEnum): + """Enumerates all the environment variables used by Genkit.""" + + GENKIT_ENV = 'GENKIT_ENV' + + +class GenkitEnvironment(StrEnum): + """Enumerates all the environments Genkit can run in.""" + + DEV = 'dev' + PROD = 'prod' + + +def is_dev_environment() -> bool: + """Returns True if the current environment is a development environment. + + Returns: + True if the current environment is a development environment. + """ + return get_current_environment() == GenkitEnvironment.DEV + + +def is_prod_environment() -> bool: + """Returns True if the current environment is a production environment. + + Returns: + True if the current environment is a production environment. + """ + return get_current_environment() == GenkitEnvironment.PROD + + +def get_current_environment() -> GenkitEnvironment: + """Returns the current environment. + + Returns: + The current environment. + """ + env = os.getenv(EnvVar.GENKIT_ENV) + if env is None: + return GenkitEnvironment.PROD + try: + return GenkitEnvironment(env) + except ValueError: + return GenkitEnvironment.PROD diff --git a/py/packages/genkit/src/genkit/core/environment_test.py b/py/packages/genkit/src/genkit/core/environment_test.py new file mode 100644 index 0000000000..bbc97b3250 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/environment_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + + +"""Unit tests for the environment module.""" + +import os +from unittest import mock + +from genkit.core.environment import ( + EnvVar, + GenkitEnvironment, + get_current_environment, + is_dev_environment, + is_prod_environment, +) + + +def test_is_dev_environment() -> None: + """Test the is_dev_environment function. + + Verifies that the is_dev_environment function correctly detects + development environments based on environment variables. + """ + # Test when GENKIT_ENV is not set + with mock.patch.dict(os.environ, clear=True): + assert not is_dev_environment() + + # Test when GENKIT_ENV is set to 'dev' + with mock.patch.dict( + os.environ, {EnvVar.GENKIT_ENV: GenkitEnvironment.DEV} + ): + assert is_dev_environment() + + # Test when GENKIT_ENV is set to something else + with mock.patch.dict( + os.environ, {EnvVar.GENKIT_ENV: GenkitEnvironment.PROD} + ): + assert not is_dev_environment() + + +def test_is_prod_environment() -> None: + """Test the is_prod_environment function. + + Verifies that the is_prod_environment function correctly detects + production environments based on environment variables. + """ + # Test when GENKIT_ENV is not set + with mock.patch.dict(os.environ, clear=True): + assert is_prod_environment() + + # Test when GENKIT_ENV is set to 'prod' + with mock.patch.dict( + os.environ, {EnvVar.GENKIT_ENV: GenkitEnvironment.PROD} + ): + assert is_prod_environment() + + # Test when GENKIT_ENV is set to something else + with mock.patch.dict( + os.environ, {EnvVar.GENKIT_ENV: GenkitEnvironment.DEV} + ): + assert not is_prod_environment() + + +def test_get_current_environment() -> None: + """Test the get_current_environment function. + + Verifies that the get_current_environment function correctly returns + the current environment based on environment variables. + """ + # Test when GENKIT_ENV is not set + with mock.patch.dict(os.environ, clear=True): + assert get_current_environment() == GenkitEnvironment.PROD + + # Test when GENKIT_ENV is set to 'prod' + with mock.patch.dict( + os.environ, {EnvVar.GENKIT_ENV: GenkitEnvironment.PROD} + ): + assert get_current_environment() == GenkitEnvironment.PROD + + # Test when GENKIT_ENV is set to 'dev' + with mock.patch.dict( + os.environ, {EnvVar.GENKIT_ENV: GenkitEnvironment.DEV} + ): + assert get_current_environment() == GenkitEnvironment.DEV + + # Test when GENKIT_ENV is set to something else + with mock.patch.dict(os.environ, {EnvVar.GENKIT_ENV: 'invalid'}): + assert get_current_environment() == GenkitEnvironment.PROD diff --git a/py/packages/genkit/src/genkit/core/error.py b/py/packages/genkit/src/genkit/core/error.py new file mode 100644 index 0000000000..586665ff5f --- /dev/null +++ b/py/packages/genkit/src/genkit/core/error.py @@ -0,0 +1,189 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Base error classes and utilities for Genkit.""" + +from typing import Any + +from genkit.core.registry import Registry +from genkit.core.status_types import StatusName, http_status_code +from pydantic import BaseModel +from typing_extensions import TypeVar + + +class HttpErrorWireFormat(BaseModel): + """Wire format for HTTP errors.""" + + details: Any = None + message: str + status: StatusName + + model_config = { + 'frozen': True, + 'validate_assignment': True, + 'extra': 'forbid', + 'populate_by_name': True, + } + + +class GenkitError(Exception): + """Base error class for Genkit errors.""" + + def __init__( + self, + *, + status: StatusName, + message: str, + detail: Any = None, + source: str | None = None, + ) -> None: + """Initialize a GenkitError. + + Args: + status: The status name for this error. + message: The error message. + detail: Optional detail information. + source: Optional source of the error. + """ + source_prefix = f'{source}: ' if source else '' + super().__init__(f'{source_prefix}{status}: {message}') + self.original_message = message + self.code = http_status_code(status) + self.status = status + self.detail = detail + self.source = source + + def to_serializable(self) -> HttpErrorWireFormat: + """Returns a JSON-serializable representation of this object. + + Returns: + An HttpErrorWireFormat model instance. + """ + # This error type is used by 3P authors with the field "detail", + # but the actual Callable protocol value is "details" + return HttpErrorWireFormat( + details=self.detail, + status=self.status, + message=self.original_message, + ) + + +class UnstableApiError(GenkitError): + """Error raised when using unstable APIs from a more stable instance.""" + + def __init__(self, level: str = 'beta', message: str | None = None) -> None: + """Initialize an UnstableApiError. + + Args: + level: The stability level required. + message: Optional message describing which feature is not allowed. + """ + msg_prefix = f'{message} ' if message else '' + super().__init__( + status='FAILED_PRECONDITION', + message=f"{msg_prefix}This API requires '{level}' stability level.\n\n" + f'To use this feature, initialize Genkit using `from genkit.{level} import genkit`.', + ) + + +class UserFacingError(GenkitError): + """Error class for issues to be returned to users. + + Using this error allows a web framework handler (e.g. FastAPI, Flask) to know it + is safe to return the message in a request. Other kinds of errors will + result in a generic 500 message to avoid the possibility of internal + exceptions being leaked to attackers. + """ + + def __init__( + self, status: StatusName, message: str, details: Any = None + ) -> None: + """Initialize a UserFacingError. + + Args: + status: The status name for this error. + message: The error message. + details: Optional details to include. + """ + super().__init__(status=status, message=message, detail=details) + + +def get_http_status(error: Any) -> int: + """Get the HTTP status code for an error. + + Args: + error: The error to get the status code for. + + Returns: + The HTTP status code (500 for non-Genkit errors). + """ + if isinstance(error, GenkitError): + return error.code + return 500 + + +def get_callable_json(error: Any) -> HttpErrorWireFormat: + """Get the JSON representation of an error for callable responses. + + Args: + error: The error to convert to JSON. + + Returns: + An HttpErrorWireFormat model instance. + """ + if isinstance(error, GenkitError): + return error.to_serializable() + return HttpErrorWireFormat( + message='Internal Error', + status='INTERNAL', + details=str(error), + ) + + +def get_error_message(error: Any) -> str: + """Extract error message from an error object. + + Args: + error: The error to get the message from. + + Returns: + The error message string. + """ + if isinstance(error, Exception): + return str(error) + return str(error) + + +def get_error_stack(error: Exception) -> str | None: + """Extract stack trace from an error object. + + Args: + error: The error to get the stack trace from. + + Returns: + The stack trace string if available. + """ + if isinstance(error, Exception): + return str(error) + return None + + +T = TypeVar('T') + + +def assert_unstable( + registry: Registry, level: str = 'beta', message: str | None = None +) -> None: + """Assert that a feature is allowed at the current stability level. + + Args: + registry: The registry instance to check stability against. + level: The maximum stability channel allowed. + message: Optional message describing which feature is not allowed. + + Raises: + UnstableApiError: If the feature is not allowed at the current stability + level. + """ + if level == 'beta' and registry.api_stability == 'stable': + raise UnstableApiError(level, message) diff --git a/py/packages/genkit/src/genkit/core/error_test.py b/py/packages/genkit/src/genkit/core/error_test.py new file mode 100644 index 0000000000..d0248351bd --- /dev/null +++ b/py/packages/genkit/src/genkit/core/error_test.py @@ -0,0 +1,133 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the error module.""" + +import pytest +from genkit.core.error import ( + GenkitError, + HttpErrorWireFormat, + UnstableApiError, + UserFacingError, + assert_unstable, + get_callable_json, + get_error_message, + get_error_stack, + get_http_status, +) +from genkit.core.registry import Registry + + +def test_assert_unstable() -> None: + """Test assert_unstable works.""" + + registry: Registry = Registry() + registry.api_stability = 'stable' + + with pytest.raises(UnstableApiError): + assert_unstable(registry, level='beta') + + with pytest.raises(UnstableApiError): + assert_unstable(registry) # Default is beta. + + +# New tests start here +def test_genkit_error() -> None: + """Test that creating a GenkitError works.""" + error = GenkitError( + status='INVALID_ARGUMENT', + message='Test message', + detail='Test detail', + source='test_source', + ) + assert error.original_message == 'Test message' + assert error.code == 400 + assert error.status == 'INVALID_ARGUMENT' + assert error.detail == 'Test detail' + assert error.source == 'test_source' + assert str(error) == 'test_source: INVALID_ARGUMENT: Test message' + + # Test without source + error_no_source = GenkitError(status='INTERNAL', message='Test message 2') + assert str(error_no_source) == 'INTERNAL: Test message 2' + + +def test_genkit_error_to_json() -> None: + """Test that GenkitError can be serialized to JSON.""" + error = GenkitError( + status='NOT_FOUND', message='Resource not found', detail={'id': 123} + ) + serializable = error.to_serializable() + assert isinstance(serializable, HttpErrorWireFormat) + assert serializable.status == 'NOT_FOUND' + assert serializable.message == 'Resource not found' + assert serializable.details == {'id': 123} + + +def test_unstable_api_error() -> None: + """Test that creating an UnstableApiError works.""" + error = UnstableApiError(level='alpha', message='Test feature') + assert error.status == 'FAILED_PRECONDITION' + assert 'Test feature' in error.original_message + assert "This API requires 'alpha' stability level" in error.original_message + + error_no_message = UnstableApiError() + assert ( + "This API requires 'beta' stability level" + in error_no_message.original_message + ) + + +def test_user_facing_error() -> None: + """Test creating a UserFacingError.""" + error = UserFacingError( + status='UNAUTHENTICATED', + message='Please log in', + details='Session expired', + ) + assert error.status == 'UNAUTHENTICATED' + assert error.original_message == 'Please log in' + assert error.detail == 'Session expired' + + +def test_get_http_status() -> None: + """Test that get_http_status returns the correct HTTP status code.""" + genkit_error = GenkitError(status='PERMISSION_DENIED', message='No access') + assert get_http_status(genkit_error) == 403 + + non_genkit_error = ValueError('Some other error') + assert get_http_status(non_genkit_error) == 500 + + +def test_get_callable_json() -> None: + """Test that get_callable_json returns the correct JSON data.""" + genkit_error = GenkitError(status='DATA_LOSS', message='Oops') + json_data = get_callable_json(genkit_error) + assert isinstance(json_data, HttpErrorWireFormat) + assert json_data.status == 'DATA_LOSS' + assert json_data.message == 'Oops' + + non_genkit_error = TypeError('Type error') + json_data = get_callable_json(non_genkit_error) + assert isinstance(json_data, HttpErrorWireFormat) + assert json_data.status == 'INTERNAL' + assert json_data.message == 'Internal Error' + + +def test_get_error_message() -> None: + """Test that get_error_message returns the correct error message.""" + error_message = get_error_message(ValueError('Test Value Error')) + assert error_message == 'Test Value Error' + + error_message = get_error_message('Test String Error') + assert error_message == 'Test String Error' + + +def test_get_error_stack() -> None: + """Test that get_error_stack returns the correct error stack.""" + try: + raise ValueError('Example Error') + except ValueError as e: + tb = get_error_stack(e) + assert tb is not None + assert 'Example Error' in tb diff --git a/py/packages/genkit/src/genkit/core/plugin_abc.py b/py/packages/genkit/src/genkit/core/plugin_abc.py new file mode 100644 index 0000000000..9c725644fa --- /dev/null +++ b/py/packages/genkit/src/genkit/core/plugin_abc.py @@ -0,0 +1,36 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Abstract base class for Genkit plugins. + +This module defines the base plugin interface that all plugins must implement. +It provides a way to initialize and register plugin functionality. +""" + +import abc + +from genkit.core.registry import Registry + + +class Plugin(abc.ABC): + """Abstract base class for implementing Genkit plugins. + + This class defines the interface that all plugins must implement. Plugins + provide a way to extend functionality by registering new actions, models, or + other capabilities. + + Attributes: + registry: Registry for plugin functionality. + """ + + @abc.abstractmethod + def initialize(self, registry: Registry) -> None: + """Initialize the plugin with the given registry. + + Args: + registry: Registry to register plugin functionality. + + Returns: + None + """ + pass diff --git a/py/packages/genkit/src/genkit/core/reflection.py b/py/packages/genkit/src/genkit/core/reflection.py index 2aff3a239a..be476699c3 100644 --- a/py/packages/genkit/src/genkit/core/reflection.py +++ b/py/packages/genkit/src/genkit/core/reflection.py @@ -2,107 +2,153 @@ # SPDX-License-Identifier: Apache-2.0 -"""Exposes an API for inspecting and interacting with Genkit in development.""" +"""Development API for inspecting and interacting with Genkit. -import json +This module provides a reflection API server for inspection and interaction +during development. It exposes endpoints for health checks, action discovery, +and action execution. +""" +import asyncio +import json +import urllib.parse from http.server import BaseHTTPRequestHandler -from pydantic import BaseModel +from typing import Any +from genkit.core.codec import dump_json +from genkit.core.constants import DEFAULT_GENKIT_VERSION from genkit.core.registry import Registry +from genkit.core.web import HTTPHeader + + +def make_reflection_server( + registry: Registry, version=DEFAULT_GENKIT_VERSION, encoding='utf-8' +): + """Create and return a ReflectionServer class with the given registry. + Args: + registry: The registry to use for the reflection server. + version: The version string to use when setting the value of + the X-GENKIT-VERSION HTTP header. + encoding: The text encoding to use; default 'utf-8'. -def make_reflection_server(registry: Registry): - """Returns a ReflectionServer class.""" + Returns: + A ReflectionServer class configured with the given registry. + """ class ReflectionServer(BaseHTTPRequestHandler): - """Exposes an API for local development.""" + """HTTP request handler for the Genkit reflection API. - ENCODING = 'utf-8' + This handler provides endpoints for inspecting and interacting with + registered Genkit actions during development. + """ - def do_GET(self): - """Handles GET requests.""" + def do_GET(self) -> None: # noqa: N802 + """Handle GET requests to the reflection API. + + Endpoints: + - /api/__health: Returns 200 OK if the server is healthy + - /api/actions: Returns JSON describing all registered actions + + For the /api/actions endpoint, returns a JSON object mapping action + keys to their metadata, including input/output schemas. + """ if self.path == '/api/__health': self.send_response(200) elif self.path == '/api/actions': self.send_response(200) - self.send_header('Content-type', 'application/json') + self.send_header(HTTPHeader.CONTENT_TYPE, 'application/json') self.end_headers() - - actions = {} - for action_type in registry.actions: - for name in registry.actions[action_type]: - action = registry.lookup_action(action_type, name) - key = f'/{action_type}/{name}' - actions[key] = { - 'key': key, - 'name': action.name, - 'inputSchema': action.inputSchema, - 'outputSchema': action.outputSchema, - 'metadata': action.metadata, - } - - self.wfile.write(bytes(json.dumps(actions), self.ENCODING)) - + actions = registry.list_serializable_actions() + self.wfile.write(bytes(json.dumps(actions), encoding)) else: self.send_response(404) self.end_headers() - def do_POST(self): - """Handles POST requests.""" + def do_POST(self) -> None: # noqa: N802 + """Handle POST requests to the reflection API. + + Flow: + 1. Reads and validates the request payload + 2. Looks up the requested action + 3. Executes the action with the provided input + 4. Returns the action result as JSON with trace ID + + The response format varies based on whether the action returns a + Pydantic model or a plain value. + """ if self.path == '/api/notify': self.send_response(200) self.end_headers() - elif self.path == '/api/runAction': - content_len = int(self.headers.get('Content-Length')) + elif self.path.startswith('/api/runAction'): + content_len = int( + self.headers.get(HTTPHeader.CONTENT_LENGTH) or 0 + ) post_body = self.rfile.read(content_len) - payload = json.loads(post_body.decode(encoding=self.ENCODING)) - print(payload) - action = registry.lookup_by_absolute_name(payload['key']) - if '/flow/' in payload['key']: - input_action = action.inputType.validate_python( - payload['input']['start']['input'] - ) - else: - input_action = action.inputType.validate_python( - payload['input'] - ) - - output = action.fn(input_action) + payload = json.loads(post_body.decode(encoding=encoding)) + action = registry.lookup_action_by_key(payload['key']) + context = payload['context'] if 'context' in payload else {} + + query = urllib.parse.urlparse(self.path).query + query = urllib.parse.parse_qs(query) + if 'stream' in query != None and query['stream'][0] == 'true': + + def send_chunk(chunk): + self.wfile.write( + bytes( + dump_json(chunk), + encoding, + ) + ) + self.wfile.write(bytes('\n', encoding)) - self.send_response(200) - self.send_header('x-genkit-version', '0.9.1') - self.send_header('Content-type', 'application/json') - self.end_headers() + self.send_response(200) + self.send_header(HTTPHeader.X_GENKIT_VERSION, '0.0.1') + self.send_header( + HTTPHeader.CONTENT_TYPE, 'application/json' + ) + self.end_headers() - if isinstance(output.response, BaseModel): + output = asyncio.run( + action.arun_raw( + raw_input=payload['input'], + on_chunk=send_chunk, + context=context, + ) + ) self.wfile.write( bytes( - '{"result": ' - + output.response.model_dump_json() - + ', "traceId": "' - + output.traceId - + '"}', - self.ENCODING, + json.dumps({ + 'result': dump_json(output.response), + 'telemetry': {'traceId': output.trace_id}, + }), + encoding, ) ) else: + output = asyncio.run( + action.arun_raw( + raw_input=payload['input'], context=context + ) + ) + + self.send_response(200) + self.send_header(HTTPHeader.X_GENKIT_VERSION, '0.0.1') + self.send_header( + HTTPHeader.CONTENT_TYPE, 'application/json' + ) + self.end_headers() + self.wfile.write( bytes( - json.dumps( - { - 'result': output.response, - 'telemetry': {'traceId': output.traceId}, - } - ), - self.ENCODING, + json.dumps({ + 'result': dump_json(output.response), + 'telemetry': {'traceId': output.trace_id}, + }), + encoding, ) ) - else: - self.send_response(404) - self.end_headers() - return ReflectionServer diff --git a/py/packages/genkit/src/genkit/core/registry.py b/py/packages/genkit/src/genkit/core/registry.py index 823622adf3..1765cc13da 100644 --- a/py/packages/genkit/src/genkit/core/registry.py +++ b/py/packages/genkit/src/genkit/core/registry.py @@ -1,28 +1,140 @@ # Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 +"""Registry for managing Genkit resources and actions. -"""The registry is used to store and lookup resources.""" +This module provides the Registry class, which is the central repository for +storing and managing various Genkit resources such as actions, flows, +plugins, and schemas. The registry enables dynamic registration and lookup +of these resources during runtime. -from typing import Dict -from genkit.core.action import Action +Example: + >>> registry = Registry() + >>> registry.register_action('', 'my_action', ...) + >>> action = registry.lookup_action('', 'my_action') +""" + +from collections.abc import Callable +from typing import Any + +from genkit.core.action import ( + Action, + ActionKind, + create_action_key, + parse_action_key, +) + +type ActionName = str class Registry: - """Stores actions, trace stores, flow state stores, plugins, and schemas.""" + """Central repository for Genkit resources. + + The Registry class serves as the central storage and management system for + various Genkit resources including actions, trace stores, flow state stores, + plugins, and schemas. It provides methods for registering new resources and + looking them up at runtime. + + Attributes: + entries: A nested dictionary mapping ActionKind to a dictionary of + action names and their corresponding Action instances. + """ + + default_model: str | None = None + + def __init__(self): + """Initialize an empty Registry instance.""" + self.entries: dict[ActionKind, dict[ActionName, Action]] = {} + # TODO: Figure out how to set this. + self.api_stability: str = 'stable' + + def register_action( + self, + kind: ActionKind, + name: str, + fn: Callable, + description: str | None = None, + metadata: dict[str, Any] | None = None, + span_metadata: dict[str, str] | None = None, + ) -> Action: + """Register a new action with the registry. + + This method creates a new Action instance with the provided parameters + and registers it in the registry under the specified kind and name. + + Args: + kind: The type of action being registered (e.g., TOOL, MODEL). + name: A unique name for the action within its kind. + fn: The function to be called when the action is executed. + description: Optional human-readable description of the action. + metadata: Optional dictionary of metadata about the action. + span_metadata: Optional dictionary of tracing span metadata. + + Returns: + The newly created and registered Action instance. + """ + action = Action( + kind=kind, + name=name, + fn=fn, + description=description, + metadata=metadata, + span_metadata=span_metadata, + ) + if kind not in self.entries: + self.entries[kind] = {} + self.entries[kind][name] = action + return action + + def lookup_action(self, kind: ActionKind, name: str) -> Action | None: + """Look up an action by its kind and name. + + Args: + kind: The type of action to look up. + name: The name of the action to look up. + + Returns: + The Action instance if found, None otherwise. + """ + if kind in self.entries and name in self.entries[kind]: + return self.entries[kind][name] + + def lookup_action_by_key(self, key: str) -> Action | None: + """Look up an action using its combined key string. + + The key format is `/`, where kind must be a valid + `ActionKind` and name must be a registered action name within that kind. + + Args: + key: The action key in the format `/`. - actions: Dict[str, Dict[str, Action]] = {} + Returns: + The `Action` instance if found, None otherwise. - def register_action(self, action_type: str, name: str, action: Action): - if action_type not in self.actions: - self.actions[action_type] = {} - self.actions[action_type][name] = action + Raises: + ValueError: If the key format is invalid or the kind is not a valid + `ActionKind`. + """ + kind, name = parse_action_key(key) + return self.lookup_action(kind, name) - def lookup_action(self, action_type: str, name: str): - if action_type in self.actions and name in self.actions[action_type]: - return self.actions[action_type][name] - return None + def list_serializable_actions(self) -> dict[str, Action] | None: + """Enlist all the actions into a dictionary. - def lookup_by_absolute_name(self, name: str): - tkns = name.split('/', 2) - return self.lookup_action(tkns[1], tkns[2]) + Returns: + A dictionary of serializable Actions. + """ + actions = {} + for kind in self.entries: + for name in self.entries[kind]: + action = self.lookup_action(kind, name) + key = create_action_key(kind, name) + # TODO: Serialize the Action instance + actions[key] = { + 'key': key, + 'name': action.name, + 'inputSchema': action.input_schema, + 'outputSchema': action.output_schema, + 'metadata': action.metadata, + } + return actions diff --git a/py/packages/genkit/src/genkit/core/registry_test.py b/py/packages/genkit/src/genkit/core/registry_test.py new file mode 100644 index 0000000000..e922cda616 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/registry_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the registry module. + +This module contains unit tests for the Registry class and its associated +functionality, ensuring proper registration and management of Genkit resources. +""" + +import pytest +from genkit.core.action import ActionKind, ActionMetadataKey +from genkit.core.registry import Registry + + +def test_register_action_with_name_and_kind() -> None: + """Ensure we can register an action with a name and kind.""" + registry = Registry() + action = registry.register_action( + name='test_action', kind=ActionKind.CUSTOM, fn=lambda x: x + ) + got = registry.lookup_action(ActionKind.CUSTOM, 'test_action') + + assert got == action + assert got.name == 'test_action' + assert got.kind == ActionKind.CUSTOM + + +def test_lookup_action_by_key() -> None: + """Ensure we can lookup an action by its key.""" + registry = Registry() + action = registry.register_action( + name='test_action', kind=ActionKind.CUSTOM, fn=lambda x: x + ) + got = registry.lookup_action_by_key('/custom/test_action') + + assert got == action + assert got.name == 'test_action' + assert got.kind == ActionKind.CUSTOM + + +def test_lookup_action_by_key_invalid_format() -> None: + """Ensure lookup_action_by_key handles invalid key format.""" + registry = Registry() + with pytest.raises(ValueError, match='Invalid action key format'): + registry.lookup_action_by_key('invalid_key') + + +def test_list_serializable_actions() -> None: + """Ensure we can list serializable actions.""" + registry = Registry() + registry.register_action( + name='test_action', kind=ActionKind.CUSTOM, fn=lambda x: x + ) + + got = registry.list_serializable_actions() + assert got == { + '/custom/test_action': { + 'key': '/custom/test_action', + 'name': 'test_action', + 'inputSchema': {}, + 'outputSchema': {}, + 'metadata': { + ActionMetadataKey.INPUT_KEY: {}, + ActionMetadataKey.OUTPUT_KEY: {}, + }, + }, + } diff --git a/py/packages/genkit/src/genkit/core/schemas.py b/py/packages/genkit/src/genkit/core/schemas.py deleted file mode 100644 index c65f00d723..0000000000 --- a/py/packages/genkit/src/genkit/core/schemas.py +++ /dev/null @@ -1,519 +0,0 @@ -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - -# DO NOT EDIT: Generated by `generate_schema_types` from `genkit-schemas.json`. -from __future__ import annotations -from enum import Enum -from typing import Any -from pydantic import BaseModel, ConfigDict, Field, RootModel - - -class Model(RootModel[Any]): - root: Any - - -class InstrumentationLibrary(BaseModel): - model_config = ConfigDict(extra='forbid') - name: str - version: str | None = None - schemaUrl: str | None = None - - -class SpanContext(BaseModel): - model_config = ConfigDict(extra='forbid') - traceId: str - spanId: str - isRemote: bool | None = None - traceFlags: float - - -class SameProcessAsParentSpan(BaseModel): - model_config = ConfigDict(extra='forbid') - value: bool - - -class State(Enum): - success = 'success' - error = 'error' - - -class SpanMetadata(BaseModel): - model_config = ConfigDict(extra='forbid') - name: str - state: State | None = None - input: Any | None = None - output: Any | None = None - isRoot: bool | None = None - metadata: dict[str, str] | None = None - - -class SpanStatus(BaseModel): - model_config = ConfigDict(extra='forbid') - code: float - message: str | None = None - - -class Annotation(BaseModel): - model_config = ConfigDict(extra='forbid') - attributes: dict[str, Any] - description: str - - -class TimeEvent(BaseModel): - model_config = ConfigDict(extra='forbid') - time: float - annotation: Annotation - - -class Code(Enum): - blocked = 'blocked' - other = 'other' - unknown = 'unknown' - - -class CandidateError(BaseModel): - model_config = ConfigDict(extra='forbid') - index: float - code: Code - message: str | None = None - - -class DataPart(BaseModel): - model_config = ConfigDict(extra='forbid') - text: Any | None = None - media: Any | None = None - toolRequest: Any | None = None - toolResponse: Any | None = None - data: Any | None = None - metadata: dict[str, Any] | None = None - - -class FinishReason(Enum): - stop = 'stop' - length = 'length' - blocked = 'blocked' - interrupted = 'interrupted' - other = 'other' - unknown = 'unknown' - - -class Content(BaseModel): - model_config = ConfigDict(extra='forbid') - text: str - media: Any | None = None - - -class Media(BaseModel): - model_config = ConfigDict(extra='forbid') - contentType: str | None = None - url: str - - -class Content1(BaseModel): - model_config = ConfigDict(extra='forbid') - text: Any | None = None - media: Media - - -class Doc(BaseModel): - model_config = ConfigDict(extra='forbid') - content: list[Content | Content1] - metadata: dict[str, Any] | None = None - - -class ToolChoice(Enum): - auto = 'auto' - required = 'required' - none = 'none' - - -class Output(BaseModel): - model_config = ConfigDict(extra='forbid') - format: str | None = None - contentType: str | None = None - instructions: bool | str | None = None - jsonSchema: Any | None = None - constrained: bool | None = None - - -class Format(Enum): - json = 'json' - text = 'text' - media = 'media' - - -class Output1(BaseModel): - model_config = ConfigDict(extra='forbid') - format: Format | None = None - schema_: dict[str, Any] | None = Field(None, alias='schema') - - -class GenerationCommonConfig(BaseModel): - model_config = ConfigDict(extra='forbid') - version: str | None = None - temperature: float | None = None - maxOutputTokens: float | None = None - topK: float | None = None - topP: float | None = None - stopSequences: list[str] | None = None - - -class GenerationUsage(BaseModel): - model_config = ConfigDict(extra='forbid') - inputTokens: float | None = None - outputTokens: float | None = None - totalTokens: float | None = None - inputCharacters: float | None = None - outputCharacters: float | None = None - inputImages: float | None = None - outputImages: float | None = None - inputVideos: float | None = None - outputVideos: float | None = None - inputAudioFiles: float | None = None - outputAudioFiles: float | None = None - custom: dict[str, float] | None = None - - -class Constrained(Enum): - none = 'none' - all = 'all' - no_tools = 'no-tools' - - -class Supports(BaseModel): - model_config = ConfigDict(extra='forbid') - multiturn: bool | None = None - media: bool | None = None - tools: bool | None = None - systemRole: bool | None = None - output: list[str] | None = None - contentType: list[str] | None = None - context: bool | None = None - constrained: Constrained | None = None - toolChoice: bool | None = None - - -class ModelInfo(BaseModel): - model_config = ConfigDict(extra='forbid') - versions: list[str] | None = None - label: str | None = None - supports: Supports | None = None - - -class Role(Enum): - system = 'system' - user = 'user' - model = 'model' - tool = 'tool' - - -class ToolDefinition(BaseModel): - model_config = ConfigDict(extra='forbid') - name: str - description: str - inputSchema: dict[str, Any] = Field( - ..., description='Valid JSON Schema representing the input of the tool.' - ) - outputSchema: dict[str, Any] | None = Field( - None, description='Valid JSON Schema describing the output of the tool.' - ) - metadata: dict[str, Any] | None = Field( - None, description='additional metadata for this tool definition' - ) - - -class ToolRequest1(BaseModel): - model_config = ConfigDict(extra='forbid') - ref: str | None = None - name: str - input: Any | None = None - - -class ToolResponse1(BaseModel): - model_config = ConfigDict(extra='forbid') - ref: str | None = None - name: str - output: Any | None = None - - -class MediaModel(RootModel[Any]): - root: Any - - -class Metadata(RootModel[dict[str, Any] | None]): - root: dict[str, Any] | None = None - - -class Text(RootModel[Any]): - root: Any - - -class ToolRequest(RootModel[Any]): - root: Any - - -class ToolResponse(RootModel[Any]): - root: Any - - -class Content2(BaseModel): - model_config = ConfigDict(extra='forbid') - text: str - media: Any | None = None - - -class Media2(BaseModel): - model_config = ConfigDict(extra='forbid') - contentType: str | None = None - url: str - - -class Content3(BaseModel): - model_config = ConfigDict(extra='forbid') - text: Any | None = None - media: Media2 - - -class Items(BaseModel): - model_config = ConfigDict(extra='forbid') - content: list[Content2 | Content3] - metadata: dict[str, Any] | None = None - - -class Config(RootModel[Any]): - root: Any - - -class OutputModel(BaseModel): - model_config = ConfigDict(extra='forbid') - format: Format | None = None - schema_: dict[str, Any] | None = Field(None, alias='schema') - - -class Tools(RootModel[list[ToolDefinition]]): - root: list[ToolDefinition] - - -class Custom(RootModel[Any]): - root: Any - - -class FinishMessage(RootModel[str]): - root: str - - -class LatencyMs(RootModel[float]): - root: float - - -class Usage(RootModel[GenerationUsage]): - root: GenerationUsage - - -class Aggregated(RootModel[bool]): - root: bool - - -class Index(RootModel[float]): - root: float - - -class Data(RootModel[Any]): - root: Any - - -class Link(BaseModel): - model_config = ConfigDict(extra='forbid') - context: SpanContext | None = None - attributes: dict[str, Any] | None = None - droppedAttributesCount: float | None = None - - -class TimeEvents(BaseModel): - model_config = ConfigDict(extra='forbid') - timeEvent: list[TimeEvent] | None = None - - -class SpanData(BaseModel): - model_config = ConfigDict(extra='forbid') - spanId: str - traceId: str - parentSpanId: str | None = None - startTime: float - endTime: float - attributes: dict[str, Any] - displayName: str - links: list[Link] | None = None - instrumentationLibrary: InstrumentationLibrary - spanKind: str - sameProcessAsParentSpan: SameProcessAsParentSpan | None = None - status: SpanStatus | None = None - timeEvents: TimeEvents | None = None - truncated: bool | None = None - - -class TraceData(BaseModel): - model_config = ConfigDict(extra='forbid') - traceId: str - displayName: str | None = None - startTime: float | None = None - endTime: float | None = None - spans: dict[str, SpanData] - - -class MediaPart(BaseModel): - model_config = ConfigDict(extra='forbid') - text: Text | None = None - media: Media - toolRequest: ToolRequest | None = None - toolResponse: ToolResponse | None = None - data: Any | None = None - metadata: Metadata | None = None - - -class TextPart(BaseModel): - model_config = ConfigDict(extra='forbid') - text: str - media: MediaModel | None = None - toolRequest: ToolRequest | None = None - toolResponse: ToolResponse | None = None - data: Data | None = None - metadata: Metadata | None = None - - -class ToolRequestPart(BaseModel): - model_config = ConfigDict(extra='forbid') - text: Text | None = None - media: MediaModel | None = None - toolRequest: ToolRequest1 - toolResponse: ToolResponse | None = None - data: Data | None = None - metadata: Metadata | None = None - - -class ToolResponsePart(BaseModel): - model_config = ConfigDict(extra='forbid') - text: Text | None = None - media: MediaModel | None = None - toolRequest: ToolRequest | None = None - toolResponse: ToolResponse1 - data: Data | None = None - metadata: Metadata | None = None - - -class Part( - RootModel[ - TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart - ] -): - root: TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart - - -class ContentModel(RootModel[list[Part]]): - root: list[Part] - - -class DocumentData(BaseModel): - model_config = ConfigDict(extra='forbid') - content: list[Part] - metadata: dict[str, Any] | None = None - - -class GenerateResponseChunk(BaseModel): - model_config = ConfigDict(extra='forbid') - role: Role | None = None - index: float | None = None - content: list[Part] - custom: Any | None = None - aggregated: bool | None = None - - -class Message(BaseModel): - model_config = ConfigDict(extra='forbid') - role: Role - content: list[Part] - metadata: dict[str, Any] | None = None - - -class ModelResponseChunk(BaseModel): - model_config = ConfigDict(extra='forbid') - role: Role | None = None - index: Index | None = None - content: ContentModel - custom: Custom | None = None - aggregated: Aggregated | None = None - - -class Messages(RootModel[list[Message]]): - root: list[Message] - - -class Candidate(BaseModel): - model_config = ConfigDict(extra='forbid') - index: float - message: Message - usage: GenerationUsage | None = None - finishReason: FinishReason - finishMessage: str | None = None - custom: Any | None = None - - -class GenerateActionOptions(BaseModel): - model_config = ConfigDict(extra='forbid') - model: str - docs: list[Doc] | None = None - messages: list[Message] - tools: list[str] | None = None - toolChoice: ToolChoice | None = None - config: Any | None = None - output: Output | None = None - returnToolRequests: bool | None = None - maxTurns: float | None = None - - -class GenerateRequest(BaseModel): - model_config = ConfigDict(extra='forbid') - messages: list[Message] - config: Any | None = None - tools: list[ToolDefinition] | None = None - toolChoice: ToolChoice | None = None - output: Output1 | None = None - context: list[Items] | None = None - candidates: float | None = None - - -class GenerateResponse(BaseModel): - model_config = ConfigDict(extra='forbid') - message: Message | None = None - finishReason: FinishReason | None = None - finishMessage: str | None = None - latencyMs: float | None = None - usage: GenerationUsage | None = None - custom: Any | None = None - request: GenerateRequest | None = None - candidates: list[Candidate] | None = None - - -class ModelRequest(BaseModel): - model_config = ConfigDict(extra='forbid') - messages: Messages - config: Config | None = None - tools: Tools | None = None - toolChoice: ToolChoice | None = None - output: OutputModel | None = None - context: list[Items] | None = None - - -class Request(RootModel[GenerateRequest]): - root: GenerateRequest - - -class ModelResponse(BaseModel): - model_config = ConfigDict(extra='forbid') - message: Message | None = None - finishReason: FinishReason - finishMessage: FinishMessage | None = None - latencyMs: LatencyMs | None = None - usage: Usage | None = None - custom: Custom | None = None - request: Request | None = None diff --git a/py/packages/genkit/src/genkit/core/status_types.py b/py/packages/genkit/src/genkit/core/status_types.py new file mode 100644 index 0000000000..724ae4a69f --- /dev/null +++ b/py/packages/genkit/src/genkit/core/status_types.py @@ -0,0 +1,236 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Enumeration of response status codes and their corresponding messages.""" + +from enum import IntEnum +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class StatusCodes(IntEnum): + """Enumeration of response status codes.""" + + # Not an error; returned on success. + # + # HTTP Mapping: 200 OK + OK = 0 + + # The operation was cancelled, typically by the caller. + # + # HTTP Mapping: 499 Client Closed Request + CANCELLED = 1 + + # Unknown error. For example, this error may be returned when + # a `Status` value received from another address space belongs to + # an error space that is not known in this address space. Also + # errors raised by APIs that do not return enough error information + # may be converted to this error. + # + # HTTP Mapping: 500 Internal Server Error + UNKNOWN = 2 + + # The client specified an invalid argument. Note that this differs + # from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments + # that are problematic regardless of the state of the system + # (e.g., a malformed file name). + # + # HTTP Mapping: 400 Bad Request + INVALID_ARGUMENT = 3 + + # The deadline expired before the operation could complete. For operations + # that change the state of the system, this error may be returned + # even if the operation has completed successfully. For example, a + # successful response from a server could have been delayed long + # enough for the deadline to expire. + # + # HTTP Mapping: 504 Gateway Timeout + DEADLINE_EXCEEDED = 4 + + # Some requested entity (e.g., file or directory) was not found. + # + # Note to server developers: if a request is denied for an entire class + # of users, such as gradual feature rollout or undocumented allowlist, + # `NOT_FOUND` may be used. If a request is denied for some users within + # a class of users, such as user-based access control, `PERMISSION_DENIED` + # must be used. + # + # HTTP Mapping: 404 Not Found + NOT_FOUND = 5 + + # The entity that a client attempted to create (e.g., file or directory) + # already exists. + # + # HTTP Mapping: 409 Conflict + ALREADY_EXISTS = 6 + + # The caller does not have permission to execute the specified + # operation. `PERMISSION_DENIED` must not be used for rejections + # caused by exhausting some resource (use `RESOURCE_EXHAUSTED` + # instead for those errors). `PERMISSION_DENIED` must not be + # used if the caller can not be identified (use `UNAUTHENTICATED` + # instead for those errors). This error code does not imply the + # request is valid or the requested entity exists or satisfies + # other pre-conditions. + # + # HTTP Mapping: 403 Forbidden + PERMISSION_DENIED = 7 + + # The request does not have valid authentication credentials for the + # operation. + # + # HTTP Mapping: 401 Unauthorized + UNAUTHENTICATED = 16 + + # Some resource has been exhausted, perhaps a per-user quota, or + # perhaps the entire file system is out of space. + # + # HTTP Mapping: 429 Too Many Requests + RESOURCE_EXHAUSTED = 8 + + # The operation was rejected because the system is not in a state + # required for the operation's execution. For example, the directory + # to be deleted is non-empty, an rmdir operation is applied to + # a non-directory, etc. + # + # Service implementors can use the following guidelines to decide + # between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: + # (a) Use `UNAVAILABLE` if the client can retry just the failing call. + # (b) Use `ABORTED` if the client should retry at a higher level. For + # example, when a client-specified test-and-set fails, indicating the + # client should restart a read-modify-write sequence. + # (c) Use `FAILED_PRECONDITION` if the client should not retry until the + # system state has been explicitly fixed. For example, if an "rmdir" + # fails because the directory is non-empty, `FAILED_PRECONDITION` + # should be returned since the client should not retry unless the files + # are deleted from the directory. + # + # HTTP Mapping: 400 Bad Request + FAILED_PRECONDITION = 9 + + # The operation was aborted, typically due to a concurrency issue such as + # a sequencer check failure or transaction abort. + # + # See the guidelines above for deciding between `FAILED_PRECONDITION`, + # `ABORTED`, and `UNAVAILABLE`. + # + # HTTP Mapping: 409 Conflict + ABORTED = 10 + + # The operation was attempted past the valid range. E.g., seeking or + # reading past end-of-file. + # + # Unlike `INVALID_ARGUMENT`, this error indicates a problem that may + # be fixed if the system state changes. For example, a 32-bit file + # system will generate `INVALID_ARGUMENT` if asked to read at an + # offset that is not in the range [0,2^32-1], but it will generate + # `OUT_OF_RANGE` if asked to read from an offset past the current + # file size. + # + # There is a fair bit of overlap between `FAILED_PRECONDITION` and + # `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific + # error) when it applies so that callers who are iterating through + # a space can easily look for an `OUT_OF_RANGE` error to detect when + # they are done. + # + # HTTP Mapping: 400 Bad Request + OUT_OF_RANGE = 11 + + # The operation is not implemented or is not supported/enabled in this + # service. + # + # HTTP Mapping: 501 Not Implemented + UNIMPLEMENTED = 12 + + # Internal errors. This means that some invariants expected by the + # underlying system have been broken. This error code is reserved + # for serious errors. + # + # HTTP Mapping: 500 Internal Server Error + INTERNAL = 13 + + # The service is currently unavailable. This is most likely a + # transient condition, which can be corrected by retrying with + # a backoff. Note that it is not always safe to retry + # non-idempotent operations. + # + # See the guidelines above for deciding between `FAILED_PRECONDITION`, + # `ABORTED`, and `UNAVAILABLE`. + # + # HTTP Mapping: 503 Service Unavailable + UNAVAILABLE = 14 + + # Unrecoverable data loss or corruption. + # + # HTTP Mapping: 500 Internal Server Error + DATA_LOSS = 15 + + +# Type alias for status names +type StatusName = Literal[ + 'OK', + 'CANCELLED', + 'UNKNOWN', + 'INVALID_ARGUMENT', + 'DEADLINE_EXCEEDED', + 'NOT_FOUND', + 'ALREADY_EXISTS', + 'PERMISSION_DENIED', + 'UNAUTHENTICATED', + 'RESOURCE_EXHAUSTED', + 'FAILED_PRECONDITION', + 'ABORTED', + 'OUT_OF_RANGE', + 'UNIMPLEMENTED', + 'INTERNAL', + 'UNAVAILABLE', + 'DATA_LOSS', +] + +# Mapping of status names to HTTP status codes +_STATUS_CODE_MAP: dict[StatusName, int] = { + 'OK': 200, + 'CANCELLED': 499, + 'UNKNOWN': 500, + 'INVALID_ARGUMENT': 400, + 'DEADLINE_EXCEEDED': 504, + 'NOT_FOUND': 404, + 'ALREADY_EXISTS': 409, + 'PERMISSION_DENIED': 403, + 'UNAUTHENTICATED': 401, + 'RESOURCE_EXHAUSTED': 429, + 'FAILED_PRECONDITION': 400, + 'ABORTED': 409, + 'OUT_OF_RANGE': 400, + 'UNIMPLEMENTED': 501, + 'INTERNAL': 500, + 'UNAVAILABLE': 503, + 'DATA_LOSS': 500, +} + + +def http_status_code(status: StatusName) -> int: + """Gets the HTTP status code for a given status name. + + Args: + status: The status name to get the HTTP code for. + + Returns: + The corresponding HTTP status code. + """ + return _STATUS_CODE_MAP[status] + + +class Status(BaseModel): + """Represents a status with a name and optional message.""" + + model_config = ConfigDict( + frozen=True, + validate_assignment=True, + extra='forbid', + populate_by_name=True, + ) + + name: StatusName + message: str = Field(default='') diff --git a/py/packages/genkit/src/genkit/core/status_types_test.py b/py/packages/genkit/src/genkit/core/status_types_test.py new file mode 100644 index 0000000000..5240897ea6 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/status_types_test.py @@ -0,0 +1,112 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for status_types module.""" + +import pytest +from genkit.core.status_types import Status, StatusCodes, http_status_code +from pydantic import ValidationError + + +def test_status_codes_values() -> None: + """Tests that StatusCodes has correct values and can be used as ints.""" + assert StatusCodes.OK == 0 + assert StatusCodes.CANCELLED == 1 + assert StatusCodes.UNKNOWN == 2 + assert StatusCodes.INVALID_ARGUMENT == 3 + assert StatusCodes.DEADLINE_EXCEEDED == 4 + assert StatusCodes.NOT_FOUND == 5 + assert StatusCodes.ALREADY_EXISTS == 6 + assert StatusCodes.PERMISSION_DENIED == 7 + assert StatusCodes.UNAUTHENTICATED == 16 + assert StatusCodes.RESOURCE_EXHAUSTED == 8 + assert StatusCodes.FAILED_PRECONDITION == 9 + assert StatusCodes.ABORTED == 10 + assert StatusCodes.OUT_OF_RANGE == 11 + assert StatusCodes.UNIMPLEMENTED == 12 + assert StatusCodes.INTERNAL == 13 + assert StatusCodes.UNAVAILABLE == 14 + assert StatusCodes.DATA_LOSS == 15 + + +def test_status_immutability() -> None: + """Tests that Status objects are immutable.""" + status = Status(name='OK') + + with pytest.raises(ValidationError): + status.name = 'NOT_FOUND' + + with pytest.raises(ValidationError): + status.message = 'New message' + + +def test_status_validation() -> None: + """Tests that Status validates inputs correctly.""" + # Test invalid status name + with pytest.raises(ValidationError): + Status(name='INVALID_STATUS') + + # Test with invalid type for name + with pytest.raises(ValidationError): + Status(name=123) + + # Test with invalid type for message + with pytest.raises(ValidationError): + Status(name='OK', message=123) + + # Test with extra fields + with pytest.raises(ValidationError): + Status(name='OK', extra_field='value') + + +def test_http_status_code_mapping() -> None: + """Tests http_status_code function returns correct HTTP status codes.""" + assert http_status_code('OK') == 200 + assert http_status_code('CANCELLED') == 499 + assert http_status_code('UNKNOWN') == 500 + assert http_status_code('INVALID_ARGUMENT') == 400 + assert http_status_code('DEADLINE_EXCEEDED') == 504 + assert http_status_code('NOT_FOUND') == 404 + assert http_status_code('ALREADY_EXISTS') == 409 + assert http_status_code('PERMISSION_DENIED') == 403 + assert http_status_code('UNAUTHENTICATED') == 401 + assert http_status_code('RESOURCE_EXHAUSTED') == 429 + assert http_status_code('FAILED_PRECONDITION') == 400 + assert http_status_code('ABORTED') == 409 + assert http_status_code('OUT_OF_RANGE') == 400 + assert http_status_code('UNIMPLEMENTED') == 501 + assert http_status_code('INTERNAL') == 500 + assert http_status_code('UNAVAILABLE') == 503 + assert http_status_code('DATA_LOSS') == 500 + + +def test_http_status_code_invalid_input() -> None: + """Tests http_status_code function with invalid input.""" + with pytest.raises(KeyError): + http_status_code('INVALID_STATUS') + + +def test_status_json_serialization() -> None: + """Tests that Status objects can be serialized to JSON.""" + status = Status(name='NOT_FOUND', message='Resource not found') + json_data = status.model_dump_json() + assert '"name":"NOT_FOUND"' in json_data + assert '"message":"Resource not found"' in json_data + + +def test_status_json_deserialization() -> None: + """Tests that Status objects can be deserialized from JSON.""" + json_data = '{"name": "NOT_FOUND", "message": "Resource not found"}' + status = Status.model_validate_json(json_data) + assert status.name == 'NOT_FOUND' + assert status.message == 'Resource not found' + + +def test_status_equality() -> None: + """Tests Status equality comparison.""" + status1 = Status(name='OK') + status2 = Status(name='OK') + status3 = Status(name='NOT_FOUND') + + assert status1 == status2 + assert status1 != status3 diff --git a/py/packages/genkit/src/genkit/core/tracing.py b/py/packages/genkit/src/genkit/core/tracing.py index 2895a75f9a..1782c46ac1 100644 --- a/py/packages/genkit/src/genkit/core/tracing.py +++ b/py/packages/genkit/src/genkit/core/tracing.py @@ -2,33 +2,56 @@ # SPDX-License-Identifier: Apache-2.0 -"""Collects telemetry.""" +"""Telemetry and tracing functionality for the Genkit framework. + +This module provides functionality for collecting and exporting telemetry data +from Genkit operations. It uses OpenTelemetry for tracing and exports span +data to a telemetry server for monitoring and debugging purposes. + +The module includes: + - A custom span exporter for sending trace data to a telemetry server + - Utility functions for converting and formatting trace attributes + - Configuration for development environment tracing +""" import json -import os import sys -from typing import Any, Dict, Sequence +from collections.abc import Sequence +from typing import Any -import requests -from opentelemetry.sdk.trace import TracerProvider +import requests # type: ignore[import-untyped] +from genkit.core.environment import is_dev_environment +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider from opentelemetry.sdk.trace.export import ( + SimpleSpanProcessor, SpanExporter, SpanExportResult, - SimpleSpanProcessor, ) -from opentelemetry import trace as trace_api -from opentelemetry.sdk.trace import ReadableSpan class TelemetryServerSpanExporter(SpanExporter): - """Implementation of :class:`SpanExporter` that prints spans to the - console. + """SpanExporter implementation that exports spans to a telemetry server. - This class can be used for diagnostic purposes. It prints the exported - spans to the console STDOUT. + This exporter sends span data to a telemetry server (default: + http://localhost:4033) for monitoring and debugging. Each span is converted + to a JSON format that includes trace ID, span ID, timing information, + attributes, and other metadata about the operation. """ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + """Export the spans to the telemetry server. + + This method processes each span in the sequence, converts it to the + required JSON format, and sends it to the telemetry server via HTTP + POST. + + Args: + spans: A sequence of ReadableSpan objects to export. + + Returns: + SpanExportResult.SUCCESS if the export was successful. + """ for span in spans: span_data = {'traceId': f'{span.context.trace_id}', 'spans': {}} span_data['spans'][span.context.span_id] = { @@ -36,7 +59,9 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: 'traceId': f'{span.context.trace_id}', 'startTime': span.start_time / 1000000, 'endTime': span.end_time / 1000000, - 'attributes': convert_attributes(span.attributes), + 'attributes': convert_attributes( + attributes=span.attributes, # type: ignore + ), 'displayName': span.name, # "links": span.links, 'spanKind': trace_api.SpanKind(span.kind).name, @@ -63,15 +88,16 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: # })), # }, } - if not span_data['spans'][span.context.span_id]['parentSpanId']: - del span_data['spans'][span.context.span_id]['parentSpanId'] + if not span_data['spans'][span.context.span_id]['parentSpanId']: # type: ignore + del span_data['spans'][span.context.span_id]['parentSpanId'] # type: ignore if not span.parent: span_data['displayName'] = span.name span_data['startTime'] = span.start_time span_data['endTime'] = span.end_time - # TODO: telemetry server URL must be dynamic, whatever tools notification says + # TODO: telemetry server URL must be dynamic, + # whatever tools notification says requests.post( 'http://localhost:4033/api/traces', data=json.dumps(span_data), @@ -85,17 +111,36 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return SpanExportResult.SUCCESS def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush any pending spans to the telemetry server. + + Args: + timeout_millis: Maximum time to wait for the flush to complete. + + Returns: + True if the flush was successful, False otherwise. + """ return True -def convert_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: - attrs: Dict[str, Any] = {} +def convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: + """Convert span attributes to a format suitable for export. + + This function creates a new dictionary containing the span attributes, + ensuring they are in a format that can be properly serialized. + + Args: + attributes: Dictionary of span attributes to convert. + + Returns: + A new dictionary containing the converted attributes. + """ + attrs: dict[str, Any] = {} for key in attributes: attrs[key] = attributes[key] return attrs -if 'GENKIT_ENV' in os.environ and os.environ['GENKIT_ENV'] == 'dev': +if is_dev_environment(): provider = TracerProvider() processor = SimpleSpanProcessor(TelemetryServerSpanExporter()) provider.add_span_processor(processor) diff --git a/py/packages/genkit/src/genkit/core/typing.py b/py/packages/genkit/src/genkit/core/typing.py new file mode 100644 index 0000000000..c040c665d7 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/typing.py @@ -0,0 +1,693 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +# +# DO NOT EDIT: Generated by `generate_schema_typing` from `genkit-schemas.json`. + +"""Schema types module defining the core data models for Genkit. + +This module contains Pydantic models that define the structure and validation +for various data types used throughout the Genkit framework, including messages, +actions, tools, and configuration options. +""" + +from __future__ import annotations + +from enum import StrEnum +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, RootModel + + +class Model(RootModel[Any]): + """Model data type class.""" + + root: Any + + +class InstrumentationLibrary(BaseModel): + """Model for instrumentationlibrary data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + name: str + version: str | None = None + schema_url: str | None = Field(None, alias='schemaUrl') + + +class SpanContext(BaseModel): + """Model for spancontext data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + trace_id: str = Field(..., alias='traceId') + span_id: str = Field(..., alias='spanId') + is_remote: bool | None = Field(None, alias='isRemote') + trace_flags: float = Field(..., alias='traceFlags') + + +class SameProcessAsParentSpan(BaseModel): + """Model for sameprocessasparentspan data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + value: bool + + +class State(StrEnum): + """Enumeration of state values.""" + + SUCCESS = 'success' + ERROR = 'error' + + +class SpanMetadata(BaseModel): + """Model for spanmetadata data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + name: str + state: State | None = None + input: Any | None = None + output: Any | None = None + is_root: bool | None = Field(None, alias='isRoot') + metadata: dict[str, str] | None = None + + +class SpanStatus(BaseModel): + """Model for spanstatus data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + code: float + message: str | None = None + + +class Annotation(BaseModel): + """Model for annotation data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + attributes: dict[str, Any] + description: str + + +class TimeEvent(BaseModel): + """Model for timeevent data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + time: float + annotation: Annotation + + +class Code(StrEnum): + """Enumeration of code values.""" + + BLOCKED = 'blocked' + OTHER = 'other' + UNKNOWN = 'unknown' + + +class CandidateError(BaseModel): + """Model for candidateerror data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + index: float + code: Code + message: str | None = None + + +class DataPart(BaseModel): + """Model for datapart data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: Any | None = None + media: Any | None = None + tool_request: Any | None = Field(None, alias='toolRequest') + tool_response: Any | None = Field(None, alias='toolResponse') + data: Any | None = None + metadata: dict[str, Any] | None = None + + +class FinishReason(StrEnum): + """Enumeration of finishreason values.""" + + STOP = 'stop' + LENGTH = 'length' + BLOCKED = 'blocked' + INTERRUPTED = 'interrupted' + OTHER = 'other' + UNKNOWN = 'unknown' + + +class Content(BaseModel): + """Model for content data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: str + media: Any | None = None + + +class Media1(BaseModel): + """Model for media1 data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + content_type: str | None = Field(None, alias='contentType') + url: str + + +class Content1(BaseModel): + """Model for content1 data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: Any | None = None + media: Media1 + + +class Doc(BaseModel): + """Model for doc data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + content: list[Content | Content1] + metadata: dict[str, Any] | None = None + + +class ToolChoice(StrEnum): + """Enumeration of toolchoice values.""" + + AUTO = 'auto' + REQUIRED = 'required' + NONE = 'none' + + +class Output(BaseModel): + """Model for output data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + format: str | None = None + content_type: str | None = Field(None, alias='contentType') + instructions: bool | str | None = None + json_schema: Any | None = Field(None, alias='jsonSchema') + constrained: bool | None = None + + +class GenerationCommonConfig(BaseModel): + """Model for generationcommonconfig data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + version: str | None = None + temperature: float | None = None + max_output_tokens: float | None = Field(None, alias='maxOutputTokens') + top_k: float | None = Field(None, alias='topK') + top_p: float | None = Field(None, alias='topP') + stop_sequences: list[str] | None = Field(None, alias='stopSequences') + + +class GenerationUsage(BaseModel): + """Model for generationusage data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + input_tokens: float | None = Field(None, alias='inputTokens') + output_tokens: float | None = Field(None, alias='outputTokens') + total_tokens: float | None = Field(None, alias='totalTokens') + input_characters: float | None = Field(None, alias='inputCharacters') + output_characters: float | None = Field(None, alias='outputCharacters') + input_images: float | None = Field(None, alias='inputImages') + output_images: float | None = Field(None, alias='outputImages') + input_videos: float | None = Field(None, alias='inputVideos') + output_videos: float | None = Field(None, alias='outputVideos') + input_audio_files: float | None = Field(None, alias='inputAudioFiles') + output_audio_files: float | None = Field(None, alias='outputAudioFiles') + custom: dict[str, float] | None = None + + +class Constrained(StrEnum): + """Enumeration of constrained values.""" + + NONE = 'none' + ALL = 'all' + NO_TOOLS = 'no-tools' + + +class Supports(BaseModel): + """Model for supports data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + multiturn: bool | None = None + media: bool | None = None + tools: bool | None = None + system_role: bool | None = Field(None, alias='systemRole') + output: list[str] | None = None + content_type: list[str] | None = Field(None, alias='contentType') + context: bool | None = None + constrained: Constrained | None = None + tool_choice: bool | None = Field(None, alias='toolChoice') + + +class ModelInfo(BaseModel): + """Model for modelinfo data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + versions: list[str] | None = None + label: str | None = None + supports: Supports | None = None + + +class OutputFormat(StrEnum): + """Enumeration of outputformat values.""" + + JSON = 'json' + TEXT = 'text' + MEDIA = 'media' + + +class Role(StrEnum): + """Enumeration of role values.""" + + SYSTEM = 'system' + USER = 'user' + MODEL = 'model' + TOOL = 'tool' + + +class ToolDefinition(BaseModel): + """Model for tooldefinition data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + name: str + description: str + input_schema: dict[str, Any] = Field( + ..., + alias='inputSchema', + description='Valid JSON Schema representing the input of the tool.', + ) + output_schema: dict[str, Any] | None = Field( + None, + alias='outputSchema', + description='Valid JSON Schema describing the output of the tool.', + ) + metadata: dict[str, Any] | None = Field( + None, description='additional metadata for this tool definition' + ) + + +class ToolRequest1(BaseModel): + """Model for toolrequest1 data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + ref: str | None = None + name: str + input: Any | None = None + + +class ToolResponse1(BaseModel): + """Model for toolresponse1 data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + ref: str | None = None + name: str + output: Any | None = None + + +class Media(RootModel[Any]): + """Media data type class.""" + + root: Any + + +class Metadata(RootModel[dict[str, Any] | None]): + """Metadata data type class.""" + + root: dict[str, Any] | None = None + + +class Text(RootModel[Any]): + """Text data type class.""" + + root: Any + + +class ToolRequest(RootModel[Any]): + """ToolRequest data type class.""" + + root: Any + + +class ToolResponse(RootModel[Any]): + """ToolResponse data type class.""" + + root: Any + + +class Data(RootModel[Any]): + """Data data type class.""" + + root: Any + + +class Content2(BaseModel): + """Model for content2 data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: str + media: Any | None = None + + +class Media3(BaseModel): + """Model for media3 data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + content_type: str | None = Field(None, alias='contentType') + url: str + + +class Content3(BaseModel): + """Model for content3 data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: Any | None = None + media: Media3 + + +class Items(BaseModel): + """Model for items data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + content: list[Content2 | Content3] + metadata: dict[str, Any] | None = None + + +class Config(RootModel[Any]): + """Config data type class.""" + + root: Any + + +class Tools(RootModel[list[ToolDefinition]]): + """Tools data type class.""" + + root: list[ToolDefinition] + + +class Custom(RootModel[Any]): + """Custom data type class.""" + + root: Any + + +class FinishMessage(RootModel[str]): + """FinishMessage data type class.""" + + root: str + + +class LatencyMs(RootModel[float]): + """LatencyMs data type class.""" + + root: float + + +class Usage(RootModel[GenerationUsage]): + """Usage data type class.""" + + root: GenerationUsage + + +class Aggregated(RootModel[bool]): + """Aggregated data type class.""" + + root: bool + + +class Index(RootModel[float]): + """Index data type class.""" + + root: float + + +class Link(BaseModel): + """Model for link data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + context: SpanContext | None = None + attributes: dict[str, Any] | None = None + dropped_attributes_count: float | None = Field( + None, alias='droppedAttributesCount' + ) + + +class TimeEvents(BaseModel): + """Model for timeevents data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + time_event: list[TimeEvent] | None = Field(None, alias='timeEvent') + + +class SpanData(BaseModel): + """Model for spandata data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + span_id: str = Field(..., alias='spanId') + trace_id: str = Field(..., alias='traceId') + parent_span_id: str | None = Field(None, alias='parentSpanId') + start_time: float = Field(..., alias='startTime') + end_time: float = Field(..., alias='endTime') + attributes: dict[str, Any] + display_name: str = Field(..., alias='displayName') + links: list[Link] | None = None + instrumentation_library: InstrumentationLibrary = Field( + ..., alias='instrumentationLibrary' + ) + span_kind: str = Field(..., alias='spanKind') + same_process_as_parent_span: SameProcessAsParentSpan | None = Field( + None, alias='sameProcessAsParentSpan' + ) + status: SpanStatus | None = None + time_events: TimeEvents | None = Field(None, alias='timeEvents') + truncated: bool | None = None + + +class TraceData(BaseModel): + """Model for tracedata data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + trace_id: str = Field(..., alias='traceId') + display_name: str | None = Field(None, alias='displayName') + start_time: float | None = Field(None, alias='startTime') + end_time: float | None = Field(None, alias='endTime') + spans: dict[str, SpanData] + + +class EmptyPart(BaseModel): + """Model for emptypart data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: Text | None = None + media: Media | None = None + tool_request: ToolRequest | None = Field(None, alias='toolRequest') + tool_response: ToolResponse | None = Field(None, alias='toolResponse') + data: Any | None = None + metadata: Metadata | None = None + + +class MediaPart(BaseModel): + """Model for mediapart data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: Text | None = None + media: Media1 + tool_request: ToolRequest | None = Field(None, alias='toolRequest') + tool_response: ToolResponse | None = Field(None, alias='toolResponse') + data: Data | None = None + metadata: Metadata | None = None + + +class OutputConfig(BaseModel): + """Model for outputconfig data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + format: OutputFormat | None = None + schema_: dict[str, Any] | None = Field(None, alias='schema') + constrained: bool | None = None + content_type: str | None = Field(None, alias='contentType') + + +class TextPart(BaseModel): + """Model for textpart data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: str + media: Media | None = None + tool_request: ToolRequest | None = Field(None, alias='toolRequest') + tool_response: ToolResponse | None = Field(None, alias='toolResponse') + data: Data | None = None + metadata: Metadata | None = None + + +class ToolRequestPart(BaseModel): + """Model for toolrequestpart data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: Text | None = None + media: Media | None = None + tool_request: ToolRequest1 = Field(..., alias='toolRequest') + tool_response: ToolResponse | None = Field(None, alias='toolResponse') + data: Data | None = None + metadata: Metadata | None = None + + +class ToolResponsePart(BaseModel): + """Model for toolresponsepart data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + text: Text | None = None + media: Media | None = None + tool_request: ToolRequest | None = Field(None, alias='toolRequest') + tool_response: ToolResponse1 = Field(..., alias='toolResponse') + data: Data | None = None + metadata: Metadata | None = None + + +class OutputModel(RootModel[OutputConfig]): + """OutputModel data type class.""" + + root: OutputConfig + + +class Part( + RootModel[ + TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart + ] +): + """Part data type class.""" + + root: TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart + + +class ContentModel(RootModel[list[Part]]): + """ContentModel data type class.""" + + root: list[Part] + + +class DocumentData(BaseModel): + """Model for documentdata data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + content: list[Part] + metadata: dict[str, Any] | None = None + + +class GenerateResponseChunk(BaseModel): + """Model for generateresponsechunk data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + role: Role | None = None + index: float | None = None + content: list[Part] + custom: Any | None = None + aggregated: bool | None = None + + +class Message(BaseModel): + """Model for message data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + role: Role + content: list[Part] + metadata: dict[str, Any] | None = None + + +class ModelResponseChunk(BaseModel): + """Model for modelresponsechunk data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + role: Role | None = None + index: Index | None = None + content: ContentModel + custom: Custom | None = None + aggregated: Aggregated | None = None + + +class Messages(RootModel[list[Message]]): + """Messages data type class.""" + + root: list[Message] + + +class Candidate(BaseModel): + """Model for candidate data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + index: float + message: Message + usage: GenerationUsage | None = None + finish_reason: FinishReason = Field(..., alias='finishReason') + finish_message: str | None = Field(None, alias='finishMessage') + custom: Any | None = None + + +class GenerateActionOptions(BaseModel): + """Model for generateactionoptions data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + model: str + docs: list[Doc] | None = None + messages: list[Message] + tools: list[str] | None = None + tool_choice: ToolChoice | None = Field(None, alias='toolChoice') + config: Any | None = None + output: Output | None = None + return_tool_requests: bool | None = Field(None, alias='returnToolRequests') + max_turns: float | None = Field(None, alias='maxTurns') + + +class GenerateRequest(BaseModel): + """Model for generaterequest data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + messages: list[Message] + config: Any | None = None + tools: list[ToolDefinition] | None = None + tool_choice: ToolChoice | None = Field(None, alias='toolChoice') + output: OutputConfig | None = None + context: list[Items] | None = None + candidates: float | None = None + + +class GenerateResponse(BaseModel): + """Model for generateresponse data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + message: Message | None = None + finish_reason: FinishReason | None = Field(None, alias='finishReason') + finish_message: str | None = Field(None, alias='finishMessage') + latency_ms: float | None = Field(None, alias='latencyMs') + usage: GenerationUsage | None = None + custom: Any | None = None + request: GenerateRequest | None = None + candidates: list[Candidate] | None = None + + +class ModelRequest(BaseModel): + """Model for modelrequest data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + messages: Messages + config: Config | None = None + tools: Tools | None = None + tool_choice: ToolChoice | None = Field(None, alias='toolChoice') + output: OutputModel | None = None + context: list[Items] | None = None + + +class Request(RootModel[GenerateRequest]): + """Request data type class.""" + + root: GenerateRequest + + +class ModelResponse(BaseModel): + """Model for modelresponse data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + message: Message | None = None + finish_reason: FinishReason = Field(..., alias='finishReason') + finish_message: FinishMessage | None = Field(None, alias='finishMessage') + latency_ms: LatencyMs | None = Field(None, alias='latencyMs') + usage: Usage | None = None + custom: Custom | None = None + request: Request | None = None diff --git a/py/packages/genkit/src/genkit/core/web.py b/py/packages/genkit/src/genkit/core/web.py new file mode 100644 index 0000000000..dda81102f1 --- /dev/null +++ b/py/packages/genkit/src/genkit/core/web.py @@ -0,0 +1,20 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""HTTP header definitions and functionality for the Genkit framework.""" + +from enum import StrEnum + + +class HTTPHeader(StrEnum): + """HTTP header names used by the Genkit framework. + + Attributes: + CONTENT_LENGTH: Standard HTTP header for specifying the content length. + CONTENT_TYPE: Standard HTTP header for specifying the media type. + X_GENKIT_VERSION: Custom header for tracking genkit version. + """ + + CONTENT_LENGTH = 'Content-Length' + CONTENT_TYPE = 'Content-Type' + X_GENKIT_VERSION = 'X-Genkit-Version' diff --git a/py/packages/genkit/src/genkit/veneer/__init__.py b/py/packages/genkit/src/genkit/veneer/__init__.py index 5f99eb6d07..e3103a09fa 100644 --- a/py/packages/genkit/src/genkit/veneer/__init__.py +++ b/py/packages/genkit/src/genkit/veneer/__init__.py @@ -1,6 +1,15 @@ # Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 +"""Veneer package for managing server and client interactions. + +This package provides functionality for managing server-side operations, +including server configuration, runtime management, and client-server +communication protocols. +""" + +from genkit.veneer.veneer import Genkit, Plugin + __all__ = [ 'Genkit', 'Plugin', diff --git a/py/packages/genkit/src/genkit/veneer/server.py b/py/packages/genkit/src/genkit/veneer/server.py new file mode 100644 index 0000000000..aeab56ccb6 --- /dev/null +++ b/py/packages/genkit/src/genkit/veneer/server.py @@ -0,0 +1,79 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Functionality used by the Genkit veneer to start multiple servers. + +The following servers may be started depending upon the host environment: + +- Reflection API server. +- Flows server. + +The reflection API server is started only in dev mode, which is enabled by the +setting the environment variable `GENKIT_ENV` to `dev`. By default, the +reflection API server binds and listens on (localhost, 3100). The flows server +is the production servers that exposes flows and actions over HTTP. +""" + +import atexit +import json +import os +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + + +@dataclass +class ServerSpec: + """ServerSpec encapsulates the scheme, host and port information. + + This class defines the server binding and listening configuration. + """ + + port: int + scheme: str = 'http' + host: str = 'localhost' + + @property + def url(self) -> str: + """URL evaluates to the host base URL given the server specs.""" + return f'{self.scheme}://{self.host}:{self.port}' + + +def create_runtime( + runtime_dir: str, + reflection_server_spec: ServerSpec, + at_exit_fn: Callable[[Path], None] | None = None, +) -> Path: + """Create a runtime configuration for use with the genkit CLI. + + The runtime information is stored in the form of a timestamped JSON file. + Note that the file will be cleaned up as soon as the program terminates. + + Args: + runtime_dir: The directory to store the runtime file in. + reflection_server_spec: The server specification for the reflection + server. + at_exit_fn: A function to call when the runtime file is deleted. + + Returns: + A path object representing the created runtime metadata file. + """ + if not os.path.exists(runtime_dir): + os.makedirs(runtime_dir) + + current_datetime = datetime.now() + runtime_file_name = f'{current_datetime.isoformat()}.json' + runtime_file_path = Path(os.path.join(runtime_dir, runtime_file_name)) + metadata = json.dumps({ + 'reflectionApiSpecVersion': 1, + 'id': f'{os.getpid()}', + 'pid': os.getpid(), + 'reflectionServerUrl': reflection_server_spec.url, + 'timestamp': f'{current_datetime.isoformat()}', + }) + runtime_file_path.write_text(metadata, encoding='utf-8') + + if at_exit_fn: + atexit.register(lambda: at_exit_fn(runtime_file_path)) + return runtime_file_path diff --git a/py/packages/genkit/src/genkit/veneer/server_test.py b/py/packages/genkit/src/genkit/veneer/server_test.py new file mode 100644 index 0000000000..912630eaea --- /dev/null +++ b/py/packages/genkit/src/genkit/veneer/server_test.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the server module.""" + +import json +import os +import tempfile + +from genkit.veneer import server + + +def test_server_spec() -> None: + """Test the ServerSpec class. + + Verifies that the ServerSpec class correctly generates URLs and + handles different schemes, hosts, and ports. + """ + assert ( + server.ServerSpec(scheme='http', host='localhost', port=3100).url + == 'http://localhost:3100' + ) + + # Test with different schemes and hosts + assert ( + server.ServerSpec(scheme='https', host='example.com', port=8080).url + == 'https://example.com:8080' + ) + + # Test with default values + spec = server.ServerSpec(port=5000) + assert spec.scheme == 'http' + assert spec.host == 'localhost' + assert spec.url == 'http://localhost:5000' + + +def test_create_runtime() -> None: + """Test the create_runtime function. + + Verifies that the create_runtime function correctly creates and + manages runtime metadata files, including cleanup on exit. + """ + with tempfile.TemporaryDirectory() as temp_dir: + spec = server.ServerSpec(port=3100) + + # Test runtime file creation + runtime_path = server.create_runtime(temp_dir, spec) + assert runtime_path.exists() + + # Verify file content + content = json.loads(runtime_path.read_text(encoding='utf-8')) + assert isinstance(content, dict) + assert 'id' in content + assert 'pid' in content + assert content['reflectionServerUrl'] == 'http://localhost:3100' + assert 'timestamp' in content + + # Test directory creation + new_dir = os.path.join(temp_dir, 'new_dir') + runtime_path = server.create_runtime(new_dir, spec) + assert os.path.exists(new_dir) + assert runtime_path.exists() diff --git a/py/packages/genkit/src/genkit/veneer/veneer.py b/py/packages/genkit/src/genkit/veneer/veneer.py index b5c26d3115..1f2162beb0 100644 --- a/py/packages/genkit/src/genkit/veneer/veneer.py +++ b/py/packages/genkit/src/genkit/veneer/veneer.py @@ -1,115 +1,215 @@ -"""A layer that provides a flat library structure for a user. +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 -Copyright 2025 Google LLC -SPDX-License-Identifier: Apache-2.0 -""" +"""Veneer user-facing API for application developers who use the SDK.""" -import atexit -import datetime -import json +import logging import os import threading - +from collections.abc import Callable +from functools import wraps from http.server import HTTPServer -from typing import Union, List, Dict, Optional, Callable, Any +from typing import Any +from genkit.ai.embedding import EmbedRequest, EmbedResponse from genkit.ai.model import ModelFn -from genkit.ai.prompt import PromptFn +from genkit.core.action import ActionKind +from genkit.core.environment import is_dev_environment +from genkit.core.plugin_abc import Plugin from genkit.core.reflection import make_reflection_server from genkit.core.registry import Registry -from genkit.core.action import Action -from genkit.core.schemas import GenerateRequest, GenerateResponse, Message - -Plugin = Callable[['Genkit'], None] +from genkit.core.typing import ( + GenerateRequest, + GenerateResponse, + GenerationCommonConfig, + Message, +) +from genkit.veneer import server +DEFAULT_REFLECTION_SERVER_SPEC = server.ServerSpec( + scheme='http', host='127.0.0.1', port=3100 +) -class Genkit: - """An entrypoint for a user that encapsulate the SDK functionality.""" +logger = logging.getLogger(__name__) - MODEL = 'model' - FLOW = 'flow' - registry: Registry = Registry() +class Genkit: + """Veneer user-facing API for application developers who use the SDK.""" def __init__( self, - plugins: Optional[List[Plugin]] = None, - model: Optional[str] = None, + plugins: list[Plugin] | None = None, + model: str | None = None, + reflection_server_spec=DEFAULT_REFLECTION_SERVER_SPEC, ) -> None: - self.model = model - if os.getenv('GENKIT_ENV') == 'dev': - cwd = os.getcwd() - runtimes_dir = os.path.join(cwd, '.genkit/runtimes') - current_datetime = datetime.datetime.now() - if not os.path.exists(runtimes_dir): - os.makedirs(runtimes_dir) - runtime_file_path = os.path.join( - runtimes_dir, f'{current_datetime.isoformat()}.json' + """Initialize a new Genkit instance. + + Args: + plugins: Optional list of plugins to initialize. + model: Optional model name to use. + reflection_server_spec: Optional server spec for the reflection + server. + """ + self.registry = Registry() + self.registry.default_model = model + + if is_dev_environment(): + runtimes_dir = os.path.join(os.getcwd(), '.genkit/runtimes') + server.create_runtime( + runtime_dir=runtimes_dir, + reflection_server_spec=reflection_server_spec, + at_exit_fn=os.remove, ) - with open(runtime_file_path, 'w', encoding='utf-8') as rf: - rf.write( - json.dumps( - { - 'id': f'{os.getpid()}', - 'pid': os.getpid(), - 'reflectionServerUrl': 'http://localhost:3100', - 'timestamp': f'{current_datetime.isoformat()}', - } - ) - ) - - def delete_runtime_file() -> None: - os.remove(runtime_file_path) - - atexit.register(delete_runtime_file) - - self.thread = threading.Thread(target=self.start_server).start() + self.thread = threading.Thread( + target=self.start_server, + args=( + reflection_server_spec.host, + reflection_server_spec.port, + ), + ) + self.thread.start() - if plugins is not None: + if not plugins: + logger.warning('No plugins provided to Genkit') + else: for plugin in plugins: - plugin(self) + if isinstance(plugin, Plugin): + plugin.initialize(registry=self.registry) + else: + raise ValueError( + f'Invalid {plugin=} provided to Genkit: ' + f'must be of type `genkit.core.plugin_abc.Plugin`' + ) - def start_server(self) -> None: + def start_server(self, host: str, port: int) -> None: + """Start the HTTP server for handling requests. + + Args: + host: The hostname to bind to. + port: The port number to listen on. + """ httpd = HTTPServer( - ('127.0.0.1', 3100), make_reflection_server(self.registry) + (host, port), + make_reflection_server(registry=self.registry), ) httpd.serve_forever() - def generate( + async def generate( self, - model: Optional[str] = None, - prompt: Optional[Union[str]] = None, - messages: Optional[List[Message]] = None, - system: Optional[Union[str]] = None, - tools: Optional[List[str]] = None, + model: str | None = None, + prompt: str | None = None, + messages: list[Message] | None = None, + system: str | None = None, + tools: list[str] | None = None, + config: GenerationCommonConfig | None = None, ) -> GenerateResponse: - model = model if model is not None else self.model + """Generate text using a language model. + + Args: + model: Optional model name to use. + prompt: Optional raw prompt string. + messages: Optional list of messages for chat models. + system: Optional system message for chat models. + tools: Optional list of tools to use. + config: Optional generation configuration. + + Returns: + The generated text response. + """ + model = model or self.registry.default_model if model is None: raise Exception('No model configured.') + if config and not isinstance(config, GenerationCommonConfig): + raise AttributeError('Invalid generate config provided') - model_action = self.registry.lookup_action(self.MODEL, model) + model_action = self.registry.lookup_action(ActionKind.MODEL, model) + return ( + await model_action.arun( + GenerateRequest(messages=messages, config=config) + ) + ).response - return model_action.fn(GenerateRequest(messages=messages)).response + async def embed( + self, model: str | None = None, documents: list[str] | None = None + ) -> EmbedResponse: + """Calculates embeddings for the given texts. + + Args: + model: Optional embedder model name to use. + documents: Texts to embed. + + Returns: + The generated response with embeddings. + """ + embed_action = self.registry.lookup_action(ActionKind.EMBEDDER, model) + + return ( + await embed_action.arun(EmbedRequest(documents=documents)) + ).response + + def flow(self, name: str | None = None) -> Callable[[Callable], Callable]: + """Decorator to register a function as a flow. + + Args: + name: Optional name for the flow. If not provided, uses the + function name. + + Returns: + A decorator function that registers the flow. + """ - def flow( - self, name: Optional[str] = None - ) -> Callable[[Callable], Callable]: def wrapper(func: Callable) -> Callable: flow_name = name if name is not None else func.__name__ - action = Action( + action = self.registry.register_action( name=flow_name, - action_type=self.FLOW, + kind=ActionKind.FLOW, fn=func, span_metadata={'genkit:metadata:flow:name': flow_name}, ) - self.registry.register_action( - action_type=self.FLOW, name=flow_name, action=action + + @wraps(func) + async def async_wrapper(*args, **kwargs): + return (await action.arun(*args, **kwargs)).response + + @wraps(func) + def sync_wrapper(*args, **kwargs): + return action.run(*args, **kwargs).response + + return async_wrapper if action.is_async else sync_wrapper + + return wrapper + + def tool( + self, description: str, name: str | None = None + ) -> Callable[[Callable], Callable]: + """Decorator to register a function as a tool. + + Args: + description: Description for the tool to be passed to the model. + name: Optional name for the flow. If not provided, uses the function name. + + Returns: + A decorator function that registers the tool. + """ + + def wrapper(func: Callable) -> Callable: + tool_name = name if name is not None else func.__name__ + action = self.registry.register_action( + name=tool_name, + kind=ActionKind.TOOL, + description=description, + fn=func, ) - def decorator(*args: Any, **kwargs: Any) -> GenerateResponse: - return action.fn(*args, **kwargs).response + @wraps(func) + async def async_wrapper(*args, **kwargs): + return (await action.arun(*args, **kwargs)).response + + @wraps(func) + def sync_wrapper(*args, **kwargs): + return action.run(*args, **kwargs).response - return decorator + return async_wrapper if action.is_async else sync_wrapper return wrapper @@ -117,27 +217,18 @@ def define_model( self, name: str, fn: ModelFn, - metadata: Optional[Dict[str, Any]] = None, + metadata: dict[str, Any] | None = None, ) -> None: - action = Action( - name=name, action_type=self.MODEL, fn=fn, metadata=metadata + """Define a custom model action. + + Args: + name: Name of the model. + fn: Function implementing the model behavior. + metadata: Optional metadata for the model. + """ + self.registry.register_action( + name=name, + kind=ActionKind.MODEL, + fn=fn, + metadata=metadata, ) - self.registry.register_action(self.MODEL, name, action) - - def define_prompt( - self, - name: str, - fn: PromptFn, - model: Optional[str] = None, - ) -> Callable[[Optional[Any]], GenerateResponse]: - def prompt(input_prompt: Optional[Any] = None) -> GenerateResponse: - req = fn(input_prompt) - return self.generate(messages=req.messages, model=model) - - action = Action(self.MODEL, name, prompt) - self.registry.register_action(self.MODEL, name, action) - - def wrapper(input_prompt: Optional[Any] = None) -> GenerateResponse: - return action.fn(input_prompt) - - return wrapper diff --git a/py/packages/handlebarz/README.md b/py/packages/handlebarz/README.md deleted file mode 100644 index 78f9d64391..0000000000 --- a/py/packages/handlebarz/README.md +++ /dev/null @@ -1 +0,0 @@ -# Handlebarz diff --git a/py/packages/handlebarz/src/handlebarz/__init__.py b/py/packages/handlebarz/src/handlebarz/__init__.py deleted file mode 100644 index ee29008003..0000000000 --- a/py/packages/handlebarz/src/handlebarz/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - - -def hello() -> str: - return 'Hello from handlebarz!' diff --git a/py/packages/handlebarz/src/handlebarz/py.typed b/py/packages/handlebarz/src/handlebarz/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/py/plugins/chroma/src/genkit/plugins/chroma/__init__.py b/py/plugins/chroma/src/genkit/plugins/chroma/__init__.py index b309c98824..279cafc7c1 100644 --- a/py/plugins/chroma/src/genkit/plugins/chroma/__init__.py +++ b/py/plugins/chroma/src/genkit/plugins/chroma/__init__.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Chroma Plugin for Genkit. -""" +"""Chroma Plugin for Genkit.""" def package_name() -> str: + """Get the package name for the Chroma plugin. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.chroma' diff --git a/py/plugins/firebase/src/genkit/plugins/firebase/__init__.py b/py/plugins/firebase/src/genkit/plugins/firebase/__init__.py index 6fac4d9f8e..382c8c522d 100644 --- a/py/plugins/firebase/src/genkit/plugins/firebase/__init__.py +++ b/py/plugins/firebase/src/genkit/plugins/firebase/__init__.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Firebase Plugin for Genkit. -""" +"""Firebase Plugin for Genkit.""" def package_name() -> str: + """Get the package name for the Firebase plugin. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.firebase' diff --git a/py/plugins/google-ai/src/genkit/plugins/google_ai/__init__.py b/py/plugins/google-ai/src/genkit/plugins/google_ai/__init__.py index 8016d48b8a..d1dad47297 100644 --- a/py/plugins/google-ai/src/genkit/plugins/google_ai/__init__.py +++ b/py/plugins/google-ai/src/genkit/plugins/google_ai/__init__.py @@ -2,12 +2,18 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Google AI Plugin for Genkit +"""Google AI Plugin for Genkit. + +This plugin provides integration with Google AI services and models. """ def package_name() -> str: + """Get the package name for the Google AI plugin. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.google_ai' diff --git a/py/plugins/google-ai/src/genkit/plugins/google_ai/models/__init__.py b/py/plugins/google-ai/src/genkit/plugins/google_ai/models/__init__.py index 7ff179d41b..b7b4aee6c7 100644 --- a/py/plugins/google-ai/src/genkit/plugins/google_ai/models/__init__.py +++ b/py/plugins/google-ai/src/genkit/plugins/google_ai/models/__init__.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Google AI Models for Genkit. -""" +"""Google AI Models for Genkit.""" def package_name() -> str: + """Get the package name for the Google AI models subpackage. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.google_ai.models' diff --git a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/__init__.py b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/__init__.py index f2e88a2ed7..bab73b0515 100644 --- a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/__init__.py +++ b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/__init__.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Google Cloud Plugin for Genkit. -""" +"""Google Cloud Plugin for Genkit.""" def package_name() -> str: + """Get the package name for the Google Cloud plugin. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.google_cloud' diff --git a/py/plugins/ollama/README.md b/py/plugins/ollama/README.md index 6cfe2070d4..b305d1cbcc 100644 --- a/py/plugins/ollama/README.md +++ b/py/plugins/ollama/README.md @@ -1,3 +1,55 @@ # Ollama Plugin This Genkit plugin provides a set of tools and utilities for working with Ollama. + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Serving Ollama Locally + +### Ollama Service Installation + +#### MacOS (brew) + +```bash +brew install ollama +``` + +#### Debian/Ubuntu (apt installer) + +```bash +curl -fsSL https://ollama.com/install.sh | sh +``` + +Other installation options may be found [here](https://ollama.com/download) + +### Start serving of Ollama locally + +```bash +ollama serve +``` +Ollama is served at http://127.0.0.1:11434 by default + +### Installing Required Model + +Once ollama service is serving - pull the required model version: + +```bash +ollama pull : +``` + +### Check installed models + +Installed models can be reviewed with following command: + +```bash +ollama list +``` + +## Examples + +For examples check `./py/samples/ollama/README.md` diff --git a/py/plugins/ollama/pyproject.toml b/py/plugins/ollama/pyproject.toml index 8c20118cd1..2e2ae87be3 100644 --- a/py/plugins/ollama/pyproject.toml +++ b/py/plugins/ollama/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries", ] -dependencies = ["genkit"] +dependencies = ["genkit", "ollama~=0.4"] description = "Genkit Ollama Plugin" license = { text = "Apache-2.0" } name = "genkit-ollama-plugin" diff --git a/py/plugins/ollama/src/genkit/plugins/ollama/__init__.py b/py/plugins/ollama/src/genkit/plugins/ollama/__init__.py index c57811a680..75cd7d7fc2 100644 --- a/py/plugins/ollama/src/genkit/plugins/ollama/__init__.py +++ b/py/plugins/ollama/src/genkit/plugins/ollama/__init__.py @@ -2,13 +2,22 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Ollama Plugin for Genkit. -""" +"""Ollama Plugin for Genkit.""" + +from genkit.plugins.ollama.plugin_api import Ollama, ollama_name def package_name() -> str: + """Get the package name for the Ollama plugin. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.ollama' -__all__ = ['package_name'] +__all__ = [ + package_name.__name__, + Ollama.__name__, + ollama_name.__name__, +] diff --git a/py/plugins/ollama/src/genkit/plugins/ollama/constants.py b/py/plugins/ollama/src/genkit/plugins/ollama/constants.py new file mode 100644 index 0000000000..a305907335 --- /dev/null +++ b/py/plugins/ollama/src/genkit/plugins/ollama/constants.py @@ -0,0 +1,10 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +from enum import StrEnum + +DEFAULT_OLLAMA_SERVER_URL = 'http://127.0.0.1:11434' + + +class OllamaAPITypes(StrEnum): + CHAT = 'chat' + GENERATE = 'generate' diff --git a/py/plugins/ollama/src/genkit/plugins/ollama/mixins.py b/py/plugins/ollama/src/genkit/plugins/ollama/mixins.py new file mode 100644 index 0000000000..200e0a48dc --- /dev/null +++ b/py/plugins/ollama/src/genkit/plugins/ollama/mixins.py @@ -0,0 +1,59 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +import logging + +from genkit.core.action import ActionRunContext, noop_streaming_callback + +# Common helpers extracted into a base class or module +from genkit.core.typing import GenerateRequest, GenerationCommonConfig, TextPart + +import ollama as ollama_api + +LOG = logging.getLogger(__name__) + + +class BaseOllamaModelMixin: + @staticmethod + def build_request_options( + config: GenerationCommonConfig, + ) -> ollama_api.Options: + if config: + return ollama_api.Options( + top_k=config.top_k, + top_p=config.top_p, + stop=config.stop_sequences, + temperature=config.temperature, + num_predict=config.max_output_tokens, + ) + + @staticmethod + def build_prompt(request: GenerateRequest) -> str: + prompt = '' + for message in request.messages: + for text_part in message.content: + if isinstance(text_part.root, TextPart): + prompt += text_part.root.text + else: + LOG.error('Non-text messages are not supported') + return prompt + + @staticmethod + def build_chat_messages(request: GenerateRequest) -> list[dict[str, str]]: + messages = [] + for message in request.messages: + item = { + 'role': message.role.value, + 'content': '', + } + for text_part in message.content: + if isinstance(text_part.root, TextPart): + item['content'] += text_part.root.text + else: + LOG.error(f'Unsupported part of message: {text_part}') + messages.append(item) + return messages + + @staticmethod + def is_streaming_request(ctx: ActionRunContext | None) -> bool: + return ctx and ctx.is_streaming diff --git a/py/plugins/ollama/src/genkit/plugins/ollama/models.py b/py/plugins/ollama/src/genkit/plugins/ollama/models.py new file mode 100644 index 0000000000..7f45f1de54 --- /dev/null +++ b/py/plugins/ollama/src/genkit/plugins/ollama/models.py @@ -0,0 +1,186 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +import logging + +from genkit.core.action import ActionRunContext +from genkit.core.typing import ( + GenerateRequest, + GenerateResponse, + Message, + Role, + TextPart, +) +from genkit.plugins.ollama.constants import ( + DEFAULT_OLLAMA_SERVER_URL, + OllamaAPITypes, +) +from genkit.plugins.ollama.mixins import BaseOllamaModelMixin +from pydantic import BaseModel, Field, HttpUrl + +import ollama as ollama_api + +LOG = logging.getLogger(__name__) + + +class ModelDefinition(BaseModel): + name: str + api_type: OllamaAPITypes + + +class EmbeddingModelDefinition(BaseModel): + name: str + dimensions: int + + +class OllamaPluginParams(BaseModel): + models: list[ModelDefinition] = Field(default_factory=list) + embedders: list[EmbeddingModelDefinition] = Field(default_factory=list) + server_address: HttpUrl = Field(default=HttpUrl(DEFAULT_OLLAMA_SERVER_URL)) + request_headers: dict[str, str] | None = None + use_async_api: bool = Field(default=True) + + +class OllamaModel(BaseOllamaModelMixin): + def __init__( + self, client: ollama_api.Client, model_definition: ModelDefinition + ): + self.client = client + self.model_definition = model_definition + + def generate( + self, request: GenerateRequest, ctx: ActionRunContext | None + ) -> GenerateResponse: + txt_response = 'Failed to get response from Ollama API' + + if self.model_definition.api_type == OllamaAPITypes.CHAT: + api_response = self._chat_with_ollama(request=request, ctx=ctx) + if api_response: + txt_response = api_response.message.content + else: + api_response = self._generate_ollama_response( + request=request, ctx=ctx + ) + if api_response: + txt_response = api_response.response + + return GenerateResponse( + message=Message( + role=Role.MODEL, + content=[TextPart(text=txt_response)], + ) + ) + + def _chat_with_ollama( + self, request: GenerateRequest, ctx: ActionRunContext | None = None + ) -> ollama_api.ChatResponse | None: + messages = self.build_chat_messages(request) + streaming_request = self.is_streaming_request(ctx=ctx) + + chat_response = self.client.chat( + model=self.model_definition.name, + messages=messages, + options=self.build_request_options(config=request.config), + stream=streaming_request, + ) + + if streaming_request: + for chunk in chat_response: + ctx.send_chunk(chunk=chunk) + else: + return chat_response + + def _generate_ollama_response( + self, request: GenerateRequest, ctx: ActionRunContext | None = None + ) -> ollama_api.GenerateResponse | None: + prompt = self.build_prompt(request) + streaming_request = self.is_streaming_request(ctx=ctx) + + request_kwargs = { + 'model': self.model_definition.name, + 'prompt': prompt, + 'options': self.build_request_options(config=request.config), + 'stream': streaming_request, + } + + generate_response = self.client.generate(**request_kwargs) + + if streaming_request: + for chunk in generate_response: + ctx.send_chunk(chunk=chunk) + else: + return generate_response + + +class AsyncOllamaModel(BaseOllamaModelMixin): + def __init__( + self, client: ollama_api.AsyncClient, model_definition: ModelDefinition + ): + self.client = client + self.model_definition = model_definition + + async def generate( + self, request: GenerateRequest, ctx: ActionRunContext | None = None + ) -> GenerateResponse: + txt_response = 'Failed to get response from Ollama API' + + if self.model_definition.api_type == OllamaAPITypes.CHAT: + api_response = await self._chat_with_ollama( + request=request, ctx=ctx + ) + if api_response: + txt_response = api_response.message.content + elif self.model_definition.api_type == OllamaAPITypes.GENERATE: + api_response = await self._generate_ollama_response( + request=request, ctx=ctx + ) + if api_response: + txt_response = api_response.response + else: + LOG.error(f'Unresolved API type: {self.model_definition.api_type}') + + return GenerateResponse( + message=Message( + role=Role.MODEL, + content=[TextPart(text=txt_response)], + ) + ) + + async def _chat_with_ollama( + self, request: GenerateRequest, ctx: ActionRunContext | None = None + ) -> ollama_api.ChatResponse | None: + messages = self.build_chat_messages(request) + streaming_request = self.is_streaming_request(ctx=ctx) + + chat_response = await self.client.chat( + model=self.model_definition.name, + messages=messages, + options=self.build_request_options(config=request.config), + stream=streaming_request, + ) + + if streaming_request: + async for chunk in chat_response: + ctx.send_chunk(chunk=chunk) + else: + return chat_response + + async def _generate_ollama_response( + self, request: GenerateRequest, ctx: ActionRunContext | None = None + ) -> ollama_api.GenerateResponse | None: + prompt = self.build_prompt(request) + streaming_request = self.is_streaming_request(ctx=ctx) + + request_kwargs = { + 'model': self.model_definition.name, + 'prompt': prompt, + 'options': self.build_request_options(config=request.config), + 'stream': streaming_request, + } + + generate_response = await self.client.generate(**request_kwargs) + + if streaming_request: + async for chunk in generate_response: + ctx.send_chunk(chunk=chunk) + else: + return generate_response diff --git a/py/plugins/ollama/src/genkit/plugins/ollama/models/__init__.py b/py/plugins/ollama/src/genkit/plugins/ollama/models/__init__.py deleted file mode 100644 index fb5547c098..0000000000 --- a/py/plugins/ollama/src/genkit/plugins/ollama/models/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - - -""" -Ollama Models for Genkit. -""" - - -def package_name() -> str: - return 'genkit.plugins.ollama.models' - - -__all__ = ['package_name'] diff --git a/py/plugins/ollama/src/genkit/plugins/ollama/plugin_api.py b/py/plugins/ollama/src/genkit/plugins/ollama/plugin_api.py new file mode 100644 index 0000000000..84596ada40 --- /dev/null +++ b/py/plugins/ollama/src/genkit/plugins/ollama/plugin_api.py @@ -0,0 +1,77 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +""" +Ollama Plugin for Genkit. +""" + +import logging +from functools import cached_property +from typing import Type + +from genkit.core.action import ActionKind +from genkit.core.plugin_abc import Plugin +from genkit.core.registry import Registry +from genkit.plugins.ollama.models import ( + AsyncOllamaModel, + OllamaAPITypes, + OllamaModel, + OllamaPluginParams, +) + +import ollama as ollama_api + +LOG = logging.getLogger(__name__) + + +def ollama_name(name: str) -> str: + return f'ollama/{name}' + + +class Ollama(Plugin): + def __init__(self, plugin_params: OllamaPluginParams): + self.plugin_params = plugin_params + self._sync_client = ollama_api.Client( + host=self.plugin_params.server_address.unicode_string() + ) + self._async_client = ollama_api.AsyncClient( + host=self.plugin_params.server_address.unicode_string() + ) + + @cached_property + def client(self) -> ollama_api.AsyncClient | ollama_api.Client: + client_cls = ( + ollama_api.AsyncClient + if self.plugin_params.use_async_api + else ollama_api.Client + ) + return client_cls( + host=self.plugin_params.server_address.unicode_string(), + ) + + @cached_property + def ollama_model_class(self) -> Type[AsyncOllamaModel | OllamaModel]: + return ( + AsyncOllamaModel + if self.plugin_params.use_async_api + else OllamaModel + ) + + def initialize(self, registry: Registry) -> None: + for model_definition in self.plugin_params.models: + model = self.ollama_model_class( + client=self.client, + model_definition=model_definition, + ) + registry.register_action( + kind=ActionKind.MODEL, + name=ollama_name(model_definition.name), + fn=model.generate, + metadata={ + 'multiturn': model_definition.api_type + == OllamaAPITypes.CHAT, + 'system_role': True, + }, + ) + # TODO: introduce embedders here + # for embedder in self.plugin_params.embedders: diff --git a/py/__init__.py b/py/plugins/ollama/tests/__init__.py similarity index 98% rename from py/__init__.py rename to py/plugins/ollama/tests/__init__.py index 12710abd8c..7229ac50e6 100644 --- a/py/__init__.py +++ b/py/plugins/ollama/tests/__init__.py @@ -1,3 +1,2 @@ # Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 - diff --git a/py/plugins/ollama/tests/conftest.py b/py/plugins/ollama/tests/conftest.py new file mode 100644 index 0000000000..5ab240cfc9 --- /dev/null +++ b/py/plugins/ollama/tests/conftest.py @@ -0,0 +1,87 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +from unittest import mock + +import pytest +from genkit.plugins.ollama import Ollama +from genkit.plugins.ollama.models import ( + ModelDefinition, + OllamaAPITypes, + OllamaPluginParams, +) +from genkit.plugins.ollama.plugin_api import ollama_api +from genkit.veneer import Genkit + + +@pytest.fixture +def ollama_model() -> str: + return 'ollama/gemma2:latest' + + +@pytest.fixture +def chat_model_plugin_params(ollama_model: str) -> OllamaPluginParams: + return OllamaPluginParams( + models=[ + ModelDefinition( + name=ollama_model.split('/')[-1], + api_type=OllamaAPITypes.CHAT, + ) + ], + ) + + +@pytest.fixture +def genkit_veneer_chat_model( + ollama_model: str, + chat_model_plugin_params: OllamaPluginParams, +) -> Genkit: + return Genkit( + plugins=[ + Ollama( + plugin_params=chat_model_plugin_params, + ) + ], + model=ollama_model, + ) + + +@pytest.fixture +def generate_model_plugin_params(ollama_model: str) -> OllamaPluginParams: + return OllamaPluginParams( + models=[ + ModelDefinition( + name=ollama_model.split('/')[-1], + api_type=OllamaAPITypes.GENERATE, + ) + ], + ) + + +@pytest.fixture +def genkit_veneer_generate_model( + ollama_model: str, + generate_model_plugin_params: OllamaPluginParams, +) -> Genkit: + return Genkit( + plugins=[ + Ollama( + plugin_params=generate_model_plugin_params, + ) + ], + model=ollama_model, + ) + + +@pytest.fixture +def mock_ollama_api_client(): + with mock.patch.object(ollama_api, 'Client') as mock_ollama_client: + yield mock_ollama_client + + +@pytest.fixture +def mock_ollama_api_async_client(): + with mock.patch.object( + ollama_api, 'AsyncClient' + ) as mock_ollama_async_client: + yield mock_ollama_async_client diff --git a/py/plugins/ollama/tests/test_plugin_api.py b/py/plugins/ollama/tests/test_plugin_api.py new file mode 100644 index 0000000000..783f85219e --- /dev/null +++ b/py/plugins/ollama/tests/test_plugin_api.py @@ -0,0 +1,97 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +from unittest import mock + +import ollama as ollama_api +import pytest +from genkit.core.typing import GenerateResponse, Message, Role, TextPart +from genkit.veneer import Genkit + + +def test_adding_ollama_chat_model_to_genkit_veneer( + ollama_model: str, + genkit_veneer_chat_model: Genkit, +): + assert len(genkit_veneer_chat_model.registry.entries) == 1 + assert ollama_model in genkit_veneer_chat_model.registry.entries['model'] + + +def test_adding_ollama_generation_model_to_genkit_veneer( + ollama_model: str, + genkit_veneer_generate_model: Genkit, +): + assert len(genkit_veneer_generate_model.registry.entries) == 1 + assert ( + ollama_model in genkit_veneer_generate_model.registry.entries['model'] + ) + + +@pytest.mark.asyncio +async def test_async_get_chat_model_response_from_llama_api_flow( + mock_ollama_api_async_client: mock.Mock, genkit_veneer_chat_model: Genkit +): + mock_response_message = 'Mocked response message' + + async def fake_chat_response(*args, **kwargs): + return ollama_api.ChatResponse( + message=ollama_api.Message( + content=mock_response_message, + role=Role.USER, + ) + ) + + mock_ollama_api_async_client.return_value.chat.side_effect = ( + fake_chat_response + ) + + async def _test_fun(): + return await genkit_veneer_chat_model.generate( + messages=[ + Message( + role=Role.USER, + content=[ + TextPart(text='Test message'), + ], + ) + ] + ) + + response = await genkit_veneer_chat_model.flow()(_test_fun)() + + assert isinstance(response, GenerateResponse) + assert response.message.content[0].root.text == mock_response_message + + +@pytest.mark.asyncio +async def test_async_get_generate_model_response_from_llama_api_flow( + mock_ollama_api_async_client: mock.Mock, + genkit_veneer_generate_model: Genkit, +): + mock_response_message = 'Mocked response message' + + async def fake_generate_response(*args, **kwargs): + return ollama_api.GenerateResponse( + response=mock_response_message, + ) + + mock_ollama_api_async_client.return_value.generate.side_effect = ( + fake_generate_response + ) + + async def _test_fun(): + return await genkit_veneer_generate_model.generate( + messages=[ + Message( + role=Role.USER, + content=[ + TextPart(text='Test message'), + ], + ) + ] + ) + + response = await genkit_veneer_generate_model.flow()(_test_fun)() + + assert isinstance(response, GenerateResponse) + assert response.message.content[0].root.text == mock_response_message diff --git a/py/plugins/pinecone/src/genkit/plugins/pinecone/__init__.py b/py/plugins/pinecone/src/genkit/plugins/pinecone/__init__.py index a9589f9800..c5dee1d338 100644 --- a/py/plugins/pinecone/src/genkit/plugins/pinecone/__init__.py +++ b/py/plugins/pinecone/src/genkit/plugins/pinecone/__init__.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Pinecone Plugin for Genkit. -""" +"""Pinecone Plugin for Genkit.""" def package_name() -> str: + """Get the package name for the Pinecone plugin. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.pinecone' diff --git a/py/plugins/vertex-ai/pyproject.toml b/py/plugins/vertex-ai/pyproject.toml index ed1fd934f4..cd59824976 100644 --- a/py/plugins/vertex-ai/pyproject.toml +++ b/py/plugins/vertex-ai/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries", ] -dependencies = ["genkit", "google-cloud-aiplatform>=1.77.0"] +dependencies = ["genkit", "google-cloud-aiplatform>=1.77.0", "pytest-mock"] description = "Genkit Google Cloud Vertex AI Plugin" license = { text = "Apache-2.0" } name = "genkit-vertex-ai-plugin" diff --git a/py/plugins/vertex-ai/src/__init__.py b/py/plugins/vertex-ai/src/__init__.py index 12710abd8c..fc9a4879b1 100644 --- a/py/plugins/vertex-ai/src/__init__.py +++ b/py/plugins/vertex-ai/src/__init__.py @@ -1,3 +1,4 @@ # Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 +"""Vertex AI plugin.""" diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/__init__.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/__init__.py index 5f3ba44a80..637fc152dd 100644 --- a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/__init__.py +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/__init__.py @@ -1,67 +1,31 @@ # Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 +"""Vertex AI plugin for Genkit. - -""" -Google Cloud Vertex AI Plugin for Genkit. +This plugin provides integration with Google Cloud's Vertex AI platform, +enabling the use of Vertex AI models and services within the Genkit framework. """ -import vertexai - -from typing import Callable, Optional -from vertexai.generative_models import GenerativeModel, Content, Part - -from genkit.core.schemas import ( - GenerateRequest, - GenerateResponse, - Message, - TextPart, -) -from genkit.veneer.veneer import Genkit +from genkit.plugins.vertex_ai.embedding import EmbeddingModels +from genkit.plugins.vertex_ai.gemini import GeminiVersion +from genkit.plugins.vertex_ai.imagen import ImagenVersion +from genkit.plugins.vertex_ai.plugin_api import VertexAI, vertexai_name def package_name() -> str: - return 'genkit.plugins.vertex_ai' - + """Get the package name for the Vertex AI plugin. -def vertexAI(project_id: Optional[str] = None) -> Callable[[Genkit], None]: - def plugin(ai: Genkit) -> None: - vertexai.init(location='us-central1', project=project_id) - - def gemini(request: GenerateRequest) -> GenerateResponse: - geminiMsgs: list[Content] = [] - for m in request.messages: - geminiParts: list[Part] = [] - for p in m.content: - if p.root.text is not None: - geminiParts.append(Part.from_text(p.root.text)) - else: - raise Exception('unsupported part type') - geminiMsgs.append(Content(role=m.role.value, parts=geminiParts)) - model = GenerativeModel('gemini-1.5-flash-002') - response = model.generate_content(contents=geminiMsgs) - return GenerateResponse( - message=Message( - role='model', content=[TextPart(text=response.text)] - ) - ) - - ai.define_model( - name='vertexai/gemini-1.5-flash', - fn=gemini, - metadata={ - 'model': { - 'label': 'banana', - 'supports': {'multiturn': True}, - } - }, - ) - - return plugin - - -def gemini(name: str) -> str: - return f'vertexai/{name}' + Returns: + The fully qualified package name as a string. + """ + return 'genkit.plugins.vertex_ai' -__all__ = ['package_name', 'vertexAI', 'gemini'] +__all__ = [ + package_name.__name__, + VertexAI.__name__, + vertexai_name.__name__, + EmbeddingModels.__name__, + GeminiVersion.__name__, + ImagenVersion.__name__, +] diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/constants.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/constants.py new file mode 100644 index 0000000000..4ed4978134 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/constants.py @@ -0,0 +1,11 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Constants used by the Vertex AI plugin. + +This module defines constants used throughout the Vertex AI plugin, +including environment variable names and configuration values. +""" + +GCLOUD_PROJECT = 'GCLOUD_PROJECT' +DEFAULT_REGION = 'us-central1' diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/embedding.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/embedding.py new file mode 100644 index 0000000000..92fb6b6f78 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/embedding.py @@ -0,0 +1,109 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Vertex AI embedding plugin.""" + +from enum import StrEnum +from typing import Any + +from genkit.ai.embedding import EmbedRequest, EmbedResponse +from vertexai.language_models import TextEmbeddingInput, TextEmbeddingModel + + +class EmbeddingModels(StrEnum): + """Embedding models supported by Vertex AI. + + Attributes: + GECKO_003_ENG: Gecko 003 English model. + TEXT_EMBEDDING_004_ENG: Text embedding 004 English model. + TEXT_EMBEDDING_005_ENG: Text embedding 005 English model. + GECKO_MULTILINGUAL: Gecko multilingual model. + TEXT_EMBEDDING_002_MULTILINGUAL: Text embedding 002 multilingual model. + """ + + GECKO_003_ENG = 'textembedding-gecko@003' + TEXT_EMBEDDING_004_ENG = 'text-embedding-004' + TEXT_EMBEDDING_005_ENG = 'text-embedding-005' + GECKO_MULTILINGUAL = 'textembedding-gecko-multilingual@001' + TEXT_EMBEDDING_002_MULTILINGUAL = 'text-multilingual-embedding-002' + + +class TaskType(StrEnum): + """Task types supported by Vertex AI. + + Attributes: + SEMANTIC_SIMILARITY: Semantic similarity task. + CLASSIFICATION: Classification task. + CLUSTERING: Clustering task. + RETRIEVAL_DOCUMENT: Retrieval document task. + RETRIEVAL_QUERY: Retrieval query task. + QUESTION_ANSWERING: Question answering task. + FACT_VERIFICATION: Fact verification task. + CODE_RETRIEVAL_QUERY: Code retrieval query task. + """ + + SEMANTIC_SIMILARITY = 'SEMANTIC_SIMILARITY' + CLASSIFICATION = 'CLASSIFICATION' + CLUSTERING = 'CLUSTERING' + RETRIEVAL_DOCUMENT = 'RETRIEVAL_DOCUMENT' + RETRIEVAL_QUERY = 'RETRIEVAL_QUERY' + + +class Embedder: + """Embedder for Vertex AI. + + Attributes: + version: The version of the embedding model to use. + task: The task type to use for the embedding. + dimensionality: The dimensionality of the embedding. + """ + + TASK = TaskType.RETRIEVAL_QUERY + + # By default, the model generates embeddings with 768 dimensions. + # Models such as `text-embedding-004`, `text-embedding-005`, + # and `text-multilingual-embedding-002`allow the output dimensionality + # to be adjusted between 1 and 768. + DIMENSIONALITY = 768 + + def __init__(self, version: EmbeddingModels): + """Initialize the embedder. + + Args: + version: The version of the embedding model to use. + """ + self._version = version + + @property + def embedding_model(self) -> TextEmbeddingModel: + """Get the embedding model. + + Returns: + The embedding model. + """ + return TextEmbeddingModel.from_pretrained(self._version) + + def handle_request(self, request: EmbedRequest) -> EmbedResponse: + """Handle an embedding request. + + Args: + request: The embedding request to handle. + + Returns: + The embedding response. + """ + inputs = [ + TextEmbeddingInput(text, self.TASK) for text in request.documents + ] + vertexai_embeddings = self.embedding_model.get_embeddings(inputs) + embeddings = [embedding.values for embedding in vertexai_embeddings] + return EmbedResponse(embeddings=embeddings) + + @property + def model_metadata(self) -> dict[str, dict[str, Any]]: + """Get the model metadata. + + Returns: + The model metadata. + """ + return {} diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/gemini.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/gemini.py new file mode 100644 index 0000000000..56ff1387e5 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/gemini.py @@ -0,0 +1,137 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Gemini model integration for Vertex AI plugin. + +This module provides classes and utilities for working with Google's +Gemini models through the Vertex AI platform. It includes version +definitions and a client class for making requests to Gemini models. +""" + +from enum import StrEnum +from typing import Any + +from genkit.core.typing import ( + GenerateRequest, + GenerateResponse, + Message, + ModelInfo, + Role, + Supports, + TextPart, +) +from vertexai.generative_models import Content, GenerativeModel, Part + + +class GeminiVersion(StrEnum): + """Available versions of the Gemini model. + + This enum defines the available versions of the Gemini model that + can be used through Vertex AI. + """ + + GEMINI_1_5_PRO = 'gemini-1.5-pro' + GEMINI_1_5_FLASH = 'gemini-1.5-flash' + GEMINI_2_0_FLASH_001 = 'gemini-2.0-flash-001' + GEMINI_2_0_FLASH_LITE_PREVIEW = 'gemini-2.0-flash-lite-preview-02-05' + GEMINI_2_0_PRO_EXP = 'gemini-2.0-pro-exp-02-05' + + +SUPPORTED_MODELS = { + GeminiVersion.GEMINI_1_5_PRO: ModelInfo( + versions=[], + label='Vertex AI - Gemini 1.5 Pro', + supports=Supports( + multiturn=True, media=True, tools=True, systemRole=True + ), + ), + GeminiVersion.GEMINI_1_5_FLASH: ModelInfo( + versions=[], + label='Vertex AI - Gemini 1.5 Flash', + supports=Supports( + multiturn=True, media=True, tools=True, systemRole=True + ), + ), + GeminiVersion.GEMINI_2_0_FLASH_001: ModelInfo( + versions=[], + label='Vertex AI - Gemini 2.0 Flash 001', + supports=Supports( + multiturn=True, media=True, tools=True, systemRole=True + ), + ), + GeminiVersion.GEMINI_2_0_FLASH_LITE_PREVIEW: ModelInfo( + versions=[], + label='Vertex AI - Gemini 2.0 Flash Lite Preview 02-05', + supports=Supports( + multiturn=True, media=True, tools=True, systemRole=True + ), + ), + GeminiVersion.GEMINI_2_0_PRO_EXP: ModelInfo( + versions=[], + label='Vertex AI - Gemini 2.0 Flash Pro Experimental 02-05', + supports=Supports( + multiturn=True, media=True, tools=True, systemRole=True + ), + ), +} + + +class Gemini: + """Client for interacting with Gemini models via Vertex AI. + + This class provides methods for making requests to Gemini models, + handling message formatting and response processing. + """ + + def __init__(self, version: str): + """Initialize a Gemini client. + + Args: + version: The version of the Gemini model to use, should be + one of the values from GeminiVersion. + """ + self._version = version + + @property + def gemini_model(self) -> GenerativeModel: + """Get the Vertex AI GenerativeModel instance. + + Returns: + A configured GenerativeModel instance for the specified version. + """ + return GenerativeModel(self._version) + + def handle_request(self, request: GenerateRequest) -> GenerateResponse: + """Handle a generation request using the Gemini model. + + Args: + request: The generation request containing messages and parameters. + + Returns: + The model's response to the generation request. + """ + messages: list[Content] = [] + for m in request.messages: + parts: list[Part] = [] + for p in m.content: + if p.root.text is not None: + parts.append(Part.from_text(p.root.text)) + else: + raise Exception('unsupported part type') + messages.append(Content(role=m.role.value, parts=parts)) + response = self.gemini_model.generate_content(contents=messages) + return GenerateResponse( + message=Message( + role=Role.MODEL, + content=[TextPart(text=response.text)], + ) + ) + + @property + def model_metadata(self) -> dict[str, dict[str, Any]]: + supports = SUPPORTED_MODELS[self._version].supports.model_dump() + return { + 'model': { + 'supports': supports, + } + } diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/imagen.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/imagen.py new file mode 100644 index 0000000000..21341bfcf4 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/imagen.py @@ -0,0 +1,112 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +from enum import StrEnum +from typing import Any + +from genkit.core.typing import ( + GenerateRequest, + GenerateResponse, + Media1, + MediaPart, + Message, + ModelInfo, + Role, + Supports, +) +from vertexai.preview.vision_models import ImageGenerationModel + + +class ImagenVersion(StrEnum): + IMAGEN3 = 'imagen-3.0-generate-002' + IMAGEN3_FAST = 'imagen-3.0-fast-generate-001' + IMAGEN2 = 'imagegeneration@006' + + +SUPPORTED_MODELS = { + ImagenVersion.IMAGEN3: ModelInfo( + label='Vertex AI - Imagen3', + supports=Supports( + media=True, + multiturn=False, + tools=False, + systemRole=False, + output=['media'], + ), + ), + ImagenVersion.IMAGEN3_FAST: ModelInfo( + label='Vertex AI - Imagen3 Fast', + supports=Supports( + media=False, + multiturn=False, + tools=False, + systemRole=False, + output=['media'], + ), + ), + ImagenVersion.IMAGEN2: ModelInfo( + label='Vertex AI - Imagen2', + supports=Supports( + media=False, + multiturn=False, + tools=False, + systemRole=False, + output=['media'], + ), + ), +} + + +class Imagen: + """Imagen - text to image model.""" + + def __init__(self, version): + self._version = version + + @property + def model(self) -> ImageGenerationModel: + return ImageGenerationModel.from_pretrained(self._version) + + def handle_request(self, request: GenerateRequest) -> GenerateResponse: + parts: list[str] = [] + for m in request.messages: + for p in m.content: + if p.root.text is not None: + parts.append(p.root.text) + else: + raise Exception('unsupported part type') + + prompt = ' '.join(parts) + images = self.model.generate_images( + prompt=prompt, + number_of_images=1, + language='en', + aspect_ratio='1:1', + safety_filter_level='block_some', + person_generation='allow_adult', + ) + + media_content = [ + MediaPart( + media=Media1( + contentType=image._mime_type, url=image._as_base64_string() + ) + ) + for image in images + ] + + return GenerateResponse( + message=Message( + role=Role.MODEL, + content=media_content, + ) + ) + + @property + def model_metadata(self) -> dict[str, Any]: + supports = SUPPORTED_MODELS[self._version].supports.model_dump() + return { + 'model': { + 'supports': supports, + } + } diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/models/__init__.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/models/__init__.py index 39639eca6f..a43b603227 100644 --- a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/models/__init__.py +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/models/__init__.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 -""" -Google Cloud Vertex AI Models for Genkit. -""" +"""Google Cloud Vertex AI Models for Genkit.""" def package_name() -> str: + """Get the package name for the Vertex AI models subpackage. + + Returns: + The fully qualified package name as a string. + """ return 'genkit.plugins.vertex_ai.models' diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/plugin_api.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/plugin_api.py new file mode 100644 index 0000000000..fe321dec93 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/plugin_api.py @@ -0,0 +1,97 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Google Cloud Vertex AI Plugin for Genkit.""" + +import logging +import os + +import vertexai +from genkit.core.action import ActionKind +from genkit.core.plugin_abc import Plugin +from genkit.core.registry import Registry +from genkit.plugins.vertex_ai import constants as const +from genkit.plugins.vertex_ai.embedding import Embedder, EmbeddingModels +from genkit.plugins.vertex_ai.gemini import Gemini, GeminiVersion +from genkit.plugins.vertex_ai.imagen import Imagen, ImagenVersion + +LOG = logging.getLogger(__name__) + + +def vertexai_name(name: str) -> str: + """Create a Vertex AI action name. + + Args: + name: Base name for the action. + + Returns: + The fully qualified Vertex AI action name. + """ + return f'vertexai/{name}' + + +class VertexAI(Plugin): + """Vertex AI plugin for Genkit. + + This plugin provides integration with Google Cloud's Vertex AI platform, + enabling the use of Vertex AI models and services within the Genkit + framework. It handles initialization of the Vertex AI client and + registration of model actions. + """ + + def __init__( + self, project_id: str | None = None, location: str | None = None + ): + """Initialize the Vertex AI plugin. + + Args: + project_id: Optional Google Cloud project ID. If not provided, + will attempt to detect from environment. + location: Optional Google Cloud region. If not provided, will + use a default region. + """ + # If not set, projectId will be read by plugin + project_id = ( + project_id if project_id else os.getenv(const.GCLOUD_PROJECT) + ) + location = location if location else const.DEFAULT_REGION + vertexai.init(project=project_id, location=location) + + def initialize(self, registry: Registry) -> None: + """Initialize the plugin by registering actions with the registry. + + This method registers the Vertex AI model actions with the provided + registry, making them available for use in the Genkit framework. + + Args: + registry: The registry to register actions with. + + Returns: + None + """ + for model_version in GeminiVersion: + gemini = Gemini(model_version) + registry.register_action( + kind=ActionKind.MODEL, + name=vertexai_name(model_version), + fn=gemini.handle_request, + metadata=gemini.model_metadata, + ) + + for embed_model in EmbeddingModels: + embedder = Embedder(embed_model) + registry.register_action( + kind=ActionKind.EMBEDDER, + name=vertexai_name(embed_model), + fn=embedder.handle_request, + metadata=embedder.model_metadata, + ) + + for imagen_version in ImagenVersion: + imagen = Imagen(imagen_version) + registry.register_action( + kind=ActionKind.MODEL, + name=vertexai_name(imagen_version), + fn=imagen.handle_request, + metadata=imagen.model_metadata, + ) diff --git a/py/plugins/vertex-ai/tests/test_gemini.py b/py/plugins/vertex-ai/tests/test_gemini.py new file mode 100644 index 0000000000..74a97b5ba2 --- /dev/null +++ b/py/plugins/vertex-ai/tests/test_gemini.py @@ -0,0 +1,52 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Test Gemini models.""" + +import pytest +from genkit.core.typing import ( + GenerateRequest, + GenerateResponse, + Message, + Role, + TextPart, +) +from genkit.plugins.vertex_ai.gemini import Gemini, GeminiVersion + + +@pytest.mark.parametrize('version', [x for x in GeminiVersion]) +def test_generate_text_response(mocker, version): + mocked_respond = 'Mocked Respond' + request = GenerateRequest( + messages=[ + Message( + role=Role.USER, + content=[ + TextPart(text=f'Hi, mock!'), + ], + ), + ] + ) + gemini = Gemini(version) + genai_model_mock = mocker.MagicMock() + model_response_mock = mocker.MagicMock() + model_response_mock.text = mocked_respond + genai_model_mock.generate_content.return_value = model_response_mock + mocker.patch( + 'genkit.plugins.vertex_ai.gemini.Gemini.gemini_model', genai_model_mock + ) + + response = gemini.handle_request(request) + assert isinstance(response, GenerateResponse) + assert response.message.content[0].root.text == mocked_respond + + +@pytest.mark.parametrize('version', [x for x in GeminiVersion]) +def test_gemini_metadata(version): + gemini = Gemini(version) + supports = gemini.model_metadata['model']['supports'] + assert isinstance(supports, dict) + assert supports['multiturn'] + assert supports['media'] + assert supports['tools'] + assert supports['system_role'] diff --git a/py/plugins/vertex-ai/tests/test_imagen.py b/py/plugins/vertex-ai/tests/test_imagen.py new file mode 100644 index 0000000000..51e8b1e457 --- /dev/null +++ b/py/plugins/vertex-ai/tests/test_imagen.py @@ -0,0 +1,57 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Test Gemini models.""" + +import pytest +from genkit.core.typing import ( + GenerateRequest, + GenerateResponse, + Media1, + Message, + Role, + TextPart, +) +from genkit.plugins.vertex_ai.imagen import Imagen, ImagenVersion + + +@pytest.mark.parametrize('version', [x for x in ImagenVersion]) +def test_generate(mocker, version): + mocked_respond = 'Supposed Base64 string' + request = GenerateRequest( + messages=[ + Message( + role=Role.USER, + content=[ + TextPart(text=f'Draw a test.'), + ], + ), + ] + ) + imagen = Imagen(version) + genai_model_mock = mocker.MagicMock() + model_response_mock = mocker.MagicMock() + model_response_mock._mime_type = '' + model_response_mock._as_base64_string.return_value = mocked_respond + genai_model_mock.generate_images.return_value = [model_response_mock] + mocker.patch( + 'genkit.plugins.vertex_ai.imagen.Imagen.model', genai_model_mock + ) + + response = imagen.handle_request(request) + assert isinstance(response, GenerateResponse) + assert isinstance(response.message.content[0].root.media, Media1) + assert response.message.content[0].root.media.url == mocked_respond + + +@pytest.mark.parametrize('version', [x for x in ImagenVersion]) +def test_gemini_metadata(version): + imagen = Imagen(version) + supports = imagen.model_metadata['model']['supports'] + assert isinstance(supports, dict) + assert not supports['multiturn'] + assert not supports['tools'] + assert not supports['system_role'] diff --git a/py/pyproject.toml b/py/pyproject.toml index 1a1f15b458..7097bfd07f 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -1,6 +1,6 @@ [project] dependencies = [ - "dotprompt", + "dotpromptz", "genkit", "genkit-chroma-plugin", "genkit-firebase-plugin", @@ -9,7 +9,6 @@ dependencies = [ "genkit-ollama-plugin", "genkit-pinecone-plugin", "genkit-vertex-ai-plugin", - "handlebarz", ] description = "Workspace for Genkit packages" license = { text = "Apache-2.0" } @@ -21,14 +20,16 @@ version = "0.1.0" [dependency-groups] dev = [ "bpython>=0.25", - "ipython>=8.31.0", + "ipython>=8.32.0", "jupyter>=1.1.1", - "pytest-asyncio>=0.25.2", + "pytest-asyncio>=0.25.3", "pytest>=8.3.4", "pytest-cov>=6.0.0", - "datamodel-code-generator>=0.26.5", + "datamodel-code-generator>=0.27.3", + "pytest-watcher>=0.4.3", ] -lint = ["mypy>=1.14.1", "ruff>=0.9.2"] + +lint = ["mypy>=1.15", "ruff>=0.9"] [tool.hatch.build.targets.wheel] packages = [] @@ -48,17 +49,22 @@ python_files = [ testpaths = ["packages", "plugins", "samples"] [tool.pytest.ini_options] -addopts = "--cov" +addopts = "--cov" +asyncio_default_fixture_loop_scope = "session" [tool.coverage.report] -fail_under = 70 +fail_under = 85 # uv based package management. [tool.uv] default-groups = ["dev", "lint"] [tool.uv.sources] -dotprompt = { workspace = true } +basic-gemini = { workspace = true } +coffee-shop = { workspace = true } +context-caching = { workspace = true } +dotpromptz = { git = "https://github.com/google/dotprompt.git", subdirectory = "python/dotpromptz", rev = "main" } +flow-sample1 = { workspace = true } genkit = { workspace = true } genkit-chroma-plugin = { workspace = true } genkit-firebase-plugin = { workspace = true } @@ -67,39 +73,129 @@ genkit-google-cloud-plugin = { workspace = true } genkit-ollama-plugin = { workspace = true } genkit-pinecone-plugin = { workspace = true } genkit-vertex-ai-plugin = { workspace = true } -handlebarz = { workspace = true } hello = { workspace = true } +menu = { workspace = true } +prompt-file = { workspace = true } +rag = { workspace = true } +vertex-ai-model-garden = { workspace = true } +vertex-ai-reranker = { workspace = true } +vertex-ai-vector-search = { workspace = true } + [tool.uv.workspace] members = ["packages/*", "plugins/*", "samples/*"] # Ruff checks and formatting. [tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "bazel-*", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] indent-width = 4 -line-length = 80 +line-length = 80 +preview = true +target-version = "py312" +unsafe-fixes = true + + +[tool.ruff.lint] +fixable = ["ALL"] +select = [ + "E", # pycodestyle (errors) + "W", # pycodestyle (warnings) + "F", # pyflakes + "I", # isort (import sorting) + "UP", # pyupgrade (Python version upgrades) + "B", # flake8-bugbear (common bugs) + "N", # pep8-naming (naming conventions) + "D", # pydocstyle + "F401", # unused imports +] + +[tool.ruff.lint.pydocstyle] +convention = "google" [tool.ruff.format] -line-ending = "lf" -quote-style = "single" +docstring-code-format = true +docstring-code-line-length = 80 +indent-style = "space" +line-ending = "lf" +quote-style = "single" +skip-magic-trailing-comma = false # Static type checking. [tool.mypy] disallow_incomplete_defs = true -disallow_untyped_defs = true -warn_unused_configs = true +disallow_untyped_defs = true +explicit_package_bases = true +mypy_path = [ + "packages/genkit/src", + "plugins/chroma/src", + "plugins/firebase/src", + "plugins/google-ai/src", + "plugins/google-cloud/src", + "plugins/ollama/src", + "plugins/pinecone/src", + "plugins/vertex-ai/src", + "samples/basic-gemini/src", + "samples/coffee-shop/src", + "samples/context-caching/src", + "samples/flow-sample1/src", + "samples/hello/src", + "samples/menu/src", + "samples/prompt-file/src", + "samples/rag/src", + "samples/vertex-ai-model-garden/src", + "samples/vertex-ai-reranker/src", + "samples/vertex-ai-vector-search/src", +] +namespace_packages = true +strict = true +warn_unused_configs = true [tool.datamodel-codegen] -#strict-types = ["str", "int", "float", "bool", "bytes"] # Don't use; produces StrictStr, StrictInt, etc. #collapse-root-models = true # Don't use; produces Any as types. +#strict-types = ["str", "int", "float", "bool", "bytes"] # Don't use; produces StrictStr, StrictInt, etc. +#use-subclass-enum = true +capitalise-enum-members = true disable-timestamp = true enable-version-header = true field-constraints = true input = "../genkit-tools/genkit-schema.json" input-file-type = "jsonschema" -output = "packages/genkit/src/genkit/core/schemas.py" +output = "packages/genkit/src/genkit/core/typing.py" output-model-type = "pydantic_v2.BaseModel" +snake-case-field = true strict-nullable = true target-python-version = "3.12" +use-default = false use-schema-description = true use-standard-collections = true +use-subclass-enum = true use-union-operator = true +use-unique-items-as-set = true diff --git a/py/packages/dotprompt/LICENSE b/py/samples/basic-gemini/LICENSE similarity index 100% rename from py/packages/dotprompt/LICENSE rename to py/samples/basic-gemini/LICENSE diff --git a/py/samples/basic-gemini/README.md b/py/samples/basic-gemini/README.md new file mode 100644 index 0000000000..096a93f472 --- /dev/null +++ b/py/samples/basic-gemini/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/basic-gemini/main.py +``` diff --git a/py/samples/basic-gemini/pyproject.toml b/py/samples/basic-gemini/pyproject.toml new file mode 100644 index 0000000000..6f05e72777 --- /dev/null +++ b/py/samples/basic-gemini/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "basic-gemini Genkit sample" +license = { text = "Apache-2.0" } +name = "basic-gemini" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/basic-gemini"] diff --git a/py/samples/basic-gemini/src/basic-gemini.py b/py/samples/basic-gemini/src/basic-gemini.py new file mode 100644 index 0000000000..a3f9d1578d --- /dev/null +++ b/py/samples/basic-gemini/src/basic-gemini.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the basic Gemini sample. + + This function demonstrates basic usage of the Gemini model in the + Genkit framework. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/packages/handlebarz/LICENSE b/py/samples/coffee-shop/LICENSE similarity index 100% rename from py/packages/handlebarz/LICENSE rename to py/samples/coffee-shop/LICENSE diff --git a/py/samples/coffee-shop/README.md b/py/samples/coffee-shop/README.md new file mode 100644 index 0000000000..f6435d917e --- /dev/null +++ b/py/samples/coffee-shop/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/coffee-shop/main.py +``` diff --git a/py/samples/coffee-shop/pyproject.toml b/py/samples/coffee-shop/pyproject.toml new file mode 100644 index 0000000000..b47512f3c3 --- /dev/null +++ b/py/samples/coffee-shop/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "coffee-shop Genkit sample" +license = { text = "Apache-2.0" } +name = "coffee-shop" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/coffee-shop"] diff --git a/py/samples/coffee-shop/src/coffee-shop.py b/py/samples/coffee-shop/src/coffee-shop.py new file mode 100644 index 0000000000..0531646b32 --- /dev/null +++ b/py/samples/coffee-shop/src/coffee-shop.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the coffee shop sample. + + This function demonstrates how to use Genkit to build a coffee shop + ordering system. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/context-caching/LICENSE b/py/samples/context-caching/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/context-caching/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/context-caching/README.md b/py/samples/context-caching/README.md new file mode 100644 index 0000000000..c17520178e --- /dev/null +++ b/py/samples/context-caching/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/context-caching/main.py +``` diff --git a/py/samples/context-caching/pyproject.toml b/py/samples/context-caching/pyproject.toml new file mode 100644 index 0000000000..b4a1c12f11 --- /dev/null +++ b/py/samples/context-caching/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "context-caching Genkit sample" +license = { text = "Apache-2.0" } +name = "context-caching" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/context-caching"] diff --git a/py/samples/context-caching/src/context-caching.py b/py/samples/context-caching/src/context-caching.py new file mode 100644 index 0000000000..259d9d9c45 --- /dev/null +++ b/py/samples/context-caching/src/context-caching.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the context caching sample. + + This function demonstrates how to use context caching in Genkit for + improved performance. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/flow-sample1/LICENSE b/py/samples/flow-sample1/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/flow-sample1/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/flow-sample1/README.md b/py/samples/flow-sample1/README.md new file mode 100644 index 0000000000..b82595296a --- /dev/null +++ b/py/samples/flow-sample1/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/flow-sample1/main.py +``` diff --git a/py/samples/flow-sample1/pyproject.toml b/py/samples/flow-sample1/pyproject.toml new file mode 100644 index 0000000000..9197b29982 --- /dev/null +++ b/py/samples/flow-sample1/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "flow-sample1 Genkit sample" +license = { text = "Apache-2.0" } +name = "flow-sample1" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/flow-sample1"] diff --git a/py/samples/flow-sample1/src/flow-sample1.py b/py/samples/flow-sample1/src/flow-sample1.py new file mode 100644 index 0000000000..e105ce8f50 --- /dev/null +++ b/py/samples/flow-sample1/src/flow-sample1.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the flow sample. + + This function demonstrates how to create and use AI flows in the + Genkit framework. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/hello/README.md b/py/samples/hello/README.md index 5df8f4ea9b..ea53266e23 100644 --- a/py/samples/hello/README.md +++ b/py/samples/hello/README.md @@ -1,6 +1,7 @@ # Hello world ## Setup environment +Use `gcloud auth application-default login` to connect to the VertexAI. ```bash uv venv @@ -12,7 +13,5 @@ source .venv/bin/activate TODO ```bash -genkit start -- python3 hello.py # Doesn't currently work with the venv configuration. -genkit start -- uv run hello.py # Starts but runtime detection fails. -genkit start -- uv run python3 hello.py # Starts but runtime detection fails. +genkit start -- uv run --directory py samples/hello/main.py ``` diff --git a/py/samples/hello/hello.py b/py/samples/hello/hello.py deleted file mode 100644 index 9a74fa4c4e..0000000000 --- a/py/samples/hello/hello.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025 Google LLC -# SPDX-License-Identifier: Apache-2.0 - - -from genkit.core.schemas import Message, TextPart, GenerateRequest -from genkit.plugins.vertex_ai import vertexAI, gemini -from genkit.veneer.veneer import Genkit -from pydantic import BaseModel, Field - -ai = Genkit(plugins=[vertexAI()], model=gemini('gemini-1.5-flash')) - - -class MyInput(BaseModel): - a: int = Field(description='a field') - b: int = Field(description='b field') - - -def hi_fn(hi_input) -> GenerateRequest: - return GenerateRequest( - messages=[ - Message( - role='user', - content=[TextPart(text='hi, my name is ' + hi_input)], - ) - ] - ) - - -# hi = ai.define_prompt( -# name="hi", -# fn=hi_fn, -# model=gemini("gemini-1.5-flash")) -# -# @ai.flow() -# def hiPrompt(): -# return hi("Pavel") - - -@ai.flow() -def say_hi(name: str): - return ai.generate( - messages=[Message(role='user', content=[TextPart(text='hi ' + name)])] - ) - - -@ai.flow() -def sum_two_numbers2(my_input: MyInput): - return my_input.a + my_input.b - - -def main() -> None: - print(say_hi('John Doe')) - print(sum_two_numbers2(MyInput(a=1, b=3))) - - -if __name__ == '__main__': - main() diff --git a/py/samples/hello/pyproject.toml b/py/samples/hello/pyproject.toml index f6430a1bdc..ca089c8df8 100644 --- a/py/samples/hello/pyproject.toml +++ b/py/samples/hello/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "genkit-vertex-ai-plugin", "pydantic>=2.10.5", ] -description = "Hello world sample" +description = "hello Genkit sample" license = { text = "Apache-2.0" } name = "hello" readme = "README.md" diff --git a/py/samples/hello/src/hello.py b/py/samples/hello/src/hello.py new file mode 100644 index 0000000000..f844229bff --- /dev/null +++ b/py/samples/hello/src/hello.py @@ -0,0 +1,207 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A hello world sample that just calls some flows.""" + +import asyncio +from typing import Any + +from genkit.ai.generate import generate_action +from genkit.core.action import ActionRunContext +from genkit.core.typing import ( + GenerateActionOptions, + GenerateRequest, + Message, + Role, + TextPart, +) +from genkit.plugins.vertex_ai import ( + EmbeddingModels, + GeminiVersion, + VertexAI, + vertexai_name, +) +from genkit.veneer.veneer import Genkit +from pydantic import BaseModel, Field + +ai = Genkit( + plugins=[VertexAI()], + model=vertexai_name(GeminiVersion.GEMINI_1_5_FLASH), +) + + +class MyInput(BaseModel): + """Input model for the sum_two_numbers2 function. + + Attributes: + a: First number to add. + b: Second number to add. + """ + + a: int = Field(description='a field') + b: int = Field(description='b field') + + +def hi_fn(hi_input) -> GenerateRequest: + """Generate a request to greet a user. + + Args: + hi_input: Input data containing user information. + + Returns: + A GenerateRequest object with the greeting message. + """ + return GenerateRequest( + messages=[ + Message( + role=Role.USER, + content=[ + TextPart(text=f'Say hi to {hi_input}'), + ], + ), + ], + ) + + +@ai.flow() +async def say_hi(name: str): + """Generate a greeting for the given name. + + Args: + name: The name of the person to greet. + + Returns: + The generated greeting response. + """ + return await ai.generate( + messages=[ + Message( + role=Role.USER, + content=[TextPart(text=f'Say hi to {name}')], + ), + ], + ) + + +@ai.flow() +async def embed_docs(docs: list[str]): + """Generate an embedding for the words in a list. + + Args: + docs: list of texts (string) + + Returns: + The generated embedding. + """ + return await ai.embed( + model=vertexai_name(EmbeddingModels.TEXT_EMBEDDING_004_ENG), + documents=docs, + ) + + +@ai.flow() +async def simple_generate_action_flow(name: str) -> Any: + """Generate a greeting for the given name. + + Args: + name: The name of the person to greet. + + Returns: + The generated greeting response. + """ + response = await generate_action( + ai.registry, + GenerateActionOptions( + model='vertexai/gemini-1.5-flash', + messages=[ + Message( + role=Role.USER, + content=[TextPart(text=f'Say hi to {name}')], + ), + ], + ), + ) + return response.text() + + +class GablorkenInput(BaseModel): + value: int = Field(description='value to calculate gablorken for') + + +@ai.tool('calculates a gablorken') +def gablorkenTool(input: GablorkenInput): + return input.value * 3 - 5 + + +@ai.flow() +async def simple_generate_action_with_tools_flow(value: int) -> Any: + """Generate a greeting for the given name. + + Args: + name: The name of the person to greet. + + Returns: + The generated greeting response. + """ + response = await generate_action( + ai.registry, + GenerateActionOptions( + model='vertexai/gemini-1.5-flash', + messages=[ + Message( + role=Role.USER, + content=[TextPart(text=f'what is a gablorken of {value}')], + ), + ], + tools=['gablorkenTool'], + ), + ) + return response.text() + + +@ai.flow() +def sum_two_numbers2(my_input: MyInput) -> Any: + """Add two numbers together. + + Args: + my_input: A MyInput object containing the two numbers to add. + + Returns: + The sum of the two numbers. + """ + return my_input.a + my_input.b + + +@ai.flow() +def streaming_sync_flow(inp: str, ctx: ActionRunContext): + """Example of sync flow.""" + ctx.send_chunk(1) + ctx.send_chunk({'chunk': 'blah'}) + ctx.send_chunk(3) + return 'streamingSyncFlow 4' + + +@ai.flow() +async def streaming_async_flow(inp: str, ctx: ActionRunContext): + """Example of async flow.""" + ctx.send_chunk(1) + ctx.send_chunk({'chunk': 'blah'}) + ctx.send_chunk(3) + return 'streamingAsyncFlow 4' + + +async def main() -> None: + """Main entry point for the hello sample. + + This function demonstrates the usage of the AI flow by generating + greetings and performing simple arithmetic operations. + """ + print(await say_hi('John Doe')) + print(sum_two_numbers2(MyInput(a=1, b=3))) + print( + await embed_docs(['banana muffins? ', 'banana bread? banana muffins?']) + ) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/py/samples/menu/LICENSE b/py/samples/menu/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/menu/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/menu/README.md b/py/samples/menu/README.md new file mode 100644 index 0000000000..72ac43df9a --- /dev/null +++ b/py/samples/menu/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/menu/main.py +``` diff --git a/py/packages/handlebarz/pyproject.toml b/py/samples/menu/pyproject.toml similarity index 70% rename from py/packages/handlebarz/pyproject.toml rename to py/samples/menu/pyproject.toml index 6196aa84fd..2aa53cc662 100644 --- a/py/packages/handlebarz/pyproject.toml +++ b/py/samples/menu/pyproject.toml @@ -13,16 +13,22 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Code Generators", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Text Processing :: Markup", ] -dependencies = [] -description = "Python implementation for Handlebars.js" +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "menu Genkit sample" license = { text = "Apache-2.0" } -name = "handlebarz" +name = "menu" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.12" version = "0.1.0" [build-system] @@ -30,4 +36,4 @@ build-backend = "hatchling.build" requires = ["hatchling"] [tool.hatch.build.targets.wheel] -packages = ["src/handlebarz"] +packages = ["src/menu"] diff --git a/py/samples/menu/src/menu.py b/py/samples/menu/src/menu.py new file mode 100644 index 0000000000..c35cafc506 --- /dev/null +++ b/py/samples/menu/src/menu.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the menu sample. + + This function demonstrates how to use Genkit to build an interactive + menu system. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/ollama/LICENSE b/py/samples/ollama/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/ollama/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/ollama/README.md b/py/samples/ollama/README.md new file mode 100644 index 0000000000..e6ed1cfc8d --- /dev/null +++ b/py/samples/ollama/README.md @@ -0,0 +1,11 @@ +# Run the sample + +## NOTE +Before running the sample make sure to install the model and start ollama serving. +In case of questions, please refer to `./py/plugins/ollama/README.md` + +## Execute "Hello World" Sample + +```bash +genkit start -- uv run hello.py +``` diff --git a/py/samples/ollama/hello.py b/py/samples/ollama/hello.py new file mode 100644 index 0000000000..ec0da7d222 --- /dev/null +++ b/py/samples/ollama/hello.py @@ -0,0 +1,65 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +import asyncio + +from genkit.core.typing import Message, Role, TextPart +from genkit.plugins.ollama import Ollama, ollama_name +from genkit.plugins.ollama.models import ( + ModelDefinition, + OllamaAPITypes, + OllamaPluginParams, +) +from genkit.veneer import Genkit + +# model can be pulled with `ollama pull *LLM_VERSION*` +LLM_VERSION = 'gemma2:latest' + +plugin_params = OllamaPluginParams( + models=[ + ModelDefinition( + name=LLM_VERSION, + api_type=OllamaAPITypes.CHAT, + ) + ], + use_async_api=True, +) + +ai = Genkit( + plugins=[ + Ollama( + plugin_params=plugin_params, + ) + ], + model=ollama_name(LLM_VERSION), +) + + +@ai.flow() +async def say_hi(hi_input: str): + """Generate a request to greet a user. + + Args: + hi_input: Input data containing user information. + + Returns: + A GenerateRequest object with the greeting message. + """ + return await ai.generate( + messages=[ + Message( + role=Role.USER, + content=[ + TextPart(text='hi ' + hi_input), + ], + ) + ] + ) + + +async def main() -> None: + response = await say_hi('John Doe') + print(response) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/py/samples/ollama/pyproject.toml b/py/samples/ollama/pyproject.toml new file mode 100644 index 0000000000..54bfe808a7 --- /dev/null +++ b/py/samples/ollama/pyproject.toml @@ -0,0 +1,30 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "pydantic>=2.10.5", +] +description = "Ollama sample" +license = { text = "Apache-2.0" } +name = "ollama_example" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" diff --git a/py/samples/prompt-file/LICENSE b/py/samples/prompt-file/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/prompt-file/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/prompt-file/README.md b/py/samples/prompt-file/README.md new file mode 100644 index 0000000000..d34e2844e9 --- /dev/null +++ b/py/samples/prompt-file/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/prompt-file/main.py +``` diff --git a/py/samples/prompt-file/pyproject.toml b/py/samples/prompt-file/pyproject.toml new file mode 100644 index 0000000000..c44147105f --- /dev/null +++ b/py/samples/prompt-file/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "prompt-file Genkit sample" +license = { text = "Apache-2.0" } +name = "prompt-file" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/prompt-file"] diff --git a/py/samples/prompt-file/src/prompt-file.py b/py/samples/prompt-file/src/prompt-file.py new file mode 100644 index 0000000000..49b6cf32fc --- /dev/null +++ b/py/samples/prompt-file/src/prompt-file.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the prompt file sample. + + This function demonstrates how to load and use prompts from external + files in Genkit. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/rag/LICENSE b/py/samples/rag/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/rag/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/rag/README.md b/py/samples/rag/README.md new file mode 100644 index 0000000000..2854ec6226 --- /dev/null +++ b/py/samples/rag/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/rag/main.py +``` diff --git a/py/packages/dotprompt/pyproject.toml b/py/samples/rag/pyproject.toml similarity index 72% rename from py/packages/dotprompt/pyproject.toml rename to py/samples/rag/pyproject.toml index 5387b1d722..cad281b0d6 100644 --- a/py/packages/dotprompt/pyproject.toml +++ b/py/samples/rag/pyproject.toml @@ -14,10 +14,19 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries", ] -dependencies = ["handlebarz"] -description = "Dotprompt is a language-neutral executable prompt template file format for Generative AI." +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "rag Genkit sample" license = { text = "Apache-2.0" } -name = "dotprompt" +name = "rag" readme = "README.md" requires-python = ">=3.12" version = "0.1.0" @@ -27,4 +36,4 @@ build-backend = "hatchling.build" requires = ["hatchling"] [tool.hatch.build.targets.wheel] -packages = ["src/dotprompt"] +packages = ["src/rag"] diff --git a/py/samples/rag/src/rag.py b/py/samples/rag/src/rag.py new file mode 100644 index 0000000000..8c340237c1 --- /dev/null +++ b/py/samples/rag/src/rag.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the RAG (Retrieval-Augmented Generation) sample. + + This function demonstrates how to implement RAG patterns using the + Genkit framework. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/vertex-ai-model-garden/LICENSE b/py/samples/vertex-ai-model-garden/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/vertex-ai-model-garden/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/vertex-ai-model-garden/README.md b/py/samples/vertex-ai-model-garden/README.md new file mode 100644 index 0000000000..3ccce9ddcc --- /dev/null +++ b/py/samples/vertex-ai-model-garden/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/vertex-ai-vector-search/main.py +``` diff --git a/py/samples/vertex-ai-model-garden/pyproject.toml b/py/samples/vertex-ai-model-garden/pyproject.toml new file mode 100644 index 0000000000..45a91a724b --- /dev/null +++ b/py/samples/vertex-ai-model-garden/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "vertex-ai-vector-search Genkit sample" +license = { text = "Apache-2.0" } +name = "vertex-ai-vector-search" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/vertex-ai-vector-search"] diff --git a/py/samples/vertex-ai-model-garden/src/vertex-ai-model-garden.py b/py/samples/vertex-ai-model-garden/src/vertex-ai-model-garden.py new file mode 100644 index 0000000000..0129e97b06 --- /dev/null +++ b/py/samples/vertex-ai-model-garden/src/vertex-ai-model-garden.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the Vertex AI Model Garden sample. + + This function demonstrates how to use Vertex AI Model Garden models + with the Genkit framework. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/vertex-ai-reranker/LICENSE b/py/samples/vertex-ai-reranker/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/vertex-ai-reranker/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/vertex-ai-reranker/README.md b/py/samples/vertex-ai-reranker/README.md new file mode 100644 index 0000000000..0d316919e1 --- /dev/null +++ b/py/samples/vertex-ai-reranker/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/vertex-ai-reranker/main.py +``` diff --git a/py/samples/vertex-ai-reranker/pyproject.toml b/py/samples/vertex-ai-reranker/pyproject.toml new file mode 100644 index 0000000000..42f0f0ca20 --- /dev/null +++ b/py/samples/vertex-ai-reranker/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "vertex-ai-reranker Genkit sample" +license = { text = "Apache-2.0" } +name = "vertex-ai-reranker" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/vertex-ai-reranker"] diff --git a/py/samples/vertex-ai-reranker/src/vertex-ai-reranker.py b/py/samples/vertex-ai-reranker/src/vertex-ai-reranker.py new file mode 100644 index 0000000000..19d3020238 --- /dev/null +++ b/py/samples/vertex-ai-reranker/src/vertex-ai-reranker.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the Vertex AI Reranker sample. + + This function demonstrates how to use Vertex AI Reranker models + with the Genkit framework for improved search results. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/vertex-ai-vector-search/LICENSE b/py/samples/vertex-ai-vector-search/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/vertex-ai-vector-search/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/vertex-ai-vector-search/README.md b/py/samples/vertex-ai-vector-search/README.md new file mode 100644 index 0000000000..1c0ea20406 --- /dev/null +++ b/py/samples/vertex-ai-vector-search/README.md @@ -0,0 +1,16 @@ +# Hello world + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/vertex-ai-model-garden/main.py +``` diff --git a/py/samples/vertex-ai-vector-search/pyproject.toml b/py/samples/vertex-ai-vector-search/pyproject.toml new file mode 100644 index 0000000000..682e8aa8c8 --- /dev/null +++ b/py/samples/vertex-ai-vector-search/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "vertex-ai-model-garden Genkit sample" +license = { text = "Apache-2.0" } +name = "vertex-ai-model-garden" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/vertex-ai-model-garden"] diff --git a/py/samples/vertex-ai-vector-search/src/vertex-ai-vector-search.py b/py/samples/vertex-ai-vector-search/src/vertex-ai-vector-search.py new file mode 100644 index 0000000000..ddf8221cbf --- /dev/null +++ b/py/samples/vertex-ai-vector-search/src/vertex-ai-vector-search.py @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""A stub for the sample to come.""" + + +def main() -> None: + """Main entry point for the Vertex AI Vector Search sample. + + This function demonstrates how to use Vertex AI Vector Search + capabilities with the Genkit framework for semantic search. + """ + print('Hey') + + +if __name__ == '__main__': + main() diff --git a/py/samples/vetex-ai-imagen/LICENSE b/py/samples/vetex-ai-imagen/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/vetex-ai-imagen/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/py/samples/vetex-ai-imagen/README.md b/py/samples/vetex-ai-imagen/README.md new file mode 100644 index 0000000000..8437f509c5 --- /dev/null +++ b/py/samples/vetex-ai-imagen/README.md @@ -0,0 +1,20 @@ +# Flower image generator + +## Setup environment +Use `gcloud auth application-default login` to connect to the VertexAI. + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +The sample generates the image of flower in a folder you run it on +in a directory you mention by --directory. + +The command to run: + +```bash +genkit start -- uv run --directory py samples/vetex-ai-imagen/example_imagen.py +``` diff --git a/py/samples/vetex-ai-imagen/example_imagen.py b/py/samples/vetex-ai-imagen/example_imagen.py new file mode 100644 index 0000000000..a247e11263 --- /dev/null +++ b/py/samples/vetex-ai-imagen/example_imagen.py @@ -0,0 +1,40 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +"""An Imagen model on VertexAI sample.""" + +import asyncio +import base64 + +from genkit.core.typing import Message, Role, TextPart +from genkit.plugins.vertex_ai import ImagenVersion, VertexAI, vertexai_name +from genkit.veneer.veneer import Genkit + +ai = Genkit( + plugins=[VertexAI()], model=vertexai_name(ImagenVersion.IMAGEN3_FAST) +) + + +@ai.flow() +async def draw_image(prompt: str): + return await ai.generate( + messages=[ + Message( + role=Role.USER, + content=[TextPart(text=prompt)], + ) + ] + ) + + +async def main() -> None: + """Main entry point for the Imagen sample.""" + response = await draw_image('Draw a flower.') + base64string = response.message.content[0].root.media.url + image = base64.b64decode(base64string, validate=True) + with open('flower.jpg', 'wb') as f: + f.write(image) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/py/samples/vetex-ai-imagen/pyproject.toml b/py/samples/vetex-ai-imagen/pyproject.toml new file mode 100644 index 0000000000..cb253aace8 --- /dev/null +++ b/py/samples/vetex-ai-imagen/pyproject.toml @@ -0,0 +1,39 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-pinecone-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "Imagen Genkit sample" +license = { text = "Apache-2.0" } +name = "imagen" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/imagen"] diff --git a/py/tests/smoke/package_test.py b/py/tests/smoke/package_test.py index 598659dfb5..14659f5222 100644 --- a/py/tests/smoke/package_test.py +++ b/py/tests/smoke/package_test.py @@ -4,6 +4,7 @@ """Smoke tests for package structure.""" # TODO: Replace this with proper imports once we have a proper implementation. +from dotpromptz import package_name as dotpromptz_package_name from genkit.ai import package_name as ai_package_name from genkit.core import package_name as core_package_name from genkit.plugins.chroma import package_name as chroma_package_name @@ -24,10 +25,23 @@ def square(n: int | float) -> int | float: + """Calculates the square of a number. + + Args: + n: The number to square. + + Returns: + The square of n. + """ return n * n def test_package_names() -> None: + """A test that ensure that the package imports work correctly. + + This test verifies that the package imports work correctly from the + end-user perspective. + """ assert ai_package_name() == 'genkit.ai' assert chroma_package_name() == 'genkit.plugins.chroma' assert core_package_name() == 'genkit.core' @@ -39,9 +53,11 @@ def test_package_names() -> None: assert pinecone_package_name() == 'genkit.plugins.pinecone' assert vertex_ai_models_package_name() == 'genkit.plugins.vertex_ai.models' assert vertex_ai_package_name() == 'genkit.plugins.vertex_ai' + assert dotpromptz_package_name() == 'dotpromptz' def test_square() -> None: + """Tests whether the square function works correctly.""" assert square(2) == 4 assert square(3) == 9 assert square(4) == 16 diff --git a/py/uv.lock b/py/uv.lock index 1c9b30889e..55a21d50b2 100644 --- a/py/uv.lock +++ b/py/uv.lock @@ -1,14 +1,16 @@ version = 1 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '4.0'", - "python_full_version >= '3.13' and python_full_version < '4.0'", + "python_full_version >= '3.13'", "python_full_version < '3.13'", ] [manifest] members = [ - "dotprompt", + "basic-gemini", + "coffee-shop", + "context-caching", + "flow-sample1", "genkit", "genkit-chroma-plugin", "genkit-firebase-plugin", @@ -18,8 +20,15 @@ members = [ "genkit-pinecone-plugin", "genkit-vertex-ai-plugin", "genkit-workspace", - "handlebarz", "hello", + "imagen", + "menu", + "ollama-example", + "prompt-file", + "rag", + "vertex-ai-model-garden", + "vertex-ai-reranker", + "vertex-ai-vector-search", ] [[package]] @@ -138,32 +147,60 @@ wheels = [ [[package]] name = "attrs" -version = "24.3.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, ] [[package]] name = "babel" -version = "2.16.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, +] + +[[package]] +name = "basic-gemini" +version = "0.1.0" +source = { editable = "samples/basic-gemini" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, ] [[package]] name = "beautifulsoup4" -version = "4.12.3" +version = "4.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, + { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, ] [[package]] @@ -240,20 +277,20 @@ wheels = [ [[package]] name = "cachetools" -version = "5.5.1" +version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] [[package]] @@ -336,6 +373,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] +[[package]] +name = "coffee-shop" +version = "0.1.0" +source = { editable = "samples/coffee-shop" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -357,42 +421,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, ] +[[package]] +name = "context-caching" +version = "0.1.0" +source = { editable = "samples/context-caching" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + [[package]] name = "coverage" -version = "7.6.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, - { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, - { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, - { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, - { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, - { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, - { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, - { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, - { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, - { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, - { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, - { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, - { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, - { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, - { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, - { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, - { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, - { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, - { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, - { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, - { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, - { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, - { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, - { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, - { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, - { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, - { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, - { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, - { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, - { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, + { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, + { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, + { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, + { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, + { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, + { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, + { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, + { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, + { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, + { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, + { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, + { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, + { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, + { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, + { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, + { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, + { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, + { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, + { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, + { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, + { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, + { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, + { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, ] [[package]] @@ -410,22 +502,29 @@ wheels = [ [[package]] name = "cwcwidth" -version = "0.1.9" +version = "0.1.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/e3/275e359662052888bbb262b947d3f157aaf685aaeef4efc8393e4f36d8aa/cwcwidth-0.1.9.tar.gz", hash = "sha256:f19d11a0148d4a8cacd064c96e93bca8ce3415a186ae8204038f45e108db76b8", size = 57892 } +sdist = { url = "https://files.pythonhosted.org/packages/23/76/03fc9fb3441a13e9208bb6103ebb7200eba7647d040008b8303a1c03e152/cwcwidth-0.1.10.tar.gz", hash = "sha256:7468760f72c1f4107be1b2b2854bc000401ea36a69daed36fb966a1e19a7a124", size = 60265 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/01/69a81a655ace57ce1423470ca29661a6821b66645ad4089e03d362a5c349/cwcwidth-0.1.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:42de102d5191fc68ef3ff6530f60c4895148ddc21aa0acaaf4612e5f7f0c38c4", size = 22281 }, - { url = "https://files.pythonhosted.org/packages/76/6a/00c1944f27116c1846ea3e84cc2f5d8711b213712d7e06183f1c49162fc3/cwcwidth-0.1.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:877e48c615b3fec88b7e640f9cf9d96704497657fb5aad2b7c0b0c59ecabff69", size = 101327 }, - { url = "https://files.pythonhosted.org/packages/fb/07/0389633bd61619000563a72d11387d98290cd1231ad3cfec964a845e0256/cwcwidth-0.1.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdbaf0a8dad20eb685df11a195a2449fe230b08a5b356d036c8d7e59d4128a88", size = 106554 }, - { url = "https://files.pythonhosted.org/packages/88/7c/5f84b644834e1a9ca41f7575bbace15f947fa46c1349b90f179843b47bc2/cwcwidth-0.1.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f6e0e023c4b127c47fd4c44cf537be209b9a28d8725f4f576f4d63744a23aa38", size = 102730 }, - { url = "https://files.pythonhosted.org/packages/6b/e1/69ff02feb0b10467b9fd0097650b1e4b6e0a2ad1ca32bcd1f936d18b27d8/cwcwidth-0.1.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b4f7d24236ce3c9d3b5e07fd75d232452f19bdddb6ae8bbfdcb97b6cb02835e8", size = 108551 }, - { url = "https://files.pythonhosted.org/packages/48/13/069554f659482f967cc380cac46f12a4cd2d55561a5f3dd0aebe900029ab/cwcwidth-0.1.9-cp312-cp312-win32.whl", hash = "sha256:ba9da6c911bf108334426890bc9f57b839a38e1afc4383a41bd70adbce470db3", size = 22223 }, - { url = "https://files.pythonhosted.org/packages/be/a2/462eebec8f0aa88751de678cbcdecd8b36ddf1ad05c25662541ef3e4455b/cwcwidth-0.1.9-cp312-cp312-win_amd64.whl", hash = "sha256:40466f16e85c338e8fc3eee87a8c9ca23416cc68b3049f68cb4cead5fb8b71b3", size = 24896 }, + { url = "https://files.pythonhosted.org/packages/87/28/8e2ab81f0116bfcec22069e4c92fda9d05b0512605ccef00b62d93719ded/cwcwidth-0.1.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d2b21ff2eb60c6793349b7fb161c40a8583a57ec32e61f47aab7938177bfdec", size = 23031 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/5adc535e2a714ecc926ea701e821a9abbe14f65cae4d615d20059b9b52a5/cwcwidth-0.1.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0316488349c3e5ca4b20de7daa1cb8e96a05d1d14d040d46e87a495da655f4a", size = 101219 }, + { url = "https://files.pythonhosted.org/packages/78/4c/18a5a06aa8db3cc28712ab957671e7718aedfc73403d84b0c2cb5cfcbc27/cwcwidth-0.1.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:848b6ffca1e32e28d2ccbb2cd395ccd3c38a7c4ec110728cd9d828eaf609b09e", size = 106565 }, + { url = "https://files.pythonhosted.org/packages/06/40/801cba5ccb9551c862ad210eba22031e4655cd74711e32756b7ce24fc751/cwcwidth-0.1.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c3a7bfe1da478c0c27c549f68c6e28a583413da3ee451854ec2d983497bd18b8", size = 102244 }, + { url = "https://files.pythonhosted.org/packages/e4/ed/60f61274fcfd0621a45e9403502e8f46968d562810a4424e5ff8d6bd50b0/cwcwidth-0.1.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cff03100f49170bc50fc399d05a31b8fcb7b0cef26df1a8068fa943387107f6c", size = 105634 }, + { url = "https://files.pythonhosted.org/packages/b1/27/8179cecd688fef894dda601455d35066adfa3d58af4e97c5ab112893b5f6/cwcwidth-0.1.10-cp312-cp312-win32.whl", hash = "sha256:2dd9a92fdfbc53fc79f0953f39708dcf743fd27450c374985f419e3d47eb89d4", size = 23507 }, + { url = "https://files.pythonhosted.org/packages/b2/b4/b7fe652a4d96f03ef051fff8313dfe827bc31578f7e67f1c98d5a5813f66/cwcwidth-0.1.10-cp312-cp312-win_amd64.whl", hash = "sha256:734d764281e3d87c40d0265543f00a653409145fa9f48a93bc0fbf9a8e7932ca", size = 26100 }, + { url = "https://files.pythonhosted.org/packages/af/f7/8c4cfe0b08053eea4da585ad5e12fef7cd11a0c9e4603ac8644c2a0b04b5/cwcwidth-0.1.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2391073280d774ab5d9af1d3aaa26ec456956d04daa1134fb71c31cd72ba5bba", size = 22344 }, + { url = "https://files.pythonhosted.org/packages/2a/48/176bbaf56520c5d6b72cbbe0d46821989eaa30df628daa5baecdd7f35458/cwcwidth-0.1.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bfbdc2943631ec770ee781b35b8876fa7e283ff2273f944e2a9ae1f3df4ecdf", size = 94907 }, + { url = "https://files.pythonhosted.org/packages/bc/fc/4dfed13b316a67bf2419a63db53566e3e5e4d4fc5a94ef493d3334be3c1f/cwcwidth-0.1.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb0103c7db8d86e260e016ff89f8f00ef5eb75c481abc346bfaa756da9f976b4", size = 100046 }, + { url = "https://files.pythonhosted.org/packages/4e/83/612eecdeddbb1329d0f4d416f643459c2c5ae7b753490e31d9dccfa6deed/cwcwidth-0.1.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3b7d38c552edf663bf13f32310f9fd6661070409807b1b5bf89917e2de804ab1", size = 96143 }, + { url = "https://files.pythonhosted.org/packages/57/98/87d10d88b5a6de3a4a3452802abed18b909510b9f118ad7f3a40ae48700a/cwcwidth-0.1.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1132be818498163a9208b9f4a28d759e08d3efeb885dfd10364434ccc7fa6a17", size = 99735 }, + { url = "https://files.pythonhosted.org/packages/61/de/e0c02f84b0418db5938eeb1269f53dee195615a856ed12f370ef79f6cd5b/cwcwidth-0.1.10-cp313-cp313-win32.whl", hash = "sha256:dcead1b7b60c99f8cda249feb8059e4da38587c34d0b5f3aa130f13c62c0ce35", size = 22922 }, + { url = "https://files.pythonhosted.org/packages/d8/4a/1d272a69f14924e43f5d6d4a7935c7a892e25c6e5b9a2c4459472132ef0c/cwcwidth-0.1.10-cp313-cp313-win_amd64.whl", hash = "sha256:b6eafd16d3edfec9acfc3d7b8856313bc252e0eccd56fb088f51ceed14c1bbdd", size = 25211 }, ] [[package]] name = "datamodel-code-generator" -version = "0.26.5" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -435,12 +534,12 @@ dependencies = [ { name = "isort" }, { name = "jinja2" }, { name = "packaging" }, - { name = "pydantic", extra = ["email"], marker = "python_full_version < '4.0'" }, + { name = "pydantic" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/e4/53153452235a387112df40f67aaf24072d4b5e33aa7bb385004f4c4baf38/datamodel_code_generator-0.26.5.tar.gz", hash = "sha256:c4a94a7dbf7972129882732d9bcee44c9ae090f57c82edd58d237b9d48c40dd0", size = 92586 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d3/80f6a2394bbf3b46b150fc75afa5b0050f91baa5771e9be87df148013d83/datamodel_code_generator-0.28.1.tar.gz", hash = "sha256:37ef5f3b488f7d7a3f0b5b3ba0f2bc1ae01bab4dc7e0f6b99ff6c40713a6beb3", size = 434901 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/d8/ead3e857d4048947fe92a731d6b1f257dcb267cc8b8918d3b72598c9b728/datamodel_code_generator-0.26.5-py3-none-any.whl", hash = "sha256:e32f986b9914a2b45093947043aa0192d704650be93151f78acf5c95676601ce", size = 114982 }, + { url = "https://files.pythonhosted.org/packages/c8/17/2876ca0a4ac7dd7cb5f56a2f0f6d9ac910969f467e8142c847c45a76b897/datamodel_code_generator-0.28.1-py3-none-any.whl", hash = "sha256:1ff8a56f9550a82bcba3e1ad7ebdb89bc655eeabbc4bc6acfb05977cbdc6381c", size = 115601 }, ] [[package]] @@ -462,11 +561,11 @@ wheels = [ [[package]] name = "decorator" -version = "5.1.1" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/e2/33c72e8409b39389b9a69e807e40d3466a63996ba4c65caaea1d31dfde16/decorator-5.2.0.tar.gz", hash = "sha256:1cf2ab68f8c1c7eae3895d82ab0daab41294cfbe6fbdebf50b44307299980762", size = 16973 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, + { url = "https://files.pythonhosted.org/packages/f6/0e/fc5d7660912606d43f32470cf952846a47512d3674fe9a3196f1a80a638b/decorator-5.2.0-py3-none-any.whl", hash = "sha256:f30a69c066f698c7c11fa1fa3425f684d3b4b01b494ee41e73c0a14f3de48427", size = 9149 }, ] [[package]] @@ -480,23 +579,14 @@ wheels = [ [[package]] name = "deprecated" -version = "1.2.15" +version = "1.2.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/a3/53e7d78a6850ffdd394d7048a31a6f14e44900adedf190f9a165f6b69439/deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d", size = 2977612 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941 }, -] - -[[package]] -name = "dnspython" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, ] [[package]] @@ -510,35 +600,28 @@ wheels = [ [[package]] name = "dotprompt" -version = "0.1.0" -source = { editable = "packages/dotprompt" } -dependencies = [ - { name = "handlebarz" }, +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/72/c37abd54a6df1b0a2b99eae7bd703e78b1e6c431df3f009ef7f4b81b7399/dotprompt-0.1.10.tar.gz", hash = "sha256:8b1875cc8411a5b204667f6d930f169914abba64c6ed210eac73f5f066754fd7", size = 42048 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/da/41f67d1a05762eedadcdd4f5be92f30ac1f0ca21dda60aa7dba7b642eabb/dotprompt-0.1.10-py3-none-any.whl", hash = "sha256:5a2d10fed9bba237971d6b68f04040a018be23e3e56eb32fab165509d3443c01", size = 29211 }, ] -[package.metadata] -requires-dist = [{ name = "handlebarz", editable = "packages/handlebarz" }] - [[package]] -name = "email-validator" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } +name = "dotpromptz" +version = "0.1.0" +source = { git = "https://github.com/google/dotprompt.git?subdirectory=python%2Fdotpromptz&rev=main#817df5924e6f5a519eb34dbe6b881ab72847e2df" } dependencies = [ - { name = "dnspython", marker = "python_full_version < '4.0'" }, - { name = "idna", marker = "python_full_version < '4.0'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, + { name = "handlebars" }, ] [[package]] name = "executing" -version = "2.1.0" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485 } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] [[package]] @@ -550,6 +633,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924 }, ] +[[package]] +name = "flow-sample1" +version = "0.1.0" +source = { editable = "samples/flow-sample1" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + [[package]] name = "fqdn" version = "1.5.1" @@ -573,7 +683,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "dotprompt", editable = "packages/dotprompt" }, + { name = "dotprompt" }, { name = "opentelemetry-api", specifier = ">=1.29.0" }, { name = "opentelemetry-sdk", specifier = ">=1.29.0" }, { name = "pydantic", specifier = ">=2.10.5" }, @@ -630,10 +740,14 @@ version = "0.1.0" source = { editable = "plugins/ollama" } dependencies = [ { name = "genkit" }, + { name = "ollama" }, ] [package.metadata] -requires-dist = [{ name = "genkit", editable = "packages/genkit" }] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "ollama", specifier = "~=0.4" }, +] [[package]] name = "genkit-pinecone-plugin" @@ -653,12 +767,14 @@ source = { editable = "plugins/vertex-ai" } dependencies = [ { name = "genkit" }, { name = "google-cloud-aiplatform" }, + { name = "pytest-mock" }, ] [package.metadata] requires-dist = [ { name = "genkit", editable = "packages/genkit" }, { name = "google-cloud-aiplatform", specifier = ">=1.77.0" }, + { name = "pytest-mock" }, ] [[package]] @@ -666,7 +782,7 @@ name = "genkit-workspace" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "dotprompt" }, + { name = "dotpromptz" }, { name = "genkit" }, { name = "genkit-chroma-plugin" }, { name = "genkit-firebase-plugin" }, @@ -675,7 +791,6 @@ dependencies = [ { name = "genkit-ollama-plugin" }, { name = "genkit-pinecone-plugin" }, { name = "genkit-vertex-ai-plugin" }, - { name = "handlebarz" }, ] [package.dev-dependencies] @@ -687,6 +802,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-watcher" }, ] lint = [ { name = "mypy" }, @@ -695,7 +811,7 @@ lint = [ [package.metadata] requires-dist = [ - { name = "dotprompt", editable = "packages/dotprompt" }, + { name = "dotpromptz", git = "https://github.com/google/dotprompt.git?subdirectory=python%2Fdotpromptz&rev=main" }, { name = "genkit", editable = "packages/genkit" }, { name = "genkit-chroma-plugin", editable = "plugins/chroma" }, { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, @@ -704,22 +820,22 @@ requires-dist = [ { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, - { name = "handlebarz", editable = "packages/handlebarz" }, ] [package.metadata.requires-dev] dev = [ { name = "bpython", specifier = ">=0.25" }, - { name = "datamodel-code-generator", specifier = ">=0.26.5" }, - { name = "ipython", specifier = ">=8.31.0" }, + { name = "datamodel-code-generator", specifier = ">=0.27.3" }, + { name = "ipython", specifier = ">=8.32.0" }, { name = "jupyter", specifier = ">=1.1.1" }, { name = "pytest", specifier = ">=8.3.4" }, - { name = "pytest-asyncio", specifier = ">=0.25.2" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-watcher", specifier = ">=0.4.3" }, ] lint = [ - { name = "mypy", specifier = ">=1.14.1" }, - { name = "ruff", specifier = ">=0.9.2" }, + { name = "mypy", specifier = ">=1.15" }, + { name = "ruff", specifier = ">=0.9" }, ] [[package]] @@ -733,7 +849,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.24.0" +version = "2.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -742,9 +858,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/56/d70d66ed1b5ab5f6c27bf80ec889585ad8f865ff32acbafd3b2ef0bfb5d0/google_api_core-2.24.0.tar.gz", hash = "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf", size = 162647 } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b7/481c83223d7b4f02c7651713fceca648fa3336e1571b9804713f66bca2d8/google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a", size = 163508 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/76/65b8b94e74bf1b6d1cc38d916089670c4da5029d25762441d8c5c19e51dd/google_api_core-2.24.0-py3-none-any.whl", hash = "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9", size = 158576 }, + { url = "https://files.pythonhosted.org/packages/b1/a6/8e30ddfd3d39ee6d2c76d3d4f64a83f77ac86a4cab67b286ae35ce9e4369/google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1", size = 160059 }, ] [package.optional-dependencies] @@ -755,21 +871,21 @@ grpc = [ [[package]] name = "google-auth" -version = "2.37.0" +version = "2.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/af/b25763b9d35dfc2c6f9c3ec34d8d3f1ba760af3a7b7e8d5c5f0579522c45/google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00", size = 268878 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/8d/4d5d5f9f500499f7bd4c93903b43e8d6976f3fc6f064637ded1a85d09b07/google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0", size = 209829 }, + { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, ] [[package]] name = "google-cloud-aiplatform" -version = "1.77.0" +version = "1.81.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -785,9 +901,9 @@ dependencies = [ { name = "shapely" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/45/7ffd099ff7554d9f4f3665611afb44d3ea59f8a3dd071e4284381d0ac3c1/google_cloud_aiplatform-1.77.0.tar.gz", hash = "sha256:1e5b77fe6c7f276d7aae65bcf08a273122a71f6c4af1f43cf45821f603a74080", size = 8287282 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/9b/1a2012018f9a47c6057f45190a85f4053847511eea7d14592d42c942c16e/google_cloud_aiplatform-1.81.0.tar.gz", hash = "sha256:1398be33bfc2725dde47555e559b89e8cb3b2d676a47a9802d9f33a89f1630bf", size = 8702664 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/b6/f7a3c8bdb08a3636d216c49768eff3369b5475edd71f6dbe590a942252b9/google_cloud_aiplatform-1.77.0-py2.py3-none-any.whl", hash = "sha256:e9dd1bcb1b9a85eddd452916cd6ad1d9ce2d487772a9e45b1814aa0ac5633689", size = 6939280 }, + { url = "https://files.pythonhosted.org/packages/18/74/59afe8abef2610f3e882ab8721f4c93fbc0365cadc4837d90e5344326318/google_cloud_aiplatform-1.81.0-py2.py3-none-any.whl", hash = "sha256:e4b6745dfd1f6215d690e9589239d2e7ae2553e39bf9c24c7b7581af0f2f6a68", size = 7266935 }, ] [[package]] @@ -810,20 +926,20 @@ wheels = [ [[package]] name = "google-cloud-core" -version = "2.4.1" +version = "2.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/1f/9d1e0ba6919668608570418a9a51e47070ac15aeff64261fb092d8be94c0/google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073", size = 35587 } +sdist = { url = "https://files.pythonhosted.org/packages/8d/96/16cc0a34f75899ace6a42bb4ef242ac4aa263089b018d1c18c007d1fd8f2/google_cloud_core-2.4.2.tar.gz", hash = "sha256:a4fcb0e2fcfd4bfe963837fad6d10943754fd79c1a50097d68540b6eb3d67f35", size = 35854 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/0f/2e2061e3fbcb9d535d5da3f58cc8de4947df1786fe6a1355960feb05a681/google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61", size = 29233 }, + { url = "https://files.pythonhosted.org/packages/9c/0f/76e813cee7568ac467d929f4f0da7ab349596e7fc4ee837b990611e07d99/google_cloud_core-2.4.2-py2.py3-none-any.whl", hash = "sha256:7459c3e83de7cb8b9ecfec9babc910efb4314030c56dd798eaad12c426f7d180", size = 29343 }, ] [[package]] name = "google-cloud-resource-manager" -version = "1.14.0" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -832,9 +948,9 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/74/db14f34283b325b775b3287cd72ce8c43688bdea26801d02017a2ccded08/google_cloud_resource_manager-1.14.0.tar.gz", hash = "sha256:daa70a3a4704759d31f812ed221e3b6f7b660af30c7862e4a0060ea91291db30", size = 430148 } +sdist = { url = "https://files.pythonhosted.org/packages/76/9d/da2e07d064926fc0d84c5f179006148cfa6fcffe6fd7aabdbf86dd20c46c/google_cloud_resource_manager-1.14.1.tar.gz", hash = "sha256:41e9e546aaa03d5160cdfa2341dbe81ef7596706c300a89b94c429f1f3411f87", size = 443094 } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/c4/2275ca35419f9a2ae66846f389490b356856bf55a9ad9f95a88399a89294/google_cloud_resource_manager-1.14.0-py2.py3-none-any.whl", hash = "sha256:4860c3ea9ace760b317ea90d4e27f1b32e54ededdcc340a7cb70c8ef238d8f7c", size = 384138 }, + { url = "https://files.pythonhosted.org/packages/47/be/ffdba56168f7e3778cd002a35fc0e94c608f088f6df24d2b980538389d71/google_cloud_resource_manager-1.14.1-py2.py3-none-any.whl", hash = "sha256:68340599f85ebf07a6e18487e460ea07cc15e132068f6b188786d01c2cf25518", size = 392325 }, ] [[package]] @@ -881,14 +997,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.66.0" +version = "1.68.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/a7/8e9cccdb1c49870de6faea2a2764fa23f627dd290633103540209f03524c/googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c", size = 114376 } +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/c08f0d9f94b45faca68e355771329cba2411c777c8713924dd1baee0e09c/googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c", size = 57367 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/0f/c0713fb2b3d28af4b2fded3291df1c4d4f79a00d15c2374a9e010870016c/googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed", size = 221682 }, + { url = "https://files.pythonhosted.org/packages/3f/85/c99a157ee99d67cc6c9ad123abb8b1bfb476fab32d2f3511c59314548e4f/googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac", size = 164985 }, ] [package.optional-dependencies] @@ -945,42 +1061,42 @@ wheels = [ [[package]] name = "grpcio" -version = "1.69.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/87/06a145284cbe86c91ca517fe6b57be5efbb733c0d6374b407f0992054d18/grpcio-1.69.0.tar.gz", hash = "sha256:936fa44241b5379c5afc344e1260d467bee495747eaf478de825bab2791da6f5", size = 12738244 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/1d/8f28f147d7f3f5d6b6082f14e1e0f40d58e50bc2bd30d2377c730c57a286/grpcio-1.69.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fc18a4de8c33491ad6f70022af5c460b39611e39578a4d84de0fe92f12d5d47b", size = 5161414 }, - { url = "https://files.pythonhosted.org/packages/35/4b/9ab8ea65e515e1844feced1ef9e7a5d8359c48d986c93f3d2a2006fbdb63/grpcio-1.69.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:0f0270bd9ffbff6961fe1da487bdcd594407ad390cc7960e738725d4807b18c4", size = 11108909 }, - { url = "https://files.pythonhosted.org/packages/99/68/1856fde2b3c3162bdfb9845978608deef3606e6907fdc2c87443fce6ecd0/grpcio-1.69.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc48f99cc05e0698e689b51a05933253c69a8c8559a47f605cff83801b03af0e", size = 5658302 }, - { url = "https://files.pythonhosted.org/packages/3e/21/3fa78d38dc5080d0d677103fad3a8cd55091635cc2069a7c06c7a54e6c4d/grpcio-1.69.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e925954b18d41aeb5ae250262116d0970893b38232689c4240024e4333ac084", size = 6306201 }, - { url = "https://files.pythonhosted.org/packages/f3/cb/5c47b82fd1baf43dba973ae399095d51aaf0085ab0439838b4cbb1e87e3c/grpcio-1.69.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d222569273720366f68a99cb62e6194681eb763ee1d3b1005840678d4884f9", size = 5919649 }, - { url = "https://files.pythonhosted.org/packages/c6/67/59d1a56a0f9508a29ea03e1ce800bdfacc1f32b4f6b15274b2e057bf8758/grpcio-1.69.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b62b0f41e6e01a3e5082000b612064c87c93a49b05f7602fe1b7aa9fd5171a1d", size = 6648974 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/ca70c14d98c6400095f19a0f4df8273d09c2106189751b564b26019f1dbe/grpcio-1.69.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:db6f9fd2578dbe37db4b2994c94a1d9c93552ed77dca80e1657bb8a05b898b55", size = 6215144 }, - { url = "https://files.pythonhosted.org/packages/b3/94/b2b0a9fd487fc8262e20e6dd0ec90d9fa462c82a43b4855285620f6e9d01/grpcio-1.69.0-cp312-cp312-win32.whl", hash = "sha256:b192b81076073ed46f4b4dd612b8897d9a1e39d4eabd822e5da7b38497ed77e1", size = 3644552 }, - { url = "https://files.pythonhosted.org/packages/93/99/81aec9f85412e3255a591ae2ccb799238e074be774e5f741abae08a23418/grpcio-1.69.0-cp312-cp312-win_amd64.whl", hash = "sha256:1227ff7836f7b3a4ab04e5754f1d001fa52a730685d3dc894ed8bc262cc96c01", size = 4399532 }, - { url = "https://files.pythonhosted.org/packages/54/47/3ff4501365f56b7cc16617695dbd4fd838c5e362bc7fa9fee09d592f7d78/grpcio-1.69.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:a78a06911d4081a24a1761d16215a08e9b6d4d29cdbb7e427e6c7e17b06bcc5d", size = 5162928 }, - { url = "https://files.pythonhosted.org/packages/c0/63/437174c5fa951052c9ecc5f373f62af6f3baf25f3f5ef35cbf561806b371/grpcio-1.69.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:dc5a351927d605b2721cbb46158e431dd49ce66ffbacb03e709dc07a491dde35", size = 11103027 }, - { url = "https://files.pythonhosted.org/packages/53/df/53566a6fdc26b6d1f0585896e1cc4825961039bca5a6a314ff29d79b5d5b/grpcio-1.69.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:3629d8a8185f5139869a6a17865d03113a260e311e78fbe313f1a71603617589", size = 5659277 }, - { url = "https://files.pythonhosted.org/packages/e6/4c/b8a0c4f71498b6f9be5ca6d290d576cf2af9d95fd9827c47364f023969ad/grpcio-1.69.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9a281878feeb9ae26db0622a19add03922a028d4db684658f16d546601a4870", size = 6305255 }, - { url = "https://files.pythonhosted.org/packages/ef/55/d9aa05eb3dfcf6aa946aaf986740ec07fc5189f20e2cbeb8c5d278ffd00f/grpcio-1.69.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc614e895177ab7e4b70f154d1a7c97e152577ea101d76026d132b7aaba003b", size = 5920240 }, - { url = "https://files.pythonhosted.org/packages/ea/eb/774b27c51e3e386dfe6c491a710f6f87ffdb20d88ec6c3581e047d9354a2/grpcio-1.69.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1ee76cd7e2e49cf9264f6812d8c9ac1b85dda0eaea063af07292400f9191750e", size = 6652974 }, - { url = "https://files.pythonhosted.org/packages/59/98/96de14e6e7d89123813d58c246d9b0f1fbd24f9277f5295264e60861d9d6/grpcio-1.69.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0470fa911c503af59ec8bc4c82b371ee4303ececbbdc055f55ce48e38b20fd67", size = 6215757 }, - { url = "https://files.pythonhosted.org/packages/7d/5b/ce922e0785910b10756fabc51fd294260384a44bea41651dadc4e47ddc82/grpcio-1.69.0-cp313-cp313-win32.whl", hash = "sha256:b650f34aceac8b2d08a4c8d7dc3e8a593f4d9e26d86751ebf74ebf5107d927de", size = 3642488 }, - { url = "https://files.pythonhosted.org/packages/5d/04/11329e6ca1ceeb276df2d9c316b5e170835a687a4d0f778dba8294657e36/grpcio-1.69.0-cp313-cp313-win_amd64.whl", hash = "sha256:028337786f11fecb5d7b7fa660475a06aabf7e5e52b5ac2df47414878c0ce7ea", size = 4399968 }, +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/a4/ddbda79dd176211b518f0f3795af78b38727a31ad32bc149d6a7b910a731/grpcio-1.70.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:ef4c14508299b1406c32bdbb9fb7b47612ab979b04cf2b27686ea31882387cff", size = 5198135 }, + { url = "https://files.pythonhosted.org/packages/30/5c/60eb8a063ea4cb8d7670af8fac3f2033230fc4b75f62669d67c66ac4e4b0/grpcio-1.70.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:aa47688a65643afd8b166928a1da6247d3f46a2784d301e48ca1cc394d2ffb40", size = 11447529 }, + { url = "https://files.pythonhosted.org/packages/fb/b9/1bf8ab66729f13b44e8f42c9de56417d3ee6ab2929591cfee78dce749b57/grpcio-1.70.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:880bfb43b1bb8905701b926274eafce5c70a105bc6b99e25f62e98ad59cb278e", size = 5664484 }, + { url = "https://files.pythonhosted.org/packages/d1/06/2f377d6906289bee066d96e9bdb91e5e96d605d173df9bb9856095cccb57/grpcio-1.70.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e654c4b17d07eab259d392e12b149c3a134ec52b11ecdc6a515b39aceeec898", size = 6303739 }, + { url = "https://files.pythonhosted.org/packages/ae/50/64c94cfc4db8d9ed07da71427a936b5a2bd2b27c66269b42fbda82c7c7a4/grpcio-1.70.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2394e3381071045a706ee2eeb6e08962dd87e8999b90ac15c55f56fa5a8c9597", size = 5910417 }, + { url = "https://files.pythonhosted.org/packages/53/89/8795dfc3db4389c15554eb1765e14cba8b4c88cc80ff828d02f5572965af/grpcio-1.70.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b3c76701428d2df01964bc6479422f20e62fcbc0a37d82ebd58050b86926ef8c", size = 6626797 }, + { url = "https://files.pythonhosted.org/packages/9c/b2/6a97ac91042a2c59d18244c479ee3894e7fb6f8c3a90619bb5a7757fa30c/grpcio-1.70.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac073fe1c4cd856ebcf49e9ed6240f4f84d7a4e6ee95baa5d66ea05d3dd0df7f", size = 6190055 }, + { url = "https://files.pythonhosted.org/packages/86/2b/28db55c8c4d156053a8c6f4683e559cd0a6636f55a860f87afba1ac49a51/grpcio-1.70.0-cp312-cp312-win32.whl", hash = "sha256:cd24d2d9d380fbbee7a5ac86afe9787813f285e684b0271599f95a51bce33528", size = 3600214 }, + { url = "https://files.pythonhosted.org/packages/17/c3/a7a225645a965029ed432e5b5e9ed959a574e62100afab553eef58be0e37/grpcio-1.70.0-cp312-cp312-win_amd64.whl", hash = "sha256:0495c86a55a04a874c7627fd33e5beaee771917d92c0e6d9d797628ac40e7655", size = 4292538 }, + { url = "https://files.pythonhosted.org/packages/68/38/66d0f32f88feaf7d83f8559cd87d899c970f91b1b8a8819b58226de0a496/grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a", size = 5199218 }, + { url = "https://files.pythonhosted.org/packages/c1/96/947df763a0b18efb5cc6c2ae348e56d97ca520dc5300c01617b234410173/grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429", size = 11445983 }, + { url = "https://files.pythonhosted.org/packages/fd/5b/f3d4b063e51b2454bedb828e41f3485800889a3609c49e60f2296cc8b8e5/grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9", size = 5663954 }, + { url = "https://files.pythonhosted.org/packages/bd/0b/dab54365fcedf63e9f358c1431885478e77d6f190d65668936b12dd38057/grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c", size = 6304323 }, + { url = "https://files.pythonhosted.org/packages/76/a8/8f965a7171ddd336ce32946e22954aa1bbc6f23f095e15dadaa70604ba20/grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f", size = 5910939 }, + { url = "https://files.pythonhosted.org/packages/1b/05/0bbf68be8b17d1ed6f178435a3c0c12e665a1e6054470a64ce3cb7896596/grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0", size = 6631405 }, + { url = "https://files.pythonhosted.org/packages/79/6a/5df64b6df405a1ed1482cb6c10044b06ec47fd28e87c2232dbcf435ecb33/grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40", size = 6190982 }, + { url = "https://files.pythonhosted.org/packages/42/aa/aeaac87737e6d25d1048c53b8ec408c056d3ed0c922e7c5efad65384250c/grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce", size = 3598359 }, + { url = "https://files.pythonhosted.org/packages/1f/79/8edd2442d2de1431b4a3de84ef91c37002f12de0f9b577fb07b452989dbc/grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68", size = 4293938 }, ] [[package]] name = "grpcio-status" -version = "1.69.0" +version = "1.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/35/52dc0d8300f879dbf9cdc95764cee9f56d5a212998cfa1a8871b262df2a4/grpcio_status-1.69.0.tar.gz", hash = "sha256:595ef84e5178d6281caa732ccf68ff83259241608d26b0e9c40a5e66eee2a2d2", size = 13662 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/d1/2397797c810020eac424e1aac10fbdc5edb6b9b4ad6617e0ed53ca907653/grpcio_status-1.70.0.tar.gz", hash = "sha256:0e7b42816512433b18b9d764285ff029bde059e9d41f8fe10a60631bd8348101", size = 13681 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/e2/346a766a4232f74f45f8bc70e636fc3a6677e6bc3893382187829085f12e/grpcio_status-1.69.0-py3-none-any.whl", hash = "sha256:d6b2a3c9562c03a817c628d7ba9a925e209c228762d6d7677ae5c9401a542853", size = 14428 }, + { url = "https://files.pythonhosted.org/packages/e6/34/49e558040e069feebac70cdd1b605f38738c0277ac5d38e2ce3d03e1b1ec/grpcio_status-1.70.0-py3-none-any.whl", hash = "sha256:fc5a2ae2b9b1c1969cc49f3262676e6854aa2398ec69cb5bd6c47cd501904a85", size = 14429 }, ] [[package]] @@ -993,9 +1109,16 @@ wheels = [ ] [[package]] -name = "handlebarz" -version = "0.1.0" -source = { editable = "packages/handlebarz" } +name = "handlebars" +version = "4.7.8.post20241027" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "js2py-wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/c096425ab6782dabbf9a7171db0a75565bdd5249d30cbc7e9972aebc7043/handlebars-4.7.8.post20241027.tar.gz", hash = "sha256:2e6e341b95d429b589fe9df881dedd34ac558c91b5c3412433968f85a289baff", size = 69704 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/05/bf995eda7d8a0d2f3b6ea249ffdd653ce8ce21e86914d9eeab691ab51e39/handlebars-4.7.8.post20241027-py3-none-any.whl", hash = "sha256:5cfa16cb0d6b3eefea7ed4909c603cfb06d69955141e523053b180821e925d91", size = 65981 }, +] [[package]] name = "hello" @@ -1061,6 +1184,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "imagen" +version = "0.1.0" +source = { editable = "samples/vetex-ai-imagen" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + [[package]] name = "importlib-metadata" version = "8.5.0" @@ -1117,7 +1267,7 @@ wheels = [ [[package]] name = "ipython" -version = "8.31.0" +version = "8.32.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1130,9 +1280,9 @@ dependencies = [ { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/35/6f90fdddff7a08b7b715fccbd2427b5212c9525cd043d26fdc45bee0708d/ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b", size = 5501011 } +sdist = { url = "https://files.pythonhosted.org/packages/36/80/4d2a072e0db7d250f134bc11676517299264ebe16d62a8619d49a78ced73/ipython-8.32.0.tar.gz", hash = "sha256:be2c91895b0b9ea7ba49d33b23e2040c352b33eb6a519cca7ce6e0c743444251", size = 5507441 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/60/d0feb6b6d9fe4ab89fe8fe5b47cbf6cd936bfd9f1e7ffa9d0015425aeed6/ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6", size = 821583 }, + { url = "https://files.pythonhosted.org/packages/e7/e1/f4474a7ecdb7745a820f6f6039dc43c66add40f1bcc66485607d93571af6/ipython-8.32.0-py3-none-any.whl", hash = "sha256:cae85b0c61eff1fc48b0a8002de5958b6528fa9c8defb1894da63f42613708aa", size = 825524 }, ] [[package]] @@ -1165,11 +1315,11 @@ wheels = [ [[package]] name = "isort" -version = "5.13.2" +version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/28/b382d1656ac0ee4cef4bf579b13f9c6c813bff8a5cb5996669592c8c75fa/isort-6.0.0.tar.gz", hash = "sha256:75d9d8a1438a9432a7d7b54f2d3b45cad9a4a0fdba43617d9873379704a8bdf1", size = 828356 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310 }, + { url = "https://files.pythonhosted.org/packages/76/c7/d6017f09ae5b1206fbe531f7af3b6dac1f67aedcbd2e79f3b386c27955d6/isort-6.0.0-py3-none-any.whl", hash = "sha256:567954102bb47bb12e0fae62606570faacddd441e45683968c8d1734fb1af892", size = 94053 }, ] [[package]] @@ -1208,6 +1358,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085 }, ] +[[package]] +name = "js2py-wheel" +version = "0.74" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjsparser-wheel" }, + { name = "six" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/59/e080d8eb641f2ceea89677ab0c56d7a2a0858a0c4bb95813bad712b8d3e3/js2py_wheel-0.74.tar.gz", hash = "sha256:8670d73abf22b211173cfe70cf92f94aa36be61f43d44c65131f1d20bb26be04", size = 567467 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/58/9485878e03c64247fa37da9380acc6019be7f63059052d1f19b3699a5a6e/Js2Py_wheel-0.74-py3-none-any.whl", hash = "sha256:c5b4af26ce0a16c26190031ddafb495a998b35c46bf5959c7feb72f581d23ca7", size = 608306 }, +] + [[package]] name = "json5" version = "0.10.0" @@ -1333,10 +1497,11 @@ wheels = [ [[package]] name = "jupyter-events" -version = "0.11.0" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, { name = "python-json-logger" }, { name = "pyyaml" }, { name = "referencing" }, @@ -1344,9 +1509,9 @@ dependencies = [ { name = "rfc3986-validator" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/65/5791c8a979b5646ca29ea50e42b6708908b789f7ff389d1a03c1b93a1c54/jupyter_events-0.11.0.tar.gz", hash = "sha256:c0bc56a37aac29c1fbc3bcfbddb8c8c49533f9cf11f1c4e6adadba936574ab90", size = 62039 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/8c/9b65cb2cd4ea32d885993d5542244641590530836802a2e8c7449a4c61c9/jupyter_events-0.11.0-py3-none-any.whl", hash = "sha256:36399b41ce1ca45fe8b8271067d6a140ffa54cec4028e95491c93b78a855cacf", size = 19423 }, + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430 }, ] [[package]] @@ -1406,7 +1571,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.3.4" +version = "4.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1423,9 +1588,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/45/1052f842e066902b1d78126df7e2269b1b9408991e1344e167b2e429f9e1/jupyterlab-4.3.4.tar.gz", hash = "sha256:f0bb9b09a04766e3423cccc2fc23169aa2ffedcdf8713e9e0fb33cac0b6859d0", size = 21797583 } +sdist = { url = "https://files.pythonhosted.org/packages/19/17/6f3d73c3e54b71bbaf03edcc4a54b0aa6328e0a134755f297ea87d425711/jupyterlab-4.3.5.tar.gz", hash = "sha256:c779bf72ced007d7d29d5bcef128e7fdda96ea69299e19b04a43635a7d641f9d", size = 21800023 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/48/af57263e53cfc220e522de047aa0993f53bab734fe812af1e03e33ac6d7c/jupyterlab-4.3.4-py3-none-any.whl", hash = "sha256:b754c2601c5be6adf87cb5a1d8495d653ffb945f021939f77776acaa94dae952", size = 11665373 }, + { url = "https://files.pythonhosted.org/packages/73/6f/94d4c879b3e2b7b9bca1913ea6fbbef180f8b1ed065b46ade40d651ec54d/jupyterlab-4.3.5-py3-none-any.whl", hash = "sha256:571bbdee20e4c5321ab5195bc41cf92a75a5cff886be5e57ce78dfa37a5e9fdb", size = 11666944 }, ] [[package]] @@ -1514,38 +1679,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, ] +[[package]] +name = "menu" +version = "0.1.0" +source = { editable = "samples/menu" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + [[package]] name = "mistune" -version = "3.1.0" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/6e/96fc7cb3288666c5de2c396eb0e338dc95f7a8e4920e43e38783a22d0084/mistune-3.1.0.tar.gz", hash = "sha256:dbcac2f78292b9dc066cd03b7a3a26b62d85f8159f2ea5fd28e55df79908d667", size = 94401 } +sdist = { url = "https://files.pythonhosted.org/packages/80/f7/f6d06304c61c2a73213c0a4815280f70d985429cda26272f490e42119c1a/mistune-3.1.2.tar.gz", hash = "sha256:733bf018ba007e8b5f2d3a9eb624034f6ee26c4ea769a98ec533ee111d504dff", size = 94613 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/b3/743ffc3f59da380da504d84ccd1faf9a857a1445991ff19bf2ec754163c2/mistune-3.1.0-py3-none-any.whl", hash = "sha256:b05198cf6d671b3deba6c87ec6cf0d4eb7b72c524636eddb6dbf13823b52cee1", size = 53694 }, + { url = "https://files.pythonhosted.org/packages/12/92/30b4e54c4d7c48c06db61595cffbbf4f19588ea177896f9b78f0fbe021fd/mistune-3.1.2-py3-none-any.whl", hash = "sha256:4b47731332315cdca99e0ded46fc0004001c1299ff773dfb48fbe1fd226de319", size = 53696 }, ] [[package]] name = "mypy" -version = "1.14.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, ] [[package]] @@ -1574,7 +1766,7 @@ wheels = [ [[package]] name = "nbconvert" -version = "7.16.5" +version = "7.16.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -1592,9 +1784,9 @@ dependencies = [ { name = "pygments" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/2c/d026c0367f2be2463d4c2f5b538e28add2bc67bc13730abb7f364ae4eb8b/nbconvert-7.16.5.tar.gz", hash = "sha256:c83467bb5777fdfaac5ebbb8e864f300b277f68692ecc04d6dab72f2d8442344", size = 856367 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/9e/2dcc9fe00cf55d95a8deae69384e9cea61816126e345754f6c75494d32ec/nbconvert-7.16.5-py3-none-any.whl", hash = "sha256:e12eac052d6fd03040af4166c563d76e7aeead2e9aadf5356db552a1784bd547", size = 258061 }, + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525 }, ] [[package]] @@ -1651,80 +1843,116 @@ wheels = [ [[package]] name = "numpy" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/d0/c12ddfd3a02274be06ffc71f3efc6d0e457b0409c4481596881e748cb264/numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f", size = 20233295 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/e6/847d15770ab7a01e807bdfcd4ead5bdae57c0092b7dc83878171b6af97bb/numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467", size = 20912636 }, - { url = "https://files.pythonhosted.org/packages/d1/af/f83580891577b13bd7e261416120e036d0d8fb508c8a43a73e38928b794b/numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a", size = 14098403 }, - { url = "https://files.pythonhosted.org/packages/2b/86/d019fb60a9d0f1d4cf04b014fe88a9135090adfadcc31c1fadbb071d7fa7/numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825", size = 5128938 }, - { url = "https://files.pythonhosted.org/packages/7a/1b/50985edb6f1ec495a1c36452e860476f5b7ecdc3fc59ea89ccad3c4926c5/numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37", size = 6661937 }, - { url = "https://files.pythonhosted.org/packages/f4/1b/17efd94cad1b9d605c3f8907fb06bcffc4ce4d1d14d46b95316cccccf2b9/numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748", size = 14049518 }, - { url = "https://files.pythonhosted.org/packages/5b/73/65d2f0b698df1731e851e3295eb29a5ab8aa06f763f7e4188647a809578d/numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0", size = 16099146 }, - { url = "https://files.pythonhosted.org/packages/d5/69/308f55c0e19d4b5057b5df286c5433822e3c8039ede06d4051d96f1c2c4e/numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278", size = 15246336 }, - { url = "https://files.pythonhosted.org/packages/f0/d8/d8d333ad0d8518d077a21aeea7b7c826eff766a2b1ce1194dea95ca0bacf/numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba", size = 17863507 }, - { url = "https://files.pythonhosted.org/packages/82/6e/0b84ad3103ffc16d6673e63b5acbe7901b2af96c2837174c6318c98e27ab/numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283", size = 6276491 }, - { url = "https://files.pythonhosted.org/packages/fc/84/7f801a42a67b9772a883223a0a1e12069a14626c81a732bd70aac57aebc1/numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb", size = 12616372 }, - { url = "https://files.pythonhosted.org/packages/e1/fe/df5624001f4f5c3e0b78e9017bfab7fdc18a8d3b3d3161da3d64924dd659/numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc", size = 20899188 }, - { url = "https://files.pythonhosted.org/packages/a9/80/d349c3b5ed66bd3cb0214be60c27e32b90a506946857b866838adbe84040/numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369", size = 14113972 }, - { url = "https://files.pythonhosted.org/packages/9d/50/949ec9cbb28c4b751edfa64503f0913cbfa8d795b4a251e7980f13a8a655/numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd", size = 5114294 }, - { url = "https://files.pythonhosted.org/packages/8d/f3/399c15629d5a0c68ef2aa7621d430b2be22034f01dd7f3c65a9c9666c445/numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be", size = 6648426 }, - { url = "https://files.pythonhosted.org/packages/2c/03/c72474c13772e30e1bc2e558cdffd9123c7872b731263d5648b5c49dd459/numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84", size = 14045990 }, - { url = "https://files.pythonhosted.org/packages/83/9c/96a9ab62274ffafb023f8ee08c88d3d31ee74ca58869f859db6845494fa6/numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff", size = 16096614 }, - { url = "https://files.pythonhosted.org/packages/d5/34/cd0a735534c29bec7093544b3a509febc9b0df77718a9b41ffb0809c9f46/numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0", size = 15242123 }, - { url = "https://files.pythonhosted.org/packages/5e/6d/541717a554a8f56fa75e91886d9b79ade2e595918690eb5d0d3dbd3accb9/numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de", size = 17859160 }, - { url = "https://files.pythonhosted.org/packages/b9/a5/fbf1f2b54adab31510728edd06a05c1b30839f37cf8c9747cb85831aaf1b/numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9", size = 6273337 }, - { url = "https://files.pythonhosted.org/packages/56/e5/01106b9291ef1d680f82bc47d0c5b5e26dfed15b0754928e8f856c82c881/numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369", size = 12609010 }, - { url = "https://files.pythonhosted.org/packages/9f/30/f23d9876de0f08dceb707c4dcf7f8dd7588266745029debb12a3cdd40be6/numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391", size = 20924451 }, - { url = "https://files.pythonhosted.org/packages/6a/ec/6ea85b2da9d5dfa1dbb4cb3c76587fc8ddcae580cb1262303ab21c0926c4/numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39", size = 14122390 }, - { url = "https://files.pythonhosted.org/packages/68/05/bfbdf490414a7dbaf65b10c78bc243f312c4553234b6d91c94eb7c4b53c2/numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317", size = 5156590 }, - { url = "https://files.pythonhosted.org/packages/f7/ec/fe2e91b2642b9d6544518388a441bcd65c904cea38d9ff998e2e8ebf808e/numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49", size = 6671958 }, - { url = "https://files.pythonhosted.org/packages/b1/6f/6531a78e182f194d33ee17e59d67d03d0d5a1ce7f6be7343787828d1bd4a/numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2", size = 14019950 }, - { url = "https://files.pythonhosted.org/packages/e1/fb/13c58591d0b6294a08cc40fcc6b9552d239d773d520858ae27f39997f2ae/numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7", size = 16079759 }, - { url = "https://files.pythonhosted.org/packages/2c/f2/f2f8edd62abb4b289f65a7f6d1f3650273af00b91b7267a2431be7f1aec6/numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb", size = 15226139 }, - { url = "https://files.pythonhosted.org/packages/aa/29/14a177f1a90b8ad8a592ca32124ac06af5eff32889874e53a308f850290f/numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648", size = 17856316 }, - { url = "https://files.pythonhosted.org/packages/95/03/242ae8d7b97f4e0e4ab8dd51231465fb23ed5e802680d629149722e3faf1/numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4", size = 6329134 }, - { url = "https://files.pythonhosted.org/packages/80/94/cd9e9b04012c015cb6320ab3bf43bc615e248dddfeb163728e800a5d96f0/numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576", size = 12696208 }, +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 }, + { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 }, + { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 }, + { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 }, + { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 }, + { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 }, + { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 }, + { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 }, + { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 }, + { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 }, + { url = "https://files.pythonhosted.org/packages/0e/8b/88b98ed534d6a03ba8cddb316950fe80842885709b58501233c29dfa24a9/numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba", size = 20916001 }, + { url = "https://files.pythonhosted.org/packages/d9/b4/def6ec32c725cc5fbd8bdf8af80f616acf075fe752d8a23e895da8c67b70/numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50", size = 14130721 }, + { url = "https://files.pythonhosted.org/packages/20/60/70af0acc86495b25b672d403e12cb25448d79a2b9658f4fc45e845c397a8/numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1", size = 5130999 }, + { url = "https://files.pythonhosted.org/packages/2e/69/d96c006fb73c9a47bcb3611417cf178049aae159afae47c48bd66df9c536/numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5", size = 6665299 }, + { url = "https://files.pythonhosted.org/packages/5a/3f/d8a877b6e48103733ac224ffa26b30887dc9944ff95dffdfa6c4ce3d7df3/numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2", size = 14064096 }, + { url = "https://files.pythonhosted.org/packages/e4/43/619c2c7a0665aafc80efca465ddb1f260287266bdbdce517396f2f145d49/numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1", size = 16114758 }, + { url = "https://files.pythonhosted.org/packages/d9/79/ee4fe4f60967ccd3897aa71ae14cdee9e3c097e3256975cc9575d393cb42/numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304", size = 15259880 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/8b55cf05db6d85b7a7d414b3d1bd5a740706df00bfa0824a08bf041e52ee/numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d", size = 17876721 }, + { url = "https://files.pythonhosted.org/packages/21/d6/b4c2f0564b7dcc413117b0ffbb818d837e4b29996b9234e38b2025ed24e7/numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693", size = 6290195 }, + { url = "https://files.pythonhosted.org/packages/97/e7/7d55a86719d0de7a6a597949f3febefb1009435b79ba510ff32f05a8c1d7/numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b", size = 12619013 }, + { url = "https://files.pythonhosted.org/packages/a6/1f/0b863d5528b9048fd486a56e0b97c18bf705e88736c8cea7239012119a54/numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890", size = 20944621 }, + { url = "https://files.pythonhosted.org/packages/aa/99/b478c384f7a0a2e0736177aafc97dc9152fc036a3fdb13f5a3ab225f1494/numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c", size = 14142502 }, + { url = "https://files.pythonhosted.org/packages/fb/61/2d9a694a0f9cd0a839501d362de2a18de75e3004576a3008e56bdd60fcdb/numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94", size = 5176293 }, + { url = "https://files.pythonhosted.org/packages/33/35/51e94011b23e753fa33f891f601e5c1c9a3d515448659b06df9d40c0aa6e/numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0", size = 6691874 }, + { url = "https://files.pythonhosted.org/packages/ff/cf/06e37619aad98a9d03bd8d65b8e3041c3a639be0f5f6b0a0e2da544538d4/numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610", size = 14036826 }, + { url = "https://files.pythonhosted.org/packages/0c/93/5d7d19955abd4d6099ef4a8ee006f9ce258166c38af259f9e5558a172e3e/numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76", size = 16096567 }, + { url = "https://files.pythonhosted.org/packages/af/53/d1c599acf7732d81f46a93621dab6aa8daad914b502a7a115b3f17288ab2/numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a", size = 15242514 }, + { url = "https://files.pythonhosted.org/packages/53/43/c0f5411c7b3ea90adf341d05ace762dad8cb9819ef26093e27b15dd121ac/numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf", size = 17872920 }, + { url = "https://files.pythonhosted.org/packages/5b/57/6dbdd45ab277aff62021cafa1e15f9644a52f5b5fc840bc7591b4079fb58/numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef", size = 6346584 }, + { url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 }, +] + +[[package]] +name = "ollama" +version = "0.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/6d/dc77539c735bbed5d0c873fb029fb86aa9f0163df169b34152914331c369/ollama-0.4.7.tar.gz", hash = "sha256:891dcbe54f55397d82d289c459de0ea897e103b86a3f1fad0fdb1895922a75ff", size = 12843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/83/c3ffac86906c10184c88c2e916460806b072a2cfe34cdcaf3a0c0e836d39/ollama-0.4.7-py3-none-any.whl", hash = "sha256:85505663cca67a83707be5fb3aeff0ea72e67846cea5985529d8eca4366564a1", size = 13210 }, +] + +[[package]] +name = "ollama-example" +version = "0.1.0" +source = { virtual = "samples/ollama" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "pydantic", specifier = ">=2.10.5" }, ] [[package]] name = "opentelemetry-api" -version = "1.29.0" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "importlib-metadata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/8e/b886a5e9861afa188d1fe671fb96ff9a1d90a23d57799331e137cc95d573/opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf", size = 62900 } +sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/53/5249ea860d417a26a3a6f1bdedfc0748c4f081a3adaec3d398bc0f7c6a71/opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8", size = 64304 }, + { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955 }, ] [[package]] name = "opentelemetry-sdk" -version = "1.29.0" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/5a/1ed4c3cf6c09f80565fc085f7e8efa0c222712fd2a9412d07424705dcf72/opentelemetry_sdk-1.29.0.tar.gz", hash = "sha256:b0787ce6aade6ab84315302e72bd7a7f2f014b0fb1b7c3295b88afe014ed0643", size = 157229 } +sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/1d/512b86af21795fb463726665e2f61db77d384e8779fdcf4cb0ceec47866d/opentelemetry_sdk-1.29.0-py3-none-any.whl", hash = "sha256:173be3b5d3f8f7d671f20ea37056710217959e774e2749d984355d1f9391a30a", size = 118078 }, + { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717 }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.50b0" +version = "0.51b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "opentelemetry-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/4e/d7c7c91ff47cd96fe4095dd7231701aec7347426fd66872ff320d6cd1fcc/opentelemetry_semantic_conventions-0.50b0.tar.gz", hash = "sha256:02dc6dbcb62f082de9b877ff19a3f1ffaa3c306300fa53bfac761c4567c83d38", size = 100459 } +sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/fb/dc15fad105450a015e913cfa4f5c27b6a5f1bea8fb649f8cae11e699c8af/opentelemetry_semantic_conventions-0.50b0-py3-none-any.whl", hash = "sha256:e87efba8fdb67fb38113efea6a349531e75ed7ffc01562f65b802fcecb5e115e", size = 166602 }, + { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416 }, ] [[package]] @@ -1811,6 +2039,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, ] +[[package]] +name = "prompt-file" +version = "0.1.0" +source = { editable = "samples/prompt-file" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.50" @@ -1825,14 +2080,14 @@ wheels = [ [[package]] name = "proto-plus" -version = "1.25.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/05/74417b2061e1bf1b82776037cad97094228fa1c1b6e82d08a78d3fb6ddb6/proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91", size = 56124 } +sdist = { url = "https://files.pythonhosted.org/packages/26/79/a5c6cbb42268cfd3ddc652dc526889044a8798c688a03ff58e5e92b743c8/proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22", size = 56136 } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/25/0b7cc838ae3d76d46539020ec39fc92bfc9acc29367e58fe912702c2a79e/proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961", size = 50126 }, + { url = "https://files.pythonhosted.org/packages/42/c3/59308ccc07b34980f9d532f7afc718a9f32b40e52cde7a740df8d55632fb/proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7", size = 50166 }, ] [[package]] @@ -1851,17 +2106,17 @@ wheels = [ [[package]] name = "psutil" -version = "6.1.1" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511 }, - { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985 }, - { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488 }, - { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477 }, - { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017 }, - { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602 }, - { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, ] [[package]] @@ -1914,21 +2169,16 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.5" +version = "2.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, -] - -[package.optional-dependencies] -email = [ - { name = "email-validator", marker = "python_full_version < '4.0'" }, + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, ] [[package]] @@ -1979,6 +2229,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] +[[package]] +name = "pyjsparser-wheel" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/a5/1ddb97c0e5c416d41f2391cfca773edc26e7632a286e6d49e14af0922c6d/pyjsparser_wheel-2.7.1.tar.gz", hash = "sha256:79e011ee4a2fc2e607e3a584e73a4f9c7091d23817513d19aa39b1c4e55e9e58", size = 25623 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/0b/bc29d3c773e3317384c7fe9362fd35d45628d7d62efce28974e49608e70f/pyjsparser_wheel-2.7.1-py3-none-any.whl", hash = "sha256:c249f326979de310b76fb0cf565ab572daa2eb3fb0544a52fbcc482cd4ef2d6c", size = 27071 }, +] + [[package]] name = "pytest" version = "8.3.4" @@ -1996,14 +2255,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "0.25.2" +version = "0.25.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/df/adcc0d60f1053d74717d21d58c0048479e9cab51464ce0d2965b086bd0e2/pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f", size = 53950 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/d8/defa05ae50dcd6019a95527200d3b3980043df5aa445d40cb0ef9f7f98ab/pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075", size = 19400 }, + { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, ] [[package]] @@ -2019,6 +2278,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, ] +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "pytest-watcher" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/72/a2a1e81f1b272ddd9a1848af4959c87c39aa95c0bbfb3007cacb86c47fa9/pytest_watcher-0.4.3.tar.gz", hash = "sha256:0cb0e4661648c8c0ff2b2d25efa5a8e421784b9e4c60fcecbf9b7c30b2d731b3", size = 10386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/3a/c44a76c6bb5e9e896d9707fb1c704a31a0136950dec9514373ced0684d56/pytest_watcher-0.4.3-py3-none-any.whl", hash = "sha256:d59b1e1396f33a65ea4949b713d6884637755d641646960056a90b267c3460f9", size = 11852 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2055,12 +2338,13 @@ wheels = [ [[package]] name = "pywinpty" -version = "2.0.14" +version = "2.0.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/82/90f8750423cba4b9b6c842df227609fb60704482d7abf6dd47e2babc055a/pywinpty-2.0.14.tar.gz", hash = "sha256:18bd9529e4a5daf2d9719aa17788ba6013e594ae94c5a0c27e83df3278b0660e", size = 27769 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/7c/917f9c4681bb8d34bfbe0b79d36bbcd902651aeab48790df3d30ba0202fb/pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2", size = 29017 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/79/759ae767a3b78d340446efd54dd1fe4f7dafa4fc7be96ed757e44bcdba54/pywinpty-2.0.14-cp312-none-win_amd64.whl", hash = "sha256:55dad362ef3e9408ade68fd173e4f9032b3ce08f68cfe7eacb2c263ea1179737", size = 1397207 }, - { url = "https://files.pythonhosted.org/packages/7d/34/b77b3c209bf2eaa6455390c8d5449241637f5957f41636a2204065d52bfa/pywinpty-2.0.14-cp313-none-win_amd64.whl", hash = "sha256:074fb988a56ec79ca90ed03a896d40707131897cefb8f76f926e3834227f2819", size = 1396698 }, + { url = "https://files.pythonhosted.org/packages/88/e5/9714def18c3a411809771a3fbcec70bffa764b9675afb00048a620fca604/pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc", size = 1405243 }, + { url = "https://files.pythonhosted.org/packages/fb/16/2ab7b3b7f55f3c6929e5f629e1a68362981e4e5fed592a2ed1cb4b4914a5/pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408", size = 1405020 }, + { url = "https://files.pythonhosted.org/packages/7c/16/edef3515dd2030db2795dbfbe392232c7a0f3dc41b98e92b38b42ba497c7/pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901", size = 1404151 }, ] [[package]] @@ -2100,60 +2384,87 @@ wheels = [ [[package]] name = "pyzmq" -version = "26.2.0" +version = "26.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", size = 1343105 }, - { url = "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", size = 1008365 }, - { url = "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", size = 665923 }, - { url = "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", size = 903400 }, - { url = "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", size = 860034 }, - { url = "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", size = 860579 }, - { url = "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", size = 1196246 }, - { url = "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", size = 1507441 }, - { url = "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", size = 1406498 }, - { url = "https://files.pythonhosted.org/packages/07/18/907134c85c7152f679ed744e73e645b365f3ad571f38bdb62e36f347699a/pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", size = 575533 }, - { url = "https://files.pythonhosted.org/packages/ce/2c/a6f4a20202a4d3c582ad93f95ee78d79bbdc26803495aec2912b17dbbb6c/pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", size = 637768 }, - { url = "https://files.pythonhosted.org/packages/5f/0e/eb16ff731632d30554bf5af4dbba3ffcd04518219d82028aea4ae1b02ca5/pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", size = 540675 }, - { url = "https://files.pythonhosted.org/packages/04/a7/0f7e2f6c126fe6e62dbae0bc93b1bd3f1099cf7fea47a5468defebe3f39d/pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", size = 1006564 }, - { url = "https://files.pythonhosted.org/packages/31/b6/a187165c852c5d49f826a690857684333a6a4a065af0a6015572d2284f6a/pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", size = 1340447 }, - { url = "https://files.pythonhosted.org/packages/68/ba/f4280c58ff71f321602a6e24fd19879b7e79793fb8ab14027027c0fb58ef/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", size = 665485 }, - { url = "https://files.pythonhosted.org/packages/77/b5/c987a5c53c7d8704216f29fc3d810b32f156bcea488a940e330e1bcbb88d/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", size = 903484 }, - { url = "https://files.pythonhosted.org/packages/29/c9/07da157d2db18c72a7eccef8e684cefc155b712a88e3d479d930aa9eceba/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", size = 859981 }, - { url = "https://files.pythonhosted.org/packages/43/09/e12501bd0b8394b7d02c41efd35c537a1988da67fc9c745cae9c6c776d31/pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", size = 860334 }, - { url = "https://files.pythonhosted.org/packages/eb/ff/f5ec1d455f8f7385cc0a8b2acd8c807d7fade875c14c44b85c1bddabae21/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", size = 1196179 }, - { url = "https://files.pythonhosted.org/packages/ec/8a/bb2ac43295b1950fe436a81fc5b298be0b96ac76fb029b514d3ed58f7b27/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", size = 1507668 }, - { url = "https://files.pythonhosted.org/packages/a9/49/dbc284ebcfd2dca23f6349227ff1616a7ee2c4a35fe0a5d6c3deff2b4fed/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", size = 1406539 }, - { url = "https://files.pythonhosted.org/packages/00/68/093cdce3fe31e30a341d8e52a1ad86392e13c57970d722c1f62a1d1a54b6/pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", size = 575567 }, - { url = "https://files.pythonhosted.org/packages/92/ae/6cc4657148143412b5819b05e362ae7dd09fb9fe76e2a539dcff3d0386bc/pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", size = 637551 }, - { url = "https://files.pythonhosted.org/packages/6c/67/fbff102e201688f97c8092e4c3445d1c1068c2f27bbd45a578df97ed5f94/pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", size = 540378 }, - { url = "https://files.pythonhosted.org/packages/3f/fe/2d998380b6e0122c6c4bdf9b6caf490831e5f5e2d08a203b5adff060c226/pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", size = 1007378 }, - { url = "https://files.pythonhosted.org/packages/4a/f4/30d6e7157f12b3a0390bde94d6a8567cdb88846ed068a6e17238a4ccf600/pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", size = 1329532 }, - { url = "https://files.pythonhosted.org/packages/82/86/3fe917870e15ee1c3ad48229a2a64458e36036e64b4afa9659045d82bfa8/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", size = 653242 }, - { url = "https://files.pythonhosted.org/packages/50/2d/242e7e6ef6c8c19e6cb52d095834508cd581ffb925699fd3c640cdc758f1/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", size = 888404 }, - { url = "https://files.pythonhosted.org/packages/ac/11/7270566e1f31e4ea73c81ec821a4b1688fd551009a3d2bab11ec66cb1e8f/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", size = 845858 }, - { url = "https://files.pythonhosted.org/packages/91/d5/72b38fbc69867795c8711bdd735312f9fef1e3d9204e2f63ab57085434b9/pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", size = 847375 }, - { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 }, - { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 }, - { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, +sdist = { url = "https://files.pythonhosted.org/packages/5a/e3/8d0382cb59feb111c252b54e8728257416a38ffcb2243c4e4775a3c990fe/pyzmq-26.2.1.tar.gz", hash = "sha256:17d72a74e5e9ff3829deb72897a175333d3ef5b5413948cae3cf7ebf0b02ecca", size = 278433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/b9/260a74786f162c7f521f5f891584a51d5a42fd15f5dcaa5c9226b2865fcc/pyzmq-26.2.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:a6549ecb0041dafa55b5932dcbb6c68293e0bd5980b5b99f5ebb05f9a3b8a8f3", size = 1348495 }, + { url = "https://files.pythonhosted.org/packages/bf/73/8a0757e4b68f5a8ccb90ddadbb76c6a5f880266cdb18be38c99bcdc17aaa/pyzmq-26.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0250c94561f388db51fd0213cdccbd0b9ef50fd3c57ce1ac937bf3034d92d72e", size = 945035 }, + { url = "https://files.pythonhosted.org/packages/cf/de/f02ec973cd33155bb772bae33ace774acc7cc71b87b25c4829068bec35de/pyzmq-26.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36ee4297d9e4b34b5dc1dd7ab5d5ea2cbba8511517ef44104d2915a917a56dc8", size = 671213 }, + { url = "https://files.pythonhosted.org/packages/d1/80/8fc583085f85ac91682744efc916888dd9f11f9f75a31aef1b78a5486c6c/pyzmq-26.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2a9cb17fd83b7a3a3009901aca828feaf20aa2451a8a487b035455a86549c09", size = 908750 }, + { url = "https://files.pythonhosted.org/packages/c3/25/0b4824596f261a3cc512ab152448b383047ff5f143a6906a36876415981c/pyzmq-26.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:786dd8a81b969c2081b31b17b326d3a499ddd1856e06d6d79ad41011a25148da", size = 865416 }, + { url = "https://files.pythonhosted.org/packages/a1/d1/6fda77a034d02034367b040973fd3861d945a5347e607bd2e98c99f20599/pyzmq-26.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2d88ba221a07fc2c5581565f1d0fe8038c15711ae79b80d9462e080a1ac30435", size = 865922 }, + { url = "https://files.pythonhosted.org/packages/ad/81/48f7fd8a71c427412e739ce576fc1ee14f3dc34527ca9b0076e471676183/pyzmq-26.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1c84c1297ff9f1cd2440da4d57237cb74be21fdfe7d01a10810acba04e79371a", size = 1201526 }, + { url = "https://files.pythonhosted.org/packages/c7/d8/818f15c6ef36b5450e435cbb0d3a51599fc884a5d2b27b46b9c00af68ef1/pyzmq-26.2.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46d4ebafc27081a7f73a0f151d0c38d4291656aa134344ec1f3d0199ebfbb6d4", size = 1512808 }, + { url = "https://files.pythonhosted.org/packages/d9/c4/b3edb7d0ae82ad6fb1a8cdb191a4113c427a01e85139906f3b655b07f4f8/pyzmq-26.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91e2bfb8e9a29f709d51b208dd5f441dc98eb412c8fe75c24ea464734ccdb48e", size = 1411836 }, + { url = "https://files.pythonhosted.org/packages/69/1c/151e3d42048f02cc5cd6dfc241d9d36b38375b4dee2e728acb5c353a6d52/pyzmq-26.2.1-cp312-cp312-win32.whl", hash = "sha256:4a98898fdce380c51cc3e38ebc9aa33ae1e078193f4dc641c047f88b8c690c9a", size = 581378 }, + { url = "https://files.pythonhosted.org/packages/b6/b9/d59a7462848aaab7277fddb253ae134a570520115d80afa85e952287e6bc/pyzmq-26.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0741edbd0adfe5f30bba6c5223b78c131b5aa4a00a223d631e5ef36e26e6d13", size = 643737 }, + { url = "https://files.pythonhosted.org/packages/55/09/f37e707937cce328944c1d57e5e50ab905011d35252a0745c4f7e5822a76/pyzmq-26.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:e5e33b1491555843ba98d5209439500556ef55b6ab635f3a01148545498355e5", size = 558303 }, + { url = "https://files.pythonhosted.org/packages/4f/2e/fa7a91ce349975971d6aa925b4c7e1a05abaae99b97ade5ace758160c43d/pyzmq-26.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:099b56ef464bc355b14381f13355542e452619abb4c1e57a534b15a106bf8e23", size = 942331 }, + { url = "https://files.pythonhosted.org/packages/64/2b/1f10b34b6dc7ff4b40f668ea25ba9b8093ce61d874c784b90229b367707b/pyzmq-26.2.1-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:651726f37fcbce9f8dd2a6dab0f024807929780621890a4dc0c75432636871be", size = 1345831 }, + { url = "https://files.pythonhosted.org/packages/4c/8d/34884cbd4a8ec050841b5fb58d37af136766a9f95b0b2634c2971deb09da/pyzmq-26.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57dd4d91b38fa4348e237a9388b4423b24ce9c1695bbd4ba5a3eada491e09399", size = 670773 }, + { url = "https://files.pythonhosted.org/packages/0f/f4/d4becfcf9e416ad2564f18a6653f7c6aa917da08df5c3760edb0baa1c863/pyzmq-26.2.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d51a7bfe01a48e1064131f3416a5439872c533d756396be2b39e3977b41430f9", size = 908836 }, + { url = "https://files.pythonhosted.org/packages/07/fa/ab105f1b86b85cb2e821239f1d0900fccd66192a91d97ee04661b5436b4d/pyzmq-26.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7154d228502e18f30f150b7ce94f0789d6b689f75261b623f0fdc1eec642aab", size = 865369 }, + { url = "https://files.pythonhosted.org/packages/c9/48/15d5f415504572dd4b92b52db5de7a5befc76bb75340ba9f36f71306a66d/pyzmq-26.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f1f31661a80cc46aba381bed475a9135b213ba23ca7ff6797251af31510920ce", size = 865676 }, + { url = "https://files.pythonhosted.org/packages/7e/35/2d91bcc7ccbb56043dd4d2c1763f24a8de5f05e06a134f767a7fb38e149c/pyzmq-26.2.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:290c96f479504439b6129a94cefd67a174b68ace8a8e3f551b2239a64cfa131a", size = 1201457 }, + { url = "https://files.pythonhosted.org/packages/6d/bb/aa7c5119307a5762b8dca6c9db73e3ab4bccf32b15d7c4f376271ff72b2b/pyzmq-26.2.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f2c307fbe86e18ab3c885b7e01de942145f539165c3360e2af0f094dd440acd9", size = 1513035 }, + { url = "https://files.pythonhosted.org/packages/4f/4c/527e6650c2fccec7750b783301329c8a8716d59423818afb67282304ce5a/pyzmq-26.2.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:b314268e716487bfb86fcd6f84ebbe3e5bec5fac75fdf42bc7d90fdb33f618ad", size = 1411881 }, + { url = "https://files.pythonhosted.org/packages/89/9f/e4412ea1b3e220acc21777a5edba8885856403d29c6999aaf00a9459eb03/pyzmq-26.2.1-cp313-cp313-win32.whl", hash = "sha256:edb550616f567cd5603b53bb52a5f842c0171b78852e6fc7e392b02c2a1504bb", size = 581354 }, + { url = "https://files.pythonhosted.org/packages/55/cd/f89dd3e9fc2da0d1619a82c4afb600c86b52bc72d7584953d460bc8d5027/pyzmq-26.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:100a826a029c8ef3d77a1d4c97cbd6e867057b5806a7276f2bac1179f893d3bf", size = 643560 }, + { url = "https://files.pythonhosted.org/packages/a7/99/5de4f8912860013f1116f818a0047659bc20d71d1bc1d48f874bdc2d7b9c/pyzmq-26.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:6991ee6c43e0480deb1b45d0c7c2bac124a6540cba7db4c36345e8e092da47ce", size = 558037 }, + { url = "https://files.pythonhosted.org/packages/06/0b/63b6d7a2f07a77dbc9768c6302ae2d7518bed0c6cee515669ca0d8ec743e/pyzmq-26.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:25e720dba5b3a3bb2ad0ad5d33440babd1b03438a7a5220511d0c8fa677e102e", size = 938580 }, + { url = "https://files.pythonhosted.org/packages/85/38/e5e2c3ffa23ea5f95f1c904014385a55902a11a67cd43c10edf61a653467/pyzmq-26.2.1-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:9ec6abfb701437142ce9544bd6a236addaf803a32628d2260eb3dbd9a60e2891", size = 1339670 }, + { url = "https://files.pythonhosted.org/packages/d2/87/da5519ed7f8b31e4beee8f57311ec02926822fe23a95120877354cd80144/pyzmq-26.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e1eb9d2bfdf5b4e21165b553a81b2c3bd5be06eeddcc4e08e9692156d21f1f6", size = 660983 }, + { url = "https://files.pythonhosted.org/packages/f6/e8/1ca6a2d59562e04d326a026c9e3f791a6f1a276ebde29da478843a566fdb/pyzmq-26.2.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90dc731d8e3e91bcd456aa7407d2eba7ac6f7860e89f3766baabb521f2c1de4a", size = 896509 }, + { url = "https://files.pythonhosted.org/packages/5c/e5/0b4688f7c74bea7e4f1e920da973fcd7d20175f4f1181cb9b692429c6bb9/pyzmq-26.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6a93d684278ad865fc0b9e89fe33f6ea72d36da0e842143891278ff7fd89c3", size = 853196 }, + { url = "https://files.pythonhosted.org/packages/8f/35/c17241da01195001828319e98517683dad0ac4df6fcba68763d61b630390/pyzmq-26.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c1bb37849e2294d519117dd99b613c5177934e5c04a5bb05dd573fa42026567e", size = 855133 }, + { url = "https://files.pythonhosted.org/packages/d2/14/268ee49bbecc3f72e225addeac7f0e2bd5808747b78c7bf7f87ed9f9d5a8/pyzmq-26.2.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:632a09c6d8af17b678d84df442e9c3ad8e4949c109e48a72f805b22506c4afa7", size = 1191612 }, + { url = "https://files.pythonhosted.org/packages/5e/02/6394498620b1b4349b95c534f3ebc3aef95f39afbdced5ed7ee315c49c14/pyzmq-26.2.1-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:fc409c18884eaf9ddde516d53af4f2db64a8bc7d81b1a0c274b8aa4e929958e8", size = 1500824 }, + { url = "https://files.pythonhosted.org/packages/17/fc/b79f0b72891cbb9917698add0fede71dfb64e83fa3481a02ed0e78c34be7/pyzmq-26.2.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:17f88622b848805d3f6427ce1ad5a2aa3cf61f12a97e684dab2979802024d460", size = 1399943 }, +] + +[[package]] +name = "rag" +version = "0.1.0" +source = { editable = "samples/rag" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, ] [[package]] name = "referencing" -version = "0.36.1" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/32/fd98246df7a0f309b58cae68b10b6b219ef2eb66747f00dfb34422687087/referencing-0.36.1.tar.gz", hash = "sha256:ca2e6492769e3602957e9b831b94211599d2aade9477f5d44110d2530cf9aade", size = 74661 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/fa/9f193ef0c9074b659009f06d7cbacc6f25b072044815bcf799b76533dbb8/referencing-0.36.1-py3-none-any.whl", hash = "sha256:363d9c65f080d0d70bc41c721dce3c7f3e77fc09f269cd5c8813da18069a6794", size = 26777 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, ] [[package]] @@ -2194,49 +2505,49 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.22.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/80/cce854d0921ff2f0a9fa831ba3ad3c65cee3a46711addf39a2af52df2cfd/rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", size = 26771 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/47/3383ee3bd787a2a5e65a9b9edc37ccf8505c0a00170e3a5e6ea5fbcd97f7/rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", size = 352334 }, - { url = "https://files.pythonhosted.org/packages/40/14/aa6400fa8158b90a5a250a77f2077c0d0cd8a76fce31d9f2b289f04c6dec/rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", size = 342111 }, - { url = "https://files.pythonhosted.org/packages/7d/06/395a13bfaa8a28b302fb433fb285a67ce0ea2004959a027aea8f9c52bad4/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", size = 384286 }, - { url = "https://files.pythonhosted.org/packages/43/52/d8eeaffab047e6b7b7ef7f00d5ead074a07973968ffa2d5820fa131d7852/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e", size = 391739 }, - { url = "https://files.pythonhosted.org/packages/83/31/52dc4bde85c60b63719610ed6f6d61877effdb5113a72007679b786377b8/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", size = 427306 }, - { url = "https://files.pythonhosted.org/packages/70/d5/1bab8e389c2261dba1764e9e793ed6830a63f830fdbec581a242c7c46bda/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", size = 442717 }, - { url = "https://files.pythonhosted.org/packages/82/a1/a45f3e30835b553379b3a56ea6c4eb622cf11e72008229af840e4596a8ea/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", size = 385721 }, - { url = "https://files.pythonhosted.org/packages/a6/27/780c942de3120bdd4d0e69583f9c96e179dfff082f6ecbb46b8d6488841f/rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", size = 415824 }, - { url = "https://files.pythonhosted.org/packages/94/0b/aa0542ca88ad20ea719b06520f925bae348ea5c1fdf201b7e7202d20871d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", size = 561227 }, - { url = "https://files.pythonhosted.org/packages/0d/92/3ed77d215f82c8f844d7f98929d56cc321bb0bcfaf8f166559b8ec56e5f1/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", size = 587424 }, - { url = "https://files.pythonhosted.org/packages/09/42/cacaeb047a22cab6241f107644f230e2935d4efecf6488859a7dd82fc47d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", size = 555953 }, - { url = "https://files.pythonhosted.org/packages/e6/52/c921dc6d5f5d45b212a456c1f5b17df1a471127e8037eb0972379e39dff4/rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", size = 221339 }, - { url = "https://files.pythonhosted.org/packages/f2/c7/f82b5be1e8456600395366f86104d1bd8d0faed3802ad511ef6d60c30d98/rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", size = 235786 }, - { url = "https://files.pythonhosted.org/packages/d0/bf/36d5cc1f2c609ae6e8bf0fc35949355ca9d8790eceb66e6385680c951e60/rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84", size = 351657 }, - { url = "https://files.pythonhosted.org/packages/24/2a/f1e0fa124e300c26ea9382e59b2d582cba71cedd340f32d1447f4f29fa4e/rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25", size = 341829 }, - { url = "https://files.pythonhosted.org/packages/cf/c2/0da1231dd16953845bed60d1a586fcd6b15ceaeb965f4d35cdc71f70f606/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4", size = 384220 }, - { url = "https://files.pythonhosted.org/packages/c7/73/a4407f4e3a00a9d4b68c532bf2d873d6b562854a8eaff8faa6133b3588ec/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5", size = 391009 }, - { url = "https://files.pythonhosted.org/packages/a9/c3/04b7353477ab360fe2563f5f0b176d2105982f97cd9ae80a9c5a18f1ae0f/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc", size = 426989 }, - { url = "https://files.pythonhosted.org/packages/8d/e6/e4b85b722bcf11398e17d59c0f6049d19cd606d35363221951e6d625fcb0/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b", size = 441544 }, - { url = "https://files.pythonhosted.org/packages/27/fc/403e65e56f65fff25f2973216974976d3f0a5c3f30e53758589b6dc9b79b/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518", size = 385179 }, - { url = "https://files.pythonhosted.org/packages/57/9b/2be9ff9700d664d51fd96b33d6595791c496d2778cb0b2a634f048437a55/rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd", size = 415103 }, - { url = "https://files.pythonhosted.org/packages/bb/a5/03c2ad8ca10994fcf22dd2150dd1d653bc974fa82d9a590494c84c10c641/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2", size = 560916 }, - { url = "https://files.pythonhosted.org/packages/ba/2e/be4fdfc8b5b576e588782b56978c5b702c5a2307024120d8aeec1ab818f0/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16", size = 587062 }, - { url = "https://files.pythonhosted.org/packages/67/e0/2034c221937709bf9c542603d25ad43a68b4b0a9a0c0b06a742f2756eb66/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f", size = 555734 }, - { url = "https://files.pythonhosted.org/packages/ea/ce/240bae07b5401a22482b58e18cfbabaa392409b2797da60223cca10d7367/rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de", size = 220663 }, - { url = "https://files.pythonhosted.org/packages/cb/f0/d330d08f51126330467edae2fa4efa5cec8923c87551a79299380fdea30d/rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9", size = 235503 }, - { url = "https://files.pythonhosted.org/packages/f7/c4/dbe1cc03df013bf2feb5ad00615038050e7859f381e96fb5b7b4572cd814/rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b", size = 347698 }, - { url = "https://files.pythonhosted.org/packages/a4/3a/684f66dd6b0f37499cad24cd1c0e523541fd768576fa5ce2d0a8799c3cba/rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b", size = 337330 }, - { url = "https://files.pythonhosted.org/packages/82/eb/e022c08c2ce2e8f7683baa313476492c0e2c1ca97227fe8a75d9f0181e95/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1", size = 380022 }, - { url = "https://files.pythonhosted.org/packages/e4/21/5a80e653e4c86aeb28eb4fea4add1f72e1787a3299687a9187105c3ee966/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83", size = 390754 }, - { url = "https://files.pythonhosted.org/packages/37/a4/d320a04ae90f72d080b3d74597074e62be0a8ecad7d7321312dfe2dc5a6a/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd", size = 423840 }, - { url = "https://files.pythonhosted.org/packages/87/70/674dc47d93db30a6624279284e5631be4c3a12a0340e8e4f349153546728/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1", size = 438970 }, - { url = "https://files.pythonhosted.org/packages/3f/64/9500f4d66601d55cadd21e90784cfd5d5f4560e129d72e4339823129171c/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3", size = 383146 }, - { url = "https://files.pythonhosted.org/packages/4d/45/630327addb1d17173adcf4af01336fd0ee030c04798027dfcb50106001e0/rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130", size = 408294 }, - { url = "https://files.pythonhosted.org/packages/5f/ef/8efb3373cee54ea9d9980b772e5690a0c9e9214045a4e7fa35046e399fee/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c", size = 556345 }, - { url = "https://files.pythonhosted.org/packages/54/01/151d3b9ef4925fc8f15bfb131086c12ec3c3d6dd4a4f7589c335bf8e85ba/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b", size = 582292 }, - { url = "https://files.pythonhosted.org/packages/30/89/35fc7a6cdf3477d441c7aca5e9bbf5a14e0f25152aed7f63f4e0b141045d/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333", size = 553855 }, - { url = "https://files.pythonhosted.org/packages/8f/e0/830c02b2457c4bd20a8c5bb394d31d81f57fbefce2dbdd2e31feff4f7003/rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730", size = 219100 }, - { url = "https://files.pythonhosted.org/packages/f8/30/7ac943f69855c2db77407ae363484b915d861702dbba1aa82d68d57f42be/rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", size = 233794 }, +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/79/2ce611b18c4fd83d9e3aecb5cba93e1917c050f556db39842889fa69b79f/rpds_py-0.23.1.tar.gz", hash = "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707", size = 26806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/8c/d17efccb9f5b9137ddea706664aebae694384ae1d5997c0202093e37185a/rpds_py-0.23.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c", size = 364369 }, + { url = "https://files.pythonhosted.org/packages/6e/c0/ab030f696b5c573107115a88d8d73d80f03309e60952b64c584c70c659af/rpds_py-0.23.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba", size = 349965 }, + { url = "https://files.pythonhosted.org/packages/b3/55/b40170f5a079c4fb0b6a82b299689e66e744edca3c3375a8b160fb797660/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31", size = 389064 }, + { url = "https://files.pythonhosted.org/packages/ab/1c/b03a912c59ec7c1e16b26e587b9dfa8ddff3b07851e781e8c46e908a365a/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149", size = 397741 }, + { url = "https://files.pythonhosted.org/packages/52/6f/151b90792b62fb6f87099bcc9044c626881fdd54e31bf98541f830b15cea/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c", size = 448784 }, + { url = "https://files.pythonhosted.org/packages/71/2a/6de67c0c97ec7857e0e9e5cd7c52405af931b303eb1e5b9eff6c50fd9a2e/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5", size = 440203 }, + { url = "https://files.pythonhosted.org/packages/db/5e/e759cd1c276d98a4b1f464b17a9bf66c65d29f8f85754e27e1467feaa7c3/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc", size = 391611 }, + { url = "https://files.pythonhosted.org/packages/1c/1e/2900358efcc0d9408c7289769cba4c0974d9db314aa884028ed7f7364f61/rpds_py-0.23.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35", size = 423306 }, + { url = "https://files.pythonhosted.org/packages/23/07/6c177e6d059f5d39689352d6c69a926ee4805ffdb6f06203570234d3d8f7/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b", size = 562323 }, + { url = "https://files.pythonhosted.org/packages/70/e4/f9097fd1c02b516fff9850792161eb9fc20a2fd54762f3c69eae0bdb67cb/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef", size = 588351 }, + { url = "https://files.pythonhosted.org/packages/87/39/5db3c6f326bfbe4576ae2af6435bd7555867d20ae690c786ff33659f293b/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad", size = 557252 }, + { url = "https://files.pythonhosted.org/packages/fd/14/2d5ad292f144fa79bafb78d2eb5b8a3a91c358b6065443cb9c49b5d1fedf/rpds_py-0.23.1-cp312-cp312-win32.whl", hash = "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057", size = 222181 }, + { url = "https://files.pythonhosted.org/packages/a3/4f/0fce63e0f5cdd658e71e21abd17ac1bc9312741ebb8b3f74eeed2ebdf771/rpds_py-0.23.1-cp312-cp312-win_amd64.whl", hash = "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165", size = 237426 }, + { url = "https://files.pythonhosted.org/packages/13/9d/b8b2c0edffb0bed15be17b6d5ab06216f2f47f9ee49259c7e96a3ad4ca42/rpds_py-0.23.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935", size = 363672 }, + { url = "https://files.pythonhosted.org/packages/bd/c2/5056fa29e6894144d7ba4c938b9b0445f75836b87d2dd00ed4999dc45a8c/rpds_py-0.23.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4", size = 349602 }, + { url = "https://files.pythonhosted.org/packages/b0/bc/33779a1bb0ee32d8d706b173825aab75c628521d23ce72a7c1e6a6852f86/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6", size = 388746 }, + { url = "https://files.pythonhosted.org/packages/62/0b/71db3e36b7780a619698ec82a9c87ab44ad7ca7f5480913e8a59ff76f050/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10", size = 397076 }, + { url = "https://files.pythonhosted.org/packages/bb/2e/494398f613edf77ba10a916b1ddea2acce42ab0e3b62e2c70ffc0757ce00/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122", size = 448399 }, + { url = "https://files.pythonhosted.org/packages/dd/53/4bd7f5779b1f463243ee5fdc83da04dd58a08f86e639dbffa7a35f969a84/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4", size = 439764 }, + { url = "https://files.pythonhosted.org/packages/f6/55/b3c18c04a460d951bf8e91f2abf46ce5b6426fb69784166a6a25827cb90a/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013", size = 390662 }, + { url = "https://files.pythonhosted.org/packages/2a/65/cc463044a3cbd616029b2aa87a651cdee8288d2fdd7780b2244845e934c1/rpds_py-0.23.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64", size = 422680 }, + { url = "https://files.pythonhosted.org/packages/fa/8e/1fa52990c7836d72e8d70cd7753f2362c72fbb0a49c1462e8c60e7176d0b/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8", size = 561792 }, + { url = "https://files.pythonhosted.org/packages/57/b8/fe3b612979b1a29d0c77f8585903d8b3a292604b26d4b300e228b8ac6360/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957", size = 588127 }, + { url = "https://files.pythonhosted.org/packages/44/2d/fde474de516bbc4b9b230f43c98e7f8acc5da7fc50ceed8e7af27553d346/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93", size = 556981 }, + { url = "https://files.pythonhosted.org/packages/18/57/767deeb27b81370bbab8f74ef6e68d26c4ea99018f3c71a570e506fede85/rpds_py-0.23.1-cp313-cp313-win32.whl", hash = "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd", size = 221936 }, + { url = "https://files.pythonhosted.org/packages/7d/6c/3474cfdd3cafe243f97ab8474ea8949236eb2a1a341ca55e75ce00cd03da/rpds_py-0.23.1-cp313-cp313-win_amd64.whl", hash = "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70", size = 237145 }, + { url = "https://files.pythonhosted.org/packages/ec/77/e985064c624230f61efa0423759bb066da56ebe40c654f8b5ba225bd5d63/rpds_py-0.23.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731", size = 359623 }, + { url = "https://files.pythonhosted.org/packages/62/d9/a33dcbf62b29e40559e012d525bae7d516757cf042cc9234bd34ca4b6aeb/rpds_py-0.23.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5", size = 345900 }, + { url = "https://files.pythonhosted.org/packages/92/eb/f81a4be6397861adb2cb868bb6a28a33292c2dcac567d1dc575226055e55/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a", size = 386426 }, + { url = "https://files.pythonhosted.org/packages/09/47/1f810c9b5e83be005341201b5389f1d240dfa440346ea7189f9b3fd6961d/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e", size = 392314 }, + { url = "https://files.pythonhosted.org/packages/83/bd/bc95831432fd6c46ed8001f01af26de0763a059d6d7e6d69e3c5bf02917a/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f", size = 447706 }, + { url = "https://files.pythonhosted.org/packages/19/3e/567c04c226b1802dc6dc82cad3d53e1fa0a773258571c74ac5d8fbde97ed/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219", size = 437060 }, + { url = "https://files.pythonhosted.org/packages/fe/77/a77d2c6afe27ae7d0d55fc32f6841502648070dc8d549fcc1e6d47ff8975/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722", size = 389347 }, + { url = "https://files.pythonhosted.org/packages/3f/47/6b256ff20a74cfebeac790ab05586e0ac91f88e331125d4740a6c86fc26f/rpds_py-0.23.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e", size = 415554 }, + { url = "https://files.pythonhosted.org/packages/fc/29/d4572469a245bc9fc81e35166dca19fc5298d5c43e1a6dd64bf145045193/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6", size = 557418 }, + { url = "https://files.pythonhosted.org/packages/9c/0a/68cf7228895b1a3f6f39f51b15830e62456795e61193d2c8b87fd48c60db/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b", size = 583033 }, + { url = "https://files.pythonhosted.org/packages/14/18/017ab41dcd6649ad5db7d00155b4c212b31ab05bd857d5ba73a1617984eb/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5", size = 554880 }, + { url = "https://files.pythonhosted.org/packages/2e/dd/17de89431268da8819d8d51ce67beac28d9b22fccf437bc5d6d2bcd1acdb/rpds_py-0.23.1-cp313-cp313t-win32.whl", hash = "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7", size = 219743 }, + { url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415 }, ] [[package]] @@ -2253,27 +2564,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, - { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, - { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, - { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, - { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, - { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, - { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, - { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, - { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, - { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, - { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, - { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, - { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, - { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, - { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, +version = "0.9.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588 }, + { url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848 }, + { url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525 }, + { url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580 }, + { url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674 }, + { url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151 }, + { url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128 }, + { url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858 }, + { url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046 }, + { url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834 }, + { url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307 }, + { url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039 }, + { url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177 }, + { url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122 }, + { url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751 }, + { url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987 }, + { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 }, ] [[package]] @@ -2296,25 +2607,25 @@ wheels = [ [[package]] name = "shapely" -version = "2.0.6" +version = "2.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/89/0d20bac88016be35ff7d3c0c2ae64b477908f1b1dfa540c5d69ac7af07fe/shapely-2.0.6.tar.gz", hash = "sha256:997f6159b1484059ec239cacaa53467fd8b5564dabe186cd84ac2944663b0bf6", size = 282361 } +sdist = { url = "https://files.pythonhosted.org/packages/21/c0/a911d1fd765d07a2b6769ce155219a281bfbe311584ebe97340d75c5bdb1/shapely-2.0.7.tar.gz", hash = "sha256:28fe2997aab9a9dc026dc6a355d04e85841546b2a5d232ed953e3321ab958ee5", size = 283413 } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/77/efd9f9d4b6a762f976f8b082f54c9be16f63050389500fb52e4f6cc07c1a/shapely-2.0.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cec9193519940e9d1b86a3b4f5af9eb6910197d24af02f247afbfb47bcb3fab0", size = 1450326 }, - { url = "https://files.pythonhosted.org/packages/68/53/5efa6e7a4036a94fe6276cf7bbb298afded51ca3396b03981ad680c8cc7d/shapely-2.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83b94a44ab04a90e88be69e7ddcc6f332da7c0a0ebb1156e1c4f568bbec983c3", size = 1298480 }, - { url = "https://files.pythonhosted.org/packages/88/a2/1be1db4fc262e536465a52d4f19d85834724fedf2299a1b9836bc82fe8fa/shapely-2.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:537c4b2716d22c92036d00b34aac9d3775e3691f80c7aa517c2c290351f42cd8", size = 2439311 }, - { url = "https://files.pythonhosted.org/packages/d5/7d/9a57e187cbf2fbbbdfd4044a4f9ce141c8d221f9963750d3b001f0ec080d/shapely-2.0.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fea108334be345c283ce74bf064fa00cfdd718048a8af7343c59eb40f59726", size = 2524835 }, - { url = "https://files.pythonhosted.org/packages/6d/0a/f407509ab56825f39bf8cfce1fb410238da96cf096809c3e404e5bc71ea1/shapely-2.0.6-cp312-cp312-win32.whl", hash = "sha256:42fd4cd4834747e4990227e4cbafb02242c0cffe9ce7ef9971f53ac52d80d55f", size = 1295613 }, - { url = "https://files.pythonhosted.org/packages/7b/b3/857afd9dfbfc554f10d683ac412eac6fa260d1f4cd2967ecb655c57e831a/shapely-2.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:665990c84aece05efb68a21b3523a6b2057e84a1afbef426ad287f0796ef8a48", size = 1442539 }, - { url = "https://files.pythonhosted.org/packages/34/e8/d164ef5b0eab86088cde06dee8415519ffd5bb0dd1bd9d021e640e64237c/shapely-2.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:42805ef90783ce689a4dde2b6b2f261e2c52609226a0438d882e3ced40bb3013", size = 1445344 }, - { url = "https://files.pythonhosted.org/packages/ce/e2/9fba7ac142f7831757a10852bfa465683724eadbc93d2d46f74a16f9af04/shapely-2.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d2cb146191a47bd0cee8ff5f90b47547b82b6345c0d02dd8b25b88b68af62d7", size = 1296182 }, - { url = "https://files.pythonhosted.org/packages/cf/dc/790d4bda27d196cd56ec66975eaae3351c65614cafd0e16ddde39ec9fb92/shapely-2.0.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3fdef0a1794a8fe70dc1f514440aa34426cc0ae98d9a1027fb299d45741c381", size = 2423426 }, - { url = "https://files.pythonhosted.org/packages/af/b0/f8169f77eac7392d41e231911e0095eb1148b4d40c50ea9e34d999c89a7e/shapely-2.0.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c665a0301c645615a107ff7f52adafa2153beab51daf34587170d85e8ba6805", size = 2513249 }, - { url = "https://files.pythonhosted.org/packages/f6/1d/a8c0e9ab49ff2f8e4dedd71b0122eafb22a18ad7e9d256025e1f10c84704/shapely-2.0.6-cp313-cp313-win32.whl", hash = "sha256:0334bd51828f68cd54b87d80b3e7cee93f249d82ae55a0faf3ea21c9be7b323a", size = 1294848 }, - { url = "https://files.pythonhosted.org/packages/23/38/2bc32dd1e7e67a471d4c60971e66df0bdace88656c47a9a728ace0091075/shapely-2.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:d37d070da9e0e0f0a530a621e17c0b8c3c9d04105655132a87cfff8bd77cc4c2", size = 1441371 }, + { url = "https://files.pythonhosted.org/packages/4f/3e/ea100eec5811bafd0175eb21828a3be5b0960f65250f4474391868be7c0f/shapely-2.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4c2b9859424facbafa54f4a19b625a752ff958ab49e01bc695f254f7db1835fa", size = 1482451 }, + { url = "https://files.pythonhosted.org/packages/ce/53/c6a3487716fd32e1f813d2a9608ba7b72a8a52a6966e31c6443480a1d016/shapely-2.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5aed1c6764f51011d69a679fdf6b57e691371ae49ebe28c3edb5486537ffbd51", size = 1345765 }, + { url = "https://files.pythonhosted.org/packages/fd/dd/b35d7891d25cc11066a70fb8d8169a6a7fca0735dd9b4d563a84684969a3/shapely-2.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c9ae8cf443187d784d57202199bf9fd2d4bb7d5521fe8926ba40db1bc33e8e", size = 2421540 }, + { url = "https://files.pythonhosted.org/packages/62/de/8dbd7df60eb23cb983bb698aac982944b3d602ef0ce877a940c269eae34e/shapely-2.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9469f49ff873ef566864cb3516091881f217b5d231c8164f7883990eec88b73", size = 2525741 }, + { url = "https://files.pythonhosted.org/packages/96/64/faf0413ebc7a84fe7a0790bf39ec0b02b40132b68e57aba985c0b6e4e7b6/shapely-2.0.7-cp312-cp312-win32.whl", hash = "sha256:6bca5095e86be9d4ef3cb52d56bdd66df63ff111d580855cb8546f06c3c907cd", size = 1296552 }, + { url = "https://files.pythonhosted.org/packages/63/05/8a1c279c226d6ad7604d9e237713dd21788eab96db97bf4ce0ea565e5596/shapely-2.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:f86e2c0259fe598c4532acfcf638c1f520fa77c1275912bbc958faecbf00b108", size = 1443464 }, + { url = "https://files.pythonhosted.org/packages/c6/21/abea43effbfe11f792e44409ee9ad7635aa93ef1c8ada0ef59b3c1c3abad/shapely-2.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a0c09e3e02f948631c7763b4fd3dd175bc45303a0ae04b000856dedebefe13cb", size = 1481618 }, + { url = "https://files.pythonhosted.org/packages/d9/71/af688798da36fe355a6e6ffe1d4628449cb5fa131d57fc169bcb614aeee7/shapely-2.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06ff6020949b44baa8fc2e5e57e0f3d09486cd5c33b47d669f847c54136e7027", size = 1345159 }, + { url = "https://files.pythonhosted.org/packages/67/47/f934fe2b70d31bb9774ad4376e34f81666deed6b811306ff574faa3d115e/shapely-2.0.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6dbf096f961ca6bec5640e22e65ccdec11e676344e8157fe7d636e7904fd36", size = 2410267 }, + { url = "https://files.pythonhosted.org/packages/f5/8a/2545cc2a30afc63fc6176c1da3b76af28ef9c7358ed4f68f7c6a9d86cf5b/shapely-2.0.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adeddfb1e22c20548e840403e5e0b3d9dc3daf66f05fa59f1fcf5b5f664f0e98", size = 2514128 }, + { url = "https://files.pythonhosted.org/packages/87/54/2344ce7da39676adec94e84fbaba92a8f1664e4ae2d33bd404dafcbe607f/shapely-2.0.7-cp313-cp313-win32.whl", hash = "sha256:a7f04691ce1c7ed974c2f8b34a1fe4c3c5dfe33128eae886aa32d730f1ec1913", size = 1295783 }, + { url = "https://files.pythonhosted.org/packages/d7/1e/6461e5cfc8e73ae165b8cff6eb26a4d65274fad0e1435137c5ba34fe4e88/shapely-2.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:aaaf5f7e6cc234c1793f2a2760da464b604584fb58c6b6d7d94144fd2692d67e", size = 1442300 }, ] [[package]] @@ -2429,6 +2740,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "tzdata" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, +] + +[[package]] +name = "tzlocal" +version = "5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/cc/11360404b20a6340b9b4ed39a3338c4af47bc63f87f6cea94dbcbde07029/tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2", size = 30480 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/9f/1c0b69d3abf4c65acac051ad696b8aea55afbb746dea8017baab53febb5e/tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c", size = 17920 }, +] + [[package]] name = "uri-template" version = "1.3.0" @@ -2447,6 +2779,111 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] +[[package]] +name = "vertex-ai-model-garden" +version = "0.1.0" +source = { editable = "samples/vertex-ai-vector-search" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + +[[package]] +name = "vertex-ai-reranker" +version = "0.1.0" +source = { editable = "samples/vertex-ai-reranker" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + +[[package]] +name = "vertex-ai-vector-search" +version = "0.1.0" +source = { editable = "samples/vertex-ai-model-garden" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-firebase-plugin" }, + { name = "genkit-google-ai-plugin" }, + { name = "genkit-google-cloud-plugin" }, + { name = "genkit-ollama-plugin" }, + { name = "genkit-pinecone-plugin" }, + { name = "genkit-vertex-ai-plugin" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-firebase-plugin", editable = "plugins/firebase" }, + { name = "genkit-google-ai-plugin", editable = "plugins/google-ai" }, + { name = "genkit-google-cloud-plugin", editable = "plugins/google-cloud" }, + { name = "genkit-ollama-plugin", editable = "plugins/ollama" }, + { name = "genkit-pinecone-plugin", editable = "plugins/pinecone" }, + { name = "genkit-vertex-ai-plugin", editable = "plugins/vertex-ai" }, + { name = "pydantic", specifier = ">=2.10.5" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + [[package]] name = "wcwidth" version = "0.2.13" diff --git a/py/taplo.toml b/taplo.toml similarity index 100% rename from py/taplo.toml rename to taplo.toml