File size: 8,225 Bytes
5daf54a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""AUBIN Loop — üretici + karar verici döngüsü (generate → decide → refine).

Bir üretici (herhangi bir LLM) aday cevaplar yazar; AUBIN adaylar arasından KALİBRE olasılıkla seçer; güven hedefin
altındaysa üreticiden, önceki adaylar geri bildirim olarak verilip yeni adaylar istenir.

Gemma'da tek ağırlık: GemmaGenerator aynı tabanı AUBIN adaptörü KAPALI kullanır (ek bellek yok), karar AUBIN adaptörüyle.
Dış üretici: OpenAIGenerator (ChatGPT, vLLM, Ollama… OpenAI-uyumlu uç). Anahtar yalnız ortam değişkeninden okunur.

    from aubin import Aubin
    from aubin.loop import AubinLoop, GemmaGenerator, last_number
    A = Aubin("emrevrg/AUBIN-12B")
    loop = AubinLoop(A, GemmaGenerator(A), extract=last_number, target=0.8)
    loop.solve("Natalia sold clips to 48 friends in April, and half as many in May. How many in total?")
    → {"answer": "72", "confidence": 0.97, "rounds": 1, "candidates": [...], "trace": [...]}
"""
from __future__ import annotations
import contextlib, json, os, re, urllib.request
import torch

SOLVE = ("Solve the task below. Reason briefly, then end with a last line exactly of the form 'Final answer: <answer>'.\n\n"
         "TASK:\n{task}{context}")


def last_number(text):
    """Sayısal cevap: 'Final answer:' satırı, yoksa metindeki son sayı (virgül/para birimi temizlenir)."""
    m = re.search(r"final answer\s*[::]\s*(.+)", text, flags=re.I)
    s = m.group(1) if m else text
    nums = re.findall(r"-?\d[\d,]*(?:\.\d+)?", s)
    if not nums:
        return None
    v = nums[0 if m else -1].replace(",", "").rstrip(".")
    try:
        f = float(v)
        return str(int(f)) if f == int(f) else str(f)
    except ValueError:
        return v


def final_line(text):
    m = re.search(r"final answer\s*[::]\s*(.+)", text, flags=re.I)
    return (m.group(1) if m else text.strip().splitlines()[-1] if text.strip() else "").strip() or None


class GemmaGenerator:
    """Aday üretici: AUBIN'in kendi tabanı, adaptör kapalı (tek ağırlık seti)."""

    def __init__(self, aubin, max_new=320, temperature=0.8, top_p=0.95):
        self.A, self.max_new, self.temp, self.top_p = aubin, max_new, temperature, top_p

    def _prompt(self, text):
        msgs = [{"role": "user", "content": text}]
        try:
            return self.A.tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False, enable_thinking=False)
        except Exception:
            return self.A.tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)

    @torch.no_grad()
    def batch(self, texts, greedy=False):
        """Birden çok istem tek toplu üretimde (döngüde ve ölçümde verim için)."""
        tok, m = self.A.tok, self.A.m
        enc = tok([self._prompt(t) for t in texts], return_tensors="pt", padding=True, add_special_tokens=False).to(self.A.dev)
        ctx = m.disable_adapter() if getattr(self.A, "adapters", None) and hasattr(m, "disable_adapter") else contextlib.nullcontext()
        kw = dict(do_sample=False) if greedy else dict(do_sample=True, temperature=self.temp, top_p=self.top_p)
        with ctx:
            gen = m.generate(**enc, max_new_tokens=self.max_new, pad_token_id=tok.pad_token_id, **kw)
        return tok.batch_decode(gen[:, enc["input_ids"].shape[1]:], skip_special_tokens=True)

    def __call__(self, prompt, n, greedy_first=False):
        out = self.batch([prompt], greedy=True) if greedy_first else []
        return out + (self.batch([prompt] * (n - len(out))) if n > len(out) else [])


