Cucco’s Compute Hack

コンピュータ関係の記事を書いていきます。

プロンプトの管理ツール

記述の多いプロンプトを使いやすくするためのツール。AIに聞きながら作成しました。

マークダウンでプロンプトのテンプレートを用意しておき、具体的な部分は変数に埋め込みプロンプトを組み立てます。


生成されたプロンプト

元のテンプレートにあった変数が、GUIの記載内容で上書きされている。

# STEP1

[原文]の内容を要約して。

### 原文

はてなブログは、あなたの思いや考えを残したり、
さまざまな人が綴った多様な価値観に触れたりできる場所です。

# STEP2

[原文]の内容を要約したドラフトが、[レビュー対象]です。

[レビュー対象]に、[原文]の内容から抜け落ちた部分がないか確認して。

### 対象

はてなブログは、あなたの思いや考えを残したり、
さまざまな人が綴った多様な価値観に触れたりできる場所です。

### レビュー対象

未入力

プログラム
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import os
import re
import time
from dotenv import load_dotenv

load_dotenv()  # Load environment variables from .env file

class MarkdownEditorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("プロンプト管理ツール")

        # 変数の初期化
        self.selected_file_path = ""
        self.variables = []
        self.entries = {}
        self.current_step = 0
        self.max_step = 0

        # Notebookウィジェットを作成
        self.notebook = ttk.Notebook(root)
        self.notebook.pack(expand=True, fill='both')

        # タブ1 - 設定
        self.tab1 = ttk.Frame(self.notebook)
        self.notebook.add(self.tab1, text='テンプレート選択')

        # 左の列 - ディレクトリ構造のツリービュー
        self.tree = ttk.Treeview(self.tab1)
        self.tree.grid(row=0, column=0, sticky='nsew')

        # 中央の列 - mdファイルの一覧表示
        self.md_listbox = tk.Listbox(self.tab1)
        self.md_listbox.grid(row=0, column=1, sticky='nsew')

        # 右の列 - mdファイルの内容表示
        self.file_content_text = scrolledtext.ScrolledText(self.tab1, wrap=tk.WORD)
        self.file_content_text.grid(row=0, column=2, sticky='nsew')

        # グリッド構造の設定
        self.tab1.grid_columnconfigure(0, weight=1, uniform="column")
        self.tab1.grid_columnconfigure(1, weight=1, uniform="column")
        self.tab1.grid_columnconfigure(2, weight=2, uniform="column")
        self.tab1.grid_rowconfigure(0, weight=1)

        # ディレクトリ構造の表示
        self.current_directory = os.getenv("MYDIRECTORY")  # ここに表示したいディレクトリを指定
        self.selected_path = self.current_directory
        self.root_node = self.tree.insert('', 'end', text=self.current_directory)
        self.tree.item(self.root_node, open=True)
        self.populate_treeview(self.root_node, self.current_directory)

        # ツリービューのイベント
        self.tree.bind("<ButtonRelease-1>", self.on_tree_click)

        # mdファイル一覧の選択イベント
        self.md_listbox.bind('<<ListboxSelect>>', self.on_md_select)

        # タブ2 - 生成
        self.tab2 = ttk.Frame(self.notebook)
        self.notebook.add(self.tab2, text='変数入力とプロンプト生成')

        # 左の列 - 説明のラベルとテキストボックス
        self.variables_frame = ttk.Frame(self.tab2, relief="solid", borderwidth=1)
        self.variables_frame.grid(row=0, column=0, padx=5, pady=5, sticky='nsew')

        # 右の列 - ボタンとプロンプト
        self.buttons_frame = ttk.Frame(self.tab2, relief="solid", borderwidth=1)
        self.buttons_frame.grid(row=0, column=1, padx=5, pady=5, sticky='nsew')

        # 各ボタンを同じ行に配置
        self.update_button = ttk.Button(self.buttons_frame, text="生成/更新", command=self.generate_prompt)
        self.update_button.grid(row=0, column=0, padx=5, pady=5, sticky='ew')

        self.prev_button = ttk.Button(self.buttons_frame, text="前のステップ", command=self.prev_step)
        self.prev_button.grid(row=0, column=1, padx=5, pady=5, sticky='ew')

        self.next_button = ttk.Button(self.buttons_frame, text="次のステップ", command=self.next_step)
        self.next_button.grid(row=0, column=2, padx=5, pady=5, sticky='ew')

        self.copy_button = ttk.Button(self.buttons_frame, text="コピー", command=self.copy_to_clipboard)
        self.copy_button.grid(row=0, column=3, padx=5, pady=5, sticky='ew')

        # self.current_stepの値を表示するラベルをボタンの右側に配置
        self.step_label = ttk.Label(self.buttons_frame, text=f"現在のステップ: {self.current_step}")
        self.step_label.grid(row=0, column=4, padx=5, pady=5, sticky='ew')

        # テキストボックスをbuttons_frame内に配置し、残りのすべての行を使用
        self.prompt_text_box = scrolledtext.ScrolledText(self.buttons_frame, height=10, wrap=tk.WORD)
        self.prompt_text_box.grid(row=1, column=0, columnspan=5, padx=5, pady=5, sticky='nsew', rowspan=100)

        # グリッド構造の設定
        self.tab2.grid_columnconfigure(0, weight=1,uniform="column")  # self.variables_frame の列
        self.tab2.grid_columnconfigure(1, weight=1,uniform="column")  # self.buttons_frame の列

        # ボタンの行の高さを固定し、幅は拡大
        self.buttons_frame.grid_rowconfigure(0, weight=0)  # ボタンとラベルの行

        # テキストボックスの行のリサイズ設定
        self.buttons_frame.grid_rowconfigure(1, weight=1)  # 2行目以降の行にテキストボックスを表示

    def populate_treeview(self, parent, path):
        for item in os.listdir(path):
            full_path = os.path.join(path, item)
            if os.path.isdir(full_path):
                node = self.tree.insert(parent, 'end', text=item)
                self.populate_treeview(node, full_path)

    def on_tree_click(self, event):
        selected_item = self.tree.selection()[0]
        self.selected_path = os.path.join(self.current_directory, self.tree.item(selected_item, "text"))
        self.list_md_files(self.selected_path)

    def list_md_files(self, directory):
        self.md_listbox.delete(0, tk.END)
        for item in os.listdir(directory):
            if item.endswith('.md'):
                self.md_listbox.insert(tk.END, item)

    def on_md_select(self, event):
        selected_file = self.md_listbox.get(self.md_listbox.curselection())
        self.selected_file_path = os.path.join(self.selected_path, selected_file)
        self.load_md_content()

    def load_md_content(self):
        with open(self.selected_file_path, 'r', encoding="utf-8") as file:
            content = file.read()
        
        self.file_content_text.delete(1.0, tk.END)
        self.file_content_text.insert(tk.END, content)
        
        # タブ2のラベルとテキストボックスを更新
        self.update_variables(content)
        self.detect_steps(content)

    def update_variables(self, content):
        # 前の入力フィールドを削除
        for widget in self.variables_frame.winfo_children():
            widget.destroy()

        # 変数の検出
        self.variables = re.findall(r'\{([^:}]+):([^}]+)\}', content)

        seen_variables = set()
        self.entries = {}

        for var_name, var_desc in self.variables:
            if var_name not in seen_variables:
                seen_variables.add(var_name)
                label = ttk.Label(self.variables_frame, text=var_desc)
                label.grid(sticky="w", padx=5, pady=2)
                entry = tk.Text(self.variables_frame, height=3)
                entry.grid(sticky="ew", padx=5, pady=2)
                self.entries[var_name] = entry

        self.variables_frame.grid_rowconfigure(len(self.entries), weight=1)


    def detect_steps(self, content):
        # ステップの検出
        self.steps = re.findall(r'# *STEP *(\d+)', content)
        self.steps = sorted(set(int(step) for step in self.steps))
        self.max_step = len(self.steps)

    def generate_prompt(self):
        if not self.selected_file_path:
            messagebox.showwarning("警告", "mdファイルが選択されていません。")
            return

        # テキストボックスに入力された内容を取得
        context = {var_name: entry.get("1.0", tk.END).strip() for var_name, entry in self.entries.items()}

        with open(self.selected_file_path, 'r', encoding='utf-8') as file:
            content = file.read()

        # 変数をテキストボックスの内容に置き換える
        result = content
        for var_name, var_value in context.items():
            result = re.sub(r'\{' + var_name + r':[^\}]+\}', re.escape(var_value), result)

        # バックスラッシュでエスケープされた正規表現の文字を元に戻す
        result=self.unescape_re_escaped_string(result)

        # 結果をプロンプトテキストボックスに表示
        if self.current_step == 0:
            self.prompt_text_box.delete(1.0, tk.END)
            self.prompt_text_box.insert(tk.END, result)
        else:
            self.show_step_content(result)

    def show_step_content(self, content):
        # ステップに応じた内容を表示
        if self.current_step == 1:
            step_pattern = r'# *STEP *1'
        else:
            step_pattern = rf'# *STEP *{self.current_step}'

        next_step_pattern = rf'# *STEP *{self.current_step + 1}'

        start = re.search(step_pattern, content)
        if start:
            start_index = start.end()
        else:
            start_index = 0

        end = re.search(next_step_pattern, content)
        if end:
            end_index = end.start()
        else:
            end_index = len(content)

        step_content = content[start_index:end_index].strip()

        self.prompt_text_box.delete(1.0, tk.END)
        self.prompt_text_box.insert(tk.END, step_content)

    def next_step(self):
        if self.current_step < self.max_step:
            self.current_step += 1
            self.generate_prompt()
            self.update_step_label()

    def prev_step(self):
        if self.current_step > 0:
            self.current_step -= 1
            self.generate_prompt()
            self.update_step_label()

    def update_step_label(self):
        self.step_label.config(text=f"現在のステップ: {self.current_step}")

    def copy_to_clipboard(self):
        content = self.prompt_text_box.get("1.0", tk.END).strip()
        self.root.clipboard_clear()
        self.root.clipboard_append(content)
        messagebox.showinfo("コピー", "プロンプトがクリップボードにコピーされました。")

    def unescape_re_escaped_string(self, escaped_string):
        # バックスラッシュでエスケープされた正規表現の文字を元に戻す
        unescaped_string = re.sub(r'\\(.)', r'\1', escaped_string)

        # エスケープされた改行を元に戻す
        unescaped_string = unescaped_string.replace('\\\n', '\n')
        # 残ったバックスラッシュとスペースをスペースに置換
        unescaped_string = unescaped_string.replace('\\ ', ' ')
        
        return unescaped_string
                           
if __name__ == "__main__":
    root = tk.Tk()
    app = MarkdownEditorApp(root)
    root.mainloop()
プロンプトのテンプレート

マークダウンとして記述。
プロンプトに複数回質問することを想定して、「#STEP(\d)」で区切り。
変数として、{val1:要約対象}や{val2:要約のドラフト}などを設定可能。val1やval2が変数名、コロンの後の「要約対象」はユーザ向けの説明。

# STEP1

[原文]の内容を要約して。

### 原文

{val1:要約対象}

# STEP2

[原文]の内容を要約したドラフトが、[レビュー対象]です。

[レビュー対象]に、[原文]の内容から抜け落ちた部分がないか確認して。

### 対象

{val1:要約対象}

### レビュー対象

{val2:要約のドラフト}

制約事項
  1. ウィンドウサイズを変更しても、テキストボックスの縦幅が変わらない。
  2. 変数として入力された内容がエスケープされる。一応戻しているが、失敗しているケースがある。