Beanbagdzf commited on
Commit
f329675
·
verified ·
1 Parent(s): 6b6f75b

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +17 -0
  2. .gitattributes +1 -0
  3. .streamlit/config.toml +22 -0
  4. .streamlit/credentials.toml +2 -0
  5. Deep-Citation/Data/acl.tsv +0 -0
  6. Deep-Citation/Data/class_def.json +23 -0
  7. Deep-Citation/Model/__init__.py +1 -0
  8. Deep-Citation/Model/model.py +89 -0
  9. Deep-Citation/Workspace/acl_scicite_wksp_trl/args.txt +21 -0
  10. Deep-Citation/Workspace/acl_scicite_wksp_trl/best_model.pt +3 -0
  11. Deep-Citation/data.py +211 -0
  12. Dockerfile +36 -0
  13. README.md +226 -3
  14. app.py +5 -0
  15. docker-compose.neo4j.yml +19 -0
  16. hf_space/build_replay_traces.py +305 -0
  17. hf_space/neo4j_workflow.py +150 -0
  18. hf_space/replay.py +130 -0
  19. hf_space/requirements.txt +28 -0
  20. hf_space/runner.py +333 -0
  21. hf_space/scipath_live/README.md +38 -0
  22. hf_space/scipath_live/__init__.py +19 -0
  23. hf_space/scipath_live/agent.py +227 -0
  24. hf_space/scipath_live/cli.py +69 -0
  25. hf_space/scipath_live/data/example_claim.json +124 -0
  26. hf_space/scipath_live/judge.py +305 -0
  27. hf_space/scipath_live/pipeline.py +101 -0
  28. hf_space/scipath_live/prompts.py +170 -0
  29. hf_space/scipath_live/tools.py +170 -0
  30. hf_space/streamlit_app.py +0 -0
  31. hf_space/streamlit_config.py +50 -0
  32. hf_space/system_live_runner.py +317 -0
  33. hf_space/system_run_cases/case_study_analysis.json +829 -0
  34. hf_space/system_run_data.py +444 -0
  35. hf_space/workflow_graph.py +1197 -0
  36. replay_traces/2208.00329/input_ids.json +7 -0
  37. replay_traces/2208.00329/logs/step_01.log +18 -0
  38. replay_traces/2208.00329/logs/step_02.log +5 -0
  39. replay_traces/2208.00329/logs/step_03.log +4 -0
  40. replay_traces/2208.00329/logs/step_04.log +6 -0
  41. replay_traces/2208.00329/logs/step_05.log +14 -0
  42. replay_traces/2208.00329/logs/step_06.log +7 -0
  43. replay_traces/2208.00329/logs/step_07.log +28 -0
  44. replay_traces/2208.00329/processed_papers/2208.00329/paper_metadata.json +55 -0
  45. replay_traces/2208.00329/processed_papers/2208.00329/usage_citing_paragraphs.json +0 -0
  46. replay_traces/2208.00329/processed_papers/2208.00329/usage_context_labels.json +292 -0
  47. replay_traces/2208.00329/processed_papers/2208.00329/usage_contexts.json +326 -0
  48. replay_traces/2208.00329/processed_papers/2208.00329/usage_contributions.json +43 -0
  49. replay_traces/2208.00329/processed_papers/2208.00329/usage_discovery_from_contributions.json +38 -0
  50. replay_traces/2208.00329/processed_papers/2208.00329/usage_uses_extends_verified.json +150 -0
.dockerignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .venv
3
+ venv
4
+ __pycache__
5
+ **/__pycache__
6
+ *.pyc
7
+ **/*.pyc
8
+ .DS_Store
9
+ .env
10
+ .streamlit/secrets.toml
11
+ .playwright
12
+ hf_space/runs
13
+ runs
14
+ *.zip
15
+ **/.pytest_cache
16
+ **/node_modules
17
+ agent-transcripts
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ replay_traces/2505.17978/processed_papers/2505.17978/usage_citing_paragraphs.json filter=lfs diff=lfs merge=lfs -text
.streamlit/config.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [browser]
2
+ gatherUsageStats = false
3
+
4
+ [server]
5
+ headless = true
6
+ # Hugging Face Spaces / Docker bind on all interfaces; local overrides via CLI are fine.
7
+ address = "0.0.0.0"
8
+ port = 7860
9
+ fileWatcherType = "auto"
10
+ runOnSave = false
11
+
12
+ [client]
13
+ toolbarMode = "viewer"
14
+ showSidebarNavigation = false
15
+
16
+ [theme]
17
+ base = "light"
18
+ primaryColor = "#0f6b5c"
19
+ backgroundColor = "#e7eef0"
20
+ secondaryBackgroundColor = "#f4f8f7"
21
+ textColor = "#13201d"
22
+ font = "sans serif"
.streamlit/credentials.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [general]
2
+ email = ""
Deep-Citation/Data/acl.tsv ADDED
The diff for this file is too large to render. See raw diff
 
Deep-Citation/Data/class_def.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "acl":
3
+ {
4
+ "BACKGROUND": "The citation provides relevant information for the domain that the present paper discusses.",
5
+ "MOTIVATION": "The citation illustrates the need for data, goals, methods, etc that is proposed in the present paper.",
6
+ "USES": "The present paper uses data, methods, etc., from the paper associated with the citation.",
7
+ "EXTENDS": "The present paper extends the data, methods, etc. from the paper associated with the citation.",
8
+ "COMPAREORCONTRAST": "The present paper expresses similarity / differences to the citation.",
9
+ "FUTURE": "The citation is a potential avenue for future work of the present paper."
10
+ },
11
+ "kim":
12
+ {
13
+ "Used": "The present paper uses at least one method that is proposed in the paper associated with the citation.",
14
+ "Not used": "The present paper does not use or extend any methods that is proposed in the paper associated with the citation.",
15
+ "Extended": "The present paper uses an extended / modified version of the method proposed in the paper associated with the citation."
16
+ },
17
+ "scicite":
18
+ {
19
+ "Background": "The citation states, mentions, or points to the background information giving more context about a problem, concept, approach, topic, or importance of the problem that is discussed in the present paper.",
20
+ "Method": "The present paper uses a method, tool, approach or dataset that is proposed in the paper associated with the citation.",
21
+ "Result": "The present paper compares its results/findings with the results/findings of the paper associated with the citation."
22
+ }
23
+ }
Deep-Citation/Model/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .model import LanguageModel, MultiHeadLanguageModel
Deep-Citation/Model/model.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+
5
+ from typing import List
6
+ from transformers import AutoModel
7
+
8
+ def mask_pooling(model_output, attention_mask):
9
+ token_embeddings = model_output[0] #First element of model_output contains all token embeddings
10
+ input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
11
+ return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)
12
+
13
+ class LanguageModel(nn.Module):
14
+ def __init__(self,
15
+ modelname: str,
16
+ device: str,
17
+ readout: str
18
+ ):
19
+ super(LanguageModel, self).__init__()
20
+ self.device = device
21
+ self.modelname = modelname
22
+ self.readout_fn = readout
23
+
24
+ self.model = AutoModel.from_pretrained(modelname)
25
+ self.hidden_size = self.model.config.hidden_size
26
+
27
+ def readout(self, model_inputs, model_outputs, readout_masks=None):
28
+ if self.readout_fn == 'cls':
29
+ if 'bert' in self.modelname or 'deberta' in self.modelname:
30
+ text_representations = model_outputs.last_hidden_state[:, 0]
31
+ elif 'xlnet' in self.modelname:
32
+ text_representations = model_outputs.last_hidden_state[:, -1]
33
+ else:
34
+ raise ValueError('Invalid model name {} for the cls readout.'.format(self.modelname))
35
+ elif self.readout_fn == 'mean':
36
+ text_representations = mask_pooling(model_outputs, model_inputs['attention_mask'])
37
+ elif self.readout_fn == 'ch' and readout_masks is not None:
38
+ text_representations = mask_pooling(model_outputs, readout_masks)
39
+ else:
40
+ raise ValueError('Invalid readout function.')
41
+ return text_representations
42
+
43
+ def _lm_forward(self, tokens):
44
+ tokens = tokens.to(self.device)
45
+ if 'readout_mask' in tokens:
46
+ readout_mask = tokens.pop('readout_mask')
47
+ else:
48
+ readout_mask = None
49
+ outputs = self.model(**tokens)
50
+ return self.readout(tokens, outputs, readout_mask)
51
+
52
+ def forward(self):
53
+ raise NotImplementedError
54
+
55
+ def save_pretrained(self, modeldir):
56
+ model_filename = os.path.join(modeldir, 'checkpoint.pt')
57
+ torch.save(self.state_dict(), model_filename)
58
+
59
+ def load_pretrained(self, modeldir):
60
+ model_filename = os.path.join(modeldir, 'checkpoint.pt')
61
+ self.load_state_dict(torch.load(model_filename))
62
+
63
+ class MultiHeadLanguageModel(LanguageModel):
64
+ def __init__(self,
65
+ modelname: str,
66
+ device: str,
67
+ readout: str,
68
+ num_classes: List
69
+ ):
70
+ super().__init__(
71
+ modelname,
72
+ device,
73
+ readout
74
+ )
75
+
76
+ self.num_classes = num_classes
77
+ self.lns = nn.ModuleList([nn.Linear(self.hidden_size, num_class) for num_class in num_classes])
78
+
79
+ def forward(self, input_tokens, input_head_indices, class_tokens, class_head_indices):
80
+ head_indices = torch.unique(input_head_indices)
81
+ text_representations = self._lm_forward(input_tokens)
82
+
83
+ final_preds = {}
84
+ for i in head_indices:
85
+ if torch.any(input_head_indices == i):
86
+ final_preds[i.item()] = self.lns[i.item()](text_representations[input_head_indices == i])
87
+ else:
88
+ final_preds[i.item()] = torch.tensor([]).to(self.device)
89
+ return final_preds
Deep-Citation/Workspace/acl_scicite_wksp_trl/args.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Namespace(dataset='acl-scicite',
2
+ lambdas='1-0.063',
3
+ data_dir='Data',
4
+ workspace='Workspace/acl_scicite_wksp_trl',
5
+ class_definition='Data/class_def.json',
6
+ batch_size=32,
7
+ lr=5e-05,
8
+ decay_rate=0.5,
9
+ decay_step=5,
10
+ num_epochs=10,
11
+ scheduler='slanted',
12
+ dropout_rate=0.2,
13
+ l2=0.0,
14
+ device='cuda',
15
+ tol=10,
16
+ inference_only=False,
17
+ seed=1,
18
+ lm='scibert',
19
+ max_length=512,
20
+ batch_size_factor=2,
21
+ readout='ch')
Deep-Citation/Workspace/acl_scicite_wksp_trl/best_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e45ab11942439f80a121dad5b2d9da392470e0cedf6a7335991fa0a1f616dcb2
3
+ size 439784777
Deep-Citation/data.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import copy
4
+ import torch
5
+ import scipy
6
+ import numpy as np
7
+ import pandas as pd
8
+ from tqdm import tqdm
9
+ from scipy.special import softmax
10
+
11
+ from transformers import AutoTokenizer
12
+
13
+ class CollateFn(object):
14
+ def __init__(self, modelname, class_definitions=None, instance_weights=False):
15
+ self.instance_weights = instance_weights
16
+ use_fast = False if 'deberta' in modelname else True
17
+ self.tokenizer = AutoTokenizer.from_pretrained(modelname, use_fast=use_fast)
18
+ cited_ids = self.tokenizer.encode('<CITED HERE>', add_special_tokens=False)
19
+ self.cited_here_tokens = torch.tensor(cited_ids, dtype=torch.long)
20
+
21
+ if class_definitions is not None:
22
+ self.class_definitions = []
23
+ self.class_head_indices = []
24
+ for i, defs in enumerate(class_definitions):
25
+ self.class_definitions += defs
26
+ self.class_head_indices.append(i * torch.ones(len(defs), dtype=torch.long))
27
+ self.class_head_indices = torch.cat(self.class_head_indices, dim=0)
28
+ self.class_tokens = self.tokenizer(
29
+ self.class_definitions,
30
+ return_tensors="pt",
31
+ max_length=512,
32
+ truncation=True,
33
+ padding=True
34
+ )
35
+
36
+ def _get_readout_mask(self, tokens):
37
+ # cited_here_tokens = torch.tensor([962, 8412, 1530, 1374])
38
+ readout_mask = torch.zeros_like(tokens['input_ids'], dtype=torch.bool)
39
+
40
+ batch_size = tokens['input_ids'].size(0)
41
+ l = tokens['input_ids'].size(1)
42
+ ctk_l = self.cited_here_tokens.size(0)
43
+ for b in range(batch_size):
44
+ for i in range(1, l - ctk_l):
45
+ if torch.equal(tokens['input_ids'][b, i:i+ctk_l], self.cited_here_tokens):
46
+ readout_mask[b, i:i+ctk_l] = True
47
+ if not readout_mask[b].any():
48
+ # Fallback to CLS if the citation marker isn't matched.
49
+ readout_mask[b, 0] = True
50
+ return readout_mask
51
+
52
+ def _tokenize_context(self, context):
53
+ tokens = self.tokenizer(
54
+ context,
55
+ return_tensors="pt",
56
+ max_length=512,
57
+ truncation=True,
58
+ padding=True
59
+ )
60
+ tokens['readout_mask'] = self._get_readout_mask(
61
+ tokens
62
+ )
63
+
64
+ return tokens
65
+
66
+ def __call__(self, samples):
67
+ if self.instance_weights:
68
+ text, labels, ds_indices, instance_weights = list(map(list, zip(*samples)))
69
+ batched_text = self._tokenize_context(text)
70
+ labels = torch.stack(labels)
71
+ ds_indices = torch.stack(ds_indices)
72
+ instance_weights = torch.stack(instance_weights)
73
+ return batched_text, labels, ds_indices, instance_weights
74
+ else:
75
+ text, labels, ds_indices = list(map(list, zip(*samples)))
76
+ batched_text = self._tokenize_context(text)
77
+ labels = torch.stack(labels)
78
+ ds_indices = torch.stack(ds_indices)
79
+
80
+ return batched_text, labels, ds_indices, copy.deepcopy(self.class_tokens), self.class_head_indices
81
+
82
+ class Dataset(object):
83
+ def __init__(self, dataframe, class_definitions, lmbd=1.0):
84
+ self.class_definitions = class_definitions
85
+ self.lmbd = lmbd
86
+ self._load_data(dataframe)
87
+
88
+ def __len__(self):
89
+ return len(self.labels)
90
+
91
+ def __getitem__(self, idx):
92
+ '''Get datapoint with index'''
93
+ return (self.text[idx], self.labels[idx], self.ds_index[idx])
94
+
95
+ def _load_data(self, annotated_data):
96
+ self.labels = torch.LongTensor(annotated_data['label'].tolist())
97
+ self.original_labels = torch.LongTensor(annotated_data['label'].tolist())
98
+ self.ds_index = torch.zeros_like(self.original_labels)
99
+ self.text = annotated_data['context'].tolist()
100
+
101
+ class MultiHeadDatasets(object):
102
+ def __init__(self, datasets, batch_size_factor=2):
103
+ self.text = []
104
+ self.ds_index = []
105
+ self.labels = []
106
+ self.class_definitions = []
107
+ self.lambdas = []
108
+
109
+ self.dataset_sizes = [len(d.labels) for d in datasets]
110
+ if len(self.dataset_sizes) > 1:
111
+ if sum(self.dataset_sizes) / self.dataset_sizes[0] <= batch_size_factor:
112
+ self.sample_auxiliary = False
113
+ self.adjusted_batch_size_factor = sum(self.dataset_sizes) / self.dataset_sizes[0]
114
+ else:
115
+ self.sample_auxiliary = True
116
+ self.sample_distribution = np.array([d.lmbd for d in datasets[1:]]) / sum([d.lmbd for d in datasets[1:]])
117
+ self.adjusted_batch_size_factor = batch_size_factor
118
+ else:
119
+ self.sample_auxiliary = False
120
+ self.adjusted_batch_size_factor = 1
121
+
122
+ for i, d in enumerate(datasets):
123
+ self.text += d.text
124
+ self.ds_index.append(i * torch.ones(len(d.text), dtype=torch.long))
125
+ self.labels.append(d.labels)
126
+ self.class_definitions.append(d.class_definitions)
127
+ self.lambdas.append(d.lmbd)
128
+ self.labels = torch.cat(self.labels, dim=0)
129
+ self.ds_index = torch.cat(self.ds_index, dim=0)
130
+
131
+ def sample_auxiliary_instace(self):
132
+ sampled_dataset_idx = np.random.choice(
133
+ np.arange(1, len(self.dataset_sizes)),
134
+ p=self.sample_distribution
135
+ )
136
+ instance_idx = np.random.choice(
137
+ self.dataset_sizes[sampled_dataset_idx]
138
+ ) + sum(self.dataset_sizes[:sampled_dataset_idx])
139
+ return instance_idx
140
+
141
+ def __len__(self):
142
+ if self.sample_auxiliary: # if the auxiliary dataset is larger than the main dataset
143
+ return self.dataset_sizes[0] * self.adjusted_batch_size_factor
144
+ return len(self.labels)
145
+
146
+ def __getitem__(self, idx):
147
+ '''Get datapoint with index'''
148
+ if idx < self.dataset_sizes[0] or not self.sample_auxiliary:
149
+ return (self.text[idx], self.labels[idx], self.ds_index[idx])
150
+ else:
151
+ real_idx = self.sample_auxiliary_instace()
152
+ return (self.text[real_idx], self.labels[real_idx], self.ds_index[real_idx])
153
+
154
+ def load_class_definitions(filename):
155
+ with open(filename, 'r') as f:
156
+ class_definitions = json.load(f)
157
+
158
+ results = {k:{} for k in class_definitions.keys()}
159
+ for k, v in class_definitions.items():
160
+ for kk, vv in v.items():
161
+ results[k][kk.lower()] = vv
162
+ return results
163
+
164
+ def create_data_channels(filename, class_definition_filename, split=None, lmbd=1.0):
165
+ data = pd.read_csv(filename, sep='\t')
166
+ data = data.fillna(' ')
167
+
168
+ print('Number of data instance: {}'.format(data.shape[0]))
169
+
170
+ # map labels to ids
171
+ unique_labels = data['label'].unique().tolist()
172
+ label2id = {lb: i for i, lb in enumerate(unique_labels)}
173
+
174
+ data['label'] = data['label'].apply(
175
+ lambda x: label2id[x])
176
+
177
+ data_train = data[data['split'] == 'train'].reset_index()
178
+ data_val = data[data['split'] == 'val'].reset_index()
179
+ data_test = data[data['split'] == 'test'].reset_index()
180
+
181
+ class_definitions = load_class_definitions(class_definition_filename)
182
+ dataname = filename.split('/')[-1].split('.')[0]
183
+ data_class_definitions = [class_definitions[dataname][lb.lower()] for lb in unique_labels]
184
+
185
+ train_data = Dataset(data_train, data_class_definitions, lmbd=lmbd)
186
+ val_data = Dataset(data_val, data_class_definitions, lmbd=lmbd)
187
+ test_data = Dataset(data_test, data_class_definitions, lmbd=lmbd)
188
+
189
+ return train_data, val_data, test_data, unique_labels
190
+
191
+ def create_single_data_object(filename, class_definition_filename, split=None, lmbd=1.0):
192
+ data = pd.read_csv(filename, sep='\t')
193
+ data = data.fillna(' ')
194
+
195
+ print('Number of data instance: {}'.format(data.shape[0]))
196
+
197
+ # map labels to ids
198
+ unique_labels = data['label'].unique()
199
+ label2id = {lb: i for i, lb in enumerate(unique_labels)}
200
+
201
+ data['label'] = data['label'].apply(
202
+ lambda x: label2id[x])
203
+
204
+ class_definitions = load_class_definitions(class_definition_filename)
205
+ dataname = filename.split('/')[-1].split('.')[0]
206
+ data_class_definitions = [class_definitions[dataname][lb.lower()] for lb in unique_labels]
207
+
208
+ if split is None:
209
+ return Dataset(data, data_class_definitions, lmbd=lmbd), unique_labels
210
+ else:
211
+ return Dataset(data[data['split'] == split].reset_index(), data_class_definitions, lmbd=lmbd), unique_labels
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ STREAMLIT_SERVER_HEADLESS=true \
7
+ STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \
8
+ PLAYWRIGHT_BROWSERS_PATH=/app/.playwright \
9
+ HOME=/tmp
10
+
11
+ WORKDIR /app
12
+
13
+ RUN apt-get update && apt-get install -y --no-install-recommends \
14
+ git \
15
+ build-essential \
16
+ curl \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ COPY requirements.txt /app/requirements.txt
20
+ COPY hf_space/requirements.txt /app/hf_space/requirements.txt
21
+ RUN python -m pip install --upgrade pip && \
22
+ pip install -r requirements.txt
23
+
24
+ # Chromium for SciPath Live Run (websearch_deep crawl tool).
25
+ RUN mkdir -p /app/.playwright && \
26
+ python -m playwright install --with-deps chromium
27
+
28
+ COPY . /app
29
+
30
+ # HF Space secrets (GEMINI_API_KEY, etc.) are injected as env vars at runtime.
31
+ EXPOSE 7860
32
+
33
+ CMD ["streamlit", "run", "hf_space/streamlit_app.py", \
34
+ "--server.address", "0.0.0.0", \
35
+ "--server.port", "7860", \
36
+ "--browser.gatherUsageStats", "false"]
README.md CHANGED
@@ -1,10 +1,233 @@
1
  ---
2
  title: SciPaths Demo
3
- emoji: 🏢
4
  colorFrom: blue
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: SciPaths Demo
3
+ emoji: 🔬
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
  ---
10
 
