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 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
+
@@ -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 rui as "python -m rui.torch.transformer" since we used relative import
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
@@ -1,2 +0,0 @@
1
- # What's this package for?
2
- Anyone who teaches deep learning using PyTorch but doesn't want to bother students with too much nitty-gritty details.
@@ -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