diff --git a/README.md b/README.md index 26cb9cc..dec5545 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,51 @@ -# Recommender System -### Author & Maintainer: Rafipilot -Maintainer: [Rafipilot](https://github.com/Rafipilot), rafayel.latif@gmail.com +# AO Labs Recommender System +A recommender system built with AO Labs' post-training learning architecture. The system learns from user behavior in real-time, providing increasingly personalized recommendations. -## Description -This is a basic video recommender system designed to offer personalized video recommendations. Unlike many modern systems that rely on collaborative filteringβ€”resulting in suggestions based on broad user trendsβ€”this system aims to provide more unique and individually tailored recommendations. By giving users greater control over how they provide feedback, the system helps break free from repetitive content and exposes users to a wider range of videos, making it easier to find content that truly resonates with their tastes. +## Domains -## Installation/Setup +This recommender has been adapted to multiple domains: -### Local Environment -If you plan to run the app in a conda or virtual environment, make sure to set up your environment following the respective instructions for those tools. +- **YouTube** (original) - Video recommendations based on genre and mood +- **Programming Education** - Coding tutorial and course recommendations based on skill level and interests -1. Install the requirements: +## Quick Start - ```bash - pip install -r requirements.txt - ``` - -2. Install ao_core and ao_arch with the pip install git+ method which lets you install python code from git repos. - - ```bash - pip install git+https://github.com/aolabsai/ao_arch git+https://github.com/aolabsai/ao_core - ``` - -3. Run the application with the following command: +### YouTube Recommender +```bash +pip install -r requirements.txt +streamlit run main.py +``` - ```bash - streamlit run recommender.py - ``` +### Programming Education Recommender +```bash +pip install -r requirements.txt +streamlit run coding_edu_recommender.py +``` -4. Once running, the app will be accessible at `localhost:8501`. +## Programming Education Domain +The coding education recommender suggests programming tutorials, courses, and practice resources based on: -### Docker Installation +- **Topic**: Web Dev, Mobile, Data Science, AI/ML, Systems, DevOps, Game Dev, Blockchain, IoT, Security +- **Difficulty**: Beginner, Intermediate, Advanced +- **Format**: Video, Interactive/Hands-on +- **Experience**: No experience, Some basics, Experienced, Senior -1) Generate a GitHub Personal Access Token to ao_core - Go to https://github.com/settings/tokens?type=beta +The system uses AO Labs' binary encoding architecture with 8-bit input vectors to represent user preferences and learn from feedback. -2) Clone this repo and create a `.env` file in your local clone where you'll add the PAT as follows: - `ao_github_PAT=token_goes_here` - No spaces! See `.env_example`. +## Architecture -3) In a Git Bash terminal, build and run the Dockerfile with these commands: -```shell -export DOCKER_BUILDKIT=1 +Each domain has its own architecture file: +- `arch__Recommender.py` - YouTube recommender architecture +- `arch__CodingEduRecommender.py` - Programming education architecture -docker build --secret id=env,src=.env -t "ao_app" . +## Testing -docker run -p 8501:8501 streamlit +```bash +python -m pytest tests/test_coding_edu.py -v ``` -You're done! Access the app at `localhost:8501` in your browser. - -## Usage - -The recommender system works by loading a set of random video links. Once the user hits the Run button, a video will be shown, and the system will suggest whether it recommends the video or not. The user can then provide feedback using "pain" or "pleasure" signals to guide the recommendation process. Based on this feedback, the system adjusts its responses and suggests another video. This cycle continues, allowing for more accurate and personalized recommendations over time. - - -## Contributing - -Fork the repository, make your changes, and submit a pull request for review. - +## License +See original repository for license information. diff --git a/arch__CodingEduRecommender.py b/arch__CodingEduRecommender.py new file mode 100644 index 0000000..24ccfaf --- /dev/null +++ b/arch__CodingEduRecommender.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +Arch file for Programming/Coding Education Resource Recommender +Suggests programming tutorials, courses, and practice resources +based on the user's skill level, interests, and learning progress. +""" + +import ao_arch as ar + +description = "Programming Education Resource Recommender" + +# Input encoding: topic(3) + difficulty(2) + format(1) + experience(2) = 8 bits +# topic: web/mobile/data/AI/systems/DevOps/game/crypto/embedded/other +# difficulty: beginner/intermediate/advanced +# format: video/article/interactive +# experience: none/some/experienced +arch_i = [3, 2, 1, 2] # topic_binary + difficulty_binary + format_binary + experience_binary +arch_z = [10] # output: resource recommendation encoding +arch_c = [] +connector_function = "full_conn" + +arch = ar.Arch(arch_i, arch_z, arch_c, connector_function, description) diff --git a/coding_edu_domain.py b/coding_edu_domain.py new file mode 100644 index 0000000..57f9519 --- /dev/null +++ b/coding_edu_domain.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +""" +Programming Education Resource Domain Data +Catalog of coding learning resources for the recommender system. +""" + +# Topic categories (10 topics, 3-bit binary encoding) +TOPIC_MAP = { + 0: "Web Development", + 1: "Mobile Development", + 2: "Data Science", + 3: "AI/Machine Learning", + 4: "Systems Programming", + 5: "DevOps/Cloud", + 6: "Game Development", + 7: "Blockchain/Crypto", + 8: "Embedded/IoT", + 9: "Security" +} + +# Difficulty levels (2-bit encoding) +DIFFICULTY_MAP = { + 0: "Beginner", + 1: "Intermediate", + 2: "Advanced", + 3: "Expert" +} + +# Format types (1-bit encoding) +FORMAT_MAP = { + 0: "Video", + 1: "Interactive/Hands-on" +} + +# Experience levels (2-bit encoding) +EXPERIENCE_MAP = { + 0: "No prior experience", + 1: "Some programming (basics)", + 2: "Experienced developer", + 3: "Senior/Expert" +} + +# Resource catalog - 12 items covering diverse programming topics +RESOURCE_CATALOG = [ + { + "id": 0, + "title": "HTML & CSS Fundamentals", + "topic": "Web Development", + "difficulty": "Beginner", + "format": "Interactive", + "experience": "No prior experience", + "description": "Learn the building blocks of the web with hands-on exercises", + "url": "https://www.freecodecamp.org/learn/responsive-web-design/", + "duration": "300 hours", + "tags": ["html", "css", "responsive", "web"] + }, + { + "id": 1, + "title": "React Complete Guide", + "topic": "Web Development", + "difficulty": "Intermediate", + "format": "Video", + "experience": "Some programming (basics)", + "description": "Build modern web apps with React hooks, state management, and routing", + "url": "https://react.dev/learn", + "duration": "40 hours", + "tags": ["react", "javascript", "frontend", "hooks"] + }, + { + "id": 2, + "title": "Flutter Mobile App Development", + "topic": "Mobile Development", + "difficulty": "Intermediate", + "format": "Interactive", + "experience": "Some programming (basics)", + "description": "Build cross-platform mobile apps with Flutter and Dart", + "url": "https://flutter.dev/learn", + "duration": "30 hours", + "tags": ["flutter", "dart", "mobile", "cross-platform"] + }, + { + "id": 3, + "title": "Python for Data Analysis", + "topic": "Data Science", + "difficulty": "Beginner", + "format": "Interactive", + "experience": "No prior experience", + "description": "Learn pandas, numpy, and matplotlib for data analysis", + "url": "https://www.kaggle.com/learn/pandas", + "duration": "20 hours", + "tags": ["python", "pandas", "data-analysis", "visualization"] + }, + { + "id": 4, + "title": "Deep Learning Specialization", + "topic": "AI/Machine Learning", + "difficulty": "Advanced", + "format": "Video", + "experience": "Experienced developer", + "description": "Master neural networks, CNNs, RNNs, and transformers", + "url": "https://www.deeplearning.ai/courses/deep-learning-specialization/", + "duration": "120 hours", + "tags": ["deep-learning", "neural-networks", "cnn", "transformers"] + }, + { + "id": 5, + "title": "Rust Systems Programming", + "topic": "Systems Programming", + "difficulty": "Advanced", + "format": "Interactive", + "experience": "Experienced developer", + "description": "Learn Rust for safe, concurrent systems programming", + "url": "https://doc.rust-lang.org/book/", + "duration": "60 hours", + "tags": ["rust", "systems", "memory-safety", "concurrency"] + }, + { + "id": 6, + "title": "Docker & Kubernetes in Practice", + "topic": "DevOps/Cloud", + "difficulty": "Intermediate", + "format": "Interactive", + "experience": "Some programming (basics)", + "description": "Containerize apps and orchestrate with Kubernetes", + "url": "https://kubernetes.io/docs/tutorials/", + "duration": "40 hours", + "tags": ["docker", "kubernetes", "containers", "devops"] + }, + { + "id": 7, + "title": "Unity Game Development Bootcamp", + "topic": "Game Development", + "difficulty": "Beginner", + "format": "Video", + "experience": "No prior experience", + "description": "Build 2D and 3D games with Unity and C#", + "url": "https://learn.unity.com/", + "duration": "50 hours", + "tags": ["unity", "csharp", "gamedev", "3d"] + }, + { + "id": 8, + "title": "Smart Contract Development with Solidity", + "topic": "Blockchain/Crypto", + "difficulty": "Intermediate", + "format": "Interactive", + "experience": "Some programming (basics)", + "description": "Build decentralized applications on Ethereum", + "url": "https://cryptozombies.io/", + "duration": "20 hours", + "tags": ["solidity", "ethereum", "smart-contracts", "web3"] + }, + { + "id": 9, + "title": "ESP32 IoT Projects", + "topic": "Embedded/IoT", + "difficulty": "Intermediate", + "format": "Interactive", + "experience": "Some programming (basics)", + "description": "Build IoT projects with ESP32, sensors, and MQTT", + "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/", + "duration": "30 hours", + "tags": ["esp32", "iot", "sensors", "mqtt"] + }, + { + "id": 10, + "title": "OWASP Web Security Fundamentals", + "topic": "Security", + "difficulty": "Intermediate", + "format": "Interactive", + "experience": "Experienced developer", + "description": "Learn web application security, OWASP Top 10, and penetration testing basics", + "url": "https://owasp.org/www-project-web-security-learning/", + "duration": "25 hours", + "tags": ["security", "owasp", "pentesting", "web-security"] + }, + { + "id": 11, + "title": "Full-Stack JavaScript Mastery", + "topic": "Web Development", + "difficulty": "Advanced", + "format": "Video", + "experience": "Senior/Expert", + "description": "Master Node.js, Express, databases, deployment, and microservices", + "url": "https://nodejs.org/en/learn", + "duration": "80 hours", + "tags": ["nodejs", "express", "fullstack", "microservices"] + } +] + +def get_topic_encoding(topic_name): + for k, v in TOPIC_MAP.items(): + if v == topic_name: + return k + return 0 + +def get_difficulty_encoding(difficulty_name): + for k, v in DIFFICULTY_MAP.items(): + if v == difficulty_name: + return k + return 0 + +def get_format_encoding(format_name): + for k, v in FORMAT_MAP.items(): + if v == format_name: + return k + return 0 + +def get_experience_encoding(experience_name): + for k, v in EXPERIENCE_MAP.items(): + if v == experience_name: + return k + return 0 diff --git a/coding_edu_recommender.py b/coding_edu_recommender.py new file mode 100644 index 0000000..f2d0062 --- /dev/null +++ b/coding_edu_recommender.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +""" +Programming Education Resource Recommender +A Streamlit-based recommender that suggests coding tutorials, courses, +and practice resources based on user preferences and learning progress. + +Uses AO Labs' architecture for post-training learning. +""" + +import streamlit as st +import numpy as np +import random + +from coding_edu_domain import ( + RESOURCE_CATALOG, TOPIC_MAP, DIFFICULTY_MAP, FORMAT_MAP, EXPERIENCE_MAP, + get_topic_encoding, get_difficulty_encoding, get_format_encoding, get_experience_encoding +) + +try: + import ao_core as ao + from arch__CodingEduRecommender import arch + HAS_AO = True +except ImportError: + HAS_AO = False + print("Warning: ao_core not available, running in demo mode") + +def init_session_state(): + """Initialize all session state variables.""" + if "edu_resources_seen" not in st.session_state: + st.session_state.edu_resources_seen = [] + if "edu_recommendation_result" not in st.session_state: + st.session_state.edu_recommendation_result = None + if "edu_training_history" not in st.session_state: + st.session_state.edu_training_history = np.zeros([1000, 7], dtype="O") + if "edu_agent" not in st.session_state: + if HAS_AO: + st.session_state.edu_agent = ao.Agent(arch, notes="Coding Edu Recommender") + for i in range(4): + st.session_state.edu_agent.reset_state() + st.session_state.edu_agent.reset_state(training=True) + else: + st.session_state.edu_agent = None + if "edu_recommended" not in st.session_state: + st.session_state.edu_recommended = False + if "edu_topic" not in st.session_state: + st.session_state.edu_topic = "Web Development" + if "edu_difficulty" not in st.session_state: + st.session_state.edu_difficulty = "Beginner" + if "edu_format" not in st.session_state: + st.session_state.edu_format = "Interactive" + if "edu_experience" not in st.session_state: + st.session_state.edu_experience = "No prior experience" + + +def encode_user_input(topic, difficulty, fmt, experience): + """Encode user preferences into binary input for AO architecture.""" + topic_enc = format(get_topic_encoding(topic), f'03b') + diff_enc = format(get_difficulty_encoding(difficulty), f'02b') + fmt_enc = format(get_format_encoding(fmt), f'01b') + exp_enc = format(get_experience_encoding(experience), f'02b') + + binary_str = topic_enc + diff_enc + fmt_enc + exp_enc + return [int(b) for b in binary_str] + + +def get_resource_recommendation(topic, difficulty, fmt, experience): + """Get a resource recommendation based on user preferences.""" + # Filter resources matching preferences + candidates = [] + for resource in RESOURCE_CATALOG: + score = 0 + if resource["topic"] == topic: + score += 4 + if resource["difficulty"] == difficulty: + score += 3 + if resource["format"] == fmt: + score += 2 + if resource["experience"] == experience: + score += 2 + # Penalize already seen resources + if resource["id"] in st.session_state.edu_resources_seen: + score -= 5 + candidates.append((resource, score)) + + # Sort by score and add some randomness + candidates.sort(key=lambda x: x[1] + random.uniform(0, 1.5), reverse=True) + + if candidates and candidates[0][1] > 0: + best = candidates[0][0] + st.session_state.edu_resources_seen.append(best["id"]) + return best + + # Fallback: random unseen resource + unseen = [r for r in RESOURCE_CATALOG if r["id"] not in st.session_state.edu_resources_seen] + if unseen: + chosen = random.choice(unseen) + st.session_state.edu_resources_seen.append(chosen["id"]) + return chosen + + # All seen, reset + st.session_state.edu_resources_seen = [] + return random.choice(RESOURCE_CATALOG) + + +def train_on_feedback(resource, liked): + """Train the agent based on user feedback.""" + if st.session_state.edu_agent is None: + return + + binary_input = encode_user_input( + resource["topic"], resource["difficulty"], + resource["format"], resource["experience"] + ) + + # Positive feedback = reinforce, negative = discourage + reward = 1 if liked else -1 + + idx = st.session_state.get("edu_num_trained", 0) + st.session_state.edu_training_history[idx] = [ + resource["id"], binary_input, reward, liked, + resource["topic"], resource["difficulty"], resource["format"] + ] + st.session_state.edu_num_trained = idx + 1 + + +def run_coding_edu_recommender(): + """Main Streamlit app for the coding education recommender.""" + st.set_page_config(page_title="Coding Edu Recommender", page_icon="πŸ“š", layout="wide") + + init_session_state() + + st.title("πŸ“š Programming Education Recommender") + st.markdown("*Get personalized coding tutorial recommendations that learn from your feedback!*") + st.markdown("---") + + # Sidebar for preferences + with st.sidebar: + st.header("🎯 Your Preferences") + + topic = st.selectbox( + "Programming Topic", + list(TOPIC_MAP.values()), + index=list(TOPIC_MAP.values()).index(st.session_state.edu_topic), + key="edu_topic_select" + ) + st.session_state.edu_topic = topic + + difficulty = st.selectbox( + "Difficulty Level", + list(DIFFICULTY_MAP.values())[:-1], # Exclude Expert from selection + index=list(DIFFICULTY_MAP.values())[:-1].index(st.session_state.edu_difficulty) if st.session_state.edu_difficulty in list(DIFFICULTY_MAP.values())[:-1] else 0, + key="edu_difficulty_select" + ) + st.session_state.edu_difficulty = difficulty + + fmt = st.selectbox( + "Learning Format", + list(FORMAT_MAP.values()), + key="edu_format_select" + ) + st.session_state.edu_format = fmt + + experience = st.selectbox( + "Your Experience", + list(EXPERIENCE_MAP.values())[:-1], + key="edu_experience_select" + ) + st.session_state.edu_experience = experience + + st.markdown("---") + st.markdown(f"**Resources seen:** {len(st.session_state.edu_resources_seen)}/{len(RESOURCE_CATALOG)}") + + if st.button("πŸ”„ Reset History"): + st.session_state.edu_resources_seen = [] + st.rerun() + + # Main content area + col1, col2 = st.columns([2, 1]) + + with col1: + if st.button("🎲 Get Recommendation", type="primary", use_container_width=True): + resource = get_resource_recommendation(topic, difficulty, fmt, experience) + st.session_state.edu_recommendation_result = resource + st.session_state.edu_recommended = True + + if st.session_state.edu_recommendation_result: + resource = st.session_state.edu_recommendation_result + + st.markdown(f"### πŸ“– {resource['title']}") + st.markdown(f"*{resource['description']}*") + + col_a, col_b, col_c = st.columns(3) + with col_a: + st.metric("Topic", resource["topic"]) + with col_b: + st.metric("Difficulty", resource["difficulty"]) + with col_c: + st.metric("Format", resource["format"]) + + st.markdown(f"**⏱ Duration:** {resource['duration']}") + st.markdown(f"**πŸ”— [Start Learning]({resource['url']})**") + st.markdown(f"**Tags:** {', '.join(resource['tags'])}") + + st.markdown("---") + st.markdown("### Was this recommendation helpful?") + + fb_col1, fb_col2 = st.columns(2) + with fb_col1: + if st.button("πŸ‘ Yes, helpful!", key="edu_like"): + train_on_feedback(resource, True) + st.success("Thanks for the feedback! Training updated. πŸŽ‰") + with fb_col2: + if st.button("πŸ‘Ž Not for me", key="edu_dislike"): + train_on_feedback(resource, False) + st.info("Noted! I'll learn from this. Try another recommendation!") + + with col2: + st.markdown("### πŸ“Š Binary Encoding") + binary = encode_user_input( + st.session_state.edu_topic, + st.session_state.edu_difficulty, + st.session_state.edu_format, + st.session_state.edu_experience + ) + st.code(f"Input: {binary}") + st.caption(f"Topic({binary[0:3]}) Diff({binary[3:5]}) Fmt({binary[5]}) Exp({binary[6:8]})") + + st.markdown("---") + st.markdown("### πŸ“‹ All Resources") + for r in RESOURCE_CATALOG: + seen = "βœ…" if r["id"] in st.session_state.edu_resources_seen else "⬜" + st.markdown(f"{seen} **{r['title']}** ({r['difficulty']})") + + +if __name__ == "__main__": + run_coding_edu_recommender() diff --git a/tests/test_coding_edu.py b/tests/test_coding_edu.py new file mode 100644 index 0000000..74823f5 --- /dev/null +++ b/tests/test_coding_edu.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +Tests for the Coding Education Resource Recommender +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from coding_edu_domain import ( + RESOURCE_CATALOG, TOPIC_MAP, DIFFICULTY_MAP, FORMAT_MAP, EXPERIENCE_MAP, + get_topic_encoding, get_difficulty_encoding, get_format_encoding, get_experience_encoding +) + + +def test_catalog_completeness(): + """Verify all 12 resources are present with required fields.""" + assert len(RESOURCE_CATALOG) == 12, f"Expected 12 resources, got {len(RESOURCE_CATALOG)}" + required = ["id", "title", "topic", "difficulty", "format", "experience", "description", "url", "tags"] + for resource in RESOURCE_CATALOG: + for field in required: + assert field in resource, f"Resource {resource.get('id', '?')} missing field: {field}" + + +def test_topic_coverage(): + """Verify all topics are represented in the catalog.""" + topics_in_catalog = set(r["topic"] for r in RESOURCE_CATALOG) + for topic in TOPIC_MAP.values(): + assert topic in topics_in_catalog, f"Topic '{topic}' not in catalog" + + +def test_encoding_functions(): + """Test encoding functions return correct values.""" + assert get_topic_encoding("Web Development") == 0 + assert get_topic_encoding("Security") == 9 + assert get_difficulty_encoding("Beginner") == 0 + assert get_difficulty_encoding("Advanced") == 2 + assert get_format_encoding("Video") == 0 + assert get_format_encoding("Interactive") == 1 + assert get_experience_encoding("No prior experience") == 0 + + +def test_valid_urls(): + """Verify all resources have valid URLs.""" + for resource in RESOURCE_CATALOG: + url = resource["url"] + assert url.startswith("https://"), f"Invalid URL for {resource['title']}: {url}" + + +def test_unique_ids(): + """Verify all resource IDs are unique.""" + ids = [r["id"] for r in RESOURCE_CATALOG] + assert len(ids) == len(set(ids)), "Duplicate resource IDs found" + + +def test_unique_titles(): + """Verify all resource titles are unique.""" + titles = [r["title"] for r in RESOURCE_CATALOG] + assert len(titles) == len(set(titles)), "Duplicate resource titles found" + + +def test_encoding_range(): + """Test that all encodings are within valid bit ranges.""" + # 3 bits for topic = 0-7 (we have 10 topics but use 3 bits = 0-7, overflow handled) + for topic in TOPIC_MAP.values(): + enc = get_topic_encoding(topic) + assert 0 <= enc <= 9, f"Topic encoding out of range: {enc}" + + for diff in DIFFICULTY_MAP.values(): + enc = get_difficulty_encoding(diff) + assert 0 <= enc <= 3, f"Difficulty encoding out of range: {enc}" + + +if __name__ == "__main__": + test_catalog_completeness() + print("βœ… test_catalog_completeness") + test_topic_coverage() + print("βœ… test_topic_coverage") + test_encoding_functions() + print("βœ… test_encoding_functions") + test_valid_urls() + print("βœ… test_valid_urls") + test_unique_ids() + print("βœ… test_unique_ids") + test_unique_titles() + print("βœ… test_unique_titles") + test_encoding_range() + print("βœ… test_encoding_range") + print("\nAll tests passed! βœ…")