drakosfire commited on
Commit
7ab07f3
·
1 Parent(s): 1398b01

Adding changes and Readme to make this into a Swords and Sorecery Rules Lawyer

Browse files
Files changed (5) hide show
  1. .gitignore +17 -0
  2. README.md +30 -2
  3. SRD_embeddings.csv +0 -3
  4. Swords&Wizardry_enhanced_output.json +0 -0
  5. app.py +75 -58
.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.pdf
2
+ *.zip
3
+ *.rar
4
+ *.7z
5
+ *.tar
6
+ *.gz
7
+ *.bz2
8
+ *.xz
9
+ *.zipx
10
+
11
+ # Folders
12
+ __pycache__
13
+ pdfs/
14
+ output/
15
+ docling/
16
+ scripts/
17
+
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: RuleLawyer
3
  emoji: 📚
4
  colorFrom: yellow
5
  colorTo: purple
@@ -9,4 +9,32 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Swords & Wizardry RAG over Rulebook
3
  emoji: 📚
4
  colorFrom: yellow
5
  colorTo: purple
 
9
  pinned: false
10
  ---
11
 
12
+ # Retrieval Augmented Generation for Wizardsa & Wizardy Rule Sets
13
+
14
+ ## Project Overview
15
+
16
+ Welcome to the Retrieval Augmented Generation (RAG) project for Tabletop Role-Playing Game (TTRPG) rule sets. This project, by Alan Meigs, aims to explore the potential of RAG techniques in parsing and interacting with complex PDF documents, specifically focusing on TTRPG rule sets.
17
+
18
+ ## Objectives
19
+
20
+ - **Experiment with RAG**: Utilize Retrieval Augmented Generation to enhance the understanding and interaction with TTRPG rule sets.
21
+ - **PDF Parsing**: Develop methods to effectively parse and extract meaningful information from PDF documents.
22
+ - **Contextual Understanding**: Provide comprehensive context to language models to improve the quality of responses related to TTRPG rules.
23
+
24
+ ## Key Features - Soon Avaiable on DungeonMind.net
25
+
26
+ - **Page-Aware Chunking**: Implement a custom text splitter that retains page information, allowing for precise referencing and context building.
27
+ - **Enhanced JSON Summaries**: Load and utilize document and page-level summaries to provide enriched context for language models.
28
+ - **Embedding and Retrieval**: Use advanced embedding models to convert text chunks into embeddings, facilitating efficient retrieval of relevant information.
29
+ - **Interactive Chatbot**: Deploy a Gradio-based chatbot interface to interact with the parsed rule sets, providing users with accurate and contextually relevant answers.
30
+
31
+ ## How to use
32
+
33
+ - **Chat with the Rulebook**: Use the chatbot to ask questions about the rulebook.
34
+
35
+ ## How to Find Me
36
+
37
+ - [LinkedIn](https://www.linkedin.com/in/alan-meigs/)
38
+ - [GitHub](https://github.com/Drakosfire)
39
+ - [DungeonMind.net](https://www.dungeonmind.net)
40
+
SRD_embeddings.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:0ffdfe9de524d440d57d359270fe8a774009188b528946a79a55f8dd7294e5fe
3
- size 51272010
 
 
 
 
Swords&Wizardry_enhanced_output.json ADDED
The diff for this file is too large to render. See raw diff
 
app.py CHANGED
@@ -8,25 +8,29 @@ from time import perf_counter as timer
8
  from datetime import datetime
9
  import textwrap
10
  import json
11
- import textwrap
12
-
13
  import gradio as gr
14
 
15
  print("Launching")
16
 
17
  client = OpenAI()
18
 
 
 
 
 
19
 
 
 
 
 
 
 
20
 
21
  # Import saved file and view
22
- embeddings_df_save_path = "./SRD_embeddings.csv"
23
  print("Loading embeddings.csv")
24
  text_chunks_and_embedding_df_load = pd.read_csv(embeddings_df_save_path)
25
  print("Embedding file loaded")
26
- embedding_model_path = "BAAI/bge-m3"
27
- print("Loading embedding model")
28
- embedding_model = SentenceTransformer(model_name_or_path=embedding_model_path,
29
- device='cpu') # choose the device to load the model to
30
 
31
  # Convert the stringified embeddings back to numpy arrays
32
  text_chunks_and_embedding_df_load['embedding'] = text_chunks_and_embedding_df_load['embedding_str'].apply(lambda x: np.array(json.loads(x)))
@@ -34,6 +38,20 @@ text_chunks_and_embedding_df_load['embedding'] = text_chunks_and_embedding_df_lo
34
  # Convert texts and embedding df to list of dicts
35
  pages_and_chunks = text_chunks_and_embedding_df_load.to_dict(orient="records")
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  # Convert embeddings to torch tensor and send to device (note: NumPy arrays are float64, torch tensors are float32 by default)
38
  embeddings = torch.tensor(np.array(text_chunks_and_embedding_df_load["embedding"].tolist()), dtype=torch.float32).to('cpu')
39
 
@@ -101,68 +119,70 @@ def print_top_results_and_scores(query: str,
101
 
102
  print(f"Query: {query}\n")
103
  print("Results:")
104
- # Loop through zipped together scores and indicies
105
- for score, index in zip(scores, indices):
 
 
 
 
106
  print(f"Score: {score:.4f}")
107
- print(f"Token Count : {pages_and_chunks[index]['chunk_token_count']}")
108
- # Print relevant sentence chunk (since the scores are in descending order, the most relevant chunk will be first)
109
- print_wrapped(pages_and_chunks[index]["sentence_chunk"])
110
- # Print the page number too so we can reference the textbook further and check the results
111
- print(f"File of Origin: {pages_and_chunks[index]['file_path']}")
112
- print("\n")
 
 
 
 
 
 
 
 
 
113
 
114
  return scores, indices
115
 
116
- def prompt_formatter(query: str,
117
- context_items: list[dict]) -> str:
118
- # Join context items into one dotted paragraph
119
- # print(context_items[0])
120
-
121
- # Alternate print method
122
- # print("\n".join([item["file_path"] + "\n" + str(item['chunk_token_count']) + "\n" + item["sentence_chunk"] for item in context_items]))
123
 
124
- context = "- " + "\n- ".join([item["sentence_chunk"] for item in context_items])
 
 
 
 
 
125
 
126
- # Create a base prompt with examples to help the model
127
- # Note: this is very customizable, I've chosen to use 3 examples of the answer style we'd like.
128
- # We could also write this in a txt file and import it in if we wanted.
129
- base_prompt = """Now use the following context items to answer the user query: {context}
130
- User query: {query}
131
- Answer:"""
132
 
133
- # Update base prompt with context items and query
134
-
135
 
136
-
137
- return base_prompt.format(context=context, query=query)
 
 
138
 
139
- system_prompt = """You are a game design expert specializing in Dungeons & Dragons 5e, answering beginner questions with descriptive, clear responses. Provide a story example. Avoid extraneous details and focus on direct answers. Use the examples provided as a guide for style and brevity. When responding:
140
 
141
  1. Identify the key point of the query.
142
  2. Provide a straightforward answer, omitting the thought process.
143
  3. Avoid additional advice or extended explanations.
144
- 4. Answer in an informative manner, aiding the user's understanding without overwhelming them.
145
- 5. DO NOT SUMMARIZE YOURSELF. DO NOT REPEAT YOURSELF.
146
- 6. End with a line break and "What else can I help with?"
147
-
148
- Refer to these examples for your response style:
149
 
150
- Example 1:
151
- Query: How do I determine what my magic ring does in D&D?
152
- Answer: To learn what your magic ring does, use the Identify spell, take a short rest to study it, or consult a knowledgeable character. Once known, follow the item's instructions to activate and use its powers.
153
 
154
- Example 2:
155
- Query: What's the effect of the spell fireball?
156
- Answer: Fireball is a 3rd-level spell creating a 20-foot-radius sphere of fire, dealing 8d6 fire damage (half on a successful Dexterity save) to creatures within. It ignites flammable objects not worn or carried.
157
 
158
- Example 3:
159
- Query: How do spell slots work for a wizard?
160
- Answer: Spell slots represent your capacity to cast spells. You use a slot of equal or higher level to cast a spell, and you regain all slots after a long rest. You don't lose prepared spells after casting; they can be reused as long as you have available slots.
161
 
162
  Use the context provided to answer the user's query concisely. """
163
 
164
-
165
-
166
  with gr.Blocks() as RulesLawyer:
167
 
168
  message_state = gr.State()
@@ -171,10 +191,8 @@ with gr.Blocks() as RulesLawyer:
171
  msg = gr.Textbox()
172
  clear = gr.ClearButton([msg, chatbot])
173
 
174
- def store_message(message):
175
-
176
- return message
177
-
178
 
179
  def respond(message, chat_history):
180
  print(datetime.now())
@@ -188,11 +206,10 @@ with gr.Blocks() as RulesLawyer:
188
 
189
  # Create a list of context items
190
  context_items = [pages_and_chunks[i] for i in indices]
191
-
192
 
193
  # Format prompt with context items
194
  prompt = prompt_formatter(query=f"Chat History : {chat_history} + {message}",
195
- context_items=context_items)
196
 
197
  bot_message = client.chat.completions.create(
198
  model="gpt-4o",
@@ -203,7 +220,7 @@ with gr.Blocks() as RulesLawyer:
203
  }
204
  ],
205
  temperature=1,
206
- max_tokens=512,
207
  top_p=1,
208
  frequency_penalty=0,
209
  presence_penalty=0
@@ -218,4 +235,4 @@ with gr.Blocks() as RulesLawyer:
218
  msg.submit(respond, [message_state, chatbot_state], [msg, chatbot])
219
 
220
  if __name__ == "__main__":
221
- RulesLawyer.launch()
 
8
  from datetime import datetime
9
  import textwrap
10
  import json
 
 
11
  import gradio as gr
12
 
13
  print("Launching")
14
 
15
  client = OpenAI()
16
 
17
+ # Load the enhanced JSON file with summaries
18
+ def load_enhanced_json(file_path):
19
+ with open(file_path, 'r') as file:
20
+ return json.load(file)
21
 
22
+ enhanced_json_file = "Swords&Wizardry_enhanced_output.json"
23
+ enhanced_data = load_enhanced_json(enhanced_json_file)
24
+
25
+ # Extract document summary and page summaries
26
+ document_summary = enhanced_data.get('document_summary', 'No document summary available.')
27
+ page_summaries = {int(page): data['summary'] for page, data in enhanced_data.get('pages', {}).items()}
28
 
29
  # Import saved file and view
30
+ embeddings_df_save_path = "Swords&Wizardry_output_embeddings.csv"
31
  print("Loading embeddings.csv")
32
  text_chunks_and_embedding_df_load = pd.read_csv(embeddings_df_save_path)
33
  print("Embedding file loaded")
 
 
 
 
34
 
35
  # Convert the stringified embeddings back to numpy arrays
36
  text_chunks_and_embedding_df_load['embedding'] = text_chunks_and_embedding_df_load['embedding_str'].apply(lambda x: np.array(json.loads(x)))
 
38
  # Convert texts and embedding df to list of dicts
39
  pages_and_chunks = text_chunks_and_embedding_df_load.to_dict(orient="records")
40
 
41
+ # Debug: Print the first few rows and column names
42
+ print("DataFrame columns:", text_chunks_and_embedding_df_load.columns)
43
+ print("\nFirst few rows of the DataFrame:")
44
+ print(text_chunks_and_embedding_df_load.head())
45
+
46
+ # Debug: Print the first item in pages_and_chunks
47
+ # print("\nFirst item in pages_and_chunks:")
48
+ # print(pages_and_chunks[0])
49
+
50
+ embedding_model_path = "BAAI/bge-m3"
51
+ print("Loading embedding model")
52
+ embedding_model = SentenceTransformer(model_name_or_path=embedding_model_path,
53
+ device='cpu') # choose the device to load the model to
54
+
55
  # Convert embeddings to torch tensor and send to device (note: NumPy arrays are float64, torch tensors are float32 by default)
56
  embeddings = torch.tensor(np.array(text_chunks_and_embedding_df_load["embedding"].tolist()), dtype=torch.float32).to('cpu')
57
 
 
119
 
120
  print(f"Query: {query}\n")
121
  print("Results:")
122
+ print(f"Number of results: {len(indices)}")
123
+ print(f"Indices: {indices}")
124
+ print(f"Total number of chunks: {len(pages_and_chunks)}")
125
+
126
+ for i, (score, index) in enumerate(zip(scores, indices)):
127
+ print(f"\nResult {i+1}:")
128
  print(f"Score: {score:.4f}")
129
+ print(f"Index: {index}")
130
+
131
+ if index < 0 or index >= len(pages_and_chunks):
132
+ print(f"Error: Index {index} is out of range!")
133
+ continue
134
+
135
+ chunk = pages_and_chunks[index]
136
+ print(f"Token Count: {chunk['chunk_token_count']}")
137
+ print("Available keys:", list(chunk.keys()))
138
+ print("sentence_chunk content:", repr(chunk.get("sentence_chunk", "NOT FOUND")))
139
+
140
+ chunk_text = chunk.get("sentence_chunk", "Chunk not found")
141
+ print_wrapped(chunk_text[:200] + "..." if len(chunk_text) > 200 else chunk_text)
142
+
143
+ print(f"File of Origin: {chunk['file_path']}")
144
 
145
  return scores, indices
146
 
147
+ def prompt_formatter(query: str, context_items: list[dict]) -> str:
148
+ # Include document summary
149
+ formatted_context = f"Document Summary: {document_summary}\n\n"
 
 
 
 
150
 
151
+ # Add context items with their page summaries
152
+ for item in context_items:
153
+ page_number = item.get('page', 'Unknown')
154
+ page_summary = page_summaries.get(page_number, 'No page summary available.')
155
+ formatted_context += f"Summary: {page_summary}\n"
156
+ formatted_context += f"Content: {item['sentence_chunk']}\n\n"
157
 
158
+ base_prompt = """Use the following context to answer the user query:
 
 
 
 
 
159
 
160
+ {context}
 
161
 
162
+ User query: {query}
163
+ Answer:"""
164
+ print(f"Prompt: {base_prompt.format(context=formatted_context, query=query)}")
165
+ return base_prompt.format(context=formatted_context, query=query)
166
 
167
+ system_prompt = """You are a friendly and technical answering system, answering questions with accurate, grounded, descriptive, clear, and specific responses. ALWAYS provide a page number citation. Provide a story example. Avoid extraneous details and focus on direct answers. Use the examples provided as a guide for style and brevity. When responding:
168
 
169
  1. Identify the key point of the query.
170
  2. Provide a straightforward answer, omitting the thought process.
171
  3. Avoid additional advice or extended explanations.
172
+ 4. Answer in an informative manner, aiding the user's understanding without overwhelming them or quoting the source.
173
+ 5. DO NOT SUMMARIZE YOURSELF. DO NOT REPEAT YOURSELF.
174
+ 6. End with page citations, a line break and "What else can I help with?"
 
 
175
 
176
+ Example:
177
+ Query: Explain how the player should think about balance and lethality in this game. Explain how the game master should think about balance and lethality?
178
+ Answer: In "Swords & Wizardry: WhiteBox," players and the game master should consider balance and lethality from different perspectives. For players, understanding that this game encourages creativity and flexibility is key. The rules are intentionally streamlined, allowing for a potentially high-risk environment where player decisions significantly impact outcomes. The players should think carefully about their actions and strategy, knowing that the game can be lethal, especially without reliance on intricate rules for safety. Page 33 discusses the possibility of characters dying when their hit points reach zero, although alternative, less harsh rules regarding unconsciousness and recovery are mentioned.
179
 
180
+ For the game master (referred to as the Referee), balancing the game involves providing fair yet challenging scenarios. The role of the Referee isn't to defeat players but to present interesting and dangerous challenges that enhance the story collaboratively. Page 39 outlines how the Referee and players work together to craft a narrative, with the emphasis on creating engaging and potentially perilous experiences without making it a zero-sum competition. Referees can choose how lethal the game will be, considering their group's preferred play style, including implementing house rules to soften deaths or adjust game balance accordingly.
 
 
181
 
182
+ Pages: 33, 39
 
 
183
 
184
  Use the context provided to answer the user's query concisely. """
185
 
 
 
186
  with gr.Blocks() as RulesLawyer:
187
 
188
  message_state = gr.State()
 
191
  msg = gr.Textbox()
192
  clear = gr.ClearButton([msg, chatbot])
193
 
194
+ def store_message(message):
195
+ return message
 
 
196
 
197
  def respond(message, chat_history):
198
  print(datetime.now())
 
206
 
207
  # Create a list of context items
208
  context_items = [pages_and_chunks[i] for i in indices]
 
209
 
210
  # Format prompt with context items
211
  prompt = prompt_formatter(query=f"Chat History : {chat_history} + {message}",
212
+ context_items=context_items)
213
 
214
  bot_message = client.chat.completions.create(
215
  model="gpt-4o",
 
220
  }
221
  ],
222
  temperature=1,
223
+ max_tokens=1000,
224
  top_p=1,
225
  frequency_penalty=0,
226
  presence_penalty=0
 
235
  msg.submit(respond, [message_state, chatbot_state], [msg, chatbot])
236
 
237
  if __name__ == "__main__":
238
+ RulesLawyer.launch()