Spaces:
Sleeping
Sleeping
File size: 11,772 Bytes
b92b744 9178ee1 b92b744 9178ee1 b92b744 9178ee1 b92b744 285b041 b92b744 d2f845c b92b744 57a9a08 285b041 57a9a08 b92b744 | 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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | """Validated public requests and evidence-constrained LLM schemas."""
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
ConceptTerm = Annotated[str, Field(min_length=2, max_length=200)]
Bucket = Literal[
"effect", "credible_null", "reported_null", "inconclusive", "failed", "unreported"
]
EffectType = Literal["SMD", "MD", "logOR", "logRR", "logHR"]
class SearchFilters(BaseModel):
"""Corpus restrictions applied before the search runs. Every bound is independently optional.
These narrow the full match set, not the ranking, so they also change bucket
counts, the year histogram, the spin rate and the file-drawer share. The
response therefore reports them back and the pipeline warns that the numbers
describe a subset of the index.
"""
model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
yearFrom: int | None = Field(default=None, ge=1500, le=2100)
yearTo: int | None = Field(default=None, ge=1500, le=2100)
minCitations: int | None = Field(default=None, ge=0, le=10000000)
maxCitations: int | None = Field(default=None, ge=0, le=10000000)
@model_validator(mode="after")
def _bounds_are_ordered(self):
if self.yearFrom is not None and self.yearTo is not None and self.yearFrom > self.yearTo:
raise ValueError("yearFrom must not be later than yearTo.")
if (
self.minCitations is not None
and self.maxCitations is not None
and self.minCitations > self.maxCitations
):
raise ValueError("minCitations must not exceed maxCitations.")
return self
@property
def active(self) -> bool:
return any(value is not None for value in self.model_dump().values())
@property
def bounds_citations(self) -> bool:
return self.minCitations is not None or self.maxCitations is not None
@property
def bounds_dates(self) -> bool:
return self.yearFrom is not None or self.yearTo is not None
class ConceptSteer(BaseModel):
"""Concepts that aim the same search, rather than a search of their own.
Each term is one more direction added to (or subtracted from) the question's
own vector before retrieval, so the tags change what ranks highest among the
question's matches. Subtracting is a direction, not a filter: a paper is
pushed down for being about the term, never removed for mentioning it.
"""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
positive: list[ConceptTerm] = Field(default_factory=list, max_length=8)
negative: list[ConceptTerm] = Field(default_factory=list, max_length=8)
@property
def active(self) -> bool:
return bool(self.positive or self.negative)
class SearchRequest(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True, allow_inf_nan=False)
idea: str = Field(min_length=8, max_length=12000)
field: str | None = Field(default=None, max_length=100)
sesoi: float | None = Field(default=None, gt=0, le=1000)
effectType: EffectType = "SMD"
plannedN: int = Field(default=200, ge=4, le=1000000)
alpha: float = Field(default=0.05, gt=0, lt=0.5)
valueSuccess: float = Field(default=100, ge=0, le=1e12)
valueNull: float = Field(default=20, ge=0, le=1e12)
studyCost: float = Field(default=30, ge=0, le=1e12)
outcomeSd: float | None = Field(default=None, gt=0, le=1e6)
baselineRisk: float | None = Field(default=None, gt=0, lt=1)
filters: SearchFilters | None = None
concepts: ConceptSteer | None = None
class Pico(BaseModel):
population: str
intervention: str
comparator: str
outcome: str
synonyms: list[str]
interventionAliases: list[str] = Field(default_factory=list)
outcomeAliases: list[str] = Field(default_factory=list)
populationAliases: list[str] = Field(default_factory=list)
studyDesigns: list[str]
sesoi: float = Field(gt=0)
sesoiRationale: str
effectType: EffectType
class NumberEvidence(BaseModel):
value: float
quote: str
class TextEvidence(BaseModel):
value: str
quote: str
class ResultEvidence(BaseModel):
"""What the report itself states for its primary between-group comparison."""
value: Literal["positive", "null", "mixed"]
quote: str
Direction = Literal["favours_intervention", "favours_comparator", "unclear"]
class DirectionEvidence(BaseModel):
"""Which arm the reported primary result favors; significance is a separate fact."""
value: Direction
quote: str
class BoolEvidence(BaseModel):
value: bool
quote: str
class Extraction(BaseModel):
"""Every returned fact must have verbatim support in the ORIGINAL abstract."""
population: TextEvidence | None
intervention: TextEvidence | None
comparator: TextEvidence | None
outcome: TextEvidence | None
outcome_unit: TextEvidence | None
design: TextEvidence | None
n: NumberEvidence | None
n_intervention: NumberEvidence | None
n_comparator: NumberEvidence | None
mean_intervention: NumberEvidence | None
mean_comparator: NumberEvidence | None
sd_intervention: NumberEvidence | None
sd_comparator: NumberEvidence | None
events_intervention: NumberEvidence | None
events_comparator: NumberEvidence | None
estimate: NumberEvidence | None
ci_low: NumberEvidence | None
ci_high: NumberEvidence | None
ci_level: NumberEvidence | None
ci_sides: TextEvidence | None
p_value: NumberEvidence | None
effect_type: TextEvidence | None
primary_outcome_met: BoolEvidence | None
reported_result: ResultEvidence | None
result_direction: DirectionEvidence | None
has_control: BoolEvidence | None
class Relevance(BaseModel):
"""Ids of retrieved studies that actually address the research question."""
relevant_ids: list[str]
class OutcomeGroup(BaseModel):
label: str
study_ids: list[str]
class OutcomeGroups(BaseModel):
"""Studies judged to measure the same outcome for the same kind of comparison."""
groups: list[OutcomeGroup]
class StudyNote(BaseModel):
id: str
note: str
class Overview(BaseModel):
"""What the matched studies are and how each bears on the question."""
summary: str
notes: list[StudyNote]
class PicoChange(BaseModel):
field: Literal["population", "intervention", "comparator", "outcome"]
to: str
reason: str
class PicoRecommendation(BaseModel):
"""How the asked PICO should change, given what the record shows; empty when it should not."""
changes: list[PicoChange]
rationale: str
class EffectTrend(BaseModel):
summary: str
patterns: list[str]
class Narrative(BaseModel):
summary: str
drivers: list[str]
Coverage = Literal["tests_claim", "tests_related", "background_only"]
NoveltyVerdict = Literal["already_done", "incremental", "open_gap", "insufficient_evidence"]
class NoveltyRequest(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True, allow_inf_nan=False)
hypothesis: str = Field(min_length=8, max_length=4000)
scan: int = Field(default=120, ge=10, le=400)
read: int = Field(default=5, ge=1, le=12)
class MapArithmetic(BaseModel):
"""Embedding arithmetic on idea phrases: start − remove + add.
The terms are embedded separately and combined; the map then reports which
real papers sit nearest the implied point. It is a way of asking "has this
combination been run", not a proof that the analogy holds.
"""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True, allow_inf_nan=False)
start: str = Field(min_length=2, max_length=4000)
remove: list[Annotated[str, Field(min_length=1, max_length=500)]] = Field(
default_factory=list, max_length=6
)
add: list[Annotated[str, Field(min_length=1, max_length=500)]] = Field(
default_factory=list, max_length=6
)
@model_validator(mode="after")
def _has_an_operation(self):
if not self.remove and not self.add:
raise ValueError("An expression needs something to remove or to add.")
return self
class MapRequest(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True, allow_inf_nan=False)
idea: str | None = Field(default=None, min_length=8, max_length=4000)
# When both are sent, the expression is placed and the idea is ignored.
arithmetic: MapArithmetic | None = None
cutoffYear: int | None = Field(default=None, ge=1900, le=2100)
refresh: bool = False
class ConceptSearchRequest(BaseModel):
"""Concepts to add and to subtract before the index is searched by nearest neighbor.
The negatives are part of the query direction, not a post-filter: a paper is
pushed away by a subtracted concept rather than removed for mentioning it.
"""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True, allow_inf_nan=False)
positive: list[ConceptTerm] = Field(min_length=1, max_length=8)
negative: list[ConceptTerm] = Field(default_factory=list, max_length=8)
limit: int = Field(default=20, ge=1, le=50)
filters: SearchFilters | None = None
class Claim(BaseModel):
"""The testable content of a hypothesis, as retrieval terms and comparable facets."""
intervention: str
system: str
outcome: str
direction: str
queries: list[str]
synonyms: list[str]
class PaperRead(BaseModel):
"""What one paper's own results say about the claim, quoted from its text."""
coverage: Coverage
system_tested: str
intervention_tested: str
outcome_measured: str
finding: str
quotes: list[str]
facets_settled: list[str]
facets_untested: list[str]
class NoveltyAssessment(BaseModel):
verdict: NoveltyVerdict
summary: str
gaps: list[str]
settled: list[str]
class IndexedNumberEvidence(BaseModel):
value: float
sentence_index: int = Field(ge=0)
class IndexedTextEvidence(BaseModel):
value: str
sentence_index: int = Field(ge=0)
class IndexedResultEvidence(BaseModel):
value: Literal["positive", "null", "mixed"]
sentence_index: int = Field(ge=0)
class IndexedDirectionEvidence(BaseModel):
value: Direction
sentence_index: int = Field(ge=0)
class IndexedBoolEvidence(BaseModel):
value: bool
sentence_index: int = Field(ge=0)
class IndexedExtraction(BaseModel):
"""Select an original sentence; application code supplies its exact quote."""
population: IndexedTextEvidence | None
intervention: IndexedTextEvidence | None
comparator: IndexedTextEvidence | None
outcome: IndexedTextEvidence | None
outcome_unit: IndexedTextEvidence | None
design: IndexedTextEvidence | None
n: IndexedNumberEvidence | None
n_intervention: IndexedNumberEvidence | None
n_comparator: IndexedNumberEvidence | None
mean_intervention: IndexedNumberEvidence | None
mean_comparator: IndexedNumberEvidence | None
sd_intervention: IndexedNumberEvidence | None
sd_comparator: IndexedNumberEvidence | None
events_intervention: IndexedNumberEvidence | None
events_comparator: IndexedNumberEvidence | None
estimate: IndexedNumberEvidence | None
ci_low: IndexedNumberEvidence | None
ci_high: IndexedNumberEvidence | None
ci_level: IndexedNumberEvidence | None
ci_sides: IndexedTextEvidence | None
p_value: IndexedNumberEvidence | None
effect_type: IndexedTextEvidence | None
primary_outcome_met: IndexedBoolEvidence | None
reported_result: IndexedResultEvidence | None
result_direction: IndexedDirectionEvidence | None
has_control: IndexedBoolEvidence | None
|