sqfoo commited on
Commit
0f3ad1b
·
verified ·
1 Parent(s): cf73536

Upload folder using huggingface_hub

Browse files
Files changed (26) hide show
  1. .DS_Store +0 -0
  2. .gitattributes +15 -0
  3. app.py +219 -0
  4. llm.py +37 -0
  5. main.py +31 -0
  6. nodes.py +37 -0
  7. pics/.DS_Store +0 -0
  8. pics/0.png +3 -0
  9. pics/1.png +3 -0
  10. pics/2.png +3 -0
  11. pics/3.png +3 -0
  12. pics/4.png +3 -0
  13. pics/5.png +3 -0
  14. pics/6.png +3 -0
  15. pics/7.png +3 -0
  16. pics/8.png +3 -0
  17. pics/E1.png +3 -0
  18. pics/E2.png +3 -0
  19. pics/E3.png +3 -0
  20. pics/E4.png +3 -0
  21. pics/E5.png +3 -0
  22. pics/origin.png +3 -0
  23. player.py +58 -0
  24. requirements.txt +267 -0
  25. settings.py +155 -0
  26. utils.py +98 -0
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.gitattributes CHANGED
@@ -33,3 +33,18 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ pics/0.png filter=lfs diff=lfs merge=lfs -text
37
+ pics/1.png filter=lfs diff=lfs merge=lfs -text
38
+ pics/2.png filter=lfs diff=lfs merge=lfs -text
39
+ pics/3.png filter=lfs diff=lfs merge=lfs -text
40
+ pics/4.png filter=lfs diff=lfs merge=lfs -text
41
+ pics/5.png filter=lfs diff=lfs merge=lfs -text
42
+ pics/6.png filter=lfs diff=lfs merge=lfs -text
43
+ pics/7.png filter=lfs diff=lfs merge=lfs -text
44
+ pics/8.png filter=lfs diff=lfs merge=lfs -text
45
+ pics/E1.png filter=lfs diff=lfs merge=lfs -text
46
+ pics/E2.png filter=lfs diff=lfs merge=lfs -text
47
+ pics/E3.png filter=lfs diff=lfs merge=lfs -text
48
+ pics/E4.png filter=lfs diff=lfs merge=lfs -text
49
+ pics/E5.png filter=lfs diff=lfs merge=lfs -text
50
+ pics/origin.png filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import gradio as gr
4
+ import numpy as np
5
+ from PIL import Image
6
+ import torch
7
+ from transformers import pipeline
8
+
9
+ from huggingface_hub import InferenceClient
10
+ from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
11
+
12
+ from settings import EVENTS
13
+ from player import Player
14
+ from utils import PROMPT, INPUT_PROMPT, extract_after_final_answer
15
+
16
+ # Initialize the HF Inference Client
17
+ # It will automatically pick up the HF_TOKEN environment variable if it's set
18
+ client = InferenceClient()
19
+
20
+ # Define the models we want to use from the Hugging Face Hub
21
+ LLM_MODEL = "Qwen/Qwen2.5-7B-Instruct"
22
+
23
+ target_size=(1024, 1024)
24
+
25
+ def compile_game_video(player, output_path="game_summary.mp4", fps=0.5):
26
+ """Compiles accumulated images into a video. fps=0.5 means 2 seconds per image."""
27
+ image_paths = player.get_images()
28
+ if not image_paths:
29
+ return None
30
+
31
+ image_arrays = []
32
+ try:
33
+ for idx, img_path in enumerate(image_paths):
34
+ if isinstance(img_path, str) and os.path.exists(img_path):
35
+ with Image.open(img_path) as img:
36
+ # 1. Force convert to RGB (strips alpha channel if PNG)
37
+ img = img.convert("RGB")
38
+
39
+ # 2. Resize to prevent MoviePy size mismatch crash
40
+ if img.size != target_size:
41
+ img = img.resize(target_size, Image.Resampling.LANCZOS if hasattr(Image, 'Resampling') else Image.LANCZOS)
42
+
43
+ # 3. Convert PIL Image directly into a NumPy array and store in RAM
44
+ img_array = np.array(img)
45
+ image_arrays.append(img_array)
46
+ else:
47
+ print(f"⚠️ Warning: Skipping missing path: {img_path}")
48
+
49
+ if not image_arrays:
50
+ print("❌ Error: No valid image arrays collected.")
51
+ return None
52
+
53
+ print(f"🎬 Compiling {len(image_arrays)} arrays directly from RAM...")
54
+ # MoviePy natively accepts a list of NumPy arrays!
55
+ clip = ImageSequenceClip(image_arrays, fps=fps)
56
+ clip.write_videofile(output_path, codec="libx264", logger=None)
57
+ return output_path
58
+ except Exception as e:
59
+ print(f"Video compilation failed: {e}")
60
+ return None
61
+
62
+ def start_game():
63
+ """Initializes or resets the game state when the app loads or restarts."""
64
+ initial_loc = 0
65
+ initial_player = Player('player') # Assuming your Player class initializes fresh attributes
66
+ initial_event = EVENTS[initial_loc]
67
+
68
+ initial_player.append_file(initial_event.filelist)
69
+
70
+ # Format the very first event presentation
71
+ initial_story = f"### {initial_event.get_title()}\n\n{initial_event.get_description()}"
72
+ status = initial_player.get_attribute()
73
+
74
+ # Return initial UI values + initial state values
75
+ return (
76
+ initial_story, # updates game_screen_ui
77
+ status['hp'], # updates status_ui
78
+ status['mp'],
79
+ status['coin'],
80
+ initial_event.get_images(),
81
+ gr.update(visible=True), # ensures user input layout is visible
82
+ gr.update(visible=False, value=None),
83
+ initial_loc, # sets state_loc
84
+ initial_player, # sets state_player
85
+ initial_event # sets state_cur
86
+ )
87
+
88
+ def play_turn(human_response, loc, player, cur, custom_token, request: gr.Request):
89
+ """Executes a single turn of the game loop based on user input."""
90
+ if not human_response.strip():
91
+ return "Please type an action to proceed!", str(player.get_attribute()), gr.update(), gr.update(), loc, player, cur
92
+
93
+ chosen_token = custom_token.strip() if custom_token and custom_token.strip() else (request.token if request else None)
94
+
95
+ if not chosen_token:
96
+ yield "❌ **Authentication Required:** Please enter a token above or use OAuth.", "N/A", gr.update(), gr.update(), loc, player, cur
97
+ return
98
+
99
+ # 1. Process AI Model Response
100
+ messages = [
101
+ {
102
+ "role": "user",
103
+ "content": PROMPT + INPUT_PROMPT.format(human_response)
104
+ }
105
+ ]
106
+ try:
107
+ response = client.chat_completion(
108
+ model=LLM_MODEL,
109
+ messages=messages,
110
+ max_tokens=500,
111
+ temperature=0.7
112
+ )
113
+ except Exception as e:
114
+ yield f"Error calling API: {str(e)}", str(player.get_attribute()), gr.update(), loc, player, cur
115
+ return
116
+
117
+ processed_response = extract_after_final_answer(response.choices[0].message.content)
118
+
119
+ # 2. Update Game State / Location
120
+ loc = cur._next(processed_response, player)
121
+ attribute = player.get_attribute()
122
+
123
+ # 3. Check for Game Over conditions
124
+ if attribute['hp'] <= 0 or attribute['mp'] <= 0:
125
+ loc = -1
126
+
127
+ cur = EVENTS[loc]
128
+ if loc == -1 or loc >= 10:
129
+ # Final summary / Game Over screens
130
+ end_title = cur.get_title()
131
+ end_desc = cur.get_description()
132
+ next_image = cur.get_images()
133
+ player.append_file(cur.filelist)
134
+
135
+ final_story = f"### 🏁 {end_title}\n\n{end_desc}\n\n**The Journey Ends Here.**"
136
+ yield final_story, attribute['hp'], attribute['mp'], attribute['coin'], next_image, gr.update(visible=False), gr.update(), loc, player, cur
137
+ # Compile video now that game loop is terminating
138
+ video_file = compile_game_video(player)
139
+
140
+ if video_file:
141
+ final_story += "\n\n🎞️ **Your adventure video is ready below!**"
142
+ yield final_story, attribute['hp'], attribute['mp'], attribute['coin'], next_image, gr.update(visible=False), gr.update(visible=True, value=video_file), loc, player, cur
143
+ return
144
+
145
+ # 4. Advance to the next event normally
146
+ cur = EVENTS[loc]
147
+ next_story = f"### {cur.get_title()}\n\n{cur.get_description()}"
148
+ next_image = cur.get_images()
149
+
150
+ player.append_file(cur.filelist)
151
+
152
+ # Clear out the text box for the next turn while returning updated states
153
+ yield next_story, attribute['hp'], attribute['mp'], attribute['coin'], next_image, gr.update(value=""), gr.update(), loc, player, cur
154
+
155
+ # --------------------------------------------------------------------------
156
+ # LAYOUT DESIGN WITH INTEGRATED GALLERY BUILDER
157
+ # --------------------------------------------------------------------------
158
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
159
+ gr.Markdown("# ⚔️ RPG Sandbox")
160
+ gr.Markdown("## You can find your adventure recap below.")
161
+
162
+
163
+ with gr.Accordion("🔑 Authentication Options", open=False):
164
+ with gr.Row():
165
+ gr.LoginButton()
166
+ user_token_input = gr.Textbox(label="Or paste your HF Token (hf_...)", type="password")
167
+
168
+ state_loc = gr.State()
169
+ state_player = gr.State()
170
+ state_cur = gr.State()
171
+
172
+ with gr.Row():
173
+ with gr.Column(scale=2):
174
+ game_screen_ui = gr.Markdown(value="### Setup authentication, then click below to wake up.")
175
+
176
+ gr.Markdown("### 📜 Status Panel")
177
+ hp_ui = gr.Textbox(label="HP", interactive=False, value="0")
178
+ mp_ui = gr.Textbox(label="Mana Points (MP)", interactive=False, value="0")
179
+ coin_ui = gr.Textbox(label="Mana Points (MP)", interactive=False, value="0")
180
+
181
+ with gr.Row(visible=False) as input_panel:
182
+ user_input = gr.Textbox(label="Your Turn Action", placeholder="Type action and hit Enter...", scale=4)
183
+ submit_btn = gr.Button("Execute Action", variant="primary", scale=1)
184
+
185
+ start_btn = gr.Button("🚀 Start / Reset Game", variant="secondary")
186
+
187
+ with gr.Column(scale=3):
188
+ gr.Markdown("### 🖼️ Story Scene")
189
+
190
+ # UPGRADED: Gr.Image swapped to gr.Gallery to swallow flexible size limits
191
+ game_gallery_ui = gr.Gallery(
192
+ label="Story Plot",
193
+ columns=[2], # Display up to 2 side-by-side tiles automatically
194
+ rows=[2],
195
+ object_fit="contain",
196
+ height=800,
197
+ interactive=False
198
+ )
199
+
200
+ # Added: Video output component, hidden by default
201
+ video_summary_ui = gr.Video(label="🎬 Adventure Recap", visible=False, autoplay=True)
202
+
203
+
204
+ start_btn.click(
205
+ fn=start_game,
206
+ outputs=[game_screen_ui, hp_ui, mp_ui, coin_ui, game_gallery_ui, input_panel, video_summary_ui, state_loc, state_player, state_cur]
207
+ )
208
+
209
+ turn_config = {
210
+ "fn": play_turn,
211
+ "inputs": [user_input, state_loc, state_player, state_cur, user_token_input],
212
+ "outputs": [game_screen_ui, hp_ui, mp_ui, coin_ui, game_gallery_ui, user_input, video_summary_ui, state_loc, state_player, state_cur]
213
+ }
214
+
215
+ submit_btn.click(**turn_config)
216
+ user_input.submit(**turn_config)
217
+
218
+ if __name__ == "__main__":
219
+ demo.queue().launch()
llm.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
2
+
3
+ HUGGINGFACE = {
4
+ 'type': 'huggingface',
5
+ 'param':{
6
+ 'repo_id': "Qwen/Qwen2.5-7B-Instruct",
7
+ 'task': 'text-generation',
8
+ 'max_new_tokens': 512,
9
+ 'do_sample': False,
10
+ 'repetition_penalty': 1.03,
11
+ 'provider': 'auto',
12
+ },
13
+ 'model': 'huggingface:Qwen2.5-7B-Instruct',
14
+ }
15
+
16
+ HUGGINGFACE_LITE = {
17
+ 'type': 'huggingface-lite',
18
+ 'param':{
19
+ 'repo_id': "Qwen/Qwen2.5-1.5B-Instruct",
20
+ 'task': 'text-generation',
21
+ 'max_new_tokens': 512,
22
+ 'do_sample': False,
23
+ 'repetition_penalty': 1.03,
24
+ 'provider': 'auto',
25
+ },
26
+ 'model': 'huggingface:Qwen2.5-1.5B-Instruct',
27
+ }
28
+
29
+ valid_LLM = [HUGGINGFACE, HUGGINGFACE_LITE]
30
+
31
+ def setup_model(config: dict, callbacks=None):
32
+ if config['type'].startswith('huggingface'):
33
+ llm = HuggingFaceEndpoint(**config['param'])
34
+ model = ChatHuggingFace(llm=llm, callbacks=callbacks)
35
+ else:
36
+ model = None
37
+ return model
main.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ load_dotenv('./.env')
3
+
4
+ from settings import EVENTS
5
+ from player import Player
6
+ from llm import *
7
+ from utils import PROMPT, INPUT_PROMPT, extract_after_final_answer
8
+
9
+ loc = 0
10
+ cur = EVENTS[loc]
11
+ player = Player('player-1')
12
+ model = setup_model(HUGGINGFACE)
13
+
14
+ while loc != -1 and loc < 10:
15
+ print(f'Title: {cur.get_title()}')
16
+ print(f'{cur.get_description()}')
17
+
18
+ response = input("Human Response: ")
19
+ response = model.invoke(PROMPT + INPUT_PROMPT.format(response))
20
+ response = extract_after_final_answer(response.content)
21
+ loc = cur._next(response, player)
22
+ attribute = player.get_attribute()
23
+ print(response, loc)
24
+ if attribute['hp'] <= 0 or attribute['mp'] <= 0:
25
+ loc = -1
26
+ cur = EVENTS[loc]
27
+
28
+ print('*'*20)
29
+ print(f'Title: {cur.get_title()}')
30
+ print(f'{cur.get_description()}')
31
+ print(player.get_attribute())
nodes.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from player import Player
2
+ from utils import *
3
+
4
+ class Node:
5
+ def __init__(self, title: str, description: str, filelist: list[str], effect: dict = None, route = None, default: int = -1):
6
+ self.title = title
7
+ self.description = description
8
+ self.filelist = filelist
9
+ self.effect = effect
10
+ self.route = route
11
+ self.default = default
12
+
13
+ def _next(self, result: str, player: Player) -> int:
14
+ result = result.upper()
15
+
16
+ paired = self.effect.get(result, None) if self.effect is not None else None
17
+ if paired is not None:
18
+ for (change, attr, amount) in paired:
19
+ change(player, attr, amount)
20
+
21
+ if self.route is None:
22
+ return -1
23
+ if isinstance(self.route, dict):
24
+ next_node = self.route.get(result, self.default)
25
+ else:
26
+ print('Here')
27
+ next_node = self.route(player)
28
+ return next_node - 1 if next_node >= 0 else -1
29
+
30
+ def get_title(self) -> str:
31
+ return self.title
32
+
33
+ def get_description(self) -> str:
34
+ return self.description
35
+
36
+ def get_images(self) -> list[str]:
37
+ return self.filelist
pics/.DS_Store ADDED
Binary file (6.15 kB). View file
 
