DeL-TaiseiOzaki commited on
Commit
6a71f13
·
1 Parent(s): d8c5049

LLMと会話する機能を追加

Browse files
app.py CHANGED
@@ -1,10 +1,9 @@
1
  import streamlit as st
2
  import tempfile
3
  import git
 
4
  from pathlib import Path
5
  from datetime import datetime
6
- import time
7
- from core.file_scanner import FileScanner
8
  from services.llm_service import LLMService
9
 
10
  # ページ設定
@@ -14,20 +13,34 @@ st.set_page_config(
14
  layout="wide"
15
  )
16
 
17
- # カスタムCSS
18
  st.markdown("""
19
  <style>
20
- .stAlert {
 
 
 
 
21
  padding: 1rem;
22
  margin: 1rem 0;
 
23
  }
24
- .css-1v0mbdj.ebxwdo61 {
25
- width: 100%;
26
- max-width: 800px;
 
 
 
 
 
 
 
 
27
  }
28
  </style>
29
  """, unsafe_allow_html=True)
30
 
 
31
  def clone_repository(repo_url: str) -> Path:
32
  """リポジトリをクローンして一時ディレクトリに保存"""
33
  temp_dir = Path(tempfile.mkdtemp())
@@ -40,15 +53,24 @@ if 'repo_content' not in st.session_state:
40
  if 'temp_dir' not in st.session_state:
41
  st.session_state.temp_dir = None
42
  if 'llm_service' not in st.session_state:
43
- st.session_state.llm_service = None
 
 
 
 
44
 
45
  # メインのUIレイアウト
46
  st.title("🔍 リポジトリ解析・質問システム")
47
 
48
- # OpenAI APIキーの設定
49
- api_key = st.sidebar.text_input("OpenAI APIキー", type="password", key="api_key")
50
- if api_key:
51
- st.session_state.llm_service = LLMService(api_key)
 
 
 
 
 
52
 
53
  # URLの入力
54
  repo_url = st.text_input(
@@ -65,38 +87,50 @@ if st.button("スキャン開始", disabled=not repo_url):
65
 
66
  with st.spinner('ファイルをスキャン中...'):
67
  scanner = FileScanner(temp_dir)
68
- files_content = scanner.scan_files()
69
-
70
- if st.session_state.llm_service:
71
- st.session_state.repo_content = LLMService.format_code_content(files_content)
72
 
73
- st.success(f"スキャン完了: {len(files_content)}個のファイルを検出")
 
 
74
 
75
  except Exception as e:
76
  st.error(f"エラーが発生しました: {str(e)}")
77
 
78
  # スキャン完了後の質問セクション
79
- if st.session_state.repo_content and st.session_state.llm_service:
80
  st.divider()
81
  st.subheader("💭 コードについて質問する")
82
 
 
 
 
 
 
 
83
  query = st.text_area(
84
  "質問を入力してください",
85
  placeholder="例: このコードの主な機能は何ですか?"
86
  )
87
 
88
- if st.button("質問する", disabled=not query):
89
- with st.spinner('回答を生成中...'):
90
- response, error = st.session_state.llm_service.get_response(
91
- st.session_state.repo_content,
92
- query
93
- )
94
-
95
- if error:
96
- st.error(error)
97
- else:
98
- st.markdown("### 回答:")
99
- st.markdown(response)
 
 
 
 
 
 
100
 
101
  # セッション終了時のクリーンアップ
102
  if st.session_state.temp_dir and Path(st.session_state.temp_dir).exists():
@@ -110,10 +144,9 @@ if st.session_state.temp_dir and Path(st.session_state.temp_dir).exists():
110
  with st.sidebar:
111
  st.subheader("📌 使い方")
112
  st.markdown("""
113
- 1. OpenAI APIキーを入力
114
- 2. GitHubリポジトリのURLを入力
115
- 3. スキャンを実行
116
- 4. コードについて質問
117
  """)
118
 
119
  st.subheader("🔍 スキャン対象")
 
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
 
9
  # ページ設定
 
13
  layout="wide"
14
  )
