モーグルとカバとパウダーの日記

モーグルやカバ(EXカービング)山スキー(BC)などがメインの日記でした。今は仕事のコンピュータ系のネタが主になっています。以前はスパム対策関連が多かったのですが最近はディープラーニング関連が多めです。

ホストWindowsのLM StudioのAPIにWSL2から接続できるようにする

わかってしまえばなんのことはない話なのですが、ちょっとハマったのでメモ。

LM Studioを使うと、OpenAI API互換のAPIで各種LLMを使うことができるようになって便利です。
なのですが、WSL2からホストのWindows上で動いているLLMのAPIがなかなか呼べなくてハマりました。
LM Studioのバージョンは0.3.11です。

とりあえず手順としては
1. 動かしたいLLMをロードしてチャットで動くことを確認
2. 左上のメニューから緑色の「Developer」を選択
3. 「Status: Running」のところをオンにする

だけなのですが、これだとWindowsの環境からはAPIを叩けるものの、WSL2環境からは触れません。
これは、WSL2環境からホストのWindowsのサービスをlocalhostとしてそのまま叩くことができない、ということが理由です。
そこでホストのIPをipconfigで調べて叩けば良さそうに思うのですが、それもうまくいきません。
というのは通常だとLM Studioが自PCからのみのアクセスを許可しているため、WSL2からの接続は別ネットワークからの接続と認識されてしまうので、それで拒否されてしまうからです。

なので、下記のように追加で他のネットワークからのアクセスを許可する設定が必要になります。

4. 先程のメニューで「Settings」を開く
5. 「Serve on Local Network」をオン

これでやっとWSL2からもAPIを動かせるようになります。
このとき、APIにアクセスするためのlocalhostではないIPとエンドポイントも表示されます。
ただこれだとローカルネットワークからのアクセス全部が許可されるため、注意が必要です。


なかなか理由がわからなくて困ってたんですが、GPT-4.5先生が

必ずAPIサーバを 0.0.0.0 でListenする必要があります。

とアドバイスしてくれたので気がつけましたとさ。

ローカルのdifyに独自の組み込みツールを作成する

この記事はDifyアドベントカレンダー2024用に書いた記事です。

ローカルのdifyに独自の組み込みツールを作成する

difyには組み込みツールという各種SaaSなどを呼び出すようなツールが準備されています。
difyは元々SaaSでのサービスをメインに作られている(と思われる)ため、例えば基本的にファイルへの出力がサポートされていないとか、(まだ)動画や音声を扱うようなものがあまりなかったりします。
しかしローカルで動かしてているdifyでは、生成されたテキストをファイルに書き出したいとか、動画をffmpeg的なもので編集したいとかが結構あります。

そこで、ファイルへのテキスト書き込みだけをする非常に簡単な組み込みツールを作ることを題材に、ローカルのdifyに独自の組み込みツールを作成して運用するための手順を説明します。

公式では下記ページにビルトインツールの作り方について説明があります。この記事と合わせて参照してみてください。
クイック統合ツール | Dify https://docs.dify.ai/ja-jp/guides/tools/quick-tool-integration

組み込みツールの場所と構成

組み込みツールのおいてある場所は、ソース内の以下のディレクトリにあります。

dify/api/core/tools/provider/builtin/

この中に、組織名(サービス名)でディレクトリを作ります。

今回は公開するものではなく自分用のツールを作るという体で「mytools」というディレクトリにしました。つまりこの場合

dify/api/core/tools/provider/builtin/mytools/

というフォルダに自前のツールを作っていくことになります。
この組織名(サービス名)は、ディレクトリ名と同一になるので、当然ユニークである必要があります。

difyは頻繁にアップデートされていきますが、多くの場合gitでpullしてアップデートしていると思います。
その際にgitでのコンフリクトが起きないように、組み込みツールのフォルダのみを追加すれば動くように設計されています。

.gitignoreの設定

それでもgitで変更が出てくるのが気持ち悪いと思うので、builtinフォルダの中にgitignoreを設定しておきます。

dify/api/core/tools/provider/builtin/.gitignore

 .gitignore
 mytools

