langroid 0.13.0__py3-none-any.whl → 0.14.0__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.
@@ -14,6 +14,7 @@ pip install "langroid[hf-embeddings]"
14
14
  """
15
15
 
16
16
  import logging
17
+ from collections import OrderedDict
17
18
  from functools import cache
18
19
  from typing import Any, Dict, List, Optional, Set, Tuple, no_type_check
19
20
 
@@ -130,12 +131,16 @@ class DocChatAgentConfig(ChatAgentConfig):
130
131
  n_fuzzy_neighbor_words: int = 100 # num neighbor words to retrieve for fuzzy match
131
132
  use_fuzzy_match: bool = True
132
133
  use_bm25_search: bool = True
134
+ use_reciprocal_rank_fusion: bool = True # ignored if using cross-encoder reranking
133
135
  cross_encoder_reranking_model: str = (
134
136
  "cross-encoder/ms-marco-MiniLM-L-6-v2" if has_sentence_transformers else ""
135
137
  )
136
138
  rerank_diversity: bool = True # rerank to maximize diversity?
137
139
  rerank_periphery: bool = True # rerank to avoid Lost In the Middle effect?
138
140
  rerank_after_adding_context: bool = True # rerank after adding context window?
141
+ # RRF (Reciprocal Rank Fusion) score = 1/(rank + reciprocal_rank_fusion_constant)
142
+ # see https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking#how-rrf-ranking-works
143
+ reciprocal_rank_fusion_constant: float = 60.0
139
144
  cache: bool = True # cache results
140
145
  debug: bool = False
141
146
  stream: bool = True # allow streaming where needed
@@ -1105,10 +1110,17 @@ class DocChatAgent(ChatAgent):
1105
1110
  Returns:
1106
1111
 
1107
1112
  """
1108
- # if we are using cross-encoder reranking, we can retrieve more docs
1109
- # during retrieval, and leave it to the cross-encoder re-ranking
1110
- # to whittle down to self.config.parsing.n_similar_docs
1111
- retrieval_multiple = 1 if self.config.cross_encoder_reranking_model == "" else 3
1113
+ # if we are using cross-encoder reranking or reciprocal rank fusion (RRF),
1114
+ # we can retrieve more docs during retrieval, and leave it to the cross-encoder
1115
+ # or RRF reranking to whittle down to self.config.parsing.n_similar_docs
1116
+ retrieval_multiple = (
1117
+ 1
1118
+ if (
1119
+ self.config.cross_encoder_reranking_model == ""
1120
+ and not self.config.use_reciprocal_rank_fusion
1121
+ )
1122
+ else 3
1123
+ )
1112
1124
 
1113
1125
  if self.vecdb is None:
1114
1126
  raise ValueError("VecDB not set")
@@ -1120,28 +1132,98 @@ class DocChatAgent(ChatAgent):
1120
1132
  q,
1121
1133
  k=self.config.parsing.n_similar_docs * retrieval_multiple,
1122
1134
  )
1135
+ # sort by score descending
1136
+ docs_and_scores = sorted(
1137
+ docs_and_scores, key=lambda x: x[1], reverse=True
1138
+ )
1139
+
1123
1140
  # keep only docs with unique d.id()
1124
- id2doc_score = {d.id(): (d, s) for d, s in docs_and_scores}
1125
- docs_and_scores = list(id2doc_score.values())
1126
- passages = [d for (d, _) in docs_and_scores]
1127
- # passages = [
1128
- # Document(content=d.content, metadata=d.metadata)
1129
- # for (d, _) in docs_and_scores
1130
- # ]
1141
+ id2_rank_semantic = {d.id(): i for i, (d, _) in enumerate(docs_and_scores)}
1142
+ id2doc = {d.id(): d for d, _ in docs_and_scores}
1143
+ # make sure we get unique docs
1144
+ passages = [id2doc[id] for id, _ in id2_rank_semantic.items()]
1131
1145
 
1146
+ id2_rank_bm25 = {}
1132
1147
  if self.config.use_bm25_search:
1133
1148
  # TODO: Add score threshold in config
1134
1149
  docs_scores = self.get_similar_chunks_bm25(query, retrieval_multiple)
1135
- passages += [d for (d, _) in docs_scores]
1150
+ if self.config.cross_encoder_reranking_model == "":
1151
+ # only if we're not re-ranking with a cross-encoder,
1152
+ # we collect these ranks for Reciprocal Rank Fusion down below.
1153
+ docs_scores = sorted(docs_scores, key=lambda x: x[1], reverse=True)
1154
+ id2_rank_bm25 = {d.id(): i for i, (d, _) in enumerate(docs_scores)}
1155
+ id2doc.update({d.id(): d for d, _ in docs_scores})
1156
+ else:
1157
+ passages += [d for (d, _) in docs_scores]
1136
1158
 
