Text Classification
PEFT
lora
document-question-answering
structured-decisions
calibration
synthetic-evaluation
Instructions to use botp/Solomon with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use botp/Solomon with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 9,155 Bytes
1d2de8a | 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 | """Inference-only source candidates and explicitly labelled retrieval baselines."""
import re
from solomon.evidence import digest,rank_candidates,validate_spans
def candidates(text):
"""Source sentences retaining exact character ranges; no authoring labels."""
ends=[m.end() for m in re.finditer(r'(?<=[.!?])(?:[ \t]+|\n+)|\n\s*\n',text)]+[len(text)]
result=[];start=0
for end in ends:
if end>start and text[start:end].strip():
result.append({'id':f'text:{start}:{end}','kind':'text','start':start,'end':end,
'text':text[start:end],'source_sha256':digest(text)})
start=end
return result
def governing_context(text,spans=None):
"""Generic rule/retraction interpretation context, independent of question gold."""
spans=candidates(text) if spans is None else spans
pattern=re.compile(r'general rule|both limbs|same (?:person|courier|member|officer)|neither limb|missing requirement|silence|withdrawn passage|dates and seniority|a withdrawn|outside these|bands are|order of the levels|no.*precedence|each passage|a grant in force|an exception to a rule',re.I)
return [s for s in spans if pattern.search(s['text'])]
def merge(text,spans):
intervals=[]
for span in sorted(spans,key=lambda s:(s['start'],s['end'])):
a,b=span['start'],span['end']
if intervals and a<=intervals[-1][1]:intervals[-1]=(intervals[-1][0],max(b,intervals[-1][1]))
else:intervals.append((a,b))
result=[{'id':f'text:{a}:{b}','kind':'text','start':a,'end':b,'text':text[a:b],'source_sha256':digest(text)} for a,b in intervals]
return validate_spans(text,result)
def render_subset(text,spans,*,include_context=True):
selected=merge(text,list(spans)+(governing_context(text) if include_context else []))
return '\n\n'.join(s['text'] for s in selected),selected
_WORD=re.compile(r'\w+')
_SCALE=re.compile(r'\bbands?\b|\bscale\b|from lowest to highest|order of the levels',re.I)
POOL_V2={'name':'idf-rare-rule-v2','idf_limit':24,'rare_df':16,'rules':True}
def _words(s):return set(_WORD.findall(s.casefold()))
def rule_candidates(text,spans=None):
"""Question-independent rule, exception, withdrawal-convention and scale passages."""
spans=candidates(text) if spans is None else spans
rules={s['id'] for s in governing_context(text,spans)}
return [s for s in spans if s['id'] in rules or _SCALE.search(s['text'])]
def candidate_pool(question,spans,text=None,*,idf_limit=24,rare_df=16,rules=True,**_):
"""Bounded source-only pool: document-IDF top-k, every passage sharing a rare
question term (typically the subject's name), plus question-independent rules.
Candidate order is source order. candidate_score stays the plain lexical-overlap
fraction used by the frozen relevance-head feature, so heads remain comparable.
Gold is never an input.
"""
import math
if idf_limit<1 or rare_df<0:raise ValueError('invalid candidate pool policy')
ranked=rank_candidates(question,spans,len(spans)) if spans else []
words=[_words(c['text']) for c in ranked];q=_words(question);n=len(ranked)
df={}
for ws in words:
for w in ws:df[w]=df.get(w,0)+1
idf=[sum(math.log((n+1)/(df[w]+.5)) for w in ws&q) for ws in words]
order=sorted(range(n),key=lambda i:(-idf[i],ranked[i]['start']))
reason={}
for i in order[:idf_limit]:reason.setdefault(ranked[i]['id'],'idf')
rare={w for w in q if df.get(w,0)<=rare_df}
for c,ws in zip(ranked,words):
if ws&rare:reason.setdefault(c['id'],'rare_term')
if rules:
for c in rule_candidates(text or '',ranked):reason.setdefault(c['id'],'rule')
return sorted(({**c,'pool_reason':reason[c['id']]} for c in ranked if c['id'] in reason),key=lambda c:c['start'])
_WITHDRAW=re.compile(r'withdr[ae]w|withdrawn|take back|disregard|rescind|retract|should not be relied|there is substituted|is deleted|expressly delete',re.I)
_REFERENCE=re.compile(r'(?:message of|wrote on|message dated) (\d{1,2} [A-Z][a-z]+)|minute (\d+)|[Ee]ntry (\d+) of [Ss]chedule (\d+)|clause ([\d.]+)|passage in my message')
_EXCEPTION=re.compile(r'does not apply to|subject to the exception|is an exception|except (?:where|that|for)\b',re.I)
_STOP={'this','that','these','those','with','under','which','what','does','file','correspondence','record','records','recorded',
'minutes','agreement','bundle','messages','message','stand','stands','taking','reading','whole','strength','position',
'open','given','have','been','applies','apply','entitled','allowed','liberty','free','from','there','their','they',
'decisions','here','schedules','schedule','papers','office','shown','show','shows','case','matters','terms','place','placed'}
DEPENDENCIES_V1={'name':'withdrawal-exception-v1','withdrawals':True,'exceptions':True,'prune_withdrawn':False,'rare_df':16}
def _topic(words,exclude):
return {w for w in words if len(w)>=4 and w not in _STOP and w not in exclude and not w.isdigit()}
def _withdrawn_block(text,passage):
"""Source range that a withdrawal refers to (dated message or numbered minute); None if not resolvable."""
m=_REFERENCE.search(passage)
if not m:return None
if m.group(1):head=re.search(r'Message \d+\. '+re.escape(m.group(1))+r'\.',text)
elif m.group(2):head=re.search(r'(?:^|\n)'+m.group(2)+r'\. ',text)
else:return None
if not head:return None
end=text.find('\n\n',head.end());return head.start(),(len(text) if end<0 else end)
def dependency_expand(question,spans,selected_ids,text,*,withdrawals=True,exceptions=True,prune_withdrawn=False,rare_df=16,**_):
"""Complete a unit's selected evidence with its withdrawal/exception dependencies.
Source text and the already-selected passages only; never gold. Units with no
selection are unchanged, so no-positive-support decisions are preserved.
- withdrawal: a passage with a withdrawal verb AND an explicit reference (message/minute/
entry/clause) that names the question's subject (a rare capitalised name, also present in a
selected passage) and its most specific in-source topic word;
- exception: an exception clause immediately following a selected passage;
- prune_withdrawn (optional): drop selected passages inside the dated message / numbered
minute a kept withdrawal refers to, when they share its subject and a topic word.
Returns {'keep','added','pruned'} as candidate id lists in source order.
"""
selected=[s for s in spans if s['id'] in set(selected_ids)]
if not selected:return {'keep':[],'added':[],'pruned':[]}
words=[_words(s['text']) for s in spans];df={}
for ws in words:
for w in ws:df[w]=df.get(w,0)+1
# Subject: capitalised non-initial question tokens (names) that are rare in the source.
names=re.findall(r'(?<!^)(?<![.?!] )\b[A-Z][\w&\'-]*',question.strip())
subject={w for n in names for w in _words(n) if len(w)>=3 and df.get(w,0)<=rare_df}
q=_words(question);present=[w for w in _topic(q,subject) if df.get(w,0)]
# The withdrawal must name the question's most specific in-source topic word (e.g. the attribute).
topic={min(present,key=lambda w:(df[w],w))} if present else set()
chosen={s['id'] for s in selected};sel_words=set().union(*(_words(s['text']) for s in selected));added=[]
for i,(s,ws) in enumerate(zip(spans,words)):
if s['id'] in chosen:continue
if (withdrawals and _WITHDRAW.search(s['text']) and _REFERENCE.search(s['text'])
and ws&subject&sel_words and ws&topic):added.append(s['id']);continue
if exceptions and i>0 and spans[i-1]['id'] in chosen and _EXCEPTION.search(s['text']):added.append(s['id'])
keep=chosen|set(added);pruned=[]
if prune_withdrawn:
for s,ws in zip(spans,words):
if s['id'] not in keep or not _WITHDRAW.search(s['text']):continue
block=_withdrawn_block(text,s['text'])
if block is None:continue
key=ws&subject;about=_topic(ws,subject)
for t,tw in zip(spans,words):
if (t['id'] in keep and t['id']!=s['id'] and block[0]<=t['start'] and t['end']<=block[1]+2
and tw&key and tw&about and not _WITHDRAW.search(t['text'])):pruned.append(t['id'])
order=[s['id'] for s in spans];pruned=set(pruned)
return {'keep':[i for i in order if i in keep and i not in pruned],'added':[i for i in order if i in set(added)],
'pruned':[i for i in order if i in pruned]}
def lexical_select(text,questions,limit=4):
source=candidates(text);chosen={}
for q in questions:
for candidate in rank_candidates(q,source,limit):
if candidate['candidate_score']>0:chosen[candidate['id']]=candidate
return {'evidence':sorted(chosen.values(),key=lambda c:c['start']),
'method':'lexical_overlap','verification':'retrieval_only','faithfulness_established':False}
def remove(text,spans):
spans=merge(text,spans);cursor=0;parts=[]
for s in spans:parts.append(text[cursor:s['start']]);cursor=s['end']
parts.append(text[cursor:]);return ''.join(parts)
|