simonbb 0.0.9__tar.gz → 0.0.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simonbb
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: A small package to facilitate teaching AI courses at business schools
5
5
  Project-URL: Homepage, https://simonbb.com
6
6
  Author-email: Huaxia Rui <huaxia.rui@simon.rochester.edu>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simonbb"
7
- version = "0.0.9"
7
+ version = "0.0.10"
8
8
  authors = [
9
9
  { name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
10
10
  ]
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  import re
5
4
  from typing import Annotated, TypedDict, get_args, Any, Protocol, Sequence
6
5
  from dataclasses import dataclass
@@ -25,6 +24,7 @@ from langchain_community.document_loaders import PyPDFLoader, UnstructuredHTMLLo
25
24
  from langchain_core.callbacks import CallbackManagerForRetrieverRun
26
25
 
27
26
  import hashlib
27
+ from uuid import uuid4
28
28
  import chromadb
29
29
  from langchain_core.embeddings import Embeddings
30
30
  from langchain_core.language_models import BaseChatModel
@@ -35,13 +35,10 @@ from langchain_text_splitters import RecursiveCharacterTextSplitter
35
35
 
36
36
  def load_documents(path: Path):
37
37
  """Load documents from a file or directory."""
38
-
39
- valid_extensions = {'.pdf', '.html', '.ejs'}
40
-
38
+ valid_extensions = {'.pdf', '.html', '.ejs'}
41
39
  docs = []
42
40
  success_cnt = 0
43
- page_cnt = 0
44
-
41
+ page_cnt = 0
45
42
  if path.exists():
46
43
  if path.is_dir():
47
44
  for file in [ item for item in path.rglob('*') if item.is_file() and item.suffix in valid_extensions ]:
@@ -77,25 +74,100 @@ def load_documents(path: Path):
77
74
 
78
75
  return docs, success_cnt, page_cnt
79
76
 
77
+ #%% RAG Chains
78
+
79
+ system_prompt = """You are a helpful assistant answering questions based on provided document snippets.
80
+ IMPORTANT INSTRUCTIONS:
81
+ 1. Base your answer ONLY on the provided context snippets below.
82
+ 2. Synthesize information from multiple snippets if needed.
83
+ 3. If the snippets contain partial information, provide the best answer you can with what's available.
84
+ 4. Only say "There wasn't enough information to answer the question" if the snippets truly contain NO relevant information.
85
+ 5. Always cite your sources using the snippet numbers in parentheses, like (1), (2), etc.
86
+
87
+ The snippets are provided in this format:
88
+ Snippet 1: <text content>
89
+ Snippet 2: <text content>
90
+ ...
91
+
92
+ {context}"""
93
+
94
+ def format_documents(documents: Sequence[Document]) -> str:
95
+ snippets = []
96
+ for i, doc in enumerate(documents, start=1):
97
+ snippets.append(f"Snippet {i}: {doc.page_content}")
98
+ context = "\n".join(snippets)
99
+ return context
100
+
101
+ def rag_basic(retriever: BaseRetriever, model: BaseChatModel) -> Runnable:
102
+ """Return an LCEL chain accepting a question string and producing an answer string."""
103
+ prompt = ChatPromptTemplate.from_messages(
104
+ [
105
+ ( "system", "Answer using only the context below. If the answer is absent, say you do not know.\n\nContext:\n{context}" ),
106
+ ("human", "{query}"),
107
+ ]
108
+ )
109
+ inputs = {
110
+ "context": retriever | format_documents,
111
+ "query": RunnablePassthrough(),
112
+ }
113
+ return inputs | prompt | model | StrOutputParser()
114
+
115
+
116
+ def rag_main(retriever: BaseRetriever, model: BaseChatModel) -> Runnable:
117
+ """Return an LCEL chain accepting a question string and producing an dic with 4 keys/values
118
+ -- 'sources': original list of Documents from retriever
119
+ -- 'query': original query
120
+ -- 'context': merged content from relevant documents
121
+ -- 'answer': final answer """
122
+ prompt = ChatPromptTemplate.from_messages([
123
+ ("system", system_prompt),
124
+ ("human", "{query}")
125
+ ])
126
+ return (
127
+ {
128
+ "sources": retriever,
129
+ "query": RunnablePassthrough(),
130
+ }
131
+ | RunnablePassthrough.assign( context=lambda x: format_documents(x["sources"]) )
132
+ | RunnablePassthrough.assign( answer=prompt | model | StrOutputParser() )
133
+ )
134
+
135
+ build_rag = rag_main
136
+
137
+
138
+ """
139
+ Retrievers are subclasses of LangChain's BaseRetriever which inherits from Pydantic's BaseModel.
140
+ Unlike ordinary Python classes where attributes declared in the class body are class attributes shared by all instances,
141
+ Pydantic model fields declared with type annotations are instance attributes, not shared class attributes, even though they are declared in the class body.
142
+ """
80
143
  #%% MultiQueryRerankRetriever
81
144
 
82
145
  class MultiQueryRerankRetriever(BaseRetriever):
83
146
  """Chroma retriever with optional multi-query retrieval and reranking."""
84
- knowledge_base: str
85
147
  embeddings: Embeddings = Field(exclude=True)
86
148
  query_llm: BaseChatModel | None = Field(default=None, exclude=True)
87
149
  reranker: Any = Field(default=None, exclude=True)
88
150
  k: int = Field(default=3, ge=1)
89
151
  top_n: int = Field(default=5, ge=1)
90
- vstore_path: str = Field( default_factory=lambda: f"{os.path.expanduser('~')}/vstore" )
152
+ persist_directory: str | Path
153
+ collection_name: str
154
+ create_collection_if_not_exists: bool = False
155
+ store: Chroma = None
91
156
  _base_retriever: BaseRetriever = PrivateAttr()
92
157
 
158
+ """ model_post_init() is a special method in Pydantic v2 that runs automatically after a model instance has been initialized and its fields have been validated."""
93
159
  def model_post_init(self, __context: Any) -> None:
94
- """Initialize the underlying Chroma retriever."""
95
- store = Chroma( persist_directory=( f"{self.vstore_path}/{self.knowledge_base}/BAAI_db/chroma_vector_store"), embedding_function=self.embeddings )
96
- self._base_retriever = store.as_retriever( search_kwargs={"k": self.k} )
160
+ """Initialize the underlying Chroma retriever. Use the last folder name as collection name"""
161
+ self.store = Chroma( persist_directory=self.persist_directory, embedding_function=self.embeddings, collection_name=self.collection_name, create_collection_if_not_exists=self.create_collection_if_not_exists )
162
+ self._base_retriever = self.store.as_retriever( search_kwargs={"k": self.k} )
97
163
  if self.query_llm is not None and self.reranker is None:
98
164
  raise ValueError( "A reranker is required when query_llm is provided." )
165
+
166
+ def ingest( self, documents: Sequence[Document], *, source_dir: str = 'placeholder', chunk_size: int = 1000, chunk_overlap: int = 150 ):
167
+ text_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "\n", ". ", " ", ""], chunk_size=chunk_size, chunk_overlap=chunk_overlap )
168
+ chunks = text_splitter.split_documents( documents )
169
+ self.store.add_documents( chunks )
170
+
99
171
  # run_manager manages callbacks, tracing, and monitoring during a retriever's execution, allowing integrations with LangSmith
