simonbb 0.0.1__tar.gz → 0.0.3__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.
- simonbb-0.0.3/PKG-INFO +38 -0
- simonbb-0.0.3/README.md +13 -0
- simonbb-0.0.3/pyproject.toml +35 -0
- simonbb-0.0.3/src/simonbb/cis433/__init__.py +0 -0
- {simonbb-0.0.1/src/simonbb → simonbb-0.0.3/src/simonbb/cis433}/gpt.py +1 -4
- {simonbb-0.0.1/src/simonbb → simonbb-0.0.3/src/simonbb/cis433}/transformer.py +2 -4
- {simonbb-0.0.1/src/simonbb → simonbb-0.0.3/src/simonbb/cis433}/utils.py +5 -164
- simonbb-0.0.3/src/simonbb/cis438/__init__.py +0 -0
- simonbb-0.0.3/src/simonbb/cis438/agentic.py +260 -0
- simonbb-0.0.3/src/simonbb/utils.py +158 -0
- simonbb-0.0.1/PKG-INFO +0 -16
- simonbb-0.0.1/README.md +0 -2
- simonbb-0.0.1/pyproject.toml +0 -23
- {simonbb-0.0.1 → simonbb-0.0.3}/LICENSE +0 -0
- {simonbb-0.0.1 → simonbb-0.0.3}/src/simonbb/__init__.py +0 -0
- {simonbb-0.0.1/src/simonbb → simonbb-0.0.3/src/simonbb/cis433}/bert.py +0 -0
simonbb-0.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simonbb
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: A small package to facilitate teaching AI/ML courses
|
|
5
|
+
Project-URL: Homepage, https://simonbb.com
|
|
6
|
+
Author-email: Huaxia Rui <huaxia.rui@simon.rochester.edu>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: langchain-chroma~=1.1.0
|
|
13
|
+
Requires-Dist: langchain-community~=0.4.1
|
|
14
|
+
Requires-Dist: langchain-core~=1.2.23
|
|
15
|
+
Requires-Dist: langchain-huggingface~=1.2.1
|
|
16
|
+
Requires-Dist: langchain~=1.2.13
|
|
17
|
+
Requires-Dist: langgraph~=1.1.3
|
|
18
|
+
Requires-Dist: matplotlib~=3.10.8
|
|
19
|
+
Requires-Dist: numpy~=2.4.0
|
|
20
|
+
Requires-Dist: sentence-transformers~=5.3.0
|
|
21
|
+
Requires-Dist: torchvision~=0.24.1
|
|
22
|
+
Requires-Dist: torch~=2.9.1
|
|
23
|
+
Requires-Dist: transformers~=4.57.3
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Background
|
|
27
|
+
|
|
28
|
+
This is a pet project out of my own teaching needs at University of Rochester.
|
|
29
|
+
I mainly teach these 2 courses to MBA/MS students who lack technical background.
|
|
30
|
+
|
|
31
|
+
- CIS 433: AI and Deep Learning
|
|
32
|
+
- CIS 438: Agentic AI Applications
|
|
33
|
+
|
|
34
|
+
Accordingly, this package contains 2 modules.
|
|
35
|
+
|
|
36
|
+
- `simonbb.cis433`: this module is useful for anyone teacheing deep learning using PyTorch but doesn't want to bother students with too much nitty-gritty details.
|
|
37
|
+
- `simonbb.cis438`: this module is useful for anyone who teaches agentic AI using langchain.
|
|
38
|
+
|
simonbb-0.0.3/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Background
|
|
2
|
+
|
|
3
|
+
This is a pet project out of my own teaching needs at University of Rochester.
|
|
4
|
+
I mainly teach these 2 courses to MBA/MS students who lack technical background.
|
|
5
|
+
|
|
6
|
+
- CIS 433: AI and Deep Learning
|
|
7
|
+
- CIS 438: Agentic AI Applications
|
|
8
|
+
|
|
9
|
+
Accordingly, this package contains 2 modules.
|
|
10
|
+
|
|
11
|
+
- `simonbb.cis433`: this module is useful for anyone teacheing deep learning using PyTorch but doesn't want to bother students with too much nitty-gritty details.
|
|
12
|
+
- `simonbb.cis438`: this module is useful for anyone who teaches agentic AI using langchain.
|
|
13
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simonbb"
|
|
7
|
+
version = "0.0.3"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
|
|
10
|
+
]
|
|
11
|
+
description = "A small package to facilitate teaching AI/ML courses"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"matplotlib~=3.10.8",
|
|
22
|
+
"torch~=2.9.1",
|
|
23
|
+
"torchvision~=0.24.1",
|
|
24
|
+
"numpy~=2.4.0",
|
|
25
|
+
"transformers~=4.57.3",
|
|
26
|
+
"langchain-core~=1.2.23",
|
|
27
|
+
"langchain~=1.2.13",
|
|
28
|
+
"langgraph~=1.1.3",
|
|
29
|
+
"langchain-community~=0.4.1",
|
|
30
|
+
"langchain-chroma~=1.1.0",
|
|
31
|
+
"langchain-huggingface~=1.2.1",
|
|
32
|
+
"sentence-transformers~=5.3.0",
|
|
33
|
+
]
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://simonbb.com"
|
|
File without changes
|
|
@@ -198,7 +198,4 @@ if __name__ == "__main__":
|
|
|
198
198
|
out = generate( model=model, idx=encoded_tensor, max_new_tokens=6, context_size=cfg["context_length"] )
|
|
199
199
|
|
|
200
200
|
print("Output:", out)
|
|
201
|
-
print( tokenizer.decode(out.squeeze(0).tolist()) )
|
|
202
|
-
|
|
203
|
-
else:
|
|
204
|
-
print(f'GPT imported from local file "{__name__}.py"')
|
|
201
|
+
print( tokenizer.decode(out.squeeze(0).tolist()) )
|
|
@@ -209,7 +209,7 @@ class Transformer(nn.Module):
|
|
|
209
209
|
return self.final_layer(x) # (batch_size, target_len, tgt_vocab_size)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
-
#%% Sanity Check: run from the parent folder of
|
|
212
|
+
#%% Sanity Check: run from the parent folder of simonbb as "python -m simonbb.cis433.transformer" since we used relative import
|
|
213
213
|
if __name__ == "__main__":
|
|
214
214
|
|
|
215
215
|
from ..utils import TextVectorizer
|
|
@@ -248,6 +248,4 @@ if __name__ == "__main__":
|
|
|
248
248
|
decoder.eval()
|
|
249
249
|
with torch.no_grad():
|
|
250
250
|
print(f"Encoder output shape: {encoder(x).shape}")
|
|
251
|
-
print(f"Decoder output shape: {decoder(x=x, context=x).shape}")
|
|
252
|
-
else:
|
|
253
|
-
print(f'Transformer imported from local file "{__name__}.py"')
|
|
251
|
+
print(f"Decoder output shape: {decoder(x=x, context=x).shape}")
|
|
@@ -5,174 +5,15 @@ A collection of utility functions for teaching deep learning
|
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
7
|
import time
|
|
8
|
-
import numpy as np
|
|
9
8
|
import copy
|
|
10
|
-
import re
|
|
11
|
-
import pickle
|
|
12
|
-
import json
|
|
13
|
-
from collections import Counter
|
|
14
|
-
import urllib.request
|
|
15
9
|
|
|
10
|
+
import numpy as np
|
|
16
11
|
import matplotlib.pyplot as plt
|
|
17
|
-
|
|
18
12
|
import torch
|
|
19
13
|
import torch.nn as nn
|
|
20
14
|
|
|
21
15
|
#%%
|
|
22
16
|
|
|
23
|
-
class TextVectorizer:
|
|
24
|
-
def __init__(self, max_tokens=20000, output_mode="int", output_sequence_length=None, ngrams=None, standardize=None):
|
|
25
|
-
self.max_tokens = max_tokens
|
|
26
|
-
self.output_mode = output_mode
|
|
27
|
-
self.output_sequence_length = output_sequence_length
|
|
28
|
-
self.ngrams = ngrams
|
|
29
|
-
self.standardize = standardize
|
|
30
|
-
self.vocab = None
|
|
31
|
-
self.word_to_idx = None
|
|
32
|
-
self.idx_to_word = None
|
|
33
|
-
self.idf_weights = None
|
|
34
|
-
|
|
35
|
-
def _tokenize(self, text):
|
|
36
|
-
if self.standardize:
|
|
37
|
-
text = self.standardize(text)
|
|
38
|
-
else:
|
|
39
|
-
text = text.lower()
|
|
40
|
-
text = re.sub(r'[^\w\s]', ' ', text)
|
|
41
|
-
tokens = text.split()
|
|
42
|
-
if self.ngrams and self.ngrams >= 2:
|
|
43
|
-
ngram_tokens = []
|
|
44
|
-
for n in range(1, self.ngrams + 1):
|
|
45
|
-
for i in range( len(tokens) - n + 1 ):
|
|
46
|
-
ngram_tokens.append(' '.join(tokens[i:i + n]))
|
|
47
|
-
return ngram_tokens
|
|
48
|
-
return tokens
|
|
49
|
-
|
|
50
|
-
def adapt(self, texts):
|
|
51
|
-
"""Build vocabulary from texts"""
|
|
52
|
-
token_counter = Counter()
|
|
53
|
-
doc_freq = Counter() # for TF-IDF
|
|
54
|
-
num_docs = 0
|
|
55
|
-
print("Adapting TextVectorizer...")
|
|
56
|
-
for text in texts:
|
|
57
|
-
num_docs += 1
|
|
58
|
-
tokens = self._tokenize(text)
|
|
59
|
-
token_counter.update(tokens)
|
|
60
|
-
doc_freq.update(set(tokens))
|
|
61
|
-
# Reserve 0 for padding, 1 for unknown, hence minus 2 to account for them.
|
|
62
|
-
most_common = token_counter.most_common(self.max_tokens - 2 if self.max_tokens else None) # most_common(n=None) returns all elements in the counter if n is omitted or None.
|
|
63
|
-
self.vocab = ['<pad>', '<unk>'] + [word for word, _ in most_common] # 0 for padding, 1 for unknown
|
|
64
|
-
self.word_to_idx = {word: idx for idx, word in enumerate(self.vocab)}
|
|
65
|
-
self.idx_to_word = {idx: word for idx, word in enumerate(self.vocab)}
|
|
66
|
-
# Compute IDF weights for tf-idf mode
|
|
67
|
-
if self.output_mode == "tf_idf":
|
|
68
|
-
self.idf_weights = np.zeros(len(self.vocab))
|
|
69
|
-
for word, idx in self.word_to_idx.items():
|
|
70
|
-
df = doc_freq.get(word, 0) + 1 # add 1 for smoothing
|
|
71
|
-
self.idf_weights[idx] = np.log(num_docs / df) + 1
|
|
72
|
-
print(f"Vocabulary size: {len(self.vocab)}")
|
|
73
|
-
|
|
74
|
-
def __call__(self, texts):
|
|
75
|
-
if isinstance(texts, str):
|
|
76
|
-
texts = [texts]
|
|
77
|
-
if self.output_mode == "int":
|
|
78
|
-
return self._vectorize_int(texts)
|
|
79
|
-
elif self.output_mode == "multi_hot":
|
|
80
|
-
return self._vectorize_multi_hot(texts)
|
|
81
|
-
elif self.output_mode == "tf_idf":
|
|
82
|
-
return self._vectorize_tfidf(texts)
|
|
83
|
-
|
|
84
|
-
def _vectorize_int(self, texts):
|
|
85
|
-
batch_indices = []
|
|
86
|
-
for text in texts:
|
|
87
|
-
tokens = self._tokenize(text)
|
|
88
|
-
indices = [self.word_to_idx.get(t, 1) for t in tokens] # 1 is <unk>
|
|
89
|
-
if self.output_sequence_length:
|
|
90
|
-
if len(indices) < self.output_sequence_length:
|
|
91
|
-
indices = indices + [0] * (self.output_sequence_length - len(indices))
|
|
92
|
-
else:
|
|
93
|
-
indices = indices[:self.output_sequence_length]
|
|
94
|
-
batch_indices.append(indices)
|
|
95
|
-
return np.array(batch_indices)
|
|
96
|
-
|
|
97
|
-
def _vectorize_multi_hot(self, texts):
|
|
98
|
-
result = np.zeros( (len(texts), len(self.vocab)), dtype=np.float32 )
|
|
99
|
-
for i, text in enumerate(texts):
|
|
100
|
-
tokens = self._tokenize(text)
|
|
101
|
-
for t in tokens:
|
|
102
|
-
idx = self.word_to_idx.get(t, 1)
|
|
103
|
-
if idx < len(self.vocab):
|
|
104
|
-
result[i, idx] = 1
|
|
105
|
-
return result
|
|
106
|
-
|
|
107
|
-
def _vectorize_tfidf(self, texts):
|
|
108
|
-
result = np.zeros( (len(texts), len(self.vocab)), dtype=np.float32 )
|
|
109
|
-
for i, text in enumerate(texts):
|
|
110
|
-
tokens = self._tokenize(text)
|
|
111
|
-
tf = Counter(tokens)
|
|
112
|
-
for t, count in tf.items():
|
|
113
|
-
idx = self.word_to_idx.get(t, 1)
|
|
114
|
-
if idx < len(self.vocab):
|
|
115
|
-
result[i, idx] = count * self.idf_weights[idx]
|
|
116
|
-
return result
|
|
117
|
-
|
|
118
|
-
def get_vocabulary(self):
|
|
119
|
-
return self.vocab
|
|
120
|
-
|
|
121
|
-
def save(self, path):
|
|
122
|
-
vec = {'max_tokens': self.max_tokens, 'output_sequence_length': self.output_sequence_length,
|
|
123
|
-
'vocab': self.vocab, 'word_to_idx': self.word_to_idx, 'idx_to_word': self.idx_to_word,
|
|
124
|
-
'has_standardizer': self.standardize is not None}
|
|
125
|
-
with open(path, 'wb') as f:
|
|
126
|
-
pickle.dump(vec, f)
|
|
127
|
-
|
|
128
|
-
# transforms a method into a class method (i.e., bound to the class rather than an individual instance)
|
|
129
|
-
@classmethod
|
|
130
|
-
def load(cls, path, standardize=None):
|
|
131
|
-
data = pickle.load(path, weights_only=False)
|
|
132
|
-
vectorizer = cls( max_tokens=data['max_tokens'], output_sequence_length=data['output_sequence_length'],
|
|
133
|
-
standardize=standardize if data.get('has_standardize') else None )
|
|
134
|
-
vectorizer.vocab = data['vocab']
|
|
135
|
-
vectorizer.word_to_idx = data['word_to_idx']
|
|
136
|
-
vectorizer.idx_to_word = data['idx_to_word']
|
|
137
|
-
return vectorizer
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def query_ollama(prompt, model="llama3", url="http://localhost:11434/api/chat"):
|
|
141
|
-
# Create the data payload as a dictionary
|
|
142
|
-
data = {
|
|
143
|
-
"model": model,
|
|
144
|
-
"messages": [
|
|
145
|
-
{"role": "user", "content": prompt}
|
|
146
|
-
],
|
|
147
|
-
"options": { # Settings below are required for deterministic responses
|
|
148
|
-
"seed": 123,
|
|
149
|
-
"temperature": 0,
|
|
150
|
-
"num_ctx": 2048
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
# Convert the dictionary to a JSON formatted string and encode it to bytes
|
|
155
|
-
payload = json.dumps(data).encode("utf-8")
|
|
156
|
-
|
|
157
|
-
# Create a request object, setting the method to POST and adding necessary headers
|
|
158
|
-
request = urllib.request.Request(url, data=payload, method="POST")
|
|
159
|
-
request.add_header("Content-Type", "application/json")
|
|
160
|
-
|
|
161
|
-
# Send the request and capture the response
|
|
162
|
-
response_data = ""
|
|
163
|
-
with urllib.request.urlopen(request) as response:
|
|
164
|
-
# Read and decode the response
|
|
165
|
-
while True:
|
|
166
|
-
line = response.readline().decode("utf-8")
|
|
167
|
-
if not line:
|
|
168
|
-
break
|
|
169
|
-
response_json = json.loads(line)
|
|
170
|
-
response_data += response_json["message"]["content"]
|
|
171
|
-
|
|
172
|
-
return response_data
|
|
173
|
-
|
|
174
|
-
#%%
|
|
175
|
-
|
|
176
17
|
class ModelCheckpoint:
|
|
177
18
|
def __init__(self, filepath, monitor="val_loss", save_best_only=True, save_optimizer_state=False):
|
|
178
19
|
self.filepath = filepath
|
|
@@ -470,10 +311,12 @@ def evaluate(model, loader, device, loss_fn=nn.CrossEntropyLoss(), accuracy_fn=N
|
|
|
470
311
|
val_accuracy = val_accuracy.item() # extract the value of a single-item tensor
|
|
471
312
|
return {'loss':val_loss, 'accuracy':val_accuracy}
|
|
472
313
|
|
|
473
|
-
#%%
|
|
314
|
+
#%% Sanity Check: run from the parent folder of simonbb as "python -m simonbb.cis433.utils" since we used relative import
|
|
474
315
|
|
|
475
316
|
if __name__ == "__main__":
|
|
476
317
|
|
|
318
|
+
from ..utils import TextVectorizer
|
|
319
|
+
|
|
477
320
|
vectorizer = TextVectorizer( max_tokens=10, output_mode="int" )
|
|
478
321
|
txts = ['how are you']
|
|
479
322
|
vectorizer.adapt( txts )
|
|
@@ -481,6 +324,4 @@ if __name__ == "__main__":
|
|
|
481
324
|
print(encoded)
|
|
482
325
|
print(vectorizer.vocab)
|
|
483
326
|
print(vectorizer.word_to_idx)
|
|
484
|
-
print(vectorizer.idx_to_word)
|
|
485
|
-
else:
|
|
486
|
-
print(f'Importing from local file "{__name__}.py"')
|
|
327
|
+
print(vectorizer.idx_to_word)
|
|
File without changes
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# module dependencies: torch torchvision langchain_core langchain langgraph langchain_community langchain_chroma langchain_huggingface sentence_transformers
|
|
2
|
+
# additional packages for class projects: langchain_ollama langchain_text_splitters unstructured streamlit_javascript
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from typing import Annotated, Literal, TypedDict, get_args
|
|
7
|
+
|
|
8
|
+
from functools import partial
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
12
|
+
|
|
13
|
+
from langchain_core.documents import Document
|
|
14
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
15
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
16
|
+
from langchain_core.runnables import RunnableLambda
|
|
17
|
+
|
|
18
|
+
from langgraph.graph import END, START, StateGraph
|
|
19
|
+
from langgraph.graph.message import add_messages
|
|
20
|
+
|
|
21
|
+
from langchain_chroma import Chroma
|
|
22
|
+
from langchain_huggingface import HuggingFaceEmbeddings
|
|
23
|
+
from langchain_community.document_loaders import PyPDFLoader, UnstructuredHTMLLoader
|
|
24
|
+
from sentence_transformers import CrossEncoder
|
|
25
|
+
|
|
26
|
+
embedding_model = HuggingFaceEmbeddings(
|
|
27
|
+
model_name="BAAI/bge-large-en-v1.5",
|
|
28
|
+
model_kwargs={'device': device},
|
|
29
|
+
encode_kwargs={'normalize_embeddings': False}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
#%%
|
|
33
|
+
|
|
34
|
+
def load_documents(path):
|
|
35
|
+
"""Load documents from a file or directory."""
|
|
36
|
+
|
|
37
|
+
docs = []
|
|
38
|
+
success_cnt = 0
|
|
39
|
+
page_cnt = 0
|
|
40
|
+
|
|
41
|
+
if os.path.isfile(path):
|
|
42
|
+
filename = os.path.basename(path)
|
|
43
|
+
print(filename)
|
|
44
|
+
ext = filename.lower()
|
|
45
|
+
if ext.endswith('.pdf'):
|
|
46
|
+
documents = PyPDFLoader(path).load()
|
|
47
|
+
elif ext.endswith(('.html', '.ejs')):
|
|
48
|
+
documents = UnstructuredHTMLLoader(path).load()
|
|
49
|
+
else:
|
|
50
|
+
print(f"Unsupported file type: {filename}")
|
|
51
|
+
return None, 0, 0
|
|
52
|
+
success_cnt = 1
|
|
53
|
+
page_cnt = len(documents)
|
|
54
|
+
docs.extend(documents)
|
|
55
|
+
elif os.path.isdir(path):
|
|
56
|
+
for filename in os.listdir(path):
|
|
57
|
+
file = os.path.join(path, filename)
|
|
58
|
+
if not os.path.isfile(file):
|
|
59
|
+
continue
|
|
60
|
+
print(filename)
|
|
61
|
+
ext = filename.lower()
|
|
62
|
+
if ext.endswith('.pdf'):
|
|
63
|
+
documents = PyPDFLoader(file).load()
|
|
64
|
+
elif ext.endswith(('.html', '.ejs')):
|
|
65
|
+
documents = UnstructuredHTMLLoader(file).load()
|
|
66
|
+
else:
|
|
67
|
+
continue
|
|
68
|
+
success_cnt += 1
|
|
69
|
+
page_cnt += len(documents)
|
|
70
|
+
docs.extend(documents)
|
|
71
|
+
else:
|
|
72
|
+
print(f"Error: '{path}' is not a valid file or directory")
|
|
73
|
+
return None, 0, 0
|
|
74
|
+
|
|
75
|
+
if success_cnt == 0:
|
|
76
|
+
print("No valid documents found!")
|
|
77
|
+
return None, 0, 0
|
|
78
|
+
|
|
79
|
+
return docs, success_cnt, page_cnt
|
|
80
|
+
|
|
81
|
+
reranker_model = CrossEncoder("BAAI/bge-reranker-large")
|
|
82
|
+
def _getRetriever( knowledge_base, query_llm=None, k=3, vstore_path = f'{os.getenv("HOME")}/vstore' ):
|
|
83
|
+
store = Chroma( persist_directory=f'{vstore_path}/{knowledge_base}/BAAI_db/chroma_vector_store', embedding_function=embedding_model )
|
|
84
|
+
base_retriever = store.as_retriever(search_kwargs={"k": k}) # a vector store becomes a retriever through the as_retriever() method
|
|
85
|
+
if query_llm==None:
|
|
86
|
+
return base_retriever
|
|
87
|
+
|
|
88
|
+
perspectives_prompt = ChatPromptTemplate.from_template(
|
|
89
|
+
"""You are an AI language model assistant. Your task is to generate 5 different versions of the given original question
|
|
90
|
+
These should be in 5 lines and provide ONLY these alternative questions separated by newlines, no explaination is needed.
|
|
91
|
+
Original question: {question}""")
|
|
92
|
+
def parse_queries_output(message):
|
|
93
|
+
#print( message.content )
|
|
94
|
+
return message.content.split('\n')
|
|
95
|
+
|
|
96
|
+
query_gen = perspectives_prompt | query_llm | parse_queries_output
|
|
97
|
+
|
|
98
|
+
def get_unique_union(document_lists): # Flatten list of lists, and deduplicate them
|
|
99
|
+
deduped_docs = { doc.page_content: doc for sublist in document_lists for doc in sublist}
|
|
100
|
+
unique_docs = list(deduped_docs.values()) # a flat list of unique docs
|
|
101
|
+
return unique_docs
|
|
102
|
+
|
|
103
|
+
def rerank_docs(query, docs, top_n=3):
|
|
104
|
+
"""Rerank documents using cross-encoder and return top_n."""
|
|
105
|
+
if not docs:
|
|
106
|
+
return []
|
|
107
|
+
pairs = [(query, doc.page_content) for doc in docs]
|
|
108
|
+
scores = reranker_model.predict(pairs)
|
|
109
|
+
scored_docs = sorted(zip(scores, docs), key=lambda x: float(x[0]), reverse=True)
|
|
110
|
+
return [doc for _, doc in scored_docs[:top_n]]
|
|
111
|
+
|
|
112
|
+
def retrieve_and_rerank(query):
|
|
113
|
+
# Generate alternative queries + include original
|
|
114
|
+
alt_queries = query_gen.invoke({"question": query})
|
|
115
|
+
all_queries = [query] + alt_queries
|
|
116
|
+
# Retrieve chunks for each query
|
|
117
|
+
doc_lists = base_retriever.batch(all_queries)
|
|
118
|
+
unique_docs = get_unique_union(doc_lists)
|
|
119
|
+
# Rerank with cross-encoder and return top 3
|
|
120
|
+
return rerank_docs(query, unique_docs, top_n=5)
|
|
121
|
+
|
|
122
|
+
return RunnableLambda(retrieve_and_rerank)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
RAG Chain
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def getRagModel(knowledge_base, llm, query_llm=None, k=3):
|
|
130
|
+
"""
|
|
131
|
+
Create and return a RAG model that returns a dictionary with 'result' and 'source_documents'.
|
|
132
|
+
"""
|
|
133
|
+
retriever = _getRetriever(knowledge_base, query_llm, k)
|
|
134
|
+
|
|
135
|
+
def _rag_chain(input_dict):
|
|
136
|
+
query = input_dict["query"]
|
|
137
|
+
docs = retriever.invoke(query)
|
|
138
|
+
snippets = []
|
|
139
|
+
for i, doc in enumerate(docs, start=1):
|
|
140
|
+
snippets.append(f"Snippet {i}: {doc.page_content}")
|
|
141
|
+
context = "\n".join(snippets)
|
|
142
|
+
system_prompt = """You are a helpful assistant answering questions based on provided document snippets.
|
|
143
|
+
IMPORTANT INSTRUCTIONS:
|
|
144
|
+
1. Base your answer ONLY on the provided context snippets below.
|
|
145
|
+
2. Synthesize information from multiple snippets if needed.
|
|
146
|
+
3. If the snippets contain partial information, provide the best answer you can with what's available.
|
|
147
|
+
4. Only say "There wasn't enough information to answer the question" if the snippets truly contain NO relevant information.
|
|
148
|
+
5. Always cite your sources using the snippet numbers in parentheses, like (1), (2), etc.
|
|
149
|
+
|
|
150
|
+
The snippets are provided in this format:
|
|
151
|
+
Snippet 1: <text content>
|
|
152
|
+
Snippet 2: <text content>
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
{context}"""
|
|
156
|
+
prompt = ChatPromptTemplate.from_messages( [ ("system", system_prompt), ("human", "{query}") ])
|
|
157
|
+
messages = prompt.format_messages( context=context, query=query )
|
|
158
|
+
answer = llm.invoke(messages).content
|
|
159
|
+
return {"answer": answer, "sources": docs}
|
|
160
|
+
|
|
161
|
+
return RunnableLambda(_rag_chain)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
multi-rag system with router
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
domain_prompt = """You are a helpful assistant answering questions based on provided document snippets relating to {domain}.
|
|
169
|
+
|
|
170
|
+
IMPORTANT INSTRUCTIONS:
|
|
171
|
+
1. Base your answer ONLY on the provided context snippets below.
|
|
172
|
+
2. Synthesize information from multiple snippets if needed.
|
|
173
|
+
3. If the snippets contain partial information, provide the best answer you can with what's available.
|
|
174
|
+
4. Only say "There wasn't enough information to answer the question" if the snippets truly contain NO relevant information.
|
|
175
|
+
5. Always cite your sources using the snippet numbers in parentheses, like (1), (2), etc.
|
|
176
|
+
|
|
177
|
+
The snippets are provided in this format:
|
|
178
|
+
Snippet 1: <text content>
|
|
179
|
+
Snippet 2: <text content>
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
{context}"""
|
|
183
|
+
|
|
184
|
+
def multiRagBuilder( domain_literals, router_prompt, chat_router, chat, query_llm=None, k=3):
|
|
185
|
+
"""
|
|
186
|
+
domain_literals: the last one is a placeholder if the router finds no domain matches the query
|
|
187
|
+
"""
|
|
188
|
+
domains = get_args(domain_literals)[:-1] # exclude the placeholder
|
|
189
|
+
nodomain = get_args(domain_literals)[-1]
|
|
190
|
+
|
|
191
|
+
class State(TypedDict):
|
|
192
|
+
messages: Annotated[list, add_messages] # annotated with the reducer function to append new messages to the existing list, rather than overwriting
|
|
193
|
+
query: str # input
|
|
194
|
+
domain: domain_literals # output
|
|
195
|
+
documents: list[Document]
|
|
196
|
+
answer: str
|
|
197
|
+
|
|
198
|
+
class Input(TypedDict):
|
|
199
|
+
query: str
|
|
200
|
+
|
|
201
|
+
class Output(TypedDict):
|
|
202
|
+
domain: domain_literals
|
|
203
|
+
documents: list[Document]
|
|
204
|
+
answer: str
|
|
205
|
+
|
|
206
|
+
def router_node(state: State) -> State:
|
|
207
|
+
user_message = HumanMessage(state["query"])
|
|
208
|
+
messages = [ SystemMessage(router_prompt), *state["messages"], user_message ]
|
|
209
|
+
res = chat_router.invoke(messages)
|
|
210
|
+
return {
|
|
211
|
+
"domain": res.content.strip(),
|
|
212
|
+
"messages": [user_message, res], # update conversation history
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
def pick_retriever( state: State) -> Literal[*domains, "generate_answer"]:
|
|
216
|
+
for domain in domains:
|
|
217
|
+
if re.search( domain, state['domain'] ):
|
|
218
|
+
return domain
|
|
219
|
+
return 'generate_answer'
|
|
220
|
+
|
|
221
|
+
def generate_answer(state: State) -> State:
|
|
222
|
+
snippets = []
|
|
223
|
+
for i, doc in enumerate(state.get('documents', []), start=1):
|
|
224
|
+
snippets.append(f"Snippet {i}: {doc.page_content}")
|
|
225
|
+
context = "\n".join(snippets)
|
|
226
|
+
if state.get('domain', '')!=nodomain:
|
|
227
|
+
prompt = ChatPromptTemplate.from_messages( [ ("system", domain_prompt), ("human", "{query}") ])
|
|
228
|
+
messages = prompt.format_messages( domain=state.get('domain', ''), context=context, query=state["query"] )
|
|
229
|
+
else:
|
|
230
|
+
prompt = ChatPromptTemplate.from_messages( [ ("system", 'You are a helpful assistant.'), ("human", "{query}") ])
|
|
231
|
+
messages = prompt.format_messages( query=state["query"] )
|
|
232
|
+
res = chat.invoke(messages)
|
|
233
|
+
return {
|
|
234
|
+
"answer": res.content,
|
|
235
|
+
"messages": res, # update conversation history
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
def retriever_node( domain: domains, state: State):
|
|
239
|
+
query = re.sub(domain, 'this class', state["query"] ) # using the course code hurts RAG since the course code is rarely used in documents.
|
|
240
|
+
#print( f"domain={domain}; query={query}")
|
|
241
|
+
documents = retrievers[domain].invoke( query )
|
|
242
|
+
return { "documents": documents, 'query': query }
|
|
243
|
+
|
|
244
|
+
builder = StateGraph(State, input_schema=Input, output_schema=Output)
|
|
245
|
+
builder.add_node("router", router_node)
|
|
246
|
+
builder.add_edge(START, "router")
|
|
247
|
+
builder.add_conditional_edges("router", pick_retriever)
|
|
248
|
+
|
|
249
|
+
retrievers = {}
|
|
250
|
+
retriever_nodes = {}
|
|
251
|
+
for domain in domains:
|
|
252
|
+
retrievers[domain] = _getRetriever(domain, query_llm, k)
|
|
253
|
+
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.
|
|
254
|
+
builder.add_node(domain, retriever_nodes[domain])
|
|
255
|
+
builder.add_edge(domain, "generate_answer")
|
|
256
|
+
|
|
257
|
+
builder.add_node("generate_answer", generate_answer)
|
|
258
|
+
builder.add_edge("generate_answer", END)
|
|
259
|
+
|
|
260
|
+
return builder
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import pickle
|
|
3
|
+
import json
|
|
4
|
+
from collections import Counter
|
|
5
|
+
import urllib.request
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TextVectorizer:
|
|
10
|
+
def __init__(self, max_tokens=20000, output_mode="int", output_sequence_length=None, ngrams=None, standardize=None):
|
|
11
|
+
self.max_tokens = max_tokens
|
|
12
|
+
self.output_mode = output_mode
|
|
13
|
+
self.output_sequence_length = output_sequence_length
|
|
14
|
+
self.ngrams = ngrams
|
|
15
|
+
self.standardize = standardize
|
|
16
|
+
self.vocab = None
|
|
17
|
+
self.word_to_idx = None
|
|
18
|
+
self.idx_to_word = None
|
|
19
|
+
self.idf_weights = None
|
|
20
|
+
|
|
21
|
+
def _tokenize(self, text):
|
|
22
|
+
if self.standardize:
|
|
23
|
+
text = self.standardize(text)
|
|
24
|
+
else:
|
|
25
|
+
text = text.lower()
|
|
26
|
+
text = re.sub(r'[^\w\s]', ' ', text)
|
|
27
|
+
tokens = text.split()
|
|
28
|
+
if self.ngrams and self.ngrams >= 2:
|
|
29
|
+
ngram_tokens = []
|
|
30
|
+
for n in range(1, self.ngrams + 1):
|
|
31
|
+
for i in range( len(tokens) - n + 1 ):
|
|
32
|
+
ngram_tokens.append(' '.join(tokens[i:i + n]))
|
|
33
|
+
return ngram_tokens
|
|
34
|
+
return tokens
|
|
35
|
+
|
|
36
|
+
def adapt(self, texts):
|
|
37
|
+
"""Build vocabulary from texts"""
|
|
38
|
+
token_counter = Counter()
|
|
39
|
+
doc_freq = Counter() # for TF-IDF
|
|
40
|
+
num_docs = 0
|
|
41
|
+
print("Adapting TextVectorizer...")
|
|
42
|
+
for text in texts:
|
|
43
|
+
num_docs += 1
|
|
44
|
+
tokens = self._tokenize(text)
|
|
45
|
+
token_counter.update(tokens)
|
|
46
|
+
doc_freq.update(set(tokens))
|
|
47
|
+
# Reserve 0 for padding, 1 for unknown, hence minus 2 to account for them.
|
|
48
|
+
most_common = token_counter.most_common(self.max_tokens - 2 if self.max_tokens else None) # most_common(n=None) returns all elements in the counter if n is omitted or None.
|
|
49
|
+
self.vocab = ['<pad>', '<unk>'] + [word for word, _ in most_common] # 0 for padding, 1 for unknown
|
|
50
|
+
self.word_to_idx = {word: idx for idx, word in enumerate(self.vocab)}
|
|
51
|
+
self.idx_to_word = {idx: word for idx, word in enumerate(self.vocab)}
|
|
52
|
+
# Compute IDF weights for tf-idf mode
|
|
53
|
+
if self.output_mode == "tf_idf":
|
|
54
|
+
self.idf_weights = np.zeros(len(self.vocab))
|
|
55
|
+
for word, idx in self.word_to_idx.items():
|
|
56
|
+
df = doc_freq.get(word, 0) + 1 # add 1 for smoothing
|
|
57
|
+
self.idf_weights[idx] = np.log(num_docs / df) + 1
|
|
58
|
+
print(f"Vocabulary size: {len(self.vocab)}")
|
|
59
|
+
|
|
60
|
+
def __call__(self, texts):
|
|
61
|
+
if isinstance(texts, str):
|
|
62
|
+
texts = [texts]
|
|
63
|
+
if self.output_mode == "int":
|
|
64
|
+
return self._vectorize_int(texts)
|
|
65
|
+
elif self.output_mode == "multi_hot":
|
|
66
|
+
return self._vectorize_multi_hot(texts)
|
|
67
|
+
elif self.output_mode == "tf_idf":
|
|
68
|
+
return self._vectorize_tfidf(texts)
|
|
69
|
+
|
|
70
|
+
def _vectorize_int(self, texts):
|
|
71
|
+
batch_indices = []
|
|
72
|
+
for text in texts:
|
|
73
|
+
tokens = self._tokenize(text)
|
|
74
|
+
indices = [self.word_to_idx.get(t, 1) for t in tokens] # 1 is <unk>
|
|
75
|
+
if self.output_sequence_length:
|
|
76
|
+
if len(indices) < self.output_sequence_length:
|
|
77
|
+
indices = indices + [0] * (self.output_sequence_length - len(indices))
|
|
78
|
+
else:
|
|
79
|
+
indices = indices[:self.output_sequence_length]
|
|
80
|
+
batch_indices.append(indices)
|
|
81
|
+
return np.array(batch_indices)
|
|
82
|
+
|
|
83
|
+
def _vectorize_multi_hot(self, texts):
|
|
84
|
+
result = np.zeros( (len(texts), len(self.vocab)), dtype=np.float32 )
|
|
85
|
+
for i, text in enumerate(texts):
|
|
86
|
+
tokens = self._tokenize(text)
|
|
87
|
+
for t in tokens:
|
|
88
|
+
idx = self.word_to_idx.get(t, 1)
|
|
89
|
+
if idx < len(self.vocab):
|
|
90
|
+
result[i, idx] = 1
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def _vectorize_tfidf(self, texts):
|
|
94
|
+
result = np.zeros( (len(texts), len(self.vocab)), dtype=np.float32 )
|
|
95
|
+
for i, text in enumerate(texts):
|
|
96
|
+
tokens = self._tokenize(text)
|
|
97
|
+
tf = Counter(tokens)
|
|
98
|
+
for t, count in tf.items():
|
|
99
|
+
idx = self.word_to_idx.get(t, 1)
|
|
100
|
+
if idx < len(self.vocab):
|
|
101
|
+
result[i, idx] = count * self.idf_weights[idx]
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
def get_vocabulary(self):
|
|
105
|
+
return self.vocab
|
|
106
|
+
|
|
107
|
+
def save(self, path):
|
|
108
|
+
vec = {'max_tokens': self.max_tokens, 'output_sequence_length': self.output_sequence_length,
|
|
109
|
+
'vocab': self.vocab, 'word_to_idx': self.word_to_idx, 'idx_to_word': self.idx_to_word,
|
|
110
|
+
'has_standardizer': self.standardize is not None}
|
|
111
|
+
with open(path, 'wb') as f:
|
|
112
|
+
pickle.dump(vec, f)
|
|
113
|
+
|
|
114
|
+
# transforms a method into a class method (i.e., bound to the class rather than an individual instance)
|
|
115
|
+
@classmethod
|
|
116
|
+
def load(cls, path, standardize=None):
|
|
117
|
+
data = pickle.load(path, weights_only=False)
|
|
118
|
+
vectorizer = cls( max_tokens=data['max_tokens'], output_sequence_length=data['output_sequence_length'],
|
|
119
|
+
standardize=standardize if data.get('has_standardize') else None )
|
|
120
|
+
vectorizer.vocab = data['vocab']
|
|
121
|
+
vectorizer.word_to_idx = data['word_to_idx']
|
|
122
|
+
vectorizer.idx_to_word = data['idx_to_word']
|
|
123
|
+
return vectorizer
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def query_ollama(prompt, model="llama3", url="http://localhost:11434/api/chat"):
|
|
127
|
+
# Create the data payload as a dictionary
|
|
128
|
+
data = {
|
|
129
|
+
"model": model,
|
|
130
|
+
"messages": [
|
|
131
|
+
{"role": "user", "content": prompt}
|
|
132
|
+
],
|
|
133
|
+
"options": { # Settings below are required for deterministic responses
|
|
134
|
+
"seed": 123,
|
|
135
|
+
"temperature": 0,
|
|
136
|
+
"num_ctx": 2048
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Convert the dictionary to a JSON formatted string and encode it to bytes
|
|
141
|
+
payload = json.dumps(data).encode("utf-8")
|
|
142
|
+
|
|
143
|
+
# Create a request object, setting the method to POST and adding necessary headers
|
|
144
|
+
request = urllib.request.Request(url, data=payload, method="POST")
|
|
145
|
+
request.add_header("Content-Type", "application/json")
|
|
146
|
+
|
|
147
|
+
# Send the request and capture the response
|
|
148
|
+
response_data = ""
|
|
149
|
+
with urllib.request.urlopen(request) as response:
|
|
150
|
+
# Read and decode the response
|
|
151
|
+
while True:
|
|
152
|
+
line = response.readline().decode("utf-8")
|
|
153
|
+
if not line:
|
|
154
|
+
break
|
|
155
|
+
response_json = json.loads(line)
|
|
156
|
+
response_data += response_json["message"]["content"]
|
|
157
|
+
|
|
158
|
+
return response_data
|
simonbb-0.0.1/PKG-INFO
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: simonbb
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: A small package for teaching deep learning
|
|
5
|
-
Project-URL: Homepage, https://simonbb.com
|
|
6
|
-
Project-URL: Issues, https://simonbb.com/issues
|
|
7
|
-
Author-email: Huaxia Rui <huaxia.rui@simon.rochester.edu>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Requires-Python: >=3.9
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
|
-
# What's this package for?
|
|
16
|
-
Anyone who teaches deep learning using PyTorch but doesn't want to bother students with too much nitty-gritty details.
|
simonbb-0.0.1/README.md
DELETED
simonbb-0.0.1/pyproject.toml
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling >= 1.26"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "simonbb"
|
|
7
|
-
version = "0.0.1"
|
|
8
|
-
authors = [
|
|
9
|
-
{ name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
|
|
10
|
-
]
|
|
11
|
-
description = "A small package for teaching deep learning"
|
|
12
|
-
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.9"
|
|
14
|
-
classifiers = [
|
|
15
|
-
"Programming Language :: Python :: 3",
|
|
16
|
-
"Operating System :: OS Independent",
|
|
17
|
-
]
|
|
18
|
-
license = "MIT"
|
|
19
|
-
license-files = ["LICEN[CS]E*"]
|
|
20
|
-
|
|
21
|
-
[project.urls]
|
|
22
|
-
Homepage = "https://simonbb.com"
|
|
23
|
-
Issues = "https://simonbb.com/issues"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|