組み込みツールの中身

このディレクトリ内には

  • _assets/
  • tools/
  • mytools.yaml
  • mytools.py

を準備します。
ここでフォルダ名と同じ名前のついている「mytools.yaml」や「mytools.py」は、全体についての設定と基本のクラス設定のみです。

tools/ ディレクトリ内に、具体的に機能ごとのスクリプトとその設定のYAMLファイルを作っていきます。
このとき、このツールごとのファイル名は他のツールのファイル名ともユニークである必要がある、ように思います。たぶん…

_assets/ ディレクトリ以下には、アイコンやスクリプトから使われるファイルが置かれます。アイコンが不要ならばこのディレクトリ自体がなくても大丈夫です。

ツールの作成方法

具体的にファイルを作成していきます。
新しく作るときには、関連しそうなツールをコピーして、その中身を変更するのが楽だと思います。

mytools.yaml

組織名(サービス名)自体の設定を書きます。
名前と作者の設定、どの種別のツールかだけです。

identity:
  author: stealthinu
  name: mytools
  label:
    en_US: My Tools
  description:
    en_US: My Tools
  icon: icon.svg
  tags:
    - utilities

mytools.py

ほぼ決め打ちで、class名をファイル名と同じ名前でつけます。
なので名前は他と被らないように注意します。

from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController

class MyToolsProvider(BuiltinToolProviderController):
    def _validate_credentials(self, credentials: dict) -> None:
        pass

tools/file_writer.yaml

ツールの名前や作者、説明、パラメータの説明を書きます。
パラメータの説明には、どんなパラメータが必要で、それが何か、データの型や必須条件などを記述します。 ただしここに設定した内容は、あくまで説明のためのもので、プログラム的には後述のget_runtime_parametersの中で指定するようになっています。

identity:
  name: file_writer
  author: stealthinu
  label:
    en_US: File Writer
description:
  human:
    en_US: Write content to a file.
  llm: Write content to a file.
parameters:
  - name: content
    type: string
    required: true
    label:
      en_US: Content
      ja_JP: 内容
    human_description:
      en_US: The content to write to the file.
      ja_JP: ファイルに書き込む内容。
    llm_description: The content to write to the file.
    form: llm

tools/file_writer.py

ここに実際の処理を書きます。絶対に必要なものは以下の2関数です。

  • get_runtime_parameters : 取得するパラメータとその型や説明を設定します。
  • _invoke : ノードで処理する内容。実際にやりたい処理を書きます。

パラメータの渡し方だけ注意すればあとは普通にpythonを書けばよいです。
ただし、ファイルの出力についてはdify側にデータを渡して処理してもらうような書き方が正しいようで、そのあたりが注意が必要です。 ここではcreate_blob_messageを使って、組み込みツール用のディレクトリにテキストファイルを出力するようにしています。

tools/file_writer.py

from typing import Any

from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter
from core.tools.tool.builtin_tool import BuiltinTool


class FileWriterTool(BuiltinTool):
    def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> list[ToolInvokeMessage]:
        content = tool_parameters.get("content")
        
        if not content:
            return [self.create_text_message("No content provided")]

        try:
            # コンテンツをUTF-8でエンコード
            binary_content = content.encode('utf-8')

            # Difyシステムにファイルを返す
            return [
                self.create_text_message("Successfully prepared content"),
                self.create_blob_message(
                    blob=binary_content,
                    meta={"mime_type": "text/plain"},
                    save_as=self.VariableKey.CUSTOM.value,
                ),
            ]
            
        except Exception as e:
            return [self.create_text_message(f"Failed to process file: {str(e)}")]

    def get_runtime_parameters(self) -> list[ToolParameter]:
        parameters = []

        # コンテンツパラメータ
        parameters.append(
            ToolParameter(
                name="content",
                label=I18nObject(
                    en_US="Content",
                    ja_JP="内容"
                ),
                human_description=I18nObject(
                    en_US="The content to write to the file.",
                    ja_JP="ファイルに書き込む内容。"
                ),
                type=ToolParameter.ToolParameterType.STRING,
                form=ToolParameter.ToolParameterForm.LLM,
                required=True
            )
        )

        return parameters

