jeevan commited on
Commit
008dc3a
·
1 Parent(s): c04cad4

app updated

Browse files
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .venv
.vscode/settings.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "python.analysis.extraPaths": [
3
+ "/Users/jeevan/Documents/Learnings/ai-engineering-bootcamp/week8day1prodapp/.venv/lib/python3.9/site-packages",
4
+ ],
5
+ "python.terminal.activateEnvironment": false,
6
+ "python.analysis.diagnosticSeverityOverrides": {
7
+ "reportMissingImports": "none"
8
+ },
9
+ }
Prototyping_LangChain_Application_with_Production_Minded_Changes_Assignment.ipynb ADDED
@@ -0,0 +1,811 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nbformat": 4,
3
+ "nbformat_minor": 0,
4
+ "metadata": {
5
+ "colab": {
6
+ "provenance": []
7
+ },
8
+ "kernelspec": {
9
+ "name": "python3",
10
+ "display_name": "Python 3"
11
+ },
12
+ "language_info": {
13
+ "name": "python"
14
+ }
15
+ },
16
+ "cells": [
17
+ {
18
+ "cell_type": "markdown",
19
+ "source": [
20
+ "# Prototyping LangChain Application with Production Minded Changes\n",
21
+ "\n",
22
+ "For our first breakout room we'll be exploring how to set-up a LangChain LCEL chain in a way that takes advantage of all of the amazing out of the box production ready features it offers.\n",
23
+ "\n",
24
+ "We'll also explore `Caching` and what makes it an invaluable tool when transitioning to production environments.\n"
25
+ ],
26
+ "metadata": {
27
+ "id": "8ZsP-j7w3zcL"
28
+ }
29
+ },
30
+ {
31
+ "cell_type": "markdown",
32
+ "source": [
33
+ "## Task 1: Dependencies and Set-Up\n",
34
+ "\n",
35
+ "Let's get everything we need - we're going to use very specific versioning today to try to mitigate potential env. issues!\n",
36
+ "\n",
37
+ "> NOTE: Dependency issues are a large portion of what you're going to be tackling as you integrate new technology into your work - please keep in mind that one of the things you should be passively learning throughout this course is ways to mitigate dependency issues."
38
+ ],
39
+ "metadata": {
40
+ "id": "PpeN9ND0HKa0"
41
+ }
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": 24,
46
+ "metadata": {
47
+ "id": "0P4IJUQF27jW"
48
+ },
49
+ "outputs": [],
50
+ "source": [
51
+ "!pip install -qU langchain_openai==0.2.0 langchain_community==0.3.0 langchain==0.3.0 pymupdf==1.24.10 qdrant-client==1.11.2 langchain_qdrant==0.1.4 langsmith==0.1.121"
52
+ ]
53
+ },
54
+ {
55
+ "cell_type": "markdown",
56
+ "source": [
57
+ "We'll need an OpenAI API Key:"
58
+ ],
59
+ "metadata": {
60
+ "id": "qYcWLzrmHgDb"
61
+ }
62
+ },
63
+ {
64
+ "cell_type": "code",
65
+ "source": [
66
+ "import os\n",
67
+ "import getpass\n",
68
+ "\n",
69
+ "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"
70
+ ],
71
+ "metadata": {
72
+ "colab": {
73
+ "base_uri": "https://localhost:8080/"
74
+ },
75
+ "id": "GZ8qfrFh_6ed",
76
+ "outputId": "4fb1a16f-1f71-4d0a-aad4-dd0d0917abc5"
77
+ },
78
+ "execution_count": 1,
79
+ "outputs": [
80
+ {
81
+ "name": "stdout",
82
+ "output_type": "stream",
83
+ "text": [
84
+ "OpenAI API Key:··········\n"
85
+ ]
86
+ }
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "markdown",
91
+ "source": [
92
+ "And the LangSmith set-up:"
93
+ ],
94
+ "metadata": {
95
+ "id": "piz2DUDuHiSO"
96
+ }
97
+ },
98
+ {
99
+ "cell_type": "code",
100
+ "source": [
101
+ "import uuid\n",
102
+ "\n",
103
+ "os.environ[\"LANGCHAIN_PROJECT\"] = f\"AIM Week 8 Assignment 1 - {uuid.uuid4().hex[0:8]}\"\n",
104
+ "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
105
+ "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
106
+ "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangChain API Key:\")"
107
+ ],
108
+ "metadata": {
109
+ "colab": {
110
+ "base_uri": "https://localhost:8080/"
111
+ },
112
+ "id": "wLZX5zowCh-q",
113
+ "outputId": "565c588a-a865-4b86-d5ca-986f35153000"
114
+ },
115
+ "execution_count": 16,
116
+ "outputs": [
117
+ {
118
+ "name": "stdout",
119
+ "output_type": "stream",
120
+ "text": [
121
+ "LangChain API Key:··········\n"
122
+ ]
123
+ }
124
+ ]
125
+ },
126
+ {
127
+ "cell_type": "markdown",
128
+ "source": [
129
+ "Let's verify our project so we can leverage it in LangSmith later."
130
+ ],
131
+ "metadata": {
132
+ "id": "WmwNTziKHrQm"
133
+ }
134
+ },
135
+ {
136
+ "cell_type": "code",
137
+ "source": [
138
+ "print(os.environ[\"LANGCHAIN_PROJECT\"])"
139
+ ],
140
+ "metadata": {
141
+ "colab": {
142
+ "base_uri": "https://localhost:8080/"
143
+ },
144
+ "id": "T6GZmkVkFcHq",
145
+ "outputId": "f4c0fdb3-24ea-429a-fa8c-23556cb7c3ed"
146
+ },
147
+ "execution_count": 17,
148
+ "outputs": [
149
+ {
150
+ "output_type": "stream",
151
+ "name": "stdout",
152
+ "text": [
153
+ "AIM Week 8 Assignment 1 - 215a8497\n"
154
+ ]
155
+ }
156
+ ]
157
+ },
158
+ {
159
+ "cell_type": "markdown",
160
+ "source": [
161
+ "## Task 2: Setting up RAG With Production in Mind\n",
162
+ "\n",
163
+ "This is the most crucial step in the process - in order to take advantage of:\n",
164
+ "\n",
165
+ "- Asyncronous requests\n",
166
+ "- Parallel Execution in Chains\n",
167
+ "- And more...\n",
168
+ "\n",
169
+ "You must...use LCEL. These benefits are provided out of the box and largely optimized behind the scenes."
170
+ ],
171
+ "metadata": {
172
+ "id": "un_ppfaAHv1J"
173
+ }
174
+ },
175
+ {
176
+ "cell_type": "markdown",
177
+ "source": [
178
+ "### Building our RAG Components: Retriever\n",
179
+ "\n",
180
+ "We'll start by building some familiar components - and showcase how they automatically scale to production features."
181
+ ],
182
+ "metadata": {
183
+ "id": "vGi-db23JMAL"
184
+ }
185
+ },
186
+ {
187
+ "cell_type": "markdown",
188
+ "source": [
189
+ "Please upload a PDF file to use in this example!"
190
+ ],
191
+ "metadata": {
192
+ "id": "zvbT3HSDJemE"
193
+ }
194
+ },
195
+ {
196
+ "cell_type": "code",
197
+ "source": [
198
+ "from google.colab import files\n",
199
+ "uploaded = files.upload()"
200
+ ],
201
+ "metadata": {
202
+ "colab": {
203
+ "base_uri": "https://localhost:8080/",
204
+ "height": 73
205
+ },
206
+ "id": "dvYczNeY91Hn",
207
+ "outputId": "c711c29b-e388-4d32-a763-f4504244eef2"
208
+ },
209
+ "execution_count": 7,
210
+ "outputs": [
211
+ {
212
+ "output_type": "display_data",
213
+ "data": {
214
+ "text/plain": [
215
+ "<IPython.core.display.HTML object>"
216
+ ],
217
+ "text/html": [
218
+ "\n",
219
+ " <input type=\"file\" id=\"files-f26e85ad-7ad3-48c1-b905-c2692a72d40e\" name=\"files[]\" multiple disabled\n",
220
+ " style=\"border:none\" />\n",
221
+ " <output id=\"result-f26e85ad-7ad3-48c1-b905-c2692a72d40e\">\n",
222
+ " Upload widget is only available when the cell has been executed in the\n",
223
+ " current browser session. Please rerun this cell to enable.\n",
224
+ " </output>\n",
225
+ " <script>// Copyright 2017 Google LLC\n",
226
+ "//\n",
227
+ "// Licensed under the Apache License, Version 2.0 (the \"License\");\n",
228
+ "// you may not use this file except in compliance with the License.\n",
229
+ "// You may obtain a copy of the License at\n",
230
+ "//\n",
231
+ "// http://www.apache.org/licenses/LICENSE-2.0\n",
232
+ "//\n",
233
+ "// Unless required by applicable law or agreed to in writing, software\n",
234
+ "// distributed under the License is distributed on an \"AS IS\" BASIS,\n",
235
+ "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
236
+ "// See the License for the specific language governing permissions and\n",
237
+ "// limitations under the License.\n",
238
+ "\n",
239
+ "/**\n",
240
+ " * @fileoverview Helpers for google.colab Python module.\n",
241
+ " */\n",
242
+ "(function(scope) {\n",
243
+ "function span(text, styleAttributes = {}) {\n",
244
+ " const element = document.createElement('span');\n",
245
+ " element.textContent = text;\n",
246
+ " for (const key of Object.keys(styleAttributes)) {\n",
247
+ " element.style[key] = styleAttributes[key];\n",
248
+ " }\n",
249
+ " return element;\n",
250
+ "}\n",
251
+ "\n",
252
+ "// Max number of bytes which will be uploaded at a time.\n",
253
+ "const MAX_PAYLOAD_SIZE = 100 * 1024;\n",
254
+ "\n",
255
+ "function _uploadFiles(inputId, outputId) {\n",
256
+ " const steps = uploadFilesStep(inputId, outputId);\n",
257
+ " const outputElement = document.getElementById(outputId);\n",
258
+ " // Cache steps on the outputElement to make it available for the next call\n",
259
+ " // to uploadFilesContinue from Python.\n",
260
+ " outputElement.steps = steps;\n",
261
+ "\n",
262
+ " return _uploadFilesContinue(outputId);\n",
263
+ "}\n",
264
+ "\n",
265
+ "// This is roughly an async generator (not supported in the browser yet),\n",
266
+ "// where there are multiple asynchronous steps and the Python side is going\n",
267
+ "// to poll for completion of each step.\n",
268
+ "// This uses a Promise to block the python side on completion of each step,\n",
269
+ "// then passes the result of the previous step as the input to the next step.\n",
270
+ "function _uploadFilesContinue(outputId) {\n",
271
+ " const outputElement = document.getElementById(outputId);\n",
272
+ " const steps = outputElement.steps;\n",
273
+ "\n",
274
+ " const next = steps.next(outputElement.lastPromiseValue);\n",
275
+ " return Promise.resolve(next.value.promise).then((value) => {\n",
276
+ " // Cache the last promise value to make it available to the next\n",
277
+ " // step of the generator.\n",
278
+ " outputElement.lastPromiseValue = value;\n",
279
+ " return next.value.response;\n",
280
+ " });\n",
281
+ "}\n",
282
+ "\n",
283
+ "/**\n",
284
+ " * Generator function which is called between each async step of the upload\n",
285
+ " * process.\n",
286
+ " * @param {string} inputId Element ID of the input file picker element.\n",
287
+ " * @param {string} outputId Element ID of the output display.\n",
288
+ " * @return {!Iterable<!Object>} Iterable of next steps.\n",
289
+ " */\n",
290
+ "function* uploadFilesStep(inputId, outputId) {\n",
291
+ " const inputElement = document.getElementById(inputId);\n",
292
+ " inputElement.disabled = false;\n",
293
+ "\n",
294
+ " const outputElement = document.getElementById(outputId);\n",
295
+ " outputElement.innerHTML = '';\n",
296
+ "\n",
297
+ " const pickedPromise = new Promise((resolve) => {\n",
298
+ " inputElement.addEventListener('change', (e) => {\n",
299
+ " resolve(e.target.files);\n",
300
+ " });\n",
301
+ " });\n",
302
+ "\n",
303
+ " const cancel = document.createElement('button');\n",
304
+ " inputElement.parentElement.appendChild(cancel);\n",
305
+ " cancel.textContent = 'Cancel upload';\n",
306
+ " const cancelPromise = new Promise((resolve) => {\n",
307
+ " cancel.onclick = () => {\n",
308
+ " resolve(null);\n",
309
+ " };\n",
310
+ " });\n",
311
+ "\n",
312
+ " // Wait for the user to pick the files.\n",
313
+ " const files = yield {\n",
314
+ " promise: Promise.race([pickedPromise, cancelPromise]),\n",
315
+ " response: {\n",
316
+ " action: 'starting',\n",
317
+ " }\n",
318
+ " };\n",
319
+ "\n",
320
+ " cancel.remove();\n",
321
+ "\n",
322
+ " // Disable the input element since further picks are not allowed.\n",
323
+ " inputElement.disabled = true;\n",
324
+ "\n",
325
+ " if (!files) {\n",
326
+ " return {\n",
327
+ " response: {\n",
328
+ " action: 'complete',\n",
329
+ " }\n",
330
+ " };\n",
331
+ " }\n",
332
+ "\n",
333
+ " for (const file of files) {\n",
334
+ " const li = document.createElement('li');\n",
335
+ " li.append(span(file.name, {fontWeight: 'bold'}));\n",
336
+ " li.append(span(\n",
337
+ " `(${file.type || 'n/a'}) - ${file.size} bytes, ` +\n",
338
+ " `last modified: ${\n",
339
+ " file.lastModifiedDate ? file.lastModifiedDate.toLocaleDateString() :\n",
340
+ " 'n/a'} - `));\n",
341
+ " const percent = span('0% done');\n",
342
+ " li.appendChild(percent);\n",
343
+ "\n",
344
+ " outputElement.appendChild(li);\n",
345
+ "\n",
346
+ " const fileDataPromise = new Promise((resolve) => {\n",
347
+ " const reader = new FileReader();\n",
348
+ " reader.onload = (e) => {\n",
349
+ " resolve(e.target.result);\n",
350
+ " };\n",
351
+ " reader.readAsArrayBuffer(file);\n",
352
+ " });\n",
353
+ " // Wait for the data to be ready.\n",
354
+ " let fileData = yield {\n",
355
+ " promise: fileDataPromise,\n",
356
+ " response: {\n",
357
+ " action: 'continue',\n",
358
+ " }\n",
359
+ " };\n",
360
+ "\n",
361
+ " // Use a chunked sending to avoid message size limits. See b/62115660.\n",
362
+ " let position = 0;\n",
363
+ " do {\n",
364
+ " const length = Math.min(fileData.byteLength - position, MAX_PAYLOAD_SIZE);\n",
365
+ " const chunk = new Uint8Array(fileData, position, length);\n",
366
+ " position += length;\n",
367
+ "\n",
368
+ " const base64 = btoa(String.fromCharCode.apply(null, chunk));\n",
369
+ " yield {\n",
370
+ " response: {\n",
371
+ " action: 'append',\n",
372
+ " file: file.name,\n",
373
+ " data: base64,\n",
374
+ " },\n",
375
+ " };\n",
376
+ "\n",
377
+ " let percentDone = fileData.byteLength === 0 ?\n",
378
+ " 100 :\n",
379
+ " Math.round((position / fileData.byteLength) * 100);\n",
380
+ " percent.textContent = `${percentDone}% done`;\n",
381
+ "\n",
382
+ " } while (position < fileData.byteLength);\n",
383
+ " }\n",
384
+ "\n",
385
+ " // All done.\n",
386
+ " yield {\n",
387
+ " response: {\n",
388
+ " action: 'complete',\n",
389
+ " }\n",
390
+ " };\n",
391
+ "}\n",
392
+ "\n",
393
+ "scope.google = scope.google || {};\n",
394
+ "scope.google.colab = scope.google.colab || {};\n",
395
+ "scope.google.colab._files = {\n",
396
+ " _uploadFiles,\n",
397
+ " _uploadFilesContinue,\n",
398
+ "};\n",
399
+ "})(self);\n",
400
+ "</script> "
401
+ ]
402
+ },
403
+ "metadata": {}
404
+ },
405
+ {
406
+ "output_type": "stream",
407
+ "name": "stdout",
408
+ "text": [
409
+ "Saving eu_ai_act.html to eu_ai_act (1).html\n"
410
+ ]
411
+ }
412
+ ]
413
+ },
414
+ {
415
+ "cell_type": "code",
416
+ "source": [
417
+ "file_path = list(uploaded.keys())[0]\n",
418
+ "file_path"
419
+ ],
420
+ "metadata": {
421
+ "colab": {
422
+ "base_uri": "https://localhost:8080/",
423
+ "height": 35
424
+ },
425
+ "id": "NtwoVUbaJlbW",
426
+ "outputId": "5aa08bae-97c5-4f49-cb23-e9dbf194ecf7"
427
+ },
428
+ "execution_count": 25,
429
+ "outputs": [
430
+ {
431
+ "output_type": "execute_result",
432
+ "data": {
433
+ "text/plain": [
434
+ "'eu_ai_act (1).html'"
435
+ ],
436
+ "application/vnd.google.colaboratory.intrinsic+json": {
437
+ "type": "string"
438
+ }
439
+ },
440
+ "metadata": {},
441
+ "execution_count": 25
442
+ }
443
+ ]
444
+ },
445
+ {
446
+ "cell_type": "markdown",
447
+ "source": [
448
+ "We'll define our chunking strategy."
449
+ ],
450
+ "metadata": {
451
+ "id": "kucGy3f0Jhdi"
452
+ }
453
+ },
454
+ {
455
+ "cell_type": "code",
456
+ "source": [
457
+ "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
458
+ "\n",
459
+ "text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)"
460
+ ],
461
+ "metadata": {
462
+ "id": "G-DNvNFd8je5"
463
+ },
464
+ "execution_count": 18,
465
+ "outputs": []
466
+ },
467
+ {
468
+ "cell_type": "markdown",
469
+ "source": [
470
+ "We'll chunk our uploaded PDF file."
471
+ ],
472
+ "metadata": {
473
+ "id": "3_zRRNcLKCZh"
474
+ }
475
+ },
476
+ {
477
+ "cell_type": "code",
478
+ "source": [
479
+ "from langchain_community.document_loaders import PyMuPDFLoader\n",
480
+ "\n",
481
+ "Loader = PyMuPDFLoader\n",
482
+ "loader = Loader(file_path)\n",
483
+ "documents = loader.load()\n",
484
+ "docs = text_splitter.split_documents(documents)\n",
485
+ "for i, doc in enumerate(docs):\n",
486
+ " doc.metadata[\"source\"] = f\"source_{i}\""
487
+ ],
488
+ "metadata": {
489
+ "id": "KOh6w9ud-ff6"
490
+ },
491
+ "execution_count": 26,
492
+ "outputs": []
493
+ },
494
+ {
495
+ "cell_type": "markdown",
496
+ "source": [
497
+ "#### QDrant Vector Database - Cache Backed Embeddings\n",
498
+ "\n",
499
+ "The process of embedding is typically a very time consuming one - we must, for ever single vector in our VDB as well as query:\n",
500
+ "\n",
501
+ "1. Send the text to an API endpoint (self-hosted, OpenAI, etc)\n",
502
+ "2. Wait for processing\n",
503
+ "3. Receive response\n",
504
+ "\n",
505
+ "This process costs time, and money - and occurs *every single time a document gets converted into a vector representation*.\n",
506
+ "\n",
507
+ "Instead, what if we:\n",
508
+ "\n",
509
+ "1. Set up a cache that can hold our vectors and embeddings (similar to, or in some cases literally a vector database)\n",
510
+ "2. Send the text to an API endpoint (self-hosted, OpenAI, etc)\n",
511
+ "3. Check the cache to see if we've already converted this text before.\n",
512
+ " - If we have: Return the vector representation\n",
513
+ " - Else: Wait for processing and proceed\n",
514
+ "4. Store the text that was converted alongside its vector representation in a cache of some kind.\n",
515
+ "5. Return the vector representation\n",
516
+ "\n",
517
+ "Notice that we can shortcut some instances of \"Wait for processing and proceed\".\n",
518
+ "\n",
519
+ "Let's see how this is implemented in the code."
520
+ ],
521
+ "metadata": {
522
+ "id": "U4XLeqJMKGdQ"
523
+ }
524
+ },
525
+ {
526
+ "cell_type": "code",
527
+ "source": [
528
+ "from qdrant_client import QdrantClient\n",
529
+ "from qdrant_client.http.models import Distance, VectorParams\n",
530
+ "from langchain_openai.embeddings import OpenAIEmbeddings\n",
531
+ "from langchain.storage import LocalFileStore\n",
532
+ "from langchain_qdrant import QdrantVectorStore\n",
533
+ "from langchain.embeddings import CacheBackedEmbeddings\n",
534
+ "\n",
535
+ "# Typical Embedding Model\n",
536
+ "core_embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
537
+ "\n",
538
+ "# Typical QDrant Client Set-up\n",
539
+ "collection_name = f\"pdf_to_parse_{uuid.uuid4()}\"\n",
540
+ "client = QdrantClient(\":memory:\")\n",
541
+ "client.create_collection(\n",
542
+ " collection_name=collection_name,\n",
543
+ " vectors_config=VectorParams(size=1536, distance=Distance.COSINE),\n",
544
+ ")\n",
545
+ "\n",
546
+ "# Adding cache!\n",
547
+ "store = LocalFileStore(\"./cache/\")\n",
548
+ "cached_embedder = CacheBackedEmbeddings.from_bytes_store(\n",
549
+ " core_embeddings, store, namespace=core_embeddings.model\n",
550
+ ")\n",
551
+ "\n",
552
+ "# Typical QDrant Vector Store Set-up\n",
553
+ "vectorstore = QdrantVectorStore(\n",
554
+ " client=client,\n",
555
+ " collection_name=collection_name,\n",
556
+ " embedding=cached_embedder)\n",
557
+ "vectorstore.add_documents(docs)\n",
558
+ "retriever = vectorstore.as_retriever(search_type=\"mmr\", search_kwargs={\"k\": 3})"
559
+ ],
560
+ "metadata": {
561
+ "id": "dzPUTCua98b2"
562
+ },
563
+ "execution_count": 9,
564
+ "outputs": []
565
+ },
566
+ {
567
+ "cell_type": "markdown",
568
+ "source": [
569
+ "##### ❓ Question #1:\n",
570
+ "\n",
571
+ "What are some limitations you can see with this approach? When is this most/least useful. Discuss with your group!\n",
572
+ "\n",
573
+ "> NOTE: There is no single correct answer here!"
574
+ ],
575
+ "metadata": {
576
+ "id": "QVZGvmNYLomp"
577
+ }
578
+ },
579
+ {
580
+ "cell_type": "markdown",
581
+ "source": [
582
+ "##### 🏗️ Activity #1:\n",
583
+ "\n",
584
+ "Create a simple experiment that tests the cache-backed embeddings."
585
+ ],
586
+ "metadata": {
587
+ "id": "vZAOhyb3L9iD"
588
+ }
589
+ },
590
+ {
591
+ "cell_type": "code",
592
+ "source": [
593
+ "### YOUR CODE HERE"
594
+ ],
595
+ "metadata": {
596
+ "id": "M_Mekif6MDqe"
597
+ },
598
+ "execution_count": null,
599
+ "outputs": []
600
+ },
601
+ {
602
+ "cell_type": "markdown",
603
+ "source": [
604
+ "### Augmentation\n",
605
+ "\n",
606
+ "We'll create the classic RAG Prompt and create our `ChatPromptTemplates` as per usual."
607
+ ],
608
+ "metadata": {
609
+ "id": "DH0i-YovL8kZ"
610
+ }
611
+ },
612
+ {
613
+ "cell_type": "code",
614
+ "source": [
615
+ "from langchain_core.prompts import ChatPromptTemplate\n",
616
+ "\n",
617
+ "rag_system_prompt_template = \"\"\"\\\n",
618
+ "You are a helpful assistant that uses the provided context to answer questions. Never reference this prompt, or the existance of context.\n",
619
+ "\"\"\"\n",
620
+ "\n",
621
+ "rag_message_list = [\n",
622
+ " {\"role\" : \"system\", \"content\" : rag_system_prompt_template},\n",
623
+ "]\n",
624
+ "\n",
625
+ "rag_user_prompt_template = \"\"\"\\\n",
626
+ "Question:\n",
627
+ "{question}\n",
628
+ "Context:\n",
629
+ "{context}\n",
630
+ "\"\"\"\n",
631
+ "\n",
632
+ "chat_prompt = ChatPromptTemplate.from_messages([\n",
633
+ " (\"system\", rag_system_prompt_template),\n",
634
+ " (\"human\", rag_user_prompt_template)\n",
635
+ "])"
636
+ ],
637
+ "metadata": {
638
+ "id": "WchaoMEx9j69"
639
+ },
640
+ "execution_count": 27,
641
+ "outputs": []
642
+ },
643
+ {
644
+ "cell_type": "markdown",
645
+ "source": [
646
+ "### Generation\n",
647
+ "\n",
648
+ "Like usual, we'll set-up a `ChatOpenAI` model - and we'll use the fan favourite `gpt-4o-mini` for today.\n",
649
+ "\n",
650
+ "However, we'll also implement...a PROMPT CACHE!\n",
651
+ "\n",
652
+ "In essence, this works in a very similar way to the embedding cache - if we've seen this prompt before, we just use the stored response."
653
+ ],
654
+ "metadata": {
655
+ "id": "UQKnByVWMpiK"
656
+ }
657
+ },
658
+ {
659
+ "cell_type": "code",
660
+ "source": [
661
+ "from langchain_core.globals import set_llm_cache\n",
662
+ "from langchain_openai import ChatOpenAI\n",
663
+ "\n",
664
+ "chat_model = ChatOpenAI(model=\"gpt-4o-mini\")"
665
+ ],
666
+ "metadata": {
667
+ "id": "fOXKkaY7ABab"
668
+ },
669
+ "execution_count": 11,
670
+ "outputs": []
671
+ },
672
+ {
673
+ "cell_type": "markdown",
674
+ "source": [
675
+ "Setting up the cache can be done as follows:"
676
+ ],
677
+ "metadata": {
678
+ "id": "mhv8IqZoM9cY"
679
+ }
680
+ },
681
+ {
682
+ "cell_type": "code",
683
+ "source": [
684
+ "from langchain_core.caches import InMemoryCache\n",
685
+ "\n",
686
+ "set_llm_cache(InMemoryCache())"
687
+ ],
688
+ "metadata": {
689
+ "id": "thqam26gAyzN"
690
+ },
691
+ "execution_count": 12,
692
+ "outputs": []
693
+ },
694
+ {
695
+ "cell_type": "markdown",
696
+ "source": [
697
+ "##### ❓ Question #2:\n",
698
+ "\n",
699
+ "What are some limitations you can see with this approach? When is this most/least useful. Discuss with your group!\n",
700
+ "\n",
701
+ "> NOTE: There is no single correct answer here!"
702
+ ],
703
+ "metadata": {
704
+ "id": "CvxEovcEM_oA"
705
+ }
706
+ },
707
+ {
708
+ "cell_type": "markdown",
709
+ "source": [
710
+ "##### 🏗️ Activity #2:\n",
711
+ "\n",
712
+ "Create a simple experiment that tests the cache-backed embeddings."
713
+ ],
714
+ "metadata": {
715
+ "id": "3iCMjVYKNEeV"
716
+ }
717
+ },
718
+ {
719
+ "cell_type": "code",
720
+ "source": [
721
+ "### YOUR CODE HERE"
722
+ ],
723
+ "metadata": {
724
+ "id": "QT5GfmsHNFqP"
725
+ },
726
+ "execution_count": null,
727
+ "outputs": []
728
+ },
729
+ {
730
+ "cell_type": "markdown",
731
+ "source": [
732
+ "## Task 3: RAG LCEL Chain\n",
733
+ "\n",
734
+ "We'll also set-up our typical RAG chain using LCEL.\n",
735
+ "\n",
736
+ "However, this time: We'll specifically call out that the `context` and `question` halves of the first \"link\" in the chain are executed *in parallel* by default!\n",
737
+ "\n",
738
+ "Thanks, LCEL!"
739
+ ],
740
+ "metadata": {
741
+ "id": "zyPnNWb9NH7W"
742
+ }
743
+ },
744
+ {
745
+ "cell_type": "code",
746
+ "source": [
747
+ "from operator import itemgetter\n",
748
+ "from langchain_core.runnables.passthrough import RunnablePassthrough\n",
749
+ "\n",
750
+ "retrieval_augmented_qa_chain = (\n",
751
+ " {\"context\": itemgetter(\"question\") | retriever, \"question\": itemgetter(\"question\")}\n",
752
+ " | RunnablePassthrough.assign(context=itemgetter(\"context\"))\n",
753
+ " | chat_prompt | chat_model\n",
754
+ " )"
755
+ ],
756
+ "metadata": {
757
+ "id": "3JNvSsx_CEtI"
758
+ },
759
+ "execution_count": 13,
760
+ "outputs": []
761
+ },
762
+ {
763
+ "cell_type": "markdown",
764
+ "source": [
765
+ "Let's test it out!"
766
+ ],
767
+ "metadata": {
768
+ "id": "Sx--wVctNdGa"
769
+ }
770
+ },
771
+ {
772
+ "cell_type": "code",
773
+ "source": [
774
+ "retrieval_augmented_qa_chain.invoke({\"question\" : \"Write 50 things about this document!\"})"
775
+ ],
776
+ "metadata": {
777
+ "colab": {
778
+ "base_uri": "https://localhost:8080/"
779
+ },
780
+ "id": "43uQegbnDQKP",
781
+ "outputId": "a9ff032b-4eb2-4f5f-f456-1fc6aa24aaec"
782
+ },
783
+ "execution_count": 14,
784
+ "outputs": [
785
+ {
786
+ "output_type": "execute_result",
787
+ "data": {
788
+ "text/plain": [
789
+ "AIMessage(content='1. The document is a PDF with a total of 19 pages.\\n2. It was created using LaTeX with hyperref.\\n3. The document has a metadata source labeled as \\'source_15\\'.\\n4. The file path of the document is \\'2406.11200v2.pdf\\'.\\n5. The document was created on June 19, 2024.\\n6. It is formatted as PDF 1.5.\\n7. The metadata includes an ID: \\'8d631703366f48b6b910115cce8ce3c5\\'.\\n8. There are no specific titles or authors listed in the metadata.\\n9. The document includes actions to parse queries into multiple aspects.\\n10. It mentions a subquery structure with attributes like brand, type, material, and features.\\n11. The brand specified in the subquery is \"TUSA\".\\n12. The type mentioned is \"swim fins\".\\n13. It discusses using an embedding tool to filter entities.\\n14. The document includes a step for computing similarity scores based on types.\\n15. There is an action to retrieve the top-20 entities with the highest similarity scores.\\n16. It lists an action to get brands of the top-20 entities.\\n17. There is a token match scoring action for brand matching.\\n18. The document utilizes an LLM reasoning API for functionality validation.\\n19. It synthesizes final scores using optimized parameters.\\n20. There is a section for acknowledgments thanking various labs and funding agencies.\\n21. The acknowledgments include support from DARPA and NSF.\\n22. It appreciates the contributions from Stanford Data Applications Initiative.\\n23. The Chan Zuckerberg Initiative is also acknowledged.\\n24. The document references multiple organizations, including Amazon and Genentech.\\n25. It emphasizes that the content does not necessarily represent the views of funding entities.\\n26. A reference is included for a paper titled \"Graph of Thoughts: Solving Elaborate Problems with Large Language Models.\"\\n27. The reference lists multiple authors and an arXiv identifier.\\n28. The document outlines a function named \\'ParseAttributeFromQuery\\'.\\n29. It details the inputs for the \\'ParseAttributeFromQuery\\' function.\\n30. The output of the function is described as a dictionary based on attributes.\\n31. Another function, \\'GetBagOfPhrases\\', is mentioned.\\n32. The \\'GetBagOfPhrases\\' function takes image IDs as input.\\n33. This function returns a list of phrases for each image.\\n34. The document describes the \\'GetEntityDocuments\\' function.\\n35. The \\'GetEntityDocuments\\' function also uses image IDs as input.\\n36. It returns text information related to each image.\\n37. The document includes a function named \\'GetClipTextEmbedding\\'.\\n38. This function is designed to embed strings into embeddings.\\n39. The \\'GetPatchIdToPhraseDict\\' function is outlined as well.\\n40. It returns a dictionary mapping patch IDs to phrases.\\n41. The document describes a function named \\'GetImages\\'.\\n42. \\'GetImages\\' returns a list of images corresponding to input IDs.\\n43. The \\'GetClipImageEmbedding\\' function is also mentioned.\\n44. It embeds a list of images into corresponding embeddings.\\n45. The document is structured to provide a clear methodology for processing queries.\\n46. It emphasizes the use of various entity and image-related functions.\\n47. The functions are designed to handle different types of data inputs.\\n48. The document appears to focus on data processing and machine learning applications.\\n49. It presents a systematic approach to extracting and analyzing query-related data.\\n50. Overall, the document serves as a technical resource outlining specific functions and methodologies related to data processing and entity recognition.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 749, 'prompt_tokens': 1389, 'total_tokens': 2138, 'completion_tokens_details': {'audio_tokens': None, 'reasoning_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_1bb46167f9', 'finish_reason': 'stop', 'logprobs': None}, id='run-9445763c-0461-43ae-9245-1490fddbac8f-0', usage_metadata={'input_tokens': 1389, 'output_tokens': 749, 'total_tokens': 2138})"
790
+ ]
791
+ },
792
+ "metadata": {},
793
+ "execution_count": 14
794
+ }
795
+ ]
796
+ },
797
+ {
798
+ "cell_type": "markdown",
799
+ "source": [
800
+ "##### 🏗️ Activity #3:\n",
801
+ "\n",
802
+ "Show, through LangSmith, the different between a trace that is leveraging cache-backed embeddings and LLM calls - and one that isn't.\n",
803
+ "\n",
804
+ "Post screenshots in the notebook!"
805
+ ],
806
+ "metadata": {
807
+ "id": "0tYAvHrJNecy"
808
+ }
809
+ }
810
+ ]
811
+ }
app.py CHANGED
@@ -2,25 +2,163 @@
2
  """
3
  IMPORTS HERE
4
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  ### Global Section ###
7
  """
