「Difyソースコードリーディング#3 ―APIのリクエストからレスポンスまで」を開催しました #もくもくDify

ジェネラティブエージェンツの大嶋です。

もくもくDifyで「Difyソースコードリーディング#3 ―APIのリクエストからレスポンスまで」というイベントを開催しました。

dify-mokumoku.connpass.com

アーカイブ動画はこちらです。

youtube.com

Difyのソースコードは以下です。

github.com

今回は 戸塚さん にも同席いただいて、一緒に話しながらコードを読んでいきました!

今回のポイント

APIのリクエストからレスポンスまで

今回の目標の1つ目は、APIのリクエストからレスポンスまで、おおよそどのような呼び出しの繋がりになっているのか把握することでした。

ワークフローを実行するAPIである、WorlflowRunApiクラスのpostメソッド を起点として、実際にワークフローのノードが処理されるところまでコードを追ってみました。

CONTRIBUTING_JA.md の記載などから想像していた通りですが、「controllers -> services -> core」という流れで呼び出されていることが分かりました。

ワークフローの実行

ワークフローはDifyの内部でグラフとして表現されており、GraphEngineクラス で各ノードを順に処理していそうなことが読み取れました。

具体的には、コードの以下の箇所のwhileループで、ノードを順に処理しているように見えます。

https://github.com/langgenius/dify/blob/0f1487325531bf4d8b38225218a91f06624dcc35/api/core/workflow/graph_engine/graph_engine.py#L204

ワークフローのノードのクラス

気になったので、ワークフローのノードのクラスもいくつか見てみました。

ワークフローの各ノードは、BaseNodeクラス を継承して実装されていることが分かりました。

例えば LLMNodeクラス でLLMの呼び出しが行われているようです。

おおまかにどんなAPIがあるか

少し話がそれましたが、今回の2つ目の目標だった、おおまかにどんなAPIがあるか把握することにも取り組みました。

DifyのAPIのエンドポイントは api/controllersディレクトリ で定義されています。

この中にさらにディレクトリがあり、それぞれおおよそ以下のAPIのようでした。

  • console ... Difyにログインしてアプリを作成したりする際のAPI
  • files ... ファイル関係(?)
  • inner_api ... Enterprise用(?)
  • service_api ... 不明
  • web ... Difyのアプリを公開した際のAPI

とくに重要なのがconsoleディレクトリとwebディレクトリで、基本的なAPIはこの2つのディレクトリ内でエンドポイントが定義されているようでした。

次回のご案内

Difyでは、モデルやツールの詳細がYAMLファイルで管理されています。 次回は、これらのYAMLファイルがどのように扱われているのか読み解くことを目標にします。 時間があれば、ワークフローを保存するYAMLファイルの出力などのコードも探してみます。 ご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

また、水曜日にもDifyのもくもく会が開催されます。 こちらではDifyの使い方の解説として、テンプレートをみていく予定となっています。 こちらもご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

勉強会「【LangChainゆる勉強会#12】LangGraphの最新ドキュメントを全体的にざっと読む」を開催しました #StudyCo

ジェネラティブエージェンツの大嶋です。

運営している勉強会コミュニティStudyCoで「【LangChainゆる勉強会#12】LangGraphの最新ドキュメントを全体的にざっと読む」というイベントを開催しました。

studyco.connpass.com

アーカイブ動画はこちらです。

youtube.com

LangGraphは最近アップデートが活発で、ドキュメントも充実してきています。 LangGraphのドキュメントに一通り目を通す時間を作ろうと、この勉強会を開催しました。

今回LangGraphのドキュメントを読んでみて、「この機能はあまり知られていなそうだな」と感じたものを、3つほどピックアップしてこの記事で紹介します。

Send APIによる動的なエッジの構成

まず、Send APIについてです。

LangGraphでは基本的な使い方として、「add_conditional_edges」を使って、条件に応じてノードを選択するエッジを作成します。 しかし、単純にadd_conditional_edgesを使うだけでは、「実行時にノードの数を動的に変更したい」といったことはできません。

そのように、より動的にエッジを構成するために、LangGraphでは「Send API」が提供されています。 Send APIのコンセプトは以下のページで解説されています。

langchain-ai.github.io

上記のページでは、Send APIを使う一般的な例として、map-reduceが挙げられています。 map-reduceでは、mapの処理として「同じ処理を実行するノードを、データの数だけ用意して実行する」ということになり、これはまさに動的にエッジを構成する例です。

