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
| Event | Trigger | Use Case |
|---|---|---|
OnInteractionStarted | Interaction begins processing | Show loading state |
OnTextReceived | Text chunk received | Display streaming text |
OnAudioReceived | Audio chunk received | Play audio (auto-handled) |
OnUtilityResult | Utility execution complete | Process classifications/extractions |
OnInteractionComplete | Interaction finished | Hide loading, enable input |
OnError | Error occurred | Display error message |
OnConnectionStateChanged | WebSocket state changes | Update 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 executedobject 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
- API Reference - Understand the protocol
- Examples - See complete implementations
- WebSocket Protocol - Deep dive into messages