**Prepared sessions.** Most of this existed: unanswered first, weakest topic next, wrong-before-right after that, all scaled by what share of the real paper each topic carries. What it could not do was change with time, say anything about itself, or be reached without filling in a form. Evidence now decays on a thirty-day half-life. Exponential rather than a fixed window because memory has a slope, not a cliff — under a window, 29 days counts fully and 31 counts for nothing — and because it is memoryless, so an answer's weight does not shift when unrelated questions are answered, which is what lets the preview stay a valid forecast. Spring is worth an eighth of last week. Two things decay: a question's recall probability, drifting towards even rather than past it, so an old right answer becomes eligible rather than wrong; and a topic's accuracy, against a prior of two "no idea" answers, which fixes "right once, known forever". Strict unanswered-first meant that on a bank of 2,900 nothing was ever recycled — spaced repetition existed and was unreachable. Review now takes up to two fifths of a session. And the damping that spread the picks across topics was applied only to seen material, so a learner with no history was handed the heaviest domain entire instead of a spread; that was live. The plan is the product. It is computed, shown, and then the session is built from that plan's own ids and the plan returned with it, so the two cannot differ; every figure in it is a tally over the chosen questions rather than a forecast. No model touches the ranking — a learner asking "why these twenty" has to get the same answer twice. **Vision.** The proxy's own `/model/info` says which models can see, so nothing is hard-coded: 77 report yes, 11 no, and 328 say nothing at all, which means absent rather than incapable — so those are asked once with an 8px PNG and the refusal cached. The deployment's main model turns out not to see, and questions carry figures the learner is looking at, so the tutor was answering about an image it had never been shown. It routes to a configured tool model now, folds the description back in as text saying plainly where it came from, and caches on the bytes because the same figure is re-sent every turn. Also fixed on the way: `article` was missing from the admin's task list, so article drafting always ran on the fallback model whatever an administrator chose; and `.jpx` stem images were sent as JPEG because `mimetypes` guesses that from the name, so the provider rejected them two hops later. An administrator must pick a tool model in Settings → AI models. Until then the tutor says a figure exists that nothing could read, rather than describing one it cannot see. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
226 lines
11 KiB
Python
226 lines
11 KiB
Python
"""Seed a small set of showcase topic articles across the main pediatric systems.
|
||
|
||
Purpose is demonstration: every article is short, clearly marked as starter
|
||
content for an educator to review, filed under a real category, and linked to
|
||
bank questions from the same category so the article -> question -> card flow
|
||
is visible end to end.
|
||
|
||
Idempotent — an article whose slug already exists is left untouched.
|
||
|
||
docker compose exec backend python -m scripts.seed_showcase_articles
|
||
"""
|
||
import hashlib
|
||
import sys
|
||
|
||
from app.database import SessionLocal
|
||
from app.models.article import Article, QuestionArticleLink
|
||
from app.services import article_service
|
||
from app.models.question import Question
|
||
from app.models.question_category import QuestionCategory
|
||
from app.models.user import User
|
||
|
||
REVIEW_NOTE = "Starter article for demonstration — review and expand before teaching from it."
|
||
|
||
# (slug, title, category name, summary, intro, [(section slug, section title, body)])
|
||
SHOWCASE = [
|
||
(
|
||
"bronchiolitis", "Bronchiolitis", "Pulmonology",
|
||
"Common lower respiratory infection of infants under 2 years.",
|
||
"Bronchiolitis is the most common lower respiratory tract infection in infants, usually caused by "
|
||
"respiratory syncytial virus (RSV). Care is supportive.",
|
||
[
|
||
("presentation", "Presentation",
|
||
"Preceded by 1–3 days of coryza, then cough, tachypnea, wheeze and crackles. Peak severity is "
|
||
"typically day 3–5 of illness. Apnea can be the presenting sign in young or preterm infants."),
|
||
("evaluation", "Evaluation",
|
||
"The diagnosis is clinical. Routine chest radiographs and viral testing are not needed for typical "
|
||
"cases. Assess work of breathing, hydration and feeding, and oxygen saturation."),
|
||
("management", "Management",
|
||
"Supportive care: suctioning, hydration and oxygen when saturations are persistently low. "
|
||
"Bronchodilators, corticosteroids and antibiotics are not routinely indicated."),
|
||
],
|
||
),
|
||
(
|
||
"acute-otitis-media", "Acute otitis media", "Otolaryngology",
|
||
"Middle-ear infection — one of the commonest reasons for antibiotics in children.",
|
||
"Acute otitis media (AOM) is a middle-ear effusion with acute signs of inflammation.",
|
||
[
|
||
("diagnosis", "Diagnosis",
|
||
"Requires a bulging tympanic membrane, or new otorrhea not due to otitis externa, together with "
|
||
"acute onset of symptoms. A cloudy, immobile drum supports effusion."),
|
||
("management", "Management",
|
||
"Analgesia for every child. Antibiotics are indicated for young infants, severe symptoms, or "
|
||
"bilateral disease in the very young; otherwise observation with follow-up is an option."),
|
||
("complications", "Complications",
|
||
"Uncommon but important: mastoiditis, tympanic membrane perforation, hearing loss from persistent "
|
||
"effusion, and rarely intracranial extension."),
|
||
],
|
||
),
|
||
(
|
||
"kawasaki-disease", "Kawasaki disease", "Cardiology",
|
||
"Medium-vessel vasculitis of childhood with coronary artery risk.",
|
||
"Kawasaki disease is an acute, self-limited vasculitis of young children; the key concern is coronary "
|
||
"artery aneurysm.",
|
||
[
|
||
("features", "Clinical features",
|
||
"Fever lasting at least 5 days plus features such as bilateral non-exudative conjunctivitis, oral "
|
||
"mucosal changes, polymorphous rash, extremity changes, and cervical lymphadenopathy."),
|
||
("evaluation", "Evaluation",
|
||
"Inflammatory markers are raised. Echocardiography assesses the coronary arteries at diagnosis and "
|
||
"on follow-up."),
|
||
("management", "Management",
|
||
"Intravenous immunoglobulin with aspirin, given as early as possible, reduces the risk of coronary "
|
||
"aneurysms."),
|
||
],
|
||
),
|
||
(
|
||
"type-1-diabetes-presentation", "Type 1 diabetes — presentation", "Endocrinology",
|
||
"Recognising new-onset diabetes and diabetic ketoacidosis in children.",
|
||
"Type 1 diabetes results from autoimmune destruction of pancreatic beta cells and often presents acutely "
|
||
"in children.",
|
||
[
|
||
("presentation", "Presentation",
|
||
"Polyuria, polydipsia and weight loss over days to weeks. Nocturnal enuresis in a previously dry "
|
||
"child is a useful clue."),
|
||
("dka", "Diabetic ketoacidosis",
|
||
"Hyperglycemia with metabolic acidosis and ketosis. Kussmaul breathing, vomiting and abdominal pain "
|
||
"are common; cerebral edema is the most feared complication."),
|
||
("initial-management", "Initial management",
|
||
"Restore circulation, correct fluid and electrolyte deficits gradually, and start an insulin "
|
||
"infusion after fluids per local protocol."),
|
||
],
|
||
),
|
||
(
|
||
"iron-deficiency-anemia", "Iron deficiency anemia", "Hematology-Oncology",
|
||
"The commonest cause of anemia in young children.",
|
||
"Iron deficiency is usually nutritional in toddlers and related to menstrual loss in adolescents.",
|
||
[
|
||
("risk-factors", "Risk factors",
|
||
"Excessive cow's milk intake in toddlers, prematurity, low dietary iron, and chronic blood loss."),
|
||
("laboratory", "Laboratory findings",
|
||
"Microcytic, hypochromic anemia with a raised red cell distribution width, low ferritin and low "
|
||
"transferrin saturation."),
|
||
("management", "Management",
|
||
"Oral iron with dietary counselling; recheck the response after several weeks. Look for a source of "
|
||
"blood loss when the history does not fit."),
|
||
],
|
||
),
|
||
(
|
||
"urinary-tract-infection", "Urinary tract infection", "Nephrology & Urology",
|
||
"A frequent cause of unexplained fever in infants.",
|
||
"Urinary tract infection should be considered in any young child with unexplained fever.",
|
||
[
|
||
("presentation", "Presentation",
|
||
"Infants present with fever, irritability, vomiting or poor feeding. Older children may describe "
|
||
"dysuria, frequency and flank pain."),
|
||
("diagnosis", "Diagnosis",
|
||
"Requires both pyuria and a positive urine culture from an appropriately collected specimen. "
|
||
"Bag specimens are unreliable for culture."),
|
||
("management", "Management",
|
||
"Antibiotics guided by local resistance patterns, adjusted once sensitivities return. Imaging "
|
||
"decisions follow age and recurrence per local guidance."),
|
||
],
|
||
),
|
||
(
|
||
"asthma-in-children", "Asthma in children", "Pulmonology",
|
||
"Chronic airway inflammation with reversible obstruction.",
|
||
"Childhood asthma presents with recurrent wheeze, cough and breathlessness that respond to bronchodilators.",
|
||
[
|
||
("diagnosis", "Diagnosis",
|
||
"A pattern of recurrent, variable symptoms with documented reversible airflow obstruction where "
|
||
"spirometry is feasible (usually from about age 5)."),
|
||
("acute-exacerbation", "Acute exacerbation",
|
||
"Inhaled short-acting beta agonists, systemic corticosteroids and oxygen as needed. Reassess the "
|
||
"response and escalate for poor responders."),
|
||
("chronic-control", "Chronic control",
|
||
"Step therapy based on symptom control, with inhaler technique and adherence reviewed at every "
|
||
"visit before stepping up."),
|
||
],
|
||
),
|
||
(
|
||
"developmental-milestones", "Developmental milestones", "Developmental & Behavioral",
|
||
"A practical framework for surveillance and red flags.",
|
||
"Developmental surveillance at every health visit, with standardised screening at set ages, is the basis "
|
||
"of early identification.",
|
||
[
|
||
("domains", "Domains",
|
||
"Gross motor, fine motor, language and social/adaptive development are tracked separately; delay in "
|
||
"one domain does not always imply global delay."),
|
||
("red-flags", "Red flags",
|
||
"Loss of previously acquired skills at any age is always a red flag and warrants prompt evaluation."),
|
||
("next-steps", "Next steps",
|
||
"Confirm with a standardised tool, check hearing and vision, and refer to early intervention "
|
||
"services while the evaluation proceeds."),
|
||
],
|
||
),
|
||
]
|
||
|
||
|
||
def _section_id(article_slug: str, section_slug: str) -> str:
|
||
"""Stable 32-hex section ID (the format the articles API validates)."""
|
||
return hashlib.md5(f"{article_slug}/{section_slug}".encode()).hexdigest()
|
||
|
||
|
||
def main():
|
||
db = SessionLocal()
|
||
try:
|
||
moderator = db.query(User.id).filter(User.role.in_(["admin", "moderator"])).order_by(User.id).first()
|
||
moderator = moderator[0] if moderator else None
|
||
categories = {c.name: c.id for c in db.query(QuestionCategory).filter(QuestionCategory.parent_id.is_(None)).all()}
|
||
|
||
created, made = 0, []
|
||
for slug, title, category_name, summary, intro, sections in SHOWCASE:
|
||
if db.query(Article.id).filter(Article.slug == slug).first():
|
||
print(f"skip (exists): {slug}")
|
||
continue
|
||
category_id = categories.get(category_name)
|
||
if category_id is None:
|
||
print(f"warning: category {category_name!r} not found; filing {slug} uncategorised")
|
||
article = Article(
|
||
slug=slug,
|
||
title=title,
|
||
summary=f"{summary} {REVIEW_NOTE}",
|
||
content=intro,
|
||
sections=[
|
||
{"id": _section_id(slug, sec_slug), "slug": sec_slug, "title": sec_title, "content": body}
|
||
for sec_slug, sec_title, body in sections
|
||
],
|
||
category_id=category_id,
|
||
user_id=moderator,
|
||
status="published",
|
||
)
|
||
db.add(article)
|
||
made.append(article)
|
||
created += 1
|
||
print(f"created: {title} ({category_name})")
|
||
db.commit()
|
||
# Seeded content has to be findable by what it says, like anything else.
|
||
for article in made:
|
||
article_service.reindex(db, article)
|
||
|
||
# Link up to three bank questions from the same category to each article.
|
||
linked = 0
|
||
for slug, title, category_name, *_ in SHOWCASE:
|
||
article = db.query(Article).filter(Article.slug == slug).first()
|
||
if not article or not article.category_id:
|
||
continue
|
||
question_ids = [
|
||
row[0] for row in db.query(Question.id)
|
||
.filter(Question.question_category_id == article.category_id)
|
||
.order_by(Question.id).limit(3).all()
|
||
]
|
||
for question_id in question_ids:
|
||
exists = db.query(QuestionArticleLink.id).filter_by(
|
||
question_id=question_id, article_id=article.id, section_id=None).first()
|
||
if not exists:
|
||
db.add(QuestionArticleLink(question_id=question_id, article_id=article.id,
|
||
section_id=None, user_id=moderator))
|
||
linked += 1
|
||
db.commit()
|
||
print(f"Showcase articles ready: {created} created, {linked} question links added.")
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|