コンテンツにスキップ

高度なライブラリ (Advanced Libraries)

大半のライブラリは griptape_nodes_library.json マニフェストによって完全に記述されます:エンジンがそれを読み取り、指定されたノードモジュールをインポートして、それらのクラスを登録します。高度なライブラリ (Advanced Library) は、自身のライフサイクルの 5 つの時点(ノード読み込み前、読み込み後、登録解除前、エンジンがリクエストハンドラーを収集するとき、およびディスパッチ後フックを収集するとき)でコードを実行できるようにするオプションの Python クラスです。

本ページはそのクラスの開発者向けリファレンスです。マニフェスト自体(メタデータ、カテゴリ、宣言、依存関係管理)については ライブラリの作成 を参照してください。

高度なライブラリを作成すべきか? (Should I write one?)

マニフェストに列挙できるファイル内の固定されたノードクラスのセットであれば、作成する必要はありません。これが一般的なケースであり、ノード以外の追加 Python コードは不要です。

以下の要件がある場合に 作成してください:

  • ライブラリのロード時およびアンロード時に プロセス全体のリソースを取得または解放 する必要がある場合: GPU コンテキスト、ネイティブ SDK への Python バインディング、バックグラウンドスレッド、コネクションプールなど。
  • セットがデータ駆動型または自動生成されるため、マニフェストに記載されていないノード型を登録 する必要がある場合。マニフェストに記載せずにノード型を登録する を参照。
  • ライブラリが独自に所有する リクエスト型を処理 (Serve) し、他のライブラリやノードから呼び出せるようにする場合。get_request_handlers を参照。
  • ワークフローが保存されるたびに独自のステップを実行するなど、エンジンが所有するリクエストの処理後に反応 する場合。get_post_dispatch_hooks を参照。
  • ワークフローパブリッシャーなど、エンジンのリクエスト型に対する 競合プロバイダーを登録 する場合。パブリッシング を参照。

配線と接続 (Wiring one up)

マニフェストからの相対パスで、advanced_library_path を使用して Python ファイルを指定します:

{
  "name": "My Library",
  "advanced_library_path": "advanced_library.py",
  "nodes": []
}

次に、そのファイル内で AdvancedNodeLibrary を継承し、必要なフックのみをオーバーライドします。すべてのフックにはデフォルトの no-op(何もしない)実装が用意されています:

from griptape_nodes.node_library.advanced_node_library import AdvancedNodeLibrary


class MyLibrary(AdvancedNodeLibrary):
    def after_library_nodes_loaded(self, library_data, library) -> None:
        print(f"Loaded {len(library.get_registered_nodes())} nodes")

エンジンがクラスを検出して構築する方法を規定する 3 つのルールがあります。いずれかに違反するとライブラリ全体のロードが失敗します:

  • クラスはそのファイル内で直接定義されている必要があります。 エンジンはモジュールをスキャンし、__module__ がインポートしたばかりのモジュールと一致する AdvancedNodeLibrary サブクラスを探します。ファイル内に インポート されたサブクラスはスキップされます。実装をパッケージ内に配置したい場合でも、マニフェストが指定したファイル内でサブクラス化してください。
  • 最初に見つかったものが優先されます。 エンジンはモジュールの順序で最初に見つかった適格なサブクラスを採用し、停止します。曖昧さを避けるため、定義するサブクラスは 1 つだけにしてください。
  • __init__ は必須引数を取ってはなりません。 エンジンは引数なしでクラスをインスタンス化します。必要な状態はすべて __init__ 内またはフック内で導出してください。

モジュールのインポートに失敗した場合、適格なサブクラスが含まれていない場合、またはインスタンス化できない場合、ライブラリは UNUSABLE とマークされ、登録に失敗し、エディタに根本原因のエラーを含む AdvancedLibraryLoadFailureProblem が表示されます。

ロードシーケンス (The load sequence)

