"""
プログラムコードをAIでドキュメント化するツール。
指定されたワイルドカードパターンに一致するソースコードファイルを読み込み、
設定ファイルに基づいたプロンプトを使用してAI(OpenAIまたはGoogle Gemini)に
ドキュメントを生成させ、指定された出力ファイルに書き出します。
:doc:`explain_program5_usage`
"""
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import argparse
import glob
import time
import re
from pathlib import Path
# 既存のライブラリ(環境に合わせてパスを通してください)
try:
from tkai_lib import read_ai_config
from tkai_lib import query_openai4, query_openai5, query_google
from tkai_lib import extract_openai5_text
except ImportError:
print("Error: tkai_lib が見つかりません。パスを確認してください。", file=sys.stderr)
sys.exit(1)
#=========================================================
# INI Reading Logic (make_textbook5.py より継承)
#=========================================================
[ドキュメント]
def search_file(infile=None):
"""
指定されたファイルパスまたはデフォルトのINIファイルを探索します。
infileが指定されない場合、スクリプトの実行ディレクトリとカレントディレクトリから
デフォルトのINIファイル(スクリプト名.ini)を探します。
infileが指定された場合、そのパスが存在するか、またはスクリプトの実行ディレクトリからの
相対パスで存在するかを確認します。
:param infile: str, 探索するファイルパス。デフォルトはNoneで、その場合はデフォルトINIファイル名を使用。
:returns: str または None, 見つかったファイルの絶対パス。見つからない場合はNone。
"""
script_path = os.path.abspath(sys.argv[0])
script_dir = os.path.dirname(script_path)
script_name = os.path.splitext(os.path.basename(script_path))[0]
default_ini = f"{script_name}.ini"
if infile is None:
for path in [os.getcwd(), script_dir]:
candidate = os.path.join(path, default_ini)
if os.path.isfile(candidate): return candidate
return None
if not os.path.isfile(infile):
candidate = os.path.join(script_dir, infile)
if os.path.isfile(candidate): return candidate
return None
return infile
[ドキュメント]
def read_ini(inifile=None):
"""
指定されたINIファイルから設定を読み込み、変数を展開します。
key=value形式の行を解析し、3重引用符で囲まれた複数行の値をサポートします。
また、`$VARIABLE_NAME` 形式の変数を、既に読み込まれた設定値で展開します。
コメント行(`#` または `;` で始まる)と空行は無視されます。
:param inifile: str または None, 読み込むINIファイルのパス。Noneの場合はsearch_fileで探索される。
:returns: dict, 読み込まれた設定をキーと値のペアで格納した辞書。
:raises FileNotFoundError: INIファイルが見つからない場合。
"""
path = search_file(inifile)
if path is None:
raise FileNotFoundError("INIファイルが見つかりませんでした")
result = {}
variables = {}
current_key = None
multiline_val = []
multiline_delim = None
with open(path, 'r', encoding='utf-8') as f:
for line in f:
line = line.rstrip()
if not line or line.startswith('#') or line.startswith(';'):
continue
# 複数行値の終了判定(stripで判定)
if multiline_delim:
if line.strip() == multiline_delim:
val = '\n'.join(multiline_val)
result[current_key] = val
variables[current_key] = val
current_key = None
multiline_val = []
multiline_delim = None
else:
multiline_val.append(line)
continue
# key=val の解析
if '=' in line:
key, val = map(str.strip, line.split('=', 1))
val = val.strip()
# 複数行値の開始判定(空文字でも対応)
if (val == '"""' or val == "'''" or
(val.startswith('"""') and not val.endswith('"""')) or \
(val.startswith("'''") and not val.endswith("'''")) ):
multiline_delim = val[:3]
content = val[3:]
multiline_val = [content] if content else []
current_key = key
continue
# 単一行の複数行値
if (val.startswith('"""') and val.endswith('"""')) or \
(val.startswith("'''") and val.endswith("'''")):
val = val[3:-3]
result[key] = val
variables[key] = val
# 変数展開(あとから一括処理)
for key, val in result.items():
def expand_var(match):
var_name = match.group(1)
return variables.get(var_name, match.group(0))
result[key] = re.sub(r"\$(\w+)\b", expand_var, val)
return result
#=========================================================
# Language Dictionary
#=========================================================
language_dict = {
".py": "python", ".pl": "perl", ".pm": "perl", ".c" : "C",
".cpp": "C++", ".pas": "pascal", ".f" : "fortran", ".js" : "Javascript",
".java": "Java", ".go": "Go", ".sh": "bash script", ".html": "HTML"
}
[ドキュメント]
def get_program_type(path):
"""
プログラムファイルのパスからプログラムのタイプ(`main` または `lib`)を判定します。
ファイル拡張子が`.pm`の場合、またはファイル名が`tk`で始まる場合、ライブラリ(`lib`)と判定します。
それ以外の場合はメインプログラム(`main`)と判定します。
:param path: str, プログラムファイルのパス。
:returns: str, プログラムのタイプ (`'main'` または `'lib'`)。
"""
base = os.path.basename(path)
name, ext = os.path.splitext(base)
if ext == ".pm" or base.startswith("tk"): return 'lib'
return 'main'
[ドキュメント]
def initialize():
"""
コマンドライン引数パーサーを初期化し、設定します。
`argparse.ArgumentParser` を設定し、必要な引数とオプションを定義します。
`pattern` (必須, ワイルドカード), `output` (任意), `--inifile`, `--api`,
`-t` (`--program_type`), `-u` (`--update`), `-w` (`--overwrite`) オプションが含まれます。
デフォルトのINIファイルパスはスクリプト名から自動生成されます。
:returns: argparse.ArgumentParser, 初期化されたコマンドライン引数パーサーオブジェクト。
"""
ini_path = os.path.splitext(sys.argv[0])[0] + ".ini"
parser = argparse.ArgumentParser(description="プログラムコードをAIでドキュメント化するツール")
parser.add_argument("pattern", help="対象ファイルのワイルドカード(例: '*.py')")
parser.add_argument("output", nargs="?", help="出力名(単一ファイル時のみ)")
parser.add_argument("--inifile", default=ini_path, help="プロンプト設定ファイル")
parser.add_argument("--api", choices=["openai", "openai5", "google", "gemini"], default='google')
parser.add_argument("-t", "--program_type", choices=["", "main", "lib"], default="")
parser.add_argument("-u", "--update", type=int, default=0)
parser.add_argument("-w", "--overwrite", type=int, default=0)
return parser
[ドキュメント]
def main():
"""
プログラムの主要な処理を実行します。
`initialize` 関数で初期化された引数を解析し、AI設定ファイル (`ai.env`) と
プロンプトINIファイルを読み込みます。
指定されたパターンに一致するファイルを検索し、一つずつ処理します。
出力ファイルが既に存在し、更新フラグや上書きフラグが設定されていない場合はスキップします。
ソースコードを読み込み、言語タイプとプログラムタイプを判定し、
INIファイルの設定に基づいてプロンプトを構築します。
AIサービス(OpenAIまたはGoogle Gemini)に問い合わせてドキュメントを生成し、
生成されたドキュメントを出力ファイルに書き込みます。
各ファイル処理後に1秒間待機し、最後にユーザーからの終了入力を待ちます。
:returns: None
"""
parser = initialize()
args = parser.parse_args()
# AI設定の読み込み
read_ai_config("ai.env")
# プロンプトINIの読み込み
try:
ini_data = read_ini(args.inifile)
print(f"Loaded INI: {search_file(args.inifile)}")
except Exception as e:
print(f"Error loading INI: {e}")
sys.exit(1)
files = glob.glob(args.pattern)
if not files: sys.exit(1)
outputs = [args.output] if args.output else [os.path.splitext(f)[0] + ".md" for f in files]
for inp, out in zip(files, outputs):
if os.path.exists(out) and not args.overwrite and (not args.update or os.path.getmtime(out) >= os.path.getmtime(inp)):
print(f"Skip: {out}")
continue
print(f"Processing: {inp} -> {out}")
try:
code = Path(inp).read_text(encoding="utf-8")
except:
code = Path(inp).read_text(encoding="shift-jis")
ext = os.path.splitext(inp)[1]
lang = language_dict.get(ext, "text")
p_type = args.program_type or get_program_type(inp)
# プロンプト構築
tpl_key = f"PROMPT_{p_type.upper()}"
template = ini_data.get(tpl_key, ini_data.get("PROMPT_MAIN"))
role = ini_data.get("SYSTEM_ROLE", "Assistant")
prompt = template.replace("{{script_name}}", inp).replace("{{code}}", code).replace("{{language}}", lang)
# AI呼び出し
if args.api == "openai5":
res = query_openai5(prompt, os.getenv("openai_model5"), instructions=role)
doc = extract_openai5_text(res)
elif args.api == "openai":
res = query_openai4(prompt, os.getenv("openai_model"), role=role)
doc = res.choices[0].message.content
else:
res = query_google(prompt, os.getenv("gemini_model"), role=role)
doc = res.text if res else None
if doc:
Path(out).write_text(doc, encoding="utf-8")
print(f"Done: {out}")
time.sleep(1)
input("\nPress ENTER to terminate>>\n")
if __name__ == "__main__":
main()