100
172
  def _generate_queries( self, query: str, run_manager: CallbackManagerForRetrieverRun ) -> list[str]:
101
173
  """Generate five alternative formulations of the query."""
@@ -143,12 +215,90 @@ class MultiQueryRerankRetriever(BaseRetriever):
143
215
  # Deduplicate and rerank
144
216
  unique_docs = self._unique_union(doc_lists)
145
217
  return self._rerank(query, unique_docs)
218
+
219
+ #%%
220
+
221
+ class DummyEmbeddings(Embeddings):
222
+ """Return constant 1-dimensional embeddings without using an LLM."""
223
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
224
+ return [[0.0] for _ in texts]
225
+ def embed_query(self, text: str) -> list[float]:
226
+ return [0.0]
227
+
228
+ class ParentDocumentRetriever(BaseRetriever):
229
+ persist_directory: str | Path
230
+ parent_collection: str
231
+ child_collection: str
232
+ embeddings: Embeddings
233
+ k: int = 5
234
+ _parent_store: Chroma = PrivateAttr()
235
+ _child_store: Chroma = PrivateAttr()
236
+
237
+ def __init__(self, **kwargs):
238
+ super().__init__(**kwargs)
239
+ self._parent_store = Chroma(
240
+ embedding_function=DummyEmbeddings(),
241
+ collection_name=self.parent_collection,
242
+ persist_directory=self.persist_directory,
243
+ )
244
+
245
+ self._child_store = Chroma(
246
+ embedding_function=self.embeddings,
247
+ collection_name=self.child_collection,
248
+ persist_directory=self.persist_directory,
249
+ )
250
+
251
+ def ingest(self, docs: list[Document], *, source_dir: str = 'placeholder', parent_chunk_size: int = 600, parent_chunk_overlap: int = 100, child_chunk_size: int = 200, child_chunk_overlap: int = 50) -> None:
252
+ """Split documents and store parent and child chunks."""
253
+ parent_splitter = RecursiveCharacterTextSplitter( chunk_size=parent_chunk_size, chunk_overlap=parent_chunk_overlap )
254
+ child_splitter = RecursiveCharacterTextSplitter( chunk_size=child_chunk_size, chunk_overlap=child_chunk_overlap )
255
+
256
+ parent_docs = []
257
+ parent_ids = []
258
+ child_docs = []
259
+
260
+ for parent in parent_splitter.split_documents(docs):
261
+ parent_id = str(uuid4())
262
+ parent_docs.append(parent)
263
+ parent_ids.append(parent_id)
264
+ for child in child_splitter.split_documents( [parent] ):
265
+ child.metadata["parent_id"] = parent_id
266
+ child_docs.append(child)
267
+ if parent_docs:
268
+ self._parent_store.add_documents( documents=parent_docs, ids=parent_ids )
269
+ if child_docs:
270
+ self._child_store.add_documents( documents=child_docs )
271
+
272
+ def _get_relevant_documents(
273
+ self,
274
+ query: str,
275
+ *,
276
+ run_manager: CallbackManagerForRetrieverRun,
277
+ ) -> list[Document]:
278
+ """Retrieve parent documents using child similarity search."""
279
+
280
+ # 1. Search child chunks
281
+ child_chunks = self._child_store.similarity_search( query, k=self.k )
282
+ # 2. Extract unique parent IDs in relevance order
283
+ parent_ids = list(dict.fromkeys( # standard trick to remove duplicates from a list or iterable while preserving the original item orde
284
+ chunk.metadata["parent_id"] for chunk in child_chunks if "parent_id" in chunk.metadata
285
+ ))
286
+ if not parent_ids:
287
+ return []
288
+ # 3. Fetch parent documents
289
+ parents = self._parent_store.get_by_ids( parent_ids )
290
+ parents_by_id = { doc.id: doc for doc in parents if doc.id is not None }
291
+ # 4. Restore relevance order
292
+ return [
293
+ parents_by_id[pid]
294
+ for pid in parent_ids if pid in parents_by_id
295
+ ]
146
296
 
