"""
state.py
Shared LangGraph state definitions for WalkXR. This file should stay generic
enough to support future agents, not just the Small Moments roleplay agent.
"""
from __future__ import annotations
from typing import Any, Dict, List, Literal, Optional, TypedDict
# Named shortcuts for common string literals
# ------------------------------------------
Role = Literal["system", "user", "assistant", "tool"]
Phase = Literal[
"input",
"memory_loaded",
"context_retrieved",
"prompt_constructed",
"llm_called",
"response_generated",
"history_updated",
"memory_updated",
"completed",
"error",
]
# Safe response in case LLM does not generate a user-friendly response
SAFE_RESPONSE = "Thank you for sharing your thoughts. Let me take a moment to process the feelings you have shared with me. Are there additional details you would like to discuss?"
# Message-level state
# -------------------
class MessageRecord(TypedDict):
"""
Message format used inside the graph state.
We use a dict-based structure instead of raw tuples so it can scale more
easily to:
- System messages
- Tool messages
- Timestamps / metadata
- Future multi-agent handoffs
Fields:
- role:
Who produced the message (user, assistant, system, or tool).
- content:
The text content of the message.
"""
role: Role
content: str
# Retrieval state
# ---------------
class RetrievedChunk(TypedDict, total=False):
"""
One retrieved context item from the RAG pipeline.
total=False means some keys are optional. This is useful because the
current retrieval code mainly returns text strings, but later the retrieval
engine may expose other metadata such as score, source, or document id.
Fields:
- text:
The actual content retrieved from the knowledgebase.
- source:
Identifier of where this chunk came from.
Example: "small_moments_doc_v1".
- score:
Relevance score from the retrieval system (higher = more relevant).
- metadata:
Additional structured info (section, tags, etc.).
Useful for debugging or advanced filtering.
"""
text: str
source: str
score: float
metadata: Dict[str, Any]
# Memory state
# ------------
class UserMemory(TypedDict, total=False):
"""
Memory about the user or current walk session.
Keep this minimal for EPIC 3. Later EPICs can expand to cross-session memory.
Fields:
- summary:
A rolling summary of the conversation so far.
Used to maintain continuity without passing the full transcript.
- preferences:
Notable user likes, dislikes, or style preferences.
Example: "prefers reflective questions", "doesn't like small talk".
- facts:
Concrete facts about the user.
Example: "starting a new job", "lives alone", "has social anxiety".
- open_loops:
Unresolved topics or threads that the agent may want to revisit.
Example: "user mentioned fear of networking but didn't explore it".
- prior_insights:
Meaningful reflections or realizations the user has already had.
Helps avoid repeating the same insights.
"""
summary: str
preferences: List[str]
facts: List[str]
open_loops: List[str]
prior_insights: List[str]
# Constructed prompt state
# ------------------------
class PromptContext(TypedDict, total=False):
"""
Structured payload prepared for the LLM node.
This is not the final rendered prompt string. Instead, it is a packaged
subset of graph state that the LLM node will format into the final prompt.
Fields:
- user_input:
The current user message for this turn.
- recent_messages:
A recent slice of the conversation history for local continuity.
- memory:
Memory about the user or current session.
- retrieved_context:
External context retrieved for the current turn.
- current_step:
The current step in the conversation/workflow.
- feedback:
The feedback provided by the supervisor to try again.
"""
user_input: str
recent_messages: List[MessageRecord]
memory: UserMemory
retrieved_context: List[RetrievedChunk]
current_step: str
feedback: str # provides brief justification for supervisor score
# Emotional state placeholder
# ---------------------------
class EmotionalState(TypedDict, total=False):
"""
Placeholder for the emotional state engine described in the roadmap.
These fields are optional because the emotional analysis system does not
need to be fully implemented yet, but the state should make room for it.
Purpose: Give the system a structured way to track the user's emotional
state, which can inform the agent's responses and reflections.
"""
valence: float
arousal: float
openness: float
friction: float
primary_emotion: str
tone: str
# Agent output state
# ------------------
class AgentOutput(TypedDict, total=False):
"""
Structured output from an agent node.
Fields:
- response_text:
The final text response shown to the user.
- source_chunks:
Text snippets used to ground the response.
- should_end_turn:
Whether the agent considers this interaction complete.
- next_step:
Suggested next step in the workflow.
Example: "reflection", "follow_up", "end".
- handoff_to:
Name of another agent to transfer control to (future multi-agent use).
"""
response_text: str
source_chunks: List[RetrievedChunk]
should_end_turn: bool
next_step: str
handoff_to: str
# Top-level graph state
# ---------------------
class WalkState(TypedDict, total=False):
"""
Shared state for the WalkXR LangGraph workflow.
This is the main state object the graph will read from and write to.
Sections:
- Identity / session:
Tracks who the user is and where they are in the experience.
- Interactions:
The current input, constructed prompt, and conversation history.
- Retrieval:
Context pulled from external knowledge (RAG).
- Memory:
Current knowledge / understanding of the user.
- Agent output:
The result of the most recent agent step.
- Control / orchestration:
Internal graph execution state (phase, errors, scratchpad).
- Evaluation:
Debugging, tracing, and scoring information.
- Testing:
Used solely by test scripts.
"""
# Identity / session
user_id: str
session_id: str
walk_id: str
active_agent: str
current_step: str
turn_index: int
# Interactions
user_input: str
conversation_history: List[MessageRecord]
# Retrieval
retrieval_query: str
retrieved_context: List[RetrievedChunk]
# Memory
memory: UserMemory
# Prompt constructions
prompt_context: PromptContext
# Future state engines
emotional_state: EmotionalState
# Agent result
agent_output: AgentOutput
# Control / orchestration
phase: Phase # Current phase of the graph execution
agent_scratchpad: Dict[str, Any] # Temporary workspace for agents to store intermediate thoughts, tool calls, etc.
error: Optional[str] # Error message if something went wrong, else None
# Evaluation
supervisor_score: str # Scores current agent_output as "good" or "bad"
supervisor_retries: int # Count number of retries to prevent exceeding max limit
max_retries_allowed: int # Maximum limit for number of retries allowed
safe_response: str # Fail-safe response if maximum retries reached
debug_print: bool
# Testing
bad_response_test: Optional[str] # Replaces LLM response with custom response to test self-reflection loop
# Helper constructors
# -------------------
def create_initial_state(
*,
user_input: str,
user_id: Optional[str] = None,
session_id: Optional[str] = None,
walk_id: Optional[str] = None,
active_agent: str = "small_moments_roleplay",
current_step: str = "conversation",
turn_index: int = 0,
conversation_history: Optional[List[MessageRecord]] = None,
max_retries_allowed: int = 3,
safe_response: str = SAFE_RESPONSE,
bad_response_test: Optional[str] = None,
debug_print: bool = False
) -> WalkState:
"""
Creates an initial WalkState used to start a new graph.
"""
return WalkState(
user_id=user_id or "unknown_user",
session_id=session_id or "default_session",
walk_id=walk_id or "default_walk",
active_agent=active_agent,
current_step=current_step,
turn_index=turn_index,
user_input=user_input,
conversation_history=conversation_history or [],
retrieval_query=user_input,
retrieved_context=[],
memory=UserMemory(
summary="",
preferences=[],
facts=[],
open_loops=[],
prior_insights=[],
),
prompt_context=PromptContext(
user_input="",
recent_messages=[],
memory=UserMemory(
summary="",
preferences=[],
facts=[],
open_loops=[],
prior_insights=[],
),
retrieved_context=[],
current_step=current_step,
feedback="",
),
emotional_state=EmotionalState(),
agent_output=AgentOutput(
response_text="",
source_chunks=[],
should_end_turn=False,
),
phase="input",
agent_scratchpad={},
error=None,
supervisor_score="good",
supervisor_retries=0,
max_retries_allowed=max_retries_allowed,
safe_response=safe_response,
debug_print=debug_print,
bad_response_test=bad_response_test
)
def append_message(state: WalkState, *, role: Role, content: str) -> WalkState:
"""
Convenience helper for adding a message to state.
Instead of needing to append the message list manually in every node, this
helper centralizes the logic and keeps the nodes.py code cleaner.
"""
state.setdefault("conversation_history", [])
state["conversation_history"].append(MessageRecord(role=role, content=content))
return state