Skip to main content

Conversation Events

The UG SDK uses an event-driven architecture to notify your application of conversation state changes and responses. This guide covers all available events and how to use them effectively.

Event Overview

EventTriggerUse Case
OnInteractionStartedInteraction begins processingShow loading state
OnTextReceivedText chunk receivedDisplay streaming text
OnAudioReceivedAudio chunk receivedPlay audio (auto-handled)
OnUtilityResultUtility execution completeProcess classifications/extractions
OnInteractionCompleteInteraction finishedHide loading, enable input
OnErrorError occurredDisplay error message
OnConnectionStateChangedWebSocket state changesUpdate connection status

Event Definitions

OnInteractionStarted

Triggered when the SDK begins processing an interaction (after sending a message or audio).

Signature:

public event Action OnInteractionStarted;

Usage:

conversation.OnInteractionStarted += () =>
{
Debug.Log("Interaction started");
loadingIndicator.SetActive(true);
DisableUserInput();
};

Best Practices:

  • Show loading indicator
  • Disable input controls to prevent duplicate messages
  • Clear previous response text
  • Start any waiting animations

OnTextReceived

Triggered for each text chunk received from the AI. Text arrives in a streaming fashion.

Signature:

public event Action<string> OnTextReceived;

Parameters:

  • string text - The text chunk received (may be partial sentence)

Usage:

private string fullResponse = "";

conversation.OnTextReceived += (text) =>
{
fullResponse += text;
dialogText.text = fullResponse;
Debug.Log($"Received text: {text}");
};

Typewriter Effect Example:

private Queue<string> textQueue = new Queue<string>();

conversation.OnTextReceived += (text) =>
{
textQueue.Enqueue(text);
if (!isTyping)
{
StartCoroutine(TypewriterCoroutine());
}
};

private IEnumerator TypewriterCoroutine()
{
isTyping = true;

while (textQueue.Count > 0)
{
string chunk = textQueue.Dequeue();
foreach (char c in chunk)
{
dialogText.text += c;
yield return new WaitForSeconds(0.03f);
}
}

isTyping = false;
}

OnAudioReceived

Triggered when audio chunks are received. The SDK handles playback automatically.

Signature:

public event Action<byte[]> OnAudioReceived;

Parameters:

  • byte[] audioData - Raw audio data (PCM format, 24kHz, 16-bit, mono)

Usage:

conversation.OnAudioReceived += (audioData) =>
{
Debug.Log($"Received {audioData.Length} bytes of audio");
// SDK plays audio automatically
// Use this event for visualizations, analytics, etc.
};

Audio Visualization Example:

private AudioVisualizer visualizer;

conversation.OnAudioReceived += (audioData) =>
{
// Calculate volume for visualization
float volume = CalculateVolume(audioData);
visualizer.UpdateVolume(volume);
};

private float CalculateVolume(byte[] audioData)
{
float sum = 0;
for (int i = 0; i < audioData.Length; i += 2)
{
short sample = System.BitConverter.ToInt16(audioData, i);
sum += Mathf.Abs(sample);
}
return sum / (audioData.Length / 2) / 32768f;
}

OnUtilityResult

Triggered when a utility execution completes (classifications, extractions).

Signature:

public event Action<string, object> OnUtilityResult;

Parameters:

  • string utilityName - The name of the utility that executed
  • object result - The result value (type depends on utility type)

Usage:

conversation.OnUtilityResult += (name, result) =>
{
Debug.Log($"Utility '{name}' returned: {result}");

switch (name)
{
case "player_intent":
HandlePlayerIntent(result as string);
break;

case "sentiment":
HandleSentiment(result as string);
break;

case "quest_info":
HandleQuestInfo(result as Dictionary<string, object>);
break;
}
};

Classify Utility Result:

// Configuration
new Utility
{
Type = "classify",
ClassificationQuestion = "What is the player's mood?",
Answers = new[] { "happy", "sad", "angry", "neutral" }
}

// Event handler
conversation.OnUtilityResult += (name, result) =>
{
if (name == "player_mood")
{
string mood = result as string; // One of: "happy", "sad", "angry", "neutral"
UpdateNPCExpression(mood);
}
};

Extract Utility Result:

// Configuration
new Utility
{
Type = "extract",
ExtractPrompt = "Extract the quest name, description, and reward from the conversation"
}

// Event handler
conversation.OnUtilityResult += (name, result) =>
{
if (name == "quest_info")
{
string extractedText = result as string;
// Parse the extracted information
ParseQuestInfo(extractedText);
}
};