各フックがどの位置で実行されるかを理解することは、フックが期待通りに動作するか、あるいは何もしないまま終わるかを分ける重要なポイントです。ライブラリの登録は以下の順序で実行されます:

ステップ エンジンの動作
1 griptape_nodes_library.json を解析してバリデーションを実行
2 ライブラリディレクトリとその venv site-packages を sys.path に追加
3 高度なライブラリモジュールをインポートし、クラスをインスタンス化
4 LibraryRegistry に Library を登録
5 マニフェストが宣言するライブラリ設定を永続化
6 before_library_nodes_loaded を呼び出す
7 library_data.nodes を反復処理し、各ノード型を登録
8 マニフェストのウィジェットを登録
9 after_library_nodes_loaded を呼び出す
10 get_request_handlers を呼び出し、返されたハンドラーを登録
11 get_post_dispatch_hooks を呼び出し、返されたフックを登録
12 ライブラリの適合性(Fitness)を計算し、ライブラリを LOADED とマーク

把握しておくべき 2 つの帰結:

  • クラスが存在する(ステップ 3)のはライブラリが登録される(ステップ 4)前であるため、__init__ 内で自分自身を LibraryRegistry から検索することはできません。
  • ステップ 7 はステップ 6 が実行された 後 に library_data.nodes を読み取るため、動的なノード登録が可能になります。

登録解除(アンロード)は逆の順序で実行されます:

ステップ エンジンの動作
1 before_library_unregistered を呼び出す
2 ライブラリのアプリイベントリスナー、ディスパッチ前およびディスパッチ後フックを削除
3 登録されていたリクエストハンドラーを削除
4 ライブラリのウィジェットの登録を解除
5 LibraryRegistry からライブラリを削除

フック一覧 (The hooks)

before_library_nodes_loaded

def before_library_nodes_loaded(self, library_data: LibrarySchema, library: Library) -> None: ...

ライブラリが登録された後、ノード型が登録される前に実行されます。ノードのインポートが依存する前提条件をセットアップしたり、library_data.nodes にノード定義を追加したりするために使用します。

library_data は Library が保持する生の LibrarySchema であり、コピーではありません。ここで変更を加えると、ステップ 7 でエンジンがロードする内容や、その後に報告される内容が変化します。

ここで例外が発生した場合、エンジンは BeforeLibraryCallbackProblem を記録し、ロード処理を継続します。ライブラリは失敗(FAILED)ではなく欠陥あり(FLAWED)となるため、壊れたフックがあってもノード自体は動作するものの、セットアップ処理は実行されなかったという状態になります。フックが正常に完了したことを前提とした設計は避けてください。

after_library_nodes_loaded

def after_library_nodes_loaded(self, library_data: LibrarySchema, library: Library) -> None: ...

マニフェスト内のすべてのノード型が登録された後に実行されます。この時点で library.get_registered_nodes() は完全なリストを返すため、完成したライブラリを参照する必要がある処理はここで行います。

また、LibraryManager.on_register_event_handler() を介して競合プロバイダーのイベントハンドラーを登録する場所でもあります(ライブラリ自身をワークフローパブリッシャーとしてアドバタイズする場合など)。

エラー処理は before フックと同様です: AfterLibraryCallbackProblem が記録され、ロードは継続します。

before_library_unregistered

def before_library_unregistered(self, library_data: LibrarySchema, library: Library) -> None: ...

エンジンがリソースを破棄する前に実行されるため、実行中はリスナーやハンドラーがまだ登録された状態を維持しています。ロード時に取得したリソース(ネイティブバインディング、GPU コンテキスト、バックグラウンドスレッド、コネクションプールなど)を解放するために使用します。

ここでのエラーは ログ出力された上で握りつぶされます(swallowed)。破棄処理の失敗によってエンジンが停止するのを防ぐため、登録解除はそのまま継続します。裏を返せば、解放に失敗したリソースは暗黙的にリークすることになります。