1159
+ id2_rank_fuzzy = {}
1137
1160
  if self.config.use_fuzzy_match:
1138
1161
  # TODO: Add score threshold in config
1139
1162
  fuzzy_match_doc_scores = self.get_fuzzy_matches(query, retrieval_multiple)
1140
- passages += [d for (d, _) in fuzzy_match_doc_scores]
1163
+ if self.config.cross_encoder_reranking_model == "":
1164
+ # only if we're not re-ranking with a cross-encoder,
1165
+ # we collect these ranks for Reciprocal Rank Fusion down below.
1166
+ fuzzy_match_doc_scores = sorted(
1167
+ fuzzy_match_doc_scores, key=lambda x: x[1], reverse=True
1168
+ )
1169
+ id2_rank_fuzzy = {
1170
+ d.id(): i for i, (d, _) in enumerate(fuzzy_match_doc_scores)
1171
+ }
1172
+ id2doc.update({d.id(): d for d, _ in fuzzy_match_doc_scores})
1173
+ else:
1174
+ passages += [d for (d, _) in fuzzy_match_doc_scores]
1141
1175
 
1142
- # keep unique passages
1143
- id2passage = {p.id(): p for p in passages}
1144
- passages = list(id2passage.values())
1176
+ if (
1177
+ self.config.cross_encoder_reranking_model == ""
1178
+ and self.config.use_reciprocal_rank_fusion
1179
+ and (self.config.use_bm25_search or self.config.use_fuzzy_match)
1180
+ ):
1181
+ # Since we're not using cross-enocder re-ranking,
1182
+ # we need to re-order the retrieved chunks from potentially three
1183
+ # different retrieval methods (semantic, bm25, fuzzy), where the
1184
+ # similarity scores are on different scales.
1185
+ # We order the retrieved chunks using Reciprocal Rank Fusion (RRF) score.
1186
+ # Combine the ranks from each id2doc_rank_* dict into a single dict,
1187
+ # where the reciprocal rank score is the sum of
1188
+ # 1/(rank + self.config.reciprocal_rank_fusion_constant).
1189
+ # See https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking
1190
+ #
1191
+ # Note: diversity/periphery-reranking below may modify the final ranking.
1192
+ id2_reciprocal_score = {}
1193
+ for id_ in (
1194
+ set(id2_rank_semantic.keys())
1195
+ | set(id2_rank_bm25.keys())
1196
+ | set(id2_rank_fuzzy.keys())
1197
+ ):
1198
+ rank_semantic = id2_rank_semantic.get(id_, float("inf"))
1199
+ rank_bm25 = id2_rank_bm25.get(id_, float("inf"))
1200
+ rank_fuzzy = id2_rank_fuzzy.get(id_, float("inf"))
1201
+ c = self.config.reciprocal_rank_fusion_constant
1202
+ reciprocal_fusion_score = (
1203
+ 1 / (rank_semantic + c) + 1 / (rank_bm25 + c) + 1 / (rank_fuzzy + c)
1204
+ )
1205
+ id2_reciprocal_score[id_] = reciprocal_fusion_score
1206
+
1207
+ # sort the docs by the reciprocal score, in descending order
1208
+ id2_reciprocal_score = OrderedDict(
1209
+ sorted(
1210
+ id2_reciprocal_score.items(),
1211
+ key=lambda x: x[1],
1212
+ reverse=True,
1213
+ )
1214
+ )
1215
+ # each method retrieved up to retrieval_multiple * n_similar_docs,
1216
+ # so we need to take the top n_similar_docs from the combined list
1217
+ passages = [
1218
+ id2doc[id]
1219
+ for i, (id, _) in enumerate(id2_reciprocal_score.items())
1220
+ if i < self.config.parsing.n_similar_docs
1221
+ ]
1222
+ # passages must have distinct ids
1223
+ assert len(passages) == len(set([d.id() for d in passages])), (
1224
+ f"Duplicate passages in retrieved docs: {len(passages)} != "
1225
+ f"{len(set([d.id() for d in passages]))}"
1226
+ )
1145
1227
 
1146
1228
  if len(passages) == 0:
1147
1229
  return []
@@ -1171,7 +1253,7 @@ class DocChatAgent(ChatAgent):
1171
1253
  passages_scores = self.add_context_window(passages_scores)