147
297
 
148
298
  #%% Hypothetical Question Retriever
149
299
 
150
- def _stable_id(source_id: str, index: int, text: str) -> str:
151
- digest = hashlib.sha256(f"{source_id}\0{index}\0{text}".encode()).hexdigest()
300
+ def _stable_id(source_dir: str, index: int, text: str) -> str:
301
+ digest = hashlib.sha256(f"{source_dir}\0{index}\0{text}".encode()).hexdigest()
152
302
  return f"chunk-{digest[:32]}"
153
303
 
154
304
  def _clean_questions(questions: Sequence[str]) -> list[str]:
@@ -169,9 +319,6 @@ def _chroma_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
169
319
  if value is not None and isinstance(value, (str, int, float, bool))
170
320
  }
171
321
 
172
- class QuestionGenerator(Protocol):
173
- """Anything capable of generating search questions for a chunk."""
174
- def generate(self, document: Document) -> list[str]: ...
175
322
 
176
323
  class _QuestionList(BaseModel):
177
324
  questions: list[str] = Field(description="Distinct questions answered by the text")
@@ -179,7 +326,7 @@ class _QuestionList(BaseModel):
179
326
  class HypotheticalQuestionGenerator:
180
327
  """LLM-backed question generator using provider-supported structured output."""
