Add basis workspace filters, stale-on-reimport, and variant usage metrics

This commit is contained in:
dwindown
2026-04-28 18:44:43 +07:00
parent 08a1352268
commit c3f7a4463b
7 changed files with 1144 additions and 92 deletions

View File

@@ -16,6 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import get_settings
from app.models.item import Item
from app.models.ai_generation_run import AIGenerationRun
from app.models.tryout import Tryout
from app.models.user_answer import UserAnswer
from app.schemas.ai import GeneratedQuestion
@@ -493,6 +494,9 @@ async def save_ai_question(
level: Literal["mudah", "sedang", "sulit"],
ai_model: str,
db: AsyncSession,
generation_run_id: int | None = None,
source_snapshot_question_id: int | None = None,
variant_status: str = "draft",
) -> Optional[int]:
"""
Save AI-generated question to database.
@@ -523,6 +527,9 @@ async def save_ai_question(
generated_by="ai",
ai_model=ai_model,
basis_item_id=basis_item_id,
generation_run_id=generation_run_id,
source_snapshot_question_id=source_snapshot_question_id,
variant_status=variant_status,
calibrated=False,
ctt_p=None,
ctt_bobot=None,
@@ -547,6 +554,50 @@ async def save_ai_question(
return None
async def create_generation_run(
basis_item_id: int,
target_level: Literal["mudah", "sulit"],
requested_count: int,
model: str,
created_by: str,
db: AsyncSession,
source_snapshot_question_id: int | None = None,
operator_notes: str | None = None,
prompt_version: str = "v1",
) -> int:
run = AIGenerationRun(
basis_item_id=basis_item_id,
source_snapshot_question_id=source_snapshot_question_id,
target_level=target_level,
requested_count=requested_count,
model=model,
prompt_version=prompt_version,
operator_notes=operator_notes,
created_by=created_by,
)
db.add(run)
await db.flush()
return int(run.id)
async def generate_questions_batch(
basis_item: Item,
target_level: Literal["mudah", "sulit"],
ai_model: str,
count: int,
) -> list[GeneratedQuestion]:
generated_items: list[GeneratedQuestion] = []
for _ in range(count):
generated = await generate_question(
basis_item=basis_item,
target_level=target_level,
ai_model=ai_model,
)
if generated is not None:
generated_items.append(generated)
return generated_items
async def get_ai_stats(db: AsyncSession) -> Dict[str, Any]:
"""
Get AI generation statistics.

View File

@@ -17,7 +17,7 @@ from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import TryoutImportSnapshot, TryoutSnapshotQuestion, Website
from app.models import Item, TryoutImportSnapshot, TryoutSnapshotQuestion, Website
SOURCE_FORMAT = "sejoli_json"
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
@@ -292,7 +292,8 @@ async def import_tryout_json_snapshot(payload: dict[str, Any], website_id: int,
new_questions += 1
continue
if existing.content_checksum != question["content_checksum"]:
content_changed = existing.content_checksum != question["content_checksum"]
if content_changed:
existing.question_title = question["title"] or question["question"]
existing.question_html = question["question"]
existing.explanation_html = question["explanation"]
@@ -313,6 +314,18 @@ async def import_tryout_json_snapshot(payload: dict[str, Any], website_id: int,
existing.is_active = True
existing.last_seen_at = now
# If source content changed, mark AI children derived from this source as stale.
if content_changed:
stale_variants_result = await db.execute(
select(Item).where(
Item.generated_by == "ai",
Item.source_snapshot_question_id == existing.id,
Item.variant_status.in_(["draft", "approved", "active"]),
)
)
for variant in stale_variants_result.scalars().all():
variant.variant_status = "stale"
removed_questions = 0
for source_question_id, existing in existing_questions.items():
if existing.is_active and source_question_id not in incoming_ids:
@@ -320,6 +333,15 @@ async def import_tryout_json_snapshot(payload: dict[str, Any], website_id: int,
existing.latest_snapshot_id = snapshot.id
existing.last_seen_at = now
removed_questions += 1
stale_removed_result = await db.execute(
select(Item).where(
Item.generated_by == "ai",
Item.source_snapshot_question_id == existing.id,
Item.variant_status.in_(["draft", "approved", "active"]),
)
)
for variant in stale_removed_result.scalars().all():
variant.variant_status = "stale"
imported_tryouts.append(
{