8
  GLOBAL CODE HERE
9
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  ### On Chat Start (Session Start) Section ###
12
  @cl.on_chat_start
13
  async def on_chat_start():
14
  """ SESSION SPECIFIC CODE HERE """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  ### Rename Chains ###
17
  @cl.author_rename
18
  def rename(orig_author: str):
19
  """ RENAME CODE HERE """
 
 
20
 
21
  ### On Message Section ###
22
  @cl.on_message
23
  async def main(message: cl.Message):
24
  """
25
  MESSAGE CODE HERE
26
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
  IMPORTS HERE
4
  """
5
+ import os
6
+ import uuid
7
+ from dotenv import load_dotenv
8
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
9
+ from langchain_community.document_loaders import PyMuPDFLoader
10
+ from qdrant_client import QdrantClient
11
+ from qdrant_client.http.models import Distance, VectorParams
12
+ from langchain_openai.embeddings import OpenAIEmbeddings
13
+ from langchain.storage import LocalFileStore
14
+ from langchain_qdrant import QdrantVectorStore
15
+ from langchain.embeddings import CacheBackedEmbeddings
16
+ from langchain_core.prompts import ChatPromptTemplate
17
+ from chainlit.types import AskFileResponse
18
+ from langchain_core.globals import set_llm_cache
19
+ from langchain_openai import ChatOpenAI
20
+ from langchain_core.caches import InMemoryCache
21
+ from operator import itemgetter
22
+ from langchain_core.runnables.passthrough import RunnablePassthrough
23
+ import chainlit as cl
24
+ from langchain_core.runnables.config import RunnableConfig
25
+
26
+ load_dotenv()
27
 
