mbudisic commited on
Commit
035bd84
·
1 Parent(s): 5e93fd0

Migrated RAG to its package

Browse files
.gitignore CHANGED
@@ -3,4 +3,5 @@ __pycache__/
3
  .venv/
4
  .env
5
  .chainlit/
6
- .files/
 
 
3
  .venv/
4
  .env
5
  .chainlit/
6
+ .files/
7
+ .vscode/
notebooks/transcript_rag.ipynb CHANGED
@@ -2,7 +2,7 @@
2
  "cells": [
3
  {
4
  "cell_type": "code",
5
- "execution_count": 121,
6
  "metadata": {},
7
  "outputs": [],
8
  "source": [
@@ -15,7 +15,7 @@
15
  },
16
  {
17
  "cell_type": "code",
18
- "execution_count": 122,
19
  "metadata": {},
20
  "outputs": [],
21
  "source": [
@@ -24,7 +24,7 @@
24
  },
25
  {
26
  "cell_type": "code",
27
- "execution_count": 123,
28
  "metadata": {},
29
  "outputs": [
30
  {
@@ -43,7 +43,7 @@
43
  },
44
  {
45
  "cell_type": "code",
46
- "execution_count": 124,
47
  "metadata": {},
48
  "outputs": [],
49
  "source": [
@@ -59,7 +59,7 @@
59
  },
60
  {
61
  "cell_type": "code",
62
- "execution_count": 125,
63
  "metadata": {},
64
  "outputs": [],
65
  "source": [
@@ -87,10 +87,11 @@
87
  },
88
  {
89
  "cell_type": "code",
90
- "execution_count": 126,
91
  "metadata": {},
92
  "outputs": [],
93
  "source": [
 
94
  "import json\n",
95
  "filename = \"../data/test.json\"\n",
96
  "\n",
@@ -99,17 +100,21 @@
99
  },
100
  {
101
  "cell_type": "code",
102
- "execution_count": 127,
103
  "metadata": {},
104
- "outputs": [],
105
- "source": [
106
- "from langchain_experimental.text_splitter import SemanticChunker\n",
107
- "from langchain_openai.embeddings import OpenAIEmbeddings\n",
108
- "from pstuts_rag.datastore import transcripts_load\n",
109
- "\n",
110
- "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
111
- "docs_chunks_semantic = transcripts_load(data,embeddings)"
112
- ]
 
 
 
 
113
  },
114
  {
115
  "cell_type": "markdown",
@@ -127,26 +132,14 @@
127
  },
128
  {
129
  "cell_type": "code",
130
- "execution_count": 128,
131
  "metadata": {},
132
  "outputs": [],
133
  "source": [
134
- "from langchain_qdrant import QdrantVectorStore\n",
135
- "from qdrant_client import QdrantClient\n",
136
- "from qdrant_client.http.models import Distance, VectorParams\n",
137
- "\n",
138
- "qdrantclient = QdrantClient(\":memory:\")\n",
139
  "\n",
140
- "vectorstore = pstuts_rag.datastore.initialize_vectorstore(\n",
141
- " client=qdrantclient,\n",
142
- " collection_name=f\"{params.filename}_qdrant\",\n",
143
- " embeddings=embeddings,\n",
144
- ")\n",
145
- "\n",
146
- "_ = vectorstore.add_documents(documents=docs_chunks_semantic)\n",
147
- "retriever =vectorstore.as_retriever(\n",
148
- " search_kwargs={\"k\": params.n_context_docs}\n",
149
- ")"
150
  ]
151
  },
152
  {
@@ -159,301 +152,111 @@
159
  ]
160
  },
161
  {
162
- "cell_type": "code",
163
- "execution_count": 139,
164
  "metadata": {},
165
- "outputs": [],
166
  "source": [
167
- "from langchain.prompts import ChatPromptTemplate\n",
168
- "\n",
169
- "prompt_template = ChatPromptTemplate.from_messages([\n",
170
- " (\"system\", \"\"\"\\\n",
171
- "You are a helpful and friendly Photoshop expert.\n",
172
- "\n",
173
- "Your job is to answer user questions based **only** on transcript excerpts from training videos. These transcripts include **timestamps** that indicate when in the video the information was spoken.\n",
174
- "\n",
175
- "The transcript is from **spoken audio**, so it may include informal phrasing, filler words, or fragmented sentences. You may interpret meaning **only to the extent it is clearly implied**, but you must not add new information or invent details.\n",
176
- "\n",
177
- "✅ Your Responsibilities\n",
178
- "\n",
179
- "1. Use **only** the transcript to answer.\n",
180
- "2. If a clear answer is **not** present in the transcript, respond exactly: \n",
181
- " \"I don't know. This isn’t covered in the training videos.\"\n",
182
- "3. When appropriate, include the **timestamp** of relevant information in your answer to help the user locate it in the original video.\n",
183
- "4. Do **not** make assumptions or draw on outside knowledge.\n",
184
- "\n",
185
- "💡 Style & Formatting Tips\n",
186
- "\n",
187
- "- Use a step-by-step format when explaining procedures 📋.\n",
188
- "- Add relevant emojis for clarity and friendliness 🎨🖱️🔧.\n",
189
- "- Keep your answers short, clear, and conversational.\n",
190
- "- The input timestamps will be in seconds. When reporting timestamps, convert them into minute:seconds format.\n",
191
- "\n",
192
- "⛔ Never Do This\n",
193
- "\n",
194
- "- ❌ Don't guess or summarize from general knowledge.\n",
195
- "- ❌ Don’t fabricate steps, names, or features not in the transcript.\n",
196
- "- ❌ Don’t omit the fallback response when required.\n",
197
- "\"\"\"),\n",
198
- " (\"user\",\"\"\"\\\n",
199
- "\n",
200
- "### Question\n",
201
- "{question}\n",
202
- "\n",
203
- "NEVER invent the explanation. ALWAYS use ONLY the context information.\n",
204
- "\n",
205
- "### Context\n",
206
- "{context}\n",
207
  "\n",
208
- "\"\"\")])\n"
209
  ]
210
  },
211
  {
212
  "cell_type": "code",
213
- "execution_count": 140,
214
  "metadata": {},
215
  "outputs": [],
216
  "source": [
217
- "def compile_references(context):\n",
218
- " references = [ \n",
219
- " {k: doc.metadata[k] for k in (\"title\",\"source\",\"start\",\"stop\")} \n",
220
- " for doc in context\n",
221
- " ] \n",
222
- " print(type(references))\n",
223
- " return json.dumps(references,indent=2)\n"
224
  ]
225
  },
226
  {
227
- "cell_type": "markdown",
 
228
  "metadata": {},
 
229
  "source": [
230
- "## Generation\n",
231
  "\n",
232
- "We will use a 4.1-nano to generate answers."
233
  ]
234
  },
235
  {
236
  "cell_type": "code",
237
- "execution_count": 141,
238
  "metadata": {},
239
  "outputs": [],
240
  "source": [
241
- "from langchain_openai import ChatOpenAI\n",
242
  "\n",
243
- "llm = ChatOpenAI(model=\"gpt-4.1-mini\",temperature=0)"
 
244
  ]
245
  },
246
  {
247
  "cell_type": "code",
248
- "execution_count": 142,
249
  "metadata": {},
250
- "outputs": [
251
- {
252
- "name": "stdout",
253
- "output_type": "stream",
254
- "text": [
255
- "<class 'list'>\n",
256
- "('Layers are the building blocks of any image in Photoshop CC. You can think '\n",
257
- " 'of layers like separate flat panes of glass stacked on top of each other. '\n",
258
- " 'Each layer contains separate pieces of content. Some parts of a layer can be '\n",
259
- " 'transparent, allowing you to see through to the layers below. This setup '\n",
260
- " 'lets you edit parts of an image independently without affecting the rest of '\n",
261
- " 'the image. You manage and work with layers in the Layers panel, where you '\n",
262
- " 'can toggle their visibility on and off using the Eye icon. (See explanation '\n",
263
- " 'around 0:28 to 1:00 and 1:25 to 2:32) 🎨🖼️\\n'\n",
264
- " 'References:\\n'\n",
265
- " '[\\n'\n",
266
- " ' {\\n'\n",
267
- " ' \"title\": \"Understand layers\",\\n'\n",
268
- " ' \"source\": '\n",
269
- " '\"https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4\",\\n'\n",
270
- " ' \"start\": 0.47,\\n'\n",
271
- " ' \"stop\": 62.14\\n'\n",
272
- " ' },\\n'\n",
273
- " ' {\\n'\n",
274
- " ' \"title\": \"Understand layers\",\\n'\n",
275
- " ' \"source\": '\n",
276
- " '\"https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4\",\\n'\n",
277
- " ' \"start\": 85.75,\\n'\n",
278
- " ' \"stop\": 152.97\\n'\n",
279
- " ' }\\n'\n",
280
- " ']')\n"
281
- ]
282
- }
283
- ],
284
  "source": [
285
- "from operator import itemgetter\n",
286
- "from langchain.schema.output_parser import StrOutputParser\n",
287
- "from langchain_core.runnables import RunnableLambda\n",
288
- "\n",
289
- "form_context = RunnableLambda(itemgetter(\"question\")) | {\n",
290
- " \"context\": retriever, \n",
291
- " \"question\": RunnablePassthrough() \n",
292
- " } \n",
293
- "\n",
294
- "answer_chain = prompt_template | llm | StrOutputParser()\n",
295
- "\n",
296
- "get_videos = form_context | \\\n",
297
- " {\"input\":RunnablePassthrough(),\"answer\": answer_chain} |\\\n",
298
- " RunnableLambda( lambda d: \n",
299
- " {**d[\"input\"], \"answer\": d[\"answer\"] + \n",
300
- " \"\\nReferences:\\n\" +\n",
301
- " compile_references(d[\"input\"][\"context\"]) \n",
302
- " } )\n",
303
- " \n",
304
- "\n",
305
- "\n",
306
- "val = get_videos.invoke({\"question\":\"What are layers\"})\n",
307
- "pp(val[\"answer\"])"
308
  ]
309
  },
310
  {
311
  "cell_type": "code",
312
- "execution_count": 143,
313
  "metadata": {},
314
  "outputs": [
315
  {
316
- "name": "stdout",
317
- "output_type": "stream",
318
- "text": [
319
- "('Layers are the building blocks of any image in Photoshop CC. You can think '\n",
320
- " 'of layers like separate flat panes of glass stacked on top of each other. '\n",
321
- " 'Each layer contains separate pieces of content. Some parts of a layer can be '\n",
322
- " 'transparent, allowing you to see through to the layers below. This setup '\n",
323
- " 'lets you edit parts of an image independently without affecting the rest of '\n",
324
- " 'the image. You manage and work with layers in the Layers panel, where you '\n",
325
- " 'can toggle their visibility on and off using the Eye icon. (See explanation '\n",
326
- " 'around 0:28 to 1:00 and 1:25 to 2:32) 🎨🖼️\\n'\n",
327
- " 'References:\\n'\n",
328
- " '[\\n'\n",
329
- " ' {\\n'\n",
330
- " ' \"title\": \"Understand layers\",\\n'\n",
331
- " ' \"source\": '\n",
332
- " '\"https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4\",\\n'\n",
333
- " ' \"start\": 0.47,\\n'\n",
334
- " ' \"stop\": 62.14\\n'\n",
335
- " ' },\\n'\n",
336
- " ' {\\n'\n",
337
- " ' \"title\": \"Understand layers\",\\n'\n",
338
- " ' \"source\": '\n",
339
- " '\"https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4\",\\n'\n",
340
- " ' \"start\": 85.75,\\n'\n",
341
- " ' \"stop\": 152.97\\n'\n",
342
- " ' }\\n'\n",
343
- " ']')\n"
344
- ]
345
  }
346
  ],
347
  "source": [
348
- "pp(val[\"answer\"])"
349
  ]
350
  },
351
  {
352
  "cell_type": "code",
353
- "execution_count": 144,
354
  "metadata": {},
355
  "outputs": [
356
  {
357
  "name": "stdout",
358
  "output_type": "stream",
359
  "text": [
360
- "('Layers are the building blocks of any image in Photoshop CC. 🖼️ They can be '\n",
361
- " 'thought of as separate flat pints of glass, stacked one on top of the other. '\n",
362
- " 'Each layer contains separate pieces of content, and some layers may have '\n",
363
- " 'transparent areas that let you see through to the layers below. The Layers '\n",
364
- " 'panel is where you select and work with layers, and you can toggle their '\n",
365
- " 'visibility by clicking the Eye icon. The main benefit of layers is that they '\n",
366
- " 'allow you to edit parts of an image independently without affecting the '\n",
367
- " 'rest. \\n'\n",
368
- " '\\n'\n",
369
- " '📺 Watch the full explanation in the video titled \"Understand layers\" here: '\n",
370
- " '[https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4](https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4)')\n"
371
- ]
372
- }
373
- ],
374
- "source": [
375
- "pp(value.content)"
376
- ]
377
- },
378
- {
379
- "cell_type": "code",
380
- "execution_count": 145,
381
- "metadata": {},
382
- "outputs": [
383
- {
384
- "ename": "NameError",
385
- "evalue": "name 'generate' is not defined",
386
- "output_type": "error",
387
- "traceback": [
388
- "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
389
- "\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
390
- "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[145]\u001b[39m\u001b[32m, line 13\u001b[39m\n\u001b[32m 10\u001b[39m context: List[Document]\n\u001b[32m 11\u001b[39m response: \u001b[38;5;28mstr\u001b[39m\n\u001b[32m---> \u001b[39m\u001b[32m13\u001b[39m graph_builder = StateGraph(State).add_sequence([retrieve, \u001b[43mgenerate\u001b[49m ])\n\u001b[32m 14\u001b[39m graph_builder.add_edge(START, \u001b[33m\"\u001b[39m\u001b[33mretrieve\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 15\u001b[39m graph = graph_builder.compile()\n",
391
- "\u001b[31mNameError\u001b[39m: name 'generate' is not defined"
392
  ]
393
  }
394
  ],
395
  "source": [
396
- "from langgraph.graph import START, StateGraph\n",
397
- "from typing_extensions import List, TypedDict,Annotated\n",
398
- "from langchain_core.documents import Document\n",
399
- "from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n",
400
- "from langchain_openai.chat_models import ChatOpenAI\n",
401
- "import operator\n",
402
- "\n",
403
- "class State(TypedDict):\n",
404
- " question: str\n",
405
- " context: List[Document]\n",
406
- " response: str\n",
407
- " \n",
408
- "graph_builder = StateGraph(State).add_sequence([retrieve, generate ])\n",
409
- "graph_builder.add_edge(START, \"retrieve\")\n",
410
- "graph = graph_builder.compile()"
411
- ]
412
- },
413
- {
414
- "cell_type": "code",
415
- "execution_count": 53,
416
- "metadata": {},
417
- "outputs": [],
418
- "source": [
419
- "from langchain.schema.output_parser import StrOutputParser\n",
420
- "response = graph.invoke({\"question\" : \"What is the layer in Photoshop\"})"
421
- ]
422
- },
423
- {
424
- "cell_type": "code",
425
- "execution_count": null,
426
- "metadata": {},
427
- "outputs": [],
428
- "source": [
429
- "response.keys()"
430
- ]
431
- },
432
- {
433
- "cell_type": "code",
434
- "execution_count": null,
435
- "metadata": {},
436
- "outputs": [],
437
- "source": [
438
- "type(response)"
439
- ]
440
- },
441
- {
442
- "cell_type": "code",
443
- "execution_count": null,
444
- "metadata": {},
445
- "outputs": [],
446
- "source": [
447
- "pp(response)"
448
- ]
449
- },
450
- {
451
- "cell_type": "code",
452
- "execution_count": null,
453
- "metadata": {},
454
- "outputs": [],
455
- "source": [
456
- "response.keys()"
457
  ]
458
  },
459
  {
 
2
  "cells": [
3
  {
4
  "cell_type": "code",
5
+ "execution_count": 9,
6
  "metadata": {},
7
  "outputs": [],
8
  "source": [
 
15
  },
16
  {
17
  "cell_type": "code",
18
+ "execution_count": 10,
19
  "metadata": {},
20
  "outputs": [],
21
  "source": [
 
24
  },
25
  {
26
  "cell_type": "code",
27
+ "execution_count": 11,
28
  "metadata": {},
29
  "outputs": [
30
  {
 
43
  },
44
  {
45
  "cell_type": "code",
46
+ "execution_count": 12,
47
  "metadata": {},
48
  "outputs": [],
49
  "source": [
 
59
  },
60
  {
61
  "cell_type": "code",
62
+ "execution_count": 13,
63
  "metadata": {},
64
  "outputs": [],
65
  "source": [
 
87
  },
88
  {
89
  "cell_type": "code",
90
+ "execution_count": 14,
91
  "metadata": {},
92
  "outputs": [],
93
  "source": [
94
+ "from ast import Dict\n",
95
  "import json\n",
96
  "filename = \"../data/test.json\"\n",
97
  "\n",
 
100
  },
101
  {
102
  "cell_type": "code",
103
+ "execution_count": null,
104
  "metadata": {},
105
+ "outputs": [
106
+ {
107
+ "data": {
108
+ "text/plain": [
109
+ "dict_values"
110
+ ]
111
+ },
112
+ "execution_count": 18,
113
+ "metadata": {},
114
+ "output_type": "execute_result"
115
+ }
116
+ ],
117
+ "source": []
118
  },
119
  {
120
  "cell_type": "markdown",
 
132
  },
133
  {
134
  "cell_type": "code",
135
+ "execution_count": 20,
136
  "metadata": {},
137
  "outputs": [],
138
  "source": [
139
+ "from pstuts_rag.rag import RetrieverFactory\n",
 
 
 
 
140
  "\n",
141
+ "retriever_factory = RetrieverFactory()\n",
142
+ "retriever_factory.add_docs(raw_docs=data)"
 
 
 
 
 
 
 
 
143
  ]
144
  },
145
  {
 
152
  ]
153
  },
154
  {
155
+ "cell_type": "markdown",
 
156
  "metadata": {},
 
157
  "source": [
158
+ "## Generation\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  "\n",
160
+ "We will use a 4.1-nano to generate answers."
161
  ]
162
  },
163
  {
164
  "cell_type": "code",
165
+ "execution_count": 25,
166
  "metadata": {},
167
  "outputs": [],
168
  "source": [
169
+ "from langchain_openai import ChatOpenAI\n",
170
+ "\n",
171
+ "llm = ChatOpenAI(model=\"gpt-4.1-mini\",temperature=0)"
 
 
 
 
172
  ]
173
  },
174
  {
175
+ "cell_type": "code",
176
+ "execution_count": 26,
177
  "metadata": {},
178
+ "outputs": [],
179
  "source": [
180
+ "from pstuts_rag.rag import RAGChainFactory\n",
181
  "\n",
182
+ "rag_factory = RAGChainFactory(retriever=retriever_factory.get_retriever())\n"
183
  ]
184
  },
185
  {
186
  "cell_type": "code",
187
+ "execution_count": 49,
188
  "metadata": {},
189
  "outputs": [],
190
  "source": [
 
191
  "\n",
192
+ "get_videos = rag_factory.get_rag_chain(llm)\n",
193
+ " \n"
194
  ]
195
  },
196
  {
197
  "cell_type": "code",
198
+ "execution_count": 50,
199
  "metadata": {},
200
+ "outputs": [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  "source": [
202
+ "val = get_videos.invoke({\"question\":\"What are layers\"})"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  ]
204
  },
205
  {
206
  "cell_type": "code",
207
+ "execution_count": 52,
208
  "metadata": {},
209
  "outputs": [
210
  {
211
+ "data": {
212
+ "text/plain": [
213
+ "{'refusal': None,\n",
214
+ " 'context': [Document(metadata={'video_id': 19172, 'title': 'Understand layers', 'desc': 'Learn what layers are and why they are so useful.', 'length': '00:04:44.75', 'source': 'https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4', 'speech_start_stop_times': [[0.47, 3.41], [3.81, 9.13], [9.309999, 15.01], [15.299999, 20.57], [20.88, 23.3], [23.83, 27.93], [29.38, 32.79], [32.96, 33.92], [34.43, 40.21], [41.91, 45.37], [45.88, 49.01], [49.54, 55.130001], [55.72, 58.49], [58.72, 62.14]], 'start': 0.47, 'stop': 62.14, '_id': '9124467014f5417eb5f90a7f350a7f4e', '_collection_name': '2f1798fb-3bc9-4ece-9bc5-1f465fc4b821_qdrant'}, page_content=\"Layers are the building blocks of any image in Photoshop CC. So, it's important to understand, what layers are and why to use them - which we'll cover in this video. If you're following along, open this layered image from the downloadable practice files for this tutorial. You might think of layers like separate flat pints of glass, stacked one on top of the other. Each layer contains separate pieces of content. To get a sense of how layers are constructed, let's take a look at this Layers panel. I've closed my other panels, so that we can focus on the Layers panel. But you can skip that. By the way: If your Layers panel isn't showing, go up to the Window menu and choose Layers from there. The Layers panel is where you go to select and work with layers. In this image there are 4 layers, each with separate content. If you click the Eye icon to the left of a layer, you can toggle the visibility of that layer off and on. So, I'm going to turn off the visibility of the tailor layer. And keep your eye on the image, so you can see what's on that layer.\"),\n",
215
+ " Document(metadata={'video_id': 19172, 'title': 'Understand layers', 'desc': 'Learn what layers are and why they are so useful.', 'length': '00:04:44.75', 'source': 'https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4', 'speech_start_stop_times': [[85.75, 88.659999], [89.42, 100.11], [101.469999, 108.64], [109.09, 117.459999], [117.75, 129.45], [129.97, 133.37], [133.73, 143.98], [144.76, 152.97]], 'start': 85.75, 'stop': 152.97, '_id': '2b41e3b5ed894393a4d50b55820888c9', '_collection_name': '2f1798fb-3bc9-4ece-9bc5-1f465fc4b821_qdrant'}, page_content=\"Now let's take a look at just one layer, the tailor layer. A quick way to turn off all the layers except the tailor layer, is to hold down the Option key on the Mac, or the ALT key on the PC, and click on the Eye icon to the left of the tailor layer. In the Document window, you can see that this layer contains just the one small photo surrounded by a gray and white checkerboard pattern. That pattern represents transparent pixels, which allow us to see down through the corresponding part of this layer to the content of the layers below. So, let's turn that content back on by going back to the Layers panel, again holding the Option key on the Mac or the ALT key on the PC and clicking on the Eye icon to the left of the tailor layer. And all the other layers and their Eye icons come back into view. So again: You might think of layers like a stack of pints of glass, each with its own artwork and in some cases transparent areas that let you see down through to the layers below. The biggest benefit of having items on separate layers like this, is that you'll be able to edit pieces of an image independently without affecting the rest of the image.\")],\n",
216
+ " 'question': 'What are layers'}"
217
+ ]
218
+ },
219
+ "execution_count": 52,
220
+ "metadata": {},
221
+ "output_type": "execute_result"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
223
  ],
224
  "source": [
225
+ "val.additional_kwargs"
226
  ]
227
  },
228
  {
229
  "cell_type": "code",
230
+ "execution_count": 53,
231
  "metadata": {},
232
  "outputs": [
233
  {
234
  "name": "stdout",
235
  "output_type": "stream",
236
  "text": [
237
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
238
+ "\n",
239
+ "Layers are the building blocks of any image in Photoshop CC. You can think of layers like separate flat panes of glass stacked one on top of the other. Each layer contains separate pieces of content. Some parts of a layer can be transparent, allowing you to see through to the layers below. This setup lets you edit parts of an image independently without affecting the rest of the image. You work with layers in the Layers panel, where you can toggle their visibility on and off to see what each layer contains (like turning off the tailor layer to see what’s on it). The transparent areas are shown as a gray and white checkerboard pattern. This concept is explained around 0:47 to 3:41 and 85:75 to 152:97 in the video. 🎨🖼️\n",
240
+ "**References**:\n",
241
+ "[\n",
242
+ " {\n",
243
+ " \"title\": \"Understand layers\",\n",
244
+ " \"source\": \"https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4\",\n",
245
+ " \"start\": 0.47,\n",
246
+ " \"stop\": 62.14\n",
247
+ " },\n",
248
+ " {\n",
249
+ " \"title\": \"Understand layers\",\n",
250
+ " \"source\": \"https://images-tv.adobe.com/avp/vr/b758b4c4-2a74-41f4-8e67-e2f2eab83c6a/f810fc5b-2b04-4e23-8fa4-5c532e7de6f8/e268fe4d-e5c7-415c-9f5c-d34d024b14d8_20170727011753.1280x720at2400_h264.mp4\",\n",
251
+ " \"start\": 85.75,\n",
252
+ " \"stop\": 152.97\n",
253
+ " }\n",
254
+ "]\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  ]
256
  }
257
  ],
258
  "source": [
259
+ "val.pretty_print()"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  ]
261
  },
262
  {
pstuts_rag/pstuts_rag/prompt_templates.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Tuple
2
+
3
+ RAG_PROMPT_TEMPLATES: List[Tuple[str, str]] = []
4
+
5
+ RAG_PROMPT_TEMPLATES.append(
6
+ (
7
+ "system",
8
+ """\
9
+ You are a helpful and friendly Photoshop expert.
10
+
11
+ Your job is to answer user questions based **only** on transcript excerpts from training videos. These transcripts include **timestamps** that indicate when in the video the information was spoken.
12
+
13
+ The transcript is from **spoken audio**, so it may include informal phrasing, filler words, or fragmented sentences. You may interpret meaning **only to the extent it is clearly implied**, but you must not add new information or invent details.
14
+
15
+ ✅ Your Responsibilities
16
+
17
+ 1. Use **only** the transcript to answer.
18
+ 2. If a clear answer is **not** present in the transcript, respond exactly:
19
+ "I don't know. This isn’t covered in the training videos."
20
+ 3. When appropriate, include the **timestamp** of relevant information in your answer to help the user locate it in the original video.
21
+ 4. Do **not** make assumptions or draw on outside knowledge.
22
+
23
+ 💡 Style & Formatting Tips
24
+
25
+ - Use a step-by-step format when explaining procedures 📋.
26
+ - Add relevant emojis for clarity and friendliness 🎨🖱️🔧.
27
+ - Keep your answers short, clear, and conversational.
28
+ - The input timestamps will be in seconds. When reporting timestamps, convert them into minute:seconds format.
29
+
30
+ ⛔ Never Do This
31
+
32
+ - ❌ Don't guess or summarize from general knowledge.
33
+ - ❌ Don’t fabricate steps, names, or features not in the transcript.
34
+ - ❌ Don’t omit the fallback response when required.
35
+ """,
36
+ )
37
+ )
38
+
39
+ RAG_PROMPT_TEMPLATES.append(
40
+ (
41
+ "user",
42
+ """\
43
+ ### Question
44
+ {question}
45
+
46
+ NEVER invent the explanation. ALWAYS use ONLY the context information.
47
+
48
+ ### Context
49
+ {context}
50
+
51
+ """,
52
+ )
53
+ )
pstuts_rag/pstuts_rag/rag.py CHANGED
@@ -1,67 +1,160 @@
 
 
 
1
  from typing import List, Optional, Union
2
- from langchain_experimental.text_splitter import SemanticChunker
3
- from langchain_openai.embeddings import OpenAIEmbeddings
4
-
5
- from langchain_qdrant import QdrantVectorStore
6
- from qdrant_client import QdrantClient
7
- from qdrant_client.http.models import Distance, VectorParams
8
-
9
- from .datastore import transcripts_load, initialize_vectorstore
10
 
11
- from langchain_experimental.text_splitter import SemanticChunker
12
- from langchain_openai.embeddings import OpenAIEmbeddings
13
  from langchain_core.documents import Document
 
 
 
 
 
14
 
15
- from pstuts_rag.loader import VideoTranscriptBulkLoader, VideoTranscriptLoader
16
-
17
  from langchain_qdrant import QdrantVectorStore
18
  from qdrant_client import QdrantClient
19
- from qdrant_client.http.models import Distance, VectorParams
20
 
21
- from langchain_core.retrievers import VectorStoreRetriever
 
 
22
 
 
 
23
 
24
- import uuid
 
25
 
26
 
27
- class RAGFactory:
28
 
29
  embeddings: OpenAIEmbeddings
30
  docs: List[Document]
31
- qdrantclient: QdrantClient
32
  name: str
33
- vectorstore: QdrantVectorStore
34
-
35
 
36
  def __init__(
37
  self,
38
- raw_docs: List[Documents],
39
- embeddings: OpenAIEmbeddings=OpenAIEmbeddings(model="text-embedding-3-small"),
40
- qdrantclient: QdrantClient = QdrantClient(":memory:"),
41
- name: str = uuid.uuid4(),
 
42
  ) -> None:
43
 
44
  self.embeddings = embeddings
45
  self.name = name
46
- self.qdrantclient = qdrantclient
47
- self.vectorstore = initialize_vectorstore(
48
- client=self.qdrantclient,
49
  collection_name=f"{self.name}_qdrant",
50
  embeddings=self.embeddings,
51
  )
52
  self.docs = []
53
 
 
54
 
55
- def add_docs(raw_docs: List[Documents]):
56
-
57
- docs = transcripts_load(raw_docs, self.embeddings)
58
- self.docs.append( docs )
59
- _ = self.vectorstore.add_documents(documents=docs)
60
-
61
- def clear() -> bool:
62
  self.docs = []
63
- return self.vectorstore.delete()
64
-
65
-
66
- def get_retriever(self, n_context_docs=2) -> VectorStoreRetriever
67
- return self.vectorstore.as_retriever(search_kwargs={"k": n_context_docs})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+ from operator import itemgetter
4
  from typing import List, Optional, Union
 
 
 
 
 
 
 
 
5
 
6
+ from langchain.schema.output_parser import StrOutputParser
 
7
  from langchain_core.documents import Document
8
+ from langchain_core.runnables import (
9
+ Runnable,
10
+ RunnableLambda,
11
+ RunnablePassthrough,
12
+ )
13
 
14
+ from langchain_openai.embeddings import OpenAIEmbeddings
 
15
  from langchain_qdrant import QdrantVectorStore
16
  from qdrant_client import QdrantClient
 
17
 
18
+ from langchain.prompts import ChatPromptTemplate
19
+ from langchain_core.vectorstores import VectorStoreRetriever
20
+ from langchain_openai import ChatOpenAI
21
 
22
+ from .datastore import initialize_vectorstore, transcripts_load
23
+ from .prompt_templates import RAG_PROMPT_TEMPLATES
24
 
25
+ from langchain_core.language_models.base import BaseLanguageModel
26
+ from langchain_core.messages import AIMessage
27
 
28
 
29
+ class RetrieverFactory:
30
 
31
  embeddings: OpenAIEmbeddings
32
  docs: List[Document]
33
+ qdrant_client: QdrantClient
34
  name: str
35
+ vector_store: QdrantVectorStore
 
36
 
37
  def __init__(
38
  self,
39
+ embeddings: OpenAIEmbeddings = OpenAIEmbeddings(
40
+ model="text-embedding-3-small"
41
+ ),
42
+ qdrant_client: QdrantClient = QdrantClient(location=":memory:"),
43
+ name: str = str(object=uuid.uuid4()),
44
  ) -> None:
45
 
46
  self.embeddings = embeddings
47
  self.name = name
48
+ self.qdrant_client = qdrant_client
49
+ self.vector_store = initialize_vectorstore(
50
+ client=self.qdrant_client,
51
  collection_name=f"{self.name}_qdrant",
52
  embeddings=self.embeddings,
53
  )
54
  self.docs = []
55
 
56
+ def add_docs(self, raw_docs) -> None:
57
 
58
+ docs: List[Document] = transcripts_load(
59
+ json_transcripts=raw_docs, embeddings=self.embeddings
60
+ )
61
+ self.docs.extend(docs)
62
+ _ = self.vector_store.add_documents(documents=docs)
63
+
64
+ def clear(self) -> bool:
65
  self.docs = []
66
+ return True if self.vector_store.delete() else False
67
+
68
+ def get_retriever(self, n_context_docs: int = 2) -> VectorStoreRetriever:
69
+ return self.vector_store.as_retriever(
70
+ search_kwargs={"k": n_context_docs}
71
+ )
72
+
73
+
74
+ class RAGChainFactory:
75
+
76
+ format_query = RunnableLambda(itemgetter("question"))
77
+ retriever: VectorStoreRetriever
78
+ add_context_to_query: Runnable
79
+ prompt_template: Runnable
80
+ answer_chain: Runnable
81
+ llm: ChatOpenAI
82
+
83
+ rag_chain: Runnable
84
+
85
+ @staticmethod
86
+ def compile_references(context: List[Document]) -> str:
87
+ references = [
88
+ {k: doc.metadata[k] for k in ("title", "source", "start", "stop")}
89
+ for doc in context
90
+ ]
91
+ return json.dumps(references, indent=2)
92
+
93
+ pack_references: RunnableLambda = RunnableLambda(
94
+ lambda d: {
95
+ **d["input"],
96
+ "answer": d["answer"]
97
+ + "\nReferences:\n"
98
+ + RAGChainFactory.compile_references(
99
+ context=d["input"]["context"]
100
+ ),
101
+ }
102
+ )
103
+
104
+ @staticmethod
105
+ def pack_references2(msg_dict: Dict[str, Any]) -> AIMessage:
106
+
107
+ answer: AIMessage = msg_dict["answer"]
108
+ input = msg_dict["input"]
109
+
110
+ references = RAGChainFactory.compile_references(
111
+ context=input["context"]
112
+ )
113
+
114
+ text_w_references = f"{answer.content}\n**References**:\n{references}"
115
+
116
+ output: AIMessage = answer.model_copy(
117
+ update={
118
+ "content": text_w_references,
119
+ "additional_kwargs": {
120
+ **answer.additional_kwargs,
121
+ "context": input["context"],
122
+ "question": input["question"],
123
+ },
124
+ }
125
+ )
126
+
127
+ return output
128
+
129
+ def __init__(
130
+ self,
131
+ retriever: VectorStoreRetriever,
132
+ ) -> None:
133
+
134
+ self.retriever = retriever
135
+
136
+ self.prepare_query = {
137
+ "context": retriever,
138
+ "question": RunnablePassthrough(),
139
+ }
140
+
141
+ self.prompt_template = ChatPromptTemplate.from_messages(
142
+ RAG_PROMPT_TEMPLATES
143
+ )
144
+
145
+ def get_rag_chain(
146
+ self,
147
+ llm: BaseLanguageModel = ChatOpenAI(
148
+ model="gpt-4.1-mini", temperature=0
149
+ ),
150
+ ) -> Runnable:
151
+
152
+ self.answer_chain = self.prompt_template | llm
153
+ self.rag_chain = (
154
+ self.format_query
155
+ | self.prepare_query
156
+ | {"input": RunnablePassthrough(), "answer": self.answer_chain}
157
+ | self.pack_references2
158
+ )
159
+
160
+ return self.rag_chain
pyproject.toml CHANGED
@@ -33,6 +33,9 @@ dependencies = [
33
  "unstructured>=0.17.2",
34
  "uvicorn>=0.25.0,<0.26.0",
35
  "websockets==14.2",
 
 
 
36
  ]
37
  authors = [{ name = "Marko Budisic", email = "mbudisic@gmail.com" }]
38
  license = "MIT"
@@ -44,44 +47,44 @@ build-backend = "hatchling.build"
44
  [tool.hatch.build.targets.wheel]
45
  packages = ["pstuts_rag/pstuts_rag"]
46
 
47
- # [project.optional-dependencies]
48
- # dev = [
49
- # "pytest>=7.0.0",
50
- # "black>=22.0.0",
51
- # "flake8>=4.0.0",
52
- # "mypy>=0.900",
53
- # ]
54
 
55
- # [tool.ruff]
56
- # line-length = 88
57
- # target-version = "py313"
58
- # select = ["E", "F", "I", "N", "W"]
59
- # ignore = []
60
 
61
- # [tool.ruff.isort]
62
- # known-first-party = ["src"]
63
 
64
- # [tool.black]
65
- # line-length = 88
66
- # target-version = ["py313"]
67
 
68
- # [tool.mypy]
69
- # python_version = "3.13"
70
- # warn_return_any = true
71
- # warn_unused_configs = true
72
- # disallow_untyped_defs = true
73
- # mypy_path = ["pstuts_rag/pstuts_rag"]
74
- # namespace_packages = true
75
- # explicit_package_bases = true
76
 
77
- # [tool.flake8]
78
- # application-import-names = "pstuts_rag"
79
- # extend-ignore = "E203,W503"
80
 
81
- # [tool.pylint.MASTER]
82
- # load-plugins = "pylint_venv" # optional but handy
83
- # source-roots = "pstuts_rag"
84
- # extension-pkg-allow-list = "numpy, torch" # compiled deps that astroid cannot parse
85
 
86
- # [tool.pylint.TYPECHECK]
87
- # ignored-modules = "pkg_resources" # suppress noisy vendored imports
 
33
  "unstructured>=0.17.2",
34
  "uvicorn>=0.25.0,<0.26.0",
35
  "websockets==14.2",
36
+ "langchain-core>=0.3.59",
37
+ "isort>=6.0.1",
38
+ "pylint-venv>=3.0.4",
39
  ]
40
  authors = [{ name = "Marko Budisic", email = "mbudisic@gmail.com" }]
41
  license = "MIT"
 
47
  [tool.hatch.build.targets.wheel]
48
  packages = ["pstuts_rag/pstuts_rag"]
49
 
50
+ [project.optional-dependencies]
51
+ dev = [
52
+ "pytest>=7.0.0",
53
+ "black>=22.0.0",
54
+ "flake8>=4.0.0",
55
+ "mypy>=0.900",
56
+ ]
57
 
58
+ [tool.ruff]
59
+ line-length = 79
60
+ target-version = "py313"
61
+ select = ["E", "F", "I", "N", "W"]
62
+ ignore = []
63
 
64
+ [tool.ruff.isort]
65
+ known-first-party = ["src"]
66
 
67
+ [tool.black]
68
+ line-length = 79
69
+ target-version = ["py313"]
70
 
71
+ [tool.mypy]
72
+ python_version = "3.13"
73
+ warn_return_any = true
74
+ warn_unused_configs = true
75
+ disallow_untyped_defs = true
76
+ mypy_path = ["pstuts_rag/pstuts_rag"]
77
+ namespace_packages = true
78
+ explicit_package_bases = true
79
 
80
+ [tool.flake8]
81
+ application-import-names = "pstuts_rag"
82
+ extend-ignore = "E203,W503"
83
 
84
+ [tool.pylint.MASTER]
85
+ load-plugins = "pylint_venv" # optional but handy
86
+ source-roots = "pstuts_rag"
87
+ extension-pkg-allow-list = "numpy, torch" # compiled deps that astroid cannot parse
88
 
89
+ [tool.pylint.TYPECHECK]
90
+ ignored-modules = "pkg_resources" # suppress noisy vendored imports
uv.lock CHANGED
@@ -235,6 +235,26 @@ wheels = [
235
  { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 },
236
  ]
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  [[package]]
239
  name = "bleach"
240
  version = "6.2.0"
@@ -613,6 +633,20 @@ wheels = [
613
  { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 },
614
  ]
615
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  [[package]]
617
  name = "fqdn"
618
  version = "1.5.1"
@@ -885,6 +919,15 @@ wheels = [
885
  { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 },
886
  ]
887
 
 
 
 
 
 
 
 
 
 
888
  [[package]]
889
  name = "ipykernel"
890
  version = "6.29.5"
@@ -970,6 +1013,15 @@ wheels = [
970
  { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321 },
971
  ]
972
 
 
 
 
 
 
 
 
 
 
973
  [[package]]
974
  name = "jedi"
975
  version = "0.19.2"
@@ -1615,6 +1667,15 @@ wheels = [
1615
  { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 },
1616
  ]
1617
 
 
 
 
 
 
 
 
 
 
1618
  [[package]]
1619
  name = "mistune"
1620
  version = "3.1.3"
@@ -1683,6 +1744,25 @@ wheels = [
1683
  { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351 },
1684
  ]
1685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1686
  [[package]]
1687
  name = "mypy-extensions"
1688
  version = "1.1.0"
@@ -2094,6 +2174,15 @@ wheels = [
2094
  { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 },
2095
  ]
2096
 
 
 
 
 
 
 
 
 
 
2097
  [[package]]
2098
  name = "pexpect"
2099
  version = "4.9.0"
@@ -2124,6 +2213,15 @@ wheels = [
2124
  { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 },
2125
  ]
2126
 
 
 
 
 
 
 
 
 
 
2127
  [[package]]
2128
  name = "portalocker"
2129
  version = "2.10.1"
@@ -2224,10 +2322,12 @@ dependencies = [
2224
  { name = "httpx" },
2225
  { name = "ipykernel" },
2226
  { name = "ipywidgets" },
 
2227
  { name = "jq" },
2228
  { name = "jupyter" },
2229
  { name = "langchain" },
2230
  { name = "langchain-community" },
 
2231
  { name = "langchain-experimental" },
2232
  { name = "langchain-openai" },
2233
  { name = "langchain-qdrant" },
@@ -2236,6 +2336,7 @@ dependencies = [
2236
  { name = "openai" },
2237
  { name = "pip" },
2238
  { name = "pydantic" },
 
2239
  { name = "python-multipart" },
2240
  { name = "ragas" },
2241
  { name = "requests" },
@@ -2247,27 +2348,42 @@ dependencies = [
2247
  { name = "websockets" },
2248
  ]
2249
 
 
 
 
 
 
 
 
 
2250
  [package.metadata]
2251
  requires-dist = [
2252
  { name = "aiohttp", specifier = ">=3.8.0" },
 
2253
  { name = "chainlit", specifier = "==2.0.4" },
2254
  { name = "dotenv", specifier = ">=0.9.9" },
2255
  { name = "fastapi", specifier = ">=0.115.3,<0.116" },
 
2256
  { name = "httpx", specifier = "==0.27.0" },
2257
  { name = "ipykernel", specifier = ">=6.29.5" },
2258
  { name = "ipywidgets", specifier = ">=8.1.7" },
 
2259
  { name = "jq", specifier = ">=1.8.0" },
2260
  { name = "jupyter", specifier = ">=1.1.1" },
2261
  { name = "langchain", specifier = ">=0.3.25" },
2262
  { name = "langchain-community", specifier = ">=0.3.23" },
 
2263
  { name = "langchain-experimental", specifier = ">=0.3.4" },
2264
  { name = "langchain-openai" },
2265
  { name = "langchain-qdrant", specifier = ">=0.2.0" },
2266
  { name = "langgraph", specifier = ">=0.4.3" },
 
2267
  { name = "numpy", specifier = "==2.2.2" },
2268
  { name = "openai", specifier = "==1.59.9" },
2269
  { name = "pip", specifier = ">=25.0.1" },
2270
  { name = "pydantic", specifier = "==2.10.1" },
 
 
2271
  { name = "python-multipart", specifier = ">=0.0.18,<0.0.19" },
2272
  { name = "ragas", specifier = ">=0.2.15" },
2273
  { name = "requests", specifier = ">=2.31.0" },
@@ -2278,6 +2394,7 @@ requires-dist = [
2278
  { name = "uvicorn", specifier = ">=0.25.0,<0.26.0" },
2279
  { name = "websockets", specifier = "==14.2" },
2280
  ]
 
2281
 
2282
  [[package]]
2283
  name = "psutil"
@@ -2338,6 +2455,15 @@ wheels = [
2338
  { url = "https://files.pythonhosted.org/packages/37/40/ad395740cd641869a13bcf60851296c89624662575621968dcfafabaa7f6/pyarrow-20.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:82f1ee5133bd8f49d31be1299dc07f585136679666b502540db854968576faf9", size = 25944982 },
2339
  ]
2340
 
 
 
 
 
 
 
 
 
 
2341
  [[package]]
2342
  name = "pycparser"
2343
  version = "2.22"
@@ -2400,6 +2526,15 @@ wheels = [
2400
  { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
2401
  ]
2402
 
 
 
 
 
 
 
 
 
 
2403
  [[package]]
2404
  name = "pygments"
2405
  version = "2.19.1"
@@ -2418,6 +2553,15 @@ wheels = [
2418
  { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
2419
  ]
2420
 
 
 
 
 
 
 
 
 
 
2421
  [[package]]
2422
  name = "pypdf"
2423
  version = "5.5.0"
@@ -2427,6 +2571,21 @@ wheels = [
2427
  { url = "https://files.pythonhosted.org/packages/a1/4e/931b90b51e3ebc69699be926b3d5bfdabae2d9c84337fd0c9fb98adbf70c/pypdf-5.5.0-py3-none-any.whl", hash = "sha256:2f61f2d32dde00471cd70b8977f98960c64e84dd5ba0d070e953fcb4da0b2a73", size = 303371 },
2428
  ]
2429
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2430
  [[package]]
2431
  name = "python-dateutil"
2432
  version = "2.9.0.post0"
 
235
  { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 },
236
  ]
237
 
238
+ [[package]]
239
+ name = "black"
240
+ version = "25.1.0"
241
+ source = { registry = "https://pypi.org/simple" }
242
+ dependencies = [
243
+ { name = "click" },
244
+ { name = "mypy-extensions" },
245
+ { name = "packaging" },
246
+ { name = "pathspec" },
247
+ { name = "platformdirs" },
248
+ ]
249
+ sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
250
+ wheels = [
251
+ { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
252
+ { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
253
+ { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
254
+ { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
255
+ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
256
+ ]
257
+
258
  [[package]]
259
  name = "bleach"
260
  version = "6.2.0"
 
633
  { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 },
634
  ]
635
 
636
+ [[package]]
637
+ name = "flake8"
638
+ version = "7.2.0"
639
+ source = { registry = "https://pypi.org/simple" }
640
+ dependencies = [
641
+ { name = "mccabe" },
642
+ { name = "pycodestyle" },
643
+ { name = "pyflakes" },
644
+ ]
645
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/c4/5842fc9fc94584c455543540af62fd9900faade32511fab650e9891ec225/flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426", size = 48177 }
646
+ wheels = [
647
+ { url = "https://files.pythonhosted.org/packages/83/5c/0627be4c9976d56b1217cb5187b7504e7fd7d3503f8bfd312a04077bd4f7/flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", size = 57786 },
648
+ ]
649
+
650
  [[package]]
651
  name = "fqdn"
652
  version = "1.5.1"
 
919
  { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 },
920
  ]
921
 
922
+ [[package]]
923
+ name = "iniconfig"
924
+ version = "2.1.0"
925
+ source = { registry = "https://pypi.org/simple" }
926
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
927
+ wheels = [
928
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
929
+ ]
930
+
931
  [[package]]
932
  name = "ipykernel"
933
  version = "6.29.5"
 
1013
  { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321 },
1014
  ]
1015
 
1016
+ [[package]]
1017
+ name = "isort"
1018
+ version = "6.0.1"
1019
+ source = { registry = "https://pypi.org/simple" }
1020
+ sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 }
1021
+ wheels = [
1022
+ { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 },
1023
+ ]
1024
+
1025
  [[package]]
1026
  name = "jedi"
1027
  version = "0.19.2"
 
1667
  { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 },
1668
  ]
1669
 
1670
+ [[package]]
1671
+ name = "mccabe"
1672
+ version = "0.7.0"
1673
+ source = { registry = "https://pypi.org/simple" }
1674
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 }
1675
+ wheels = [
1676
+ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 },
1677
+ ]
1678
+
1679
  [[package]]
1680
  name = "mistune"
1681
  version = "3.1.3"
 
1744
  { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351 },
1745
  ]
1746
 
1747
+ [[package]]
1748
+ name = "mypy"
1749
+ version = "1.15.0"
1750
+ source = { registry = "https://pypi.org/simple" }
1751
+ dependencies = [
1752
+ { name = "mypy-extensions" },
1753
+ { name = "typing-extensions" },
1754
+ ]
1755
+ sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
1756
+ wheels = [
1757
+ { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
1758
+ { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
1759
+ { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
1760
+ { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
1761
+ { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
1762
+ { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
1763
+ { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
1764
+ ]
1765
+
1766
  [[package]]
1767
  name = "mypy-extensions"
1768
  version = "1.1.0"
 
2174
  { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 },
2175
  ]
2176
 
2177
+ [[package]]
2178
+ name = "pathspec"
2179
+ version = "0.12.1"
2180
+ source = { registry = "https://pypi.org/simple" }
2181
+ sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
2182
+ wheels = [
2183
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
2184
+ ]
2185
+
2186
  [[package]]
2187
  name = "pexpect"
2188
  version = "4.9.0"
 
2213
  { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 },
2214
  ]
2215
 
2216
+ [[package]]
2217
+ name = "pluggy"
2218
+ version = "1.5.0"
2219
+ source = { registry = "https://pypi.org/simple" }
2220
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
2221
+ wheels = [
2222
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
2223
+ ]
2224
+
2225
  [[package]]
2226
  name = "portalocker"
2227
  version = "2.10.1"
 
2322
  { name = "httpx" },
2323
  { name = "ipykernel" },
2324
  { name = "ipywidgets" },
2325
+ { name = "isort" },
2326
  { name = "jq" },
2327
  { name = "jupyter" },
2328
  { name = "langchain" },
2329
  { name = "langchain-community" },
2330
+ { name = "langchain-core" },
2331
  { name = "langchain-experimental" },
2332
  { name = "langchain-openai" },
2333
  { name = "langchain-qdrant" },
 
2336
  { name = "openai" },
2337
  { name = "pip" },
2338
  { name = "pydantic" },
2339
+ { name = "pylint-venv" },
2340
  { name = "python-multipart" },
2341
  { name = "ragas" },
2342
  { name = "requests" },
 
2348
  { name = "websockets" },
2349
  ]
2350
 
2351
+ [package.optional-dependencies]
2352
+ dev = [
2353
+ { name = "black" },
2354
+ { name = "flake8" },
2355
+ { name = "mypy" },
2356
+ { name = "pytest" },
2357
+ ]
2358
+
2359
  [package.metadata]
2360
  requires-dist = [
2361
  { name = "aiohttp", specifier = ">=3.8.0" },
2362
+ { name = "black", marker = "extra == 'dev'", specifier = ">=22.0.0" },
2363
  { name = "chainlit", specifier = "==2.0.4" },
2364
  { name = "dotenv", specifier = ">=0.9.9" },
2365
  { name = "fastapi", specifier = ">=0.115.3,<0.116" },
2366
+ { name = "flake8", marker = "extra == 'dev'", specifier = ">=4.0.0" },
2367
  { name = "httpx", specifier = "==0.27.0" },
2368
  { name = "ipykernel", specifier = ">=6.29.5" },
2369
  { name = "ipywidgets", specifier = ">=8.1.7" },
2370
+ { name = "isort", specifier = ">=6.0.1" },
2371
  { name = "jq", specifier = ">=1.8.0" },
2372
  { name = "jupyter", specifier = ">=1.1.1" },
2373
  { name = "langchain", specifier = ">=0.3.25" },
2374
  { name = "langchain-community", specifier = ">=0.3.23" },
2375
+ { name = "langchain-core", specifier = ">=0.3.59" },
2376
  { name = "langchain-experimental", specifier = ">=0.3.4" },
2377
  { name = "langchain-openai" },
2378
  { name = "langchain-qdrant", specifier = ">=0.2.0" },
2379
  { name = "langgraph", specifier = ">=0.4.3" },
2380
+ { name = "mypy", marker = "extra == 'dev'", specifier = ">=0.900" },
2381
  { name = "numpy", specifier = "==2.2.2" },
2382
  { name = "openai", specifier = "==1.59.9" },
2383
  { name = "pip", specifier = ">=25.0.1" },
2384
  { name = "pydantic", specifier = "==2.10.1" },
2385
+ { name = "pylint-venv", specifier = ">=3.0.4" },
2386
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
2387
  { name = "python-multipart", specifier = ">=0.0.18,<0.0.19" },
2388
  { name = "ragas", specifier = ">=0.2.15" },
2389
  { name = "requests", specifier = ">=2.31.0" },
 
2394
  { name = "uvicorn", specifier = ">=0.25.0,<0.26.0" },
2395
  { name = "websockets", specifier = "==14.2" },
2396
  ]
2397
+ provides-extras = ["dev"]
2398
 
2399
  [[package]]
2400
  name = "psutil"
 
2455
  { url = "https://files.pythonhosted.org/packages/37/40/ad395740cd641869a13bcf60851296c89624662575621968dcfafabaa7f6/pyarrow-20.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:82f1ee5133bd8f49d31be1299dc07f585136679666b502540db854968576faf9", size = 25944982 },
2456
  ]
2457
 
2458
+ [[package]]
2459
+ name = "pycodestyle"
2460
+ version = "2.13.0"
2461
+ source = { registry = "https://pypi.org/simple" }
2462
+ sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312 }
2463
+ wheels = [
2464
+ { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424 },
2465
+ ]
2466
+
2467
  [[package]]
2468
  name = "pycparser"
2469
  version = "2.22"
 
2526
  { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
2527
  ]
2528
 
2529
+ [[package]]
2530
+ name = "pyflakes"
2531
+ version = "3.3.2"
2532
+ source = { registry = "https://pypi.org/simple" }
2533
+ sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175 }
2534
+ wheels = [
2535
+ { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164 },
2536
+ ]
2537
+
2538
  [[package]]
2539
  name = "pygments"
2540
  version = "2.19.1"
 
2553
  { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
2554
  ]
2555
 
2556
+ [[package]]
2557
+ name = "pylint-venv"
2558
+ version = "3.0.4"
2559
+ source = { registry = "https://pypi.org/simple" }
2560
+ sdist = { url = "https://files.pythonhosted.org/packages/48/ac/88e53ad1362392e68a36a4f08ca82c76e6cb6321d8ecba901cca5a70dcc1/pylint_venv-3.0.4.tar.gz", hash = "sha256:4c71c2ad14fc0549699440bcc460994ffddc1481b9078404b4c43d2806ef0f59", size = 5772 }
2561
+ wheels = [
2562
+ { url = "https://files.pythonhosted.org/packages/a6/10/040e1928236e3d34b26639e3427df88c7249a85aadc621cea2158589b4f8/pylint_venv-3.0.4-py3-none-any.whl", hash = "sha256:31006a3df398f58f962c9e5620e756b284e8b2bc490594ce5ee5da41920cb32c", size = 5312 },
2563
+ ]
2564
+
2565
  [[package]]
2566
  name = "pypdf"
2567
  version = "5.5.0"
 
2571
  { url = "https://files.pythonhosted.org/packages/a1/4e/931b90b51e3ebc69699be926b3d5bfdabae2d9c84337fd0c9fb98adbf70c/pypdf-5.5.0-py3-none-any.whl", hash = "sha256:2f61f2d32dde00471cd70b8977f98960c64e84dd5ba0d070e953fcb4da0b2a73", size = 303371 },
2572
  ]
2573
 
2574
+ [[package]]
2575
+ name = "pytest"
2576
+ version = "8.3.5"
2577
+ source = { registry = "https://pypi.org/simple" }
2578
+ dependencies = [
2579
+ { name = "colorama", marker = "sys_platform == 'win32'" },
2580
+ { name = "iniconfig" },
2581
+ { name = "packaging" },
2582
+ { name = "pluggy" },
2583
+ ]
2584
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
2585
+ wheels = [
2586
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
2587
+ ]
2588
+
2589
  [[package]]
2590
  name = "python-dateutil"
2591
  version = "2.9.0.post0"