Yasu777 commited on
Commit
8a6a561
·
verified ·
1 Parent(s): 1b2ae6b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +90 -894
app.py CHANGED
@@ -1,926 +1,122 @@
1
  import streamlit as st
 
 
 
 
2
  import os
3
- import re
4
- import subprocess
5
- import tempfile
6
- import json
7
- from groq import Groq
8
- from prompts import load_system_prompt, create_default_prompt_files
9
- from streamlit_ace import st_ace
10
 
11
  # デフォルトのプロンプトファイルを作成
12
  create_default_prompt_files()
13
 
14
- # Streamlitアプリのセットアップ
15
  st.set_page_config(
16
- page_title="コーディングアシスタント & エディタ",
17
  page_icon="💻",
18
- layout="wide"
19
  )
20
 
21
- # セッション状態初期化
 
 
 
22
  if "files" not in st.session_state:
23
  st.session_state.files = {} # ファイル名: コンテンツ
24
 
25
  if "current_file" not in st.session_state:
26
  st.session_state.current_file = None
27
 
28
- # APIキーの取得(Hugging Face Spaces環境変数から)
29
- api_key = os.getenv("GROQ_API_KEY", "")
30
-
31
- # 使用可能なモデルのリスト
32
- MODELS = {
33
- "mistral_saba": "mistral-saba-24b",
34
- "qwen_coder": "qwen-2.5-coder-32b",
35
- "deepseek": "deepseek-r1-distill-llama-70b"
36
- }
37
-
38
- # 動作モードの定義
39
- MODES = ["通常モード", "実装モード", "エラー修正モード"]
40
-
41
- # サイドバー - モード選択とファイル管理
42
- with st.sidebar:
43
- st.title("コーディングアシスタント & エディタ 💻")
44
-
45
- # モード選択タブ
46
- selected_mode = st.radio(
47
- "モード選択",
48
- MODES
49
- )
50
-
51
- # モード説明の表示
52
- mode_descriptions = {
53
- "通常モード": "Mistral Sabaがユーザー入力を分析し、必要に応じてDeepSeekの知識も活用した上で、Qwen Coderが回答を生成します。",
54
- "実装モード": "ユーザー入力の分析に基づき、Qwen Coderが設計書を作成した後、実装コードを提供します。",
55
- "エラー修正モード": "提示されたコードのエラーを分析し、Qwen Coderが修正案を提供します。"
56
- }
57
-
58
- st.markdown(f"**{selected_mode}**: {mode_descriptions[selected_mode]}")
59
-
60
- # ファイル管理セクション
61
- st.header("ファイル管理")
62
-
63
- # 新規ファイル作成
64
- new_file_name = st.text_input("新規ファイル名", value="", key="new_file")
65
- new_file_col1, new_file_col2 = st.columns([3, 1])
66
 
67
- with new_file_col1:
68
- file_extension = st.selectbox(
69
- "拡張子",
70
- options=[".py", ".js", ".html", ".css", ".json", ".txt"]
71
- )
72
 
73
- with new_file_col2:
74
- if st.button("作成", key="create_file_btn"):
75
- full_filename = new_file_name + file_extension
76
- if full_filename not in st.session_state.files and new_file_name:
77
- st.session_state.files[full_filename] = ""
78
- st.session_state.current_file = full_filename
79
- st.success(f"ファイル '{full_filename}' を作成しました")
80
- st.rerun()
81
- elif not new_file_name:
82
- st.error("ファイル名を入力してください")
83
- else:
84
- st.error(f"ファイル '{full_filename}' は既に存在します")
85
 
86
- # 既存ファイル一覧
87
- if st.session_state.files:
88
- st.subheader("既存ファイル")
89
- for filename in st.session_state.files.keys():
90
- col1, col2, col3 = st.columns([3, 1, 1])
91
- with col1:
92
- if st.button(filename, key=f"open_{filename}"):
93
- st.session_state.current_file = filename
94
- st.rerun()
95
- with col2:
96
- if st.button("削除", key=f"delete_{filename}"):
97
- del st.session_state.files[filename]
98
- if st.session_state.current_file == filename:
99
- st.session_state.current_file = None
100
- st.rerun()
101
- with col3:
102
- if st.button("コピー", key=f"copy_{filename}"):
103
- name, ext = os.path.splitext(filename)
104
- new_name = f"{name}_copy{ext}"
105
- counter = 1
106
- while new_name in st.session_state.files:
107
- new_name = f"{name}_copy_{counter}{ext}"
108
- counter += 1
109
- st.session_state.files[new_name] = st.session_state.files[filename]
110
- st.rerun()
111
-
112
- # チャット履歴のクリア機能
113
- if st.button("会話履歴をクリア"):
114
- st.session_state.messages = []
115
- st.session_state.thinking_process = []
116
- st.session_state.is_first_message = True
117
- st.session_state.conversation_state = {
118
- "topic": None,
119
- "last_question_type": None,
120
- "mentioned_entities": set(),
121
- "follow_up_count": 0
122
- }
123
- st.rerun()
124
-
125
- # 接続情報と使い方のガイド
126
- st.sidebar.markdown("---")
127
- st.subheader("使い方ガイド")
128
- with st.expander("アプリの機能", expanded=True):
129
- st.markdown("""
130
- ### 主な機能
131
- 1. **コードエディタ** - 複数のプログラミング言語に対応したシンタックスハイライト付きエディタ
132
- 2. **Pythonコード実行** - エディタ上のPythonコードをその場で実行
133
- 3. **AIアシスタント** - コーディングに関する質問や実装の提案
134
- 4. **ファイル管理** - 複数のファイルの作成・保存・編集
135
- 5. **エラー修正** - コードのエラーを自動で診断・修正
136
-
137
- ### 特殊コマンド
138
- - `/mode 通常` - 通常の質問応答モードに切り替え
139
- - `/mode 実装` - コード実装モードに切り替え
140
- - `/mode エラー修正` - エラー修正モードに切り替え
141
- """)
142
 