get_request_handlers

def get_request_handlers(self) -> list[tuple[type[RequestPayload], Callable]]: ...

エンジンがあなたに代わって登録し、ライブラリのアンロード時に自動的に登録解除する (request_type, handler) のペアのリストを返します。同期ハンドラーと非同期ハンドラーの両方が動作します。これにより、ライブラリは自身のノード、他のライブラリ、および外部クライアントがすべて呼び出せるサービスを公開できます。

構築すべき 3 つの要素があります。コンテキスト全体については 実装例 を参照してください。

1. ペイロードを定義する。 リクエスト型と少なくとも 1 つの結果型を定義します。それぞれ PayloadRegistry に登録されたデータクラスであり、WebSocket や MCP 経由で名前によって解決できるようになります:

@dataclass
@PayloadRegistry.register
class ConvertColorspaceRequest(RequestPayload):
    color: tuple[float, float, float]
    source: str
    target: str


@dataclass
@PayloadRegistry.register
class ConvertColorspaceResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
    color: tuple[float, float, float]


@dataclass
@PayloadRegistry.register
class ConvertColorspaceResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
    pass

これらは、高度なライブラリとノードの両方がインポートする独自のモジュールに配置します。いずれかがロードされる時点ですでにライブラリディレクトリは sys.path に存在するため、通常の import colorspace_events が解決され、両方のファイルが同じモジュールオブジェクトと同じペイロードクラスを取得します。このモジュールには一意で明確な名前を付けてください(すべてのライブラリディレクトリが同じ sys.path に入るため、events.py のような一般的な名前だと他のライブラリのファイルと衝突する危険があります)。

2. フックからペアを返す。

def get_request_handlers(self) -> list[tuple[type[RequestPayload], Callable]]:
    return [(ConvertColorspaceRequest, self._handle_convert_colorspace)]

戻り値の型アノテーションには単純な Callable を指定してください。基底クラスは Callable[[RequestPayload], ResultPayload] を宣言していますが、具体的なリクエスト型でアノテーションされたハンドラーは、パラメータ型の反変性(contravariant)のためそれに直接代入できません。ハンドラー自体の型アノテーションを正確に保つことは、基底クラスのシグネチャと完全に一致させることよりも重要です。

3. リクエストをディスパッチする。 オーケストレータープロセス内の呼び出し元は、通常のイベントバスを介してリクエストを送信し、結果を受け取ります:

result = GriptapeNodes.handle_request(ConvertColorspaceRequest(color=(1.0, 0.0, 0.0), source="rgb", target="hsv"))
if result.failed():
    msg = f"Attempted to convert a color in '{self.name}'. Failed because {result.result_details}"
    raise RuntimeError(msg)

success = cast("ConvertColorspaceResultSuccess", result)

呼び出し元の 2 つのルール:

  • __init__ ではなく、必ず process からディスパッチしてください。 リクエストを送信するノードコンストラクタは reentrant-bus-in-init 厳格モードルールに違反し、エンジンの起動を待つハンドラーとの間でデッドロックを引き起こす可能性があります。詳細は 厳格モードリファレンス を参照してください。
  • 常に失敗を処理してください。 提供元のライブラリがインストールされていない、ロードに失敗した、あるいはアンロードされた可能性があります。そのような場合、リクエストにはハンドラーが一切存在せず、エンジンはライブラリ固有の失敗型ではなく一般的な失敗結果を返します。成功型に絞り込む前に必ず result.failed() をチェックしてください。

