Spaces:
Sleeping
Sleeping
File size: 6,653 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 145 146 | from __future__ import annotations
import re
from decimal import Decimal, InvalidOperation
from typing import Any, Iterator, List, Optional
from pydantic import BaseModel
from app.agents.cerebras_client import CerebrasClient
from app.agents.d3.registry import D3Template
from app.agents.senses_agent import VISION_MODEL_ID
class D3SourceValidationError(ValueError):
pass
class D3TransformNarrative(BaseModel):
answer_markdown: str
def _numeric_leaves(value: Any) -> Iterator[Decimal]:
if isinstance(value, BaseModel):
for field_value in value.__dict__.values():
yield from _numeric_leaves(field_value)
elif isinstance(value, dict):
for field_value in value.values():
yield from _numeric_leaves(field_value)
elif isinstance(value, (list, tuple)):
for field_value in value:
yield from _numeric_leaves(field_value)
elif isinstance(value, (int, float)) and not isinstance(value, bool):
yield Decimal(str(value)).normalize()
elif isinstance(value, str) and re.fullmatch(r"\s*[-+]?\d[\d,]*(?:\.\d+)?%?\s*", value):
cleaned = value.strip().replace(",", "").rstrip("%")
try:
yield Decimal(cleaned).normalize()
except InvalidOperation:
return
def _source_numbers(text: str) -> set[Decimal]:
found: set[Decimal] = set()
for token in re.findall(r"[-+]?\d[\d,]*(?:\.\d+)?", text):
try:
found.add(Decimal(token.replace(",", "")).normalize())
except InvalidOperation:
continue
return found
class D3DataExtractor:
def __init__(self, client: Optional[CerebrasClient] = None) -> None:
self._client = client or CerebrasClient()
def fill(
self,
template: D3Template,
concept: str,
chunks: List[dict],
familiarity: str,
*,
strict_source: bool = False,
image_base64: str = "",
selected_metric: str = "",
) -> BaseModel:
chunk_text = "\n\n".join(f"[{c.get('source','?')}]: {c['text']}" for c in chunks)[:3000]
if strict_source:
extraction_rule = (
"Transcribe only labels and values that are visibly present in the SELECTED SOURCE. "
"Never invent, estimate, interpolate, calculate an unstated aggregate, or substitute example data. "
"Keep model/method names exactly associated with their source row. If a metric is specified, use "
"only that column. If the schema cannot be filled faithfully, fail instead of fabricating output."
)
else:
extraction_rule = (
"Prefer real numbers, labels, and relationships found verbatim in the SOURCE MATERIAL. If the source "
"does not contain the data this chart needs, invent a plausible representative illustrative example."
)
messages = [
{"role": "system", "content": (
f"You provide data for a specific trusted visualization. {extraction_rule}\n\n"
f"CHART: {template.title} — {template.data_requirements}\n"
f"SELECTED METRIC: {selected_metric or 'not explicitly named'}\n\nSELECTED SOURCE:\n{chunk_text}"
)},
{"role": "user", "content": (
([{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}},
{"type": "text", "text": f"REQUEST: {concept}\nLEVEL: {familiarity}\nFill the schema from this selected image and any selected text."}]
if image_base64 else f"REQUEST: {concept}\nLEVEL: {familiarity}\nFill the schema from the selected text.")
)},
]
# Never declines -- the system prompt above asks the model to synthesize a
# plausible illustrative example when the source lacks real data, rather
# than leave the schema empty. D3Engine reports whether the result actually
# came from the source via a separate grounding heuristic (see d3/engine.py).
data = self._client.structured_complete(
messages,
template.schema,
model=VISION_MODEL_ID if image_base64 else None,
reasoning_effort="low" if strict_source else "medium",
)
if strict_source and not image_base64:
allowed_numbers = _source_numbers(chunk_text)
missing = sorted({value for value in _numeric_leaves(data) if value not in allowed_numbers})
if missing:
raise D3SourceValidationError(
"Extracted visualization contains numeric values absent from the selected source: "
+ ", ".join(str(value) for value in missing[:6])
)
return data
def explain(
self,
*,
concept: str,
template: D3Template,
data: BaseModel,
chunks: List[dict],
image_base64: str = "",
) -> str:
"""Answer the analytical part of a transformation request from the same source."""
chunk_text = "\n\n".join(str(c.get("text", "")) for c in chunks)[:5000]
system = (
"Give a concise research-assistant answer accompanying a source-selected visualization. "
"Directly answer the user's analytical question in at most four short sentences. Use only "
"facts and numeric comparisons visible in the selected source or the extracted chart data. "
"For improvement questions, identify the compared baseline and where values increased; do "
"not call a larger value an improvement when the metric direction is unknown. Do not discuss "
"the routing process, template selection, or extraction. If the source cannot support part of "
"the requested interpretation, state that limitation briefly."
)
text = (
f"REQUEST:\n{concept}\n\nVISUAL FORM: {template.title}\n\n"
f"EXTRACTED DATA:\n{data.model_dump_json()}\n\nSELECTED TEXT:\n{chunk_text}"
)
content: Any = text
if image_base64:
content = [
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}},
{"type": "text", "text": text},
]
result = self._client.structured_complete(
[{"role": "system", "content": system}, {"role": "user", "content": content}],
D3TransformNarrative,
model=VISION_MODEL_ID if image_base64 else None,
reasoning_effort="low",
)
return result.answer_markdown.strip()
|