Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions ai-ml/roadmap_generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Roadmap Generator

## Overview

The Roadmap Generator is a module of the QuantumLearningWorkspace project (ai-ml). It generates a simple, ordered study roadmap from subject/topic input using the Groq API.

This module is **general-purpose**: it generates a roadmap for a subject or set of topics, and does not depend on Weak-topic Detection or any other module's output. A caller may optionally supply per-topic `priority` hints (e.g. sourced from weak-topic results elsewhere) without this module importing or depending on that source.

## Features

- Ordered, sequential study roadmap generation
- Configurable step count
- Optional per-topic priority hints to influence ordering
- Service layer for integration with other code

## Project Structure

```
roadmap_generator/
│
├── app/
│ ├── api/ # reserved for API endpoints — not yet built,
│ │ pending cross-team contract confirmation
│ ├── generators/
│ ├── models/
│ ├── services/
│ ├── utils/
│ ├── validators/
│ └── config.py
│
├── tests/
├── .gitignore
├── requirements.txt
└── README.md
```

## Installation

From the `ai-ml/` directory:

```bash
pip install -r roadmap_generator/requirements.txt
```

## Environment Variables

Add to your `.env` (same one used by the other ai-ml modules):

```env
GROQ_API_KEY=your_groq_api_key_here
```

## Running the Tests

From `ai-ml/`:

```bash
pytest roadmap_generator/tests
```

Tests that call the live Groq API are skipped automatically if `GROQ_API_KEY` isn't set.

## Usage

```python
from roadmap_generator.app.services.roadmap_service import RoadmapService

service = RoadmapService()
roadmap = service.generate_roadmap(
topic_names=["Recursion", "Dynamic programming"],
subject="Algorithms",
step_count=6,
)
```

## Supported Inputs

- `topic_names`: list of topic/subject name strings (required)
- `subject`: optional overall title for the roadmap
- `step_count`: number of steps to generate (3-15, default 6)
- `priorities`: optional `{topic_name: "high"|"normal"|"low"}` map

## Technologies Used

- Python 3.11
- Groq API
- Pydantic
- python-dotenv
- pytest

## Author

Developed as part of the QuantumLearningWorkspace Internship Project — Team Lambda.
26 changes: 26 additions & 0 deletions ai-ml/roadmap_generator/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os
from dotenv import load_dotenv

load_dotenv()

# ==========================================
# Groq Configuration
# ==========================================

GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")

GROQ_MODEL = "openai/gpt-oss-120b"
# NOTE: llama-3.3-70b-versatile (used elsewhere, e.g. quiz_generator's
# config.py) has been deprecated by Groq. openai/gpt-oss-120b is
# their current recommended general-purpose/reasoning replacement as
# of this writing. Worth flagging to the team — quiz_generator likely
# hits the same 404 right now.


# ==========================================
# Roadmap Configuration
# ==========================================

DEFAULT_STEP_COUNT = 6
MIN_STEP_COUNT = 3
MAX_STEP_COUNT = 15
33 changes: 33 additions & 0 deletions ai-ml/roadmap_generator/app/generators/base_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from abc import ABC, abstractmethod
from typing import List

from roadmap_generator.app.models.topic import Topic
from roadmap_generator.app.models.roadmap import Roadmap


class BaseGenerator(ABC):
"""
Base class for roadmap generators. Mirrors quiz_generator's
BaseGenerator pattern so the two modules stay consistent in
style, even though they don't depend on each other.
"""

@abstractmethod
def generate(
self,
topics: List[Topic],
subject: str = "",
step_count: int = 6,
) -> Roadmap:
"""
Generate a study roadmap from the given topics.

Parameters:
topics (List[Topic]): The subject(s)/topic(s) to build a roadmap for.
subject (str): Optional overall subject/title for the roadmap.
step_count (int): Target number of steps in the roadmap.

Returns:
Roadmap: A structured, ordered study roadmap.
"""
pass
103 changes: 103 additions & 0 deletions ai-ml/roadmap_generator/app/generators/roadmap_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import json

from groq import Groq

from roadmap_generator.app.generators.base_generator import BaseGenerator
from roadmap_generator.app.config import GROQ_API_KEY, GROQ_MODEL, DEFAULT_STEP_COUNT
from roadmap_generator.app.models.topic import Topic
from roadmap_generator.app.models.roadmap import Roadmap, RoadmapStep


class RoadmapGenerator(BaseGenerator):
"""
Generates a simple, ordered study roadmap for a subject or set of
topics using the Groq API. General-purpose: does not depend on
Weak-topic Detection or any other module's output.
"""

def __init__(self):
self.client = Groq(api_key=GROQ_API_KEY)

def generate(
self,
topics: list[Topic],
subject: str = "",
step_count: int = DEFAULT_STEP_COUNT,
) -> Roadmap:
if not topics:
raise ValueError("At least one topic is required to generate a roadmap.")

topic_lines = []
for t in topics:
line = f"- {t.name}"
if t.description:
line += f" ({t.description})"
if t.priority:
line += f" [priority: {t.priority}]"
topic_lines.append(line)
topics_block = "\n".join(topic_lines)

subject_label = subject or ", ".join(t.name for t in topics)