本機構の制約事項:

  • ライブラリ自身がそのリクエスト型を所有している必要があります。 自身のパッケージ内で RequestPayload サブクラスを定義してください。
  • リクエスト型ごとにエンジン全体で 1 つのハンドラーのみ。 すでにハンドラーを持つ型を登録しようとすると例外が発生し、RequestHandlerRegistrationProblem として報告されます。複数のライブラリが競合し、呼び出し元が名前で選択するようなリクエスト型の場合は、代わりに after_library_nodes_loaded 内で LibraryManager.on_register_event_handler() を使用してください。
  • オーケストレーター専用。 ワーカーサブプロセス内で分離実行されているライブラリは、ハンドラーをそのワーカー内に登録するため、オーケストレーターからは到達できません。リクエストは「No manager found」で失敗します。エンジンはロード時にこの組み合わせに対して RequestHandlersWorkerIncompatibleProblem の警告を発します。ワーカーによるノードの分離 を参照してください。

他のコードは、library.get_registered_request_handler_types() を使用してロード済みライブラリが何を公開しているかを検出し、dataclasses.fields() や typing.get_type_hints() を使用して各型を検査できます。

get_post_dispatch_hooks

def get_post_dispatch_hooks(self) -> list[tuple[type[RequestPayload], Callable]]: ...

エンジンがあなたに代わって登録し、ライブラリのアンロード時に解除する (request_type, callback) ペアのリストを返します。そのリクエスト型に対するエンジン自身のハンドラーが結果を生成した後、コールバックが (request, result) を引数として呼び出されます。これにより、監査ログへの追記、チャットチャンネルへの投稿、エクスポートのトリガーなど、自身が所有していないリクエスト型に対してエンジンで何かが発生した際に独自のステップを実行できます。

これは get_request_handlers の鏡のような関係であり、違いは「所有権」にあります:

get_request_handlers get_post_dispatch_hooks
リクエスト型の占有 あり — エンジン全体で 1 つのハンドラー なし — 任意の数のライブラリが同じ型をフック可能
実行タイミング エンジンハンドラーの 代わりに 実行 エンジンハンドラーが結果を生成した 後 に実行
結果の変更 可能(その処理が結果そのものになる) 不可(通知のみ)

これが、WorkflowManager がすでに所有しており get_request_handlers では登録に失敗する SaveWorkflowRequest をフックできる理由です。

同期および非同期コールバックの両方がサポートされています:

def get_post_dispatch_hooks(self):
    return [(SaveWorkflowRequest, self._on_workflow_saved)]


async def _on_workflow_saved(self, request: RequestPayload, result: ResultPayload) -> None:
    if not isinstance(result, SaveWorkflowResultSuccess):
        return
    await self._append_audit_line(result.file_path)

本機構の制約事項:

  • 通知専用。 戻り値は無視され、コールバックが結果を変更したり処理を失敗させたりすることはできません。発生した例外はログ出力されて無視され、同じリクエストに対する他のフックの実行は停止しません。
  • 両方の結果で発火。 コールバックは成功と失敗の両方で発火します(ハンドラーから漏出した例外から生成された失敗を含みます)。上記の例のように、結果の型を分岐してフィルタリングしてください。
  • 通常は非同期タスク、状況により同期ブロック。 エンジンにアクティブなイベントループがある場合、フックはデタッチされた非同期タスクとしてスケジュールされるため、エディタへのレスポンスはフックの完了を待ちません。ただし、CLI コマンド、ブートストラップワークフローの実行、ワーカースレッドなど、イベントループが存在しないパスでは、フックはインラインで実行され、呼び出し元は処理が戻るまでブロックされます。それらのパスで発火する可能性がある場合は、フックを軽量に保つか、重い処理をプロセス外に移動してください。
  • 完全一致の型マッチング。 登録されたリクエスト型と完全に一致する場合にのみ発火し、そのサブクラスでは発火しません。
  • 引数は読み取り専用。 リクエストや結果を変更しないでください。どちらもエンジンがシリアライズしようとしている結果イベントから参照されています。omit_from_result とマークされたフィールドは受け取る時点でクリアされているため、フックを使ってそれらを読み取ることはできません。
  • フック内からエンジンリクエストを発行しないこと。 エンジンの操作の深さ(operation-depth)やノード実行状態はプロセス全体で共有されているため、フックから送信されたリクエストは実行中の処理を乱す可能性があります。HTTP 通信やファイル書き込みなどの外部処理を行ってください。
  • オーケストレーター専用。 フックはライブラリをロードしたプロセスのイベントマネージャーに登録されるため、ワーカーモードのライブラリのフックはオーケストレーターが処理したリクエストを確認できません。エンジンはロード時にこの組み合わせに対して PostDispatchHooksWorkerIncompatibleProblem を報告します。
  • 永続性なし。 プロセス終了時に実行中のフックは破棄されます。到達保証が必要な処理には使用しないでください。

