ストリーミング応答の処理
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>
);
}
次に読む
- メッセージを送る — 基本的な送信の例
- ナレッジベースへの問い合わせ — RAG のナレッジベースと組み合わせた質問応答
- SSE イベントの詳細 — イベント形式の完全なリファレンス