dockerの運用方法

difyの運用はdockerで行われていることが多いのではないかと思います。
組み込みツールを入れて docker compose up しても、組み込みツールが入った状況で dify が動くことはありません。これは docker-compose で動いている dify-api が、dokcer hubから落としてきたdockerイメージを利用するためです。
そこで、ローカルで dify-api のdockerイメージを作成して、docker-compose のオーバーライド設定で、その docker-image を使うように設定してやります。

dify-apiは下記のようにしてローカルに「dify-api」というdockerイメージで作成します。

$ cd dify/api
$ docker build . -t dify-api

そのままdocker-composeを使うと、ローカルのdify-api dockerイメージが利用されないため、以下のようなdocker-compose.override.yamlを作成します。

dify/docker/docker-compose.override.yaml

services:
  api:
    image: dify-api:latest 

  worker:
    image: dify-api:latest

この状況で

$ docker compose up -d

すると組み込みツールが入った状況でdifyが起動します。

動作確認

difyが起動したら、「ツール」を確認して「My Tools」が存在しているか確認してください。
さらに「My Tools」を選択すると「File Writer」があること、パラメータがContent文字列が必須であることが確認できます。

スタジオでワークフローを作成し、「開始」の入力フィールドに「query」という文字列入力のパラメータを追加します。
開始 -> + -> ツール -> My Tools/File Writer を選択し、「内容」に「開始/(x)query」を選択します。 File Writer -> + -> 「終了」で、出力変数に「FIle Writer/(x)files」を選択します。

dify-filewriter

組み込みツールがファイルを出力する場所

ツールがファイルを出力する場所は、difyをdockerで動かしている場合、以下のdocker用のディレクトリの中に、さらにUIDでディレクトリが作られて、その中に出力されます。

dify/docker/volumes/app/storage/tools/

例えば以下のようなファイル名でテキストファイルが作成されます。

dify/docker/volumes/app/storage/tools/de4a84eb-ac6e-45d3-a8b6-49f776dfc900/cf4ff00b019a4f9c81cce53cc0137aa3.txt

URLを確認しても正しそうに見えるのにフィッシングサイトにつながってしまう攻撃手法

フィッシングのHTMLメール等で、ちゃんとURLを確認しても正しそうに見えるのに、フィッシングサイトに繋がってしまう攻撃手法が紹介されていました。


この手法を使って、以下のように例を作ってみました。

こんなふうにURLが書いてあると、ソースを確認しても「https ://www.smbc-card.com/…」に繋がりそうに見えますが、実際にはヤフーの天気が表示されてしまいます。

<a href="https://www.smbc-card.com⁄login⁄user=foobar@%77%65%61%74%68%65%72%2E%79%61%68%6F%6F%2E%63%6F%2E%6A%70/%77%65%61%74%68%65%72/">www.smbc-card.com</a>


実際のリンク例

https://www.smbc-card.com⁄login⁄user=foobar@%77%65%61%74%68%65%72%2E%79%61%68%6F%6F%2E%63%6F%2E%6A%70/%77%65%61%74%68%65%72/


twitterで簡単な説明を書いたのですが、もう少し詳しく説明します。



偽装手法

基本的な手法としてBasic認証のURL記述が利用されています。

Basic認証とは、ウェブサイトへユーザ名とパスワードでアクセス認証するための最も単純な認証方式の一つです。
Basic認証ではユーザ名とパスワードが必要ですが、URLにユーザ名とパスワードを含んだ形での記述が出来ます。
URLの形式は以下のようになっています。

https://username:password@example.com

このときパスワードは省略して

https://username@example.com

と書くことが出来ます。


この形式を悪用し、攻撃者は以下のようなURLを作成しています。

https://www.smbc-card.com@weather.yahoo.co.jp/weather/

つまり、ぱっと見で接続先サイトに見えるところが実はユーザ名になっているわけです。


攻撃者さらに、下記のようにURLエンコーディングを使用して実際のアクセス先を隠蔽することで、より疑われにくいURLにしています。

