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 modules.tag import Tag
from typing import Generic 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): _ = vf.task[0].must.include_tag("topic", "1")
TOPIC = "topic" _ = vf.task[1].must.include_tag("topic", "2")
DIFFICULTY = "difficulty" _ = 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): i = 0
AVERAGE = "average" for variant in variants:
VARIANCE = "variance" print(f"Variant {i + 1}:")
print(*[task.question for task in variant.tasks])
i += 1
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")

View File

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

Binary file not shown.

View File

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

View File

@ -2,7 +2,7 @@ import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Generic from typing import Generic
from option import NONE, Option from option import Option
from modules.utils.types import C, V 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 from typing import TypeVar
def indent(text: str, spaces: int = 2) -> str: def indent(text: str, spaces: int = 2) -> str:
prefix = " " * spaces prefix = " " * spaces
return "\n".join(prefix + line for line in text.splitlines()) 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 typing import Generic
from modules.task import QTask
from modules.utils.types import A, C, Q, V from modules.utils.types import A, C, Q, V
from modules.variant import QVariant 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_pool import QTaskPool
from modules.variant_builder.task_selector import QTaskSelector from modules.variant_builder.task_selector import QTaskSelector
from modules.variant_builder.variant_set import QVariantSet from modules.variant_builder.variant_set import QVariantSet
from modules.variant_builder.variant_task import VariantTask
@dataclass class VariantFactory(Generic[C, V, Q, A]):
class VariantBuilder(Generic[C, V, Q, A]):
task_pool: QTaskPool[C, V, Q, A] task_pool: QTaskPool[C, V, Q, A]
previos_variants: QVariantSet[C, V, Q, A] previous_variants: QVariantSet[C, V, Q, A] = QVariantSet()
current_variant: QVariant[C, V, Q, A]
task_selector: QTaskSelector[C, V, Q, A] 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 typing import Generic
from modules.task import QTask
from modules.utils.types import A, C, Q, V from modules.utils.types import A, C, Q, V
from modules.variant import QVariant from modules.variant import QVariant
from modules.variant_builder.task_pool import QTaskPool from modules.variant_builder.task_pool import QTaskPool
@ -8,8 +9,9 @@ from modules.variant_builder.variant_set import QVariantSet
@dataclass @dataclass
class DynamicFilterCtx(Generic[C, V, Q, A]): class DynamicCtx(Generic[C, V, Q, A]):
task_pool: QTaskPool[C, V, Q, A] task_pool: QTaskPool[C, V, Q, A]
previous_variants: QVariantSet[C, V, Q, A] previous_variants: QVariantSet[C, V, Q, A] = field(
current_variant: QVariant[C, V, Q, A] default_factory=QVariantSet[C, V, Q, A]
task_number: int )
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.tag import Tag, Tags
from modules.utils.types import A, C, Q, V from modules.utils.types import A, C, Q, V
from modules.variant_builder.filters import Filter 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 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 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_be_one_of import MustBeOneOfFilter
from modules.variant_builder.filters.static.must_include_all_tags import ( from modules.variant_builder.filters.static.must_include_all_tags import (
MustIncludeAllTagsFilter, MustIncludeAllTagsFilter,
@ -92,6 +89,19 @@ class FilterBuilder(Generic[C, V, Q, A]):
) -> Self: ) -> Self:
return self.add_static(MustIncludeAllTagsFilter(tag, **kwargs)) 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( def be_inverse_of(
self, self,
filter: Callable[["FilterBuilder[C, V, Q, A]"], "FilterBuilder[C, V, Q, A]"], 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.task import QTask
from modules.utils.types import A, C, Q, V from modules.utils.types import A, C, Q, V
from modules.variant import QVariant 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.task_pool import QTaskPool
from modules.variant_builder.variant_set import QVariantSet from modules.variant_builder.variant_set import QVariantSet
@runtime_checkable
class FilterDynamic(ABC, Generic[C, V, Q, A]): class FilterDynamic(ABC, Generic[C, V, Q, A]):
@abstractmethod @abstractmethod
def check_if_satisfied( 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: ... ) -> bool: ...
def __invert__(self) -> "FilterDynamic[C, V, Q, A]": def __invert__(self) -> "FilterDynamic[C, V, Q, A]":
@ -33,7 +32,7 @@ class DynamicFilterNegator(FilterDynamic[C, V, Q, A]):
@override @override
def check_if_satisfied( 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: ) -> bool:
return not self.filter.check_if_satisfied(task, ctx) 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.task import QTask
from modules.utils.types import A, C, Q, V 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 from modules.variant_builder.filters.dynamic import FilterDynamic
@ -14,7 +14,7 @@ class CompositeFilterDynamic(FilterDynamic[C, V, Q, A]):
@override @override
def check_if_satisfied( 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: ) -> bool:
return all(filter.check_if_satisfied(task, ctx) for filter in self.filters) 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 dataclasses import dataclass, field
from typing import Generic from typing import Generic
@ -5,6 +6,6 @@ from modules.task import QTask
from modules.utils.types import A, C, Q, V from modules.utils.types import A, C, Q, V
@dataclass(frozen=True) class QTaskPool(list[QTask[C, V, Q, A]], Generic[C, V, Q, A]):
class QTaskPool(Generic[C, V, Q, A]): def shuffle(self):
pool: list[QTask[C, V, Q, A]] random.shuffle(self)

View File

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

View File

@ -6,9 +6,4 @@ from modules.variant import QVariant
@dataclass @dataclass
class QVariantSet(Generic[C, V, Q, A]): class QVariantSet(list[QVariant[C, V, Q, A]], 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

View File

@ -1,6 +1,15 @@
from dataclasses import dataclass, field
from typing import Generic from typing import Generic
from option import Option
from modules.utils.types import A, C, Q, V 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]): 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")