181
328
 
182
- def __init__(self, model: BaseChatModel, *, questions_per_chunk: int = 4) -> None:
329
+ def __init__(self, llm: BaseChatModel, *, questions_per_chunk: int = 4) -> None:
183
330
  if questions_per_chunk < 1:
184
331
  raise ValueError("questions_per_chunk must be positive")
185
332
  self.questions_per_chunk = questions_per_chunk
@@ -189,7 +336,7 @@ class HypotheticalQuestionGenerator:
189
336
  ("human", "Text:\n{text}"),
190
337
  ]
191
338
  )
192
- self._chain = prompt | model.with_structured_output(_QuestionList)
339
+ self._chain = prompt | llm.with_structured_output(_QuestionList)
193
340
 
194
341
  def generate(self, document: Document) -> list[str]:
195
342
  result = self._chain.invoke( {"count": self.questions_per_chunk, "text": document.page_content} )
@@ -203,38 +350,40 @@ class IngestionResult:
203
350
  chunk_ids: tuple[str, ...]
204
351
 
205
352
  class HypotheticalQuestionRetriever(BaseRetriever):
206
- """Search question vectors, then resolve their chunk IDs from the chunk collection."""
207
- model_config = ConfigDict(arbitrary_types_allowed=True)
208
- question_store: Chroma
209
- chunk_collection: Any
210
- generator: Any
353
+ """Search question vectors, then resolve their chunk IDs from the chunk collection."""
354
+ llm: Any = None
355
+ persist_directory: str | Path
356
+ embeddings: Embeddings
357
+ question_collection: str
358
+ chunk_collection: str
211
359
  question_k: int = 12
212
360
  chunk_k: int = 4
213
361
  metadata_filter: dict[str, Any] | None = None
362
+ create_collection_if_not_exists: bool = False
363
+ _question_store: Chroma = PrivateAttr()
364
+ _chunk_collection: Any = PrivateAttr()
365
+ _generator: Any = PrivateAttr()
214
366
 
215
- def __init__(self, *, persist_directory: str | Path, embeddings: Embeddings, generator: QuestionGenerator, question_collection: str = "hypothetical_questions", chunk_collection: str = "original_chunks", question_k: int = 12, chunk_k: int = 4, metadata_filter: dict[str, Any] | None = None, **kwargs: Any) -> None:
216
- client = chromadb.PersistentClient(path=str(persist_directory))
217
- question_store = Chroma( client=client, collection_name=question_collection, embedding_function=embeddings )
218
- chunks = client.get_or_create_collection( name=chunk_collection, embedding_function=None )
219
- super().__init__(
220
- question_store=question_store,
221
- chunk_collection=chunks,
222
- generator=generator,
223
- question_k=question_k,
224
- chunk_k=chunk_k,
225
- metadata_filter=metadata_filter,
226
- **kwargs,
227
- )
367
+ model_config = ConfigDict(arbitrary_types_allowed=True)
228
368
 
229
- def ingest( self, documents: Sequence[Document], *, source_id: str, split: bool = True, chunk_size: int = 1000, chunk_overlap: int = 150, replace_source: bool = True ) -> IngestionResult:
230
- if not source_id: # source_id is a unique identifier for the source document or documents being ingested
231
- raise ValueError("source_id must not be empty")
369
+ def __init__(self, **kwargs) -> None:
370
+ super().__init__(**kwargs)
371
+ client = chromadb.PersistentClient( path= self.persist_directory )
372
+ self._question_collection = Chroma( client=client, collection_name=self.question_collection, embedding_function=self.embeddings, create_collection_if_not_exists=self.create_collection_if_not_exists )
373
+ self._chunk_collection = client.get_or_create_collection( name=self.chunk_collection, embedding_function=None )
374
+
375
+ def ingest( self, documents: Sequence[Document], *, source_dir: str, split: bool = True, chunk_size: int = 500, chunk_overlap: int = 150, replace_source: bool = True ) -> IngestionResult:
376
+ if not self.llm:
377
+ raise ValueError("To ingest, llm must be provided in the constructor.")
378
+ self._generator = HypotheticalQuestionGenerator( self.llm )
379
+ if not source_dir: # source_dir is a unique identifier for the source document or documents being ingested
380
+ raise ValueError("source_dir must not be empty")
232
381
  splitter = RecursiveCharacterTextSplitter( chunk_size=chunk_size, chunk_overlap=chunk_overlap, add_start_index=True )
