MiniSEAL / src /hitl /proposal_engine.py
aaronr43210's picture
Upload src
27d37fd verified
Raw History Blame Contribute Delete
4.18 kB
# src/hitl/proposal_engine.py
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
from src.hitl.db import QuarantineDatabase
from src.hitl.validators import RiskValidator
def find_latest_adapter(search_roots):
"""Return the newest PEFT adapter directory across the given roots, or None.
A directory is an adapter iff it contains adapter_config.json. Matching by
that marker (not by name) means adapters from ANY stage are discovered:
SFT (final_adapter), SEAL (seal_adapter), HITL (lora_adapter_v*), DPO
(dpo_adapter_v*). (The old code only matched dirs named "adapter_v*", so
none of the trainers' outputs were ever found.)
"""
found = []
for root in search_roots:
if not os.path.isdir(root):
continue
for dirpath, _dirs, files in os.walk(root):
if "adapter_config.json" in files:
found.append(dirpath)
if not found:
return None
found.sort(key=os.path.getmtime, reverse=True)
return found[0]
class ProposalEngine:
def __init__(self, db: QuarantineDatabase, validator: RiskValidator, base_model_path: str, tokenizer_path: str, device: str):
self.db = db
self.validator = validator
self.device = device
# We ignore base_model_path passed from legacy code and enforce HF Qwen
actual_base_model = "Qwen/Qwen2.5-1.5B-Instruct"
self.tokenizer = AutoTokenizer.from_pretrained(actual_base_model)
print(f"[HITL] Loading Base Model: {actual_base_model}")
self.model = AutoModelForCausalLM.from_pretrained(
actual_base_model,
torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
device_map="auto" if torch.cuda.is_available() else None
)
# Discover the newest LoRA adapter from any stage (SFT/SEAL/HITL/DPO).
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
adapter = find_latest_adapter([
os.path.join(project_root, "ckpts", "adapters"),
os.path.join(project_root, "model_weights"),
])
if adapter:
print(f"[HITL] Loading latest LoRA adapter: {adapter}")
self.model = PeftModel.from_pretrained(self.model, adapter)
else:
print("[HITL] WARNING: No trained LoRA adapters found. Using base model only.")
self.model.eval()
def extract_and_stage(self, passage: str, questions: list[str]) -> int:
"""
Executes a zero-shot model forward pass to generate self-corrections,
validates the risk level, and writes the results to the Quarantine DB.
"""
proposals_created = 0
for q in questions:
messages = [
{"role": "system", "content": "You are a helpful assistant. Answer the user's question accurately using ONLY the provided context."},
{"role": "user", "content": f"Context: {passage}\n\nQuestion: {q}"}
]
text = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = self.tokenizer([text], return_tensors="pt").to(self.device)
with torch.no_grad():
output_ids = self.model.generate(
**inputs,
max_new_tokens=150,
temperature=0.1,
top_p=0.9,
do_sample=True,
pad_token_id=self.tokenizer.eos_token_id
)
generated_tokens = output_ids[0][inputs.input_ids.shape[1]:]
raw_answer = self.tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
# Run automated risk analysis
risk, flags = self.validator.analyze(q, raw_answer)
# Stage into the isolated SQLite database
self.db.add_proposal(passage, q, raw_answer, risk, flags)
proposals_created += 1
return proposals_created