@sindarin/persona
v0.4.1
Published
Sindarin Persona SDK
Readme
Sindarin Persona SDK
Sindarin Persona enables you to add lifelike conversational AI agents to your applications. In browser environments, it features ultra low latency voice I/O with industry-leading turn-taking and transcription, and high-fidelity speech synthesis. In Node.js environments, it provides text-based conversational capabilities with the same powerful AI backend.
Demo: https://sindarin.tech
Installation
Install the package with:
npm install @sindarin/persona
# or
yarn add @sindarin/personaor
var script = document.createElement("script");
script.src = "https://api.prod.dal.lat.sindarin.tech/PersonaClientPublicV2?apikey=<YOUR_CLIENT_KEY>";
script.onload = () => {
var personaClient = new window.PersonaClient.default("<YOUR_CLIENT_KEY>");
// ... initialize, etc
};
document.head.appendChild(script);Usage
The package needs to be configured with your account's secret key, which is available in the Persona Web App / Settings.
import PersonaClient from "@sindarin/persona";
const personaClient = new PersonaClient("<YOUR_CLIENT_KEY>");
const handleStartChatButtonClick = async () => {
// Example config using a persona defined in the Playground; there are many ways to do things!
const config = {
userId: "admin",
personaName: "John",
options: {
debugMode: true,
streamTranscripts: true,
shouldNotSaveConversation: true,
},
};
try {
await personaClient.init(config);
configurePersonaEvents();
} catch (error) {
console.log(error);
}
};
const handlePauseChatButtonClick = async () => {
try {
await personaClient.pause();
} catch (error) {
console.log(error);
}
};
const handleResumeChatButtonClick = async () => {
try {
await personaClient.resume();
} catch (error) {
console.log(error);
}
};
const handleStopChatButtonClick = async () => {
try {
await personaClient.end();
} catch (error) {
console.log(error);
}
};
const handleMuteButtonClick = async () => {
try {
// Mute incoming audio (stops and clears any buffered audio)
await personaClient.mute();
} catch (error) {
console.log(error);
}
};
const handleUnmuteButtonClick = async () => {
try {
// Unmute to resume listening to incoming audio
await personaClient.unmute();
} catch (error) {
console.log(error);
}
};
const handleUpdateState = async (newState) => {
try {
// Simple state update
personaClient.updateState(newState);
// Or with additional context (backwards compatible with old 'thought' string parameter)
personaClient.updateState(newState, true, {
text: 'The user clicked a button',
shouldInterrupt: false,
canBeInterrupted: false,
shouldEnqueue: true
});
} catch (error) {
console.log(error);
}
};
const handleReactTo = async (thought) => {
try {
// Simple reaction
personaClient.reactTo(thought);
// Or with options
personaClient.reactTo(thought, {
shouldReact: true,
shouldInterrupt: false,
canBeInterrupted: true,
shouldEnqueue: true
});
} catch (error) {
console.log(error);
}
};
const handleTextMessage = async (text) => {
try {
// textMessage always interrupts current AI speech
// and the AI's response can be interrupted by default
personaClient.textMessage(text, { canBeInterrupted: true });
} catch (error) {
console.log(error);
}
};
const configurePersonaEvents = () => {
personaClient.on("messages_update", (messages) => {
console.log(messages);
});
personaClient.on("state_updated", (newState) => {
console.log(newState);
})
personaClient.on("action", (action) => {
console.log(action);
})
};Configuration
Initialize with config object
You can initialize a conversation with the following config fields:
const config = {
userId: "admin", // optional
personaName: "John", // optional
personaId: "example_id", // optional; this is found in the Persona Playground
details: { firstName: "John" }, // optional; arbitrary data you can inject into your prompt with {{details.x}}
metadata: { orderId: "1" }, // optional; arbitrary data used for conversation retrieval / filtering later
personaConfig: {
initialMessage: "Hello {{details.firstName}}!",
prompt: "You are a fun and helpful persona.",
actions: { properties: {} },
ttsConfig: {
voiceId: "example_id", // voice IDs can be found and tested in the Playground
},
llmConfig: {
llm: "example_llm_name", // e.g. llama-3-70b, llama-3-8b, gpt-4o, hermes-2-mixtral-8x7B-dpo
temperature: 0.8, // 0 to 1
repetitionPenalty: 1 // -3.5 to 3.5 for non-openai models; -2 to 2 for openai models
},
interjections: { // the ability for the persona to speak spontaneously
enabled: true,
minWait: 7500,
maxWait: 10000,
thought: "Nobody has said anything in a while. I should nudge the conversation along."
},
chargeLimitEnabled: true, // optional; should the conversation be cut off after a certain amount of charges incurred?
chargeLimit: 2, // optional; the charge limit
lifecycleEvents: { // optional; if the webhook endpoint returns { state } or { details } in the response body, they will be injected
onInit: {
webhook: {
url: "http://example.com",
method: "GET",
body: {},
headers: {}
}
},
onStateChange: { /* ... */ },
onEnd: { /* ... */ }
}
},
options: {
debugMode: true, // emit debug events in messages
streamTranscripts: true, // stream transcripts in realtime
shouldNotSaveConversation: true, // do not save transcripts
},
};
try {
await personaClient.init(config);
} catch (error) {
console.log(error);
}| Option | Default | Description |
| ----------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| userId | null | [Optional] Your unique ID for the current user. |
| personaName | null | [Optional; this or personaId or personaConfig required] The persona name in the Playground specifying a particular persona config. |
| personaId | null | [Optional; this or personaName or personaConfig required] The persona ID in the Playground specifying a particular persona config. This takes priority over personaName if both are provided. |
| details | null | [Optional] An object containing arbitrary information that you can inject into your prompt before the conversation starts using {{details.x}}. |
| metadata | null | [Optional] An object containing arbitrary information for the developer to retrieve the conversation by. |
| personaConfig | null | [Optional; this or personaName or personaId required] An ad hoc persona configuration specified entirely in your code. |
| options | - | [Optional] Additional options (below). |
| options.debugMode | false | Messages array will contain additional debug events (info, warn, error) for debugging purposes during a conversation. |
| options.streamTranscripts | false | Temporary transcripts will be rendered in the messages array.
| options.shouldNotSaveConversation | false | If true, the transcript will not be saved after the conversation ends. |
Node.js Compatibility
This SDK now supports both browser and Node.js environments. The same npm package can be used in both contexts, with automatic feature detection and adaptation.
Environment Detection
The SDK automatically detects the runtime environment and adjusts its behavior accordingly:
import PersonaClient, { isNode, isBrowser, hasAudioSupport } from "@sindarin/persona";
// Check the environment
console.log("Running in Node.js:", isNode);
console.log("Running in Browser:", isBrowser);
console.log("Audio support available:", hasAudioSupport);Feature Availability by Environment
| Feature | Browser | Node.js |
|---------|---------|---------|
| Voice Input (Microphone) | ✅ | ❌ (console warning) |
| Voice Output (Speaker) | ✅ | ❌ (console warning) |
| Audio Controls (mute, unmute) | ✅ | ⚠️ (no-op with warning) |
| Text Messaging (sayText, reactTo, textMessage) | ✅ | ✅ |
| State Management (updateState) | ✅ | ✅ |
| WebSocket Events | ✅ | ✅ |
| Message History | ✅ | ✅ |
| LLM Completions (getChatCompletion, prompt) | ✅ | ✅ |
| All PersonaService Methods | ✅ | ✅ |
Node.js Usage Example
import PersonaClient from "@sindarin/persona";
const personaClient = new PersonaClient("<YOUR_CLIENT_KEY>");
async function startTextChat() {
const config = {
userId: "server-user",
personaName: "Assistant",
options: {
debugMode: true,
streamTranscripts: true,
},
};
try {
await personaClient.init(config);
// Voice I/O will be automatically disabled with a warning
// Use text-based interactions
personaClient.sayText("Hello from Node.js!");
personaClient.textMessage("This is a user message", { canBeInterrupted: true });
personaClient.reactTo("User clicked something");
// Handle events
personaClient.on("messages_update", (messages) => {
console.log("Messages:", messages);
});
// Use LLM completions
const response = await personaClient.prompt("What is the weather today?");
console.log("AI Response:", response);
} catch (error) {
console.error("Error:", error);
}
}
startTextChat();Audio Controls
The SDK provides methods to control audio playback:
mute()- Mutes incoming AI audio and immediately stops/clears any buffered audioawait personaClient.mute();unmute()- Unmutes to resume listening to incoming AI audioawait personaClient.unmute();
Note: These methods only affect audio playback in browser environments where audio is enabled. In text-only or Node.js environments, they will log a warning but have no effect.
Text Messaging Methods
The SDK provides three methods for sending text to the AI:
sayText(text: string)- Makes the AI speak the provided textpersonaClient.sayText("Hello, how are you?");reactTo(text: string, options?)- Sends an event/thought for the AI to react topersonaClient.reactTo("User clicked the help button", { shouldReact: true, // Should AI generate a response (default: true) shouldInterrupt: false, // Should interrupt current AI speech (default: false) canBeInterrupted: true, // Can this response be interrupted (default: true) shouldEnqueue: false // Should enqueue this message (default: false) });textMessage(text: string, options?)- Sends a user message that always interruptspersonaClient.textMessage("What's the weather like?", { canBeInterrupted: true // Can the AI's response be interrupted (default: true) });Note:
textMessagealways interrupts any ongoing AI speech, unlikereactTowhich has configurable interruption.
State Management
updateState(state: unknown, shouldReact?: boolean, thoughtOrOptions?: string | options) - Updates the conversation state
The third parameter supports both old (string) and new (object) formats for backwards compatibility:
// Simple state update
personaClient.updateState({ score: 100 });
// With shouldReact flag
personaClient.updateState({ score: 100 }, true);
// Old API (backwards compatible) - string as third parameter
personaClient.updateState({ score: 100 }, true, "User scored a point");
// New API - object with additional options
personaClient.updateState({ score: 100 }, true, {
text: 'User scored a point',
shouldInterrupt: false, // Should interrupt current AI speech (default: false)
canBeInterrupted: true, // Can this response be interrupted (default: true)
shouldEnqueue: false // Should enqueue the reaction to be spoken *after* any current AI speech finishes(default: false)
});Backwards Compatibility
This update maintains full backwards compatibility with existing browser implementations. All existing code will continue to work without any changes. The Node.js support is additive and does not affect browser functionality.
Error Handling Options
try {
await personaClient.init(config);
} catch (error) {
console.log(error);
}
personaClient.on("error", (error) => {
console.log(error);
});
personaClient.on("connect_error", (error) => {
console.log(error);
});