メインコンテンツまでスキップ
Asgard の SSE イベントを処理し、リアルタイムのタイプライター風表示を作る完全な例

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

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

SSE のイベント種別​

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

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();
};

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

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}")

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

次に読む​