Spaces:
Running
Running
Commit ·
795e0f4
1
Parent(s): d5e201f
Update to pixeltable 0.7.8 / gradio 5.49: UDFs to udfs.py, pinned deps, python 3.11
Browse files
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: ⚔️
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
|
@@ -23,7 +23,7 @@ An interactive text-based RPG powered by Pixeltable and OpenAI that creates dyna
|
|
| 23 |
- Real-time story generation
|
| 24 |
|
| 25 |
## 🛠️ Requirements
|
| 26 |
-
- Python 3.
|
| 27 |
- Pixeltable
|
| 28 |
- OpenAI API key
|
| 29 |
- Gradio
|
|
@@ -50,4 +50,4 @@ Scenario: Your smuggling ship is docked at a secret trading post in the Andromed
|
|
| 50 |
```
|
| 51 |
|
| 52 |
## 📝 Credits
|
| 53 |
-
Built with [Pixeltable](https://github.com/pixeltable/pixeltable) | Licensed under Apache 2.0
|
|
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.49.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
|
|
|
| 23 |
- Real-time story generation
|
| 24 |
|
| 25 |
## 🛠️ Requirements
|
| 26 |
+
- Python 3.11+
|
| 27 |
- Pixeltable
|
| 28 |
- OpenAI API key
|
| 29 |
- Gradio
|
|
|
|
| 50 |
```
|
| 51 |
|
| 52 |
## 📝 Credits
|
| 53 |
+
Built with [Pixeltable](https://github.com/pixeltable/pixeltable) | [Docs](https://docs.pixeltable.com) | Licensed under Apache 2.0
|
app.py
CHANGED
|
@@ -1,70 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
# Initialize Pixeltable
|
| 16 |
-
pxt.drop_dir('ai_rpg', force=True)
|
| 17 |
pxt.create_dir('ai_rpg')
|
| 18 |
|
| 19 |
-
@pxt.udf
|
| 20 |
-
def generate_messages(genre: str, player_name: str, initial_scenario: str, player_input: str, turn_number: int) -> list[dict]:
|
| 21 |
-
return [
|
| 22 |
-
{
|
| 23 |
-
'role': 'system',
|
| 24 |
-
'content': f"""You are the game master for a {genre} RPG. The player's name is {player_name}.
|
| 25 |
-
Provide your response in two clearly separated sections using exactly this format:
|
| 26 |
-
|
| 27 |
-
STORY: [Your engaging narrative response to the player's action]
|
| 28 |
-
|
| 29 |
-
OPTIONS:
|
| 30 |
-
1. [A dialogue option]
|
| 31 |
-
2. [A random action they could take]
|
| 32 |
-
3. [A unique or unexpected choice]"""
|
| 33 |
-
},
|
| 34 |
-
{
|
| 35 |
-
'role': 'user',
|
| 36 |
-
'content': f"Current scenario: {initial_scenario}\n"
|
| 37 |
-
f"Player's action: {player_input}\n"
|
| 38 |
-
f"Turn number: {turn_number}\n\n"
|
| 39 |
-
"Provide the story response and options:"
|
| 40 |
-
}
|
| 41 |
-
]
|
| 42 |
-
|
| 43 |
-
@pxt.udf
|
| 44 |
-
def get_story(response: str) -> str:
|
| 45 |
-
"""Extract just the story part from the response"""
|
| 46 |
-
parts = response.split("OPTIONS:")
|
| 47 |
-
if len(parts) != 2:
|
| 48 |
-
return response
|
| 49 |
-
|
| 50 |
-
story = parts[0].replace("STORY:", "").strip()
|
| 51 |
-
return story
|
| 52 |
-
|
| 53 |
-
@pxt.udf
|
| 54 |
-
def get_options(response: str) -> list[str]:
|
| 55 |
-
"""Extract the options from the response"""
|
| 56 |
-
parts = response.split("OPTIONS:")
|
| 57 |
-
if len(parts) != 2:
|
| 58 |
-
return ["Continue...", "Take another action", "Try something else"]
|
| 59 |
-
|
| 60 |
-
options = re.findall(r'\d\.\s*(.*?)(?=\d\.|$)', parts[1], re.DOTALL)
|
| 61 |
-
options = [opt.strip() for opt in options[:3]]
|
| 62 |
-
|
| 63 |
-
while len(options) < 3:
|
| 64 |
-
options.append("Take another action...")
|
| 65 |
-
|
| 66 |
-
return options
|
| 67 |
-
|
| 68 |
# Create a single table for all game data
|
| 69 |
interactions = pxt.create_table(
|
| 70 |
'ai_rpg.interactions',
|
|
@@ -91,8 +41,7 @@ interactions.add_computed_column(messages=generate_messages(
|
|
| 91 |
interactions.add_computed_column(ai_response=openai.chat_completions(
|
| 92 |
messages=interactions.messages,
|
| 93 |
model='gpt-4o-mini-2024-07-18',
|
| 94 |
-
|
| 95 |
-
temperature=0.8
|
| 96 |
))
|
| 97 |
|
| 98 |
interactions.add_computed_column(full_response=interactions.ai_response.choices[0].message.content)
|
|
@@ -176,7 +125,13 @@ def create_interface():
|
|
| 176 |
</div>
|
| 177 |
"""
|
| 178 |
)
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
with gr.Row():
|
| 181 |
with gr.Column():
|
| 182 |
with gr.Accordion("🎯 What does it do?", open=False):
|
|
@@ -273,6 +228,8 @@ def create_interface():
|
|
| 273 |
)
|
| 274 |
|
| 275 |
def start_new_game(name, genre_choice, scenario_text):
|
|
|
|
|
|
|
| 276 |
if not name or not genre_choice or not scenario_text:
|
| 277 |
return "Please fill in all fields before starting.", [], []
|
| 278 |
|
|
@@ -299,6 +256,8 @@ def create_interface():
|
|
| 299 |
return f"Error starting game: {str(e)}", [], []
|
| 300 |
|
| 301 |
def process_player_action(action_choice):
|
|
|
|
|
|
|
| 302 |
try:
|
| 303 |
if not action_choice:
|
| 304 |
return "Please select an action to continue.", [], []
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
import gradio as gr
|
| 5 |
import pixeltable as pxt
|
|
|
|
|
|
|
|
|
|
| 6 |
from pixeltable.functions import openai
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
from udfs import generate_messages, get_options, get_story
|
| 9 |
+
|
| 10 |
+
# The OpenAI key arrives as the OPENAI_API_KEY env var (a secret on HF Spaces).
|
| 11 |
+
# Never prompt on stdin: there is no TTY, so getpass() would block startup forever.
|
| 12 |
+
has_api_key = bool(os.environ.get('OPENAI_API_KEY'))
|
| 13 |
|
| 14 |
# Initialize Pixeltable
|
| 15 |
+
pxt.drop_dir('ai_rpg', force=True, if_not_exists='ignore')
|
| 16 |
pxt.create_dir('ai_rpg')
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# Create a single table for all game data
|
| 19 |
interactions = pxt.create_table(
|
| 20 |
'ai_rpg.interactions',
|
|
|
|
| 41 |
interactions.add_computed_column(ai_response=openai.chat_completions(
|
| 42 |
messages=interactions.messages,
|
| 43 |
model='gpt-4o-mini-2024-07-18',
|
| 44 |
+
model_kwargs={'max_tokens': 500, 'temperature': 0.8}
|
|
|
|
| 45 |
))
|
| 46 |
|
| 47 |
interactions.add_computed_column(full_response=interactions.ai_response.choices[0].message.content)
|
|
|
|
| 125 |
</div>
|
| 126 |
"""
|
| 127 |
)
|
| 128 |
+
|
| 129 |
+
if not has_api_key:
|
| 130 |
+
gr.Markdown(
|
| 131 |
+
'> **`OPENAI_API_KEY` is not set.** '
|
| 132 |
+
'Add it in the Space Settings -> Secrets tab, then restart the Space.'
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
with gr.Row():
|
| 136 |
with gr.Column():
|
| 137 |
with gr.Accordion("🎯 What does it do?", open=False):
|
|
|
|
| 228 |
)
|
| 229 |
|
| 230 |
def start_new_game(name, genre_choice, scenario_text):
|
| 231 |
+
if not has_api_key:
|
| 232 |
+
return 'OPENAI_API_KEY is not set. Add it in Settings -> Secrets and restart the Space.', [], []
|
| 233 |
if not name or not genre_choice or not scenario_text:
|
| 234 |
return "Please fill in all fields before starting.", [], []
|
| 235 |
|
|
|
|
| 256 |
return f"Error starting game: {str(e)}", [], []
|
| 257 |
|
| 258 |
def process_player_action(action_choice):
|
| 259 |
+
if not has_api_key:
|
| 260 |
+
return 'OPENAI_API_KEY is not set. Add it in Settings -> Secrets and restart the Space.', [], []
|
| 261 |
try:
|
| 262 |
if not action_choice:
|
| 263 |
return "Please select an action to continue.", [], []
|
requirements.txt
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
openai>=1.0.0
|
| 5 |
-
python-dotenv>=0.19.0
|
|
|
|
| 1 |
+
pixeltable==0.7.8
|
| 2 |
+
openai==3.16.2
|
| 3 |
+
pandas==2.3.3
|
|
|
|
|
|
udfs.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
import pixeltable as pxt
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# UDFs must live in a named module: pixeltable >= 0.6 rejects @pxt.udf defined in
|
| 7 |
+
# the __main__ script, and HF Spaces launches this app with `python app.py`.
|
| 8 |
+
@pxt.udf
|
| 9 |
+
def generate_messages(genre: str, player_name: str, initial_scenario: str, player_input: str, turn_number: int) -> list[dict]:
|
| 10 |
+
return [
|
| 11 |
+
{
|
| 12 |
+
'role': 'system',
|
| 13 |
+
'content': f"""You are the game master for a {genre} RPG. The player's name is {player_name}.
|
| 14 |
+
Provide your response in two clearly separated sections using exactly this format:
|
| 15 |
+
|
| 16 |
+
STORY: [Your engaging narrative response to the player's action]
|
| 17 |
+
|
| 18 |
+
OPTIONS:
|
| 19 |
+
1. [A dialogue option]
|
| 20 |
+
2. [A random action they could take]
|
| 21 |
+
3. [A unique or unexpected choice]"""
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
'role': 'user',
|
| 25 |
+
'content': f"Current scenario: {initial_scenario}\n"
|
| 26 |
+
f"Player's action: {player_input}\n"
|
| 27 |
+
f"Turn number: {turn_number}\n\n"
|
| 28 |
+
"Provide the story response and options:"
|
| 29 |
+
}
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@pxt.udf
|
| 34 |
+
def get_story(response: str) -> str:
|
| 35 |
+
"""Extract just the story part from the response"""
|
| 36 |
+
parts = response.split('OPTIONS:')
|
| 37 |
+
if len(parts) != 2:
|
| 38 |
+
return response
|
| 39 |
+
|
| 40 |
+
story = parts[0].replace('STORY:', '').strip()
|
| 41 |
+
return story
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@pxt.udf
|
| 45 |
+
def get_options(response: str) -> list[str]:
|
| 46 |
+
"""Extract the options from the response"""
|
| 47 |
+
parts = response.split('OPTIONS:')
|
| 48 |
+
if len(parts) != 2:
|
| 49 |
+
return ['Continue...', 'Take another action', 'Try something else']
|
| 50 |
+
|
| 51 |
+
options = re.findall(r'\d\.\s*(.*?)(?=\d\.|$)', parts[1], re.DOTALL)
|
| 52 |
+
options = [opt.strip() for opt in options[:3]]
|
| 53 |
+
|
| 54 |
+
while len(options) < 3:
|
| 55 |
+
options.append('Take another action...')
|
| 56 |
+
|
| 57 |
+
return options
|