143
- with st.expander("Hugging Face Spacesでの設定方法"):
144
- st.markdown("""
145
- ### Hugging Face Spacesでの設定手順
146
-
147
- 1. **APIキーの設定**:
148
- - Spacesのダッシュボードに移動
149
- - Settings > Repository secrets
150
- - 新しいシークレットを追加: キー名 `GROQ_API_KEY`、値に自分のGroq APIキーを入力
151
-
152
- 2. **依存関係の追加**:
153
- - `requirements.txt` に以下を追加:
154
- ```
155
- streamlit
156
- groq
157
- streamlit-ace
158
- ```
159
-
160
- 3. **ファイル構造**:
161
- - `app.py`: メインアプリケーションファイル
162
- - `prompts.py`: プロンプト管理用ファイル
163
- - `prompts/`: プロンプトを保存するディレクトリ
164
- """)
165
 
166
- # メインエリアを2つのカラムに分割
167
- code_col, chat_col = st.columns([1, 1])
168
 
169
- # コードエディタカラム
170
- with code_col:
171
- st.header("コードエディタ")
172
 
173
- if st.session_state.current_file:
174
- # ファイルの拡張子に基づいて言語を設定
175
- file_ext = os.path.splitext(st.session_state.current_file)[1].lower()
176
- lang_map = {
177
- ".py": "python",
178
- ".js": "javascript",
179
- ".html": "html",
180
- ".css": "css",
181
- ".json": "json",
182
- ".txt": "text"
183
- }
184
- language = lang_map.get(file_ext, "python")
185
-
186
- # エディタの表示
187
- st.subheader(f"編集中: {st.session_state.current_file}")
188
-
189
- code = st_ace(
190
- value=st.session_state.files[st.session_state.current_file],
191
- language=language,
192
- theme="monokai",
193
- keybinding="vscode",
194
- font_size=14,
195
- min_lines=20,
196
- key=f"ace_editor_{st.session_state.current_file}"
197
- )
198
-
199
- # コードを保存
200
- st.session_state.files[st.session_state.current_file] = code
201
-
202
- # Python codeの場合、実行ボタンを表示
203
- if language == "python":
204
- if st.button("コードを実行", key="run_code_btn"):
205
- with st.spinner("コードを実行中..."):
206
- # 一時ファイルを作成してコードを実行
207
- with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as tmp:
208
- tmp.write(code.encode())
209
- tmp_name = tmp.name
210
-
211
- try:
212
- # サブプロセスでPythonコードを実行
213
- result = subprocess.run(
214
- ["python", tmp_name],
215
- capture_output=True,
216
- text=True,
217
- timeout=10 # 10秒のタイムアウト
218
- )
219
-
220
- # 実行結果を表示
221
- if result.stdout:
222
- st.code(result.stdout, language="text")
223
-
224
- if result.stderr:
225
- st.error(result.stderr)
226
-
227
- # エラーが発生した場合、自動的にエラー修正モードに切り替える
228
- if st.button("エラー修正AIに相談", key="ask_error_fix"):
229
- st.session_state.current_mode = "エラー修正モード"
230
- error_query = f"次のPythonコードにエラーがあります。修正方法を教えてください。\n\n```python\n{code}\n```\n\nエラーメッセージ:\n```\n{result.stderr}\n```"
231
-
232
- # チャットにエラー修正用のメッセージを追加
233
- if "messages" not in st.session_state:
234
- st.session_state.messages = []
235
-
236
- st.session_state.messages.append({"role": "user", "content": error_query})
237
- st.rerun()
238
-
239
- except subprocess.TimeoutExpired:
240
- st.error("実行がタイムアウトしました(10秒以上かかりました)")
241
-
242
- except Exception as e:
243
- st.error(f"実行中にエラーが発生しました: {str(e)}")
244
-
245
- finally:
246
- # 一時ファイルを削除
247
- if os.path.exists(tmp_name):
248
- os.unlink(tmp_name)
249
-
250
- # ファイルエクスポート機能
251
- st.download_button(
252
- label="ファイルをダウンロード",
253
- data=code,
254
- file_name=st.session_state.current_file,
255
- key="download_file"
256
- )
257
 
258
- else:
259
- st.info("左のサイドバーから既存のファイルを選択するか、新しいファイルを作成してください。")
260
 