15
 
16
+ # ダークテーマの設定
17
  st.markdown("""
18
  <style>
19
+ .stApp {
20
+ background-color: #0e1117;
21
+ color: #ffffff;
22
+ }
23
+ .chat-message {
24
  padding: 1rem;
25
  margin: 1rem 0;
26
+ border-radius: 0.5rem;
27
  }
28
+ .assistant-message {
29
+ background-color: #1e2329;
30
+ color: #ffffff;
31
+ }
32
+ .stButton button {
33
+ background-color: #2ea44f;
34
+ color: #ffffff;
35
+ }
36
+ .stTextArea textarea {
37
+ background-color: #1e2329;
38
+ color: #ffffff;
39
  }
40
  </style>
41
  """, unsafe_allow_html=True)
42
 
43
+
44
  def clone_repository(repo_url: str) -> Path:
45
  """リポジトリをクローンして一時ディレクトリに保存"""
46
  temp_dir = Path(tempfile.mkdtemp())
 
53
  if 'temp_dir' not in st.session_state:
54
  st.session_state.temp_dir = None
55
  if 'llm_service' not in st.session_state:
56
+ try:
57
+ st.session_state.llm_service = LLMService()
58
+ except ValueError as e:
59
+ st.error(str(e))
60
+ st.stop()
61
 
62
  # メインのUIレイアウト
63
  st.title("🔍 リポジトリ解析・質問システム")
64
 
65
+ # サイドバーでモデル選択
66
+ available_models = st.session_state.llm_service.settings.get_available_models()
67
+ if len(available_models) > 1:
68
+ selected_model = st.sidebar.selectbox(
69
+ "使用するモデル",
70
+ available_models,
71
+ index=available_models.index(st.session_state.llm_service.current_model)
72
+ )
73
+ st.session_state.llm_service.switch_model(selected_model)
74
 
75
  # URLの入力
76
  repo_url = st.text_input(
 
87
 
88
  with st.spinner('ファイルをスキャン中...'):
89
  scanner = FileScanner(temp_dir)
90
+ files = scanner.scan_files() # List[FileInfo] を取得
91
+ st.session_state.repo_content = LLMService.format_code_content(files)
 
 
92
 
93
+ st.success(f"スキャン完了: {len(files)}個のファイルを検出")
94
+ # 新しいスキャン時に会話履歴をクリア
95
+ st.session_state.llm_service.clear_history()
96
 
97
  except Exception as e:
98
  st.error(f"エラーが発生しました: {str(e)}")
99
 
100
  # スキャン完了後の質問セクション
101
+ if st.session_state.repo_content:
102
  st.divider()
103
  st.subheader("💭 コードについて質問する")
104
 
105
+ # 会話履歴の表示(アシスタントの回答のみ)
106
+ for message in st.session_state.llm_service.conversation_history:
107
+ if message.role == "assistant": # アシスタントの回答のみを表示
108
+ st.markdown(f'<div class="chat-message assistant-message">{message.content}</div>',
109
+ unsafe_allow_html=True)
110
+
111
  query = st.text_area(
112
  "質問を入力してください",
113
  placeholder="例: このコードの主な機能は何ですか?"
114
  )
115
 
116
+ col1, col2 = st.columns([1, 5])
117
+ with col1:
118
+ if st.button("履歴クリア"):
119
+ st.session_state.llm_service.clear_history()
120
+ st.rerun()
121
+
122
+ with col2:
123
+ if st.button("質問する", disabled=not query):
124
+ with st.spinner('回答を生成中...'):
125
+ response, error = st.session_state.llm_service.get_response(
126
+ st.session_state.repo_content,
127
+ query
128
+ )
129
+
130
+ if error:
131
+ st.error(error)
132
+ else:
133
+ st.rerun() # 会話履歴を更新するために再表示
134
 
135
  # セッション終了時のクリーンアップ
136
  if st.session_state.temp_dir and Path(st.session_state.temp_dir).exists():
 
144
  with st.sidebar:
145
  st.subheader("📌 使い方")
146
  st.markdown("""
147
+ 1. GitHubリポジトリのURLを入力
148
+ 2. スキャンを実行
149
+ 3. コードについて質問(最大5ターンの会話が可能)
 
150
  """)
151
 
152
  st.subheader("🔍 スキャン対象")
config/__pycache__/__init__.cpython-310.pyc CHANGED
Binary files a/config/__pycache__/__init__.cpython-310.pyc and b/config/__pycache__/__init__.cpython-310.pyc differ
 
config/llm_settings.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
core/__pycache__/__init__.cpython-310.pyc CHANGED
Binary files a/core/__pycache__/__init__.cpython-310.pyc and b/core/__pycache__/__init__.cpython-310.pyc differ
 
core/__pycache__/file_scanner.cpython-310.pyc CHANGED
Binary files a/core/__pycache__/file_scanner.cpython-310.pyc and b/core/__pycache__/file_scanner.cpython-310.pyc differ
 
main.py DELETED
@@ -1,69 +0,0 @@
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())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scan.sh DELETED
@@ -1,49 +0,0 @@
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/__init__.py ADDED
File without changes
services/llm_service.py CHANGED
@@ -1,26 +1,39 @@
1
- from typing import Optional
2
  import openai
