Skip to content

カスタム機能

機能(Feature)は、実行時にAIエージェントの機能を拡張および強化する方法を提供します。これらはモジュール式で構成可能(composable)になるよう設計されており、ニーズに合わせて自由に組み合わせて使用できます。

Koogで標準提供されている機能に加えて、適切な機能インターフェースを継承することで、独自の機能を実装することも可能です。このページでは、現在のKoog APIを使用して独自の機能を構築するための基本的な構成要素について説明します。

機能インターフェース

Koogは、カスタム機能を実装するために拡張できる以下のインターフェースを提供しています。

NOTE

グラフベース、関数型、およびプランナーエージェントのすべてにインストール可能なカスタム機能を作成するには、これらすべてのインターフェースを実装する必要があります。

カスタム機能の実装

カスタム機能を実装するには、以下の手順に従って機能の構造を作成する必要があります。

  1. 機能クラスを作成する。
  2. 設定クラス(Configuration class)を定義する。この設定クラスは FeatureConfig クラスを継承します。
  3. AIAgentGraphFeatureAIAgentFunctionalFeatureAIAgentPlannerFeature の一部またはすべてを実装するコンパニオンオブジェクトを作成する。
  4. 機能に一意のストレージキー(storage key)を付与する。これは、エージェントパイプライン内での機能の識別と取得に使用されます。このキーは、エージェントに登録されたすべての機能を含むエージェントパイプライン内部のマップで使用されます。エージェントを実行すると、登録されたすべての機能を処理する必要があり、このキーを使用してマップから機能を取得します。
  5. 必要なメソッドを実装する。

以下のコードサンプルは、グラフベース、関数型、およびプランナーエージェントにインストール可能なカスタム機能を実装する際の一般的なパターンを示しています。

kotlin
class MyFeature(val someProperty: String) {
    class Config : FeatureConfig() {
        var configProperty: String = "default"
    }

    companion object Feature : AIAgentGraphFeature<Config, MyFeature>, AIAgentFunctionalFeature<Config, MyFeature>, AIAgentPlannerFeature<Config, MyFeature> {
        // コンテキスト内での取得に使用する一意のストレージキー
        override val key = createStorageKey<MyFeature>("my-feature")
        override fun createInitialConfig(agentConfig: AIAgentConfig): Config = Config()

        // グラフベースエージェント向けの機能のインストール
        override fun install(config: Config, pipeline: AIAgentGraphPipeline) : MyFeature {
            val feature = MyFeature(config.configProperty)

            pipeline.interceptAgentStarting(this) { context ->
                // イベントハンドラの実装
            }
            return feature
        }

        // 関数型エージェント向けの機能のインストール
        override fun install(config: Config, pipeline: AIAgentFunctionalPipeline) : MyFeature {
            val feature = MyFeature(config.configProperty)

            pipeline.interceptAgentStarting(this) { context ->
                // イベントハンドラの実装
            }
            return feature
        }

        // プランナーエージェント向けの機能のインストール
        override fun install(config: Config, pipeline: AIAgentPlannerPipeline) : MyFeature {
            val feature = MyFeature(config.configProperty)

            pipeline.interceptAgentStarting(this) { context ->
                // イベントハンドラの実装
            }
            return feature
        }
    }
}

エージェントを作成する際、install メソッドを使用して機能をインストールします。

kotlin
val agent = AIAgent(
    promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
    systemPrompt = "You are a helpful assistant. Answer user questions concisely.",
    llmModel = OpenAIModels.Chat.GPT4o
) {
    install(MyFeature) {
        configProperty = "value"
    }
}

パイプラインインターセプター

インターセプター(Interceptor)は、エージェントの実行パイプラインにフックしてカスタムロジックを実装できる、エージェントのライフサイクル内のさまざまなポイントを表します。Koogには、さまざまなイベントを監視するために使用できる一連の事前定義されたインターセプターが含まれています。

以下は、機能の install メソッドから登録できるインターセプターです。リストされているインターセプターはタイプ別にグループ化されており、グラフベース、関数型、およびプランナーエージェントのパイプラインに適用されます。実際の機能を開発する際のノイズを減らし、コストを最適化するために、その機能に必要なインターセプターのみを登録してください。