class OpenAIGenerator:
    """OpenAI-uyumlu sohbet ucu. Anahtar koda/istem yazılmaz: api_key_env ortam değişkeninden okunur."""

    def __init__(self, model="gpt-4o-mini", base_url="https://api.openai.com/v1", api_key_env="OPENAI_API_KEY",
                 temperature=0.8, max_tokens=512):
        self.model, self.url, self.env, self.temp, self.max_tokens = model, base_url.rstrip("/"), api_key_env, temperature, max_tokens

    def __call__(self, prompt, n, greedy_first=False):
        body = {"model": self.model, "messages": [{"role": "user", "content": prompt}], "n": n,
                "temperature": self.temp, "max_tokens": self.max_tokens}
        req = urllib.request.Request(self.url + "/chat/completions", data=json.dumps(body).encode(),
                                     headers={"Content-Type": "application/json",
                                              "Authorization": "Bearer " + os.environ.get(self.env, "")})
        with urllib.request.urlopen(req, timeout=120) as r:
            d = json.load(r)
        return [c["message"]["content"] for c in d["choices"]]


class AubinLoop:
    """generate → decide → refine. decider: Aubin / AubinEnsemble (decide(state, questions) arayüzü)."""

    def __init__(self, decider, generator, extract=final_line, target=0.8, max_rounds=3, n=4, none_option=True,
                 mode="aubin", rationale_chars=600):
        self.D, self.G, self.extract = decider, generator, extract
        self.target, self.max_rounds, self.n, self.none = target, max_rounds, n, none_option
        self.mode, self.rc = mode, rationale_chars              # mode: "aubin" | "aubin+vote" (AUBIN olasılığı × oy payı)

    def question(self, task, cands):
        crit = {f"c{i}": f"{c['answer']} — reasoning: {c['text'][-self.rc:].strip()}" for i, c in enumerate(cands)}
        if self.none:
            crit["none"] = "none of the proposed answers is correct"
        return {"type": "choice", "instructions": "Which proposed answer to the task is correct? Check the reasoning.",
                "criteria": crit}

    def decide(self, task, context, cands):
        res = self.D.decide({"task": task, "context": context or ""}, {"pick": self.question(task, cands)})["pick"]
        probs = dict(res["probabilities"])
        if self.mode == "aubin+vote":                        # az örnekte oy gürültülü: +1 düzeltmeli oy payıyla çarpılır
            tot = sum(c["votes"] for c in cands) + len(cands)
            for i, c in enumerate(cands):
                probs[f"c{i}"] = probs.get(f"c{i}", 0.0) * (c["votes"] + 1) / tot
            z = sum(probs.values()) or 1.0
            probs = {k: v / z for k, v in probs.items()}
        best = max(probs, key=probs.get)
        return best, probs[best], probs

    def solve(self, task, context=None, prior=None):
        """prior: önceden üretilmiş aday metinleri (ör. ölçümde toplu üretilenler) — ilk turda üretici çağrılmaz."""
        cands, trace, feedback = [], [], ""
        for r in range(self.max_rounds):
            outs = prior if (r == 0 and prior is not None) else self.G(SOLVE.format(task=task, context=(
                "\n\nCONTEXT:\n" + context if context else "")) + feedback, self.n)
            for o in outs:
                a = self.extract(o)
                if a is None:
                    continue
                hit = next((c for c in cands if c["answer"] == a), None)
                if hit:
                    hit["votes"] += 1
                else:
                    cands.append({"answer": a, "text": o, "votes": 1})
            if not cands:
                continue
            best, conf, probs = self.decide(task, context, cands)
            trace.append({"round": r + 1, "candidates": [(c["answer"], c["votes"]) for c in cands], "pick": best,
                          "confidence": round(conf, 4)})
            if best != "none" and conf >= self.target:
                break
            feedback = ("\n\nEarlier proposed answers were judged unreliable: " + ", ".join(c["answer"] for c in cands) +
                        ". Solve again carefully from scratch.")
        if not cands:
            return {"answer": None, "confidence": 0.0, "rounds": len(trace), "candidates": [], "trace": trace}
        real = {k: v for k, v in probs.items() if k != "none"}
        k = best if best != "none" else max(real, key=real.get)
        c = cands[int(k[1:])]
        return {"answer": c["answer"], "confidence": round(float(probs[k]), 4), "rounds": len(trace),
                "candidates": [(x["answer"], x["votes"]) for x in cands], "trace": trace,
                "rejected_all": best == "none"}