3
- from pathlib import Path
 
 
 
 
 
 
 
 
4
 
5
  class LLMService:
6
- def __init__(self, api_key: str):
7
- """
8
- LLMサービスの初期化
9
- Args:
10
- api_key: OpenAI APIキー
11
- """
12
- self.api_key = api_key
13
- openai.api_key = api_key
 
 
 
 
14
 
 
 
 
 
 
 
 
 
15
  def create_prompt(self, content: str, query: str) -> str:
16
- """
17
- プロンプトを生成
18
- Args:
19
- content: コードの内容
20
- query: ユーザーからの質問
21
- Returns:
22
- 生成されたプロンプト
23
- """
24
  return f"""以下はGitHubリポジトリのコード解析結果です。このコードについて質問に答えてください。
25
 
26
  コード解析結果:
@@ -30,49 +43,68 @@ class LLMService:
30
 
31
  できるだけ具体的に、コードの内容を参照しながら回答してください。"""
32
 
33
- def get_response(self, content: str, query: str) -> tuple[str, Optional[str]]:
34
- """
35
- LLMを使用して回答を生成
36
- Args:
37
- content: コードの内容
38
- query: ユーザーからの質問
39
- Returns:
40
- (回答, エラーメッセージ)のタプル
41
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  try:
43
  prompt = self.create_prompt(content, query)
 
44
 
45
- response = openai.ChatCompletion.create(
46
- model="gpt-3.5-turbo-16k",
47
- messages=[
48
- {
49
- "role": "system",
50
- "content": "あなたはコードアナリストとして、リポジトリの解析と質問への回答を行います。"
51
- },
52
- {
53
- "role": "user",
54
- "content": prompt
55
- }
56
- ]
57
- )
 
58
 
59
- return response.choices[0].message.content, None
 
60
 
61
  except Exception as e:
62
  return None, f"エラーが発生しました: {str(e)}"
63
 
64
  @staticmethod
65
- def format_code_content(files_content: dict) -> str:
66
- """
67
- ファイル内容をプロンプト用にフォーマット
68
- Args:
69
- files_content: ファイルパスと内容の辞書
70
- Returns:
71
- フォーマットされたテキスト
72
- """
73
  formatted_content = []
74
- for file_path, content in files_content.items():
75
  formatted_content.append(
76
- f"#ファイルパス\n{file_path}\n------------\n{content}\n"
77
  )
78
  return "\n".join(formatted_content)
 
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
  コード解析結果:
 
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-3.5-turbo-16k",
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)