不正なペア(呼び出し可能でないコールバック、リクエスト型でないキーなど)は PostDispatchHookRegistrationProblem として報告され、リストの残りの登録が停止してライブラリは FLAWED になります。

マニフェストに記載せずにノード型を登録する

ノードセットがデータ駆動型である場合、自動生成される場合、または単に手動でマニフェストを保守するのが困難なほど大規模である場合は、マニフェストの "nodes": [] を空のままにして、before_library_nodes_loaded 内で定義を動的に合成できます。

これは ロードシーケンス のステップ 6 とステップ 7 のおかげで機能します:フックが先に実行され、library_data はエンジンが次に読み取るのと同じオブジェクトであり、library_data.nodes は通常のリストであるためです。

class MyLibrary(AdvancedNodeLibrary):
    def before_library_nodes_loaded(self, library_data, library) -> None:
        library_data.nodes.extend(
            NodeDefinition(
                class_name=spec["class_name"],
                file_path="generated_nodes.py",
                metadata=NodeMetadata(
                    category="dynamic",
                    description=spec["description"],
                    display_name=spec["display_name"],
                ),
            )
            for spec in load_specs()
        )

エディタが読み取る情報はマニフェストの nodes リストから直接取得されるわけではありません。ListNodeTypesInLibrary と GetAllInfoForLibrary は両方ともメモリ内の Library を読み取るため、合成されたノード型もマニフェストで明示宣言されたものと全く同様にノードパレットに表示されます。ただし、カテゴリは依然としてマニフェストから読み取られるため、合成先として予定しているすべてのカテゴリをマニフェスト側で宣言しておいてください。

この方法で追加された定義は手書きのものと区別がつかないため、ローダーのすべての動作(遅延モジュールロード、複数クラスでのインポートメモ化、ワークフロー再開時の安定名前空間エイリアシング、ノードごとの問題報告、適切な適合性評価)をそのまま継承します。

クラスの生成元 (Where the classes come from)

エンジンは、NodeDefinition.file_path にあるファイルをインポートし、getattr(module, class_name) を呼び出すことでノード型を解決します。モジュールレベルの __getattr__(PEP 562)はこの要件を満たすため、1 つのファイルで 1 つも class 文を書くことなく、ライブラリ内のすべてのノード型を動的にサポートできます:

def __getattr__(name: str) -> type[DataNode]:
    spec = find_spec(name)
    if spec is None:
        msg = f"module {__name__!r} has no attribute {name!r}"
        raise AttributeError(msg)
    node_class = build_node_class(spec)
    globals()[name] = node_class  # キャッシュ: 次回のルックアップは __getattr__ をスキップ
    return node_class

生成されたクラスはモジュールのグローバル変数にキャッシュしてください。エンジンは解決されたクラスをキャッシュし、isinstance チェックで比較し、pickle がそれを参照するため、同じノード型の 2 回のルックアップは同一のオブジェクトを返す必要があります。

クラス構築時は明示的に __module__ を設定すること

type(name, bases, namespace) は現在のモジュールを自動的に付与してくれません。名前空間に __module__ キーがない場合、クラス生成は呼び出し元フレームのグローバルから __name__ を読み取ります。BaseNode サブクラスは ABCMeta を持っているため、そのフレームは標準ライブラリの abc モジュール内になり、クラスは誤って __module__ == "abc" を名乗ってしまいます。

