This commit is contained in:
2025-04-23 01:46:29 +03:00
parent 575d212488
commit f9eb2f35a8
46 changed files with 287 additions and 139 deletions

Binary file not shown.

View File

@ -1,54 +1,41 @@
from enum import Enum
from typing import Generic
from modules.tag import Tag
from modules.task import QTask
from modules.task.factory import QTaskFactory
from modules.variant_builder import VariantFactory
from modules.variant_builder.task_pool import QTaskPool
from option import Some
tasks_on_first_topic = [QTask(f"1.{i + 1}", tags=Tag("topic", "1")) for i in range(5)]
tasks_on_second_topic = [QTask(f"2.{i + 1}", tags=Tag("topic", "2")) for i in range(5)]
tasks_on_third_topic = [QTask(f"3.{i + 1}", tags=Tag("topic", "3")) for i in range(5)]
tasks_on_forth_topic = [QTask(f"4.{i + 1}", tags=Tag("topic", "4")) for i in range(5)]
tasks_on_fifth_topic = [QTask(f"5.{i + 1}", tags=Tag("topic", "5")) for i in range(5)]
tasks_on_sixth_topic = [QTask(f"6.{i + 1}", tags=Tag("topic", "6")) for i in range(5)]
tasks_on_seventh_topic = [QTask(f"7.{i + 1}", tags=Tag("topic", "7")) for i in range(5)]
from Quizard import Quizard, QuizTask, QuizTaskGeneratorMetadata, TaskGenerator
task_pool = QTaskPool(
tasks_on_first_topic
+ tasks_on_second_topic
+ tasks_on_third_topic
+ tasks_on_forth_topic
+ tasks_on_fifth_topic
+ tasks_on_sixth_topic
+ tasks_on_seventh_topic
)
vf = VariantFactory(number_of_tasks=7, task_pool=task_pool)
class TagCategory(str, Enum):
TOPIC = "topic"
DIFFICULTY = "difficulty"
_ = vf.task[0].must.include_tag("topic", "1")
_ = vf.task[1].must.include_tag("topic", "2")
_ = vf.task[2].must.include_tag("topic", "3")
_ = vf.task[3].must.include_tag("topic", "4")
_ = vf.task[4].must.include_tag("topic", "5")
_ = vf.task[5].must.include_tag("topic", "6")
_ = vf.task[6].must.include_tag("topic", "7")
variants = vf.generate_variants(number_of_variants=30)
class TopicTag(str, Enum):
AVERAGE = "average"
VARIANCE = "variance"
class MyTaskGenerator(TaskGenerator[TagCategory, TopicTag]):
def __init__(self):
self.metadata = QuizTaskGeneratorMetadata[TagCategory, TopicTag].from_values(
name=self.__class__.__name__,
)
class AverageTask(MyTaskGenerator):
def generate(self):
return QuizTask(
"What is an average of 3, 4, 5 and 6?",
"4.5",
tags=[
(TagCategory.TOPIC, TopicTag.AVERAGE),
],
)
class AverageTask1(MyTaskGenerator):
def generate(self):
return QuizTask(
"What is an average of 1, 2, 3 and 4?",
"2.5",
tags=[
(TagCategory.TOPIC, TopicTag.VARIANCE),
],
)
default_amount = Some(1)
quizard = Quizard([AverageTask(), AverageTask1()], 3)
quizard.fillTaskPool()
print(*[task.generator_metadata.id for task in quizard.taskPool], sep="\n\n")
i = 0
for variant in variants:
print(f"Variant {i + 1}:")
print(*[task.question for task in variant.tasks])
i += 1

View File