エージェントと環境のライフサイクル:

  • interceptEnvironmentCreated: エージェント環境が作成されたときに、その環境を変換します。
  • interceptAgentStarting: エージェントの実行開始前に呼び出されます。
  • interceptAgentCompleted: エージェントの実行が正常に完了したときに呼び出されます。
  • interceptAgentExecutionFailed: エージェントの実行が失敗したときに呼び出されます。
  • interceptAgentClosing: エージェントの実行が終了する直前に呼び出されます(クリーンアップポイント)。

戦略(Strategy)のライフサイクル:

  • interceptStrategyStarting: 戦略の実行開始前に呼び出されます。
  • interceptStrategyCompleted: 戦略の実行が正常に完了したときに呼び出されます。

LLM呼び出しのライフサイクル:

  • interceptLLMCallStarting: LLM呼び出しの前に呼び出されます。
  • interceptLLMCallFailed: LLM呼び出しが失敗したときに呼び出されます(基底のプロンプトエグゼキューターまたはモデレーション呼び出しが例外をスローした際)。
  • interceptLLMCallCompleted: LLM呼び出しの後に呼び出されます。

LLMストリーミングのライフサイクル:

  • interceptLLMStreamingStarting: ストリーミング開始前に呼び出されます。
  • interceptLLMStreamingFrameReceived: ストリームフレームを受信するたびに呼び出されます。
  • interceptLLMStreamingFailed: ストリーミングが失敗したときに呼び出されます。
  • interceptLLMStreamingCompleted: ストリーミング完了後に呼び出されます。

ツール呼び出しのライフサイクル:

  • interceptToolCallStarting: ツール呼び出しの前に呼び出されます。
  • interceptToolValidationFailed: ツールの入力バリデーションが失敗したときに呼び出されます。
  • interceptToolCallFailed: ツールの実行が失敗したときに呼び出されます。
  • interceptToolCallCompleted: ツールが完了した(結果が得られた)後に呼び出されます。

グラフベースエージェント固有のインターセプター

以下のインターセプターは AIAgentGraphPipeline でのみ使用可能であり、ノードおよびサブグラフのライフサイクルイベントを監視できます。

ノード実行のライフサイクル:

  • interceptNodeExecutionStarting: ノードの実行開始前に呼び出されます。
  • interceptNodeExecutionCompleted: ノードの実行完了後に呼び出されます。
  • interceptNodeExecutionFailed: ノードの実行がエラーで失敗したときに呼び出されます。

サブグラフ実行のライフサイクル:

  • interceptSubgraphExecutionStarting: サブグラフの実行開始直前に呼び出されます。
  • interceptSubgraphExecutionCompleted: サブグラフの実行完了後に呼び出されます。
  • interceptSubgraphExecutionFailed: サブグラフの実行が失敗したときに呼び出されます。

機能が特定のタイプのイベントを処理するには、対応するパイプラインインターセプターを登録する必要があります。

エージェントイベントのフィルタリング

エージェントに機能をインストールする際、その機能に登録されているすべてのイベントを処理したくない場合があります。一部のイベントをフィルタリングするには、FeatureConfig.setEventFilter 関数を使用してフィルタを適用します。

以下の例は、機能に対して LLM 呼び出しの開始イベントと完了イベントのみを許可する方法を示しています。

kotlin
install(MyFeature) {
    setEventFilter { context ->
        context.eventType is AgentLifecycleEventType.LLMCallStarting ||
            context.eventType is AgentLifecycleEventType.LLMCallCompleted
    }
}

機能のイベントフィルタリングを無効にする

機能のロジックがエージェントイベントの完全な構造に依存している場合、イベントフィルタリングが予期しない動作を引き起こす可能性があります。これを防ぐには、機能を実装する際に、機能設定の setEventFilter をオーバーライドして、機能のインストール時に設定されたカスタムフィルタを無視するようにし、イベントフィルタリングを無効にする必要があります。

エージェントイベントストリーム全体の処理に依存する機能の例としては OpenTelemetry があります。これは、完全なエージェントイベント構造を使用して、継承されたスパン(span)構造を構成するためです。

以下は、機能のイベントフィルタリングを無効にする例です。

