WalkXR-AI / src / walkxr_ai / core / state.py
state.py
Raw

"""
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