Spaces:
Running
Running
initial commit
Browse files- .DS_Store +0 -0
- .gitignore +19 -0
- LICENSE +21 -0
- README.md +183 -8
- app.py +363 -0
- assets/.DS_Store +0 -0
- env.example +3 -0
- lessons/animals.txt +70 -0
- lessons/example_colors.txt +79 -0
- lessons/numbers_1_to_10.txt +61 -0
- lessons/shapes.txt +61 -0
- lessons/weather_and_seasons.txt +109 -0
- modal/invoke.py +44 -0
- modal/main.py +236 -0
- pyproject.toml +24 -0
- requirements.txt +2 -0
- src/.DS_Store +0 -0
- src/learnbee/__init__.py +0 -0
- src/learnbee/llm_call.py +199 -0
- src/learnbee/mcp_client.py +81 -0
- src/learnbee/mcp_server.py +96 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
.venv
|
| 11 |
+
|
| 12 |
+
# Environment variables
|
| 13 |
+
.env
|
| 14 |
+
|
| 15 |
+
# misc
|
| 16 |
+
MEMO.md
|
| 17 |
+
backup/
|
| 18 |
+
|
| 19 |
+
!.gitkeep
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Alejandro
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,14 +1,189 @@
|
|
| 1 |
---
|
| 2 |
-
title: Learnbee
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
-
pinned:
|
| 10 |
license: mit
|
| 11 |
-
short_description:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Learnbee-mcp
|
| 3 |
+
emoji: 🎓
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.33.0
|
| 8 |
+
python_version: 3.12
|
| 9 |
app_file: app.py
|
| 10 |
+
pinned: true
|
| 11 |
license: mit
|
| 12 |
+
short_description: Interactive educational tutor for early childhood education (ages 3-6).
|
| 13 |
+
thumbnail: https://huggingface.co/spaces/Agents-MCP-Hackathon/consilium_mcp/blob/main/assets/screenshot.png
|
| 14 |
+
tags:
|
| 15 |
+
- agent-demo-track
|
| 16 |
+
- mcp-server-track
|
| 17 |
+
- education
|
| 18 |
+
- early-childhood
|
| 19 |
---
|
| 20 |
|
| 21 |
+
<!-- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference -->
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Learnbee-mcp 🎓
|
| 25 |
+
|
| 26 |
+
## What is this?
|
| 27 |
+
|
| 28 |
+
**Learnbee-mcp** is an interactive educational system designed for children in early childhood education (ages 3-6). It uses the Model Context Protocol (MCP) to provide educational conversations and interactive activities based on lesson content.
|
| 29 |
+
|
| 30 |
+
The system allows children to interact with friendly educational tutors who guide learning through questions, hints, and age-appropriate explanations, rather than giving direct answers. This encourages curiosity, critical thinking, and active participation in the learning process.
|
| 31 |
+
|
| 32 |
+
### Key Features
|
| 33 |
+
|
| 34 |
+
- 🎯 **Customizable Educational Tutors**: Choose from different tutors with friendly personalities (Professor Owl, Star Explorer, Logic Bot, Nature Guide, Story Friend)
|
| 35 |
+
- 📚 **Customizable Lessons**: Load educational content from text files in the `./lessons` directory
|
| 36 |
+
- 🎚️ **Difficulty Levels**: Adjust the difficulty level (beginner, intermediate, advanced) according to the child's needs
|
| 37 |
+
- 🌍 **Multilingual**: Automatic language detection - the tutor responds in the same language the child uses
|
| 38 |
+
- 🧠 **Key Concept Extraction**: The system automatically identifies key educational concepts from each lesson
|
| 39 |
+
- 🛡️ **Child-Safe**: Built-in safety filters to keep conversations age-appropriate
|
| 40 |
+
|
| 41 |
+
### Demo
|
| 42 |
+
|
| 43 |
+
<p align="center">
|
| 44 |
+
<img src="./assets/screenshot.png" alt="Screenshot" height="200"/><br>
|
| 45 |
+
<span>Interactive educational interface</span>
|
| 46 |
+
</p>
|
| 47 |
+
|
| 48 |
+
## How to Use
|
| 49 |
+
|
| 50 |
+
1. **Select a Lesson**: Choose a lesson from the dropdown menu (lessons are loaded from `.txt` files in the `./lessons` directory)
|
| 51 |
+
2. **Choose a Tutor**: Select one of the available educational tutors
|
| 52 |
+
3. **Adjust Difficulty Level**: Select the appropriate level for the child
|
| 53 |
+
4. **Load the Lesson**: Click "Load Lesson & Prepare Tutor" to load the content and extract key concepts
|
| 54 |
+
5. **Start Learning!**: Begin a conversation with the tutor about the lesson content
|
| 55 |
+
|
| 56 |
+
### Tips
|
| 57 |
+
|
| 58 |
+
- The tutor will not give direct answers, but will ask follow-up questions and offer hints to encourage thinking
|
| 59 |
+
- If the child deviates from the topic, the tutor will gently redirect the conversation back to the lesson
|
| 60 |
+
- The tutor automatically detects and responds in the same language the child uses - just start chatting in your preferred language!
|
| 61 |
+
|
| 62 |
+
## Development
|
| 63 |
+
|
| 64 |
+
If you want to develop this project, here are the details to get you started.
|
| 65 |
+
|
| 66 |
+
### Prerequisites
|
| 67 |
+
|
| 68 |
+
- **Python 3.12+**
|
| 69 |
+
- **Gradio**: Provides the user interface for educational conversations
|
| 70 |
+
- **OpenAI API**: Used to generate tutor responses and extract key concepts
|
| 71 |
+
- This project uses [uv](https://github.com/astral-sh/uv) for dependency management. Please install uv if you haven't already.
|
| 72 |
+
|
| 73 |
+
### Installation
|
| 74 |
+
|
| 75 |
+
After cloning the repository, you can run the following command to install dependencies:
|
| 76 |
+
|
| 77 |
+
```sh
|
| 78 |
+
uv sync --frozen
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Or using pip:
|
| 82 |
+
|
| 83 |
+
```sh
|
| 84 |
+
pip install -r requirements.txt
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Configuration
|
| 88 |
+
|
| 89 |
+
1. **Set up environment variables**:
|
| 90 |
+
- Copy the `.env.example` file to `.env`:
|
| 91 |
+
```sh
|
| 92 |
+
cp .env.example .env
|
| 93 |
+
```
|
| 94 |
+
- Edit the `.env` file and add your OpenAI API key:
|
| 95 |
+
```
|
| 96 |
+
OPENAI_API_KEY=your_api_key_here
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
2. **Prepare lessons**:
|
| 100 |
+
- Create text files (`.txt`) in the `./lessons` directory with educational content
|
| 101 |
+
- Each file should contain the lesson content in plain text
|
| 102 |
+
- The system will automatically extract key concepts from each lesson
|
| 103 |
+
|
| 104 |
+
### Run Locally
|
| 105 |
+
|
| 106 |
+
```sh
|
| 107 |
+
uv run gradio app.py
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Or using python directly:
|
| 111 |
+
|
| 112 |
+
```sh
|
| 113 |
+
python app.py
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
- Hot reloading is enabled by default.
|
| 117 |
+
|
| 118 |
+
### Project Structure
|
| 119 |
+
|
| 120 |
+
```
|
| 121 |
+
learnbee-mcp/
|
| 122 |
+
├── app.py # Main Gradio application
|
| 123 |
+
├── requirements.txt # Project dependencies
|
| 124 |
+
├── .env # Environment variables (not committed to repository)
|
| 125 |
+
├── .env.example # Environment variables template
|
| 126 |
+
├── lessons/ # Directory with lesson files (.txt)
|
| 127 |
+
│ ├── example_colors.txt # Colors lesson
|
| 128 |
+
│ ├── numbers_1_to_10.txt # Numbers lesson
|
| 129 |
+
│ ├── shapes.txt # Shapes lesson
|
| 130 |
+
│ ├── animals.txt # Animals lesson
|
| 131 |
+
│ └── weather_and_seasons.txt # Weather and seasons lesson
|
| 132 |
+
└── src/
|
| 133 |
+
└── learnbee/ # Main module
|
| 134 |
+
├── __init__.py
|
| 135 |
+
├── llm_call.py # LLM client with OpenAI API
|
| 136 |
+
├── mcp_server.py # MCP server for managing lessons
|
| 137 |
+
└── mcp_client.py # MCP client (optional)
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### Deploy to Hugging Face Spaces
|
| 141 |
+
|
| 142 |
+
1. Make sure all dependencies are in `requirements.txt`
|
| 143 |
+
2. Set up the `OPENAI_API_KEY` environment variable in Hugging Face Spaces settings
|
| 144 |
+
3. Push the code to the repository
|
| 145 |
+
|
| 146 |
+
```sh
|
| 147 |
+
git add .
|
| 148 |
+
git commit -m "Deploy Learnbee-mcp"
|
| 149 |
+
git push
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
## Customization
|
| 153 |
+
|
| 154 |
+
### Adding New Tutors
|
| 155 |
+
|
| 156 |
+
You can add new tutors by editing the `TUTOR_NAMES` list in `app.py`:
|
| 157 |
+
|
| 158 |
+
```python
|
| 159 |
+
TUTOR_NAMES = ["Professor Owl", "Star Explorer", "Logic Bot", "Nature Guide", "Story Friend", "Your New Tutor"]
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
### Creating New Lessons
|
| 163 |
+
|
| 164 |
+
1. Create a text file in the `./lessons` directory
|
| 165 |
+
2. Write educational content appropriate for ages 3-6
|
| 166 |
+
3. The system will automatically detect and load the new lesson
|
| 167 |
+
|
| 168 |
+
### Adjusting Tutor Behavior
|
| 169 |
+
|
| 170 |
+
You can modify the `system_prompt` in the `custom_respond` function in `app.py` to adjust the tutor's pedagogical behavior.
|
| 171 |
+
|
| 172 |
+
## Technologies Used
|
| 173 |
+
|
| 174 |
+
- **Gradio**: Framework for interactive user interfaces
|
| 175 |
+
- **OpenAI API**: Language model for generating educational responses
|
| 176 |
+
- **Model Context Protocol (MCP)**: Protocol for managing lesson context
|
| 177 |
+
- **Python 3.12+**: Main programming language
|
| 178 |
+
|
| 179 |
+
## License
|
| 180 |
+
|
| 181 |
+
MIT License
|
| 182 |
+
|
| 183 |
+
## Contributing
|
| 184 |
+
|
| 185 |
+
Contributions are welcome. Please open an issue or pull request if you have suggestions or improvements.
|
| 186 |
+
|
| 187 |
+
---
|
| 188 |
+
|
| 189 |
+
**Learnbee-mcp** - Making learning interactive and fun for the little ones 🎓✨
|
app.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Add src directory to Python path for Hugging Face Spaces compatibility
|
| 5 |
+
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 6 |
+
SRC_DIR = os.path.join(PROJECT_ROOT, "src")
|
| 7 |
+
sys.path.insert(0, SRC_DIR)
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
from numpy import add
|
| 13 |
+
|
| 14 |
+
from learnbee.llm_call import LLMCall
|
| 15 |
+
from learnbee.mcp_server import get_lesson_content, get_lesson_list
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
LESSON_CONTENT_MAX_LENGTH = 50000
|
| 19 |
+
|
| 20 |
+
# Available tutor names for early childhood education
|
| 21 |
+
TUTOR_NAMES = ["Professor Owl", "Star Explorer", "Logic Bot", "Nature Guide", "Story Friend"]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_lesson_content(lesson_name, selected_tutor, progress=gr.Progress()):
|
| 25 |
+
"""Load lesson content and extract key concepts. Returns introduction message for chatbot."""
|
| 26 |
+
if not lesson_name:
|
| 27 |
+
return "", "", "Please select a lesson first.", []
|
| 28 |
+
|
| 29 |
+
# Get current chatbot state (empty list if none)
|
| 30 |
+
chatbot_messages = []
|
| 31 |
+
|
| 32 |
+
progress(0.1, desc="Loading lesson content...")
|
| 33 |
+
|
| 34 |
+
lesson_content = get_lesson_content(lesson_name, LESSON_CONTENT_MAX_LENGTH)
|
| 35 |
+
|
| 36 |
+
progress(0.5, desc="Extracting key concepts from the lesson...")
|
| 37 |
+
|
| 38 |
+
# Extract key concepts using LLM
|
| 39 |
+
try:
|
| 40 |
+
call_llm = LLMCall()
|
| 41 |
+
concepts = call_llm.extract_key_concepts(lesson_content)
|
| 42 |
+
|
| 43 |
+
progress(0.7, desc="Generating lesson introduction...")
|
| 44 |
+
|
| 45 |
+
# Generate lesson introduction with summary and example questions
|
| 46 |
+
introduction = ""
|
| 47 |
+
if concepts:
|
| 48 |
+
try:
|
| 49 |
+
introduction = call_llm.generate_lesson_introduction(
|
| 50 |
+
lesson_content, lesson_name, concepts
|
| 51 |
+
)
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Error generating introduction: {str(e)}")
|
| 54 |
+
# Continue without introduction if it fails
|
| 55 |
+
|
| 56 |
+
progress(1.0, desc="Complete!")
|
| 57 |
+
|
| 58 |
+
if concepts:
|
| 59 |
+
concepts_display = ', '.join(concepts[:5])
|
| 60 |
+
if len(concepts) > 5:
|
| 61 |
+
concepts_display += f" and {len(concepts) - 5} more"
|
| 62 |
+
|
| 63 |
+
# Build simple status message (just confirmation)
|
| 64 |
+
status_message = (
|
| 65 |
+
f"✅ Successfully loaded '{lesson_name}'!\n\n"
|
| 66 |
+
f"📚 Found {len(concepts)} key concepts: {concepts_display}\n\n"
|
| 67 |
+
f"🎓 Your tutor is ready! Check the chat for a welcome message."
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Prepare chatbot message with introduction
|
| 71 |
+
if introduction:
|
| 72 |
+
# Format the introduction as a friendly greeting from the tutor
|
| 73 |
+
tutor_greeting = (
|
| 74 |
+
f"Hello! 👋 I'm {selected_tutor}, and I'm so excited to learn with you today!\n\n"
|
| 75 |
+
f"{introduction}\n\n"
|
| 76 |
+
f"Let's start our learning adventure! What would you like to explore first? 🌟"
|
| 77 |
+
)
|
| 78 |
+
chatbot_messages = [{"role": "assistant", "content": tutor_greeting}]
|
| 79 |
+
else:
|
| 80 |
+
# Fallback greeting if introduction generation fails
|
| 81 |
+
tutor_greeting = (
|
| 82 |
+
f"Hello! 👋 I'm {selected_tutor}, and I'm excited to learn with you today!\n\n"
|
| 83 |
+
f"We're going to explore: {concepts_display}\n\n"
|
| 84 |
+
f"What would you like to learn about first? 🌟"
|
| 85 |
+
)
|
| 86 |
+
chatbot_messages = [{"role": "assistant", "content": tutor_greeting}]
|
| 87 |
+
|
| 88 |
+
return (
|
| 89 |
+
lesson_name,
|
| 90 |
+
lesson_content,
|
| 91 |
+
status_message,
|
| 92 |
+
chatbot_messages,
|
| 93 |
+
)
|
| 94 |
+
else:
|
| 95 |
+
status_message = (
|
| 96 |
+
f"⚠️ Loaded '{lesson_name}' but no key concepts were automatically detected.\n"
|
| 97 |
+
f"You can still chat with your tutor about the lesson content!"
|
| 98 |
+
)
|
| 99 |
+
tutor_greeting = (
|
| 100 |
+
f"Hello! 👋 I'm {selected_tutor}, and I'm ready to learn with you!\n\n"
|
| 101 |
+
f"Let's explore the lesson '{lesson_name}' together. What would you like to know? 🌟"
|
| 102 |
+
)
|
| 103 |
+
chatbot_messages = [{"role": "assistant", "content": tutor_greeting}]
|
| 104 |
+
return (
|
| 105 |
+
lesson_name,
|
| 106 |
+
lesson_content,
|
| 107 |
+
status_message,
|
| 108 |
+
chatbot_messages,
|
| 109 |
+
)
|
| 110 |
+
except Exception as e:
|
| 111 |
+
status_message = (
|
| 112 |
+
f"❌ Error extracting concepts: {str(e)}\n\n"
|
| 113 |
+
f"You can still try chatting about the lesson content."
|
| 114 |
+
)
|
| 115 |
+
tutor_greeting = (
|
| 116 |
+
f"Hello! 👋 I'm {selected_tutor}, and I'm here to help you learn!\n\n"
|
| 117 |
+
f"Let's explore together. What would you like to know? 🌟"
|
| 118 |
+
)
|
| 119 |
+
chatbot_messages = [{"role": "assistant", "content": tutor_greeting}]
|
| 120 |
+
return (
|
| 121 |
+
lesson_name,
|
| 122 |
+
lesson_content,
|
| 123 |
+
status_message,
|
| 124 |
+
chatbot_messages,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def custom_respond(
|
| 129 |
+
message, history, lesson_name, lesson_content, selected_tutor, difficulty_level
|
| 130 |
+
):
|
| 131 |
+
"""Custom respond function with educational system prompt."""
|
| 132 |
+
if not lesson_name or not selected_tutor:
|
| 133 |
+
yield "Please select a lesson and tutor first."
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
if not lesson_content:
|
| 137 |
+
lesson_content = get_lesson_content(lesson_name, LESSON_CONTENT_MAX_LENGTH)
|
| 138 |
+
|
| 139 |
+
# Generate educational system prompt with enhanced pedagogy
|
| 140 |
+
# fmt: off
|
| 141 |
+
system_prompt = (
|
| 142 |
+
f"You are {selected_tutor}, a friendly and patient Educational Tutor specializing in early childhood education (ages 3-6).\n\n"
|
| 143 |
+
|
| 144 |
+
"CORE PEDAGOGICAL PRINCIPLES:\n"
|
| 145 |
+
"1. Socratic Method: Guide through questions, not answers. Help children discover knowledge themselves.\n"
|
| 146 |
+
"2. Scaffolding: Break complex ideas into smaller, manageable steps. Build understanding gradually.\n"
|
| 147 |
+
"3. Positive Reinforcement: Celebrate attempts, not just correct answers. Use encouraging phrases like 'Great thinking!' or 'You're on the right track!'\n"
|
| 148 |
+
"4. Active Learning: Encourage hands-on thinking, examples from their world, and personal connections.\n"
|
| 149 |
+
"5. Repetition with Variation: Reinforce concepts through different examples and contexts.\n\n"
|
| 150 |
+
|
| 151 |
+
"COMMUNICATION GUIDELINES:\n"
|
| 152 |
+
"- Use very simple, age-appropriate language (3-6 year olds).\n"
|
| 153 |
+
"- Keep sentences short (5-10 words maximum).\n"
|
| 154 |
+
"- Use concrete examples from children's daily lives (toys, family, pets, food, nature).\n"
|
| 155 |
+
"- Incorporate playful elements: emojis, simple analogies, and fun comparisons.\n"
|
| 156 |
+
"- Be warm, enthusiastic, and patient. Show excitement about learning!\n"
|
| 157 |
+
"- Use the child's name when possible (refer to them as 'you' or 'little learner').\n\n"
|
| 158 |
+
|
| 159 |
+
"TEACHING STRATEGIES BY DIFFICULTY LEVEL:\n"
|
| 160 |
+
f"- {difficulty_level.upper()} level:\n"
|
| 161 |
+
+ (" * Beginner: Use very simple words, lots of examples, visual descriptions, and yes/no questions.\n"
|
| 162 |
+
if difficulty_level == "beginner" else
|
| 163 |
+
" * Intermediate: Introduce slightly more complex concepts, encourage longer explanations, use 'why' and 'how' questions.\n"
|
| 164 |
+
if difficulty_level == "intermediate" else
|
| 165 |
+
" * Advanced: Challenge with problem-solving, encourage predictions, explore connections between concepts.\n") +
|
| 166 |
+
"\n"
|
| 167 |
+
|
| 168 |
+
"INTERACTION PATTERNS:\n"
|
| 169 |
+
"- When a child asks a question: Respond with a guiding question first, then offer a hint if needed.\n"
|
| 170 |
+
"- When a child gives an answer: Validate their thinking, then ask a follow-up to deepen understanding.\n"
|
| 171 |
+
"- When a child seems confused: Break it down into smaller pieces, use a different example, or try a simpler approach.\n"
|
| 172 |
+
"- When a child shows excitement: Match their energy and build on their interest.\n"
|
| 173 |
+
"- When off-topic: Acknowledge their interest, then gently connect it back: 'That's interesting! Now, let's think about how that relates to our lesson...'\n\n"
|
| 174 |
+
|
| 175 |
+
"SAFETY AND BOUNDARIES:\n"
|
| 176 |
+
"- Only discuss topics appropriate for ages 3-12.\n"
|
| 177 |
+
"- If asked about inappropriate topics, gently redirect: 'Let's focus on our fun lesson instead!'\n"
|
| 178 |
+
"- Keep all content educational and positive.\n"
|
| 179 |
+
"- Never provide medical, legal, or safety advice beyond basic age-appropriate concepts.\n\n"
|
| 180 |
+
|
| 181 |
+
"LESSON CONTEXT:\n"
|
| 182 |
+
"====================\n"
|
| 183 |
+
f"{lesson_content}\n"
|
| 184 |
+
"====================\n\n"
|
| 185 |
+
|
| 186 |
+
"YOUR ROLE:\n"
|
| 187 |
+
"You are teaching this lesson content to a young child. Make it engaging, interactive, and fun. "
|
| 188 |
+
"Remember: the goal is not just to convey information, but to spark curiosity and build confidence in learning.\n\n"
|
| 189 |
+
|
| 190 |
+
"LANGUAGE INSTRUCTION:\n"
|
| 191 |
+
"IMPORTANT: Always respond in the EXACT same language that the child uses in their messages. "
|
| 192 |
+
"If the child writes in Spanish, respond in Spanish. If they write in English, respond in English. "
|
| 193 |
+
"If they write in French, respond in French. Match the child's language automatically. "
|
| 194 |
+
"This is critical for effective communication with young learners.\n"
|
| 195 |
+
)
|
| 196 |
+
# fmt: on
|
| 197 |
+
|
| 198 |
+
# Call the respond method with educational system prompt
|
| 199 |
+
call_llm = LLMCall()
|
| 200 |
+
for response in call_llm.respond(
|
| 201 |
+
message,
|
| 202 |
+
history,
|
| 203 |
+
system_prompt=system_prompt,
|
| 204 |
+
tutor_name=selected_tutor,
|
| 205 |
+
difficulty_level=difficulty_level
|
| 206 |
+
):
|
| 207 |
+
yield response
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def gradio_ui():
|
| 211 |
+
lesson_name = gr.BrowserState("")
|
| 212 |
+
selected_tutor = gr.BrowserState(TUTOR_NAMES[0] if TUTOR_NAMES else "")
|
| 213 |
+
|
| 214 |
+
lesson_choices = json.loads(get_lesson_list())
|
| 215 |
+
|
| 216 |
+
with gr.Blocks() as demo:
|
| 217 |
+
|
| 218 |
+
with gr.Tab("Chat"):
|
| 219 |
+
# Title
|
| 220 |
+
with gr.Row():
|
| 221 |
+
gr.Markdown("# Learnbee-mcp - Educational Tutor")
|
| 222 |
+
|
| 223 |
+
# Status
|
| 224 |
+
with gr.Row():
|
| 225 |
+
status_markdown = gr.Markdown(label="Status")
|
| 226 |
+
status_markdown.value = (
|
| 227 |
+
# fmt: off
|
| 228 |
+
"👋 Welcome to Learnbee-mcp! 🎓<br><br>"
|
| 229 |
+
"📖 <strong>Getting Started:</strong><br>"
|
| 230 |
+
"1. Select a lesson from the dropdown<br>"
|
| 231 |
+
"2. Choose your favorite tutor<br>"
|
| 232 |
+
"3. Pick a difficulty level<br>"
|
| 233 |
+
"4. Click 'Load Lesson & Prepare Tutor'<br>"
|
| 234 |
+
"5. Start learning and chatting! 💬<br><br>"
|
| 235 |
+
"✨ This educational system is designed for early childhood education (ages 3-6)."
|
| 236 |
+
# fmt: on
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
# Hidden textbox for lesson content
|
| 240 |
+
lesson_content = gr.Textbox(visible=False)
|
| 241 |
+
|
| 242 |
+
with gr.Row():
|
| 243 |
+
|
| 244 |
+
with gr.Column(scale=1):
|
| 245 |
+
# Lesson selection
|
| 246 |
+
with gr.Row():
|
| 247 |
+
lesson_dropdown = gr.Dropdown(
|
| 248 |
+
label="📚 Select a Lesson",
|
| 249 |
+
choices=lesson_choices,
|
| 250 |
+
interactive=True,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
# Tutor selection
|
| 254 |
+
with gr.Row():
|
| 255 |
+
tutor_dropdown = gr.Dropdown(
|
| 256 |
+
label="🦸 Select a Tutor",
|
| 257 |
+
choices=TUTOR_NAMES,
|
| 258 |
+
value=TUTOR_NAMES[0] if TUTOR_NAMES else None,
|
| 259 |
+
interactive=True,
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# Difficulty level selection
|
| 263 |
+
with gr.Row():
|
| 264 |
+
difficulty_dropdown = gr.Dropdown(
|
| 265 |
+
label="📊 Difficulty Level",
|
| 266 |
+
choices=["beginner", "intermediate", "advanced"],
|
| 267 |
+
value="beginner",
|
| 268 |
+
interactive=True,
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
with gr.Row():
|
| 272 |
+
load_button = gr.Button(
|
| 273 |
+
"Load Lesson & Prepare Tutor", variant="primary"
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
def update_tutor_selection(tutor):
|
| 277 |
+
"""Update selected tutor."""
|
| 278 |
+
return tutor
|
| 279 |
+
|
| 280 |
+
tutor_dropdown.change(
|
| 281 |
+
fn=update_tutor_selection,
|
| 282 |
+
inputs=[tutor_dropdown],
|
| 283 |
+
outputs=[selected_tutor],
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
with gr.Row():
|
| 287 |
+
gr.Markdown(
|
| 288 |
+
"🌍 **Multilingual Support:** The tutor will automatically respond in the same language you use!<br>"
|
| 289 |
+
"Just start chatting in your preferred language (English, Spanish, French, etc.) and the tutor will match it.<br>"
|
| 290 |
+
"<br>"
|
| 291 |
+
"🔄 **Note:** Once you start chatting, you can't change the lesson or tutor. <br>"
|
| 292 |
+
"If you want to pick a different one, just hit the reset button and start fresh! 😊<br>"
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
with gr.Column(scale=2):
|
| 296 |
+
# Chat interface - defined before use
|
| 297 |
+
chat_interface = gr.ChatInterface(
|
| 298 |
+
fn=custom_respond,
|
| 299 |
+
additional_inputs=[
|
| 300 |
+
lesson_dropdown,
|
| 301 |
+
lesson_content,
|
| 302 |
+
tutor_dropdown,
|
| 303 |
+
difficulty_dropdown,
|
| 304 |
+
],
|
| 305 |
+
type="messages",
|
| 306 |
+
autofocus=False
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
# Connect load button after chat_interface is defined
|
| 310 |
+
load_button.click(
|
| 311 |
+
fn=load_lesson_content,
|
| 312 |
+
inputs=[lesson_dropdown, tutor_dropdown],
|
| 313 |
+
outputs=[
|
| 314 |
+
lesson_name,
|
| 315 |
+
lesson_content,
|
| 316 |
+
status_markdown,
|
| 317 |
+
chat_interface.chatbot_value,
|
| 318 |
+
],
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
reset_button = gr.Button("Reset", variant="secondary")
|
| 322 |
+
reset_button.click(
|
| 323 |
+
lambda: (
|
| 324 |
+
gr.update(value=""),
|
| 325 |
+
gr.update(value=""),
|
| 326 |
+
gr.update(value=TUTOR_NAMES[0] if TUTOR_NAMES else None),
|
| 327 |
+
gr.update(value="beginner"),
|
| 328 |
+
"Status reset.",
|
| 329 |
+
[],
|
| 330 |
+
),
|
| 331 |
+
outputs=[
|
| 332 |
+
lesson_dropdown,
|
| 333 |
+
lesson_content,
|
| 334 |
+
tutor_dropdown,
|
| 335 |
+
difficulty_dropdown,
|
| 336 |
+
status_markdown,
|
| 337 |
+
chat_interface.chatbot_value,
|
| 338 |
+
],
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
with gr.Tab("List Lessons"):
|
| 342 |
+
gr.Markdown("📚 Get the list of available lessons.")
|
| 343 |
+
btn = gr.Button("Get")
|
| 344 |
+
output_text = gr.Textbox(label="Lessons")
|
| 345 |
+
btn.click(get_lesson_list, None, output_text)
|
| 346 |
+
|
| 347 |
+
with gr.Tab("Lesson Content"):
|
| 348 |
+
gr.Markdown("📖 Get the content of a lesson by its name.")
|
| 349 |
+
lesson_name_input = gr.Textbox(label="Lesson Name")
|
| 350 |
+
lesson_len = gr.Number(label="Max Length", value=1000)
|
| 351 |
+
lesson_content_output = gr.Textbox(label="Lesson Content", lines=20)
|
| 352 |
+
btn = gr.Button("Get")
|
| 353 |
+
btn.click(get_lesson_content, [lesson_name_input, lesson_len], lesson_content_output)
|
| 354 |
+
|
| 355 |
+
return demo
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
if __name__ == "__main__":
|
| 359 |
+
demo = gradio_ui()
|
| 360 |
+
|
| 361 |
+
# Launch the Gradio app with MCP server enabled.
|
| 362 |
+
# NOTE: It is required to restart the app when you add or remove MCP tools.
|
| 363 |
+
demo.launch(mcp_server=True)
|
assets/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
env.example
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenAI API Configuration
|
| 2 |
+
# Copy this file to .env and replace with your actual API key
|
| 3 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
lessons/animals.txt
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Animals - Early Childhood Education
|
| 2 |
+
|
| 3 |
+
This lesson introduces children to different types of animals in a fun and engaging way.
|
| 4 |
+
|
| 5 |
+
Animal Categories:
|
| 6 |
+
|
| 7 |
+
Farm Animals - Animals that live on farms
|
| 8 |
+
These animals help farmers and give us food!
|
| 9 |
+
- Cow: Says "Moo!" Gives us milk. Has spots or is brown.
|
| 10 |
+
- Pig: Says "Oink!" Loves to play in mud. Has a curly tail.
|
| 11 |
+
- Chicken: Says "Cluck!" Lays eggs. Has feathers and wings.
|
| 12 |
+
- Horse: Says "Neigh!" Can run very fast. People ride horses.
|
| 13 |
+
- Sheep: Says "Baa!" Gives us wool for warm clothes. Very fluffy!
|
| 14 |
+
- Duck: Says "Quack!" Loves to swim. Has webbed feet.
|
| 15 |
+
- Goat: Says "Meh!" Loves to climb. Very playful!
|
| 16 |
+
|
| 17 |
+
Wild Animals - Animals that live in nature
|
| 18 |
+
These animals live in forests, jungles, and grasslands!
|
| 19 |
+
- Lion: The king of the jungle! Has a big mane. Says "Roar!"
|
| 20 |
+
- Elephant: Very big and gentle. Has a long trunk. Very smart!
|
| 21 |
+
- Monkey: Loves to climb trees. Very playful and funny!
|
| 22 |
+
- Bear: Big and furry. Loves honey. Sleeps in winter.
|
| 23 |
+
- Tiger: Has stripes. Very strong and fast. Lives in the jungle.
|
| 24 |
+
- Giraffe: Very tall! Has a long neck to reach leaves.
|
| 25 |
+
- Zebra: Has black and white stripes. Looks like a striped horse!
|
| 26 |
+
|
| 27 |
+
Pets - Animals that live with people
|
| 28 |
+
These animals are our friends and live in our homes!
|
| 29 |
+
- Dog: Man's best friend! Very loyal and friendly. Says "Woof!"
|
| 30 |
+
- Cat: Soft and cuddly. Loves to purr and play. Says "Meow!"
|
| 31 |
+
- Fish: Lives in water. Swims all day. Very colorful!
|
| 32 |
+
- Bird: Can fly! Sings beautiful songs. Has feathers.
|
| 33 |
+
- Rabbit: Soft and fluffy. Has long ears. Hops around!
|
| 34 |
+
- Hamster: Very small and cute. Loves to run on a wheel.
|
| 35 |
+
|
| 36 |
+
Animal Sounds:
|
| 37 |
+
- Dogs say "Woof woof!"
|
| 38 |
+
- Cats say "Meow!"
|
| 39 |
+
- Cows say "Moo!"
|
| 40 |
+
- Pigs say "Oink oink!"
|
| 41 |
+
- Ducks say "Quack quack!"
|
| 42 |
+
- Birds say "Tweet tweet!"
|
| 43 |
+
- Lions say "Roar!"
|
| 44 |
+
- Sheep say "Baa baa!"
|
| 45 |
+
|
| 46 |
+
Animal Habitats:
|
| 47 |
+
- Farm animals live on farms
|
| 48 |
+
- Wild animals live in forests, jungles, or grasslands
|
| 49 |
+
- Pets live in our homes
|
| 50 |
+
- Some animals live in water (fish, dolphins)
|
| 51 |
+
- Some animals live in trees (monkeys, birds)
|
| 52 |
+
- Some animals live underground (rabbits, moles)
|
| 53 |
+
|
| 54 |
+
Animal Activities:
|
| 55 |
+
1. Animal sounds: Practice making animal sounds
|
| 56 |
+
2. Animal movements: Move like different animals (hop like a bunny, walk like a bear)
|
| 57 |
+
3. Animal matching: Match animals to their homes
|
| 58 |
+
4. Animal stories: Tell stories about animals
|
| 59 |
+
5. Animal drawing: Draw your favorite animals
|
| 60 |
+
|
| 61 |
+
Animal Fun Facts:
|
| 62 |
+
- Elephants are the biggest land animals
|
| 63 |
+
- Giraffes are the tallest animals
|
| 64 |
+
- Cheetahs are the fastest animals
|
| 65 |
+
- Whales are the biggest animals in the ocean
|
| 66 |
+
- Ants are very strong for their size
|
| 67 |
+
- Butterflies start as caterpillars
|
| 68 |
+
|
| 69 |
+
Remember: Animals are our friends! We should be kind to all animals and take care of them. Each animal is special and has its own way of living!
|
| 70 |
+
|
lessons/example_colors.txt
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Colors - Early Childhood Education
|
| 2 |
+
|
| 3 |
+
This lesson introduces children to basic colors in a fun and engaging way.
|
| 4 |
+
|
| 5 |
+
Primary Colors - The basic colors that can't be made by mixing:
|
| 6 |
+
|
| 7 |
+
Red - The color of energy and love
|
| 8 |
+
Red is bright and bold! It catches our attention.
|
| 9 |
+
Examples: apples, strawberries, fire trucks, stop signs, roses, cherries, hearts
|
| 10 |
+
Look around: Can you find something red?
|
| 11 |
+
|
| 12 |
+
Blue - The color of sky and water
|
| 13 |
+
Blue is calm and peaceful. It's the color of the sky and ocean!
|
| 14 |
+
Examples: the sky, the ocean, blueberries, jeans, birds, flowers
|
| 15 |
+
Look around: Can you find something blue?
|
| 16 |
+
|
| 17 |
+
Yellow - The color of sunshine
|
| 18 |
+
Yellow is bright and happy! It's the color of the sun.
|
| 19 |
+
Examples: the sun, bananas, daisies, lemons, stars, corn, rubber ducks
|
| 20 |
+
Look around: Can you find something yellow?
|
| 21 |
+
|
| 22 |
+
Secondary Colors - Colors made by mixing primary colors:
|
| 23 |
+
|
| 24 |
+
Green - Made by mixing blue and yellow
|
| 25 |
+
Green is the color of nature! It's everywhere in nature.
|
| 26 |
+
Examples: grass, leaves, frogs, trees, peas, cucumbers, limes
|
| 27 |
+
Look around: Can you find something green?
|
| 28 |
+
|
| 29 |
+
Orange - Made by mixing red and yellow
|
| 30 |
+
Orange is warm and friendly! It's the color of autumn.
|
| 31 |
+
Examples: oranges, pumpkins, carrots, sunsets, tigers, goldfish
|
| 32 |
+
Look around: Can you find something orange?
|
| 33 |
+
|
| 34 |
+
Purple - Made by mixing red and blue
|
| 35 |
+
Purple is royal and magical! It's a special color.
|
| 36 |
+
Examples: grapes, violets, eggplants, plums, flowers, some butterflies
|
| 37 |
+
Look around: Can you find something purple?
|
| 38 |
+
|
| 39 |
+
Other Fun Colors:
|
| 40 |
+
|
| 41 |
+
Pink - A light red
|
| 42 |
+
Pink is soft and sweet! Many people love pink.
|
| 43 |
+
Examples: flowers, cotton candy, flamingos, some toys, bubble gum
|
| 44 |
+
|
| 45 |
+
Brown - The color of earth
|
| 46 |
+
Brown is the color of wood and chocolate!
|
| 47 |
+
Examples: tree trunks, chocolate, bears, dirt, some animals
|
| 48 |
+
|
| 49 |
+
Black - The darkest color
|
| 50 |
+
Black is the color of night! It's very dark.
|
| 51 |
+
Examples: night sky, some animals, shadows, some clothes
|
| 52 |
+
|
| 53 |
+
White - The lightest color
|
| 54 |
+
White is pure and clean! It's the color of clouds.
|
| 55 |
+
Examples: clouds, snow, milk, some flowers, paper
|
| 56 |
+
|
| 57 |
+
Color Activities:
|
| 58 |
+
1. Color Hunt: Look around and find objects of different colors
|
| 59 |
+
2. Color Sorting: Group objects by their colors
|
| 60 |
+
3. Color Mixing: Mix paints to create new colors (red + yellow = orange!)
|
| 61 |
+
4. Color Drawing: Draw pictures using your favorite colors
|
| 62 |
+
5. Color Matching: Match objects to color cards
|
| 63 |
+
6. Color Stories: Tell stories about colors
|
| 64 |
+
|
| 65 |
+
Color Games:
|
| 66 |
+
- "I spy something red!" - Take turns finding colors
|
| 67 |
+
- Color bingo: Find colors on a bingo card
|
| 68 |
+
- Color dance: Move to music and call out colors
|
| 69 |
+
- Color memory: Remember which colors you saw
|
| 70 |
+
|
| 71 |
+
Learning About Colors:
|
| 72 |
+
- Colors help us describe things: "The apple is red"
|
| 73 |
+
- Colors can show feelings: red for excitement, blue for calm
|
| 74 |
+
- Colors are everywhere in nature: green grass, blue sky, yellow sun
|
| 75 |
+
- Artists use colors to make beautiful pictures
|
| 76 |
+
- We use colors to organize and sort things
|
| 77 |
+
|
| 78 |
+
Remember: Colors are everywhere! Learning about colors helps us describe and understand the world around us. Every color is special and beautiful!
|
| 79 |
+
|
lessons/numbers_1_to_10.txt
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Numbers 1 to 10 - Early Childhood Education
|
| 2 |
+
|
| 3 |
+
This lesson introduces children to numbers from 1 to 10 in a fun and engaging way.
|
| 4 |
+
|
| 5 |
+
Learning Numbers:
|
| 6 |
+
|
| 7 |
+
1 - One
|
| 8 |
+
One is the first number. You have one nose, one mouth, and one head!
|
| 9 |
+
Examples: one apple, one toy, one friend
|
| 10 |
+
|
| 11 |
+
2 - Two
|
| 12 |
+
Two means a pair. You have two eyes, two ears, and two hands!
|
| 13 |
+
Examples: two shoes, two socks, two birds
|
| 14 |
+
|
| 15 |
+
3 - Three
|
| 16 |
+
Three is a small group. You might have three toys or see three flowers.
|
| 17 |
+
Examples: three cookies, three stars, three cats
|
| 18 |
+
|
| 19 |
+
4 - Four
|
| 20 |
+
Four is more than three! Count four fingers on one hand (not counting the thumb).
|
| 21 |
+
Examples: four wheels on a car, four legs on a chair, four seasons
|
| 22 |
+
|
| 23 |
+
5 - Five
|
| 24 |
+
Five is a whole hand! You have five fingers on each hand.
|
| 25 |
+
Examples: five toes on one foot, five petals on some flowers, five senses
|
| 26 |
+
|
| 27 |
+
6 - Six
|
| 28 |
+
Six is five plus one more. It's more than a whole hand!
|
| 29 |
+
Examples: six sides on a dice, six legs on an insect, six eggs in a carton
|
| 30 |
+
|
| 31 |
+
7 - Seven
|
| 32 |
+
Seven is a special number. There are seven days in a week!
|
| 33 |
+
Examples: seven colors in a rainbow, seven dwarfs in a story, seven notes in music
|
| 34 |
+
|
| 35 |
+
8 - Eight
|
| 36 |
+
Eight is two groups of four. It's getting bigger!
|
| 37 |
+
Examples: eight legs on a spider, eight tentacles on an octopus, eight planets
|
| 38 |
+
|
| 39 |
+
9 - Nine
|
| 40 |
+
Nine is almost ten! It's one less than ten.
|
| 41 |
+
Examples: nine lives of a cat, nine innings in baseball, nine months
|
| 42 |
+
|
| 43 |
+
10 - Ten
|
| 44 |
+
Ten is a big number! It's two whole hands together.
|
| 45 |
+
Examples: ten fingers on both hands, ten toes on both feet, ten candles on a birthday cake
|
| 46 |
+
|
| 47 |
+
Counting Activities:
|
| 48 |
+
1. Count objects around you: toys, books, crayons
|
| 49 |
+
2. Count body parts: fingers, toes, eyes
|
| 50 |
+
3. Count steps as you walk
|
| 51 |
+
4. Count items in groups: "How many apples do you see?"
|
| 52 |
+
5. Practice writing numbers with your finger in the air
|
| 53 |
+
|
| 54 |
+
Number Games:
|
| 55 |
+
- Find groups of objects: "Can you find three red things?"
|
| 56 |
+
- Match numbers: "Show me five fingers!"
|
| 57 |
+
- Count backwards from 10 to 1
|
| 58 |
+
- Sing number songs and rhymes
|
| 59 |
+
|
| 60 |
+
Remember: Numbers help us count, measure, and understand how many things we have. Practice counting every day!
|
| 61 |
+
|
lessons/shapes.txt
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Shapes - Early Childhood Education
|
| 2 |
+
|
| 3 |
+
This lesson introduces children to basic shapes in a fun and engaging way.
|
| 4 |
+
|
| 5 |
+
Basic Shapes:
|
| 6 |
+
|
| 7 |
+
Circle - Round and smooth
|
| 8 |
+
A circle has no corners. It goes round and round!
|
| 9 |
+
Examples: the sun, a ball, a wheel, a clock, a coin, a pizza
|
| 10 |
+
Look around: Can you find something round?
|
| 11 |
+
|
| 12 |
+
Square - Four equal sides
|
| 13 |
+
A square has four corners and four equal sides. It looks like a box!
|
| 14 |
+
Examples: a window, a book, a tile, a picture frame, a dice, a cracker
|
| 15 |
+
Look around: Can you find something square?
|
| 16 |
+
|
| 17 |
+
Triangle - Three sides and three corners
|
| 18 |
+
A triangle has three sides and three pointy corners. It looks like a slice of pizza!
|
| 19 |
+
Examples: a roof, a slice of pizza, a traffic sign, a sail, a mountain, a sandwich
|
| 20 |
+
Look around: Can you find something triangular?
|
| 21 |
+
|
| 22 |
+
Rectangle - Long square
|
| 23 |
+
A rectangle has four corners like a square, but two sides are longer!
|
| 24 |
+
Examples: a door, a book, a phone, a table, a flag, a chocolate bar
|
| 25 |
+
Look around: Can you find something rectangular?
|
| 26 |
+
|
| 27 |
+
Oval - Stretched circle
|
| 28 |
+
An oval is like a circle that got stretched. It's round but longer!
|
| 29 |
+
Examples: an egg, a football, a watermelon, a mirror, a leaf
|
| 30 |
+
Look around: Can you find something oval?
|
| 31 |
+
|
| 32 |
+
Star - Pointy shape
|
| 33 |
+
A star has five points that stick out. It sparkles in the sky!
|
| 34 |
+
Examples: stars in the sky, a starfish, a star cookie, a badge, a decoration
|
| 35 |
+
Look around: Can you find something star-shaped?
|
| 36 |
+
|
| 37 |
+
Heart - Love shape
|
| 38 |
+
A heart has two rounded parts at the top. It means love!
|
| 39 |
+
Examples: a heart on a card, a heart cookie, a heart decoration, a leaf
|
| 40 |
+
Look around: Can you find something heart-shaped?
|
| 41 |
+
|
| 42 |
+
Shape Activities:
|
| 43 |
+
1. Shape Hunt: Look around your room and find different shapes
|
| 44 |
+
2. Draw shapes: Use crayons to draw circles, squares, and triangles
|
| 45 |
+
3. Shape sorting: Group objects by their shapes
|
| 46 |
+
4. Shape matching: Find objects that match a shape card
|
| 47 |
+
5. Build with shapes: Use blocks to make shapes
|
| 48 |
+
|
| 49 |
+
Shape Games:
|
| 50 |
+
- "I spy a circle!" - Take turns finding shapes
|
| 51 |
+
- Shape puzzles: Put shape pieces together
|
| 52 |
+
- Shape dance: Move your body to make shapes
|
| 53 |
+
- Shape stories: Tell stories about shapes
|
| 54 |
+
|
| 55 |
+
Combining Shapes:
|
| 56 |
+
- A house might have a square body and a triangle roof
|
| 57 |
+
- A car might have circles for wheels and rectangles for windows
|
| 58 |
+
- A snowman has three circles stacked on top of each other!
|
| 59 |
+
|
| 60 |
+
Remember: Shapes are everywhere! Learning about shapes helps us describe and understand the world around us. Look for shapes in nature, in your home, and in everything you see!
|
| 61 |
+
|
lessons/weather_and_seasons.txt
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Weather and Seasons - Early Childhood Education
|
| 2 |
+
|
| 3 |
+
This lesson introduces children to different types of weather and the four seasons in a fun and engaging way.
|
| 4 |
+
|
| 5 |
+
Types of Weather:
|
| 6 |
+
|
| 7 |
+
Sunny Day - Bright and warm
|
| 8 |
+
The sun is shining! It's a perfect day to play outside.
|
| 9 |
+
- The sky is blue
|
| 10 |
+
- The sun is bright and yellow
|
| 11 |
+
- It feels warm
|
| 12 |
+
- Great for playing in the park, going to the beach, or having a picnic
|
| 13 |
+
- We wear light clothes and maybe a hat
|
| 14 |
+
|
| 15 |
+
Rainy Day - Wet and cloudy
|
| 16 |
+
Rain falls from the clouds! Plants and flowers love the rain.
|
| 17 |
+
- The sky is gray and cloudy
|
| 18 |
+
- Raindrops fall from the sky
|
| 19 |
+
- We hear pitter-patter sounds
|
| 20 |
+
- We use umbrellas and raincoats
|
| 21 |
+
- Puddles form on the ground
|
| 22 |
+
- Great for jumping in puddles (with boots!)
|
| 23 |
+
|
| 24 |
+
Snowy Day - Cold and white
|
| 25 |
+
Snowflakes fall from the sky! Everything looks white and magical.
|
| 26 |
+
- The sky is gray
|
| 27 |
+
- Snowflakes fall gently
|
| 28 |
+
- Everything is covered in white
|
| 29 |
+
- It's very cold
|
| 30 |
+
- We wear warm coats, gloves, and boots
|
| 31 |
+
- Great for building snowmen and making snow angels
|
| 32 |
+
|
| 33 |
+
Windy Day - Air is moving
|
| 34 |
+
The wind blows! It moves leaves, flags, and our hair.
|
| 35 |
+
- We can feel the air moving
|
| 36 |
+
- Leaves dance in the wind
|
| 37 |
+
- Kites fly high in the sky
|
| 38 |
+
- Our hair moves
|
| 39 |
+
- Sometimes it's gentle, sometimes it's strong
|
| 40 |
+
|
| 41 |
+
Cloudy Day - Clouds cover the sky
|
| 42 |
+
Clouds fill the sky! They can be white, gray, or dark.
|
| 43 |
+
- The sky is covered with clouds
|
| 44 |
+
- The sun might be hiding
|
| 45 |
+
- Clouds can look like shapes (animals, objects)
|
| 46 |
+
- Sometimes clouds bring rain
|
| 47 |
+
|
| 48 |
+
The Four Seasons:
|
| 49 |
+
|
| 50 |
+
Spring - Season of new beginnings
|
| 51 |
+
Everything starts to grow and bloom!
|
| 52 |
+
- Weather: Mild and rainy
|
| 53 |
+
- Flowers bloom
|
| 54 |
+
- Baby animals are born
|
| 55 |
+
- Trees get new green leaves
|
| 56 |
+
- Days get longer
|
| 57 |
+
- We wear light jackets
|
| 58 |
+
- Activities: Planting seeds, flying kites, seeing flowers
|
| 59 |
+
|
| 60 |
+
Summer - Season of sunshine
|
| 61 |
+
The warmest season! Perfect for outdoor fun.
|
| 62 |
+
- Weather: Hot and sunny
|
| 63 |
+
- Long, bright days
|
| 64 |
+
- We wear shorts and t-shirts
|
| 65 |
+
- Ice cream tastes great!
|
| 66 |
+
- Activities: Swimming, playing at the beach, picnics, playing outside
|
| 67 |
+
- Many fruits are ready to eat
|
| 68 |
+
|
| 69 |
+
Fall (Autumn) - Season of change
|
| 70 |
+
Leaves change colors and fall from trees!
|
| 71 |
+
- Weather: Cool and crisp
|
| 72 |
+
- Leaves turn red, orange, and yellow
|
| 73 |
+
- Leaves fall from trees
|
| 74 |
+
- We wear sweaters
|
| 75 |
+
- Activities: Raking leaves, jumping in leaf piles, picking apples
|
| 76 |
+
- Animals prepare for winter
|
| 77 |
+
|
| 78 |
+
Winter - Season of snow and cold
|
| 79 |
+
The coldest season! Sometimes it snows.
|
| 80 |
+
- Weather: Cold, sometimes snowy
|
| 81 |
+
- Short days, long nights
|
| 82 |
+
- Trees have no leaves
|
| 83 |
+
- We wear warm coats, hats, and gloves
|
| 84 |
+
- Activities: Building snowmen, ice skating, drinking hot chocolate
|
| 85 |
+
- Some animals hibernate (sleep through winter)
|
| 86 |
+
|
| 87 |
+
Weather Activities:
|
| 88 |
+
1. Weather observation: Look outside and describe the weather
|
| 89 |
+
2. Weather calendar: Draw the weather each day
|
| 90 |
+
3. Weather sounds: Listen to rain, wind, or thunder
|
| 91 |
+
4. Dress for weather: Practice choosing the right clothes
|
| 92 |
+
5. Weather stories: Tell stories about different weather
|
| 93 |
+
|
| 94 |
+
Season Activities:
|
| 95 |
+
1. Season sorting: Match activities to seasons
|
| 96 |
+
2. Season art: Draw pictures of each season
|
| 97 |
+
3. Season songs: Sing songs about seasons
|
| 98 |
+
4. Nature walks: Observe changes in nature
|
| 99 |
+
5. Seasonal foods: Learn about foods that grow in each season
|
| 100 |
+
|
| 101 |
+
Weather Safety:
|
| 102 |
+
- On sunny days: Wear sunscreen and a hat
|
| 103 |
+
- On rainy days: Use an umbrella and stay dry
|
| 104 |
+
- On snowy days: Dress warmly and be careful on ice
|
| 105 |
+
- On windy days: Be careful of flying objects
|
| 106 |
+
- During storms: Stay inside and stay safe
|
| 107 |
+
|
| 108 |
+
Remember: Weather changes every day, and seasons change throughout the year. Each type of weather and each season is special and brings different fun activities!
|
| 109 |
+
|
modal/invoke.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import modal
|
| 2 |
+
|
| 3 |
+
APP_NAME = "llm-server"
|
| 4 |
+
|
| 5 |
+
ENABLE_STREAMING = True
|
| 6 |
+
SYSTEM_PROMPT = "You are a friendly Chatbot. Please respond in the same language as the user."
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
VLLMModel = modal.Cls.from_name(APP_NAME, "VLLMModel")
|
| 10 |
+
model = VLLMModel()
|
| 11 |
+
|
| 12 |
+
chat_history = []
|
| 13 |
+
chat_history.append(
|
| 14 |
+
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]}
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# User prompt
|
| 18 |
+
user_prompt = "Hi!"
|
| 19 |
+
print(f"USER: {user_prompt}\n")
|
| 20 |
+
chat_history.append(
|
| 21 |
+
{"role": "user", "content": [{"type": "text", "text": user_prompt}]}
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
print("Calling chat function...")
|
| 25 |
+
|
| 26 |
+
# AI response
|
| 27 |
+
if ENABLE_STREAMING:
|
| 28 |
+
"""Streaming version"""
|
| 29 |
+
print("AI: ", end="", flush=True)
|
| 30 |
+
response = ""
|
| 31 |
+
for chunk in model.generate_stream.remote_gen(chat_history):
|
| 32 |
+
print(chunk, end="", flush=True)
|
| 33 |
+
response += chunk
|
| 34 |
+
print()
|
| 35 |
+
|
| 36 |
+
else:
|
| 37 |
+
"""Non-streaming version"""
|
| 38 |
+
response = model.generate.remote(chat_history)
|
| 39 |
+
print("AI:", response)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
chat_history.append(
|
| 43 |
+
{"role": "assistant", "content": [{"type": "text", "text": response}]}
|
| 44 |
+
)
|
modal/main.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
|
| 3 |
+
from huggingface_hub import snapshot_download
|
| 4 |
+
import vllm
|
| 5 |
+
|
| 6 |
+
import modal
|
| 7 |
+
|
| 8 |
+
APP_NAME = "llm-server"
|
| 9 |
+
VOLUME_NAME = APP_NAME + "-volume"
|
| 10 |
+
MOUNT_VOLUME = modal.Volume.from_name(VOLUME_NAME, create_if_missing=True)
|
| 11 |
+
MOUNT_DIR = "/data"
|
| 12 |
+
|
| 13 |
+
# Model identifier for the Hugging Face model
|
| 14 |
+
# NOTE: Gemma-3 GGUF models are not supported by vLLM yet (2025-06-10).
|
| 15 |
+
# NOTE: vLLM allocate all GPU memory according to the value specified by `gpu_memory_utilization` at initialization.
|
| 16 |
+
# https://huggingface.co/google/gemma-3-4b-it
|
| 17 |
+
MODEL_IDENTIFIER = "google/gemma-3-4b-it" # GPU memory requirements: 10GB when MAX_MODEL_TOKENS=2k, 20GB when MAX_MODEL_TOKENS=128k
|
| 18 |
+
# https://huggingface.co/google/gemma-3-12b-it
|
| 19 |
+
# MODEL_IDENTIFIER = "google/gemma-3-12b-it"
|
| 20 |
+
# https://huggingface.co/google/gemma-3-27b-it
|
| 21 |
+
# MODEL_IDENTIFIER = "google/gemma-3-27b-it"
|
| 22 |
+
|
| 23 |
+
# https://modal.com/docs/guide/gpu#specifying-gpu-type
|
| 24 |
+
GPU_NAME = "A100-40GB"
|
| 25 |
+
GPU_NUM = 1 # Number of GPUs to use
|
| 26 |
+
GPU = f"{GPU_NAME}:{GPU_NUM}"
|
| 27 |
+
|
| 28 |
+
# https://modal.com/pricing
|
| 29 |
+
# | GPU | Memory | Price |
|
| 30 |
+
# |-----------|--------|----------|
|
| 31 |
+
# | B200 | 180 GB | $6.25 /h |
|
| 32 |
+
# | H200 | 141 GB | $4.54 /h |
|
| 33 |
+
# | H100 | 80 GB | $3.95 /h |
|
| 34 |
+
# | A100-80GB | 80 GB | $2.50 /h |
|
| 35 |
+
# | A100-40GB | 40 GB | $2.10 /h |
|
| 36 |
+
# | L40S | 48 GB | $1.95 /h |
|
| 37 |
+
# | A10G | 24 GB | $1.10 /h |
|
| 38 |
+
# | L4 | 24 GB | $0.80 /h |
|
| 39 |
+
# | T4 | 16 GB | $0.59 /h |
|
| 40 |
+
|
| 41 |
+
# MAX_MODEL_TOKENS >= Input + Output
|
| 42 |
+
MAX_MODEL_TOKENS = 128 * 1024 # Gemma-3-4B~ has 128K context length
|
| 43 |
+
MAX_OUTPUT_TOKENS = 512
|
| 44 |
+
|
| 45 |
+
image = (
|
| 46 |
+
# https://hub.docker.com/r/nvidia/cuda/tags?name=12.8
|
| 47 |
+
# https://hub.docker.com/layers/nvidia/cuda/12.8.1-devel-ubuntu24.04
|
| 48 |
+
modal.Image.from_registry("nvidia/cuda:12.8.1-devel-ubuntu24.04", add_python="3.12")
|
| 49 |
+
.pip_install(
|
| 50 |
+
[
|
| 51 |
+
"accelerate>=1.7.0",
|
| 52 |
+
"bitsandbytes>=0.46.0",
|
| 53 |
+
"sentencepiece>=0.2.0",
|
| 54 |
+
"torch==2.8.0",
|
| 55 |
+
"transformers>=4.52.4",
|
| 56 |
+
"vllm>=0.9.0.1",
|
| 57 |
+
]
|
| 58 |
+
)
|
| 59 |
+
.env(
|
| 60 |
+
{
|
| 61 |
+
"HF_HOME": MOUNT_DIR + "/huggingface",
|
| 62 |
+
"VLLM_CACHE_ROOT": MOUNT_DIR + "/vllm",
|
| 63 |
+
}
|
| 64 |
+
)
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
app = modal.App(APP_NAME, image=image)
|
| 68 |
+
|
| 69 |
+
# NOTE: `@app.cls`, `@modal.enter()`, and `@modal.method()` are used like `@app.function()`
|
| 70 |
+
# https://modal.com/docs/guide/lifecycle-functions
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@app.cls(
|
| 74 |
+
gpu=GPU,
|
| 75 |
+
image=image,
|
| 76 |
+
volumes={MOUNT_DIR: MOUNT_VOLUME},
|
| 77 |
+
secrets=[modal.Secret.from_name("huggingface-secret")],
|
| 78 |
+
scaledown_window=15 * 60,
|
| 79 |
+
timeout=30 * 60,
|
| 80 |
+
)
|
| 81 |
+
class VLLMModel:
|
| 82 |
+
|
| 83 |
+
@modal.enter()
|
| 84 |
+
def setup(self):
|
| 85 |
+
# Ensure the cache volume is the latest
|
| 86 |
+
MOUNT_VOLUME.reload()
|
| 87 |
+
|
| 88 |
+
# NOTE:"HF_TOKEN" environment variable is required for Hugging Face authentication
|
| 89 |
+
|
| 90 |
+
# self._download_model(MODEL_IDENTIFIER) # This is not needed because vLLM can download the model automatically.
|
| 91 |
+
|
| 92 |
+
self._load_model()
|
| 93 |
+
|
| 94 |
+
# Commit the volume to ensure the model is saved
|
| 95 |
+
MOUNT_VOLUME.commit()
|
| 96 |
+
|
| 97 |
+
def _download_model(self, repo_id: str):
|
| 98 |
+
"""Download the model from Hugging Face if not already present."""
|
| 99 |
+
# Ensure the cache volume is the latest
|
| 100 |
+
MOUNT_VOLUME.reload()
|
| 101 |
+
|
| 102 |
+
snapshot_download(
|
| 103 |
+
repo_id=repo_id,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# Commit downloaded model
|
| 107 |
+
MOUNT_VOLUME.commit()
|
| 108 |
+
|
| 109 |
+
def _load_model(self):
|
| 110 |
+
|
| 111 |
+
self.llm = vllm.LLM(
|
| 112 |
+
model=MODEL_IDENTIFIER,
|
| 113 |
+
tensor_parallel_size=1,
|
| 114 |
+
dtype="auto",
|
| 115 |
+
max_model_len=MAX_MODEL_TOKENS,
|
| 116 |
+
gpu_memory_utilization=0.9,
|
| 117 |
+
trust_remote_code=True,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# Show GPU information
|
| 121 |
+
subprocess.run(["nvidia-smi"])
|
| 122 |
+
|
| 123 |
+
@modal.method()
|
| 124 |
+
def generate(self, chat_history):
|
| 125 |
+
"""Generate a response"""
|
| 126 |
+
formatted_text = self._get_formatted_text(chat_history)
|
| 127 |
+
|
| 128 |
+
input_token_len = self._check_input_length(formatted_text)
|
| 129 |
+
if input_token_len + MAX_OUTPUT_TOKENS > MAX_MODEL_TOKENS:
|
| 130 |
+
raise ValueError(
|
| 131 |
+
f"Input length exceeds the maximum allowed tokens: {MAX_MODEL_TOKENS}. "
|
| 132 |
+
f"Current input length: {input_token_len} tokens."
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
sampling_params = self._get_sampling_params()
|
| 136 |
+
|
| 137 |
+
outputs = self.llm.generate([formatted_text], sampling_params)
|
| 138 |
+
response = outputs[0].outputs[0].text
|
| 139 |
+
|
| 140 |
+
return response
|
| 141 |
+
|
| 142 |
+
@modal.method()
|
| 143 |
+
def generate_stream(self, chat_history):
|
| 144 |
+
"""
|
| 145 |
+
Generate a streaming response
|
| 146 |
+
NOTE: This function may NOT generate streaming output as expected
|
| 147 |
+
"""
|
| 148 |
+
formatted_text = self._get_formatted_text(chat_history)
|
| 149 |
+
|
| 150 |
+
input_token_len = self._check_input_length(formatted_text)
|
| 151 |
+
if input_token_len + MAX_OUTPUT_TOKENS > MAX_MODEL_TOKENS:
|
| 152 |
+
raise ValueError(
|
| 153 |
+
f"Input length exceeds the maximum allowed tokens: {MAX_MODEL_TOKENS}. "
|
| 154 |
+
f"Current input length: {input_token_len} tokens."
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
sampling_params = self._get_sampling_params()
|
| 158 |
+
|
| 159 |
+
# Streaming generation with vLLM
|
| 160 |
+
for output in self.llm.generate([formatted_text], sampling_params):
|
| 161 |
+
for completion_output in output.outputs:
|
| 162 |
+
yield completion_output.text
|
| 163 |
+
|
| 164 |
+
def _get_formatted_text(self, chat_history):
|
| 165 |
+
"""Format the chat history"""
|
| 166 |
+
tokenizer = self.llm.get_tokenizer()
|
| 167 |
+
return tokenizer.apply_chat_template(
|
| 168 |
+
chat_history,
|
| 169 |
+
tokenize=False,
|
| 170 |
+
add_generation_prompt=True,
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
def _check_input_length(self, formatted_text):
|
| 174 |
+
tokenizer = self.llm.get_tokenizer()
|
| 175 |
+
input_token_len = len(tokenizer(formatted_text)["input_ids"])
|
| 176 |
+
return input_token_len
|
| 177 |
+
|
| 178 |
+
def _get_sampling_params(self):
|
| 179 |
+
"""Get sampling parameters for generation"""
|
| 180 |
+
return vllm.SamplingParams(
|
| 181 |
+
temperature=1.0,
|
| 182 |
+
top_k=50,
|
| 183 |
+
top_p=1.0,
|
| 184 |
+
max_tokens=MAX_OUTPUT_TOKENS,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@app.local_entrypoint()
|
| 189 |
+
def main():
|
| 190 |
+
SYSTEM_PROMPT = (
|
| 191 |
+
"You are a friendly Chatbot. Please respond in the same language as the user."
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# Initialize chat history list
|
| 195 |
+
chat_history = []
|
| 196 |
+
chat_history.append(
|
| 197 |
+
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
user_prompt = "Hi!"
|
| 201 |
+
print(f"USER: {user_prompt}\n")
|
| 202 |
+
chat_history.append(
|
| 203 |
+
{
|
| 204 |
+
"role": "user",
|
| 205 |
+
"content": [
|
| 206 |
+
# {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"},
|
| 207 |
+
{"type": "text", "text": user_prompt}
|
| 208 |
+
],
|
| 209 |
+
}
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
model = VLLMModel()
|
| 213 |
+
|
| 214 |
+
# Call non-streaming function
|
| 215 |
+
response = model.generate.remote(chat_history)
|
| 216 |
+
print("AI:", response)
|
| 217 |
+
chat_history.append(
|
| 218 |
+
{"role": "assistant", "content": [{"type": "text", "text": response}]}
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
user_prompt = "What is your name?"
|
| 222 |
+
print(f"USER: {user_prompt}\n")
|
| 223 |
+
chat_history.append(
|
| 224 |
+
{
|
| 225 |
+
"role": "user",
|
| 226 |
+
"content": [{"type": "text", "text": user_prompt}],
|
| 227 |
+
}
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Call streaming function
|
| 231 |
+
print("AI: ", end="", flush=True)
|
| 232 |
+
response = ""
|
| 233 |
+
for chunk in model.generate_stream.remote_gen(chat_history):
|
| 234 |
+
print(chunk, end="", flush=True)
|
| 235 |
+
response += chunk
|
| 236 |
+
print()
|
pyproject.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "learnbee-mcp"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Educational MCP for early childhood education (ages 3-6)"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"gradio[mcp]>=5.33.0",
|
| 9 |
+
"openai>=1.0.0",
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
[dependency-groups]
|
| 13 |
+
dev = [
|
| 14 |
+
"accelerate>=1.7.0",
|
| 15 |
+
"bitsandbytes>=0.46.0",
|
| 16 |
+
"sentencepiece>=0.2.0",
|
| 17 |
+
"torch>=2.8.0",
|
| 18 |
+
"transformers>=4.52.4",
|
| 19 |
+
"vllm>=0.9.0.1",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
[build-system]
|
| 23 |
+
requires = ["setuptools>=61"]
|
| 24 |
+
build-backend = "setuptools.build_meta"
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openai==2.8.0
|
| 2 |
+
gradio[mcp]>=5.49.0
|
src/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
src/learnbee/__init__.py
ADDED
|
File without changes
|
src/learnbee/llm_call.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Generator
|
| 3 |
+
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
|
| 7 |
+
# Load environment variables from .env file
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class LLMCall:
|
| 12 |
+
"""LLM client using OpenAI API for educational tutoring."""
|
| 13 |
+
|
| 14 |
+
def __init__(self, model: str = "gpt-4o-mini"):
|
| 15 |
+
"""
|
| 16 |
+
Initialize the LLM client.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
model (str): The OpenAI model to use. Defaults to "gpt-4o-mini".
|
| 20 |
+
"""
|
| 21 |
+
self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 22 |
+
self.model = model
|
| 23 |
+
|
| 24 |
+
def _convert_history(self, message: str, gradio_history: list) -> list[dict]:
|
| 25 |
+
"""Convert Gradio history format to OpenAI API format."""
|
| 26 |
+
messages = []
|
| 27 |
+
for h in gradio_history:
|
| 28 |
+
# Skip system messages in history (we'll add it separately)
|
| 29 |
+
if h.get("role") == "system":
|
| 30 |
+
continue
|
| 31 |
+
messages.append(
|
| 32 |
+
{
|
| 33 |
+
"role": h.get("role", "user"),
|
| 34 |
+
"content": h.get("content", ""),
|
| 35 |
+
}
|
| 36 |
+
)
|
| 37 |
+
# Add current user input
|
| 38 |
+
messages.append({"role": "user", "content": message})
|
| 39 |
+
return messages
|
| 40 |
+
|
| 41 |
+
def respond(
|
| 42 |
+
self,
|
| 43 |
+
message: str,
|
| 44 |
+
history: list,
|
| 45 |
+
system_prompt: str = None,
|
| 46 |
+
tutor_name: str = None,
|
| 47 |
+
difficulty_level: str = "beginner",
|
| 48 |
+
) -> Generator[str, None, None]:
|
| 49 |
+
"""
|
| 50 |
+
Generate a response to the user message using the OpenAI LLM.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
message (str): The user's message.
|
| 54 |
+
history (list): The conversation history.
|
| 55 |
+
system_prompt (str): The system prompt (optional, will be constructed if not provided).
|
| 56 |
+
tutor_name (str): The name of the tutor.
|
| 57 |
+
difficulty_level (str): The difficulty level (beginner, intermediate, advanced).
|
| 58 |
+
|
| 59 |
+
Yields:
|
| 60 |
+
str: Streaming response chunks.
|
| 61 |
+
"""
|
| 62 |
+
# Construct messages for OpenAI API
|
| 63 |
+
messages = []
|
| 64 |
+
|
| 65 |
+
# Add system prompt
|
| 66 |
+
if system_prompt:
|
| 67 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 68 |
+
|
| 69 |
+
# Add conversation history (excluding system messages)
|
| 70 |
+
for h in history:
|
| 71 |
+
if h.get("role") != "system":
|
| 72 |
+
messages.append(
|
| 73 |
+
{
|
| 74 |
+
"role": h.get("role", "user"),
|
| 75 |
+
"content": h.get("content", ""),
|
| 76 |
+
}
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Add current user message
|
| 80 |
+
messages.append({"role": "user", "content": message})
|
| 81 |
+
|
| 82 |
+
# Make streaming API call with educational-appropriate settings
|
| 83 |
+
# Lower temperature for more consistent, educational responses
|
| 84 |
+
stream = self.client.chat.completions.create(
|
| 85 |
+
model=self.model,
|
| 86 |
+
messages=messages,
|
| 87 |
+
stream=True,
|
| 88 |
+
temperature=0.6, # Balanced: creative enough for engagement, consistent for learning
|
| 89 |
+
max_tokens=500, # Limit response length for age-appropriate brevity
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
response = ""
|
| 93 |
+
for chunk in stream:
|
| 94 |
+
if chunk.choices[0].delta.content is not None:
|
| 95 |
+
content = chunk.choices[0].delta.content
|
| 96 |
+
response += content
|
| 97 |
+
yield response
|
| 98 |
+
|
| 99 |
+
def extract_key_concepts(self, lesson_content: str) -> list[str]:
|
| 100 |
+
"""
|
| 101 |
+
Extract key concepts from the lesson content.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
lesson_content (str): The content of the lesson.
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
list[str]: A list of 5 to 10 key concepts from the lesson.
|
| 108 |
+
"""
|
| 109 |
+
system_prompt = (
|
| 110 |
+
"Your task is to extract 5 to 10 key educational concepts from the provided lesson content. "
|
| 111 |
+
"These concepts should be appropriate for early childhood education (ages 3-6). "
|
| 112 |
+
"Return only the concept names, one per line. "
|
| 113 |
+
"Do not include any additional text, explanations, or numbering. "
|
| 114 |
+
"Each concept should be a simple, clear phrase that a child could understand. "
|
| 115 |
+
"Example output:\n"
|
| 116 |
+
"Colors\n"
|
| 117 |
+
"Numbers\n"
|
| 118 |
+
"Shapes\n"
|
| 119 |
+
"Animals\n"
|
| 120 |
+
"Nature\n"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
messages = [
|
| 124 |
+
{"role": "system", "content": system_prompt},
|
| 125 |
+
{"role": "user", "content": lesson_content},
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
response = self.client.chat.completions.create(
|
| 129 |
+
model=self.model,
|
| 130 |
+
messages=messages,
|
| 131 |
+
temperature=0.3,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
content = response.choices[0].message.content
|
| 135 |
+
print("Response from LLM:", content)
|
| 136 |
+
|
| 137 |
+
# Split the response by new lines and strip whitespace
|
| 138 |
+
concepts = [concept.strip() for concept in content.split("\n") if concept.strip()]
|
| 139 |
+
|
| 140 |
+
# Limit to 10 concepts
|
| 141 |
+
return concepts[:10]
|
| 142 |
+
|
| 143 |
+
def generate_lesson_introduction(
|
| 144 |
+
self, lesson_content: str, lesson_name: str, concepts: list[str]
|
| 145 |
+
) -> str:
|
| 146 |
+
"""
|
| 147 |
+
Generate an educational introduction for the lesson including:
|
| 148 |
+
- A brief summary of the activity
|
| 149 |
+
- Key concepts
|
| 150 |
+
- Example questions to guide the child
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
lesson_content (str): The content of the lesson.
|
| 154 |
+
lesson_name (str): The name of the lesson.
|
| 155 |
+
concepts (list[str]): List of key concepts extracted from the lesson.
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
str: A formatted introduction with summary, concepts, and example questions.
|
| 159 |
+
"""
|
| 160 |
+
concepts_text = ", ".join(concepts[:8]) # Show up to 8 concepts
|
| 161 |
+
|
| 162 |
+
system_prompt = (
|
| 163 |
+
"You are an educational expert creating an introduction for a lesson for children ages 3-12. "
|
| 164 |
+
"Create a friendly, engaging introduction that includes:\n\n"
|
| 165 |
+
"1. A brief, exciting summary of what the child will learn (2-3 sentences, very simple language)\n"
|
| 166 |
+
"2. A list of the key concepts they'll explore\n"
|
| 167 |
+
"3. 2-3 example questions that the tutor could ask to start the conversation and guide the child\n\n"
|
| 168 |
+
"Format your response as follows:\n"
|
| 169 |
+
"SUMMARY:\n"
|
| 170 |
+
"[Brief summary here]\n\n"
|
| 171 |
+
"KEY CONCEPTS:\n"
|
| 172 |
+
"[List concepts here, one per line with a bullet point]\n\n"
|
| 173 |
+
"EXAMPLE QUESTIONS TO GET STARTED:\n"
|
| 174 |
+
"[2-3 engaging questions, one per line with a bullet point]\n\n"
|
| 175 |
+
"Use very simple, age-appropriate language. Make it fun and exciting! "
|
| 176 |
+
"The questions should be open-ended and encourage exploration."
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
user_prompt = (
|
| 180 |
+
f"Lesson Name: {lesson_name}\n\n"
|
| 181 |
+
f"Key Concepts: {concepts_text}\n\n"
|
| 182 |
+
f"Lesson Content:\n{lesson_content[:2000]}\n\n"
|
| 183 |
+
"Create an engaging introduction for this lesson."
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
messages = [
|
| 187 |
+
{"role": "system", "content": system_prompt},
|
| 188 |
+
{"role": "user", "content": user_prompt},
|
| 189 |
+
]
|
| 190 |
+
|
| 191 |
+
response = self.client.chat.completions.create(
|
| 192 |
+
model=self.model,
|
| 193 |
+
messages=messages,
|
| 194 |
+
temperature=0.7, # Slightly higher for creativity
|
| 195 |
+
max_tokens=400,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
introduction = response.choices[0].message.content
|
| 199 |
+
return introduction
|
src/learnbee/mcp_client.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
from mcp import ClientSession
|
| 6 |
+
from mcp.client.sse import sse_client
|
| 7 |
+
|
| 8 |
+
MCP_SERVER_URL = os.environ.get(
|
| 9 |
+
"MCP_SERVER_URL", "http://localhost:7860/gradio_api/mcp/sse"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class MCPClient:
|
| 14 |
+
|
| 15 |
+
async def with_session(self, func):
|
| 16 |
+
"""
|
| 17 |
+
Create a session with the MCP server and execute the provided function.
|
| 18 |
+
- See: https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse
|
| 19 |
+
"""
|
| 20 |
+
async with sse_client(MCP_SERVER_URL) as streams:
|
| 21 |
+
async with ClientSession(streams[0], streams[1]) as session:
|
| 22 |
+
await session.initialize()
|
| 23 |
+
return await func(session)
|
| 24 |
+
|
| 25 |
+
async def list(self):
|
| 26 |
+
"""List available tools from the MCP server."""
|
| 27 |
+
async def _list(session):
|
| 28 |
+
response = await session.list_tools()
|
| 29 |
+
return response
|
| 30 |
+
|
| 31 |
+
return await self.with_session(_list)
|
| 32 |
+
|
| 33 |
+
async def get_book_list(self) -> str:
|
| 34 |
+
"""Get the list of books available on the MCP server."""
|
| 35 |
+
async def _get_book_list(session):
|
| 36 |
+
tool_name = "get_book_list"
|
| 37 |
+
response = await session.call_tool(tool_name)
|
| 38 |
+
if response.isError:
|
| 39 |
+
raise ValueError(f"Error calling tool: {tool_name}")
|
| 40 |
+
return response.content[0].text
|
| 41 |
+
|
| 42 |
+
return await self.with_session(_get_book_list)
|
| 43 |
+
|
| 44 |
+
async def get_book_content(self, book_name: str, max_length: int = 0) -> str:
|
| 45 |
+
"""Get the content of a book from the MCP server."""
|
| 46 |
+
async def _get_book_content(session):
|
| 47 |
+
tool_name = "get_book_content"
|
| 48 |
+
input_data = {"book_name": book_name, "max_length": max_length}
|
| 49 |
+
response = await session.call_tool(tool_name, input_data)
|
| 50 |
+
if response.isError:
|
| 51 |
+
raise ValueError(f"Error calling tool: {tool_name}")
|
| 52 |
+
return response.content[0].text
|
| 53 |
+
|
| 54 |
+
return await self.with_session(_get_book_content)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
async def main():
|
| 58 |
+
mcp_client = MCPClient()
|
| 59 |
+
|
| 60 |
+
tools = await mcp_client.list()
|
| 61 |
+
print("Available tools:")
|
| 62 |
+
print("=" * 20)
|
| 63 |
+
for tool in tools.tools:
|
| 64 |
+
print(f"Name: {tool.name}")
|
| 65 |
+
print(f"Description: {tool.description}")
|
| 66 |
+
print(f"Input Schema: {tool.inputSchema}")
|
| 67 |
+
print(f"Annotations: {tool.annotations}")
|
| 68 |
+
print("-" * 20)
|
| 69 |
+
|
| 70 |
+
book_list_str = await mcp_client.get_book_list()
|
| 71 |
+
book_list = json.loads(book_list_str)
|
| 72 |
+
print(f"Number of books available: {len(book_list)}")
|
| 73 |
+
|
| 74 |
+
book_name = book_list[0]
|
| 75 |
+
book_content = await mcp_client.get_book_content(book_name, max_length=100)
|
| 76 |
+
print(f"Content of the book '{book_name}':")
|
| 77 |
+
print(book_content + "...")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
asyncio.run(main())
|
src/learnbee/mcp_server.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from learnbee.llm_call import LLMCall
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def get_lesson_list() -> str:
|
| 8 |
+
"""
|
| 9 |
+
Get list of available lessons.
|
| 10 |
+
|
| 11 |
+
Returns:
|
| 12 |
+
str: JSON string containing the list of lesson names.
|
| 13 |
+
"""
|
| 14 |
+
lessons_dir = Path("./lessons")
|
| 15 |
+
if not lessons_dir.exists():
|
| 16 |
+
return json.dumps("Error: Lessons directory not found.")
|
| 17 |
+
|
| 18 |
+
text_files = []
|
| 19 |
+
for file in lessons_dir.iterdir():
|
| 20 |
+
if file.is_file() and file.suffix.lower() == ".txt":
|
| 21 |
+
text_files.append(file.stem)
|
| 22 |
+
|
| 23 |
+
return json.dumps(sorted(text_files))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def get_lesson_content(lesson_name: str, max_length: int = 0) -> str:
|
| 27 |
+
"""
|
| 28 |
+
Get the content of a lesson.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
lesson_name (str): The name of the lesson (without .txt extension).
|
| 32 |
+
max_length (int): The maximum length of the content to return. If 0, return the full content.
|
| 33 |
+
Returns:
|
| 34 |
+
str: The content of the lesson, or an error message if the lesson is not found.
|
| 35 |
+
"""
|
| 36 |
+
lessons_dir = Path("./lessons")
|
| 37 |
+
lesson_file = lessons_dir / f"{lesson_name}.txt"
|
| 38 |
+
if not lesson_file.exists():
|
| 39 |
+
return f"Error: Lesson '{lesson_name}' not found."
|
| 40 |
+
|
| 41 |
+
with open(lesson_file, "r", encoding="utf-8") as f:
|
| 42 |
+
content = f.read()
|
| 43 |
+
|
| 44 |
+
if not max_length:
|
| 45 |
+
return content
|
| 46 |
+
else:
|
| 47 |
+
return content[:max_length]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def get_lesson_introduction(lesson_name: str) -> str:
|
| 51 |
+
"""
|
| 52 |
+
Get an educational introduction for a lesson including summary, key concepts, and example questions.
|
| 53 |
+
This function serves as an MCP tool to help guide children with their first message.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
lesson_name (str): The name of the lesson (without .txt extension).
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
str: A formatted introduction with summary, concepts, and example questions, or an error message.
|
| 60 |
+
"""
|
| 61 |
+
lessons_dir = Path("./lessons")
|
| 62 |
+
lesson_file = lessons_dir / f"{lesson_name}.txt"
|
| 63 |
+
if not lesson_file.exists():
|
| 64 |
+
return f"Error: Lesson '{lesson_name}' not found."
|
| 65 |
+
|
| 66 |
+
# Get lesson content
|
| 67 |
+
lesson_content = get_lesson_content(lesson_name, max_length=50000)
|
| 68 |
+
|
| 69 |
+
if lesson_content.startswith("Error:"):
|
| 70 |
+
return lesson_content
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
# Extract key concepts
|
| 74 |
+
call_llm = LLMCall()
|
| 75 |
+
concepts = call_llm.extract_key_concepts(lesson_content)
|
| 76 |
+
|
| 77 |
+
if not concepts:
|
| 78 |
+
return f"Error: Could not extract key concepts from lesson '{lesson_name}'."
|
| 79 |
+
|
| 80 |
+
# Generate introduction
|
| 81 |
+
introduction = call_llm.generate_lesson_introduction(
|
| 82 |
+
lesson_content, lesson_name, concepts
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
return introduction
|
| 86 |
+
except Exception as e:
|
| 87 |
+
return f"Error generating introduction: {str(e)}"
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
print("Available lessons:", get_lesson_list())
|
| 92 |
+
|
| 93 |
+
lesson_name = "example_lesson"
|
| 94 |
+
lesson_content = get_lesson_content(lesson_name)
|
| 95 |
+
print(f"Start of '{lesson_name}':\n{lesson_content[:500]}...\n")
|
| 96 |
+
print(f"End of '{lesson_name}':\n{lesson_content[-500:]}\n")
|