https://www.smbc-card.com@%77%65%61%74%68%65%72%2E%79%61%68%6F%6F%2E%63%6F%2E%6A%70/%77%65%61%74%68%65%72/


さらに、一般的なスラッシュ「/」の代わりに分数スラッシュ「⁄」(U+2044)を使用しています。
ユーザ名部分に「/」が使えないのですが、U+2044のスラッシュならば使うことが出来ます。
これにより、ドメインだけでなくより自然にログイン用のURLっぽく偽装することが可能です。

https://www.smbc-card.com⁄login⁄user=foobar@%77%65%61%74%68%65%72%2E%79%61%68%6F%6F%2E%63%6F%2E%6A%70/%77%65%61%74%68%65%72/

ここまでしなくても、ドメイン名を見せかけることができれば十分に思いますが、ドメイン名(本来のユーザ名)の後ろに送付先のユーザID情報を持たせたいとかがあったのかもしれません。

対策方法

単に、真に接続する先のURLを確認してもらうだけでも難しいのに、このような方法があるというのを普通の人に認識してもらってURLの確認してもらうのはまず不可能だと思います。

なので究極的には「利用している会社からであっても、送られてきたメール・SNSメッセージのURLは開かない」に尽きると思います。

名前を騙られて送られてくるメールや、アカウントを乗っ取られて送られてくるメッセージもあるため、知っている相手から送られてきたものでも、タイミング的におかしければ信用しないべきでしょう。

NSEG 勉強会 #110 「IT 技術者とカネ」

NSEG勉強会がいつもとちょっと違った内容のネタで開かれ、自分も発表をしてきました。

nseg.connpass.com

今回は内容どころか演題も非公開にさせていただいたのですが、渾身の内容だったと思います。
真面目に技術的なことばっかやってても、大変な目に巻き込まれることがあるので気をつけないと、という内容です。

ちょうど一つ前のエントリが受注先の倒産話ですが、そちらではなくてその一つ前のほうの話でした。

受注先の会社が倒産した件

去年の8月くらいから、LLM+RAGのPoCおよびシステム開発の仕事を受注させていただいていた会社が倒産しました。

一緒に仕事させていただいていたCTOの東さんが書かれたエントリです。
ちょっと話題になったんですが、今(2024/8/30現在)は非公開になっちゃってますね。
note.com

受託開発で関わっていた人たちはみなさん債権者となってしまいました。もちろん僕もです。
今年の春には業務拡大のためにオフィスを移転されたりして、景気いいなあ…と思っていたところでこれだったのでだいぶびっくりしました。

仕事してた会社が(事実上の)倒産が1年ぶり2連続という状況で、お祓いにでも行ったほうがいいのかしら… となっています。

GCPの会社アカウントに登録しても他のプロジェクトに入れない問題

Googleの会社アカウントを発行していただいてあり、そのアカウントでGCPにも権限登録してもらったにもかかわらず、GCPログインしても他のプロジェクトが見えてこない、入れない、という問題が起きました。

アカウントの登録は出来ているため、自分でプロジェクトを作ることは出来て、それは管理者の方からは問題なく見ることも出来ていました。
なので、ログイン出来ないとかそういう問題ではなく、また権限も何度も確認していただいて、問題ない状況でした。

すごく色々と調べたり調べてもらったりして、で最終的には下記のインビテーションのURLを叩く、という方法で解決できました。

stackoverflow.com

一度入れてしまえばあとは通常通り使えるようになりました。

とにかく原因がわからなくってめっちゃ困ったので、参考になれば。

NSEGで「RAGの仕組みとよくある課題」という話をしました

半年ぶりにNSEG開催されました。

nseg.connpass.com

ここ1年くらいLLM+RAGのシステムを書いていたので、RAGについての簡単な説明と、どういった課題があるのかについて発表しました。

speakerdeck.com

自分の関わってたものでは、とにかく回答品質を上げるために、精度の高いテキスト化が一番重要でした。
渡されてくるPDFをライブラリ等でそのままテキスト化しても「人間が見た目での構造化がされている」ため、あまり精度が高くならず、高品質にするには人間が構造化したテキストへ変換するのがもっとも効果的でした。

