2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オープンソースAIアプリビルダー「Giselle」の技術スタック

Posted at

はじめに

Giselleは、ROUTE06が開発するAIアプリビルダーです。ノードベースのビジュアルインターフェースを通じて、複雑なAIワークフローを直感的に構築できることが特徴です。ソースコードはgithub.com/giselles-ai/giselleでオープンソースとして公開しています。

本記事では、Giselleの技術スタックを詳しく解説します。モノレポ構成、フロントエンド設計、バックエンド実装、そしてマルチLLM対応アーキテクチャについて、実際のコードを交えながら紹介していきます。

これからAIプロダクトを開発する方や、モダンなTypeScriptプロジェクトの設計パターンに興味がある方の参考になれば幸いです。

技術スタック全体像

Giselleで採用している主要な技術を一覧でまとめます。

カテゴリ 技術
ビルドシステム Turborepo + pnpm
フレームワーク Next.js + React
言語 TypeScript
スタイリング Tailwind CSS
状態管理 Zustand
UIコンポーネント Radix UI + Lucide React
ワークフローUI XYFlow (React Flow)
リッチテキスト TipTap
データベース PostgreSQL + Drizzle ORM
ベクトル検索 pgVector
認証 Supabase Auth
AI SDK Vercel AI SDK
LLMプロバイダ OpenAI / Anthropic / Google
可観測性 Langfuse
ホスティング Vercel

モノレポ構成

ワークスペース設計

Giselleは、Turborepo + pnpmによるモノレポ構成を採用しています。pnpm-workspace.yamlでは4つのワークスペースを定義しています。

