PierreBrunelle commited on
Commit
ad043e2
·
verified ·
1 Parent(s): 2f51cfb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +387 -1
app.py CHANGED
@@ -1,4 +1,390 @@
1
- from interface.app import create_interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  if __name__ == "__main__":
4
  demo = create_interface()
 
1
+ import gradio as gr
2
+ import pixeltable as pxt
3
+ import numpy as np
4
+ from datetime import datetime
5
+ from pixeltable.functions.huggingface import sentence_transformer
6
+ from pixeltable.functions import openai
7
+ import os
8
+ import getpass
9
+ import re
10
+
11
+ # Set up OpenAI API key
12
+ if 'OPENAI_API_KEY' not in os.environ:
13
+ os.environ['OPENAI_API_KEY'] = getpass.getpass('Enter your OpenAI API key: ')
14
+
15
+ # Initialize Pixeltable
16
+ pxt.drop_dir('ai_rpg', force=True)
17
+ pxt.create_dir('ai_rpg')
18
+
19
+ # Create a UDF for generating messages
20
+ @pxt.udf
21
+ def generate_messages(genre: str, player_name: str, initial_scenario: str, player_input: str, turn_number: int) -> list[dict]:
22
+ return [
23
+ {
24
+ 'role': 'system',
25
+ 'content': f"""You are the game master for a {genre} RPG. The player's name is {player_name}.
26
+ Provide an engaging response to the player's action and present exactly 3 numbered options for their next move:
27
+ 1. A dialogue option (saying something)
28
+ 2. A random action they could take
29
+ 3. A unique or unexpected choice
30
+
31
+ Format each option with a number (1., 2., 3.) followed by the action description."""
32
+ },
33
+ {
34
+ 'role': 'user',
35
+ 'content': f"Current scenario: {initial_scenario}\n"
36
+ f"Player's action: {player_input}\n"
37
+ f"Turn number: {turn_number}\n\n"
38
+ "Provide a response and 3 options:"
39
+ }
40
+ ]
41
+
42
+ @pxt.udf
43
+ def extract_options(story_text: str) -> list[str]:
44
+ """Extract the three options from the story text"""
45
+ # Look for numbered options (1., 2., 3.) and grab the text after them
46
+ options = re.findall(r'\d\.\s*(.*?)(?=\d\.|$)', story_text, re.DOTALL)
47
+ # Clean up the options and ensure we have exactly 3
48
+ options = [opt.strip() for opt in options[:3]]
49
+ while len(options) < 3:
50
+ options.append("Take another action...")
51
+ return options
52
+
53
+ # Create a single table for all game data
54
+ interactions = pxt.create_table(
55
+ 'ai_rpg.interactions',
56
+ {
57
+ 'session_id': pxt.StringType(),
58
+ 'player_name': pxt.StringType(),
59
+ 'genre': pxt.StringType(),
60
+ 'initial_scenario': pxt.StringType(),
61
+ 'turn_number': pxt.IntType(),
62
+ 'player_input': pxt.StringType(),
63
+ 'timestamp': pxt.TimestampType(),
64
+ }
65
+ )
66
+
67
+ # Add computed columns for AI responses
68
+ interactions['messages'] = generate_messages(
69
+ interactions.genre,
70
+ interactions.player_name,
71
+ interactions.initial_scenario,
72
+ interactions.player_input,
73
+ interactions.turn_number
74
+ )
75
+
76
+ interactions['ai_response'] = openai.chat_completions(
77
+ messages=interactions.messages,
78
+ model='gpt-4o-mini-2024-07-18',
79
+ max_tokens=500,
80
+ temperature=0.8
81
+ )
82
+
83
+ interactions['story_text'] = interactions.ai_response.choices[0].message.content
84
+ interactions['options'] = extract_options(interactions.story_text)
85
+
86
+ class RPGGame:
87
+ def __init__(self):
88
+ self.current_session_id = None
89
+ self.turn_number = 0
90
+
91
+ def start_game(self, player_name: str, genre: str, scenario: str) -> str:
92
+ session_id = f"session_{datetime.now().strftime('%Y%m%d%H%M%S')}_{player_name}"
93
+ self.current_session_id = session_id
94
+ self.turn_number = 0
95
+
96
+ # Create initial interaction with all session data
97
+ interactions.insert([{
98
+ 'session_id': session_id,
99
+ 'player_name': player_name,
100
+ 'genre': genre,
101
+ 'initial_scenario': scenario,
102
+ 'turn_number': 0,
103
+ 'player_input': "Game starts",
104
+ 'timestamp': datetime.now()
105
+ }])
106
+
107
+ # Get initial story and options
108
+ initial_response = interactions.select(interactions.story_text).where(
109
+ (interactions.session_id == session_id) &
110
+ (interactions.turn_number == 0)
111
+ ).collect()['story_text'][0]
112
+
113
+ return session_id, initial_response
114
+
115
+ def process_action(self, action: str) -> str:
116
+ if not self.current_session_id:
117
+ return "No active game session. Please start a new game."
118
+
119
+ self.turn_number += 1
120
+
121
+ # Get session info from previous turn
122
+ prev_turn = interactions.select(
123
+ interactions.player_name,
124
+ interactions.genre,
125
+ interactions.initial_scenario
126
+ ).where(
127
+ (interactions.session_id == self.current_session_id) &
128
+ (interactions.turn_number == 0)
129
+ ).collect()
130
+
131
+ # Record player action with session data
132
+ interactions.insert([{
133
+ 'session_id': self.current_session_id,
134
+ 'player_name': prev_turn['player_name'][0],
135
+ 'genre': prev_turn['genre'][0],
136
+ 'initial_scenario': prev_turn['initial_scenario'][0],
137
+ 'turn_number': self.turn_number,
138
+ 'player_input': action,
139
+ 'timestamp': datetime.now()
140
+ }])
141
+
142
+ # Get AI response
143
+ response = interactions.select(interactions.story_text).where(
144
+ (interactions.session_id == self.current_session_id) &
145
+ (interactions.turn_number == self.turn_number)
146
+ ).collect()['story_text'][0]
147
+
148
+ return response
149
+
150
+ def create_interface():
151
+ game = RPGGame()
152
+
153
+ with gr.Blocks(theme=gr.themes.Base()) as demo:
154
+ # Header with title and description
155
+ gr.Markdown(
156
+ """
157
+ <div style="margin-bottom: 20px;">
158
+ <h1 style="margin-bottom: 0.5em;">🎲 AI RPG Adventure</h1>
159
+ <p>An interactive RPG experience from Pixeltable and powered by OpenAI! Get started with an example below.</p>
160
+ </div>
161
+ """
162
+ )
163
+
164
+ # Side-by-side accordions
165
+ with gr.Row():
166
+ with gr.Column():
167
+ with gr.Accordion("🎯 What does it do?", open=False):
168
+ gr.Markdown("""
169
+ This AI RPG Adventure demonstrates Pixeltable's capabilities:
170
+
171
+ - 🎮 Creates dynamic, AI-driven interactive stories
172
+ - 🔄 Maintains game state and history using Pixeltable tables
173
+ - 💭 Generates contextual options based on player actions
174
+ - 📝 Processes natural language inputs for custom actions
175
+ - 🤖 Uses LLMs to create engaging narratives
176
+ - 📊 Tracks and displays game progression
177
+
178
+ Perfect for understanding how Pixeltable can manage complex,
179
+ stateful applications with AI integration! ✨
180
+ """)
181
+
182
+ with gr.Column():
183
+ with gr.Accordion("🛠️ How does it work?", open=False):
184
+ gr.Markdown("""
185
+ The app leverages several Pixeltable features:
186
+
187
+ 1. 📦 **Data Management**: Uses Pixeltable tables to store game state,
188
+ player actions, and AI responses
189
+
190
+ 2. 🤖 **AI Integration**: Seamlessly connects with language models
191
+ for story generation and response processing
192
+
193
+ 3. 🔄 **State Tracking**: Maintains session history and player
194
+ choices using Pixeltable's computed columns
195
+
196
+ 4. ⚙️ **Custom Processing**: Uses Pixeltable UDFs to handle game
197
+ logic and AI prompt generation
198
+
199
+ 5. 🎯 **Interactive Flow**: Processes player inputs and generates
200
+ contextual responses in real-time
201
+ """)
202
+
203
+ with gr.Row():
204
+ # Setup column
205
+ with gr.Column():
206
+ player_name = gr.Textbox(
207
+ label="👤 Your Character's Name",
208
+ placeholder="Enter your character's name..."
209
+ )
210
+ genre = gr.Dropdown(
211
+ choices=[
212
+ "🧙‍♂️ Fantasy",
213
+ "🚀 Sci-Fi",
214
+ "👻 Horror",
215
+ "🔍 Mystery",
216
+ "🌋 Post-Apocalyptic",
217
+ "🤖 Cyberpunk",
218
+ "⚙️ Steampunk"
219
+ ],
220
+ label="🎭 Choose Your Genre"
221
+ )
222
+ scenario = gr.Textbox(
223
+ label="📖 Starting Scenario",
224
+ lines=3,
225
+ placeholder="Describe the initial setting and situation..."
226
+ )
227
+ start_button = gr.Button("🎮 Begin Adventure", variant="primary")
228
+
229
+ # Game interaction column
230
+ with gr.Column():
231
+ story_display = gr.Textbox(
232
+ label="📜 Story",
233
+ lines=8,
234
+ interactive=False
235
+ )
236
+
237
+ gr.Markdown("### 🎯 Choose Your Action")
238
+
239
+ with gr.Row():
240
+ with gr.Column():
241
+ action_input = gr.Radio(
242
+ choices=[],
243
+ label="🎲 Select an action or write your own below:",
244
+ interactive=True
245
+ )
246
+ custom_action = gr.Textbox(
247
+ label="✨ Custom Action",
248
+ placeholder="Write your own action here...",
249
+ lines=2
250
+ )
251
+ submit_action = gr.Button("⚡ Take Action", variant="secondary")
252
+
253
+ # Example scenarios
254
+ gr.Markdown("### 💫 Example Adventures")
255
+ gr.Examples(
256
+ examples=[
257
+ ["Eldric", "🧙‍♂️ Fantasy", "You find yourself in an ancient forest clearing, standing before a mysterious glowing portal. Your journey begins..."],
258
+ ["Commander Nova", "🚀 Sci-Fi", "Aboard the starship Nebula, alarms blare as unknown entities approach. The fate of the crew rests in your hands..."],
259
+ ["Detective Blake", "🔍 Mystery", "In the fog-shrouded streets of Victorian London, a peculiar letter arrives at your doorstep..."],
260
+ ],
261
+ inputs=[player_name, genre, scenario]
262
+ )
263
+
264
+ # History display
265
+ history_df = gr.Dataframe(
266
+ headers=["📅 Turn", "🎯 Player Action", "💬 Game Response"],
267
+ label="📚 Adventure History",
268
+ wrap=True,
269
+ row_count=5,
270
+ col_count=(3, "fixed")
271
+ )
272
+ def start_new_game(name, genre_choice, scenario_text):
273
+ if not name or not genre_choice or not scenario_text:
274
+ return "Please fill in all fields before starting.", [], "", []
275
+
276
+ try:
277
+ _, initial_response = game.start_game(name, genre_choice, scenario_text)
278
+
279
+ # Get options from the initial response
280
+ options = interactions.select(interactions.options).where(
281
+ (interactions.session_id == game.current_session_id) &
282
+ (interactions.turn_number == 0)
283
+ ).collect()['options'][0]
284
+
285
+ # Get initial history
286
+ history_df = interactions.select(
287
+ turn=interactions.turn_number,
288
+ action=interactions.player_input,
289
+ response=interactions.story_text
290
+ ).where(
291
+ interactions.session_id == game.current_session_id
292
+ ).order_by(
293
+ interactions.turn_number
294
+ ).collect().to_pandas()
295
+
296
+ history_data = [
297
+ [str(row['turn']), row['action'], row['response']]
298
+ for _, row in history_df.iterrows()
299
+ ]
300
+
301
+ return initial_response, gr.Radio(choices=options, interactive=True), "", history_data
302
+ except Exception as e:
303
+ return f"Error starting game: {str(e)}", [], "", []
304
+
305
+ def process_player_action(action_choice, custom_action):
306
+ try:
307
+ # Use custom action if provided, otherwise use selected choice
308
+ action = custom_action if custom_action else action_choice
309
+ if not action:
310
+ return "Please either select an action or write your own.", [], "", []
311
+
312
+ response = game.process_action(action)
313
+
314
+ # Get new options
315
+ options = interactions.select(interactions.options).where(
316
+ (interactions.session_id == game.current_session_id) &
317
+ (interactions.turn_number == game.turn_number)
318
+ ).collect()['options'][0]
319
+
320
+ # Get updated history
321
+ history_df = interactions.select(
322
+ turn=interactions.turn_number,
323
+ action=interactions.player_input,
324
+ response=interactions.story_text
325
+ ).where(
326
+ interactions.session_id == game.current_session_id
327
+ ).order_by(
328
+ interactions.turn_number
329
+ ).collect().to_pandas()
330
+
331
+ history_data = [
332
+ [str(row['turn']), row['action'], row['response']]
333
+ for _, row in history_df.iterrows()
334
+ ]
335
+
336
+ return response, gr.Radio(choices=options, interactive=True), "", history_data
337
+ except Exception as e:
338
+ return f"Error: {str(e)}", [], "", []
339
+
340
+ # Connect the start button
341
+ start_button.click(
342
+ start_new_game,
343
+ inputs=[player_name, genre, scenario],
344
+ outputs=[story_display, action_input, custom_action, history_df]
345
+ )
346
+
347
+ # Single action submit button
348
+ submit_action.click(
349
+ process_player_action,
350
+ inputs=[action_input, custom_action],
351
+ outputs=[story_display, action_input, custom_action, history_df]
352
+ )
353
+
354
+ # Footer with links
355
+ gr.HTML(
356
+ """
357
+ <div style="margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #e5e7eb;">
358
+ <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
359
+ <div style="flex: 1;">
360
+ <h4 style="margin: 0; color: #374151;">🚀 Built with Pixeltable</h4>
361
+ <p style="margin: 0.5rem 0; color: #6b7280;">
362
+ Open Source AI Data infrastructure.
363
+ </p>
364
+ </div>
365
+ <div style="flex: 1;">
366
+ <h4 style="margin: 0; color: #374151;">🔗 Resources</h4>
367
+ <div style="display: flex; gap: 1.5rem; margin-top: 0.5rem;">
368
+ <a href="https://github.com/pixeltable/pixeltable" target="_blank" style="color: #4F46E5; text-decoration: none; display: flex; align-items: center; gap: 0.25rem;">
369
+ 💻 GitHub
370
+ </a>
371
+ <a href="https://docs.pixeltable.com" target="_blank" style="color: #4F46E5; text-decoration: none; display: flex; align-items: center; gap: 0.25rem;">
372
+ 📚 Documentation
373
+ </a>
374
+ <a href="https://huggingface.co/Pixeltable" target="_blank" style="color: #4F46E5; text-decoration: none; display: flex; align-items: center; gap: 0.25rem;">
375
+ 🤗 Hugging Face
376
+ </a>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ <p style="margin: 1rem 0 0; text-align: center; color: #9CA3AF; font-size: 0.875rem;">
381
+ This work is licensed under the Apache License 2.0. You can freely use, modify, and distribute this code, provided you include appropriate attribution and maintain the original license notice.
382
+ </p>
383
+ </div>
384
+ """
385
+ )
386
+
387
+ return demo
388
 
389
  if __name__ == "__main__":
390
  demo = create_interface()