Dell G15でWiFiが繋がってるのにインターネット接続が切れる問題

最近、コワーキングスペースで仕事をするようになってノートPCとディスプレイ持って通っているのですが、やってるとWiFiが繋がっているのにインターネット接続が切れる、という状況がちょくちょく発生して困ることがありました。
また、いきなりWiFiの電波強度が1になっていたりして、なんで?となっていました。

調べてみると、WiFiの電波強度が1になるのは2.4Gになってしまっていて、接続速度も50Mとかになっていました。
が、それだけでインターネットへの接続が切れてしまうというのもおかしいので、もうちょっと調べてみました。

そうするとこのノートだと結構ある問題?らしいということがわかりました。

www.dell.com

WiFiのドライバーというか「Killer Intelligence Center」という専用のアプリがあり、そいつが余計なことしてYouTubeとかへの接続ができなくなったりするらしい。

n1729.com
n1729.com

でもWiFi自体は繋がっているのにインターネット接続が切れるというのは、Smart Byteというアプリによる例が多かったのですが、自分の環境には入っていませんでした。

でもなんにしてもKiller Intelligence Centerがあやしいのでこれの制御を一旦全部切ってみました。
するとそれだけで安定的に繋がるようになりました。

上記のエントリではKiller Network Service自体を止めるようにされていて、これ結構メモリやCPU使ってるとき有るので、そうしてしまったほうがよさそうに思います。

NSEG2024 新年フリープレゼン大会で音声変換と生成AIについて話しました

久しぶりにNSEG勉強会が行われたので、この1年半(実質は1年くらい)やってた、ディープラーニングを利用した音声変換やニューラル圧縮について話をしました。

NSEG 2024 新年フリープレゼン大会 - 資料一覧 - connpass nseg.connpass.com

音声変換と生成AI:開発者視点からの1.5年の振り返り - Speaker Deck
speakerdeck.com

今回、いつものような技術的な解説ではなくて、どちらかというと僕のこの1年半にあったことの振り返りを他の方にも追体験していただき、その中で音声系を中心とした生成系AIでどんなことが起きてたかを感じてもらう、みたいなものをめざしました。

なのでプレゼン資料の枚数がすごく多くなってしまったのですが、内容は薄めです。

でも、以前やった勉強会で話したことが、例えば5年前に話してたようなことが、当時はこんな感覚だったんだなってことが資料として使えたので、今回のこれも、2022~3年あたりで音声生成とか変換ってこのあたりの地点だったんだな、というのが振り替えれるものになるのではと思っています。

とにかく、ディープラーニング系の技術は元々流速早かったのが、生成系AIの流れで更に急速に速くなっているので、なんとかその流れに乗って、どう変化していくのかを味わいたいところです。

このプレゼンで書いてるように、もうあと4年以内できっと僕の能力を超えられてると思っているので…

GKE内のpostgresをGUIで触りたい

GKEのpostgresの中身を確認したいとき、一旦アプリケーションのpodのshellに入って、そこからpsqlコマンド叩いて接続してSQL叩いて、という方法で見ていました。

が、さすがにだるすぎるので、普通に簡単にGUIで見れる方法があるだろうとGPT-4様に聞いてみました。
kubectlでポートフォワード設定出来るので、ポートフォワードしてそこに繋げばいいとのこと。

kubectl port-forward svc/postgres-service 15432:5432

これでGKE内のpostgresのポート「5432」→ ローカルのポート「15432」へポートフォワードしてくれるとのこと。

でまあこれで用は足せるのですが、実は普段からk9sを使っているので、k9sを使ってもできることがわかりました。

k9sを起動して、serviceとかpodの表示をしていると上部エニューに
「<shift-f> Port-Forward」
と出てくるので、postgresのサービスやpodの上で S-f 押すと、ポートフォワードの設定画面が出るので適当に好きなフォワード先のポートを指定します。

それだけです。

これだけのことなのですが、GKE内のデータ確認の心理的めんどくささがだいぶ減りました。