Files
arindam200--awesome-ai-apps/memory_agents/study_coach_agent/app.py
T
2026-07-13 13:37:43 +08:00

483 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import base64
import json
import os
import streamlit as st
from dotenv import load_dotenv
from memory_utils import MemoriManager # type: ignore[unresolved-import]
from study_graph import ( # type: ignore[unresolved-import]
LearnerProfile,
StudyLog,
run_full_evaluation,
run_initial_verification,
)
load_dotenv()
st.set_page_config(
page_title="AI Study Coach with Memori",
layout="wide",
)
def _load_inline_image(path: str, height_px: int) -> str:
"""Return an inline <img> tag for a local PNG, or empty string on failure."""
try:
with open(path, "rb") as f:
encoded = base64.b64encode(f.read()).decode()
return (
f"<img src='data:image/png;base64,{encoded}' "
f"style='height:{height_px}px; width:auto; display:inline-block; "
f"vertical-align:middle; margin:0 8px;' alt='Logo'>"
)
except Exception:
return ""
# Branded title with Memori logo
memori_img_inline = _load_inline_image(
"assets/Memori_Logo.png",
height_px=85,
)
title_html = f"""
<div style='display:flex; align-items:center; width:120%; padding:8px 0;'>
<h1 style='margin:0; padding:0; font-size:2.2rem; font-weight:800; display:flex; align-items:center; gap:10px;'>
<span>Study Coach Agent with</span>
{memori_img_inline}
</h1>
</div>
"""
st.markdown(title_html, unsafe_allow_html=True)
@st.cache_resource
def get_memori_manager(openai_key: str, db_url: str | None) -> MemoriManager:
return MemoriManager(
openai_api_key=openai_key,
db_url=db_url,
)
def _ensure_state():
if "learner_profile" not in st.session_state:
st.session_state.learner_profile = None
if "quiz" not in st.session_state:
st.session_state.quiz = []
if "explanation_prompt" not in st.session_state:
st.session_state.explanation_prompt = ""
if "answers" not in st.session_state:
st.session_state.answers = []
if "explanation" not in st.session_state:
st.session_state.explanation = ""
if "last_result" not in st.session_state:
st.session_state.last_result = None
if "progress_messages" not in st.session_state:
st.session_state.progress_messages = []
def _maybe_restore_profile_from_memori(memori_mgr: MemoriManager) -> None:
"""
On fresh app loads (after refresh), try to reconstruct the learner profile
from Memori so the user doesn't have to re-enter it.
"""
if st.session_state.learner_profile is not None:
return
try:
system_prompt = (
"You are an AI study coach with access to a long-term memory store "
"about a single learner. Using that memory, reconstruct the most "
"recent learner profile that was described.\n\n"
"Respond ONLY with a JSON object with the following keys:\n"
' "name": string,\n'
' "main_goal": string,\n'
' "timeframe": string,\n'
' "subjects": list of strings,\n'
' "weekly_hours": integer,\n'
' "preferred_formats": list of strings.\n\n'
"If you truly have no stored information about the learner, respond "
"with an empty JSON object: {}"
)
response = memori_mgr.openai_client.chat.completions.create(
model="gpt-4o-mini",
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": "Return the last known learner profile now.",
},
],
)
raw = response.choices[0].message.content or "{}"
data = json.loads(raw)
if not isinstance(data, dict) or not data:
return
# Build LearnerProfile; if it fails, just ignore and keep requiring manual entry
profile = LearnerProfile(
name=str(data.get("name", "")).strip(),
main_goal=str(data.get("main_goal", "")).strip(),
timeframe=str(data.get("timeframe", "")).strip(),
subjects=[
str(s).strip() for s in data.get("subjects", []) if str(s).strip()
],
weekly_hours=int(data.get("weekly_hours", 1) or 1),
preferred_formats=[
str(f).strip()
for f in data.get("preferred_formats", [])
if str(f).strip()
],
)
# Basic sanity check: require at least a name and goal
if profile.name and profile.main_goal:
st.session_state.learner_profile = profile
except Exception:
# Fail silently; user can always re-enter profile if needed.
return
def sidebar_keys():
with st.sidebar:
st.subheader("🔑 API Keys")
openai_api_key_input = st.text_input(
"OpenAI API Key",
value=os.getenv("OPENAI_API_KEY", ""),
type="password",
)
memori_api_key_input = st.text_input(
"Memori API Key (optional)",
value=os.getenv("MEMORI_API_KEY", ""),
type="password",
help="Used for Memori Advanced Augmentation and higher quotas.",
)
db_url_input = st.text_input(
"CockroachDB URL",
value=os.getenv("MEMORI_DB_URL", ""),
help=(
"CockroachDB connection string using the Postgres+psycopg driver, e.g. "
"postgresql+psycopg://user:password@host:26257/database"
),
)
if st.button("Save Settings"):
if openai_api_key_input:
os.environ["OPENAI_API_KEY"] = openai_api_key_input
if memori_api_key_input:
os.environ["MEMORI_API_KEY"] = memori_api_key_input
if db_url_input:
os.environ["MEMORI_DB_URL"] = db_url_input
if openai_api_key_input or memori_api_key_input or db_url_input:
st.success("✅ Settings saved for this session")
else:
st.warning(
"Please enter at least an OpenAI API key and CockroachDB URL"
)
st.markdown("---")
st.markdown("### ️ About")
st.markdown(
"""
This is an **AI Study Coach** demo built for Memori:
- Plans and tracks study sessions.
- Uses **LangGraph** to verify understanding with quizzes + explanations.
- Uses **Memori v3** as long-term learning memory.
"""
)
def study_plan_tab(memori_mgr: MemoriManager):
st.markdown("#### 🧭 Study Plan & Learner Profile")
with st.form("profile_form"):
col1, col2 = st.columns(2)
with col1:
name = st.text_input("Name or handle", placeholder="e.g. 3rdSon")
main_goal = st.text_input(
"Main goal",
placeholder="e.g. Pass AWS SAA, master LangGraph, finish CS50",
)
timeframe = st.text_input(
"Timeframe",
placeholder="e.g. 3 months, 6 weeks",
)
with col2:
weekly_hours = st.number_input(
"Planned study hours per week", min_value=1, max_value=80, value=7
)
subjects = st.text_input(
"Subjects / topics (comma-separated)",
placeholder="e.g. LangGraph, Memori, algorithms",
)
preferred_formats = st.multiselect(
"Preferred learning formats",
options=[
"videos",
"docs",
"practice problems",
"flashcards",
"projects",
],
)
submitted = st.form_submit_button("Save Profile")
if submitted:
if not name or not main_goal or not timeframe:
st.error("Please fill in at least name, main goal, and timeframe.")
return
profile = LearnerProfile(
name=name.strip(),
main_goal=main_goal.strip(),
timeframe=timeframe.strip(),
subjects=[s.strip() for s in subjects.split(",") if s.strip()],
weekly_hours=weekly_hours,
preferred_formats=preferred_formats,
)
st.session_state.learner_profile = profile
# Log structured profile into Memori so it can be recalled later
try:
memori_mgr.log_learner_profile(profile.model_dump())
st.success("✅ Profile saved and stored in Memori.")
except Exception as e:
st.warning(f"Profile saved in session, but Memori logging failed: {e}")
if st.session_state.learner_profile:
p: LearnerProfile = st.session_state.learner_profile
st.markdown("##### Current Profile")
st.write(
{
"name": p.name,
"goal": p.main_goal,
"timeframe": p.timeframe,
"subjects": p.subjects,
"weekly_hours": p.weekly_hours,
"preferred_formats": p.preferred_formats,
}
)
def today_session_tab(memori_mgr: MemoriManager):
st.markdown("#### 📅 Todays Study Session")
profile: LearnerProfile | None = st.session_state.learner_profile
if not profile:
st.info("Set up your study profile first in the **Study Plan** tab.")
return
col1, col2 = st.columns(2)
with col1:
topic = st.text_input(
"What did you study today?", placeholder="e.g. LangGraph basics"
)
duration = st.number_input(
"How many minutes did you study?",
min_value=5,
max_value=600,
value=45,
)
resource_type = st.selectbox(
"Main resource type",
options=["video", "article", "course", "problems", "other"],
)
with col2:
perceived_difficulty = st.selectbox(
"How difficult was it?",
options=["easy", "medium", "hard"],
)
mood = st.text_input(
"How did you feel?",
placeholder="e.g. focused, tired, motivated, frustrated",
)
notes = st.text_area("Any additional notes?", height=80)
if st.button("Generate quiz & explanation check", type="primary"):
if not topic:
st.error("Please enter what you studied today.")
return
log = StudyLog(
topic=topic.strip(),
duration_minutes=duration,
resource_type=resource_type,
perceived_difficulty=perceived_difficulty,
mood=mood.strip() or None,
free_notes=notes.strip() or None,
)
try:
mgr = memori_mgr # alias
initial = run_initial_verification(
profile=profile, log=log, llm_client=mgr.openai_client
)
st.session_state.quiz = initial.quiz
st.session_state.explanation_prompt = initial.explanation_prompt
st.session_state.answers = ["" for _ in initial.quiz]
st.session_state.explanation = ""
st.session_state.current_log = log
except Exception as e:
st.error(f"Failed to generate quiz: {e}")
# Show quiz if available
quiz = st.session_state.quiz
if quiz:
st.markdown("##### 🧪 Quick Understanding Check")
new_answers: list[str] = []
for i, q in enumerate(quiz):
ans = st.text_area(
f"Q{i + 1}. {q.question}",
value=(
st.session_state.answers[i]
if i < len(st.session_state.answers)
else ""
),
height=80,
)
new_answers.append(ans)
st.session_state.answers = new_answers
st.markdown("##### ✍️ Explain in your own words")
st.session_state.explanation = st.text_area(
"Explanation",
value=st.session_state.explanation,
placeholder=st.session_state.explanation_prompt,
height=160,
)
if st.button("Evaluate my understanding", type="secondary"):
try:
mgr = memori_mgr
log: StudyLog = st.session_state.current_log
result = run_full_evaluation(
profile=profile,
log=log,
user_quiz_answers=st.session_state.answers,
user_explanation=st.session_state.explanation,
llm_client=mgr.openai_client,
)
st.session_state.last_result = result
# Log study session into Memori
summary = (
f"Study session summary:\n"
f"- Topic: {log.topic}\n"
f"- Duration (min): {log.duration_minutes}\n"
f"- Resource: {log.resource_type}\n"
f"- Difficulty: {log.perceived_difficulty}\n"
f"- Mood: {log.mood or 'N/A'}\n"
f"- Score: {result.score}\n"
f"- Feedback: {result.feedback or ''}\n"
f"- Next step: {result.next_step_recommendation or ''}"
)
mgr.log_study_session(summary)
except Exception as e:
st.error(f"Failed to evaluate and log session: {e}")
# Show last result
if st.session_state.last_result:
r = st.session_state.last_result
st.markdown("##### 🎯 Result")
if r.score is not None:
st.metric("Understanding score", f"{r.score}/100")
if r.feedback:
st.markdown("**Feedback**")
st.write(r.feedback)
if r.next_step_recommendation:
st.markdown("**Recommended next step**")
st.write(r.next_step_recommendation)
def progress_tab(memori_mgr: MemoriManager):
st.markdown("#### 📈 Progress & Memory (Memori-powered)")
st.markdown(
"Ask questions about your learning history, weak/strong topics, or patterns.\n\n"
"Examples:\n"
"- *What are my weakest topics right now?*\n"
"- *When do I usually perform best?*\n"
"- *Do I learn better from videos or practice problems?*"
)
# Display chat history
for message in st.session_state.progress_messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Chat input
prompt = st.chat_input("Ask about your learning progress…")
if prompt:
# User message
st.session_state.progress_messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# Assistant response via Memori
with st.chat_message("assistant"):
with st.spinner("🔍 Checking your study memories…"):
try:
answer = memori_mgr.summarize_progress(prompt)
st.session_state.progress_messages.append(
{"role": "assistant", "content": answer}
)
st.markdown(answer)
except Exception as e:
err = f"❌ Failed to query Memori: {e}"
st.session_state.progress_messages.append(
{"role": "assistant", "content": err}
)
st.error(err)
def main():
sidebar_keys()
_ensure_state()
try:
db_url = os.getenv("MEMORI_DB_URL", "") or None
openai_key = os.getenv("OPENAI_API_KEY", "")
memori_mgr = get_memori_manager(openai_key, db_url)
except Exception as e:
st.error(
f"Failed to initialize Memori / OpenAI. "
f"Check your OPENAI_API_KEY and DB settings. Details: {e}"
)
return
# After Memori is ready, try to restore learner profile from Memori on fresh loads.
# This lets the app remember your profile across refreshes and new runs.
if st.session_state.learner_profile is None:
profile_dict: dict | None = None
try:
profile_dict = memori_mgr.get_latest_learner_profile()
except Exception:
profile_dict = None
if profile_dict:
try:
st.session_state.learner_profile = LearnerProfile(**profile_dict)
except Exception:
# Fall back to LLM-based reconstruction if structured load fails.
_maybe_restore_profile_from_memori(memori_mgr)
else:
# No structured profile found try to reconstruct via Memori + LLM.
_maybe_restore_profile_from_memori(memori_mgr)
tab1, tab2, tab3 = st.tabs(
["🧭 Study Plan", "📅 Todays Session", "📈 Progress & Memory"]
)
with tab1:
study_plan_tab(memori_mgr)
with tab2:
today_session_tab(memori_mgr)
with tab3:
progress_tab(memori_mgr)
if __name__ == "__main__":
main()