メインコンテンツまでスキップ

REFERENCE

開発者リファレンス

認証、API、SDK、Flow Agent のプロセッサ、パラメータ解決のリファレンスです。

ストリーミング応答の処理

Asgard の SSE イベントを処理し、リアルタイムのタイプライター風表示を作る完全な例

Asgard API は AI が生成したテキストを Server-Sent Events(SSE)のストリームで返します。この例では、各種の SSE イベントを解析し、フロントエンドでリアルタイムのタイプライター風の表示を作ります。

  1. SSE のイベント種別​

    イベント種別説明推奨する処理
    asgard.run.initリクエストの初期化読み込み中の表示を出す
    asgard.message.deltaメッセージの断片(タイプライター)積み上げてその場で表示する
    asgard.message.complete完全なメッセージ最終的な内容に置き換える
    asgard.run.doneリクエストの完了読み込み中の表示を消す
  2. JavaScript EventSource の例​

    EventSource はブラウザが標準で持つ SSE の API で、単純なストリーミングに向いています。

    // 注意: EventSource は GET しか扱えず、Asgard API は POST なので、
    // バックエンドで中継するか、fetch + ReadableStream を使ってください(下の例)。

    // バックエンドが GET の SSE エンドポイントとして包んでいる場合:
    const source = new EventSource('/api/chat?channelId=channel-001&text=こんにちは');

    let fullText = '';

    source.onmessage = (event) => {
    try {
    const data = JSON.parse(event.data);

    switch (data.eventType) {
    case 'asgard.run.init':
    console.log('対話の開始, runId:', data.fact.runInit?.runId);
    document.getElementById('status').textContent = '考えています...';
    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 = '完了';
    source.close();
    break;
    }
    } catch (e) {
    console.error('イベントの解析に失敗しました', e);
    }
    };

    source.onerror = (err) => {
    console.error('SSE の接続エラー', err);
    source.close();
    };
  3. JavaScript fetch + ReadableStream の例​

    fetch と ReadableStream を使うと Asgard の POST API を直接呼べます。フロントエンドでもっともよく使われる方法です。

    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');

    // 最後の行は未完成のことがあるので残す
    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('ストリーミング開始');
    break;

    case 'asgard.message.delta': {
    const delta = event.fact.messageDelta.message.text;
    fullText += delta;
    // コールバックで UI を更新する(タイプライター風の表示)
    onDelta?.(delta, fullText);
    break;
    }

    case 'asgard.message.complete':
    // complete イベントの全文を使い、内容をそろえる
    fullText = event.fact.messageComplete.message.text;
    break;

    case 'asgard.run.done':
    onComplete?.(fullText);
    return fullText;
    }
    } catch (e) {
    // JSON でない行は無視する
    }
    }
    }

    return fullText;
    }

    // 使用例(DOM を操作してタイプライター風に表示する)
    const outputEl = document.getElementById('output');

    streamMessage(
    'channel-001',
    'Asgard プラットフォームの主な機能を教えてください',
    (delta, accumulated) => {
    // delta が届くたびに画面を更新する
    outputEl.textContent = accumulated;
    },
    (finalText) => {
    console.log('ストリーミング終了、応答全体:', finalText);
    }
    );
  4. Python sseclient の例​

    import requests
    import json
    import os

    # 依存パッケージのインストール: 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("ストリーミング開始...")

    elif event_type == "asgard.message.delta":
    delta = data["fact"]["messageDelta"]["message"]["text"]
    full_text += delta
    # 届いた順に出力する(タイプライター風の表示)
    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("\nストリーミング終了")
    break

    except json.JSONDecodeError:
    pass

    return full_text


    if __name__ == "__main__":
    result = stream_message("channel-001", "Asgard プラットフォームの主な機能を教えてください")
    print(f"\n応答全体:\n{result}")
  5. React Hook でのストリーミング​

    ストリーミングの処理をまとめたカスタム React Hook です。React のコンポーネントからそのまま使えます。

    import { useState, useCallback, useRef } from 'react';

    const BASE_URL = 'https://api.asgard-ai.com';
    const NAMESPACE = 'your-namespace';
    const BOT_PROVIDER = 'your-bot-provider';

    /**
    * Asgard の SSE ストリーミング応答を扱うカスタム Hook
    */
    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) => {
    // 実行中の前のリクエストを取り消す
    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 };
    }

    // 使用例のコンポーネント
    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 ? '考えています...' : '入力を待っています')}
    {isStreaming && <span className="cursor">|</span>}
    </div>

    {error && <div className="error">エラー: {error}</div>}

    <form onSubmit={handleSubmit}>
    <input
    value={input}
    onChange={(e) => setInput(e.target.value)}
    placeholder="メッセージを入力..."
    disabled={isStreaming}
    />
    <button type="submit" disabled={isStreaming || !input.trim()}>
    送信
    </button>
    {isStreaming && (
    <button type="button" onClick={stop}>
    停止
    </button>
    )}
    </form>
    </div>
    );
    }
  6. 次に読む​

始める準備はできましたか?

Asgard をチームでどう活用できるか、お気軽にご相談ください。

お問い合わせ