プロンプトの管理ツール
記述の多いプロンプトを使いやすくするためのツール。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:要約のドラフト}
制約事項
- ウィンドウサイズを変更しても、テキストボックスの縦幅が変わらない。
- 変数として入力された内容がエスケープされる。一応戻しているが、失敗しているケースがある。