261
- # チャットカラム
262
- with chat_col:
263
- st.header("AIアシスタント")
264
-
265
- # セッション初期化
266
- if "messages" not in st.session_state:
267
- st.session_state.messages = []
268
-
269
- if "thinking_process" not in st.session_state:
270
- st.session_state.thinking_process = []
271
-
272
- if "current_mode" not in st.session_state:
273
- st.session_state.current_mode = selected_mode
274
-
275
- if "is_first_message" not in st.session_state:
276
- st.session_state.is_first_message = True
277
-
278
- # 会話状態の初期化
279
- if "conversation_state" not in st.session_state:
280
- st.session_state.conversation_state = {
281
- "topic": None, # 会話の主要トピック
282
- "last_question_type": None, # 前回の質問のタイプ
283
- "mentioned_entities": set(), # 会話で言及されたエンティティ
284
- "follow_up_count": 0 # フォローアップ質問の数
285
- }
286
-
287
- # モード変更の検出
288
- if st.session_state.current_mode != selected_mode:
289
- st.session_state.current_mode = selected_mode
290
- st.session_state.is_first_message = True
291
-
292
- # モード変更時にこれまでのチャットを要約
293
- if len(st.session_state.messages) > 0:
294
- with st.spinner("前回の会話内容を要約しています..."):
295
- try:
296
- # チャット履歴を文字列にまとめる
297
- chat_history = ""
298
- for msg in st.session_state.messages:
299
- role = "ユーザー" if msg["role"] == "user" else "アシスタント"
300
- chat_history += f"{role}: {msg['content']}\n\n"
301
-
302
- # 要約を生成
303
- client = Groq(api_key=api_key)
304
- summarize_prompt = f"""
305
- 以下はユーザーとアシスタントの間の会話です。この会話を300単語以内で要約してください。
306
- 特に重要な内容、技術的な詳細、解決した問題に焦点を当ててください。
307
-
308
- {chat_history}
309
- """
310
-
311
- # Mistral Sabaモデルのシステムプロンプトをロード
312
- summary_system_prompt = load_system_prompt(MODELS["mistral_saba"])
313
-
314
- summary_response = client.chat.completions.create(
315
- model=MODELS["mistral_saba"],
316
- messages=[
317
- {"role": "system", "content": summary_system_prompt},
318
- {"role": "user", "content": summarize_prompt}
319
- ],
320
- temperature=0.3,
321
- max_tokens=1000
322
- )
323
-
324
- summary = summary_response.choices[0].message.content
325
- st.session_state.chat_summary = summary
326
-
327
- # 要約をthinking_processに追加
328
- st.session_state.thinking_process.append({
329
- "title": "前回の会話要約",
330
- "content": summary
331
- })
332
-
333
- # ユーザーに要約を通知
334
- st.info(f"モードが変更されました。前回の会話の要約:\n\n{summary}")
335
- except Exception as e:
336
- st.error(f"会話要約の生成中にエラーが発生しました: {str(e)}")
337
-
338
- # 物理学関連のキーワード
339
- PHYSICS_KEYWORDS = [
340
- "物理", "力学", "電磁気", "熱力学", "量子", "相対性", "波動", "力", "運動", "エネルギー",
341
- "physics", "mechanics", "electromagnetism", "thermodynamics", "quantum", "relativity",
342
- "wave", "force", "motion", "energy", "シミュレーション", "simulation", "重力", "gravity",
343
- "加速度", "acceleration", "速度", "velocity", "衝突", "collision", "反発", "rebound",
344
- "摩擦", "friction", "剛体", "rigid body", "粒子", "particle", "流体", "fluid",
345
- "弾性", "elastic", "軌道", "trajectory", "バネ", "spring", "圧力", "pressure"
346
- ]
347
-
348
- # テキストが物理学に関連しているかチェック
349
- def is_physics_related(text):
350
- lower_text = text.lower()
351
-
352
- # 明確な物理関連キーワード(より具体的で誤検出が少ないもの)
353
- explicit_physics_keywords = [
354
- "物理法則", "物理シミュレーション", "ニュートン力学", "電磁気学", "熱力学", "量子力学", "相対性理論",
355
- "physics simulation", "newton's laws", "electromagnetism", "thermodynamics",
356
- "quantum mechanics", "relativity theory"
357
- ]
358
-
359
- # 単語境界を考慮すべきキーワード(一般的な単語だが、単独で使われた場合は物理関連の可能性が高い)
360
- word_boundary_keywords = [
361
- r"\b力学\b", r"\b重力\b", r"\b摩擦\b", r"\b衝突\b", r"\b運動方程式\b", r"\b加速度\b", r"\b速度\b", r"\b質量\b",
362
- r"\bgravity\b", r"\bfriction\b", r"\bcollision\b", r"\bvelocity\b", r"\bacceleration\b", r"\bmass\b"
363
- ]
364
-
365
- # 明確な物理用語が含まれているか
366
- explicit_match = any(keyword.lower() in lower_text for keyword in explicit_physics_keywords)
367
-
368
- # 境界を考慮すべき単語が含まれているか(単語として独立して存在する場合のみ)
369
- boundary_match = any(re.search(pattern, lower_text) for pattern in word_boundary_keywords)
370
-
371
- # 物理シミュレーション関連の特定のフレーズパターン
372
- physics_phrases = [
373
- r"物理.*シミュレ(ーション|ート)",
374
- r"physics.*simulation",
375
- r"ボールの.*衝突",
376
- r"ball.*collision",
377
- r"重力.*計算",
378
- r"gravity.*calculation",
379
- r"運動方程式",
380
- r"equation.*motion",
381
- r"力学.*モデル",
382
- r"mechanics.*model"
383
- ]
384
- phrase_match = any(re.search(pattern, lower_text) for pattern in physics_phrases)
385
-
386
- # 誤検出しやすいコンテキストのチェック(これらが含まれる場合は物理関連ではない可能性が高い)
387
- non_physics_contexts = [
388
- "アプリの動作", "システムの動作", "プログラムの動作", "ソフトウェアの動作",
389
- "app behavior", "system behavior", "program behavior", "software behavior",
390
- "UIの動き", "インターフェースの動き", "ボタンの動き",
391
- "UI movement", "interface movement", "button movement"
392
- ]
393
- negative_context = any(context.lower() in lower_text for context in non_physics_contexts)
394
-
395
- # 物理関連であると判断するロジック:
396
- # 1. 明確な物理用語が含まれている、または
397
- # 2. 境界を考慮すべき単語が含まれていて、かつ誤検出コンテキストが含まれていない、または
398
- # 3. 物理シミュレーション関連の特定のフレーズが含まれている
399
- return explicit_match or (boundary_match and not negative_context) or phrase_match
400
-
401
- # テキスト内にコードブロックが含まれているかを確認
402
- def contains_code_block(text):
403
- # マークダウンのコードブロックパターン
404
- code_block_pattern = r"```[\w]*\n[\s\S]*?\n```"
405
- return bool(re.search(code_block_pattern, text))
406
-
407
- # テキストからすべてのコードブロックを抽出する
408
- def extract_all_code_blocks(text):
409
- """テ���ストから全てのコードブロックを抽出する"""
410
- code_block_pattern = r"```(\w+)?\n([\s\S]*?)\n```"
411
- matches = re.findall(code_block_pattern, text)
412
-
413
- if not matches:
414
- return []
415
-
416
- code_blocks = []
417
- for lang, code in matches:
418
- lang = lang or "python" # 言語指定がない場合はPythonと仮定
419
- code_blocks.append(f"```{lang}\n{code}\n```")
420
-
421
- return code_blocks
422
-
423
- # 生成されたコードを新しいファイルに保存する関数
424
- def save_generated_code_to_new_file(code_text, suggested_name=None):
425
- # コードブロックのマークアップを削除
426
- code_pattern = r"```(?:\w+)?\n([\s\S]*?)\n```"
427
- match = re.search(code_pattern, code_text)
428
- clean_code = match.group(1) if match else code_text
429
-
430
- # 言語の推測
431
- language = "py" # デフォルトはPython
432
- lang_pattern = r"```(\w+)\n"
433
- lang_match = re.search(lang_pattern, code_text)
434
- if lang_match:
435
- detected_lang = lang_match.group(1).lower()
436
- lang_mapping = {
437
- "python": "py",
438
- "javascript": "js",
439
- "html": "html",
440
- "css": "css",
441
- "json": "json",
442
- "java": "java",
443
- "cpp": "cpp",
444
- "c++": "cpp",
445
- "c": "c",
446
- "go": "go",
447
- "rust": "rs",
448
- "typescript": "ts"
449
- }
450
- language = lang_mapping.get(detected_lang, detected_lang)
451
-
452
- # ファイル名の生成
453
- if not suggested_name:
454
- import hashlib
455
- import time
456
- # タイムスタンプとコードの一部からハッシュを生成
457
- timestamp = str(int(time.time()))
458
- code_hash = hashlib.md5(clean_code[:100].encode()).hexdigest()[:6]
459
- suggested_name = f"generated_{timestamp}_{code_hash}"
460
-
461
- # 拡張子が含まれていなければ追加
462
- if "." not in suggested_name:
463
- suggested_name = f"{suggested_name}.{language}"
464
-
465
- # 同名ファイルが存在する場合は連番を付ける
466
- base_name, ext = os.path.splitext(suggested_name)
467
- counter = 1
468
- file_name = suggested_name
469
- while file_name in st.session_state.files:
470
- file_name = f"{base_name}_{counter}{ext}"
471
- counter += 1
472
-
473
- # ファイルを保存
474
- st.session_state.files[file_name] = clean_code
475
- st.session_state.current_file = file_name
476
-
477
- return file_name
478
-
479
- # 過去のメッセージを表示
480
- for message in st.session_state.messages:
481
- with st.chat_message(message["role"]):
482
- st.markdown(message["content"])
483
-
484
- # 思考プロセスの表示(開閉可能なエクスパンダー)
485
- if st.session_state.thinking_process:
486
- with st.expander("思考プロセスを表示"):
487
- for i, thought in enumerate(st.session_state.thinking_process):
488
- st.subheader(thought["title"])
489
- st.markdown(thought["content"])
490
- st.divider()
491
-
492
- # 会話状態を更新する関数
493
- def update_conversation_state(user_input, ai_response):
494
- # 主要トピックの更新(最初のメッセージから)
495
- if st.session_state.conversation_state["topic"] is None and user_input:
496
- # 簡易的なトピック抽出(最初の文か最初の30単語)
497
- first_sentence = user_input.split('.')[0]
498
- topic = first_sentence[:100] + "..." if len(first_sentence) > 100 else first_sentence
499
- st.session_state.conversation_state["topic"] = topic
500
-
501
- # フォローアップカウントの更新
502
- if "?" in user_input or "?" in user_input:
503
- st.session_state.conversation_state["follow_up_count"] += 1
504
-
505
- # その他の状態更新ロジックをここに追加
506
- pass
507
-
508
- # APIを使用した思考プロセスと回答の生成
509
- def generate_response(user_input):
510
- client = Groq(api_key=api_key)
511
- thinking_results = []
512
-
513
- # ユーザー入力が物理関連かどうかをチェック
514
- is_physics = is_physics_related(user_input)
515
-
516
- # Step 1: Mistral Sabaによる入力分析
517
- mistral_system_prompt = load_system_prompt(MODELS["mistral_saba"])
518
- mistral_prompt = f"""
519
- 以下のユーザー入力を分析してください。ユーザーの質問や要求の本質、重要なキーワード、技術的課題を抽出し、
520
- どのようなアプローチで回答すべきかを詳細に検討してください。
521
-
522
- ユーザー入力: {user_input}
523
- """
524
-
525
- mistral_response = client.chat.completions.create(
526
- model=MODELS["mistral_saba"],
527
- messages=[
528
- {"role": "system", "content": mistral_system_prompt},
529
- {"role": "user", "content": mistral_prompt}
530
- ],
531
- temperature=0.3,
532
- max_tokens=2000
533
- )
534
-
535
- mistral_analysis = mistral_response.choices[0].message.content
536
- thinking_results.append({
537
- "title": "Mistral Sabaによる分析",
538
- "content": mistral_analysis
539
- })
540
-
541
- # 物理関連の場合のみDeepSeekの意見を取得
542
- deepseek_knowledge = ""
543
- if is_physics:
544
- deepseek_system_prompt = load_system_prompt(MODELS["deepseek"])
545
- deepseek_prompt = f"""
546
- 以下のユーザー入力は物理学に関連しています。物理学の観点から、この質問や課題に対する
547
- 科学的知識、法則、公式、アプローチを提供してください。特に関連する物理概念と実装方法の
548
- 関連性があれば説明してください。
549
-
550
- ユーザー入力: {user_input}
551
- """
552
-
553
- deepseek_response = client.chat.completions.create(
554
- model=MODELS["deepseek"],
555
- messages=[
556
- {"role": "system", "content": deepseek_system_prompt},
557
- {"role": "user", "content": deepseek_prompt}
558
- ],
559
- temperature=0.3,
560
- max_tokens=2000
561
- )
562
-
563
- deepseek_knowledge = deepseek_response.choices[0].message.content
564
- thinking_results.append({
565
- "title": "DeepSeekによる物理学的知見",
566
- "content": deepseek_knowledge
567
- })
568
-
569
- # Step 2: モード別の応答生成
570
- if st.session_state.current_mode == "通常モード":
571
- # 物理関連の場合のみDeepSeekの情報を含める
572
- deepseek_section = ""
573
- physics_consideration = ""
574
-
575
- if is_physics:
576
- physics_consideration = " DeepSeekの物理学的知見を考慮して"
577
- deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
578
-
579
- # 現在のエディタのコードを取得(コードエディタで何かが開かれている場合)
580
- editor_code = ""
581
- editor_reference = ""
582
- if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
583
- editor_code = st.session_state.files[st.session_state.current_file]
584
- editor_reference = f"現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```"
585
-
586
- # 通常モード用のシステムプロンプトをロード
587
- system_prompt = load_system_prompt(MODELS["qwen_coder"], "通常モード")
588
-
589
- qwen_prompt = f"""
590
- 以下のユーザー入力に対してコーディングアシスタントとして回答してください。
591
- Mistral Sabaの分析結果{physics_consideration}を考慮して、
592
- 最適なソリューションを提供してください。
593
-
594
- ユーザー入力: {user_input}
595
-
596
- Mistral Sabaの分析:
597
- {mistral_analysis}
598
- """
599
-
600
- # 物理関連の場合のみDeepSeekの情報を追加
601
- if is_physics:
602
- qwen_prompt += f"\n\n{deepseek_section}"
603
-
604
- # エディタのコードが存在する場合は参照として追加
605
- if editor_code:
606
- qwen_prompt += f"\n\n{editor_reference}\n\nエディタで開かれているコードに関して、ユーザーの質問に答えるか、必要に応じてコードの改善を提案してください。"
607
-
608
- qwen_response = client.chat.completions.create(
609
- model=MODELS["qwen_coder"],
610
- messages=[
611
- {"role": "system", "content": system_prompt},
612
- {"role": "user", "content": qwen_prompt}
613
- ],
614
- temperature=0.5,
615
- max_tokens=4000
616
- )
617
-
618
- final_response = qwen_response.choices[0].message.content
619
-
620
- elif st.session_state.current_mode == "実装モード":
621
- # 物理関連の条件分岐を変数で処理
622
- deepseek_section = ""
623
- physics_note = ""
624
-
625
- if is_physics:
626
- deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
627
- physics_note = "この実装には物理シミュレーションが含まれているため、物理法則の正確性と数値的安定性を特に重視してください。"
628
-
629
- # Step 2a: 設計フェーズ - 専用のプロンプトを使用
630
- design_system_prompt = load_system_prompt(MODELS["qwen_coder"], "実装モード_設計")
631
-
632
- design_prompt = f"""
633
- 以下のユーザー入力と分析結果に基づいて、実装に必要な設計書を作成してください。
634
-
635
- ユーザー入力: {user_input}
636
-
637
- Mistral Sabaの分析:
638
- {mistral_analysis}
639
- """
640
-
641
- # 物理関連の場合のみDeepSeekの情報を追加
642
- if is_physics:
643
- design_prompt += f"\n\n{deepseek_section}"
644
-
645
- design_response = client.chat.completions.create(
646
- model=MODELS["qwen_coder"],
647
- messages=[
648
- {"role": "system", "content": design_system_prompt},
649
- {"role": "user", "content": design_prompt}
650
- ],
651
- temperature=0.4,
652
- max_tokens=3000
653
- )
654
-
655
- design_doc = design_response.choices[0].message.content
656
- thinking_results.append({
657
- "title": "Qwen Coderによる設計書",
658
- "content": design_doc
659
- })
660
-
661
- # Step 2b: 実装フェーズ - 専用のプロンプトを使用
662
- implementation_system_prompt = load_system_prompt(MODELS["qwen_coder"], "実装モード_実装")
663
-
664
- implementation_prompt = f"""
665
- 以下の設計書に基づいて、完全な実装コードを提供してください。
666
- コードは実行可能で、エラー処理が適切に行われているものにしてください。
667
- 各コンポーネントには十分なコメントを付け、使用方法の例も含めてください。
668
- {physics_note}
669
-
670
- 設計書:
671
- {design_doc}
672
-
673
- ユーザーの元々の要求:
674
- {user_input}
675
- """
676
-
677
- implementation_response = client.chat.completions.create(
678
- model=MODELS["qwen_coder"],
679
- messages=[
680
- {"role": "system", "content": implementation_system_prompt},
681
- {"role": "user", "content": implementation_prompt}
682
- ],
683
- temperature=0.5,
684
- max_tokens=4000
685
- )
686
-
687
- implementation_result = implementation_response.choices[0].message.content
688
-
689
- # 設計書と実装を組み合わせた最終結果
690
- final_response = f"# 設計書\n\n{design_doc}\n\n# 実装コード\n\n{implementation_result}"
691
-
692
- # コードの自動抽出と保存機能
693
- code_blocks = extract_all_code_blocks(implementation_result)
694
- if code_blocks and len(code_blocks) > 0:
695
- # 抽出したコードブロックに対してボタンを作成するための識別子を付加
696
- final_response += "\n\n**実装されたコードをエディタに保存できます。チャット上の「コードを保存」ボタンをクリックしてください。**"
697
- # 最初のコードブロックを抽出してセッション状態に保存
698
- st.session_state.last_generated_code = code_blocks[0]
699
-
700
- elif st.session_state.current_mode == "エラー修正モード":
701
- # エディタのコードを取得(存在する場合)
702
- editor_code = ""
703
- editor_reference = ""
704
- if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
705
- editor_code = st.session_state.files[st.session_state.current_file]
706
- editor_reference = f"現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```\n\nこのコードにエラーがある可能性があります。ユーザーからの情報と合わせて分析してください。"
707
-
708
- # 物理関連の条件分岐を変数で処理
709
- deepseek_section = ""
710
-
711
- if is_physics:
712
- deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
713
-
714
- # エラー修正モード用のシステムプロンプトをロード
715
- error_system_prompt = load_system_prompt(MODELS["qwen_coder"], "エラー修正モード")
716
-
717
- error_prompt = f"""
718
- 以下のユーザー入力はコードのエラーに関するものです。
719
- エラーの原因を特定し、修正案を提供してください。
720
- 説明では:
721
- 1. エラーの根本原因
722
- 2. 問題箇所の特定
723
- 3. 修正方法の詳細な説明
724
- 4. 修正されたコード
725
- 5. 同様のエラーを防ぐためのベストプラクティス
726
- を含めてください。
727
-
728
- ユーザー入力: {user_input}
729
-
730
- Mistral Sabaの分析:
731
- {mistral_analysis}
732
- """
733
-
734
- # エディタのコードを追加
735
- if editor_code:
736
- error_prompt += f"\n\n{editor_reference}"
737
-
738
- # 物理関連の場合のみDeepSeekの情報を追加
739
- if is_physics:
740
- error_prompt += f"\n\n{deepseek_section}"
741
-
742
- error_response = client.chat.completions.create(
743
- model=MODELS["qwen_coder"],
744
- messages=[
745
- {"role": "system", "content": error_system_prompt},
746
- {"role": "user", "content": error_prompt}
747
- ],
748
- temperature=0.4,
749
- max_tokens=4000
750
- )
751
-
752
- final_response = error_response.choices[0].message.content
753
-
754
- return final_response, thinking_results
755
-
756
- # 最後に生成されたコードを保存するセッション変数の初期化
757
- if "last_generated_code" not in st.session_state:
758
- st.session_state.last_generated_code = None
759
-
760
- # 「コードを保存」ボタンが押された場合の処理
761
- if st.session_state.last_generated_code and st.button("AIが生成したコードをエディタに保存"):
762
- with st.spinner("コードを保存中..."):
763
- file_name = save_generated_code_to_new_file(st.session_state.last_generated_code)
764
- st.success(f"コードを '{file_name}' として保存しました")
765
- st.session_state.last_generated_code = None # 保存したのでクリア
766
- st.rerun()
767
-
768
- # ユーザー入力
769
- if prompt := st.chat_input("質問を入力してください"):
770
- # ユーザーメッセージを表示
771
- st.chat_message("user").markdown(prompt)
772
-
773
- # ユーザーメッセージをセッション状態に追加
774
- st.session_state.messages.append({"role": "user", "content": prompt})
775
-
776
- # モード切替コマンドを検出
777
- if prompt.strip().startswith("/mode"):
778
- parts = prompt.strip().split()
779
- if len(parts) < 2:
780
- response = "モード指定が不完全です。使用方法: /mode [通常|実装|エラー修正]"
781
- else:
782
- mode = parts[1].lower()
783
- valid_modes = {"通常": "通常モード", "実装": "実装モード", "エラー修正": "エラー修正モード"}
784
-
785
- if mode not in valid_modes and mode not in valid_modes.values():
786
- response = f"無効なモードです。使用可能なモード: {', '.join(valid_modes.keys())}"
787
- else:
788
- old_mode = st.session_state.current_mode
789
- if mode in valid_modes:
790
- st.session_state.current_mode = valid_modes[mode]
791
- else:
792
- st.session_state.current_mode = mode
793
-
794
- st.session_state.is_first_message = True
795
-
796
- # モード変更時の説明メッセージを用意
797
- mode_descriptions = {
798
- "通常モード": "通常の質問応答モードに切り替えました。",
799
- "実装モード": "アイデアから設計と実装を生成するモードに切り替えました。",
800
- "エラー修正モード": "エラーメッセージから問題を診断・修正するモードに切り替えました。"
801
- }
802
-
803
- instructions = {
804
- "通常モード": "質問や議論したいトピックを入力してください。",
805
- "実装モード": "実装したい機能やアイデアを入力してください。",
806
- "エラー修正モード": "発生したエラーメッセージとコードを入力してください。"
807
- }
808
-
809
- response = f"モードを「{old_mode}」から「{st.session_state.current_mode}」に変更しました。\n\n{mode_descriptions[st.session_state.current_mode]}\n\n{instructions[st.session_state.current_mode]}"
810
-
811
- # レスポンスを表示
812
- st.chat_message("assistant").markdown(response)
813
-
814
- # アシスタントメッセージをセッション状態に追加
815
- st.session_state.messages.append({"role": "assistant", "content": response})
816
-
817
- # 通常の質問処理
818
- else:
819
- # APIキーがある場合のみ実行
820
- if api_key:
821
- try:
822
- # 最初のメッセージはチェーンを使用、それ以降はQwen Coderのみで応答
823
- if st.session_state.is_first_message:
824
- with st.spinner("回答を生成中...(複数のモデルによる分析を行っています)"):
825
- response, thinking_results = generate_response(prompt)
826
- st.session_state.thinking_process = thinking_results
827
- st.session_state.is_first_message = False
828
- else:
829
- with st.spinner("回答を生成中..."):
830
- # 直接Qwen Coderで応答
831
- client = Groq(api_key=api_key)
832
-
833
- # 現在のモードに合わせたシステムプロンプトをロード
834
- current_system_prompt = load_system_prompt(MODELS["qwen_coder"], st.session_state.current_mode)
835
-
836
- # 会話の連続性のための追加指示
837
- current_system_prompt += """
838
-
839
- 重要: これは継続中の会話です。前の質問や回答を踏まえて、会話の自然な流れを維持してください。
840
- ユーザーの最新の質問だけに集中するのではなく、会話全体の文脈を考慮してください。
841
- 繰り返しは避け、新しい情報や洞察を提供してください。
842
- """
843
-
844
- # 過去の会話履歴を完全に構築
845
- context = ""
846
- if len(st.session_state.messages) > 2:
847
- # 最近の会話履歴(最大トークン制限を考慮して適切な数に制限)
848
- recent_messages = st.session_state.messages[-10:] # 最近の10メッセージまで
849
- for msg in recent_messages:
850
- role = "ユーザー" if msg["role"] == "user" else "アシスタント"
851
- # 全文を含める
852
- context += f"{role}: {msg['content']}\n\n"
853
-
854
- # 現在のエディタのコードを取得(存在する場合)
855
- editor_code = ""
856
- editor_reference = ""
857
- if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
858
- editor_code = st.session_state.files[st.session_state.current_file]
859
- editor_reference = f"\n\n現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```\n\nエディタで開かれているコードに関して、ユーザーの質問に答えるか、必要に応じてコードの改善を提案してください。"
860
-
861
- qwen_prompt = f"""
862
- あなたはユーザーと継続的な会話を行っているアシスタントです。
863
- これまでの会話の流れを踏まえて、最新のユーザー入力に適切に応答してください。
864
-
865
- これまでの会話:
866
- {context}
867
-
868
- 最新のユーザー入力: {prompt}
869
-
870
- 前の会話の文脈を考慮して、この最新の質問に直接回答してください。
871
- 前回までの内容を繰り返すのではなく、会話を自然に進めてください。
872
- """
873
-
874
- # エディタのコードが存在する場合は参照として追加
875
- if editor_code:
876
- qwen_prompt += editor_reference
877
-
878
- qwen_response = client.chat.completions.create(
879
- model=MODELS["qwen_coder"],
880
- messages=[
881
- {"role": "system", "content": current_system_prompt},
882
- {"role": "user", "content": qwen_prompt}
883
- ],
884
- temperature=0.5,
885
- max_tokens=4000
886
- )
887
-
888
- response = qwen_response.choices[0].message.content
889
-
890
- # レスポンスを表示
891
- st.chat_message("assistant").markdown(response)
892
-
893
- # アシスタントメッセージをセッション状態に追加
894
- st.session_state.messages.append({"role": "assistant", "content": response})
895
-
896
- # 会話状態を��新
897
- update_conversation_state(prompt, response)
898
-
899
- # モード切替提案を追加(UIには表示しない)
900
- mode_suggestions = {
901
- "通常モード": "\n\n---\n*実装を生成するには、「/mode 実装」と入力して実装モードに切り替えてください。*",
902
- "実装モード": "\n\n---\n*このコードを検証・修正するには、「/mode エラー修正」と入力して検証修正モードに切り替えてください。*",
903
- "エラー修正モード": "\n\n---\n*新しい実装を生成するには、「/mode 実装」と入力して実装モードに切り替えてください。*"
904
- }
905
-
906
- # 提案はセッションステートには保存するが、UIには表示しない(次回のcontext生成で使用)
907
- suggestion = mode_suggestions.get(st.session_state.current_mode, "")
908
- st.session_state.messages[-1]["content"] += suggestion
909
-
910
- except Exception as e:
911
- st.error(f"エラーが発生しました: {str(e)}")
912
- else:
913
- st.error("GROQ_API_KEYが設定されていません。Hugging Face Spacesの環境変数で設定してください。")
914
 
