├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── publish.yml ├── pyproject.toml ├── tests ├── conftest.py └── test_llm_embed.py ├── datasette_llm_embed.py ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | venv 6 | .eggs 7 | .pytest_cache 8 | *.egg-info 9 | .DS_Store 10 | .vscode 11 | dist 12 | build 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | groups: 8 | python-packages: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.8", "3.9", "3.10", "3.11"] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | cache: pip 21 | cache-dependency-path: setup.py 22 | - name: Install dependencies 23 | run: | 24 | pip install -e '.[test]' 25 | - name: Run tests 26 | run: | 27 | pytest 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "datasette-llm-embed" 3 | version = "0.2" 4 | description = "llm_embed(model_id, text) SQL function for Datasette" 5 | readme = "README.md" 6 | authors = [{name = "Simon Willison"}] 7 | license = {text = "Apache-2.0"} 8 | classifiers = [ 9 | "License :: OSI Approved :: Apache Software License", 10 | "Framework :: Datasette", 11 | ] 12 | dependencies = [ 13 | "datasette", 14 | "llm" 15 | ] 16 | 17 | [project.urls] 18 | Homepage = "https://github.com/simonw/datasette-llm-embed" 19 | Changelog = "https://github.com/simonw/datasette-llm-embed/releases" 20 | Issues = "https://github.com/simonw/datasette-llm-embed/issues" 21 | CI = "https://github.com/simonw/datasette-llm-embed/actions" 22 | 23 | [project.entry-points.datasette] 24 | llm_embed = "datasette_llm_embed" 25 | 26 | [project.optional-dependencies] 27 | test = ["pytest", "pytest-asyncio"] 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.8", "3.9", "3.10", "3.11"] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | cache: pip 23 | cache-dependency-path: setup.py 24 | - name: Install dependencies 25 | run: | 26 | pip install '.[test]' 27 | - name: Run tests 28 | run: | 29 | pytest 30 | deploy: 31 | runs-on: ubuntu-latest 32 | needs: [test] 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set up Python 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: "3.11" 39 | cache: pip 40 | cache-dependency-path: setup.py 41 | - name: Install dependencies 42 | run: | 43 | pip install setuptools wheel twine build 44 | - name: Publish 45 | env: 46 | TWINE_USERNAME: __token__ 47 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 48 | run: | 49 | python -m build 50 | twine upload dist/* 51 | 52 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from llm.plugins import pm 3 | import llm 4 | 5 | 6 | class EmbedDemo(llm.EmbeddingModel): 7 | model_id = "embed-demo" 8 | 9 | def embed_batch(self, texts): 10 | for text in texts: 11 | words = text.split()[:16] 12 | embedding = [len(word) for word in words] 13 | # Pad with 0 up to 16 words 14 | embedding += [0] * (16 - len(embedding)) 15 | yield embedding 16 | 17 | 18 | class EmbedKeyDemo(llm.EmbeddingModel): 19 | model_id = "embed-key-demo" 20 | 21 | def embed_batch(self, texts): 22 | for text in texts: 23 | words = text.split()[:15] 24 | embedding = [len(word) for word in words] 25 | # Pad with 0 up to 15 words 26 | embedding += [0] * (15 - len(embedding)) 27 | # Last word is the length of the key 28 | embedding.append(len(self.key)) 29 | yield embedding 30 | 31 | 32 | @pytest.fixture 33 | def embed_demo(): 34 | return EmbedDemo() 35 | 36 | 37 | @pytest.fixture 38 | def embed_key_demo(): 39 | return EmbedKeyDemo() 40 | 41 | 42 | @pytest.fixture(autouse=True) 43 | def register_embed_demo_model(embed_demo, embed_key_demo): 44 | class MockModelsPlugin: 45 | __name__ = "MockModelsPlugin" 46 | 47 | @llm.hookimpl 48 | def register_embedding_models(self, register): 49 | register(embed_demo) 50 | register(embed_key_demo) 51 | 52 | pm.register(MockModelsPlugin(), name="undo-mock-models-plugin") 53 | try: 54 | yield 55 | finally: 56 | pm.unregister(name="undo-mock-models-plugin") 57 | -------------------------------------------------------------------------------- /datasette_llm_embed.py: -------------------------------------------------------------------------------- 1 | from datasette import hookimpl 2 | import json 3 | import llm 4 | 5 | 6 | def llm_embed_factory(datasette): 7 | config = datasette.plugin_config("datasette-llm-embed") or {} 8 | keys = config.get("keys") or {} 9 | 10 | def llm_embed(model_id, text): 11 | try: 12 | model = llm.get_embedding_model(model_id) 13 | if model.model_id in keys: 14 | model.key = keys[model.model_id] 15 | return llm.encode(model.embed(text)) 16 | except Exception as e: 17 | return str(e) 18 | 19 | return llm_embed 20 | 21 | 22 | def llm_embed_cosine(a, b): 23 | try: 24 | return llm.cosine_similarity(llm.decode(a), llm.decode(b)) 25 | except Exception as e: 26 | return str(e) 27 | 28 | 29 | def llm_embed_decode(blob): 30 | return json.dumps(llm.decode(blob)) 31 | 32 | 33 | @hookimpl 34 | def prepare_connection(datasette, conn): 35 | conn.create_function("llm_embed_decode", 1, llm_embed_decode) 36 | conn.create_function("llm_embed", 2, llm_embed_factory(datasette)) 37 | conn.create_function("llm_embed_cosine", 2, llm_embed_cosine) 38 | conn.create_aggregate("llm_embed_average", 1, AverageVectorAgg) 39 | 40 | 41 | class AverageVectorAgg: 42 | with_scores = False 43 | 44 | def __init__(self): 45 | self.accumulated = [] 46 | self.vector_size = 0 47 | 48 | def step(self, embedding): 49 | vector = llm.decode(embedding) 50 | if len(self.accumulated) == 0: 51 | self.accumulated = list(vector) 52 | else: 53 | for i in range(len(self.accumulated)): 54 | self.accumulated[i] += vector[i] 55 | self.vector_size += 1 56 | 57 | def finalize(self): 58 | vector = [item / self.vector_size for item in self.accumulated] 59 | return llm.encode(vector) 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datasette-llm-embed 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/datasette-llm-embed.svg)](https://pypi.org/project/datasette-llm-embed/) 4 | [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-llm-embed?include_prereleases&label=changelog)](https://github.com/simonw/datasette-llm-embed/releases) 5 | [![Tests](https://github.com/simonw/datasette-llm-embed/workflows/Test/badge.svg)](https://github.com/simonw/datasette-llm-embed/actions?query=workflow%3ATest) 6 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-llm-embed/blob/main/LICENSE) 7 | 8 | Datasette plugin adding a `llm_embed(model_id, text)` SQL function. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | datasette install datasette-llm-embed 14 | ``` 15 | 16 | ## Usage 17 | 18 | Adds a SQL function that can be called like this: 19 | ```sql 20 | select llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some text') 21 | ``` 22 | This embeds the provided text using the specified embedding model and returns a binary blob, suitable for use with plugins such as [datasette-faiss](https://datasette.io/plugins/datasette-faiss). 23 | 24 | The models need to be installed using [LLM](https://llm.datasette.io/) plugins such as [llm-sentence-transformers](https://github.com/simonw/llm-sentence-transformers). 25 | 26 | Use `llm_embed_cosine(a, b)` to calculate cosine similarity between two vector blobs: 27 | 28 | ```sql 29 | select llm_embed_cosine( 30 | llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some text'), 31 | llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some other text') 32 | ) 33 | ``` 34 | 35 | The `llm_embed_decode()` function can be used to decode a binary BLOB into a JSON array of floats: 36 | 37 | ```sql 38 | select llm_embed_decode( 39 | llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some text') 40 | ) 41 | ``` 42 | 43 | ## Models that require API keys 44 | 45 | If your embedding model needs an API key - for example the `ada-002` model from OpenAI - you can configure that key in `metadata.yml` (or JSON) like this: 46 | 47 | ```yaml 48 | plugins: 49 | datasette-llm-embed: 50 | keys: 51 | ada-002: 52 | $env: OPENAI_API_KEY 53 | ``` 54 | The key here should be the full model ID of the model - not an alias. 55 | 56 | You can then set the `OPENAI_API_KEY` environment variable to the key you want to use before starting Datasette: 57 | ```bash 58 | export OPENAI_API_KEY=sk-1234567890 59 | ``` 60 | Once configured, calls like this will use the API key that has been provided: 61 | ```sql 62 | select llm_embed('ada-002', 'This is some text') 63 | ``` 64 | 65 | ## Development 66 | 67 | To set up this plugin locally, first checkout the code. Then create a new virtual environment: 68 | ```bash 69 | cd datasette-llm-embed 70 | python3 -m venv venv 71 | source venv/bin/activate 72 | ``` 73 | Now install the dependencies and test dependencies: 74 | ``` 75 | pip install -e '.[test]' 76 | ``` 77 | ``` 78 | To run the tests: 79 | ```bash 80 | pytest 81 | ``` 82 | -------------------------------------------------------------------------------- /tests/test_llm_embed.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from datasette.app import Datasette 3 | import llm 4 | import pytest 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_llm_embed(): 9 | ds = Datasette() 10 | await ds.invoke_startup() 11 | response = await ds.client.get( 12 | "/_memory.json", 13 | params={ 14 | "_shape": "array", 15 | "sql": "select llm_embed('embed-demo', 'hello world') as e", 16 | }, 17 | ) 18 | assert response.status_code == 200 19 | encoded = response.json()[0]["e"]["encoded"] 20 | assert llm.decode(base64.b64decode(encoded)) == ( 21 | 5.0, 22 | 5.0, 23 | 0, 24 | 0, 25 | 0, 26 | 0, 27 | 0, 28 | 0, 29 | 0, 30 | 0, 31 | 0, 32 | 0, 33 | 0, 34 | 0, 35 | 0, 36 | 0, 37 | ) 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_llm_embed_cosine(): 42 | ds = Datasette() 43 | response = await ds.client.get( 44 | "/_memory.json", 45 | params={ 46 | "_shape": "array", 47 | "sql": """ 48 | select llm_embed_cosine( 49 | llm_embed('embed-demo', 'hello world'), 50 | llm_embed('embed-demo', 'hello again') 51 | ) as cosine""", 52 | }, 53 | ) 54 | assert response.status_code == 200 55 | pytest.approx(response.json()[0]["cosine"], 0.0001) == 0.9999 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_llm_embed_uses_key(): 60 | ds = Datasette( 61 | metadata={ 62 | "plugins": { 63 | "datasette-llm-embed": { 64 | "keys": { 65 | "embed-key-demo": "default-key", 66 | "aliased": "alias", 67 | } 68 | } 69 | } 70 | } 71 | ) 72 | response = await ds.client.get( 73 | "/_memory.json", 74 | params={ 75 | "_shape": "array", 76 | "sql": "select llm_embed('embed-key-demo', 'hello world') as e", 77 | }, 78 | ) 79 | assert response.status_code == 200 80 | encoded = response.json()[0]["e"]["encoded"] 81 | assert llm.decode(base64.b64decode(encoded)) == ( 82 | 5.0, 83 | 5.0, 84 | 0, 85 | 0, 86 | 0, 87 | 0, 88 | 0, 89 | 0, 90 | 0, 91 | 0, 92 | 0, 93 | 0, 94 | 0, 95 | 0, 96 | 0, 97 | 11.0, 98 | ) 99 | 100 | 101 | @pytest.mark.asyncio 102 | async def test_llm_embed_decode(): 103 | ds = Datasette() 104 | response = await ds.client.get( 105 | "/_memory.json", 106 | params={ 107 | "_shape": "array", 108 | "_json": "decoded", 109 | "sql": """ 110 | select llm_embed_decode( 111 | llm_embed('embed-demo', 'hello world') 112 | ) as decoded""", 113 | }, 114 | ) 115 | assert response.status_code == 200 116 | assert response.json()[0]["decoded"] == [ 117 | 5.0, 118 | 5.0, 119 | 0.0, 120 | 0.0, 121 | 0.0, 122 | 0.0, 123 | 0.0, 124 | 0.0, 125 | 0.0, 126 | 0.0, 127 | 0.0, 128 | 0.0, 129 | 0.0, 130 | 0.0, 131 | 0.0, 132 | 0.0, 133 | ] 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------