Spaces:
Paused
Paused
File size: 18,915 Bytes
634b5dc |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# app.py
import streamlit as st
import datetime
import os
# Import all necessary modules from your project
from config import GOOGLE_API_KEY, STUDENT_DB_PATH, CHROMA_DB_PATH, RAG_COLLECTION_NAME
from db_manager import init_student_db, get_all_student_names, get_student_characteristics, add_or_update_student
from rag_manager import add_documents_to_rag, query_rag # get_all_student_observations_from_rag is used by chat_processor
from chat_processor import extract_info_from_chat, update_student_characteristics_from_rag, batch_update_all_students_characteristics
from feedback_generator import (
generate_boss_feedback,
generate_public_feedback,
generate_parent_feedback,
get_events_summary_for_day
)
# import prompts # Prompts are used by other modules, not directly here typically
# --- Page Configuration and Initialization ---
st.set_page_config(page_title="晚托反馈助手", layout="wide", initial_sidebar_state="expanded")
# --- Check API Key ---
# On Hugging Face, this will be set via Secrets. For local, from .env or environment.
if not GOOGLE_API_KEY:
st.error("错误:GOOGLE_API_KEY 未配置。请在Hugging Face Space的Secrets中设置该值,或在本地的.env文件中配置。应用功能将受限。")
# st.stop() # Option to stop app, or let it run with limited functionality
# --- Initialize Databases (Idempotent) ---
# These functions now have internal error handling and directory creation
init_student_db() # For SQLite
# ChromaDB is initialized within rag_manager.py upon import/first use.
# --- Session State Management ---
# Helps persist data across Streamlit reruns
if 'processed_chat_extracts' not in st.session_state: # Renamed for clarity
st.session_state.processed_chat_extracts = [] # Stores list of {"student_name": ..., "observation": ...}
if 'current_processing_date' not in st.session_state: # Renamed
st.session_state.current_processing_date = datetime.date.today()
if 'student_list_cache' not in st.session_state: # Renamed
st.session_state.student_list_cache = get_all_student_names() # Initial load
# Feedback text states
if 'feedback_boss_text' not in st.session_state: st.session_state.feedback_boss_text = ""
if 'feedback_public_text' not in st.session_state: st.session_state.feedback_public_text = ""
if 'feedback_parent_text' not in st.session_state: st.session_state.feedback_parent_text = ""
if 'selected_student_for_parent_fb' not in st.session_state: st.session_state.selected_student_for_parent_fb = None
# --- Helper Functions for UI ---
def refresh_student_list_cache():
st.session_state.student_list_cache = get_all_student_names()
st.toast("学生列表已刷新。")
# --- Main Application UI ---
st.title("🚀 晚托反馈自动化助手")
# Sidebar for navigation and info
with st.sidebar:
st.header("导航")
menu_options = ["处理聊天记录", "生成反馈报告", "学生特点管理"]
choice = st.radio("选择功能:", menu_options, key="nav_menu")
st.markdown("---")
st.subheader("系统状态")
if GOOGLE_API_KEY:
st.success("Gemini API Key 已加载。")
else:
st.warning("Gemini API Key 未配置。")
# Simple check if DB files exist (more robust checks are within db/rag managers)
# These paths are inside the container / HF Space file system
student_db_exists = os.path.exists(STUDENT_DB_PATH)
chroma_dir_exists = os.path.exists(CHROMA_DB_PATH) and os.listdir(CHROMA_DB_PATH) # Check if dir exists and is not empty
if student_db_exists: st.markdown(f"✔️ 学生库: `{STUDENT_DB_PATH}`")
else: st.markdown(f"⚠️ 学生库未找到: `{STUDENT_DB_PATH}`")
if chroma_dir_exists: st.markdown(f"✔️ RAG库: `{CHROMA_DB_PATH}` (集合: {RAG_COLLECTION_NAME})")
else: st.markdown(f"⚠️ RAG库未找到: `{CHROMA_DB_PATH}`")
if st.button("🔄 刷新学生列表", key="sidebar_refresh_students"):
refresh_student_list_cache()
# --- Page 1: 处理聊天记录 ---
if choice == "处理聊天记录":
st.header("💬 聊天记录处理与数据构建")
st.markdown("在此粘贴每日微信聊天记录,AI将提取关键信息并存入知识库。")
# Date selection for the chat log
selected_date_for_processing = st.date_input(
"请选择聊天记录对应的日期",
value=st.session_state.current_processing_date, # Use session state for persistence
key="chat_date_input"
)
# Update session state if date changes
if selected_date_for_processing != st.session_state.current_processing_date:
st.session_state.current_processing_date = selected_date_for_processing
st.session_state.processed_chat_extracts = [] # Clear old extracts if date changes
st.experimental_rerun()
chat_log_text = st.text_area("在此粘贴聊天记录内容:", height=250, key="chat_log_input_area",
help="输入聊天内容后,点击“分析聊天记录”。")
if st.button("🤖 使用AI分析聊天记录", type="primary", key="analyze_chat_button"):
if not chat_log_text.strip():
st.warning("请输入聊天记录内容。")
elif not GOOGLE_API_KEY:
st.error("API Key未配置,无法分析。")
else:
with st.spinner("AI正在分析聊天记录,提取信息中..."):
st.session_state.processed_chat_extracts = extract_info_from_chat(chat_log_text)
if st.session_state.processed_chat_extracts:
st.success(f"AI成功提取到 {len(st.session_state.processed_chat_extracts)} 条信息!")
else:
st.info("AI分析完成,但未能从聊天记录中提取到格式化信息。")
# No st.error here as extract_info_from_chat might return empty on purpose
if st.session_state.processed_chat_extracts:
st.subheader("提取到的信息预览:")
preview_container = st.container()
with preview_container:
for item in st.session_state.processed_chat_extracts:
st.markdown(f"- **{item.get('student_name', 'N/A')}**: {item.get('observation', 'N/A')}")
st.markdown("---")
if st.button("➕ 确认并存入数据库和RAG知识库", key="store_extracted_data_button"):
with st.spinner("正在存储数据到RAG和学生数据库..."):
docs_to_rag = []
metadatas_to_rag = []
ids_to_rag = [] # RAG manager now generates robust IDs if None
processed_student_names_today = set()
date_str = st.session_state.current_processing_date.strftime("%Y-%m-%d")
for item_idx, item in enumerate(st.session_state.processed_chat_extracts):
s_name = item.get("student_name")
obs = item.get("observation")
if not s_name or not obs:
st.warning(f"跳过不完整的提取项: {item}")
continue
docs_to_rag.append(f"{s_name} 在 {date_str} 的表现: {obs}")
metadatas_to_rag.append({"student_name": str(s_name), "date": str(date_str), "source": "chat_log"})
# Let rag_manager handle ID generation if not provided or use robust ones here
# ids_to_rag.append(f"chat_{date_str.replace('-','')}_{str(s_name).replace(' ','_')}_{item_idx}")
add_or_update_student(s_name) # Ensure student exists in DB
processed_student_names_today.add(s_name)
storage_successful = False
if docs_to_rag:
if add_documents_to_rag(docs_to_rag, metadatas_to_rag, ids_to_rag): # ids can be None
storage_successful = True
else:
st.info("没有有效的提取信息可供存储。")
if storage_successful:
st.success(f"成功将 {len(docs_to_rag)} 条信息存入RAG。学生列表已更新。")
refresh_student_list_cache()
# Optionally trigger characteristics update for these students
if processed_student_names_today:
st.info("数据已存储。您可以前往“学生特点管理”页面更新这些学生的特点总结。")
st.session_state.processed_chat_extracts = [] # Clear after storing
st.experimental_rerun() # Rerun to clear preview and update UI
elif docs_to_rag: # If docs were there but storage failed
st.error("数据存入RAG失败。请检查日志。")
# --- Page 2: 生成反馈报告 ---
elif choice == "生成反馈报告":
st.header("📝 生成每日反馈报告")
st.markdown("根据已处理的信息或学生特点,选择不同模式生成反馈。")
feedback_target_date = st.date_input(
"请选择生成反馈对应的日期",
value=st.session_state.current_processing_date,
key="feedback_date_selector"
)
feedback_date_str = feedback_target_date.strftime("%Y-%m-%d")
# Determine summary for Boss/Public feedback
# Use extracts if date matches and extracts exist, otherwise query RAG
daily_summary_for_general_feedback = ""
processed_extracts_for_feedback_date = []
if feedback_target_date == st.session_state.current_processing_date and st.session_state.processed_chat_extracts:
processed_extracts_for_feedback_date = st.session_state.processed_chat_extracts
st.info(f"将使用为 {feedback_date_str} 刚处理的聊天记录生成反馈。")
temp_summary_parts = []
for item in processed_extracts_for_feedback_date:
temp_summary_parts.append(f"- {item.get('student_name', 'N/A')}: {item.get('observation', 'N/A')}")
if temp_summary_parts:
daily_summary_for_general_feedback = "\n".join(temp_summary_parts)
else:
daily_summary_for_general_feedback = get_events_summary_for_day(feedback_date_str) # Fallback
else:
with st.spinner(f"正在为日期 {feedback_date_str} 从知识库获取信息摘要..."):
daily_summary_for_general_feedback = get_events_summary_for_day(feedback_date_str)
st.markdown("---")
col1, col2 = st.columns(2)
with col1:
st.subheader("👔 给老板的反馈")
if st.button("生成老板反馈", key="generate_boss_fb"):
if not GOOGLE_API_KEY: st.error("API Key未配置。"); st.stop()
with st.spinner("正在生成老板反馈..."):
st.session_state.feedback_boss_text = generate_boss_feedback(daily_summary_for_general_feedback)
if st.session_state.feedback_boss_text: st.success("老板反馈生成成功!")
else: st.error("生成老板反馈失败或无内容返回。")
if st.session_state.feedback_boss_text:
st.text_area("老板反馈内容:", value=st.session_state.feedback_boss_text, height=200, key="boss_feedback_display")
with col2:
st.subheader("📢 公共反馈")
if st.button("生成公共反馈", key="generate_public_fb"):
if not GOOGLE_API_KEY: st.error("API Key未配置。"); st.stop()
with st.spinner("正在生成公共反馈..."):
st.session_state.feedback_public_text = generate_public_feedback(daily_summary_for_general_feedback)
if st.session_state.feedback_public_text: st.success("公共反馈生成成功!")
else: st.error("生成公共反馈失败或无内容返回。")
if st.session_state.feedback_public_text:
st.text_area("公共反馈内容:", value=st.session_state.feedback_public_text, height=200, key="public_feedback_display")
st.markdown("---")
st.subheader("👨👩👧👦 给家长的反馈")
if not st.session_state.student_list_cache:
st.warning("学生列表为空。请先通过“处理聊天记录”功能添加学生并处理数据。")
else:
st.session_state.selected_student_for_parent_fb = st.selectbox(
"选择学生:",
options=[""] + st.session_state.student_list_cache, # Add empty option for placeholder
index=0, # Default to empty
format_func=lambda x: "请选择..." if x == "" else x,
key="parent_feedback_student_selector"
)
feedback_modes_map = {
"正常模式 (基于当日记录)": "normal",
"偷懒模式 (组合历史事件)": "lazy",
"LLM特点生成 (创意发挥)": "llm_direct"
}
selected_mode_display_name = st.radio(
"选择反馈模式:",
options=list(feedback_modes_map.keys()),
key="parent_feedback_mode_selector"
)
mode_value = feedback_modes_map[selected_mode_display_name]
if st.button(f"为选定学生生成家长反馈", key="generate_parent_fb"):
if not GOOGLE_API_KEY: st.error("API Key未配置。"); st.stop()
if not st.session_state.selected_student_for_parent_fb:
st.warning("请先选择一个学生。")
else:
student_name = st.session_state.selected_student_for_parent_fb
with st.spinner(f"正在为 {student_name} ({selected_mode_display_name}) 生成家长反馈..."):
# Pass today's extracted data for the student if available (for "normal" mode)
student_specific_extracts_today = []
if feedback_target_date == st.session_state.current_processing_date and st.session_state.processed_chat_extracts:
student_specific_extracts_today = [
item for item in st.session_state.processed_chat_extracts if item.get("student_name") == student_name
]
st.session_state.feedback_parent_text = generate_parent_feedback(
student_name,
mode_value,
feedback_date_str,
student_specific_extracts_today # Pass specific extracts for normal mode
)
if st.session_state.feedback_parent_text:
st.success(f"为 {student_name} 生成家长反馈成功!")
else:
st.error(f"为 {student_name} 生成家长反馈失败或无内容返回。")
if st.session_state.feedback_parent_text and st.session_state.selected_student_for_parent_fb:
st.text_area(
f"给 {st.session_state.selected_student_for_parent_fb} 家长的反馈:",
value=st.session_state.feedback_parent_text,
height=300,
key="parent_feedback_display"
)
# --- Page 3: 学生特点管理 ---
elif choice == "学生特点管理":
st.header("🧑🎓 学生特点数据库管理")
st.markdown("查看和更新AI总结的学生特点。特点会基于RAG中的历史记录生成。")
if st.button("🔄 强制刷新学生列表和显示", key="admin_refresh_students_btn"):
refresh_student_list_cache()
st.experimental_rerun()
if not st.session_state.student_list_cache:
st.info("当前没有学生数据。请先通过“处理聊天记录”功能添加并存储学生相关信息。")
else:
st.subheader("当前学生列表及特点:")
num_students = len(st.session_state.student_list_cache)
cols_per_row = 3 # Adjust number of columns for display
for i in range(0, num_students, cols_per_row):
cols = st.columns(cols_per_row)
for j in range(cols_per_row):
student_idx = i + j
if student_idx < num_students:
student_name = st.session_state.student_list_cache[student_idx]
with cols[j]:
with st.expander(f"{student_name}", expanded=False):
characteristics = get_student_characteristics(student_name)
st.markdown(f"**AI总结特点:**\n {characteristics if characteristics else '暂无总结。'}")
if st.button(f"更新 {student_name} 特点", key=f"update_char_{student_name}_{student_idx}"):
if not GOOGLE_API_KEY: st.error("API Key未配置。"); st.stop()
with st.spinner(f"正在为 {student_name} 更新特点..."):
update_student_characteristics_from_rag(student_name)
st.success(f"{student_name} 的特点已更新!请重新展开查看。")
st.experimental_rerun() # Rerun to reflect changes
st.markdown("---")
st.subheader("批量操作")
if st.button("✨ 批量更新所有学生的特点总结", key="batch_update_all_chars_btn"):
if not GOOGLE_API_KEY: st.error("API Key未配置。"); st.stop()
if not st.session_state.student_list_cache:
st.warning("没有学生可供批量更新。")
else:
# Confirmation dialog for safety
# Using a more explicit confirmation
placeholder = st.empty()
with placeholder.container():
st.warning(f"此操作将为数据库中所有 {len(st.session_state.student_list_cache)} 位学生重新生成特点总结,可能需要较长时间并消耗API额度。")
if st.button("我确认执行批量更新", key="confirm_batch_update"):
placeholder.empty() # Remove confirmation message
with st.spinner("正在批量更新所有学生特点,请耐心等待..."):
batch_update_all_students_characteristics() # This function has internal st.progress
st.success("所有学生特点总结批量更新完毕!")
st.experimental_rerun()
elif st.button("取消批量更新", key="cancel_batch_update"):
placeholder.empty()
st.info("批量更新已取消。")
# --- Footer ---
st.markdown("---")
st.markdown("晚托反馈助手 v1.0.0 (HF Dockerized) | 技术支持: Gemini LLM + RAG")
|