ホスト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を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エンコードされた部分が不正ドメインです。アクセス先のドメインURLエンコードしてもちゃんとブラウザでアクセスできちゃうんですね・・・ pic.twitter.com/yL6oUfFPa4
— Koichi (@x64koichi) 2024年8月29日
この手法を使って、以下のように例を作ってみました。
こんなふうに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>
実際のリンク例
twitterで簡単な説明を書いたのですが、もう少し詳しく説明します。
あーなるほど。ユーザ認証の形式「https ://username:password@example.com」を利用しているのか。
— Kiyoshi SATOH (@stealthinu) 2024年8月29日
「https ://www .smbc.com/foo/bar@%74%72%65%77%62」の「www .smbc.com/foo/bar」部分がユーザ名として扱われるのね。アクセス先の「example .com」はURLエンコードされてて怪しくても気付けない。 https://t.co/sKNbV3xTy8
偽装手法
基本的な手法としてBasic認証のURL記述が利用されています。
Basic認証とは、ウェブサイトへユーザ名とパスワードでアクセス認証するための最も単純な認証方式の一つです。
Basic認証ではユーザ名とパスワードが必要ですが、URLにユーザ名とパスワードを含んだ形での記述が出来ます。
URLの形式は以下のようになっています。
https://username:password@example.com
- 接続するサイト: https://example.com
- ユーザ名: username
- パスワード: password
このときパスワードは省略して
https://username@example.com
と書くことが出来ます。
この形式を悪用し、攻撃者は以下のようなURLを作成しています。
https://www.smbc-card.com@weather.yahoo.co.jp/weather/
- 接続するサイト: https://weather.yahoo.co.jp/weather/
- ユーザ名: www.smbc-card.com
つまり、ぱっと見で接続先サイトに見えるところが実はユーザ名になっているわけです。
攻撃者さらに、下記のように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勉強会がいつもとちょっと違った内容のネタで開かれ、自分も発表をしてきました。
今回は内容どころか演題も非公開にさせていただいたのですが、渾身の内容だったと思います。
真面目に技術的なことばっかやってても、大変な目に巻き込まれることがあるので気をつけないと、という内容です。
ちょうど一つ前のエントリが受注先の倒産話ですが、そちらではなくてその一つ前のほうの話でした。
受注先の会社が倒産した件
去年の8月くらいから、LLM+RAGのPoCおよびシステム開発の仕事を受注させていただいていた会社が倒産しました。
一緒に仕事させていただいていたCTOの東さんが書かれたエントリです。
ちょっと話題になったんですが、今(2024/8/30現在)は非公開になっちゃってますね。
note.com
受託開発で関わっていた人たちはみなさん債権者となってしまいました。もちろん僕もです。
今年の春には業務拡大のためにオフィスを移転されたりして、景気いいなあ…と思っていたところでこれだったのでだいぶびっくりしました。
仕事してた会社が(事実上の)倒産が1年ぶり2連続という状況で、お祓いにでも行ったほうがいいのかしら… となっています。
GCPの会社アカウントに登録しても他のプロジェクトに入れない問題
Googleの会社アカウントを発行していただいてあり、そのアカウントでGCPにも権限登録してもらったにもかかわらず、GCPログインしても他のプロジェクトが見えてこない、入れない、という問題が起きました。
アカウントの登録は出来ているため、自分でプロジェクトを作ることは出来て、それは管理者の方からは問題なく見ることも出来ていました。
なので、ログイン出来ないとかそういう問題ではなく、また権限も何度も確認していただいて、問題ない状況でした。
すごく色々と調べたり調べてもらったりして、で最終的には下記のインビテーションのURLを叩く、という方法で解決できました。
一度入れてしまえばあとは通常通り使えるようになりました。
とにかく原因がわからなくってめっちゃ困ったので、参考になれば。
NSEGで「RAGの仕組みとよくある課題」という話をしました
半年ぶりにNSEG開催されました。
ここ1年くらいLLM+RAGのシステムを書いていたので、RAGについての簡単な説明と、どういった課題があるのかについて発表しました。
自分の関わってたものでは、とにかく回答品質を上げるために、精度の高いテキスト化が一番重要でした。
渡されてくるPDFをライブラリ等でそのままテキスト化しても「人間が見た目での構造化がされている」ため、あまり精度が高くならず、高品質にするには人間が構造化したテキストへ変換するのがもっとも効果的でした。
Dell G15でWiFiが繋がってるのにインターネット接続が切れる問題
最近、コワーキングスペースで仕事をするようになってノートPCとディスプレイ持って通っているのですが、やってるとWiFiが繋がっているのにインターネット接続が切れる、という状況がちょくちょく発生して困ることがありました。
また、いきなりWiFiの電波強度が1になっていたりして、なんで?となっていました。
調べてみると、WiFiの電波強度が1になるのは2.4Gになってしまっていて、接続速度も50Mとかになっていました。
が、それだけでインターネットへの接続が切れてしまうというのもおかしいので、もうちょっと調べてみました。
そうするとこのノートだと結構ある問題?らしいということがわかりました。
WiFiのドライバーというか「Killer Intelligence Center」という専用のアプリがあり、そいつが余計なことしてYouTubeとかへの接続ができなくなったりするらしい。
でも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内のデータ確認の心理的めんどくささがだいぶ減りました。