map-reduceの実装例は、次のページに掲載されています。

langchain-ai.github.io

ステートの共有

次に、ステートの共有についてです。

チャットボットなどのアプリケーションを提供するうえで、「スレッド内の会話履歴を記憶させたい」というのに加えて、「スレッドをまたいでユーザー単位で記憶を共有したい」といったケースが考えられます。 そのようなケースのサンプルコードが以下のページに掲載されています。

langchain-ai.github.io

上記のページのコードから、とくに重要な箇所を簡単に説明します。

まず、グラフのステートとして、「SharedValue」という、あるキーをもとにシェアする値を定義します。

from langgraph.managed.shared_value import SharedValue

class AgentState(MessagesState):
    info: Annotated[dict, SharedValue.on("user_id")]

実行時のconfigで、SharedValueで指定したキーに適当な値を設定します。

config = {"configurable": {"thread_id": "1", "user_id": "1"}}

for update in graph.stream({"messages": [{"role": "user", "content": "hi"}]}, config, stream_mode="updates"):
    print(update)

このようにSharedValueを使うことで、「スレッドをまたいでユーザー単位で記憶を共有する」という実装ができるということです。

NodeInterruptによる動的なブレークポイントの設定

最後に、NodeInterruptについてです。

LangGraphでは、「interrupt_before」という設定によって、あるノードに処理がきた際に、処理を中断することができます。

より動的に処理を中断する方法として「NodeInterrupt」が提供されています。 サンプルコードは次のページに掲載されています。

langchain-ai.github.io

こちらも、上記のページのコードから、とくに重要な箇所を簡単に説明します。

from langgraph.errors import NodeInterrupt

def step_2(state: State) -> State:
    if len(state['input']) > 5:
        raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
    
    print("---Step 2---")
    return state

簡単に言えば、NodeInterruptをraiseすると、動的にグラフの実行をinterruptできるということです。

ノードの遷移のタイミングで処理を中断したい場合は「interrupt_before」を使えば十分ですが、より動的な制御が必要な場合は「NodeInterrupt」を使えるということになります。

おわりに

以上、LangGraphのあまり知られていなそうな機能を3つ紹介しました。

今回の勉強会は「LangGraphの最新ドキュメントを全体的にざっと読む」というテーマだったため、1つ1つの内容は深堀りできませんでした。 開催後のアンケートでも、「このあたりを深堀りしてほしい」といった意見をいくつかいただいており、もう少し深堀りする会も開催したいと考えています。

LangChain / LangGraph、その他のテーマで引き続き勉強会を開催していきます。 LangGraphの話でもLangGraph以外の話でも、もしも「こんな話が聞きたい」というテーマがあれば、ぜひお声がけください!

LT大会で「LangGraphでのHuman-in-the-Loopの実装」というタイトルで話しました #StudyCo

ジェネラティブエージェンツの大嶋です。

StudyCoのLT大会で「LangGraphでのHuman-in-the-Loopの実装」というタイトルで話しました。

studyco.connpass.com

発表資料はこちらです。

speakerdeck.com

ソースコードはこちらです。

github.com

この記事には、発表内容に含めなかった感想などを書こうと思います。

LangGraphのフレームワークらしさ

LangGraphの機能を活用したHuman-in-the-Loopを実装してみると、LangGraphが処理の全体的な流れを担い、その流れの部品を実装することになります。

そのことから、LangGraphのフレームワークらしさを改めて実感しました。

個人的にはLangChain(とくにLCEL)もフレームワークだと理解することが実は重要で、フレームワークのつもりでキャッチアップすると上手に使いやすい、という感覚がある気がしています。

より本格的なアプリではどうか

今回のLTで紹介したのは、Streamlitのアプリケーションでの簡単なデモ程度の実装でした。

実際のアプリケーションでHuman-in-the-Loopのように処理の流れの根幹に関わる機能を実装するときは、フレームワークの機能を使う/使わないという判断も重要だと思います。

より本格的に、例えばSlackアプリに組み込んだりすると、LangGraphでのHuman-in-the-Loopの実装がどこまで便利で実用的なのかよりよく理解できそうです。近々挑戦したいです。

Notebookのコードをアプリケーションに

今回話題にしたLangGraphでのHuman-in-the-Loopの実装については、公式ドキュメントに解説とNotebookでのサンプルコードが記載されています。

しかし、Notebookのサンプルコードでは、実際にアプリケーションに組み込むにはどうするんだ?と試行錯誤することになりがちです。