28
  ### Global Section ###
29
  """
30
  GLOBAL CODE HERE
31
  """
32
+ os.environ["LANGCHAIN_PROJECT"] = f"AIM Week 8 Assignment 1 - {uuid.uuid4().hex[0:8]}"
33
+ os.environ["LANGCHAIN_TRACING_V2"] = "true"
34
+ os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
35
+
36
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
37
+
38
+ rag_system_prompt_template = """\
39
+ You are a helpful assistant that uses the provided context to answer questions.
40
+ Never reference this prompt, or the existance of context.
41
+ """
42
+
43
+ rag_message_list = [
44
+ {"role" : "system", "content" : rag_system_prompt_template},
45
+ ]
46
+
47
+ rag_user_prompt_template = """\
48
+ Question:
49
+ {question}
50
+ Context:
51
+ {context}
52
+ """
53
+
54
+ chat_prompt = ChatPromptTemplate.from_messages([
55
+ ("system", rag_system_prompt_template),
56
+ ("human", rag_user_prompt_template)
57
+ ])
58
+
59
+ chat_model = ChatOpenAI(model="gpt-4o-mini")
60
+ # Typical Embedding Model
61
+ core_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
62
+
63
+ def process_file(file: AskFileResponse):
64
+ import tempfile
65
+
66
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as tempfile:
67
+ with open(tempfile.name, "wb") as f:
68
+ f.write(file.content)
69
+
70
+ Loader = PyMuPDFLoader
71
+
72
+ loader = Loader(tempfile.name)
73
+ documents = loader.load()
74
+ docs = text_splitter.split_documents(documents)
75
+ for i, doc in enumerate(docs):
76
+ doc.metadata["source"] = f"source_{i}"
77
+ return docs
78
+
79
 