pics/0.png ADDED

Git LFS Details

  • SHA256: 24aa14d87ba76f74ba3a30e0a25511a598ed15fc64acd46d75f1d993ccae7013
  • Pointer size: 132 Bytes
  • Size of remote file: 2.71 MB
pics/1.png ADDED

Git LFS Details

  • SHA256: a8d9ae7e283d639133343d6dc4586cf138dfd140c5d8535a6e88b9823c11d78b
  • Pointer size: 132 Bytes
  • Size of remote file: 2.23 MB
pics/2.png ADDED

Git LFS Details

  • SHA256: 17308f4a5785a67e57abe03636a259a0365db9f0807d2d852c9df0f7e51219b9
  • Pointer size: 132 Bytes
  • Size of remote file: 2.46 MB
pics/3.png ADDED

Git LFS Details

  • SHA256: 9385704c88264027dfa9164a63af47e36fbe400c43889725bf21c25090849e3a
  • Pointer size: 132 Bytes
  • Size of remote file: 2.43 MB
pics/4.png ADDED

Git LFS Details

  • SHA256: 5884af81ce9742c2ecbe84875de0e01c3cde7c7550d65662d53360190c7f949b
  • Pointer size: 132 Bytes
  • Size of remote file: 3.76 MB
pics/5.png ADDED