11
+ # SciPaths Demo
12
+
13
+ Self-contained demo of **SciPaths** (annotation process) and **SciFy CodeAgent** (system run / live run) on SciPaths claims.
14
+
15
+ The Hugging Face Space launches Streamlit from `hf_space/streamlit_app.py`. This folder is deployable on its own — it does not depend on sibling repos such as `dryrun/`.
16
+
17
+ ## Citation
18
+
19
+ If you find this useful, please cite our paper as:
20
+
21
+ ```bibtex
22
+ @misc{chamoun2026scipathsforecastingpathwaysscientific,
23
+ title={SciPaths: Forecasting Pathways to Scientific Discovery},
24
+ author={Eric Chamoun and Yizhou Chi and Yulong Chen and Rui Cao and Zifeng Ding and Michalis Korakakis and Andreas Vlachos},
25
+ year={2026},
26
+ eprint={2605.14600},
27
+ archivePrefix={arXiv},
28
+ primaryClass={cs.CL},
29
+ url={https://arxiv.org/abs/2605.14600},
30
+ }
31
+ ```
32
+
33
+ Paper URL: https://arxiv.org/abs/2605.14600
34
+
35
+ ## Required Secrets
36
+
37
+ Set this in the Space settings before publishing:
38
+
39
+ ```text
40
+ GEMINI_API_KEY=<Google Gemini API key>
41
+ ```
42
+
43
+ Optional, for saving completed run artifacts to a Hugging Face Dataset:
44
+
45
+ ```text
46
+ HF_WRITE_TOKEN=<Hugging Face write token>
47
+ RUNS_REPO_ID=<owner/dataset-name>
48
+ RUNS_REPO_TYPE=dataset
49
+ ```
50
+
51
+ Optional, for higher Semantic Scholar limits:
52
+
53
+ ```text
54
+ SEMANTIC_SCHOLAR_API_KEY=<Semantic Scholar API key>
55
+ ```
56
+
57
+ ## Run The Demo Locally
58
+
59
+ ```bash
60
+ pip install -r requirements.txt
61
+ streamlit run hf_space/streamlit_app.py
62
+ ```
63
+
64
+ Then enter an arXiv URL or ID, for example:
65
+
66
+ ```text
67
+ https://arxiv.org/abs/2211.08788
68
+ ```
69
+
70
+ The app writes each run under:
71
+
72
+ ```text
73
+ hf_space/runs/<job_id>/
74
+ ```
75
+
76
+ ## Run One Example From The Command Line
77
+
78
+ This example stores all intermediate files under `runs/example/processed_papers`.
79
+
80
+ ```bash
81
+ mkdir -p runs/example
82
+ printf '[{"id":"2211.08788","title":"","id_type":"ArXiv"}]\n' > runs/example/input_ids.json
83
+
84
+ python src/step_01_fetch/fetch_metadata.py \
85
+ --ids runs/example/input_ids.json \
86
+ --outdir runs/example/processed_papers
87
+
88
+ python src/step_02_mark_citations/replace_citation_markers.py \
89
+ --root runs/example/processed_papers
90
+
91
+ python src/step_03_usage_contexts/build_usage_contexts.py \
92
+ --root runs/example/processed_papers \
93
+ --out-name usage_contexts.json
94
+
95
+ python src/step_04_label_citations/label_citation_functions.py \
96
+ --root runs/example/processed_papers \
97
+ --model-path Deep-Citation/Workspace/acl_scicite_wksp_trl/best_model.pt \
98
+ --model-data-dir Deep-Citation/Data \
99
+ --model-class-def Deep-Citation/Data/class_def.json \
100
+ --model-lm scibert \
101
+ --device cpu
102
+
103
+ python src/step_05_verify_uses_extends/verify_uses_extends.py \
104
+ --root runs/example/processed_papers \
105
+ --k 0 \
106
+ --batch-size 25
107
+
108
+ python src/step_06_extract_paragraphs/extract_arxiv_paragraphs.py \
109
+ --root runs/example/processed_papers
110
+
111
+ python src/step_07_extract_and_refine/extract_contributions_from_citations.py \
112
+ --root runs/example/processed_papers
113
+
114
+ python src/step_07_extract_and_refine/refine_and_filter_clusters_llm.py \
115
+ --root runs/example/processed_papers \
116
+ --inplace \
117
+ --overwrite
118
+
119
+ PYTHONPATH=src \
120
+ python -m step_08_annotation.cli run \
121
+ --paper-dir runs/example/processed_papers/2211.08788 \
122
+ --provider gemini \
123
+ --model gemini/gemini-3.1-pro-preview \
124
+ --formatter-model gemini/gemini-3.1-pro-preview \
125
+ --judge-model gemini/gemini-3.1-pro-preview \
126
+ --candidate-count 3 \
127
+ --output-root runs/example/two_pass_outputs
128
+ ```
129
+
130
+ The final UI payload is written as `pass_2_ui_payload.json` inside the annotation run directory printed by the last command.
131
+
132
+ ## Run Each Step On A Set Of Papers
133
+
134
+ Create an ID file with one entry per paper:
135
+
136
+ ```json
137
+ [
138
+ {"id": "2211.08788", "title": "", "id_type": "ArXiv"},
139
+ {"id": "2311.14919", "title": "", "id_type": "ArXiv"}
140
+ ]
141
+ ```
142
+
143
+ Save it as `runs/batch/input_ids.json`, then run:
144
+
145
+ ```bash
146
+ mkdir -p runs/batch
147
+
148
+ # 1. Fetch metadata + LaTeX for each input paper.
149
+ python src/step_01_fetch/fetch_metadata.py \
150
+ --ids runs/batch/input_ids.json \
151
+ --outdir runs/batch/processed_papers
152
+
153
+ # 2. Add explicit citation markers to the target-paper text.
154
+ python src/step_02_mark_citations/replace_citation_markers.py \
155
+ --root runs/batch/processed_papers
156
+
157
+ # 3. Build downstream citation usage contexts.
158
+ python src/step_03_usage_contexts/build_usage_contexts.py \
159
+ --root runs/batch/processed_papers \
160
+ --out-name usage_contexts.json
161
+
162
+ # 4. Label citation functions with the bundled Deep-Citation classifier.
163
+ python src/step_04_label_citations/label_citation_functions.py \
164
+ --root runs/batch/processed_papers \
165
+ --model-path Deep-Citation/Workspace/acl_scicite_wksp_trl/best_model.pt \
166
+ --model-data-dir Deep-Citation/Data \
167
+ --model-class-def Deep-Citation/Data/class_def.json \
168
+ --model-lm scibert \
169
+ --device cpu
170
+
171
+ # 5. Verify USES/EXTENDS citations with an LLM.
172
+ python src/step_05_verify_uses_extends/verify_uses_extends.py \
173
+ --root runs/batch/processed_papers \
174
+ --k 0 \
175
+ --batch-size 25
176
+
177
+ # 6. Extract arXiv paragraphs from downstream citing papers.
178
+ python src/step_06_extract_paragraphs/extract_arxiv_paragraphs.py \
179
+ --root runs/batch/processed_papers
180
+
181
+ # 7. Extract downstream contribution clusters, then merge/filter them.
182
+ python src/step_07_extract_and_refine/extract_contributions_from_citations.py \
183
+ --root runs/batch/processed_papers
184
+
185
+ python src/step_07_extract_and_refine/refine_and_filter_clusters_llm.py \
186
+ --root runs/batch/processed_papers \
187
+ --inplace \
188
+ --overwrite
189
+
190
+ # 8. Annotate each ready paper: target contributions, enabling contributions, and groundings.
191
+ for paper_dir in runs/batch/processed_papers/*; do
192
+ [ -d "$paper_dir" ] || continue
193
+ [ -f "$paper_dir/usage_discovery_from_contributions.json" ] || continue
194
+ PYTHONPATH=src \
195
+ python -m step_08_annotation.cli run \
196
+ --paper-dir "$paper_dir" \
197
+ --provider gemini \
198
+ --model gemini/gemini-3.1-pro-preview \
199
+ --formatter-model gemini/gemini-3.1-pro-preview \
200
+ --judge-model gemini/gemini-3.1-pro-preview \
201
+ --candidate-count 3 \
202
+ --output-root runs/batch/two_pass_outputs
203
+ done
204
+ ```
205
+
206
+ ## Pipeline Steps
207
+
208
+ 1. **Fetch metadata + LaTeX.** Downloads target-paper metadata, references, citing-paper metadata, and arXiv source where available.
209
+ 2. **Add citation markers.** Inserts normalized citation markers into the target paper so downstream citation contexts can be aligned.
210
+ 3. **Build usage contexts.** Collects text windows around downstream citations to the target paper.
211
+ 4. **Label citation functions.** Uses the bundled Deep-Citation classifier to label citation contexts as background, use, extension, comparison, and related categories.
212
+ 5. **Verify USES/EXTENDS.** Uses an LLM to check whether candidate downstream citations genuinely use or extend the target paper.
213
+ 6. **Extract arXiv paragraphs.** Retrieves fuller paragraphs from citing papers so the system has enough context for contribution extraction.
214
+ 7. **Extract and refine target-contribution clusters.** Extracts what downstream papers use the target paper for, clusters near-duplicates, and filters weak/non-usage evidence.
215
+ 8. **Annotate pathways.** Derives target contributions from the refined clusters, decomposes each into enabling contributions, selects primary groundings, and records additional grounding studies.
216
+
217
+ ## Important Files
218
+
219
+ ```text
220
+ hf_space/streamlit_app.py Streamlit UI
221
+ hf_space/runner.py Orchestrates steps 1-7 for the UI
222
+ hf_space/streamlit_config.py Example papers and tab names
223
+ src/common/ Shared LLM and paper-package utilities
224
+ src/step_01_fetch/ Metadata, references, citations, and LaTeX
225
+ src/step_02_mark_citations/ Citation-marker insertion
226
+ src/step_03_usage_contexts/ Downstream usage-context construction
227
+ src/step_04_label_citations/ Deep-Citation citation-function labeling
228
+ src/step_05_verify_uses_extends/ LLM verification of USES/EXTENDS citations
229
+ src/step_06_extract_paragraphs/ ArXiv paragraph extraction from citing papers
230
+ src/step_07_extract_and_refine/ Contribution extraction and cluster refinement
231
+ src/step_08_annotation/ Target/enabling contribution annotation and grounding
232
+ Deep-Citation/ Bundled citation-function classifier assets
233
+ ```
app.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from hf_space.streamlit_app import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
docker-compose.neo4j.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Optional dedicated Neo4j if ports 7474/7687 are free.
2
+ # By default the app uses whatever answers bolt://localhost:7687
3
+ # (NEO4J_URI / NEO4J_USER / NEO4J_PASSWORD).
4
+ services:
5
+ neo4j:
6
+ image: neo4j:5.26-community
7
+ container_name: scipaths-neo4j
8
+ ports:
9
+ - "7475:7474"
10
+ - "7688:7687"
11
+ environment:
12
+ NEO4J_AUTH: neo4j/scipaths123
13
+ NEO4J_PLUGINS: '["apoc"]'
14
+ NEO4J_dbms_memory_heap_max__size: 1G
15
+ volumes:
16
+ - scipaths_neo4j_data:/data
17
+
18
+ volumes:
19
+ scipaths_neo4j_data:
hf_space/build_replay_traces.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run the demo example papers end-to-end and save replay traces."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import shutil
9
+ import sys
10
+ import time
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+
14
+ SRC = Path(__file__).resolve().parent
15
+ REPO_ROOT = SRC.parent
16
+ for extra in (SRC, REPO_ROOT / "src"):
17
+ extra_str = str(extra)
18
+ if extra_str not in sys.path:
19
+ sys.path.insert(0, extra_str)
20
+
21
+ from dotenv import load_dotenv
22
+
23
+ load_dotenv(REPO_ROOT / ".env")
24
+ load_dotenv(REPO_ROOT.parent / "dryrun" / ".env", override=False)
25
+
26
+ # Align Gemini env aliases used across the repo.
27
+ if not os.getenv("GEMINI_API_KEY"):
28
+ for alt in ("GOOGLE_GENAI_API_KEY", "GOOGLE_API_KEY"):
29
+ if os.getenv(alt):
30
+ os.environ["GEMINI_API_KEY"] = os.environ[alt]
31
+ break
32
+
33
+ import runner as runner_module
34
+ from runner import PipelineConfig
35
+ from common.paper_package import load_paper_package
36
+ from step_08_annotation.pipeline import TwoPassAnnotationPipeline
37
+ from streamlit_config import EXAMPLES
38
+
39
+ REPLAY_ROOT = REPO_ROOT / "replay_traces"
40
+ WORK_ROOT = REPO_ROOT / "hf_space" / "runs" / "replay_build"
41
+
42
+
43
+ def _env(name: str, default: str) -> str:
44
+ return (os.getenv(name) or default).strip()
45
+
46
+
47
+ def _load_json(path: Path):
48
+ if not path.exists():
49
+ return None
50
+ try:
51
+ return json.loads(path.read_text(encoding="utf-8"))
52
+ except Exception:
53
+ return None
54
+
55
+
56
+ def _copy_if_exists(src: Path, dst: Path) -> bool:
57
+ if not src.exists():
58
+ return False
59
+ dst.parent.mkdir(parents=True, exist_ok=True)
60
+ if src.is_dir():
61
+ if dst.exists():
62
+ shutil.rmtree(dst)
63
+ shutil.copytree(src, dst)
64
+ else:
65
+ shutil.copy2(src, dst)
66
+ return True
67
+
68
+
69
+ def _run_annotation(paper_dir: Path, annotation_root: Path) -> tuple[dict | None, Path | None, str | None]:
70
+ discovery = _load_json(paper_dir / "usage_discovery_from_contributions.json") or {}
71
+ clusters = discovery.get("clusters") or []
72
+ if not clusters:
73
+ return None, None, "No refined downstream usage clusters; annotation skipped."
74
+
75
+ llm_provider = _env("LLM_PROVIDER", "gemini")
76
+ llm_model = _env("LLM_MODEL", "gemini-3.1-pro-preview")
77
+ formatter_model = _env("ANNOTATION_FORMATTER_MODEL", "gemini/gemini-3.1-pro-preview")
78
+ judge_model = _env("ANNOTATION_JUDGE_MODEL", "gemini/gemini-3.1-pro-preview")
79
+ candidate_count = int(_env("ANNOTATION_CANDIDATE_COUNT", "3"))
80
+
81
+ paper = load_paper_package(paper_dir)
82
+ pipeline = TwoPassAnnotationPipeline(
83
+ provider=llm_provider,
84
+ model=llm_model,
85
+ formatter_model=formatter_model or None,
86
+ judge_model=judge_model or None,
87
+ output_root=annotation_root,
88
+ annotator_id="replay_trace_builder",
89
+ candidate_count=max(1, candidate_count),
90
+ formatter_max_attempts=3,
91
+ include_reference_examples=True,
92
+ prompt_profile="full",
93
+ )
94
+ result = pipeline.run(paper)
95
+ return result.result, result.run_dir, None
96
+
97
+
98
+ def _package_trace(
99
+ *,
100
+ label: str,
101
+ arxiv_id: str,
102
+ paper_input: str,
103
+ job_dir: Path,
104
+ paper_dir: Path,
105
+ events: list[str],
106
+ status: str,
107
+ annotation_run_dir: Path | None,
108
+ annotation_skipped_reason: str | None,
109
+ pipeline_failed_reason: str | None,
110
+ pipeline_stopped_reason: str | None,
111
+ ) -> Path:
112
+ out_dir = REPLAY_ROOT / arxiv_id
113
+ if out_dir.exists():
114
+ shutil.rmtree(out_dir)
115
+ out_dir.mkdir(parents=True, exist_ok=True)
116
+
117
+ paper_out = out_dir / "processed_papers" / arxiv_id
118
+ paper_out.mkdir(parents=True, exist_ok=True)
119
+
120
+ # Core replay payloads (keep disk footprint manageable).
121
+ keep_files = [
122
+ "paper_metadata.json",
123
+ "usage_contexts.json",
124
+ "usage_context_labels.json",
125
+ "usage_uses_extends_verified.json",
126
+ "usage_citing_paragraphs.json",
127
+ "usage_contributions.json",
128
+ "usage_discovery_from_contributions.json",
129
+ ]
130
+ for name in keep_files:
131
+ _copy_if_exists(paper_dir / name, paper_out / name)
132
+
133
+ _copy_if_exists(job_dir / "logs", out_dir / "logs")
134
+ _copy_if_exists(job_dir / "summary.txt", out_dir / "summary.txt")
135
+ _copy_if_exists(job_dir / "run_config.json", out_dir / "run_config.json")
136
+ _copy_if_exists(job_dir / "input_ids.json", out_dir / "input_ids.json")
137
+
138
+ annotation_payload_path = None
139
+ if annotation_run_dir and annotation_run_dir.exists():
140
+ ann_dst = out_dir / "two_pass_outputs" / annotation_run_dir.name
141
+ _copy_if_exists(annotation_run_dir, ann_dst)
142
+ payload = ann_dst / "pass_2_ui_payload.json"
143
+ if payload.exists():
144
+ annotation_payload_path = str(payload.relative_to(out_dir))
145
+
146
+ discovery = _load_json(paper_out / "usage_discovery_from_contributions.json") or {}
147
+ contributions = _load_json(paper_out / "usage_contributions.json") or {}
148
+ payload = None
149
+ if annotation_payload_path:
150
+ payload = _load_json(out_dir / annotation_payload_path)
151
+
152
+ public_export = {
153
+ "citation_clusters": (discovery or {}).get("clusters") or [],
154
+ "target_contribution_decompositions": (payload or {}).get("claims") or [],
155
+ }
156
+ (out_dir / "scipaths_run_results.json").write_text(
157
+ json.dumps(public_export, indent=2, ensure_ascii=False),
158
+ encoding="utf-8",
159
+ )
160
+
161
+ meta = {
162
+ "label": label,
163
+ "arxiv_id": arxiv_id,
164
+ "paper_input": paper_input,
165
+ "status": status,
166
+ "built_at": datetime.now(timezone.utc).isoformat(),
167
+ "source_job_dir": str(job_dir),
168
+ "paper_dir": str((out_dir / "processed_papers" / arxiv_id).relative_to(out_dir)),
169
+ "annotation_payload_path": annotation_payload_path,
170
+ "annotation_skipped_reason": annotation_skipped_reason,
171
+ "pipeline_failed_reason": pipeline_failed_reason,
172
+ "pipeline_stopped_reason": pipeline_stopped_reason,
173
+ "events": events,
174
+ "cluster_count": len((discovery or {}).get("clusters") or []),
175
+ "contribution_count": len((contributions or {}).get("contributions") or []),
176
+ "claim_count": len((payload or {}).get("claims") or []) if isinstance(payload, dict) else 0,
177
+ }
178
+ (out_dir / "replay_meta.json").write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")
179
+ return out_dir
180
+
181
+
182
+ def run_one(label: str, paper_input: str) -> dict:
183
+ if not os.getenv("GEMINI_API_KEY"):
184
+ raise SystemExit("GEMINI_API_KEY is required to build replay traces.")
185
+
186
+ arxiv_id = runner_module.parse_arxiv_id(paper_input)
187
+ print(f"\n=== Building replay trace for {label} ({arxiv_id}) ===", flush=True)
188
+
189
+ cfg = PipelineConfig(
190
+ repo_root=REPO_ROOT,
191
+ source_root=REPO_ROOT / "src" / "processed_papers",
192
+ paper_input=paper_input,
193
+ llm_provider=_env("LLM_PROVIDER", "gemini"),
194
+ llm_model=_env("LLM_MODEL", "gemini-3.1-pro-preview"),
195
+ llm_model_step4=_env("LLM_MODEL_STEP4", "gemini-3-flash-preview"),
196
+ model_path="Deep-Citation/Workspace/acl_scicite_wksp_trl/best_model.pt",
197
+ model_data_dir="Deep-Citation/Data",
198
+ model_class_def="Deep-Citation/Data/class_def.json",
199
+ model_lm="scibert",
200
+ device="cpu",
201
+ embedding_model="sentence-transformers/all-mpnet-base-v2",
202
+ )
203
+
204
+ events: list[str] = []
205
+ artifact_path = None
206
+ pipeline_failed_reason = None
207
+ pipeline_stopped_reason = None
208
+ t0 = time.time()
209
+
210
+ for line, maybe_artifact in runner_module.run_pipeline(cfg, WORK_ROOT):
211
+ if line:
212
+ print(line, flush=True)
213
+ events.append(line)
214
+ if line.startswith("Pipeline stopped:"):
215
+ pipeline_stopped_reason = line
216
+ if "failed" in line.lower():
217
+ pipeline_failed_reason = line
218
+ if maybe_artifact:
219
+ artifact_path = maybe_artifact
220
+
221
+ if not artifact_path:
222
+ raise RuntimeError(f"Pipeline produced no artifact for {arxiv_id}")
223
+
224
+ job_dir = Path(str(artifact_path)).with_suffix("")
225
+ paper_dir = job_dir / "processed_papers" / arxiv_id
226
+ annotation_run_dir = None
227
+ annotation_skipped_reason = None
228
+
229
+ if pipeline_failed_reason:
230
+ status = "Failed"
231
+ annotation_skipped_reason = f"{pipeline_failed_reason} Annotation was not run."
232
+ elif pipeline_stopped_reason:
233
+ status = "Stopped"
234
+ annotation_skipped_reason = f"{pipeline_stopped_reason} Annotation was not run."
235
+ else:
236
+ print("[annotation] starting", flush=True)
237
+ try:
238
+ _run_output, annotation_run_dir, skip = _run_annotation(
239
+ paper_dir=paper_dir,
240
+ annotation_root=job_dir / "two_pass_outputs",
241
+ )
242
+ if skip:
243
+ annotation_skipped_reason = skip
244
+ status = "Completed"
245
+ print(f"[annotation] skipped: {skip}", flush=True)
246
+ else:
247
+ status = "Completed"
248
+ events.append(f"[annotation] complete: {annotation_run_dir}")
249
+ print(f"[annotation] complete: {annotation_run_dir}", flush=True)
250
+ except Exception as exc:
251
+ status = "Failed"
252
+ pipeline_failed_reason = f"Annotation failed: {exc}"
253
+ annotation_skipped_reason = pipeline_failed_reason
254
+ events.append(pipeline_failed_reason)
255
+ print(pipeline_failed_reason, flush=True)
256
+
257
+ out_dir = _package_trace(
258
+ label=label,
259
+ arxiv_id=arxiv_id,
260
+ paper_input=paper_input,
261
+ job_dir=job_dir,
262
+ paper_dir=paper_dir,
263
+ events=events,
264
+ status=status,
265
+ annotation_run_dir=annotation_run_dir,
266
+ annotation_skipped_reason=annotation_skipped_reason,
267
+ pipeline_failed_reason=pipeline_failed_reason,
268
+ pipeline_stopped_reason=pipeline_stopped_reason,
269
+ )
270
+ elapsed = time.time() - t0
271
+ print(f"Saved replay trace -> {out_dir} ({status}, {elapsed/60:.1f} min)", flush=True)
272
+ return {
273
+ "label": label,
274
+ "arxiv_id": arxiv_id,
275
+ "status": status,
276
+ "trace_dir": str(out_dir),
277
+ "elapsed_sec": elapsed,
278
+ "annotation_skipped_reason": annotation_skipped_reason,
279
+ "pipeline_failed_reason": pipeline_failed_reason,
280
+ "pipeline_stopped_reason": pipeline_stopped_reason,
281
+ }
282
+
283
+
284
+ def main() -> int:
285
+ REPLAY_ROOT.mkdir(parents=True, exist_ok=True)
286
+ WORK_ROOT.mkdir(parents=True, exist_ok=True)
287
+
288
+ results = []
289
+ for label, paper_input in EXAMPLES.items():
290
+ results.append(run_one(label, paper_input))
291
+
292
+ index = {
293
+ "built_at": datetime.now(timezone.utc).isoformat(),
294
+ "examples": results,
295
+ }
296
+ index_path = REPLAY_ROOT / "index.json"
297
+ index_path.write_text(json.dumps(index, indent=2, ensure_ascii=False), encoding="utf-8")
298
+ print(f"\nWrote index -> {index_path}", flush=True)
299
+ for item in results:
300
+ print(f"- {item['arxiv_id']}: {item['status']} -> {item['trace_dir']}", flush=True)
301
+ return 0 if all(item["status"] == "Completed" for item in results) else 1
302
+
303
+
304
+ if __name__ == "__main__":
305
+ raise SystemExit(main())
hf_space/neo4j_workflow.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Neo4j sync for SciPaths workflow graph animation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Optional
7
+
8
+ _DRIVER = None
9
+
10
+ NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
11
+ NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
12
+ NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "graphscout123")
13
+
14
+
15
+ def neo4j_available() -> bool:
16
+ try:
17
+ driver = get_driver()
18
+ if driver is None:
19
+ return False
20
+ driver.verify_connectivity()
21
+ return True
22
+ except Exception:
23
+ return False
24
+
25
+
26
+ def get_driver():
27
+ global _DRIVER
28
+ if _DRIVER is not None:
29
+ return _DRIVER
30
+ try:
31
+ from neo4j import GraphDatabase
32
+ except ImportError:
33
+ return None
34
+ try:
35
+ _DRIVER = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))
36
+ _DRIVER.verify_connectivity()
37
+ return _DRIVER
38
+ except Exception:
39
+ _DRIVER = None
40
+ return None
41
+
42
+
43
+ def reset_run(run_id: str) -> bool:
44
+ driver = get_driver()
45
+ if driver is None or not run_id:
46
+ return False
47
+ try:
48
+ with driver.session() as session:
49
+ session.run(
50
+ "MATCH (n:SciPathsNode {run_id: $run_id}) DETACH DELETE n",
51
+ run_id=run_id,
52
+ )
53
+ return True
54
+ except Exception:
55
+ return False
56
+
57
+
58
+ def upsert_graph(run_id: str, nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> bool:
59
+ """Replace the run's graph snapshot with the current cumulative nodes/edges."""
60
+ driver = get_driver()
61
+ if driver is None or not run_id:
62
+ return False
63
+ try:
64
+ with driver.session() as session:
65
+ session.execute_write(_upsert_tx, run_id, nodes, edges)
66
+ return True
67
+ except Exception:
68
+ return False
69
+
70
+
71
+ def _upsert_tx(tx, run_id: str, nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> None:
72
+ tx.run("MATCH (n:SciPathsNode {run_id: $run_id}) DETACH DELETE n", run_id=run_id)
73
+ for node in nodes:
74
+ tx.run(
75
+ """
76
+ CREATE (n:SciPathsNode {
77
+ run_id: $run_id,
78
+ id: $id,
79
+ kind: $kind,
80
+ label: $label,
81
+ title: $title,
82
+ detail: $detail,
83
+ step_added: $step_added,
84
+ pulse: $pulse,
85
+ color: $color,
86
+ size: $size
87
+ })
88
+ """,
89
+ run_id=run_id,
90
+ id=str(node.get("id") or ""),
91
+ kind=str(node.get("kind") or "node"),
92
+ label=str(node.get("label") or ""),
93
+ title=str(node.get("title") or node.get("label") or ""),
94
+ detail=str(node.get("detail") or ""),
95
+ step_added=int(node.get("step_added") or 0),
96
+ pulse=bool(node.get("pulse")),
97
+ color=str(node.get("color") or "#6a7a74"),
98
+ size=int(node.get("size") or 16),
99
+ )
100
+ for edge in edges:
101
+ tx.run(
102
+ """
103
+ MATCH (a:SciPathsNode {run_id: $run_id, id: $source})
104
+ MATCH (b:SciPathsNode {run_id: $run_id, id: $target})
105
+ CREATE (a)-[:SCI_REL {
106
+ run_id: $run_id,
107
+ id: $id,
108
+ kind: $kind,
109
+ pulse: $pulse,
110
+ muted: $muted
111
+ }]->(b)
112
+ """,
113
+ run_id=run_id,
114
+ source=str(edge.get("source") or ""),
115
+ target=str(edge.get("target") or ""),
116
+ id=str(edge.get("id") or ""),
117
+ kind=str(edge.get("kind") or "rel"),
118
+ pulse=bool(edge.get("pulse")),
119
+ muted=bool(edge.get("muted")),
120
+ )
121
+
122
+
123
+ def fetch_graph(run_id: str) -> Optional[dict[str, Any]]:
124
+ driver = get_driver()
125
+ if driver is None or not run_id:
126
+ return None
127
+ try:
128
+ with driver.session() as session:
129
+ nodes = session.run(
130
+ """
131
+ MATCH (n:SciPathsNode {run_id: $run_id})
132
+ RETURN n.id AS id, n.kind AS kind, n.label AS label, n.title AS title,
133
+ n.detail AS detail, n.step_added AS step_added, n.pulse AS pulse,
134
+ n.color AS color, n.size AS size
135
+ """,
136
+ run_id=run_id,
137
+ ).data()
138
+ edges = session.run(
139
+ """
140
+ MATCH (a:SciPathsNode {run_id: $run_id})-[r:SCI_REL]->(b:SciPathsNode {run_id: $run_id})
141
+ RETURN r.id AS id, a.id AS source, b.id AS target, r.kind AS kind,
142
+ r.pulse AS pulse, r.muted AS muted
143
+ """,
144
+ run_id=run_id,
145
+ ).data()
146
+ if not nodes:
147
+ return None
148
+ return {"nodes": nodes, "edges": edges}
149
+ except Exception:
150
+ return None
hf_space/replay.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load precomputed demo traces for Streamlit replay mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+ REPO_ROOT = Path(__file__).resolve().parent.parent
11
+ REPLAY_ROOT = REPO_ROOT / "replay_traces"
12
+
13
+
14
+ def _load_json(path: Path) -> Any | None:
15
+ if not path.exists():
16
+ return None
17
+ try:
18
+ return json.loads(path.read_text(encoding="utf-8"))
19
+ except Exception:
20
+ return None
21
+
22
+
23
+ def parse_arxiv_id(paper_input: str) -> str | None:
24
+ s = (paper_input or "").strip()
25
+ if not s:
26
+ return None
27
+ if "arxiv.org" in s:
28
+ m = re.search(r"arxiv\.org/(abs|pdf)/([^/?#]+)", s)
29
+ if not m:
30
+ return None
31
+ s = m.group(2)
32
+ s = s.replace(".pdf", "")
33
+ s = re.sub(r"v\d+$", "", s)
34
+ if not re.match(r"^[0-9]{4}\.[0-9]{4,5}$", s):
35
+ return None
36
+ return s
37
+
38
+
39
+ def load_index() -> dict:
40
+ data = _load_json(REPLAY_ROOT / "index.json")
41
+ return data if isinstance(data, dict) else {"examples": []}
42
+
43
+
44
+ def list_replay_examples() -> list[dict]:
45
+ examples = load_index().get("examples") or []
46
+ out = []
47
+ for item in examples:
48
+ if not isinstance(item, dict):
49
+ continue
50
+ arxiv_id = item.get("arxiv_id")
51
+ if not arxiv_id:
52
+ continue
53
+ meta_path = REPLAY_ROOT / str(arxiv_id) / "replay_meta.json"
54
+ if not meta_path.exists():
55
+ continue
56
+ out.append(item)
57
+ return out
58
+
59
+
60
+ def find_trace_dir(paper_input: str) -> Optional[Path]:
61
+ arxiv_id = parse_arxiv_id(paper_input)
62
+ if not arxiv_id:
63
+ return None
64
+ trace_dir = REPLAY_ROOT / arxiv_id
65
+ if (trace_dir / "replay_meta.json").exists():
66
+ return trace_dir
67
+ return None
68
+
69
+
70
+ def load_trace(paper_input: str) -> Optional[dict]:
71
+ """Return a session-ready payload for a saved demo trace, or None."""
72
+ trace_dir = find_trace_dir(paper_input)
73
+ if trace_dir is None:
74
+ return None
75
+
76
+ meta = _load_json(trace_dir / "replay_meta.json")
77
+ if not isinstance(meta, dict):
78
+ return None
79
+
80
+ paper_rel = meta.get("paper_dir") or f"processed_papers/{meta.get('arxiv_id')}"
81
+ paper_dir = (trace_dir / paper_rel).resolve()
82
+ if not paper_dir.exists():
83
+ return None
84
+
85
+ payload_rel = meta.get("annotation_payload_path")
86
+ payload_path = (trace_dir / payload_rel).resolve() if payload_rel else None
87
+ if payload_path and not payload_path.exists():
88
+ payload_path = None
89
+
90
+ events = []
91
+ for line in meta.get("events") or []:
92
+ text = str(line).strip()
93
+ if not text:
94
+ continue
95
+ # Normalize absolute annotation paths for display.
96
+ if text.startswith("[annotation] complete:"):
97
+ events.append("Step 8 complete")
98
+ events.append("Pipeline completed successfully.")
99
+ continue
100
+ if text == "Pipeline completed successfully.":
101
+ # Keep a single completion marker after annotation normalization.
102
+ if events and events[-1] == "Pipeline completed successfully.":
103
+ continue
104
+ events.append(text)
105
+
106
+ # Ensure a readable end marker for completed traces.
107
+ status = str(meta.get("status") or "Completed")
108
+ if status == "Completed" and "Pipeline completed successfully." not in events:
109
+ events.append("Pipeline completed successfully.")
110
+
111
+ return {
112
+ "label": meta.get("label") or meta.get("arxiv_id"),
113
+ "arxiv_id": meta.get("arxiv_id"),
114
+ "paper_input": meta.get("paper_input") or f"https://arxiv.org/abs/{meta.get('arxiv_id')}",
115
+ "status": status,
116
+ "run_status": status,
117
+ "run_events": events,
118
+ "run_logs": list(events),
119
+ "run_dir_path": str(trace_dir),
120
+ "paper_dir_path": str(paper_dir),
121
+ "annotation_payload_path": str(payload_path) if payload_path else None,
122
+ "annotation_skipped_reason": meta.get("annotation_skipped_reason"),
123
+ "pipeline_failed_reason": meta.get("pipeline_failed_reason"),
124
+ "pipeline_stopped_reason": meta.get("pipeline_stopped_reason"),
125
+ "artifact_path": str(trace_dir / "summary.txt") if (trace_dir / "summary.txt").exists() else None,
126
+ "remote_artifact_ref": "",
127
+ "replay_mode": True,
128
+ "cluster_count": meta.get("cluster_count"),
129
+ "claim_count": meta.get("claim_count"),
130
+ }
hf_space/requirements.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.36.0
2
+ arxiv==2.2.0
3
+ requests==2.32.5
4
+ google-generativeai
5
+ litellm
6
+ rapidfuzz
7
+ bibtexparser
8
+ sentence-transformers
9
+ # SciPath Live Run (parametric / websearch_deep CodeAgent)
10
+ smolagents
11
+ ddgs
12
+ playwright
13
+ PyPDF2
14
+ python-dotenv
15
+ openai
16
+ # Pin stack for Deep-Citation step 4 on macOS x86_64:
17
+ # torch 2.2.x needs numpy<2; newer torch wheels are unavailable on this platform,
18
+ # and transformers 5.x refuses torch<2.4.
19
+ transformers==4.46.3
20
+ torch==2.2.2
21
+ huggingface_hub>=0.23,<1.0
22
+ typer
23
+ tqdm
24
+ pydantic
25
+ numpy==1.26.4
26
+ pandas
27
+ scipy==1.11.4
28
+ scikit-learn==1.4.2
hf_space/runner.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ import uuid
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Generator, List, Optional, Tuple
12
+
13
+
14
+ @dataclass
15
+ class PipelineConfig:
16
+ repo_root: Path
17
+ source_root: Path
18
+ paper_input: str
19
+ llm_provider: str
20
+ llm_model: str
21
+ llm_model_step4: str
22
+ model_path: str
23
+ model_data_dir: str
24
+ model_class_def: str
25
+ model_lm: str
26
+ device: str
27
+ embedding_model: str
28
+
29
+
30
+ @dataclass
31
+ class PipelineResult:
32
+ job_id: str
33
+ job_dir: Path
34
+ paper_dir: Path
35
+ zip_path: Path
36
+
37
+
38
+ STEP_LABELS = {
39
+ 1: "Fetch metadata + LaTeX for input paper",
40
+ 2: "Add citation markers",
41
+ 3: "Build usage contexts",
42
+ 4: "Label citation functions",
43
+ 5: "Verify USES/EXTENDS",
44
+ 6: "Extract arXiv paragraphs",
45
+ 7: "Extract target contributions and refine clusters",
46
+ }
47
+
48
+ FULL_STEPS = [1, 2, 3, 4, 5, 6, 7]
49
+ STOP_PREFIX = "Pipeline stopped:"
50
+
51
+
52
+ def parse_arxiv_id(paper_input: str) -> str:
53
+ s = (paper_input or "").strip()
54
+ if not s:
55
+ raise ValueError("paper_input is required")
56
+ if "arxiv.org" in s:
57
+ m = re.search(r"arxiv\.org/(abs|pdf)/([^/?#]+)", s)
58
+ if not m:
59
+ raise ValueError(f"Could not parse arXiv ID from URL: {s}")
60
+ s = m.group(2)
61
+ s = s.replace(".pdf", "")
62
+ s = re.sub(r"v\d+$", "", s)
63
+ if not re.match(r"^[0-9]{4}\.[0-9]{4,5}$", s):
64
+ raise ValueError(f"Invalid arXiv ID format: {s}")
65
+ return s
66
+
67
+
68
+ def _build_commands(
69
+ cfg: PipelineConfig,
70
+ step: int,
71
+ job_processed_root: Path,
72
+ paper_id: str,
73
+ ids_path: Optional[Path],
74
+ ) -> List[List[str]]:
75
+ py = sys.executable
76
+ if step == 1:
77
+ assert ids_path is not None
78
+ return [[
79
+ py,
80
+ "src/step_01_fetch/fetch_metadata.py",
81
+ "--ids",
82
+ str(ids_path),
83
+ "--outdir",
84
+ str(job_processed_root),
85
+ ]]
86
+ if step == 2:
87
+ return [[py, "src/step_02_mark_citations/replace_citation_markers.py", "--root", str(job_processed_root)]]
88
+ if step == 3:
89
+ return [[py, "src/step_03_usage_contexts/build_usage_contexts.py", "--root", str(job_processed_root), "--out-name", "usage_contexts.json"]]
90
+ if step == 4:
91
+ return [[
92
+ py,
93
+ "src/step_04_label_citations/label_citation_functions.py",
94
+ "--root",
95
+ str(job_processed_root),
96
+ "--model-path",
97
+ cfg.model_path,
98
+ "--model-data-dir",
99
+ cfg.model_data_dir,
100
+ "--model-class-def",
101
+ cfg.model_class_def,
102
+ "--model-lm",
103
+ cfg.model_lm,
104
+ "--device",
105
+ cfg.device,
106
+ ]]
107
+ if step == 5:
108
+ return [[
109
+ py,
110
+ "src/step_05_verify_uses_extends/verify_uses_extends.py",
111
+ "--root",
112
+ str(job_processed_root),
113
+ "--k",
114
+ "0",
115
+ "--batch-size",
116
+ "25",
117
+ ]]
118
+ if step == 6:
119
+ return [[py, "src/step_06_extract_paragraphs/extract_arxiv_paragraphs.py", "--root", str(job_processed_root)]]
120
+ if step == 7:
121
+ return [
122
+ [py, "src/step_07_extract_and_refine/extract_contributions_from_citations.py", "--root", str(job_processed_root)],
123
+ [py, "src/step_07_extract_and_refine/refine_and_filter_clusters_llm.py", "--root", str(job_processed_root), "--inplace", "--overwrite"],
124
+ ]
125
+ raise ValueError(f"Unknown step: {step}")
126
+
127
+
128
+ def _write_single_id_file(job_dir: Path, arxiv_id: str) -> Path:
129
+ ids_path = job_dir / "input_ids.json"
130
+ payload = [{"id": arxiv_id, "title": "", "id_type": "ArXiv"}]
131
+ ids_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
132
+ return ids_path
133
+
134
+
135
+ def _write_run_metadata(cfg: PipelineConfig, job_dir: Path, paper_id: str, arxiv_id: str) -> None:
136
+ payload = {
137
+ "paper_input": cfg.paper_input,
138
+ "paper_id": paper_id,
139
+ "arxiv_id": arxiv_id,
140
+ "source_root": str(cfg.source_root),
141
+ "steps": FULL_STEPS + ["annotation"],
142
+ "llm_provider": cfg.llm_provider,
143
+ "llm_model": cfg.llm_model,
144
+ "llm_model_step4": cfg.llm_model_step4,
145
+ "device": cfg.device,
146
+ "embedding_model": cfg.embedding_model,
147
+ "timestamp": int(time.time()),
148
+ }
149
+ (job_dir / "run_config.json").write_text(json.dumps(payload, indent=2), encoding="utf-8")
150
+
151
+
152
+ def _zip_job_dir(job_dir: Path) -> Path:
153
+ zip_base = job_dir.parent / job_dir.name
154
+ archive = shutil.make_archive(str(zip_base), "zip", root_dir=str(job_dir))
155
+ return Path(archive)
156
+
157
+
158
+ def _tail_log(path: Path, max_lines: int = 60) -> str:
159
+ try:
160
+ lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
161
+ except Exception:
162
+ return ""
163
+ if not lines:
164
+ return ""
165
+ return "\n".join(lines[-max_lines:])
166
+
167
+
168
+ def _load_json(path: Path, default=None):
169
+ try:
170
+ return json.loads(path.read_text(encoding="utf-8"))
171
+ except Exception:
172
+ return default
173
+
174
+
175
+ def _write_summary_and_zip(job_dir: Path, summary_lines: List[str]) -> Path:
176
+ (job_dir / "summary.txt").write_text("\n".join(summary_lines), encoding="utf-8")
177
+ return _zip_job_dir(job_dir)
178
+
179
+
180
+ def _count_verified_uses_extends(payload: dict) -> int:
181
+ records = payload.get("confirmed") or payload.get("verified_contexts") or payload.get("contexts") or payload.get("items") or []
182
+ if not isinstance(records, list):
183
+ return 0
184
+ accepted = {"USES", "EXTENDS", "Uses", "Extends"}
185
+ return sum(1 for item in records if isinstance(item, dict) and item.get("label") in accepted)
186
+
187
+
188
+ def _stop_reason_after_step(step: int, paper_dir: Path) -> str | None:
189
+ if step == 1:
190
+ if not paper_dir.exists():
191
+ return "metadata could not be fetched for this paper"
192
+ if not (paper_dir / "processed_main.tex").exists():
193
+ return "arXiv source could not be retrieved or converted for this paper"
194
+ citations = _load_json(paper_dir / "citations_metadata.json", [])
195
+ if not isinstance(citations, list) or not citations:
196
+ return "Semantic Scholar returned no citing papers for this target paper"
197
+
198
+ if step == 3:
199
+ usage = _load_json(paper_dir / "usage_contexts.json", {})
200
+ if not isinstance(usage, dict):
201
+ return "citation usage contexts could not be built"
202
+ if int(usage.get("num_contexts") or 0) == 0:
203
+ return "no citation usage contexts were found"
204
+
205
+ if step == 4:
206
+ labels = _load_json(paper_dir / "usage_context_labels.json", {})
207
+ contexts = labels.get("labels") if isinstance(labels, dict) else None
208
+ if not isinstance(contexts, list) or not contexts:
209
+ return "citation-function labeling produced no labeled contexts"
210
+
211
+ if step == 5:
212
+ verified = _load_json(paper_dir / "usage_uses_extends_verified.json", {})
213
+ if not isinstance(verified, dict):
214
+ return "USES/EXTENDS verification did not produce an output file"
215
+ if _count_verified_uses_extends(verified) == 0:
216
+ return "no downstream citations were verified as USES or EXTENDS"
217
+
218
+ if step == 6:
219
+ paragraphs = _load_json(paper_dir / "usage_citing_paragraphs.json", {})
220
+ citing = paragraphs.get("citing_papers") if isinstance(paragraphs, dict) else None
221
+ if not isinstance(citing, list) or not citing:
222
+ return "no citing-paper paragraphs could be extracted from arXiv"
223
+ usable = [
224
+ item for item in citing
225
+ if isinstance(item, dict)
226
+ and not item.get("error")
227
+ and (item.get("matched_paragraphs") or item.get("target_citing_paragraphs"))
228
+ ]
229
+ if not usable:
230
+ return "arXiv paragraph extraction returned no usable citing-paper text"
231
+
232
+ if step == 7:
233
+ contributions = _load_json(paper_dir / "usage_contributions.json", {})
234
+ items = contributions.get("contributions") if isinstance(contributions, dict) else None
235
+ if not isinstance(items, list) or not items:
236
+ return "no downstream target-contribution evidence could be extracted"
237
+ refined = _load_json(paper_dir / "usage_discovery_from_contributions.json", {})
238
+ clusters = refined.get("clusters") if isinstance(refined, dict) else None
239
+ if not isinstance(clusters, list) or not clusters:
240
+ return "no valid downstream usage clusters survived refinement"
241
+
242
+ return None
243
+
244
+
245
+ def run_pipeline(cfg: PipelineConfig, output_root: Path) -> Generator[Tuple[str, Optional[str]], None, PipelineResult]:
246
+ output_root.mkdir(parents=True, exist_ok=True)
247
+ job_id = f"job_{int(time.time())}_{uuid.uuid4().hex[:8]}"
248
+ job_dir = output_root / job_id
249
+ job_processed_root = job_dir / "processed_papers"
250
+ job_logs = job_dir / "logs"
251
+
252
+ job_processed_root.mkdir(parents=True, exist_ok=True)
253
+ job_logs.mkdir(parents=True, exist_ok=True)
254
+
255
+ arxiv_id = parse_arxiv_id(cfg.paper_input)
256
+ paper_id = arxiv_id
257
+ ids_path = _write_single_id_file(job_dir, arxiv_id)
258
+ _write_run_metadata(cfg, job_dir, paper_id, arxiv_id)
259
+
260
+ base_env = os.environ.copy()
261
+ base_env["LLM_PROVIDER"] = cfg.llm_provider
262
+ base_env["LLM_MODEL"] = cfg.llm_model
263
+
264
+ summary_lines: List[str] = []
265
+ paper_dir = job_processed_root / paper_id
266
+
267
+ max_step = 8
268
+ for step in FULL_STEPS:
269
+ label = STEP_LABELS[step]
270
+ log_file = job_logs / f"step_{step:02d}.log"
271
+ summary_lines.append(f"[{step}] {label}")
272
+ yield (f"Step {step}/{max_step}: {label}", None)
273
+
274
+ env = base_env.copy()
275
+ if step == 5 and cfg.llm_model_step4:
276
+ env["LLM_MODEL"] = cfg.llm_model_step4
277
+
278
+ with log_file.open("w", encoding="utf-8") as lf:
279
+ return_code = 0
280
+ failed_cmd: List[str] | None = None
281
+ for cmd in _build_commands(cfg, step, job_processed_root, paper_id, ids_path):
282
+ lf.write(f"$ {' '.join(cmd)}\n\n")
283
+ proc = subprocess.Popen(
284
+ cmd,
285
+ cwd=str(cfg.repo_root),
286
+ stdout=subprocess.PIPE,
287
+ stderr=subprocess.STDOUT,
288
+ text=True,
289
+ encoding="utf-8",
290
+ errors="ignore",
291
+ env=env,
292
+ )
293
+ assert proc.stdout is not None
294
+ for line in proc.stdout:
295
+ lf.write(line)
296
+ return_code = proc.wait()
297
+ if return_code != 0:
298
+ failed_cmd = cmd
299
+ break
300
+
301
+ if return_code != 0:
302
+ summary_lines.append(f"FAILED at step {step}")
303
+ zip_path = _write_summary_and_zip(job_dir, summary_lines)
304
+ tail = _tail_log(log_file)
305
+ if tail:
306
+ yield (
307
+ f"Step {step} failed.\n\nCommand: {' '.join(failed_cmd or [])}\n\nLast log lines:\n{tail}",
308
+ str(zip_path),
309
+ )
310
+ else:
311
+ yield (f"Step {step} failed. Command: {' '.join(failed_cmd or [])}", str(zip_path))
312
+ return PipelineResult(job_id=job_id, job_dir=job_dir, paper_dir=paper_dir, zip_path=zip_path)
313
+ else:
314
+ yield (f"Step {step} complete", None)
315
+
316
+ if step == 1 and not paper_dir.exists():
317
+ summary_lines.append("FAILED: fetch_metadata did not create paper directory")
318
+ zip_path = _write_summary_and_zip(job_dir, summary_lines)
319
+ yield (f"Step 1 finished but paper dir missing: {paper_dir}", str(zip_path))
320
+ return PipelineResult(job_id=job_id, job_dir=job_dir, paper_dir=paper_dir, zip_path=zip_path)
321
+
322
+ stop_reason = _stop_reason_after_step(step, paper_dir)
323
+ if stop_reason:
324
+ message = f"{STOP_PREFIX} {stop_reason}."
325
+ summary_lines.append(message)
326
+ zip_path = _write_summary_and_zip(job_dir, summary_lines)
327
+ yield (message, str(zip_path))
328
+ return PipelineResult(job_id=job_id, job_dir=job_dir, paper_dir=paper_dir, zip_path=zip_path)
329
+
330
+ summary_lines.append("SUCCESS")
331
+ zip_path = _write_summary_and_zip(job_dir, summary_lines)
332
+ yield ("Pipeline completed successfully.", str(zip_path))
333
+ return PipelineResult(job_id=job_id, job_dir=job_dir, paper_dir=paper_dir, zip_path=zip_path)
hf_space/scipath_live/README.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SciPath Live (self-contained)
2
+
3
+ Minimal SciPath Task A CodeAgent used by **System Run → Live Run**.
4
+
5
+ ## Included methods
6
+
7
+ | Method | Tools |
8
+ |--------|--------|
9
+ | `parametric` | `check_answer_format` |
10
+ | `websearch_deep` | `web_search_tool`, `deep_web_search_tool`, `check_answer_format` |
11
+
12
+ ## Demo claim
13
+
14
+ `data/example_claim.json` — one claim from AVerImaTeC (`2505.17978`, claim C1), including gold enabling contributions for Task A evaluation.
15
+
16
+ ## CLI
17
+
18
+ From `hf_space/`:
19
+
20
+ ```bash
21
+ python -m scipath_live.cli run --method parametric --output-dir runs/system_live/test
22
+ python -m scipath_live.cli evaluate --result-json runs/system_live/test/result.json
23
+ ```
24
+
25
+ ## Env
26
+
27
+ - `GOOGLE_GENAI_API_KEY` or `GEMINI_API_KEY` (generation + judge)
28
+ - For `websearch_deep`: install Chromium into the demo-local cache:
29
+
30
+ ```bash
31
+ # from demo_2026_07/
32
+ export PLAYWRIGHT_BROWSERS_PATH="$(pwd)/.playwright"
33
+ python -m playwright install chromium
34
+ ```
35
+
36
+ `system_live_runner` / `tools.py` always force `PLAYWRIGHT_BROWSERS_PATH` to `demo_2026_07/.playwright` so Cursor sandbox temp paths are not used.
37
+
38
+ This package does **not** depend on `dryrun/` or other repos.
hf_space/scipath_live/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Self-contained SciPath Task A CodeAgent (parametric + websearch_deep)."""
2
+
3
+ from .pipeline import load_example_claim
4
+
5
+ __all__ = ["load_example_claim", "run_method", "evaluate_task_a"]
6
+
7
+
8
+ def run_method(*args, **kwargs):
9
+ from .pipeline import run_method as _run
10
+
11
+ return _run(*args, **kwargs)
12
+
13
+
14
+ def evaluate_task_a(*args, **kwargs):
15
+ from .pipeline import evaluate_task_a as _eval
16
+
17
+ return _eval(*args, **kwargs)
18
+
19
+ __all__ = ["evaluate_task_a", "load_example_claim", "run_method"]
hf_space/scipath_live/agent.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Slim SciPath CodeAgent runner (parametric + websearch_deep only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Optional
11
+
12
+ from dotenv import load_dotenv
13
+ from smolagents import CodeAgent, LiteLLMModel
14
+
15
+ from .prompts import get_scipath_controller_prompt, parse_ingredients_from_answer
16
+ from .tools import check_answer_format, deep_web_search_tool, web_search_tool
17
+
18
+ GEMINI_MODEL_IDS = {
19
+ "gemini-2.0-flash",
20
+ "gemini-2.5-flash",
21
+ "gemini-2.5-pro",
22
+ "gemini-3-flash-preview",
23
+ "gemini-3.1-pro-preview",
24
+ "gemini-3.5-flash",
25
+ }
26
+
27
+
28
+ def _memory_steps_to_list(agent: CodeAgent) -> list[dict[str, Any]]:
29
+ steps_list: list[dict[str, Any]] = []
30
+ for i, step in enumerate(getattr(agent.memory, "steps", []) or []):
31
+ item: dict[str, Any] = {
32
+ "step_type": type(step).__name__,
33
+ "step_index": i,
34
+ "timestamp": datetime.now().isoformat(),
35
+ }
36
+ for attr in ("step_number", "task", "code_action", "observations", "model_output"):
37
+ if hasattr(step, attr):
38
+ value = getattr(step, attr)
39
+ item[attr] = str(value) if value is not None and not isinstance(value, (str, int, float, bool, list, dict)) else value
40
+ if getattr(step, "error", None):
41
+ item["error"] = str(step.error)
42
+ steps_list.append(item)
43
+ return steps_list
44
+
45
+
46
+ def _clip(text: Any, limit: int = 1200) -> str:
47
+ s = str(text or "").strip()
48
+ if len(s) <= limit:
49
+ return s
50
+ return s[: limit - 1] + "…"
51
+
52
+
53
+ def format_agent_trace(steps: list[dict[str, Any]]) -> list[str]:
54
+ """Human-readable SciFy CodeAgent step trace for the Live Run terminal."""
55
+ lines: list[str] = []
56
+ lines.append("=== SciFy CodeAgent trace ===")
57
+ action_steps = [s for s in steps if s.get("step_type") == "ActionStep"]
58
+ if not action_steps:
59
+ lines.append("(no action steps recorded)")
60
+ return lines
61
+
62
+ for step in action_steps:
63
+ n = step.get("step_number")
64
+ if n is None:
65
+ n = int(step.get("step_index") or 0) + 1
66
+ lines.append("")
67
+ lines.append(f"── Step {n} ──")
68
+ thought = _clip(step.get("model_output"), 1600)
69
+ if thought:
70
+ lines.append("Thought / model output:")
71
+ lines.extend(thought.splitlines())
72
+ code = _clip(step.get("code_action"), 1600)
73
+ if code:
74
+ lines.append("Code action:")
75
+ lines.extend(code.splitlines())
76
+ obs = _clip(step.get("observations"), 1200)
77
+ if obs:
78
+ lines.append("Observations:")
79
+ lines.extend(obs.splitlines())
80
+ err = _clip(step.get("error"), 600)
81
+ if err:
82
+ lines.append(f"Error: {err}")
83
+ lines.append("")
84
+ lines.append("=== end trace ===")
85
+ return lines
86
+
87
+
88
+ def _print_trace(steps: list[dict[str, Any]]) -> None:
89
+ for line in format_agent_trace(steps):
90
+ print(line, flush=True)
91
+
92
+
93
+ def _ensure_api_keys() -> None:
94
+ load_dotenv()
95
+ google = (
96
+ os.getenv("GOOGLE_GENAI_API_KEY")
97
+ or os.getenv("GEMINI_API_KEY")
98
+ or os.getenv("GOOGLE_API_KEY")
99
+ or ""
100
+ ).strip()
101
+ if google:
102
+ os.environ.setdefault("GEMINI_API_KEY", google)
103
+ os.environ.setdefault("GOOGLE_API_KEY", google)
104
+ os.environ.setdefault("GOOGLE_GENAI_API_KEY", google)
105
+
106
+
107
+ def run_agent(
108
+ query: str,
109
+ *,
110
+ model_name: str = "gemini-3-flash-preview",
111
+ method: str = "parametric",
112
+ max_steps: int = 40,
113
+ ) -> tuple[Any, list[dict[str, Any]]]:
114
+ """Run SciPath Task A CodeAgent for one claim."""
115
+ if method not in {"parametric", "websearch_deep"}:
116
+ raise ValueError(f"Unsupported method for demo live run: {method}")
117
+
118
+ _ensure_api_keys()
119
+ litellm_model_id = (
120
+ f"gemini/{model_name}" if model_name in GEMINI_MODEL_IDS else model_name
121
+ )
122
+ model = LiteLLMModel(
123
+ model_id=litellm_model_id,
124
+ temperature=0.1,
125
+ max_tokens=16384,
126
+ timeout=2400,
127
+ )
128
+
129
+ if method == "parametric":
130
+ tool_list = [check_answer_format]
131
+ else:
132
+ tool_list = [web_search_tool, deep_web_search_tool, check_answer_format]
133
+
134
+ agent = CodeAgent(
135
+ tools=tool_list,
136
+ model=model,
137
+ verbosity_level=2,
138
+ additional_authorized_imports=["json", "os", "typing", "pprint"],
139
+ max_steps=max_steps,
140
+ executor_kwargs={"timeout_seconds": None},
141
+ )
142
+ prompt = get_scipath_controller_prompt(query, method)
143
+ result = agent.run(prompt)
144
+ logs = _memory_steps_to_list(agent)
145
+ return result, logs
146
+
147
+
148
+ def run_claim_to_dir(
149
+ *,
150
+ claim: str,
151
+ paper_id: str,
152
+ claim_idx: int,
153
+ paper_title: str,
154
+ method: str,
155
+ output_dir: Path,
156
+ model_name: str = "gemini-3-flash-preview",
157
+ ) -> dict[str, Any]:
158
+ """Run one claim and write answers.jsonl + logs.jsonl into output_dir."""
159
+ output_dir = Path(output_dir)
160
+ output_dir.mkdir(parents=True, exist_ok=True)
161
+
162
+ problem_id = f"{paper_id}__claim{claim_idx}"
163
+ claim_row = {
164
+ "type": "problem",
165
+ "format_version": "1.0",
166
+ "problem_id": problem_id,
167
+ "problem_version": "1.0",
168
+ "domain": "ai",
169
+ "subdomain": "scipath",
170
+ "claim": claim,
171
+ }
172
+ index_row = {
173
+ "problem_id": problem_id,
174
+ "paper_id": paper_id,
175
+ "claim_idx": claim_idx,
176
+ "paper_title": paper_title,
177
+ "claim": claim,
178
+ "split": "dev",
179
+ }
180
+ (output_dir / "claim.jsonl").write_text(
181
+ json.dumps(claim_row, ensure_ascii=False) + "\n", encoding="utf-8"
182
+ )
183
+ (output_dir / "task_index.jsonl").write_text(
184
+ json.dumps(index_row, ensure_ascii=False) + "\n", encoding="utf-8"
185
+ )
186
+
187
+ print(f"[scipath_live] Starting CodeAgent method={method} model={model_name}", flush=True)
188
+ print(f"[scipath_live] Claim: {claim}", flush=True)
189
+ t0 = time.time()
190
+ answer, logs = run_agent(claim, model_name=model_name, method=method)
191
+ wall = time.time() - t0
192
+ print(f"[scipath_live] Agent finished in {wall:.1f}s ({len(logs)} memory steps)", flush=True)
193
+ _print_trace(logs)
194
+
195
+ ingredients = parse_ingredients_from_answer(answer)
196
+ if not ingredients and isinstance(answer, dict):
197
+ ingredients = parse_ingredients_from_answer(answer)
198
+ print(f"[scipath_live] Parsed {len(ingredients)} ingredients", flush=True)
199
+
200
+ out = {
201
+ "type": "scipath_task_a",
202
+ "format_version": "1.0",
203
+ "problem_id": problem_id,
204
+ "problem_version": "1.0",
205
+ "team": "scify",
206
+ "run_id": f"live_{method}",
207
+ "ingredients": ingredients,
208
+ "raw_answer": answer if isinstance(answer, (dict, list, str)) else str(answer),
209
+ "wall_clock_time": wall,
210
+ "method": method,
211
+ "model_name": model_name,
212
+ "paper_id": paper_id,
213
+ "claim_idx": claim_idx,
214
+ "paper_title": paper_title,
215
+ "claim": claim,
216
+ }
217
+ (output_dir / "answers.jsonl").write_text(
218
+ json.dumps(out, ensure_ascii=False) + "\n", encoding="utf-8"
219
+ )
220
+ (output_dir / "logs.jsonl").write_text(
221
+ json.dumps({"problem_id": problem_id, "steps": logs}, ensure_ascii=False) + "\n",
222
+ encoding="utf-8",
223
+ )
224
+ (output_dir / "result.json").write_text(
225
+ json.dumps(out, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
226
+ )
227
+ return out
hf_space/scipath_live/cli.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """CLI entry for SciPath Live Run (used as a subprocess from Streamlit)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Allow `python -m scipath_live.cli` and `python scipath_live/cli.py`
12
+ PACKAGE_ROOT = Path(__file__).resolve().parent
13
+ HF_SPACE = PACKAGE_ROOT.parent
14
+ if str(HF_SPACE) not in sys.path:
15
+ sys.path.insert(0, str(HF_SPACE))
16
+
17
+ from scipath_live.pipeline import evaluate_task_a, load_example_claim, run_method # noqa: E402
18
+
19
+
20
+ def main() -> int:
21
+ parser = argparse.ArgumentParser(description="SciPath Live Run CLI")
22
+ parser.add_argument(
23
+ "action",
24
+ choices=["run", "evaluate"],
25
+ help="run a method or evaluate an existing run result JSON",
26
+ )
27
+ parser.add_argument(
28
+ "--method",
29
+ default="parametric",
30
+ choices=["parametric", "websearch_deep", "codeagent_parametric", "codeagent_websearch_deep"],
31
+ )
32
+ parser.add_argument("--output-dir", default="")
33
+ parser.add_argument("--model", default="gemini-3-flash-preview")
34
+ parser.add_argument("--judge-model", default="gemini/gemini-3.1-pro-preview")
35
+ parser.add_argument(
36
+ "--result-json",
37
+ default="",
38
+ help="For evaluate: path to result.json from a prior run",
39
+ )
40
+ args = parser.parse_args()
41
+
42
+ if args.action == "run":
43
+ out = Path(args.output_dir) if args.output_dir else None
44
+ result = run_method(
45
+ args.method,
46
+ output_dir=out,
47
+ model_name=args.model,
48
+ )
49
+ print(json.dumps({"ok": True, "result": result}, ensure_ascii=False))
50
+ return 0
51
+
52
+ if not args.result_json:
53
+ print(json.dumps({"ok": False, "error": "--result-json is required for evaluate"}))
54
+ return 2
55
+ run_result = json.loads(Path(args.result_json).read_text(encoding="utf-8"))
56
+ # Ensure output_dir is set for writing judged JSON.
57
+ if not run_result.get("output_dir"):
58
+ run_result["output_dir"] = str(Path(args.result_json).parent)
59
+ judged = evaluate_task_a(
60
+ run_result=run_result,
61
+ claim_record=load_example_claim(),
62
+ judge_model=args.judge_model,
63
+ )
64
+ print(json.dumps({"ok": True, "judged": judged}, ensure_ascii=False))
65
+ return 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())
hf_space/scipath_live/data/example_claim.json ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "paper_id": "2505.17978",
3
+ "claim_idx": 0,
4
+ "claim_id": "C1",
5
+ "paper_title": "AVerImaTeC: A Dataset for Automatic Verification of Image-Text Claims with Evidence from the Web",
6
+ "claim": "Dataset: A real-world image-text claim verification dataset annotated with question-answer reasoning paths and web evidence, enabling the training and evaluation of multimodal fact-checking systems.",
7
+ "enabling_contributions": [
8
+ {
9
+ "enabling_contribution_id": "2505.17978::ingredient::01",
10
+ "enabling_contribution": "Source fact-checking articles containing multimodal claims",
11
+ "canonical_ref_id": "__NONE__",
12
+ "canonical_grounding": null,
13
+ "additional_ref_ids": [
14
+ "ref_averitec",
15
+ "ref_ammeba"
16
+ ],
17
+ "additional_groundings": [
18
+ {
19
+ "ref_id": "ref_averitec",
20
+ "bib_key": "",
21
+ "paper_id": "",
22
+ "external_ids": {},
23
+ "ref_title": "AVeriTeC: A Dataset for Real-world Claim Verification with Evidence from the Web",
24
+ "ref_year": "",
25
+ "ref_authors": ""
26
+ },
27
+ {
28
+ "ref_id": "ref_ammeba",
29
+ "bib_key": "",
30
+ "paper_id": "",
31
+ "external_ids": {},
32
+ "ref_title": "AMMeBa: A Large-Scale Survey and Dataset of Media-Based Misinformation In-The-Wild",
33
+ "ref_year": "",
34
+ "ref_authors": ""
35
+ }
36
+ ],
37
+ "canonical_annotation": {
38
+ "role": "DATA_SOURCE",
39
+ "roles": [
40
+ "DATA_SOURCE"
41
+ ],
42
+ "contribution": "Provides the raw real-world image-text claims that are normalized and annotated to form the dataset.",
43
+ "rationale": "The dataset is a composite of claims extracted from multiple prior fact-checking resources. No single prior study fully represents this substrate, making NONE the correct canonical grounding, with the primary sources listed as additional groundings.",
44
+ "evidence_span": "we incorporated partially annotated articles from three sources: 1) filtered articles from AVeriTec... 2) articles verifying image-related claims from AMMEBA"
45
+ }
46
+ },
47
+ {
48
+ "enabling_contribution_id": "2505.17978::ingredient::02",
49
+ "enabling_contribution": "Question-answering representation for verification rationale",
50
+ "canonical_ref_id": "ref_complex",
51
+ "canonical_grounding": {
52
+ "ref_id": "ref_complex",
53
+ "bib_key": "",
54
+ "paper_id": "",
55
+ "external_ids": {},
56
+ "ref_title": "Complex Claim Verification with Evidence Retrieved in the Wild",
57
+ "ref_year": "",
58
+ "ref_authors": ""
59
+ },
60
+ "additional_ref_ids": [],
61
+ "additional_groundings": [],
62
+ "canonical_annotation": {
63
+ "role": "CONCEPTUAL_FRAMEWORK",
64
+ "roles": [
65
+ "CONCEPTUAL_FRAMEWORK"
66
+ ],
67
+ "contribution": "Provides the QA-based reasoning representation used to structure the evidence retrieval and verification process.",
68
+ "rationale": "The paper explicitly adopts QA as the reasoning representation for fact-checking, citing prior work that established this paradigm. Chen et al. (CLAIMDECOMP) is a clean representative for QA-based claim decomposition.",
69
+ "evidence_span": "To capture the rationale in claim verification, a complex reasoning task, various reasoning representations have been explored... and QA"
70
+ }
71
+ },
72
+ {
73
+ "enabling_contribution_id": "2505.17978::ingredient::03",
74
+ "enabling_contribution": "Four-way veracity labeling schema",
75
+ "canonical_ref_id": "ref_averitec",
76
+ "canonical_grounding": {
77
+ "ref_id": "ref_averitec",
78
+ "bib_key": "",
79
+ "paper_id": "",
80
+ "external_ids": {},
81
+ "ref_title": "AVeriTeC: A Dataset for Real-world Claim Verification with Evidence from the Web",
82
+ "ref_year": "",
83
+ "ref_authors": ""
84
+ },
85
+ "additional_ref_ids": [],
86
+ "additional_groundings": [],
87
+ "canonical_annotation": {
88
+ "role": "CONCEPTUAL_FRAMEWORK",
89
+ "roles": [
90
+ "CONCEPTUAL_FRAMEWORK"
91
+ ],
92
+ "contribution": "Provides the specific four-way label space (supported, refuted, not enough evidence, conflicting) used to annotate the claims.",
93
+ "rationale": "The paper explicitly adopts the exact four-way veracity labeling schema introduced in AVeriTeC.",
94
+ "evidence_span": "We follow the four-way veracity labeling schema from [Schlichtkrull et al., 2023]: supported, refuted, not enough evidence, and conflicting/cherry-picking."
95
+ }
96
+ },
97
+ {
98
+ "enabling_contribution_id": "2505.17978::ingredient::04",
99
+ "enabling_contribution": "Multi-stage annotation pipeline with evidence sufficiency checks",
100
+ "canonical_ref_id": "ref_averitec",
101
+ "canonical_grounding": {
102
+ "ref_id": "ref_averitec",
103
+ "bib_key": "",
104
+ "paper_id": "",
105
+ "external_ids": {},
106
+ "ref_title": "AVeriTeC: A Dataset for Real-world Claim Verification with Evidence from the Web",
107
+ "ref_year": "",
108
+ "ref_authors": ""
109
+ },
110
+ "additional_ref_ids": [],
111
+ "additional_groundings": [],
112
+ "canonical_annotation": {
113
+ "role": "EVALUATION_PROTOCOL",
114
+ "roles": [
115
+ "EVALUATION_PROTOCOL"
116
+ ],
117
+ "contribution": "Provides the foundational multi-stage annotation and verification workflow that the paper extends for image-text claims.",
118
+ "rationale": "The paper explicitly states that its annotation pipeline is an extension of the process proposed in AVeriTeC, making it the canonical grounding for this workflow.",
119
+ "evidence_span": "extending the annotation process proposed in [Schlichtkrull et al., 2023] to the domain of image-text claims."
120
+ }
121
+ }
122
+ ],
123
+ "source": "AVerImaTeC"
124
+ }
hf_space/scipath_live/judge.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task A ingredient matching judge (demo-local, single-claim)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import re
9
+ from typing import Any
10
+
11
+ import litellm
12
+ from dotenv import load_dotenv
13
+
14
+ MATCHING_JUDGE_PROMPT = """\
15
+ You are evaluating whether predicted enabling contributions match reference enabling contributions for a scientific discovery.
16
+
17
+ A full match requires that the predicted item express the same functional requirement needed to realize the discovery. It may use different wording, but it must preserve the key resource, method, process, role, and level of specificity.
18
+
19
+ A partial match means the predicted item is related but too broad, too narrow, missing an important role/detail, or merges multiple distinct reference requirements.
20
+
21
+ Do not mark broad umbrella items as full matches for multiple distinct reference contributions.
22
+
23
+ Discovery claim:
24
+ {claim}
25
+
26
+ Reference enabling contributions:
27
+ {references}
28
+
29
+ Predicted enabling contributions:
30
+ {predicted}
31
+
32
+ Return candidate matches only where match is "full" or "partial". Omit unrelated pairs.
33
+
34
+ Respond with JSON only (no markdown):
35
+ {{"matches": [{{"reference_idx": <1-based index>, "predicted_idx": <1-based index>, "match": "full"|"partial", "reasoning": "<one sentence>"}}]}}\
36
+ """
37
+
38
+
39
+ def _format_reference_ingredient(silver_ingredient: dict) -> str:
40
+ annotation = silver_ingredient.get("canonical_annotation") or {}
41
+ roles = annotation.get("roles")
42
+ if isinstance(roles, list):
43
+ role_text = ", ".join(str(r) for r in roles)
44
+ else:
45
+ role_text = annotation.get("role", "") or silver_ingredient.get("role", "")
46
+
47
+ ingredient = (
48
+ silver_ingredient.get("enabling_contribution")
49
+ or silver_ingredient.get("ingredient")
50
+ or silver_ingredient.get("description")
51
+ or ""
52
+ )
53
+ lines = [f"Ingredient: {ingredient}"]
54
+ if role_text:
55
+ lines.append(f"Role: {role_text}")
56
+ if annotation.get("contribution"):
57
+ lines.append(f"Contribution: {annotation['contribution']}")
58
+ if annotation.get("rationale"):
59
+ lines.append(f"Rationale: {annotation['rationale']}")
60
+ return "\n".join(lines)
61
+
62
+
63
+ def _format_predicted_ingredient(idx: int, ingredient: dict) -> str:
64
+ lines = [f"[{idx}] {ingredient.get('description', '')}"]
65
+ if ingredient.get("role"):
66
+ lines.append(f" Role: {ingredient['role']}")
67
+ if ingredient.get("rationale"):
68
+ lines.append(f" Rationale: {ingredient['rationale']}")
69
+ return "\n".join(lines)
70
+
71
+
72
+ def _parse_matching_judgment(raw: str, n_references: int, n_predicted: int) -> dict | None:
73
+ raw = re.sub(r"^```(?:json)?\s*", "", raw.strip())
74
+ raw = re.sub(r"\s*```$", "", raw)
75
+ try:
76
+ data = json.loads(raw)
77
+ except json.JSONDecodeError:
78
+ return None
79
+ raw_matches = data.get("matches")
80
+ if not isinstance(raw_matches, list):
81
+ return None
82
+ matches = []
83
+ for match in raw_matches:
84
+ if not isinstance(match, dict):
85
+ continue
86
+ ref_idx = match.get("reference_idx")
87
+ pred_idx = match.get("predicted_idx")
88
+ label = str(match.get("match", "")).lower()
89
+ if label not in {"full", "partial"}:
90
+ continue
91
+ if not isinstance(ref_idx, int) or not isinstance(pred_idx, int):
92
+ continue
93
+ if not (1 <= ref_idx <= n_references and 1 <= pred_idx <= n_predicted):
94
+ continue
95
+ matches.append(
96
+ {
97
+ "reference_idx": ref_idx,
98
+ "predicted_idx": pred_idx,
99
+ "match": label,
100
+ "reasoning": str(match.get("reasoning", "")),
101
+ }
102
+ )
103
+ return {"matches": matches}
104
+
105
+
106
+ def maximum_full_matching(
107
+ matches: list[dict], n_references: int, n_predicted: int
108
+ ) -> list[dict]:
109
+ edges: dict[int, list[dict]] = {i: [] for i in range(1, n_references + 1)}
110
+ seen: set[tuple[int, int]] = set()
111
+ for match in matches:
112
+ if match.get("match") != "full":
113
+ continue
114
+ ref_idx = match.get("reference_idx")
115
+ pred_idx = match.get("predicted_idx")
116
+ if not isinstance(ref_idx, int) or not isinstance(pred_idx, int):
117
+ continue
118
+ if not (1 <= ref_idx <= n_references and 1 <= pred_idx <= n_predicted):
119
+ continue
120
+ key = (ref_idx, pred_idx)
121
+ if key in seen:
122
+ continue
123
+ seen.add(key)
124
+ edges[ref_idx].append(match)
125
+ for ref_edges in edges.values():
126
+ ref_edges.sort(key=lambda m: m["predicted_idx"])
127
+
128
+ pred_to_match: dict[int, dict] = {}
129
+
130
+ def _try_match(ref_idx: int, visited: set[int]) -> bool:
131
+ for edge in edges.get(ref_idx, []):
132
+ pred_idx = edge["predicted_idx"]
133
+ if pred_idx in visited:
134
+ continue
135
+ visited.add(pred_idx)
136
+ previous = pred_to_match.get(pred_idx)
137
+ if previous is None or _try_match(previous["reference_idx"], visited):
138
+ pred_to_match[pred_idx] = edge
139
+ return True
140
+ return False
141
+
142
+ for ref_idx in range(1, n_references + 1):
143
+ _try_match(ref_idx, set())
144
+ return sorted(
145
+ pred_to_match.values(), key=lambda m: (m["reference_idx"], m["predicted_idx"])
146
+ )
147
+
148
+
149
+ async def judge_ingredient_matching(
150
+ model: str,
151
+ claim: str,
152
+ reference_ingredients: list[dict],
153
+ predicted_ingredients: list[dict],
154
+ max_retries: int = 3,
155
+ ) -> dict:
156
+ references_text = "\n\n".join(
157
+ f"[{i + 1}] " + _format_reference_ingredient(ing).replace("\n", "\n ")
158
+ for i, ing in enumerate(reference_ingredients)
159
+ ) or "(none)"
160
+ predicted_text = "\n\n".join(
161
+ _format_predicted_ingredient(i + 1, ing)
162
+ for i, ing in enumerate(predicted_ingredients)
163
+ ) or "(none)"
164
+ prompt = MATCHING_JUDGE_PROMPT.format(
165
+ claim=claim, references=references_text, predicted=predicted_text
166
+ )
167
+ for _ in range(max_retries):
168
+ msg = await litellm.acompletion(
169
+ model=model,
170
+ max_tokens=8192,
171
+ messages=[{"role": "user", "content": prompt}],
172
+ )
173
+ raw = msg.choices[0].message.content.strip()
174
+ result = _parse_matching_judgment(
175
+ raw, len(reference_ingredients), len(predicted_ingredients)
176
+ )
177
+ if result is not None:
178
+ return result
179
+ raise ValueError("Failed to parse valid matching judgment")
180
+
181
+
182
+ def _gold_to_ui_ingredients(gold: list[dict]) -> list[dict[str, str]]:
183
+ out = []
184
+ for g in gold:
185
+ ann = g.get("canonical_annotation") or {}
186
+ role = ""
187
+ if isinstance(ann.get("roles"), list) and ann["roles"]:
188
+ role = str(ann["roles"][0])
189
+ else:
190
+ role = str(ann.get("role") or g.get("role") or "")
191
+ out.append(
192
+ {
193
+ "description": str(
194
+ g.get("enabling_contribution") or g.get("description") or ""
195
+ ),
196
+ "role": role,
197
+ }
198
+ )
199
+ return out
200
+
201
+
202
+ def evaluate_prediction(
203
+ *,
204
+ claim: str,
205
+ gold_ingredients: list[dict],
206
+ predicted_ingredients: list[dict],
207
+ judge_model: str = "gemini/gemini-3.1-pro-preview",
208
+ ) -> dict[str, Any]:
209
+ """Sync wrapper: judge one claim and compute Task A scores."""
210
+ load_dotenv()
211
+ google = (
212
+ os.getenv("GOOGLE_GENAI_API_KEY")
213
+ or os.getenv("GEMINI_API_KEY")
214
+ or os.getenv("GOOGLE_API_KEY")
215
+ or ""
216
+ ).strip()
217
+ if google:
218
+ os.environ.setdefault("GEMINI_API_KEY", google)
219
+ os.environ.setdefault("GOOGLE_API_KEY", google)
220
+
221
+ matching = asyncio.run(
222
+ judge_ingredient_matching(
223
+ judge_model, claim, gold_ingredients, predicted_ingredients
224
+ )
225
+ )
226
+ matches = matching.get("matches") or []
227
+ full_pairs = maximum_full_matching(
228
+ matches, len(gold_ingredients), len(predicted_ingredients)
229
+ )
230
+ n_matched = len(full_pairs)
231
+ n_ref = len(gold_ingredients)
232
+ n_pred = len(predicted_ingredients)
233
+ recall = (n_matched / n_ref) if n_ref else 0.0
234
+ precision = (n_matched / n_pred) if n_pred else 0.0
235
+ f1 = (
236
+ (2 * precision * recall / (precision + recall))
237
+ if (precision + recall)
238
+ else 0.0
239
+ )
240
+
241
+ # Build recall_judgments for UI gold column.
242
+ full_by_ref = {m["reference_idx"]: m for m in full_pairs}
243
+ partial_by_ref: dict[int, list[dict]] = {}
244
+ for m in matches:
245
+ if m.get("match") == "partial":
246
+ partial_by_ref.setdefault(m["reference_idx"], []).append(m)
247
+
248
+ judgments = []
249
+ for i, g in enumerate(gold_ingredients, start=1):
250
+ desc = str(g.get("enabling_contribution") or g.get("description") or "")
251
+ ann = g.get("canonical_annotation") or {}
252
+ role = ""
253
+ if isinstance(ann.get("roles"), list) and ann["roles"]:
254
+ role = str(ann["roles"][0])
255
+ else:
256
+ role = str(ann.get("role") or "")
257
+ full = full_by_ref.get(i)
258
+ partials = partial_by_ref.get(i) or []
259
+ if full:
260
+ pred = predicted_ingredients[full["predicted_idx"] - 1]
261
+ judgments.append(
262
+ {
263
+ "reference_idx": i,
264
+ "reference_ingredient": desc,
265
+ "reference_role": role,
266
+ "covered": True,
267
+ "best_match_idx": full["predicted_idx"],
268
+ "reasoning": full.get("reasoning", ""),
269
+ "best_match": pred,
270
+ "partial_matches": [],
271
+ }
272
+ )
273
+ else:
274
+ judgments.append(
275
+ {
276
+ "reference_idx": i,
277
+ "reference_ingredient": desc,
278
+ "reference_role": role,
279
+ "covered": False,
280
+ "best_match_idx": None,
281
+ "reasoning": "",
282
+ "best_match": None,
283
+ "partial_matches": [
284
+ {
285
+ "reference_idx": p["reference_idx"],
286
+ "predicted_idx": p["predicted_idx"],
287
+ "match": "partial",
288
+ "reasoning": p.get("reasoning", ""),
289
+ }
290
+ for p in partials
291
+ ],
292
+ }
293
+ )
294
+
295
+ return {
296
+ "recall": recall,
297
+ "precision": precision,
298
+ "f1": f1,
299
+ "matched_pairs": full_pairs,
300
+ "partial_matches": [m for m in matches if m.get("match") == "partial"],
301
+ "recall_judgments": judgments,
302
+ "gold_ingredients": _gold_to_ui_ingredients(gold_ingredients),
303
+ "predicted_ingredients": predicted_ingredients,
304
+ "judge_model": judge_model,
305
+ }
hf_space/scipath_live/pipeline.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """High-level Live Run API used by Streamlit (self-contained)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import uuid
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+ PACKAGE_DIR = Path(__file__).resolve().parent
11
+ DATA_DIR = PACKAGE_DIR / "data"
12
+ EXAMPLE_CLAIM_PATH = DATA_DIR / "example_claim.json"
13
+ DEFAULT_RUNS_ROOT = PACKAGE_DIR.parent / "runs" / "system_live"
14
+
15
+ METHOD_CLI = {
16
+ "codeagent_parametric": "parametric",
17
+ "parametric": "parametric",
18
+ "codeagent_websearch_deep": "websearch_deep",
19
+ "websearch_deep": "websearch_deep",
20
+ }
21
+
22
+
23
+ def load_example_claim(path: Optional[Path] = None) -> dict[str, Any]:
24
+ claim_path = Path(path) if path else EXAMPLE_CLAIM_PATH
25
+ data = json.loads(claim_path.read_text(encoding="utf-8"))
26
+ if not isinstance(data, dict) or not data.get("claim"):
27
+ raise ValueError(f"Invalid example claim at {claim_path}")
28
+ return data
29
+
30
+
31
+ def new_job_dir(runs_root: Optional[Path] = None, method: str = "parametric") -> Path:
32
+ root = Path(runs_root) if runs_root else DEFAULT_RUNS_ROOT
33
+ job = root / f"job_{method}_{uuid.uuid4().hex[:10]}"
34
+ job.mkdir(parents=True, exist_ok=True)
35
+ return job
36
+
37
+
38
+ def run_method(
39
+ method_id: str,
40
+ *,
41
+ claim_record: Optional[dict[str, Any]] = None,
42
+ output_dir: Optional[Path] = None,
43
+ model_name: str = "gemini-3-flash-preview",
44
+ ) -> dict[str, Any]:
45
+ """Run parametric or websearch_deep on the demo example claim (or provided record)."""
46
+ method = METHOD_CLI.get(method_id)
47
+ if not method:
48
+ raise ValueError(f"Unknown method id: {method_id}")
49
+ from .agent import run_claim_to_dir
50
+
51
+ record = claim_record or load_example_claim()
52
+ out_dir = Path(output_dir) if output_dir else new_job_dir(method=method)
53
+ result = run_claim_to_dir(
54
+ claim=str(record["claim"]),
55
+ paper_id=str(record.get("paper_id") or "example"),
56
+ claim_idx=int(record.get("claim_idx") or 0),
57
+ paper_title=str(record.get("paper_title") or ""),
58
+ method=method,
59
+ output_dir=out_dir,
60
+ model_name=model_name,
61
+ )
62
+ result["output_dir"] = str(out_dir)
63
+ result["method_id"] = (
64
+ "codeagent_parametric" if method == "parametric" else "codeagent_websearch_deep"
65
+ )
66
+ return result
67
+
68
+
69
+ def evaluate_task_a(
70
+ *,
71
+ run_result: dict[str, Any],
72
+ claim_record: Optional[dict[str, Any]] = None,
73
+ judge_model: str = "gemini/gemini-3.1-pro-preview",
74
+ ) -> dict[str, Any]:
75
+ """Judge a finished live run against gold enabling contributions."""
76
+ from .judge import evaluate_prediction
77
+
78
+ record = claim_record or load_example_claim()
79
+ gold = list(record.get("enabling_contributions") or [])
80
+ predicted = list(run_result.get("ingredients") or [])
81
+ judged = evaluate_prediction(
82
+ claim=str(record.get("claim") or run_result.get("claim") or ""),
83
+ gold_ingredients=gold,
84
+ predicted_ingredients=predicted,
85
+ judge_model=judge_model,
86
+ )
87
+ payload = {
88
+ "paper_id": record.get("paper_id"),
89
+ "claim_idx": record.get("claim_idx"),
90
+ "paper_title": record.get("paper_title"),
91
+ "claim": record.get("claim"),
92
+ "method_id": run_result.get("method_id"),
93
+ "method": run_result.get("method"),
94
+ **judged,
95
+ }
96
+ out_dir = run_result.get("output_dir")
97
+ if out_dir:
98
+ path = Path(out_dir) / "task_a_judged.json"
99
+ path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
100
+ payload["judged_path"] = str(path)
101
+ return payload
hf_space/scipath_live/prompts.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SciPath Task A prompts and answer validation (demo-local copy)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ SCIPATH_ROLES = {
9
+ "CORE_METHOD",
10
+ "CONCEPTUAL_FRAMEWORK",
11
+ "DATA_SOURCE",
12
+ "TRAINING_DATA",
13
+ "MODEL_INITIALIZATION",
14
+ "EVALUATION_PROTOCOL",
15
+ "IMPLEMENTATION_TOOLING",
16
+ }
17
+
18
+ TOOLS_GUIDE_PARAMETRIC = ""
19
+
20
+ TOOLS_GUIDE_WEBSEARCH_DEEP = """
21
+ You have access to web search and must use it to get relevant information about the claim.
22
+ <tools>
23
+ - web_search_tool(): get a list of (url,summary of the webpage) from the WWW
24
+ - deep_web_search_tool(): retrieve content from a website given the url. The content is a dictionary with keys 'title', 'url' and 'content'.
25
+ </tools>
26
+ """
27
+
28
+ PROCEDURE_PARAMETRIC = """
29
+ <procedure>
30
+ Step 1. Analyze the discovery claim and identify necessary enabling contributions from your knowledge.
31
+ Step 2. Validate JSON with check_answer_format(), then return final_answer().
32
+ </procedure>
33
+ """
34
+
35
+ PROCEDURE_WEBSEARCH_DEEP = """
36
+ <procedure>
37
+ Step 1. Decompose the claim into a set of searchable short queries.
38
+ Step 2. Start from the first query. Use web_search_tool() to get relevant information about the claim.
39
+ Step 2.1. Based on the snippets, select the most relevant urls from search results and use deep_web_search_tool() on each url to get more relevant context.
40
+ Step 2.2. If the collected information is sufficient, proceed to Step 3. Otherwise, move to the next query and repeat Step 2.
41
+ Step 3. Summarize all the information retrieved from the web.
42
+ Step 4. Synthesize the minimal set of enabling contributions as JSON ingredients.
43
+ Step 5. Validate JSON with check_answer_format(), then return final_answer() with the ingredients object.
44
+ </procedure>
45
+ """
46
+
47
+ SCIPATH_OUTPUT_SPEC = """
48
+ <output>
49
+ Return a JSON object with exactly one top-level key: "ingredients".
50
+
51
+ Each ingredient must have:
52
+ - "description": concise functional description of the enabling contribution
53
+ - "role": one of CORE_METHOD, CONCEPTUAL_FRAMEWORK, DATA_SOURCE, TRAINING_DATA,
54
+ MODEL_INITIALIZATION, EVALUATION_PROTOCOL, IMPLEMENTATION_TOOLING
55
+ - "rationale": one sentence explaining why it is necessary
56
+
57
+ Example:
58
+ {
59
+ "ingredients": [
60
+ {
61
+ "description": "Pretrained transformer encoder for sentence embeddings",
62
+ "role": "MODEL_INITIALIZATION",
63
+ "rationale": "Needed to initialize the downstream model used in the discovery."
64
+ }
65
+ ]
66
+ }
67
+
68
+ Rules:
69
+ - Only include necessary enabling contributions for realizing the discovery claim.
70
+ - Exclude generic background, motivation, baselines, and comparison-only datasets.
71
+ - Use concrete strings; never copy placeholder values such as "...".
72
+ - Invoke check_answer_format() on your JSON, then call final_answer() with the same object.
73
+ </output>
74
+ """
75
+
76
+ SCIPATH_TASK_DESCRIPTION = """
77
+ You are forecasting enabling contributions required to realize a scientific discovery claim
78
+ (SciPath Task A, Setting 1: claim only).
79
+
80
+ An enabling contribution is a functional component whose absence would prevent the discovery
81
+ from being realized in its claimed form. It may come from prior work or be paper-specific
82
+ (e.g., a data pipeline or annotation procedure).
83
+
84
+ Use available search/retrieval tools when present to identify plausible prior work, then
85
+ synthesize the minimal set of enabling contributions.
86
+ """
87
+
88
+ SCIPATH_MUST_FOLLOW = """
89
+ <must follow conditions>
90
+ - Gather evidence with tools before producing the final ingredient list when tools are available.
91
+ - Separate evidence gathering from final synthesis across steps.
92
+ - NEVER return final_answer without validating via check_answer_format().
93
+ - Write only one code block per step when executing code.
94
+ </must follow conditions>
95
+ """
96
+
97
+ _METHOD_TOOLS = {
98
+ "parametric": TOOLS_GUIDE_PARAMETRIC,
99
+ "websearch_deep": TOOLS_GUIDE_WEBSEARCH_DEEP,
100
+ }
101
+
102
+ _METHOD_PROCEDURE = {
103
+ "parametric": PROCEDURE_PARAMETRIC,
104
+ "websearch_deep": PROCEDURE_WEBSEARCH_DEEP,
105
+ }
106
+
107
+
108
+ def get_scipath_controller_prompt(query: str, method: str) -> str:
109
+ tools = _METHOD_TOOLS.get(method, TOOLS_GUIDE_WEBSEARCH_DEEP)
110
+ procedure = _METHOD_PROCEDURE.get(method, PROCEDURE_WEBSEARCH_DEEP)
111
+ return f"""
112
+ {SCIPATH_TASK_DESCRIPTION}
113
+
114
+ {tools}
115
+
116
+ {procedure}
117
+
118
+ {SCIPATH_OUTPUT_SPEC}
119
+
120
+ {SCIPATH_MUST_FOLLOW}
121
+
122
+ Discovery claim:
123
+ {query}
124
+ """
125
+
126
+
127
+ def validate_scipath_answer(final_answer_dict: dict) -> tuple[bool, str | None, dict | None]:
128
+ if not isinstance(final_answer_dict, dict):
129
+ return False, "Final answer must be a JSON object.", None
130
+
131
+ ingredients = final_answer_dict.get("ingredients")
132
+ if not isinstance(ingredients, list) or not ingredients:
133
+ return False, "Field 'ingredients' must be a non-empty list.", None
134
+
135
+ placeholder_values = {"", "...", "…", "null", "none", "n/a"}
136
+ cleaned: list[dict[str, str]] = []
137
+ for idx, ingredient in enumerate(ingredients):
138
+ if not isinstance(ingredient, dict):
139
+ return False, f"Ingredient {idx} must be an object.", None
140
+ description = str(ingredient.get("description", "")).strip()
141
+ role = str(ingredient.get("role", "")).strip().upper()
142
+ rationale = str(ingredient.get("rationale", "")).strip()
143
+ if (
144
+ description.lower() in placeholder_values
145
+ or role.lower() in placeholder_values
146
+ or rationale.lower() in placeholder_values
147
+ ):
148
+ return False, f"Ingredient {idx} contains placeholder or empty fields.", None
149
+ if role not in SCIPATH_ROLES:
150
+ return False, f"Ingredient {idx} has invalid role {role!r}.", None
151
+ cleaned.append(
152
+ {"description": description, "role": role, "rationale": rationale}
153
+ )
154
+
155
+ return True, None, {"ingredients": cleaned}
156
+
157
+
158
+ def parse_ingredients_from_answer(answer: Any) -> list[dict]:
159
+ if isinstance(answer, str):
160
+ try:
161
+ answer = json.loads(answer)
162
+ except json.JSONDecodeError:
163
+ return []
164
+ if not isinstance(answer, dict):
165
+ return []
166
+ if isinstance(answer.get("ingredients"), list):
167
+ ok, _, validated = validate_scipath_answer(answer)
168
+ if ok and validated:
169
+ return validated["ingredients"]
170
+ return []
hf_space/scipath_live/tools.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tools for SciPath Live: format check, web search, deep page fetch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from io import BytesIO
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import requests
12
+ from pydantic import ValidationError
13
+ from smolagents import DuckDuckGoSearchTool, tool
14
+
15
+ from .prompts import validate_scipath_answer
16
+
17
+ WEBSEARCH_TOOL = DuckDuckGoSearchTool(max_results=5, rate_limit=2)
18
+
19
+ # Prefer demo-local browser cache over Cursor/temp PLAYWRIGHT_BROWSERS_PATH.
20
+ _DEMO_PLAYWRIGHT_DIR = Path(__file__).resolve().parents[2] / ".playwright"
21
+
22
+
23
+ def _ensure_playwright_browsers_path() -> Path:
24
+ _DEMO_PLAYWRIGHT_DIR.mkdir(parents=True, exist_ok=True)
25
+ os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(_DEMO_PLAYWRIGHT_DIR)
26
+ return _DEMO_PLAYWRIGHT_DIR
27
+
28
+
29
+ def _parse_search_results(search_output: str) -> list[dict[str, str]]:
30
+ evidence_items: list[dict[str, str]] = []
31
+ pattern = r"\[([^\]]+)\]\(([^\)]+)\)(.*?)(?=\[|$)"
32
+ for match in re.finditer(pattern, search_output or "", re.DOTALL):
33
+ title = match.group(1).strip()
34
+ url = match.group(2).strip()
35
+ content = match.group(3).strip()
36
+ summary = content.split("·", 1)[1].strip() if "·" in content else content
37
+ summary = " ".join(summary.split()).rstrip("…").strip()
38
+ if summary:
39
+ evidence_items.append(
40
+ {
41
+ "type": "web_search_result",
42
+ "source": url,
43
+ "summary": f"{title}. {summary}",
44
+ }
45
+ )
46
+ return evidence_items
47
+
48
+
49
+ def _get_pdf_content(url: str) -> dict[str, str]:
50
+ try:
51
+ import PyPDF2
52
+
53
+ response = requests.get(url, timeout=30)
54
+ response.raise_for_status()
55
+ pdf_reader = PyPDF2.PdfReader(BytesIO(response.content))
56
+ pages = []
57
+ for i, page in enumerate(pdf_reader.pages):
58
+ pages.append(page.extract_text() or "")
59
+ if i >= 29:
60
+ break
61
+ content = "\n\n".join(pages)
62
+ if "Abstract" in content:
63
+ content = content.split("Abstract", 1)[1]
64
+ title = "PDF Document"
65
+ if pdf_reader.metadata and getattr(pdf_reader.metadata, "title", None):
66
+ title = pdf_reader.metadata.title or title
67
+ return {"title": title, "url": url, "content": content}
68
+ except Exception as exc:
69
+ return {"title": "Error", "url": url, "content": f"Error extracting PDF: {exc}"}
70
+
71
+
72
+ def get_webpage(url: str) -> dict[str, str]:
73
+ """Fetch webpage or PDF content (Playwright for HTML, PyPDF2 for PDFs)."""
74
+ if "sciencedirect" in url:
75
+ return {
76
+ "title": "ScienceDirect Article",
77
+ "url": url,
78
+ "content": "Automatically fetching content from ScienceDirect is not supported.",
79
+ }
80
+ if url.lower().endswith(".pdf"):
81
+ return _get_pdf_content(url)
82
+
83
+ try:
84
+ from playwright.sync_api import sync_playwright
85
+ except Exception as exc:
86
+ return {
87
+ "title": "Error",
88
+ "url": url,
89
+ "content": f"Playwright unavailable: {exc}",
90
+ }
91
+
92
+ browsers_dir = _ensure_playwright_browsers_path()
93
+
94
+ def _launch_browser(playwright_api: Any) -> Any:
95
+ # Prefer installed Google Chrome (matches host arch) when available.
96
+ try:
97
+ return playwright_api.chromium.launch(channel="chrome", headless=True)
98
+ except Exception:
99
+ return playwright_api.chromium.launch(headless=True)
100
+
101
+ try:
102
+ with sync_playwright() as p:
103
+ browser = _launch_browser(p)
104
+ try:
105
+ page = browser.new_page()
106
+ page.goto(url, timeout=15000)
107
+ page.wait_for_load_state("domcontentloaded")
108
+ title = page.title() or ""
109
+ text = (
110
+ page.inner_text("body")
111
+ if page.locator("body").count()
112
+ else page.content()
113
+ )
114
+ # Cap content size for the agent context.
115
+ text = (text or "")[:40000]
116
+ return {"title": title, "url": page.url, "content": text}
117
+ except Exception:
118
+ return _get_pdf_content(url)
119
+ finally:
120
+ browser.close()
121
+ except Exception as exc:
122
+ msg = str(exc)
123
+ if "Executable doesn't exist" in msg or "browserType.launch" in msg.lower():
124
+ return {
125
+ "title": "Error",
126
+ "url": url,
127
+ "content": (
128
+ "Playwright Chromium is not installed for this demo. "
129
+ f"Run: bash scripts/setup_playwright.sh "
130
+ f"(PLAYWRIGHT_BROWSERS_PATH={browsers_dir})"
131
+ ),
132
+ }
133
+ return {"title": "Error", "url": url, "content": f"Playwright error: {exc}"}
134
+
135
+
136
+ @tool
137
+ def check_answer_format(final_answer_dict: dict) -> Any:
138
+ """Validate the final SciPath ingredients JSON format.
139
+
140
+ Args:
141
+ final_answer_dict: the final answer/output that is to be returned to the user
142
+ """
143
+ try:
144
+ ok, err, validated = validate_scipath_answer(final_answer_dict)
145
+ return ok, err, validated
146
+ except ValidationError as e:
147
+ return False, str(e), None
148
+ except Exception as e:
149
+ return False, f"Unexpected error: {e}", None
150
+
151
+
152
+ @tool
153
+ def web_search_tool(short_search_query: str) -> list[dict]:
154
+ """Perform a web search and return structured results.
155
+
156
+ Args:
157
+ short_search_query: A concise search query (typically 1-6 words)
158
+ """
159
+ search_results = WEBSEARCH_TOOL(short_search_query)
160
+ return _parse_search_results(search_results)
161
+
162
+
163
+ @tool
164
+ def deep_web_search_tool(url: str) -> dict:
165
+ """Fetch and return webpage/PDF content for a URL.
166
+
167
+ Args:
168
+ url: the url of a webpage
169
+ """
170
+ return get_webpage(url)
hf_space/streamlit_app.py ADDED
The diff for this file is too large to render. See raw diff
 
hf_space/streamlit_config.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from workflow_graph import STEP_COPY
2
+
3
+ EXAMPLES = {
4
+ "Paper 1": "https://arxiv.org/abs/2208.00329",
5
+ # AveriTeC demo hidden from the picker for now.
6
+ # "Paper 2 (AveriTeC)": "https://arxiv.org/abs/2505.17978",
7
+ "Paper 2": "https://arxiv.org/abs/2211.08788",
8
+ }
9
+
10
+ # System Run setting picker labels (match SciPaths Dev case-study report).
11
+ SYSTEM_RUN_METHODS = {
12
+ "Parametric - LLM Only": "codeagent_parametric",
13
+ "Websearch Deep - LLM + Web Search + Crawl": "codeagent_websearch_deep",
14
+ }
15
+
16
+ TAB_NAMES = [
17
+ "Run",
18
+ "Clusters",
19
+ "Decomposition",
20
+ ]
21
+
22
+ APP_VIEWS = [
23
+ "Annotation Process",
24
+ "System Run",
25
+ ]
26
+
27
+ # Left-nav items (mina-style icon + label rows). Icons use Streamlit Material symbols.
28
+ APP_NAV = [
29
+ {
30
+ "view": "Annotation Process",
31
+ "icon": ":material/account_tree:",
32
+ },
33
+ {
34
+ "view": "System Run",
35
+ "icon": ":material/play_circle:",
36
+ },
37
+ ]
38
+
39
+ METHOD_NOTES = {
40
+ "Pipeline scope": "Runs steps 0, 1, 2, 3, 4, 5, 6, and 8, then launches cluster-first two-pass annotation.",
41
+ "Input": "Accepts a single arXiv URL or arXiv ID.",
42
+ "Cluster-first annotation": "Uses all refined downstream USES/EXTENDS clusters to derive target contributions, then decomposes each target contribution separately.",
43
+ "Stopping rule": "If no valid downstream usage clusters remain after refinement and filtering, annotation is skipped.",
44
+ }
45
+
46
+ DISPLAY_STEPS = [1, 2, 3, 4, 5, 6, 7, 8]
47
+
48
+
49
+ def pipeline_steps_markdown() -> str:
50
+ return "\n".join(f"{idx}. {STEP_COPY[idx]}" for idx in DISPLAY_STEPS)
hf_space/system_live_runner.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit helpers for System Run → Live Run (self-contained scipath_live)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any, Callable, Optional
11
+
12
+ from scipath_live.pipeline import (
13
+ DEFAULT_RUNS_ROOT,
14
+ load_example_claim,
15
+ new_job_dir,
16
+ )
17
+
18
+ HF_SPACE = Path(__file__).resolve().parent
19
+ DEMO_ROOT = HF_SPACE.parent
20
+ CLI = HF_SPACE / "scipath_live" / "cli.py"
21
+ # Stable browser cache for this demo (avoids Cursor sandbox temp paths).
22
+ PLAYWRIGHT_BROWSERS_DIR = DEMO_ROOT / ".playwright"
23
+
24
+ METHOD_UI = [
25
+ {"id": "codeagent_parametric", "cli": "parametric", "label": "Parametric - LLM Only"},
26
+ {
27
+ "id": "codeagent_websearch_deep",
28
+ "cli": "websearch_deep",
29
+ "label": "Websearch Deep - LLM + Web Search + Crawl",
30
+ },
31
+ ]
32
+
33
+
34
+ def example_claim() -> dict[str, Any]:
35
+ return load_example_claim()
36
+
37
+
38
+ def _env_for_child() -> dict[str, str]:
39
+ env = dict(os.environ)
40
+ # Prefer demo-local .env if present.
41
+ for candidate in (HF_SPACE / ".env", HF_SPACE.parent / ".env"):
42
+ if not candidate.exists():
43
+ continue
44
+ try:
45
+ from dotenv import dotenv_values
46
+
47
+ for key, value in dotenv_values(candidate).items():
48
+ if value and key not in env:
49
+ env[key] = value
50
+ except Exception:
51
+ pass
52
+ google = (
53
+ env.get("GOOGLE_GENAI_API_KEY")
54
+ or env.get("GEMINI_API_KEY")
55
+ or env.get("GOOGLE_API_KEY")
56
+ or ""
57
+ ).strip()
58
+ if google:
59
+ env["GEMINI_API_KEY"] = google
60
+ env["GOOGLE_API_KEY"] = google
61
+ env["GOOGLE_GENAI_API_KEY"] = google
62
+ env["PYTHONPATH"] = (
63
+ f"{HF_SPACE}{os.pathsep}{env.get('PYTHONPATH', '')}".rstrip(os.pathsep)
64
+ )
65
+ # Stream agent prints line-by-line into the Live Run terminal.
66
+ env["PYTHONUNBUFFERED"] = "1"
67
+ env["PYTHONIOENCODING"] = "utf-8"
68
+ # Always use demo-local Playwright browsers (override Cursor sandbox cache).
69
+ PLAYWRIGHT_BROWSERS_DIR.mkdir(parents=True, exist_ok=True)
70
+ env["PLAYWRIGHT_BROWSERS_PATH"] = str(PLAYWRIGHT_BROWSERS_DIR)
71
+ return env
72
+
73
+
74
+ def _is_result_payload_line(line: str) -> bool:
75
+ text = (line or "").strip()
76
+ return text.startswith("{") and '"ok"' in text
77
+
78
+
79
+ def _append_trace_from_logs_jsonl(
80
+ job_dir: Path, log_callback: Optional[Callable[[str], None]]
81
+ ) -> None:
82
+ """Fallback: if stdout missed the trace, reconstruct from logs.jsonl."""
83
+ if not log_callback:
84
+ return
85
+ path = job_dir / "logs.jsonl"
86
+ if not path.exists():
87
+ return
88
+ try:
89
+ from scipath_live.agent import format_agent_trace
90
+
91
+ payload = json.loads(path.read_text(encoding="utf-8"))
92
+ steps = payload.get("steps") or []
93
+ for line in format_agent_trace(steps):
94
+ log_callback(line)
95
+ except Exception as exc:
96
+ log_callback(f"[warn] Could not load agent trace: {exc}")
97
+
98
+
99
+ def run_live_method(
100
+ method_id: str,
101
+ *,
102
+ log_callback: Optional[Callable[[str], None]] = None,
103
+ model_name: str = "gemini-3-flash-preview",
104
+ ) -> dict[str, Any]:
105
+ """Subprocess-run a method; stream stdout lines via log_callback."""
106
+ cli_method = next((m["cli"] for m in METHOD_UI if m["id"] == method_id), None)
107
+ if not cli_method:
108
+ raise ValueError(f"Unknown method: {method_id}")
109
+
110
+ job_dir = new_job_dir(DEFAULT_RUNS_ROOT, method=cli_method)
111
+ cmd = [
112
+ sys.executable,
113
+ str(CLI),
114
+ "run",
115
+ "--method",
116
+ cli_method,
117
+ "--output-dir",
118
+ str(job_dir),
119
+ "--model",
120
+ model_name,
121
+ ]
122
+ if log_callback:
123
+ log_callback(f"$ {' '.join(cmd)}")
124
+
125
+ proc = subprocess.Popen(
126
+ cmd,
127
+ cwd=str(HF_SPACE),
128
+ env=_env_for_child(),
129
+ stdout=subprocess.PIPE,
130
+ stderr=subprocess.STDOUT,
131
+ text=True,
132
+ bufsize=1,
133
+ )
134
+ assert proc.stdout is not None
135
+ last_json = ""
136
+ saw_trace = False
137
+ for line in proc.stdout:
138
+ line = line.rstrip("\n")
139
+ if not line:
140
+ continue
141
+ if _is_result_payload_line(line):
142
+ # Keep machine payload out of the human terminal.
143
+ last_json = line
144
+ continue
145
+ if "SciFy CodeAgent trace" in line:
146
+ saw_trace = True
147
+ if log_callback:
148
+ log_callback(line)
149
+ code = proc.wait()
150
+ if code != 0:
151
+ raise RuntimeError(f"Live run failed with exit code {code}")
152
+
153
+ if last_json:
154
+ payload = json.loads(last_json)
155
+ if not payload.get("ok"):
156
+ raise RuntimeError(payload.get("error") or "Live run failed")
157
+ result = payload["result"]
158
+ else:
159
+ # Fallback: read result.json written by the agent.
160
+ result_path = job_dir / "result.json"
161
+ if not result_path.exists():
162
+ raise RuntimeError("Live run produced no result.json")
163
+ result = json.loads(result_path.read_text(encoding="utf-8"))
164
+ result["output_dir"] = str(job_dir)
165
+ result["method_id"] = method_id
166
+
167
+ if not saw_trace:
168
+ _append_trace_from_logs_jsonl(job_dir, log_callback)
169
+
170
+ result.setdefault("output_dir", str(job_dir))
171
+ result.setdefault("method_id", method_id)
172
+ return result
173
+
174
+
175
+ def evaluate_live_run(
176
+ run_result: dict[str, Any],
177
+ *,
178
+ log_callback: Optional[Callable[[str], None]] = None,
179
+ judge_model: str = "gemini/gemini-3.1-pro-preview",
180
+ ) -> dict[str, Any]:
181
+ result_json = Path(run_result.get("output_dir") or ".") / "result.json"
182
+ if not result_json.exists():
183
+ # Write a minimal result file for the CLI.
184
+ result_json.parent.mkdir(parents=True, exist_ok=True)
185
+ result_json.write_text(
186
+ json.dumps(run_result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
187
+ )
188
+
189
+ cmd = [
190
+ sys.executable,
191
+ str(CLI),
192
+ "evaluate",
193
+ "--result-json",
194
+ str(result_json),
195
+ "--judge-model",
196
+ judge_model,
197
+ ]
198
+ if log_callback:
199
+ log_callback(f"$ {' '.join(cmd)}")
200
+
201
+ proc = subprocess.run(
202
+ cmd,
203
+ cwd=str(HF_SPACE),
204
+ env=_env_for_child(),
205
+ capture_output=True,
206
+ text=True,
207
+ )
208
+ if log_callback and proc.stdout:
209
+ for line in proc.stdout.splitlines():
210
+ log_callback(line)
211
+ if proc.returncode != 0:
212
+ err = (proc.stderr or proc.stdout or "").strip()
213
+ raise RuntimeError(err or f"Evaluate failed with exit code {proc.returncode}")
214
+
215
+ # Last JSON line is the payload.
216
+ payload = None
217
+ for line in reversed((proc.stdout or "").splitlines()):
218
+ if line.startswith("{"):
219
+ payload = json.loads(line)
220
+ break
221
+ if not payload or not payload.get("ok"):
222
+ raise RuntimeError((payload or {}).get("error") or "Evaluate produced no result")
223
+ return payload["judged"]
224
+
225
+
226
+ def live_result_to_case_card(run_result: dict[str, Any], judged: Optional[dict] = None) -> dict[str, Any]:
227
+ """Normalize live artifacts into the System Run case-card shape."""
228
+ method_id = run_result.get("method_id") or "codeagent_parametric"
229
+ method_label = next(
230
+ (m["label"] for m in METHOD_UI if m["id"] == method_id), method_id
231
+ )
232
+ predicted = []
233
+ source_pred = (judged or {}).get("predicted_ingredients") or run_result.get("ingredients") or []
234
+ for i, item in enumerate(source_pred, start=1):
235
+ if not isinstance(item, dict):
236
+ continue
237
+ predicted.append(
238
+ {
239
+ "idx": i,
240
+ "description": str(item.get("description") or ""),
241
+ "role": str(item.get("role") or ""),
242
+ "rationale": str(item.get("rationale") or ""),
243
+ }
244
+ )
245
+
246
+ if judged:
247
+ gold = []
248
+ for j in judged.get("recall_judgments") or []:
249
+ gold.append(
250
+ {
251
+ "description": str(j.get("reference_ingredient") or ""),
252
+ "role": str(j.get("reference_role") or ""),
253
+ "badge": (
254
+ "full"
255
+ if j.get("covered")
256
+ else ("partial" if j.get("partial_matches") else "miss")
257
+ ),
258
+ "match_notes": [],
259
+ "judge_note": str(j.get("reasoning") or ""),
260
+ "pair_pred_idx": j.get("best_match_idx"),
261
+ }
262
+ )
263
+ # Reconstruct match notes for partials
264
+ for pm in j.get("partial_matches") or []:
265
+ gold[-1]["match_notes"].append(
266
+ f"Partial ↔ pred #{pm.get('predicted_idx')}: {pm.get('reasoning', '')}"
267
+ )
268
+ if gold[-1]["badge"] == "miss":
269
+ gold[-1]["badge"] = "partial"
270
+ # Live Run is not framed as success/failure — always yellow (part).
271
+ f1 = float(judged.get("f1") or 0.0)
272
+ return {
273
+ "method_id": method_id,
274
+ "method_label": method_label.split(" - ")[0],
275
+ "case_key": "live",
276
+ "case_chip": f"{method_label.split(' - ')[0]} · Live",
277
+ "outcome_tone": "part",
278
+ "tools": (
279
+ ["check_answer_format"]
280
+ if "parametric" in method_id
281
+ else ["web_search_tool", "deep_web_search_tool", "check_answer_format"]
282
+ ),
283
+ "paper_id": judged.get("paper_id") or run_result.get("paper_id"),
284
+ "claim_idx": judged.get("claim_idx") or run_result.get("claim_idx"),
285
+ "paper_title": judged.get("paper_title") or run_result.get("paper_title"),
286
+ "claim": judged.get("claim") or run_result.get("claim"),
287
+ "recall": float(judged.get("recall") or 0.0),
288
+ "precision": float(judged.get("precision") or 0.0),
289
+ "f1": f1,
290
+ "gold_ingredients": gold,
291
+ "predicted_ingredients": predicted,
292
+ "tool_panel": {"kind": "none"},
293
+ }
294
+
295
+ # Pre-evaluate: predictions only.
296
+ return {
297
+ "method_id": method_id,
298
+ "method_label": method_label.split(" - ")[0],
299
+ "case_key": "featured",
300
+ "case_chip": f"{method_label.split(' - ')[0]} · Live (unevaluated)",
301
+ "outcome_tone": "part",
302
+ "tools": (
303
+ ["check_answer_format"]
304
+ if "parametric" in method_id
305
+ else ["web_search_tool", "deep_web_search_tool", "check_answer_format"]
306
+ ),
307
+ "paper_id": run_result.get("paper_id"),
308
+ "claim_idx": run_result.get("claim_idx"),
309
+ "paper_title": run_result.get("paper_title"),
310
+ "claim": run_result.get("claim"),
311
+ "recall": 0.0,
312
+ "precision": 0.0,
313
+ "f1": 0.0,
314
+ "gold_ingredients": [],
315
+ "predicted_ingredients": predicted,
316
+ "tool_panel": {"kind": "none"},
317
+ }
hf_space/system_run_cases/case_study_analysis.json ADDED
@@ -0,0 +1,829 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "SciPaths Dev Case Studies across CodeAgent settings",
3
+ "analysis_model": "gemini/gemini-3.1-pro-preview",
4
+ "settings": {
5
+ "codeagent_parametric": {
6
+ "tools": [
7
+ "check_answer_format"
8
+ ],
9
+ "summary_meta": {
10
+ "source": "gold",
11
+ "gold_file": "/Users/dingzifeng/SciFy-UMBC/dryrun/ForecastingDiscoveryPathways-7FCC/archive/dev.json",
12
+ "gold_claim_field": "rewritten_capability",
13
+ "setting": 1,
14
+ "generator_model": "gemini-3-flash-preview",
15
+ "generator_backend": "codeagent",
16
+ "codeagent_method": "parametric",
17
+ "n_claims": 50,
18
+ "n_total_claims": 50,
19
+ "n_errors": 0,
20
+ "mean_n_predicted": 4.56
21
+ },
22
+ "success": {
23
+ "paper_id": "neurips-2023-poster-2075",
24
+ "claim_idx": 0,
25
+ "paper_title": "Pick-a-Pic: An Open Dataset of User Preferences for Text-to-Image Generation",
26
+ "claim": "Tool: A human-preference prompt-conditioned image scorer, enabling automatic ranking of text-to-image model outputs",
27
+ "recall": 1.0,
28
+ "precision": 0.75,
29
+ "f1": 0.857,
30
+ "predicted_ingredients": [
31
+ {
32
+ "description": "Large-scale dataset of human pairwise preferences for generated images",
33
+ "role": "TRAINING_DATA",
34
+ "rationale": "Necessary to provide the ground-truth human judgments required to train the scorer for alignment with subjective preferences."
35
+ },
36
+ {
37
+ "description": "Pre-trained vision-language model backbone (e.g., CLIP)",
38
+ "role": "MODEL_INITIALIZATION",
39
+ "rationale": "Provides the foundational cross-modal representations needed to assess the semantic relationship between text prompts and visual content."
40
+ },
41
+ {
42
+ "description": "Prompt-conditioned reward modeling architecture and objective",
43
+ "role": "CORE_METHOD",
44
+ "rationale": "Defines the specific neural network structure and loss function used to map image-text pairs to a scalar preference score."
45
+ },
46
+ {
47
+ "description": "Human-preference alignment evaluation benchmark",
48
+ "role": "EVALUATION_PROTOCOL",
49
+ "rationale": "Required to validate that the automated scorer's rankings accurately reflect human preferences on unseen samples."
50
+ }
51
+ ],
52
+ "recall_judgments": [
53
+ {
54
+ "reference_idx": 1,
55
+ "reference_ingredient": "Pre-trained vision-language encoder for prompt-conditioned image scoring",
56
+ "reference_role": "MODEL_INITIALIZATION",
57
+ "covered": true,
58
+ "best_match_idx": 2,
59
+ "reasoning": "Both describe the use of a pre-trained vision-language model (like CLIP) to initialize the cross-modal representations.",
60
+ "best_match": {
61
+ "description": "Pre-trained vision-language model backbone (e.g., CLIP)",
62
+ "role": "MODEL_INITIALIZATION",
63
+ "rationale": "Provides the foundational cross-modal representations needed to assess the semantic relationship between text prompts and visual content."
64
+ },
65
+ "partial_matches": []
66
+ },
67
+ {
68
+ "reference_idx": 2,
69
+ "reference_ingredient": "Large-scale human-preference dataset over text-to-image generations",
70
+ "reference_role": "TRAINING_DATA",
71
+ "covered": true,
72
+ "best_match_idx": 1,
73
+ "reasoning": "Both explicitly identify the large-scale dataset of pairwise human preferences over generated images as the training data.",
74
+ "best_match": {
75
+ "description": "Large-scale dataset of human pairwise preferences for generated images",
76
+ "role": "TRAINING_DATA",
77
+ "rationale": "Necessary to provide the ground-truth human judgments required to train the scorer for alignment with subjective preferences."
78
+ },
79
+ "partial_matches": []
80
+ },
81
+ {
82
+ "reference_idx": 3,
83
+ "reference_ingredient": "Pairwise preference-learning objective for reward-style image ranking",
84
+ "reference_role": "CORE_METHOD",
85
+ "covered": true,
86
+ "best_match_idx": 3,
87
+ "reasoning": "Both specify the reward-modeling/preference-learning objective required to train the scoring function to reflect human preferences.",
88
+ "best_match": {
89
+ "description": "Prompt-conditioned reward modeling architecture and objective",
90
+ "role": "CORE_METHOD",
91
+ "rationale": "Defines the specific neural network structure and loss function used to map image-text pairs to a scalar preference score."
92
+ },
93
+ "partial_matches": []
94
+ }
95
+ ],
96
+ "matched_pairs": [
97
+ {
98
+ "reference_idx": 1,
99
+ "predicted_idx": 2,
100
+ "match": "full",
101
+ "reasoning": "Both describe the use of a pre-trained vision-language model (like CLIP) to initialize the cross-modal representations."
102
+ },
103
+ {
104
+ "reference_idx": 2,
105
+ "predicted_idx": 1,
106
+ "match": "full",
107
+ "reasoning": "Both explicitly identify the large-scale dataset of pairwise human preferences over generated images as the training data."
108
+ },
109
+ {
110
+ "reference_idx": 3,
111
+ "predicted_idx": 3,
112
+ "match": "full",
113
+ "reasoning": "Both specify the reward-modeling/preference-learning objective required to train the scoring function to reflect human preferences."
114
+ }
115
+ ],
116
+ "partial_matches": [],
117
+ "log_line": 19,
118
+ "n_steps": 2,
119
+ "tool_summary": {
120
+ "searches": [],
121
+ "retrieves": [],
122
+ "deeps": []
123
+ }
124
+ },
125
+ "failure": {
126
+ "paper_id": "neurips-2024-poster-219",
127
+ "claim_idx": 0,
128
+ "paper_title": "RGFN: Synthesizable Molecular Generation Using GFlowNets",
129
+ "claim": "Method: A generative framework for synthesizable molecular design, enabling generation of molecular candidates together with feasible synthesis pathways.",
130
+ "recall": 0.0,
131
+ "precision": 0.0,
132
+ "f1": 0.0,
133
+ "predicted_ingredients": [
134
+ {
135
+ "description": "Markov Decision Process (MDP) formulation of molecular synthesis",
136
+ "role": "CONCEPTUAL_FRAMEWORK",
137
+ "rationale": "Frames the generation of molecules as a sequence of discrete synthetic steps, ensuring the output is synthesizable by construction."
138
+ },
139
+ {
140
+ "description": "Library of reaction templates extracted from reaction databases (e.g., USPTO)",
141
+ "role": "DATA_SOURCE",
142
+ "rationale": "Provides the set of valid chemical transformations that the generative model can use to build molecules."
143
+ },
144
+ {
145
+ "description": "Database of commercially available building blocks",
146
+ "role": "DATA_SOURCE",
147
+ "rationale": "Serves as the required starting materials for any proposed synthesis pathway."
148
+ },
149
+ {
150
+ "description": "Neural policy network for reaction and building block selection",
151
+ "role": "CORE_METHOD",
152
+ "rationale": "The primary engine that learns to navigate the chemical space by selecting optimal synthetic actions."
153
+ },
154
+ {
155
+ "description": "Synthetic feasibility and property evaluation protocol",
156
+ "role": "EVALUATION_PROTOCOL",
157
+ "rationale": "Used to validate that the generated molecules meet the desired chemical properties and that the pathways are realistic."
158
+ }
159
+ ],
160
+ "recall_judgments": [
161
+ {
162
+ "reference_idx": 1,
163
+ "reference_ingredient": "Generative Flow Network (GFlowNet) framework trained with trajectory balance",
164
+ "reference_role": "CORE_METHOD",
165
+ "covered": false,
166
+ "best_match_idx": null,
167
+ "reasoning": "",
168
+ "best_match": null,
169
+ "partial_matches": [
170
+ {
171
+ "reference_idx": 1,
172
+ "predicted_idx": 1,
173
+ "match": "partial",
174
+ "reasoning": "The predicted item captures the sequential MDP formulation underlying the generation process but is too broad and misses the specific GFlowNet framework and trajectory balance."
175
+ }
176
+ ]
177
+ },
178
+ {
179
+ "reference_idx": 2,
180
+ "reference_ingredient": "Graph transformer architecture for state and action representation",
181
+ "reference_role": "CORE_METHOD",
182
+ "covered": false,
183
+ "best_match_idx": null,
184
+ "reasoning": "",
185
+ "best_match": null,
186
+ "partial_matches": [
187
+ {
188
+ "reference_idx": 2,
189
+ "predicted_idx": 4,
190
+ "match": "partial",
191
+ "reasoning": "The predicted item identifies the neural policy network used to navigate the space but lacks the specific graph transformer architecture required by the reference."
192
+ }
193
+ ]
194
+ },
195
+ {
196
+ "reference_idx": 3,
197
+ "reference_ingredient": "Predefined action space of robust chemical reaction templates and molecular building blocks",
198
+ "reference_role": "CONCEPTUAL_FRAMEWORK",
199
+ "covered": false,
200
+ "best_match_idx": null,
201
+ "reasoning": "",
202
+ "best_match": null,
203
+ "partial_matches": [
204
+ {
205
+ "reference_idx": 3,
206
+ "predicted_idx": 2,
207
+ "match": "partial",
208
+ "reasoning": "The predicted item captures the reaction templates portion of the reference's predefined action space but omits the molecular building blocks."
209
+ },
210
+ {
211
+ "reference_idx": 3,
212
+ "predicted_idx": 3,
213
+ "match": "partial",
214
+ "reasoning": "The predicted item captures the molecular building blocks portion of the reference's action space but omits the reaction templates."
215
+ }
216
+ ]
217
+ },
218
+ {
219
+ "reference_idx": 4,
220
+ "reference_ingredient": "Fingerprint-based action embedding scheme for scalable fragment selection",
221
+ "reference_role": "CORE_METHOD",
222
+ "covered": false,
223
+ "best_match_idx": null,
224
+ "reasoning": "",
225
+ "best_match": null,
226
+ "partial_matches": []
227
+ },
228
+ {
229
+ "reference_idx": 5,
230
+ "reference_ingredient": "Training loss of the model",
231
+ "reference_role": "CORE_METHOD",
232
+ "covered": false,
233
+ "best_match_idx": null,
234
+ "reasoning": "",
235
+ "best_match": null,
236
+ "partial_matches": []
237
+ }
238
+ ],
239
+ "matched_pairs": [],
240
+ "partial_matches": [
241
+ {
242
+ "reference_idx": 1,
243
+ "predicted_idx": 1,
244
+ "match": "partial",
245
+ "reasoning": "The predicted item captures the sequential MDP formulation underlying the generation process but is too broad and misses the specific GFlowNet framework and trajectory balance."
246
+ },
247
+ {
248
+ "reference_idx": 2,
249
+ "predicted_idx": 4,
250
+ "match": "partial",
251
+ "reasoning": "The predicted item identifies the neural policy network used to navigate the space but lacks the specific graph transformer architecture required by the reference."
252
+ },
253
+ {
254
+ "reference_idx": 3,
255
+ "predicted_idx": 2,
256
+ "match": "partial",
257
+ "reasoning": "The predicted item captures the reaction templates portion of the reference's predefined action space but omits the molecular building blocks."
258
+ },
259
+ {
260
+ "reference_idx": 3,
261
+ "predicted_idx": 3,
262
+ "match": "partial",
263
+ "reasoning": "The predicted item captures the molecular building blocks portion of the reference's action space but omits the reaction templates."
264
+ }
265
+ ],
266
+ "log_line": 38,
267
+ "n_steps": 2,
268
+ "tool_summary": {
269
+ "searches": [],
270
+ "retrieves": [],
271
+ "deeps": []
272
+ }
273
+ },
274
+ "gemini_analysis": {
275
+ "tooling_clarification": "No external search or retrieval tools were provided for evidence gathering. The agent relied entirely on its internal parametric memory, using tools only to verify the output format.",
276
+ "success_why": [
277
+ "The model successfully recalled the exact architecture and training data for 'Pick-a-Pic', a prominent 2023 text-to-image alignment dataset.",
278
+ "It accurately identified the CLIP backbone, the large-scale human preference dataset, and the reward modeling objective without needing external grounding."
279
+ ],
280
+ "success_tool_role": [
281
+ "No evidence tools were used.",
282
+ "Success was driven entirely by the LLM's strong parametric knowledge of highly cited, recent (2023) multimodal AI research."
283
+ ],
284
+ "failure_diagnosis": "Faced with a highly specific 2024 molecular design paper (RGFN), the model hallucinated a generic, plausible pipeline for synthesizable molecular generation (MDP, USPTO templates, neural policy network) but completely missed the actual GFlowNet methodology.",
285
+ "failure_error_bullets": [
286
+ "[Missed core method] Failed to identify the GFlowNet framework and graph transformer architectures.",
287
+ "[Wrong specificity] Predicted a generic Markov Decision Process (MDP) instead of the specific GFlowNet trained with trajectory balance.",
288
+ "[Missing data/source] Missed the fingerprint-based action embedding scheme."
289
+ ],
290
+ "failure_what_needed": [
291
+ "External retrieval of the paper's abstract to ground the generic claim ('generative framework for synthesizable molecular design') to the specific RGFN/GFlowNet methodology."
292
+ ]
293
+ }
294
+ },
295
+ "codeagent_retrieval_plus_websearch": {
296
+ "tools": [
297
+ "retrieve_documents",
298
+ "web_search_tool",
299
+ "check_answer_format"
300
+ ],
301
+ "summary_meta": {
302
+ "source": "gold",
303
+ "gold_file": "/Users/dingzifeng/SciFy-UMBC/dryrun/ForecastingDiscoveryPathways-7FCC/archive/dev.json",
304
+ "gold_claim_field": "rewritten_capability",
305
+ "setting": 1,
306
+ "generator_model": "gemini-3-flash-preview",
307
+ "generator_backend": "codeagent",
308
+ "codeagent_method": "retrieval_plus_websearch",
309
+ "n_claims": 50,
310
+ "n_total_claims": 50,
311
+ "n_errors": 0,
312
+ "mean_n_predicted": 4.7
313
+ },
314
+ "case": {
315
+ "paper_id": "2023.acl-short.123",
316
+ "claim_idx": 0,
317
+ "paper_title": "An Embarrassingly Easy but Strong Baseline for Nested Named Entity Recognition",
318
+ "claim": "Tool: A standardized preprocessing and split-construction workflow for nested NER benchmarks, enabling fair comparison across models.",
319
+ "recall": 0.5,
320
+ "precision": 0.2,
321
+ "f1": 0.286,
322
+ "predicted_ingredients": [
323
+ {
324
+ "description": "Original nested NER datasets (e.g., GENIA, ACE 2004, ACE 2005)",
325
+ "role": "DATA_SOURCE",
326
+ "rationale": "These provide the raw annotated text data that the workflow standardizes for benchmarking purposes."
327
+ },
328
+ {
329
+ "description": "Unified data representation format for nested entities",
330
+ "role": "CONCEPTUAL_FRAMEWORK",
331
+ "rationale": "A consistent schema is necessary to represent overlapping spans across diverse datasets in a single format."
332
+ },
333
+ {
334
+ "description": "Automated preprocessing pipeline for tokenization and span alignment",
335
+ "role": "CORE_METHOD",
336
+ "rationale": "Ensures that all datasets are processed using identical linguistic rules to prevent bias from different tokenizers."
337
+ },
338
+ {
339
+ "description": "Standardized split-construction protocol (e.g., Lu and Roth splits)",
340
+ "role": "EVALUATION_PROTOCOL",
341
+ "rationale": "Guarantees that models are compared on identical subsets of the data to maintain experimental fairness."
342
+ },
343
+ {
344
+ "description": "Nested span evaluation metric implementation",
345
+ "role": "EVALUATION_PROTOCOL",
346
+ "rationale": "Required to accurately and consistently measure performance on overlapping entity structures across different models."
347
+ }
348
+ ],
349
+ "recall_judgments": [
350
+ {
351
+ "reference_idx": 1,
352
+ "reference_ingredient": "Raw nested NER corpora",
353
+ "reference_role": "DATA_SOURCE",
354
+ "covered": true,
355
+ "best_match_idx": 1,
356
+ "reasoning": "Both identify the original nested NER corpora (such as ACE2004, ACE2005, and GENIA) that provide the raw annotated text data for standardization.",
357
+ "best_match": {
358
+ "description": "Original nested NER datasets (e.g., GENIA, ACE 2004, ACE 2005)",
359
+ "role": "DATA_SOURCE",
360
+ "rationale": "These provide the raw annotated text data that the workflow standardizes for benchmarking purposes."
361
+ },
362
+ "partial_matches": []
363
+ },
364
+ {
365
+ "reference_idx": 2,
366
+ "reference_ingredient": "Tokenization and annotation correction heuristics",
367
+ "reference_role": "IMPLEMENTATION_TOOLING",
368
+ "covered": false,
369
+ "best_match_idx": null,
370
+ "reasoning": "",
371
+ "best_match": null,
372
+ "partial_matches": [
373
+ {
374
+ "reference_idx": 2,
375
+ "predicted_idx": 3,
376
+ "match": "partial",
377
+ "reasoning": "Both refer to the tokenization and span alignment process, but the reference specifies the underlying heuristics to fix tokenization and annotation conflicts, whereas the prediction describes the broader automated pipeline."
378
+ }
379
+ ]
380
+ }
381
+ ],
382
+ "retrieved_doc_titles": [
383
+ "Statistical Learning for Accurate and Interpretable Battery Lifetime Prediction",
384
+ "Accurate Performance Characterization, Reporting, and Benchmarking for Indoor Photovoltaics",
385
+ "Efficient implicit integration for finite-strain viscoplasticity with a nested multiplicative split",
386
+ "Prediction of charge separation in GaAs/AlAs cylindrical nanostructures",
387
+ "Effect of the structure of lead iodine perovskites on the photovoltaic efficiencies",
388
+ "The high strain-rate behaviour of three molecular weights of polyethylene examined with a magnesium alloy split-Hopkinson pressure bar",
389
+ "Landau Theory of Charge and Spin Ordering in the Nickelates",
390
+ "Excitonic effects in two-dimensional semiconductors: Path integral Monte Carlo approach"
391
+ ],
392
+ "gemini_analysis": {
393
+ "model": "gemini/gemini-3.1-pro-preview",
394
+ "focus": "performance when retrieval returned docs",
395
+ "headline": "Even with web search plus returned retrieval docs, the pathway stayed weakly grounded: retrieval was off-domain, so the model leaned on parametric knowledge and over-generated (F1 0.29).",
396
+ "what_was_retrieved": [
397
+ "Web search was used in most steps (alongside retrieval early, then alone later), with queries about nested NER preprocessing / benchmark standardization.",
398
+ "Retrieval returned documents from unrelated domains (materials science, physics, battery lifetime), likely via polysemous terms such as \"nested\", \"split\", and \"benchmarking\".",
399
+ "No retrieved documents were about NLP / Named Entity Recognition."
400
+ ],
401
+ "effect_on_prediction": [
402
+ "With off-domain retrieval and no strong grounding from tools, the model fell back on parametric knowledge of nested NER workflows.",
403
+ "That fallback recovered some recall (e.g., raw nested NER corpora / GENIA & ACE-style datasets) and a partial match on tokenization preprocessing.",
404
+ "Without relevant tool text to constrain the pathway, it also over-generated extra workflow pieces (unified format, evaluation metric), hurting precision (P=0.2)."
405
+ ],
406
+ "error_taxonomy_bullets": [
407
+ "[Domain Mismatch in Retrieval] Returned abstracts matched generic keywords to the wrong scientific fields, so retrieval did not surface the gold ingredients.",
408
+ "[Web search under-used for grounding] Despite repeated nested-NER web queries, the final ingredients still look like a generic preprocessing pipeline rather than the paper-specific tokenization/annotation heuristics.",
409
+ "[Parametric Reliance + Over-generation] The model filled gaps from memory and added plausible but non-gold steps, lowering precision."
410
+ ],
411
+ "takeaway": "Web search ran throughout the trajectory, including after documents were returned. Retrieval returned materials/physics papers rather than nested-NER sources, so neither tool provided the specific gold ingredients.",
412
+ "web_search_also_used": true,
413
+ "web_search_queries_example": [
414
+ "standardized preprocessing nested NER benchmarks workflow",
415
+ "nested NER benchmarks fair comparison tool",
416
+ "nested NER dataset preprocessing standardization",
417
+ "standardizing nested NER benchmarks"
418
+ ]
419
+ }
420
+ }
421
+ },
422
+ "codeagent_websearch_deep": {
423
+ "tools": [
424
+ "web_search_tool",
425
+ "deep_web_search_tool",
426
+ "check_answer_format"
427
+ ],
428
+ "summary_meta": {
429
+ "generator_model": "gemini-3-flash-preview",
430
+ "generator_backend": "codeagent",
431
+ "codeagent_method": "websearch_deep",
432
+ "mean_f1": 0.318,
433
+ "n_claims": 50
434
+ },
435
+ "success": {
436
+ "paper_id": "neurips-2025-spotlight-472",
437
+ "claim_idx": 1,
438
+ "paper_title": "ShapeLLM-Omni: A Native Multimodal LLM for 3D Generation and Understanding",
439
+ "claim": "Resource: A native 3D multimodal language model, enabling unified text, image, and 3D generation, understanding, and language-guided editing.",
440
+ "recall": 1.0,
441
+ "precision": 1.0,
442
+ "f1": 1.0,
443
+ "predicted_ingredients": [
444
+ {
445
+ "description": "3D Vector-Quantized Variational Autoencoder (VQVAE)",
446
+ "role": "CORE_METHOD",
447
+ "rationale": "Necessary to map 3D objects into a discrete latent space of 3D-aware tokens that the LLM can natively process and generate."
448
+ },
449
+ {
450
+ "description": "3D-Alpaca Dataset",
451
+ "role": "TRAINING_DATA",
452
+ "rationale": "A large-scale instruction-tuning dataset required to train the model on unified 3D generation, understanding, and editing tasks."
453
+ },
454
+ {
455
+ "description": "Qwen-2.5-vl-7B-Instruct",
456
+ "role": "MODEL_INITIALIZATION",
457
+ "rationale": "Provides the foundational multimodal (text and image) capabilities that are extended to include 3D functionality."
458
+ },
459
+ {
460
+ "description": "Unified Token-based Multimodal Architecture",
461
+ "role": "CONCEPTUAL_FRAMEWORK",
462
+ "rationale": "Enables the model to treat 3D tokens as a native modality alongside text and images for interleaved generation and understanding."
463
+ }
464
+ ],
465
+ "recall_judgments": [
466
+ {
467
+ "reference_idx": 1,
468
+ "reference_ingredient": "Pretrained multimodal vision-language backbone",
469
+ "reference_role": "MODEL_INITIALIZATION",
470
+ "covered": true,
471
+ "best_match_idx": 3,
472
+ "reasoning": "Both identify the pretrained Qwen-2.5-VL vision-language model used as the foundational backbone for initialization.",
473
+ "best_match": {
474
+ "description": "Qwen-2.5-vl-7B-Instruct",
475
+ "role": "MODEL_INITIALIZATION",
476
+ "rationale": "Provides the foundational multimodal (text and image) capabilities that are extended to include 3D functionality."
477
+ },
478
+ "partial_matches": []
479
+ },
480
+ {
481
+ "reference_idx": 2,
482
+ "reference_ingredient": "Discrete 3D tokenization through a 3D VQVAE",
483
+ "reference_role": "CORE_METHOD",
484
+ "covered": true,
485
+ "best_match_idx": 1,
486
+ "reasoning": "Both describe the use of a 3D VQVAE to convert 3D objects into discrete tokens that the model can process natively.",
487
+ "best_match": {
488
+ "description": "3D Vector-Quantized Variational Autoencoder (VQVAE)",
489
+ "role": "CORE_METHOD",
490
+ "rationale": "Necessary to map 3D objects into a discrete latent space of 3D-aware tokens that the LLM can natively process and generate."
491
+ },
492
+ "partial_matches": []
493
+ },
494
+ {
495
+ "reference_idx": 3,
496
+ "reference_ingredient": "Fully autoregressive early-fusion next-token modeling over text, image-conditioned inputs, and 3D tokens",
497
+ "reference_role": "CONCEPTUAL_FRAMEWORK, CORE_METHOD",
498
+ "covered": true,
499
+ "best_match_idx": 4,
500
+ "reasoning": "Both refer to the unified, token-based conceptual framework that treats 3D, text, and image tokens in a single interleaved architecture.",
501
+ "best_match": {
502
+ "description": "Unified Token-based Multimodal Architecture",
503
+ "role": "CONCEPTUAL_FRAMEWORK",
504
+ "rationale": "Enables the model to treat 3D tokens as a native modality alongside text and images for interleaved generation and understanding."
505
+ },
506
+ "partial_matches": []
507
+ },
508
+ {
509
+ "reference_idx": 4,
510
+ "reference_ingredient": "3D instruction-tuning corpus",
511
+ "reference_role": "TRAINING_DATA",
512
+ "covered": true,
513
+ "best_match_idx": 2,
514
+ "reasoning": "Both specify the 3D-Alpaca instruction-tuning dataset used to train and adapt the model for 3D tasks.",
515
+ "best_match": {
516
+ "description": "3D-Alpaca Dataset",
517
+ "role": "TRAINING_DATA",
518
+ "rationale": "A large-scale instruction-tuning dataset required to train the model on unified 3D generation, understanding, and editing tasks."
519
+ },
520
+ "partial_matches": []
521
+ }
522
+ ],
523
+ "matched_pairs": [
524
+ {
525
+ "reference_idx": 1,
526
+ "predicted_idx": 3,
527
+ "match": "full",
528
+ "reasoning": "Both identify the pretrained Qwen-2.5-VL vision-language model used as the foundational backbone for initialization."
529
+ },
530
+ {
531
+ "reference_idx": 2,
532
+ "predicted_idx": 1,
533
+ "match": "full",
534
+ "reasoning": "Both describe the use of a 3D VQVAE to convert 3D objects into discrete tokens that the model can process natively."
535
+ },
536
+ {
537
+ "reference_idx": 3,
538
+ "predicted_idx": 4,
539
+ "match": "full",
540
+ "reasoning": "Both refer to the unified, token-based conceptual framework that treats 3D, text, and image tokens in a single interleaved architecture."
541
+ },
542
+ {
543
+ "reference_idx": 4,
544
+ "predicted_idx": 2,
545
+ "match": "full",
546
+ "reasoning": "Both specify the 3D-Alpaca instruction-tuning dataset used to train and adapt the model for 3D tasks."
547
+ }
548
+ ]
549
+ },
550
+ "failure": {
551
+ "paper_id": "neurips-2023-poster-1534",
552
+ "claim_idx": 0,
553
+ "paper_title": "Continuous-Time Functional Diffusion Processes",
554
+ "claim": "Method: A diffusion-based generative framework for continuous function spaces, enabling modeling of continuous and irregularly sampled data.",
555
+ "recall": 0.0,
556
+ "precision": 0.0,
557
+ "f1": 0.0,
558
+ "predicted_ingredients": [
559
+ {
560
+ "description": "Denoising diffusion probabilistic modeling adapted for finite-dimensional marginals of functions",
561
+ "role": "CORE_METHOD",
562
+ "rationale": "Provides the generative mechanism to sample from functional distributions by reversing a noise process applied to function values at arbitrary locations."
563
+ },
564
+ {
565
+ "description": "Permutation-invariant attention-based architecture",
566
+ "role": "IMPLEMENTATION_TOOLING",
567
+ "rationale": "Ensures the model can process sets of input-output pairs of varying sizes while satisfying the exchangeability property required for stochastic processes."
568
+ },
569
+ {
570
+ "description": "Conceptual framework of exchangeability and consistency for stochastic processes",
571
+ "role": "CONCEPTUAL_FRAMEWORK",
572
+ "rationale": "Provides the theoretical foundation for defining a valid distribution over an infinite-dimensional function space via its finite marginals."
573
+ },
574
+ {
575
+ "description": "Coordinate-based representation of data observations",
576
+ "role": "CONCEPTUAL_FRAMEWORK",
577
+ "rationale": "Enables the model to handle irregularly sampled data by treating observations as evaluations of a function at specific continuous coordinates."
578
+ },
579
+ {
580
+ "description": "Score-based or denoising objective defined over function values at given coordinates",
581
+ "role": "CORE_METHOD",
582
+ "rationale": "Allows the model to learn the underlying structure of the function space by predicting noise or scores at arbitrary input points."
583
+ }
584
+ ],
585
+ "recall_judgments": [
586
+ {
587
+ "reference_idx": 1,
588
+ "reference_ingredient": "Score-based generative modeling with continuous-time stochastic differential equations",
589
+ "reference_role": "CONCEPTUAL_FRAMEWORK",
590
+ "covered": false,
591
+ "best_match_idx": null,
592
+ "reasoning": "",
593
+ "best_match": null,
594
+ "partial_matches": [
595
+ {
596
+ "reference_idx": 1,
597
+ "predicted_idx": 1,
598
+ "match": "partial",
599
+ "reasoning": "Both identify the core diffusion generative mechanism, but the reference specifies a continuous-time SDE formulation while the prediction focuses on adapting diffusion for finite marginals."
600
+ },
601
+ {
602
+ "reference_idx": 1,
603
+ "predicted_idx": 5,
604
+ "match": "partial",
605
+ "reasoning": "Both highlight the score-based or denoising formulation, though the prediction restricts it to function values at given coordinates."
606
+ }
607
+ ]
608
+ },
609
+ {
610
+ "reference_idx": 2,
611
+ "reference_ingredient": "Infinite-dimensional stochastic differential equation framework for forward and reverse diffusion in Hilbert spaces",
612
+ "reference_role": "CONCEPTUAL_FRAMEWORK",
613
+ "covered": false,
614
+ "best_match_idx": null,
615
+ "reasoning": "",
616
+ "best_match": null,
617
+ "partial_matches": [
618
+ {
619
+ "reference_idx": 2,
620
+ "predicted_idx": 3,
621
+ "match": "partial",
622
+ "reasoning": "Both provide the theoretical foundation for defining distributions over infinite-dimensional function spaces, but use different mathematical frameworks (Hilbert space SDEs versus exchangeability/consistency)."
623
+ }
624
+ ]
625
+ },
626
+ {
627
+ "reference_idx": 3,
628
+ "reference_ingredient": "Countable-evaluation formulation that connects continuous functions to discrete observations for practical learning and inference",
629
+ "reference_role": "CORE_METHOD",
630
+ "covered": false,
631
+ "best_match_idx": null,
632
+ "reasoning": "",
633
+ "best_match": null,
634
+ "partial_matches": [
635
+ {
636
+ "reference_idx": 3,
637
+ "predicted_idx": 1,
638
+ "match": "partial",
639
+ "reasoning": "Both address the need to connect continuous function spaces to discrete observations (countable evaluations versus finite-dimensional marginals) for practical inference."
640
+ },
641
+ {
642
+ "reference_idx": 3,
643
+ "predicted_idx": 4,
644
+ "match": "partial",
645
+ "reasoning": "Both focus on bridging continuous functions with discrete data points, though they describe it differently (countable-evaluation formulation versus coordinate-based representations)."
646
+ }
647
+ ]
648
+ },
649
+ {
650
+ "reference_idx": 4,
651
+ "reference_ingredient": "Implicit neural representation-based practical implementation of the functional score network",
652
+ "reference_role": "CORE_METHOD",
653
+ "covered": false,
654
+ "best_match_idx": null,
655
+ "reasoning": "",
656
+ "best_match": null,
657
+ "partial_matches": [
658
+ {
659
+ "reference_idx": 4,
660
+ "predicted_idx": 4,
661
+ "match": "partial",
662
+ "reasoning": "Implicit neural representations are a form of coordinate-based representation, but the prediction describes applying them to data observations rather than the functional score network."
663
+ }
664
+ ]
665
+ }
666
+ ],
667
+ "partial_matches": [
668
+ {
669
+ "reference_idx": 1,
670
+ "predicted_idx": 1,
671
+ "match": "partial",
672
+ "reasoning": "Both identify the core diffusion generative mechanism, but the reference specifies a continuous-time SDE formulation while the prediction focuses on adapting diffusion for finite marginals."
673
+ },
674
+ {
675
+ "reference_idx": 1,
676
+ "predicted_idx": 5,
677
+ "match": "partial",
678
+ "reasoning": "Both highlight the score-based or denoising formulation, though the prediction restricts it to function values at given coordinates."
679
+ },
680
+ {
681
+ "reference_idx": 2,
682
+ "predicted_idx": 3,
683
+ "match": "partial",
684
+ "reasoning": "Both provide the theoretical foundation for defining distributions over infinite-dimensional function spaces, but use different mathematical frameworks (Hilbert space SDEs versus exchangeability/consistency)."
685
+ },
686
+ {
687
+ "reference_idx": 3,
688
+ "predicted_idx": 1,
689
+ "match": "partial",
690
+ "reasoning": "Both address the need to connect continuous function spaces to discrete observations (countable evaluations versus finite-dimensional marginals) for practical inference."
691
+ },
692
+ {
693
+ "reference_idx": 3,
694
+ "predicted_idx": 4,
695
+ "match": "partial",
696
+ "reasoning": "Both focus on bridging continuous functions with discrete data points, though they describe it differently (countable-evaluation formulation versus coordinate-based representations)."
697
+ },
698
+ {
699
+ "reference_idx": 4,
700
+ "predicted_idx": 4,
701
+ "match": "partial",
702
+ "reasoning": "Implicit neural representations are a form of coordinate-based representation, but the prediction describes applying them to data observations rather than the functional score network."
703
+ }
704
+ ]
705
+ },
706
+ "gemini_analysis": {
707
+ "model": "gemini/gemini-3.1-pro-preview",
708
+ "focus": "tool-aware (web_search_tool + deep_web_search_tool)",
709
+ "tooling_clarification": "The agent uses `web_search_tool` to retrieve search engine snippets and `deep_web_search_tool` to fetch and read full webpage content. There is no 'crawl' tool; `deep_web_search_tool` is the sole mechanism for extracting full-text page content.",
710
+ "success_search_contribution": [
711
+ "Formulated targeted queries (e.g., 'native 3D multimodal language model unified text image 3D generation') to locate primary and secondary sources for ShapeLLM-Omni.",
712
+ "Successfully identified the project page ('jamesyjl.github.io/ShapeLLM/') as a viable alternative source.",
713
+ "Used follow-up searches ('ShapeLLM-Omni 3D-Alpaca dataset source Objaverse') to verify specific entities like '3D-Alpaca' and '3D-aware discrete tokens' found in earlier steps."
714
+ ],
715
+ "success_deep_contribution": [
716
+ "Attempted to fetch the arXiv HTML ('https://arxiv.org/html/2506.01853v1') but encountered a PDF parse error ('No /Root object!'), prompting a necessary pivot.",
717
+ "Successfully fetched the project page ('https://jamesyjl.github.io/ShapeLLM/'), extracting exact architectural details ('3D VQVAE', 'Qwen-2.5-vl-7B-Instruct') and dataset names ('3D-Alpaca') that perfectly aligned with the gold ingredients."
718
+ ],
719
+ "success_why_overall": [
720
+ "The agent demonstrated tool resilience by pivoting to a project page when the primary arXiv HTML deep-fetch failed.",
721
+ "The deep-fetched project page contained highly specific, structured information about the method's architecture and training data, which the agent accurately synthesized into the final ingredients."
722
+ ],
723
+ "failure_diagnosis": "The agent successfully surfaced the correct paper via search but failed to deep-fetch any of its URLs, instead deep-fetching and extracting ingredients from a neighboring method (Neural Diffusion Processes).",
724
+ "failure_search_what_went_wrong": [
725
+ "Search successfully found the correct target paper ('Continuous-Time Functional Diffusion Processes') and its URLs (NeurIPS abstract, arXiv HTML, GitHub project page) in steps 5 and 7.",
726
+ "Search snippets contained highly relevant gold terms (e.g., 'hilbert-spaces', 'stochastic-partial-differential-equations'), but the agent ignored them.",
727
+ "The agent incorrectly pivoted its search strategy to query a neighboring method ('Neural Diffusion Processes Dutordoir ICML 2023 architecture')."
728
+ ],
729
+ "failure_deep_what_went_wrong": [
730
+ "Never called `deep_web_search_tool` on the correct FDP URLs found in search (e.g., 'arxiv.org/html/2303.00800v3').",
731
+ "Wasted deep-fetch calls on guessed/wrong arXiv IDs (e.g., 2303.01631, 2205.15306) which returned completely unrelated papers.",
732
+ "Deep-fetched OpenReview and PMLR pages for the wrong paper ('Neural Diffusion Processes'), leading to the extraction of incorrect methodological details."
733
+ ],
734
+ "failure_error_bullets": [
735
+ "[Substitutes neighboring method] The agent extracted ingredients for 'Neural Diffusion Processes' (finite marginals, exchangeability, attention) instead of the gold 'Functional Diffusion Processes' (Hilbert-space SDEs, countable evaluation, INR) because it deep-fetched the wrong paper's PMLR page.",
736
+ "[Candidate retrieval failure] Although `web_search_tool` surfaced the correct URLs, the agent failed to retrieve their full content via `deep_web_search_tool`, relying instead on deep-fetches of incorrect papers.",
737
+ "[Missed core method] By focusing on the wrong paper's deep-fetched content, the agent completely missed the core continuous-time SDE and Hilbert space framework of the target paper."
738
+ ],
739
+ "failure_what_needed": [
740
+ "The agent needed to pass the correct URLs found by `web_search_tool` (e.g., 'https://arxiv.org/html/2303.00800v3') directly into `deep_web_search_tool` instead of guessing arXiv IDs or pivoting to fetch pages for Neural Diffusion Processes."
741
+ ]
742
+ },
743
+ "tool_traces": {
744
+ "tools_available": {
745
+ "web_search_tool": "Broad web search; returns list of {source URL, summary}. NOT a crawler.",
746
+ "deep_web_search_tool": "Given a URL, fetches webpage/PDF content as {title,url,content}. This is the page-fetch / crawl-like tool. There is NO separate tool named crawl.",
747
+ "check_answer_format": "Validates final JSON schema."
748
+ },
749
+ "success": {
750
+ "paper_id": "neurips-2025-spotlight-472",
751
+ "claim_idx": 1,
752
+ "n_steps": 4,
753
+ "web_search_queries": [
754
+ "native 3D multimodal language model unified text image 3D generation",
755
+ "3D multimodal language model understanding and language-guided editing",
756
+ "native 3D-LLM text image 3D generation understanding editing",
757
+ "ShapeLLM-Omni 3D-Alpaca dataset source Objaverse"
758
+ ],
759
+ "web_search_hits_notable": [
760
+ "arxiv.org/html/2506.01853v1 ShapeLLM-Omni",
761
+ "neurips.cc/virtual/2025/poster/116179 mentioning 3D-Alpaca",
762
+ "project page jamesyjl.github.io/ShapeLLM/"
763
+ ],
764
+ "deep_fetches": [
765
+ {
766
+ "url": "https://arxiv.org/html/2506.01853v1",
767
+ "outcome": "FAILED: Error fetching webpage: No /Root object! - Is this really a PDF?"
768
+ },
769
+ {
770
+ "url": "https://jamesyjl.github.io/ShapeLLM/",
771
+ "outcome": "SUCCESS: project page content mentioning ShapeLLM-Omni, 3D tokens, training dataset details, backbone mentions"
772
+ }
773
+ ],
774
+ "final_ingredients": [
775
+ "3D VQVAE",
776
+ "3D-Alpaca Dataset",
777
+ "Qwen-2.5-vl-7B-Instruct",
778
+ "Unified Token-based Multimodal Architecture"
779
+ ]
780
+ },
781
+ "failure": {
782
+ "paper_id": "neurips-2023-poster-1534",
783
+ "claim_idx": 0,
784
+ "n_steps": 17,
785
+ "web_search_queries_sample": [
786
+ "diffusion-based generative framework for continuous function spaces",
787
+ "score-based generative modeling in function space",
788
+ "Functional Diffusion Processes NeurIPS 2023",
789
+ "Continuous-Time Functional Diffusion Processes irregularly sampled data",
790
+ "Neural Diffusion Processes Dutordoir ICML 2023 architecture"
791
+ ],
792
+ "web_search_correct_hits_but_underused": [
793
+ "NeurIPS 2023 abstract page for Continuous-Time Functional Diffusion Processes (found in step 5)",
794
+ "arxiv.org/html/2303.00800v3 Continuous-Time Functional Diffusion Processes (found in step 7)",
795
+ "1zb.github.io/functional-diffusion/ project page (found in step 7)"
796
+ ],
797
+ "deep_fetches": [
798
+ {
799
+ "url": "https://arxiv.org/abs/2302.03046",
800
+ "intended": "guessed target paper",
801
+ "outcome": "attempted in malformed multi-code step; no usable obs"
802
+ },
803
+ {
804
+ "url": "https://arxiv.org/abs/2303.01631",
805
+ "intended": "FDP paper",
806
+ "outcome": "WRONG PAPER: Real-Time Tube-Based Non-Gaussian Risk Bounded Motion Planning..."
807
+ },
808
+ {
809
+ "url": "https://arxiv.org/abs/2205.15306",
810
+ "intended": "Neural Diffusion Processes",
811
+ "outcome": "WRONG PAPER: How to Compose Shortest Paths"
812
+ },
813
+ {
814
+ "url": "https://openreview.net/forum?id=tV7GSY5GYG",
815
+ "intended": "NDP OpenReview",
816
+ "outcome": "fetched NDP OpenReview page (neighboring method, not gold FDP)"
817
+ },
818
+ {
819
+ "url": "https://proceedings.mlr.press/v202/dutordoir23a.html",
820
+ "intended": "NDP PMLR",
821
+ "outcome": "fetched NDP paper page; agent synthesized NDP ingredients"
822
+ }
823
+ ],
824
+ "failure_mode_tooling": "Search surfaced the correct FDP paper, but deep_web_search_tool was applied to wrong arXiv IDs and then to a neighboring Neural Diffusion Processes paper. Final ingredients mirror NDP (finite marginals, exchangeability, attention) rather than FDP (Hilbert-space continuous-time SDEs, countable evaluation, INR score network)."
825
+ }
826
+ }
827
+ }
828
+ }
829
+ }
hf_space/system_run_data.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load curated System Run case studies for the Streamlit demo.
2
+
3
+ Labels and section structure follow the SciPaths Dev · CodeAgent Case Studies HTML report.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from functools import lru_cache
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ CASES_DIR = Path(__file__).resolve().parent / "system_run_cases"
14
+ ANALYSIS_PATH = CASES_DIR / "case_study_analysis.json"
15
+
16
+ SYSTEM_RUN_METHODS: list[dict[str, str]] = [
17
+ {
18
+ "id": "codeagent_parametric",
19
+ "label": "Parametric - LLM Only",
20
+ "banner_title": "1 · codeagent_parametric",
21
+ "banner_desc": "Parametric-only generation (no search/retrieval). Mean F1 ≈ 0.20.",
22
+ },
23
+ {
24
+ "id": "codeagent_websearch_deep",
25
+ # Audience label: "Crawl" = deep_web_search_tool (page/PDF fetch; no separate crawl tool).
26
+ "label": "Websearch Deep - LLM + Web Search + Crawl",
27
+ "banner_title": "2 · codeagent_websearch_deep",
28
+ "banner_desc": (
29
+ "Tools: web_search_tool + deep_web_search_tool (page fetch / crawl). "
30
+ "Mean F1 ≈ 0.32."
31
+ ),
32
+ },
33
+ ]
34
+
35
+ DEFAULT_SYSTEM_METHOD = SYSTEM_RUN_METHODS[0]["id"]
36
+ DEFAULT_SYSTEM_CASE = "success"
37
+
38
+ # Case keys exposed in the UI — labels match the HTML report.
39
+ _METHOD_CASES: dict[str, list[tuple[str, str]]] = {
40
+ "codeagent_parametric": [("success", "Success"), ("failure", "Failure")],
41
+ "codeagent_websearch_deep": [("success", "Success"), ("failure", "Failure")],
42
+ }
43
+
44
+ CASE_OUTCOME_LABEL: dict[str, str] = {
45
+ "success": "Success",
46
+ "failure": "Failure",
47
+ }
48
+
49
+ TOOLING_NOTE_HTML = (
50
+ "<strong>Tooling by setting.</strong>"
51
+ "<br/>· <code>parametric</code>: parametric knowledge only (no search/retrieval)."
52
+ "<br/>· <code>websearch_deep</code>: <code>web_search_tool</code> + "
53
+ "<code>deep_web_search_tool</code> (URL page fetch)."
54
+ )
55
+
56
+
57
+ @lru_cache(maxsize=1)
58
+ def load_analysis() -> dict[str, Any]:
59
+ with ANALYSIS_PATH.open("r", encoding="utf-8") as f:
60
+ data = json.load(f)
61
+ if not isinstance(data, dict):
62
+ raise ValueError(f"Invalid case study analysis at {ANALYSIS_PATH}")
63
+ return data
64
+
65
+
66
+ def method_ids() -> list[str]:
67
+ return [m["id"] for m in SYSTEM_RUN_METHODS]
68
+
69
+
70
+ def method_meta(method_id: str) -> dict[str, str]:
71
+ for m in SYSTEM_RUN_METHODS:
72
+ if m["id"] == method_id:
73
+ return m
74
+ return {"id": method_id, "label": method_id, "banner_title": method_id, "banner_desc": ""}
75
+
76
+
77
+ def method_label(method_id: str) -> str:
78
+ return method_meta(method_id).get("label") or method_id
79
+
80
+
81
+ def case_chip_label(method_id: str, case_key: str) -> str:
82
+ """Top-of-card label, e.g. 'Parametric · Success'."""
83
+ pretty = {
84
+ "codeagent_parametric": "Parametric",
85
+ "codeagent_websearch_deep": "WebSearch Deep",
86
+ }.get(method_id, method_label(method_id))
87
+ outcome = CASE_OUTCOME_LABEL.get(case_key, case_key)
88
+ return f"{pretty} · {outcome}"
89
+
90
+
91
+ def list_cases(method_id: str) -> list[tuple[str, str]]:
92
+ return list(_METHOD_CASES.get(method_id, []))
93
+
94
+
95
+ def default_case_for_method(method_id: str) -> str:
96
+ cases = list_cases(method_id)
97
+ return cases[0][0] if cases else DEFAULT_SYSTEM_CASE
98
+
99
+
100
+ def outcome_tone(case_key: str, f1: float) -> str:
101
+ if case_key == "success":
102
+ return "ok"
103
+ if case_key == "failure":
104
+ return "bad"
105
+ if f1 >= 0.75:
106
+ return "ok"
107
+ if f1 <= 0.05:
108
+ return "bad"
109
+ return "part"
110
+
111
+
112
+ def _setting(method_id: str) -> dict[str, Any]:
113
+ settings = load_analysis().get("settings") or {}
114
+ block = settings.get(method_id)
115
+ if not isinstance(block, dict):
116
+ raise KeyError(f"Unknown system-run method: {method_id}")
117
+ return block
118
+
119
+
120
+ def _raw_case_block(method_id: str, case_key: str) -> dict[str, Any]:
121
+ setting = _setting(method_id)
122
+ if case_key == "featured":
123
+ block = setting.get("case")
124
+ else:
125
+ block = setting.get(case_key)
126
+ if not isinstance(block, dict):
127
+ raise KeyError(f"Unknown case {case_key!r} for method {method_id}")
128
+ return block
129
+
130
+
131
+ def _gold_ingredients(case: dict[str, Any]) -> list[dict[str, Any]]:
132
+ """Gold column with full/partial/miss badges and match notes (HTML report style)."""
133
+ predicted = case.get("predicted_ingredients") or []
134
+ gold: list[dict[str, Any]] = []
135
+ for judgment in case.get("recall_judgments") or []:
136
+ if not isinstance(judgment, dict):
137
+ continue
138
+ desc = str(judgment.get("reference_ingredient") or "").strip()
139
+ role = str(judgment.get("reference_role") or "").strip()
140
+ if not desc:
141
+ continue
142
+ covered = bool(judgment.get("covered"))
143
+ partials = judgment.get("partial_matches") or []
144
+ if not isinstance(partials, list):
145
+ partials = []
146
+ match_notes: list[str] = []
147
+ judge_note = ""
148
+ pair_pred_idx: Optional[int] = None
149
+ if covered:
150
+ badge = "full"
151
+ best = judgment.get("best_match") if isinstance(judgment.get("best_match"), dict) else {}
152
+ best_desc = str(best.get("description") or "").strip()
153
+ if best_desc:
154
+ match_notes.append(f"Matched to pred: {best_desc}")
155
+ judge_note = str(judgment.get("reasoning") or "").strip()
156
+ raw_idx = judgment.get("best_match_idx")
157
+ if isinstance(raw_idx, int) and raw_idx > 0:
158
+ pair_pred_idx = raw_idx
159
+ elif best_desc:
160
+ # Fall back to description match against predicted list (1-based).
161
+ for i, pred in enumerate(predicted, start=1):
162
+ if isinstance(pred, dict) and str(pred.get("description") or "").strip() == best_desc:
163
+ pair_pred_idx = i
164
+ break
165
+ elif partials:
166
+ badge = "partial"
167
+ for pm in partials:
168
+ if not isinstance(pm, dict):
169
+ continue
170
+ pred_idx = pm.get("predicted_idx")
171
+ reason = str(pm.get("reasoning") or "").strip()
172
+ if pred_idx is not None and reason:
173
+ match_notes.append(f"Partial ↔ pred #{pred_idx}: {reason}")
174
+ elif reason:
175
+ match_notes.append(reason)
176
+ else:
177
+ badge = "miss"
178
+ gold.append(
179
+ {
180
+ "description": desc,
181
+ "role": role,
182
+ "badge": badge,
183
+ "match_notes": match_notes,
184
+ "judge_note": judge_note,
185
+ "pair_pred_idx": pair_pred_idx,
186
+ }
187
+ )
188
+ return gold
189
+
190
+
191
+ def build_pair_rows(
192
+ gold: list[dict[str, Any]], predicted: list[dict[str, Any]]
193
+ ) -> list[dict[str, Any]]:
194
+ """Align gold↔prediction rows for success-case connector lines."""
195
+ by_idx = {
196
+ int(p["idx"]): p
197
+ for p in predicted
198
+ if isinstance(p.get("idx"), int)
199
+ }
200
+ used: set[int] = set()
201
+ rows: list[dict[str, Any]] = []
202
+ for g in gold:
203
+ pred_idx = g.get("pair_pred_idx")
204
+ pred = by_idx.get(int(pred_idx)) if isinstance(pred_idx, int) else None
205
+ linked = bool(pred is not None and g.get("badge") == "full")
206
+ if linked and isinstance(pred_idx, int):
207
+ used.add(pred_idx)
208
+ rows.append({"gold": g, "pred": pred, "linked": linked})
209
+ for p in predicted:
210
+ idx = p.get("idx")
211
+ if isinstance(idx, int) and idx not in used:
212
+ rows.append({"gold": None, "pred": p, "linked": False})
213
+ return rows
214
+
215
+
216
+ def _tool_panel(method_id: str, case_key: str, case: dict[str, Any], setting: dict[str, Any]) -> dict[str, Any]:
217
+ """Structured tool evidence / traces for the case card."""
218
+ if method_id == "codeagent_retrieval_plus_websearch":
219
+ analysis = case.get("gemini_analysis") if isinstance(case.get("gemini_analysis"), dict) else {}
220
+ return {
221
+ "kind": "retrieval",
222
+ "intro": (
223
+ "This run used both retrieve_documents and web_search_tool. "
224
+ "Web search continued in later steps after documents were returned."
225
+ ),
226
+ "queries": list(analysis.get("web_search_queries_example") or []),
227
+ "retrieved_docs": list(case.get("retrieved_doc_titles") or []),
228
+ }
229
+
230
+ if method_id == "codeagent_websearch_deep":
231
+ traces = setting.get("tool_traces") if isinstance(setting.get("tool_traces"), dict) else {}
232
+ trace = traces.get("success" if case_key == "success" else "failure")
233
+ if not isinstance(trace, dict):
234
+ return {"kind": "none"}
235
+ deep_fetches = []
236
+ for item in trace.get("deep_fetches") or []:
237
+ if not isinstance(item, dict):
238
+ continue
239
+ outcome = str(item.get("outcome") or "")
240
+ ok = outcome.upper().startswith("SUCCESS")
241
+ deep_fetches.append(
242
+ {
243
+ "url": str(item.get("url") or ""),
244
+ "outcome": outcome,
245
+ "ok": ok,
246
+ }
247
+ )
248
+ return {
249
+ "kind": "deep",
250
+ "queries": list(
251
+ trace.get("web_search_queries")
252
+ or trace.get("web_search_queries_sample")
253
+ or []
254
+ ),
255
+ "hits": list(
256
+ trace.get("web_search_hits_notable")
257
+ or trace.get("web_search_correct_hits_but_underused")
258
+ or []
259
+ ),
260
+ "deep_fetches": deep_fetches,
261
+ }
262
+
263
+ return {"kind": "none"}
264
+
265
+
266
+ def get_case(method_id: str, case_key: str) -> dict[str, Any]:
267
+ """Normalized case card payload for the UI."""
268
+ setting = _setting(method_id)
269
+ case = _raw_case_block(method_id, case_key)
270
+ tools = [str(t) for t in (setting.get("tools") or [])]
271
+ predicted = []
272
+ for idx, item in enumerate(case.get("predicted_ingredients") or [], start=1):
273
+ if not isinstance(item, dict):
274
+ continue
275
+ predicted.append(
276
+ {
277
+ "idx": idx,
278
+ "description": str(item.get("description") or "").strip(),
279
+ "role": str(item.get("role") or "").strip(),
280
+ "rationale": str(item.get("rationale") or "").strip(),
281
+ }
282
+ )
283
+ f1 = float(case.get("f1") or 0.0)
284
+ return {
285
+ "method_id": method_id,
286
+ "method_label": method_label(method_id),
287
+ "case_key": case_key,
288
+ "case_chip": case_chip_label(method_id, case_key),
289
+ "outcome_tone": outcome_tone(case_key, f1),
290
+ "tools": tools,
291
+ "paper_id": str(case.get("paper_id") or ""),
292
+ "claim_idx": case.get("claim_idx"),
293
+ "paper_title": str(case.get("paper_title") or ""),
294
+ "claim": str(case.get("claim") or ""),
295
+ "recall": float(case.get("recall") or 0.0),
296
+ "precision": float(case.get("precision") or 0.0),
297
+ "f1": f1,
298
+ "gold_ingredients": _gold_ingredients(case),
299
+ "predicted_ingredients": predicted,
300
+ "tool_panel": _tool_panel(method_id, case_key, case, setting),
301
+ "banner": method_meta(method_id),
302
+ }
303
+
304
+
305
+ def get_analysis(method_id: str, case_key: Optional[str] = None) -> dict[str, Any]:
306
+ setting = _setting(method_id)
307
+ if method_id == "codeagent_retrieval_plus_websearch":
308
+ case = setting.get("case") or {}
309
+ analysis = case.get("gemini_analysis") if isinstance(case, dict) else None
310
+ return dict(analysis) if isinstance(analysis, dict) else {}
311
+
312
+ analysis = setting.get("gemini_analysis")
313
+ if not isinstance(analysis, dict):
314
+ return {}
315
+ out = dict(analysis)
316
+ out["_case_key"] = case_key or ""
317
+ return out
318
+
319
+
320
+ def analysis_view_for_case(method_id: str, case_key: str) -> dict[str, Any]:
321
+ """Structured analysis view matching HTML report headings."""
322
+ analysis = get_analysis(method_id, case_key)
323
+ if not analysis:
324
+ return {"model_tag": "Gemini 3.1 Pro analysis", "blocks": []}
325
+
326
+ blocks: list[dict[str, Any]] = []
327
+
328
+ if method_id == "codeagent_retrieval_plus_websearch":
329
+ headline = str(analysis.get("headline") or "").strip()
330
+ takeaway = str(analysis.get("takeaway") or "").strip()
331
+ diag = headline
332
+ if takeaway:
333
+ diag = f"{headline}\n{takeaway}" if headline else takeaway
334
+ if diag:
335
+ blocks.append({"type": "diagnosis", "text": diag, "strong_first": True})
336
+ two_col = []
337
+ for key, title in (
338
+ ("what_was_retrieved", "What the tools returned"),
339
+ ("effect_on_prediction", "Effect on prediction"),
340
+ ):
341
+ items = analysis.get(key)
342
+ if isinstance(items, list) and items:
343
+ two_col.append({"title": title, "items": [str(x) for x in items]})
344
+ if two_col:
345
+ blocks.append({"type": "two_col", "cols": two_col})
346
+ errs = analysis.get("error_taxonomy_bullets")
347
+ if isinstance(errs, list) and errs:
348
+ blocks.append(
349
+ {"type": "list", "title": "Error analysis", "items": [str(x) for x in errs]}
350
+ )
351
+ return {"model_tag": "Gemini 3.1 Pro analysis", "blocks": blocks}
352
+
353
+ if method_id == "codeagent_parametric":
354
+ if case_key == "success":
355
+ why = analysis.get("success_why")
356
+ if isinstance(why, list) and why:
357
+ blocks.append(
358
+ {
359
+ "type": "list",
360
+ "title": "Why this succeeded",
361
+ "items": [str(x) for x in why],
362
+ }
363
+ )
364
+ else:
365
+ if analysis.get("failure_diagnosis"):
366
+ blocks.append(
367
+ {
368
+ "type": "diagnosis",
369
+ "text": f"Diagnosis. {analysis['failure_diagnosis']}",
370
+ "strong_first": True,
371
+ }
372
+ )
373
+ errs = analysis.get("failure_error_bullets")
374
+ if isinstance(errs, list) and errs:
375
+ blocks.append(
376
+ {
377
+ "type": "list",
378
+ "title": "Error taxonomy",
379
+ "items": [str(x) for x in errs],
380
+ }
381
+ )
382
+ needed = analysis.get("failure_what_needed")
383
+ if isinstance(needed, list) and needed:
384
+ blocks.append(
385
+ {
386
+ "type": "list",
387
+ "title": "What recovery would have required",
388
+ "items": [str(x) for x in needed],
389
+ }
390
+ )
391
+ return {"model_tag": "Gemini 3.1 Pro analysis", "blocks": blocks}
392
+
393
+ # websearch_deep
394
+ if case_key == "success":
395
+ two_col = []
396
+ for key, title in (
397
+ ("success_search_contribution", "web_search_tool"),
398
+ ("success_deep_contribution", "deep_web_search_tool"),
399
+ ):
400
+ items = analysis.get(key)
401
+ if isinstance(items, list) and items:
402
+ two_col.append({"title": title, "items": [str(x) for x in items]})
403
+ if two_col:
404
+ blocks.append({"type": "two_col", "cols": two_col})
405
+ why = analysis.get("success_why_overall")
406
+ if isinstance(why, list) and why:
407
+ blocks.append(
408
+ {"type": "list", "title": "Why this succeeded", "items": [str(x) for x in why]}
409
+ )
410
+ else:
411
+ if analysis.get("failure_diagnosis"):
412
+ blocks.append(
413
+ {
414
+ "type": "diagnosis",
415
+ "text": f"Diagnosis. {analysis['failure_diagnosis']}",
416
+ "strong_first": True,
417
+ }
418
+ )
419
+ two_col = []
420
+ for key, title in (
421
+ ("failure_search_what_went_wrong", "web_search_tool"),
422
+ ("failure_deep_what_went_wrong", "deep_web_search_tool"),
423
+ ):
424
+ items = analysis.get(key)
425
+ if isinstance(items, list) and items:
426
+ two_col.append({"title": title, "items": [str(x) for x in items]})
427
+ if two_col:
428
+ blocks.append({"type": "two_col", "cols": two_col})
429
+ errs = analysis.get("failure_error_bullets")
430
+ if isinstance(errs, list) and errs:
431
+ blocks.append(
432
+ {"type": "list", "title": "Error taxonomy", "items": [str(x) for x in errs]}
433
+ )
434
+ needed = analysis.get("failure_what_needed")
435
+ if isinstance(needed, list) and needed:
436
+ blocks.append(
437
+ {
438
+ "type": "list",
439
+ "title": "What recovery would have required",
440
+ "items": [str(x) for x in needed],
441
+ }
442
+ )
443
+
444
+ return {"model_tag": "Gemini 3.1 Pro analysis", "blocks": blocks}
hf_space/workflow_graph.py ADDED
@@ -0,0 +1,1197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evolving SciPaths workflow graph (Neo4j-backed).
2
+
3
+ Visual formatting follows Mina Brain's knowledge-graph canvas strictly:
4
+ filled circle + white inner stroke + colored ring, 25-char labels with white
5
+ halo, expanding write-pulse rings, #fafbfd stage, click detail card.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ from pathlib import Path
13
+ from typing import Any, Optional
14
+
15
+ import streamlit as st
16
+
17
+ try:
18
+ import neo4j_workflow as neo4j_store
19
+ except Exception: # pragma: no cover
20
+ neo4j_store = None # type: ignore
21
+
22
+ # Short labels shared by Steps panel + workflow graph captions.
23
+ STEP_COPY: dict[int, str] = {
24
+ 1: "Load the paper",
25
+ 2: "Find where it’s cited",
26
+ 3: "Collect citation contexts",
27
+ 4: "Classify how it’s used",
28
+ 5: "Keep real reuse (uses / extends)",
29
+ 6: "Pull the citing passages",
30
+ 7: "Group similar reuse themes and summarize target contributions",
31
+ 8: "Find enabling contributions and corresponding prior studies",
32
+ }
33
+
34
+ WORK_PANEL_STEPS: list[tuple[int, str]] = [(n, STEP_COPY[n]) for n in range(1, 9)]
35
+
36
+ CANVAS_LABEL_MAX = 25
37
+
38
+ # Mina Brain NODE_COLORS mapped onto SciPaths kinds (fill / ring / text).
39
+ KIND_META = {
40
+ "target": {
41
+ "fill": "#4F6EF7",
42
+ "ring": "#A5B4FC",
43
+ "text": "#1E3A8A",
44
+ "r": 22,
45
+ "type_label": "Target paper",
46
+ "color": "#4F6EF7",
47
+ "size": 22,
48
+ },
49
+ "citing": {
50
+ "fill": "#60A5FA",
51
+ "ring": "#BFDBFE",
52
+ "text": "#1D4ED8",
53
+ "r": 14,
54
+ "type_label": "Citing paper",
55
+ "color": "#60A5FA",
56
+ "size": 14,
57
+ },
58
+ "cluster": {
59
+ "fill": "#8B5CF6",
60
+ "ring": "#DDD6FE",
61
+ "text": "#5B21B6",
62
+ "r": 16,
63
+ "type_label": "Reuse theme",
64
+ "color": "#8B5CF6",
65
+ "size": 16,
66
+ },
67
+ "claim": {
68
+ "fill": "#FBBF24",
69
+ "ring": "#FDE68A",
70
+ "text": "#B45309",
71
+ "r": 15,
72
+ "type_label": "Target contribution",
73
+ "color": "#FBBF24",
74
+ "size": 15,
75
+ },
76
+ "ingredient": {
77
+ "fill": "#94A3B8",
78
+ "ring": "#E2E8F0",
79
+ "text": "#475569",
80
+ "r": 12,
81
+ "type_label": "Enabling contribution",
82
+ "color": "#94A3B8",
83
+ "size": 12,
84
+ },
85
+ "study": {
86
+ "fill": "#34D399",
87
+ "ring": "#A7F3D0",
88
+ "text": "#047857",
89
+ "r": 13,
90
+ "type_label": "Prior study",
91
+ "color": "#34D399",
92
+ "size": 13,
93
+ },
94
+ }
95
+
96
+ # Category order + compact per-kind palette the canvas shell + rail consume.
97
+ _KIND_ORDER = ["target", "claim", "ingredient", "cluster", "study", "citing"]
98
+ _KIND_META_JS = {
99
+ k: {"fill": v["fill"], "ring": v["ring"], "text": v["text"], "label": v["type_label"]}
100
+ for k, v in KIND_META.items()
101
+ }
102
+
103
+
104
+ def _load_json(path: Path) -> Any | None:
105
+ if not path.exists():
106
+ return None
107
+ try:
108
+ return json.loads(path.read_text(encoding="utf-8"))
109
+ except Exception:
110
+ return None
111
+
112
+
113
+ def _clean(text: str) -> str:
114
+ return re.sub(r"\s+", " ", (text or "").strip())
115
+
116
+
117
+ def canvas_label(text: str, *, limit: int = CANVAS_LABEL_MAX) -> str:
118
+ cleaned = _clean(text)
119
+ if len(cleaned) <= limit:
120
+ return cleaned
121
+ return cleaned[:limit].rstrip() + "…"
122
+
123
+
124
+ def visible_step_from_events(events: list[str]) -> int:
125
+ started = 0
126
+ for raw in events or []:
127
+ text = str(raw)
128
+ m = re.search(r"Step\s+(\d+)\s*(?:/|\s+complete)", text, re.IGNORECASE)
129
+ if m:
130
+ started = max(started, int(m.group(1)))
131
+ continue
132
+ lower = text.lower()
133
+ if "[annotation]" in lower or "annotate" in lower:
134
+ started = max(started, 8)
135
+ return min(8, started)
136
+
137
+
138
+ def pulse_step_from_events(events: list[str]) -> int:
139
+ started: set[int] = set()
140
+ completed: set[int] = set()
141
+ for raw in events or []:
142
+ text = str(raw)
143
+ m_done = re.search(r"Step\s+(\d+)\s+complete", text, re.IGNORECASE)
144
+ if m_done:
145
+ completed.add(int(m_done.group(1)))
146
+ continue
147
+ m_start = re.search(r"Step\s+(\d+)\s*/", text)
148
+ if m_start:
149
+ started.add(int(m_start.group(1)))
150
+ continue
151
+ lower = text.lower()
152
+ if "[annotation]" in lower or "annotate" in lower:
153
+ if "complete" in lower or "skipped" in lower:
154
+ completed.add(8)
155
+ else:
156
+ started.add(8)
157
+ active = [n for n in sorted(started) if n not in completed]
158
+ return active[-1] if active else 0
159
+
160
+
161
+ def _paper_title_from_dir(paper_dir: Optional[Path]) -> str:
162
+ if not paper_dir:
163
+ return "Target paper"
164
+ data = _load_json(paper_dir / "paper_metadata.json")
165
+ record = None
166
+ if isinstance(data, list) and data and isinstance(data[0], dict):
167
+ record = data[0]
168
+ elif isinstance(data, dict):
169
+ record = data
170
+ return _clean(str((record or {}).get("title") or "")) or "Target paper"
171
+
172
+
173
+ def _paper_abstract_from_dir(paper_dir: Optional[Path]) -> str:
174
+ if not paper_dir:
175
+ return ""
176
+ data = _load_json(paper_dir / "paper_metadata.json")
177
+ record = None
178
+ if isinstance(data, list) and data and isinstance(data[0], dict):
179
+ record = data[0]
180
+ elif isinstance(data, dict):
181
+ record = data
182
+ abstract = _clean(str((record or {}).get("abstract") or ""))
183
+ abstract = re.sub(r"^(abstract)\s*[:.]?\s*", "", abstract, flags=re.IGNORECASE)
184
+ abstract = re.sub(r"(?<=[.!?\)\]\"'”’])\d+$", "", abstract)
185
+ return abstract
186
+
187
+
188
+ def _node(
189
+ *,
190
+ nid: str,
191
+ kind: str,
192
+ short: str,
193
+ title: str,
194
+ detail: str,
195
+ step_added: int,
196
+ pulse: bool,
197
+ ) -> dict[str, Any]:
198
+ meta = KIND_META.get(kind, KIND_META["citing"])
199
+ return {
200
+ "id": nid,
201
+ "kind": kind,
202
+ "label": canvas_label(short),
203
+ "title": _clean(title) or short,
204
+ "detail": _clean(detail),
205
+ "step_added": step_added,
206
+ "pulse": pulse,
207
+ "color": meta["fill"],
208
+ "fill": meta["fill"],
209
+ "ring": meta["ring"],
210
+ "text": meta["text"],
211
+ "r": meta["r"],
212
+ "size": meta["r"],
213
+ "type_label": meta["type_label"],
214
+ }
215
+
216
+
217
+ def _collect_citing(paper_dir: Optional[Path], limit: int = 6) -> list[dict[str, str]]:
218
+ if not paper_dir:
219
+ return []
220
+ out: list[dict[str, str]] = []
221
+ seen: set[str] = set()
222
+ contrib = _load_json(paper_dir / "usage_contributions.json") or {}
223
+ items = contrib.get("contributions") if isinstance(contrib, dict) else None
224
+ if isinstance(items, list):
225
+ for item in items:
226
+ if not isinstance(item, dict):
227
+ continue
228
+ cid = str(item.get("citing_paper_id") or "").strip()
229
+ title = _clean(str(item.get("citing_title") or "Citing paper"))
230
+ if not cid or cid in seen:
231
+ continue
232
+ seen.add(cid)
233
+ out.append(
234
+ {
235
+ "id": f"citing:{cid}",
236
+ "title": title,
237
+ "detail": _clean(
238
+ str(item.get("paper_claim") or item.get("claim") or item.get("evidence_span") or "")
239
+ ),
240
+ "label_tag": item.get("label") or "",
241
+ }
242
+ )
243
+ if len(out) >= limit:
244
+ return out
245
+ return out
246
+
247
+
248
+ def _collect_clusters(paper_dir: Optional[Path], limit: int = 4) -> list[dict[str, str]]:
249
+ if not paper_dir:
250
+ return []
251
+ discovery = _load_json(paper_dir / "usage_discovery_from_contributions.json") or {}
252
+ clusters = discovery.get("clusters") if isinstance(discovery, dict) else None
253
+ out: list[dict[str, str]] = []
254
+ if not isinstance(clusters, list):
255
+ return out
256
+ for item in clusters[:limit]:
257
+ if not isinstance(item, dict):
258
+ continue
259
+ cid = str(item.get("cluster_id") or f"C{len(out) + 1}")
260
+ title = _clean(
261
+ str(item.get("representative_claim") or item.get("cluster_title") or f"Theme {cid}")
262
+ )
263
+ out.append(
264
+ {
265
+ "id": f"cluster:{cid}",
266
+ "cluster_id": cid,
267
+ "title": title,
268
+ "detail": _clean(str(item.get("merge_rationale") or f"{item.get('count', '')} contribution instances")),
269
+ }
270
+ )
271
+ return out
272
+
273
+
274
+ def _collect_annotation(payload: Optional[dict]) -> tuple[list[dict], list[dict], list[dict]]:
275
+ claims: list[dict] = []
276
+ ingredients: list[dict] = []
277
+ studies: list[dict] = []
278
+ if not isinstance(payload, dict):
279
+ return claims, ingredients, studies
280
+ for claim in (payload.get("claims") or [])[:4]:
281
+ if not isinstance(claim, dict):
282
+ continue
283
+ claim_id = str(claim.get("claim_id") or f"C{len(claims) + 1}")
284
+ title = _clean(str(claim.get("rewritten_claim") or claim.get("text") or claim_id))
285
+ claims.append(
286
+ {
287
+ "id": f"claim:{claim_id}",
288
+ "claim_id": claim_id,
289
+ "cluster_id": str(claim.get("cluster_id") or ""),
290
+ "title": title,
291
+ "detail": _clean(str(claim.get("decision") or "")),
292
+ }
293
+ )
294
+ for ing in (claim.get("ingredients") or [])[:3]:
295
+ if not isinstance(ing, dict):
296
+ continue
297
+ iid = str(ing.get("ingredient_id") or f"{claim_id}.I{len(ingredients) + 1}")
298
+ ann = ing.get("canonical_annotation") if isinstance(ing.get("canonical_annotation"), dict) else {}
299
+ ingredients.append(
300
+ {
301
+ "id": f"ing:{iid}",
302
+ "ingredient_id": iid,
303
+ "claim_id": claim_id,
304
+ "title": _clean(str(ing.get("ingredient") or iid)),
305
+ "detail": _clean(
306
+ str((ann or {}).get("contribution") or (ann or {}).get("rationale") or "")
307
+ ),
308
+ "role": _clean(str((ann or {}).get("role") or "")),
309
+ }
310
+ )
311
+ g = ing.get("canonical_grounding") if isinstance(ing.get("canonical_grounding"), dict) else None
312
+ if g:
313
+ sid = str(g.get("paper_id") or g.get("ref_id") or g.get("ref_title") or iid)
314
+ studies.append(
315
+ {
316
+ "id": f"study:{sid}",
317
+ "ingredient_id": iid,
318
+ "title": _clean(str(g.get("ref_title") or g.get("ref_id") or "Prior study")),
319
+ "detail": _clean(str(g.get("ref_authors") or g.get("ref_year") or "")),
320
+ }
321
+ )
322
+ seen: set[str] = set()
323
+ uniq: list[dict] = []
324
+ for s in studies:
325
+ if s["id"] in seen:
326
+ continue
327
+ seen.add(s["id"])
328
+ uniq.append(s)
329
+ return claims, ingredients, uniq[:6]
330
+
331
+
332
+ def build_graph_model(
333
+ *,
334
+ paper_dir: Optional[Path],
335
+ payload: Optional[dict],
336
+ visible_step: int,
337
+ pulse_step: int = 0,
338
+ caption: str = "",
339
+ ) -> dict[str, Any]:
340
+ step = max(0, min(8, int(visible_step or 0)))
341
+ nodes: list[dict[str, Any]] = []
342
+ edges: list[dict[str, Any]] = []
343
+
344
+ if step >= 1:
345
+ title = _paper_title_from_dir(paper_dir)
346
+ abstract = _paper_abstract_from_dir(paper_dir)
347
+ nodes.append(
348
+ _node(
349
+ nid="target",
350
+ kind="target",
351
+ short="Target",
352
+ title=title,
353
+ detail=abstract[:500],
354
+ step_added=1,
355
+ pulse=pulse_step == 1,
356
+ )
357
+ )
358
+
359
+ citing = _collect_citing(paper_dir) if step >= 2 else []
360
+ if step >= 2:
361
+ rows = citing or [
362
+ {"id": f"citing:placeholder:{i}", "title": f"Citing paper {i+1}", "detail": "Waiting for citation artifacts…", "label_tag": ""}
363
+ for i in range(3)
364
+ ]
365
+ for i, item in enumerate(rows):
366
+ nodes.append(
367
+ _node(
368
+ nid=item["id"],
369
+ kind="citing",
370
+ short=f"Citing {i + 1}",
371
+ title=item["title"],
372
+ detail=item.get("detail") or item.get("label_tag") or "",
373
+ step_added=2,
374
+ pulse=pulse_step in {2, 3, 4, 5, 6},
375
+ )
376
+ )
377
+ edges.append(
378
+ {
379
+ "id": f"e-target-{item['id']}",
380
+ "source": "target",
381
+ "target": item["id"],
382
+ "kind": "cite",
383
+ "muted": step < 5,
384
+ "pulse": pulse_step in {2, 3, 4, 5},
385
+ "step_added": 2,
386
+ }
387
+ )
388
+
389
+ # Step 7: reuse themes + target contributions arrive together.
390
+ clusters = _collect_clusters(paper_dir) if step >= 7 else []
391
+ claims, ingredients, studies = (
392
+ _collect_annotation(payload) if step >= 7 else ([], [], [])
393
+ )
394
+ if step >= 7 and clusters:
395
+ for item in clusters:
396
+ cid = item.get("cluster_id") or "?"
397
+ nodes.append(
398
+ _node(
399
+ nid=item["id"],
400
+ kind="cluster",
401
+ short=f"Theme {cid}",
402
+ title=item["title"],
403
+ detail=item.get("detail") or "",
404
+ step_added=7,
405
+ pulse=pulse_step == 7,
406
+ )
407
+ )
408
+ edges.append(
409
+ {
410
+ "id": f"e-cluster-{item['id']}",
411
+ "source": "target",
412
+ "target": item["id"],
413
+ "kind": "theme",
414
+ "pulse": pulse_step == 7,
415
+ "muted": False,
416
+ "step_added": 7,
417
+ }
418
+ )
419
+
420
+ if step >= 7:
421
+ for item in claims:
422
+ cid = item.get("claim_id") or "?"
423
+ nodes.append(
424
+ _node(
425
+ nid=item["id"],
426
+ kind="claim",
427
+ short=f"Claim {cid}",
428
+ title=item["title"],
429
+ detail=item.get("detail") or "",
430
+ step_added=7,
431
+ pulse=pulse_step == 7,
432
+ )
433
+ )
434
+ src = f"cluster:{item.get('cluster_id')}" if item.get("cluster_id") else "target"
435
+ if not any(n["id"] == src for n in nodes):
436
+ src = "target"
437
+ edges.append(
438
+ {
439
+ "id": f"e-claim-{item['id']}",
440
+ "source": src,
441
+ "target": item["id"],
442
+ "kind": "derive",
443
+ "pulse": pulse_step == 7,
444
+ "muted": False,
445
+ "step_added": 7,
446
+ }
447
+ )
448
+
449
+ # Step 8: enabling contributions + prior studies.
450
+ if step >= 8:
451
+ for i, item in enumerate(ingredients):
452
+ nodes.append(
453
+ _node(
454
+ nid=item["id"],
455
+ kind="ingredient",
456
+ short=f"Enable {i + 1}",
457
+ title=item["title"],
458
+ detail=" · ".join(x for x in [item.get("role") or "", item.get("detail") or ""] if x),
459
+ step_added=8,
460
+ pulse=pulse_step == 8,
461
+ )
462
+ )
463
+ src = f"claim:{item.get('claim_id')}"
464
+ if any(n["id"] == src for n in nodes):
465
+ edges.append(
466
+ {
467
+ "id": f"e-ing-{item['id']}",
468
+ "source": src,
469
+ "target": item["id"],
470
+ "kind": "enable",
471
+ "pulse": pulse_step == 8,
472
+ "muted": False,
473
+ "step_added": 8,
474
+ }
475
+ )
476
+ for i, item in enumerate(studies):
477
+ nodes.append(
478
+ _node(
479
+ nid=item["id"],
480
+ kind="study",
481
+ short=f"Prior {i + 1}",
482
+ title=item["title"],
483
+ detail=item.get("detail") or "",
484
+ step_added=8,
485
+ pulse=pulse_step == 8,
486
+ )
487
+ )
488
+ src = f"ing:{item.get('ingredient_id')}"
489
+ if any(n["id"] == src for n in nodes):
490
+ edges.append(
491
+ {
492
+ "id": f"e-study-{item['id']}",
493
+ "source": src,
494
+ "target": item["id"],
495
+ "kind": "ground",
496
+ "pulse": pulse_step == 8,
497
+ "muted": False,
498
+ "step_added": 8,
499
+ }
500
+ )
501
+
502
+ if step <= 0:
503
+ phase_caption = "Waiting to run"
504
+ elif pulse_step:
505
+ phase_caption = STEP_COPY.get(pulse_step, "")
506
+ elif step >= 8:
507
+ phase_caption = "Workflow complete"
508
+ else:
509
+ phase_caption = caption or STEP_COPY.get(step, "")
510
+
511
+ return {
512
+ "step": step,
513
+ "pulse_step": pulse_step,
514
+ "caption": phase_caption,
515
+ "nodes": nodes,
516
+ "edges": edges,
517
+ "backend": "memory",
518
+ }
519
+
520
+
521
+ def _graph_payload(model: dict[str, Any]) -> dict[str, Any]:
522
+ """Serialize a graph model into the payload the canvas shell consumes."""
523
+ nodes_in = model.get("nodes") or []
524
+ edges_in = model.get("edges") or []
525
+
526
+ nodes_js = []
527
+ for n in nodes_in:
528
+ kind = str(n.get("kind") or "citing")
529
+ meta = KIND_META.get(kind, KIND_META["citing"])
530
+ nodes_js.append(
531
+ {
532
+ "id": n["id"],
533
+ "label": n.get("label") or "",
534
+ "kind": kind,
535
+ "fill": n.get("fill") or meta["fill"],
536
+ "ring": n.get("ring") or meta["ring"],
537
+ "text": n.get("text") or meta["text"],
538
+ "r": float(n.get("r") or meta["r"]),
539
+ "pulse": bool(n.get("pulse")),
540
+ "fullTitle": n.get("title") or "",
541
+ "detail": n.get("detail") or "",
542
+ "typeLabel": n.get("type_label") or meta["type_label"],
543
+ }
544
+ )
545
+ edges_js = []
546
+ for e in edges_in:
547
+ edges_js.append(
548
+ {
549
+ "id": e.get("id"),
550
+ "source": e.get("source"),
551
+ "target": e.get("target"),
552
+ "pulse": bool(e.get("pulse")),
553
+ "muted": bool(e.get("muted")),
554
+ }
555
+ )
556
+ return {
557
+ "nodes": nodes_js,
558
+ "edges": edges_js,
559
+ "kindMeta": _KIND_META_JS,
560
+ "kindOrder": _KIND_ORDER,
561
+ }
562
+
563
+
564
+ def courier_html(payload: dict[str, Any]) -> str:
565
+ """Tiny invisible frame that posts a graph model into the persistent shell.
566
+
567
+ Runs in its own Streamlit component iframe, reaches the parent document, and
568
+ postMessages the model to the graph iframe — so the graph updates in place
569
+ instead of the whole component being re-emitted (which reloads it).
570
+ """
571
+ model_str = json.dumps(payload)
572
+ embed = json.dumps(model_str).replace("</", "<\\/")
573
+ return (
574
+ "<!DOCTYPE html><html><head><meta charset=\"utf-8\" /></head><body>"
575
+ "<script>"
576
+ "(function(){"
577
+ "var model=JSON.parse(" + embed + ");"
578
+ "function post(){try{var f=window.parent.document.querySelectorAll('iframe');"
579
+ "for(var i=0;i<f.length;i++){try{f[i].contentWindow.postMessage({type:'scipaths-graph',model:model},'*');}catch(e){}}}catch(e){}}"
580
+ "post();var n=0;var t=setInterval(function(){n++;post();if(n>10)clearInterval(t);},160);"
581
+ "})();"
582
+ "</script></body></html>"
583
+ )
584
+
585
+
586
+ def graph_shell_html(height: int = 520) -> str:
587
+ """Stable, model-free canvas shell. Data arrives via postMessage (incremental).
588
+
589
+ The HTML is constant for a given height, so Streamlit reuses the same iframe
590
+ across reruns instead of remounting it. The graph is populated / grown by
591
+ ``courier_html`` messages.
592
+ """
593
+ payload = json.dumps(
594
+ {"nodes": [], "edges": [], "kindMeta": _KIND_META_JS, "kindOrder": _KIND_ORDER}
595
+ )
596
+ row_h = max(300, height - 30)
597
+ return f"""<!DOCTYPE html>
598
+ <html>
599
+ <head>
600
+ <meta charset="utf-8" />
601
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
602
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
603
+ <style>
604
+ html, body {{ margin:0; padding:0; background:transparent; font-family: Inter, system-ui, sans-serif; color:#13201d; }}
605
+ * {{ box-sizing:border-box; }}
606
+ .wrap {{ border-top:1px solid rgba(19,32,29,0.12); padding-top:0.5rem; }}
607
+ .kicker {{ font-size:0.68rem; font-weight:700; letter-spacing:0.12em; text-transform:uppercase; color:#6a7a74; margin-bottom:0.4rem; }}
608
+ .row {{ display:flex; gap:0.6rem; align-items:stretch; height:{row_h}px; }}
609
+ /* ── Control rail ── */
610
+ .rail {{
611
+ width:210px; flex:0 0 210px; display:flex; flex-direction:column; overflow:hidden;
612
+ border:1px solid rgba(15,23,42,0.10); border-radius:11px; background:#ffffff;
613
+ }}
614
+ .rail-head {{ padding:0.55rem 0.75rem; border-bottom:1px solid rgba(15,23,42,0.08);
615
+ font-size:0.66rem; font-weight:700; letter-spacing:0.09em; text-transform:uppercase; color:#6a7a74; }}
616
+ .rail-body {{ flex:1; overflow-y:auto; padding:0.6rem 0.65rem; }}
617
+ .rail-search {{ position:relative; margin-bottom:0.7rem; }}
618
+ .rail-search input {{
619
+ width:100%; height:30px; padding:0 1.6rem 0 0.6rem; font-size:0.76rem; font-family:inherit;
620
+ border:1px solid rgba(15,23,42,0.16); border-radius:7px; outline:none; color:#13201d;
621
+ }}
622
+ .rail-search input:focus {{ border-color:#4F6EF7; box-shadow:0 0 0 3px rgba(79,110,247,0.16); }}
623
+ .rail-search .clr {{ position:absolute; right:6px; top:50%; transform:translateY(-50%);
624
+ border:0; background:transparent; color:#94a3b8; cursor:pointer; font-size:0.9rem; line-height:1; }}
625
+ .rail-actions {{ display:grid; grid-template-columns:1fr 1fr; gap:0.35rem; margin-bottom:0.75rem; }}
626
+ .rail-actions button {{
627
+ font-size:0.66rem; font-weight:600; font-family:inherit; padding:0.34rem 0.3rem; cursor:pointer;
628
+ border:1px solid rgba(15,23,42,0.16); border-radius:7px; background:#fff; color:#13201d;
629
+ }}
630
+ .rail-actions button:hover {{ background:#f4f6fb; }}
631
+ .rail-actions button.on {{ border-color:#4F6EF7; background:#eef1fe; color:#1E3A8A; }}
632
+ .sec-head {{ display:flex; align-items:center; justify-content:space-between; margin:0 0 0.35rem 0.15rem; }}
633
+ .sec-head .lbl {{ font-size:0.63rem; font-weight:700; letter-spacing:0.07em; text-transform:uppercase; color:#94a3b8; }}
634
+ .sec-head .acts {{ display:flex; gap:0.45rem; }}
635
+ .sec-head .acts button {{ border:0; background:transparent; font-size:0.62rem; font-weight:600; color:#4F6EF7; cursor:pointer; padding:0; }}
636
+ .cat {{
637
+ width:100%; display:flex; align-items:center; gap:0.5rem; padding:0.28rem 0.35rem; cursor:pointer;
638
+ border:0; background:transparent; border-radius:6px; text-align:left; font-family:inherit;
639
+ }}
640
+ .cat:hover {{ background:#f4f6fb; }}
641
+ .cat .box {{ width:13px; height:13px; flex:0 0 13px; border-radius:3px; border:1.5px solid rgba(15,23,42,0.28);
642
+ display:flex; align-items:center; justify-content:center; color:#fff; font-size:9px; }}
643
+ .cat .box.on {{ background:#4F6EF7; border-color:#4F6EF7; }}
644
+ .cat .dot {{ width:12px; height:12px; flex:0 0 12px; border-radius:50%; }}
645
+ .cat .name {{ flex:1; font-size:0.74rem; color:#334155; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }}
646
+ .cat .cnt {{ font-size:0.68rem; color:#94a3b8; font-variant-numeric:tabular-nums; }}
647
+ .cat.off .name {{ color:#b6c0cc; text-decoration:line-through; }}
648
+ .cat.off .dot {{ opacity:0.35; }}
649
+ /* ── Stage ── */
650
+ .stage {{
651
+ position:relative; flex:1; min-width:0; border-radius:11px; overflow:hidden;
652
+ background:#fafbfd; border:1px solid rgba(15,23,42,0.08);
653
+ }}
654
+ canvas {{ display:block; width:100%; height:100%; cursor:grab; }}
655
+ canvas.dragging {{ cursor:grabbing; }}
656
+ .card {{
657
+ position:absolute; left:12px; top:12px; width:250px; max-height:calc(100% - 24px);
658
+ overflow:auto; background:rgba(255,255,255,0.97); border:1px solid rgba(15,23,42,0.10);
659
+ border-radius:10px; padding:0.7rem 0.75rem; box-shadow:0 10px 30px rgba(15,23,42,0.10);
660
+ display:none; z-index:2;
661
+ }}
662
+ .card.open {{ display:block; }}
663
+ .card-type {{ font-size:0.66rem; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:#4F6EF7; margin-bottom:0.25rem; }}
664
+ .card-title {{ font-size:0.9rem; font-weight:700; line-height:1.3; margin:0 0 0.4rem 0; color:#13201d; }}
665
+ .card-detail {{ font-size:0.78rem; line-height:1.45; color:#3d4f4a; margin:0; white-space:pre-wrap; }}
666
+ .card-close {{ position:absolute; right:8px; top:6px; border:0; background:transparent; color:#6a7a74; font-size:1rem; cursor:pointer; }}
667
+ .hint {{ position:absolute; left:10px; bottom:8px; font-size:0.66rem; color:#94a3b8; pointer-events:none;
668
+ background:rgba(255,255,255,0.9); padding:0.2rem 0.45rem; border-radius:6px; }}
669
+ </style>
670
+ </head>
671
+ <body>
672
+ <div class="wrap">
673
+ <div class="kicker">Workflow</div>
674
+ <div class="row">
675
+ <aside class="rail">
676
+ <div class="rail-head">Graph controls</div>
677
+ <div class="rail-body">
678
+ <div class="rail-search">
679
+ <input id="q" type="text" placeholder="Highlight nodes" autocomplete="off" />
680
+ <button class="clr" id="qClr" title="Clear" style="display:none">×</button>
681
+ </div>
682
+ <div class="rail-actions">
683
+ <button id="fitBtn">Fit view</button>
684
+ <button id="resetBtn">Reset</button>
685
+ </div>
686
+ <div class="sec-head">
687
+ <span class="lbl">Node categories</span>
688
+ <span class="acts">
689
+ <button id="allBtn">All</button>
690
+ <button id="noneBtn">None</button>
691
+ </span>
692
+ </div>
693
+ <div id="cats"></div>
694
+ </div>
695
+ </aside>
696
+ <div class="stage" id="stage">
697
+ <canvas id="cv"></canvas>
698
+ <div id="card" class="card">
699
+ <button class="card-close" id="closeBtn" aria-label="Close">×</button>
700
+ <div class="card-type" id="cardType"></div>
701
+ <div class="card-title" id="cardTitle"></div>
702
+ <p class="card-detail" id="cardDetail"></p>
703
+ </div>
704
+ <div class="hint">Drag to move · scroll to zoom · click a node for details</div>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ <script>
709
+ const payload = {payload};
710
+ const KM = payload.kindMeta || {{}};
711
+ const stage = document.getElementById('stage');
712
+ const canvas = document.getElementById('cv');
713
+ const ctx = canvas.getContext('2d');
714
+ const card = document.getElementById('card');
715
+ const cardType = document.getElementById('cardType');
716
+ const cardTitle = document.getElementById('cardTitle');
717
+ const cardDetail = document.getElementById('cardDetail');
718
+ document.getElementById('closeBtn').onclick = () => {{ selected = null; card.classList.remove('open'); }};
719
+
720
+ let W = 0, H = 0, dpr = 1;
721
+ function resize() {{
722
+ dpr = window.devicePixelRatio || 1;
723
+ W = stage.clientWidth; H = stage.clientHeight;
724
+ canvas.width = Math.floor(W * dpr);
725
+ canvas.height = Math.floor(H * dpr);
726
+ canvas.style.width = W + 'px';
727
+ canvas.style.height = H + 'px';
728
+ }}
729
+ resize();
730
+ window.addEventListener('resize', () => {{ resize(); }});
731
+
732
+ // ── Position memory across reruns (Mina keeps xy so the graph grows, not jumps).
733
+ const memKey = 'scipaths-mina-pos';
734
+ let saved = {{}};
735
+ try {{ saved = JSON.parse(sessionStorage.getItem(memKey) || '{{}}'); }} catch (e) {{ saved = {{}}; }}
736
+
737
+ // Mutable graph state — grown in place by incremental postMessage updates.
738
+ let nodes = [];
739
+ const byId = new Map();
740
+ let edges = [];
741
+ let target = null;
742
+
743
+ // ── Filter / search state ──
744
+ const hiddenKinds = new Set();
745
+ let query = '';
746
+ const isVisible = (n) => !hiddenKinds.has(n.kind);
747
+
748
+ // ── Write pulse — Mina: staggered one-shot expanding rings + travelling dashes.
749
+ const PULSE_RING_MS = 1200;
750
+ const PULSE_RING_OFFSETS = [0, 0.35];
751
+ const PULSE_LIFETIME_MS = PULSE_RING_MS * (1 + Math.max(...PULSE_RING_OFFSETS));
752
+ const PULSE_DASH = [7, 5];
753
+ let pulseNodeIds = new Set();
754
+ let pulseEdgeIds = new Set();
755
+ let pulseStart = 0;
756
+
757
+ // ── View transform (zoom + pan), Mina-style ──
758
+ let zoom = 0.95;
759
+ let panX = (W / 2) * (1 - zoom);
760
+ let panY = (H / 2) * (1 - zoom);
761
+ let alpha = 1;
762
+ const toWorld = (px, py) => ({{ x: (px - panX) / zoom, y: (py - panY) / zoom }});
763
+
764
+ let dragNode = null, panning = false, moved = false;
765
+ let last = {{ x: 0, y: 0 }};
766
+ let selected = null;
767
+
768
+ function persist() {{
769
+ const out = {{}};
770
+ nodes.forEach(n => {{ out[n.id] = {{ x: n.x, y: n.y }}; }});
771
+ try {{ sessionStorage.setItem(memKey, JSON.stringify(out)); }} catch (e) {{}}
772
+ }}
773
+
774
+ function nodeAt(px, py) {{
775
+ const w = toWorld(px, py);
776
+ for (let i = nodes.length - 1; i >= 0; i--) {{
777
+ const n = nodes[i];
778
+ if (!isVisible(n)) continue;
779
+ const dx = n.x - w.x, dy = n.y - w.y;
780
+ if (dx * dx + dy * dy <= (n.r + 5) * (n.r + 5)) return n;
781
+ }}
782
+ return null;
783
+ }}
784
+ const getPos = (ev) => {{ const r = canvas.getBoundingClientRect(); return {{ x: ev.clientX - r.left, y: ev.clientY - r.top }}; }};
785
+
786
+ canvas.addEventListener('mousedown', (ev) => {{
787
+ const p = getPos(ev); const n = nodeAt(p.x, p.y); moved = false;
788
+ if (n) {{ dragNode = n; alpha = Math.max(alpha, 0.4); }} else {{ panning = true; }}
789
+ last = p; canvas.classList.add('dragging');
790
+ }});
791
+ canvas.addEventListener('mousemove', (ev) => {{
792
+ const p = getPos(ev);
793
+ if (dragNode) {{ const w = toWorld(p.x, p.y); dragNode.x = w.x; dragNode.y = w.y; dragNode.vx = 0; dragNode.vy = 0; moved = true; }}
794
+ else if (panning) {{ panX += p.x - last.x; panY += p.y - last.y; moved = true; }}
795
+ else {{ canvas.style.cursor = nodeAt(p.x, p.y) ? 'pointer' : 'grab'; }}
796
+ last = p;
797
+ }});
798
+ window.addEventListener('mouseup', (ev) => {{
799
+ if (ev.target === canvas && !moved) {{
800
+ const p = getPos(ev); const n = nodeAt(p.x, p.y);
801
+ if (n) {{
802
+ selected = n;
803
+ cardType.textContent = n.typeLabel || n.kind || 'Node';
804
+ cardTitle.textContent = n.fullTitle || n.label || '';
805
+ cardDetail.textContent = n.detail || 'No additional detail for this node.';
806
+ card.classList.add('open');
807
+ }} else {{ selected = null; card.classList.remove('open'); }}
808
+ }}
809
+ if (dragNode) persist();
810
+ dragNode = null; panning = false; canvas.classList.remove('dragging');
811
+ }});
812
+ canvas.addEventListener('wheel', (ev) => {{
813
+ ev.preventDefault();
814
+ const p = getPos(ev); const before = toWorld(p.x, p.y);
815
+ const factor = ev.deltaY < 0 ? 1.1 : 0.9;
816
+ zoom = Math.max(0.3, Math.min(2.5, zoom * factor));
817
+ panX = p.x - before.x * zoom; panY = p.y - before.y * zoom;
818
+ }}, {{ passive: false }});
819
+
820
+ // ── Force layout — Mina model: inverse-square repulsion + springs +
821
+ // center gravity + collision relaxation + annealing alpha (looser). ──
822
+ function tick() {{
823
+ const N = nodes.length;
824
+ for (let i = 0; i < N; i++) {{
825
+ for (let j = i + 1; j < N; j++) {{
826
+ const a = nodes[i], b = nodes[j];
827
+ let dx = b.x - a.x, dy = b.y - a.y;
828
+ let d2 = dx * dx + dy * dy; if (d2 < 1) d2 = 1;
829
+ const d = Math.sqrt(d2);
830
+ const force = (5600 * alpha) / d2;
831
+ dx /= d; dy /= d;
832
+ if (a !== dragNode && !a.fixed) {{ a.vx -= dx * force; a.vy -= dy * force; }}
833
+ if (b !== dragNode && !b.fixed) {{ b.vx += dx * force; b.vy += dy * force; }}
834
+ }}
835
+ }}
836
+ edges.forEach(e => {{
837
+ const a = byId.get(e.source), b = byId.get(e.target);
838
+ if (!a || !b) return;
839
+ const ideal = 155;
840
+ let dx = b.x - a.x, dy = b.y - a.y;
841
+ const d = Math.max(1, Math.sqrt(dx * dx + dy * dy));
842
+ const force = ((d - ideal) / d) * 0.045 * alpha;
843
+ dx *= force; dy *= force;
844
+ if (!a.fixed && a !== dragNode) {{ a.vx += dx; a.vy += dy; }}
845
+ if (!b.fixed && b !== dragNode) {{ b.vx -= dx; b.vy -= dy; }}
846
+ }});
847
+ const cx = W / 2, cy = H / 2;
848
+ nodes.forEach(n => {{
849
+ if (n.fixed || n === dragNode) return;
850
+ n.vx += (cx - n.x) * 0.0042 * alpha;
851
+ n.vy += (cy - n.y) * 0.0042 * alpha;
852
+ n.vx *= 0.86; n.vy *= 0.86;
853
+ n.x += n.vx; n.y += n.vy;
854
+ }});
855
+ // Hard collision relaxation so nothing overlaps (with label breathing room).
856
+ for (let i = 0; i < N; i++) {{
857
+ for (let j = i + 1; j < N; j++) {{
858
+ const a = nodes[i], b = nodes[j];
859
+ const dx = b.x - a.x, dy = b.y - a.y;
860
+ const min = a.r + b.r + 18;
861
+ const d = Math.sqrt(dx * dx + dy * dy) || 0.01;
862
+ if (d < min) {{
863
+ const push = (min - d) / d;
864
+ const ox = dx * push * 0.5, oy = dy * push * 0.5;
865
+ if (a !== dragNode && !a.fixed) {{ a.x -= ox; a.y -= oy; }}
866
+ if (b !== dragNode && !b.fixed) {{ b.x += ox; b.y += oy; }}
867
+ }}
868
+ }}
869
+ }}
870
+ if (target) {{ target.x = cx; target.y = cy; target.vx = 0; target.vy = 0; }}
871
+ alpha = Math.max(0.02, alpha * 0.9955);
872
+ for (const n of nodes) saved[n.id] = {{ x: n.x, y: n.y }};
873
+ }}
874
+
875
+ function draw() {{
876
+ tick();
877
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
878
+ ctx.clearRect(0, 0, W, H);
879
+ ctx.fillStyle = '#fafbfd';
880
+ ctx.fillRect(0, 0, W, H);
881
+ ctx.translate(panX, panY);
882
+ ctx.scale(zoom, zoom);
883
+
884
+ const q = query.trim().toLowerCase();
885
+ const pulseAge = performance.now() - pulseStart;
886
+ const pulseAlive = pulseAge < PULSE_LIFETIME_MS;
887
+ const pulseFade = pulseAlive ? 1 - pulseAge / PULSE_LIFETIME_MS : 0;
888
+
889
+ // edges
890
+ edges.forEach(e => {{
891
+ const a = byId.get(e.source), b = byId.get(e.target);
892
+ if (!a || !b || !isVisible(a) || !isVisible(b)) return;
893
+ const pulsing = pulseAlive && pulseEdgeIds.has(e.id);
894
+ ctx.beginPath();
895
+ ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
896
+ ctx.strokeStyle = e.muted ? 'rgba(148,163,184,0.30)' : 'rgba(100,116,139,0.42)';
897
+ ctx.lineWidth = (e.muted ? 1 : 1.4) + (pulsing ? 1.2 * pulseFade : 0);
898
+ if (pulsing) {{
899
+ ctx.setLineDash(PULSE_DASH);
900
+ ctx.lineDashOffset = -((pulseAge / 26) % (PULSE_DASH[0] + PULSE_DASH[1]));
901
+ }} else {{
902
+ ctx.setLineDash(e.muted ? [3, 3] : []);
903
+ ctx.lineDashOffset = 0;
904
+ }}
905
+ ctx.stroke();
906
+ ctx.setLineDash([]); ctx.lineDashOffset = 0;
907
+ }});
908
+
909
+ // nodes — exact Mina draw order
910
+ nodes.forEach(n => {{
911
+ if (!isVisible(n)) return;
912
+ const x = n.x, y = n.y, r = n.r;
913
+ const isSelected = selected && selected.id === n.id;
914
+ const dimmed = q.length > 0 && !String(n.label || '').toLowerCase().includes(q);
915
+ ctx.globalAlpha = dimmed ? 0.15 : 1;
916
+
917
+ if (pulseAlive && pulseNodeIds.has(n.id)) {{
918
+ for (const offset of PULSE_RING_OFFSETS) {{
919
+ const p = pulseAge / PULSE_RING_MS - offset;
920
+ if (p <= 0 || p >= 1) continue;
921
+ ctx.beginPath();
922
+ ctx.arc(x, y, r + 3 + p * 26, 0, Math.PI * 2);
923
+ ctx.strokeStyle = n.fill;
924
+ ctx.globalAlpha = (1 - p) * 0.55;
925
+ ctx.lineWidth = 2.5 * (1 - p) + 0.5;
926
+ ctx.stroke();
927
+ }}
928
+ ctx.globalAlpha = dimmed ? 0.15 : 1;
929
+ }}
930
+
931
+ if (isSelected) {{
932
+ ctx.beginPath();
933
+ ctx.arc(x, y, r + 7, 0, Math.PI * 2);
934
+ ctx.fillStyle = n.fill + '22'; ctx.fill();
935
+ ctx.beginPath();
936
+ ctx.arc(x, y, r + 5, 0, Math.PI * 2);
937
+ ctx.strokeStyle = n.ring; ctx.lineWidth = 1.5; ctx.stroke();
938
+ }}
939
+
940
+ ctx.beginPath();
941
+ ctx.arc(x, y, r, 0, Math.PI * 2);
942
+ ctx.fillStyle = n.fill; ctx.fill();
943
+ ctx.lineWidth = 2; ctx.strokeStyle = '#fff'; ctx.stroke();
944
+ ctx.beginPath();
945
+ ctx.arc(x, y, r + 1.5, 0, Math.PI * 2);
946
+ ctx.lineWidth = 1.25; ctx.strokeStyle = n.ring; ctx.stroke();
947
+
948
+ const raw = String(n.label || '');
949
+ const label = raw.length > 26 ? raw.slice(0, 25) + '…' : raw;
950
+ const weight = n.kind === 'target' ? '700 12px' : (n.kind === 'cluster' ? '600 11px' : '500 10px');
951
+ ctx.font = weight + ' Inter, sans-serif';
952
+ ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic';
953
+ ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(250,251,253,0.9)';
954
+ ctx.strokeText(label, x, y + r + 13);
955
+ ctx.fillStyle = n.text;
956
+ ctx.fillText(label, x, y + r + 13);
957
+ ctx.globalAlpha = 1;
958
+ }});
959
+
960
+ requestAnimationFrame(draw);
961
+ }}
962
+ requestAnimationFrame(draw);
963
+ setTimeout(persist, 900);
964
+
965
+ // ── Fit / reset view ──
966
+ function fitView() {{
967
+ const vis = nodes.filter(isVisible);
968
+ if (!vis.length) return;
969
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
970
+ vis.forEach(n => {{ minX = Math.min(minX, n.x - n.r); minY = Math.min(minY, n.y - n.r);
971
+ maxX = Math.max(maxX, n.x + n.r + 20); maxY = Math.max(maxY, n.y + n.r + 20); }});
972
+ const gw = Math.max(1, maxX - minX), gh = Math.max(1, maxY - minY);
973
+ zoom = Math.max(0.3, Math.min(1.6, Math.min((W - 40) / gw, (H - 40) / gh)));
974
+ panX = W / 2 - ((minX + maxX) / 2) * zoom;
975
+ panY = H / 2 - ((minY + maxY) / 2) * zoom;
976
+ }}
977
+ document.getElementById('fitBtn').onclick = fitView;
978
+ document.getElementById('resetBtn').onclick = () => {{
979
+ hiddenKinds.clear(); query = ''; document.getElementById('q').value = '';
980
+ document.getElementById('qClr').style.display = 'none';
981
+ selected = null; card.classList.remove('open');
982
+ try {{ sessionStorage.removeItem(memKey); }} catch (e) {{}}
983
+ alpha = 1; renderCats();
984
+ const cx = W / 2, cy = H / 2;
985
+ nodes.forEach((n, i) => {{
986
+ const a = (i / Math.max(nodes.length, 1)) * Math.PI * 2;
987
+ n.x = n.id === 'target' ? cx : cx + Math.cos(a) * (90 + Math.random() * 70);
988
+ n.y = n.id === 'target' ? cy : cy + Math.sin(a) * (90 + Math.random() * 70);
989
+ n.vx = 0; n.vy = 0;
990
+ }});
991
+ zoom = 0.95; panX = (W / 2) * (1 - zoom); panY = (H / 2) * (1 - zoom);
992
+ }};
993
+
994
+ // ── Search ──
995
+ const qInput = document.getElementById('q');
996
+ const qClr = document.getElementById('qClr');
997
+ qInput.addEventListener('input', () => {{ query = qInput.value; qClr.style.display = query ? 'block' : 'none'; }});
998
+ qClr.onclick = () => {{ query = ''; qInput.value = ''; qClr.style.display = 'none'; }};
999
+
1000
+ // ── Node category filter rail ──
1001
+ function counts() {{
1002
+ const m = {{}};
1003
+ nodes.forEach(n => {{ m[n.kind] = (m[n.kind] || 0) + 1; }});
1004
+ return m;
1005
+ }}
1006
+ function renderCats() {{
1007
+ const c = counts();
1008
+ const order = (payload.kindOrder || Object.keys(KM)).filter(k => (c[k] || 0) > 0);
1009
+ const host = document.getElementById('cats');
1010
+ host.innerHTML = '';
1011
+ order.forEach(k => {{
1012
+ const meta = KM[k] || {{ fill: '#94a3b8', ring: '#e2e8f0', label: k }};
1013
+ const on = !hiddenKinds.has(k);
1014
+ const btn = document.createElement('button');
1015
+ btn.className = 'cat' + (on ? '' : ' off');
1016
+ btn.innerHTML =
1017
+ '<span class="box' + (on ? ' on' : '') + '">' + (on ? '✓' : '') + '</span>' +
1018
+ '<span class="dot" style="background:' + meta.fill + ';box-shadow:0 0 0 2px ' + meta.ring + '"></span>' +
1019
+ '<span class="name">' + meta.label + '</span>' +
1020
+ '<span class="cnt">' + (c[k] || 0) + '</span>';
1021
+ btn.onclick = () => {{
1022
+ if (hiddenKinds.has(k)) hiddenKinds.delete(k); else hiddenKinds.add(k);
1023
+ alpha = Math.max(alpha, 0.3); renderCats();
1024
+ }};
1025
+ host.appendChild(btn);
1026
+ }});
1027
+ }}
1028
+ document.getElementById('allBtn').onclick = () => {{ hiddenKinds.clear(); alpha = Math.max(alpha, 0.3); renderCats(); }};
1029
+ document.getElementById('noneBtn').onclick = () => {{
1030
+ Object.keys(counts()).forEach(k => hiddenKinds.add(k)); renderCats();
1031
+ }};
1032
+ renderCats();
1033
+
1034
+ // ── Incremental model merge (Mina reveal): add new nodes near a neighbor,
1035
+ // keep existing positions, pulse writes, drop nodes no longer present. ──
1036
+ let lastSig = '';
1037
+ function applyModel(model) {{
1038
+ if (!model) return;
1039
+ const inNodes = model.nodes || [];
1040
+ const inEdges = model.edges || [];
1041
+ const sig = inNodes.map(n => n.id + (n.pulse ? '*' : '')).join(',') + '|' + inEdges.map(e => e.id).join(',');
1042
+ if (sig === lastSig) return;
1043
+ lastSig = sig;
1044
+ if (model.kindMeta) Object.assign(KM, model.kindMeta);
1045
+ if (model.kindOrder) payload.kindOrder = model.kindOrder;
1046
+ const incIds = new Set(inNodes.map(n => n.id));
1047
+ const adj = {{}};
1048
+ inEdges.forEach(e => {{ (adj[e.source] = adj[e.source] || []).push(e.target); (adj[e.target] = adj[e.target] || []).push(e.source); }});
1049
+ const pN = new Set(), pE = new Set();
1050
+ let added = false;
1051
+ inNodes.forEach(n => {{
1052
+ let nd = byId.get(n.id);
1053
+ if (nd) {{
1054
+ nd.label = n.label; nd.kind = n.kind; nd.fill = n.fill; nd.ring = n.ring;
1055
+ nd.text = n.text; nd.r = n.r; nd.typeLabel = n.typeLabel; nd.fullTitle = n.fullTitle; nd.detail = n.detail;
1056
+ if (n.pulse) pN.add(n.id);
1057
+ }} else {{
1058
+ const prev = saved[n.id];
1059
+ let x, y;
1060
+ if (prev) {{ x = prev.x; y = prev.y; }}
1061
+ else {{
1062
+ const nb = (adj[n.id] || []).map(id => byId.get(id)).find(Boolean);
1063
+ const base = nb ? {{ x: nb.x, y: nb.y }} : {{ x: W / 2, y: H / 2 }};
1064
+ const a = Math.random() * Math.PI * 2, rr = 70 + Math.random() * 60;
1065
+ x = base.x + Math.cos(a) * rr; y = base.y + Math.sin(a) * rr;
1066
+ }}
1067
+ nd = Object.assign({{}}, n, {{ x, y, vx: 0, vy: 0, fixed: n.id === 'target' }});
1068
+ nodes.push(nd); byId.set(n.id, nd); added = true; pN.add(n.id);
1069
+ }}
1070
+ }});
1071
+ for (let i = nodes.length - 1; i >= 0; i--) {{
1072
+ if (!incIds.has(nodes[i].id)) {{ byId.delete(nodes[i].id); nodes.splice(i, 1); }}
1073
+ }}
1074
+ edges = inEdges.filter(e => byId.get(e.source) && byId.get(e.target)).map(e => Object.assign({{}}, e));
1075
+ edges.forEach(e => {{ if (e.pulse) pE.add(e.id); }});
1076
+ pulseNodeIds = pN; pulseEdgeIds = pE;
1077
+ if (pN.size || pE.size || added) {{ pulseStart = performance.now(); alpha = Math.max(alpha, 0.55); }}
1078
+ target = byId.get('target'); if (target) target.fixed = true;
1079
+ renderCats();
1080
+ }}
1081
+ window.addEventListener('message', (e) => {{
1082
+ const d = e.data;
1083
+ if (d && d.type === 'scipaths-graph') applyModel(d.model);
1084
+ }});
1085
+ if (payload.nodes && payload.nodes.length) applyModel(payload);
1086
+ </script>
1087
+ </body>
1088
+ </html>"""
1089
+
1090
+
1091
+ def sync_and_load_graph(run_id: str, model: dict[str, Any]) -> dict[str, Any]:
1092
+ """Push cumulative graph to Neo4j and read it back for rendering."""
1093
+ if not run_id or neo4j_store is None:
1094
+ model = dict(model)
1095
+ model["backend"] = "memory"
1096
+ return model
1097
+ nodes = model.get("nodes") or []
1098
+ edges = model.get("edges") or []
1099
+ ok = neo4j_store.upsert_graph(run_id, nodes, edges)
1100
+ if not ok:
1101
+ model = dict(model)
1102
+ model["backend"] = "memory"
1103
+ return model
1104
+ fetched = neo4j_store.fetch_graph(run_id)
1105
+ if not fetched:
1106
+ model = dict(model)
1107
+ model["backend"] = "neo4j-write"
1108
+ return model
1109
+ # Preserve caption/step and enrich fetched nodes with Mina colors.
1110
+ out_nodes = []
1111
+ for n in fetched.get("nodes") or []:
1112
+ kind = str(n.get("kind") or "citing")
1113
+ meta = KIND_META.get(kind, KIND_META["citing"])
1114
+ out_nodes.append(
1115
+ {
1116
+ **n,
1117
+ "type_label": meta["type_label"],
1118
+ "color": n.get("color") or meta["fill"],
1119
+ "fill": n.get("fill") or meta["fill"],
1120
+ "ring": n.get("ring") or meta["ring"],
1121
+ "text": n.get("text") or meta["text"],
1122
+ "r": n.get("r") or meta["r"],
1123
+ "size": n.get("size") or meta["r"],
1124
+ }
1125
+ )
1126
+ return {
1127
+ "step": model.get("step"),
1128
+ "pulse_step": model.get("pulse_step"),
1129
+ "caption": model.get("caption"),
1130
+ "nodes": out_nodes,
1131
+ "edges": fetched.get("edges") or [],
1132
+ "backend": "neo4j",
1133
+ }
1134
+
1135
+
1136
+ def build_synced_model(
1137
+ *,
1138
+ paper_dir: Optional[Path],
1139
+ payload: Optional[dict],
1140
+ events: list[str],
1141
+ run_id: str = "",
1142
+ ) -> dict[str, Any]:
1143
+ """Build the cumulative graph model for the given events and sync to Neo4j."""
1144
+ visible = visible_step_from_events(events)
1145
+ pulse = pulse_step_from_events(events)
1146
+ joined = " ".join(str(e) for e in (events or []))
1147
+ if "Pipeline completed successfully." in joined or (
1148
+ "Step 8 complete" in joined or ("annotation" in joined.lower() and "complete" in joined.lower())
1149
+ ):
1150
+ if visible >= 8:
1151
+ pulse = 0
1152
+ visible = 8
1153
+
1154
+ model = build_graph_model(
1155
+ paper_dir=paper_dir,
1156
+ payload=payload,
1157
+ visible_step=visible,
1158
+ pulse_step=pulse,
1159
+ )
1160
+ return sync_and_load_graph(run_id or "default", model)
1161
+
1162
+
1163
+ def render_graph_shell(placeholder, *, height: int = 520) -> None:
1164
+ """Mount the persistent, model-free canvas shell (constant HTML → no remount)."""
1165
+ html = graph_shell_html(height)
1166
+ if placeholder is None:
1167
+ st.iframe(html, height=height)
1168
+ return
1169
+ with placeholder.container():
1170
+ st.iframe(html, height=height)
1171
+
1172
+
1173
+ def push_graph_update(courier_slot, model: dict[str, Any], *, height: int = 0) -> None:
1174
+ """Push a model into the mounted shell via an invisible courier frame."""
1175
+ html = courier_html(_graph_payload(model))
1176
+ if courier_slot is None:
1177
+ st.components.v1.html(html, height=height)
1178
+ return
1179
+ with courier_slot.container():
1180
+ st.components.v1.html(html, height=height)
1181
+
1182
+
1183
+ def render_workflow_into(
1184
+ placeholder,
1185
+ *,
1186
+ paper_dir: Optional[Path],
1187
+ payload: Optional[dict],
1188
+ events: list[str],
1189
+ run_id: str = "",
1190
+ height: int = 520,
1191
+ ) -> None:
1192
+ """Back-compat single-slot render: mount shell then push once into it."""
1193
+ render_graph_shell(placeholder, height=height)
1194
+ model = build_synced_model(
1195
+ paper_dir=paper_dir, payload=payload, events=events, run_id=run_id
1196
+ )
1197
+ push_graph_update(None, model)
replay_traces/2208.00329/input_ids.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "2208.00329",
4
+ "title": "",
5
+ "id_type": "ArXiv"
6
+ }
7
+ ]
replay_traces/2208.00329/logs/step_01.log ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_01_fetch/fetch_metadata.py --ids /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/input_ids.json --outdir /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
2
+
3
+ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/src/step_01_fetch/fetch_metadata.py:95: DeprecationWarning: The 'Search.results' method is deprecated, use 'Client.results' instead
4
+ paper = next(search.results())
5
+ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/src/step_01_fetch/fetch_metadata.py:124: DeprecationWarning: Python 3.14 will, by default, filter extracted tar archives and reject files or modify their metadata. Use the filter argument to control this behavior.
6
+ tar.extractall(path=source_dir)
7
+ [INFO] Using ID list from /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/input_ids.json
8
+ [INFO] Output will be saved to /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
9
+ [DEBUG] fetch_one_acl_id: id=2208.00329 id_type=ArXiv title_len=0 s2_key_present=yes s2_key_len=44
10
+ [DEBUG] wrote metadata to /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers/2208.00329/paper_metadata.json
11
+ [INFO] Wrote combined BibTeX to /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers/2208.00329/references.bib
12
+ [INFO] Processed LaTeX for 2208.00329 at /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers/2208.00329/processed_main.tex
13
+ [WARN] 429 Too Many Requests → retrying in 1.50s
14
+ [WARN] 429 Too Many Requests → retrying in 2.15s
15
+ [WARN] 429 Too Many Requests → retrying in 4.24s
16
+ [WARN] 429 Too Many Requests → retrying in 8.26s
17
+ [SUCCESS]
18
+ done in 28.508277893066406 s
replay_traces/2208.00329/logs/step_02.log ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_02_mark_citations/replace_citation_markers.py --root /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
2
+
3
+ [INFO] Found 1 paper dirs under /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
4
+ [OK] 2208.00329: updated 19 contexts over 20
5
+ [SUMMARY] total_updated=19 over 20; skipped_incomplete=0
replay_traces/2208.00329/logs/step_03.log ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_03_usage_contexts/build_usage_contexts.py --root /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers --out-name usage_contexts.json
2
+
3
+ [INFO] Found 1 paper dirs under /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
4
+ [OK] 2208.00329: wrote usage_contexts.json (20 contexts from 7 citations)
replay_traces/2208.00329/logs/step_04.log ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_04_label_citations/label_citation_functions.py --root /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers --model-path Deep-Citation/Workspace/acl_scicite_wksp_trl/best_model.pt --model-data-dir Deep-Citation/Data --model-class-def Deep-Citation/Data/class_def.json --model-lm scibert --device cpu
2
+
3
+ [INFO] Found 1 paper dirs under /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
4
+ Number of data instance: 1941
5
+ [LABELED] 2208.00329
6
+ [SUMMARY] labeled=1, skipped=0, missing_usage=0, empty_contexts=0
replay_traces/2208.00329/logs/step_05.log ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_05_verify_uses_extends/verify_uses_extends.py --root /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers --k 0 --batch-size 25
2
+
3
+ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/src/common/llm_client.py:4: FutureWarning:
4
+
5
+ All support for the `google.generativeai` package has ended. It will no longer be receiving
6
+ updates or bug fixes. Please switch to the `google.genai` package as soon as possible.
7
+ See README for more details:
8
+
9
+ https://github.com/google-gemini/deprecated-generative-ai-python/blob/main/README.md
10
+
11
+ import google.generativeai as genai
12
+ [INFO] Found 1 paper dirs under /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
13
+ [VERIFIED] 2208.00329
14
+ [SUMMARY] verified=1, skipped=0, missing_labels=0, no_candidates=0
replay_traces/2208.00329/logs/step_06.log ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_06_extract_paragraphs/extract_arxiv_paragraphs.py --root /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
2
+
3
+ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/src/step_06_extract_paragraphs/extract_arxiv_paragraphs.py:47: DeprecationWarning: Python 3.14 will, by default, filter extracted tar archives and reject files or modify their metadata. Use the filter argument to control this behavior.
4
+ tar.extractall(path)
5
+ [INFO] Found 1 paper dirs under /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
6
+ [PROCESSED] 2208.00329
7
+ [SUMMARY] processed=1, skipped=0, missing_usage=0, missing_verified=0, empty_citing=0
replay_traces/2208.00329/logs/step_07.log ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_07_extract_and_refine/extract_contributions_from_citations.py --root /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
2
+
3
+ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/src/common/llm_client.py:4: FutureWarning:
4
+
5
+ All support for the `google.generativeai` package has ended. It will no longer be receiving
6
+ updates or bug fixes. Please switch to the `google.genai` package as soon as possible.
7
+ See README for more details:
8
+
9
+ https://github.com/google-gemini/deprecated-generative-ai-python/blob/main/README.md
10
+
11
+ import google.generativeai as genai
12
+ [INFO] Found 1 paper dirs under /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
13
+ [LABELED] 2208.00329
14
+ [SUMMARY] labeled=1, skipped=0, missing_verified=0, missing_arxiv_paragraphs=0, no_confirmed=0
15
+ $ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/.venv/bin/python src/step_07_extract_and_refine/refine_and_filter_clusters_llm.py --root /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers --inplace --overwrite
16
+
17
+ /Users/dingzifeng/SciFy-UMBC/demo_2026_07/src/common/llm_client.py:4: FutureWarning:
18
+
19
+ All support for the `google.generativeai` package has ended. It will no longer be receiving
20
+ updates or bug fixes. Please switch to the `google.genai` package as soon as possible.
21
+ See README for more details:
22
+
23
+ https://github.com/google-gemini/deprecated-generative-ai-python/blob/main/README.md
24
+
25
+ import google.generativeai as genai
26
+ [INFO] Found 1 paper dirs under /Users/dingzifeng/SciFy-UMBC/demo_2026_07/hf_space/runs/replay_build/job_1785099279_8df5ecbb/processed_papers
27
+ [REFINED] 2208.00329
28
+ [SUMMARY] refined=1, skipped=0, missing_inputs=0, empty_clusters=0
replay_traces/2208.00329/processed_papers/2208.00329/paper_metadata.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "paperId": "e894fb15054d3bc9659060406a12dfd1055ae32e",
4
+ "externalIds": {
5
+ "DBLP": "journals/corr/abs-2208-00329",
6
+ "ArXiv": "2208.00329",
7
+ "DOI": "10.1162/tacl_a_00600",
8
+ "CorpusId": 251223896
9
+ },
10
+ "url": "https://www.semanticscholar.org/paper/e894fb15054d3bc9659060406a12dfd1055ae32e",
11
+ "title": "PASTA: A Dataset for Modeling PArticipant STAtes in Narratives",
12
+ "venue": "Transactions of the Association for Computational Linguistics",
13
+ "year": 2022,
14
+ "referenceCount": 59,
15
+ "citationCount": 7,
16
+ "openAccessPdf": {
17
+ "url": "https://direct.mit.edu/tacl/article-pdf/doi/10.1162/tacl_a_00600/2173956/tacl_a_00600.pdf",
18
+ "status": "GOLD",
19
+ "license": "CCBY",
20
+ "disclaimer": "Notice: Paper or abstract available at https://arxiv.org/abs/2208.00329, which is subject to the license by the author or copyright owner provided with this content. Please go to the source to verify the license and copyright information for your use."
21
+ },
22
+ "tldr": {
23
+ "model": "tldr@v2.0.0",
24
+ "text": "This work introduces a new crowdsourced English-language, Participant States dataset, PASTA, and introduces three state-based reasoning tasks that test for the ability to infer when a state is entailed by a story, to revise a story conditioned on a counterfactual state, and to explain the most likely state change given a revised story."
25
+ },
26
+ "publicationDate": "2022-07-31",
27
+ "authors": [
28
+ {
29
+ "authorId": "72059959",
30
+ "name": "Sayontan Ghosh"
31
+ },
32
+ {
33
+ "authorId": "144007901",
34
+ "name": "Mahnaz Koupaee"
35
+ },
36
+ {
37
+ "authorId": "2254303864",
38
+ "name": "I-Sen Chen"
39
+ },
40
+ {
41
+ "authorId": "2034063",
42
+ "name": "Francis Ferraro"
43
+ },
44
+ {
45
+ "authorId": "1729918",
46
+ "name": "Nathanael Chambers"
47
+ },
48
+ {
49
+ "authorId": "35217367",
50
+ "name": "Niranjan Balasubramanian"
51
+ }
52
+ ],
53
+ "abstract": "Abstract The events in a narrative are understood as a coherent whole via the underlying states of their participants. Often, these participant states are not explicitly mentioned, instead left to be inferred by the reader. A model that understands narratives should likewise infer these implicit states, and even reason about the impact of changes to these states on the narrative. To facilitate this goal, we introduce a new crowdsourced English-language, Participant States dataset, PASTA. This dataset contains inferable participant states; a counterfactual perturbation to each state; and the changes to the story that would be necessary if the counterfactual were true. We introduce three state-based reasoning tasks that test for the ability to infer when a state is entailed by a story, to revise a story conditioned on a counterfactual state, and to explain the most likely state change given a revised story. Experiments show that today\u2019s LLMs can reason about states to some degree, but there is large room for improvement, especially in problems requiring access and ability to reason with diverse types of knowledge (e.g., physical, numerical, factual).1"
54
+ }
55
+ ]
replay_traces/2208.00329/processed_papers/2208.00329/usage_citing_paragraphs.json ADDED
The diff for this file is too large to render. See raw diff
 
replay_traces/2208.00329/processed_papers/2208.00329/usage_context_labels.json ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "paper_id": "2208.00329",
3
+ "num_contexts": 20,
4
+ "label_set": [
5
+ "Background",
6
+ "Uses",
7
+ "Extends",
8
+ "CompareOrContrast",
9
+ "Motivation",
10
+ "Future"
11
+ ],
12
+ "labels": [
13
+ {
14
+ "id": 1,
15
+ "label": "Uses",
16
+ "confidence": 0.9254917502403259,
17
+ "confidence_margin": 0.8626327514648438,
18
+ "cue_span": "",
19
+ "rationale": "scibert_model",
20
+ "citing_paper_id": "c1d02fc3786fb5e5b0543703af95046253109d4e",
21
+ "citing_title": "Inductive Bias Extraction and Matching for LLM Prompts",
22
+ "text": "The PASTA dataset is an extension of ROCStories that substitutes alternative events into the stories, which may or may not match up with the original summary sentence <CITED HERE>."
23
+ },
24
+ {
25
+ "id": 2,
26
+ "label": "Uses",
27
+ "confidence": 0.5409758687019348,
28
+ "confidence_margin": 0.14535099267959595,
29
+ "cue_span": "",
30
+ "rationale": "scibert_model",
31
+ "citing_paper_id": "a6924f6b715327b0f24d044e9e8ef799b3f4a0a2",
32
+ "citing_title": "Electrifying the Story Climax via Axial Self-Attention in GPT-3 Transformer Block",
33
+ "text": "The dataset is often separated into four subsets: training, testing, evaluation, and validation [32\u201339]."
34
+ },
35
+ {
36
+ "id": 3,
37
+ "label": "Background",
38
+ "confidence": 0.8934071660041809,
39
+ "confidence_margin": 0.7892690300941467,
40
+ "cue_span": "",
41
+ "rationale": "scibert_model",
42
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
43
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
44
+ "text": "PASTA <CITED HERE> examined implied states and preturbed these states to examine changes in situational narratives."
45
+ },
46
+ {
47
+ "id": 4,
48
+ "label": "Uses",
49
+ "confidence": 0.9904201626777649,
50
+ "confidence_margin": 0.9823774695396423,
51
+ "cue_span": "",
52
+ "rationale": "scibert_model",
53
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
54
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
55
+ "text": "In the following paragraphs, we briefly describe the PASTA <CITED HERE> and SAGA (Vallu-rupalli et al., 2024) datasets and how these datasets\u2019 annotations relate to conditions and outcomes."
56
+ },
57
+ {
58
+ "id": 5,
59
+ "label": "Background",
60
+ "confidence": 0.9986065030097961,
61
+ "confidence_margin": 0.9977977275848389,
62
+ "cue_span": "",
63
+ "rationale": "scibert_model",
64
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
65
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
66
+ "text": "\u2026of reasoning is challenging because: (1) Conditions that to relate to entity properties and states are not always explicitly stated in a narrative but are implicitly understood through forming a coherent mental representation <CITED HERE> and acquiring this implicit knowledge is not easy."
67
+ },
68
+ {
69
+ "id": 6,
70
+ "label": "Background",
71
+ "confidence": 0.9968738555908203,
72
+ "confidence_margin": 0.9947959780693054,
73
+ "cue_span": "",
74
+ "rationale": "scibert_model",
75
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
76
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
77
+ "text": "\u2026of these conditions, like Sam trusting his/her dad or Sam being a small child, are relevant to the goal, with a high likelihood of influencing the outcome, while other conditions that <CITED HERE>; Vallurupalli et al., 2024). might be true are irrelevant to the outcome\u2014like Sam being male."
78
+ },
79
+ {
80
+ "id": 7,
81
+ "label": "Background",
82
+ "confidence": 0.9994476437568665,
83
+ "confidence_margin": 0.9992915987968445,
84
+ "cue_span": "",
85
+ "rationale": "scibert_model",
86
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
87
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
88
+ "text": "( 4) Large Language Models (LLMs) (Brown et al., 2020; Ouyang et al., 2022, inter alia ), while powerful, do not necessarily perform well on tasks requiring counterfactual reasoning (Fang et al., 2025; Lin, 2004; <CITED HERE>; Qin et al., 2019)."
89
+ },
90
+ {
91
+ "id": 8,
92
+ "label": "Uses",
93
+ "confidence": 0.9994866847991943,
94
+ "confidence_margin": 0.9991527199745178,
95
+ "cue_span": "",
96
+ "rationale": "scibert_model",
97
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
98
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
99
+ "text": "We expand the 3 categories used in PASTA <CITED HERE>, for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026"
100
+ },
101
+ {
102
+ "id": 9,
103
+ "label": "Uses",
104
+ "confidence": 0.9968955516815186,
105
+ "confidence_margin": 0.9944096207618713,
106
+ "cue_span": "",
107
+ "rationale": "scibert_model",
108
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
109
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
110
+ "text": "We leverage two previously released datasets PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig."
111
+ },
112
+ {
113
+ "id": 10,
114
+ "label": "Uses",
115
+ "confidence": 0.9981223940849304,
116
+ "confidence_margin": 0.997296929359436,
117
+ "cue_span": "",
118
+ "rationale": "scibert_model",
119
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
120
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
121
+ "text": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026"
122
+ },
123
+ {
124
+ "id": 11,
125
+ "label": "Uses",
126
+ "confidence": 0.915876567363739,
127
+ "confidence_margin": 0.8748534321784973,
128
+ "cue_span": "",
129
+ "rationale": "scibert_model",
130
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
131
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
132
+ "text": "With this HIT we obtain a new set of goal annotations reusing and modifying the free-form text annotations from the actual story to obtain annotations that are also minimally updated reflecting the process used for obtaining the counterfactual in PASTA <CITED HERE>."
133
+ },
134
+ {
135
+ "id": 12,
136
+ "label": "Background",
137
+ "confidence": 0.9987213015556335,
138
+ "confidence_margin": 0.9978130459785461,
139
+ "cue_span": "",
140
+ "rationale": "scibert_model",
141
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
142
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
143
+ "text": "In PASTA <CITED HERE>, original ROCStories (\u201cactual stories\u201d) have up to three \u201calternative\u201d stories."
144
+ },
145
+ {
146
+ "id": 13,
147
+ "label": "Uses",
148
+ "confidence": 0.8018349409103394,
149
+ "confidence_margin": 0.689209520816803,
150
+ "cue_span": "",
151
+ "rationale": "scibert_model",
152
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
153
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
154
+ "text": "\u2026stories shown: in alternative 1 (top right), a different Figure 1: A participant\u2019s goal inferred from the actual story when applied to 3 alternative stories, drawn from the PASTA dataset <CITED HERE>; slightly varying actions in the stories lead to different goal achievement outcomes."
155
+ },
156
+ {
157
+ "id": 14,
158
+ "label": "Background",
159
+ "confidence": 0.9973942041397095,
160
+ "confidence_margin": 0.996073305606842,
161
+ "cue_span": "",
162
+ "rationale": "scibert_model",
163
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
164
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
165
+ "text": "PASTA <CITED HERE> introduced implied states that stories depend upon and examined the tasks of state inference from alternative stories and story rewriting for alternative states."
166
+ },
167
+ {
168
+ "id": 15,
169
+ "label": "Background",
170
+ "confidence": 0.9965572357177734,
171
+ "confidence_margin": 0.9946063160896301,
172
+ "cue_span": "",
173
+ "rationale": "scibert_model",
174
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
175
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
176
+ "text": "Third, while pre-trained large language models (LLMs) are powerful (Brown et al., 2020; Wei et al., 2022, inter alia ), they may perform poorly on tasks requiring robust reasoning <CITED HERE>; Zellers et al., 2019; Qin et al., 2019)."
177
+ },
178
+ {
179
+ "id": 16,
180
+ "label": "Uses",
181
+ "confidence": 0.9981212019920349,
182
+ "confidence_margin": 0.996633768081665,
183
+ "cue_span": "",
184
+ "rationale": "scibert_model",
185
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
186
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
187
+ "text": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset <CITED HERE>."
188
+ },
189
+ {
190
+ "id": 17,
191
+ "label": "Background",
192
+ "confidence": 0.9918141961097717,
193
+ "confidence_margin": 0.9873102307319641,
194
+ "cue_span": "",
195
+ "rationale": "scibert_model",
196
+ "citing_paper_id": "d723193c0453223803ffb926354cd6d2dee32b06",
197
+ "citing_title": "EvEval: A Comprehensive Evaluation of Event Semantics for Large Language Models",
198
+ "text": "Learning other relations such as event temporality needs more context information and knowledge since it is more subjected to specific situations <CITED HERE>."
199
+ },
200
+ {
201
+ "id": 18,
202
+ "label": "Background",
203
+ "confidence": 0.9065139293670654,
204
+ "confidence_margin": 0.8178352117538452,
205
+ "cue_span": "",
206
+ "rationale": "scibert_model",
207
+ "citing_paper_id": "293583961efdf22c0905cc04465aa88ea96826ec",
208
+ "citing_title": "Causal schema induction for knowledge discovery",
209
+ "text": ", 2021), of participant states <CITED HERE>; Vallurupalli et al., 2022), and of disease where organisms are conceived of as causative agents, e."
210
+ },
211
+ {
212
+ "id": 19,
213
+ "label": "Background",
214
+ "confidence": 0.9994509816169739,
215
+ "confidence_margin": 0.9992090463638306,
216
+ "cue_span": "",
217
+ "rationale": "scibert_model",
218
+ "citing_paper_id": "293583961efdf22c0905cc04465aa88ea96826ec",
219
+ "citing_title": "Causal schema induction for knowledge discovery",
220
+ "text": "Participant-centeredness is featured in studies of narrative (Propp, 1968; Caselli and Vossen, 2016; Brahman et al., 2021), of participant states <CITED HERE>; Vallurupalli et al., 2022), and of disease where organisms are conceived of as causative agents, e.g., the pathogen tubercle\u2026"
221
+ },
222
+ {
223
+ "id": 20,
224
+ "label": "Background",
225
+ "confidence": 0.9978366494178772,
226
+ "confidence_margin": 0.9965800642967224,
227
+ "cue_span": "",
228
+ "rationale": "scibert_model",
229
+ "citing_paper_id": "2b914691a44ab1c8bcb0104b8079d86908d98729",
230
+ "citing_title": "Who Plays Which Role? Protagonist Detection and Classification in Moral Discourse",
231
+ "text": "Beyond moral discourse, modeling actors and their roles has a long tradition in discourse analysis, for instance, through semantic role labeling (Ruppenhofer et al., 2009; Roth and Lap-ata, 2015; Bornheim et al., 2024) and participant modeling (Tilk et al., 2016; <CITED HERE>."
232
+ }
233
+ ],
234
+ "citing_paper_labels": [
235
+ {
236
+ "citing_paper_id": "c1d02fc3786fb5e5b0543703af95046253109d4e",
237
+ "citing_title": "Inductive Bias Extraction and Matching for LLM Prompts",
238
+ "label": "Uses",
239
+ "evidence_context_ids": [
240
+ 1
241
+ ]
242
+ },
243
+ {
244
+ "citing_paper_id": "a6924f6b715327b0f24d044e9e8ef799b3f4a0a2",
245
+ "citing_title": "Electrifying the Story Climax via Axial Self-Attention in GPT-3 Transformer Block",
246
+ "label": "Uses",
247
+ "evidence_context_ids": [
248
+ 2
249
+ ]
250
+ },
251
+ {
252
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
253
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
254
+ "label": "Uses",
255
+ "evidence_context_ids": [
256
+ 4,
257
+ 8,
258
+ 9,
259
+ 10
260
+ ]
261
+ },
262
+ {
263
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
264
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
265
+ "label": "Uses",
266
+ "evidence_context_ids": [
267
+ 11,
268
+ 13,
269
+ 16
270
+ ]
271
+ },
272
+ {
273
+ "citing_paper_id": "d723193c0453223803ffb926354cd6d2dee32b06",
274
+ "citing_title": "EvEval: A Comprehensive Evaluation of Event Semantics for Large Language Models",
275
+ "label": "Background",
276
+ "evidence_context_ids": []
277
+ },
278
+ {
279
+ "citing_paper_id": "293583961efdf22c0905cc04465aa88ea96826ec",
280
+ "citing_title": "Causal schema induction for knowledge discovery",
281
+ "label": "Background",
282
+ "evidence_context_ids": []
283
+ },
284
+ {
285
+ "citing_paper_id": "2b914691a44ab1c8bcb0104b8079d86908d98729",
286
+ "citing_title": "Who Plays Which Role? Protagonist Detection and Classification in Moral Discourse",
287
+ "label": "Background",
288
+ "evidence_context_ids": []
289
+ }
290
+ ],
291
+ "final_label": "Uses"
292
+ }
replay_traces/2208.00329/processed_papers/2208.00329/usage_contexts.json ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "paper_id": "2208.00329",
3
+ "total_citations": 7,
4
+ "num_contexts": 20,
5
+ "num_citing_with_context": 7,
6
+ "num_citing_without_context": 0,
7
+ "num_influential_citations": 2,
8
+ "num_influential_with_context": 2,
9
+ "influential_contexts": [
10
+ {
11
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
12
+ "title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
13
+ "external_ids": {
14
+ "DBLP": "journals/corr/abs-2506-01253",
15
+ "ArXiv": "2506.01253",
16
+ "DOI": "10.48550/arXiv.2506.01253",
17
+ "CorpusId": 279074979
18
+ },
19
+ "contexts": [
20
+ {
21
+ "text": "PASTA <CITED HERE> examined implied states and preturbed these states to examine changes in situational narratives.",
22
+ "text_raw": "PASTA (Ghosh et al., 2023) examined implied states and preturbed these states to examine changes in situational narratives.",
23
+ "intents": []
24
+ },
25
+ {
26
+ "text": "In the following paragraphs, we briefly describe the PASTA <CITED HERE> and SAGA (Vallu-rupalli et al., 2024) datasets and how these datasets\u2019 annotations relate to conditions and outcomes.",
27
+ "text_raw": "In the following paragraphs, we briefly describe the PASTA (Ghosh et al., 2023) and SAGA (Vallu-rupalli et al., 2024) datasets and how these datasets\u2019 annotations relate to conditions and outcomes.",
28
+ "intents": []
29
+ },
30
+ {
31
+ "text": "\u2026of reasoning is challenging because: (1) Conditions that to relate to entity properties and states are not always explicitly stated in a narrative but are implicitly understood through forming a coherent mental representation <CITED HERE> and acquiring this implicit knowledge is not easy.",
32
+ "text_raw": "\u2026of reasoning is challenging because: (1) Conditions that to relate to entity properties and states are not always explicitly stated in a narrative but are implicitly understood through forming a coherent mental representation (Ghosh et al., 2023) and acquiring this implicit knowledge is not easy.",
33
+ "intents": []
34
+ },
35
+ {
36
+ "text": "\u2026of these conditions, like Sam trusting his/her dad or Sam being a small child, are relevant to the goal, with a high likelihood of influencing the outcome, while other conditions that <CITED HERE>; Vallurupalli et al., 2024). might be true are irrelevant to the outcome\u2014like Sam being male.",
37
+ "text_raw": "\u2026of these conditions, like Sam trusting his/her dad or Sam being a small child, are relevant to the goal, with a high likelihood of influencing the outcome, while other conditions that (Ghosh et al., 2023; Vallurupalli et al., 2024). might be true are irrelevant to the outcome\u2014like Sam being male.",
38
+ "intents": []
39
+ },
40
+ {
41
+ "text": "( 4) Large Language Models (LLMs) (Brown et al., 2020; Ouyang et al., 2022, inter alia ), while powerful, do not necessarily perform well on tasks requiring counterfactual reasoning (Fang et al., 2025; Lin, 2004; <CITED HERE>; Qin et al., 2019).",
42
+ "text_raw": "( 4) Large Language Models (LLMs) (Brown et al., 2020; Ouyang et al., 2022, inter alia ), while powerful, do not necessarily perform well on tasks requiring counterfactual reasoning (Fang et al., 2025; Lin, 2004; Ghosh et al., 2023; Qin et al., 2019).",
43
+ "intents": []
44
+ },
45
+ {
46
+ "text": "We expand the 3 categories used in PASTA <CITED HERE>, for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026",
47
+ "text_raw": "We expand the 3 categories used in PASTA (Ghosh et al., 2023), for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026",
48
+ "intents": []
49
+ },
50
+ {
51
+ "text": "We leverage two previously released datasets PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig.",
52
+ "text_raw": "We leverage two previously released datasets PASTA (Ghosh et al., 2023) and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig.",
53
+ "intents": []
54
+ },
55
+ {
56
+ "text": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026",
57
+ "text_raw": "We leverage PASTA (Ghosh et al., 2023) and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026",
58
+ "intents": []
59
+ }
60
+ ]
61
+ },
62
+ {
63
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
64
+ "title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
65
+ "external_ids": {
66
+ "DBLP": "journals/corr/abs-2408-05793",
67
+ "ArXiv": "2408.05793",
68
+ "DOI": "10.48550/arXiv.2408.05793",
69
+ "CorpusId": 271855571
70
+ },
71
+ "contexts": [
72
+ {
73
+ "text": "With this HIT we obtain a new set of goal annotations reusing and modifying the free-form text annotations from the actual story to obtain annotations that are also minimally updated reflecting the process used for obtaining the counterfactual in PASTA <CITED HERE>.",
74
+ "text_raw": "With this HIT we obtain a new set of goal annotations reusing and modifying the free-form text annotations from the actual story to obtain annotations that are also minimally updated reflecting the process used for obtaining the counterfactual in PASTA (Ghosh et al., 2023).",
75
+ "intents": [
76
+ "methodology"
77
+ ]
78
+ },
79
+ {
80
+ "text": "In PASTA <CITED HERE>, original ROCStories (\u201cactual stories\u201d) have up to three \u201calternative\u201d stories.",
81
+ "text_raw": "In PASTA (Ghosh et al., 2023), original ROCStories (\u201cactual stories\u201d) have up to three \u201calternative\u201d stories.",
82
+ "intents": [
83
+ "background"
84
+ ]
85
+ },
86
+ {
87
+ "text": "\u2026stories shown: in alternative 1 (top right), a different Figure 1: A participant\u2019s goal inferred from the actual story when applied to 3 alternative stories, drawn from the PASTA dataset <CITED HERE>; slightly varying actions in the stories lead to different goal achievement outcomes.",
88
+ "text_raw": "\u2026stories shown: in alternative 1 (top right), a different Figure 1: A participant\u2019s goal inferred from the actual story when applied to 3 alternative stories, drawn from the PASTA dataset (Ghosh et al., 2023); slightly varying actions in the stories lead to different goal achievement outcomes.",
89
+ "intents": [
90
+ "background"
91
+ ]
92
+ },
93
+ {
94
+ "text": "PASTA <CITED HERE> introduced implied states that stories depend upon and examined the tasks of state inference from alternative stories and story rewriting for alternative states.",
95
+ "text_raw": "PASTA (Ghosh et al., 2023) introduced implied states that stories depend upon and examined the tasks of state inference from alternative stories and story rewriting for alternative states.",
96
+ "intents": [
97
+ "background"
98
+ ]
99
+ },
100
+ {
101
+ "text": "Third, while pre-trained large language models (LLMs) are powerful (Brown et al., 2020; Wei et al., 2022, inter alia ), they may perform poorly on tasks requiring robust reasoning <CITED HERE>; Zellers et al., 2019; Qin et al., 2019).",
102
+ "text_raw": "Third, while pre-trained large language models (LLMs) are powerful (Brown et al., 2020; Wei et al., 2022, inter alia ), they may perform poorly on tasks requiring robust reasoning (Ghosh et al., 2023; Zellers et al., 2019; Qin et al., 2019).",
103
+ "intents": [
104
+ "background"
105
+ ]
106
+ },
107
+ {
108
+ "text": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset <CITED HERE>.",
109
+ "text_raw": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset (Ghosh et al., 2023).",
110
+ "intents": [
111
+ "methodology"
112
+ ]
113
+ }
114
+ ]
115
+ }
116
+ ],
117
+ "citing_papers": [
118
+ {
119
+ "citing_paper_id": "c1d02fc3786fb5e5b0543703af95046253109d4e",
120
+ "title": "Inductive Bias Extraction and Matching for LLM Prompts",
121
+ "external_ids": {
122
+ "ArXiv": "2508.10295",
123
+ "DBLP": "journals/corr/abs-2508-10295",
124
+ "DOI": "10.48550/arXiv.2508.10295",
125
+ "CorpusId": 280649627
126
+ },
127
+ "is_influential": false,
128
+ "contexts": [
129
+ {
130
+ "text": "The PASTA dataset is an extension of ROCStories that substitutes alternative events into the stories, which may or may not match up with the original summary sentence <CITED HERE>.",
131
+ "text_raw": "The PASTA dataset is an extension of ROCStories that substitutes alternative events into the stories, which may or may not match up with the original summary sentence (Ghosh et al., 2023).",
132
+ "intents": []
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ "citing_paper_id": "a6924f6b715327b0f24d044e9e8ef799b3f4a0a2",
138
+ "title": "Electrifying the Story Climax via Axial Self-Attention in GPT-3 Transformer Block",
139
+ "external_ids": {
140
+ "DBLP": "journals/sncs/VD25",
141
+ "DOI": "10.1007/s42979-025-04091-1",
142
+ "CorpusId": 279483975
143
+ },
144
+ "is_influential": false,
145
+ "contexts": [
146
+ {
147
+ "text": "The dataset is often separated into four subsets: training, testing, evaluation, and validation [32\u201339].",
148
+ "text_raw": "The dataset is often separated into four subsets: training, testing, evaluation, and validation [32\u201339].",
149
+ "intents": []
150
+ }
151
+ ]
152
+ },
153
+ {
154
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
155
+ "title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
156
+ "external_ids": {
157
+ "DBLP": "journals/corr/abs-2506-01253",
158
+ "ArXiv": "2506.01253",
159
+ "DOI": "10.48550/arXiv.2506.01253",
160
+ "CorpusId": 279074979
161
+ },
162
+ "is_influential": true,
163
+ "contexts": [
164
+ {
165
+ "text": "PASTA <CITED HERE> examined implied states and preturbed these states to examine changes in situational narratives.",
166
+ "text_raw": "PASTA (Ghosh et al., 2023) examined implied states and preturbed these states to examine changes in situational narratives.",
167
+ "intents": []
168
+ },
169
+ {
170
+ "text": "In the following paragraphs, we briefly describe the PASTA <CITED HERE> and SAGA (Vallu-rupalli et al., 2024) datasets and how these datasets\u2019 annotations relate to conditions and outcomes.",
171
+ "text_raw": "In the following paragraphs, we briefly describe the PASTA (Ghosh et al., 2023) and SAGA (Vallu-rupalli et al., 2024) datasets and how these datasets\u2019 annotations relate to conditions and outcomes.",
172
+ "intents": []
173
+ },
174
+ {
175
+ "text": "\u2026of reasoning is challenging because: (1) Conditions that to relate to entity properties and states are not always explicitly stated in a narrative but are implicitly understood through forming a coherent mental representation <CITED HERE> and acquiring this implicit knowledge is not easy.",
176
+ "text_raw": "\u2026of reasoning is challenging because: (1) Conditions that to relate to entity properties and states are not always explicitly stated in a narrative but are implicitly understood through forming a coherent mental representation (Ghosh et al., 2023) and acquiring this implicit knowledge is not easy.",
177
+ "intents": []
178
+ },
179
+ {
180
+ "text": "\u2026of these conditions, like Sam trusting his/her dad or Sam being a small child, are relevant to the goal, with a high likelihood of influencing the outcome, while other conditions that <CITED HERE>; Vallurupalli et al., 2024). might be true are irrelevant to the outcome\u2014like Sam being male.",
181
+ "text_raw": "\u2026of these conditions, like Sam trusting his/her dad or Sam being a small child, are relevant to the goal, with a high likelihood of influencing the outcome, while other conditions that (Ghosh et al., 2023; Vallurupalli et al., 2024). might be true are irrelevant to the outcome\u2014like Sam being male.",
182
+ "intents": []
183
+ },
184
+ {
185
+ "text": "( 4) Large Language Models (LLMs) (Brown et al., 2020; Ouyang et al., 2022, inter alia ), while powerful, do not necessarily perform well on tasks requiring counterfactual reasoning (Fang et al., 2025; Lin, 2004; <CITED HERE>; Qin et al., 2019).",
186
+ "text_raw": "( 4) Large Language Models (LLMs) (Brown et al., 2020; Ouyang et al., 2022, inter alia ), while powerful, do not necessarily perform well on tasks requiring counterfactual reasoning (Fang et al., 2025; Lin, 2004; Ghosh et al., 2023; Qin et al., 2019).",
187
+ "intents": []
188
+ },
189
+ {
190
+ "text": "We expand the 3 categories used in PASTA <CITED HERE>, for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026",
191
+ "text_raw": "We expand the 3 categories used in PASTA (Ghosh et al., 2023), for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026",
192
+ "intents": []
193
+ },
194
+ {
195
+ "text": "We leverage two previously released datasets PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig.",
196
+ "text_raw": "We leverage two previously released datasets PASTA (Ghosh et al., 2023) and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig.",
197
+ "intents": []
198
+ },
199
+ {
200
+ "text": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026",
201
+ "text_raw": "We leverage PASTA (Ghosh et al., 2023) and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026",
202
+ "intents": []
203
+ }
204
+ ]
205
+ },
206
+ {
207
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
208
+ "title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
209
+ "external_ids": {
210
+ "DBLP": "journals/corr/abs-2408-05793",
211
+ "ArXiv": "2408.05793",
212
+ "DOI": "10.48550/arXiv.2408.05793",
213
+ "CorpusId": 271855571
214
+ },
215
+ "is_influential": true,
216
+ "contexts": [
217
+ {
218
+ "text": "With this HIT we obtain a new set of goal annotations reusing and modifying the free-form text annotations from the actual story to obtain annotations that are also minimally updated reflecting the process used for obtaining the counterfactual in PASTA <CITED HERE>.",
219
+ "text_raw": "With this HIT we obtain a new set of goal annotations reusing and modifying the free-form text annotations from the actual story to obtain annotations that are also minimally updated reflecting the process used for obtaining the counterfactual in PASTA (Ghosh et al., 2023).",
220
+ "intents": [
221
+ "methodology"
222
+ ]
223
+ },
224
+ {
225
+ "text": "In PASTA <CITED HERE>, original ROCStories (\u201cactual stories\u201d) have up to three \u201calternative\u201d stories.",
226
+ "text_raw": "In PASTA (Ghosh et al., 2023), original ROCStories (\u201cactual stories\u201d) have up to three \u201calternative\u201d stories.",
227
+ "intents": [
228
+ "background"
229
+ ]
230
+ },
231
+ {
232
+ "text": "\u2026stories shown: in alternative 1 (top right), a different Figure 1: A participant\u2019s goal inferred from the actual story when applied to 3 alternative stories, drawn from the PASTA dataset <CITED HERE>; slightly varying actions in the stories lead to different goal achievement outcomes.",
233
+ "text_raw": "\u2026stories shown: in alternative 1 (top right), a different Figure 1: A participant\u2019s goal inferred from the actual story when applied to 3 alternative stories, drawn from the PASTA dataset (Ghosh et al., 2023); slightly varying actions in the stories lead to different goal achievement outcomes.",
234
+ "intents": [
235
+ "background"
236
+ ]
237
+ },
238
+ {
239
+ "text": "PASTA <CITED HERE> introduced implied states that stories depend upon and examined the tasks of state inference from alternative stories and story rewriting for alternative states.",
240
+ "text_raw": "PASTA (Ghosh et al., 2023) introduced implied states that stories depend upon and examined the tasks of state inference from alternative stories and story rewriting for alternative states.",
241
+ "intents": [
242
+ "background"
243
+ ]
244
+ },
245
+ {
246
+ "text": "Third, while pre-trained large language models (LLMs) are powerful (Brown et al., 2020; Wei et al., 2022, inter alia ), they may perform poorly on tasks requiring robust reasoning <CITED HERE>; Zellers et al., 2019; Qin et al., 2019).",
247
+ "text_raw": "Third, while pre-trained large language models (LLMs) are powerful (Brown et al., 2020; Wei et al., 2022, inter alia ), they may perform poorly on tasks requiring robust reasoning (Ghosh et al., 2023; Zellers et al., 2019; Qin et al., 2019).",
248
+ "intents": [
249
+ "background"
250
+ ]
251
+ },
252
+ {
253
+ "text": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset <CITED HERE>.",
254
+ "text_raw": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset (Ghosh et al., 2023).",
255
+ "intents": [
256
+ "methodology"
257
+ ]
258
+ }
259
+ ]
260
+ },
261
+ {
262
+ "citing_paper_id": "d723193c0453223803ffb926354cd6d2dee32b06",
263
+ "title": "EvEval: A Comprehensive Evaluation of Event Semantics for Large Language Models",
264
+ "external_ids": {
265
+ "ArXiv": "2305.15268",
266
+ "DBLP": "journals/corr/abs-2305-15268",
267
+ "DOI": "10.48550/arXiv.2305.15268",
268
+ "CorpusId": 258866165
269
+ },
270
+ "is_influential": false,
271
+ "contexts": [
272
+ {
273
+ "text": "Learning other relations such as event temporality needs more context information and knowledge since it is more subjected to specific situations <CITED HERE>.",
274
+ "text_raw": "Learning other relations such as event temporality needs more context information and knowledge since it is more subjected to specific situations (Ghosh et al., 2022).",
275
+ "intents": [
276
+ "background"
277
+ ]
278
+ }
279
+ ]
280
+ },
281
+ {
282
+ "citing_paper_id": "293583961efdf22c0905cc04465aa88ea96826ec",
283
+ "title": "Causal schema induction for knowledge discovery",
284
+ "external_ids": {
285
+ "ArXiv": "2303.15381",
286
+ "DBLP": "journals/corr/abs-2303-15381",
287
+ "DOI": "10.48550/arXiv.2303.15381",
288
+ "CorpusId": 257766554
289
+ },
290
+ "is_influential": false,
291
+ "contexts": [
292
+ {
293
+ "text": ", 2021), of participant states <CITED HERE>; Vallurupalli et al., 2022), and of disease where organisms are conceived of as causative agents, e.",
294
+ "text_raw": ", 2021), of participant states (Ghosh et al., 2022; Vallurupalli et al., 2022), and of disease where organisms are conceived of as causative agents, e.",
295
+ "intents": [
296
+ "background"
297
+ ]
298
+ },
299
+ {
300
+ "text": "Participant-centeredness is featured in studies of narrative (Propp, 1968; Caselli and Vossen, 2016; Brahman et al., 2021), of participant states <CITED HERE>; Vallurupalli et al., 2022), and of disease where organisms are conceived of as causative agents, e.g., the pathogen tubercle\u2026",
301
+ "text_raw": "Participant-centeredness is featured in studies of narrative (Propp, 1968; Caselli and Vossen, 2016; Brahman et al., 2021), of participant states (Ghosh et al., 2022; Vallurupalli et al., 2022), and of disease where organisms are conceived of as causative agents, e.g., the pathogen tubercle\u2026",
302
+ "intents": [
303
+ "background"
304
+ ]
305
+ }
306
+ ]
307
+ },
308
+ {
309
+ "citing_paper_id": "2b914691a44ab1c8bcb0104b8079d86908d98729",
310
+ "title": "Who Plays Which Role? Protagonist Detection and Classification in Moral Discourse",
311
+ "external_ids": {
312
+ "DBLP": "conf/eacl/SommerB26",
313
+ "DOI": "10.18653/v1/2026.eacl-srw.27",
314
+ "CorpusId": 286822758
315
+ },
316
+ "is_influential": false,
317
+ "contexts": [
318
+ {
319
+ "text": "Beyond moral discourse, modeling actors and their roles has a long tradition in discourse analysis, for instance, through semantic role labeling (Ruppenhofer et al., 2009; Roth and Lap-ata, 2015; Bornheim et al., 2024) and participant modeling (Tilk et al., 2016; <CITED HERE>.",
320
+ "text_raw": "Beyond moral discourse, modeling actors and their roles has a long tradition in discourse analysis, for instance, through semantic role labeling (Ruppenhofer et al., 2009; Roth and Lap-ata, 2015; Bornheim et al., 2024) and participant modeling (Tilk et al., 2016; Ghosh et al., 2023).",
321
+ "intents": []
322
+ }
323
+ ]
324
+ }
325
+ ]
326
+ }
replay_traces/2208.00329/processed_papers/2208.00329/usage_contributions.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "paper_id": "2208.00329",
3
+ "final_label": "EXTENDS",
4
+ "contributions": [
5
+ {
6
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
7
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
8
+ "label": "USES",
9
+ "paper_claim": "The citing paper adopts the methodology used for obtaining counterfactuals in the target paper to generate minimally updated goal annotations.",
10
+ "claim": "The citing paper adopts the methodology used for obtaining counterfactuals in the target paper to generate minimally updated goal annotations.",
11
+ "cluster_title": "Uses target methodology for dataset creation",
12
+ "cluster_key": "USES|methodology|dataset_creation",
13
+ "evidence_span": "reflecting the process used for obtaining the counterfactual in ~PASTA~<CITED HERE>",
14
+ "rationale": "The citing paper explicitly states that their dataset annotation process uses or mirrors the method used to obtain counterfactuals in the target paper.",
15
+ "contexts": [
16
+ "Target-citing paragraph: We use a third HIT when one or more annotators identify that a participant's goal is inferrable from $S^{c_k}$. With this HIT we obtain a new set of goal annotations reusing and modifying the free-form text annotations from the actual story to obtain annotations that are also minimally updated reflecting the process used for obtaining the counterfactual in ~PASTA~<CITED HERE>. See \\cref{tab:HITS-annotation} for the annotations and the HITs used for obtaining them.",
17
+ "Target-citing paragraph: \\subsubsection{Worker Selection and Qualifications}\n\\label{app:HIT_information:qualifications}\nFor our initial alpha runs of the HITs we used all workers who meet our community standard quality criteria, such as requiring a 98\\% or greater HIT acceptance rate and the completion of 1000 approved HITs. In addition, we required the worker's stated location to be in the USA, UK, Canada, Australia, or New Zealand. We used the location requirement to avoid language-based artifacts given the language-dependent semantic phenomena and the high subjectivity our work can elicit. \nWe did not use requester-generated qualification tests, though in early iterations we found that annotators who had completed at least 50 HITs in our prior work~<CITED HERE> provided the most reliable annotations; the vast majority of our responses are from this group."
18
+ ],
19
+ "source": "arxiv_paragraphs"
20
+ },
21
+ {
22
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
23
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
24
+ "label": "USES",
25
+ "paper_claim": "The authors leverage the PASTA dataset and its annotations to generate and validate story outcomes.",
26
+ "claim": "The authors leverage the PASTA dataset and its annotations to generate and validate story outcomes.",
27
+ "cluster_title": "Uses target dataset for data generation",
28
+ "cluster_key": "USES|dataset|data_generation",
29
+ "evidence_span": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions.",
30
+ "rationale": "The citing paper explicitly uses the PASTA dataset and its annotated conditions to generate outcomes for SAGA and News Stories, as well as to validate them.",
31
+ "contexts": [
32
+ "Target sentence: We expand the 3 categories used in PASTA <CITED HERE>, for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026",
33
+ "Target sentence: We leverage two previously released datasets PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig.",
34
+ "Target sentence: We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026"
35
+ ],
36
+ "source": "usage_contexts_fallback"
37
+ }
38
+ ],
39
+ "source": "arxiv_paragraphs",
40
+ "fallback_citing_ids": [
41
+ "ab73ca481256767addce56b20eb48ac183169e01"
42
+ ]
43
+ }
replay_traces/2208.00329/processed_papers/2208.00329/usage_discovery_from_contributions.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "paper_id": "2208.00329",
3
+ "decision": "",
4
+ "justification": "",
5
+ "clusters": [
6
+ {
7
+ "cluster_id": "C1",
8
+ "count": "1",
9
+ "representative_claim": "Uses target methodology for dataset creation",
10
+ "cluster_key": "USES|methodology|dataset_creation",
11
+ "cluster_title": "Uses target methodology for dataset creation",
12
+ "claim_indices": [
13
+ "0"
14
+ ],
15
+ "source_cluster_ids": [
16
+ "C1"
17
+ ],
18
+ "merge_rationale": "The citing paper adopts the methodology used in the target paper to generate its own dataset."
19
+ },
20
+ {
21
+ "cluster_id": "C2",
22
+ "count": "1",
23
+ "representative_claim": "Uses target dataset for data generation",
24
+ "cluster_key": "USES|dataset|data_generation",
25
+ "cluster_title": "Uses target dataset for data generation",
26
+ "claim_indices": [
27
+ "1"
28
+ ],
29
+ "source_cluster_ids": [
30
+ "C2"
31
+ ],
32
+ "merge_rationale": "The citing paper uses the PASTA dataset and its annotations directly to generate and validate story outcomes."
33
+ }
34
+ ],
35
+ "dropped_clusters": [],
36
+ "cluster_refine_method": "llm_centroid_merge_filter",
37
+ "cluster_refine_source": "usage_contributions.json"
38
+ }
replay_traces/2208.00329/processed_papers/2208.00329/usage_uses_extends_verified.json ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "paper_id": "2208.00329",
3
+ "target": {
4
+ "title": "PASTA: A Dataset for Modeling PArticipant STAtes in Narratives",
5
+ "first_author_last": "Ghosh",
6
+ "year": "2022"
7
+ },
8
+ "candidates_total": 9,
9
+ "candidates_considered": 9,
10
+ "verification_batch_size": 25,
11
+ "verification_num_batches": 1,
12
+ "candidates_selected": 4,
13
+ "verified": [
14
+ {
15
+ "id": 8,
16
+ "label": "EXTENDS",
17
+ "cue_span": "We expand the 3 categories used in PASTA",
18
+ "rationale": "The citing paper explicitly expands upon the categorical framework introduced in the target paper.",
19
+ "text": "We expand the 3 categories used in PASTA <CITED HERE>, for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026",
20
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
21
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
22
+ "original_label": "Uses"
23
+ },
24
+ {
25
+ "id": 10,
26
+ "label": "USES",
27
+ "cue_span": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes",
28
+ "rationale": "The authors explicitly state they are leveraging the target dataset to generate and validate outcomes.",
29
+ "text": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026",
30
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
31
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
32
+ "original_label": "Uses"
33
+ },
34
+ {
35
+ "id": 16,
36
+ "label": "USES",
37
+ "cue_span": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset",
38
+ "rationale": "The authors used stories specifically from the PASTA dataset for their own annotation process.",
39
+ "text": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset <CITED HERE>.",
40
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
41
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
42
+ "original_label": "Uses"
43
+ },
44
+ {
45
+ "id": 9,
46
+ "label": "USES",
47
+ "cue_span": "We leverage two previously released datasets PASTA",
48
+ "rationale": "The authors explicitly mention leveraging the PASTA dataset as part of their work.",
49
+ "text": "We leverage two previously released datasets PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig.",
50
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
51
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
52
+ "original_label": "Uses"
53
+ },
54
+ {
55
+ "id": 4,
56
+ "label": "NOT_CONFIRMED",
57
+ "cue_span": "",
58
+ "rationale": "The sentence only indicates an intention to describe the datasets, not a specific use or extension of them.",
59
+ "text": "In the following paragraphs, we briefly describe the PASTA <CITED HERE> and SAGA (Vallu-rupalli et al., 2024) datasets and how these datasets\u2019 annotations relate to conditions and outcomes.",
60
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
61
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
62
+ "original_label": "Uses"
63
+ },
64
+ {
65
+ "id": 1,
66
+ "label": "NOT_CONFIRMED",
67
+ "cue_span": "",
68
+ "rationale": "The sentence provides a description of what the target paper introduces without stating that the citing paper uses it.",
69
+ "text": "The PASTA dataset is an extension of ROCStories that substitutes alternative events into the stories, which may or may not match up with the original summary sentence <CITED HERE>.",
70
+ "citing_paper_id": "c1d02fc3786fb5e5b0543703af95046253109d4e",
71
+ "citing_title": "Inductive Bias Extraction and Matching for LLM Prompts",
72
+ "original_label": "Uses"
73
+ },
74
+ {
75
+ "id": 11,
76
+ "label": "NOT_CONFIRMED",
77
+ "cue_span": "",
78
+ "rationale": "The sentence mentions that the resulting annotations reflect a process from the target paper but lacks an explicit adoption cue.",
79
+ "text": "With this HIT we obtain a new set of goal annotations reusing and modifying the free-form text annotations from the actual story to obtain annotations that are also minimally updated reflecting the process used for obtaining the counterfactual in PASTA <CITED HERE>.",
80
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
81
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
82
+ "original_label": "Uses"
83
+ },
84
+ {
85
+ "id": 13,
86
+ "label": "NOT_CONFIRMED",
87
+ "cue_span": "",
88
+ "rationale": "The sentence lacks an explicit citing-paper actor phrase such as 'we' or 'our' as required by the instructions.",
89
+ "text": "\u2026stories shown: in alternative 1 (top right), a different Figure 1: A participant\u2019s goal inferred from the actual story when applied to 3 alternative stories, drawn from the PASTA dataset <CITED HERE>; slightly varying actions in the stories lead to different goal achievement outcomes.",
90
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
91
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
92
+ "original_label": "Uses"
93
+ },
94
+ {
95
+ "id": 2,
96
+ "label": "NOT_CONFIRMED",
97
+ "cue_span": "",
98
+ "rationale": "The sentence is a general description of dataset subsets and lacks an explicit adoption cue by the citing authors.",
99
+ "text": "The dataset is often separated into four subsets: training, testing, evaluation, and validation [32\u201339].",
100
+ "citing_paper_id": "a6924f6b715327b0f24d044e9e8ef799b3f4a0a2",
101
+ "citing_title": "Electrifying the Story Climax via Axial Self-Attention in GPT-3 Transformer Block",
102
+ "original_label": "Uses"
103
+ }
104
+ ],
105
+ "confirmed": [
106
+ {
107
+ "id": 8,
108
+ "label": "EXTENDS",
109
+ "cue_span": "We expand the 3 categories used in PASTA",
110
+ "rationale": "The citing paper explicitly expands upon the categorical framework introduced in the target paper.",
111
+ "text": "We expand the 3 categories used in PASTA <CITED HERE>, for error analysis on 200 random states on the story state inference task, to 4 categories and group all conditions into these as follows: (a) Physical: This category includes natural physical attributes of an entity such as size, age,\u2026",
112
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
113
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
114
+ "original_label": "Uses"
115
+ },
116
+ {
117
+ "id": 10,
118
+ "label": "USES",
119
+ "cue_span": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes",
120
+ "rationale": "The authors explicitly state they are leveraging the target dataset to generate and validate outcomes.",
121
+ "text": "We leverage PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) datasets to generate outcomes of SAGA and News Stories and validate these using both our generated and PASTA\u2019s crowd-annotated conditions. outcome is implicitly understood through constructing counterfactual mental\u2026",
122
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
123
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
124
+ "original_label": "Uses"
125
+ },
126
+ {
127
+ "id": 16,
128
+ "label": "USES",
129
+ "cue_span": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset",
130
+ "rationale": "The authors used stories specifically from the PASTA dataset for their own annotation process.",
131
+ "text": "We annotated ROC stories (Mostafazadeh et al., 2016) that have corresponding alternative stories in the PASTA dataset <CITED HERE>.",
132
+ "citing_paper_id": "494f3c55553161be831f725a7b55c6095470c5cb",
133
+ "citing_title": "SAGA: A Participant-specific Examination of Story Alternatives and Goal Applicability for a Deeper Understanding of Complex Events",
134
+ "original_label": "Uses"
135
+ },
136
+ {
137
+ "id": 9,
138
+ "label": "USES",
139
+ "cue_span": "We leverage two previously released datasets PASTA",
140
+ "rationale": "The authors explicitly mention leveraging the PASTA dataset as part of their work.",
141
+ "text": "We leverage two previously released datasets PASTA <CITED HERE> and SAGA (Vallurupalli et al., 2024) consisting of participants\u2019 goal and state annotations (shown in Fig.",
142
+ "citing_paper_id": "ab73ca481256767addce56b20eb48ac183169e01",
143
+ "citing_title": "CoRE: Condition-based Reasoning for Identifying Outcome Variance in Complex Events",
144
+ "original_label": "Uses"
145
+ }
146
+ ],
147
+ "confirmed_extends": 1,
148
+ "confirmed_uses": 3,
149
+ "final_label": "EXTENDS"
150
+ }