Genkit を Hono で利用する / Using Genkit with Hono
はじめに
Genkit は Firebase が提供する AI ワークフローの作成を支援するフレームワークです。
具体的には Google や Google Cloud の AI 機能のプラグインやテンプレート等をシンプルに抽象化してくれています。
公式のプラグインにないその他の AI サービスの機能を組み合わせたい場合も、抽象化合わせてコードを書くことで簡単に利用することができます。
また、モニタリングを便利にするための抽象化も行われているので、モダンな Web サービスに組み込むために必要な基本機能もあります。
個人的に Genkit が好きな理由としては、後発のフレームワークなだけあり TypeScript ファーストで Zod によりインプットとアウトプットを定義しスキーマから型補完までできるところです。
その、Genkit で作っていた AI ワークフローのスキーマを Web アプリケーションに組み込むときに同じく Zod によるスキーマで良い感じに型補完を効かせつつ開発のできる Hono で利用する方法について記載します。
Genkit Hono Plugin
Genkit には Express.js に組み込むためのプラグインが提供されていますが、Hono のプラグインは調査時点では見当たりませんでした。
そのため、ここでは上記のプラグインを参考に簡単に Genkit Hono Plugin を作成しました。
Genkit Hono Plugin コード
import { createFactory } from "hono/factory";
import { zValidator } from "@hono/zod-validator";
import type { Action, ActionContext, z } from "genkit";
import {
type ContextProvider,
type RequestData,
getCallableJSON,
getHttpStatus,
} from "genkit/context";
import { logger } from "genkit/logging";
import type { StatusCode } from "hono/utils/http-status";
const factory = createFactory();
export function honoHandler<
C extends ActionContext = ActionContext,
I extends z.ZodTypeAny = z.ZodTypeAny,
O extends z.ZodTypeAny = z.ZodTypeAny,
S extends z.ZodTypeAny = z.ZodTypeAny,
>(
action: Action<I, O, S>,
opts?: {
contextProvider?: ContextProvider<C, I>;
},
) {
return factory.createHandlers(
zValidator("json", action.__action.inputSchema as I),
async (c) => {
const { stream } = c.req.query();
const input = (await c.req.json()) as z.infer<I>;
let context: Record<string, unknown>;
try {
context =
(await opts?.contextProvider?.({
method: c.req.method as RequestData["method"],
headers: Object.fromEntries(
Object.entries(c.req.header).map(([key, value]) => [
key.toLowerCase(),
Array.isArray(value) ? value.join(" ") : String(value),
]),
),
input,
})) || {};
} catch (e: unknown) {
logger.error(
`Auth policy failed with error: ${(e as Error).message}\n${(e as Error).stack}`,
);
c.status(getHttpStatus(e) as StatusCode);
return c.json(getCallableJSON(e));
}
if (c.req.header("Accept") === "text/event-stream" || stream === "true") {
throw new Error("Streaming is not supported"); // reason: hono/client cannot complete the type
}
try {
const result = await action.run(input, { context });
c.header("x-genkit-trace-id", result.telemetry.traceId);
c.header("x-genkit-span-id", result.telemetry.spanId);
// Responses for non-streaming flows are passed back with the flow result stored in a field called "result."
c.status(200);
const parsed = action.__action.outputSchema?.safeParse(result.result);
console.log("parsed", parsed);
if (parsed?.success) {
return c.json(parsed.data as z.infer<O>);
}
logger.error(parsed?.error);
return c.json(getCallableJSON(parsed?.error));
} catch (e) {
// Errors for non-streaming flows are passed back as standard API errors.
logger.error(
`Non-streaming request failed with error: ${(e as Error).message}\n${(e as Error).stack}`,
);
c.status(getHttpStatus(e) as StatusCode);
return c.json(getCallableJSON(e));
}
},
);
}
プラグインは以下のような形で利用します。
import { genkit } from "genkit";
import { Hono } from "hono";
import { honoHandler } from "./genkit-hono";
const ai = genkit({...});
const app = new Hono();
const echo = ai.defineFlow(
{
name: "echo",
inputSchema: z.string(),
outputSchema: z.object({
text: z.string(),
}),
},
async (input) => {
logger.debug(`Echoing: ${input}`);
return {
text: input,
};
},
);
const echo = app.post("/echo", ...honoHandler(echoFlow));
export type ServerApp = typeof echo // | typeof echo2 複数ある場合は Union タイプにする。
// 任意のプラットフォームで app を実行する (以下は Bun の例)
Bun.serve({
fetch: app.fetch,
});
import { hc } from "hono/client";
import type { ServerApp } from "./server";
(async () => {
const client = hc<ServerApp>(`http://localhost:3000`);
const res = await client.echo.$post({
json: "Hello, World!",
});
if (res.ok) {
const data = await res.json();
console.log("res.ok", data);
}
})();
以下のように VS Code で型が補完されていることを確認できました。
まとめ
以上、簡単ですが Genkit を Hono で利用する方法でした。
なお、ストリーミングレスポンスを使うと hono/client で型補完をうまく扱えなかったのと、僕のユースケースでは不要だったため未実装となっています。
Genkit の Node.js もコア部分は安定していそうですが、公式プラグインでも新しいモデルの機能を使おうとするとうまくいかないことも多いので、現時点で使う際は Genkit のコードを追いつつ足りない機能は自分か AI で実装することが前提になりそうな所感でした。
Discussion