trustgraph-cloze 2.10.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: trustgraph-cloze
3
+ Version: 2.10.14
4
+ Summary: Cloze model classification service for TrustGraph.
5
+ Author-email: "trustgraph.ai" <security@trustgraph.ai>
6
+ Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: trustgraph-base<2.11,>=2.10
12
+ Requires-Dist: trustgraph-flow<2.11,>=2.10
13
+ Requires-Dist: torch
14
+ Requires-Dist: transformers
15
+ Requires-Dist: scikit-learn
16
+ Requires-Dist: pulsar-client
17
+ Requires-Dist: pyyaml
18
+ Requires-Dist: prometheus-client
19
+
20
+ Cloze model service for TrustGraph.
@@ -0,0 +1 @@
1
+ Cloze model service for TrustGraph.
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "trustgraph-cloze"
7
+ dynamic = ["version"]
8
+ authors = [{name = "trustgraph.ai", email = "security@trustgraph.ai"}]
9
+ description = "Cloze model classification service for TrustGraph."
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "trustgraph-base>=2.10,<2.11",
14
+ "trustgraph-flow>=2.10,<2.11",
15
+ "torch",
16
+ "transformers",
17
+ "scikit-learn",
18
+ "pulsar-client",
19
+ "pyyaml",
20
+ "prometheus-client",
21
+ ]
22
+ classifiers = [
23
+ "Programming Language :: Python :: 3",
24
+ "Operating System :: OS Independent",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/trustgraph-ai/trustgraph"
29
+
30
+ [project.scripts]
31
+ cloze-hf = "trustgraph.model.cloze.hf:run"
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["trustgraph*"]
35
+
36
+ [tool.setuptools.dynamic]
37
+ version = {attr = "trustgraph.cloze_version.__version__"}
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "2.10.14"
@@ -0,0 +1,2 @@
1
+
2
+ from . service import *
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from . service import run
4
+
5
+ if __name__ == '__main__':
6
+ run()
@@ -0,0 +1,537 @@
1
+
2
+ """
3
+ Cloze model service. Auto-trains a masked language model from labeled
4
+ examples in config, and serves single-label classification with
5
+ confidence scores.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import torch
12
+ from functools import partial
13
+ from sklearn.model_selection import train_test_split
14
+ from dataclasses import dataclass, field
15
+
16
+ from trustgraph.schema import ClozeRequest, ClozeResponse, ClozeModelInfo, Error
17
+ from trustgraph.base import FlowProcessor, ConsumerSpec, ProducerSpec
18
+ from trustgraph.base import ParameterSpec
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ default_ident = "cloze"
23
+ default_model = "distilbert-base-uncased"
24
+
25
+ TRAINING_CONFIG_TYPE = "cloze-training"
26
+
27
+ DEFAULT_TEST_SPLIT = 0.2
28
+ DEFAULT_EPOCHS = 20
29
+ DEFAULT_BATCH_SIZE = 6
30
+ DEFAULT_BACKBONE_LR = 2e-5
31
+ DEFAULT_HEAD_LR = 1e-4
32
+
33
+
34
+ @dataclass
35
+ class TrainedModel:
36
+ model: object = None
37
+ tokenizer: object = None
38
+ tool_tokens: list = field(default_factory=list)
39
+ tool_token_ids: list = field(default_factory=list)
40
+ device: object = None
41
+ accuracy: float | None = None
42
+ train_size: int = 0
43
+ test_size: int = 0
44
+
45
+
46
+ class Processor(FlowProcessor):
47
+
48
+ def __init__(self, **params):
49
+
50
+ model = params.get("model", default_model)
51
+ concurrency = params.get("concurrency", 2)
52
+
53
+ super(Processor, self).__init__(
54
+ **params | {
55
+ "model": model,
56
+ "concurrency": concurrency,
57
+ }
58
+ )
59
+
60
+ self.base_model_name = model
61
+
62
+ self.register_specification(
63
+ ParameterSpec(name="model")
64
+ )
65
+
66
+ self.register_config_handler(
67
+ self.on_training_config, types=[TRAINING_CONFIG_TYPE]
68
+ )
69
+
70
+ self.register_specification(
71
+ ConsumerSpec(
72
+ name="request",
73
+ schema=ClozeRequest,
74
+ handler=self.on_request,
75
+ concurrency=concurrency,
76
+ )
77
+ )
78
+
79
+ self.register_specification(
80
+ ProducerSpec(
81
+ name="response",
82
+ schema=ClozeResponse,
83
+ )
84
+ )
85
+
86
+ # Keyed by model instance name
87
+ self.models = {}
88
+
89
+ # Tracks models currently being trained: {(workspace, instance_name)}
90
+ self.training = set()
91
+
92
+ async def on_training_config(self, workspace, config, version):
93
+
94
+ logger.info(
95
+ f"Loading cloze training configuration version {version} "
96
+ f"for workspace {workspace}"
97
+ )
98
+
99
+ if TRAINING_CONFIG_TYPE not in config:
100
+ return
101
+
102
+ training_configs = config[TRAINING_CONFIG_TYPE]
103
+
104
+ if workspace not in self.models:
105
+ self.models[workspace] = {}
106
+
107
+ # Track which instances were updated
108
+ updated = set()
109
+
110
+ for instance_name, raw_value in training_configs.items():
111
+
112
+ try:
113
+ training_data = json.loads(raw_value)
114
+ training_key = (workspace, instance_name)
115
+ self.training.add(training_key)
116
+
117
+ try:
118
+ loop = asyncio.get_event_loop()
119
+ trained = await loop.run_in_executor(
120
+ None,
121
+ partial(
122
+ self._train_model, instance_name, training_data,
123
+ ),
124
+ )
125
+ finally:
126
+ self.training.discard(training_key)
127
+
128
+ if trained is not None:
129
+ self.models[workspace][instance_name] = trained
130
+ updated.add(instance_name)
131
+ logger.info(
132
+ f"Model instance '{instance_name}' trained "
133
+ f"for workspace {workspace}"
134
+ )
135
+
136
+ except Exception as e:
137
+ logger.error(
138
+ f"Failed to train model instance '{instance_name}' "
139
+ f"for workspace {workspace}: {e}"
140
+ )
141
+
142
+ # Remove instances no longer in config
143
+ for name in list(self.models[workspace].keys()):
144
+ if name not in training_configs:
145
+ del self.models[workspace][name]
146
+ logger.info(
147
+ f"Removed model instance '{name}' "
148
+ f"for workspace {workspace}"
149
+ )
150
+
151
+ def _train_model(self, instance_name, training_data):
152
+
153
+ from transformers import AutoTokenizer, AutoModelForMaskedLM
154
+ from torch.utils.data import Dataset, DataLoader
155
+ from torch.optim import AdamW
156
+
157
+ labels = training_data.get("labels", [])
158
+ if not labels:
159
+ logger.warning(
160
+ f"No labels in training data for '{instance_name}'"
161
+ )
162
+ return None
163
+
164
+ min_accuracy = training_data.get("min-accuracy")
165
+ test_split = training_data.get("test-split", DEFAULT_TEST_SPLIT)
166
+ epochs = training_data.get("epochs", DEFAULT_EPOCHS)
167
+
168
+ # Collect all examples and tool tokens
169
+ tool_tokens = []
170
+ seed_words = {}
171
+ all_train_examples = []
172
+ all_test_examples = []
173
+
174
+ for label_def in labels:
175
+ token = label_def["token"]
176
+ seed = label_def["seed"]
177
+ examples = label_def.get("examples", [])
178
+
179
+ tool_tokens.append(token)
180
+ seed_words[token] = seed
181
+
182
+ if test_split > 0 and len(examples) >= 2:
183
+ train_ex, test_ex = train_test_split(
184
+ examples, test_size=test_split, random_state=42
185
+ )
186
+ else:
187
+ train_ex = examples
188
+ test_ex = []
189
+
190
+ for ex in train_ex:
191
+ all_train_examples.append({"query": ex, "tool": token})
192
+ for ex in test_ex:
193
+ all_test_examples.append({"query": ex, "tool": token})
194
+
195
+ if not all_train_examples:
196
+ logger.warning(
197
+ f"No training examples for '{instance_name}'"
198
+ )
199
+ return None
200
+
201
+ logger.info(
202
+ f"Training '{instance_name}': {len(all_train_examples)} train, "
203
+ f"{len(all_test_examples)} test examples, "
204
+ f"{len(tool_tokens)} labels"
205
+ )
206
+
207
+ # Load base model and tokenizer
208
+ tokenizer = AutoTokenizer.from_pretrained(self.base_model_name)
209
+ model = AutoModelForMaskedLM.from_pretrained(self.base_model_name)
210
+
211
+ # Register tool tokens
212
+ tokenizer.add_special_tokens(
213
+ {"additional_special_tokens": tool_tokens}
214
+ )
215
+ model.resize_token_embeddings(len(tokenizer))
216
+
217
+ # Warm-start embeddings from seed words
218
+ with torch.no_grad():
219
+ input_embeddings = model.get_input_embeddings().weight
220
+ output_embeddings = (
221
+ model.get_output_embeddings().weight
222
+ if model.get_output_embeddings() is not None
223
+ else input_embeddings
224
+ )
225
+
226
+ for tool_token, base_word in seed_words.items():
227
+ tool_id = tokenizer.convert_tokens_to_ids(tool_token)
228
+ base_id = tokenizer.convert_tokens_to_ids(base_word)
229
+
230
+ input_embeddings[tool_id] = (
231
+ input_embeddings[base_id].clone()
232
+ )
233
+ if output_embeddings is not input_embeddings:
234
+ output_embeddings[tool_id] = (
235
+ output_embeddings[base_id].clone()
236
+ )
237
+
238
+ # Build datasets
239
+ class ClozeDataset(Dataset):
240
+ def __init__(self, data, tokenizer, max_length=48):
241
+ self.data = data
242
+ self.tokenizer = tokenizer
243
+ self.max_length = max_length
244
+
245
+ def __len__(self):
246
+ return len(self.data)
247
+
248
+ def __getitem__(self, idx):
249
+ item = self.data[idx]
250
+ prompt = (
251
+ f"Query: {item['query']} "
252
+ f"Tool: {self.tokenizer.mask_token}"
253
+ )
254
+
255
+ encoding = self.tokenizer(
256
+ prompt,
257
+ padding="max_length",
258
+ truncation=True,
259
+ max_length=self.max_length,
260
+ return_tensors="pt"
261
+ )
262
+
263
+ input_ids = encoding["input_ids"].squeeze(0)
264
+ attention_mask = encoding["attention_mask"].squeeze(0)
265
+
266
+ labels = torch.full_like(input_ids, -100)
267
+ mask_positions = (
268
+ input_ids == self.tokenizer.mask_token_id
269
+ ).nonzero(as_tuple=True)[0]
270
+
271
+ target_token_id = self.tokenizer.convert_tokens_to_ids(
272
+ item["tool"]
273
+ )
274
+ labels[mask_positions[0]] = target_token_id
275
+
276
+ return {
277
+ "input_ids": input_ids,
278
+ "attention_mask": attention_mask,
279
+ "labels": labels,
280
+ }
281
+
282
+ device = torch.device(
283
+ "cuda" if torch.cuda.is_available()
284
+ else (
285
+ "mps" if torch.backends.mps.is_available()
286
+ else "cpu"
287
+ )
288
+ )
289
+
290
+ model.to(device)
291
+
292
+ train_dataset = ClozeDataset(all_train_examples, tokenizer)
293
+ train_loader = DataLoader(
294
+ train_dataset, batch_size=DEFAULT_BATCH_SIZE, shuffle=True
295
+ )
296
+
297
+ # Differential learning rates
298
+ backbone_params = [
299
+ p for n, p in model.named_parameters()
300
+ if "vocab" not in n and "embeddings" not in n
301
+ ]
302
+ head_params = [
303
+ p for n, p in model.named_parameters()
304
+ if "vocab" in n or "embeddings" in n
305
+ ]
306
+
307
+ optimizer = AdamW([
308
+ {
309
+ "params": backbone_params,
310
+ "lr": DEFAULT_BACKBONE_LR,
311
+ "weight_decay": 0.01,
312
+ },
313
+ {
314
+ "params": head_params,
315
+ "lr": DEFAULT_HEAD_LR,
316
+ "weight_decay": 0.01,
317
+ },
318
+ ])
319
+
320
+ # Training loop
321
+ model.train()
322
+ for epoch in range(1, epochs + 1):
323
+ total_loss = 0.0
324
+ for batch in train_loader:
325
+ optimizer.zero_grad()
326
+
327
+ input_ids = batch["input_ids"].to(device)
328
+ attention_mask = batch["attention_mask"].to(device)
329
+ labels = batch["labels"].to(device)
330
+
331
+ outputs = model(
332
+ input_ids=input_ids,
333
+ attention_mask=attention_mask,
334
+ labels=labels,
335
+ )
336
+ loss = outputs.loss
337
+
338
+ loss.backward()
339
+ optimizer.step()
340
+ total_loss += loss.item()
341
+
342
+ avg_loss = total_loss / len(train_loader)
343
+ if epoch % 10 == 0 or epoch == 1:
344
+ logger.info(
345
+ f" [{instance_name}] Epoch {epoch:02d}/"
346
+ f"{epochs:02d} - Loss: {avg_loss:.4f}"
347
+ )
348
+
349
+ # Evaluate if we have test data
350
+ tool_token_ids = [
351
+ tokenizer.convert_tokens_to_ids(t) for t in tool_tokens
352
+ ]
353
+
354
+ accuracy = None
355
+
356
+ if all_test_examples:
357
+ model.eval()
358
+ correct = 0
359
+
360
+ with torch.no_grad():
361
+ for item in all_test_examples:
362
+ prompt = (
363
+ f"Query: {item['query']} "
364
+ f"Tool: {tokenizer.mask_token}"
365
+ )
366
+ inputs = tokenizer(
367
+ prompt, return_tensors="pt"
368
+ ).to(device)
369
+
370
+ outputs = model(**inputs)
371
+ logits = outputs.logits
372
+
373
+ mask_idx = (
374
+ inputs["input_ids"] == tokenizer.mask_token_id
375
+ ).nonzero(as_tuple=True)[1][0]
376
+ mask_logits = logits[0, mask_idx, :]
377
+
378
+ candidate_logits = mask_logits[tool_token_ids]
379
+ best_tool_idx = torch.argmax(candidate_logits).item()
380
+ best_tool_token = tool_tokens[best_tool_idx]
381
+
382
+ if best_tool_token == item["tool"]:
383
+ correct += 1
384
+
385
+ accuracy = correct / len(all_test_examples)
386
+ logger.info(
387
+ f" [{instance_name}] Test accuracy: "
388
+ f"{correct}/{len(all_test_examples)} ({accuracy:.1%})"
389
+ )
390
+
391
+ if min_accuracy is not None and accuracy < min_accuracy:
392
+ logger.error(
393
+ f" [{instance_name}] Accuracy {accuracy:.1%} below "
394
+ f"minimum threshold {min_accuracy:.1%}, "
395
+ f"rejecting model"
396
+ )
397
+ return None
398
+
399
+ return TrainedModel(
400
+ model=model,
401
+ tokenizer=tokenizer,
402
+ tool_tokens=tool_tokens,
403
+ tool_token_ids=tool_token_ids,
404
+ device=device,
405
+ accuracy=accuracy,
406
+ train_size=len(all_train_examples),
407
+ test_size=len(all_test_examples),
408
+ )
409
+
410
+ async def on_request(self, msg, consumer, flow):
411
+
412
+ request = msg.value()
413
+ id = msg.properties()["id"]
414
+ producer = flow("response")
415
+
416
+ workspace = flow.workspace
417
+ operation = request.operation or "predict"
418
+
419
+ if operation == "list-models":
420
+ response = self._list_models(workspace)
421
+ await producer.send(response, properties={"id": id})
422
+ return
423
+
424
+ instance_name = request.model
425
+ query = request.query
426
+
427
+ ws_models = self.models.get(workspace, {})
428
+ trained = ws_models.get(instance_name)
429
+
430
+ if trained is None:
431
+ if (workspace, instance_name) in self.training:
432
+ error_type = "model-training"
433
+ error_message = (
434
+ f"Model instance '{instance_name}' is currently "
435
+ f"training in workspace '{workspace}'"
436
+ )
437
+ else:
438
+ error_type = "model-not-found"
439
+ error_message = (
440
+ f"No trained model instance '{instance_name}' "
441
+ f"in workspace '{workspace}'"
442
+ )
443
+
444
+ response = ClozeResponse(
445
+ error=Error(
446
+ type=error_type,
447
+ message=error_message,
448
+ ),
449
+ )
450
+ await producer.send(response, properties={"id": id})
451
+ return
452
+
453
+ try:
454
+ label, confidence = self._predict(trained, query)
455
+ response = ClozeResponse(
456
+ label=label,
457
+ confidence=confidence,
458
+ )
459
+
460
+ except Exception as e:
461
+ logger.error(f"Prediction error: {e}")
462
+ response = ClozeResponse(
463
+ error=Error(
464
+ type="prediction-error",
465
+ message=str(e),
466
+ ),
467
+ )
468
+
469
+ await producer.send(response, properties={"id": id})
470
+
471
+ def _list_models(self, workspace):
472
+
473
+ models = []
474
+
475
+ ws_models = self.models.get(workspace, {})
476
+ for name, trained in ws_models.items():
477
+ models.append(ClozeModelInfo(
478
+ name=name,
479
+ state="ready",
480
+ labels=list(trained.tool_tokens),
481
+ accuracy=trained.accuracy,
482
+ train_size=trained.train_size,
483
+ test_size=trained.test_size,
484
+ ))
485
+
486
+ for ws, instance_name in self.training:
487
+ if ws == workspace and instance_name not in ws_models:
488
+ models.append(ClozeModelInfo(
489
+ name=instance_name,
490
+ state="training",
491
+ ))
492
+
493
+ return ClozeResponse(models=models)
494
+
495
+ def _predict(self, trained, query):
496
+
497
+ trained.model.eval()
498
+
499
+ prompt = (
500
+ f"Query: {query} Tool: {trained.tokenizer.mask_token}"
501
+ )
502
+ inputs = trained.tokenizer(
503
+ prompt, return_tensors="pt"
504
+ ).to(trained.device)
505
+
506
+ with torch.no_grad():
507
+ outputs = trained.model(**inputs)
508
+ logits = outputs.logits
509
+
510
+ mask_idx = (
511
+ inputs["input_ids"] == trained.tokenizer.mask_token_id
512
+ ).nonzero(as_tuple=True)[1][0]
513
+ mask_logits = logits[0, mask_idx, :]
514
+
515
+ candidate_logits = mask_logits[trained.tool_token_ids]
516
+ candidate_probs = torch.softmax(candidate_logits, dim=-1)
517
+ best_idx = torch.argmax(candidate_probs).item()
518
+
519
+ label = trained.tool_tokens[best_idx]
520
+ confidence = candidate_probs[best_idx].item()
521
+
522
+ return label, confidence
523
+
524
+ @staticmethod
525
+ def add_args(parser):
526
+
527
+ FlowProcessor.add_args(parser)
528
+
529
+ parser.add_argument(
530
+ '-m', '--model',
531
+ default=default_model,
532
+ help=f'Base model for fine-tuning (default: {default_model})'
533
+ )
534
+
535
+ def run():
536
+
537
+ Processor.launch(default_ident, __doc__)
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: trustgraph-cloze
3
+ Version: 2.10.14
4
+ Summary: Cloze model classification service for TrustGraph.
5
+ Author-email: "trustgraph.ai" <security@trustgraph.ai>
6
+ Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: trustgraph-base<2.11,>=2.10
12
+ Requires-Dist: trustgraph-flow<2.11,>=2.10
13
+ Requires-Dist: torch
14
+ Requires-Dist: transformers
15
+ Requires-Dist: scikit-learn
16
+ Requires-Dist: pulsar-client
17
+ Requires-Dist: pyyaml
18
+ Requires-Dist: prometheus-client
19
+
20
+ Cloze model service for TrustGraph.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ trustgraph/__init__.py
4
+ trustgraph/cloze_version.py
5
+ trustgraph/model/__init__.py
6
+ trustgraph/model/cloze/__init__.py
7
+ trustgraph/model/cloze/hf/__init__.py
8
+ trustgraph/model/cloze/hf/__main__.py
9
+ trustgraph/model/cloze/hf/service.py
10
+ trustgraph_cloze.egg-info/PKG-INFO
11
+ trustgraph_cloze.egg-info/SOURCES.txt
12
+ trustgraph_cloze.egg-info/dependency_links.txt
13
+ trustgraph_cloze.egg-info/entry_points.txt
14
+ trustgraph_cloze.egg-info/requires.txt
15
+ trustgraph_cloze.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cloze-hf = trustgraph.model.cloze.hf:run
@@ -0,0 +1,8 @@
1
+ trustgraph-base<2.11,>=2.10
2
+ trustgraph-flow<2.11,>=2.10
3
+ torch
4
+ transformers
5
+ scikit-learn
6
+ pulsar-client
7
+ pyyaml
8
+ prometheus-client