Download src/streamlit_app.py from Soha85/eduagent: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/spaces/Soha85/eduagent/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/Soha85/eduagent/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/Soha85/eduagent/resolve/main/src/streamlit_app.py
16.4 kB
| """ | |
| Educational AI Agent β works on Streamlit Cloud, Hugging Face Spaces, and Kaggle. | |
| Uses Groq API for fast inference (free tier available). | |
| """ | |
| import os | |
| import json | |
| import re | |
| import streamlit as st | |
| from groq import Groq | |
| # ββ Page config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.set_page_config( | |
| page_title="EduAgent β AI Learning Assistant", | |
| page_icon="π", | |
| layout="wide", | |
| ) | |
| # ββ Minimal custom CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| /* Code blocks */ | |
| .stCodeBlock { border-left: 3px solid #7c3aed; } | |
| /* Tool call badge */ | |
| .tool-badge { | |
| display: inline-block; | |
| background: #ede9fe; | |
| color: #5b21b6; | |
| border-radius: 6px; | |
| padding: 2px 10px; | |
| font-size: 0.78rem; | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| } | |
| .result-box { | |
| background: #f5f3ff; | |
| border-left: 3px solid #7c3aed; | |
| padding: 8px 14px; | |
| border-radius: 0 6px 6px 0; | |
| font-size: 0.9rem; | |
| margin-bottom: 8px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 1. TOOL DEFINITIONS (sent to the LLM so it knows what it can call) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TOOLS = [ | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "explain_concept", | |
| "description": ( | |
| "Explain an educational concept clearly at a chosen difficulty level. " | |
| "Use this when the user asks 'what is', 'explain', 'define', or similar." | |
| ), | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "concept": {"type": "string", "description": "The concept to explain"}, | |
| "level": { | |
| "type": "string", | |
| "enum": ["beginner", "intermediate", "advanced"], | |
| "description": "Target difficulty level", | |
| }, | |
| "subject": {"type": "string", "description": "Subject area, e.g. 'math', 'physics'"}, | |
| }, | |
| "required": ["concept", "level"], | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "generate_quiz", | |
| "description": "Generate multiple-choice quiz questions to test understanding of a topic.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "topic": {"type": "string", "description": "Topic to quiz on"}, | |
| "num_questions": {"type": "integer", "description": "Number of questions (1-5)"}, | |
| "difficulty": { | |
| "type": "string", | |
| "enum": ["easy", "medium", "hard"], | |
| }, | |
| }, | |
| "required": ["topic", "num_questions"], | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "solve_problem", | |
| "description": ( | |
| "Solve a math, science, or logic problem step by step. " | |
| "Show all working clearly." | |
| ), | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "problem": {"type": "string", "description": "The problem statement"}, | |
| "subject": {"type": "string", "description": "Subject area"}, | |
| }, | |
| "required": ["problem"], | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "create_study_plan", | |
| "description": "Create a structured study plan for learning a subject or topic.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "subject": {"type": "string", "description": "Subject or topic to study"}, | |
| "duration_weeks": {"type": "integer", "description": "Available weeks"}, | |
| "goal": {"type": "string", "description": "Learning goal or exam target"}, | |
| }, | |
| "required": ["subject", "duration_weeks"], | |
| }, | |
| }, | |
| }, | |
| ] | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 2. TOOL EXECUTION (local Python functions the agent can call) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def execute_tool(tool_name: str, tool_args: dict, client: Groq, model: str) -> str: | |
| """Route tool calls to local implementations or a second LLM call.""" | |
| # For educational tools, we call the LLM again with a focused prompt. | |
| # In a real app you could swap these for database lookups, calculators, etc. | |
| prompts = { | |
| "generate_quiz": ( | |
| f"Create {tool_args.get('num_questions', 3)} multiple-choice questions about " | |
| f"'{tool_args.get('topic', 'the requested topic')}' at {tool_args.get('difficulty','medium')} difficulty. " | |
| "Format each question as:\n**Q1.** ...\na) ...\nb) ...\nc) ...\nd) ...\nβ Answer: ..." | |
| ), | |
| "solve_problem": ( | |
| f"Solve this step-by-step:\n{tool_args.get('problem', tool_args)}\n" | |
| "Show every step clearly. Box the final answer." | |
| ), | |
| "create_study_plan": ( | |
| f"Create a {tool_args.get('duration_weeks', 4)}-week study plan for '{tool_args.get('subject', 'the subject')}'. " | |
| f"Goal: {tool_args.get('goal','general mastery')}. " | |
| "Use a weekly breakdown with topics and practice tasks." | |
| ), | |
| "explain_concept": ( | |
| f"Explain '{tool_args['concept']}' at {tool_args.get('level','intermediate')} level" | |
| f"{' in ' + tool_args['subject'] if tool_args.get('subject') else ''}. " | |
| "Use clear language, an analogy, and a short example. Markdown ok." | |
| ), | |
| } | |
| prompt = prompts.get(tool_name, f"Handle this: {tool_args}") | |
| response = client.chat.completions.create( | |
| model=model, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=1024, | |
| temperature=0.7, | |
| ) | |
| return response.choices[0].message.content | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 3. AGENT LOOP (single turn: may call multiple tools before final answer) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_agent(user_message: str, history: list, client: Groq, model: str): | |
| """ | |
| Agentic loop: | |
| 1. Send user message + history to LLM with tools. | |
| 2. If LLM wants to call tools β execute them β feed results back. | |
| 3. Repeat until LLM returns a final text answer. | |
| Yields (type, content) tuples for streaming UI updates. | |
| """ | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "You are EduAgent, a friendly and expert educational AI tutor. " | |
| "You have tools to explain concepts, generate quizzes, solve problems, " | |
| "and create study plans. Use the most appropriate tool for each request. " | |
| "You may chain multiple tools if needed. Always be encouraging and clear." | |
| ), | |
| } | |
| ] + history + [{"role": "user", "content": user_message}] | |
| try: | |
| response = client.chat.completions.create( | |
| model=model, | |
| messages=messages, | |
| tools=TOOLS, | |
| tool_choice="auto", | |
| max_tokens=2048, | |
| temperature=0.7, | |
| ) | |
| except BadRequestError as e: | |
| if "decommissioned" in str(e): | |
| yield ("answer", "β οΈ This model has been retired by Groq. Please select a different model from the sidebar.", []) | |
| return | |
| raise | |
| tool_calls_log = [] | |
| for _iteration in range(5): # max tool-call depth guard | |
| response = client.chat.completions.create( | |
| model=model, | |
| messages=messages, | |
| tools=TOOLS, | |
| tool_choice="auto", | |
| max_tokens=2048, | |
| temperature=0.7, | |
| ) | |
| msg = response.choices[0].message | |
| # ββ No tool calls β final answer ββββββββββββββββββββββββββββββββββββββ | |
| if not msg.tool_calls: | |
| yield ("answer", msg.content, tool_calls_log) | |
| return | |
| # ββ Process each tool call ββββββββββββββββββββββββββββββββββββββββββββ | |
| messages.append({"role": "assistant", "content": msg.content, "tool_calls": msg.tool_calls}) | |
| for tc in msg.tool_calls: | |
| fn_name = tc.function.name | |
| fn_args = json.loads(tc.function.arguments) | |
| yield ("tool_start", fn_name, fn_args) | |
| result = execute_tool(fn_name, fn_args, client, model) | |
| yield ("tool_result", fn_name, result) | |
| tool_calls_log.append({"tool": fn_name, "args": fn_args, "result": result}) | |
| messages.append({ | |
| "role": "tool", | |
| "tool_call_id": tc.id, | |
| "content": result, | |
| }) | |
| yield ("answer", "I reached the maximum reasoning steps. Please rephrase your question.", tool_calls_log) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4. STREAMLIT UI | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_api_key() -> str | None: | |
| """Retrieve Groq API key from env or Streamlit secrets (all platforms).""" | |
| # Streamlit Cloud / HF Spaces: set in app secrets | |
| try: | |
| return st.secrets["GROQ_API_KEY"] | |
| except Exception: | |
| pass | |
| # Kaggle / local: environment variable | |
| return os.environ.get("GROQ_API_KEY") | |
| def main(): | |
| # ββ Sidebar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with st.sidebar: | |
| st.title("βοΈ Settings") | |
| api_key_input = st.text_input( | |
| "Groq API Key", | |
| type="password", | |
| placeholder="gsk_...", | |
| help="Get a free key at console.groq.com", | |
| ) | |
| api_key = api_key_input or get_api_key() | |
| model = st.selectbox( | |
| "Model", | |
| [ | |
| "llama-3.3-70b-versatile", # best quality, 1K req/day free | |
| "llama-3.1-8b-instant", # fastest, 14.4K req/day free β safest for testing | |
| "gemma2-9b-it", # Google Gemma, 15K TPM free | |
| "meta-llama/llama-4-scout-17b-16e-instruct", | |
| ], | |
| index=1, | |
| help="All models run on Groq's ultra-fast inference.", | |
| ) | |
| st.divider() | |
| st.markdown("**Try asking:**") | |
| examples = [ | |
| "Explain neural networks for a beginner", | |
| "Quiz me on photosynthesis (3 questions)", | |
| "Solve: integrate xΒ² from 0 to 3", | |
| "Make a 4-week Python study plan", | |
| "Explain recursion then quiz me on it", | |
| ] | |
| for ex in examples: | |
| if st.button(ex, use_container_width=True, key=ex): | |
| st.session_state["pending_input"] = ex | |
| st.divider() | |
| if st.button("ποΈ Clear chat", use_container_width=True): | |
| st.session_state["messages"] = [] | |
| st.session_state["history"] = [] | |
| st.rerun() | |
| st.caption("Powered by [Groq](https://groq.com) Β· Free tier available") | |
| # ββ Main area ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.title("π EduAgent") | |
| st.caption("An AI learning assistant with tool-use β explains, quizzes, solves, and plans.") | |
| if not api_key: | |
| st.warning( | |
| "Add your **Groq API key** in the sidebar to start. " | |
| "Get one free at [console.groq.com](https://console.groq.com).", | |
| icon="π", | |
| ) | |
| st.stop() | |
| # Init state | |
| if "messages" not in st.session_state: | |
| st.session_state["messages"] = [] # rendered chat | |
| if "history" not in st.session_state: | |
| st.session_state["history"] = [] # LLM-format history | |
| # Render existing chat | |
| for msg in st.session_state["messages"]: | |
| with st.chat_message(msg["role"]): | |
| st.markdown(msg["content"]) | |
| # Handle sidebar example buttons | |
| pending = st.session_state.pop("pending_input", None) | |
| user_input = st.chat_input("Ask anything educationalβ¦") or pending | |
| if not user_input: | |
| st.stop() | |
| # Show user message | |
| with st.chat_message("user"): | |
| st.markdown(user_input) | |
| st.session_state["messages"].append({"role": "user", "content": user_input}) | |
| # Run agent | |
| client = Groq(api_key=api_key) | |
| final_answer = "" | |
| tool_summaries = [] | |
| with st.chat_message("assistant"): | |
| status_placeholder = st.empty() | |
| answer_placeholder = st.empty() | |
| for event in run_agent(user_input, st.session_state["history"], client, model): | |
| etype = event[0] | |
| if etype == "tool_start": | |
| _, tool_name, tool_args = event | |
| readable = tool_name.replace("_", " ").title() | |
| status_placeholder.markdown( | |
| f'<span class="tool-badge">π§ {readable}</span>', | |
| unsafe_allow_html=True, | |
| ) | |
| elif etype == "tool_result": | |
| _, tool_name, result = event | |
| readable = tool_name.replace("_", " ").title() | |
| tool_summaries.append(f"**{readable}** β result ready β") | |
| elif etype == "answer": | |
| _, final_answer, _ = event | |
| status_placeholder.empty() | |
| answer_placeholder.markdown(final_answer) | |
| # Show tool trace in expander if tools were used | |
| if tool_summaries: | |
| with st.expander("π Agent reasoning trace", expanded=False): | |
| for s in tool_summaries: | |
| st.markdown(s) | |
| # Update session history (for multi-turn) | |
| st.session_state["messages"].append({"role": "assistant", "content": final_answer}) | |
| st.session_state["history"].append({"role": "user", "content": user_input}) | |
| st.session_state["history"].append({"role": "assistant", "content": final_answer}) | |
| # Keep history bounded (last 10 turns = 20 messages) | |
| if len(st.session_state["history"]) > 20: | |
| st.session_state["history"] = st.session_state["history"][-20:] | |
| if __name__ == "__main__": | |
| main() |