Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/trace-ace-v129-tutor-uptake.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Trace Ace V129 Tutor Uptake

on:
pull_request:
branches: [agent/v121-cache-batch2]
paths:
- '.github/workflows/trace-ace-v129-tutor-uptake.yml'
- 'competitions/trace_the_ace/v129_tutor_uptake.py'
workflow_dispatch:

env:
PREPARED_RUN_ID: '32400309220'

jobs:
evaluate:
runs-on: ubuntu-24.04
timeout-minutes: 12
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- name: Install dependencies
run: python -m pip install --disable-pip-version-check numpy scipy scikit-learn pandas fastembed==0.8.0
- name: Restore frozen transcript archive from GitHub cache only
id: transcript-cache
uses: actions/cache@v4
with:
path: transcripts.zip
key: trace-ace-transcripts-v1-603547640
- name: Require exact cache hit
run: |
test '${{ steps.transcript-cache.outputs.cache-hit }}' = 'true'
test "$(stat -c%s transcripts.zip)" = '603547640'
echo 'e685b85b04694e130c25b17d09cdd1892fbda5e9fa685e98b2300114b915aa2d transcripts.zip' | sha256sum -c -
- name: Download exact frozen prepared artifact
uses: actions/download-artifact@v4
with:
name: v121-prepared
path: v121_prepared
repository: heathsanchez/mathgraph
run-id: ${{ env.PREPARED_RUN_ID }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Verify frozen sample
run: grep -q 'b1612f9fe4558680e468afb2a2452b75c603c244934fe62f7345feee68a61bc1' v121_prepared/manifest.json
- name: Run frozen V129
run: python competitions/trace_the_ace/v129_tutor_uptake.py --archive transcripts.zip --dir v121_prepared --out v129_tutor_uptake.json
- name: Show decision
run: cat v129_tutor_uptake.json
- uses: actions/upload-artifact@v4
with:
name: trace-ace-v129-tutor-uptake
path: v129_tutor_uptake.json
retention-days: 14
57 changes: 57 additions & 0 deletions .github/workflows/trace-ace-v131-crowding-gated-uptake.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Trace Ace V131 Crowding Gated Uptake
on:
pull_request:
branches: [agent/v121-cache-batch2]
paths:
- 'competitions/trace_the_ace/v129_tutor_uptake.py'
- 'competitions/trace_the_ace/v131_crowding_gated_uptake.py'
- '.github/workflows/trace-ace-v131-crowding-gated-uptake.yml'
workflow_dispatch:

env:
PREPARED_RUN_ID: 32400309220

jobs:
evaluate:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- name: Install dependencies
run: python -m pip install --disable-pip-version-check numpy scipy scikit-learn pandas fastembed==0.8.0
- name: Restore frozen transcript archive from GitHub cache only
id: transcript-cache
uses: actions/cache@v4
with:
path: transcripts.zip
key: trace-ace-transcripts-v1-603547640
- name: Require exact cache hit
run: |
test '${{ steps.transcript-cache.outputs.cache-hit }}' = 'true'
test "$(stat -c%s transcripts.zip)" = '603547640'
echo 'e685b85b04694e130c25b17d09cdd1892fbda5e9fa685e98b2300114b915aa2d transcripts.zip' | sha256sum -c -
- name: Download exact frozen prepared artifact
uses: actions/download-artifact@v4
with:
name: v121-prepared
path: v121_prepared
repository: heathsanchez/mathgraph
run-id: ${{ env.PREPARED_RUN_ID }}
github-token: ${{ github.token }}
- name: Verify frozen sample
run: grep -q 'b1612f9fe4558680e468afb2a2452b75c603c244934fe62f7345feee68a61bc1' v121_prepared/manifest.json
- name: Run frozen V131
run: python competitions/trace_the_ace/v131_crowding_gated_uptake.py --archive transcripts.zip --dir v121_prepared --out v131_crowding_gated_uptake.json
- name: Show decision
if: always()
run: cat v131_crowding_gated_uptake.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: trace-ace-v131-crowding-gated-uptake
path: v131_crowding_gated_uptake.json
retention-days: 14
94 changes: 94 additions & 0 deletions competitions/trace_the_ace/v129_tutor_uptake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""V129 TUTOR UPTAKE residual over frozen V97.

Primary separator: whether the tutor's next utterance lexically/structurally takes
up the student's immediately preceding language. V75 raw hashing preserves words,
but not this cross-turn relation explicitly.

Control: deterministically rotate tutor replies among student->tutor pairs inside
each session, preserving the exact student/tutor text multisets and pair count while
destroying local response alignment. No labels enter feature construction. No sweep.
"""
from __future__ import annotations
import argparse,csv,io,json,re,zipfile,hashlib
from pathlib import Path
import numpy as np
from scipy.sparse import load_npz
from sklearn.linear_model import LogisticRegression
from v110_residual_collider_state_discovery import ll,logit
from v121_pretrained_semantic_residual import p97_oof,collision_mask
from v75_canonical_trajectory import SEED

TOK=re.compile(r"[a-z0-9]+(?:\.[0-9]+)?",re.I)
NEG=re.compile(r"\b(?:no|not quite|incorrect|wrong|careful|try again|almost|remember|instead|actually)\b",re.I)
POS=re.compile(r"\b(?:yes|yeah|correct|right|exactly|perfect|good|great|well done|that's it|thats it|you got it|spot on)\b",re.I)
QUESTION=re.compile(r"\?|\b(?:what|which|how|why|can you|could you|tell me|work out|calculate|solve|find)\b",re.I)
STOP={'the','a','an','and','or','to','of','in','on','for','with','is','are','be','as','by','from','this','that','these','those','you','your','we','it','its'}
EPS=1e-5

def toks(s): return {x for x in TOK.findall(str(s).lower()) if len(x)>1 and x not in STOP}
def jac(a,b):
A,B=toks(a),toks(b)
return len(A&B)/len(A|B) if A and B else 0.0

def pairs(rows):
out=[]
for i,r in enumerate(rows[:-1]):
if str(r.get('role','')).lower()!='student': continue
s=str(r.get('content',''))
j=None
for k in range(i+1,min(len(rows),i+4)):
if str(rows[k].get('role','')).lower()=='tutor': j=k; break
if str(rows[k].get('role','')).lower()=='student': break
if j is not None: out.append((s,str(rows[j].get('content',''))))
return out

def feats(P,shift=0):
if not P: return np.zeros(10,float)
S=[p[0] for p in P]; T=[p[1] for p in P]; n=len(P)
if shift and n>1: T=T[shift%n:]+T[:shift%n]
o=np.array([jac(s,t) for s,t in zip(S,T)],float)
neg=np.array([bool(NEG.search(t)) for t in T]); pos=np.array([bool(POS.search(t)) for t in T]); q=np.array([bool(QUESTION.search(t)) for t in T])
substantive=np.array([len(toks(s))>=2 or bool(re.search(r'\d|[=+\-/*×÷%]',s)) for s in S])
def mean(mask): return float(o[mask].mean()) if mask.any() else 0.0
return np.array([o.mean(),np.quantile(o,.75),np.quantile(o,.9),o.max(),mean(neg),mean(pos),mean(q),mean(substantive),float((o>0).mean()),np.log1p(n)],float)

def residual_oof(P,X,y,splits):
q=np.zeros(len(y),float)
for tr,va in splits:
mu=X[tr].mean(0); sd=X[tr].std(0)+1e-6
A=np.c_[logit(P[tr]),(X[tr]-mu)/sd]; B=np.c_[logit(P[va]),(X[va]-mu)/sd]
m=LogisticRegression(C=.05,max_iter=300,solver='liblinear',random_state=SEED).fit(A,y[tr])
q[va]=m.predict_proba(B)[:,1]
return np.clip(q,EPS,1-EPS)

def evalg(name,groups,X75,Xr,y,support,obj,Xreal,Xctrl):
P,splits=p97_oof(X75,Xr,y,groups,support); Q=residual_oof(P,Xreal,y,splits); C=residual_oof(P,Xctrl,y,splits)
b=ll(y,P); r=ll(y,Q); c=ll(y,C); mask=collision_mask(P,y,obj,.01)
out={'geometry':name,'baseline_v97_ll':b,'uptake':{'ll':r,'gain':b-r},'rotated_reply_control':{'ll':c,'gain':b-c},'uptake_minus_control_gain':c-r,'hard_collision':{'rows':int(mask.sum())}}
if mask.any():
bb=ll(y[mask],P[mask]); rr=ll(y[mask],Q[mask]); cc=ll(y[mask],C[mask])
out['hard_collision'].update({'baseline_ll':bb,'uptake_ll':rr,'uptake_gain':bb-rr,'control_ll':cc,'uptake_minus_control_gain':cc-rr})
return out

def main(a):
d=Path(a.dir); z=np.load(d/'arrays.npz',allow_pickle=True); y=z['y']; obj=z['objectives']; support=z['support']; sessions=z['sessions']
X75=load_npz(d/'X75.npz'); Xr=load_npz(d/'Xr.npz'); cache={}
with zipfile.ZipFile(a.archive) as za:
names=set(za.namelist())
for sid in np.unique(sessions):
name=f'{sid}.csv'
if name not in names: raise RuntimeError('missing '+name)
with za.open(name) as f: rows=list(csv.DictReader(io.TextIOWrapper(f,encoding='utf-8-sig',newline='')))
P=pairs(rows); shift=1+(int(hashlib.sha256(str(sid).encode()).hexdigest()[:8],16)%max(1,len(P)-1)) if len(P)>1 else 0
cache[str(sid)]=(feats(P,0),feats(P,shift))
R=np.vstack([cache[str(s)][0] for s in sessions]); C=np.vstack([cache[str(s)][1] for s in sessions])
res={'protocol':'V129_TUTOR_UPTAKE','rows':int(len(y)),'primary':'student -> next tutor lexical uptake','control':'deterministic within-session rotation of tutor replies','precommit':{'promote_gain_each_geometry':.0015,'phase_change_gain_each_geometry':.003,'real_minus_control_each_geometry':.001,'hard_collision_gain_each_geometry':'>0','no_parameter_sweep':True}}
res['objective_grouped']=evalg('objective_grouped',obj,X75,Xr,y,support,obj,R,C); res['session_grouped']=evalg('session_grouped',sessions,X75,Xr,y,support,obj,R,C)
def ok(x,t): return x['uptake']['gain']>=t and x['uptake_minus_control_gain']>=.001 and x['hard_collision'].get('uptake_gain',-1)>0
po=ok(res['objective_grouped'],.0015); ps=ok(res['session_grouped'],.0015); ph=ok(res['objective_grouped'],.003) and ok(res['session_grouped'],.003)
verdict='PHASE_CHANGE_CANDIDATE' if ph else 'PROMOTE_TUTOR_UPTAKE_LAW' if po and ps else 'SUPPRESS_TUTOR_UPTAKE'
res['decision']={'objective_pass':bool(po),'session_pass':bool(ps),'verdict':verdict}
Path(a.out).write_text(json.dumps(res,indent=2)); print(json.dumps(res,indent=2),flush=True)
if __name__=='__main__':
p=argparse.ArgumentParser(); p.add_argument('--archive',required=True); p.add_argument('--dir',required=True); p.add_argument('--out',required=True); main(p.parse_args())
109 changes: 109 additions & 0 deletions competitions/trace_the_ace/v131_crowding_gated_uptake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""V131: deployable prediction-crowding gate for V129 tutor uptake.

Derived from the residual constraint intersection:
- V129 uptake is globally harmful,
- but improves label-defined same-objective hard collisions and beats reply rotation,
- therefore the missing object may be an applicability predicate, not a new feature.

This test removes the forbidden label clause from the hard-collision definition.
A row is gate-positive iff another row with the same objective has a V97 prediction
within 0.01. That predicate is batch-visible and label-free at inference time.
Inside the gate we use the frozen V129 uptake residual correction; outside it we
leave V97 unchanged. The control uses identically gated rotated-reply correction.
No threshold sweep.
"""
from __future__ import annotations
import argparse,csv,hashlib,io,json,zipfile
from pathlib import Path
import numpy as np
from scipy.sparse import load_npz

from v110_residual_collider_state_discovery import ll
from v121_pretrained_semantic_residual import p97_oof
from v129_tutor_uptake import feats,pairs,residual_oof

TOL=0.01


def crowding_mask(P: np.ndarray, objectives: np.ndarray, tol: float=TOL) -> np.ndarray:
m=np.zeros(len(P),bool)
for o in np.unique(objectives):
z=np.where(objectives==o)[0]
if len(z)<2: continue
p=P[z]
D=np.abs(p[:,None]-p[None,:])
np.fill_diagonal(D,np.inf)
m[z[np.min(D,axis=1)<=tol]]=True
return m


def evalg(name,groups,X75,Xr,y,support,obj,R,C):
P,splits=p97_oof(X75,Xr,y,groups,support)
Q=residual_oof(P,R,y,splits)
QC=residual_oof(P,C,y,splits)
gate=crowding_mask(P,obj,TOL)
G=P.copy(); GC=P.copy(); G[gate]=Q[gate]; GC[gate]=QC[gate]
b=ll(y,P); g=ll(y,G); gc=ll(y,GC)
out={
'geometry':name,
'baseline_v97_ll':b,
'gate_rows':int(gate.sum()),
'gate_fraction':float(gate.mean()),
'gated_uptake':{'ll':g,'gain':b-g},
'gated_rotated_control':{'ll':gc,'gain':b-gc},
'real_minus_control_gain':gc-g,
}
if gate.any():
bb=ll(y[gate],P[gate]); rr=ll(y[gate],Q[gate]); cc=ll(y[gate],QC[gate])
out['gate_only']={
'baseline_ll':bb,
'uptake_ll':rr,
'uptake_gain':bb-rr,
'control_ll':cc,
'real_minus_control_gain':cc-rr,
}
return out


def main(a):
d=Path(a.dir); z=np.load(d/'arrays.npz',allow_pickle=True)
y=z['y']; obj=z['objectives']; support=z['support']; sessions=z['sessions']
X75=load_npz(d/'X75.npz'); Xr=load_npz(d/'Xr.npz')
cache={}
with zipfile.ZipFile(a.archive) as za:
names=set(za.namelist())
for sid in np.unique(sessions):
name=f'{sid}.csv'
if name not in names: raise RuntimeError('missing '+name)
with za.open(name) as f:
rows=list(csv.DictReader(io.TextIOWrapper(f,encoding='utf-8-sig',newline='')))
P=pairs(rows)
shift=1+(int(hashlib.sha256(str(sid).encode()).hexdigest()[:8],16)%max(1,len(P)-1)) if len(P)>1 else 0
cache[str(sid)]=(feats(P,0),feats(P,shift))
R=np.vstack([cache[str(s)][0] for s in sessions]); C=np.vstack([cache[str(s)][1] for s in sessions])
res={
'protocol':'V131_CROWDING_GATED_UPTAKE',
'rows':int(len(y)),
'gate':'same objective has another row within abs(V97_i-V97_j)<=0.01; no labels',
'precommit':{
'global_gain_each_geometry':0.0005,
'real_minus_control_each_geometry':0.0005,
'gate_only_gain_each_geometry':'>0',
'threshold_sweep':False,
'tol':TOL,
}
}
res['objective_grouped']=evalg('objective_grouped',obj,X75,Xr,y,support,obj,R,C)
res['session_grouped']=evalg('session_grouped',sessions,X75,Xr,y,support,obj,R,C)
def ok(r):
return r['gated_uptake']['gain']>=.0005 and r['real_minus_control_gain']>=.0005 and r.get('gate_only',{}).get('uptake_gain',-1)>0
po,ps=ok(res['objective_grouped']),ok(res['session_grouped'])
res['decision']={
'objective_pass':bool(po),'session_pass':bool(ps),
'verdict':'PROMOTE_DEPLOYABLE_UPTAKE_GATE' if po and ps else 'SUPPRESS_CROWDING_GATE'
}
Path(a.out).write_text(json.dumps(res,indent=2)); print(json.dumps(res,indent=2),flush=True)

if __name__=='__main__':
p=argparse.ArgumentParser(); p.add_argument('--archive',required=True); p.add_argument('--dir',required=True); p.add_argument('--out',required=True); main(p.parse_args())
Loading