ロード時にはエラーは発生しません。不具合は後から現れます: 保存されたワークフローを逆シリアル化(unpickle)する際、__module__ をインポートしてその上の __qualname__ をルックアップするため、クラスが定義した値を保持するワークフローは再度開くことができなくなります。両方を明示的に渡してください:

return type(
    spec["class_name"],
    (DataNode,),
    {
        "__init__": __init__,
        "process": process,
        "__module__": __name__,
        "__qualname__": spec["class_name"],
    },
)

なぜクラスを直接登録しないのか?

Library.register_new_node_type() と Library.register_lazy_node_type() はパブリックメソッドであり、これらを after_library_nodes_loaded から呼び出してもノード型を登録して動作させることができます。それでも定義を合成する手法が推奨される理由は 2 つあります:

  • 適合性 (Fitness): エンジンはステップ 7 のマニフェスト駆動ループに基づいてライブラリが正常にロードされたかを判断します。"nodes": [] で after フックで全てを登録するライブラリは UNUSABLE と判定され、ノード型自体は登録されていても、登録全体が失敗として報告されてしまいます。
  • 安定した名前空間: ローダーは、保存されたワークフローを再度開く際に griptape_nodes.node_libraries.<library>.<file> が解決できるように、保留中の安定モジュールローダーも登録します。クラスを直接登録するとこれがスキップされます。

制限事項 (Limitations)

  • 分離(ワーカー)ライブラリはサポートされていません。 ライブラリがワーカーサブプロセス内で実行されている場合、オーケストレーターはワーカーから送り返されたスキーマからスタブクラスを再構築し、マニフェストの nodes リストから各スタブのメタデータを解決します。ワーカーのレジストリにのみ存在するノード型は警告とともに出力から除外されます。動的登録ライブラリはオーケストレーターで実行するか、ノードをマニフェストに記載してください。
  • 宣言の検証はマニフェストのみを参照します。 model_usage や model_provider_usage の参照検証はディスクから読み取られたマニフェストに対して実行されるため、合成されたノード上のこれらの宣言はチェックされません。誤ったモデル参照はロード時ではなく実行時に失敗します。
  • ノード名の衝突チェックは依然として適用されます。 合成されたクラス名も、宣言されたクラス名と同じクロスライブラリ衝突チェックの対象となります。プレフィックスを付けることを推奨します。

実装例 (Examples)

完全に動作する 2 つのサンプルライブラリです。ワークスペースの libraries ディレクトリにフォルダを配置し、エディタのライブラリ設定から登録してエンジンを再起動してテストできます。

実装例: リクエスト型を処理するライブラリ

ConvertColorspaceRequest を所有し、高度なライブラリからそれを処理し、自身のノードからそれを消費するライブラリ:

  • griptape_nodes_library.json: 1 つのノードと高度なライブラリを宣言するマニフェスト
  • colorspace_events.py: PayloadRegistry に登録されたリクエストおよび結果ペイロード
  • advanced_library.py: get_request_handlers からハンドラーを返して実装
  • nodes.py: process() からリクエストをディスパッチし、失敗を処理するノード

Convert Colorspace ノードを追加し、color を [0, 0.5, 1]、source を rgb、target を hsv に設定して実行します。同じエンジン内の他のライブラリも ConvertColorspaceRequest を送信して同じ結果を取得できるようになります。

実装例: ノード型を動的に登録するライブラリ

マニフェストでは何も宣言せず、JSON ファイルから 4 つのノード型を動的に登録するライブラリ:

4 つのノードを含む Dynamic カテゴリが表示されます。既存の operator 値を再利用して node_specs.json に新しいエントリを追加して再起動すると、マニフェストや Python コードを変更することなく 5 つ目のノードが自動的に追加されます。