Spaces:
Runtime error
Runtime error
Commit
·
7ab07f3
1
Parent(s):
1398b01
Adding changes and Readme to make this into a Swords and Sorecery Rules Lawyer
Browse files- .gitignore +17 -0
- README.md +30 -2
- SRD_embeddings.csv +0 -3
- Swords&Wizardry_enhanced_output.json +0 -0
- 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:
|
3 |
emoji: 📚
|
4 |
colorFrom: yellow
|
5 |
colorTo: purple
|
@@ -9,4 +9,32 @@ app_file: app.py
|
|
9 |
pinned: false
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = "
|
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 |
-
|
105 |
-
|
|
|
|
|
|
|
|
|
106 |
print(f"Score: {score:.4f}")
|
107 |
-
print(f"
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
|
114 |
return scores, indices
|
115 |
|
116 |
-
def prompt_formatter(query: str,
|
117 |
-
|
118 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
125 |
|
126 |
-
|
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 |
-
|
134 |
-
|
135 |
|
136 |
-
|
137 |
-
|
|
|
|
|
138 |
|
139 |
-
system_prompt = """You are a
|
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
|
151 |
-
Query:
|
152 |
-
Answer:
|
153 |
|
154 |
-
|
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 |
-
|
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 |
-
|
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=
|
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()
|