OnInteractionComplete

Triggered when the entire interaction is complete (all text, audio, and utilities processed).

Signature:

public event Action OnInteractionComplete;

Usage:

conversation.OnInteractionComplete += () =>
{
Debug.Log("Interaction complete");
loadingIndicator.SetActive(false);
EnableUserInput();
ResetResponseBuffer();
};

Best Practices:

  • Hide loading indicators
  • Re-enable user input
  • Clear any temporary state
  • Trigger follow-up actions (e.g., auto-advance dialog)

OnError

Triggered when an error occurs during the conversation.

Signature:

public event Action<string> OnError;

Parameters:

  • string errorMessage - Description of the error

Usage:

conversation.OnError += (error) =>
{
Debug.LogError($"Conversation error: {error}");
ShowErrorDialog(error);
EnableUserInput();
loadingIndicator.SetActive(false);
};

Error Handling Strategy:

private void ShowErrorDialog(string error)
{
// Parse error type
if (error.Contains("network") || error.Contains("connection"))
{
errorText.text = "Connection lost. Please check your internet.";
retryButton.gameObject.SetActive(true);
}
else if (error.Contains("unauthorized") || error.Contains("authentication"))
{
errorText.text = "Session expired. Please log in again.";
loginButton.gameObject.SetActive(true);
}
else
{
errorText.text = "Something went wrong. Please try again.";
retryButton.gameObject.SetActive(true);
}

errorDialog.SetActive(true);
}

OnConnectionStateChanged

Triggered when the WebSocket connection state changes.

Signature:

public event Action<ConnectionState> OnConnectionStateChanged;

Parameters:

  • ConnectionState state - The new connection state

Connection States:

public enum ConnectionState
{
Disconnected, // Not connected
Connecting, // Attempting connection
Connected, // Connected but not authenticated
Authenticated, // Authenticated and ready
Error // Connection error
}

Usage:

conversation.OnConnectionStateChanged += (state) =>
{
Debug.Log($"Connection state: {state}");
UpdateConnectionIndicator(state);
};

private void UpdateConnectionIndicator(ConnectionState state)
{
switch (state)
{
case ConnectionState.Disconnected:
indicator.color = Color.gray;
statusText.text = "Offline";
break;

case ConnectionState.Connecting:
indicator.color = Color.yellow;
statusText.text = "Connecting...";
break;

case ConnectionState.Connected:
indicator.color = Color.blue;
statusText.text = "Connected";
break;

case ConnectionState.Authenticated:
indicator.color = Color.green;
statusText.text = "Ready";
break;

case ConnectionState.Error:
indicator.color = Color.red;
statusText.text = "Error";
break;
}
}

Event Lifecycle

Understanding the typical event sequence helps structure your code:

Text Interaction Flow

1. SendMessage() called
2. OnInteractionStarted fired
3. OnTextReceived fired (multiple times, streaming)
4. OnAudioReceived fired (multiple times, if audio enabled)
5. OnUtilityResult fired (once per utility)
6. OnInteractionComplete fired

Voice Interaction Flow

1. StartRecording() called
2. (User speaks)
3. StopRecording() called
4. OnInteractionStarted fired
5. OnTextReceived fired (multiple times)
6. OnAudioReceived fired (multiple times)
7. OnUtilityResult fired (if utilities configured)
8. OnInteractionComplete fired

Error Flow

1. SendMessage() or StartRecording() called
2. OnInteractionStarted fired
3. (Error occurs)
4. OnError fired
5. OnInteractionComplete may or may not fire

Complete Event Handler Example

using UnityEngine;
using UnityEngine.UI;
using UGSDK;
using System.Collections.Generic;

public class ConversationEventHandler : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private Text npcDialogText;
[SerializeField] private GameObject loadingIndicator;
[SerializeField] private Image connectionIndicator;
[SerializeField] private GameObject errorPanel;
[SerializeField] private Text errorText;

private Conversation conversation;
private string currentResponse = "";
private Queue<string> textQueue = new Queue<string>();

public void InitializeConversation(Conversation conv)
{
conversation = conv;
SubscribeToAllEvents();
}

private void SubscribeToAllEvents()
{
conversation.OnConnectionStateChanged += HandleConnectionStateChanged;
conversation.OnInteractionStarted += HandleInteractionStarted;
conversation.OnTextReceived += HandleTextReceived;
conversation.OnAudioReceived += HandleAudioReceived;
conversation.OnUtilityResult += HandleUtilityResult;
conversation.OnInteractionComplete += HandleInteractionComplete;
conversation.OnError += HandleError;
}

