Skip to main content

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.

  1. The SSE event types​

    Event typeWhat it isWhat to do with it
    asgard.run.initThe request initialisesShow a loading state
    asgard.message.deltaA piece of the message, for the typewriterAccumulate it and repaint
    asgard.message.completeThe complete messageReplace with the final content
    asgard.run.doneThe request is finishedHide the loading state
  2. JavaScript with EventSource​

    EventSource is 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();
    };
  3. JavaScript with fetch and ReadableStream​

    fetch with a ReadableStream calls 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);
    }
    );
  4. 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}")
  5. 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>
    );
    }
  6. Next​

Ready to get started?

Want to see how Asgard fits your team? Talk to us.

Contact us