npm package: @sindarin/persona
Description: Sindarin Persona SDK
Author: Sindarin Ventures
Latest version: 0.4.1

# Sindarin Persona SDK

[![Version](https://img.shields.io/npm/v/%40sindarin%2Fpersona)](https://www.npmjs.org/package/%40sindarin%2Fpersona)

**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:

```sh
npm install @sindarin/persona
# or
yarn add @sindarin/persona
```

or

```javascript
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][api-keys].

```js
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:

```js
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:

```js
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

```js
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:

1. **`mute()`** - Mutes incoming AI audio and immediately stops/clears any buffered audio
   ```js
   await personaClient.mute();
   ```

2. **`unmute()`** - Unmutes to resume listening to incoming AI audio
   ```js
   await 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:

1. **`sayText(text: string)`** - Makes the AI speak the provided text
   ```js
   personaClient.sayText("Hello, how are you?");
   ```

2. **`reactTo(text: string, options?)`** - Sends an event/thought for the AI to react to
   ```js
   personaClient.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)
   });
   ```

3. **`textMessage(text: string, options?)`** - Sends a user message that always interrupts
   ```js
   personaClient.textMessage("What's the weather like?", {
     canBeInterrupted: true    // Can the AI's response be interrupted (default: true)
   });
   ```
   Note: `textMessage` always interrupts any ongoing AI speech, unlike `reactTo` which 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:

```js
// 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

```js
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);
});
```

[api-keys]: https://persona-webapp-beta.vercel.app/