Ollamaを使ってみる その4(LLM同士の会話、Gemma3:4B、Python、Node-RED)
はじめに
今回はOllamaを利用して、ローカルLLM同士で会話させてみました。
以前の記事でPythonのライブラリを利用して、会話の履歴を保持したままやり取りできるようになりました。その出力を、ローカルLLMの入力として繋げることで、会話できるようにしています。
▼PCは10万円ぐらいで購入したゲーミングノートPCを利用しています。すごくスペックが高いPCというわけではありません。
▼以前の記事はこちら
Node-REDのフロー作成
以前の記事でOllamaを利用したTCPサーバーを立てることができるようにしました。今回はそのフローをもとに作成していきます。
▼こちらの記事です。
PythonでそれぞれOllamaを利用したTCPサーバーを立てて、そのサーバーに対してtcp requestノードでメッセージを送信します。
▼フローはこちら

この時点のJSONファイルを残していなかったのですが、ほとんど以前の記事のままです。switchノードでサーバーを起動したときのメッセージを除外しています。
▼Client connectedとListening onが含まれないメッセージが流れていきます。

一番最初の人間の入力は、injectノードかtext inputノードから行うようにしています。なおTCPサーバーはbyeを送信すると終了するようになっています。Ollamaとコマンドプロンプトで会話するときと同じです。
会話させてみる
作成したフローで実行してみました。ローカルLLMのモデルはどちらもGemma3:4Bです。
まずはこんにちはという挨拶から始めました。
▼人間の会話よりも速く、雑談が進行していました。

こんにちはと送っただけでしたが、AI同士で質問内容を提案して会話が進んでいきました。なぜかAIが素敵な景色を見た前提です。
▼実行するたびに会話の内容は変わります。AIはYouTubeの動画を見たり、学習データとして小説を読んだりするんですね...

途中でエラーが表示されることがあったので、対処する必要がありそうです。
▼会話を中断したかったかのような終わり方をしていました。

回答を短くするようにして、Delayなどで調整をしたのですが、また使いながら改良していこうと思っています。
▼回答が短く、会話が続かないときみたいになっていることもありました。

▼同じモデル同士だからなのか、基本的にお互いに対して肯定的です。

▼いい感じに会話が続くと、専門知識を交えた議論が進んでいきました。


▼動画ではこちら。ロボットのシミュレーションについて会話が続いています。
フローの改良
Dashboard 2.0を利用して、フローを改良しました。
▼かなり規模が大きくなりましたが、全体のフローはこちら

