Finite State Machines for Large Language Models
Project description
LLM-FSM: Adding State to the Stateless
The Problem: Stateless LLMs in Structured Conversations
Large Language Models have revolutionized natural language processing with their remarkable generation capabilities. However, they have a fundamental limitation: they are inherently stateless. Each interaction is processed independently with only the context provided in the prompt.
This statelessness creates significant challenges for building robust conversational applications:
- State Fragility: Without explicit tracking, conversations easily lose their place
- Context Limitations: As conversations grow, context windows fill up quickly
- Transition Ambiguity: Determining when to move to different conversation stages is difficult
- Information Extraction Inconsistency: Extracting structured data from free-form text is unreliable
- Validation Challenges: Ensuring required information is collected before proceeding is complex
Consider a flight booking scenario:
User: I'd like to book a flight
System: Where would you like to fly to?
User: I'm thinking maybe Hawaii
System: Great choice! And where will you be departing from?
User: Actually, I'd prefer Bali instead of Hawaii
Without explicit state tracking, the system might miss the change in destination or maintain inconsistent information.
The Solution: Finite State Machines + LLMs
LLM-FSM elegantly combines classical Finite State Machines with modern Large Language Models:
"We keep the state as a JSON structure inside the system prompt of an LLM, describing transition nodes and conditions for that specific state, along with any emittance of symbols that the LLM might do."
This hybrid approach gives you the best of both worlds:
- ✅ Predictable conversation flows with clear rules and transitions
- ✅ Natural language understanding powered by state-of-the-art LLMs
- ✅ Persistent context across the entire conversation
- ✅ Dynamic adaptation to user inputs
- ✅ Expressive Logic for complex transitional decision-making
Key Features
- 🚦 Structured Conversation Flows: Define states, transitions, and conditions
- 🧠 LLM-Powered Entity Extraction: Let the LLM do what it does best
- 👤 Persona Support: Define a consistent tone and style for responses
- 📝 Persistent Context Management: Maintain information throughout the conversation
- 🔄 Provider-Agnostic: Works with OpenAI, Anthropic, and other LLM providers via LiteLLM
- 📊 Visualization: See your FSM structure with a built-in ASCII visualizer
- 📝 Validation & Error Handling: Catch and handle issues gracefully
- 🪵 Comprehensive Logging: Detailed logs for debugging and monitoring
- 🧪 Test-Friendly: Easy to unit test and verify behavior
- 🧮 JsonLogic Expressions: Powerful conditional logic for transitions
Theoretical Foundation
The Nature of Finite State Machines
Finite State Machines (FSMs) represent one of computer science's most elegant and powerful abstractions. Dating back to the theoretical work of mathematicians like Alan Turing and John von Neumann, FSMs provide a formal model for systems that can exist in exactly one of a finite number of states at any given time.
The core components of an FSM are remarkably simple:
- States: The distinct conditions in which a system can exist
- Transitions: Rules governing movement between states based on inputs
- Initial State: Where the system begins execution
- Terminal States: Where the system concludes execution (optional)
The Theoretical Synthesis
The LLM-FSM framework represents a theoretical bridge between two fundamentally different computational paradigms:
- Deterministic Computation (FSMs): Rule-based, predictable, and structured
- Probabilistic Computation (LLMs): Adaptable, nuanced, and natural
This synthesis creates a hybrid system where:
- The FSM provides the "skeleton" that ensures logical conversation flow
- The LLM provides the "muscles and skin" that handle natural language understanding and production
Example Conversation
Here's a sample conversation using a personal information collection FSM:
System: Hello! I'm an assistant that will be collecting some information.
Could you please tell me your name?
User: I'm Alex Thompson
System: Nice to meet you, Alex Thompson! Could you please provide your email address?
User: alex.thompson@example.com
System: Thank you! Could you please tell me your date of birth?
User: March 15, 1992
System: Great! I've collected:
- Name: Alex Thompson
- Email: alex.thompson@example.com
- Date of Birth: March 15, 1992
Is this information correct?
User: Actually, my email is alex.j.thompson@example.com
System: I've updated your email to alex.j.thompson@example.com. Is the information correct now?
User: Yes, that's correct
System: Thank you for confirming your information! Your profile has been saved.
Installation
# Clone the repository
git clone https://github.com/your-repo/llm-fsm.git
cd llm-fsm
# Install dependencies
pip install -r requirements.txt
# Set up environment variables
cp .env.example .env
# Edit .env with your API keys
Or install via pip:
pip install llm-fsm
Quick Start
Python API
from llm_fsm.llm import LiteLLMInterface
from llm_fsm.fsm import FSMManager
from llm_fsm.utilities import load_fsm_definition
# Initialize the LLM interface
llm_interface = LiteLLMInterface(
model="gpt-4o", # Use any model supported by LiteLLM
api_key="your-api-key",
temperature=0.5
)
# Create an FSM manager
fsm_manager = FSMManager(
fsm_loader=load_fsm_definition,
llm_interface=llm_interface
)
# Start a conversation with your FSM
conversation_id, response = fsm_manager.start_conversation("examples/personal_information_collection.json")
print(f"System: {response}")
# Process user input
user_input = "My name is Alex Thompson"
response = fsm_manager.process_message(conversation_id, user_input)
print(f"System: {response}")
# Continue the conversation until completion
while not fsm_manager.is_conversation_ended(conversation_id):
user_input = input("You: ")
response = fsm_manager.process_message(conversation_id, user_input)
print(f"System: {response}")
# Get the collected data when done
user_data = fsm_manager.get_conversation_data(conversation_id)
print(f"Collected data: {user_data}")
Simplified API
For an even easier approach, you can use the streamlined LLM_FSM class:
from llm_fsm import LLM_FSM
# Create the LLM-FSM instance
fsm = LLM_FSM.from_file(
path="examples/personal_information_collection.json",
model="gpt-4o",
api_key="your-api-key"
)
# Start a conversation
conversation_id, response = fsm.converse("")
print(f"System: {response}")
# Continue conversation
while not fsm.is_conversation_ended(conversation_id):
user_input = input("You: ")
_, response = fsm.converse(user_input, conversation_id)
print(f"System: {response}")
Starting Conversations with Initial Context
You can pre-populate context data when starting a conversation, which is useful for personalization, session continuation, or skipping unnecessary states:
# Define initial context with user information
initial_context = {
"name": "Alex Thompson",
"email": "alex.thompson@example.com",
"preferred_genres": ["science fiction", "mystery", "fantasy"],
"membership_level": "premium"
}
# Start a conversation with initial context
conversation_id, response = fsm_manager.start_conversation(
"examples/personal_information_collection.json",
initial_context=initial_context
)
Command Line Interface
You can also run conversations directly from the command line:
# Run a conversation with a specific FSM
llm-fsm --fsm examples/personal_information_collection.json
# Visualize an FSM using ASCII art
llm-fsm-visualize --fsm examples/personal_information_collection.json
# Validate an FSM definition
llm-fsm-validate --fsm examples/personal_information_collection.json
Core Architecture
FSM Definition
At its core, LLM-FSM uses a JSON structure to define states, transitions, and conditions:
{
"name": "Personal Information Collection",
"description": "A conversation flow to collect user information",
"initial_state": "welcome",
"persona": "A helpful and friendly assistant who speaks in a warm, conversational tone",
"states": {
"welcome": {
"id": "welcome",
"description": "Initial welcome state",
"purpose": "Welcome the user and explain purpose",
"transitions": [
{
"target_state": "collect_name",
"description": "Always transition to collecting name",
"priority": 0
}
],
"instructions": "Warmly welcome the user and explain that you'll be collecting information."
},
"collect_name": {
"id": "collect_name",
"description": "Collect user's name",
"purpose": "Ask for and record the user's full name",
"required_context_keys": ["name"],
"transitions": [
{
"target_state": "collect_email",
"description": "Transition once name is obtained",
"conditions": [
{
"description": "Name has been provided",
"requires_context_keys": ["name"]
}
],
"priority": 0
}
],
"instructions": "Ask the user for their full name. Extract and store it in the 'name' context variable."
}
}
}
JsonLogic Expressions for Powerful Transition Conditions
LLM-FSM includes a powerful JsonLogic implementation that allows for complex conditional logic in state transitions. This enables sophisticated decision-making based on the conversation context:
{
"transitions": [
{
"target_state": "premium_support",
"description": "Route to premium support",
"conditions": [
{
"description": "Customer is premium member",
"logic": {
"or": [
{"==": [{"var": "customer.tier"}, "premium"]},
{">": [{"var": "customer.lifetime_value"}, 5000]}
]
}
}
]
}
]
}
The expression system supports:
- Comparison Operators:
==,===,!=,!==,>,>=,<,<= - Logical Operators:
!,!!,and,or - Context Access:
varfor retrieving values from context - Validation Operators:
missing,missing_somefor checking required fields - Conditional Logic:
iffor if/else decision branches - Arithmetic Operations:
+,-,*,/,% - String Operations:
catfor concatenation - Membership Checks:
in,contains
The Execution Flow
When a user sends a message:
-
Prompt Construction: We build a system prompt containing:
- Current state information
- Available transitions
- Extraction instructions
- Current context values
- Recent conversation history
- Response format instructions
-
LLM Processing: The LLM:
- Extracts relevant information from the user's message
- Determines which state to transition to
- Generates a natural language response
- Returns a structured JSON response
-
State Management: The FSM Manager:
- Updates the context with extracted information
- Validates the proposed state transition (using JsonLogic expressions if present)
- Updates the current state
- Stores the conversation history
LLM Response Format
The LLM returns a structured JSON response:
{
"transition": {
"target_state": "collect_email",
"context_update": {"name": "Alex Thompson"}
},
"message": "Nice to meet you, Alex Thompson! Could you please provide your email address?",
"reasoning": "The user provided their full name 'Alex Thompson', so I'm transitioning to collect their email."
}
This format cleanly separates:
- User-facing content (the message)
- System-facing content (transition decision and context updates)
- Debugging information (reasoning)
Conversation Patterns
LLM-FSM supports various conversation patterns:
1. Linear Flows
Step-by-step information collection:
- Personal information forms
- Survey administration
- Onboarding processes
2. Conversational Loops
Maintain ongoing engagement:
- Recommendation systems
- Coaching conversations
- Learning assistants
3. Decision Trees
Guide users through branching options:
- Product recommendations
- Troubleshooting flows
- Decision support
4. Hybrid Patterns
Combine multiple patterns:
- Customer support (identification → troubleshooting → resolution)
- Medical triage (symptoms → assessment → recommendations)
- Educational systems (assessment → instruction → testing)
Key Design Decisions
1. LLM as the NLU Engine
Instead of brittle extraction rules in code:
# Old approach
def extract_name(message):
patterns = [r"My name is ([\w\s]+)", r"I am ([\w\s]+)"]
for pattern in patterns:
match = re.search(pattern, message)
if match:
return match.group(1)
return None
We leverage the LLM's understanding:
When the user provides their name, extract it and store it in the 'name' context variable.
Consider explicit mentions ('My name is John') and implicit mentions ('Call me John').
2. Context Outside States
We maintain context at the conversation level rather than within individual states:
# Context naturally persists across state transitions
{
"current_state": "collect_email",
"context": {"name": "John Smith", "email": "john@example.com"}
}
3. Expressive Transition Conditions with JsonLogic
Using JsonLogic expressions for transition conditions:
# Complex routing logic based on multiple factors
{
"and": [
{"==": [{"var": "customer.status"}, "vip"]},
{"or": [
{"==": [{"var": "issue.category"}, "billing"]},
{">": [{"var": "issue.priority"}, 3]}
]}
]
}
Included Examples
The repository includes several example FSMs:
- Personal Information Collection: A linear flow for collecting user details (
examples/personal_information_collection.json) - Book Recommendation System: A conversational loop with engagement detection (
examples/conversational_loop.json) - Product Recommendation: A decision tree with different endings based on user preferences (
examples/tree_conversation_with_4_endings.json) - Three Little Pigs Story: An interactive storytelling experience with a custom persona (
examples/three_little_pigs_story_time.json)
Persona Support
One powerful feature of LLM-FSM is the ability to define a consistent persona for the entire conversation flow:
{
"name": "Three Little Pigs Interactive Story",
"description": "An interactive storytelling experience based on the classic tale",
"initial_state": "introduction",
"persona": "You are J.R.R Tolkien master epic story teller.",
"states": {
// State definitions
}
}
The persona:
- Defines a consistent tone, style, and voice for all responses
- Is maintained across all states in the conversation
- Is incorporated into the system prompt for each LLM request
- Enables creating specialized conversational experiences (storytellers, educators, customer service agents, etc.)
License
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm_fsm-0.1.1.tar.gz.
File metadata
- Download URL: llm_fsm-0.1.1.tar.gz
- Upload date:
- Size: 77.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
616e8a255f09f0458634f7a7415bd9d41f84d9f0b6b97c8678fa162e97c1d9bb
|
|
| MD5 |
ce7535a6c90a2093a68554a581574503
|
|
| BLAKE2b-256 |
b0151ff267ae26e939dda13fbdb66123d415c0c42f73d876e06ec23c9cef82ec
|
File details
Details for the file llm_fsm-0.1.1-py3-none-any.whl.
File metadata
- Download URL: llm_fsm-0.1.1-py3-none-any.whl
- Upload date:
- Size: 68.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8d5748ecfafe7e87b21690a11fec5186a20968be4148351d568ed583b111d9a
|
|
| MD5 |
7d3e01c9bfa91263859a9cab6d761d8a
|
|
| BLAKE2b-256 |
fe70b5f37736fe7f2f4fcedbe78d9e6be3cabec956acfd3d0a5604c47ef81ff3
|