Spaces:
Sleeping
Sleeping
DeL-TaiseiOzaki
commited on
Commit
·
230b1a5
1
Parent(s):
bc9366d
first commit
Browse files- .gitignore +13 -0
- README.md +96 -1
- app.py +182 -0
- config/__init__.py +0 -0
- config/llm_setting.py +24 -0
- config/settings.py +18 -0
- core/__init__.py +0 -0
- core/file_scanner.py +60 -0
- core/git_manager.py +34 -0
- main.py +69 -0
- requirements.txt +6 -0
- scan.sh +49 -0
- services/llm_service.py +110 -0
- utils/__init__.py +0 -0
- utils/file_writer.py +24 -0
- utils/logger.py +26 -0
.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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|