@ -20,7 +20,6 @@ class Tag(Generic[C, V]):
val: V
@dataclass
class Tags(Generic[C, V]):
"""A collection of tags grouped by category
@ -28,9 +27,11 @@ class Tags(Generic[C, V]):
_dict: (dict[C, set[V]]): Internal dictionary storing tags grouped by category.
"""
_dict: dict[C, set[V]] = field(default_factory=dict)
_dict: dict[C, set[V]]
def __init__(self, iter: Iterable[Tag[C, V]] | None = None):
self._dict = {}
if iter:
for tag in iter:
self._dict.setdefault(tag.cat, set()).add(tag.val)
@ -101,9 +102,7 @@ class Tags(Generic[C, V]):
lines: list[str] = []
for category, values in self._dict.items():
cat_str = str(category.value if isinstance(category, Enum) else category)
val_strs = sorted(
[str(v.value if isinstance(v, Enum) else v for v in values)]
)
val_strs = [str(v.value if isinstance(v, Enum) else v) for v in values]
lines.append(f"{cat_str}: {', '.join(val_strs)}")
return "\n".join(lines)
@ -113,6 +112,7 @@ class Tags(Generic[C, V]):
Yields:
Tag[C, V]: Each tag in the collection.
"""
for category, values in self._dict.items():
for value in values:
yield Tag(category, value)

Binary file not shown.

View File

@ -1,37 +1,42 @@
import uuid
from dataclasses import dataclass
from typing import Generic, override
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Generic, override
from option import Option
from modules.tag import Tag, Tags
from modules.task.factory.metadata import QTaskFactoryMetadata
from modules.utils.types import A, C, Q, V
from modules.utils.utils import indent
if TYPE_CHECKING:
from modules.task.factory.metadata import QTaskFactoryMetadata
@dataclass
class QTask(Generic[C, V, Q, A]):
question: Q
answer: A
tags: Tags[C, V]
factory_metadata: Option[QTaskFactoryMetadata[C, V]] = Option[
QTaskFactoryMetadata[C, V]
id: uuid.UUID
factory_metadata: Option["QTaskFactoryMetadata[C, V]"] = Option[
"QTaskFactoryMetadata[C, V]"
].maybe(None)
id: uuid.UUID = uuid.uuid4()
def __init__(
self,
question: Q,
answer: A,
tags: Tags[C, V] | list[Tag[C, V]] | None = None,
answer: A = None,
tags: Tags[C, V] | list[Tag[C, V]] | Tag[C, V] | None = None,
):
self.question = question
self.answer = answer
if isinstance(tags, Tag):
self.tags = Tags[C, V]([tags])
if isinstance(tags, list):
self.tags = Tags[C, V](tags)
elif isinstance(tags, Tags):
self.tags = tags
self.id = uuid.uuid4()
@override
def __str__(self) -> str:

Binary file not shown.

View File

@ -9,7 +9,6 @@ from modules.task.factory.metadata import QTaskFactoryMetadata
from modules.utils.types import A, C, Q, V
@runtime_checkable
class QTaskFactory(ABC, Generic[C, V, Q, A]):
id: uuid.UUID
metadata: QTaskFactoryMetadata[C, V]

View File

@ -2,7 +2,7 @@ import uuid
from dataclasses import dataclass, field
from typing import Generic
from option import NONE, Option
from option import Option
from modules.utils.types import C, V

Binary file not shown.

View File

@ -1,6 +1,11 @@
from random import Random
from typing import TypeVar
def indent(text: str, spaces: int = 2) -> str:
prefix = " " * spaces
return "\n".join(prefix + line for line in text.splitlines())
rnd = Random()
rnd.seed(42)

Binary file not shown.

View File

@ -1,16 +1,96 @@
from dataclasses import dataclass
from typing import Generic
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.variant import QVariant
from modules.variant_builder.context import DynamicCtx
from modules.variant_builder.default_task_selector import LeastUsedTaskSelector
from modules.variant_builder.filters import Filter
from modules.variant_builder.task_pool import QTaskPool
from modules.variant_builder.task_selector import QTaskSelector
from modules.variant_builder.variant_set import QVariantSet
from modules.variant_builder.variant_task import VariantTask
@dataclass
class VariantBuilder(Generic[C, V, Q, A]):
class VariantFactory(Generic[C, V, Q, A]):
task_pool: QTaskPool[C, V, Q, A]
previos_variants: QVariantSet[C, V, Q, A]
current_variant: QVariant[C, V, Q, A]
previous_variants: QVariantSet[C, V, Q, A] = QVariantSet()
task_selector: QTaskSelector[C, V, Q, A]
task: list[VariantTask[C, V, Q, A]]
number_of_tasks: int
def __init__(
self,
number_of_tasks: int,
task_pool: QTaskPool[C, V, Q, A],
task_selector: QTaskSelector[C, V, Q, A] | None = None,
):
self.task = [VariantTask() for _ in range(number_of_tasks)]
self.number_of_tasks = number_of_tasks
self.task_pool = task_pool
self.task_selector = (
task_selector if task_selector is not None else LeastUsedTaskSelector()
)
def generate_variants(self, number_of_variants: int) -> QVariantSet[C, V, Q, A]:
variant_task_filters: list[Filter[C, V, Q, A]] = [
b.must.build() for b in self.task
]
static_filter_matches_per_task = self._get_static_filter_matches(
variant_task_filters
)
dynamic_context = DynamicCtx(
self.task_pool,
)
for _ in range(number_of_variants):
variant_tasks: list[QTask[C, V, Q, A]] = []
for task_index in range(self.number_of_tasks):
dynamic_filtered_matches = self._get_dynamic_filter_matches(
static_filter_matches_per_task[task_index],
variant_task_filters[task_index],
dynamic_context,
)
selected_task = self.task_selector.select(
dynamic_filtered_matches, dynamic_context
)
variant_tasks.append(selected_task)
dynamic_context.current_variant_tasks.append(selected_task)
variant = QVariant(variant_tasks)
dynamic_context.previous_variants.append(variant)
dynamic_context.current_variant_tasks.clear()
return dynamic_context.previous_variants
def _get_static_filter_matches(
self, task_filters: list[Filter[C, V, Q, A]]
) -> list[list[QTask[C, V, Q, A]]]:
statical_filter_matches_per_task: list[list[QTask[C, V, Q, A]]] = [
[] for _ in range(self.number_of_tasks)
]
for task in self.task_pool:
task_filter_index = 0
for filter in task_filters:
if filter.static.is_satisfied(task):
statical_filter_matches_per_task[task_filter_index].append(task)
task_filter_index += 1
return statical_filter_matches_per_task
def _get_dynamic_filter_matches(
self,
statically_filtered_pool: list[QTask[C, V, Q, A]],
filter: Filter[C, V, Q, A],
ctx: DynamicCtx[C, V, Q, A],
) -> list[QTask[C, V, Q, A]]:
dynamic_filter_matches: list[QTask[C, V, Q, A]] = []
for task in statically_filtered_pool:
if filter.dynamic.check_if_satisfied(task, ctx):
dynamic_filter_matches.append(task)
return dynamic_filter_matches

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Generic
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.variant import QVariant
from modules.variant_builder.task_pool import QTaskPool
@ -8,8 +9,9 @@ from modules.variant_builder.variant_set import QVariantSet
@dataclass
class DynamicFilterCtx(Generic[C, V, Q, A]):
class DynamicCtx(Generic[C, V, Q, A]):
task_pool: QTaskPool[C, V, Q, A]
previous_variants: QVariantSet[C, V, Q, A]
current_variant: QVariant[C, V, Q, A]
task_number: int
previous_variants: QVariantSet[C, V, Q, A] = field(
default_factory=QVariantSet[C, V, Q, A]
)
current_variant_tasks: list[QTask[C, V, Q, A]] = field(default_factory=list)

View File

@ -0,0 +1,63 @@
import uuid
from math import inf
from test import best_candidate, min_max_intersections
from typing import override
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.utils.utils import rnd
from modules.variant_builder.context import DynamicCtx
from modules.variant_builder.task_pool import QTaskPool
from modules.variant_builder.task_selector import QTaskSelector
class LeastUsedTaskSelector(QTaskSelector[C, V, Q, A]):
task_usage_count: dict[uuid.UUID, int]
def __init__(self):
self.task_usage_count = {}
@override
def select(
self, filtered_tasks: list[QTask[C, V, Q, A]], ctx: DynamicCtx[C, V, Q, A]
) -> QTask[C, V, Q, A]:
rnd.shuffle(filtered_tasks)
task_scores: list[int] = []
min_max_intersections = inf
for task in filtered_tasks:
max_intersections = 0
for variant in ctx.previous_variants:
intersections = 0
for t in ctx.current_variant_tasks:
if t in variant.tasks:
intersections += 1
if task in variant.tasks:
intersections += 1
if intersections > max_intersections:
max_intersections = intersections
task_scores.append(max_intersections)
if min_max_intersections > max_intersections:
min_max_intersections = max_intersections
if len(ctx.current_variant_tasks) == 6:
print(min_max_intersections)
best_candidates: list[QTask[C, V, Q, A]] = []
for task, score in zip(filtered_tasks, task_scores):
if score == min_max_intersections:
best_candidates.append(task)
least_used_score = inf
least_used_task: QTask[C, V, Q, A] = best_candidates[0]
for task in best_candidates:
if self.task_usage_count.setdefault(task.id, 0) < least_used_score:
least_used_score = self.task_usage_count[task.id]
least_used_task = task
self.task_usage_count[least_used_task.id] += 1
return least_used_task

View File

@ -5,11 +5,8 @@ from typing import Callable, Generic, Self, overload, override
from modules.tag import Tag, Tags
from modules.utils.types import A, C, Q, V
from modules.variant_builder.filters import Filter
from modules.variant_builder.filters.composite import CompositeFilter
from modules.variant_builder.filters.dynamic import FilterDynamic
from modules.variant_builder.filters.dynamic.composite import CompositeFilterDynamic
from modules.variant_builder.filters.static import FilterStatic
from modules.variant_builder.filters.static.composite import CompositeFilterStatic
from modules.variant_builder.filters.static.must_be_one_of import MustBeOneOfFilter
from modules.variant_builder.filters.static.must_include_all_tags import (
MustIncludeAllTagsFilter,
@ -92,6 +89,19 @@ class FilterBuilder(Generic[C, V, Q, A]):
) -> Self:
return self.add_static(MustIncludeAllTagsFilter(tag, **kwargs))
@overload
def include_tag(self, category: C, value: V) -> Self: ...
@overload
def include_tag(self, category: Tag[C, V], value: None = None) -> Self: ...
def include_tag(self, category: C | Tag[C, V], value: V | None = None) -> Self:
if isinstance(category, Tag):
return self.include_all_tags(category)
else:
assert value is not None
return self.include_all_tags([Tag(category, value)])
def be_inverse_of(
self,
filter: Callable[["FilterBuilder[C, V, Q, A]"], "FilterBuilder[C, V, Q, A]"],

View File

@ -5,16 +5,15 @@ from typing import Generic, Protocol, override, runtime_checkable
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.variant import QVariant
from modules.variant_builder.context import DynamicFilterCtx
from modules.variant_builder.context import DynamicCtx
from modules.variant_builder.task_pool import QTaskPool
from modules.variant_builder.variant_set import QVariantSet
@runtime_checkable
class FilterDynamic(ABC, Generic[C, V, Q, A]):
@abstractmethod
def check_if_satisfied(
self, task: QTask[C, V, Q, A], ctx: DynamicFilterCtx[C, V, Q, A]
self, task: QTask[C, V, Q, A], ctx: DynamicCtx[C, V, Q, A]
) -> bool: ...
def __invert__(self) -> "FilterDynamic[C, V, Q, A]":
@ -33,7 +32,7 @@ class DynamicFilterNegator(FilterDynamic[C, V, Q, A]):
@override
def check_if_satisfied(
self, task: QTask[C, V, Q, A], ctx: DynamicFilterCtx[C, V, Q, A]
self, task: QTask[C, V, Q, A], ctx: DynamicCtx[C, V, Q, A]
) -> bool:
return not self.filter.check_if_satisfied(task, ctx)

View File

@ -3,7 +3,7 @@ from typing import override
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.variant_builder.context import DynamicFilterCtx
from modules.variant_builder.context import DynamicCtx
from modules.variant_builder.filters.dynamic import FilterDynamic
@ -14,7 +14,7 @@ class CompositeFilterDynamic(FilterDynamic[C, V, Q, A]):
@override
def check_if_satisfied(
self, task: QTask[C, V, Q, A], ctx: DynamicFilterCtx[C, V, Q, A]
self, task: QTask[C, V, Q, A], ctx: DynamicCtx[C, V, Q, A]
) -> bool:
return all(filter.check_if_satisfied(task, ctx) for filter in self.filters)

View File

@ -1,6 +0,0 @@
from modules.utils.types import A, C, Q, V
from modules.variant_builder.filters.dynamic import FilterDynamic
class FilterNegatorDynamic(FilterDynamic[C, V, Q, A]):

View File

@ -1,16 +0,0 @@
from dataclasses import dataclass
from typing import override
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.variant_builder.filters import FilterBuilder
from modules.variant_builder.filters.static import FilterStatic
@dataclass
class CompositeFilterStatic(FilterStatic[C, V, Q, A]):
filters: list[FilterStatic[C, V, Q, A]]
@override
def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool:
return all(filter.is_satisfied(task) for filter in self.filters)

View File

@ -1,19 +0,0 @@
from dataclasses import dataclass
from typing import override
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.variant_builder.filters.static import FilterStatic
@dataclass
class StaticFilterNegator(FilterStatic[C, V, Q, A]):
filter: FilterStatic[C, V, Q, A]
@override
def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool:
return not self.filter.is_satisfied(task)
@override
def __invert__(self) -> "FilterStatic[C, V, Q, A]":
return self.filter

View File

@ -1,3 +1,4 @@
import random
from dataclasses import dataclass, field
from typing import Generic
@ -5,6 +6,6 @@ from modules.task import QTask
from modules.utils.types import A, C, Q, V
@dataclass(frozen=True)
class QTaskPool(Generic[C, V, Q, A]):
pool: list[QTask[C, V, Q, A]]
class QTaskPool(list[QTask[C, V, Q, A]], Generic[C, V, Q, A]):
def shuffle(self):
random.shuffle(self)

View File

@ -1,13 +1,15 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Protocol
from typing import Generic, Protocol
from modules.task import QTask
from modules.utils.types import A, C, Q, V
from modules.variant_builder.context import DynamicCtx
from modules.variant_builder.task_pool import QTaskPool
@dataclass
class QTaskSelector(Protocol[C, V, Q, A]):
task_pool: QTaskPool[C, V, Q, A]
def select(self, filtered_task_pool_indexes: list[int]) -> QTask[C, V, Q, A]: ...
class QTaskSelector(ABC, Generic[C, V, Q, A]):
@abstractmethod
def select(
self, filtered_tasks: list[QTask[C, V, Q, A]], ctx: DynamicCtx[C, V, Q, A]
) -> QTask[C, V, Q, A]: ...

View File

@ -6,9 +6,4 @@ from modules.variant import QVariant
@dataclass
class QVariantSet(Generic[C, V, Q, A]):
variants: list[QVariant[C, V, Q, A]] = field(default_factory=list)
def __iter__(self):
for variant in self.variants:
yield variant
class QVariantSet(list[QVariant[C, V, Q, A]], Generic[C, V, Q, A]): ...

View File

@ -1,6 +1,15 @@
from dataclasses import dataclass, field
from typing import Generic
from option import Option
from modules.utils.types import A, C, Q, V
from modules.variant_builder.filters import Filter
from modules.variant_builder.filters.builder import FilterBuilder
class VariantTask(Generic[C, V, Q, A]):
must: FilterBuilder[C, V, Q, A]
def __init__(self):
self.must = FilterBuilder[C, V, Q, A]()

33
test.py Normal file
View File

@ -0,0 +1,33 @@
from math import inf
tasks1 = [f"1.{i + 1}" for i in range(5)]
tasks2 = [f"2.{i + 1}" for i in range(5)]
tasks3 = [f"3.{i + 1}" for i in range(5)]
all_variants = []
for i in tasks1:
for j in tasks2:
for k in tasks3:
all_variants.append([i, j, k])
selected_variants = [all_variants[0]]
for _ in range(9):
best_candidate = all_variants[0]
min_max_intersections = inf
for variant in all_variants:
max_intersections = -inf
for selected_variant in selected_variants:
intersections = 0
for j in range(3):
if selected_variant[j] == variant[j]:
intersections += 1
if max_intersections < intersections:
max_intersections = intersections
if max_intersections < min_max_intersections:
min_max_intersections = max_intersections
best_candidate = variant
selected_variants.append(best_candidate)
print(any([]), sep="\n")