Git LFS Details

  • SHA256: 4d46b6a55551c39aeebef70a1994d6304c5a217086f8e3f0fae6eb80d15ac528
  • Pointer size: 132 Bytes
  • Size of remote file: 3.36 MB
pics/6.png ADDED

Git LFS Details

  • SHA256: 641840fd6dc9c57477c506b73d0367896c4055fa6125f9d784a3298dd7c148dd
  • Pointer size: 132 Bytes
  • Size of remote file: 3.16 MB
pics/7.png ADDED

Git LFS Details

  • SHA256: f503281183eccc038011765c28ffc08b8253af1fb97c91b895e4ae4a4e89a521
  • Pointer size: 132 Bytes
  • Size of remote file: 3.06 MB
pics/8.png ADDED

Git LFS Details

  • SHA256: 02d233f6b48fa49f3bef0127b3aef58fe29032217e1d848f9a1b844b33ac9126
  • Pointer size: 132 Bytes
  • Size of remote file: 3.28 MB
pics/E1.png ADDED

Git LFS Details

  • SHA256: 30d96389d545e5c4204192fa84b3b99470df925c76cf6c9a8320aa36df6211b0
  • Pointer size: 132 Bytes
  • Size of remote file: 3.66 MB
pics/E2.png ADDED

Git LFS Details

  • SHA256: 570fa850684bef96e914bafaa736d86be9077acf3aaaa1f9e3357cfdf1b0b1bd
  • Pointer size: 132 Bytes
  • Size of remote file: 3.61 MB