LangChain/LangGraphの使い方を広めるうえでも、簡単ではありますがアプリケーション化したコードを紹介できてよかったです。

おわりに

今回のLT大会自体でも、自分の知らない話をたくさん聞くことができ、とても勉強になりました。

また時々LT大会を開催する予定なので、「自分も話したい!」という方は是非お声がけください!

「Difyソースコードリーディング#2 ―Difyの開発環境を起動してみる」を開催しました #もくもくDify

ジェネラティブエージェンツの大嶋です。

もくもくDifyで「Difyソースコードリーディング#2 ―Difyの開発環境を起動してみる」というイベントを開催しました。

dify-mokumoku.connpass.com

アーカイブ動画はこちらです。

youtube.com

Difyのソースコードは以下です。

github.com

今回のポイント

Difyの開発環境の起動方法

Difyの開発環境の起動方法自体は、CONTRIBUTING_JA.md の「インストール」の箇所に書かれている通りの手順でした。

手順自体は少し長めでしたが、まったくエラーに遭遇せず起動できました。

開発環境の起動と、docker/docker-compose.yamlでの起動の違い

補足になりますが、CONTRIBUTING_JA.md に書かれているのは、Dify自体の開発のためのセットアップ手順です。

docker/docker-compose.yaml にもDifyをDocker Composeで起動する手順が書かれていますが、こちらの場合は開発モードではなく、コードを変更しても反映されません。

UIの文言の修正

せっかく開発環境を起動できたので、UIの文言を少し変更してみました。

web/i18n ディレクトリのファイルを編集すると、しっかりUIに反映されました。

設定の変更

昨日のDifyもくもく会で話題になった、チャンクサイズの最大値の変更も挑戦してみました。

チャンクサイズを1000より大きくすると Custom segment length should be between 50 and 1000 と表示されるので Custom segment length should be between で検索してコードの該当箇所を探しました。

最終的にはINDEXING_MAX_SEGMENTATION_TOKENS_LENGTHという環境変数を変更することで、チャンクサイズの最大値を変更できることが確認できました。

コードの該当箇所:https://github.com/langgenius/dify/blob/3e7597f2bd0ff686e7a1ea1972f0421ed0568fbb/api/.env.example#L258

dify-docs

Difyのアプリケーション以外に、dify-docsも少し見てみました。

github.com

こちらはGitBookが使われているようで、マークダウン形式のドキュメントを更新すれば反映されるようです。

translate.py という自動翻訳のスクリプトが置かれていて、このスクリプトの内部ではDifyのアプリケーションを呼び出しているようでした。

dify-sandbox

最後に、dify-sandboxも少しみてみました。

github.com

Go言語で実装されていて、システムコールを制限したりした環境でPythonやNode.jsを実行できるようでした。

詳細までは分からなかったので、またそのうちじっくり読みたいです。

次回のご案内

Difyソースコードリーディングの第3回として、次回はAPIのリクエストからレスポンスまでどんなふうに実装されてるのか読み解いたり、どんなAPIがあるのか見てみる予定です。 ご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

また、水曜日にもDifyのもくもく会が開催されます。 こちらではDifyの使い方の解説として、テンプレートをみていく予定となっています。 こちらもご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

LangChain Meetup Tokyo #2で「LangChainの現在とv0.3にむけて」というタイトルで話しました #LangChainJP

ジェネラティブエージェンツの大嶋です。

LangChain Meetup Tokyo #2で「LangChainの現在とv0.3にむけて」というタイトルで話しました。

langchain.connpass.com

(西見さんが撮影した写真)

発表資料はこちらです。

speakerdeck.com

当日の様子はこちら。

togetter.com

「LangChainの現在とv0.3にむけて」の補足的な話

LangChainは以前に比べて変更も落ち着き、使い方も程よくまとまってきて、扱いやすくなっていると感じます。

LangSmithやLangGraphといったエコシステムを活用できる強みも大きいです。

先日、langchain-coreを0.1.42から0.2.36(その時点の最新)まで一気にバージョンアップする機会がありましたが、その際ERROR・WARNINGには一つも遭遇しませんでした。

  • 最新の使い方は公式ドキュメントでキャッチアップして、deprecatedな機能を避ける
  • 無理やりカスタマイズして解決しようとしない(無理のない方法で対応する)
  • コードの構成上、LangChainに依存する範囲を小さくする・コードの多くの箇所が依存するLangChainの機能を限定する

といった点を意識すると、より扱いやすくなるように思います。

おまけの話

