Groxy2 / app.py
Yasu777's picture
Update app.py
a8ffc25 verified
raw
history blame
45.8 kB
import streamlit as st
import os
import re
import subprocess
import tempfile
import json
from groq import Groq
from prompts import load_system_prompt, create_default_prompt_files
from streamlit_ace import st_ace
# デフォルトのプロンプトファイルを作成
create_default_prompt_files()
# Streamlitアプリのセットアップ
st.set_page_config(
page_title="コーディングアシスタント & エディタ",
page_icon="💻",
layout="wide"
)
# セッション状態の初期化
if "files" not in st.session_state:
st.session_state.files = {} # ファイル名: コンテンツ
if "current_file" not in st.session_state:
st.session_state.current_file = None
# APIキーの取得(Hugging Face Spaces環境変数から)
api_key = os.getenv("GROQ_API_KEY", "")
# 使用可能なモデルのリスト
MODELS = {
"mistral_saba": "mistral-saba-24b",
"qwen_coder": "qwen-2.5-coder-32b",
"deepseek": "deepseek-r1-distill-llama-70b"
}
# 動作モードの定義
MODES = ["通常モード", "実装モード", "エラー修正モード"]
# サイドバー - モード選択とファイル管理
with st.sidebar:
st.title("コーディングアシスタント & エディタ 💻")
# モード選択タブ
selected_mode = st.radio(
"モード選択",
MODES
)
# モード説明の表示
mode_descriptions = {
"通常モード": "Mistral Sabaがユーザー入力を分析し、必要に応じてDeepSeekの知識も活用した上で、Qwen Coderが回答を生成します。",
"実装モード": "ユーザー入力の分析に基づき、Qwen Coderが設計書を作成した後、実装コードを提供します。",
"エラー修正モード": "提示されたコードのエラーを分析し、Qwen Coderが修正案を提供します。"
}
st.markdown(f"**{selected_mode}**: {mode_descriptions[selected_mode]}")
# ファイル管理セクション
st.header("ファイル管理")
# 新規ファイル作成
new_file_name = st.text_input("新規ファイル名", value="", key="new_file")
new_file_col1, new_file_col2 = st.columns([3, 1])
with new_file_col1:
file_extension = st.selectbox(
"拡張子",
options=[".py", ".js", ".html", ".css", ".json", ".txt"]
)
with new_file_col2:
if st.button("作成", key="create_file_btn"):
full_filename = new_file_name + file_extension
if full_filename not in st.session_state.files and new_file_name:
st.session_state.files[full_filename] = ""
st.session_state.current_file = full_filename
st.success(f"ファイル '{full_filename}' を作成しました")
st.rerun()
elif not new_file_name:
st.error("ファイル名を入力してください")
else:
st.error(f"ファイル '{full_filename}' は既に存在します")
# 既存ファイル一覧
if st.session_state.files:
st.subheader("既存ファイル")
for filename in st.session_state.files.keys():
col1, col2, col3 = st.columns([3, 1, 1])
with col1:
if st.button(filename, key=f"open_{filename}"):
st.session_state.current_file = filename
st.rerun()
with col2:
if st.button("削除", key=f"delete_{filename}"):
del st.session_state.files[filename]
if st.session_state.current_file == filename:
st.session_state.current_file = None
st.rerun()
with col3:
if st.button("コピー", key=f"copy_{filename}"):
name, ext = os.path.splitext(filename)
new_name = f"{name}_copy{ext}"
counter = 1
while new_name in st.session_state.files:
new_name = f"{name}_copy_{counter}{ext}"
counter += 1
st.session_state.files[new_name] = st.session_state.files[filename]
st.rerun()
# チャット履歴のクリア機能
if st.button("会話履歴をクリア"):
st.session_state.messages = []
st.session_state.thinking_process = []
st.session_state.is_first_message = True
st.session_state.conversation_state = {
"topic": None,
"last_question_type": None,
"mentioned_entities": set(),
"follow_up_count": 0
}
st.rerun()
# 接続情報と使い方のガイド
st.sidebar.markdown("---")
st.subheader("使い方ガイド")
with st.expander("アプリの機能", expanded=True):
st.markdown("""
### 主な機能
1. **コードエディタ** - 複数のプログラミング言語に対応したシンタックスハイライト付きエディタ
2. **Pythonコード実行** - エディタ上のPythonコードをその場で実行
3. **AIアシスタント** - コーディングに関する質問や実装の提案
4. **ファイル管理** - 複数のファイルの作成・保存・編集
5. **エラー修正** - コードのエラーを自動で診断・修正
### 特殊コマンド
- `/mode 通常` - 通常の質問応答モードに切り替え
- `/mode 実装` - コード実装モードに切り替え
- `/mode エラー修正` - エラー修正モードに切り替え
""")
with st.expander("Hugging Face Spacesでの設定方法"):
st.markdown("""
### Hugging Face Spacesでの設定手順
1. **APIキーの設定**:
- Spacesのダッシュボードに移動
- Settings > Repository secrets
- 新しいシークレットを追加: キー名 `GROQ_API_KEY`、値に自分のGroq APIキーを入力
2. **依存関係の追加**:
- `requirements.txt` に以下を追加:
```
streamlit
groq
streamlit-ace
```
3. **ファイル構造**:
- `app.py`: メインアプリケーションファイル
- `prompts.py`: プロンプト管理用ファイル
- `prompts/`: プロンプトを保存するディレクトリ
""")
# メインエリアを2つのカラムに分割
code_col, chat_col = st.columns([1, 1])
# コードエディタカラム
with code_col:
st.header("コードエディタ")
if st.session_state.current_file:
# ファイルの拡張子に基づいて言語を設定
file_ext = os.path.splitext(st.session_state.current_file)[1].lower()
lang_map = {
".py": "python",
".js": "javascript",
".html": "html",
".css": "css",
".json": "json",
".txt": "text"
}
language = lang_map.get(file_ext, "python")
# エディタの表示
st.subheader(f"編集中: {st.session_state.current_file}")
code = st_ace(
value=st.session_state.files[st.session_state.current_file],
language=language,
theme="monokai",
keybinding="vscode",
font_size=14,
min_lines=20,
key=f"ace_editor_{st.session_state.current_file}"
)
# コードを保存
st.session_state.files[st.session_state.current_file] = code
# Python codeの場合、実行ボタンを表示
if language == "python":
if st.button("コードを実行", key="run_code_btn"):
with st.spinner("コードを実行中..."):
# 一時ファイルを作成してコードを実行
with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as tmp:
tmp.write(code.encode())
tmp_name = tmp.name
try:
# サブプロセスでPythonコードを実行
result = subprocess.run(
["python", tmp_name],
capture_output=True,
text=True,
timeout=10 # 10秒のタイムアウト
)
# 実行結果を表示
if result.stdout:
st.code(result.stdout, language="text")
if result.stderr:
st.error(result.stderr)
# エラーが発生した場合、自動的にエラー修正モードに切り替える
if st.button("エラー修正AIに相談", key="ask_error_fix"):
st.session_state.current_mode = "エラー修正モード"
error_query = f"次のPythonコードにエラーがあります。修正方法を教えてください。\n\n```python\n{code}\n```\n\nエラーメッセージ:\n```\n{result.stderr}\n```"
# チャットにエラー修正用のメッセージを追加
if "messages" not in st.session_state:
st.session_state.messages = []
st.session_state.messages.append({"role": "user", "content": error_query})
st.rerun()
except subprocess.TimeoutExpired:
st.error("実行がタイムアウトしました(10秒以上かかりました)")
except Exception as e:
st.error(f"実行中にエラーが発生しました: {str(e)}")
finally:
# 一時ファイルを削除
if os.path.exists(tmp_name):
os.unlink(tmp_name)
# ファイルエクスポート機能
st.download_button(
label="ファイルをダウンロード",
data=code,
file_name=st.session_state.current_file,
key="download_file"
)
else:
st.info("左のサイドバーから既存のファイルを選択するか、新しいファイルを作成してください。")
# チャットカラム
with chat_col:
st.header("AIアシスタント")
# セッション初期化
if "messages" not in st.session_state:
st.session_state.messages = []
if "thinking_process" not in st.session_state:
st.session_state.thinking_process = []
if "current_mode" not in st.session_state:
st.session_state.current_mode = selected_mode
if "is_first_message" not in st.session_state:
st.session_state.is_first_message = True
# 会話状態の初期化
if "conversation_state" not in st.session_state:
st.session_state.conversation_state = {
"topic": None, # 会話の主要トピック
"last_question_type": None, # 前回の質問のタイプ
"mentioned_entities": set(), # 会話で言及されたエンティティ
"follow_up_count": 0 # フォローアップ質問の数
}
# モード変更の検出
if st.session_state.current_mode != selected_mode:
st.session_state.current_mode = selected_mode
st.session_state.is_first_message = True
# モード変更時にこれまでのチャットを要約
if len(st.session_state.messages) > 0:
with st.spinner("前回の会話内容を要約しています..."):
try:
# チャット履歴を文字列にまとめる
chat_history = ""
for msg in st.session_state.messages:
role = "ユーザー" if msg["role"] == "user" else "アシスタント"
chat_history += f"{role}: {msg['content']}\n\n"
# 要約を生成
client = Groq(api_key=api_key)
summarize_prompt = f"""
以下はユーザーとアシスタントの間の会話です。この会話を300単語以内で要約してください。
特に重要な内容、技術的な詳細、解決した問題に焦点を当ててください。
{chat_history}
"""
# Mistral Sabaモデルのシステムプロンプトをロード
summary_system_prompt = load_system_prompt(MODELS["mistral_saba"])
summary_response = client.chat.completions.create(
model=MODELS["mistral_saba"],
messages=[
{"role": "system", "content": summary_system_prompt},
{"role": "user", "content": summarize_prompt}
],
temperature=0.3,
max_tokens=1000
)
summary = summary_response.choices[0].message.content
st.session_state.chat_summary = summary
# 要約をthinking_processに追加
st.session_state.thinking_process.append({
"title": "前回の会話要約",
"content": summary
})
# ユーザーに要約を通知
st.info(f"モードが変更されました。前回の会話の要約:\n\n{summary}")
except Exception as e:
st.error(f"会話要約の生成中にエラーが発生しました: {str(e)}")
# 物理学関連のキーワード
PHYSICS_KEYWORDS = [
"物理", "力学", "電磁気", "熱力学", "量子", "相対性", "波動", "力", "運動", "エネルギー",
"physics", "mechanics", "electromagnetism", "thermodynamics", "quantum", "relativity",
"wave", "force", "motion", "energy", "シミュレーション", "simulation", "重力", "gravity",
"加速度", "acceleration", "速度", "velocity", "衝突", "collision", "反発", "rebound",
"摩擦", "friction", "剛体", "rigid body", "粒子", "particle", "流体", "fluid",
"弾性", "elastic", "軌道", "trajectory", "バネ", "spring", "圧力", "pressure"
]
# テキストが物理学に関連しているかチェック
def is_physics_related(text):
lower_text = text.lower()
# 明確な物理関連キーワード(より具体的で誤検出が少ないもの)
explicit_physics_keywords = [
"物理法則", "物理シミュレーション", "ニュートン力学", "電磁気学", "熱力学", "量子力学", "相対性理論",
"physics simulation", "newton's laws", "electromagnetism", "thermodynamics",
"quantum mechanics", "relativity theory"
]
# 単語境界を考慮すべきキーワード(一般的な単語だが、単独で使われた場合は物理関連の可能性が高い)
word_boundary_keywords = [
r"\b力学\b", r"\b重力\b", r"\b摩擦\b", r"\b衝突\b", r"\b運動方程式\b", r"\b加速度\b", r"\b速度\b", r"\b質量\b",
r"\bgravity\b", r"\bfriction\b", r"\bcollision\b", r"\bvelocity\b", r"\bacceleration\b", r"\bmass\b"
]
# 明確な物理用語が含まれているか
explicit_match = any(keyword.lower() in lower_text for keyword in explicit_physics_keywords)
# 境界を考慮すべき単語が含まれているか(単語として独立して存在する場合のみ)
boundary_match = any(re.search(pattern, lower_text) for pattern in word_boundary_keywords)
# 物理シミュレーション関連の特定のフレーズパターン
physics_phrases = [
r"物理.*シミュレ(ーション|ート)",
r"physics.*simulation",
r"ボールの.*衝突",
r"ball.*collision",
r"重力.*計算",
r"gravity.*calculation",
r"運動方程式",
r"equation.*motion",
r"力学.*モデル",
r"mechanics.*model"
]
phrase_match = any(re.search(pattern, lower_text) for pattern in physics_phrases)
# 誤検出しやすいコンテキストのチェック(これらが含まれる場合は物理関連ではない可能性が高い)
non_physics_contexts = [
"アプリの動作", "システムの動作", "プログラムの動作", "ソフトウェアの動作",
"app behavior", "system behavior", "program behavior", "software behavior",
"UIの動き", "インターフェースの動き", "ボタンの動き",
"UI movement", "interface movement", "button movement"
]
negative_context = any(context.lower() in lower_text for context in non_physics_contexts)
# 物理関連であると判断するロジック:
# 1. 明確な物理用語が含まれている、または
# 2. 境界を考慮すべき単語が含まれていて、かつ誤検出コンテキストが含まれていない、または
# 3. 物理シミュレーション関連の特定のフレーズが含まれている
return explicit_match or (boundary_match and not negative_context) or phrase_match
# テキスト内にコードブロックが含まれているかを確認
def contains_code_block(text):
# マークダウンのコードブロックパターン
code_block_pattern = r"```[\w]*\n[\s\S]*?\n```"
return bool(re.search(code_block_pattern, text))
# テキストからすべてのコードブロックを抽出する
def extract_all_code_blocks(text):
"""テキストから全てのコードブロックを抽出する"""
code_block_pattern = r"```(\w+)?\n([\s\S]*?)\n```"
matches = re.findall(code_block_pattern, text)
if not matches:
return []
code_blocks = []
for lang, code in matches:
lang = lang or "python" # 言語指定がない場合はPythonと仮定
code_blocks.append(f"```{lang}\n{code}\n```")
return code_blocks
# 生成されたコードを新しいファイルに保存する関数
def save_generated_code_to_new_file(code_text, suggested_name=None):
# コードブロックのマークアップを削除
code_pattern = r"```(?:\w+)?\n([\s\S]*?)\n```"
match = re.search(code_pattern, code_text)
clean_code = match.group(1) if match else code_text
# 言語の推測
language = "py" # デフォルトはPython
lang_pattern = r"```(\w+)\n"
lang_match = re.search(lang_pattern, code_text)
if lang_match:
detected_lang = lang_match.group(1).lower()
lang_mapping = {
"python": "py",
"javascript": "js",
"html": "html",
"css": "css",
"json": "json",
"java": "java",
"cpp": "cpp",
"c++": "cpp",
"c": "c",
"go": "go",
"rust": "rs",
"typescript": "ts"
}
language = lang_mapping.get(detected_lang, detected_lang)
# ファイル名の生成
if not suggested_name:
import hashlib
import time
# タイムスタンプとコードの一部からハッシュを生成
timestamp = str(int(time.time()))
code_hash = hashlib.md5(clean_code[:100].encode()).hexdigest()[:6]
suggested_name = f"generated_{timestamp}_{code_hash}"
# 拡張子が含まれていなければ追加
if "." not in suggested_name:
suggested_name = f"{suggested_name}.{language}"
# 同名ファイルが存在する場合は連番を付ける
base_name, ext = os.path.splitext(suggested_name)
counter = 1
file_name = suggested_name
while file_name in st.session_state.files:
file_name = f"{base_name}_{counter}{ext}"
counter += 1
# ファイルを保存
st.session_state.files[file_name] = clean_code
st.session_state.current_file = file_name
return file_name
# 過去のメッセージを表示
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# 思考プロセスの表示(開閉可能なエクスパンダー)
if st.session_state.thinking_process:
with st.expander("思考プロセスを表示"):
for i, thought in enumerate(st.session_state.thinking_process):
st.subheader(thought["title"])
st.markdown(thought["content"])
st.divider()
# 会話状態を更新する関数
def update_conversation_state(user_input, ai_response):
# 主要トピックの更新(最初のメッセージから)
if st.session_state.conversation_state["topic"] is None and user_input:
# 簡易的なトピック抽出(最初の文か最初の30単語)
first_sentence = user_input.split('.')[0]
topic = first_sentence[:100] + "..." if len(first_sentence) > 100 else first_sentence
st.session_state.conversation_state["topic"] = topic
# フォローアップカウントの更新
if "?" in user_input or "?" in user_input:
st.session_state.conversation_state["follow_up_count"] += 1
# その他の状態更新ロジックをここに追加
pass
# APIを使用した思考プロセスと回答の生成
def generate_response(user_input):
client = Groq(api_key=api_key)
thinking_results = []
# ユーザー入力が物理関連かどうかをチェック
is_physics = is_physics_related(user_input)
# Step 1: Mistral Sabaによる入力分析
mistral_system_prompt = load_system_prompt(MODELS["mistral_saba"])
mistral_prompt = f"""
以下のユーザー入力を分析してください。ユーザーの質問や要求の本質、重要なキーワード、技術的課題を抽出し、
どのようなアプローチで回答すべきかを詳細に検討してください。
ユーザー入力: {user_input}
"""
mistral_response = client.chat.completions.create(
model=MODELS["mistral_saba"],
messages=[
{"role": "system", "content": mistral_system_prompt},
{"role": "user", "content": mistral_prompt}
],
temperature=0.3,
max_tokens=2000
)
mistral_analysis = mistral_response.choices[0].message.content
thinking_results.append({
"title": "Mistral Sabaによる分析",
"content": mistral_analysis
})
# 物理関連の場合のみDeepSeekの意見を取得
deepseek_knowledge = ""
if is_physics:
deepseek_system_prompt = load_system_prompt(MODELS["deepseek"])
deepseek_prompt = f"""
以下のユーザー入力は物理学に関連しています。物理学の観点から、この質問や課題に対する
科学的知識、法則、公式、アプローチを提供してください。特に関連する物理概念と実装方法の
関連性があれば説明してください。
ユーザー入力: {user_input}
"""
deepseek_response = client.chat.completions.create(
model=MODELS["deepseek"],
messages=[
{"role": "system", "content": deepseek_system_prompt},
{"role": "user", "content": deepseek_prompt}
],
temperature=0.3,
max_tokens=2000
)
deepseek_knowledge = deepseek_response.choices[0].message.content
thinking_results.append({
"title": "DeepSeekによる物理学的知見",
"content": deepseek_knowledge
})
# Step 2: モード別の応答生成
if st.session_state.current_mode == "通常モード":
# 物理関連の場合のみDeepSeekの情報を含める
deepseek_section = ""
physics_consideration = ""
if is_physics:
physics_consideration = " DeepSeekの物理学的知見を考慮して"
deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
# 現在のエディタのコードを取得(コードエディタで何かが開かれている場合)
editor_code = ""
editor_reference = ""
if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
editor_code = st.session_state.files[st.session_state.current_file]
editor_reference = f"現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```"
# 通常モード用のシステムプロンプトをロード
system_prompt = load_system_prompt(MODELS["qwen_coder"], "通常モード")
qwen_prompt = f"""
以下のユーザー入力に対してコーディングアシスタントとして回答してください。
Mistral Sabaの分析結果{physics_consideration}を考慮して、
最適なソリューションを提供してください。
ユーザー入力: {user_input}
Mistral Sabaの分析:
{mistral_analysis}
"""
# 物理関連の場合のみDeepSeekの情報を追加
if is_physics:
qwen_prompt += f"\n\n{deepseek_section}"
# エディタのコードが存在する場合は参照として追加
if editor_code:
qwen_prompt += f"\n\n{editor_reference}\n\nエディタで開かれているコードに関して、ユーザーの質問に答えるか、必要に応じてコードの改善を提案してください。"
qwen_response = client.chat.completions.create(
model=MODELS["qwen_coder"],
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": qwen_prompt}
],
temperature=0.5,
max_tokens=4000
)
final_response = qwen_response.choices[0].message.content
elif st.session_state.current_mode == "実装モード":
# 物理関連の条件分岐を変数で処理
deepseek_section = ""
physics_note = ""
if is_physics:
deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
physics_note = "この実装には物理シミュレーションが含まれているため、物理法則の正確性と数値的安定性を特に重視してください。"
# Step 2a: 設計フェーズ - 専用のプロンプトを使用
design_system_prompt = load_system_prompt(MODELS["qwen_coder"], "実装モード_設計")
design_prompt = f"""
以下のユーザー入力と分析結果に基づいて、実装に必要な設計書を作成してください。
ユーザー入力: {user_input}
Mistral Sabaの分析:
{mistral_analysis}
"""
# 物理関連の場合のみDeepSeekの情報を追加
if is_physics:
design_prompt += f"\n\n{deepseek_section}"
design_response = client.chat.completions.create(
model=MODELS["qwen_coder"],
messages=[
{"role": "system", "content": design_system_prompt},
{"role": "user", "content": design_prompt}
],
temperature=0.4,
max_tokens=3000
)
design_doc = design_response.choices[0].message.content
thinking_results.append({
"title": "Qwen Coderによる設計書",
"content": design_doc
})
# Step 2b: 実装フェーズ - 専用のプロンプトを使用
implementation_system_prompt = load_system_prompt(MODELS["qwen_coder"], "実装モード_実装")
implementation_prompt = f"""
以下の設計書に基づいて、完全な実装コードを提供してください。
コードは実行可能で、エラー処理が適切に行われているものにしてください。
各コンポーネントには十分なコメントを付け、使用方法の例も含めてください。
{physics_note}
設計書:
{design_doc}
ユーザーの元々の要求:
{user_input}
"""
implementation_response = client.chat.completions.create(
model=MODELS["qwen_coder"],
messages=[
{"role": "system", "content": implementation_system_prompt},
{"role": "user", "content": implementation_prompt}
],
temperature=0.5,
max_tokens=4000
)
implementation_result = implementation_response.choices[0].message.content
# 設計書と実装を組み合わせた最終結果
final_response = f"# 設計書\n\n{design_doc}\n\n# 実装コード\n\n{implementation_result}"
# コードの自動抽出と保存機能
code_blocks = extract_all_code_blocks(implementation_result)
if code_blocks and len(code_blocks) > 0:
# 抽出したコードブロックに対してボタンを作成するための識別子を付加
final_response += "\n\n**実装されたコードをエディタに保存できます。チャット上の「コードを保存」ボタンをクリックしてください。**"
# 最初のコードブロックを抽出してセッション状態に保存
st.session_state.last_generated_code = code_blocks[0]
elif st.session_state.current_mode == "エラー修正モード":
# エディタのコードを取得(存在する場合)
editor_code = ""
editor_reference = ""
if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
editor_code = st.session_state.files[st.session_state.current_file]
editor_reference = f"現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```\n\nこのコードにエラーがある可能性があります。ユーザーからの情報と合わせて分析してください。"
# 物理関連の条件分岐を変数で処理
deepseek_section = ""
if is_physics:
deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
# エラー修正モード用のシステムプロンプトをロード
error_system_prompt = load_system_prompt(MODELS["qwen_coder"], "エラー修正モード")
error_prompt = f"""
以下のユーザー入力はコードのエラーに関するものです。
エラーの原因を特定し、修正案を提供してください。
説明では:
1. エラーの根本原因
2. 問題箇所の特定
3. 修正方法の詳細な説明
4. 修正されたコード
5. 同様のエラーを防ぐためのベストプラクティス
を含めてください。
ユーザー入力: {user_input}
Mistral Sabaの分析:
{mistral_analysis}
"""
# エディタのコードを追加
if editor_code:
error_prompt += f"\n\n{editor_reference}"
# 物理関連の場合のみDeepSeekの情報を追加
if is_physics:
error_prompt += f"\n\n{deepseek_section}"
error_response = client.chat.completions.create(
model=MODELS["qwen_coder"],
messages=[
{"role": "system", "content": error_system_prompt},
{"role": "user", "content": error_prompt}
],
temperature=0.4,
max_tokens=4000
)
final_response = error_response.choices[0].message.content
return final_response, thinking_results
# 最後に生成されたコードを保存するセッション変数の初期化
if "last_generated_code" not in st.session_state:
st.session_state.last_generated_code = None
# 「コードを保存」ボタンが押された場合の処理
if st.session_state.last_generated_code and st.button("AIが生成したコードをエディタに保存"):
with st.spinner("コードを保存中..."):
file_name = save_generated_code_to_new_file(st.session_state.last_generated_code)
st.success(f"コードを '{file_name}' として保存しました")
st.session_state.last_generated_code = None # 保存したのでクリア
st.rerun()
# ユーザー入力
if prompt := st.chat_input("質問を入力してください"):
# ユーザーメッセージを表示
st.chat_message("user").markdown(prompt)
# ユーザーメッセージをセッション状態に追加
st.session_state.messages.append({"role": "user", "content": prompt})
# モード切替コマンドを検出
if prompt.strip().startswith("/mode"):
parts = prompt.strip().split()
if len(parts) < 2:
response = "モード指定が不完全です。使用方法: /mode [通常|実装|エラー修正]"
else:
mode = parts[1].lower()
valid_modes = {"通常": "通常モード", "実装": "実装モード", "エラー修正": "エラー修正モード"}
if mode not in valid_modes and mode not in valid_modes.values():
response = f"無効なモードです。使用可能なモード: {', '.join(valid_modes.keys())}"
else:
old_mode = st.session_state.current_mode
if mode in valid_modes:
st.session_state.current_mode = valid_modes[mode]
else:
st.session_state.current_mode = mode
st.session_state.is_first_message = True
# モード変更時の説明メッセージを用意
mode_descriptions = {
"通常モード": "通常の質問応答モードに切り替えました。",
"実装モード": "アイデアから設計と実装を生成するモードに切り替えました。",
"エラー修正モード": "エラーメッセージから問題を診断・修正するモードに切り替えました。"
}
instructions = {
"通常モード": "質問や議論したいトピックを入力してください。",
"実装モード": "実装したい機能やアイデアを入力してください。",
"エラー修正モード": "発生したエラーメッセージとコードを入力してください。"
}
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]}"
# レスポンスを表示
st.chat_message("assistant").markdown(response)
# アシスタントメッセージをセッション状態に追加
st.session_state.messages.append({"role": "assistant", "content": response})
# 通常の質問処理
else:
# APIキーがある場合のみ実行
if api_key:
try:
# 最初のメッセージはチェーンを使用、それ以降はQwen Coderのみで応答
if st.session_state.is_first_message:
with st.spinner("回答を生成中...(複数のモデルによる分析を行っています)"):
response, thinking_results = generate_response(prompt)
st.session_state.thinking_process = thinking_results
st.session_state.is_first_message = False
else:
with st.spinner("回答を生成中..."):
# 直接Qwen Coderで応答
client = Groq(api_key=api_key)
# 現在のモードに合わせたシステムプロンプトをロード
current_system_prompt = load_system_prompt(MODELS["qwen_coder"], st.session_state.current_mode)
# 会話の連続性のための追加指示
current_system_prompt += """
重要: これは継続中の会話です。前の質問や回答を踏まえて、会話の自然な流れを維持してください。
ユーザーの最新の質問だけに集中するのではなく、会話全体の文脈を考慮してください。
繰り返しは避け、新しい情報や洞察を提供してください。
"""
# 過去の会話履歴を完全に構築
context = ""
if len(st.session_state.messages) > 2:
# 最近の会話履歴(最大トークン制限を考慮して適切な数に制限)
recent_messages = st.session_state.messages[-10:] # 最近の10メッセージまで
for msg in recent_messages:
role = "ユーザー" if msg["role"] == "user" else "アシスタント"
# 全文を含める
context += f"{role}: {msg['content']}\n\n"
# 現在のエディタのコードを取得(存在する場合)
editor_code = ""
editor_reference = ""
if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
editor_code = st.session_state.files[st.session_state.current_file]
editor_reference = f"\n\n現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```\n\nエディタで開かれているコードに関して、ユーザーの質問に答えるか、必要に応じてコードの改善を提案してください。"
qwen_prompt = f"""
あなたはユーザーと継続的な会話を行っているアシスタントです。
これまでの会話の流れを踏まえて、最新のユーザー入力に適切に応答してください。
これまでの会話:
{context}
最新のユーザー入力: {prompt}
前の会話の文脈を考慮して、この最新の質問に直接回答してください。
前回までの内容を繰り返すのではなく、会話を自然に進めてください。
"""
# エディタのコードが存在する場合は参照として追加
if editor_code:
qwen_prompt += editor_reference
qwen_response = client.chat.completions.create(
model=MODELS["qwen_coder"],
messages=[
{"role": "system", "content": current_system_prompt},
{"role": "user", "content": qwen_prompt}
],
temperature=0.5,
max_tokens=4000
)
response = qwen_response.choices[0].message.content
# レスポンスを表示
st.chat_message("assistant").markdown(response)
# アシスタントメッセージをセッション状態に追加
st.session_state.messages.append({"role": "assistant", "content": response})
# 会話状態を更新
update_conversation_state(prompt, response)
# モード切替提案を追加(UIには表示しない)
mode_suggestions = {
"通常モード": "\n\n---\n*実装を生成するには、「/mode 実装」と入力して実装モードに切り替えてください。*",
"実装モード": "\n\n---\n*このコードを検証・修正するには、「/mode エラー修正」と入力して検証修正モードに切り替えてください。*",
"エラー修正モード": "\n\n---\n*新しい実装を生成するには、「/mode 実装」と入力して実装モードに切り替えてください。*"
}
# 提案はセッションステートには保存するが、UIには表示しない(次回のcontext生成で使用)
suggestion = mode_suggestions.get(st.session_state.current_mode, "")
st.session_state.messages[-1]["content"] += suggestion
except Exception as e:
st.error(f"エラーが発生しました: {str(e)}")
else:
st.error("GROQ_API_KEYが設定されていません。Hugging Face Spacesの環境変数で設定してください。")
# APIキーがない場合の警告表示
if not api_key:
st.warning("""
### ⚠️ GROQ_API_KEYが設定されていません
このアプリを使用するには、Hugging Face Spacesの環境変数設定でGROQ_API_KEYを設定する必要があります:
1. Spacesのダッシュボードに移動
2. Settings > Repository secrets
3. 新しいシークレットを追加: キー名 `GROQ_API_KEY`、値に自分のGroq APIキーを入力
4. アプリを再起動
""")