Python SDK
Install the foilengine package and integrate NPC conversations into your Python project.
Installation
pip install foilengineQuick Start
from foilengine import FoilEngineClient
client = FoilEngineClient(
api_key="pk_live_...",
llm_api_key="sk-...", # your LLM provider API key
llm_model="gpt-4o", # any LiteLLM-supported model
)
# Discover your personas
personas = client.personas.list()
print(personas[0].name) # "Grumpy Barista"
# Initialize a session
session = client.chat.init_session(
persona_id=personas[0].id,
user_session_id="player-001",
player_name="Alex",
player_gender="non-binary",
)
print(session.message) # NPC's greeting
# Send a message
response = client.chat.send_message(
persona_id=personas[0].id,
message="What do you recommend?",
user_session_id="player-001",
)
print(response.message) # NPC's reply
print(response.current_state) # State machine state
print(response.score) # Session scoreConfiguration
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key | str | required | Your API key |
base_url | str | https://api.foilengine.io | API base URL (override for local dev) |
timeout | float | 30.0 | Request timeout in seconds |
max_retries | int | 3 | Max retries on 429/5xx errors |
llm_api_key | str | required for chat | Your LLM provider API key (OpenAI, Anthropic, etc.) |
llm_model | str | None | None | Default LLM model for all pipeline steps |
llm_eval_model | str | None | None | Override model for message evaluation |
llm_response_model | str | None | None | Override model for NPC response generation |
llm_summarization_model | str | None | None | Override model for conversation summarization |
llm_eval_api_key | str | None | None | Override API key for evaluation (use a different provider) |
llm_response_api_key | str | None | None | Override API key for NPC response generation |
llm_summarization_api_key | str | None | None | Override API key for conversation summarization |
debug | bool | False | Enable debug logging (logs requests, responses, and timing to the foilengine logger) |
cache_ttl | float | 60 | Cache TTL in seconds for personas.list() (set to 0 to disable) |
hooks | list[RequestHook] | None | None | Request lifecycle hooks for logging, analytics, or telemetry |
Available Methods
Personas
| Method | Returns | Description |
|---|---|---|
client.personas.list() | list[Persona] | List all published personas |
Utilities
| Method | Returns | Description |
|---|---|---|
client.validate_llm_key(model=None) | dict | Validate your LLM API key (returns {"valid": bool, "model": str}) |
Machines
| Method | Returns | Description |
|---|---|---|
client.machines.list(persona_id, user_session_id) | list[MachineInfo] | List available machines for a player |
Chat
| Method | Returns | Description |
|---|---|---|
client.chat.init_session(...) | SessionInit | Start a new conversation |
client.chat.send_message(...) | ChatResponse | Send a message, get NPC reply |
client.chat.send_message_stream(...) | Iterator[ChatStreamEvent] | Stream NPC response via SSE (metadata first, then text chunks, then done) |
client.chat.get_session(...) | SessionStatus | Check if a session exists |
client.chat.get_history(...) | ChatHistory | Get full message history |
client.chat.reset(...) | ResetResult | Delete a session |
Async Support
Use AsyncFoilEngineClient for async/await:
from foilengine import AsyncFoilEngineClient
async def main():
async with AsyncFoilEngineClient(
api_key="pk_live_...",
llm_api_key="sk-...",
llm_model="gpt-4o",
) as client:
personas = await client.personas.list_async()
response = await client.chat.send_message_async(
persona_id=personas[0].id,
message="Hello!",
user_session_id="player-001",
)
print(response.message)Error Handling
All API errors are raised as typed exceptions:
from foilengine import (
FoilEngineClient,
AuthenticationError,
NotFoundError,
RateLimitError,
)
client = FoilEngineClient(
api_key="pk_live_...",
llm_api_key="sk-...",
)
try:
session = client.chat.get_session("persona-id", "player-001")
except NotFoundError:
# No existing session — initialize one
session = client.chat.init_session(...)
except AuthenticationError:
print("Invalid API key")
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after}s")| Exception | Status | When |
|---|---|---|
BadRequestError | 400 | Invalid UUID, missing fields |
AuthenticationError | 401 | Missing or invalid API key |
ForbiddenError | 403 | Not owner or persona unpublished |
NotFoundError | 404 | Resource not found |
RateLimitError | 429 | Rate limit exceeded |
ServerError | 500 | Internal server error |
💡Tip
The SDK automatically retries on 429 and 5xx errors with jittered exponential backoff (up to 3 retries). You only need to handle errors that persist after retries. Enable
debug=True to see request/response logs.Streaming Responses
Stream NPC responses token-by-token for a typing effect. Metadata (state, score, decision) arrives before any text, so you can update your game UI immediately.
for event in client.chat.send_message_stream(
persona_id=persona_id,
message="Tell me about your potions.",
user_session_id="player-001",
):
if event.event_type == "metadata":
# All game state available before text starts
meta = event.data
print(f"State: {meta.current_state}, Score: {meta.score}")
elif event.event_type == "text_delta":
# Progressive text chunks
print(event.data["text"], end="", flush=True)
elif event.event_type == "done":
# Full ChatResponse for final reconciliation
print(f"\nFinal: {event.data.message}")Events
Register callbacks to react to game-relevant events like state transitions, score changes, or machine completion. Events fire automatically after each send_message() call.
client.on("state_change", lambda e: print(f"State: {e['from_state']} -> {e['to_state']}"))
client.on("score_change", lambda e: print(f"Score: {e['old_score']} -> {e['new_score']}"))
client.on("machine_completed", lambda e: print(f"Done! Outcome: {e['outcome']}"))
client.on("machines_unlocked", lambda e: print(f"Unlocked: {e['machines']}"))
client.on("session_ended", lambda e: print(f"Session ended: {e['outcome']}"))| Event | Data | When |
|---|---|---|
state_change | from_state, to_state, decision | State machine transitions to a new state |
score_change | old_score, new_score, delta | Session score changes |
machine_completed | outcome, final_score, session_id | Conversation reaches a terminal state |
machines_unlocked | machines (list of machine_key, name) | New machines become available after completion |
session_ended | outcome, final_score, session_id | Session receives an outcome (ACCEPT, REJECT, KICK_OUT) |
Request Hooks
Add custom logging, analytics, or telemetry with request lifecycle hooks.
from foilengine import FoilEngineClient, RequestHook
class LoggingHook(RequestHook):
def before_request(self, method, url, headers, body):
print(f"-> {method} {url}")
def after_response(self, method, url, status, elapsed_ms, body):
print(f"<- {status} in {elapsed_ms:.0f}ms")
def on_error(self, method, url, error):
print(f"!! {error}")
client = FoilEngineClient(
api_key="pk_live_...",
llm_api_key="sk-...",
hooks=[LoggingHook()],
)Multi-Machine Example
# Discover available machines for a player
machines = client.machines.list(persona_id, "player-001")
for m in machines:
status = "active" if m.has_session else "available"
linked = " (linked)" if m.is_linked else ""
print(f" {m.name}: {status}{linked}")
# Start a specific machine
session = client.chat.init_session(
persona_id=persona_id,
user_session_id="player-001",
player_name="Alex",
player_gender="non-binary",
machine_id=machines[1].machine_id, # target a specific scenario
)Complete Example
A full, runnable script that initializes a session, sends messages, handles streaming, and listens for events:
from foilengine import FoilEngineClient
client = FoilEngineClient(
api_key="pk_live_...",
llm_api_key="sk-...",
llm_model="gpt-4o",
)
# Listen for game-relevant events
client.on("state_change", lambda e: print(f" [state] {e['from_state']} -> {e['to_state']}"))
client.on("score_change", lambda e: print(f" [score] {e['old_score']} -> {e['new_score']}"))
client.on("machine_completed", lambda e: print(f" [done] Outcome: {e['outcome']}, Score: {e['final_score']}"))
# Discover personas
personas = client.personas.list()
persona = personas[0]
print(f"Chatting with: {persona.name}")
# Start a session
session = client.chat.init_session(
persona_id=persona.id,
user_session_id="player-001",
player_name="Alex",
player_gender="non-binary",
)
print(f"NPC: {session.message}")
# Send a message (non-streaming)
response = client.chat.send_message(
persona_id=persona.id,
message="What do you have for sale?",
user_session_id="player-001",
)
print(f"NPC: {response.message}")
print(f" State: {response.current_state}, Score: {response.score}")
# Send a message (streaming)
print("NPC: ", end="")
for event in client.chat.send_message_stream(
persona_id=persona.id,
message="Tell me more about that.",
user_session_id="player-001",
):
if event.event_type == "metadata":
pass # State/score available here before text starts
elif event.event_type == "text_delta":
print(event.data["text"], end="", flush=True)
elif event.event_type == "done":
print() # Newline after streaming completes
# Check session status
status = client.chat.get_session(persona.id, "player-001")
print(f"Session state: {status.current_state}, score: {status.score}")
# Reset when done
client.chat.reset(persona.id, "player-001")
print("Session reset.")