study-buddy / app /services /visualization_service.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
3.04 kB
from __future__ import annotations
from typing import Any, Optional
from app.agents.d3.engine import D3Engine
from app.agents.formula_engine import FormulaEngine
from app.agents.modality_router import ModalityDecision, VisualModalityRouter
from app.agents.shell_visual_engine import ShellVisualEngine
from app.agents.text_ref_engine import TextRefEngine
from app.agents.tutor_agent import TutorAgent
from app.schemas.graph import HTML5VisualPayload
class VisualizationService:
"""Thin dispatch boundary in front of the 5 visual-generation engines.
Does no chunk/RAG work itself -- it only (1) delegates modality classification
to `VisualModalityRouter`, and (2) routes an already-decided modality to whichever
of the 5 engines owns it: formula/graph/2d_text/3d/2d_anim. It never declines on
its own, even with zero/weak chunks -- each engine independently handles that by
synthesizing an illustrative result and reporting `HTML5VisualPayload.source`
accordingly, since the student has already explicitly asked for a visual.
"""
def __init__(
self,
router: Optional[VisualModalityRouter] = None,
formula: Optional[FormulaEngine] = None,
text_ref: Optional[TextRefEngine] = None,
d3: Optional[D3Engine] = None,
shell: Optional[ShellVisualEngine] = None,
) -> None:
self.router = router or VisualModalityRouter()
self.formula = formula or FormulaEngine()
self.text_ref = text_ref or TextRefEngine()
self.d3 = d3 or D3Engine()
self.shell = shell or ShellVisualEngine()
def classify(
self,
selection_text: str,
card_markdown: str,
chunks: list[dict[str, Any]],
familiarity: str,
) -> ModalityDecision:
return self.router.classify(selection_text, card_markdown, chunks, familiarity)
def generate(
self,
concept: str,
modality: str,
familiarity: str,
chunks: list[dict[str, Any]] | None = None,
web_results: list[dict[str, Any]] | None = None,
) -> HTML5VisualPayload:
chunks = chunks or []
if modality == "formula":
return self.formula.generate(concept, chunks, familiarity)
if modality == "2d_text":
return self.text_ref.generate(concept, chunks, familiarity, web_results)
if modality == "graph":
return self.d3.generate(concept, chunks, familiarity)
if modality in ("3d", "2d_anim"):
return self.shell.generate(concept, modality, chunks, familiarity)
# Defensive fallback for a bad/unrecognized modality string -- the router
# itself never produces one of these, since `ModalityDecision.modality`
# is a closed Literal of the 5 real values.
reason = "This one is better discussed in chat."
return HTML5VisualPayload(
html_code=TutorAgent._decline_html(concept, reason),
animation_type="2d_text",
explanation=reason,
)