prompt = f"""
Create a simple, ordered study roadmap for the following subject/topics.

Subject: {subject_label}

Topics to cover:
{topics_block}

Rules:
- Produce exactly {step_count} sequential steps.
- Each step should build logically on the previous one (foundational
concepts before advanced ones).
- Keep each step's description concise and actionable (1-2 sentences).
- Give a rough estimated_duration for each step (e.g. "2-3 days"),
as a general suggestion, not a strict deadline.
- If a topic has a stated priority of "high", make sure it is covered
reasonably early in the roadmap, but the roadmap should still make
logical sense as a learning sequence.

Respond with ONLY a JSON array, no other text, in this exact shape:
[
{{
"step_number": 1,
"topic": "...",
"description": "...",
"estimated_duration": "..."
}}
]
"""

response = self.client.chat.completions.create(
model=GROQ_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.5,
)

raw = response.choices[0].message.content or ""
raw = raw.strip()
if raw.startswith("```"):
raw = raw.strip("`")
if raw.startswith("json"):
raw = raw[4:]
raw = raw.strip()

try:
items = json.loads(raw)
except json.JSONDecodeError as exc:
raise RuntimeError(
f"RoadmapGenerator: could not parse LLM response as JSON: {exc}"
) from exc

steps = [
RoadmapStep(
step_number=item.get("step_number", idx + 1),
topic=item["topic"],
description=item["description"],
estimated_duration=item.get("estimated_duration"),
)
for idx, item in enumerate(items)
]

return Roadmap(subject=subject_label, steps=steps, total_steps=len(steps))
32 changes: 32 additions & 0 deletions ai-ml/roadmap_generator/app/models/roadmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import List, Optional

from pydantic import BaseModel, Field


class RoadmapStep(BaseModel):
"""A single stage in a generated study roadmap."""

step_number: int = Field(..., description="Order of this step in the roadmap, starting at 1.")

topic: str = Field(..., description="The topic covered in this step.")

description: str = Field(
..., description="What the learner should focus on or do during this step."
)

estimated_duration: Optional[str] = Field(
default=None,
description="Rough suggested time for this step, e.g. '2-3 days'. Optional — LLM-provided, not a guarantee.",
)


class Roadmap(BaseModel):
"""The full generated study roadmap for a subject or set of topics."""

subject: str = Field(
default="", description="Overall subject/title this roadmap covers, if provided."
)

steps: List[RoadmapStep] = Field(default_factory=list)

total_steps: int = Field(default=0)
25 changes: 25 additions & 0 deletions ai-ml/roadmap_generator/app/models/topic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Optional

from pydantic import BaseModel, Field


class Topic(BaseModel):
"""
Represents a single subject/topic used as input for roadmap
generation. Deliberately generic: this module does not depend on
Weak-topic Detection, but a caller (e.g. a future integration)
could populate `priority` from weak-topic results without this
module needing to know anything about that source.
"""

name: str = Field(..., min_length=1, description="Topic or subject name.")

description: Optional[str] = Field(
default=None,
description="Optional extra context about the topic to guide generation.",
)

priority: Optional[str] = Field(
default=None,
description="Optional hint: 'high', 'normal', 'low'. Not required.",
)
46 changes: 46 additions & 0 deletions ai-ml/roadmap_generator/app/services/roadmap_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from roadmap_generator.app.generators.roadmap_generator import RoadmapGenerator
from roadmap_generator.app.models.topic import Topic
from roadmap_generator.app.models.roadmap import Roadmap
from roadmap_generator.app.validators.topic_validator import (
validate_topic_names,
validate_step_count,
)
from roadmap_generator.app.config import DEFAULT_STEP_COUNT


class RoadmapService:
"""
Coordinates the roadmap-generation workflow. This is the
integration point other code (a future API layer, or another
module) should call, rather than using RoadmapGenerator directly.
"""

def __init__(self):
self.generator = RoadmapGenerator()

def generate_roadmap(
self,
topic_names: list[str],
subject: str = "",
step_count: int = DEFAULT_STEP_COUNT,
priorities: dict[str, str] | None = None,
) -> Roadmap:
"""
Generates a study roadmap from a plain list of topic names.

priorities: optional {topic_name: "high"|"normal"|"low"} map.
This is how a caller (e.g. a future weak-topic integration)
could nudge topic ordering WITHOUT this module importing or
depending on that other module — the caller does the mapping,
this service just accepts plain data.
"""
validate_topic_names(topic_names)
validate_step_count(step_count)

priorities = priorities or {}
topics = [
Topic(name=name, priority=priorities.get(name))
for name in topic_names
]

return self.generator.generate(topics, subject=subject, step_count=step_count)
23 changes: 23 additions & 0 deletions ai-ml/roadmap_generator/app/validators/topic_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from roadmap_generator.app.config import MIN_STEP_COUNT, MAX_STEP_COUNT


def validate_topic_names(topic_names: list[str]) -> None:
"""
Validates raw topic name input before it's wrapped into Topic
models. Raises ValueError on invalid input.
"""
if not topic_names:
raise ValueError("At least one topic name is required.")

for name in topic_names:
if not name or not name.strip():
raise ValueError("Topic names must not be empty or whitespace-only.")


def validate_step_count(step_count: int) -> None:
"""Ensures the requested roadmap length is within a sane range."""
if not (MIN_STEP_COUNT <= step_count <= MAX_STEP_COUNT):
raise ValueError(
f"step_count must be between {MIN_STEP_COUNT} and {MAX_STEP_COUNT} "
f"(got {step_count})."
)
4 changes: 4 additions & 0 deletions ai-ml/roadmap_generator/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
groq
pydantic
python-dotenv
pytest
Loading
Loading