from __future__ import annotations import logging from app.schemas.visual_lesson import EvidenceClaim, EvidenceSource, VisualizationRequest from app.services.visual_evidence import VisualEvidenceResolver, _clean logger = logging.getLogger(__name__) CANONICAL_ODE_SOURCES = [ EvidenceSource( source_id="builtin:openstax-ode", origin="builtin", title="OpenStax Calculus Volume 2 — Introduction to Differential Equations", url="https://openstax.org/books/calculus-volume-2/pages/4-1-basics-of-differential-equations", excerpt="Canonical reference metadata for initial-value problems, solution curves, and slope fields.", authority="canonical", ), EvidenceSource( source_id="builtin:openstax-logistic", origin="builtin", title="OpenStax Calculus Volume 2 — The Logistic Equation", url="https://openstax.org/books/calculus-volume-2/pages/4-4-the-logistic-equation", excerpt="Canonical reference metadata for logistic growth, equilibria, and carrying capacity.", authority="canonical", ), ] class DifferentialEquationEvidenceResolver: def __init__(self, base: VisualEvidenceResolver | None = None) -> None: self.base = base or VisualEvidenceResolver() async def resolve(self, project_id: str, request: VisualizationRequest, family: str) -> tuple[list[EvidenceSource], list[EvidenceClaim], list[str]]: prompt = request.prompt.lower() contextual = any(term in prompt for term in ("paper", "selected", "according to", "reported model", "figure")) sources = self.base._selection_sources(request) if contextual else [] warnings: list[str] = [] if contextual and request.project_context_enabled: try: rows = await self.base._project_search(project_id, [_clean(request.prompt)], request.active_document_ids) sources.extend(self.base._candidate_sources(rows, "project")[:4]) logger.info("Differential-equation contextual evidence request=%s rows=%d", request.request_id, len(rows)) except Exception: warnings.append("The selected project context was unavailable; the stated equation and canonical mathematics were used.") else: logger.info("Differential-equation evidence canonical-only request=%s family=%s", request.request_id, family) sources.extend(CANONICAL_ODE_SOURCES) claims = [ EvidenceClaim(claim_id="ode-initial-value", text="A solution curve is determined by the differential equation together with its initial conditions.", claim_type="standard_definition", support_level="canonical", source_ids=["builtin:openstax-ode"]), EvidenceClaim(claim_id="ode-direction-field", text="A direction field displays the local derivative prescribed by a first-order differential equation.", claim_type="standard_definition", support_level="canonical", source_ids=["builtin:openstax-ode"]), EvidenceClaim(claim_id="ode-logistic", text="The logistic equation has equilibrium states at 0 and K, with maximum positive growth at K/2 when r and K are positive.", claim_type="standard_definition", support_level="canonical", source_ids=["builtin:openstax-logistic"]), EvidenceClaim(claim_id="ode-numerical-method", text="The damped-oscillator trajectory is compiled with a fixed-step fourth-order Runge–Kutta method.", claim_type="derived_fact", support_level="derived", source_ids=[]), ] return sources, claims, warnings