openai-cookbook/examples/chatgpt/rag-quickstart/pinecone-retool/gpt-action-pinecone-retool-rag.ipynb

492 lines
422 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This notebook provides a step-by-step guide for using Pinecone as a vector database to store OpenAI embeddings. As an example, it demonstrates how to integrate this setup with Retool to create a REST endpoint, enabling seamless interaction with ChatGPT as an action. However, Retool is just one of many approaches available for connecting your Pinecone database to ChatGPT.\n",
"\n",
"[Pinecone](https://www.pinecone.io/) is a fully managed vector database designed for storing, indexing, and querying large-scale vector embeddings. It enables fast and efficient similarity searches, making it ideal for AI-powered applications like recommendation systems, semantic search, and natural language processing.\n",
"\n",
"[Retool](https://retool.com/) is a low-code platform that simplifies building custom internal tools by connecting to databases, APIs, and third-party services. It enables users to create powerful, user-friendly interfaces and workflows with minimal coding, making it ideal for streamlining business operations and integrating complex systems.\n",
"\n",
"\n",
"## Pre-requisites\n",
"\n",
"- A Pinecone account\n",
"- A Retool account\n",
"- A Custom GPT with actions enabled\n",
"- An OpenAI API key\n",
"\n",
"\n",
"## Table of Contents\n",
"\n",
"1. [Setup Pinecone](#setup-pinecone)\n",
"2. [Setup Noteboook](#setup-notebook)\n",
"3. [Prepare Data](#prepare-data)\n",
"4. [Create a Pinecone Index](#create-a-pinecone-index)\n",
"5. [Populate the Pinecone Index](#populate-the-pinecone-index)\n",
"4. [Create a Retool Workflow](#create-a-retool-app)\n",
"5. [Create a Custom GPT Action](#create-a-custom-gpt-action)\n",
"\n",
"\n",
"## Setup Pinecone\n",
"\n",
"If you haven't got a Pinecone account, sign up for an account. You're ready to move on to the next section once you get the following screen. Go to API Keys and create a new API key. \n",
"\n",
"\n",
"![Vectors in Pinecone](../../../../images/pinecone-dashboard.png)\n",
"\n",
"\n",
"## Setup Notebook \n",
"\n",
"Install required libraries from OpenAI and Pinecone."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"!pip install -qU openai pinecone"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Import the OpenAI and Pinecone libraries."
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"\n",
"from pinecone.grpc import PineconeGRPC as Pinecone\n",
"from pinecone import ServerlessSpec\n",
"from openai import OpenAI\n",
"client = OpenAI() \n",
"\n",
"pc = Pinecone(api_key=\"YOUR API KEY\")\n",
"## OpenAI key by default is set to the environment variable `OPENAI_API_KEY`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Prepare Data\n",
"\n",
"Define a sample dataset to embed store in Pinecone and to search over from ChatGPT. "
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"data = [\n",
" {\"id\": \"vec1\", \"text\": \"OpenAI is a leading AI research organization focused on advancing artificial intelligence.\"},\n",
" {\"id\": \"vec2\", \"text\": \"The ChatGPT platform is renowned for its natural language processing capabilities.\"},\n",
" {\"id\": \"vec3\", \"text\": \"Many users leverage ChatGPT for tasks like creative writing, coding assistance, and customer support.\"},\n",
" {\"id\": \"vec4\", \"text\": \"OpenAI has revolutionized AI development with innovations like GPT-4 and its user-friendly APIs.\"},\n",
" {\"id\": \"vec5\", \"text\": \"ChatGPT makes AI-powered conversations accessible to millions, enhancing productivity and creativity.\"},\n",
" {\"id\": \"vec6\", \"text\": \"OpenAI was founded in December 2015 as an organization dedicated to advancing digital intelligence for the benefit of humanity.\"}\n",
"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We are now ready to convert the text to embeddings. The example below is the most simple implementation of this function. If your text is longer than the context window of the model you are using, you will need to chunk the text into smaller pieces."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[-0.014436143450438976, 0.036902640014886856, -0.012699315324425697, -0.01584937982261181, -0.008578476496040821, 0.02739301137626171, 0.009020938538014889, 0.026151476427912712, 0.005854364484548569, 0.013630466535687447, -0.019455114379525185, -0.005824646912515163, 0.0029750606045126915, 0.002575524151325226, -0.02570241130888462, 0.011497404426336288, 0.01334649883210659, 0.008915276266634464, -0.0016864730278030038, -0.04960855841636658, 0.004081215243786573, -0.022558949887752533, -0.011193624697625637, -0.0034934673458337784, 0.013280459679663181, -0.005114726722240448, 0.01757960394024849, 0.01596825011074543, -0.06202390417456627, 0.02364199049770832, 0.043136727064847946, -0.0127917705103755, 0.006006254348903894, -0.020736271515488625, -0.024447668343782425, 0.03394408896565437, 0.025398630648851395, 0.01868906058371067, -0.005874176509678364, 0.024857111275196075, 0.041604623198509216, -0.028000570833683014, -0.016166366636753082, 0.0027885001618415117, 0.017672058194875717, -0.005068499594926834, 0.020207958295941353, -0.03222707286477089, -0.030562886968255043, 0.003688282798975706, 0.020353244617581367, -0.0037477179430425167, 0.04355937987565994, -0.014568221755325794, -0.027340179309248924, -0.02442125231027603, 0.038936641067266464, 0.00781902763992548, 0.054204877465963364, -0.019006047397851944, 0.008169034495949745, 0.03515920788049698, -0.03354785218834877, 0.0065477751195430756, 0.02216271683573723, -0.020366452634334564, 0.029849665239453316, 0.01204552873969078, -0.007310526445508003, 0.055895477533340454, -0.04625377431511879, -0.017751304432749748, -0.06028047576546669, 0.02583448961377144, 0.013894623145461082, 0.014013493433594704, 0.01617957465350628, 0.017064498737454414, -0.014277649112045765, -0.0006661692168563604, 0.010645500384271145, -0.017843760550022125, -0.006102011073380709, 0.0011631132801994681, 0.0049000997096300125, 0.02290235459804535, 0.03640074282884598, -0.05769174173474312, 0.02306084707379341, -0.01717016100883484, -0.03830266743898392, 0.056635115295648575, -0.0395970344543457, 0.004870382137596607, 0.020062673836946487, -0.01880793087184429, -0.01757960394024849, 0.015955042093992233, 0.0096945371478796, 0.01820037141442299, -0.006788817699998617, -0.01633806899189949, 0.02426275797188282, -0.0041967835277318954, -0.0400196835398674, -0.007713364902883768, -0.0073633575811982155, 0.008961503393948078, -0.027208101004362106, 0.06461263447999954, 0.017711682245135307, 0.001751686679199338, -0.04295181855559349, 0.0036156396381556988, -0.04281974211335182, 0.040917813777923584, -0.03537053242325783, 0.027472257614135742, 0.003179781837388873, -0.007039766293019056, -0.025636371225118637, -0.0017748003592714667, 0.022809898480772972, -0.05689927190542221, -0.010698331519961357, 0.030272314324975014, 0.01018983032554388, -0.005930309649556875, 0.00651805754750967, 0.02158157154917717, -0.02781566232442856, 0.05293692648410797, -0.015439937822520733, 0.02463257685303688, 0.006039273925125599, 0.025675995275378227, 0.02339104376733303, 0.03700830042362213, 0.015822963789105415, 0.01802866905927658, -0.017421109601855278, -0.0140267014503479, -0.03214782476425171, 0.003453843994066119, -0.03943853825330734, -0.0028429825324565172, 0.02442125231027603, 0.07295997440814972, 0.005418506916612387, -0.01853056624531746, -0.028528884053230286, 0.03106478415429592, -0.007402981165796518, -0.025966567918658257, -0.0002705538645386696, -0.032702554017305374, -0.010546441189944744, 0.005864270497113466, 0.002002635272219777, 0.026798659935593605, 0.03198933228850365, 0.009417173452675343, 0.05124632641673088, 0.0453820563852787, 0.008360547944903374, 0.042687661945819855, 0.003701490582898259, 0.021145714446902275, -0.06175975129008293, 0.03346860781311989, 0.035053543746471405, 0.011464384384453297, 0.0031880366150289774, -0.02343066595494747, -0.03193650022149086, 0.005785023793578148, 0.0037345101591199636, -0.006623719818890095, 0.016813550144433975, 0.03957061842083931, -0.0077926116064190865, 0.006699664983898401, 0.02657412737607956, 0.002826472744345665, 0.0130163030698
]
}
],
"source": [
"def embed(text):\n",
" text = text.replace(\"\\n\", \" \") # Ensure text doesn't have newlines\n",
" res = client.embeddings.create(input=[text], model=\"text-embedding-3-large\")\n",
" \n",
" return res.data[0].embedding\n",
"\n",
"doc_embeds = [embed(d[\"text\"]) for d in data]\n",
"\n",
"print(doc_embeds)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create a Pinecone Index\n",
"\n",
"The next step is to create a Pinecone index, we'll do this programmatically, alternatively you can do this from the Pinecone dashboard."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"def create_index():\n",
" index_name = \"openai-cookbook-pinecone-retool\"\n",
"\n",
" if not pc.has_index(index_name):\n",
" pc.create_index(\n",
" name=index_name,\n",
" dimension=3072,\n",
" metric=\"cosine\",\n",
" spec=ServerlessSpec(\n",
" cloud='aws',\n",
" region='us-east-1'\n",
" )\n",
" )\n",
" \n",
" return pc.Index(index_name)\n",
"\n",
"index = create_index()\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Populate the Pinecone Index\n",
"\n",
"Now that we've created the index, we can populate it with our embeddings. Before we do this we need to append the ID to the embeddings along with the raw text, this is so we can retrieve the original text when we query the index.\n",
"\n",
"When upserting vectors we choose a namespace, this is optional but can be useful if you want to store multiple datasets in the same index as it allows you to partition the data. For example if you needed to store a dataset of customer support queries and a dataset of product descriptions you could create two namespaces and query over each one separately."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"def append_vectors(data, doc_embeds):\n",
" vectors = []\n",
" for d, e in zip(data, doc_embeds):\n",
" vectors.append({\n",
" \"id\": d['id'],\n",
" \"values\": e,\n",
" \"metadata\": {'text': d['text']}\n",
" })\n",
"\n",
" return vectors\n",
"\n",
"vectors = append_vectors(data, doc_embeds)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"upserted_count: 6"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index.upsert(\n",
" vectors=vectors,\n",
" namespace=\"ns1\"\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You should now see the vectors in the Pinecone Dashboard. \n",
"\n",
"![Vectors in Pinecone](../../../../images/pinecone-dashboard-2.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The vectors should now be visible in the Pincone Dashbaord. \n",
"\n",
"To test the search functionality we can query the index. Below we are taking a sample question, running this through the same embedding function and then checking the index for matching vectors.\n",
"\n",
"`top_k` refers to the number of results we want to return.\n",
"`include_values` and `include_metadata` are used to return the embeddings and original text of the results."
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'matches': [{'id': 'vec6',\n",
" 'metadata': {'text': 'OpenAI was founded in December 2015 as an '\n",
" 'organization dedicated to advancing '\n",
" 'digital intelligence for the benefit of '\n",
" 'humanity.'},\n",
" 'score': 0.7864019,\n",
" 'sparse_values': {'indices': [], 'values': []},\n",
" 'values': []}],\n",
" 'namespace': 'ns1',\n",
" 'usage': {'read_units': 6}}\n"
]
}
],
"source": [
"query = \"When was OpenAI founded?\"\n",
"\n",
"x = embed(query)\n",
"\n",
"results = index.query(\n",
" namespace=\"ns1\",\n",
" vector=x,\n",
" top_k=1,\n",
" include_values=False,\n",
" include_metadata=True\n",
")\n",
"\n",
"print(results)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create a Retool Workflow\n",
"\n",
"Now we have a working vector database, we can create a Retool workflow to connect to it to run our queries from ChatGPT. \n",
"\n",
"Open Retool and create a new workflow. \n",
"\n",
"<img src=\"../../../../images/retool-new-workflow.png\" alt=\"Create Retool Workflowt\" width=\"500\"/>\n",
"\n",
"You should now see the following screen.\n",
"\n",
"![Retool Workflow 2](../../../../images/retool-workflow-1.png)\n",
"\n",
"In this example we'll be using Python to query the Pinecone index. To do this we'll need to import the `pinecone` and `openai` library. First switch to Python. \n",
"\n",
"<!--ARCADE EMBED START--><div style=\"position: relative; padding-bottom: calc(55.43981481481482% + 41px); height: 0; width: 100%;\"><iframe src=\"https://demo.arcade.software/DnaN9MnRjDaBL9HWKabX?embed&embed_mobile=inline&embed_desktop=inline&show_copy_link=true\" title=\"Cookbook - Retool Libraries\" frameborder=\"0\" loading=\"lazy\" webkitallowfullscreen mozallowfullscreen allowfullscreen allow=\"clipboard-write\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; color-scheme: light;\" ></iframe></div><!--ARCADE EMBED END-->\n",
"\n",
"We are now ready to add our code to the code block. \n",
"\n",
"Start by declaring the libraries we just imported to this workflow. \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```python\n",
"from pinecone import Pinecone\n",
"from openai import OpenAI\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We now need to set the API keys for Pinecone and OpenAI. You can put these directly in the code block or use [Retool Configuration Variables](https://docs.retool.com/org-users/guides/config-vars). Configuration variables are recommended as they are more secure, this shown below.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```python\n",
"client = OpenAI(api_key=retoolContext.configVars.openai_api_key) \n",
"pc = Pinecone(api_key=retoolContext.configVars.pinecone_api_key)\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can then reuse our OpenAI Embedding and Pinecone query functions from above in the Retool code snippet and return the results. Below is the completed code block. \n",
"\n",
"```startTrigger.data.query``` is a variable passed in from the start trigger of the workflow. This is where the user query from ChatGPT will be passed in."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```python\n",
"from pinecone import Pinecone\n",
"from openai import OpenAI\n",
"\n",
"client = OpenAI(api_key=retoolContext.configVars.openai_api_key) \n",
"pc = Pinecone(api_key=retoolContext.configVars.pinecone_api_key)\n",
"index = pc.Index(\"openai-cookbook-pinecone-retool\")\n",
"\n",
"\n",
"def embed(query):\n",
" res = client.embeddings.create(\n",
" input=query,\n",
" model=\"text-embedding-3-large\"\n",
" )\n",
" doc_embeds = [r.embedding for r in res.data] \n",
" return doc_embeds \n",
"\n",
"x = embed([startTrigger.data.query])\n",
"\n",
"results = index.query(\n",
" namespace=\"ns1\",\n",
" vector=x[0],\n",
" top_k=2,\n",
" include_values=False,\n",
" include_metadata=True\n",
")\n",
"\n",
"return results.to_dict()['matches']\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This should look like this in the UI. You can test this by clicking the run button at the top of the code block. You should see the results returned in the Data section at the bottom of the code block.\n",
"\n",
"![Retool Workflow 3](../../../../images/retool-workflow-2.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We now have a workflow with a start trigger that will take a user query pass this to our Vector_Search code block. This will return the top 2 results from the Pinecone index. Next we need to add a block that will take these results and respond to the start trigger request.\n",
"\n",
"\n",
"<!--ARCADE EMBED START--><div style=\"position: relative; padding-bottom: calc(55.43981481481482% + 41px); height: 0; width: 100%;\"><iframe src=\"https://demo.arcade.software/6lyRo3PP2iWq814KvY1f?embed&embed_mobile=tab&embed_desktop=inline&show_copy_link=true\" title=\"Cookbook - Retool Return\" frameborder=\"0\" loading=\"lazy\" webkitallowfullscreen mozallowfullscreen allowfullscreen allow=\"clipboard-write\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; color-scheme: light;\" ></iframe></div><!--ARCADE EMBED END-->"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally we need to configure the start trigger to support calling via API to allow it to be used as a ChatGPT action. \n",
"\n",
"Go to Triggers, and toggle the switch to enable the Webhook. Click on the Webhook to open the configuration screen. We can optionally add an Alias to better describe what this webhook will trigger. In this case we'll call it `vector_search`. This provides a more identifiable name in the URL. When complete click Save Changes.\n",
"\n",
"![Retool Workflow 4](../../../../images/retool-workflow-3.png)\n",
"\n",
"The final step is to deploy this workflow. Click the Deploy button at the top of the screen. The workflow is now accessible via API. You can test this by clicking the copy button next to the Alias URL, choosing Copy as cURL and then running this in the terminal.\n",
"\n",
"<img src=\"../../../../images/retool-workflow-4.png\" alt=\"Retool Workflow 5\" width=\"400\"/>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create a Custom GPT Action\n",
"\n",
"We now have a working Vector Database, and a way of querying this over API through the Retool Workflow. The next step is to connect the Retool Workflow to ChatGPT via an action. \n",
"\n",
"Go to you GPT, and create a new action. Below is an example of the OpenAPI spec required to connect to the Retool Workflow. You will need to replace the URL and API key with your own. \n",
"\n",
"```openapi\n",
"openapi: 3.1.0\n",
"info:\n",
" title: Vector Search API\n",
" description: An API for performing vector-based search queries.\n",
" version: 1.0.0\n",
"servers:\n",
" - url: YOUR_URL_HERE\n",
" description: Sandbox server for the Vector Search API\n",
"paths:\n",
" /url/vector-search:\n",
" post:\n",
" operationId: performVectorSearch\n",
" summary: Perform a vector-based search query.\n",
" description: Sends a query to the vector search API and retrieves results.\n",
" requestBody:\n",
" required: true\n",
" content:\n",
" application/json:\n",
" schema:\n",
" type: object\n",
" properties:\n",
" query:\n",
" type: string\n",
" description: The search query.\n",
" required:\n",
" - query\n",
" responses:\n",
" '200':\n",
" description: Successful response containing search results.\n",
" '400':\n",
" description: Bad Request. The input data is invalid.\n",
" '500':\n",
" description: Internal Server Error. Something went wrong on the server side.\n",
"```\n",
"\n",
"Under the Authentication section set the auth method to API Key. Paste in your API from the Retool Workflow trigger settings. Then set Auth Type to Custom and set the Custom Header Name to ```X-Workflow-Api-Key```\n",
"\n",
"<img src=\"../../../../images/chatgpt-auth-config.png\" alt=\"ChatGPT Auth Config\" width=\"400\"/>\n",
"\n",
"\n",
"Your setup is now complete. You can test this by sending a message to your GPT asking for information from the vector database. \n",
"\n",
"<img src=\"../../../../images/gpt-rag-result.png\" alt=\"GPT RAG Result\" width=\"600\"/>"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "myenv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.6"
}
},
"nbformat": 4,
"nbformat_minor": 2
}