langroid 0.32.1__py3-none-any.whl → 0.32.2__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/language_models/openai_gpt.py +16 -1
- {langroid-0.32.1.dist-info → langroid-0.32.2.dist-info}/METADATA +2 -2
- {langroid-0.32.1.dist-info → langroid-0.32.2.dist-info}/RECORD +6 -40
- pyproject.toml +1 -1
- langroid/agent/.chainlit/config.toml +0 -121
- langroid/agent/.chainlit/translations/bn.json +0 -231
- langroid/agent/.chainlit/translations/en-US.json +0 -229
- langroid/agent/.chainlit/translations/gu.json +0 -231
- langroid/agent/.chainlit/translations/he-IL.json +0 -231
- langroid/agent/.chainlit/translations/hi.json +0 -231
- langroid/agent/.chainlit/translations/kn.json +0 -231
- langroid/agent/.chainlit/translations/ml.json +0 -231
- langroid/agent/.chainlit/translations/mr.json +0 -231
- langroid/agent/.chainlit/translations/ta.json +0 -231
- langroid/agent/.chainlit/translations/te.json +0 -231
- langroid/agent/.chainlit/translations/zh-CN.json +0 -229
- langroid/agent/helpers.py +0 -0
- langroid/agent/junk +0 -13
- langroid/agent/structured_message.py +0 -9
- langroid/agent/typed_task.py +0 -19
- langroid/agent_config.py +0 -0
- langroid/embedding_models/clustering.py +0 -189
- langroid/experimental/team-save.py +0 -391
- langroid/language_models/.chainlit/config.toml +0 -121
- langroid/language_models/.chainlit/translations/en-US.json +0 -231
- langroid/parsing/code-parsing.md +0 -86
- langroid/parsing/config.py +0 -0
- langroid/parsing/image_text.py +0 -32
- langroid/parsing/url_loader_cookies.py +0 -73
- langroid/prompts/chat-gpt4-system-prompt.md +0 -68
- langroid/utils/.chainlit/config.toml +0 -121
- langroid/utils/.chainlit/translations/en-US.json +0 -231
- langroid/utils/docker.py +0 -37
- langroid/utils/llms/__init__.py +0 -0
- langroid/utils/llms/strings.py +0 -8
- langroid/utils/web/__init__.py +0 -0
- langroid/utils/web/login.py +0 -83
- langroid/vector_store/qdrant_cloud.py +0 -6
- {langroid-0.32.1.dist-info → langroid-0.32.2.dist-info}/LICENSE +0 -0
- {langroid-0.32.1.dist-info → langroid-0.32.2.dist-info}/WHEEL +0 -0
@@ -1,189 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
from collections import Counter
|
3
|
-
from typing import Callable, List, Tuple
|
4
|
-
|
5
|
-
import faiss
|
6
|
-
import numpy as np
|
7
|
-
from sklearn.cluster import DBSCAN
|
8
|
-
from sklearn.neighbors import NearestNeighbors
|
9
|
-
from sklearn.preprocessing import StandardScaler
|
10
|
-
|
11
|
-
from langroid.mytypes import Document
|
12
|
-
|
13
|
-
logging.getLogger("faiss").setLevel(logging.ERROR)
|
14
|
-
logging.getLogger("faiss-cpu").setLevel(logging.ERROR)
|
15
|
-
|
16
|
-
|
17
|
-
def find_optimal_clusters(X: np.ndarray, max_clusters: int, threshold=0.1) -> int:
|
18
|
-
"""
|
19
|
-
Find the optimal number of clusters for FAISS K-means using the Elbow Method.
|
20
|
-
|
21
|
-
Args:
|
22
|
-
X (np.ndarray): A 2D NumPy array of data points.
|
23
|
-
max_clusters (int): The maximum number of clusters to try.
|
24
|
-
threshold (float): Threshold for the rate of change in inertia values.
|
25
|
-
Defaults to 0.1.
|
26
|
-
|
27
|
-
Returns:
|
28
|
-
int: The optimal number of clusters.
|
29
|
-
"""
|
30
|
-
inertias = []
|
31
|
-
max_clusters = min(max_clusters, X.shape[0])
|
32
|
-
cluster_range = range(1, max_clusters + 1)
|
33
|
-
|
34
|
-
for nclusters in cluster_range:
|
35
|
-
kmeans = faiss.Kmeans(X.shape[1], nclusters, niter=20, verbose=False)
|
36
|
-
kmeans.train(X)
|
37
|
-
centroids = kmeans.centroids
|
38
|
-
distances = np.sum(np.square(X[:, None] - centroids), axis=-1)
|
39
|
-
inertia = np.sum(np.min(distances, axis=-1))
|
40
|
-
inertias.append(inertia)
|
41
|
-
|
42
|
-
# Calculate the rate of change in inertia values
|
43
|
-
rate_of_change = [
|
44
|
-
abs((inertias[i + 1] - inertias[i]) / inertias[i])
|
45
|
-
for i in range(len(inertias) - 1)
|
46
|
-
]
|
47
|
-
|
48
|
-
# Find the optimal number of clusters based on the rate of change threshold
|
49
|
-
optimal_clusters = 1
|
50
|
-
for i, roc in enumerate(rate_of_change):
|
51
|
-
if roc < threshold:
|
52
|
-
optimal_clusters = i + 1
|
53
|
-
break
|
54
|
-
|
55
|
-
return optimal_clusters
|
56
|
-
|
57
|
-
|
58
|
-
def densest_clusters(
|
59
|
-
embeddings: List[np.ndarray], k: int = 5
|
60
|
-
) -> List[Tuple[np.ndarray, int]]:
|
61
|
-
"""
|
62
|
-
Find the top k densest clusters in the given list of embeddings using FAISS K-means.
|
63
|
-
See here:
|
64
|
-
'https://github.com/facebookresearch/faiss/wiki/Faiss-building-blocks%3A-clustering%
|
65
|
-
2C-PCA%2C-quantization'
|
66
|
-
|
67
|
-
Args:
|
68
|
-
embeddings (List[np.ndarray]): A list of embedding vectors.
|
69
|
-
k (int, optional): The number of densest clusters to find. Defaults to 5.
|
70
|
-
|
71
|
-
Returns:
|
72
|
-
List[Tuple[np.ndarray, int]]: A list of representative vectors and their indices
|
73
|
-
from the k densest clusters.
|
74
|
-
"""
|
75
|
-
# Convert the list of embeddings to a NumPy array
|
76
|
-
X = np.vstack(embeddings)
|
77
|
-
|
78
|
-
# FAISS K-means clustering
|
79
|
-
ncentroids = find_optimal_clusters(X, max_clusters=2 * k, threshold=0.1)
|
80
|
-
k = min(k, ncentroids)
|
81
|
-
niter = 20
|
82
|
-
verbose = True
|
83
|
-
d = X.shape[1]
|
84
|
-
kmeans = faiss.Kmeans(d, k, niter=niter, verbose=verbose)
|
85
|
-
kmeans.train(X)
|
86
|
-
|
87
|
-
# Get the cluster centroids
|
88
|
-
centroids = kmeans.centroids
|
89
|
-
|
90
|
-
# Find the nearest neighbors of the centroids in the original embeddings
|
91
|
-
nbrs = NearestNeighbors(n_neighbors=1, algorithm="auto").fit(X)
|
92
|
-
distances, indices = nbrs.kneighbors(centroids)
|
93
|
-
|
94
|
-
# Sort the centroids by their nearest neighbor distances
|
95
|
-
sorted_centroids_indices = np.argsort(distances, axis=0).flatten()
|
96
|
-
|
97
|
-
# Select the top k densest clusters
|
98
|
-
densest_clusters_indices = sorted_centroids_indices[:k]
|
99
|
-
|
100
|
-
# Get the representative vectors and their indices from the densest clusters
|
101
|
-
representative_vectors = [
|
102
|
-
(idx, embeddings[idx]) for idx in densest_clusters_indices
|
103
|
-
]
|
104
|
-
|
105
|
-
return representative_vectors
|
106
|
-
|
107
|
-
|
108
|
-
def densest_clusters_DBSCAN(
|
109
|
-
embeddings: np.ndarray, k: int = 10
|
110
|
-
) -> List[Tuple[int, np.ndarray]]:
|
111
|
-
"""
|
112
|
-
Find the representative vector and corresponding index from each of the k densest
|
113
|
-
clusters in the given embeddings.
|
114
|
-
|
115
|
-
Args:
|
116
|
-
embeddings (np.ndarray): A NumPy array of shape (n, d), where n is the number
|
117
|
-
of embedding vectors and d is their dimensionality.
|
118
|
-
k (int): Number of densest clusters to find.
|
119
|
-
|
120
|
-
Returns:
|
121
|
-
List[Tuple[int, np.ndarray]]: A list of tuples containing the index and
|
122
|
-
representative vector for each of the k densest
|
123
|
-
clusters.
|
124
|
-
"""
|
125
|
-
|
126
|
-
# Normalize the embeddings if necessary
|
127
|
-
scaler = StandardScaler()
|
128
|
-
embeddings_normalized = scaler.fit_transform(embeddings)
|
129
|
-
|
130
|
-
# Choose a clustering algorithm (DBSCAN in this case)
|
131
|
-
# Tune eps and min_samples for your use case
|
132
|
-
dbscan = DBSCAN(eps=4, min_samples=5)
|
133
|
-
|
134
|
-
# Apply the clustering algorithm
|
135
|
-
cluster_labels = dbscan.fit_predict(embeddings_normalized)
|
136
|
-
|
137
|
-
# Compute the densities of the clusters
|
138
|
-
cluster_density = Counter(cluster_labels)
|
139
|
-
|
140
|
-
# Sort clusters by their density
|
141
|
-
sorted_clusters = sorted(cluster_density.items(), key=lambda x: x[1], reverse=True)
|
142
|
-
|
143
|
-
# Select top-k densest clusters
|
144
|
-
top_k_clusters = sorted_clusters[:k]
|
145
|
-
|
146
|
-
# Find a representative vector for each cluster
|
147
|
-
representatives = []
|
148
|
-
for cluster_id, _ in top_k_clusters:
|
149
|
-
if cluster_id == -1:
|
150
|
-
continue # Skip the noise cluster (label -1)
|
151
|
-
indices = np.where(cluster_labels == cluster_id)[0]
|
152
|
-
centroid = embeddings[indices].mean(axis=0)
|
153
|
-
closest_index = indices[
|
154
|
-
np.argmin(np.linalg.norm(embeddings[indices] - centroid, axis=1))
|
155
|
-
]
|
156
|
-
representatives.append((closest_index, embeddings[closest_index]))
|
157
|
-
|
158
|
-
return representatives
|
159
|
-
|
160
|
-
|
161
|
-
def densest_doc_clusters(
|
162
|
-
docs: List[Document], k: int, embedding_fn: Callable[[str], np.ndarray]
|
163
|
-
) -> List[Document]:
|
164
|
-
"""
|
165
|
-
Find the documents corresponding to the representative vectors of the k densest
|
166
|
-
clusters in the given list of documents.
|
167
|
-
|
168
|
-
Args:
|
169
|
-
docs (List[Document]): A list of Document instances, each containing a "content"
|
170
|
-
field to be embedded and a "metadata" field.
|
171
|
-
k (int): Number of densest clusters to find.
|
172
|
-
embedding_fn (Callable[[str], np.ndarray]): A function that maps a string to an
|
173
|
-
embedding vector.
|
174
|
-
|
175
|
-
Returns:
|
176
|
-
List[Document]: A list of Document instances corresponding to the representative
|
177
|
-
vectors of the k densest clusters.
|
178
|
-
"""
|
179
|
-
|
180
|
-
# Extract embeddings from the documents
|
181
|
-
embeddings = np.array(embedding_fn([doc.content for doc in docs]))
|
182
|
-
|
183
|
-
# Find the densest clusters and their representative indices
|
184
|
-
representative_indices_and_vectors = densest_clusters(embeddings, k)
|
185
|
-
|
186
|
-
# Extract the corresponding documents
|
187
|
-
representative_docs = [docs[i] for i, _ in representative_indices_and_vectors]
|
188
|
-
|
189
|
-
return representative_docs
|
@@ -1,391 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
from abc import ABC, abstractmethod
|
3
|
-
from typing import Callable, Dict, List, Optional, Union
|
4
|
-
|
5
|
-
import langroid as lr
|
6
|
-
from langroid.language_models.mock_lm import MockLMConfig
|
7
|
-
|
8
|
-
# Fix logging level type
|
9
|
-
logging.basicConfig(level=logging.WARNING)
|
10
|
-
logger = logging.getLogger(__name__)
|
11
|
-
|
12
|
-
|
13
|
-
def sum_fn(s: str) -> str:
|
14
|
-
"""Dummy response for MockLM"""
|
15
|
-
nums = [
|
16
|
-
int(subpart)
|
17
|
-
for part in s.split()
|
18
|
-
for subpart in part.split(",")
|
19
|
-
if subpart.isdigit()
|
20
|
-
]
|
21
|
-
return str(sum(nums) + 1)
|
22
|
-
|
23
|
-
|
24
|
-
def user_message(msg: Union[str, lr.ChatDocument]) -> lr.ChatDocument:
|
25
|
-
"""Create a user-role msg from a string or ChatDocument"""
|
26
|
-
if isinstance(msg, lr.ChatDocument):
|
27
|
-
return msg
|
28
|
-
return lr.ChatDocument(
|
29
|
-
content=msg,
|
30
|
-
metadata=lr.ChatDocMetaData(
|
31
|
-
sender=lr.Entity.USER,
|
32
|
-
sender_name="user",
|
33
|
-
),
|
34
|
-
)
|
35
|
-
|
36
|
-
|
37
|
-
class InputContext:
|
38
|
-
"""Context for a Component to respond to"""
|
39
|
-
|
40
|
-
def __init__(self) -> None:
|
41
|
-
self.messages: List[lr.ChatDocument] = []
|
42
|
-
|
43
|
-
def add(
|
44
|
-
self, results: Union[str, List[str], lr.ChatDocument, List[lr.ChatDocument]]
|
45
|
-
) -> None:
|
46
|
-
"""
|
47
|
-
Add messages to the input messages list
|
48
|
-
"""
|
49
|
-
msgs: List[lr.ChatDocument] = []
|
50
|
-
if isinstance(results, str):
|
51
|
-
msgs = [user_message(results)]
|
52
|
-
elif isinstance(results, lr.ChatDocument):
|
53
|
-
msgs = [results]
|
54
|
-
elif isinstance(results, list):
|
55
|
-
if len(results) == 0:
|
56
|
-
return
|
57
|
-
if isinstance(results[0], str):
|
58
|
-
msgs = [user_message(r) for r in results]
|
59
|
-
else:
|
60
|
-
msgs = [r for r in results if isinstance(r, lr.ChatDocument)]
|
61
|
-
self.messages.extend(msgs)
|
62
|
-
|
63
|
-
def clear(self) -> None:
|
64
|
-
self.messages.clear()
|
65
|
-
|
66
|
-
def get_context(self) -> lr.ChatDocument:
|
67
|
-
"""Construct a user-role ChatDocument from the input messages"""
|
68
|
-
if len(self.messages) == 0:
|
69
|
-
return lr.ChatDocument(content="", metadata={"sender": lr.Entity.USER})
|
70
|
-
content = "\n".join(
|
71
|
-
f"{msg.metadata.sender_name}: {msg.content}" for msg in self.messages
|
72
|
-
)
|
73
|
-
return lr.ChatDocument(content=content, metadata={"sender": lr.Entity.USER})
|
74
|
-
|
75
|
-
|
76
|
-
class Scheduler(ABC):
|
77
|
-
"""Schedule the Components of a Team"""
|
78
|
-
|
79
|
-
def __init__(self) -> None:
|
80
|
-
self.init_state()
|
81
|
-
|
82
|
-
def init_state(self) -> None:
|
83
|
-
self.stepped = False
|
84
|
-
self.responders: List[str] = []
|
85
|
-
self.responder_counts: Dict[str, int] = {}
|
86
|
-
self.current_result: List[lr.ChatDocument] = []
|
87
|
-
|
88
|
-
@abstractmethod
|
89
|
-
def step(self) -> None:
|
90
|
-
pass
|
91
|
-
|
92
|
-
@abstractmethod
|
93
|
-
def done(self) -> bool:
|
94
|
-
pass
|
95
|
-
|
96
|
-
@abstractmethod
|
97
|
-
def result(self) -> List[lr.ChatDocument]:
|
98
|
-
pass
|
99
|
-
|
100
|
-
def run(self) -> List[lr.ChatDocument]:
|
101
|
-
self.init_state()
|
102
|
-
while not self.done():
|
103
|
-
self.step()
|
104
|
-
return self.result()
|
105
|
-
|
106
|
-
|
107
|
-
class Component(ABC):
|
108
|
-
"""A component of a Team"""
|
109
|
-
|
110
|
-
def __init__(self) -> None:
|
111
|
-
self.input = InputContext()
|
112
|
-
self._listeners: List["Component"] = []
|
113
|
-
self.name: str = ""
|
114
|
-
|
115
|
-
@abstractmethod
|
116
|
-
def run(self) -> List[lr.ChatDocument]:
|
117
|
-
pass
|
118
|
-
|
119
|
-
def listen(self, component: Union["Component", List["Component"]]) -> None:
|
120
|
-
if isinstance(component, list):
|
121
|
-
for comp in component:
|
122
|
-
comp.listeners.append(self)
|
123
|
-
else:
|
124
|
-
component.listeners.append(self)
|
125
|
-
|
126
|
-
@property
|
127
|
-
def listeners(self) -> List["Component"]:
|
128
|
-
return self._listeners
|
129
|
-
|
130
|
-
def _notify(self, results: List[lr.ChatDocument]) -> None:
|
131
|
-
logger.warning(f"{self.name} Notifying listeners...")
|
132
|
-
for listener in self.listeners:
|
133
|
-
logger.warning(f"--> Listener {listener.name} notified")
|
134
|
-
listener.input.add(results)
|
135
|
-
|
136
|
-
|
137
|
-
class SimpleScheduler(Scheduler):
|
138
|
-
def __init__(
|
139
|
-
self,
|
140
|
-
components: List[Component],
|
141
|
-
) -> None:
|
142
|
-
super().__init__()
|
143
|
-
self.components = components # Get components from team
|
144
|
-
self.stepped: bool = False
|
145
|
-
|
146
|
-
def step(self) -> None:
|
147
|
-
results = []
|
148
|
-
for comp in self.components:
|
149
|
-
result = comp.run()
|
150
|
-
if result:
|
151
|
-
results.extend(result)
|
152
|
-
self.current_result = results
|
153
|
-
self.stepped = True
|
154
|
-
|
155
|
-
def done(self) -> bool:
|
156
|
-
"""done after 1 step, i.e. all components have responded"""
|
157
|
-
return self.stepped
|
158
|
-
|
159
|
-
def result(self) -> List[lr.ChatDocument]:
|
160
|
-
return self.current_result
|
161
|
-
|
162
|
-
|
163
|
-
class OrElseScheduler(Scheduler):
|
164
|
-
"""
|
165
|
-
Implements "OrElse scheduling", i.e. if the components are A, B, C, then
|
166
|
-
in each step, it will try for a valid response from A OrElse B OrElse C,
|
167
|
-
i.e. the first component that gives a valid response is chosen.
|
168
|
-
In the next step, it will start from the next component in the list,
|
169
|
-
cycling back to the first component after the last component.
|
170
|
-
(There may be a better name than OrElseScheduler though.)
|
171
|
-
"""
|
172
|
-
|
173
|
-
def __init__(
|
174
|
-
self,
|
175
|
-
components: List[Component],
|
176
|
-
) -> None:
|
177
|
-
super().__init__()
|
178
|
-
self.components = components
|
179
|
-
self.team: Optional[Team] = None
|
180
|
-
self.current_index: int = 0
|
181
|
-
|
182
|
-
def init_state(self) -> None:
|
183
|
-
super().init_state()
|
184
|
-
self.current_index = 0
|
185
|
-
|
186
|
-
def is_valid(self, result: Optional[List[lr.ChatDocument]]) -> bool:
|
187
|
-
return result is not None and len(result) > 0
|
188
|
-
|
189
|
-
def step(self) -> None:
|
190
|
-
start_index = self.current_index
|
191
|
-
n = len(self.components)
|
192
|
-
|
193
|
-
for i in range(n):
|
194
|
-
idx = (start_index + i) % n
|
195
|
-
comp = self.components[idx]
|
196
|
-
result = comp.run()
|
197
|
-
if self.is_valid(result):
|
198
|
-
self.responders.append(comp.name)
|
199
|
-
self.responder_counts[comp.name] = (
|
200
|
-
self.responder_counts.get(comp.name, 0) + 1
|
201
|
-
)
|
202
|
-
self.current_result = result
|
203
|
-
# cycle to next component
|
204
|
-
self.current_index = (idx + 1) % n
|
205
|
-
return
|
206
|
-
|
207
|
-
def done(self) -> bool:
|
208
|
-
if self.team is None:
|
209
|
-
return False
|
210
|
-
return self.team.done(self)
|
211
|
-
|
212
|
-
def result(self) -> List[lr.ChatDocument]:
|
213
|
-
return self.current_result
|
214
|
-
|
215
|
-
|
216
|
-
class Team(Component):
|
217
|
-
def __init__(
|
218
|
-
self,
|
219
|
-
name: str,
|
220
|
-
done_condition: Optional[Callable[["Team", Scheduler], bool]] = None,
|
221
|
-
) -> None:
|
222
|
-
super().__init__()
|
223
|
-
self.name = name
|
224
|
-
self.components: List[Component] = []
|
225
|
-
self.scheduler: Optional[Scheduler] = None
|
226
|
-
self.done_condition = done_condition or Team.default_done_condition
|
227
|
-
|
228
|
-
def set_done_condition(
|
229
|
-
self, done_condition: Callable[["Team", Scheduler], bool]
|
230
|
-
) -> None:
|
231
|
-
self.done_condition = done_condition
|
232
|
-
|
233
|
-
def done(self, scheduler: Scheduler) -> bool:
|
234
|
-
return self.done_condition(self, scheduler)
|
235
|
-
|
236
|
-
def default_done_condition(self, scheduler: Scheduler) -> bool:
|
237
|
-
# Default condition, can be overridden
|
238
|
-
return False
|
239
|
-
|
240
|
-
def add_scheduler(self, scheduler_class: type) -> None:
|
241
|
-
self.scheduler = scheduler_class(self.components)
|
242
|
-
if hasattr(self.scheduler, "team"):
|
243
|
-
setattr(self.scheduler, "team", self)
|
244
|
-
|
245
|
-
def add(self, component: Union[Component, List[Component]]) -> None:
|
246
|
-
if isinstance(component, list):
|
247
|
-
self.components.extend(component)
|
248
|
-
else:
|
249
|
-
self.components.append(component)
|
250
|
-
|
251
|
-
def reset(self) -> None:
|
252
|
-
self.input.clear()
|
253
|
-
if self.scheduler is not None:
|
254
|
-
self.scheduler.init_state()
|
255
|
-
|
256
|
-
def run(self, input: str | lr.ChatDocument | None = None) -> List[lr.ChatDocument]:
|
257
|
-
if input is not None:
|
258
|
-
self.input.add(input)
|
259
|
-
if self.scheduler is None:
|
260
|
-
raise ValueError(
|
261
|
-
f"Team '{self.name}' has no scheduler. Call add_scheduler() first."
|
262
|
-
)
|
263
|
-
input_str = self.input.get_context().content
|
264
|
-
logger.warning(f"Running team {self.name}... on input = {input_str}")
|
265
|
-
# push the input of self to each component that's a listener of self.
|
266
|
-
n_pushed = 0
|
267
|
-
for comp in self.components:
|
268
|
-
|
269
|
-
if comp in self.listeners:
|
270
|
-
comp.input.add(self.input.messages)
|
271
|
-
n_pushed += 1
|
272
|
-
if len(self.input.messages) > 0 and n_pushed == 0:
|
273
|
-
logger.warning(
|
274
|
-
"""
|
275
|
-
Warning: Team inputs not pushed to any components!
|
276
|
-
You may not be able to run any components unless they have their
|
277
|
-
own inputs. Make sure to set up component to listen to parent team
|
278
|
-
if needed.
|
279
|
-
"""
|
280
|
-
)
|
281
|
-
# clear own input since we've pushed it to internal components
|
282
|
-
self.input.clear()
|
283
|
-
|
284
|
-
result = self.scheduler.run()
|
285
|
-
if len(result) > 0:
|
286
|
-
self._notify(result)
|
287
|
-
result_value = result[0].content if len(result) > 0 else "null"
|
288
|
-
logger.warning(f"Team {self.name} done: {result_value}")
|
289
|
-
return result
|
290
|
-
|
291
|
-
|
292
|
-
class DummyAgent:
|
293
|
-
def __init__(self, name: str) -> None:
|
294
|
-
self.name = name
|
295
|
-
|
296
|
-
def process(self, data: str) -> str:
|
297
|
-
return f"{self.name} processed: {data}"
|
298
|
-
|
299
|
-
|
300
|
-
class TaskComponent(Component):
|
301
|
-
def __init__(self, task: lr.Task) -> None:
|
302
|
-
super().__init__()
|
303
|
-
self.task = task
|
304
|
-
self.name = task.agent.config.name
|
305
|
-
|
306
|
-
def run(self, input: str | lr.ChatDocument | None = None) -> List[lr.ChatDocument]:
|
307
|
-
if input is not None:
|
308
|
-
self.input.add(input)
|
309
|
-
input_msg = self.input.get_context()
|
310
|
-
if input_msg.content == "":
|
311
|
-
return []
|
312
|
-
logger.warning(f"Running task {self.name} on input = {input_msg.content}")
|
313
|
-
result = self.task.run(input_msg)
|
314
|
-
result_value = result.content if result else "null"
|
315
|
-
logger.warning(f"Task {self.name} done: {result_value}")
|
316
|
-
result_list = [result] if result else []
|
317
|
-
if len(result_list) > 0:
|
318
|
-
self._notify(result_list)
|
319
|
-
self.input.clear() # clear own input since we just consumed it!
|
320
|
-
return result_list
|
321
|
-
|
322
|
-
|
323
|
-
def make_task(name: str, sys: str = "") -> TaskComponent:
|
324
|
-
llm_config = MockLMConfig(response_fn=sum_fn)
|
325
|
-
agent = lr.ChatAgent(
|
326
|
-
lr.ChatAgentConfig(
|
327
|
-
llm=llm_config,
|
328
|
-
name=name,
|
329
|
-
)
|
330
|
-
)
|
331
|
-
# set as single_round since there are no Tools
|
332
|
-
task = lr.Task(agent, interactive=False, single_round=True)
|
333
|
-
return TaskComponent(task)
|
334
|
-
|
335
|
-
|
336
|
-
if __name__ == "__main__":
|
337
|
-
# Create agents, tasks
|
338
|
-
t1 = make_task("a1")
|
339
|
-
t2 = make_task("a2")
|
340
|
-
t3 = make_task("a3")
|
341
|
-
|
342
|
-
# done conditions for each time
|
343
|
-
def team1_done_condition(team: Team, scheduler: Scheduler) -> bool:
|
344
|
-
return (
|
345
|
-
scheduler.responder_counts.get("a1", 0) >= 2
|
346
|
-
and scheduler.responder_counts.get("a2", 0) >= 2
|
347
|
-
)
|
348
|
-
|
349
|
-
def team2_done_condition(team: Team, scheduler: Scheduler) -> bool:
|
350
|
-
return "a3" in scheduler.responders
|
351
|
-
|
352
|
-
def general_team_done_condition(team: Team, scheduler: Scheduler) -> bool:
|
353
|
-
# Example: all components have responded at least once
|
354
|
-
return len(set(scheduler.responders)) == len(team.components)
|
355
|
-
|
356
|
-
# Create teams
|
357
|
-
team1 = Team("T1", done_condition=team1_done_condition)
|
358
|
-
team2 = Team("T2", done_condition=team2_done_condition)
|
359
|
-
|
360
|
-
team = Team("Team", done_condition=general_team_done_condition)
|
361
|
-
|
362
|
-
team1.add_scheduler(OrElseScheduler)
|
363
|
-
team2.add_scheduler(OrElseScheduler)
|
364
|
-
team.add_scheduler(OrElseScheduler)
|
365
|
-
|
366
|
-
team.add([team1, team2])
|
367
|
-
|
368
|
-
# Build hierarchy
|
369
|
-
team1.add([t1, t2])
|
370
|
-
team2.add(t3)
|
371
|
-
|
372
|
-
# Set up listening
|
373
|
-
# team2.listen(team1) # listens to team1 final result
|
374
|
-
team1.listen(team)
|
375
|
-
t1.listen(team1)
|
376
|
-
t2.listen(t1)
|
377
|
-
t1.listen(t2)
|
378
|
-
# TODO should we forbid listening to a component OUTSIDE the team?
|
379
|
-
|
380
|
-
# t3 listens to its parent team2 =>
|
381
|
-
# any input to team2 gets pushed to t3 when t3 runs
|
382
|
-
team2.listen([t1, t2])
|
383
|
-
t3.listen(team2)
|
384
|
-
|
385
|
-
# TODO - we should either define which component of a team gets the teams inputs,
|
386
|
-
# or explicitly add messages to a specific component of the team
|
387
|
-
|
388
|
-
print("Running top-level team...")
|
389
|
-
result = team.run("1")
|
390
|
-
|
391
|
-
##########
|
@@ -1,121 +0,0 @@
|
|
1
|
-
[project]
|
2
|
-
# Whether to enable telemetry (default: true). No personal data is collected.
|
3
|
-
enable_telemetry = true
|
4
|
-
|
5
|
-
|
6
|
-
# List of environment variables to be provided by each user to use the app.
|
7
|
-
user_env = []
|
8
|
-
|
9
|
-
# Duration (in seconds) during which the session is saved when the connection is lost
|
10
|
-
session_timeout = 3600
|
11
|
-
|
12
|
-
# Enable third parties caching (e.g LangChain cache)
|
13
|
-
cache = false
|
14
|
-
|
15
|
-
# Authorized origins
|
16
|
-
allow_origins = ["*"]
|
17
|
-
|
18
|
-
# Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317)
|
19
|
-
# follow_symlink = false
|
20
|
-
|
21
|
-
[features]
|
22
|
-
# Show the prompt playground
|
23
|
-
prompt_playground = true
|
24
|
-
|
25
|
-
# Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
|
26
|
-
unsafe_allow_html = false
|
27
|
-
|
28
|
-
# Process and display mathematical expressions. This can clash with "$" characters in messages.
|
29
|
-
latex = false
|
30
|
-
|
31
|
-
# Automatically tag threads with the current chat profile (if a chat profile is used)
|
32
|
-
auto_tag_thread = true
|
33
|
-
|
34
|
-
# Authorize users to spontaneously upload files with messages
|
35
|
-
[features.spontaneous_file_upload]
|
36
|
-
enabled = true
|
37
|
-
accept = ["*/*"]
|
38
|
-
max_files = 20
|
39
|
-
max_size_mb = 500
|
40
|
-
|
41
|
-
[features.audio]
|
42
|
-
# Threshold for audio recording
|
43
|
-
min_decibels = -45
|
44
|
-
# Delay for the user to start speaking in MS
|
45
|
-
initial_silence_timeout = 3000
|
46
|
-
# Delay for the user to continue speaking in MS. If the user stops speaking for this duration, the recording will stop.
|
47
|
-
silence_timeout = 1500
|
48
|
-
# Above this duration (MS), the recording will forcefully stop.
|
49
|
-
max_duration = 15000
|
50
|
-
# Duration of the audio chunks in MS
|
51
|
-
chunk_duration = 1000
|
52
|
-
# Sample rate of the audio
|
53
|
-
sample_rate = 44100
|
54
|
-
|
55
|
-
[UI]
|
56
|
-
# Name of the app and chatbot.
|
57
|
-
name = "Chatbot"
|
58
|
-
|
59
|
-
# Show the readme while the thread is empty.
|
60
|
-
show_readme_as_default = true
|
61
|
-
|
62
|
-
# Description of the app and chatbot. This is used for HTML tags.
|
63
|
-
# description = ""
|
64
|
-
|
65
|
-
# Large size content are by default collapsed for a cleaner ui
|
66
|
-
default_collapse_content = true
|
67
|
-
|
68
|
-
# The default value for the expand messages settings.
|
69
|
-
default_expand_messages = false
|
70
|
-
|
71
|
-
# Hide the chain of thought details from the user in the UI.
|
72
|
-
hide_cot = false
|
73
|
-
|
74
|
-
# Link to your github repo. This will add a github button in the UI's header.
|
75
|
-
# github = ""
|
76
|
-
|
77
|
-
# Specify a CSS file that can be used to customize the user interface.
|
78
|
-
# The CSS file can be served from the public directory or via an external link.
|
79
|
-
# custom_css = "/public/test.css"
|
80
|
-
|
81
|
-
# Specify a Javascript file that can be used to customize the user interface.
|
82
|
-
# The Javascript file can be served from the public directory.
|
83
|
-
# custom_js = "/public/test.js"
|
84
|
-
|
85
|
-
# Specify a custom font url.
|
86
|
-
# custom_font = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
|
87
|
-
|
88
|
-
# Specify a custom meta image url.
|
89
|
-
# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"
|
90
|
-
|
91
|
-
# Specify a custom build directory for the frontend.
|
92
|
-
# This can be used to customize the frontend code.
|
93
|
-
# Be careful: If this is a relative path, it should not start with a slash.
|
94
|
-
# custom_build = "./public/build"
|
95
|
-
|
96
|
-
[UI.theme]
|
97
|
-
#layout = "wide"
|
98
|
-
#font_family = "Inter, sans-serif"
|
99
|
-
# Override default MUI light theme. (Check theme.ts)
|
100
|
-
[UI.theme.light]
|
101
|
-
#background = "#FAFAFA"
|
102
|
-
#paper = "#FFFFFF"
|
103
|
-
|
104
|
-
[UI.theme.light.primary]
|
105
|
-
#main = "#F80061"
|
106
|
-
#dark = "#980039"
|
107
|
-
#light = "#FFE7EB"
|
108
|
-
|
109
|
-
# Override default MUI dark theme. (Check theme.ts)
|
110
|
-
[UI.theme.dark]
|
111
|
-
#background = "#FAFAFA"
|
112
|
-
#paper = "#FFFFFF"
|
113
|
-
|
114
|
-
[UI.theme.dark.primary]
|
115
|
-
#main = "#F80061"
|
116
|
-
#dark = "#980039"
|
117
|
-
#light = "#FFE7EB"
|
118
|
-
|
119
|
-
|
120
|
-
[meta]
|
121
|
-
generated_by = "1.1.202"
|