langroid 0.13.0__py3-none-any.whl → 0.15.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.
- langroid/agent/special/doc_chat_agent.py +99 -17
- langroid/language_models/openai_gpt.py +18 -6
- {langroid-0.13.0.dist-info → langroid-0.15.0.dist-info}/METADATA +4 -1
- {langroid-0.13.0.dist-info → langroid-0.15.0.dist-info}/RECORD +7 -7
- pyproject.toml +2 -1
- {langroid-0.13.0.dist-info → langroid-0.15.0.dist-info}/LICENSE +0 -0
- {langroid-0.13.0.dist-info → langroid-0.15.0.dist-info}/WHEEL +0 -0
@@ -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
|
1109
|
-
# during retrieval, and leave it to the cross-encoder
|
1110
|
-
# to whittle down to self.config.parsing.n_similar_docs
|
1111
|
-
retrieval_multiple =
|
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
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1143
|
-
|
1144
|
-
|
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]]:
|
@@ -21,6 +21,7 @@ from typing import (
|
|
21
21
|
)
|
22
22
|
|
23
23
|
import openai
|
24
|
+
from cerebras.cloud.sdk import AsyncCerebras, Cerebras
|
24
25
|
from groq import AsyncGroq, Groq
|
25
26
|
from httpx import Timeout
|
26
27
|
from openai import AsyncOpenAI, OpenAI
|
@@ -371,8 +372,8 @@ class OpenAIGPT(LanguageModel):
|
|
371
372
|
Class for OpenAI LLMs
|
372
373
|
"""
|
373
374
|
|
374
|
-
client: OpenAI | Groq
|
375
|
-
async_client: AsyncOpenAI | AsyncGroq
|
375
|
+
client: OpenAI | Groq | Cerebras
|
376
|
+
async_client: AsyncOpenAI | AsyncGroq | AsyncCerebras
|
376
377
|
|
377
378
|
def __init__(self, config: OpenAIGPTConfig = OpenAIGPTConfig()):
|
378
379
|
"""
|
@@ -479,6 +480,7 @@ class OpenAIGPT(LanguageModel):
|
|
479
480
|
self.api_key = DUMMY_API_KEY
|
480
481
|
|
481
482
|
self.is_groq = self.config.chat_model.startswith("groq/")
|
483
|
+
self.is_cerebras = self.config.chat_model.startswith("cerebras/")
|
482
484
|
|
483
485
|
if self.is_groq:
|
484
486
|
self.config.chat_model = self.config.chat_model.replace("groq/", "")
|
@@ -489,6 +491,16 @@ class OpenAIGPT(LanguageModel):
|
|
489
491
|
self.async_client = AsyncGroq(
|
490
492
|
api_key=self.api_key,
|
491
493
|
)
|
494
|
+
elif self.is_cerebras:
|
495
|
+
self.config.chat_model = self.config.chat_model.replace("cerebras/", "")
|
496
|
+
self.api_key = os.getenv("CEREBRAS_API_KEY", DUMMY_API_KEY)
|
497
|
+
self.client = Cerebras(
|
498
|
+
api_key=self.api_key,
|
499
|
+
)
|
500
|
+
# TODO there is not async client, so should we do anything here?
|
501
|
+
self.async_client = AsyncCerebras(
|
502
|
+
api_key=self.api_key,
|
503
|
+
)
|
492
504
|
else:
|
493
505
|
self.client = OpenAI(
|
494
506
|
api_key=self.api_key,
|
@@ -1096,8 +1108,8 @@ class OpenAIGPT(LanguageModel):
|
|
1096
1108
|
if self.config.use_chat_for_completion:
|
1097
1109
|
return self.chat(messages=prompt, max_tokens=max_tokens)
|
1098
1110
|
|
1099
|
-
if self.is_groq:
|
1100
|
-
raise ValueError("Groq
|
1111
|
+
if self.is_groq or self.is_cerebras:
|
1112
|
+
raise ValueError("Groq, Cerebras do not support pure completions")
|
1101
1113
|
|
1102
1114
|
if settings.debug:
|
1103
1115
|
print(f"[grey37]PROMPT: {escape(prompt)}[/grey37]")
|
@@ -1174,8 +1186,8 @@ class OpenAIGPT(LanguageModel):
|
|
1174
1186
|
if self.config.use_chat_for_completion:
|
1175
1187
|
return await self.achat(messages=prompt, max_tokens=max_tokens)
|
1176
1188
|
|
1177
|
-
if self.is_groq:
|
1178
|
-
raise ValueError("Groq
|
1189
|
+
if self.is_groq or self.is_cerebras:
|
1190
|
+
raise ValueError("Groq, Cerebras do not support pure completions")
|
1179
1191
|
|
1180
1192
|
if settings.debug:
|
1181
1193
|
print(f"[grey37]PROMPT: {escape(prompt)}[/grey37]")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: langroid
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.15.0
|
4
4
|
Summary: Harness LLMs with Multi-Agent Programming
|
5
5
|
License: MIT
|
6
6
|
Author: Prasad Chalasani
|
@@ -38,6 +38,7 @@ Provides-Extra: vecdbs
|
|
38
38
|
Requires-Dist: aiohttp (>=3.9.1,<4.0.0)
|
39
39
|
Requires-Dist: async-generator (>=1.10,<2.0)
|
40
40
|
Requires-Dist: bs4 (>=0.0.1,<0.0.2)
|
41
|
+
Requires-Dist: cerebras-cloud-sdk (>=1.1.0,<2.0.0)
|
41
42
|
Requires-Dist: chainlit (==1.1.202) ; extra == "all" or extra == "chainlit"
|
42
43
|
Requires-Dist: chromadb (>=0.4.21,<=0.4.23) ; extra == "vecdbs" or extra == "all" or extra == "chromadb"
|
43
44
|
Requires-Dist: colorlog (>=6.7.0,<7.0.0)
|
@@ -153,6 +154,8 @@ This Multi-Agent paradigm is inspired by the
|
|
153
154
|
`Langroid` is a fresh take on LLM app-development, where considerable thought has gone
|
154
155
|
into simplifying the developer experience; it does not use `Langchain`.
|
155
156
|
|
157
|
+
: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/)
|
158
|
+
|
156
159
|
📢 Companies are using/adapting Langroid in **production**. Here is a quote:
|
157
160
|
|
158
161
|
>[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=
|
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
|
@@ -72,7 +72,7 @@ langroid/language_models/azure_openai.py,sha256=G4le3j4YLHV7IwgB2C37hO3MKijZ1Kjy
|
|
72
72
|
langroid/language_models/base.py,sha256=ytJ_0Jw5erbqrqLPp4JMCo_nIkwzUvBqoKUr8Sae9Qg,21792
|
73
73
|
langroid/language_models/config.py,sha256=9Q8wk5a7RQr8LGMT_0WkpjY8S4ywK06SalVRjXlfCiI,378
|
74
74
|
langroid/language_models/mock_lm.py,sha256=HuiAvjHiCfffYF5xjFJUq945HVTW0QPbeUUctOnNCzQ,3868
|
75
|
-
langroid/language_models/openai_gpt.py,sha256=
|
75
|
+
langroid/language_models/openai_gpt.py,sha256=1wG1nXho6bLOWyWqlR51uY45ZFkt5NWXx0hbXzKLVoQ,62050
|
76
76
|
langroid/language_models/prompt_formatter/__init__.py,sha256=2-5cdE24XoFDhifOLl8yiscohil1ogbP1ECkYdBlBsk,372
|
77
77
|
langroid/language_models/prompt_formatter/base.py,sha256=eDS1sgRNZVnoajwV_ZIha6cba5Dt8xjgzdRbPITwx3Q,1221
|
78
78
|
langroid/language_models/prompt_formatter/hf_formatter.py,sha256=PVJppmjRvD-2DF-XNC6mE05vTZ9wbu37SmXwZBQhad0,5055
|
@@ -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=
|
141
|
-
langroid-0.
|
142
|
-
langroid-0.
|
143
|
-
langroid-0.
|
144
|
-
langroid-0.
|
140
|
+
pyproject.toml,sha256=lazmZZ-COR5jUFIhJLYTnonrxAPmAstppjFy6GkS5UE,7137
|
141
|
+
langroid-0.15.0.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
|
142
|
+
langroid-0.15.0.dist-info/METADATA,sha256=8Ffr9Et34izfKr-7AJdO7HjRalVA5rzu1ADDss089Dk,55481
|
143
|
+
langroid-0.15.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
144
|
+
langroid-0.15.0.dist-info/RECORD,,
|
pyproject.toml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "langroid"
|
3
|
-
version = "0.
|
3
|
+
version = "0.15.0"
|
4
4
|
description = "Harness LLMs with Multi-Agent Programming"
|
5
5
|
authors = ["Prasad Chalasani <pchalasani@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -89,6 +89,7 @@ async-generator = "^1.10"
|
|
89
89
|
|
90
90
|
python-magic = "^0.4.27"
|
91
91
|
json-repair = "^0.27.0"
|
92
|
+
cerebras-cloud-sdk = "^1.1.0"
|
92
93
|
|
93
94
|
|
94
95
|
[tool.poetry.extras]
|
File without changes
|
File without changes
|