pics/E3.png ADDED

Git LFS Details

  • SHA256: 4b4bc1bb09e4078b14cb2802573dfa655761cdf6ca7f98711e975f57cea51185
  • Pointer size: 132 Bytes
  • Size of remote file: 3.47 MB
pics/E4.png ADDED

Git LFS Details

  • SHA256: 2830e4295f4bcc3ba0274bdb37928ce15ef5e846638bbeceab91bc1475f5cd06
  • Pointer size: 132 Bytes
  • Size of remote file: 3.68 MB
pics/E5.png ADDED

Git LFS Details

  • SHA256: 536772eb0d1277a2f1217389bad593a829956dfaa5b324d547767bc65617ed95
  • Pointer size: 132 Bytes
  • Size of remote file: 1.62 MB
pics/origin.png ADDED

Git LFS Details

  • SHA256: d5dd9730915ba4f584195f3afc11a994928ad9e8bb8ad5c05d94ec183176e4fd
  • Pointer size: 132 Bytes
  • Size of remote file: 3.59 MB
player.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ class Player:
4
+ def __init__(self, name: str):
5
+ self.name = name
6
+ print(f"Player's Name is {name} and initialize all his attributes")
7
+ self.hp = 75
8
+ self.mp = 50
9
+ self.coin = 20
10
+ self.items = []
11
+ self.frames = []
12
+
13
+ def reduce_hp(self, amount: int):
14
+ print(f'Reduce {amount} HP')
15
+ self.hp = max(0, self.hp-amount)
16
+
17
+ def add_hp(self, amount: int):
18
+ print(f'Add {amount} HP')
19
+ self.hp += amount
20
+
21
+ def reduce_mp(self, amount: int):
22
+ print(f'Reduce {amount} MP')
23
+ self.mp = max(0, self.mp-amount)
24
+
25
+ def add_mp(self, amount: int):
26
+ print(f'Add {amount} MP')
27
+ self.mp += amount
28
+
29
+ def reduce_coin(self, amount: int):
30
+ print(f'Reduce {amount} Coin')
31
+ self.coin = max(0, self.coin-amount)
32
+
33
+ def add_coin(self, amount: int):
34
+ print(f'Add {amount} Coin')
35
+ self.coin += amount
36
+
37
+ def add_item(self, item):
38
+ print(f'Add {item} to the item bag')
39
+ self.items.append(item)
40
+
41
+ def use_item(self, item):
42
+ if item not in self.items:
43
+ print(f'Could not use {item}')
44
+ else:
45
+ self.items.remove(item)
46
+ print(f'Used up {item} and there are {len(item)} items left')
47
+
48
+ def get_attribute(self) -> dict:
49
+ return {'hp': self.hp, 'mp': self.mp, 'coin': self.coin}
50
+
51
+ def set_name(self, name: str):
52
+ self.name = name
53
+
54
+ def append_file(self, filelist: list[str]):
55
+ self.frames += filelist
56
+
57
+ def get_images(self):
58
+ return self.frames
requirements.txt ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ acres==0.5.0
2
+ aiofiles==24.1.0
3
+ aiohappyeyeballs==2.6.1
4
+ aiohttp==3.12.15
5
+ aiosignal==1.4.0
6
+ annotated-doc==0.0.4
7
+ annotated-types==0.7.0
8
+ anyio==4.10.0
9
+ appnope==0.1.4
10
+ argon2-cffi==25.1.0
11
+ argon2-cffi-bindings==25.1.0
12
+ arrow==1.3.0
13
+ asttokens==3.0.0
14
+ async-lru==2.0.5
15
+ attrs==25.3.0
16
+ Authlib==1.7.2
17
+ babel==2.17.0
18
+ backoff==2.2.1
19
+ beautifulsoup4==4.13.5
20
+ bleach==6.2.0
21
+ brotli==1.2.0
22
+ cachetools==5.5.2
23
+ certifi==2025.8.3
24
+ cffi==2.0.0
25
+ charset-normalizer==3.4.3
26
+ ci-info==0.3.0
27
+ click==8.4.1
28
+ clickhouse-connect==0.15.1
29
+ comm==0.2.3
30
+ configobj==5.0.9
31
+ configparser==7.2.0
32
+ contourpy==1.3.3
33
+ cryptography==45.0.7
34
+ cycler==0.12.1
35
+ dataclasses-json==0.6.7
36
+ ddgs==9.14.1
37
+ debugpy==1.8.16
38
+ decorator==5.2.1
39
+ defusedxml==0.7.1
40
+ distro==1.9.0
41
+ duckdb==1.5.2
42
+ duckduckgo_search==8.1.1
43
+ emoji==2.14.1
44
+ environs==9.5.0
45
+ etelemetry==0.3.1
46
+ executing==2.2.0
47
+ faiss-cpu==1.8.0
48
+ fastapi==0.136.1
49
+ fastjsonschema==2.21.2
50
+ ffmpy==1.0.0
51
+ filelock==3.25.2
52
+ filetype==1.2.0
53
+ fitz==0.0.1.dev2
54
+ fonttools==4.62.1
55
+ fqdn==1.5.1
56
+ frozenlist==1.7.0
57
+ fsspec==2026.2.0
58
+ google-ai-generativelanguage==0.6.18
59
+ google-api-core==2.25.1
60
+ google-auth==2.49.2
61
+ google-genai==1.73.1
62
+ googleapis-common-protos==1.70.0
63
+ gradio==6.6.0
64
+ gradio_client==2.1.0
65
+ greenlet==3.2.4
66
+ groovy==0.1.2
67
+ grpcio==1.69.0
68
+ grpcio-status==1.69.0
69
+ h11==0.16.0
70
+ hf-gradio==0.4.1
71
+ hf-xet==1.5.1
72
+ hnswlib==0.8.0
73
+ html5lib==1.1
74
+ httpcore==1.0.9
75
+ httplib2==0.30.0
76
+ httptools==0.7.1
77
+ httpx==0.28.1
78
+ httpx-sse==0.4.1
79
+ huggingface_hub==0.36.2
80
+ idna==3.10
81
+ ImageIO==2.37.3
82
+ imageio-ffmpeg==0.6.0
83
+ inquirerpy==0.3.4
84
+ ipykernel==6.30.1
85
+ ipython==9.5.0
86
+ ipython_pygments_lexers==1.1.1
87
+ ipywidgets==8.1.7
88
+ isoduration==20.11.0
89
+ itsdangerous==2.2.0
90
+ jedi==0.19.2
91
+ Jinja2==3.1.6
92
+ joblib==1.5.2
93
+ joserfc==1.7.1
94
+ json5==0.12.1
95
+ jsonpatch==1.33
96
+ jsonpointer==3.0.0
97
+ jsonschema==4.25.1
98
+ jsonschema-specifications==2025.4.1
99
+ jupyter-events==0.12.0
100
+ jupyter-lsp==2.3.0
101
+ jupyter_server==2.17.0
102
+ jupyter_server_terminals==0.5.3
103
+ jupyterlab_pygments==0.3.0
104
+ jupyterlab_server==2.27.3
105
+ jupyterlab_widgets==3.0.15
106
+ kiwisolver==1.5.0
107
+ langchain==0.3.27
108
+ langchain-community==0.3.29
109
+ langchain-core==0.3.75
110
+ langchain-google-genai==2.1.10
111
+ langchain-huggingface==0.3.1
112
+ langchain-milvus==0.1.7
113
+ langchain-text-splitters==0.3.9
114
+ langdetect==1.0.9
115
+ langgraph==0.6.7
116
+ langgraph-checkpoint==2.1.1
117
+ langgraph-prebuilt==0.6.4
118
+ langgraph-sdk==0.2.6
119
+ langsmith==0.4.19
120
+ lark==1.2.2
121
+ looseversion==1.3.0
122
+ lxml==6.0.1
123
+ lz4==4.4.5
124
+ Markdown==3.8.2
125
+ markdown-it-py==4.0.0
126
+ MarkupSafe==3.0.2
127
+ marshmallow==3.26.1
128
+ matplotlib==3.10.9
129
+ matplotlib-inline==0.1.7
130
+ mdurl==0.1.2
131
+ milvus-lite==2.4.12
132
+ mistune==3.1.4
133
+ moviepy==2.2.1
134
+ mpmath==1.3.0
135
+ multidict==6.6.4
136
+ mypy_extensions==1.1.0
137
+ nbclient==0.10.2
138
+ nbconvert==7.16.6
139
+ nbformat==5.10.4
140
+ nest-asyncio==1.6.0
141
+ networkx==3.6.1
142
+ nibabel==5.3.2
143
+ nipype==1.10.0
144
+ nltk==3.9.1
145
+ notebook_shim==0.2.4
146
+ numpy==1.26.4
147
+ olefile==0.47
148
+ orjson==3.11.3
149
+ ormsgpack==1.10.0
150
+ packaging==25.0
151
+ pandas==2.3.2
152
+ pandocfilters==1.5.1
153
+ parso==0.8.5
154
+ pathlib==1.0.1
155
+ pexpect==4.9.0
156
+ pfzy==0.3.4
157
+ pillow==11.3.0
158
+ platformdirs==4.4.0
159
+ playwright-stealth==2.0.3
160
+ posthog==7.14.0
161
+ primp==1.2.3
162
+ proglog==0.1.12
163
+ prometheus_client==0.22.1
164
+ prompt_toolkit==3.0.52
165
+ propcache==0.3.2
166
+ proto-plus==1.26.1
167
+ protobuf==5.29.5
168
+ prov==2.1.1
169
+ psutil==7.0.0
170
+ ptyprocess==0.7.0
171
+ pure_eval==0.2.3
172
+ puremagic==1.30
173
+ pyasn1==0.6.1
174
+ pyasn1_modules==0.4.2
175
+ pycparser==2.22
176
+ pydantic==2.13.4
177
+ pydantic-settings==2.10.1
178
+ pydantic_core==2.46.4
179
+ pydot==4.0.1
180
+ pydub==0.25.1
181
+ pyee==13.0.1
182
+ Pygments==2.19.2
183
+ pymilvus==2.4.9
184
+ PyMuPDF==1.26.4
185
+ pyparsing==3.2.3
186
+ pypdf==6.0.0
187
+ python-dateutil==2.9.0.post0
188
+ python-dotenv==1.1.1
189
+ python-iso639==2025.2.18
190
+ python-json-logger==3.3.0
191
+ python-magic==0.4.27
192
+ python-multipart==0.0.32
193
+ python-oxmsg==0.0.2
194
+ pytz==2025.2
195
+ pyxnat==1.6.3
196
+ PyYAML==6.0.2
197
+ pyzmq==27.0.2
198
+ RapidFuzz==3.14.0
199
+ rdflib==7.1.4
200
+ referencing==0.36.2
201
+ regex==2025.9.1
202
+ requests==2.32.5
203
+ requests-toolbelt==1.0.0
204
+ rfc3339-validator==0.1.4
205
+ rfc3986-validator==0.1.1
206
+ rfc3987-syntax==1.1.0
207
+ rich==15.0.0
208
+ rpds-py==0.27.1
209
+ rsa==4.9.1
210
+ safehttpx==0.1.7
211
+ safetensors==0.6.2
212
+ scikit-learn==1.7.1
213
+ scipy==1.16.1
214
+ seaborn==0.13.2
215
+ semantic-version==2.10.0
216
+ Send2Trash==1.8.3
217
+ sentence-transformers==5.1.0
218
+ setuptools==82.0.1
219
+ shellingham==1.5.4
220
+ simplejson==3.20.1
221
+ six==1.17.0
222
+ sniffio==1.3.1
223
+ soupsieve==2.8
224
+ SQLAlchemy==2.0.43
225
+ stack-data==0.6.3
226
+ starlette==0.52.1
227
+ sympy==1.14.0
228
+ tenacity==9.1.2
229
+ terminado==0.18.1
230
+ threadpoolctl==3.6.0
231
+ tinycss2==1.4.0
232
+ tokenizers==0.21.4
233
+ tomlkit==0.13.3
234
+ torch==2.2.2
235
+ torchvision==0.17.2
236
+ tornado==6.5.2
237
+ tqdm==4.67.1
238
+ traitlets==5.14.3
239
+ traits==7.0.2
240
+ transformers==4.49.0
241
+ typer==0.25.1
242
+ types-python-dateutil==2.9.0.20250822
243
+ types-requests==2.32.4.20250809
244
+ typing-inspect==0.9.0
245
+ typing-inspection==0.4.2
246
+ typing_extensions==4.15.0
247
+ tzdata==2025.2
248
+ ujson==5.11.0
249
+ unstructured==0.18.14
250
+ unstructured-client==0.42.3
251
+ uri-template==1.3.0
252
+ urllib3==2.5.0
253
+ uvicorn==0.46.0
254
+ uvloop==0.22.1
255
+ watchfiles==1.1.1
256
+ wcwidth==0.2.13
257
+ webcolors==24.11.1
258
+ webencodings==0.5.1
259
+ websocket-client==1.8.0
260
+ websockets==16.0
261
+ wheel==0.47.0
262
+ widgetsnbextension==4.0.14
263
+ wikipedia==1.4.0
264
+ wrapt==1.17.3
265
+ xxhash==3.5.0
266
+ yarl==1.20.1
267
+ zstandard==0.24.0
settings.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ from nodes import Node
4
+ from utils import *
5
+
6
+ EVENTS = [
7
+ Node(
8
+ "Dungeon Entrance",
9
+ "A massive gate blocks the dungeon. A stone guardian awakens and says: 'Only those with strength may enter. What will you do to pass through it? \n\nAttack with sword, or magic, or even BRIDE it?",
10
+ ['pics/0.png'],
11
+ {
12
+ PHYSICAL_ATTACK: [(minus_attribute, 'hp', 20)],
13
+ MAGICAL_ATTACK: [(minus_attribute, 'mp', 20)],
14
+ SPEND_MONEY: [(minus_attribute, 'coin', 30)]
15
+ },
16
+ {
17
+ PHYSICAL_ATTACK: 2,
18
+ MAGICAL_ATTACK: 3,
19
+ SPEND_MONEY: 4,
20
+ LEAVE: -1
21
+ },
22
+ default = random.randint(2, 4)
23
+ ),
24
+ Node(
25
+ "Injured Warrior",
26
+ "Inside the dungeon, you find a dying warrior. He carries a rare potion. What will you do? \n\nHelp Him, or Attack Him?",
27
+ ['pics/1.png'],
28
+ {
29
+ PHYSICAL_REFILL: [(add_attribute, 'hp', 5)],
30
+ PHYSICAL_ATTACK: [(minus_attribute, 'hp', 5)],
31
+ MAGICAL_ATTACK: [(minus_attribute, 'mp', 5)],
32
+ },
33
+ {
34
+ PHYSICAL_REFILL: 5,
35
+ PHYSICAL_ATTACK: 5,
36
+ MAGICAL_ATTACK: 5,
37
+ LEAVE: -1
38
+ },
39
+ default = 5
40
+ ),
41
+ Node(
42
+ "Magic Altar",
43
+ "A mysterious altar offers power. What will you do? \n\nAttack this altar or Refill your stamina here?",
44
+ ['pics/2.png'],
45
+ {
46
+ MAGICAL_REFILL: [(add_attribute, 'mp', 10), (minus_attribute, "hp", 5)],
47
+ PHYSICAL_ATTACK: [(minus_attribute, 'hp', 15)],
48
+ PHYSICAL_REFILL: [(add_attribute, 'hp', 10)]
49
+ },
50
+ {
51
+ MAGICAL_REFILL: 5,
52
+ PHYSICAL_ATTACK: 5,
53
+ },
54
+ default = 5
55
+ ),
56
+ Node(
57
+ "Merchant Room",
58
+ "A merchant appears and says: 'I trade only with survivors.' What will you do? \n\nSpend money, or Rob him?",
59
+ ['pics/3.png'],
60
+ {
61
+ PHYSICAL_ATTACK: [(minus_attribute, "hp", 15), (add_attribute, 'coun', 10)],
62
+ MAGICAL_ATTACK: [(minus_attribute, 'mp', 10), (add_attribute, 'coin', 10)],
63
+ SPEND_MONEY: [(minus_attribute, 'coin', 25), (add_attribute, 'mp', 5), (add_attribute, 'hp', 5)],
64
+ },
65
+ {
66
+ PHYSICAL_ATTACK: 5,
67
+ MAGICAL_ATTACK: 5,
68
+ MAGICAL_REFILL: 5,
69
+ SPEND_MONEY: 5,
70
+ EARN_MONEY: 5,
71
+ },
72
+ default = 5
73
+ ),
74
+ Node(
75
+ "Underground Lake",
76
+ "A giant monster blocks the way. What will you do? \n\nAttack or Run?",
77
+ ['pics/4.png'],
78
+ {
79
+ PHYSICAL_ATTACK: [(minus_attribute, "hp", 30)],
80
+ MAGICAL_ATTACK: [(minus_attribute, 'mp', 20)],
81
+ LEAVE: [(minus_attribute, 'hp', 10)],
82
+ },
83
+ {
84
+ PHYSICAL_ATTACK: 6,
85
+ MAGICAL_ATTACK: 7,
86
+ LEAVE: 8,
87
+ },
88
+ default = random.randint(6, 8)
89
+ ),
90
+ Node(
91
+ "Ancient Library",
92
+ "You find dragon history. What will you do? \n\nDestroy, or Rest, or Buy Weapon?",
93
+ ['pics/5.png', 'pics/origin.png'],
94
+ {
95
+ PHYSICAL_REFILL: [(add_attribute, "hp", 10)],
96
+ MAGICAL_REFILL: [(add_attribute, 'mp', 10)],
97
+ EARN_MONEY: [(add_attribute, 'coin', 20)],
98
+ MAGICAL_ATTACK: [(minus_attribute, 'mp', 15)]
99
+ },
100
+ {
101
+ PHYSICAL_REFILL: 9,
102
+ MAGICAL_REFILL: 9,
103
+ EARN_MONEY: 9,
104
+ },
105
+ default = 9
106
+ ),
107
+ Node(
108
+ "Dragon Prison",
109
+ "A baby dragon is trapped. It is telling you the ORIGIN of DRAGON. What will you do? \n\nAttack, or Run, or Rescue?",
110
+ ['pics/6.png', 'pics/origin.png'],
111
+ {
112
+ PHYSICAL_ATTACK: [(minus_attribute, "hp", 30)],
113
+ MAGICAL_ATTACK: [(minus_attribute, 'mp', 20)],
114
+ LEAVE: [(minus_attribute, 'hp', 10)],
115
+ },
116
+ {
117
+ PHYSICAL_REFILL: 9,
118
+ MAGICAL_REFILL: 9,
119
+ EARN_MONEY: 9,
120
+ },
121
+ default = 9
122
+ ),
123
+ Node(
124
+ "Dark Corridor",
125
+ "A shadow creature attacks. Its attack is fierce and fast. What will you do? \n\nFight back, or Run away?",
126
+ ['pics/7.png'],
127
+ {
128
+ PHYSICAL_ATTACK: [(minus_attribute, "hp", 45), (add_attribute, 'coin', 30)],
129
+ MAGICAL_ATTACK: [(minus_attribute, "mp", 35), (add_attribute, 'coin', 30)],
130
+ LEAVE: [(minus_attribute, 'hp', 20), (minus_attribute, 'mp', 20), (minus_attribute, 'coin', 20)]
131
+ },
132
+ {
133
+ PHYSICAL_ATTACK: 9,
134
+ MAGICAL_ATTACK: 9,
135
+ LEAVE: 9
136
+ },
137
+ default = 9
138
+ ),
139
+ Node(
140
+ "Dragon Chamber",
141
+ "The ancient dragon wakes. Its eyes glow in the darkness. 'Another adventurer seeking my treasure?' What will you do?\n\nAttack and Rescue, or Run away?",
142
+ ['pics/8.png'],
143
+ {
144
+ PHYSICAL_ATTACK: [(minus_attribute, "hp", 30), (add_attribute, 'coin', 50)],
145
+ MAGICAL_ATTACK: [(minus_attribute, "mp", 25), (add_attribute, 'coin', 50)],
146
+ LEAVE: [(minus_attribute, 'hp', 15), (minus_attribute, 'mp', 15), (minus_attribute, 'coin', 15)]
147
+ },
148
+ route=check_ending
149
+ ),
150
+ Node("Perfect Victory", "You defeat the dragon without losing yourself. The dungeon recognizes you as the true successor.", ['pics/E1.png']),
151
+ Node("Exhausted Victory", "You defeat the dragon, but the battle drains every last bit of your power.", ['pics/E2.png']),
152
+ Node("You Survive", "You barely survive, but you lost everything", ['pics/E3.png']),
153
+ Node("Tyrant Ending", "Player seizes the dragon's power. Becomes the next monster.", ['pics/E4.png']),
154
+ Node("Game Over", "You have lost everything and this game", ['pics/E5.png'])
155
+ ]
utils.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re, json
2
+ from player import Player
3
+
4
+ PHYSICAL_ATTACK = 'PHYSICAL_ATTACK'
5
+ MAGICAL_ATTACK = 'MAGICAL_ATTACK'
6
+ SPEND_MONEY = "SPEND_MONEY"
7
+ LEAVE = "LEAVE"
8
+
9
+ PHYSICAL_REFILL = "PHYSICAL_REFILL"
10
+ MAGICAL_REFILL = 'MAGICAL_REFILL'
11
+ EARN_MONEY = "EARN_MONEY"
12
+
13
+ PROMPT = """
14
+ You have to identify the category that the primary action described in the response belongs to.
15
+
16
+ Here are the strict categories:
17
+ - PHYSICAL_ATTACK: Any action that causes or threatens physical damage, like stabbing, punching, firing guns, or weapon coercion.
18
+ - MAGICAL_ATTACK: Any action that causes damage with spells or magic, like summoning a monster, striking a target with a flood or thunder.
19
+ - SPEND_MONEY: Using money/coins to solve a problem, like bribing an NPC or buying an item.
20
+ - PHYSICAL_REFILL: Any action that refills HP, offers medical aid, rests, or performs supportive/altruistic deeds (e.g., helping an injured NPC, patching wounds, resting) without spending coins or items.
21
+ - MAGICAL_REFILL: Any action that refills MP, meditates, or channels spiritual energy without spending coins or items.
22
+ - EARN_MONEY: Any action focused on earning money (like looting gold, selling items, or doing a job), provided it does not involve a PHYSICAL_ATTACK or MAGICAL_ATTACK.
23
+ - LEAVE: Running away, retreating, doing absolutely nothing, or ignoring the situation entirely.
24
+
25
+ The input format will be [RESPONSE]: "HUMAN RESPONSE"
26
+ You must output your answer strictly in the following JSON format:
27
+ {"FINAL ANSWER": "YOUR_ANSWER"}
28
+
29
+ Remember: Identify ONLY the single most dominant primary action. If the action is purely a non-violent, helpful, or restorative interaction, classify it as PHYSICAL_REFILL. If the response is completely empty or completely irrelevant to the game, output LEAVE.
30
+
31
+ ---
32
+ EXAMPLES:
33
+
34
+ Input: [RESPONSE]: "I will bandage his wounds and help him stand up."
35
+ Output: {"FINAL ANSWER": "PHYSICAL_REFILL"}
36
+
37
+ Input: [RESPONSE]: "I see an injured traveler on the road. I will help him."
38
+ Output: {"FINAL ANSWER": "PHYSICAL_REFILL"}
39
+
40
+ Input: [RESPONSE]: "I draw my sword to threaten the shopkeeper, then summon lightning to strike him."
41
+ Output: {"FINAL ANSWER": "MAGICAL_ATTACK"}
42
+
43
+ Input: [RESPONSE]: "I turn around and walk away back into the forest."
44
+ Output: {"FINAL ANSWER": "LEAVE"}
45
+ """
46
+
47
+ INPUT_PROMPT = '\n[RESPONSE]: {}'
48
+
49
+ def add_attribute(player: Player, attribute: str, amount: int):
50
+ attribute = attribute.lower()
51
+ if attribute == 'hp':
52
+ player.add_hp(amount)
53
+ elif attribute == 'mp':
54
+ player.add_mp(amount)
55
+ elif attribute == 'coin':
56
+ player.add_coin(amount)
57
+
58
+ def minus_attribute(player: Player, attribute: str, amount: int):
59
+ attribute = attribute.lower()
60
+ if attribute == 'hp':
61
+ player.reduce_hp(amount)
62
+ elif attribute == 'mp':
63
+ player.reduce_mp(amount)
64
+ elif attribute == 'coin':
65
+ player.reduce_coin(amount)
66
+
67
+ def check_ending(player: Player):
68
+ print('Ending', player.hp, player.mp)
69
+ if player.hp >= 50 and player.mp >= 20:
70
+ return 10
71
+ elif player.hp > 0 and player.mp > 0:
72
+ if player.coin <= 10:
73
+ return 12
74
+ else:
75
+ return 11
76
+ elif player.mp < 0:
77
+ return 13
78
+ else:
79
+ return 13
80
+
81
+ def extract_after_final_answer(text: str) -> str:
82
+ # re.findall finds every instance of { ... }
83
+ # [^}]* ensures we don't accidentally skip over nested structures
84
+ matches = re.findall(r'(\{.*?\})', text, re.DOTALL)
85
+ if not matches:
86
+ return None
87
+
88
+ # We take the last match [-1]
89
+ last_json_str = matches[-1]
90
+
91
+ try:
92
+ # Replace single quotes with double quotes for valid JSON
93
+ valid_json_str = last_json_str.replace("'", '"')
94
+ data = json.loads(valid_json_str)
95
+ return data.get("FINAL ANSWER", "LEAVE")
96
+ except json.JSONDecodeError:
97
+ print(f'Could not fetch the keywords: "FINAL ANSWER" with the given response: {text}')
98
+ return "LEAVE"