スポンサーリンク
Pythonstreamlit

Streamlitで問い合わせ管理ツールを作ってみた!SQLite+検索+Markdown出力対応

Python
この記事は約20分で読めます。

業務で問い合わせ対応や障害調査をしていると、「以前どう対応したか分からない」「過去に作成した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(
                    "データがありません"
                )
            
        

コメント

タイトルとURLをコピーしました