REFERENCE
Developer Reference
Authentication, APIs, SDKs, Flow Agent processors and parameter resolution, in full.
Handling a streamed response
A complete example of handling Asgard SSE events and building a live typewriter effect
The Asgard API returns AI-generated text as a Server-Sent Events (SSE) stream. This example parses the various SSE event types and builds a live typewriter effect in the front end.
The SSE event types
Event type What it is What to do with it asgard.run.initThe request initialises Show a loading state asgard.message.deltaA piece of the message, for the typewriter Accumulate it and repaint asgard.message.completeThe complete message Replace with the final content asgard.run.doneThe request is finished Hide the loading state JavaScript with EventSource
EventSourceis the browser's own SSE API, and it suits the simpler streaming cases.// Note: EventSource only does GET, and the Asgard API is POST,
// so you need a back-end proxy, or fetch + ReadableStream (see below).
// If your back end already wraps this as a GET SSE endpoint:
const source = new EventSource('/api/chat?channelId=channel-001&text=hello');
let fullText = '';
source.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
switch (data.eventType) {
case 'asgard.run.init':
console.log('Conversation started, runId:', data.fact.runInit?.runId);
document.getElementById('status').textContent = 'Thinking...';
break;
case 'asgard.message.delta':
fullText += data.fact.messageDelta.message.text;
document.getElementById('response').textContent = fullText;
break;
case 'asgard.message.complete':
fullText = data.fact.messageComplete.message.text;
document.getElementById('response').textContent = fullText;
break;
case 'asgard.run.done':
document.getElementById('status').textContent = 'Done';
source.close();
break;
}
} catch (e) {
console.error('Could not parse the event', e);
}
};
source.onerror = (err) => {
console.error('SSE connection error', err);
source.close();
};JavaScript with fetch and ReadableStream
fetchwith aReadableStreamcalls the Asgard POST API directly, which is how most front ends integrate.const BASE_URL = 'https://api.asgard-ai.com';
const NAMESPACE = 'your-namespace';
const BOT_PROVIDER = 'your-bot-provider';
const API_KEY = process.env.ASGARD_API_KEY;
async function streamMessage(channelId, text, onDelta, onComplete) {
const url = `${BASE_URL}/generic/ns/${NAMESPACE}/bot-provider/${BOT_PROVIDER}/message/sse`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY,
},
body: JSON.stringify({
customChannelId: channelId,
text,
action: 'NONE',
}),
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
// keep the last line, which may be incomplete
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const jsonStr = line.slice(5).trim();
if (!jsonStr) continue;
try {
const event = JSON.parse(jsonStr);
switch (event.eventType) {
case 'asgard.run.init':
console.log('Stream started');
break;
case 'asgard.message.delta': {
const delta = event.fact.messageDelta.message.text;
fullText += delta;
// call the callback to update the interface (the typewriter effect)
onDelta?.(delta, fullText);
break;
}
case 'asgard.message.complete':
// take the full text from the complete event, to stay consistent
fullText = event.fact.messageComplete.message.text;
break;
case 'asgard.run.done':
onComplete?.(fullText);
return fullText;
}
} catch (e) {
// ignore a line that is not JSON
}
}
}
return fullText;
}
// usage, driving the DOM for the typewriter effect
const outputEl = document.getElementById('output');
streamMessage(
'channel-001',
'Tell me what the Asgard platform does',
(delta, accumulated) => {
// repaint on every delta
outputEl.textContent = accumulated;
},
(finalText) => {
console.log('Stream finished, full response:', finalText);
}
);Python with sseclient
import requests
import json
import os
# install the dependencies: pip install requests sseclient-py
import sseclient
BASE_URL = "https://api.asgard-ai.com"
NAMESPACE = "your-namespace"
BOT_PROVIDER = "your-bot-provider"
API_KEY = os.environ.get("ASGARD_API_KEY")
def stream_message(channel_id: str, text: str):
url = f"{BASE_URL}/generic/ns/{NAMESPACE}/bot-provider/{BOT_PROVIDER}/message/sse"
headers = {
"Content-Type": "application/json",
"X-API-KEY": API_KEY,
"Accept": "text/event-stream",
}
payload = {
"customChannelId": channel_id,
"text": text,
"action": "NONE",
}
full_text = ""
with requests.post(url, headers=headers, json=payload, stream=True) as response:
response.raise_for_status()
client = sseclient.SSEClient(response)
for event in client.events():
if not event.data:
continue
try:
data = json.loads(event.data)
event_type = data.get("eventType")
if event_type == "asgard.run.init":
print("Stream started...")
elif event_type == "asgard.message.delta":
delta = data["fact"]["messageDelta"]["message"]["text"]
full_text += delta
# print as it arrives, for the typewriter effect
print(delta, end="", flush=True)
elif event_type == "asgard.message.complete":
full_text = data["fact"]["messageComplete"]["message"]["text"]
elif event_type == "asgard.run.done":
print("\nStream finished")
break
except json.JSONDecodeError:
pass
return full_text
if __name__ == "__main__":
result = stream_message("channel-001", "Tell me what the Asgard platform does")
print(f"\nFull response:\n{result}")A React hook
Here is a custom React hook that wraps the streaming logic and can be dropped into a component.
import { useState, useCallback, useRef } from 'react';
const BASE_URL = 'https://api.asgard-ai.com';
const NAMESPACE = 'your-namespace';
const BOT_PROVIDER = 'your-bot-provider';
/**
* A custom hook for handling the Asgard SSE stream
*/
function useAsgardStream(apiKey) {
const [response, setResponse] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const sendMessage = useCallback(
async (channelId, text) => {
// cancel the request still in flight
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setIsStreaming(true);
setResponse('');
setError(null);
const url = `${BASE_URL}/generic/ns/${NAMESPACE}/bot-provider/${BOT_PROVIDER}/message/sse`;
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': apiKey,
},
body: JSON.stringify({
customChannelId: channelId,
text,
action: 'NONE',
}),
signal: abortControllerRef.current.signal,
});
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const jsonStr = line.slice(5).trim();
if (!jsonStr) continue;
try {
const event = JSON.parse(jsonStr);
if (event.eventType === 'asgard.message.delta') {
const delta = event.fact.messageDelta.message.text;
setResponse((prev) => prev + delta);
}
if (event.eventType === 'asgard.run.done') {
setIsStreaming(false);
return;
}
} catch (_) {}
}
}
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsStreaming(false);
}
},
[apiKey]
);
const stop = useCallback(() => {
abortControllerRef.current?.abort();
setIsStreaming(false);
}, []);
return { response, isStreaming, error, sendMessage, stop };
}
// an example component
function ChatWidget({ apiKey }) {
const [input, setInput] = useState('');
const { response, isStreaming, error, sendMessage, stop } = useAsgardStream(apiKey);
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage('channel-001', input);
setInput('');
};
return (
<div>
<div className="response-box">
{response || (isStreaming ? 'Thinking...' : 'Waiting for input')}
{isStreaming && <span className="cursor">|</span>}
</div>
{error && <div className="error">Error: {error}</div>}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isStreaming}
/>
<button type="submit" disabled={isStreaming || !input.trim()}>
Send
</button>
{isStreaming && (
<button type="button" onClick={stop}>
Stop
</button>
)}
</form>
</div>
);
}Next
- Sending a message — the basic send example
- Querying a knowledge base — question and answer against a RAG knowledge base
- The SSE events in detail — the full event format