[{"id":"4695b2de51635342","type":"venv","z":"22eb2b8f4786695c","venvconfig":"015784e9e3e0310a","name":"Node-2","code":"import socket\nimport ollama\nimport threading\nimport sys\n\nsys.stdout.reconfigure(encoding=\"utf-8\")\n\nHOST = '127.0.0.1'\nPORT = 5002\nMAX_HISTORY = 20\nchat_history = []\nserver_socket = None\nrunning = True\n\ndef chat_with_ollama(user_input):\n global chat_history\n \n # 履歴の長さを制限\n if len(chat_history) >= MAX_HISTORY:\n chat_history.pop(0)\n\n chat_history.append({'role': 'user', 'content': user_input})\n \n response = ollama.chat(model='gemma3:4b', messages=chat_history)\n chat_reply = response['message']['content']\n chat_history.append({'role': 'assistant', 'content': chat_reply})\n return chat_reply\n\ndef handle_client(client_socket):\n \"\"\" クライアントごとの処理 \"\"\"\n global running\n with client_socket:\n print(f\"Client connected: {client_socket.getpeername()}\")\n while True:\n try:\n data = client_socket.recv(1024).decode().strip()\n if not data:\n break\n if data.lower() == \"bye\":\n print(\"Received 'bye', shutting down server...\")\n client_socket.sendall(\"Ollama Shutdown\".encode(\"utf-8\"))\n running = False # サーバーの実行を停止\n break\n\n response = chat_with_ollama(data)\n client_socket.sendall(response.encode(\"utf-8\"))\n except (ConnectionResetError, BrokenPipeError, UnicodeDecodeError) as e:\n print(f\"Error: {e}\")\n break\n print(\"Client disconnected.\")\n\ndef server_thread():\n \"\"\" サーバーをスレッドで実行 \"\"\"\n global server_socket, running\n\n server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n \n try:\n server_socket.bind((HOST, PORT))\n print(f\"Server bound to port {PORT} successfully.\")\n except Exception as e:\n print(f\"Error binding server to port {PORT}: {e}\")\n return\n\n try:\n server_socket.listen()\n print(f\"Server is now listening on {HOST}:{PORT}...\")\n except Exception as e:\n print(f\"Error in server listening: {e}\")\n return\n\n server_socket.settimeout(1.0) # 1秒ごとに `Ctrl+C` チェック\n\n try:\n while running:\n try:\n client_socket, addr = server_socket.accept()\n print(f\"Accepted connection from {addr}\")\n client_thread = threading.Thread(target=handle_client, args=(client_socket,))\n client_thread.start()\n except socket.timeout:\n continue # タイムアウトしてもループを継続\n except Exception as e:\n print(f\"Server error: {e}\")\n finally:\n if server_socket:\n server_socket.close()\n print(\"Server shut down.\")\n\ndef main():\n global running\n try:\n server = threading.Thread(target=server_thread)\n server.start()\n\n while running:\n pass # メインスレッドを維持して `Ctrl+C` を待つ\n except KeyboardInterrupt:\n print(\"\\nShutting down server...\")\n running = False # サーバーの実行を停止\n server.join() # スレッドを待機\n sys.exit(0)\n\nif __name__ == \"__main__\":\n main()\n","continuous":true,"x":580,"y":4440,"wires":[["cd3f2a91dc5324a2"]]},{"id":"8c5e08c29ef38f4e","type":"inject","z":"22eb2b8f4786695c","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":430,"y":4440,"wires":[["4695b2de51635342"]]},{"id":"1637bd2a5c4d2f1a","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"terminate","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":4480,"wires":[["4695b2de51635342"]]},{"id":"d8b424b38989a7f9","type":"ui-button","z":"22eb2b8f4786695c","group":"59d519b3e322c6b9","name":"","label":"Start","order":1,"width":"2","height":"1","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"true","payloadType":"bool","topic":"topic","topicType":"msg","buttonColor":"green","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":190,"y":4440,"wires":[["4695b2de51635342","2f2e104de7d1d83d","c50d5dedab76010c"]]},{"id":"cd3f2a91dc5324a2","type":"debug","z":"22eb2b8f4786695c","name":"debug 466","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":730,"y":4440,"wires":[]},{"id":"e0d0682c92447c9e","type":"ui-button","z":"22eb2b8f4786695c","group":"59d519b3e322c6b9","name":"","label":"Shutdown","order":2,"width":"2","height":"1","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"false","payloadType":"bool","topic":"topic","topicType":"msg","buttonColor":"red","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":200,"y":4480,"wires":[["1637bd2a5c4d2f1a","2f2e104de7d1d83d","afbaf17313ed3656"]]},{"id":"2f2e104de7d1d83d","type":"ui-switch","z":"22eb2b8f4786695c","name":"","label":"Continue","group":"59d519b3e322c6b9","order":3,"width":"2","height":"1","passthru":true,"decouple":false,"topic":"topic","topicType":"msg","style":"","className":"","layout":"row-spread","clickableArea":"switch","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":260,"y":4840,"wires":[["d2676ffab4375515"]]},{"id":"c50d5dedab76010c","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"send-once","pt":"flow","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":4520,"wires":[[]]},{"id":"afbaf17313ed3656","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"send-once","pt":"flow","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":4560,"wires":[[]]},{"id":"d2676ffab4375515","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"continue-node-2","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":4840,"wires":[[]]},{"id":"83776ba6a7ac02c5","type":"ui-button","z":"22eb2b8f4786695c","group":"f0bbe692c3fc1950","name":"","label":"Start","order":1,"width":"2","height":"1","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"true","payloadType":"bool","topic":"topic","topicType":"msg","buttonColor":"green","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":190,"y":4360,"wires":[["7543d889d219314b","ebed58f313ff3b17","c50d5dedab76010c"]]},{"id":"7dd877a42e38125c","type":"ui-button","z":"22eb2b8f4786695c","group":"f0bbe692c3fc1950","name":"","label":"Shutdown","order":2,"width":"2","height":"1","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"false","payloadType":"bool","topic":"topic","topicType":"msg","buttonColor":"red","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":200,"y":4400,"wires":[["ad1e10bf3e127848","ebed58f313ff3b17","afbaf17313ed3656"]]},{"id":"3b5d0fcc82970fbe","type":"inject","z":"22eb2b8f4786695c","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":290,"y":4560,"wires":[["afbaf17313ed3656"]]},{"id":"7543d889d219314b","type":"venv","z":"22eb2b8f4786695c","venvconfig":"015784e9e3e0310a","name":"Node-1","code":"import socket\nimport ollama\nimport threading\nimport sys\n\nsys.stdout.reconfigure(encoding=\"utf-8\")\n\nHOST = '127.0.0.1'\nPORT = 5001\nMAX_HISTORY = 20\nchat_history = []\nserver_socket = None\nrunning = True\n\ndef chat_with_ollama(user_input):\n global chat_history\n \n # 履歴の長さを制限\n if len(chat_history) >= MAX_HISTORY:\n chat_history.pop(0)\n\n chat_history.append({'role': 'user', 'content': user_input})\n \n response = ollama.chat(model='gemma3:4b', messages=chat_history)\n chat_reply = response['message']['content']\n chat_history.append({'role': 'assistant', 'content': chat_reply})\n return chat_reply\n\ndef handle_client(client_socket):\n \"\"\" クライアントごとの処理 \"\"\"\n global running\n with client_socket:\n print(f\"Client connected: {client_socket.getpeername()}\")\n while True:\n try:\n data = client_socket.recv(1024).decode().strip()\n if not data:\n break\n if data.lower() == \"bye\":\n print(\"Received 'bye', shutting down server...\")\n client_socket.sendall(\"Ollama Shutdown\".encode(\"utf-8\"))\n running = False # サーバーの実行を停止\n break\n\n response = chat_with_ollama(data)\n client_socket.sendall(response.encode(\"utf-8\"))\n except (ConnectionResetError, BrokenPipeError, UnicodeDecodeError) as e:\n print(f\"Error: {e}\")\n break\n print(\"Client disconnected.\")\n\ndef server_thread():\n \"\"\" サーバーをスレッドで実行 \"\"\"\n global server_socket, running\n\n server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n \n try:\n server_socket.bind((HOST, PORT))\n print(f\"Server bound to port {PORT} successfully.\")\n except Exception as e:\n print(f\"Error binding server to port {PORT}: {e}\")\n return\n\n try:\n server_socket.listen()\n print(f\"Server is now listening on {HOST}:{PORT}...\")\n except Exception as e:\n print(f\"Error in server listening: {e}\")\n return\n\n server_socket.settimeout(1.0) # 1秒ごとに `Ctrl+C` チェック\n\n try:\n while running:\n try:\n client_socket, addr = server_socket.accept()\n print(f\"Accepted connection from {addr}\")\n client_thread = threading.Thread(target=handle_client, args=(client_socket,))\n client_thread.start()\n except socket.timeout:\n continue # タイムアウトしてもループを継続\n except Exception as e:\n print(f\"Server error: {e}\")\n finally:\n if server_socket:\n server_socket.close()\n print(\"Server shut down.\")\n\ndef main():\n global running\n try:\n server = threading.Thread(target=server_thread)\n server.start()\n\n while running:\n pass # メインスレッドを維持して `Ctrl+C` を待つ\n except KeyboardInterrupt:\n print(\"\\nShutting down server...\")\n running = False # サーバーの実行を停止\n server.join() # スレッドを待機\n sys.exit(0)\n\nif __name__ == \"__main__\":\n main()\n","continuous":true,"x":580,"y":4360,"wires":[["f2c4cc31c97b0dcd"]]},{"id":"ebed58f313ff3b17","type":"ui-switch","z":"22eb2b8f4786695c","name":"","label":"Continue","group":"f0bbe692c3fc1950","order":3,"width":"2","height":"1","passthru":true,"decouple":false,"topic":"topic","topicType":"msg","style":"","className":"","layout":"row-spread","clickableArea":"switch","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":260,"y":4780,"wires":[["cdefba7dbbd18f31"]]},{"id":"ad1e10bf3e127848","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"terminate","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":4400,"wires":[["7543d889d219314b"]]},{"id":"96ee55fd5dc236e9","type":"inject","z":"22eb2b8f4786695c","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":430,"y":4360,"wires":[["7543d889d219314b"]]},{"id":"f2c4cc31c97b0dcd","type":"debug","z":"22eb2b8f4786695c","name":"debug 463","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":730,"y":4360,"wires":[]},{"id":"cdefba7dbbd18f31","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"continue-node-1","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":4780,"wires":[[]]},{"id":"f19311be04711f62","type":"inject","z":"22eb2b8f4786695c","name":"","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":930,"y":4520,"wires":[["f99b02e76ff22398"]]},{"id":"f99b02e76ff22398","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"あなたはAI同士の対話システムの一部です。\nあなたのユーザー名はNode-2です。\nユーザー名を含めてあなたの意見を短く回答してください。\nあなたはNode-1とは異なる視点から議論してください。","output":"str","x":1080,"y":4520,"wires":[["b7f918efd016f500"]]},{"id":"1c5b9934c0c846c2","type":"ui-button","z":"22eb2b8f4786695c","group":"133cd157318ae5b6","name":"","label":"Send Node-2","order":4,"width":0,"height":0,"emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"","payloadType":"str","topic":"topic","topicType":"msg","buttonColor":"","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":950,"y":4560,"wires":[["f99b02e76ff22398"]]},{"id":"b7f918efd016f500","type":"ui-text-input","z":"22eb2b8f4786695c","group":"133cd157318ae5b6","name":"","label":"Initial Setting (Node-2)","order":3,"width":"6","height":"2","topic":"topic","topicType":"msg","mode":"textarea","tooltip":"","delay":300,"passthru":true,"sendOnDelay":false,"sendOnBlur":true,"sendOnEnter":true,"className":"","clearable":false,"sendOnClear":false,"icon":"","iconPosition":"left","iconInnerPosition":"inside","x":1280,"y":4520,"wires":[["ea0788b313cf5a0c"]]},{"id":"ea0788b313cf5a0c","type":"tcp request","z":"22eb2b8f4786695c","name":"","server":"127.0.0.1","port":"5002","out":"time","ret":"string","splitc":" 100","newline":"","trim":false,"tls":"","x":1510,"y":4520,"wires":[["a6365f703cdefc49","df0c96fa2fd89288"]]},{"id":"a6365f703cdefc49","type":"ui-template","z":"22eb2b8f4786695c","group":"59d519b3e322c6b9","page":"","ui":"","name":"","order":4,"width":"0","height":"0","head":"","format":"<template>\n <h2>Response</h2>\n <div id=\"node-2\"></div>\n</template>\n\n<script>\n this.$socket.on('msg-input:' + this.id, function(msg) {\n const markdownText = msg.payload;\n const htmlContent = marked(markdownText);\n document.getElementById('node-2').innerHTML = htmlContent;\n });\n</script>\n\n<style>\n #node-2 {\n padding: 10px;\n font-family: Arial, sans-serif;\n }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1240,"y":4980,"wires":[[]]},{"id":"df0c96fa2fd89288","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"voiceID","pt":"msg","to":"29","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1720,"y":4520,"wires":[["4c1faedac11c9e5b"]]},{"id":"1e54c4a6e7d25700","type":"switch","z":"22eb2b8f4786695c","name":"","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"Client connected","vt":"str"},{"t":"cont","v":"Listening on","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":1070,"y":4900,"wires":[[],[],["a6365f703cdefc49","c1153858ace9e786","2af30fdcb359b087","e74efabd3871c5f9"]]},{"id":"e8da1162f4b37224","type":"inject","z":"22eb2b8f4786695c","name":"Reset","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":960,"y":4800,"wires":[["af6a3cbae08d5c05","a6365f703cdefc49"]]},{"id":"4c1faedac11c9e5b","type":"link out","z":"22eb2b8f4786695c","name":"gTTS","mode":"link","links":["fe521de6f72d8f1f"],"x":1845,"y":4520,"wires":[]},{"id":"39b1d7c666707e4f","type":"tcp request","z":"22eb2b8f4786695c","name":"TCP:5002","server":"127.0.0.1","port":"5002","out":"time","ret":"string","splitc":" 100","newline":"","trim":false,"tls":"","x":920,"y":4860,"wires":[["8de97e9714f5509a","1e54c4a6e7d25700"]]},{"id":"c1153858ace9e786","type":"switch","z":"22eb2b8f4786695c","name":"","property":"continue-node-2","propertyType":"flow","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":1230,"y":4940,"wires":[["543dec888bf5f431"]]},{"id":"2af30fdcb359b087","type":"file","z":"22eb2b8f4786695c","name":"","filename":"node-2.txt","filenameType":"str","appendNewline":true,"createDir":true,"overwriteFile":"false","encoding":"none","x":1460,"y":5000,"wires":[[]]},{"id":"e74efabd3871c5f9","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"voiceID","pt":"msg","to":"29","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1260,"y":4900,"wires":[["6dd95bf3bbfb8997"]]},{"id":"af6a3cbae08d5c05","type":"ui-template","z":"22eb2b8f4786695c","group":"f0bbe692c3fc1950","page":"","ui":"","name":"","order":4,"width":"0","height":"0","head":"","format":"<template>\n <h2>Response</h2>\n <div id=\"node-1\"></div>\n</template>\n\n<script>\n this.$socket.on('msg-input:' + this.id, function(msg) {\n const markdownText = msg.payload;\n const htmlContent = marked(markdownText);\n document.getElementById('node-1').innerHTML = htmlContent;\n });\n</script>\n\n<style>\n #node-1 {\n padding: 10px;\n font-family: Arial, sans-serif;\n }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1240,"y":4780,"wires":[[]]},{"id":"81bbb0b4a775f1ca","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"voiceID","pt":"msg","to":"23","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1720,"y":4460,"wires":[["4c1faedac11c9e5b"]]},{"id":"534885a899478efa","type":"inject","z":"22eb2b8f4786695c","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"bye","payloadType":"str","x":770,"y":4820,"wires":[["39b1d7c666707e4f"]]},{"id":"75c39b9166c382eb","type":"link in","z":"22eb2b8f4786695c","name":"TCP:5002","links":["4b22afbf2a44012b"],"x":805,"y":4860,"wires":[["39b1d7c666707e4f","e5418034e0b5aca8"]]},{"id":"8de97e9714f5509a","type":"debug","z":"22eb2b8f4786695c","name":"debug 467","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1090,"y":4860,"wires":[]},{"id":"b931621545cb30bb","type":"catch","z":"22eb2b8f4786695c","name":"","scope":["d81bba47fda884db"],"uncaught":false,"x":1070,"y":4940,"wires":[["c1153858ace9e786"]]},{"id":"543dec888bf5f431","type":"delay","z":"22eb2b8f4786695c","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1380,"y":4940,"wires":[["b531d62bdb740a8a"]]},{"id":"6dd95bf3bbfb8997","type":"link out","z":"22eb2b8f4786695c","name":"gTTS","mode":"link","links":["fe521de6f72d8f1f"],"x":1325,"y":4860,"wires":[]},{"id":"cb8d6ee0ec00e9fc","type":"switch","z":"22eb2b8f4786695c","name":"","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"Client connected","vt":"str"},{"t":"cont","v":"Listening on","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":1070,"y":4700,"wires":[[],[],["af6a3cbae08d5c05","f2201ff3b9c9a3f3","d4fd4dde6372035a","7a1d68da210cdb9b"]]},{"id":"610b72b12f56b765","type":"tcp request","z":"22eb2b8f4786695c","name":"","server":"127.0.0.1","port":"5001","out":"time","ret":"string","splitc":"100","newline":"","trim":false,"tls":"","x":1510,"y":4460,"wires":[["af6a3cbae08d5c05","81bbb0b4a775f1ca"]]},{"id":"e5418034e0b5aca8","type":"debug","z":"22eb2b8f4786695c","name":"debug 516","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":840,"y":4920,"wires":[]},{"id":"b531d62bdb740a8a","type":"switch","z":"22eb2b8f4786695c","name":"","property":"user-input","propertyType":"flow","rules":[{"t":"neq","v":"","vt":"prev"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":1450,"y":4880,"wires":[["47aeddebdfb078d4"],["ce02f2ed9038e99a"]]},{"id":"d81bba47fda884db","type":"tcp request","z":"22eb2b8f4786695c","name":"TCP:5001","server":"127.0.0.1","port":"5001","out":"time","ret":"string","splitc":"100","newline":"","trim":false,"tls":"","x":920,"y":4660,"wires":[["254fd2239e5cf0ce","cb8d6ee0ec00e9fc"]]},{"id":"f2201ff3b9c9a3f3","type":"switch","z":"22eb2b8f4786695c","name":"","property":"continue-node-1","propertyType":"flow","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":1230,"y":4740,"wires":[["d5e19c1212f721af"]]},{"id":"d4fd4dde6372035a","type":"file","z":"22eb2b8f4786695c","name":"","filename":"node-1.txt","filenameType":"str","appendNewline":true,"createDir":true,"overwriteFile":"false","encoding":"none","x":1460,"y":4800,"wires":[[]]},{"id":"7a1d68da210cdb9b","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"voiceID","pt":"msg","to":"23","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1260,"y":4700,"wires":[["37ea1b8efa506958"]]},{"id":"589e31dd8ec5d417","type":"ui-text-input","z":"22eb2b8f4786695c","group":"133cd157318ae5b6","name":"","label":"Initial Setting (Node-1)","order":1,"width":"6","height":"2","topic":"topic","topicType":"msg","mode":"textarea","tooltip":"","delay":300,"passthru":true,"sendOnDelay":false,"sendOnBlur":true,"sendOnEnter":true,"className":"","clearable":false,"sendOnClear":false,"icon":"","iconPosition":"left","iconInnerPosition":"inside","x":1280,"y":4460,"wires":[["610b72b12f56b765"]]},{"id":"47aeddebdfb078d4","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{{payload}}\nユーザーからの入力:{{flow.user-input}}","output":"str","x":1600,"y":4860,"wires":[["ce02f2ed9038e99a"]]},{"id":"ce02f2ed9038e99a","type":"junction","z":"22eb2b8f4786695c","x":1580,"y":4920,"wires":[["919980cc7ae31924"]]},{"id":"9c47865553875349","type":"inject","z":"22eb2b8f4786695c","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"bye","payloadType":"str","x":770,"y":4620,"wires":[["d81bba47fda884db"]]},{"id":"35f2faa12208f01d","type":"switch","z":"22eb2b8f4786695c","name":"","property":"send-once","propertyType":"flow","rules":[{"t":"false"}],"checkall":"true","repair":false,"outputs":1,"x":610,"y":4660,"wires":[["570cc79486946e82","d81bba47fda884db"]]},{"id":"0ded08d45b7f994f","type":"link in","z":"22eb2b8f4786695c","name":"TCP:5002","links":["919980cc7ae31924"],"x":805,"y":4680,"wires":[["d81bba47fda884db","27802d1da7d5cfe8"]]},{"id":"254fd2239e5cf0ce","type":"debug","z":"22eb2b8f4786695c","name":"debug 464","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1090,"y":4660,"wires":[]},{"id":"3ba40b2bff69ada1","type":"catch","z":"22eb2b8f4786695c","name":"","scope":["39b1d7c666707e4f"],"uncaught":false,"x":1070,"y":4740,"wires":[["f2201ff3b9c9a3f3"]]},{"id":"d5e19c1212f721af","type":"delay","z":"22eb2b8f4786695c","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1380,"y":4740,"wires":[["27ead22d6d2630d6"]]},{"id":"37ea1b8efa506958","type":"link out","z":"22eb2b8f4786695c","name":"gTTS","mode":"link","links":["fe521de6f72d8f1f"],"x":1325,"y":4660,"wires":[]},{"id":"7c879421615c256c","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"あなたはAI同士の対話システムの一部です。\nあなたのユーザー名はNode-1です。\nユーザー名を含めてあなたの意見を短く回答してください。","output":"str","x":1080,"y":4460,"wires":[["589e31dd8ec5d417"]]},{"id":"919980cc7ae31924","type":"link out","z":"22eb2b8f4786695c","name":"TCP:5001","mode":"link","links":["0ded08d45b7f994f"],"x":1635,"y":4920,"wires":[]},{"id":"c7a14f4465e71cbc","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"user-input","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":4660,"wires":[["35f2faa12208f01d"]]},{"id":"570cc79486946e82","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"send-once","pt":"flow","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":4720,"wires":[[]]},{"id":"27802d1da7d5cfe8","type":"debug","z":"22eb2b8f4786695c","name":"debug 515","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":920,"y":4600,"wires":[]},{"id":"27ead22d6d2630d6","type":"switch","z":"22eb2b8f4786695c","name":"","property":"user-input","propertyType":"flow","rules":[{"t":"neq","v":"","vt":"prev"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":1450,"y":4680,"wires":[["30db202a149c3c40"],["45ab7364b58a431d"]]},{"id":"9d13eadce7ba2dea","type":"inject","z":"22eb2b8f4786695c","name":"","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":930,"y":4460,"wires":[["7c879421615c256c"]]},{"id":"da494bbf861827b9","type":"ui-button","z":"22eb2b8f4786695c","group":"133cd157318ae5b6","name":"","label":"Send Node-1","order":2,"width":0,"height":0,"emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"","payloadType":"str","topic":"topic","topicType":"msg","buttonColor":"","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":950,"y":4420,"wires":[["7c879421615c256c"]]},{"id":"8ceef8ca5d75e5f5","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"ユーザーの入力:{{payload}}","output":"str","x":420,"y":4720,"wires":[["c7a14f4465e71cbc"]]},{"id":"2390935be5742f29","type":"ui-button","z":"22eb2b8f4786695c","group":"133cd157318ae5b6","name":"","label":"Send","order":6,"width":0,"height":0,"emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"","payloadType":"str","topic":"topic","topicType":"msg","buttonColor":"","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":270,"y":4660,"wires":[["c7a14f4465e71cbc"]]},{"id":"30db202a149c3c40","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{{payload}}\nユーザーからの入力:{{flow.user-input}}","output":"str","x":1600,"y":4660,"wires":[["45ab7364b58a431d"]]},{"id":"45ab7364b58a431d","type":"junction","z":"22eb2b8f4786695c","x":1580,"y":4720,"wires":[["4b22afbf2a44012b"]]},{"id":"930a483a4ca15f5e","type":"inject","z":"22eb2b8f4786695c","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"こんにちは","payloadType":"str","x":260,"y":4720,"wires":[["8ceef8ca5d75e5f5"]]},{"id":"4b22afbf2a44012b","type":"link out","z":"22eb2b8f4786695c","name":"TCP:5002","mode":"link","links":["75c39b9166c382eb"],"x":1635,"y":4720,"wires":[]},{"id":"015784e9e3e0310a","type":"venv-config","venvname":"AI","version":"3.10"},{"id":"59d519b3e322c6b9","type":"ui-group","name":"Node-2 (Gemma3:4B)","page":"b7ce0bd27a143e8e","width":"6","height":"1","order":3,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"f0bbe692c3fc1950","type":"ui-group","name":"Node-1 (Gemma3:4B)","page":"b7ce0bd27a143e8e","width":"6","height":"1","order":2,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"133cd157318ae5b6","type":"ui-group","name":"User","page":"b7ce0bd27a143e8e","width":"5","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"b7ce0bd27a143e8e","type":"ui-page","name":"AI","ui":"ba89d595c555beb9","path":"/ai","icon":"brain","layout":"grid","theme":"e2c9a4f37a42314e","breakpoints":[{"name":"Default","px":"0","cols":"20"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"ba89d595c555beb9","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":"1","showDisconnectNotification":true},{"id":"e2c9a4f37a42314e","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]
今回は含めていませんが、さらに音声合成の処理につなげています。
ダッシュボード画面ではそれぞれのAIに対する初期設定の送信、TCPサーバーの起動とシャットダウン、ユーザーからの入力を操作できます。
▼画面はこんな感じです。

ちなみにAIからの応答をMarkdown形式で表示するのに若干苦労しました。また別の記事にまとめようと思っています。
ダッシュボード右側のグラフは、去年の大阪24時間AIハッカソンの際に同じチームの方が作成されていたものが面白かったので入れてみました。
▼ハッカソンではこんな画面でした。

▼このポストでも利用されています。様々なAI系のツールを試されています。
今回は処理に余計な負荷をかけたくなかったので、10秒間隔で更新するようになっています。
▼Windowsのコマンドを利用して計算するようになっています。

[{"id":"24608505abbec992","type":"exec","z":"22eb2b8f4786695c","command":"nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits","addpay":"","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"GPU Usage","x":890,"y":5980,"wires":[["36a350315036bb0a","808df70d9f1baa6c"],["ee9d23ec3a5ad0fc"],[]]},{"id":"36a350315036bb0a","type":"debug","z":"22eb2b8f4786695c","name":"debug 486","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1070,"y":5960,"wires":[]},{"id":"ee9d23ec3a5ad0fc","type":"debug","z":"22eb2b8f4786695c","name":"debug 487","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1070,"y":6000,"wires":[]},{"id":"0419118f42586e94","type":"ui-gauge","z":"22eb2b8f4786695c","name":"","group":"cddf1256830eb7c5","order":3,"width":3,"height":"1","gtype":"gauge-34","gstyle":"needle","title":"GPU","units":"%","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#5cd65c"},{"from":"50","color":"#ffc800"},{"from":"80","color":"#ea5353"}],"min":0,"max":"100","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":1390,"y":6040,"wires":[]},{"id":"808df70d9f1baa6c","type":"csv","z":"22eb2b8f4786695c","name":"","spec":"rfc","sep":",","hdrin":"","hdrout":"none","multi":"one","ret":"\\r\\n","temp":"","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":1050,"y":6040,"wires":[["60f8cb9196c2e001"]]},{"id":"60f8cb9196c2e001","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.col1","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1220,"y":6040,"wires":[["0419118f42586e94"]]},{"id":"1e3aea53de34dacc","type":"exec","z":"22eb2b8f4786695c","command":"powershell -command \"& { $cpu_usage = (Get-Counter '\\Processor(_Total)\\% Processor Time').CounterSamples.CookedValue; [math]::Round($cpu_usage, 2) }\"","addpay":"","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"CPU Usage","x":890,"y":5840,"wires":[["5337286141bcbdea","e39ccfb856f25d7d"],["cf2779ab819608be"],[]]},{"id":"4ef7736fea215c1b","type":"inject","z":"22eb2b8f4786695c","name":"","props":[],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":730,"y":5840,"wires":[["1e3aea53de34dacc","24608505abbec992","5a616651108085dd"]]},{"id":"5337286141bcbdea","type":"debug","z":"22eb2b8f4786695c","name":"debug 489","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1070,"y":5820,"wires":[]},{"id":"cf2779ab819608be","type":"debug","z":"22eb2b8f4786695c","name":"debug 490","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1070,"y":5860,"wires":[]},{"id":"e39ccfb856f25d7d","type":"ui-gauge","z":"22eb2b8f4786695c","name":"","group":"cddf1256830eb7c5","order":1,"width":3,"height":"1","gtype":"gauge-34","gstyle":"needle","title":"CPU","units":"%","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#5cd65c"},{"from":"50","color":"#ffc800"},{"from":"80","color":"#ea5353"}],"min":0,"max":"100","sizeThickness":"16","sizeGap":4,"sizeKeyThickness":"8","styleRounded":true,"styleGlow":false,"className":"","x":1050,"y":5900,"wires":[]},{"id":"5a616651108085dd","type":"exec","z":"22eb2b8f4786695c","command":"powershell -command \"& { $TotalMemory = (Get-WmiObject Win32_OperatingSystem).TotalVisibleMemorySize / 1024; $FreeMemory = (Get-WmiObject Win32_OperatingSystem).FreePhysicalMemory / 1024; $UsedMemory = $TotalMemory - $FreeMemory; $UsagePercent = ($UsedMemory / $TotalMemory) * 100; Write-Output \\\"Total: $([math]::Round($TotalMemory,2)) GB, Used: $([math]::Round($UsedMemory,2)) GB, Usage: $([math]::Round($UsagePercent,2)) %\\\" }\"","addpay":"","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"Memory Usage","x":900,"y":6120,"wires":[["1f8af5c4c8059cdf","c01ce2400e8076f7"],["b3c2f86be0c0d0d7"],[]]},{"id":"1f8af5c4c8059cdf","type":"debug","z":"22eb2b8f4786695c","name":"debug 491","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1090,"y":6100,"wires":[]},{"id":"b3c2f86be0c0d0d7","type":"debug","z":"22eb2b8f4786695c","name":"debug 492","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1090,"y":6140,"wires":[]},{"id":"c01ce2400e8076f7","type":"csv","z":"22eb2b8f4786695c","name":"","spec":"rfc","sep":",","hdrin":"","hdrout":"none","multi":"one","ret":"\\r\\n","temp":"","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":1070,"y":6180,"wires":[["6f28389f0f2a32f1"]]},{"id":"6f28389f0f2a32f1","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.col3","tot":"msg"},{"t":"change","p":"payload","pt":"msg","from":"Usage: ","fromt":"str","to":"","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":" %","fromt":"str","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1240,"y":6180,"wires":[["d7751a2b5ebd6177"]]},{"id":"d7751a2b5ebd6177","type":"ui-gauge","z":"22eb2b8f4786695c","name":"","group":"cddf1256830eb7c5","order":2,"width":3,"height":"1","gtype":"gauge-34","gstyle":"needle","title":"Memory","units":"%","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#5cd65c"},{"from":"50","color":"#ffc800"},{"from":"80","color":"#ea5353"}],"min":0,"max":"100","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":1420,"y":6180,"wires":[]},{"id":"cddf1256830eb7c5","type":"ui-group","name":"Usage","page":"b7ce0bd27a143e8e","width":"3","height":"1","order":4,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"b7ce0bd27a143e8e","type":"ui-page","name":"AI","ui":"ba89d595c555beb9","path":"/ai","icon":"brain","layout":"grid","theme":"e2c9a4f37a42314e","breakpoints":[{"name":"Default","px":"0","cols":"20"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"ba89d595c555beb9","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":"1","showDisconnectNotification":true},{"id":"e2c9a4f37a42314e","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]
再度会話させてみる
作成したダッシュボード画面で操作して、AI同士で会話させてみました。
▼三人寄れば文殊の知恵ということわざについてどう思うかを聞いてみました。途中でユーザーからの入力も受け付けています。
▼対立的な立場で会話させると、なんだか険悪な感じで会話が進みました。
会話のテーマが抽象的だったからなのか、Node-2はテーマよりも言葉とか姿勢について指摘していることが多いという印象です。
実行できることは確認できたので、いろいろ試してみようと思っています。
最後に
この後音声合成と再生の処理に繋げると、ラジオみたいに会話が続いていました。会話の内容はテキストとしても残っているので、AIラジオとして利用することもできそうです。
どう使うかは会話のテーマ次第ですが、AIが生成したプログラムについても議論してもらえると、勝手に改善してくれるのではないかと考えています。