Spaces:
Sleeping
Sleeping
File size: 7,983 Bytes
2e818da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | from __future__ import annotations
import logging
from app.agents.cerebras_client import CerebrasClient
from app.schemas.visual_lesson import NucleicVisualizationDraft, PlanCritique, VisualizationRequest
from app.services.nucleic_resolver import ResolvedNucleic
logger = logging.getLogger(__name__)
class NucleicVisualAgent:
def __init__(self, client: CerebrasClient | None = None) -> None:
self.client = client or CerebrasClient()
@staticmethod
def fallback(request: VisualizationRequest, resolved: ResolvedNucleic) -> NucleicVisualizationDraft:
text = f"{request.prompt} {request.selection_text}".lower()
if resolved.structure:
primary = "structure"
elif resolved.intent.mode == "comparison":
primary = "comparison"
elif resolved.nucleobase_molecule and any(
term in text for term in ("3d", "molecule", "element", "atom", "ball-and-stick", "model")
):
primary = "molecule_3d"
elif resolved.intent.focus_base:
primary = "chemistry"
elif any(term in text for term in ("chemistry", "chemical", "nucleotide", "atom", "bond")):
primary = "chemistry"
else:
primary = "helix_3d"
captions = {
"structure": "Official coordinates; rotate, zoom, and focus a verified polymer chain.",
"comparison": "Geometry and chemistry are aligned so structural differences remain visually comparable.",
"chemistry": "Select a base to inspect its bond pattern and connection to sugar and phosphate.",
"helix_3d": "Backbones wind in opposite directions around paired bases; select a base for its chemistry.",
}
if resolved.intent.focus_base and resolved.nucleobase_molecule:
molecule = resolved.nucleobase_molecule
caption = (
f"{molecule.name.title()} · {molecule.molecular_formula} · PubChem CID {molecule.pubchem_cid}. "
+ ("Select an atom to inspect its element and bonds." if primary == "molecule_3d" else "The bond graph shows the isolated nucleobase, without sugar or phosphate.")
)
else:
caption = captions[primary]
return NucleicVisualizationDraft(
primary_view=primary,
visible_feature_ids=[feature.feature_id for feature in resolved.features[:8]],
caption=caption,
)
def _plan(self, request: VisualizationRequest, resolved: ResolvedNucleic, feedback: list[str] | None = None) -> NucleicVisualizationDraft:
catalog = "\n".join(f"- {item.feature_id}: {item.label}" for item in resolved.features)
context = "\n".join(
f"- {source.origin}: {source.excerpt[:280]}"
for source in resolved.sources
if source.origin in {"selection", "project", "web"} and source.excerpt
)[:2400]
messages = [
{
"role": "system",
"content": (
"Choose a visual composition for a nucleic-acid visualization. The visual must answer the prompt; it is not a lesson. "
"Choose only from the supplied feature IDs and one primary view. Use helix_3d for overall form, chemistry for bonds or nucleotides, "
"molecule_3d only when an official nucleobase conformer exists, comparison only for an actual comparison request, and structure only "
"when official macromolecular coordinates exist. The caption must be one short "
"sentence under 180 characters. Do not create sequences, facts, identifiers, coordinates, labels, commands, or teaching steps."
),
},
{
"role": "user",
"content": (
f"PROMPT:\n{request.prompt}\nMODE: {resolved.intent.mode}\nMOLECULE: {resolved.intent.molecule}\n"
f"HAS OFFICIAL STRUCTURE: {bool(resolved.structure)}\nHAS OFFICIAL NUCLEOBASE CONFORMER: "
f"{bool(resolved.nucleobase_molecule)}\nFEATURES:\n{catalog}\nVISUAL CONTEXT:\n{context or '- none'}"
+ ("\nREVISION:\n" + "\n".join(feedback) if feedback else "")
),
},
]
return self.client.structured_complete(
messages,
NucleicVisualizationDraft,
reasoning_effort="medium",
timeout=8.0,
trace_label=f"nucleic-plan:{request.request_id}",
)
def _critic(self, request: VisualizationRequest, resolved: ResolvedNucleic, draft: NucleicVisualizationDraft) -> PlanCritique:
messages = [
{
"role": "system",
"content": (
"Review a visual-first nucleic-acid composition. Reject it if it does not answer the prompt, selects unavailable features, "
"uses structure or molecule_3d without the corresponding verified coordinates, uses comparison for a non-comparison request, "
"or lets prose dominate the visual."
),
},
{"role": "user", "content": f"PROMPT: {request.prompt}\nPLAN: {draft.model_dump_json()}\nAVAILABLE: {[item.feature_id for item in resolved.features]}"},
]
return self.client.structured_complete(
messages,
PlanCritique,
reasoning_effort="low",
timeout=8.0,
trace_label=f"nucleic-critic:{request.request_id}",
)
def plan_with_review(self, request: VisualizationRequest, resolved: ResolvedNucleic) -> tuple[NucleicVisualizationDraft, list[str]]:
warnings: list[str] = []
logger.info("Nucleic visual planner start request=%s", request.request_id)
try:
draft = self._plan(request, resolved)
except Exception as exc:
logger.warning("Nucleic visual planner fallback request=%s error=%s", request.request_id, exc)
return self.fallback(request, resolved), ["Visual composition planning was unavailable; a deterministic prompt-matched composition was used."]
try:
logger.info("Nucleic visual critic start request=%s", request.request_id)
critique = self._critic(request, resolved, draft)
if not critique.approved:
logger.info("Nucleic visual revision start request=%s", request.request_id)
draft = self._plan(request, resolved, critique.feedback)
except Exception as exc:
logger.warning("Nucleic visual critic fallback request=%s error=%s", request.request_id, exc)
warnings.append("Visual composition review was unavailable; the schema-valid composition was used.")
available = {feature.feature_id for feature in resolved.features}
if any(feature_id not in available for feature_id in draft.visible_feature_ids):
draft = self.fallback(request, resolved)
warnings.append("The proposed callouts referenced unavailable features and were replaced deterministically.")
if resolved.features and not draft.visible_feature_ids:
draft = self.fallback(request, resolved)
if draft.primary_view == "structure" and not resolved.structure:
draft = self.fallback(request, resolved)
if draft.primary_view == "molecule_3d" and not resolved.nucleobase_molecule:
draft = self.fallback(request, resolved)
if draft.primary_view == "comparison" and resolved.intent.mode != "comparison":
draft = self.fallback(request, resolved)
required_view = self.fallback(request, resolved).primary_view
if draft.primary_view != required_view:
draft = draft.model_copy(update={"primary_view": required_view})
logger.info("Nucleic visual planner done request=%s view=%s", request.request_id, draft.primary_view)
return draft, warnings
|