前回の生成AIを使った自然言語で問い合わせをするというところで、ollamaをインストールして使ったのですが、TimescaleDBやGrafana、Metabaseに合わせてコンテナ上で動作するようにします。
ラズパイ上で動作しているollamaを停止して、起動しないようにします。
sudo systemctl stop ollama
sudo systemctl disable ollama
Ollamaのステータス確認
sudo systemctl status ollama
起動中
● ollama.service - Ollama Service
Loaded: loaded (/etc/systemd/system/ollama.service; enabled; preset: enabled)
Active: active (running) since Sun 2026-02-08 10:54:06 JST; 1 day 19h ago
Invocation: b5c9d1e01ccf48779c419d5e053fe244
Main PID: 1767 (ollama)
Tasks: 9 (limit: 19362)
CPU: 96ms
CGroup: /system.slice/ollama.service
└─1767 /usr/local/bin/ollama serve
Feb 08 10:54:06 raspi5 systemd[1]: Started ollama.service - Ollama Service.
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.371+09:00 level=INFO source=routes.go:1636 msg="server config" env="map[CUDA_VISIBLE_DEVICES: GGML_VK_VISIBLE_D>
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=images.go:473 msg="total blobs: 11"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=images.go:480 msg="total unused blobs removed: 0"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=routes.go:1689 msg="Listening on 127.0.0.1:11434 (version 0.15.5)"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.377+09:00 level=INFO source=runner.go:67 msg="discovering available GPUs..."
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.378+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engi>
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.798+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engi>
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=types.go:60 msg="inference compute" id=cpu library=cpu compute="" name=cpu descript>
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=routes.go:1739 msg="vram-based default context" total_vram="0 B" default_num_ctx=40>
停止後
○ ollama.service - Ollama Service
Loaded: loaded (/etc/systemd/system/ollama.service; disabled; preset: enabled)
Active: inactive (dead)
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=images.go:480 msg="total unused blobs removed: 0"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=routes.go:1689 msg="Listening on 127.0.0.1:11434 (version 0.15.5)"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.377+09:00 level=INFO source=runner.go:67 msg="discovering available GPUs..."
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.378+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 40417"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.798+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 36165"
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=types.go:60 msg="inference compute" id=cpu library=cpu compute="" name=cpu description=cpu libdirs=ollama driver="" pci_id=">
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=routes.go:1739 msg="vram-based default context" total_vram="0 B" default_num_ctx=4096
Feb 10 06:31:23 raspi5 systemd[1]: Stopping ollama.service - Ollama Service...
Feb 10 06:31:23 raspi5 systemd[1]: ollama.service: Deactivated successfully.
Feb 10 06:31:23 raspi5 systemd[1]: Stopped ollama.service - Ollama Service.
本題に入ります
Webアプリの作成
Webアプリ用のディレクトリを作成します。
mkdir -p ~/iot-factory/ai_app
cd ~/iot-factory/ai_app
ライブラリの一覧
requirements.txtにライブラリの一覧を書きます
vi requirements.txt
streamlit
pandas
psycopg2-binary
ollama
Dokcerfileの作成
vi Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Postgres用ライブラリとcurl(ヘルスチェック用)
RUN apt-get update && apt-get install -y \
libpq-dev gcc curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Streamlitのポート
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
動作するWebアプリを作成
ここはGeminiに頑張ってもらいました。w
vi app.py
import streamlit as st
import ollama
import psycopg2
import pandas as pd
import re
import os
# --- 設定エリア ---
# DB接続設定
DB_CONFIG = {
"host": "timescaledb", # Dockerのサービス名
"database": "factory_iot",
"user": "postgres",
"password": "password1234",
"port": "5432"
}
# Ollamaクライアントの初期化
# コンテナ間通信のため、ホスト名を指定したクライアントを作成します
ollama_host = os.getenv("OLLAMA_HOST", "http://ollama:11434")
client = ollama.Client(host=ollama_host)
# モデル設定
SQL_MODEL = "qwen2.5-coder:3b"
REPORT_MODEL = "llama3.2:3b"
SCHEMA_INFO = """
テーブル名: mqtt_consumer
カラム:
- time (TIMESTAMPTZ): 計測時刻
- company_name(TEXT):会社名
- office_name(TEXT):事業所名
- factory_name(TEXT):工場名
- line_name(TEXT):ライン名
- equipment_name(TEXT):機器名
- temperature (FLOAT): 気温
- pressure (FLOAT): 気圧
- humidity (FLOAT): 湿度
- noise (FLOAT): 騒音
- light (FLOAT): 照度
- etvoc (FLOAT): 二酸化炭素
- eco2 (INT): CO2濃度
"""
# --- 関数定義 ---
def get_sql_from_ai(question):
prompt = f"""
You are a PostgreSQL expert. Convert the question to a SQL query.
- Return ONLY the SQL. No markdown.
- Use 'sensor_data' table.
Schema: {SCHEMA_INFO}
Question: {question}
"""
try:
# ★ここを修正: ollama.chat() ではなく client.chat() を使う
response = client.chat(model=SQL_MODEL, messages=[{'role': 'user', 'content': prompt}])
sql = response['message']['content'].strip()
sql = re.sub(r'```sql\n?', '', sql)
sql = re.sub(r'```', '', sql)
return sql
except Exception as e:
return f"Error: {e}"
def execute_query_to_df(sql):
conn = None
try:
conn = psycopg2.connect(**DB_CONFIG)
df = pd.read_sql(sql, conn)
return df, None
except Exception as e:
return None, str(e)
finally:
if conn: conn.close()
def generate_report(question, df):
# データが空の場合のガード処理を追加
if df is None or df.empty:
return "データがないためレポートを作成できません。"
summary = df.describe().to_string()
prompt = f"""
Answer the user's question based on the data summary in Japanese.
Question: {question}
Data Summary: {summary}
"""
try:
# ★ここを修正: ollama.chat() ではなく client.chat() を使う
response = client.chat(model=REPORT_MODEL, messages=[{'role': 'user', 'content': prompt}])
return response['message']['content']
except Exception as e:
return f"Error: {e}"
# --- 画面構築 ---
st.set_page_config(page_title="Environment AI Dashboard", layout="wide")
st.title("📊 環境データ AI分析ダッシュボード")
if prompt := st.chat_input("質問を入力 (例: 昨日の気温の変化をグラフで見せて)"):
st.chat_message("user").markdown(prompt)
with st.chat_message("assistant"):
with st.spinner("AIが考え中..."):
sql = get_sql_from_ai(prompt)
st.code(sql, language="sql")
if "Error" in sql:
st.error(sql)
else:
df, error = execute_query_to_df(sql)
if error:
st.error(f"SQLエラー: {error}")
elif df is None or df.empty:
st.warning("データなし")
else:
if 'time' in df.columns:
df['time'] = pd.to_datetime(df['time'])
df = df.set_index('time')
st.line_chart(df)
else:
st.dataframe(df)
report = generate_report(prompt, df)
st.markdown(report)
docker-compose.ymlの修正
前回使っていたdocker-compose.ymlを修正します。
cd ~/iot-factory/
vi docker-compose.yml
services:
# --- 1. MQTT Broker ---
mosquitto:
image: eclipse-mosquitto:latest
container_name: mosquitto
restart: always
ports:
- "1883:1883"
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
# --- 2. Database (TimescaleDB / PostgreSQL) ---
timescaledb:
image: timescale/timescaledb:latest-pg16
container_name: timescaledb
restart: always
environment:
- POSTGRES_PASSWORD=password1234
- POSTGRES_USER=postgres
- POSTGRES_DB=factory_iot
ports:
- "5432:5432"
volumes:
- ./timescaledb:/var/lib/postgresql/data
# ヘルスチェックを追加(他のコンテナが待てるようにするため)
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# --- 3. Data Collector (Telegraf) ---
telegraf:
image: telegraf:latest
container_name: telegraf
restart: always
depends_on:
timescaledb:
condition: service_healthy
mosquitto:
condition: service_started
volumes:
- ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
environment:
- POSTGRES_PASSWORD=password1234
# --- 4. Visualization A (Grafana) ---
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: always
ports:
- "3000:3000"
volumes:
- ./grafana:/var/lib/grafana
# --- 5. Visualization B (Metabase) ---
metabase:
image: metabase/metabase:latest
container_name: metabase
restart: always
ports:
- "3001:3000"
volumes:
- ./metabase:/metabase-data
environment:
- MB_DB_FILE=/metabase-data/metabase.db
# --- 6. AI Engine (Ollama) [NEW] ---
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: always
ports:
- "11434:11434" # ホストからもAPIを叩けるように公開
volumes:
- ./ollama_data:/root/.ollama # モデルデータを永続化
# --- 7. AI Dashboard (Streamlit + Pandas) [NEW] ---
ai_dashboard:
build: ./ai_app # ai_appフォルダのDockerfileを使ってビルド
container_name: ai_dashboard
restart: always
ports:
- "8501:8501" # ブラウザでアクセスするポート
volumes:
- ./ai_app:/app # コード変更を即反映させる
environment:
# コンテナ内からOllamaへの接続先を指定
- OLLAMA_HOST=http://ollama:11434
depends_on:
timescaledb:
condition: service_healthy
ollama:
condition: service_started
ビルドして起動
docker compose up -d --build
実行結果
[+] up 6/6
✔ Image ollama/ollama:latest Pulled 59.5s
[+] Building 81.6s (13/13) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading from stdin 546B 0.0s
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 450B 0.0s
=> [internal] load metadata for docker.io/library/python:3.11-slim 3.6s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 4.51kB 0.0s
=> [1/6] FROM docker.io/library/python:3.11-slim@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf 2.9s
=> => resolve docker.io/library/python:3.11-slim@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf 0.1s
=> => sha256:93304c4769c26f8efc1479b815e3d84a136217013671ad87e5fa98da27cea089 251B / 251B 0.4s
=> => sha256:e9e50e44f49d8e67d2fb6488786b2879eeb473e5ea7f238fc2655b7154abde80 14.31MB / 14.31MB 1.2s
=> => sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 30.14MB / 30.14MB 1.5s
=> => sha256:05c5a241432232a0a200b8395f09c5da514ed2d1d0fff93fb57bbd229f308114 1.27MB / 1.27MB 1.2s
=> => extracting sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 0.7s
=> => extracting sha256:05c5a241432232a0a200b8395f09c5da514ed2d1d0fff93fb57bbd229f308114 0.1s
=> => extracting sha256:e9e50e44f49d8e67d2fb6488786b2879eeb473e5ea7f238fc2655b7154abde80 0.4s
=> => extracting sha256:93304c4769c26f8efc1479b815e3d84a136217013671ad87e5fa98da27cea089 0.0s
=> [2/6] WORKDIR /app 3.7s
=> [3/6] RUN apt-get update && apt-get install -y libpq-dev gcc curl && rm -rf /var/lib/apt/lists/* 18.5s
=> [4/6] COPY requirements.txt . 0.1s
=> [5/6] RUN pip install --no-cache-dir -r requirements.txt 28.4s
=> [6/6] COPY . . 0.1s
=> exporting to image 23.9s
=> => exporting layers 19.6s
=> => exporting manifest sha256:75c0200afa75a5c42882110398097fdac36539b9264cbca3b456fbbcc7be7a5d 0.0s
=> => exporting config sha256:ad97c4d7c9910c874f505c02414714d2716874f2227d6e3cf11647956dd7c64b 0.0s
=> => exporting attestation manifest sha256:2ce78ef2b61fdfda03d46ca5bd36a08da031a5cbfe5d1c883a8b8a3fe861e6d4 0.0s
=> => exporting manifest list sha256:50732867d6d5df12c9f0c4ce6941ab3418844d8bfbfe484364663951a832fdcd 0.0s
=> => naming to docker.io/library/iot-factory-ai_dashboard:latest 0.0s
[+] up 14/14king to docker.io/library/iot-factory-ai_dashboard:latest 4.1s
✔ Image ollama/ollama:latest Pulled 59.5s
✔ Image iot-factory-ai_dashboard Built 81.6s
✔ Container grafana Running 0.0s
✔ Container metabase Running 0.0s
✔ Container ollama Created 2.2s
✔ Container timescaledb Healthy 13.4s
✔ Container mosquitto Running 0.0s
✔ Container telegraf Running 0.0s
✔ Container ai_dashboard Created
状態確認
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
ai_dashboard iot-factory-ai_dashboard "streamlit run app.p…" ai_dashboard About a minute ago Up About a minute 0.0.0.0:8501->8501/tcp, [::]:8501->8501/tcp
grafana grafana/grafana:latest "/run.sh" grafana 2 days ago Up 44 hours 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
metabase metabase/metabase:latest "/app/run_metabase.sh" metabase 2 days ago Up 44 hours 0.0.0.0:3001->3000/tcp, [::]:3001->3000/tcp
mosquitto eclipse-mosquitto:latest "/docker-entrypoint.…" mosquitto 2 days ago Up 44 hours 0.0.0.0:1883->1883/tcp, [::]:1883->1883/tcp
ollama ollama/ollama:latest "/bin/ollama serve" ollama About a minute ago Up About a minute 0.0.0.0:11434->11434/tcp, [::]:11434->11434/tcp
telegraf telegraf:latest "/entrypoint.sh tele…" telegraf 2 days ago Up 44 hours 8092/udp, 8125/udp, 8094/tcp
timescaledb timescale/timescaledb:latest-pg16 "docker-entrypoint.s…" timescaledb About a minute ago Up About a minute (healthy) 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp
モデルのダウンロード
Ollamaで使用するモデルをダウンロードします。
このダウンロードは初回一回のみとなります。
Ollamaコンテナに入ってコマンドを実行します。
コード生成用モデル
docker exec -it ollama ollama pull qwen2.5-coder:3b
実行結果
pulling manifest
pulling 4a188102020e: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.9 GB
pulling 66b9ea09bd5b: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 68 B
pulling 1e65450c3067: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.6 KB
pulling 45fc3ea7579a: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 7.4 KB
pulling bb967eff3bda: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 487 B
verifying sha256 digest
writing manifest
success
日本語レポート用モデル
docker exec -it ollama ollama pull llama3.2:3b
実行結果
pulling manifest
pulling dde5aa3fc5ff: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 2.0 GB
pulling 966de95ca8a6: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.4 KB
pulling fcc5a6bec9da: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 7.7 KB
pulling a70ff7e570d9: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 6.0 KB
pulling 56bb8bd477a5: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 96 B
pulling 34bb5ab01051: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 561 B
verifying sha256 digest
writing manifest
success
実行
ブラウザで http://<ラズパイのIP>:8501 にアクセスすれば、TimescaleDBのデータを可視化できるAIダッシュボードが表示されます。
「今朝の温度をグラフで表示してください。」と聞いたら以下のような回答がきました。
時間が9時間ずれていますね。
「今朝」と指定しましたが、21時くらいの温度を表示しています。
あと、生成したSQLがエラーになることが多いのでプロンプトを工夫する必要があるかもしれません。
時刻修正
時刻を修正するために以下のようにapp.yを修正しました。
if 'time' in df.columns:
df['time'] = pd.to_datetime(df['time'])
# ここから
if df['time'].dt.tz is None:
df['time'] = df['time'].dt.tz_localize('UTC')
df['time'] = df['time'].dt.tz_convert('Asia/Tokyo').dt.tz_localize(None)
# ここまでを追加
df = df.set_index('time')
st.line_chart(df)
今回の修正でディレクトリ構成は以下のようになっています。
iot-factory/
├── docker-compose.yml # (修正)
├── mosquitto/ # (既存)
├── grafana/ # (既存)
└── ai_app/ # (新規作成)
├── Dockerfile #
├── requirements.txt #
└── app.py #
次回は、Raspbery Pi AI Cameraでも繋げてみますか。