packages:
  - apps/*           # アプリケーション
  - internal-packages/*  # 内部UIパッケージ
  - packages/*       # 公開SDKパッケージ
  - tools/*          # ビルドツール

packages/配下には26個のSDKパッケージがあり、それぞれが明確な責務を持っています。

packages/
├── protocol/           # コアデータ型定義
├── language-model/     # LLM抽象化レイヤー
├── language-model-registry/  # プロバイダ管理
├── langfuse/           # 可観測性統合
├── rag/                # RAG実装
├── stream/             # ストリーミング
├── text-editor/        # TipTapラッパー
├── github-tool/        # GitHub連携
└── ...

catalogによる依存関係の集中管理

pnpm workspaceのcatalog機能を活用し、すべての依存関係バージョンをpnpm-workspace.yamlで一元管理しています。

catalog:
  '@ai-sdk/anthropic': 2.0.49
  '@ai-sdk/google': 2.0.49
  '@ai-sdk/openai': 2.0.72
  '@xyflow/react': 12.9.0
  drizzle-orm: 0.44.2
  next: 16.0.10
  react: 19.2.1
  typescript: 5.7.3
  zustand: 5.0.8

各パッケージのpackage.jsonではcatalog:プレフィックスで参照します。

{
  "devDependencies": {
    "@biomejs/biome": "catalog:",
    "typescript": "catalog:"
  }
}

この方式には以下のメリットがあります。

  • バージョンの一貫性: モノレポ全体で同一バージョンを使用
  • 更新の容易さ: 1箇所の変更で全パッケージに反映
  • 依存関係の可視化: 使用ライブラリが一覧で把握可能

また、minimumReleaseAge: 1440(24時間)を設定し、リリース直後のパッケージの自動採用を防いでいます。セキュリティ修正が必要な場合はminimumReleaseAgeExcludeで個別に例外を設けます。

Turborepoによるビルド最適化

turbo.jsonでタスク間の依存関係を定義し、並列実行とキャッシュを活用しています。

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "out/**"]
    },
    "dev": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    },
    "check-types": {
      "dependsOn": ["^check-types"]
    }
  }
}

dependsOn: ["^build"]により、依存先パッケージのビルドが先に完了することを保証しつつ、独立したパッケージは並列でビルドされます。

ルートのpackage.jsonには以下のようなスクリプトが定義されています。

{
  "scripts": {
    "build": "turbo build",
    "build-sdk": "turbo build --filter '@giselles-ai/*'",
    "check-types": "turbo check-types",
    "format": "biome check --write .",
    "tidy": "knip --no-config-hints"
  }
}

knipによる未使用依存の検出、biomeによるフォーマット・リントを組み合わせ、コード品質を維持しています。

フロントエンド設計

XYFlowによるビジュアルワークフロー

GiselleのワークフローエディタはXYFlow(React Flow)で構築されています。ノードベースのグラフUIを提供し、AIエージェント間の接続を視覚的に表現します。

import {
  ReactFlow,
  type Connection,
  type Edge,
  type OnNodesChange,
  useReactFlow,
} from "@xyflow/react";

function V2NodeCanvas() {
  const { nodes, connections } = useAppDesignerStore((s) => ({
    nodes: s.nodes,
    connections: s.connections,
  }));

  const nodeTypes = useMemo(
    () => ({
      card: CardXyFlowNode,
      pill: PillXyFlowNode,
    }),
    [],
  );

  const handleConnect = useCallback((connection: Connection) => {
    const outputNode = nodes.find((n) => n.id === connection.source);
    const inputNode = nodes.find((n) => n.id === connection.target);

    const supported = isSupportedConnection(outputNode, inputNode);
    if (!supported.canConnect) {
      toast.error(supported.message);
      return;
    }
    connectNodes(outputNode.id, inputNode.id);
  }, [nodes, connectNodes]);

  return (
    <ReactFlow
      nodes={reactFlowNodes}
      edges={edges}
      nodeTypes={nodeTypes}
      onConnect={handleConnect}
      colorMode="dark"
    >
      <Background />
      <Panel position="bottom-center">
        <Toolbar />
      </Panel>
    </ReactFlow>
  );
}

XYFlowの採用理由は以下の通りです。

  • 成熟したエコシステム: 多くの実績とコミュニティ
  • カスタマイズ性: ノードタイプ、エッジスタイルを完全制御可能
  • パフォーマンス: 仮想化による大規模グラフのサポート

TipTapによるリッチテキストエディタ

AIエージェントへの指示入力には、TipTapを採用しています。プレーンテキストではなく、リッチテキストとメンション機能を組み合わせることで、他ノードの出力を参照する直感的なUIを実現しています。

import Mention from "@tiptap/extension-mention";
import Placeholder from "@tiptap/extension-placeholder";
import { EditorProvider, useCurrentEditor } from "@tiptap/react";

export function TextEditor({
  value,
  onValueChange,
  connections,
  placeholder,
}) {
  const extensions = useMemo(() => {
    const mentionExtension = Mention.configure({
      suggestion: createSuggestion(connections),
    });

    return [
      ...baseExtensions,
      SourceExtensionReact.configure({ nodes }),
      mentionExtension,
      Placeholder.configure({ placeholder }),
    ];
  }, [connections, placeholder]);

  return (
    <EditorProvider
      extensions={extensions}
      content={value ? JSON.parse(value) : undefined}
      onUpdate={(p) => {
        onValueChange?.(JSON.stringify(p.editor.getJSON()));
      }}
    />
  );
}

TipTapを選んだ理由は以下です。

  • 拡張性: Mention、Placeholderなど豊富な拡張機能
  • ProseMirror基盤: 堅牢なドキュメントモデル
  • JSON出力: 構造化データとして保存・処理が容易

Zustandによる状態管理

グローバル状態の管理にはZustandを採用しています。Reduxと比較してボイラープレートが少なく、TypeScriptとの親和性が高い点が決め手でした。

import { create } from "zustand";

type TaskOverlayState = {
  isVisible: boolean;
  overlayApp: OverlayAppSummary | null;
  overlayInput: GenerationContextInput | null;
  showOverlay: (payload: {
    app: OverlayAppSummary;
    input?: GenerationContextInput | null;
  }) => void;
  hideOverlay: () => void;
};

export const useTaskOverlayStore = create<TaskOverlayState>((set) => ({
  isVisible: false,
  overlayApp: null,
  overlayInput: null,
  showOverlay: ({ app, input = null }) =>
    set({
      isVisible: true,
      overlayApp: app,
      overlayInput: input,
    }),
  hideOverlay: () => set({ isVisible: false, overlayApp: null, overlayInput: null }),
}));

コンポーネントからはuseShallowと組み合わせて必要な部分だけを購読します。

const { nodes, connections } = useAppDesignerStore(
  useShallow((s) => ({
    nodes: s.nodes,
    connections: s.connections,
  }))
);

バックエンド設計

Drizzle ORMによる型安全なDB操作

データベースアクセスにはDrizzle ORMを採用しています。TypeScriptファーストの設計で、スキーマ定義からクエリまで一貫した型安全性を提供します。

import {
  pgTable,
  serial,
  text,
  timestamp,
  integer,
  index,
  unique,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

export const teams = pgTable("teams", {
  id: text("id").$type<TeamId>().notNull().unique(),
  dbId: serial("db_id").primaryKey(),
  name: text("name").notNull(),
  plan: text("plan").$type<TeamPlan>().notNull().default("free"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at")
    .defaultNow()
    .notNull()
    .$onUpdate(() => new Date()),
});

export const teamRelations = relations(teams, ({ many }) => ({
  apps: many(apps),
  tasks: many(tasks),
}));

export const workspaces = pgTable("workspaces", {
  id: text("id").$type<WorkspaceId>().notNull().unique(),
  dbId: serial("db_id").primaryKey(),
  name: text("name"),
  teamDbId: integer("team_db_id")
    .notNull()
    .references(() => teams.dbId, { onDelete: "cascade" }),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

$type<T>()によるブランド型の適用で、IDの取り違えをコンパイル時に防止できます。TeamIdWorkspaceIdは別の型として扱われるため、誤った代入はエラーになります。

pgVectorによるベクトル埋め込み検索

RAG(Retrieval Augmented Generation)機能のため、PostgreSQLのpgVector拡張を活用しています。ドキュメントやGitHubリポジトリのコンテンツをベクトル化し、類似検索を実現します。

import { vectorWithoutDimensions } from "./custom-types";
import { sql } from "drizzle-orm";

export const documentEmbeddings = pgTable(
  "document_embeddings",
  {
    dbId: serial("db_id").primaryKey(),
    documentVectorStoreDbId: integer("document_vector_store_db_id")
      .notNull()
      .references(() => documentVectorStores.dbId, { onDelete: "cascade" }),
    embeddingProfileId: integer("embedding_profile_id")
      .$type<EmbeddingProfileId>()
      .notNull(),
    embeddingDimensions: integer("embedding_dimensions")
      .$type<EmbeddingDimensions>()
      .notNull(),
    chunkContent: text("chunk_content").notNull(),
    embedding: vectorWithoutDimensions("embedding").notNull(),
  },
  (table) => [
    // 1536次元用のHNSWインデックス(OpenAI text-embedding-3-small)
    index("doc_embs_embedding_1536_idx")
      .using("hnsw", sql`(${table.embedding}::vector(1536)) vector_cosine_ops`)
      .where(sql`${table.embeddingDimensions} = 1536`),
    // 3072次元用のHNSWインデックス(OpenAI text-embedding-3-large)
    index("doc_embs_embedding_3072_idx")
      .using("hnsw", sql`(${table.embedding}::halfvec(3072)) halfvec_cosine_ops`)
      .where(sql`${table.embeddingDimensions} = 3072`),
  ],
);

部分インデックスを活用し、埋め込み次元ごとに最適化されたHNSWインデックスを作成しています。3072次元にはhalfvec(半精度浮動小数点)を使用し、ストレージ効率とパフォーマンスのバランスを取っています。

マルチLLM対応アーキテクチャ

Language Model Registry

Giselleの差別化ポイントの一つが、複数のLLMプロバイダを統一的なインターフェースで扱える設計です。@giselles-ai/language-model-registryパッケージで、各プロバイダのモデルを定義・管理しています。現在Private Previewとして提供しており、近日中にPublicリリースを予定しています。

// packages/language-model-registry/src/language-models.ts
import { anthropic } from "./anthropic";
import { google } from "./google";
import { openai } from "./openai";

export const languageModels = [
  ...Object.values(openai),
  ...Object.values(anthropic),
  ...Object.values(google),
];

export function getEntry(languageModelId: LanguageModelId) {
  const languageModel = languageModels.find(
    (model) => model.id === languageModelId
  );
  if (!languageModel) {
    throw new Error(`Language model with ID ${languageModelId} not found`);
  }
  return languageModel;
}

各プロバイダのモデル定義は、defineLanguageModel関数で統一的に記述します。

// packages/language-model-registry/src/anthropic.ts
import * as z from "zod/v4";
import { defineLanguageModel, definePricing } from "./language-model";

const anthropicProvider = {
  id: "anthropic",
  title: "Anthropic",
  metadata: {
    website: "https://www.anthropic.com/",
    documentationUrl: "https://platform.claude.com/docs/en/api/overview",
  },
} as const;

export const anthropic = {
  "anthropic/claude-sonnet-4.5": defineLanguageModel({
    provider: anthropicProvider,
    id: "anthropic/claude-sonnet-4.5",
    name: "Claude Sonnet 4.5",
    description: "...",
    contextWindow: 200_000,
    maxOutputTokens: 64_000,
    pricing: {
      input: definePricing(3.0),
      output: definePricing(15.0),
    },
    requiredTier: "pro",
    configurationOptions: {
      temperature: {
        description: "Amount of randomness injected into the response.",
        schema: z.number().min(0).max(1),
        ui: { min: 0.0, max: 1.0, step: 0.1 },
      },
      thinking: {
        description: "Whether to include reasoning text in the response.",
        schema: z.boolean(),
      },
    },
    defaultConfiguration: {
      temperature: 1.0,
      thinking: false,
    },
  }),
  // 他のモデル...
};

この設計により、新しいモデルやプロバイダの追加が容易になります。Zodスキーマで設定オプションを定義することで、UIの動的生成と入力バリデーションを両立しています。

Langfuseによる可観測性

LLMアプリケーションの運用において、可観測性は重要です。GiselleではLangfuseを統合し、すべてのLLM呼び出しをトレースしています。

// packages/langfuse/src/trace-generation.ts
import { Langfuse } from "langfuse";
import { calculateDisplayCost } from "@giselles-ai/language-model";

export async function traceGeneration(args: {
  generation: CompletedGeneration | FailedGeneration;
  inputMessages: ModelMessage[];
  userId?: string;
}) {
  const langfuse = new Langfuse();

  const trace = langfuse.trace({
    name: "generation",
    userId: args.userId,
    input: await prepareLangfuseInput(args.inputMessages),
  });

  const usage = args.generation.usage ?? {
    inputTokens: 0,
    outputTokens: 0,
    totalTokens: 0,
  };

  const cost = await calculateDisplayCost(
    llm.provider,
    llm.id,
    { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens }
  );

  trace.generation({
    name: "generateText",
    model: llm.id,
    input: langfuseInput,
    output: args.generation.outputs,
    usage: {
      unit: "TOKENS",
      input: usage.inputTokens,
      output: usage.outputTokens,
      inputCost: cost.inputCostForDisplay,
      outputCost: cost.outputCostForDisplay,
    },
    startTime: new Date(args.generation.startedAt),
    endTime: new Date(args.generation.completedAt),
  });

  await langfuse.flushAsync();
}

トレースには以下の情報が記録されます。

  • 入出力: プロンプトとレスポンスの全文
  • トークン使用量: 入力・出力トークン数
  • コスト: モデルごとの料金計算結果
  • レイテンシ: 開始・終了時刻
  • メタデータ: 使用ツール、エラー情報など

プロバイダごとの機能(Anthropicのthinking、Googleのsearch grounding等)もタグとして記録され、後から分析可能です。

まとめ

Giselleの技術スタックについて解説しました。主なポイントを振り返ります。

モノレポ構成

  • Turborepo + pnpmによる効率的なビルド
  • catalogによる依存関係の一元管理
  • 26個のパッケージによる責務の分離

フロントエンド

  • XYFlowによるノードベースUI
  • TipTapによるリッチテキスト入力
  • Zustandによるシンプルな状態管理

バックエンド

  • Drizzle ORMによる型安全なDB操作
  • pgVectorによるベクトル検索

AI/LLM

  • Language Model Registryによるマルチプロバイダ対応
  • Langfuseによる可観測性確保

GiselleはApache License 2.0のオープンソースプロジェクトです。コードはGitHubで公開されています。Issueやプルリクエストによる貢献も歓迎します。

本日、Product Hunt でローンチ 🚀

本日、Product Hunt でローンチしました!

2025年12月29日(月)17時から24時間、ぜひご覧いただき、応援よろしくお願いします!

Giselle - Build and run AI workflows. Open source. | Product Hunt

参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?