業務で問い合わせ対応や障害調査をしていると、「以前どう対応したか分からない」「過去に作成したSQLやスクリプトが見つからない」ということがよくあります。
そこで今回は、PythonのStreamlitを使って問い合わせ管理ツールを作成してみました。単なるメモ帳ではなく、検索機能やMarkdown出力機能も備えた簡易ナレッジベースとして利用できます。
作った機能
管理できる情報は以下の通りです。
- システム
- カテゴリー
- 件名
- 内容
- 詳細
- ソース種別
- ソースコード
- 関連資料URL
- 登録日時
- 更新日時
カテゴリーは「障害対応」「定型作業」「設定変更」「問い合わせ」「調査」の5種類にしました。
また、ソースコードも一緒に保存できるため、SQLやPython、PowerShellなどを問い合わせ履歴とセットで管理できます。
SQLiteを採用した理由
最初はMarkdownファイルへ直接保存しようと考えていました。しかし、
- 編集したい
- 削除したい
- 検索したい
という要件が出てきたためSQLiteを採用しました。
構成は非常にシンプルです。
Streamlit
↓
SQLite
↓
問い合わせ管理
↓
Markdown出力サーバー不要で運用できるため、個人利用や小規模チームにも向いています。
検索機能を実装
問い合わせが増えると一覧だけでは探しにくくなります。
そこで以下の検索機能を追加しました。
- キーワード検索
- システム検索
- カテゴリー検索
検索対象は、
- 件名
- 内容
- 詳細
- ソースコード
です。
例えば「SQL」と入力すると、SQLを含む問い合わせだけをすぐに探せます。
コードも管理できる
今回特に便利だったのがソースコード管理機能です。
例えば以下のように調査などで利用したソース(ここではSQL)も登録することが可能です。
SELECT *
FROM employee
WHERE status = 'ERROR';問い合わせ内容と一緒に保存できるため、後で再利用しやすくなりました。
Markdown出力
登録した情報はシステムごとにMarkdownファイルとして出力できます。
出力例は以下のようになります。
## Streamlitエラー対応
- カテゴリー: 調査
### 内容
画面表示エラー
### 詳細
キー重複が原因
### ソースコード
```python
st.text_area(
"内容",
key="new_content"
)
```Markdown形式なので、そのままGitHubやNotebookLMへ取り込めます。
まとめ
今回作成した問い合わせ管理ツールでは、
- 問い合わせ登録
- 編集・削除
- キーワード検索
- ソースコード管理
- Markdown出力
を実装しました。
StreamlitとSQLiteだけでも、実務で十分使える問い合わせ管理ツールを作ることができます。特にMarkdown出力機能を組み合わせることで、問い合わせ履歴をそのままナレッジベースとして活用できるようになりました。今後はタグ機能や日付検索なども追加して、さらに使いやすくしていきたいと思います。
一応現時点で作成したソースは以下の通りです。
import sqlite3
from datetime import datetime
import pandas as pd
import streamlit as st
# =====================================================
# 設定
# =====================================================
st.set_page_config(
page_title="問い合わせ管理",
page_icon="📝",
layout="wide"
)
DB_FILE = "inquiry.db"
# =====================================================
# DB
# =====================================================
def init_db():
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS inquiries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
system TEXT,
category TEXT,
title TEXT,
content TEXT,
detail TEXT,
source_type TEXT,
source_code TEXT,
url TEXT,
created_at TEXT,
updated_at TEXT
)
""")
conn.commit()
conn.close()
init_db()
# =====================================================
# マスタ
# =====================================================
systems = {
"樹脂AMO": [
"障害対応",
"定型作業",
"設定変更",
"問い合わせ",
"調査"
],
"東洋紡": [
"障害対応",
"定型作業",
"設定変更",
"問い合わせ",
"調査"
],
"プライシング": [
"障害対応",
"定型作業",
"設定変更",
"問い合わせ",
"調査"
]
}
def export_markdown(system_name):
conn = sqlite3.connect(DB_FILE)
df = pd.read_sql(
"""
SELECT *
FROM inquiries
WHERE system = ?
ORDER BY updated_at DESC
""",
conn,
params=(system_name,)
)
conn.close()
if df.empty:
return None
file_name = f"{system_name}.md"
with open(file_name, "w", encoding="utf-8") as f:
f.write(f"# {system_name} 問い合わせ履歴\n\n")
for _, row in df.iterrows():
f.write(f"## {row['title']}\n\n")
f.write(f"- カテゴリー: {row['category']}\n")
f.write(f"- 登録日時: {row['created_at']}\n")
f.write(f"- 更新日時: {row['updated_at']}\n\n")
f.write("### 内容\n\n")
f.write(f"{row['content']}\n\n")
f.write("### 詳細\n\n")
f.write(f"{row['detail']}\n\n")
if row["source_code"]:
f.write("### ソースコード\n\n")
f.write(f"```{row['source_type']}\n")
f.write(row["source_code"])
f.write("\n```\n\n")
if row["url"]:
f.write("### 関連資料\n\n")
f.write(f"{row['url']}\n\n")
f.write("---\n\n")
return file_name
source_types = [
"python",
"sql",
"powershell",
"bash",
"text"
]
# =====================================================
# タイトル
# =====================================================
st.title("📝 問い合わせ管理")
tab1, tab2, tab3 = st.tabs([
"登録",
"一覧・編集",
"Markdown出力"
])
# =====================================================
# 登録
# =====================================================
with tab1:
st.subheader("問い合わせ登録")
system = st.selectbox(
"システム",
list(systems.keys())
)
category = st.selectbox(
"カテゴリー",
systems[system]
)
title = st.text_input("件名")
content = st.text_area(
"内容",
height=120
)
detail = st.text_area(
"詳細",
height=200
)
source_type = st.selectbox(
"ソース種別",
source_types
)
source_code = st.text_area(
"ソースコード",
height=300
)
url = st.text_input(
"関連資料URL"
)
if st.button("登録", type="primary"):
now = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("""
INSERT INTO inquiries (
system,
category,
title,
content,
detail,
source_type,
source_code,
url,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
system,
category,
title,
content,
detail,
source_type,
source_code,
url,
now,
now
))
conn.commit()
conn.close()
st.success("登録しました")
st.session_state["new_title"] = ""
st.session_state["new_content"] = ""
st.session_state["new_detail"] = ""
st.session_state["new_source_code"] = ""
st.session_state["new_url"] = ""
st.rerun()
# =====================================================
# 一覧・編集
# =====================================================
with tab2:
st.subheader("検索")
search_text = st.text_input(
"キーワード",
key="search_text"
)
search_system = st.selectbox(
"システム",
["全件"] + list(systems.keys()),
key="search_system"
)
search_category = st.selectbox(
"カテゴリー",
[
"全件",
"障害対応",
"定型作業",
"設定変更",
"問い合わせ",
"調査"
],
key="search_category"
)
conn = sqlite3.connect(DB_FILE)
query = """
SELECT
id,
system,
category,
title,
updated_at
FROM inquiries
WHERE 1 = 1
"""
params = []
if search_system != "全件":
query += " AND system = ?"
params.append(search_system)
if search_category != "全件":
query += " AND category = ?"
params.append(search_category)
if search_text:
query += """
AND (
title LIKE ?
OR content LIKE ?
OR detail LIKE ?
OR source_code LIKE ?
)
"""
keyword = f"%{search_text}%"
params.extend([
keyword,
keyword,
keyword,
keyword
])
query += """
ORDER BY updated_at DESC
"""
df = pd.read_sql(
query,
conn,
params=params
)
conn.close()
st.dataframe(
df,
use_container_width=True,
hide_index=True
)
if not df.empty:
selected_id = st.selectbox(
"編集対象",
df["id"].tolist()
)
conn = sqlite3.connect(DB_FILE)
row = pd.read_sql(
"""
SELECT *
FROM inquiries
WHERE id = ?
""",
conn,
params=(selected_id,)
).iloc[0]
conn.close()
st.divider()
edit_system = st.selectbox(
"システム",
list(systems.keys()),
index=list(systems.keys()).index(
row["system"]
),
key="edit_system"
)
edit_category = st.selectbox(
"カテゴリー",
systems[edit_system],
index=systems[edit_system].index(
row["category"]
),
key="edit_category"
)
edit_title = st.text_input(
"件名",
value=row["title"],
key="edit_title"
)
edit_content = st.text_area(
"内容",
value=row["content"],
height=120,
key="edit_content"
)
edit_detail = st.text_area(
"詳細",
value=row["detail"],
height=200,
key="edit_detail"
)
edit_source_type = st.selectbox(
"ソース種別",
source_types,
index=source_types.index(
row["source_type"]
if row["source_type"] in source_types
else "text"
),
key="edit_source_type"
)
edit_source_code = st.text_area(
"ソースコード",
value=row["source_code"]
if row["source_code"]
else "",
height=300,
key="edit_source_code"
)
edit_url = st.text_input(
"関連資料URL",
value=row["url"]
if row["url"]
else "",
key="edit_url"
)
col1, col2 = st.columns(2)
with col1:
if st.button(
"更新",
key="update_button"
):
now = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("""
UPDATE inquiries
SET
system=?,
category=?,
title=?,
content=?,
detail=?,
source_type=?,
source_code=?,
url=?,
updated_at=?
WHERE id=?
""", (
edit_system,
edit_category,
edit_title,
edit_content,
edit_detail,
edit_source_type,
edit_source_code,
edit_url,
now,
selected_id
))
conn.commit()
conn.close()
st.success("更新しました")
st.rerun()
with col2:
if st.button(
"削除",
key="delete_button"
):
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute(
"DELETE FROM inquiries WHERE id=?",
(selected_id,)
)
conn.commit()
conn.close()
st.success("削除しました")
st.rerun()
else:
st.info("データがありません")
# ←ここに追加
with tab3:
st.subheader("Markdown出力")
export_system = st.selectbox(
"出力対象システム",
list(systems.keys()),
key="export_system"
)
if st.button(
"Markdown生成",
key="export_button"
):
file_name = export_markdown(
export_system
)
if file_name:
with open(
file_name,
"r",
encoding="utf-8"
) as f:
md_content = f.read()
st.download_button(
label="Markdownダウンロード",
data=md_content,
file_name=file_name,
mime="text/markdown"
)
st.success(
f"{file_name} を生成しました"
)
else:
st.warning(
"データがありません"
)

コメント