|
import streamlit as st |
|
from langchain import PromptTemplate |
|
from typing import TypedDict, List, Dict, Optional |
|
from langchain.graphs import StateGraph |
|
from dataclasses import dataclass, field |
|
import random |
|
|
|
|
|
@dataclass |
|
class StoryState: |
|
current_step: int = 0 |
|
max_steps: int = 5 |
|
story_log: List[str] = field(default_factory=list) |
|
user_inputs: List[str] = field(default_factory=list) |
|
character1_responses: List[str] = field(default_factory=list) |
|
character2_responses: List[str] = field(default_factory=list) |
|
story_outcome: Optional[str] = None |
|
|
|
class MicroStory(TypedDict): |
|
title: str |
|
initial_setup: str |
|
character1_name: str |
|
character2_name: str |
|
steps: List[str] |
|
success_conditions: List[str] |
|
failure_conditions: List[str] |
|
|
|
from langchain.graphs import StateGraph |
|
from typing import Dict, List, Any |
|
from dataclasses import dataclass |
|
from enum import Enum |
|
|
|
class StoryNodeType(Enum): |
|
SETUP = "setup" |
|
USER_INPUT = "user_input" |
|
CHARACTER1_RESPONSE = "character1_response" |
|
CHARACTER2_RESPONSE = "character2_response" |
|
EVALUATION = "evaluation" |
|
|
|
@dataclass |
|
class StoryGraphState: |
|
current_node: StoryNodeType |
|
story_data: Dict[str, Any] |
|
accumulated_context: List[Dict[str, str]] |
|
step_count: int = 0 |
|
|
|
def create_story_graph() -> StateGraph: |
|
"""Creates the state graph for story progression""" |
|
graph = StateGraph() |
|
|
|
|
|
def setup_to_user_input(state: StoryGraphState) -> StoryGraphState: |
|
state.current_node = StoryNodeType.USER_INPUT |
|
return state |
|
|
|
def user_input_to_char1(state: StoryGraphState, user_input: str) -> StoryGraphState: |
|
state.current_node = StoryNodeType.CHARACTER1_RESPONSE |
|
state.accumulated_context.append({"role": "user", "content": user_input}) |
|
return state |
|
|
|
def char1_to_char2(state: StoryGraphState, char1_response: str) -> StoryGraphState: |
|
state.current_node = StoryNodeType.CHARACTER2_RESPONSE |
|
state.accumulated_context.append({"role": "character1", "content": char1_response}) |
|
return state |
|
|
|
def char2_to_evaluation(state: StoryGraphState, char2_response: str) -> StoryGraphState: |
|
state.current_node = StoryNodeType.EVALUATION |
|
state.accumulated_context.append({"role": "character2", "content": char2_response}) |
|
state.step_count += 1 |
|
return state |
|
|
|
def evaluation_to_next(state: StoryGraphState) -> StoryGraphState: |
|
if state.step_count >= 5: |
|
|
|
return state |
|
|
|
state.current_node = StoryNodeType.USER_INPUT |
|
return state |
|
|
|
|
|
graph.add_node("setup", setup_to_user_input) |
|
graph.add_node("user_input", user_input_to_char1) |
|
graph.add_node("character1_response", char1_to_char2) |
|
graph.add_node("character2_response", char2_to_evaluation) |
|
graph.add_node("evaluation", evaluation_to_next) |
|
|
|
|
|
graph.add_edge("setup", "user_input") |
|
graph.add_edge("user_input", "character1_response") |
|
graph.add_edge("character1_response", "character2_response") |
|
graph.add_edge("character2_response", "evaluation") |
|
graph.add_edge("evaluation", "user_input") |
|
|
|
return graph |
|
|
|
class StoryRunner: |
|
def __init__(self, story_data: Dict[str, Any]): |
|
self.graph = create_story_graph() |
|
self.state = StoryGraphState( |
|
current_node=StoryNodeType.SETUP, |
|
story_data=story_data, |
|
accumulated_context=[] |
|
) |
|
|
|
def process_user_input(self, user_input: str) -> Dict[str, Any]: |
|
"""Process user input and advance the story state""" |
|
if self.state.current_node != StoryNodeType.USER_INPUT: |
|
raise ValueError("Not ready for user input") |
|
|
|
|
|
self.state = self.graph.transition("user_input", self.state, user_input) |
|
self.state = self.graph.transition("character1_response", self.state, None) |
|
self.state = self.graph.transition("character2_response", self.state, None) |
|
self.state = self.graph.transition("evaluation", self.state, None) |
|
|
|
|
|
return { |
|
"step_count": self.state.step_count, |
|
"is_complete": self.state.step_count >= 5, |
|
"current_context": self.state.accumulated_context[-3:] if self.state.accumulated_context else [], |
|
"current_node": self.state.current_node.value |
|
} |
|
|
|
def get_full_context(self) -> List[Dict[str, str]]: |
|
"""Get the full conversation context""" |
|
return self.state.accumulated_context |
|
|
|
def is_complete(self) -> bool: |
|
"""Check if the story is complete""" |
|
return self.state.step_count >= 5 |
|
|
|
|
|
|
|
STORY_CATEGORIES = { |
|
"Mystery": [ |
|
{ |
|
"title": "The Library Mystery", |
|
"initial_setup": "In the ancient library of St. Bartholomew's, a rare manuscript has gone missing.", |
|
"character1_name": "Detective Nash", |
|
"character2_name": "Librarian Wells", |
|
"steps": [ |
|
"You notice strange symbols carved into the reading desk", |
|
"A student mentions seeing someone in medieval clothing", |
|
"The manuscript tracking system shows impossible timestamps", |
|
"Temperature drops significantly in the rare books section", |
|
"You find a hidden door behind the card catalog" |
|
], |
|
"success_conditions": [ |
|
"mentioned checking the security cameras", |
|
"investigated the symbols", |
|
"questioned the student further", |
|
"connected medieval sighting with timestamps" |
|
], |
|
"failure_conditions": [ |
|
"accused the librarian", |
|
"ignored the symbols", |
|
"left the library", |
|
"called the police immediately" |
|
] |
|
}, |
|
{ |
|
"title": "The Digital Deception", |
|
"initial_setup": "A tech startup's revolutionary AI algorithm has been stolen right before a major demo.", |
|
"character1_name": "Cyber Detective Chen", |
|
"character2_name": "System Admin Rodriguez", |
|
"steps": [ |
|
"The server logs show multiple failed login attempts", |
|
"An employee reports receiving a suspicious email", |
|
"The backup system was manually disabled", |
|
"Strange network traffic appears during off-hours", |
|
"A hidden backdoor program is discovered" |
|
], |
|
"success_conditions": [ |
|
"checked email headers", |
|
"analyzed network logs", |
|
"investigated backup system", |
|
"traced the backdoor" |
|
], |
|
"failure_conditions": [ |
|
"restored from backup immediately", |
|
"ignored the suspicious email", |
|
"reset all passwords without investigation", |
|
"blamed the system admin" |
|
] |
|
} |
|
], |
|
"Adventure": [ |
|
{ |
|
"title": "The Lost Temple", |
|
"initial_setup": "Deep in the Amazon rainforest, you've discovered the entrance to an ancient temple.", |
|
"character1_name": "Dr. Rivera", |
|
"character2_name": "Guide Santos", |
|
"steps": [ |
|
"Ancient markings warn of a curse", |
|
"You find a mechanism with multiple levers", |
|
"A strange humming sound emanates from deeper within", |
|
"The floor tiles show a peculiar pattern", |
|
"A beam of light reveals a hidden chamber" |
|
], |
|
"success_conditions": [ |
|
"documented the markings", |
|
"observed the pattern", |
|
"tested the mechanism carefully", |
|
"followed the light beam" |
|
], |
|
"failure_conditions": [ |
|
"ignored the warnings", |
|
"pulled levers randomly", |
|
"split up the group", |
|
"took artifacts without examination" |
|
] |
|
} |
|
], |
|
"Sci-Fi": [ |
|
{ |
|
"title": "The Quantum Anomaly", |
|
"initial_setup": "At a cutting-edge research facility, a quantum experiment has created an unexplained phenomenon.", |
|
"character1_name": "Dr. Zhang", |
|
"character2_name": "Engineer Parker", |
|
"steps": [ |
|
"Quantum readings are off the charts", |
|
"Equipment starts behaving erratically", |
|
"A shimmer appears in the air", |
|
"Time seems to flow differently near the anomaly", |
|
"Multiple reality signatures detected" |
|
], |
|
"success_conditions": [ |
|
"monitored quantum fluctuations", |
|
"calibrated equipment", |
|
"documented time discrepancies", |
|
"maintained safe distance" |
|
], |
|
"failure_conditions": [ |
|
"shut down power immediately", |
|
"entered the anomaly", |
|
"ignored safety protocols", |
|
"attempted to contain without data" |
|
] |
|
} |
|
] |
|
} |
|
|
|
|
|
STORY_LOOKUP = { |
|
story["title"]: story |
|
for category in STORY_CATEGORIES.values() |
|
for story in category |
|
} |
|
|
|
|
|
CHARACTER1_TEMPLATE = """ |
|
Context: You are {character1_name} in this story. |
|
Story Progress: {story_log} |
|
User's Latest Action: {user_input} |
|
|
|
Respond to the user's action in character, considering: |
|
1. Your role and personality |
|
2. The current story situation |
|
3. The potential consequences of their action |
|
|
|
Response: |
|
""" |
|
|
|
CHARACTER2_TEMPLATE = """ |
|
Context: You are {character2_name} in this story. |
|
Story Progress: {story_log} |
|
User's Latest Action: {user_input} |
|
Other Character's Response: {character1_response} |
|
|
|
Respond to both the user and {character1_name}, considering: |
|
1. Your role and personality |
|
2. The current story developments |
|
3. Your relationship with {character1_name} |
|
4. The potential impact on the story's outcome |
|
|
|
Response: |
|
""" |
|
|
|
def initialize_session_state(): |
|
if 'story_state' not in st.session_state: |
|
st.session_state.story_state = StoryState() |
|
if 'selected_category' not in st.session_state: |
|
st.session_state.selected_category = list(STORY_CATEGORIES.keys())[0] |
|
if 'current_story' not in st.session_state: |
|
|
|
st.session_state.current_story = random.choice(STORY_CATEGORIES[st.session_state.selected_category]) |
|
|
|
def evaluate_outcome(state: StoryState, story: MicroStory) -> str: |
|
user_actions = " ".join(state.user_inputs).lower() |
|
|
|
|
|
success_matches = sum(1 for cond in story["success_conditions"] if cond.lower() in user_actions) |
|
failure_matches = sum(1 for cond in story["failure_conditions"] if cond.lower() in user_actions) |
|
|
|
|
|
success_ratio = success_matches / len(story["success_conditions"]) |
|
|
|
if failure_matches >= 2: |
|
return "The story ends in failure. Critical mistakes were made." |
|
elif success_ratio >= 0.7: |
|
return "The story concludes successfully! Well done!" |
|
else: |
|
return "The story ends with mixed results. Some opportunities were missed." |
|
|
|
def update_story_state(state: StoryState, user_input: str, char1_response: str, char2_response: str): |
|
state.current_step += 1 |
|
state.user_inputs.append(user_input) |
|
state.character1_responses.append(char1_response) |
|
state.character2_responses.append(char2_response) |
|
|
|
|
|
if state.current_step >= state.max_steps: |
|
state.story_outcome = evaluate_outcome(state, st.session_state.current_story) |
|
|
|
def generate_character_response( |
|
character_name: str, |
|
story_log: List[str], |
|
user_input: str, |
|
other_response: Optional[str] = None, |
|
is_character1: bool = True |
|
) -> str: |
|
|
|
|
|
if is_character1: |
|
return f"{character_name}: That's an interesting approach. Let's see where this leads..." |
|
else: |
|
return f"{character_name}: I have my doubts about this, but we'll see..." |
|
|
|
def main(): |
|
st.set_page_config(page_title="Interactive Story", layout="wide") |
|
initialize_session_state() |
|
|
|
|
|
st.sidebar.header('Story Selection') |
|
st.sidebar.divider() |
|
|
|
|
|
selected_category = st.sidebar.selectbox( |
|
"Select Story Category", |
|
list(STORY_CATEGORIES.keys()), |
|
index=list(STORY_CATEGORIES.keys()).index(st.session_state.selected_category) |
|
) |
|
|
|
|
|
if selected_category != st.session_state.selected_category: |
|
st.session_state.selected_category = selected_category |
|
st.session_state.current_story = random.choice(STORY_CATEGORIES[selected_category]) |
|
st.session_state.story_state = StoryState() |
|
st.rerun() |
|
|
|
|
|
with st.sidebar.expander(f"Available {selected_category} Stories"): |
|
for story in STORY_CATEGORIES[selected_category]: |
|
st.write(f"📖 {story['title']}") |
|
|
|
|
|
specific_story = st.sidebar.selectbox( |
|
"Select Specific Story", |
|
[story["title"] for story in STORY_CATEGORIES[selected_category]], |
|
index=[story["title"] for story in STORY_CATEGORIES[selected_category]].index(st.session_state.current_story["title"]) |
|
) |
|
|
|
|
|
if specific_story != st.session_state.current_story["title"]: |
|
st.session_state.current_story = STORY_LOOKUP[specific_story] |
|
st.session_state.story_state = StoryState() |
|
st.rerun() |
|
|
|
|
|
st.sidebar.divider() |
|
st.sidebar.subheader("Story Progress") |
|
progress = (st.session_state.story_state.current_step / 5) * 100 |
|
st.sidebar.progress(progress) |
|
st.sidebar.write(f"Step {st.session_state.story_state.current_step + 1}/5") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
st.header("Story Progress") |
|
st.write(f"**{st.session_state.current_story['title']}**") |
|
st.write(st.session_state.current_story['initial_setup']) |
|
|
|
|
|
for step_num, (step, user_input) in enumerate(zip( |
|
st.session_state.current_story['steps'][:st.session_state.story_state.current_step], |
|
st.session_state.story_state.user_inputs |
|
)): |
|
st.write(f"Step {step_num + 1}: {step}") |
|
st.write(f"Your action: {user_input}") |
|
st.write("---") |
|
|
|
|
|
if st.session_state.story_state.story_outcome is None: |
|
current_step = st.session_state.current_story['steps'][st.session_state.story_state.current_step] |
|
st.write(f"Current Situation: {current_step}") |
|
|
|
|
|
with col2: |
|
st.header(st.session_state.current_story['character1_name']) |
|
for response in st.session_state.story_state.character1_responses: |
|
st.write(response) |
|
|
|
|
|
with col3: |
|
st.header(st.session_state.current_story['character2_name']) |
|
for response in st.session_state.story_state.character2_responses: |
|
st.write(response) |
|
|
|
|
|
if st.session_state.story_state.story_outcome is None: |
|
user_input = st.text_input( |
|
"What do you do?", |
|
key=f"user_input_{st.session_state.story_state.current_step}" |
|
) |
|
|
|
if user_input: |
|
|
|
char1_response = generate_character_response( |
|
st.session_state.current_story['character1_name'], |
|
st.session_state.story_state.story_log, |
|
user_input |
|
) |
|
|
|
char2_response = generate_character_response( |
|
st.session_state.current_story['character2_name'], |
|
st.session_state.story_state.story_log, |
|
user_input, |
|
char1_response, |
|
False |
|
) |
|
|
|
|
|
update_story_state(st.session_state.story_state, user_input, char1_response, char2_response) |
|
st.rerun() |
|
else: |
|
|
|
st.write(st.session_state.story_state.story_outcome) |
|
if st.button("Start New Story"): |
|
st.session_state.story_state = StoryState() |
|
st.session_state.current_story = random.choice(MICRO_STORIES) |
|
st.rerun() |
|
|
|
if __name__ == "__main__": |
|
main() |