1172
1254
  passages = [p for p, _ in passages_scores]
1173
1255
 
1174
- return passages
1256
+ return passages[: self.config.parsing.n_similar_docs]
1175
1257
 
1176
1258
  @no_type_check
1177
1259
  def get_relevant_extracts(self, query: str) -> Tuple[str, List[Document]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langroid
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  License: MIT
6
6
  Author: Prasad Chalasani
@@ -153,6 +153,8 @@ This Multi-Agent paradigm is inspired by the
153
153
  `Langroid` is a fresh take on LLM app-development, where considerable thought has gone
154
154
  into simplifying the developer experience; it does not use `Langchain`.
155
155
 
156
+ :fire: Read the (WIP) [overview of the langroid architecture](https://langroid.github.io/langroid/blog/2024/08/15/overview-of-langroids-multi-agent-architecture-prelim/)
157
+
156
158
  📢 Companies are using/adapting Langroid in **production**. Here is a quote:
157
159
 
158
160
  >[Nullify](https://www.nullify.ai) uses AI Agents for secure software development.
@@ -10,7 +10,7 @@ langroid/agent/helpers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  langroid/agent/junk,sha256=LxfuuW7Cijsg0szAzT81OjWWv1PMNI-6w_-DspVIO2s,339
11
11
  langroid/agent/openai_assistant.py,sha256=2rjCZw45ysNBEGNzQM4uf0bTC4KkatGYAWcVcW4xcek,34337
12
12
  langroid/agent/special/__init__.py,sha256=gik_Xtm_zV7U9s30Mn8UX3Gyuy4jTjQe9zjiE3HWmEo,1273
13
- langroid/agent/special/doc_chat_agent.py,sha256=dqm0Gp11Mfl4hOWN4sUR1uZL-oHEmHzcB6bNN6WFgqw,54784
13
+ langroid/agent/special/doc_chat_agent.py,sha256=r1uPunYf2lQcqYQ4fsD8Q5gB9cZyf7cn0KPcR_CLtrU,59065
14
14
  langroid/agent/special/lance_doc_chat_agent.py,sha256=s8xoRs0gGaFtDYFUSIRchsgDVbS5Q3C2b2mr3V1Fd-Q,10419
15
15
  langroid/agent/special/lance_rag/__init__.py,sha256=QTbs0IVE2ZgDg8JJy1zN97rUUg4uEPH7SLGctFNumk4,174
16
16
  langroid/agent/special/lance_rag/critic_agent.py,sha256=OtFuHthKQLkdVkvuZ2m0GNq1qOYLqHkm1pfLRFnSg5c,9548
@@ -137,8 +137,8 @@ langroid/vector_store/meilisearch.py,sha256=6frB7GFWeWmeKzRfLZIvzRjllniZ1cYj3Hmh
137
137
  langroid/vector_store/momento.py,sha256=qR-zBF1RKVHQZPZQYW_7g-XpTwr46p8HJuYPCkfJbM4,10534
138
138
  langroid/vector_store/qdrant_cloud.py,sha256=3im4Mip0QXLkR6wiqVsjV1QvhSElfxdFSuDKddBDQ-4,188
139
139
  langroid/vector_store/qdrantdb.py,sha256=v88lqFkepADvlN6lByUj9I4NEKa9X9lWH16uTPPbYrE,17457
140
- pyproject.toml,sha256=g99bgxP-XUiTx-KsdFICVJuV2bB89areQkDRU5sIgmk,7107
141
- langroid-0.13.0.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
142
- langroid-0.13.0.dist-info/METADATA,sha256=Znhge-Z8nn_L7Lxeh8dWs04d4ejZfj0NCCRutJJSkdg,55259
143
- langroid-0.13.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
144
- langroid-0.13.0.dist-info/RECORD,,
140
+ pyproject.toml,sha256=W5AMGnCoX4SvE5HYNJlJcernYJ-sbIVoVmfpVifMMm8,7107
141
+ langroid-0.14.0.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
142
+ langroid-0.14.0.dist-info/METADATA,sha256=hEJyAJh8I1K9102zVxSya1pVgXxTUNkPXKo__JUtf54,55430
143
+ langroid-0.14.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
144
+ langroid-0.14.0.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langroid"
3
- version = "0.13.0"
3
+ version = "0.14.0"
4
4
  description = "Harness LLMs with Multi-Agent Programming"
5
5
  authors = ["Prasad Chalasani <pchalasani@gmail.com>"]
6
6
  readme = "README.md"