Spaces:
Running
Running
File size: 8,352 Bytes
2e277ca 4656d22 1c9a1a6 6294c0d 2e277ca 4656d22 2e277ca 4000e70 2e277ca 4656d22 1fd0bdd 2e277ca 4656d22 2e277ca 4656d22 2e277ca 1b005cd 14ea97e 2e277ca 1b005cd 2e277ca 14ea97e 1fd0bdd 2e277ca 0913109 32570a4 ea32eab 2e277ca ea32eab 32570a4 14ea97e 2b8aa30 32570a4 0913109 14ea97e 32570a4 1b005cd 2e277ca 32570a4 0913109 1b005cd df68cdc 14ea97e 0913109 df68cdc 0913109 2e277ca 1927a5d 2e277ca 0913109 78c1caf 2e277ca 78c1caf 2e277ca 0913109 2e277ca 78c1caf 2e277ca 78c1caf 2e277ca |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
import os
import asyncio
import textwrap
import re
import httpx
import gradio as gr
from PIL import Image
from io import BytesIO
from llama_index.core.workflow import (
Workflow,
step,
StartEvent,
StopEvent,
Context,
Event,
)
from llama_index.llms.groq import Groq
# --- Secrets ---
GROQ_API_KEY = os.environ.get("Groq_Token")
HF_TOKEN = os.environ.get("HF_TOKEN2")
# --- Event and Workflow Definitions ---
class StoryContext(Event):
story_part: str
inventory: list[str]
class SceneReadyEvent(Event):
pass
class UserChoice(Event):
choice: str
class StoryEnd(Event):
final_message: str
# Helper function to generate an image and return its path
async def generate_image(prompt: str, hf_token: str) -> str | None:
API_URL = "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell"
headers = {"Authorization": f"Bearer {hf_token}"}
full_prompt = f"epic fantasy art, digital painting, cinematic lighting, masterpiece, {prompt}"
payload = {"inputs": full_prompt}
try:
async with httpx.AsyncClient() as client:
response = await client.post(API_URL, headers=headers, json=payload, timeout=180.0)
response.raise_for_status()
image = Image.open(BytesIO(response.content))
# Save to a temporary file that Gradio can serve
image.save("scene_image.png")
return "scene_image.png"
except (httpx.TimeoutException, httpx.RequestError, IOError) as e:
print(f"Image generation failed: {e}")
return None
# The full workflow class
class StorytellerWorkflow(Workflow):
def __init__(self, **kwargs):
super().__init__(timeout=300, **kwargs)
@step
async def generate_story_part(self, ev: StartEvent | UserChoice, ctx: Context) -> StoryContext | StoryEnd:
inventory = await ctx.store.get("inventory", [])
prompt = ""
if isinstance(ev, StartEvent):
# The prompt for the first turn
prompt = """
You are a creative text adventure game master. Your output is for a console game.
Start a new story about a curious explorer entering a recently discovered, glowing cave.
Keep the tone mysterious and exciting. After the story part, provide two distinct choices for the player to make.
Format your response exactly like this: STORY: [The story text goes here] CHOICES: 1. [First choice] 2. [Second choice]
"""
elif isinstance(ev, UserChoice):
last_story_part = await ctx.store.get("last_story_part")
# --- EXPLICIT PROMPT ---
prompt = f"""
You are a creative text adventure game master.
The story so far: "{last_story_part}"
The player chose: "{ev.choice}"
The player's inventory: {inventory}
Continue the story.
If a choice results in an item, use `[ADD_ITEM: item name]`. If the story should end, write "[END]".
Format your response exactly like this:
STORY:
[The story text goes here]
CHOICES:
1. [First choice on its own line]
2. [Second choice on its own line]
"""
llm = Groq(model="llama3-8b-8192", api_key=GROQ_API_KEY)
response = await llm.acomplete(prompt)
response_text = str(response)
items_found = re.findall(r"\[ADD_ITEM: (.*?)\]", response_text)
if items_found:
for item in items_found:
if item not in inventory:
inventory.append(item)
response_text = re.sub(r"\[ADD_ITEM: (.*?)\]", "", response_text).strip()
if response_text.strip().startswith("[END]"):
final_message = response_text.strip().replace("[END]", "")
return StoryEnd(final_message=f"\n--- THE END ---\n{final_message}")
try:
story_section = response_text.split("STORY:")[1].split("CHOICES:")[0].strip()
choices_section = response_text.split("CHOICES:")[1].strip()
full_story_part = f"{story_section}\n\nChoices:\n{choices_section}"
except IndexError:
full_story_part = "The story continues... but the path is blurry."
await ctx.store.set("last_story_part", full_story_part)
await ctx.store.set("inventory", inventory)
# Return the simplified event, without the is_new_scene flag
return StoryContext(story_part=full_story_part, inventory=inventory)
@step
def end_story(self, ev: StoryEnd) -> StopEvent:
"""This step satisfies the workflow validator by providing a path to a StopEvent."""
return StopEvent(result=ev.final_message)
# These two steps are no longer needed, as Gradio's UI will handle the display logic.
# @step async def display_scene(...)
# @step async def get_user_choice(...)
# --- Gradio UI and Application Logic ---
async def run_turn(user_input, game_state):
# This function no longer needs the component objects passed in.
if game_state is None:
game_state = {'inventory': [], 'last_story_part': None}
event = StartEvent()
else:
event = UserChoice(choice=user_input)
workflow = StorytellerWorkflow()
ctx = Context(workflow=workflow)
await ctx.store.set("inventory", game_state['inventory'])
await ctx.store.set("last_story_part", game_state['last_story_part'])
result_event = await workflow.generate_story_part(event, ctx)
if isinstance(result_event, StoryEnd):
yield (None, result_event.final_message, "", None)
return
if isinstance(result_event, StoryContext):
narrative, choices_text = result_event.story_part.split("Choices:", 1)
# --- THIS IS THE NEW DEFENSIVE CODE ---
# It finds any choice number (like " 2." or " 3.") that has a space before it
# and replaces that space with a newline character. This forces them to be on separate lines.
choices_text = re.sub(r" (\d\.)", r"\n\1", choices_text)
story_display_text = f"{textwrap.fill(narrative, width=80)}\n\nChoices:{choices_text}"
new_game_state = {
'inventory': result_event.inventory,
'last_story_part': result_event.story_part
}
inventory_text = f"**Inventory:** {', '.join(new_game_state['inventory']) if new_game_state['inventory'] else 'Empty'}"
yield (None, story_display_text, inventory_text, new_game_state)
# 2. Generate the image.
image_path = None
if HF_TOKEN:
image_path = await generate_image(narrative, HF_TOKEN)
# 3. Yield a second, complete tuple with the new image path.
yield (image_path, story_display_text, inventory_text, new_game_state)
def create_demo():
with gr.Blocks(theme=gr.themes.Soft()) as demo:
game_state = gr.State(None)
gr.Markdown("# LlamaIndex Workflow: Dynamic Storyteller")
gr.Markdown("An AI-powered text adventure game where every scene can be illustrated by AI.")
with gr.Row():
with gr.Column(scale=1):
image_display = gr.Image(label="Scene", interactive=False)
inventory_display = gr.Markdown("**Inventory:** Empty")
with gr.Column(scale=2):
story_display = gr.Textbox(label="Story", lines=15, interactive=False)
user_input = gr.Textbox(label="What do you do?", placeholder="Type your choice and press Enter...")
# The inputs and outputs lists should be simple.
inputs = [user_input, game_state]
outputs = [image_display, story_display, inventory_display, game_state]
user_input.submit(
fn=run_turn,
inputs=inputs,
outputs=outputs
)
load_inputs = [gr.State(None), game_state]
demo.load(
fn=run_turn,
inputs=load_inputs,
outputs=outputs
)
return demo
if __name__ == "__main__":
if not GROQ_API_KEY or not HF_TOKEN:
print("ERROR: API keys not found. Make sure to set GROQ_API_KEY and HF_TOKEN in your Hugging Face Space Secrets.")
else:
app = create_demo()
app.launch() |