昨日は想定より進行が早かったため、追加でLangGraphでのHuman-in-the-Loopの実装例を紹介しました。

ソースコードは以下のGitHubリポジトリで公開しています。

github.com

こちらの詳細は今週木曜日のLT大会で話す予定です。

studyco.connpass.com

おわりに

他の方の発表も楽しく、今回のLangChain Meetupはとても良い会だったと思います。

Discordも盛り上がっていて、次回も楽しみです。

LangChain Meetupでも他の場でも、今後もLangChainについてアップデートや使い方のコツなどを発信していきます!

LangChainでPythonのDIライブラリ「Injector」を使う例の紹介 #LangChain

ジェネラティブエージェンツの大嶋です。

クラスの外部から依存を注入するDI(Dependency Injection)のPythonパッケージとしてInjectorがあります。

github.com

この記事では、LangChainでDIライブラリのInjectorを使う例を紹介します。

※LangChainを使うのはあくまで一例です。Injector自体は幅広く活用できます。

関数でChainを作成する実装

InjectorやDIの話に入る前に、なぜそのようなものがあると嬉しいのかから書いていきます。

LangChainでシンプルなRAGのChainを作成して実行する関数は、次のように実装できます。

def invoke_retrieval_chain(model: BaseChatModel, retriever: RetrieverLike):
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "Answer the question.\n\nContext: {context}"),
            ("human", "{question}"),
        ],
    )

    chain = (
        {"context": retriever, "question": RunnablePassthrough()}
        | prompt
        | model
        | StrOutputParser()
    )

    return chain.invoke(question)

この関数では、引数でmodelやretrieverを変更できるようになっています。

このように、「ある処理で使う依存関係を状況に応じて切り替えられるようにしたい」ということは少なくありません。

クラスを使ったDependency Injection

依存関係を状況に応じて切り替えられるようにする定番の方法の1つは、クラスを使うことです。

以下のコードでは、RetrievalChainクラスが、依存関係にあるmodelとretrieverをコンストラクタで受け取るようになっています。

class RetrievalChain:
    def __init__(self, model: BaseChatModel, retriever: RetrieverLike):
        self.model = model
        self.retriever = retriever

    def invoke(self, question: str) -> str:
        prompt = ChatPromptTemplate.from_messages(
            [
                ("system", "Answer the question.\n\nContext: {context}"),
                ("human", "{question}"),
            ],
        )

        chain = (
            {"context": self.retriever, "question": RunnablePassthrough()}
            | prompt
            | self.model
            | StrOutputParser()
        )

        return chain.invoke(question)

このコードは以下のように呼び出すことになります。

def main() -> None:
    model = ChatOpenAI(model="gpt-4o-mini")
    retriever = TavilySearchAPIRetriever(k=3)
    chain = RetrievalChain(model=model, retriever=retriever)
    output = chain.invoke("東京の明日の天気は?")
    print(output)

chain = RetrievalChain(model=model, retriever=retriever) の箇所で依存関係を注入し、そのあとで output = chain.invoke("東京の明日の天気は?") のように呼び出すようになっています。

このように、あるクラスが必要とする依存関係を外部から注入できるようにする実装パターンは、依存性の注入(DI:Dependency injection)と呼ばれます。

上記のコードぐらいであれば、手作業で依存を注入してもそれほど大変ではないかもしれません。

しかし、本格的なアプリケーションになってくると、依存関係の受け渡しの手間が大きくなる場合があります。

そのような状況では、DI用のライブラリを使うと便利です。

Injectorの導入

ここから、PythonのDIライブラリ「Injector」を導入したコードの例を紹介します。

まず、依存関係を注入したいクラスのコンストラクタに @inject というデコレータをつけます。

from injector import inject

class RetrievalChain:
    @inject
    def __init__(self, model: BaseChatModel, retriever: RetrieverLike):
        self.model = model
        self.retriever = retriever

続いて、コードのどこかで、依存関係として注入するインスタンスを定義します。

from injector import Module, provider

class ProdModule(Module):
    @provider
    def model(self) -> BaseChatModel:
        return ChatOpenAI(model="gpt-4o-mini")

    @provider
    def retriever(self) -> RetrieverLike:
        return TavilySearchAPIRetriever(k=3)

Injectorで依存を解決したうえでRetrievalChainを使うコードは次のようになります。

from injector import Injector

def main() -> None:
    injector = Injector([ProdModule()])
    chain = injector.get(RetrievalChain)
    output = chain.invoke("東京の明日の天気は?")
    print(output)