915
  # APIキーがない場合の警告表示
916
  if not api_key:
917
  st.warning("""
918
  ### ⚠️ GROQ_API_KEYが設定されていません
919
 
920
- このアプリを使用するには、Hugging Face Spacesの環境変数設定でGROQ_API_KEYを設定する必要があります:
921
 
922
- 1. Spacesのダッシュボードに移動
923
- 2. Settings > Repository secrets
924
- 3. 新しいシークレットを追加: キー名 `GROQ_API_KEY`、値に自分のGroq APIキーを入力
925
- 4. アプリを再起動
926
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ from editor import setup_editor
3
+ from chat import setup_chat
4
+ from config import setup_config, load_api_key
5
+ from utils import create_default_prompt_files
6
  import os
 
 
 
 
 
 
 
7
 
8
  # デフォルトのプロンプトファイルを作成
9
  create_default_prompt_files()
10
 
11
+ # アプリ設定
12
  st.set_page_config(
13
+ page_title="コーディングアシスタント",
14
  page_icon="💻",
15
+ layout="centered" # モバイルフレンドリーな中央寄せレイアウト
16
  )
17
 
18
+ # APIキー取得
19
+ api_key = load_api_key()
20
+
21
+ # セッションの初期化
22
  if "files" not in st.session_state:
23
  st.session_state.files = {} # ファイル名: コンテンツ
24
 
25
  if "current_file" not in st.session_state:
26
  st.session_state.current_file = None
27
 
28
+ if "messages" not in st.session_state:
29
+ st.session_state.messages = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ if "thinking_process" not in st.session_state:
32
+ st.session_state.thinking_process = []
 
 
 
33
 
34
+ if "current_mode" not in st.session_state:
35
+ st.session_state.current_mode = "通常モード"
 
 
 
 
 
 
 
 
 
 
36
 
37
+ if "is_first_message" not in st.session_state:
38
+ st.session_state.is_first_message = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ if "mobile_mode" not in st.session_state:
41
+ # デバイスの画面幅に基づいて自動判定(JavaScript使用)
42
+ st.session_state.mobile_mode = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ if "active_tab" not in st.session_state:
45
+ st.session_state.active_tab = "エディタ"
46
 
47
+ if "last_generated_code" not in st.session_state:
48
+ st.session_state.last_generated_code = None
 
49
 
50
+ if "ai_edit_mode" not in st.session_state:
51
+ st.session_state.ai_edit_mode = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ if "edit_instructions" not in st.session_state:
54
+ st.session_state.edit_instructions = ""
55
 
56
+ # 会話状態の初期化
57
+ if "conversation_state" not in st.session_state:
58
+ st.session_state.conversation_state = {
59
+ "topic": None,
60
+ "last_question_type": None,
61
+ "mentioned_entities": set(),
62
+ "follow_up_count": 0
63
+ }
64
+
65
+ # モバイルモードトグル
66
+ mobile_mode = st.checkbox("モバイルモード", value=st.session_state.mobile_mode, key="mobile_toggle")
67
+ st.session_state.mobile_mode = mobile_mode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  # APIキーがない場合の警告表示
70
  if not api_key:
71
  st.warning("""
72
  ### ⚠️ GROQ_API_KEYが設定されていません
73
 
74
+ このアプリを使用するには、GROQ_API_KEYを設定する必要があります:
75
 
76
+ 1. Hugging Face Spacesの場合: Spacesのダッシュボード -> Settings -> Repository secrets
77
+ 2. ローカル環境の場合: .streamlit/secrets.toml または環境変数
78
+ """)
79
+ st.stop()
80
+
81
+ # モバイルモードの場合はタブで切り替え、デスクトップモードの場合は並べて表示
82
+ if st.session_state.mobile_mode:
83
+ # サイドバーには最小限の設定のみを表示
84
+ with st.sidebar:
85
+ setup_config(compact=True)
86
+
87
+ # タブで画面を切り替える
88
+ tab1, tab2 = st.tabs(["エディタ", "AIチャット"])
89
+
90
+ # タブの選択状態を保存
91
+ if tab1.button("エディタモード", use_container_width=True):
92
+ st.session_state.active_tab = "エディタ"
93
+ st.rerun()
94
+
95
+ if tab2.button("チャットモード", use_container_width=True):
96
+ st.session_state.active_tab = "チャット"
97
+ st.rerun()
98
+
99
+ # アクティブなタブに応じてコンテンツを表示
100
+ if st.session_state.active_tab == "エディタ":
101
+ with tab1:
102
+ setup_editor(api_key=api_key, mobile=True)
103
+ with tab2:
104
+ st.info("エディタモードがアクティブです。チャットモードに切り替えてください。")
105
+ else:
106
+ with tab1:
107
+ st.info("チャットモードがアクティブです。エディタモードに切り替えてください。")
108
+ with tab2:
109
+ setup_chat(api_key=api_key, mobile=True)
110
+ else:
111
+ # サイドバー設定
112
+ with st.sidebar:
113
+ setup_config(compact=False)
114
+
115
+ # コードエディタとチャットを並べて表示
116
+ col1, col2 = st.columns([1, 1])
117
+
118
+ with col1:
119
+ setup_editor(api_key=api_key, mobile=False)
120
+
121
+ with col2:
122
+ setup_chat(api_key=api_key, mobile=False)