エージェント統合
任意のAIエージェントをHyperGenに接続 — Claude、OpenAI、Ollama、カスタムシステム。パターンは常に同じ — エージェントがHTMLを生成し、サーバーがSSE経由でストリーミング。
エージェント統合
HyperGenは設計上エージェント非依存です。HTML文字列を生成できるシステムなら何でも、HyperGenを通じてUIを生成できます。このガイドでは、いくつかの代表的なエージェントバックエンドとの統合パターンを示します。
普遍的なパターン
すべてのHyperGenエージェント統合は同じ3つのステップに従います:
- エージェントがHTML文字列を生成(インタラクティビティ用のHTMX属性とテーマ設定用の
--hg-*CSS変数付き) - サーバーがHTMLを非同期ジェネレーターにyield
createSSEStream()がSSE経由でiframeに配信
エージェント (任意) ──HTML文字列──> サーバー (非同期ジェネレーター) ──SSE──> iframe (HTMXがレンダリング)サーバーサイドの非同期ジェネレーターが統合ポイントです。その前はすべてエージェント固有で、その後はすべてHyperGenのドロップインが処理します。
Claude Agent SDK
この例はexamples/claude-agent/ディレクトリに基づいています。
パターン: Claude Agent SDKのquery()がSDKMessageオブジェクトをyieldします。アダプター関数が各メッセージをHTMLフラグメントに変換します。
import { createSSEStream, HyperGenResponse } from "./hypergen-server";
import { AgentSDK, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
const agent = new AgentSDK({
model: "claude-sonnet-4-20250514",
tools: [weatherTool, searchTool],
});
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
const stream = agent.query("Show me the weather in Tokyo");
for await (const message of stream) {
const html = sdkMessageToHtml(message);
if (html) yield html;
}
}),
);
});アダプター関数がSDKメッセージタイプをHTMLにマッピングします:
function sdkMessageToHtml(message: SDKMessage): string | null {
switch (message.type) {
case "assistant": {
// アシスタントのレスポンスからテキストブロックを抽出
const textBlocks = message.message.content.filter(
(block) => block.type === "text",
);
if (textBlocks.length === 0) return null;
const text = textBlocks.map((b) => b.text).join("");
return `<div style="
border-left: 3px solid var(--hg-accent, #4f46e5);
padding: 12px 16px;
margin: 8px 0;
background: var(--hg-surface-elevated, #f9fafb);
border-radius: var(--hg-radius, 8px);
line-height: 1.6;
">${text}</div>`;
}
case "result": {
if (message.subtype === "success") {
return `<div style="
background: var(--hg-surface-elevated, #f9fafb);
padding: 16px;
border-radius: var(--hg-radius, 8px);
border: 1px solid var(--hg-border, #e5e7eb);
">${message.result}</div>`;
}
return `<div style="color: var(--hg-error, #ef4444);">
Agent error: ${message.subtype}
</div>`;
}
default:
return null;
}
}ツール結果をインタラクティブHTMLに
ツールの結果をHTMX属性付きのインタラクティブHTMLに変換することで、真の力が発揮されます:
function renderWeatherCard(data: WeatherData): string {
return `<div id="weather-${data.city}" style="
background: var(--hg-surface-elevated);
border: 1px solid var(--hg-border);
border-radius: var(--hg-radius);
padding: 16px;
margin: 8px 0;
">
<h3 style="color: var(--hg-text);">${data.city}</h3>
<p style="font-size: 36px; font-weight: bold; color: var(--hg-accent);">
${data.temperature}°C
</p>
<p style="color: var(--hg-text-muted);">${data.conditions}</p>
<button
hx-post="/api/action"
hx-vals='{"action": "refresh_weather", "city": "${data.city}"}'
hx-target="#weather-${data.city}"
hx-swap="outerHTML"
style="
background: var(--hg-accent);
color: var(--hg-accent-fg);
border: none;
padding: 8px 16px;
border-radius: var(--hg-radius-sm);
cursor: pointer;
margin-top: 8px;
">
Refresh
</button>
</div>`;
}ユーザーが「Refresh」をクリックすると、HTMXが/api/actionにPOSTし、サーバーがエージェントを再度呼び出し(または最新データを取得し)、HTMXがスワップする新しいHTMLを返します。
OpenAI互換API
OpenAI、AnthropicのMessages API、または任意のOpenAI互換エンドポイント(Groq、Togetherなど)向け:
import { createSSEStream, HyperGenResponse } from "./hypergen-server";
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-4o",
messages: [
{
role: "system",
content: `You are a UI generator. Output valid HTML fragments styled with CSS variables:
--hg-surface, --hg-surface-elevated, --hg-text, --hg-text-muted,
--hg-accent, --hg-accent-fg, --hg-border, --hg-radius, --hg-space-4.
Use HTMX attributes (hx-post, hx-target, hx-swap) for interactivity.
Respond with ONLY HTML, no markdown, no explanation.`,
},
{
role: "user",
content: "Show me a dashboard with a counter and a task list",
},
],
}),
});
const data = await response.json();
const html = data.choices[0].message.content;
yield html;
}),
);
});トークン単位のストリーミング
リアルタイムのプログレッシブレンダリングには、OpenAIのストリーミングAPIを使用し、完全なフラグメントが得られるまでHTMLを蓄積します:
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-4o",
stream: true,
messages: [
{ role: "system", content: "Output HTML styled with --hg-* CSS variables." },
{ role: "user", content: "Generate a welcome card" },
],
}),
});
let buffer = "";
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split("\n")) {
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
const json = JSON.parse(line.slice(6));
const delta = json.choices[0]?.delta?.content;
if (delta) buffer += delta;
}
}
// 完成したHTMLフラグメントをyield
if (buffer) yield buffer;
}),
);
});フラグメント境界
タイプライター効果を出すために到着した部分的なHTMLをyieldする(swapStrategy: "beforeend"を使用)か、完全なレスポンスをバッファリングして一度にyieldしてクリーンなレンダリングにするか選べます。正しい選択はUXによります。
ローカルLLM(Ollama)
OllamaはLLMをローカルで実行し、OpenAI互換APIを公開します:
import { createSSEStream, HyperGenResponse } from "./hypergen-server";
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
const response = await fetch("http://localhost:11434/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3",
prompt: `Generate an HTML dashboard card styled with these CSS variables:
var(--hg-surface-elevated), var(--hg-text), var(--hg-accent),
var(--hg-border), var(--hg-radius), var(--hg-space-4).
Use hx-post="/api/action" for interactive buttons.
Output ONLY valid HTML, no markdown.`,
stream: false,
}),
});
const data = await response.json();
yield data.response;
}),
);
});または、OllamaのOpenAI互換エンドポイントをドロップインで使用:
// OpenAIの例と同じコード、URLとモデルを変更するだけ
const response = await fetch("http://localhost:11434/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3",
messages: [
{ role: "system", content: "Output HTML styled with --hg-* CSS variables." },
{ role: "user", content: "Generate a task list" },
],
}),
});カスタムエージェント(非LLM)
HyperGenはLLMを必要としません。HTMLを生成できるシステムなら何でも「エージェント」になれます — ルールベースエンジン、テンプレートシステム、データベース駆動のジェネレーターなど。
ルールベースシステム
import { createSSEStream, HyperGenResponse } from "./hypergen-server";
interface Alert {
severity: "info" | "warning" | "critical";
message: string;
timestamp: Date;
}
function alertToHtml(alert: Alert): string {
const colors = {
info: "var(--hg-accent, #3b82f6)",
warning: "var(--hg-warning, #f59e0b)",
critical: "var(--hg-error, #ef4444)",
};
return `<div style="
border-left: 4px solid ${colors[alert.severity]};
background: var(--hg-surface-elevated);
padding: 12px 16px;
margin: 8px 0;
border-radius: var(--hg-radius);
">
<strong style="color: ${colors[alert.severity]};">
${alert.severity.toUpperCase()}
</strong>
<p style="color: var(--hg-text); margin: 4px 0 0;">${alert.message}</p>
<small style="color: var(--hg-text-muted);">
${alert.timestamp.toLocaleTimeString()}
</small>
<button
hx-post="/api/action"
hx-vals='{"action": "acknowledge", "id": "${alert.timestamp.getTime()}"}'
hx-target="closest div"
hx-swap="outerHTML"
style="
display: block; margin-top: 8px;
background: none; border: 1px solid var(--hg-border);
padding: 4px 12px; border-radius: var(--hg-radius-sm);
cursor: pointer; color: var(--hg-text-muted);
">
Acknowledge
</button>
</div>`;
}
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
// 監視システムからアラートをストリーミング
for await (const alert of monitoringSystem.subscribe()) {
yield alertToHtml(alert);
}
}),
);
});データベース駆動ジェネレーター
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
const records = await db.query("SELECT * FROM products LIMIT 10");
yield `<h2 style="color: var(--hg-text); margin-bottom: var(--hg-space-4);">
Products (${records.length})
</h2>`;
for (const record of records) {
yield `<div style="
display: flex; justify-content: space-between; align-items: center;
padding: var(--hg-space-2) var(--hg-space-4);
border-bottom: 1px solid var(--hg-border);
">
<div>
<strong style="color: var(--hg-text);">${record.name}</strong>
<p style="color: var(--hg-text-muted); margin: 0;">$${record.price}</p>
</div>
<button
hx-post="/api/action"
hx-vals='{"action": "view_product", "id": "${record.id}"}'
hx-target="#product-detail"
hx-swap="innerHTML"
style="background: var(--hg-accent); color: var(--hg-accent-fg); border: none; padding: 6px 12px; border-radius: var(--hg-radius-sm); cursor: pointer;">
View
</button>
</div>`;
}
yield `<div id="product-detail" style="margin-top: var(--hg-space-4);"></div>`;
}),
);
});ベストプラクティス
LLMエージェント用のシステムプロンプト
LLMを使用する場合、システムプロンプトに以下の指示を含めてください:
You are a UI generator. Output valid HTML fragments with:
- CSS variables for theming: var(--hg-surface), var(--hg-text), var(--hg-accent), etc.
- HTMX attributes for interactivity: hx-post, hx-target, hx-swap, hx-vals
- Self-contained fragments (include <style> blocks if needed)
- No markdown, no code fences, no explanation — just HTMLエージェント出力のバリデーション
LLMは不正なHTMLを生成する可能性があります。バリデーションステップの追加を検討してください:
function sanitizeHtml(html: string): string {
// LLMが時々追加するマークダウンのコードフェンスを除去
return html
.replace(/^```html?\n?/i, "")
.replace(/\n?```$/i, "")
.trim();
}
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
const raw = await agent.generate("Create a card");
yield sanitizeHtml(raw);
}),
);
});エラーハンドリング
エージェント呼び出しをtry/catchで囲み、エラーUIをyieldします:
createSSEStream(async function* () {
try {
for await (const fragment of agent.stream()) {
yield fragment;
}
} catch (error) {
yield `<div style="
background: var(--hg-surface-elevated);
border-left: 4px solid var(--hg-error, #ef4444);
padding: 12px 16px;
border-radius: var(--hg-radius);
color: var(--hg-text);
">
<strong>Something went wrong</strong>
<p style="color: var(--hg-text-muted); margin-top: 4px;">
${error instanceof Error ? error.message : "Unknown error"}
</p>
<button
hx-get="/api/stream"
hx-target="#hg-root"
hx-swap="innerHTML"
style="margin-top: 8px; background: var(--hg-accent); color: var(--hg-accent-fg); border: none; padding: 8px 16px; border-radius: var(--hg-radius-sm); cursor: pointer;">
Retry
</button>
</div>`;
}
});