kotlin
class MyFeatureConfig : FeatureConfig() {
    override fun setEventFilter(filter: (AgentLifecycleEventContext) -> Boolean) {
        // この機能のイベントフィルタリングを無効にする
        throw UnsupportedOperationException("Event filtering is not allowed.")
    }
}

例:基本的なロギング機能

以下の例は、エージェントのライフサイクルイベントをログに記録する基本的なロギング機能を実装する方法を示しています。この機能はグラフベース、関数型、およびプランナーエージェントで利用可能であるべきなため、コードの重複を避けるために、すべてのエージェントタイプに共通のインターセプターを installCommon メソッドで実装しています。各エージェントタイプ固有のインターセプターは、installGraphPipelineinstallFunctionalPipeline、および installPlannerPipeline メソッドで実装しています。

kotlin
class LoggingFeature(val loggerName: String) {
    class Config : FeatureConfig() {
        var loggerName: String = "agent-logs"
    }

    companion object Feature :
        AIAgentGraphFeature<Config, LoggingFeature>,
        AIAgentFunctionalFeature<Config, LoggingFeature>,
        AIAgentPlannerFeature<Config, LoggingFeature> {

        override val key = createStorageKey<LoggingFeature>("logging-feature")

        override fun createInitialConfig(agentConfig: AIAgentConfig): Config = Config()

        override fun install(config: Config, pipeline: AIAgentGraphPipeline) : LoggingFeature {
            val logging = LoggingFeature(config.loggerName)
            val logger = KotlinLogging.logger(config.loggerName)

            installGraphPipeline(pipeline, logger)

            return logging
        }

        override fun install(config: Config, pipeline: AIAgentFunctionalPipeline) : LoggingFeature {
            val logging = LoggingFeature(config.loggerName)
            val logger = KotlinLogging.logger(config.loggerName)

            installFunctionalPipeline(pipeline, logger)

            return logging
        }

        override fun install(config: Config, pipeline: AIAgentPlannerPipeline) : LoggingFeature {
            val logging = LoggingFeature(config.loggerName)
            val logger = KotlinLogging.logger(config.loggerName)

            installPlannerPipeline(pipeline, logger)

            return logging
        }

        private fun installCommon(
            pipeline: AIAgentPipeline,
            logger: KLogger,
        ) {
            pipeline.interceptAgentStarting(this) { e ->
                logger.info { "Agent starting: runId=${e.runId}" }
            }
            pipeline.interceptStrategyStarting(this) { e ->
                logger.info { "Strategy ${e.strategy.name} starting" }
            }
            pipeline.interceptLLMCallStarting(this) { e ->
                logger.info { "Making LLM call with ${e.tools.size} tools" }
            }
            pipeline.interceptLLMCallCompleted(this) { e ->
                logger.info { "Received ${e.responses.size} response(s)" }
            }
        }

        private fun installGraphPipeline(
            pipeline: AIAgentGraphPipeline,
            logger: KLogger,
        ) {
            installCommon(pipeline, logger)

            pipeline.interceptNodeExecutionStarting(this) { e ->
                logger.info { "Node ${e.node.name} input: ${e.input}" }
            }
            pipeline.interceptNodeExecutionCompleted(this) { e ->
                logger.info { "Node ${e.node.name} output: ${e.output}" }
            }
        }

        private fun installFunctionalPipeline(
            pipeline: AIAgentFunctionalPipeline,
            logger: KLogger
        ) {
            installCommon(pipeline, logger)
        }

        private fun installPlannerPipeline(
            pipeline: AIAgentPlannerPipeline,
            logger: KLogger
        ) {
            installCommon(pipeline, logger)
        }
    }
}

以下は、カスタムロギング機能をエージェントにインストールする例です。この例では、基本的な機能のインストールとともに、ロガーの名前を指定できるカスタム設定プロパティ loggerName の使用方法を示しています。

kotlin
val agent = AIAgent(
    promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
    systemPrompt = "You are a helpful assistant. Answer user questions concisely.",
    llmModel = OpenAIModels.Chat.GPT4o
) {
    install(LoggingFeature) {
        loggerName = "my-custom-logger"
    }
}

agent.run("What is Kotlin?")