langroid 0.46.0__py3-none-any.whl → 0.47.1__py3-none-any.whl

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.
@@ -50,7 +50,7 @@ from langroid.parsing.search import (
50
50
  preprocess_text,
51
51
  )
52
52
  from langroid.parsing.table_loader import describe_dataframe
53
- from langroid.parsing.url_loader import URLLoader
53
+ from langroid.parsing.url_loader import BaseCrawlerConfig, TrafilaturaConfig, URLLoader
54
54
  from langroid.parsing.urls import get_list_from_user, get_urls_paths_bytes_indices
55
55
  from langroid.prompts.prompts_config import PromptsConfig
56
56
  from langroid.prompts.templates import SUMMARY_ANSWER_PROMPT_GPT4
@@ -192,6 +192,7 @@ class DocChatAgentConfig(ChatAgentConfig):
192
192
  library="pymupdf4llm",
193
193
  ),
194
194
  )
195
+ crawler_config: Optional[BaseCrawlerConfig] = TrafilaturaConfig()
195
196
 
196
197
  # Allow vecdb to be None in case we want to explicitly set it later
197
198
  vecdb: Optional[VectorStoreConfig] = QdrantDBConfig(
@@ -236,9 +237,7 @@ class DocChatAgent(ChatAgent):
236
237
  self.chunked_docs: List[Document] = []
237
238
  self.chunked_docs_clean: List[Document] = []
238
239
  self.response: None | Document = None
239
-
240
- if len(config.doc_paths) > 0:
241
- self.ingest()
240
+ self.ingest()
242
241
 
243
242
  def clear(self) -> None:
244
243
  """Clear the document collection and the specific collection in vecdb"""
@@ -274,8 +273,9 @@ class DocChatAgent(ChatAgent):
274
273
  # But let's get all the chunked docs so we can
275
274
  # do keyword and other non-vector searches
276
275
  if self.vecdb is None:
277
- raise ValueError("VecDB not set")
278
- self.setup_documents(filter=self.config.filter)
276
+ logger.warning("VecDB not set: cannot ingest docs.")
277
+ else:
278
+ self.setup_documents(filter=self.config.filter)
279
279
  return
280
280
  self.ingest_doc_paths(self.config.doc_paths) # type: ignore
281
281
 
@@ -336,11 +336,15 @@ class DocChatAgent(ChatAgent):
336
336
  urls_meta = {u: idx2meta[u] for u in url_idxs}
337
337
  paths_meta = {p: idx2meta[p] for p in path_idxs}
338
338
  docs: List[Document] = []
339
- parser = Parser(self.config.parsing)
339
+ parser: Parser = Parser(self.config.parsing)
340
340
  if len(urls) > 0:
341
341
  for ui in url_idxs:
342
342
  meta = urls_meta.get(ui, {})
343
- loader = URLLoader(urls=[all_paths[ui]], parser=parser) # type: ignore
343
+ loader = URLLoader(
344
+ urls=[all_paths[ui]],
345
+ parsing_config=self.config.parsing,
346
+ crawler_config=self.config.crawler_config,
347
+ ) # type: ignore
344
348
  url_docs = loader.load()
345
349
  # update metadata of each doc with meta
346
350
  for d in url_docs:
@@ -466,6 +470,11 @@ class DocChatAgent(ChatAgent):
466
470
  docs = docs[: self.config.parsing.max_chunks]
467
471
  # vecdb should take care of adding docs in batches;
468
472
  # batching can be controlled via vecdb.config.batch_size
473
+ if not docs:
474
+ logging.warning(
475
+ "No documents to ingest after processing. Skipping VecDB addition."
476
+ )
477
+ return 0 # Return 0 since no documents were added
469
478
  self.vecdb.add_documents(docs)
470
479
  self.original_docs_length = self.doc_length(docs)
471
480
  self.setup_documents(docs, filter=self.config.filter)
@@ -1,5 +1,5 @@
1
1
  """
2
- Deprecated: use DocChatAgent instead, with DocChatAgentConfig.retrieve_only=True,
2
+ DEPRECATED: use DocChatAgent instead, with DocChatAgentConfig.retrieve_only=True,
3
3
  and if you want to retrieve FULL relevant doc-contents rather than just extracts,
4
4
  then set DocChatAgentConfig.extraction_granularity=-1
5
5
 
@@ -47,10 +47,11 @@ class RetrieverAgent(DocChatAgent):
47
47
  )
48
48
 
49
49
  def get_records(self) -> Sequence[Document]:
50
- raise NotImplementedError
50
+ # subclasses should override
51
+ return []
51
52
 
52
53
  def ingest(self) -> None:
53
54
  records = self.get_records()
54
55
  if self.vecdb is None:
55
- raise ValueError("No vector store specified")
56
+ logger.warning("Vector store not configured. Cannot ingest records.")
56
57
  self.vecdb.add_documents(records)
@@ -1,120 +1,340 @@
1
1
  import logging
2
2
  import os
3
+ from abc import ABC, abstractmethod
3
4
  from tempfile import NamedTemporaryFile
4
- from typing import List, no_type_check
5
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
5
6
 
6
- import requests
7
+ from dotenv import load_dotenv
7
8
 
9
+ from langroid.exceptions import LangroidImportError
8
10
  from langroid.mytypes import DocMetaData, Document
9
11
  from langroid.parsing.document_parser import DocumentParser, ImagePdfParser
10
12
  from langroid.parsing.parser import Parser, ParsingConfig
13
+ from langroid.pydantic_v1 import BaseSettings
11
14
 
12
- logging.getLogger("trafilatura").setLevel(logging.ERROR)
15
+ if TYPE_CHECKING:
16
+ from firecrawl import FirecrawlApp
13
17
 
18
+ load_dotenv()
14
19
 
15
- class URLLoader:
16
- """
17
- Load a list of URLs and extract the text content.
18
- Alternative approaches could use `bs4` or `scrapy`.
19
-
20
- TODO - this currently does not handle cookie dialogs,
21
- i.e. if there is a cookie pop-up, most/all of the extracted
22
- content could be cookie policy text.
23
- We could use `playwright` to simulate a user clicking
24
- the "accept" button on the cookie dialog.
25
- """
26
-
27
- def __init__(self, urls: List[str], parser: Parser = Parser(ParsingConfig())):
28
- self.urls = urls
29
- self.parser = parser
20
+ logging.getLogger("url_loader").setLevel(logging.WARNING)
30
21
 
31
- @no_type_check
32
- def load(self) -> List[Document]:
33
- import trafilatura
34
- from trafilatura.downloads import (
35
- add_to_compressed_dict,
36
- buffered_downloads,
37
- load_download_buffer,
38
- )
39
22
 
40
- docs = []
41
- threads = 4
42
- # converted the input list to an internal format
43
- dl_dict = add_to_compressed_dict(self.urls)
44
- # processing loop
45
- while not dl_dict.done:
46
- buffer, dl_dict = load_download_buffer(
47
- dl_dict,
48
- sleep_time=5,
49
- )
50
- for url, result in buffered_downloads(buffer, threads):
51
- if (
52
- url.lower().endswith(".pdf")
53
- or url.lower().endswith(".docx")
54
- or url.lower().endswith(".doc")
55
- ):
56
- try:
57
- doc_parser = DocumentParser.create(
58
- url,
59
- self.parser.config,
60
- )
61
- except Exception as e:
62
- logging.error(f"Error parsing {url}: {e}")
63
- continue
23
+ # Base crawler config and specific configurations
24
+ class BaseCrawlerConfig(BaseSettings):
25
+ """Base configuration for web crawlers."""
26
+
27
+ parser: Optional[Parser] = None
28
+
29
+
30
+ class TrafilaturaConfig(BaseCrawlerConfig):
31
+ """Configuration for Trafilatura crawler."""
32
+
33
+ threads: int = 4
34
+
35
+
36
+ class FirecrawlConfig(BaseCrawlerConfig):
37
+ """Configuration for Firecrawl crawler."""
38
+
39
+ api_key: str = ""
40
+ mode: str = "scrape"
41
+ params: Dict[str, Any] = {}
42
+ timeout: Optional[int] = None
43
+
44
+ class Config:
45
+ # Leverage Pydantic's BaseSettings to
46
+ # allow setting of fields via env vars,
47
+ # e.g. FIRECRAWL_MODE=scrape and FIRECRAWL_API_KEY=...
48
+ env_prefix = "FIRECRAWL_"
49
+
50
+
51
+ class BaseCrawler(ABC):
52
+ """Abstract base class for web crawlers."""
53
+
54
+ def __init__(self, config: BaseCrawlerConfig):
55
+ """Initialize the base crawler.
56
+
57
+ Args:
58
+ config: Configuration for the crawler
59
+ """
60
+ self.parser = config.parser if self.needs_parser else None
61
+ self.config: BaseCrawlerConfig = config
62
+
63
+ @property
64
+ @abstractmethod
65
+ def needs_parser(self) -> bool:
66
+ """Indicates whether the crawler requires a parser."""
67
+ pass
68
+
69
+ @abstractmethod
70
+ def crawl(self, urls: List[str]) -> List[Document]:
71
+ pass
72
+
73
+ def _process_document(self, url: str) -> List[Document]:
74
+ if self.parser:
75
+ import requests
76
+ from requests.structures import CaseInsensitiveDict
77
+
78
+ if self._is_document_url(url):
79
+ try:
80
+ doc_parser = DocumentParser.create(url, self.parser.config)
64
81
  new_chunks = doc_parser.get_doc_chunks()
65
- if len(new_chunks) == 0:
82
+ if not new_chunks:
66
83
  # If the document is empty, try to extract images
67
84
  img_parser = ImagePdfParser(url, self.parser.config)
68
85
  new_chunks = img_parser.get_doc_chunks()
69
- docs.extend(new_chunks)
70
- else:
71
- # Try to detect content type and handle accordingly
86
+ return new_chunks
87
+ except Exception as e:
88
+ logging.error(f"Error parsing {url}: {e}")
89
+ return []
90
+
91
+ else:
92
+ try:
93
+ headers = requests.head(url).headers
94
+ except Exception as e:
95
+ logging.warning(f"Error getting headers for {url}: {e}")
96
+ headers = CaseInsensitiveDict()
97
+
98
+ content_type = headers.get("Content-Type", "").lower()
99
+ temp_file_suffix = None
100
+ if "application/pdf" in content_type:
101
+ temp_file_suffix = ".pdf"
102
+ elif (
103
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
104
+ in content_type
105
+ ):
106
+ temp_file_suffix = ".docx"
107
+ elif "application/msword" in content_type:
108
+ temp_file_suffix = ".doc"
109
+
110
+ if temp_file_suffix:
72
111
  try:
73
- headers = requests.head(url).headers
74
- except Exception as e:
75
- logging.warning(f"Error getting headers for {url}: {e}")
76
- headers = {}
77
- content_type = headers.get("Content-Type", "").lower()
78
- temp_file_suffix = None
79
- if "application/pdf" in content_type:
80
- temp_file_suffix = ".pdf"
81
- elif (
82
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
83
- in content_type
84
- ):
85
- temp_file_suffix = ".docx"
86
- elif "application/msword" in content_type:
87
- temp_file_suffix = ".doc"
88
-
89
- if temp_file_suffix:
90
- # Download the document content
91
112
  response = requests.get(url)
92
113
  with NamedTemporaryFile(
93
114
  delete=False, suffix=temp_file_suffix
94
115
  ) as temp_file:
95
116
  temp_file.write(response.content)
96
117
  temp_file_path = temp_file.name
97
- # Process the downloaded document
98
118
  doc_parser = DocumentParser.create(
99
119
  temp_file_path, self.parser.config
100
120
  )
101
- docs.extend(doc_parser.get_doc_chunks())
102
- # Clean up the temporary file
121
+ docs = doc_parser.get_doc_chunks()
103
122
  os.remove(temp_file_path)
104
- else:
105
- text = trafilatura.extract(
106
- result,
107
- no_fallback=False,
108
- favor_recall=True,
123
+ return docs
124
+ except Exception as e:
125
+ logging.error(f"Error downloading/parsing {url}: {e}")
126
+ return []
127
+ return []
128
+
129
+ def _is_document_url(self, url: str) -> bool:
130
+ return any(url.lower().endswith(ext) for ext in [".pdf", ".docx", ".doc"])
131
+
132
+
133
+ class CrawlerFactory:
134
+ """Factory for creating web crawlers."""
135
+
136
+ @staticmethod
137
+ def create_crawler(config: BaseCrawlerConfig) -> BaseCrawler:
138
+ """Create a crawler instance based on configuration type.
139
+
140
+ Args:
141
+ config: Configuration for the crawler
142
+
143
+ Returns:
144
+ A BaseCrawler instance
145
+
146
+ Raises:
147
+ ValueError: If config type is not supported
148
+ """
149
+ if isinstance(config, TrafilaturaConfig):
150
+ return TrafilaturaCrawler(config)
151
+ elif isinstance(config, FirecrawlConfig):
152
+ return FirecrawlCrawler(config)
153
+ else:
154
+ raise ValueError(f"Unsupported crawler configuration type: {type(config)}")
155
+
156
+
157
+ class TrafilaturaCrawler(BaseCrawler):
158
+ """Crawler implementation using Trafilatura."""
159
+
160
+ def __init__(self, config: TrafilaturaConfig):
161
+ """Initialize the Trafilatura crawler.
162
+
163
+ Args:
164
+ config: Configuration for the crawler
165
+ """
166
+ super().__init__(config)
167
+ self.config: TrafilaturaConfig = config
168
+
169
+ @property
170
+ def needs_parser(self) -> bool:
171
+ return True
172
+
173
+ def crawl(self, urls: List[str]) -> List[Document]:
174
+ import trafilatura
175
+ from trafilatura.downloads import (
176
+ add_to_compressed_dict,
177
+ buffered_downloads,
178
+ load_download_buffer,
179
+ )
180
+
181
+ docs = []
182
+ dl_dict = add_to_compressed_dict(urls)
183
+
184
+ while not dl_dict.done:
185
+ buffer, dl_dict = load_download_buffer(dl_dict, sleep_time=5)
186
+ for url, result in buffered_downloads(buffer, self.config.threads):
187
+ parsed_doc = self._process_document(url)
188
+ if parsed_doc:
189
+ docs.extend(parsed_doc)
190
+ else:
191
+ text = trafilatura.extract(
192
+ result, no_fallback=False, favor_recall=True
193
+ )
194
+ if text is None and result is not None and isinstance(result, str):
195
+ text = result
196
+ if text:
197
+ docs.append(
198
+ Document(content=text, metadata=DocMetaData(source=url))
109
199
  )
110
- if (
111
- text is None
112
- and result is not None
113
- and isinstance(result, str)
114
- ):
115
- text = result
116
- if text is not None and text != "":
117
- docs.append(
118
- Document(content=text, metadata=DocMetaData(source=url))
200
+
201
+ return docs
202
+
203
+
204
+ class FirecrawlCrawler(BaseCrawler):
205
+ """Crawler implementation using Firecrawl."""
206
+
207
+ def __init__(self, config: FirecrawlConfig) -> None:
208
+ """Initialize the Firecrawl crawler.
209
+
210
+ Args:
211
+ config: Configuration for the crawler
212
+ """
213
+ super().__init__(config)
214
+ self.config: FirecrawlConfig = config
215
+
216
+ @property
217
+ def needs_parser(self) -> bool:
218
+ return False
219
+
220
+ def _return_save_incremental_results(
221
+ self, app: "FirecrawlApp", crawl_id: str, output_dir: str = "firecrawl_output"
222
+ ) -> List[Document]:
223
+ # Code used verbatim from firecrawl blog with few modifications
224
+ # https://www.firecrawl.dev/blog/mastering-the-crawl-endpoint-in-firecrawl
225
+ import json
226
+ import time
227
+ from pathlib import Path
228
+
229
+ from tqdm import tqdm
230
+
231
+ pbar = tqdm(desc="Pages saved", unit=" pages", dynamic_ncols=True)
232
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
233
+ processed_urls: set[str] = set()
234
+ docs = []
235
+
236
+ while True:
237
+ # Check current status
238
+ status = app.check_crawl_status(crawl_id)
239
+ new_pages = 0
240
+
241
+ # Save new pages
242
+ for page in status["data"]:
243
+ url = page["metadata"]["url"]
244
+ if url not in processed_urls:
245
+ content = page.get("markdown", "")
246
+ filename = f"{output_dir}/{len(processed_urls)}.md"
247
+ with open(filename, "w") as f:
248
+ f.write(content)
249
+ docs.append(
250
+ Document(content=content, metadata=DocMetaData(source=url))
251
+ )
252
+ processed_urls.add(url)
253
+ new_pages += 1
254
+ pbar.update(new_pages) # Update progress bar with new pages
255
+
256
+ # Break if crawl is complete
257
+ if status["status"] == "completed":
258
+ print(f"Saved {len(processed_urls)} pages.")
259
+ with open(f"{output_dir}/full_results.json", "w") as f:
260
+ json.dump(status, f, indent=2)
261
+ break
262
+
263
+ time.sleep(5) # Wait before checking again
264
+ return docs
265
+
266
+ def crawl(self, urls: List[str]) -> List[Document]:
267
+ try:
268
+ from firecrawl import FirecrawlApp
269
+ except ImportError:
270
+ raise LangroidImportError("firecrawl", "firecrawl")
271
+
272
+ app = FirecrawlApp(api_key=self.config.api_key)
273
+ docs = []
274
+ params = self.config.params.copy() # Create a copy of the existing params
275
+
276
+ if self.config.timeout is not None:
277
+ params["timeout"] = self.config.timeout # Add/override timeout in params
278
+
279
+ if self.config.mode == "scrape":
280
+ for url in urls:
281
+ try:
282
+ result = app.scrape_url(url, params=params)
283
+ metadata = result.get(
284
+ "metadata", {}
285
+ ) # Default to empty dict if missing
286
+ status_code = metadata.get("statusCode")
287
+
288
+ if status_code == 200:
289
+ docs.append(
290
+ Document(
291
+ content=result["markdown"],
292
+ metadata=DocMetaData(source=url),
119
293
  )
294
+ )
295
+ except Exception as e:
296
+ logging.warning(
297
+ f"Firecrawl encountered an error for {url}: {e}. "
298
+ "Skipping but continuing."
299
+ )
300
+ elif self.config.mode == "crawl":
301
+ if not isinstance(urls, list) or len(urls) != 1:
302
+ raise ValueError(
303
+ "Crawl mode expects 'urls' to be a list containing a single URL."
304
+ )
305
+
306
+ # Start the crawl
307
+ crawl_status = app.async_crawl_url(url=urls[0], params=params)
308
+
309
+ # Save results incrementally
310
+ docs = self._return_save_incremental_results(app, crawl_status["id"])
120
311
  return docs
312
+
313
+
314
+ class URLLoader:
315
+ """Loads URLs and extracts text using a specified crawler."""
316
+
317
+ def __init__(
318
+ self,
319
+ urls: List[Any],
320
+ parsing_config: ParsingConfig = ParsingConfig(),
321
+ crawler_config: Optional[BaseCrawlerConfig] = None,
322
+ ):
323
+ """Initialize the URL loader.
324
+
325
+ Args:
326
+ urls: List of URLs to load
327
+ parsing_config: Configuration for parsing
328
+ crawler_config: Configuration for the crawler
329
+ """
330
+ self.urls = urls
331
+ self.parsing_config = parsing_config
332
+
333
+ if crawler_config is None:
334
+ crawler_config = TrafilaturaConfig(parser=Parser(parsing_config))
335
+
336
+ self.crawler = CrawlerFactory.create_crawler(crawler_config)
337
+
338
+ def load(self) -> List[Document]:
339
+ """Load the URLs using the specified crawler."""
340
+ return self.crawler.crawl(self.urls)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langroid
3
- Version: 0.46.0
3
+ Version: 0.47.1
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  Author-email: Prasad Chalasani <pchalasani@gmail.com>
6
6
  License: MIT
@@ -121,6 +121,8 @@ Provides-Extra: exa
121
121
  Requires-Dist: exa-py>=1.8.7; extra == 'exa'
122
122
  Provides-Extra: fastembed
123
123
  Requires-Dist: fastembed<0.4.0,>=0.3.1; extra == 'fastembed'
124
+ Provides-Extra: firecrawl
125
+ Requires-Dist: firecrawl-py>=1.13.5; extra == 'firecrawl'
124
126
  Provides-Extra: google-genai
125
127
  Requires-Dist: google-genai>=1.0.0; extra == 'google-genai'
126
128
  Provides-Extra: google-generativeai
@@ -14,11 +14,11 @@ langroid/agent/xml_tool_message.py,sha256=6SshYZJKIfi4mkE-gIoSwjkEYekQ8GwcSiCv7a
14
14
  langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  langroid/agent/callbacks/chainlit.py,sha256=UHB6P_J40vsVnssosqkpkOVWRf9NK4TOY0_G2g_Arsg,20900
16
16
  langroid/agent/special/__init__.py,sha256=gik_Xtm_zV7U9s30Mn8UX3Gyuy4jTjQe9zjiE3HWmEo,1273
17
- langroid/agent/special/doc_chat_agent.py,sha256=nEiHzU5Ztb0Y7rPMg4kSf2M6bGS5s1Av_y5w0idAGIE,64763
17
+ langroid/agent/special/doc_chat_agent.py,sha256=SrotZ0qw51fKDXlDP2lwTho0PPTuqUogFAT4jjq0ne0,65231
18
18
  langroid/agent/special/lance_doc_chat_agent.py,sha256=s8xoRs0gGaFtDYFUSIRchsgDVbS5Q3C2b2mr3V1Fd-Q,10419
19
19
  langroid/agent/special/lance_tools.py,sha256=qS8x4wi8mrqfbYV2ztFzrcxyhHQ0ZWOc-zkYiH7awj0,2105
20
20
  langroid/agent/special/relevance_extractor_agent.py,sha256=zIx8GUdVo1aGW6ASla0NPQjYYIpmriK_TYMijqAx3F8,4796
21
- langroid/agent/special/retriever_agent.py,sha256=lvMvf-u9rSosg4YASuFdUbGLgkzLPknXAbJZfZ1LZCc,1868
21
+ langroid/agent/special/retriever_agent.py,sha256=i5TvwDRN5XtT8lSPkoq3Sv8jpoBylisEB2ivRbpunyc,1913
22
22
  langroid/agent/special/table_chat_agent.py,sha256=d9v2wsblaRx7oMnKhLV7uO_ujvk9gh59pSGvBXyeyNc,9659
23
23
  langroid/agent/special/arangodb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  langroid/agent/special/arangodb/arangodb_agent.py,sha256=12Y54c84c9qXV-YXRBcI5HaqyiY75JR4TmqlURYKJAM,25851
@@ -91,7 +91,7 @@ langroid/parsing/routing.py,sha256=-FcnlqldzL4ZoxuDwXjQPNHgBe9F9-F4R6q7b_z9CvI,1
91
91
  langroid/parsing/search.py,sha256=0NJ5-Rou_BbrHAD7O9b20bKjZJnbadjObvGm4Zq8Kis,9818
92
92
  langroid/parsing/spider.py,sha256=hAVM6wxh1pQ0EN4tI5wMBtAjIk0T-xnpi-ZUzWybhos,3258
93
93
  langroid/parsing/table_loader.py,sha256=qNM4obT_0Y4tjrxNBCNUYjKQ9oETCZ7FbolKBTcz-GM,3410
94
- langroid/parsing/url_loader.py,sha256=obi_kj6ehBkdh5mXNtYCXpm3KCuExoy2D1ODVlFbXbQ,4895
94
+ langroid/parsing/url_loader.py,sha256=tNLyCo8A08GcB8KFr04YKDO9KFHyqNacKU0-DuWlu4I,11721
95
95
  langroid/parsing/urls.py,sha256=Tjzr64YsCusiYkY0LEGB5-rSuX8T2P_4DVoOFKAeKuI,8081
96
96
  langroid/parsing/utils.py,sha256=WwqzOhbQRlorbVvddDIZKv9b1KqZCBDm955lgIHDXRw,12828
97
97
  langroid/parsing/web_search.py,sha256=sARV1Tku4wiInhuCz0kRaMHcoF6Ok6CLu7vapLS8hjs,8222
@@ -127,7 +127,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
127
127
  langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
128
128
  langroid/vector_store/qdrantdb.py,sha256=O6dSBoDZ0jzfeVBd7LLvsXu083xs2fxXtPa9gGX3JX4,18443
129
129
  langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
130
- langroid-0.46.0.dist-info/METADATA,sha256=S5uBIAjkQEV4KbQSZ2OH-YMoMfGIJtnulBdaYNouSpw,63389
131
- langroid-0.46.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
132
- langroid-0.46.0.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
133
- langroid-0.46.0.dist-info/RECORD,,
130
+ langroid-0.47.1.dist-info/METADATA,sha256=EI6zx7u_-h8K_P_e9BXM1gF8rnEPB1ENObZ-USOXSlU,63473
131
+ langroid-0.47.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
132
+ langroid-0.47.1.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
133
+ langroid-0.47.1.dist-info/RECORD,,