233
382
  input_docs = splitter.split_documents( list(documents) ) if split else list(documents)
234
383
 
235
384
  if replace_source:
236
- self.question_store.delete(where={"source_id": source_id})
237
- self.chunk_collection.delete(where={"source_id": source_id})
385
+ self._question_collection.delete( where={"source_dir": source_dir} )
386
+ self._chunk_collection.delete( where={"source_dir": source_dir} )
238
387
 
239
388
  chunk_ids: list[str] = []
240
389
  question_docs: list[Document] = []
@@ -243,32 +392,31 @@ class HypotheticalQuestionRetriever(BaseRetriever):
243
392
  chunk_texts: list[str] = []
244
393
 
245
394
  for index, doc in enumerate(input_docs):
246
- chunk_id = _stable_id(source_id, index, doc.page_content)
247
- metadata = _chroma_metadata( {**doc.metadata, "source_id": source_id, "chunk_id": chunk_id} )
395
+ chunk_id = _stable_id(source_dir, index, doc.page_content)
396
+ metadata = _chroma_metadata( {**doc.metadata, "source_dir": source_dir, "chunk_id": chunk_id} )
248
397
  chunk_ids.append( chunk_id )
249
398
  chunk_metadatas.append( metadata )
250
399
  chunk_texts.append( doc.page_content )
251
-
252
- questions = _clean_questions( self.generator.generate(doc) )
400
+ questions = _clean_questions( self._generator.generate(doc) )
253
401
  for question_index, question in enumerate(questions):
254
402
  question_ids.append( f"{chunk_id}:q:{question_index}" )
255
403
  question_docs.append(
256
404
  Document( page_content=question,
257
- metadata={"chunk_id": chunk_id, "source_id": source_id, "question_index": question_index },
405
+ metadata={"chunk_id": chunk_id, "source_dir": source_dir, "question_index": question_index },
258
406
  )
259
407
  )
260
408
  if question_docs: # LangChain's Chroma.add_documents() triggers embedding
261
- self.question_store.add_documents( question_docs, ids=question_ids )
409
+ self._question_collection.add_documents( question_docs, ids=question_ids )
262
410
 
263
411
  if chunk_ids: # Chroma's underlying collection API stores text and metadata without embeddings.