80
  ### On Chat Start (Session Start) Section ###
81
  @cl.on_chat_start
82
  async def on_chat_start():
83
  """ SESSION SPECIFIC CODE HERE """
84
+ files = None
85
+
86
+ while files == None:
87
+ # Async method: This allows the function to pause execution while waiting for the user to upload a file,
88
+ # without blocking the entire application. It improves responsiveness and scalability.
89
+ files = await cl.AskFileMessage(
90
+ content="Please upload a PDF file to begin!",
91
+ accept=["application/pdf"],
92
+ max_size_mb=20,
93
+ timeout=180,
94
+ max_files=1
95
+ ).send()
96
+
97
+ file = files[0]
98
+ msg = cl.Message(
99
+ content=f"Processing `{file.name}`...",
100
+ )
101
+ await msg.send()
102
+ docs = process_file(file)
103
+
104
+ # Typical QDrant Client Set-up
105
+ collection_name = f"pdf_to_parse_{uuid.uuid4()}"
106
+ client = QdrantClient(":memory:")
107
+ client.create_collection(
108
+ collection_name=collection_name,
109
+ vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
110
+ )
111
+
112
+ # Adding cache!
113
+ store = LocalFileStore("./cache/")
114
+ cached_embedder = CacheBackedEmbeddings.from_bytes_store(
115
+ core_embeddings, store, namespace=core_embeddings.model
116
+ )
117
+
118
+ # Typical QDrant Vector Store Set-up
119
+ vectorstore = QdrantVectorStore(
120
+ client=client,
121
+ collection_name=collection_name,
122
+ embedding=cached_embedder)
123
+ vectorstore.add_documents(docs)
124
+ retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 3})
125
+
126
+ retrieval_augmented_qa_chain = (
127
+ {"context": itemgetter("question") | retriever, "question": itemgetter("question")}
128
+ | RunnablePassthrough.assign(context=itemgetter("context"))
129
+ | chat_prompt | chat_model
130
+ )
131
+
132
+ # Let the user know that the system is ready
133
+ msg.content = f"Processing `{file.name}` done. You can now ask questions!"
134
+ await msg.update()
135
+
136
+ cl.user_session.set("chain", retrieval_augmented_qa_chain)
137
+
138
 
139
  ### Rename Chains ###
140
  @cl.author_rename
141
  def rename(orig_author: str):
142
  """ RENAME CODE HERE """
143
+ rename_dict = {"ChatOpenAI": "the Generator...", "VectorStoreRetriever": "the Retriever..."}
144
+ return rename_dict.get(orig_author, orig_author)
145
 
146
  ### On Message Section ###
147
  @cl.on_message
148
  async def main(message: cl.Message):
149
  """
150
  MESSAGE CODE HERE
151
+ """
152
+ runnable = cl.user_session.get("chain")
153
+
154
+ msg = cl.Message(content="")
155
+
156
+ # Async method: Using astream allows for asynchronous streaming of the response,
157
+ # improving responsiveness and user experience by showing partial results as they become available.
158
+ async for chunk in runnable.astream(
159
+ {"question": message.content},
160
+ config=RunnableConfig(callbacks=[cl.LangchainCallbackHandler()]),
161
+ ):
162
+ await msg.stream_token(chunk.content)
163
+
164
+ await msg.send()