File size: 17,208 Bytes
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
 
 
5249af8
11a4188
 
5249af8
11a4188
 
 
 
5249af8
11a4188
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
5249af8
 
11a4188
5249af8
 
11a4188
 
5249af8
 
11a4188
 
 
 
 
5249af8
 
11a4188
 
 
 
 
 
5249af8
11a4188
 
 
 
5249af8
11a4188
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
 
5249af8
11a4188
 
 
 
5249af8
11a4188
 
5249af8
11a4188
 
 
 
 
5249af8
11a4188
 
 
 
 
 
 
 
5249af8
11a4188
 
 
 
5249af8
11a4188
 
 
 
 
 
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
5249af8
11a4188
 
 
 
 
 
5249af8
 
11a4188
 
 
 
 
 
5249af8
 
11a4188
 
 
 
 
 
 
 
 
 
5249af8
 
 
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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
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

# Data Structures
@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()

    # Define state transitions
    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:
            # Story is complete, stay in evaluation
            return state
        # Move to next user input
        state.current_node = StoryNodeType.USER_INPUT
        return state

    # Add nodes and edges
    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)

    # Connect nodes
    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")

        # Advance through the graph
        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 current state info
        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

# Sample Stories Database
# Story Categories and Templates
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"
            ]
        }
    ]
}

# Flatten categories for easy access by title
STORY_LOOKUP = {
    story["title"]: story 
    for category in STORY_CATEGORIES.values() 
    for story in category
}

# Character Response Templates
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:
        # Select random story from current category
        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()
    
    # Count matches for success and failure conditions
    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)
    
    # Calculate success ratio
    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)
    
    # Check if story should end
    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:
    # This would normally use watsonx.ai or another LLM
    # For now, return placeholder responses
    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()
    
    # Sidebar Configuration
    st.sidebar.header('Story Selection')
    st.sidebar.divider()
    
    # Category Selection
    selected_category = st.sidebar.selectbox(
        "Select Story Category",
        list(STORY_CATEGORIES.keys()),
        index=list(STORY_CATEGORIES.keys()).index(st.session_state.selected_category)
    )
    
    # Update category and story if changed
    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()  # Reset state for new story
        st.rerun()
    
    # Display available stories in category
    with st.sidebar.expander(f"Available {selected_category} Stories"):
        for story in STORY_CATEGORIES[selected_category]:
            st.write(f"📖 {story['title']}")
    
    # Optional: Select specific story
    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"])
    )
    
    # Update if specific story changed
    if specific_story != st.session_state.current_story["title"]:
        st.session_state.current_story = STORY_LOOKUP[specific_story]
        st.session_state.story_state = StoryState()  # Reset state for new story
        st.rerun()
    
    # Display story stats
    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")
    
    # Create three columns
    col1, col2, col3 = st.columns(3)
    
    # Story Progress Column
    with col1:
        st.header("Story Progress")
        st.write(f"**{st.session_state.current_story['title']}**")
        st.write(st.session_state.current_story['initial_setup'])
        
        # Display story log
        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("---")
        
        # Display current step if story isn't finished
        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}")
    
    # Character 1 Column
    with col2:
        st.header(st.session_state.current_story['character1_name'])
        for response in st.session_state.story_state.character1_responses:
            st.write(response)
    
    # Character 2 Column
    with col3:
        st.header(st.session_state.current_story['character2_name'])
        for response in st.session_state.story_state.character2_responses:
            st.write(response)
    
    # User Input Section
    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:
            # Generate character responses
            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 state
            update_story_state(st.session_state.story_state, user_input, char1_response, char2_response)
            st.rerun()
    else:
        # Display story outcome
        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()