まず、injector = Injector([ProdModule()]) の箇所で、依存関係として何を使うのかを設定しています。[ProdModule()] のようにリストを渡すことができるので、依存関係は複数のクラスに分けて定義することもできます。

続いて、chain = injector.get(RetrievalChain) の箇所で、依存関係を注入したうえで RetrievalChain のインスタンスを作成しています。

※Injectorで依存関係の定義する方法は、Module を継承したクラスを使う以外に、binder を引数とする関数を実装する方法もあります。詳細は InjectorのREADME を参照してください。

依存関係の切り替え

依存関係を切り替えたいシーンの定番としては、自動テストが挙げられます。

次のようなコードで、テスト時に使いたい依存関係を定義して使うことができます。

class TestModule(Module):
    @provider
    def model(self) -> BaseChatModel:
        responses: list[BaseMessage] = [AIMessage("fake output")]
        return FakeMessagesListChatModel(responses=responses)

    @provider
    def retriever(self) -> RetrieverLike:
        return RunnableLambda(lambda _: [Document("fake document")])


def test_retrieval_chain() -> None:
    injector = Injector([TestModule()])
    chain = injector.get(RetrievalChain)
    output = chain.invoke("東京の明日の天気は?")
    assert output == "fake output"

まとめ

この記事では、PythonのDIライブラリ「Injector」を使う例を紹介しました。

実際には、ここで紹介した程度の簡単なコードでは、Injectorまで使わずコンストラクタで依存関係を注入できれば十分なことが多いと思います。

もしもアプリケーションが大きくなってきて、依存関係の解決が大変だと思ったときは、Injectorの導入を検討してもいいかもしれません。

なお、この記事で使用したInjectorの使用例は以下のGitHubリポジトリで公開しています。

github.com

「Difyソースコードリーディング#1 ―Difyのシステム構成をざっくり把握」を開催しました #もくもくDify

ジェネラティブエージェンツの大嶋です。

もくもくDifyで「Difyソースコードリーディング#1 ―Difyのシステム構成をざっくり把握」というイベントを開催しました。

dify-mokumoku.connpass.com

アーカイブ動画はこちらです。

youtube.com

Difyのソースコードは以下です。

github.com

ポイントのまとめ

今回ソースコードリーディングしていて、ポイントだと思った点をまとめます。

docker/docker-compose.yaml

まず、今回は第一回としてDifyのシステム構成(フロントエンド・バックエンド・データベースなどの構成)を把握したかったので、そのあたりが把握できそうなファイルから読んでみました。

Difyのソースツリーを見るとdockerというディレクトリがあり、その中にあったdocker-compose.yamlを読むと、基本的なシステム構成を把握できるようになっていました。

docker/docker-compose.png

同じディレクトリにはとくに中心的な構成要素のつながりを整理した図もあり、参考になりました。

CONTRIBUTING_JA.md

その後、コントリビュータ向けのCONTRIBUTING_JA.mdを開いて読んでみると、ソースコードの構成についての説明もあり、ソースコードリーディングの起点として役立つ内容がたくさん記載されていました。 CONTRIBUTING_JA.mdには、以下のような内容が記載されていました。

  • モデルプロバイダーやツールプロバイダーを追加するときに読むべきガイドへのリンク
  • バックエンドがFlask・SQLAlchemy・Celeryで作られていること
  • フロントエンドがNext.js・Tailwind CSSで作られていること
  • バックエンドとフロントエンドのおおまかなディレクトリ構成

Difyのソースコードリーディングに挑戦する方は、まずはこのCONTRIBUTING_JA.md(またはCONTRIBUTING.mdなど)を読んでみるのがおすすめです。

api/core/model_runtime/model_providers/openai/llm/gpt-4o-2024-08-06.yaml

OpenAIのLLMとのインテグレーションについては、モデルごとの機能やコンテキストサイズなどがYAMLファイルに記載されており、モデルの追加時に素早く対応できるようになっていました。

api

その後、apiディレクトリ(バックエンド)のソースコードをざっと見て、雰囲気をつかんでいきました。

api/models

データモデルに対応するコードも簡単に見て、今回のソースコードリーディングは終了となりました。

次回のご案内

Difyソースコードリーディングの第2回として、次回はDifyの開発環境を起動していろいろさわってみるつもりです。 ご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

また、水曜日にもDifyのもくもく会が開催されます。 こちらではDifyの使い方の解説として、テンプレートをみていく予定となっています。 こちらもご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com