├── .editorconfig ├── .github └── workflows │ ├── ExtensionTemplate.yml │ ├── MainDistributionPipeline.yml │ └── _extension_deploy.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── Makefile ├── docs ├── NEXT_README.md ├── README.md └── UPDATING.md ├── extension_config.cmake ├── scripts ├── bootstrap-template.py ├── extension-upload.sh └── setup-custom-toolchain.sh ├── src ├── include │ ├── http_functions.hpp │ ├── http_metadata_cache.hpp │ ├── http_state.hpp │ ├── open_prompt_extension.hpp │ └── open_prompt_secret.hpp ├── open_prompt_extension.cpp └── open_prompt_secret.cpp ├── test ├── README.md └── sql │ └── open_prompt.test └── vcpkg.json /.editorconfig: -------------------------------------------------------------------------------- 1 | duckdb/.editorconfig -------------------------------------------------------------------------------- /.github/workflows/ExtensionTemplate.yml: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: this workflow is for testing the extension template itself, 3 | # this workflow will be removed when scripts/bootstrap-template.py is run 4 | # 5 | name: Extension Template 6 | on: [push, pull_request,repository_dispatch] 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || '' }}-${{ github.base_ref || '' }}-${{ github.ref != 'refs/heads/main' || github.sha }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | linux: 13 | name: Linux 14 | if: ${{ vars.RUN_RENAME_TEST == 'true' || github.repository == 'duckdb/extension-template' }} 15 | runs-on: ubuntu-latest 16 | container: ubuntu:18.04 17 | strategy: 18 | matrix: 19 | # Add commits/tags to build against other DuckDB versions 20 | duckdb_version: [ '' ] 21 | env: 22 | VCPKG_TOOLCHAIN_PATH: ${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake 23 | VCPKG_TARGET_TRIPLET: 'x64-linux' 24 | GEN: ninja 25 | ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true 26 | defaults: 27 | run: 28 | shell: bash 29 | 30 | steps: 31 | - name: Install required ubuntu packages 32 | run: | 33 | apt-get update -y -qq 34 | apt-get install -y -qq software-properties-common 35 | add-apt-repository ppa:git-core/ppa 36 | apt-get update -y -qq 37 | apt-get install -y -qq ninja-build make gcc-multilib g++-multilib libssl-dev wget openjdk-8-jdk zip maven unixodbc-dev libc6-dev-i386 lib32readline6-dev libssl-dev libcurl4-gnutls-dev libexpat1-dev gettext unzip build-essential checkinstall libffi-dev curl libz-dev openssh-client 38 | 39 | - name: Install Git 2.18.5 40 | run: | 41 | wget https://github.com/git/git/archive/refs/tags/v2.18.5.tar.gz 42 | tar xvf v2.18.5.tar.gz 43 | cd git-2.18.5 44 | make 45 | make prefix=/usr install 46 | git --version 47 | 48 | - uses: actions/checkout@v3 49 | with: 50 | fetch-depth: 0 51 | submodules: 'true' 52 | 53 | - name: Checkout DuckDB to version 54 | if: ${{ matrix.duckdb_version != ''}} 55 | run: | 56 | cd duckdb 57 | git checkout ${{ matrix.duckdb_version }} 58 | 59 | - uses: ./duckdb/.github/actions/ubuntu_18_setup 60 | 61 | - name: Setup vcpkg 62 | uses: lukka/run-vcpkg@v11.1 63 | with: 64 | vcpkgGitCommitId: a1a1cbc975abf909a6c8985a6a2b8fe20bbd9bd6 65 | 66 | - name: Rename extension 67 | run: | 68 | python3 scripts/bootstrap-template.py ext_1_a_123b_b11 69 | 70 | - name: Build 71 | run: | 72 | make 73 | 74 | - name: Test 75 | run: | 76 | make test 77 | 78 | macos: 79 | name: MacOS 80 | if: ${{ vars.RUN_RENAME_TEST == 'true' || github.repository == 'duckdb/extension-template' }} 81 | runs-on: macos-latest 82 | strategy: 83 | matrix: 84 | # Add commits/tags to build against other DuckDB versions 85 | duckdb_version: [ ''] 86 | env: 87 | VCPKG_TOOLCHAIN_PATH: ${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake 88 | VCPKG_TARGET_TRIPLET: 'x64-osx' 89 | OSX_BUILD_ARCH: 'x86_64' 90 | GEN: ninja 91 | defaults: 92 | run: 93 | shell: bash 94 | 95 | steps: 96 | - uses: actions/checkout@v3 97 | with: 98 | fetch-depth: 0 99 | submodules: 'true' 100 | 101 | - name: Install Ninja 102 | run: brew install ninja 103 | 104 | - uses: actions/setup-python@v2 105 | with: 106 | python-version: '3.11' 107 | 108 | - name: Checkout DuckDB to version 109 | if: ${{ matrix.duckdb_version != ''}} 110 | run: | 111 | cd duckdb 112 | git checkout ${{ matrix.duckdb_version }} 113 | 114 | - name: Setup vcpkg 115 | uses: lukka/run-vcpkg@v11.1 116 | with: 117 | vcpkgGitCommitId: a1a1cbc975abf909a6c8985a6a2b8fe20bbd9bd6 118 | 119 | - name: Rename extension 120 | run: | 121 | python scripts/bootstrap-template.py ext_1_a_123b_b11 122 | 123 | - name: Build 124 | run: | 125 | make 126 | 127 | - name: Test 128 | run: | 129 | make test 130 | 131 | windows: 132 | name: Windows 133 | if: ${{ vars.RUN_RENAME_TEST == 'true' || github.repository == 'duckdb/extension-template' }} 134 | runs-on: windows-latest 135 | strategy: 136 | matrix: 137 | # Add commits/tags to build against other DuckDB versions 138 | duckdb_version: [ '' ] 139 | env: 140 | VCPKG_TOOLCHAIN_PATH: ${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake 141 | VCPKG_TARGET_TRIPLET: 'x64-windows-static-md' 142 | defaults: 143 | run: 144 | shell: bash 145 | 146 | steps: 147 | - uses: actions/checkout@v3 148 | with: 149 | fetch-depth: 0 150 | submodules: 'true' 151 | 152 | - uses: actions/setup-python@v2 153 | with: 154 | python-version: '3.11' 155 | 156 | - name: Checkout DuckDB to version 157 | # Add commits/tags to build against other DuckDB versions 158 | if: ${{ matrix.duckdb_version != ''}} 159 | run: | 160 | cd duckdb 161 | git checkout ${{ matrix.duckdb_version }} 162 | 163 | - name: Setup vcpkg 164 | uses: lukka/run-vcpkg@v11.1 165 | with: 166 | vcpkgGitCommitId: a1a1cbc975abf909a6c8985a6a2b8fe20bbd9bd6 167 | 168 | - name: Rename extension 169 | run: | 170 | python scripts/bootstrap-template.py ext_1_a_123b_b11 171 | 172 | - name: Build 173 | run: | 174 | make 175 | 176 | - name: Test extension 177 | run: | 178 | build/release/test/Release/unittest.exe -------------------------------------------------------------------------------- /.github/workflows/MainDistributionPipeline.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This workflow calls the main distribution pipeline from DuckDB to build, test and (optionally) release the extension 3 | # 4 | name: Main Extension Distribution Pipeline 5 | on: 6 | push: 7 | paths-ignore: 8 | - "**/*.md" 9 | - "**/*.yml" 10 | pull_request: 11 | workflow_dispatch: 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || '' }}-${{ github.base_ref || '' }}-${{ github.ref != 'refs/heads/main' || github.sha }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | duckdb-next-build: 19 | name: Build extension binaries 20 | uses: duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml@main 21 | with: 22 | duckdb_version: main 23 | ci_tools_version: main 24 | extension_name: open_prompt 25 | 26 | duckdb-stable-build: 27 | name: Build extension binaries 28 | uses: duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml@v1.1.1 29 | with: 30 | duckdb_version: v1.1.1 31 | ci_tools_version: v1.1.1 32 | extension_name: open_prompt 33 | 34 | duckdb-stable-deploy: 35 | name: Deploy extension binaries 36 | needs: duckdb-stable-build 37 | uses: ./.github/workflows/_extension_deploy.yml 38 | secrets: inherit 39 | with: 40 | duckdb_version: v1.1.1 41 | extension_name: open_prompt 42 | deploy_latest: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' }} 43 | -------------------------------------------------------------------------------- /.github/workflows/_extension_deploy.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Reusable workflow that deploys the artifacts produced by github.com/duckdb/duckdb/.github/workflows/_extension_distribution.yml 3 | # 4 | # note: this workflow needs to be located in the extension repository, as it requires secrets to be passed to the 5 | # deploy script. However, it should generally not be necessary to modify this workflow in your extension repository, as 6 | # this workflow can be configured to use a custom deploy script. 7 | 8 | 9 | name: Extension Deployment 10 | on: 11 | workflow_call: 12 | inputs: 13 | # The name of the extension 14 | extension_name: 15 | required: true 16 | type: string 17 | # DuckDB version to build against 18 | duckdb_version: 19 | required: true 20 | type: string 21 | # ';' separated list of architectures to exclude, for example: 'linux_amd64;osx_arm64' 22 | exclude_archs: 23 | required: false 24 | type: string 25 | default: "" 26 | # Whether to upload this deployment as the latest. This may overwrite a previous deployment. 27 | deploy_latest: 28 | required: false 29 | type: boolean 30 | default: false 31 | # Whether to upload this deployment under a versioned path. These will not be deleted automatically 32 | deploy_versioned: 33 | required: false 34 | type: boolean 35 | default: false 36 | # Postfix added to artifact names. Can be used to guarantee unique names when this workflow is called multiple times 37 | artifact_postfix: 38 | required: false 39 | type: string 40 | default: "" 41 | # Override the default deploy script with a custom script 42 | deploy_script: 43 | required: false 44 | type: string 45 | default: "./scripts/extension-upload.sh" 46 | # Override the default matrix parse script with a custom script 47 | matrix_parse_script: 48 | required: false 49 | type: string 50 | default: "./duckdb/scripts/modify_distribution_matrix.py" 51 | 52 | jobs: 53 | generate_matrix: 54 | name: Generate matrix 55 | runs-on: ubuntu-latest 56 | outputs: 57 | deploy_matrix: ${{ steps.parse-matrices.outputs.deploy_matrix }} 58 | steps: 59 | - uses: actions/checkout@v3 60 | with: 61 | fetch-depth: 0 62 | submodules: 'true' 63 | 64 | - name: Checkout DuckDB to version 65 | run: | 66 | cd duckdb 67 | git checkout ${{ inputs.duckdb_version }} 68 | 69 | - id: parse-matrices 70 | run: | 71 | python3 ${{ inputs.matrix_parse_script }} --input ./duckdb/.github/config/distribution_matrix.json --deploy_matrix --output deploy_matrix.json --exclude "${{ inputs.exclude_archs }}" --pretty 72 | deploy_matrix="`cat deploy_matrix.json`" 73 | echo deploy_matrix=$deploy_matrix >> $GITHUB_OUTPUT 74 | echo `cat $GITHUB_OUTPUT` 75 | 76 | deploy: 77 | name: Deploy 78 | runs-on: ubuntu-latest 79 | needs: generate_matrix 80 | if: ${{ needs.generate_matrix.outputs.deploy_matrix != '{}' && needs.generate_matrix.outputs.deploy_matrix != '' }} 81 | strategy: 82 | matrix: ${{fromJson(needs.generate_matrix.outputs.deploy_matrix)}} 83 | 84 | steps: 85 | - uses: actions/checkout@v3 86 | with: 87 | fetch-depth: 0 88 | submodules: 'true' 89 | 90 | - name: Checkout DuckDB to version 91 | run: | 92 | cd duckdb 93 | git checkout ${{ inputs.duckdb_version }} 94 | 95 | - uses: actions/download-artifact@v3 96 | with: 97 | name: ${{ inputs.extension_name }}-${{ inputs.duckdb_version }}-extension-${{matrix.duckdb_arch}}${{inputs.artifact_postfix}}${{startsWith(matrix.duckdb, 'wasm') && '.wasm' || ''}} 98 | path: | 99 | /tmp/extension 100 | 101 | - name: Deploy 102 | shell: bash 103 | env: 104 | AWS_ACCESS_KEY_ID: ${{ secrets.S3_DEPLOY_ID }} 105 | AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_DEPLOY_KEY }} 106 | AWS_DEFAULT_REGION: ${{ secrets.S3_REGION }} 107 | BUCKET_NAME: ${{ secrets.S3_BUCKET }} 108 | DUCKDB_EXTENSION_SIGNING_PK: ${{ secrets.S3_DUCKDB_ORG_EXTENSION_SIGNING_PK }} 109 | run: | 110 | pwd 111 | python3 -m pip install pip awscli 112 | git config --global --add safe.directory '*' 113 | cd duckdb 114 | git fetch --tags 115 | export DUCKDB_VERSION=`git tag --points-at HEAD` 116 | export DUCKDB_VERSION=${DUCKDB_VERSION:=`git log -1 --format=%h`} 117 | cd .. 118 | git fetch --tags 119 | export EXT_VERSION=`git tag --points-at HEAD` 120 | export EXT_VERSION=${EXT_VERSION:=`git log -1 --format=%h`} 121 | ${{ inputs.deploy_script }} ${{ inputs.extension_name }} $EXT_VERSION $DUCKDB_VERSION ${{ matrix.duckdb_arch }} $BUCKET_NAME ${{inputs.deploy_latest || 'true' && 'false'}} ${{inputs.deploy_versioned || 'true' && 'false'}} 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | build 3 | .idea 4 | cmake-build-debug 5 | duckdb_unittest_tempdir/ 6 | .DS_Store 7 | testext 8 | test/python/__pycache__/ 9 | .Rhistory 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "duckdb"] 2 | path = duckdb 3 | url = https://github.com/duckdb/duckdb 4 | branch = main 5 | [submodule "extension-ci-tools"] 6 | path = extension-ci-tools 7 | url = https://github.com/duckdb/extension-ci-tools 8 | branch = main -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | # Set extension name here 4 | set(TARGET_NAME open_prompt) 5 | 6 | # DuckDB's extension distribution supports vcpkg. As such, dependencies can be added in ./vcpkg.json and then 7 | # used in cmake with find_package. Feel free to remove or replace with other dependencies. 8 | # Note that it should also be removed from vcpkg.json to prevent needlessly installing it.. 9 | find_package(OpenSSL REQUIRED) 10 | 11 | set(EXTENSION_NAME ${TARGET_NAME}_extension) 12 | set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension) 13 | 14 | project(${TARGET_NAME}) 15 | include_directories(src/include duckdb/third_party/httplib) 16 | 17 | set(EXTENSION_SOURCES src/open_prompt_extension.cpp src/open_prompt_secret.cpp) 18 | 19 | if(MINGW) 20 | set(OPENSSL_USE_STATIC_LIBS TRUE) 21 | endif() 22 | 23 | # Find OpenSSL before building extensions 24 | find_package(OpenSSL REQUIRED) 25 | 26 | build_static_extension(${TARGET_NAME} ${EXTENSION_SOURCES}) 27 | build_loadable_extension(${TARGET_NAME} " " ${EXTENSION_SOURCES}) 28 | 29 | include_directories(${OPENSSL_INCLUDE_DIR}) 30 | target_link_libraries(${LOADABLE_EXTENSION_NAME} duckdb_mbedtls ${OPENSSL_LIBRARIES}) 31 | target_link_libraries(${EXTENSION_NAME} duckdb_mbedtls ${OPENSSL_LIBRARIES}) 32 | 33 | if(MINGW) 34 | set(WIN_LIBS crypt32 ws2_32 wsock32) 35 | find_package(ZLIB) 36 | target_link_libraries(${LOADABLE_EXTENSION_NAME} ZLIB::ZLIB ${WIN_LIBS}) 37 | target_link_libraries(${EXTENSION_NAME} ZLIB::ZLIB ${WIN_LIBS}) 38 | endif() 39 | 40 | install( 41 | TARGETS ${EXTENSION_NAME} 42 | EXPORT "${DUCKDB_EXPORT_SET}" 43 | LIBRARY DESTINATION "${INSTALL_LIB_DIR}" 44 | ARCHIVE DESTINATION "${INSTALL_LIB_DIR}") 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018-2024 Stichting DuckDB Foundation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJ_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 2 | 3 | # Configuration of extension 4 | EXT_NAME=http_client 5 | EXT_CONFIG=${PROJ_DIR}extension_config.cmake 6 | 7 | # Include the Makefile from extension-ci-tools 8 | include extension-ci-tools/makefiles/duckdb_extension.Makefile 9 | -------------------------------------------------------------------------------- /docs/NEXT_README.md: -------------------------------------------------------------------------------- 1 | # Quack 2 | 3 | This repository is based on https://github.com/duckdb/extension-template, check it out if you want to build and ship your own DuckDB extension. 4 | 5 | --- 6 | 7 | This extension, Quack, allow you to ... . 8 | 9 | 10 | ## Building 11 | ### Managing dependencies 12 | DuckDB extensions uses VCPKG for dependency management. Enabling VCPKG is very simple: follow the [installation instructions](https://vcpkg.io/en/getting-started) or just run the following: 13 | ```shell 14 | git clone https://github.com/Microsoft/vcpkg.git 15 | ./vcpkg/bootstrap-vcpkg.sh 16 | export VCPKG_TOOLCHAIN_PATH=`pwd`/vcpkg/scripts/buildsystems/vcpkg.cmake 17 | ``` 18 | Note: VCPKG is only required for extensions that want to rely on it for dependency management. If you want to develop an extension without dependencies, or want to do your own dependency management, just skip this step. Note that the example extension uses VCPKG to build with a dependency for instructive purposes, so when skipping this step the build may not work without removing the dependency. 19 | 20 | ### Build steps 21 | Now to build the extension, run: 22 | ```sh 23 | make 24 | ``` 25 | The main binaries that will be built are: 26 | ```sh 27 | ./build/release/duckdb 28 | ./build/release/test/unittest 29 | ./build/release/extension/quack/quack.duckdb_extension 30 | ``` 31 | - `duckdb` is the binary for the duckdb shell with the extension code automatically loaded. 32 | - `unittest` is the test runner of duckdb. Again, the extension is already linked into the binary. 33 | - `quack.duckdb_extension` is the loadable binary as it would be distributed. 34 | 35 | ## Running the extension 36 | To run the extension code, simply start the shell with `./build/release/duckdb`. 37 | 38 | Now we can use the features from the extension directly in DuckDB. The template contains a single scalar function `quack()` that takes a string arguments and returns a string: 39 | ``` 40 | D select quack('Jane') as result; 41 | ┌───────────────┐ 42 | │ result │ 43 | │ varchar │ 44 | ├───────────────┤ 45 | │ Quack Jane 🐥 │ 46 | └───────────────┘ 47 | ``` 48 | 49 | ## Running the tests 50 | Different tests can be created for DuckDB extensions. The primary way of testing DuckDB extensions should be the SQL tests in `./test/sql`. These SQL tests can be run using: 51 | ```sh 52 | make test 53 | ``` 54 | 55 | ### Installing the deployed binaries 56 | To install your extension binaries from S3, you will need to do two things. Firstly, DuckDB should be launched with the 57 | `allow_unsigned_extensions` option set to true. How to set this will depend on the client you're using. Some examples: 58 | 59 | CLI: 60 | ```shell 61 | duckdb -unsigned 62 | ``` 63 | 64 | Python: 65 | ```python 66 | con = duckdb.connect(':memory:', config={'allow_unsigned_extensions' : 'true'}) 67 | ``` 68 | 69 | NodeJS: 70 | ```js 71 | db = new duckdb.Database(':memory:', {"allow_unsigned_extensions": "true"}); 72 | ``` 73 | 74 | Secondly, you will need to set the repository endpoint in DuckDB to the HTTP url of your bucket + version of the extension 75 | you want to install. To do this run the following SQL query in DuckDB: 76 | ```sql 77 | SET custom_extension_repository='bucket.s3.eu-west-1.amazonaws.com//latest'; 78 | ``` 79 | Note that the `/latest` path will allow you to install the latest extension version available for your current version of 80 | DuckDB. To specify a specific version, you can pass the version instead. 81 | 82 | After running these steps, you can install and load your extension using the regular INSTALL/LOAD commands in DuckDB: 83 | ```sql 84 | INSTALL quack 85 | LOAD quack 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # DuckDB Open Prompt Extension 4 | Simple extension to query OpenAI Completion API endpoints such as Ollama/OpenAI/etc 5 | 6 | > Experimental: USE AT YOUR OWN RISK! 7 | 8 | ## Installing and Loading 9 | ```sql 10 | INSTALL open_prompt FROM community; 11 | LOAD open_prompt; 12 | ``` 13 | 14 | ### Functions 15 | - `open_prompt(prompt)` 16 | - `set_api_url(/v1/chat/completions)` 17 | - `set_api_token(optional_auth_token)` 18 | - `set_model_name(model_name)` 19 | 20 | #### Requirements 21 | 22 | - DuckDB v1.1.1 or higher 23 | - API must support `/v1/chat/completions` 24 | 25 | ### Settings 26 | Setup the completions API configuration w/ optional auth token and model name 27 | ```sql 28 | SET VARIABLE openprompt_api_url = 'http://localhost:11434/v1/chat/completions'; 29 | SET VARIABLE openprompt_api_token = 'your_api_key_here'; 30 | SET VARIABLE openprompt_model_name = 'qwen2.5:0.5b'; 31 | ``` 32 | 33 | Alternatively the following ENV variables can be used at runtime 34 | ``` 35 | OPEN_PROMPT_API_URL='http://localhost:11434/v1/chat/completions' 36 | OPEN_PROMPT_API_TOKEN='your_api_key_here' 37 | OPEN_PROMPT_MODEL_NAME='qwen2.5:0.5b' 38 | OPEN_PROMPT_API_TIMEOUT='30' 39 | ``` 40 | 41 | For persistent usage, configure parameters using DuckDB SECRETS 42 | ```sql 43 | CREATE SECRET IF NOT EXISTS open_prompt ( 44 | TYPE open_prompt, 45 | PROVIDER config, 46 | api_token 'your-api-token', 47 | api_url 'http://localhost:11434/v1/chat/completions', 48 | model_name 'qwen2.5:0.5b', 49 | api_timeout '30' 50 | ); 51 | ``` 52 | 53 | 54 | ### Usage 55 | ```sql 56 | D SELECT open_prompt('Write a one-line poem about ducks') AS response; 57 | ┌────────────────────────────────────────────────┐ 58 | │ response │ 59 | │ varchar │ 60 | ├────────────────────────────────────────────────┤ 61 | │ Ducks quacking at dawn, swimming in the light. │ 62 | └────────────────────────────────────────────────┘ 63 | ``` 64 | 65 | #### JSON Structured Output 66 | For supported models you can request structured JSON output by providing a schema 67 | 68 | ```sql 69 | SET VARIABLE openprompt_api_url = 'http://localhost:11434/v1/chat/completions'; 70 | SET VARIABLE openprompt_api_token = 'your_api_key_here'; 71 | SET VARIABLE openprompt_model_name = 'llama3.2:3b'; 72 | 73 | SELECT open_prompt('I want ice cream', json_schema := '{ 74 | "type": "object", 75 | "properties": { 76 | "summary": { "type": "string" }, 77 | "sentiment": { "type": "string", "enum": ["pos", "neg", "neutral"] } 78 | }, 79 | "required": ["summary", "sentiment"], 80 | "additionalProperties": false 81 | }'); 82 | ``` 83 | 84 | For smaller models the `system_prompt` can be used to request JSON schema in _best-effort_ mode 85 | 86 | ```sql 87 | SET VARIABLE openprompt_api_url = 'http://localhost:11434/v1/chat/completions'; 88 | SET VARIABLE openprompt_api_token = 'your_api_key_here'; 89 | SET VARIABLE openprompt_model_name = 'qwen2.5:1.5b'; 90 | SELECT open_prompt('I want ice cream.', 91 | system_prompt:='The respose MUST be a JSON with the following schema: { 92 | "type": "object", 93 | "properties": { 94 | "summary": { "type": "string" }, 95 | "sentiment": { "type": "string", "enum": ["pos", "neg", "neutral"] } 96 | }, 97 | "required": ["summary", "sentiment"], 98 | "additionalProperties": false 99 | }'); 100 | ``` 101 | 102 | 103 |
104 | 105 | 106 | 107 | ### Ollama self-hosted 108 | Test the open_prompt extension using a local or remote Ollama with Completions API 109 | 110 | #### CPU only 111 | ``` 112 | docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama 113 | ``` 114 | #### Nvidia GPU 115 | Install the Nvidia container toolkit. Run Ollama inside a Docker container 116 | ``` 117 | docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama 118 | ``` 119 | 120 |
121 | 122 | ###### Disclaimers 123 | 124 | * Configuration and formats loosely inspired by the [Motherduck prompt()](https://motherduck.com/docs/sql-reference/motherduck-sql-reference/ai-functions/prompt/) 125 | 126 | > DuckDB ® is a trademark of DuckDB Foundation. Motherduck ® is a trademark of the Motherduck Corporation. Any trademarks, service marks, and logos mentioned or depicted are the property of their respective owners. The use of any third-party trademarks, brand names, product names, and company names is purely informative or intended as parody and does not imply endorsement, affiliation, or association with the respective owners. 127 | -------------------------------------------------------------------------------- /docs/UPDATING.md: -------------------------------------------------------------------------------- 1 | # Extension updating 2 | When cloning this template, the target version of DuckDB should be the latest stable release of DuckDB. However, there 3 | will inevitably come a time when a new DuckDB is released and the extension repository needs updating. This process goes 4 | as follows: 5 | 6 | - Bump submodules 7 | - `./duckdb` should be set to latest tagged release 8 | - `./extension-ci-tools` should be set to updated branch corresponding to latest DuckDB release. So if you're building for DuckDB `v1.1.0` there will be a branch in `extension-ci-tools` named `v1.1.0` to which you should check out. 9 | - Bump versions in `./github/workflows` 10 | - `duckdb_version` input in `duckdb-stable-build` job in `MainDistributionPipeline.yml` should be set to latest tagged release 11 | - `duckdb_version` input in `duckdb-stable-deploy` job in `MainDistributionPipeline.yml` should be set to latest tagged release 12 | - the reusable workflow `duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml` for the `duckdb-stable-build` job should be set to latest tagged release 13 | 14 | # API changes 15 | DuckDB extensions built with this extension template are built against the internal C++ API of DuckDB. This API is not guaranteed to be stable. 16 | What this means for extension development is that when updating your extensions DuckDB target version using the above steps, you may run into the fact that your extension no longer builds properly. 17 | 18 | Currently, DuckDB does not (yet) provide a specific change log for these API changes, but it is generally not too hard to figure out what has changed. 19 | 20 | For figuring out how and why the C++ API changed, we recommend using the following resources: 21 | - DuckDB's [Release Notes](https://github.com/duckdb/duckdb/releases) 22 | - DuckDB's history of [Core extension patches](https://github.com/duckdb/duckdb/commits/main/.github/patches/extensions) 23 | - The git history of the relevant C++ Header file of the API that has changed -------------------------------------------------------------------------------- /extension_config.cmake: -------------------------------------------------------------------------------- 1 | # This file is included by DuckDB's build system. It specifies which extension to load 2 | 3 | # Extension from this repo 4 | duckdb_extension_load(open_prompt 5 | SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR} 6 | LOAD_TESTS 7 | ) 8 | 9 | # Any extra extensions that should be built 10 | # duckdb_extension_load(json) 11 | -------------------------------------------------------------------------------- /scripts/bootstrap-template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys, os, shutil, re 4 | from pathlib import Path 5 | 6 | 7 | def is_snake_case(s): 8 | # Define the regex pattern for snake case with numbers 9 | pattern = r"^[a-z0-9]+(_[a-z0-9]+)*$" 10 | 11 | # Use re.match to check if the string matches the pattern 12 | if re.match(pattern, s): 13 | return True 14 | else: 15 | return False 16 | 17 | 18 | def to_camel_case(snake_str): 19 | return "".join(x.capitalize() for x in snake_str.lower().split("_")) 20 | 21 | 22 | def replace(file_name, to_find, to_replace): 23 | with open(file_name, "r", encoding="utf8") as file: 24 | filedata = file.read() 25 | filedata = filedata.replace(to_find, to_replace) 26 | with open(file_name, "w", encoding="utf8") as file: 27 | file.write(filedata) 28 | 29 | 30 | def replace_everywhere(to_find, to_replace): 31 | for path in files_to_search: 32 | replace(path, to_find, to_replace) 33 | replace(path, to_find.capitalize(), to_camel_case(to_replace)) 34 | replace(path, to_find.upper(), to_replace.upper()) 35 | 36 | replace("./CMakeLists.txt", to_find, to_replace) 37 | replace("./Makefile", to_find, to_replace) 38 | replace("./Makefile", to_find.capitalize(), to_camel_case(to_replace)) 39 | replace("./Makefile", to_find.upper(), to_replace.upper()) 40 | replace("./README.md", to_find, to_replace) 41 | replace("./extension_config.cmake", to_find, to_replace) 42 | replace("./scripts/setup-custom-toolchain.sh", to_find, to_replace) 43 | 44 | 45 | if __name__ == "__main__": 46 | if len(sys.argv) != 2: 47 | raise Exception( 48 | "usage: python3 bootstrap-template.py " 49 | ) 50 | 51 | name_extension = sys.argv[1] 52 | 53 | if name_extension[0].isdigit(): 54 | raise Exception("Please dont start your extension name with a number.") 55 | 56 | if not is_snake_case(name_extension): 57 | raise Exception( 58 | "Please enter the name of your extension in valid snake_case containing only lower case letters and numbers" 59 | ) 60 | 61 | shutil.copyfile("docs/NEXT_README.md", "README.md") 62 | os.remove("docs/NEXT_README.md") 63 | os.remove("docs/README.md") 64 | 65 | files_to_search = [] 66 | files_to_search.extend(Path("./.github").rglob("./**/*.yml")) 67 | files_to_search.extend(Path("./test").rglob("./**/*.test")) 68 | files_to_search.extend(Path("./src").rglob("./**/*.hpp")) 69 | files_to_search.extend(Path("./src").rglob("./**/*.cpp")) 70 | files_to_search.extend(Path("./src").rglob("./**/*.txt")) 71 | files_to_search.extend(Path("./src").rglob("./*.md")) 72 | 73 | replace_everywhere("quack", name_extension) 74 | replace_everywhere("Quack", name_extension.capitalize()) 75 | replace_everywhere("", name_extension) 76 | 77 | string_to_replace = name_extension 78 | string_to_find = "quack" 79 | 80 | # rename files 81 | os.rename(f"test/sql/{string_to_find}.test", f"test/sql/{string_to_replace}.test") 82 | os.rename( 83 | f"src/{string_to_find}_extension.cpp", f"src/{string_to_replace}_extension.cpp" 84 | ) 85 | os.rename( 86 | f"src/include/{string_to_find}_extension.hpp", 87 | f"src/include/{string_to_replace}_extension.hpp", 88 | ) 89 | 90 | # remove template-specific files 91 | os.remove(".github/workflows/ExtensionTemplate.yml") 92 | 93 | # finally, remove this bootstrap file 94 | os.remove(__file__) 95 | -------------------------------------------------------------------------------- /scripts/extension-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Extension upload script 4 | 5 | # Usage: ./extension-upload.sh 6 | # : Name of the extension 7 | # : Version (commit / version tag) of the extension 8 | # : Version (commit / version tag) of DuckDB 9 | # : Architecture target of the extension binary 10 | # : S3 bucket to upload to 11 | # : Set this as the latest version ("true" / "false", default: "false") 12 | # : Set this as a versioned version that will prevent its deletion 13 | 14 | set -e 15 | 16 | if [[ $4 == wasm* ]]; then 17 | ext="/tmp/extension/$1.duckdb_extension.wasm" 18 | else 19 | ext="/tmp/extension/$1.duckdb_extension" 20 | fi 21 | 22 | echo $ext 23 | 24 | script_dir="$(dirname "$(readlink -f "$0")")" 25 | 26 | # calculate SHA256 hash of extension binary 27 | cat $ext > $ext.append 28 | 29 | if [[ $4 == wasm* ]]; then 30 | # 0 for custom section 31 | # 113 in hex = 275 in decimal, total lenght of what follows (1 + 16 + 2 + 256) 32 | # [1(continuation) + 0010011(payload) = \x93, 0(continuation) + 10(payload) = \x02] 33 | echo -n -e '\x00' >> $ext.append 34 | echo -n -e '\x93\x02' >> $ext.append 35 | # 10 in hex = 16 in decimal, lenght of name, 1 byte 36 | echo -n -e '\x10' >> $ext.append 37 | echo -n -e 'duckdb_signature' >> $ext.append 38 | # the name of the WebAssembly custom section, 16 bytes 39 | # 100 in hex, 256 in decimal 40 | # [1(continuation) + 0000000(payload) = ff, 0(continuation) + 10(payload)], 41 | # for a grand total of 2 bytes 42 | echo -n -e '\x80\x02' >> $ext.append 43 | fi 44 | 45 | # (Optionally) Sign binary 46 | if [ "$DUCKDB_EXTENSION_SIGNING_PK" != "" ]; then 47 | echo "$DUCKDB_EXTENSION_SIGNING_PK" > private.pem 48 | $script_dir/../duckdb/scripts/compute-extension-hash.sh $ext.append > $ext.hash 49 | openssl pkeyutl -sign -in $ext.hash -inkey private.pem -pkeyopt digest:sha256 -out $ext.sign 50 | rm -f private.pem 51 | fi 52 | 53 | # Signature is always there, potentially defaulting to 256 zeros 54 | truncate -s 256 $ext.sign 55 | 56 | # append signature to extension binary 57 | cat $ext.sign >> $ext.append 58 | 59 | # compress extension binary 60 | if [[ $4 == wasm_* ]]; then 61 | brotli < $ext.append > "$ext.compressed" 62 | else 63 | gzip < $ext.append > "$ext.compressed" 64 | fi 65 | 66 | set -e 67 | 68 | # Abort if AWS key is not set 69 | if [ -z "$AWS_ACCESS_KEY_ID" ]; then 70 | echo "No AWS key found, skipping.." 71 | exit 0 72 | fi 73 | 74 | # upload versioned version 75 | if [[ $7 = 'true' ]]; then 76 | if [[ $4 == wasm* ]]; then 77 | aws s3 cp $ext.compressed s3://$5/$1/$2/$3/$4/$1.duckdb_extension.wasm --acl public-read --content-encoding br --content-type="application/wasm" 78 | else 79 | aws s3 cp $ext.compressed s3://$5/$1/$2/$3/$4/$1.duckdb_extension.gz --acl public-read 80 | fi 81 | fi 82 | 83 | # upload to latest version 84 | if [[ $6 = 'true' ]]; then 85 | if [[ $4 == wasm* ]]; then 86 | aws s3 cp $ext.compressed s3://$5/$3/$4/$1.duckdb_extension.wasm --acl public-read --content-encoding br --content-type="application/wasm" 87 | else 88 | aws s3 cp $ext.compressed s3://$5/$3/$4/$1.duckdb_extension.gz --acl public-read 89 | fi 90 | fi 91 | -------------------------------------------------------------------------------- /scripts/setup-custom-toolchain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is an example script that can be used to install additional toolchain dependencies. Feel free to remove this script 4 | # if no additional toolchains are required 5 | 6 | # To enable this script, set the `custom_toolchain_script` option to true when calling the reusable workflow 7 | # `.github/workflows/_extension_distribution.yml` from `https://github.com/duckdb/extension-ci-tools` 8 | 9 | # note that the $DUCKDB_PLATFORM environment variable can be used to discern between the platforms 10 | echo "This is the sample custom toolchain script running for architecture '$DUCKDB_PLATFORM' for the quack extension." 11 | 12 | -------------------------------------------------------------------------------- /src/include/http_functions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "duckdb.hpp" 4 | 5 | namespace duckdb { 6 | 7 | struct HTTPFunctions { 8 | public: 9 | static void Register(DatabaseInstance &db) { 10 | RegisterHTTPRequestFunction(db); 11 | } 12 | 13 | private: 14 | //! Register HTTPRequest functions 15 | static void RegisterHTTPRequestFunction(DatabaseInstance &db); 16 | }; 17 | 18 | } // namespace duckdb 19 | -------------------------------------------------------------------------------- /src/include/http_metadata_cache.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "duckdb/common/atomic.hpp" 4 | #include "duckdb/common/chrono.hpp" 5 | #include "duckdb/common/list.hpp" 6 | #include "duckdb/common/mutex.hpp" 7 | #include "duckdb/common/string.hpp" 8 | #include "duckdb/common/types.hpp" 9 | #include "duckdb/common/unordered_map.hpp" 10 | #include "duckdb/main/client_context.hpp" 11 | #include "duckdb/main/client_context_state.hpp" 12 | 13 | #include 14 | #include 15 | 16 | namespace duckdb { 17 | 18 | struct HTTPMetadataCacheEntry { 19 | idx_t length; 20 | time_t last_modified; 21 | }; 22 | 23 | // Simple cache with a max age for an entry to be valid 24 | class HTTPMetadataCache : public ClientContextState { 25 | public: 26 | explicit HTTPMetadataCache(bool flush_on_query_end_p, bool shared_p) 27 | : flush_on_query_end(flush_on_query_end_p), shared(shared_p) {}; 28 | 29 | void Insert(const string &path, HTTPMetadataCacheEntry val) { 30 | if (shared) { 31 | lock_guard parallel_lock(lock); 32 | map[path] = val; 33 | } else { 34 | map[path] = val; 35 | } 36 | }; 37 | 38 | void Erase(string path) { 39 | if (shared) { 40 | lock_guard parallel_lock(lock); 41 | map.erase(path); 42 | } else { 43 | map.erase(path); 44 | } 45 | }; 46 | 47 | bool Find(string path, HTTPMetadataCacheEntry &ret_val) { 48 | if (shared) { 49 | lock_guard parallel_lock(lock); 50 | auto lookup = map.find(path); 51 | if (lookup != map.end()) { 52 | ret_val = lookup->second; 53 | return true; 54 | } else { 55 | return false; 56 | } 57 | } else { 58 | auto lookup = map.find(path); 59 | if (lookup != map.end()) { 60 | ret_val = lookup->second; 61 | return true; 62 | } else { 63 | return false; 64 | } 65 | } 66 | }; 67 | 68 | void Clear() { 69 | if (shared) { 70 | lock_guard parallel_lock(lock); 71 | map.clear(); 72 | } else { 73 | map.clear(); 74 | } 75 | } 76 | 77 | //! Called by the ClientContext when the current query ends 78 | void QueryEnd(ClientContext &context) override { 79 | if (flush_on_query_end) { 80 | Clear(); 81 | } 82 | } 83 | 84 | protected: 85 | mutex lock; 86 | unordered_map map; 87 | bool flush_on_query_end; 88 | bool shared; 89 | }; 90 | 91 | } // namespace duckdb 92 | -------------------------------------------------------------------------------- /src/include/http_state.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "duckdb/common/file_opener.hpp" 4 | #include "duckdb/main/client_context.hpp" 5 | #include "duckdb/main/client_data.hpp" 6 | #include "duckdb/common/atomic.hpp" 7 | #include "duckdb/common/optional_ptr.hpp" 8 | #include "duckdb/main/client_context_state.hpp" 9 | 10 | namespace duckdb { 11 | 12 | class CachedFileHandle; 13 | 14 | //! Represents a file that is intended to be fully downloaded, then used in parallel by multiple threads 15 | class CachedFile : public enable_shared_from_this { 16 | friend class CachedFileHandle; 17 | 18 | public: 19 | unique_ptr GetHandle() { 20 | auto this_ptr = shared_from_this(); 21 | return make_uniq(this_ptr); 22 | } 23 | 24 | private: 25 | //! Cached Data 26 | shared_ptr data; 27 | //! Data capacity 28 | uint64_t capacity = 0; 29 | //! Size of file 30 | idx_t size; 31 | //! Lock for initializing the file 32 | mutex lock; 33 | //! When initialized is set to true, the file is safe for parallel reading without holding the lock 34 | atomic initialized = {false}; 35 | }; 36 | 37 | //! Handle to a CachedFile 38 | class CachedFileHandle { 39 | public: 40 | explicit CachedFileHandle(shared_ptr &file_p); 41 | 42 | //! allocate a buffer for the file 43 | void AllocateBuffer(idx_t size); 44 | //! Indicate the file is fully downloaded and safe for parallel reading without lock 45 | void SetInitialized(idx_t total_size); 46 | //! Grow buffer to new size, copying over `bytes_to_copy` to the new buffer 47 | void GrowBuffer(idx_t new_capacity, idx_t bytes_to_copy); 48 | //! Write to the buffer 49 | void Write(const char *buffer, idx_t length, idx_t offset = 0); 50 | 51 | bool Initialized() { 52 | return file->initialized; 53 | } 54 | const char *GetData() { 55 | return file->data.get(); 56 | } 57 | uint64_t GetCapacity() { 58 | return file->capacity; 59 | } 60 | //! Return the size of the initialized file 61 | idx_t GetSize() { 62 | D_ASSERT(file->initialized); 63 | return file->size; 64 | } 65 | 66 | private: 67 | unique_ptr> lock; 68 | shared_ptr file; 69 | }; 70 | 71 | class HTTPState : public ClientContextState { 72 | public: 73 | //! Reset all counters and cached files 74 | void Reset(); 75 | //! Get cache entry, create if not exists 76 | shared_ptr &GetCachedFile(const string &path); 77 | //! Helper functions to get the HTTP state 78 | static shared_ptr TryGetState(ClientContext &context); 79 | static shared_ptr TryGetState(optional_ptr opener); 80 | 81 | bool IsEmpty() { 82 | return head_count == 0 && get_count == 0 && put_count == 0 && post_count == 0 && total_bytes_received == 0 && 83 | total_bytes_sent == 0; 84 | } 85 | 86 | atomic head_count {0}; 87 | atomic get_count {0}; 88 | atomic put_count {0}; 89 | atomic post_count {0}; 90 | atomic total_bytes_received {0}; 91 | atomic total_bytes_sent {0}; 92 | 93 | //! Called by the ClientContext when the current query ends 94 | void QueryEnd(ClientContext &context) override { 95 | Reset(); 96 | } 97 | void WriteProfilingInformation(std::ostream &ss) override; 98 | 99 | private: 100 | //! Mutex to lock when getting the cached file(Parallel Only) 101 | mutex cached_files_mutex; 102 | //! In case of fully downloading the file, the cached files of this query 103 | unordered_map> cached_files; 104 | }; 105 | 106 | } // namespace duckdb 107 | -------------------------------------------------------------------------------- /src/include/open_prompt_extension.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "duckdb.hpp" 4 | 5 | namespace duckdb { 6 | 7 | using HeaderMap = case_insensitive_map_t; 8 | 9 | class OpenPromptExtension : public Extension { 10 | public: 11 | void Load(DuckDB &db) override; 12 | std::string Name() override; 13 | std::string Version() const override; 14 | 15 | }; 16 | 17 | } // namespace duckdb 18 | -------------------------------------------------------------------------------- /src/include/open_prompt_secret.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "duckdb/main/secret/secret.hpp" 4 | #include "duckdb/main/extension_util.hpp" 5 | 6 | namespace duckdb { 7 | 8 | struct CreateOpenPromptSecretFunctions { 9 | public: 10 | static void Register(DatabaseInstance &instance); 11 | }; 12 | 13 | } // namespace duckdb 14 | -------------------------------------------------------------------------------- /src/open_prompt_extension.cpp: -------------------------------------------------------------------------------- 1 | #define DUCKDB_EXTENSION_MAIN 2 | #include "open_prompt_extension.hpp" 3 | #include "duckdb.hpp" 4 | #include "duckdb/function/scalar_function.hpp" 5 | #include "duckdb/main/extension_util.hpp" 6 | #include "duckdb/common/atomic.hpp" 7 | #include "duckdb/common/exception/http_exception.hpp" 8 | #include 9 | 10 | #include "duckdb/main/secret/secret_manager.hpp" 11 | #include "duckdb/main/secret/secret.hpp" 12 | #include "duckdb/main/secret/secret_storage.hpp" 13 | 14 | #include "open_prompt_secret.hpp" 15 | 16 | #ifdef USE_ZLIB 17 | #define CPPHTTPLIB_ZLIB_SUPPORT 18 | #endif 19 | 20 | #define CPPHTTPLIB_OPENSSL_SUPPORT 21 | #include "httplib.hpp" 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "yyjson.hpp" 33 | 34 | #include 35 | 36 | namespace duckdb { 37 | struct OpenPromptData: FunctionData { 38 | idx_t model_idx; 39 | idx_t json_schema_idx; 40 | idx_t json_system_prompt_idx; 41 | unique_ptr Copy() const override { 42 | auto res = make_uniq(); 43 | res->model_idx = model_idx; 44 | res->json_schema_idx = json_schema_idx; 45 | res->json_system_prompt_idx = json_system_prompt_idx; 46 | return unique_ptr(std::move(res)); 47 | }; 48 | bool Equals(const FunctionData &other) const { 49 | return model_idx == other.Cast().model_idx && 50 | json_schema_idx == other.Cast().json_schema_idx && 51 | json_system_prompt_idx==other.Cast().json_system_prompt_idx; 52 | }; 53 | OpenPromptData() { 54 | model_idx = 0; 55 | json_schema_idx = 0; 56 | json_system_prompt_idx = 0; 57 | } 58 | }; 59 | 60 | unique_ptr OpenPromptBind(ClientContext &context, ScalarFunction &bound_function, 61 | vector> &arguments) { 62 | auto res = make_uniq(); 63 | for (idx_t i = 1; i < arguments.size(); ++i) { 64 | const auto &argument = arguments[i]; 65 | if (i == 1 && argument->alias.empty()) { 66 | res->model_idx = i; 67 | } else if (argument->alias == "json_schema") { 68 | res->json_schema_idx = i; 69 | } else if (argument->alias == "system_prompt") { 70 | res->json_system_prompt_idx = i; 71 | } 72 | } 73 | return std::move(res); 74 | } 75 | 76 | static std::pair SetupHttpClient(const std::string &url) { 77 | std::string scheme, domain, path, client_url; 78 | size_t pos = url.find("://"); 79 | std::string mod_url = url; 80 | if (pos != std::string::npos) { 81 | scheme = mod_url.substr(0, pos); 82 | mod_url.erase(0, pos + 3); 83 | } 84 | 85 | pos = mod_url.find("/"); 86 | if (pos != std::string::npos) { 87 | domain = mod_url.substr(0, pos); 88 | path = mod_url.substr(pos); 89 | } else { 90 | domain = mod_url; 91 | path = "/"; 92 | } 93 | 94 | // Construct client url with scheme if specified 95 | if (scheme.length() > 0) { 96 | client_url = scheme + "://" + domain; 97 | } else { 98 | client_url = domain; 99 | } 100 | 101 | duckdb_httplib_openssl::Client client(client_url); 102 | client.set_read_timeout(20, 0); // 20 seconds 103 | client.set_follow_location(true); // Follow redirects 104 | 105 | return std::make_pair(std::move(client), path); 106 | } 107 | 108 | static void HandleHttpError(const duckdb_httplib_openssl::Result &res, const std::string &request_type) { 109 | std::string err_message = "HTTP " + request_type + " request failed. "; 110 | 111 | switch (res.error()) { 112 | case duckdb_httplib_openssl::Error::Connection: 113 | err_message += "Connection error."; 114 | break; 115 | case duckdb_httplib_openssl::Error::BindIPAddress: 116 | err_message += "Failed to bind IP address."; 117 | break; 118 | case duckdb_httplib_openssl::Error::Read: 119 | err_message += "Error reading response."; 120 | break; 121 | case duckdb_httplib_openssl::Error::Write: 122 | err_message += "Error writing request."; 123 | break; 124 | case duckdb_httplib_openssl::Error::ExceedRedirectCount: 125 | err_message += "Too many redirects."; 126 | break; 127 | case duckdb_httplib_openssl::Error::Canceled: 128 | err_message += "Request was canceled."; 129 | break; 130 | case duckdb_httplib_openssl::Error::SSLConnection: 131 | err_message += "SSL connection failed."; 132 | break; 133 | case duckdb_httplib_openssl::Error::SSLLoadingCerts: 134 | err_message += "Failed to load SSL certificates."; 135 | break; 136 | case duckdb_httplib_openssl::Error::SSLServerVerification: 137 | err_message += "SSL server verification failed."; 138 | break; 139 | case duckdb_httplib_openssl::Error::UnsupportedMultipartBoundaryChars: 140 | err_message += "Unsupported characters in multipart boundary."; 141 | break; 142 | case duckdb_httplib_openssl::Error::Compression: 143 | err_message += "Error during compression."; 144 | break; 145 | default: 146 | err_message += "Unknown error."; 147 | break; 148 | } 149 | throw std::runtime_error(err_message); 150 | } 151 | 152 | // Settings management 153 | static std::string GetConfigValue(ClientContext &context, const string &var_name, const string &default_value) { 154 | // Try SET value from current session 155 | { 156 | Value value; 157 | auto &config = ClientConfig::GetConfig(context); 158 | if (config.GetUserVariable(var_name, value) && !value.IsNull()) { 159 | return value.ToString(); 160 | } 161 | } 162 | // Try GET environment variables 163 | { 164 | // Create uppercase ENV version: OPEN_PROMPT_SETTING 165 | std::string stripped_name = var_name; 166 | const std::string prefix = "openprompt_"; 167 | if (stripped_name.substr(0, prefix.length()) == prefix) { 168 | stripped_name = stripped_name.substr(prefix.length()); 169 | } 170 | std::string env_var_name = "OPEN_PROMPT_" + stripped_name; 171 | std::transform(env_var_name.begin(), env_var_name.end(), env_var_name.begin(), ::toupper); 172 | 173 | const char* env_value = std::getenv(env_var_name.c_str()); 174 | if (env_value != nullptr && strlen(env_value) > 0) { 175 | std::string result(env_value); 176 | return result; 177 | } 178 | } 179 | // Try GET from secrets 180 | { 181 | // Create lowercase secret version: open_prompt_setting 182 | std::string secret_key = var_name; 183 | const std::string prefix = "openprompt_"; 184 | if (secret_key.substr(0, prefix.length()) == prefix) { 185 | secret_key = secret_key.substr(prefix.length()); 186 | } 187 | // secret_key = "open_prompt_" + secret_key; 188 | std::transform(secret_key.begin(), secret_key.end(), secret_key.begin(), ::tolower); 189 | 190 | auto &secret_manager = SecretManager::Get(context); 191 | try { 192 | auto transaction = CatalogTransaction::GetSystemCatalogTransaction(context); 193 | auto secret_match = secret_manager.LookupSecret(transaction, "open_prompt", "open_prompt"); 194 | if (secret_match.HasMatch()) { 195 | auto &secret = secret_match.GetSecret(); 196 | if (secret.GetType() != "open_prompt") { 197 | throw InvalidInputException("Invalid secret type. Expected 'open_prompt', got '%s'", secret.GetType()); 198 | } 199 | const auto *kv_secret = dynamic_cast(&secret); 200 | if (!kv_secret) { 201 | throw InvalidInputException("Invalid secret format for 'open_prompt' secret"); 202 | } 203 | Value secret_value; 204 | if (kv_secret->TryGetValue(secret_key, secret_value)) { 205 | return secret_value.ToString(); 206 | } 207 | } 208 | } catch (...) { 209 | // If secret lookup fails, fall back to user variables 210 | return default_value; 211 | } 212 | } 213 | // Fall back to default value 214 | return default_value; 215 | } 216 | 217 | static void SetConfigValue(DataChunk &args, ExpressionState &state, Vector &result, 218 | const string &var_name, const string &value_type) { 219 | UnaryExecutor::Execute(args.data[0], result, args.size(), 220 | [&](string_t value) { 221 | try { 222 | if (value == "" || value.GetSize() == 0) { 223 | throw std::invalid_argument(value_type + " cannot be empty."); 224 | } 225 | 226 | ClientConfig::GetConfig(state.GetContext()).SetUserVariable( 227 | var_name, 228 | Value::CreateValue(value.GetString()) 229 | ); 230 | return StringVector::AddString(result, value_type + " set to: " + value.GetString()); 231 | } catch (std::exception &e) { 232 | return StringVector::AddString(result, "Failed to set " + value_type + ": " + e.what()); 233 | } 234 | }); 235 | } 236 | 237 | static void SetApiToken(DataChunk &args, ExpressionState &state, Vector &result) { 238 | SetConfigValue(args, state, result, "openprompt_api_token", "API token"); 239 | } 240 | 241 | static void SetApiUrl(DataChunk &args, ExpressionState &state, Vector &result) { 242 | SetConfigValue(args, state, result, "openprompt_api_url", "API URL"); 243 | } 244 | 245 | static void SetApiTimeout(DataChunk &args, ExpressionState &state, Vector &result) { 246 | SetConfigValue(args, state, result, "openprompt_api_timeout", "API timeout"); 247 | } 248 | 249 | static void SetModelName(DataChunk &args, ExpressionState &state, Vector &result) { 250 | SetConfigValue(args, state, result, "openprompt_model_name", "Model name"); 251 | } 252 | 253 | // Complete OpenPromptRequestFunction 254 | static void OpenPromptRequestFunction(DataChunk &args, ExpressionState &state, Vector &result) { 255 | D_ASSERT(args.data.size() >= 1); // At least prompt required 256 | 257 | UnaryExecutor::Execute(args.data[0], result, args.size(), 258 | [&](string_t user_prompt) { 259 | auto &func_expr = state.expr.Cast(); 260 | auto &info = func_expr.bind_info->Cast(); 261 | auto &context = state.GetContext(); 262 | 263 | std::string api_url = GetConfigValue(context, "openprompt_api_url", 264 | "http://localhost:11434/v1/chat/completions"); 265 | std::string api_token = GetConfigValue(context, "openprompt_api_token", ""); 266 | std::string model_name = GetConfigValue(context, "openprompt_model_name", "qwen2.5:0.5b"); 267 | std::string api_timeout = GetConfigValue(context, "openprompt_api_timeout", ""); 268 | std::string json_schema; 269 | std::string system_prompt; 270 | 271 | if (info.model_idx != 0) { 272 | model_name = args.data[info.model_idx].GetValue(0).ToString(); 273 | } 274 | if (info.json_schema_idx != 0) { 275 | json_schema = args.data[info.json_schema_idx].GetValue(0).ToString(); 276 | } 277 | if (info.json_system_prompt_idx != 0) { 278 | system_prompt = args.data[info.json_system_prompt_idx].GetValue(0).ToString(); 279 | } 280 | 281 | unique_ptr doc( 282 | duckdb_yyjson::yyjson_mut_doc_new(nullptr), &duckdb_yyjson::yyjson_mut_doc_free); 283 | auto obj = duckdb_yyjson::yyjson_mut_obj(doc.get()); 284 | duckdb_yyjson::yyjson_mut_doc_set_root(doc.get(), obj); 285 | duckdb_yyjson::yyjson_mut_obj_add(obj, 286 | duckdb_yyjson::yyjson_mut_str(doc.get(), "model"), 287 | duckdb_yyjson::yyjson_mut_str(doc.get(), model_name.c_str()) 288 | ); 289 | if (!json_schema.empty()) { 290 | auto response_format = duckdb_yyjson::yyjson_mut_obj(doc.get()); 291 | duckdb_yyjson::yyjson_mut_obj_add(response_format, 292 | duckdb_yyjson::yyjson_mut_str(doc.get(), "type"), 293 | duckdb_yyjson::yyjson_mut_str(doc.get(), "json_object")); 294 | auto yyschema = duckdb_yyjson::yyjson_mut_raw(doc.get(), json_schema.c_str()); 295 | duckdb_yyjson::yyjson_mut_obj_add(response_format, 296 | duckdb_yyjson::yyjson_mut_str(doc.get(), "schema"), 297 | yyschema); 298 | duckdb_yyjson::yyjson_mut_obj_add(obj, 299 | duckdb_yyjson::yyjson_mut_str(doc.get(),"response_format"), 300 | response_format); 301 | } 302 | auto messages = duckdb_yyjson::yyjson_mut_arr(doc.get()); 303 | string str_messages[2][2] = { 304 | {"system", system_prompt}, 305 | {"user", user_prompt.GetString()} 306 | }; 307 | for (auto message : str_messages) { 308 | if (message[1].empty()) { 309 | continue; 310 | } 311 | auto yymessage = duckdb_yyjson::yyjson_mut_arr_add_obj(doc.get(),messages); 312 | duckdb_yyjson::yyjson_mut_obj_add(yymessage, 313 | duckdb_yyjson::yyjson_mut_str(doc.get(), "role"), 314 | duckdb_yyjson::yyjson_mut_str(doc.get(), message[0].c_str())); 315 | duckdb_yyjson::yyjson_mut_obj_add(yymessage, 316 | duckdb_yyjson::yyjson_mut_str(doc.get(), "content"), 317 | duckdb_yyjson::yyjson_mut_str(doc.get(), message[1].c_str())); 318 | } 319 | duckdb_yyjson::yyjson_mut_obj_add(obj, duckdb_yyjson::yyjson_mut_str(doc.get(), "messages"), 320 | messages); 321 | duckdb_yyjson::yyjson_write_err err; 322 | auto request_body = duckdb_yyjson::yyjson_mut_write_opts(doc.get(), 0, nullptr, nullptr, &err); 323 | if (request_body == nullptr) { 324 | throw std::runtime_error(err.msg); 325 | } 326 | string str_request_body(request_body); 327 | free(request_body); 328 | 329 | try { 330 | auto client_and_path = SetupHttpClient(api_url); 331 | auto &client = client_and_path.first; 332 | auto &path = client_and_path.second; 333 | 334 | duckdb_httplib_openssl::Headers headers; 335 | headers.emplace("Content-Type", "application/json"); 336 | if (!api_token.empty()) { 337 | headers.emplace("Authorization", "Bearer " + api_token); 338 | } 339 | 340 | if (!api_timeout.empty()) { 341 | client.set_read_timeout(stoi(api_timeout), 0); 342 | } 343 | 344 | auto res = client.Post(path.c_str(), headers, str_request_body, "application/json"); 345 | 346 | if (!res) { 347 | HandleHttpError(res, "POST"); 348 | } 349 | 350 | if (res->status != 200) { 351 | throw std::runtime_error("HTTP error " + std::to_string(res->status) + ": " + res->reason); 352 | } 353 | 354 | try { 355 | unique_ptr doc( 356 | duckdb_yyjson::yyjson_read(res->body.c_str(), res->body.length(), 0), 357 | &duckdb_yyjson::yyjson_doc_free 358 | ); 359 | 360 | if (!doc) { 361 | throw std::runtime_error("Failed to parse JSON response"); 362 | } 363 | 364 | auto root = duckdb_yyjson::yyjson_doc_get_root(doc.get()); 365 | if (!root) { 366 | throw std::runtime_error("Invalid JSON response: no root object"); 367 | } 368 | 369 | auto choices = duckdb_yyjson::yyjson_obj_get(root, "choices"); 370 | if (!choices || !duckdb_yyjson::yyjson_is_arr(choices)) { 371 | throw std::runtime_error("Invalid response format: missing choices array"); 372 | } 373 | 374 | auto first_choice = duckdb_yyjson::yyjson_arr_get_first(choices); 375 | if (!first_choice) { 376 | throw std::runtime_error("Empty choices array in response"); 377 | } 378 | 379 | auto message = duckdb_yyjson::yyjson_obj_get(first_choice, "message"); 380 | if (!message) { 381 | throw std::runtime_error("Missing message in response"); 382 | } 383 | 384 | auto content = duckdb_yyjson::yyjson_obj_get(message, "content"); 385 | if (!content) { 386 | throw std::runtime_error("Missing content in response"); 387 | } 388 | 389 | auto content_str = duckdb_yyjson::yyjson_get_str(content); 390 | if (!content_str) { 391 | throw std::runtime_error("Invalid content in response"); 392 | } 393 | 394 | return StringVector::AddString(result, content_str); 395 | } catch (std::exception &e) { 396 | throw std::runtime_error("Failed to parse response: " + std::string(e.what())); 397 | } 398 | } catch (std::exception &e) { 399 | return StringVector::AddString(result, "Error: " + std::string(e.what())); 400 | } 401 | }); 402 | } 403 | 404 | // Complete LoadInternal function 405 | static void LoadInternal(DatabaseInstance &instance) { 406 | ScalarFunctionSet open_prompt("open_prompt"); 407 | 408 | open_prompt.AddFunction(ScalarFunction( 409 | {LogicalType::VARCHAR}, LogicalType::VARCHAR, OpenPromptRequestFunction, 410 | OpenPromptBind)); 411 | open_prompt.AddFunction(ScalarFunction( 412 | {LogicalType::VARCHAR, LogicalType::VARCHAR}, LogicalType::VARCHAR, OpenPromptRequestFunction, 413 | OpenPromptBind)); 414 | open_prompt.AddFunction(ScalarFunction( 415 | {LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::VARCHAR}, 416 | LogicalType::VARCHAR, OpenPromptRequestFunction, 417 | OpenPromptBind)); 418 | open_prompt.AddFunction(ScalarFunction( 419 | {LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::VARCHAR}, 420 | LogicalType::VARCHAR, OpenPromptRequestFunction, 421 | OpenPromptBind)); 422 | 423 | // Register Secret functions 424 | CreateOpenPromptSecretFunctions::Register(instance); 425 | 426 | ExtensionUtil::RegisterFunction(instance, open_prompt); 427 | 428 | ExtensionUtil::RegisterFunction(instance, ScalarFunction( 429 | "set_api_token", {LogicalType::VARCHAR}, LogicalType::VARCHAR, SetApiToken)); 430 | ExtensionUtil::RegisterFunction(instance, ScalarFunction( 431 | "set_api_url", {LogicalType::VARCHAR}, LogicalType::VARCHAR, SetApiUrl)); 432 | ExtensionUtil::RegisterFunction(instance, ScalarFunction( 433 | "set_model_name", {LogicalType::VARCHAR}, LogicalType::VARCHAR, SetModelName)); 434 | ExtensionUtil::RegisterFunction(instance, ScalarFunction( 435 | "set_api_timeout", {LogicalType::VARCHAR}, LogicalType::VARCHAR, SetApiTimeout)); 436 | } 437 | 438 | void OpenPromptExtension::Load(DuckDB &db) { 439 | LoadInternal(*db.instance); 440 | } 441 | 442 | std::string OpenPromptExtension::Name() { 443 | return "open_prompt"; 444 | } 445 | 446 | std::string OpenPromptExtension::Version() const { 447 | #ifdef EXT_VERSION_OPENPROMPT 448 | return EXT_VERSION_OPENPROMPT; 449 | #else 450 | return ""; 451 | #endif 452 | } 453 | 454 | } // namespace duckdb 455 | 456 | extern "C" { 457 | DUCKDB_EXTENSION_API void open_prompt_init(duckdb::DatabaseInstance &db) { 458 | duckdb::DuckDB db_wrapper(db); 459 | db_wrapper.LoadExtension(); 460 | } 461 | 462 | DUCKDB_EXTENSION_API const char *open_prompt_version() { 463 | return duckdb::DuckDB::LibraryVersion(); 464 | } 465 | } 466 | 467 | #ifndef DUCKDB_EXTENSION_MAIN 468 | #error DUCKDB_EXTENSION_MAIN not defined 469 | #endif 470 | -------------------------------------------------------------------------------- /src/open_prompt_secret.cpp: -------------------------------------------------------------------------------- 1 | #include "open_prompt_secret.hpp" 2 | #include "duckdb/common/exception.hpp" 3 | #include "duckdb/main/secret/secret.hpp" 4 | #include "duckdb/main/extension_util.hpp" 5 | 6 | namespace duckdb { 7 | 8 | static void CopySecret(const std::string &key, const CreateSecretInput &input, KeyValueSecret &result) { 9 | auto val = input.options.find(key); 10 | if (val != input.options.end()) { 11 | result.secret_map[key] = val->second; 12 | } 13 | } 14 | 15 | static void RegisterCommonSecretParameters(CreateSecretFunction &function) { 16 | // Register open_prompt common parameters 17 | function.named_parameters["api_token"] = LogicalType::VARCHAR; 18 | function.named_parameters["api_url"] = LogicalType::VARCHAR; 19 | function.named_parameters["model_name"] = LogicalType::VARCHAR; 20 | function.named_parameters["api_timeout"] = LogicalType::VARCHAR; 21 | } 22 | 23 | static void RedactCommonKeys(KeyValueSecret &result) { 24 | // Redact sensitive information 25 | result.redact_keys.insert("api_token"); 26 | } 27 | 28 | static unique_ptr CreateOpenPromptSecretFromConfig(ClientContext &context, CreateSecretInput &input) { 29 | auto scope = input.scope; 30 | auto result = make_uniq(scope, input.type, input.provider, input.name); 31 | 32 | // Copy all relevant secrets 33 | CopySecret("api_token", input, *result); 34 | CopySecret("api_url", input, *result); 35 | CopySecret("model_name", input, *result); 36 | CopySecret("api_timeout", input, *result); 37 | 38 | // Redact sensitive keys 39 | RedactCommonKeys(*result); 40 | 41 | return std::move(result); 42 | } 43 | 44 | void CreateOpenPromptSecretFunctions::Register(DatabaseInstance &instance) { 45 | string type = "open_prompt"; 46 | 47 | // Register the new type 48 | SecretType secret_type; 49 | secret_type.name = type; 50 | secret_type.deserializer = KeyValueSecret::Deserialize; 51 | secret_type.default_provider = "config"; 52 | ExtensionUtil::RegisterSecretType(instance, secret_type); 53 | 54 | // Register the config secret provider 55 | CreateSecretFunction config_function = {type, "config", CreateOpenPromptSecretFromConfig}; 56 | RegisterCommonSecretParameters(config_function); 57 | ExtensionUtil::RegisterFunction(instance, config_function); 58 | } 59 | 60 | } // namespace duckdb 61 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Testing this extension 2 | This directory contains all the tests for this extension. The `sql` directory holds tests that are written as [SQLLogicTests](https://duckdb.org/dev/sqllogictest/intro.html). DuckDB aims to have most its tests in this format as SQL statements, so for the quack extension, this should probably be the goal too. 3 | 4 | The root makefile contains targets to build and run all of these tests. To run the SQLLogicTests: 5 | ```bash 6 | make test 7 | ``` 8 | or 9 | ```bash 10 | make test_debug 11 | ``` -------------------------------------------------------------------------------- /test/sql/open_prompt.test: -------------------------------------------------------------------------------- 1 | # name: test/sql/rusty_quack.test 2 | # description: test rusty_quack extension 3 | # group: [quack] 4 | 5 | # Before we load the extension, this will fail 6 | statement error 7 | SELECT open_prompt('error'); 8 | ---- 9 | Catalog Error: Scalar Function with name open_prompt does not exist! 10 | 11 | # Require statement will ensure the extension is loaded from now on 12 | require open_prompt 13 | 14 | # Confirm the extension works by setting a secret 15 | query I 16 | CREATE SECRET IF NOT EXISTS open_prompt ( 17 | TYPE open_prompt, 18 | PROVIDER config, 19 | api_token 'xxxxx', 20 | api_url 'https://api.groq.com/openai/v1/chat/completions', 21 | model_name 'llama-3.3-70b-versatile', 22 | api_timeout '30' 23 | ); 24 | ---- 25 | true 26 | 27 | # Confirm the secret exists 28 | query I 29 | SELECT name FROM duckdb_secrets() WHERE name = 'open_prompt' ; 30 | ---- 31 | open_prompt 32 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "openssl" 4 | ] 5 | } --------------------------------------------------------------------------------