264
- self.chunk_collection.upsert( ids=chunk_ids, documents=chunk_texts, metadatas=chunk_metadatas,
412
+ self._chunk_collection.upsert( ids=chunk_ids, documents=chunk_texts, metadatas=chunk_metadatas,
265
413
  embeddings=[[0.0] for _ in chunk_ids] ) # placeholder vectors since Chroma records require vectors
266
414
 
267
415
  return IngestionResult(len(chunk_ids), len(question_docs), tuple(chunk_ids))
268
416
 
269
417
  # override _get_relevant_documents() method from BaseRetriever
270
418
  def _get_relevant_documents(self, query: str, *, run_manager: Any = None) -> list[Document]:
271
- matches = self.question_store.similarity_search( query, k=self.question_k, filter=self.metadata_filter )
419
+ matches = self._question_collection.similarity_search( query, k=self.question_k, filter=self.metadata_filter )
272
420
  chunk_ids: list[str] = []
273
421
  seen: set[str] = set()
274
422
  for m in matches:
@@ -281,7 +429,7 @@ class HypotheticalQuestionRetriever(BaseRetriever):
281
429
  if not chunk_ids:
282
430
  return []
283
431
 
284
- records = self.chunk_collection.get(ids=chunk_ids, include=["documents", "metadatas"])
432
+ records = self._chunk_collection.get(ids=chunk_ids, include=["documents", "metadatas"])
285
433
  by_id = {
286
434
  record_id: Document(page_content=text, metadata=metadata or {})
287
435
  for record_id, text, metadata in zip(
@@ -290,65 +438,7 @@ class HypotheticalQuestionRetriever(BaseRetriever):
290
438
  }
291
439
  return [by_id[chunk_id] for chunk_id in chunk_ids if chunk_id in by_id]
292
440
 
293
- #%% RAG Chains
294
-
295
- system_prompt = """You are a helpful assistant answering questions based on provided document snippets.
296
- IMPORTANT INSTRUCTIONS:
297
- 1. Base your answer ONLY on the provided context snippets below.
298
- 2. Synthesize information from multiple snippets if needed.
299
- 3. If the snippets contain partial information, provide the best answer you can with what's available.
300
- 4. Only say "There wasn't enough information to answer the question" if the snippets truly contain NO relevant information.
301
- 5. Always cite your sources using the snippet numbers in parentheses, like (1), (2), etc.
302
-
303
- The snippets are provided in this format:
304
- Snippet 1: <text content>
305
- Snippet 2: <text content>
306
- ...
307
-
308
- {context}"""
309
-
310
- def format_documents(documents: Sequence[Document]) -> str:
311
- snippets = []
312
- for i, doc in enumerate(documents, start=1):
313
- snippets.append(f"Snippet {i}: {doc.page_content}")
314
- context = "\n".join(snippets)
315
- return context
316
-
317
- def rag_basic(retriever: BaseRetriever, model: BaseChatModel) -> Runnable:
318
- """Return an LCEL chain accepting a question string and producing an answer string."""
319
- prompt = ChatPromptTemplate.from_messages(
320
- [
321
- ( "system", "Answer using only the context below. If the answer is absent, say you do not know.\n\nContext:\n{context}" ),
322
- ("human", "{query}"),
323
- ]
324
- )
325
- inputs = {
326
- "context": retriever | format_documents,
327
- "query": RunnablePassthrough(),
328
- }
329
- return inputs | prompt | model | StrOutputParser()
330
-
331
-
332
- def rag_main(retriever: BaseRetriever, model: BaseChatModel) -> Runnable:
333
- """Return an LCEL chain accepting a question string and producing an dic with 4 keys/values
334
- -- 'sources': original list of Documents from retriever
335
- -- 'query': original query
336
- -- 'context': merged content from relevant documents
337
- -- 'answer': final answer """
338
- prompt = ChatPromptTemplate.from_messages([
339
- ("system", system_prompt),
340
- ("human", "{query}")
341
- ])
342
- return (
343
- {
344
- "sources": retriever,
345
- "query": RunnablePassthrough(),
346
- }
347
- | RunnablePassthrough.assign( context=lambda x: format_documents(x["sources"]) )
348
- | RunnablePassthrough.assign( answer=prompt | model | StrOutputParser() )
349
- )
350
441
 
351
- build_rag = rag_main
352
442
 
353
443
  #%% RAG with Routing
354
444
 
@@ -368,7 +458,7 @@ Snippet 2: <text content>
368
458
 
369
459
  {context}"""
370
460
 
371
- def multiRagBuilder( domain_literals, router_prompt, llm, embeddings, reranker, query_llm=None, k=3):
461
+ def multiRagBuilder( persist_directory, domain_literals, router_prompt, llm, embeddings, reranker, query_llm=None, k=3):
372
462
  """
373
463
  domain_literals: the last one is a placeholder if the router finds no domain matches the query
374
464
  """
@@ -444,7 +534,7 @@ def multiRagBuilder( domain_literals, router_prompt, llm, embeddings, reranker,
444
534
  retrievers = {}
445
535
  retriever_nodes = {}
446
536
  for domain in domains:
447
- retrievers[domain] = MultiQueryRerankRetriever(knowledge_base=domain, embeddings=embeddings, query_llm=query_llm, reranker=reranker, k=k)
537
+ retrievers[domain] = MultiQueryRerankRetriever( persist_directory=persist_directory, collection_name=domain, embeddings=embeddings, query_llm=query_llm, reranker=reranker, k=k)
448
538
  retriever_nodes[domain] = partial( retriever_node, domain ) # the 1st argument is the partial function. the rest are frozen arguments passed positionally to fill the leftmost parameters of the original function.
449
539
  builder.add_node(domain, retriever_nodes[domain])
450
540
  builder.add_edge(domain, "generate_answer")
File without changes
File without changes
File without changes