DeL-TaiseiOzaki commited on
Commit
230b1a5
·
1 Parent(s): bc9366d

first commit

Browse files
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .env
5
+ .venv
6
+ env/
7
+ venv/
8
+ ENV/
9
+ .streamlit/secrets.toml
10
+ output/
11
+ .idea/
12
+ .vscode/
13
+ *.log
README.md CHANGED
@@ -11,4 +11,99 @@ license: apache-2.0
11
  short_description: プログラミング関連ファイルを再帰的にスキャンし、内容を単一のテキストファイルにエクスポートするツールです。GitHubリ
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  short_description: プログラミング関連ファイルを再帰的にスキャンし、内容を単一のテキストファイルにエクスポートするツールです。GitHubリ
12
  ---
13
 
14
+ # get_repository_info_by_llm
15
+
16
+ プログラミング関連ファイルを再帰的にスキャンし、内容を単一のテキストファイルにエクスポートするツールです。GitHubリポジトリまたはローカルディレクトリに対応しています。
17
+
18
+ ## 機能
19
+
20
+ - GitHubリポジトリのクローンとスキャン
21
+ - ローカルディレクトリのスキャン
22
+ - 再帰的なファイル検索
23
+ - 主要なプログラミング言語ファイルの検出
24
+ - UTF-8/CP932エンコーディングの自動検出
25
+ - 結果のテキストファイル出力
26
+
27
+ ## 必要条件
28
+
29
+ - Python 3.7以上
30
+ - Git(GitHubリポジトリをスキャンする場合)
31
+
32
+ ## インストール
33
+
34
+ 1. リポジトリをクローン
35
+ ```bash
36
+ git clone [このリポジトリのURL]
37
+ cd directory-scanner
38
+ ```
39
+
40
+ 2. 必要なディレクトリを作成
41
+ ```bash
42
+ mkdir output
43
+ ```
44
+
45
+ ## 使用方法
46
+
47
+ ### コマンドライン
48
+ ```bash
49
+ # GitHubリポジトリをスキャン
50
+ python main.py https://github.com/username/repository.git
51
+
52
+ # ローカルディレクトリをスキャン
53
+ python main.py /path/to/directory
54
+ ```
55
+
56
+ ### シェルスクリプトを使用
57
+ ```bash
58
+ # スクリプトに実行権限を付与
59
+ chmod +x scan.sh
60
+
61
+ # GitHubリポジトリをスキャン
62
+ ./scan.sh https://github.com/username/repository.git
63
+
64
+ # ローカルディレクトリをスキャン
65
+ ./scan.sh /path/to/directory
66
+ ```
67
+
68
+ ## 出力形式
69
+
70
+ スキャン結果は `output` ディレクトリに保存され、以下の形式で出力されます:
71
+
72
+ ```
73
+ #ファイルパス
74
+ path/to/file.py
75
+ ------------
76
+ ファイルの内容
77
+ ```
78
+
79
+ ## スキャン対象
80
+
81
+ ### 対象となるファイル拡張子
82
+ - Python (.py)
83
+ - JavaScript (.js)
84
+ - Java (.java)
85
+ - C/C++ (.c, .h, .cpp, .hpp)
86
+ - Go (.go)
87
+ - Rust (.rs)
88
+ - PHP (.php)
89
+ - Ruby (.rb)
90
+ - TypeScript (.ts)
91
+ - その他 (.scala, .kt, .cs, .swift, .m, .sh, .pl, .r)
92
+
93
+ ### 除外されるディレクトリ
94
+ - .git
95
+ - __pycache__
96
+ - node_modules
97
+ - venv
98
+ - .env
99
+ - build
100
+ - dist
101
+ - target
102
+ - bin
103
+ - obj
104
+
105
+ ## 注意事項
106
+
107
+ - GitHubリポジトリをスキャンする場合、一時的にローカルにクローンされます
108
+ - スキャン完了後、クローンされたリポジトリは自動的に削除されます
109
+ - 大きなファイルや特殊なエンコーディングのファイルは読み取れない場合があります
app.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import tempfile
3
+ import git
4
+ from core.file_scanner import FileScanner
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+ from services.llm_service import LLMService
8
+ from core.file_scanner import FileInfo
9
+ from typing import List
10
+
11
+ # ページ設定
12
+ st.set_page_config(
13
+ page_title="Repository Code Analysis",
14
+ page_icon="🔍",
15
+ layout="wide"
16
+ )
17
+
18
+ # ダークテーマの設定
19
+ st.markdown("""
20
+ <style>
21
+ .stApp {
22
+ background-color: #0e1117;
23
+ color: #ffffff;
24
+ }
25
+ .chat-message {
26
+ padding: 1rem;
27
+ margin: 1rem 0;
28
+ border-radius: 0.5rem;
29
+ }
30
+ .assistant-message {
31
+ background-color: #1e2329;
32
+ color: #ffffff;
33
+ }
34
+ .stButton button {
35
+ background-color: #2ea44f;
36
+ color: #ffffff;
37
+ }
38
+ .stTextArea textarea {
39
+ background-color: #1e2329;
40
+ color: #ffffff;
41
+ }
42
+ </style>
43
+ """, unsafe_allow_html=True)
44
+
45
+
46
+ def clone_repository(repo_url: str) -> Path:
47
+ """リポジトリをクローンして一時ディレクトリに保存"""
48
+ temp_dir = Path(tempfile.mkdtemp())
49
+ git.Repo.clone_from(repo_url, temp_dir)
50
+ return temp_dir
51
+
52
+ def create_download_content(files: List[FileInfo]) -> str:
53
+ content = "# スキャン結果\n\n"
54
+ for file in files:
55
+ content += f"## {file.path}\n"
56
+ content += f"サイズ: {file.formatted_size}\n"
57
+ content += f"エンコーディング: {file.encoding or '不明'}\n\n"
58
+ if file.content:
59
+ content += f"```{file.extension[1:] if file.extension else ''}\n"
60
+ content += file.content
61
+ content += "\n```\n\n"
62
+ return content
63
+
64
+ # セッション状態の初期化
65
+ if 'repo_content' not in st.session_state:
66
+ st.session_state.repo_content = None
67
+ if 'temp_dir' not in st.session_state:
68
+ st.session_state.temp_dir = None
69
+ if 'llm_service' not in st.session_state:
70
+ try:
71
+ st.session_state.llm_service = LLMService()
72
+ except ValueError as e:
73
+ st.error(str(e))
74
+ st.stop()
75
+
76
+ # メインのUIレイアウト
77
+ st.title("🔍 リポジトリ解析・質問システム")
78
+
79
+ # サイドバーでモデル選択
80
+ available_models = st.session_state.llm_service.settings.get_available_models()
81
+ if len(available_models) > 1:
82
+ selected_model = st.sidebar.selectbox(
83
+ "使用するモデル",
84
+ available_models,
85
+ index=available_models.index(st.session_state.llm_service.current_model)
86
+ )
87
+ st.session_state.llm_service.switch_model(selected_model)
88
+
89
+ # URLの入力
90
+ repo_url = st.text_input(
91
+ "GitHubリポジトリのURLを入力",
92
+ placeholder="https://github.com/username/repository.git"
93
+ )
94
+
95
+ # スキャン実行ボタン
96
+ if st.button("スキャン開始", disabled=not repo_url):
97
+ try:
98
+ with st.spinner('リポジトリをクローン中...'):
99
+ temp_dir = clone_repository(repo_url)
100
+ st.session_state.temp_dir = temp_dir
101
+
102
+ with st.spinner('ファイルをスキャン中...'):
103
+ scanner = FileScanner(temp_dir)
104
+ files = scanner.scan_files() # List[FileInfo] を取得
105
+ st.session_state.repo_content = LLMService.format_code_content(files)
106
+
107
+ st.success(f"スキャン完了: {len(files)}個のファイルを検出")
108
+ # 新しいスキャン時に会話履歴をクリア
109
+ st.session_state.llm_service.clear_history()
110
+
111
+ except Exception as e:
112
+ st.error(f"エラーが発生しました: {str(e)}")
113
+
114
+ # スキャン完了後の質問セクション
115
+ if st.session_state.repo_content:
116
+ st.divider()
117
+ st.subheader("💭 コードについて質問する")
118
+
119
+ # スキャン結果のダウンロードボタン
120
+ scan_result = create_download_content(files) # filesはスキャン結果
121
+ st.download_button(
122
+ label="スキャン結果をダウンロード",
123
+ data=scan_result,
124
+ file_name=f"scan_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md",
125
+ mime="text/markdown"
126
+ )
127
+
128
+ # 会話履歴の表示(アシスタントの回答のみ)
129
+ for message in st.session_state.llm_service.conversation_history:
130
+ if message.role == "assistant": # アシスタントの回答のみを表示
131
+ st.markdown(f'<div class="chat-message assistant-message">{message.content}</div>',
132
+ unsafe_allow_html=True)
133
+
134
+ query = st.text_area(
135
+ "質問を入力してください",
136
+ placeholder="例: このコードの主な機能は何ですか?"
137
+ )
138
+
139
+ col1, col2 = st.columns([1, 5])
140
+ with col1:
141
+ if st.button("履歴クリア"):
142
+ st.session_state.llm_service.clear_history()
143
+ st.rerun()
144
+
145
+ with col2:
146
+ if st.button("質問する", disabled=not query):
147
+ with st.spinner('回答を生成中...'):
148
+ response, error = st.session_state.llm_service.get_response(
149
+ st.session_state.repo_content,
150
+ query
151
+ )
152
+
153
+ if error:
154
+ st.error(error)
155
+ else:
156
+ st.rerun() # 会話履歴を更新するために再表示
157
+
158
+ # セッション終了時のクリーンアップ
159
+ if st.session_state.temp_dir and Path(st.session_state.temp_dir).exists():
160
+ try:
161
+ import shutil
162
+ shutil.rmtree(st.session_state.temp_dir)
163
+ except:
164
+ pass
165
+
166
+ # サイドバー情報
167
+ with st.sidebar:
168
+ st.subheader("📌 使い方")
169
+ st.markdown("""
170
+ 1. GitHubリポジトリのURLを入力
171
+ 2. スキャンを実行
172
+ 3. コードについて質問(最大5ターンの会話が可能)
173
+ """)
174
+
175
+ st.subheader("🔍 スキャン対象")
176
+ st.markdown("""
177
+ - Python (.py)
178
+ - JavaScript (.js)
179
+ - Java (.java)
180
+ - C/C++ (.c, .h, .cpp, .hpp)
181
+ - その他の主要なプログラミング言語
182
+ """)
config/__init__.py ADDED
File without changes
config/llm_setting.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from typing import Literal
4
+
5
+ class LLMSettings:
6
+ def __init__(self):
7
+ load_dotenv()
8
+
9
+ self.openai_api_key = os.getenv('OPENAI_API_KEY')
10
+ self.anthropic_api_key = os.getenv('ANTHROPIC_API_KEY')
11
+ self.default_llm = os.getenv('DEFAULT_LLM', 'claude')
12
+
13
+ # API キーの存在確認
14
+ if not self.openai_api_key and not self.anthropic_api_key:
15
+ raise ValueError("少なくとも1つのAPIキーが必要です。")
16
+
17
+ def get_available_models(self) -> list[Literal['claude', 'gpt']]:
18
+ """利用可能なモデルのリストを返す"""
19
+ models = []
20
+ if self.anthropic_api_key:
21
+ models.append('claude')
22
+ if self.openai_api_key:
23
+ models.append('gpt')
24
+ return models
config/settings.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from datetime import datetime
3
+
4
+ class Settings:
5
+ DEFAULT_OUTPUT_DIR = Path("output")
6
+ TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
7
+
8
+ @classmethod
9
+ def get_timestamp(cls) -> str:
10
+ return datetime.now().strftime(cls.TIMESTAMP_FORMAT)
11
+
12
+ @classmethod
13
+ def get_clone_dir(cls, timestamp: str) -> Path:
14
+ return cls.DEFAULT_OUTPUT_DIR / f"repo_clone_{timestamp}"
15
+
16
+ @classmethod
17
+ def get_output_file(cls, timestamp: str) -> Path:
18
+ return cls.DEFAULT_OUTPUT_DIR / f"scan_result_{timestamp}.txt"
core/__init__.py ADDED
File without changes
core/file_scanner.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import List, Dict, Optional
3
+ from dataclasses import dataclass
4
+
5
+ @dataclass
6
+ class FileInfo:
7
+ path: Path
8
+ content: Optional[str] = None
9
+
10
+ class FileScanner:
11
+ # スキャン対象の拡張子
12
+ TARGET_EXTENSIONS = {
13
+ '.py', '.js', '.java', '.cpp', '.hpp', '.c', '.h',
14
+ '.go', '.rs', '.php', '.rb', '.ts', '.scala', '.kt',
15
+ '.cs', '.swift', '.m', '.sh', '.pl', '.r'
16
+ }
17
+
18
+ # スキャン対象から除外するディレクトリ
19
+ EXCLUDED_DIRS = {
20
+ '.git', '__pycache__', 'node_modules', 'venv', '.env',
21
+ 'build', 'dist', 'target', 'bin', 'obj'
22
+ }
23
+
24
+ def __init__(self, base_dir: Path):
25
+ self.base_dir = base_dir
26
+
27
+ def _should_scan_file(self, path: Path) -> bool:
28
+ if any(excluded in path.parts for excluded in self.EXCLUDED_DIRS):
29
+ return False
30
+ return path.suffix.lower() in self.TARGET_EXTENSIONS
31
+
32
+ def _read_file_content(self, file_path: Path) -> Optional[str]:
33
+ try:
34
+ # まずUTF-8で試す
35
+ try:
36
+ with file_path.open('r', encoding='utf-8') as f:
37
+ return f.read()
38
+ except UnicodeDecodeError:
39
+ # UTF-8で失敗したらcp932を試す
40
+ with file_path.open('r', encoding='cp932') as f:
41
+ return f.read()
42
+ except (OSError, UnicodeDecodeError):
43
+ return None
44
+
45
+ def scan_files(self) -> List[FileInfo]:
46
+ if not self.base_dir.exists():
47
+ raise FileNotFoundError(f"Directory not found: {self.base_dir}")
48
+
49
+ files = []
50
+
51
+ for entry in self.base_dir.rglob('*'):
52
+ if entry.is_file() and self._should_scan_file(entry):
53
+ content = self._read_file_content(entry)
54
+ if content is not None:
55
+ files.append(FileInfo(
56
+ path=entry.relative_to(self.base_dir),
57
+ content=content
58
+ ))
59
+
60
+ return sorted(files, key=lambda x: str(x.path))
core/git_manager.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ class GitManager:
5
+ def __init__(self, repo_url: str, target_dir: Path):
6
+ self.repo_url = repo_url
7
+ self.target_dir = target_dir
8
+
9
+ def clone_repository(self) -> bool:
10
+ try:
11
+ if self.target_dir.exists():
12
+ raise FileExistsError(f"Directory already exists: {self.target_dir}")
13
+
14
+ self.target_dir.parent.mkdir(parents=True, exist_ok=True)
15
+
16
+ subprocess.run(
17
+ ["git", "clone", self.repo_url, str(self.target_dir)],
18
+ check=True,
19
+ capture_output=True,
20
+ text=True
21
+ )
22
+ return True
23
+
24
+ except subprocess.CalledProcessError as e:
25
+ raise RuntimeError(f"Clone error: {e.stderr}")
26
+
27
+ def cleanup(self):
28
+ if self.target_dir.exists():
29
+ subprocess.run(
30
+ ["rm", "-rf", str(self.target_dir)],
31
+ check=True,
32
+ capture_output=True,
33
+ text=True
34
+ )
main.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+ from config.settings import Settings
4
+ from core.git_manager import GitManager
5
+ from core.file_scanner import FileScanner
6
+ from utils.file_writer import FileWriter
7
+
8
+ def main():
9
+ # コマンドライン引数からパスを取得
10
+ if len(sys.argv) != 2:
11
+ print("Usage: python main.py <github_url or directory_path>")
12
+ return 1
13
+
14
+ target_path = sys.argv[1]
15
+ timestamp = Settings.get_timestamp()
16
+ output_file = Settings.get_output_file(timestamp)
17
+
18
+ # GitHubのURLかローカルパスかを判定
19
+ is_github = target_path.startswith(('http://', 'https://')) and 'github.com' in target_path
20
+
21
+ try:
22
+ if is_github:
23
+ # GitHubリポジトリの場合
24
+ clone_dir = Settings.get_clone_dir(timestamp)
25
+ print(f"Cloning repository: {target_path}")
26
+
27
+ git_manager = GitManager(target_path, clone_dir)
28
+ git_manager.clone_repository()
29
+
30
+ scanner = FileScanner(clone_dir)
31
+ cleanup_needed = True
32
+ else:
33
+ # ローカルディレクトリの場合
34
+ target_dir = Path(target_path)
35
+ if not target_dir.exists():
36
+ print(f"Error: Directory not found: {target_dir}")
37
+ return 1
38
+
39
+ scanner = FileScanner(target_dir)
40
+ cleanup_needed = False
41
+
42
+ # ファイルスキャンと保存
43
+ print("Scanning files...")
44
+ files = scanner.scan_files()
45
+
46
+ print(f"Writing contents to {output_file}")
47
+ writer = FileWriter(output_file)
48
+ writer.write_contents(files)
49
+
50
+ print(f"Found {len(files)} files")
51
+ print(f"Results saved to {output_file}")
52
+
53
+ except Exception as e:
54
+ print(f"Error: {e}")
55
+ return 1
56
+
57
+ finally:
58
+ # GitHubリポジトリの場合はクリーンアップ
59
+ if is_github and cleanup_needed and 'git_manager' in locals():
60
+ try:
61
+ git_manager.cleanup()
62
+ print("Cleanup completed")
63
+ except Exception as e:
64
+ print(f"Cleanup error: {e}")
65
+
66
+ return 0
67
+
68
+ if __name__ == "__main__":
69
+ exit(main())
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ openai
3
+ gitpython
4
+ python-dotenv
5
+ pathlib
6
+ chardet
scan.sh ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # エラーが発生した場合に停止
4
+ set -e
5
+
6
+ # デフォルトのターゲットパスを設定
7
+ # ここを変更することで対象を変更できます
8
+ TARGET_PATH="https://github.com/DeL-TaiseiOzaki/idebate_scraping.git" # 例: Linuxカーネル
9
+ # TARGET_PATH="/path/to/your/directory" # ローカルディレクトリの例
10
+
11
+ # 必要なディレクトリの存在確認
12
+ if [ ! -d "output" ]; then
13
+ mkdir output
14
+ fi
15
+
16
+ # Pythonの存在確認
17
+ if ! command -v python3 &> /dev/null; then
18
+ echo "Error: Python3 is not installed"
19
+ exit 1
20
+ fi
21
+
22
+ # GitHubリポジトリの場合、Gitの存在確認
23
+ if [[ $TARGET_PATH == http* ]] && [[ $TARGET_PATH == *github.com* ]]; then
24
+ if ! command -v git &> /dev/null; then
25
+ echo "Error: Git is not installed"
26
+ exit 1
27
+ fi
28
+ echo "Scanning GitHub repository: $TARGET_PATH"
29
+ else
30
+ if [ ! -d "$TARGET_PATH" ]; then
31
+ echo "Error: Directory not found: $TARGET_PATH"
32
+ exit 1
33
+ fi
34
+ echo "Scanning local directory: $TARGET_PATH"
35
+ fi
36
+
37
+ # スキャンの実行
38
+ echo "Starting directory scan..."
39
+ python3 main.py "$TARGET_PATH"
40
+
41
+ exit_code=$?
42
+
43
+ if [ $exit_code -eq 0 ]; then
44
+ echo "Scan completed successfully!"
45
+ echo "Results are saved in the 'output' directory"
46
+ else
47
+ echo "Scan failed with exit code: $exit_code"
48
+ exit $exit_code
49
+ fi
services/llm_service.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, List, Dict, Any
2
+ import openai
3
+ import anthropic
4
+ from dataclasses import dataclass
5
+ from config.llm_settings import LLMSettings
6
+ from core.file_scanner import FileInfo
7
+
8
+ @dataclass
9
+ class Message:
10
+ role: str
11
+ content: str
12
+
13
+ class LLMService:
14
+ MAX_TURNS = 5
15
+
16
+ def __init__(self):
17
+ """LLMサービスの初期化"""
18
+ self.settings = LLMSettings()
19
+ self.current_model = self.settings.default_llm
20
+
21
+ # API クライアントの初期化
22
+ if self.settings.anthropic_api_key:
23
+ self.claude_client = anthropic.Anthropic(api_key=self.settings.anthropic_api_key)
24
+ if self.settings.openai_api_key:
25
+ openai.api_key = self.settings.openai_api_key
26
+
27
+ self.conversation_history: List[Message] = []
28
+
29
+ def switch_model(self, model: str):
30
+ """使用するモデルを切り替え"""
31
+ if model not in self.settings.get_available_models():
32
+ raise ValueError(f"モデル {model} は利用できません")
33
+ self.current_model = model
34
+
35
+ def create_prompt(self, content: str, query: str) -> str:
36
+ """プロンプトを生成"""
37
+ return f"""以下はGitHubリポジトリのコード解析結果です。このコードについて質問に答えてください。
38
+
39
+ コード解析結果:
40
+ {content}
41
+
42
+ 質問: {query}
43
+
44
+ できるだけ具体的に、コードの内容を参照しながら回答してください。"""
45
+
46
+ def _add_to_history(self, role: str, content: str):
47
+ """会話履歴に追加(最大5ターン)"""
48
+ self.conversation_history.append(Message(role=role, content=content))
49
+ # 最大ターン数を超えた場合、古い会話を削除
50
+ if len(self.conversation_history) > self.MAX_TURNS * 2: # 各ターンは質問と回答で2メッセージ
51
+ self.conversation_history = self.conversation_history[-self.MAX_TURNS * 2:]
52
+
53
+ def _format_messages_for_claude(self) -> List[Dict[str, str]]:
54
+ """Claude用にメッセージをフォーマット"""
55
+ return [{"role": msg.role, "content": msg.content}
56
+ for msg in self.conversation_history]
57
+
58
+ def _format_messages_for_gpt(self) -> List[Dict[str, str]]:
59
+ """GPT用にメッセージをフォーマット"""
60
+ return [
61
+ {"role": "system", "content": "あなたはコードアナリストとして、リポジトリの解析と質問への回答を行います。"},
62
+ *[{"role": msg.role, "content": msg.content}
63
+ for msg in self.conversation_history]
64
+ ]
65
+
66
+ def get_conversation_history(self) -> List[Dict[str, str]]:
67
+ """会話履歴を取得"""
68
+ return [{"role": msg.role, "content": msg.content}
69
+ for msg in self.conversation_history]
70
+
71
+ def clear_history(self):
72
+ """会話履歴をクリア"""
73
+ self.conversation_history = []
74
+
75
+ def get_response(self, content: str, query: str) -> tuple[Optional[str], Optional[str]]:
76
+ """LLMを使用して回答を生成"""
77
+ try:
78
+ prompt = self.create_prompt(content, query)
79
+ self._add_to_history("user", prompt)
80
+
81
+ if self.current_model == 'claude':
82
+ response = self.claude_client.messages.create(
83
+ model="claude-3-sonnet-20240229",
84
+ max_tokens=4000,
85
+ messages=self._format_messages_for_claude()
86
+ )
87
+ answer = response.content[0].text
88
+
89
+ else: # gpt
90
+ response = openai.ChatCompletion.create(
91
+ model="gpt-4o",
92
+ messages=self._format_messages_for_gpt()
93
+ )
94
+ answer = response.choices[0].message.content
95
+
96
+ self._add_to_history("assistant", answer)
97
+ return answer, None
98
+
99
+ except Exception as e:
100
+ return None, f"エラーが発生しました: {str(e)}"
101
+
102
+ @staticmethod
103
+ def format_code_content(files: List[FileInfo]) -> str:
104
+ """ファイル内容をプロンプト用にフォーマット"""
105
+ formatted_content = []
106
+ for file_info in files:
107
+ formatted_content.append(
108
+ f"#ファイルパス\n{file_info.path}\n------------\n{file_info.content}\n"
109
+ )
110
+ return "\n".join(formatted_content)
utils/__init__.py ADDED
File without changes
utils/file_writer.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import List
3
+ from core.file_scanner import FileInfo
4
+
5
+ class FileWriter:
6
+ def __init__(self, output_file: Path):
7
+ self.output_file = output_file
8
+
9
+ def write_contents(self, files: List[FileInfo]) -> None:
10
+ self.output_file.parent.mkdir(parents=True, exist_ok=True)
11
+
12
+ with self.output_file.open('w', encoding='utf-8') as f:
13
+ for file_info in files:
14
+ # ファイルパスのセクション
15
+ f.write("#ファイルパス\n")
16
+ f.write(str(file_info.path))
17
+ f.write("\n------------\n")
18
+
19
+ # ファイル内容
20
+ if file_info.content is not None:
21
+ f.write(file_info.content)
22
+ else:
23
+ f.write("# Failed to read content")
24
+ f.write("\n\n")
utils/logger.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import List
3
+ from datetime import datetime
4
+ from core.file_scanner import FileInfo
5
+
6
+ class ScanLogger:
7
+ def __init__(self, log_file: Path):
8
+ self.log_file = log_file
9
+
10
+ def write_log(self, repo_url: str, files: List[FileInfo], stats: dict):
11
+ """スキャン結果をログファイルに書き込みます"""
12
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
13
+
14
+ with self.log_file.open('w', encoding='utf-8') as f:
15
+ f.write(f"スキャン日時: {datetime.now()}\n")
16
+ f.write(f"リポジトリ: {repo_url}\n")
17
+ f.write(f"ファイル数: {len(files)}\n\n")
18
+
19
+ f.write("=== ファイル種類の統計 ===\n")
20
+ for ext, count in stats.items():
21
+ f.write(f"{ext}: {count}個\n")
22
+ f.write("\n")
23
+
24
+ f.write("=== ファイルパス一覧 ===\n")
25
+ for file_info in files:
26
+ f.write(f"{file_info.path} ({file_info.formatted_size})\n")