private void HandleConnectionStateChanged(ConnectionState state)
{
Debug.Log($"Connection: {state}");

Color indicatorColor = state switch
{
ConnectionState.Disconnected => Color.gray,
ConnectionState.Connecting => Color.yellow,
ConnectionState.Connected => Color.blue,
ConnectionState.Authenticated => Color.green,
ConnectionState.Error => Color.red,
_ => Color.white
};

connectionIndicator.color = indicatorColor;
}

private void HandleInteractionStarted()
{
Debug.Log("Interaction started");
currentResponse = "";
textQueue.Clear();
npcDialogText.text = "";
loadingIndicator.SetActive(true);
errorPanel.SetActive(false);
}

private void HandleTextReceived(string text)
{
Debug.Log($"Text: {text}");
textQueue.Enqueue(text);

// Immediately append (or use coroutine for typewriter effect)
currentResponse += text;
npcDialogText.text = currentResponse;
}

private void HandleAudioReceived(byte[] audioData)
{
Debug.Log($"Audio: {audioData.Length} bytes");
// Audio playback handled automatically
// Could trigger lip-sync or audio visualization here
}

private void HandleUtilityResult(string utilityName, object result)
{
Debug.Log($"Utility '{utilityName}': {result}");

// Route to specific handlers
switch (utilityName)
{
case "player_intent":
ProcessPlayerIntent(result as string);
break;

case "sentiment":
ProcessSentiment(result as string);
break;
}
}

private void HandleInteractionComplete()
{
Debug.Log("Interaction complete");
loadingIndicator.SetActive(false);
}

private void HandleError(string error)
{
Debug.LogError($"Error: {error}");
errorText.text = error;
errorPanel.SetActive(true);
loadingIndicator.SetActive(false);
}

private void ProcessPlayerIntent(string intent)
{
// Game-specific logic based on intent
Debug.Log($"Player wants to: {intent}");
}

private void ProcessSentiment(string sentiment)
{
// Adjust NPC behavior based on sentiment
Debug.Log($"Player sentiment: {sentiment}");
}

private void OnDestroy()
{
if (conversation != null)
{
// Unsubscribe to prevent memory leaks
conversation.OnConnectionStateChanged -= HandleConnectionStateChanged;
conversation.OnInteractionStarted -= HandleInteractionStarted;
conversation.OnTextReceived -= HandleTextReceived;
conversation.OnAudioReceived -= HandleAudioReceived;
conversation.OnUtilityResult -= HandleUtilityResult;
conversation.OnInteractionComplete -= HandleInteractionComplete;
conversation.OnError -= HandleError;
}
}
}

Best Practices

1. Always Unsubscribe in OnDestroy

Prevent memory leaks by unsubscribing from events:

private void OnDestroy()
{
if (conversation != null)
{
conversation.OnTextReceived -= HandleTextReceived;
// ... unsubscribe from all events
}
}

2. Handle Errors Gracefully

Always subscribe to OnError and provide user feedback:

conversation.OnError += (error) =>
{
// Log for debugging
Debug.LogError(error);

// Show user-friendly message
ShowErrorMessage("Couldn't connect. Please try again.");

// Reset UI state
EnableInputControls();
};

3. Use State Machines

For complex dialog flows, use a state machine pattern:

public enum DialogState
{
Idle,
WaitingForResponse,
DisplayingText,
PlayingAudio,
Complete
}

private DialogState currentState = DialogState.Idle;

conversation.OnInteractionStarted += () => currentState = DialogState.WaitingForResponse;
conversation.OnTextReceived += (text) => currentState = DialogState.DisplayingText;
conversation.OnInteractionComplete += () => currentState = DialogState.Complete;

4. Buffer Streaming Text

Queue text chunks for smooth display:

private Queue<string> textBuffer = new Queue<string>();

conversation.OnTextReceived += (text) =>
{
textBuffer.Enqueue(text);
};

private void Update()
{
if (textBuffer.Count > 0 && Time.time > nextTextTime)
{
dialogText.text += textBuffer.Dequeue();
nextTextTime = Time.time + textDelay;
}
}

5. Separate UI and Logic

Keep event handlers focused on UI updates:

// Good: Separate concerns
conversation.OnUtilityResult += (name, result) =>
{
UpdateUI(name, result);
ProcessGameLogic(name, result);
};

// Avoid: Mixing everything
conversation.OnUtilityResult += (name, result) =>
{
// 100 lines of mixed UI and logic code
};

Next Steps