emrevrg's picture
rename: Norovox Embed Zero
7a8d537 verified
Raw History Blame Contribute Delete
17.7 kB
"""Norovox Embed Zero Agent — hafıza, web araştırması, kendi kendine öğrenme ve uzun bağlam; Norovox Embed Zero vektörleri üzerinde.
from norovox_embed import NorovoxEmbed
from norovox_embed.agent import Agent
a = Agent(NorovoxEmbed.from_pretrained()) # düşünen çekirdek: varsayılan yerel Norovox Alpha
print(a.chat("İstanbul'da dere ıslahı neden sel riskini artırır?"))
Ne yapar:
* HAFIZA (unutmaz): her konuşma turu, okunan her web parçası ve öğrenilen her bilgi Embed vektörüyle diske yazılır
(~/.norovox/memory). Yeni soruda en ilgili eski turlar ve bilgiler benzerlikle geri çağrılır → bağlam penceresi dolsa da
hiçbir şey kaybolmaz.
* WEB ARAŞTIRMASI: hafızada yeterince ilgili bilgi yoksa (benzerlik eşiği) ya da düşünen çekirdek ARA: komutu verirse web'de
arar (DuckDuckGo HTML + Wikipedia), sayfaları okur, parçalara böler, Embed ile sıralar, en iyi parçaları kaynaklarıyla verir.
* KENDİ KENDİNE ÖĞRENME: web'den gelen ve cevapta kullanılan parçalar hafızaya "öğrenildi" olarak eklenir; aynı konu bir daha
sorulduğunda web'e çıkmadan cevaplanır.
* UZUN/ZOR İŞLER: plan → ara → oku → yeniden ara (en çok `max_steps` tur) → cevap; her adımın gerekçesi `trace` içinde.
* DÜŞÜNEN ÇEKİRDEK takılabilir: `NorovoxAlphaThinker` (kendi çekirdeğimiz, yerel) ya da OpenAI-uyumlu herhangi bir uç nokta
(`EndpointThinker`, ör. llama-server). Konuşma kalitesi takılan çekirdeğe bağlıdır; hafıza/arama/öğrenme Embed'in işidir.
"""
from __future__ import annotations
import html
import json
import os
import re
import time
import urllib.parse
import urllib.request
import numpy as np
UA = {"User-Agent": "NorovoxEmbedAgent/0.1 (https://huggingface.co/emrevrg/Norovox-Embed-Zero)"} # Wikimedia istemci kimliği kuralı
_CACHE = {}
# ------------------------------------------------------------------ hafıza
class Memory:
"""Kalıcı vektör hafıza: metin + tür + kaynak + zaman; vektörler float16 (768) diske yazılır."""
def __init__(self, embed, path=None):
self.e = embed
self.path = path or os.path.join(os.path.expanduser("~"), ".norovox", "memory")
os.makedirs(self.path, exist_ok=True)
self.meta_p, self.vec_p = os.path.join(self.path, "meta.jsonl"), os.path.join(self.path, "vec.npy")
self.meta = [json.loads(l) for l in open(self.meta_p, encoding="utf-8")] if os.path.exists(self.meta_p) else []
self.V = np.load(self.vec_p).astype(np.float32) if os.path.exists(self.vec_p) else np.zeros((0, 768), np.float32)
self._keys = {m["key"] for m in self.meta}
@staticmethod
def _key(t):
return re.sub(r"\W+", "", t.lower())[:200]
def add(self, texts, kind="note", source=""):
texts = [t for t in texts if t and t.strip() and self._key(t) not in self._keys]
if not texts:
return 0
v = self.e.encode(texts, kind="document")
with open(self.meta_p, "a", encoding="utf-8") as f:
for t in texts:
m = {"text": t, "kind": kind, "source": source, "t": time.time(), "key": self._key(t)}
self.meta.append(m); self._keys.add(m["key"]); f.write(json.dumps(m, ensure_ascii=False) + "\n")
self.V = np.concatenate([self.V, v.astype(np.float32)]) if len(self.V) else v.astype(np.float32)
np.save(self.vec_p, self.V.astype(np.float16))
return len(texts)
def search(self, query, k=6, kinds=None):
if not len(self.V):
return []
q = self.e.encode([query], kind="query")[0]
s = self.V @ q
idx = np.argsort(-s)
out = []
for i in idx:
if kinds and self.meta[i]["kind"] not in kinds:
continue
out.append({**self.meta[i], "score": float(s[i])})
if len(out) >= k:
break
return out
def __len__(self):
return len(self.meta)
# ------------------------------------------------------------------ web
def _get(url, timeout=12, tries=3):
"""GET + önbellek; 429/5xx'te artan beklemeyle yeniden dener (sunucu kısıtlamasına saygı)."""
if url in _CACHE:
return _CACHE[url]
import urllib.error
for i in range(tries):
try:
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=timeout) as r:
raw = r.read(2_000_000)
cs = r.headers.get_content_charset() or "utf-8"
_CACHE[url] = raw.decode(cs, errors="replace")
return _CACHE[url]
except urllib.error.HTTPError as e:
if e.code in (429, 500, 502, 503, 504) and i < tries - 1:
time.sleep(int(e.headers.get("Retry-After", "0") or 0) or 2 * (i + 1)); continue
raise
except urllib.error.URLError:
if i < tries - 1:
time.sleep(2 * (i + 1)); continue
raise
def _text(page):
page = re.sub(r"(?is)<(script|style|noscript|header|footer|nav|svg)[^>]*>.*?</\1>", " ", page)
page = re.sub(r"(?s)<[^>]+>", " ", page)
return re.sub(r"\s+", " ", html.unescape(page)).strip()
STOP = set("""ve veya ile ama fakat de da mi mı mu mü ne neden nasıl niçin niye hangi kim kime nedir midir mıdır mudur müdür bir bu şu o
için gibi kadar daha çok en çok az olarak olan olur olursa olunca ise ki ya yani tekrar açıklar açıklar mısın misin musun anlat söyle
artar azalır nelerdir nerede ne zaman zaman the a an of to in on for and or is are was were why how what which who when where does do
did can could would should will explain again about with from that this these those it its as by be been""".split())
def keywords(q, n=6):
"""Uzun soru → arama motoruna uygun kısa anahtar sözcük sorgusu (durak sözcükler atılır, sıra korunur)."""
w = [x for x in re.findall(r"[\wçğıöşüÇĞİÖŞÜ]+", q.lower()) if x not in STOP and len(x) > 2]
return " ".join(w[:n])
def web_search(query, n=6):
"""Resmi, anahtarsız uç noktalar: Wikipedia tam metin arama (tr + en) + DuckDuckGo Instant Answer API.
İsteğe bağlı: NOROVOX_SEARXNG=<url> (kendi SearXNG sunucun) ya da NOROVOX_BRAVE_KEY=<anahtar> (Brave Search API).
Dönen: [(başlık, url)]; Wikipedia sayfaları read_chunks'ta düz metin API'siyle okunur."""
out = []
sx, bk = os.environ.get("NOROVOX_SEARXNG"), os.environ.get("NOROVOX_BRAVE_KEY")
try:
if sx:
j = json.loads(_get(sx.rstrip("/") + "/search?format=json&q=" + urllib.parse.quote(query)))
out += [(r.get("title", ""), r["url"]) for r in j.get("results", [])[:n]]
if bk:
req = urllib.request.Request("https://api.search.brave.com/res/v1/web/search?q=" + urllib.parse.quote(query),
headers={**UA, "Accept": "application/json", "X-Subscription-Token": bk})
with urllib.request.urlopen(req, timeout=12) as r:
j = json.loads(r.read())
out += [(r.get("title", ""), r["url"]) for r in j.get("web", {}).get("results", [])[:n]]
except Exception: # noqa: BLE001
pass
for lang in ("tr", "en"):
try:
j = json.loads(_get(f"https://{lang}.wikipedia.org/w/api.php?action=query&list=search&format=json&srlimit=3&srsearch="
+ urllib.parse.quote(query)))
out += [(r["title"], f"https://{lang}.wikipedia.org/wiki/" + urllib.parse.quote(r["title"].replace(" ", "_")))
for r in j["query"]["search"]]
except Exception: # noqa: BLE001
pass
try:
j = json.loads(_get("https://api.duckduckgo.com/?format=json&no_html=1&q=" + urllib.parse.quote(query)))
if j.get("AbstractURL"):
out.append((j.get("Heading", ""), j["AbstractURL"]))
for t in j.get("RelatedTopics", [])[:3]:
if isinstance(t, dict) and t.get("FirstURL"):
out.append((t.get("Text", "")[:80], t["FirstURL"]))
except Exception: # noqa: BLE001
pass
seen, res = set(), []
for t, u in out:
if u not in seen:
seen.add(u); res.append((t, u))
return res[:n]
def _page_text(url):
m = re.match(r"https://(\w+)\.wikipedia\.org/wiki/(.+)", url)
if m: # Wikipedia: temiz düz metin API'si
j = json.loads(_get(f"https://{m.group(1)}.wikipedia.org/w/api.php?action=query&prop=extracts&explaintext=1&format=json&titles="
+ m.group(2)))
return " ".join(p.get("extract", "") for p in j["query"]["pages"].values())
return _text(_get(url))
def read_chunks(url, size=700, overlap=120, limit=40):
try:
t = re.sub(r"\s+", " ", _page_text(url))
except Exception: # noqa: BLE001
return []
ch, i = [], 0
while i < len(t) and len(ch) < limit:
ch.append(t[i:i + size]); i += size - overlap
return [c for c in ch if len(c) > 120]
# ------------------------------------------------------------------ düşünen çekirdekler
class EndpointThinker:
"""OpenAI-uyumlu sohbet uç noktası (ör. llama-server --port 8080)."""
def __init__(self, url="http://127.0.0.1:8080/v1/chat/completions", model="local", max_tokens=700):
self.url, self.model, self.max_tokens = url, model, max_tokens
def __call__(self, messages):
body = json.dumps({"model": self.model, "messages": messages, "max_tokens": self.max_tokens, "temperature": 0.3}).encode()
req = urllib.request.Request(self.url, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=600) as r:
return json.loads(r.read())["choices"][0]["message"]["content"]
class NorovoxAlphaThinker:
"""Kendi çekirdeğimiz (NorovoxAlphaCore, nedensel kip) ile yerel üretim; ağırlık deposu ve dosya adı ayarlanabilir."""
def __init__(self, repo="emrevrg/Norovox-Alpha-0.3B", ckpt="norovox_alpha_sft.pt", device=None, max_new=400):
import sys
import torch
from huggingface_hub import hf_hub_download
from transformers import AutoTokenizer
sys.path.insert(0, os.path.dirname(hf_hub_download(repo, "norovox_alpha_core.py")))
from norovox_alpha_core import NorovoxAlphaCore, CoreConfig
ck = torch.load(hf_hub_download(repo, ckpt), map_location="cpu", weights_only=False)
self.core = NorovoxAlphaCore(CoreConfig(**ck["cfg"])); self.core.load_state_dict(ck.get("model", ck), strict=False)
self.core.lm_head.weight = self.core.embed.weight
self.dev = device or ("cuda" if torch.cuda.is_available() else "cpu"); self.core.to(self.dev).eval()
self.tok, self.torch, self.max_new = AutoTokenizer.from_pretrained(repo), torch, max_new
def __call__(self, messages):
torch = self.torch
prompt = "".join(f"<|{m['role']}|>\n{m['content']}\n" for m in messages) + "<|assistant|>\n"
ids = torch.tensor([self.tok(prompt, add_special_tokens=False)["input_ids"][-1800:]], device=self.dev)
out = []
with torch.no_grad():
for _ in range(self.max_new):
lg = self.core(ids)["logits"][0, -1].float()
nxt = int(torch.multinomial(torch.softmax(lg / 0.7, -1), 1))
if nxt == self.tok.eos_token_id:
break
out.append(nxt); ids = torch.cat([ids, torch.tensor([[nxt]], device=self.dev)], 1)
if len(out) % 16 == 0 and "<|" in self.tok.decode(out[-16:]):
break
return self.tok.decode(out, skip_special_tokens=True).split("<|")[0].strip()
# ------------------------------------------------------------------ ajan
SYSTEM = ("Sen Norovox'sun. Cevabı yalnız verilen BAĞLAM ve HAFIZA'ya dayandır, kaynak numarası ver [1]. Bilgi yetmiyorsa tek satır "
"'ARA: <arama sorgusu>' yaz (başka bir şey yazma). Zor işlerde önce kısa bir plan yaz, sonra adım adım yürü, sonunda net cevap ver.")
class Agent:
def __init__(self, embed, thinker=None, memory_path=None, web=True, recall_min=0.6, max_steps=3, budget_chars=6000):
self.e, self.mem = embed, Memory(embed, memory_path)
self.think = thinker
self.web, self.recall_min, self.max_steps, self.budget = web, recall_min, max_steps, budget_chars
self.history, self.trace = [], []
def _thinker(self):
if self.think is None:
self.think = NorovoxAlphaThinker()
return self.think
def _covers(self, query, rec):
"""Hafıza kaydı soruyu karşılıyor mu: benzerlik eşiği VE sorunun anahtar sözcüklerinden en az ikisi (tek sözcüklü soruda biri)
kayıtta geçiyor. Yalnız benzerlik yetmez: gömme uzayında ilgisiz metinler de orta düzey benzerlik alır."""
kw = [w for w in keywords(query, 8).split() if len(w) > 2]
txt = rec["text"].lower()
need = min(2, len(kw)) if kw else 1
return rec["score"] >= self.recall_min and sum(w[:6] in txt for w in kw) >= need
def research(self, query, k=6):
"""Web: ara → oku → Embed ile parçaları sırala → en iyi k parça (kaynakla)."""
hits, seen = [], set()
for qq in dict.fromkeys([keywords(query), keywords(query, 3), query]): # kısa → kısa-öz → tam soru
for h in web_search(qq) if qq else []:
if h[1] not in seen:
seen.add(h[1]); hits.append(h)
if len(hits) >= 4:
break
chunks, src = [], []
for title, url in hits[:5]:
for c in read_chunks(url):
chunks.append(c); src.append(url)
if not chunks:
return []
q = self.e.encode([query], kind="query")[0]; D = self.e.encode(chunks, kind="document")
order = np.argsort(-(D @ q))[:k]
return [{"text": chunks[i], "source": src[i], "score": float(D[i] @ q)} for i in order]
def _context(self, query, web_hits):
recall = self.mem.search(query, k=8)
ctx, used = [], 0
for i, r in enumerate(web_hits + [x for x in recall if x["kind"] != "turn"]):
s = f"[{i + 1}] ({r.get('source') or r.get('kind')}) {r['text']}"
if used + len(s) > self.budget:
break
ctx.append(s); used += len(s)
turns = [x for x in recall if x["kind"] == "turn"][:4]
return ctx, turns, recall
def chat(self, user):
self.trace = []
query, web_hits = user, []
recall = self.mem.search(user, k=4, kinds=("learned", "note", "web"))
if self.web and not any(self._covers(user, r) for r in recall):
web_hits = self.research(user); self.trace.append({"step": "web", "query": user, "hits": len(web_hits)})
answer = ""
for step in range(self.max_steps):
ctx, turns, _ = self._context(query, web_hits)
msgs = [{"role": "system", "content": SYSTEM}]
if turns:
msgs.append({"role": "system", "content": "HAFIZA (eski konuşmalar):\n" + "\n".join(t["text"] for t in turns)})
msgs += self.history[-6:]
msgs.append({"role": "user", "content": ("BAĞLAM:\n" + "\n".join(ctx) + "\n\nSORU: " if ctx else "") + user})
answer = self._thinker()(msgs)
m = re.match(r"\s*ARA:\s*(.+)", answer)
if m and self.web and step < self.max_steps - 1:
query = m.group(1).strip(); more = self.research(query)
self.trace.append({"step": "web", "query": query, "hits": len(more)}); web_hits = more + web_hits
continue
break
if web_hits: # kendi kendine öğrenme: kullanılan web bilgisi hafızaya
n = self.mem.add([h["text"] for h in web_hits[:4]], kind="learned", source=web_hits[0]["source"])
self.trace.append({"step": "learn", "added": n})
self.mem.add([f"Kullanıcı: {user}\nNorovox: {answer}"], kind="turn")
self.history += [{"role": "user", "content": user}, {"role": "assistant", "content": answer}]
return answer
def learn(self, texts, source="kullanıcı"):
"""Doğrudan öğret: metinler hafızaya eklenir, sonraki sorularda kullanılır."""
return self.mem.add(texts if isinstance(texts, list) else [texts], kind="note", source=source)
def main():
"""python -m norovox_embed.agent [--endpoint URL] [--memory DIR] [--no-web]"""
import argparse
from norovox_embed import NorovoxEmbed
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="emrevrg/Norovox-Embed-Zero"); ap.add_argument("--endpoint", default="")
ap.add_argument("--memory", default=None); ap.add_argument("--no-web", action="store_true")
a = ap.parse_args()
th = EndpointThinker(a.endpoint) if a.endpoint else None
ag = Agent(NorovoxEmbed.from_pretrained(a.model), thinker=th, memory_path=a.memory, web=not a.no_web)
print(f"Norovox — hafızada {len(ag.mem)} kayıt. Çıkmak için boş satır; öğretmek için: /öğren <metin>")
while True:
try:
u = input("> ").strip()
except EOFError:
break
if not u:
break
if u.startswith("/öğren "):
print("öğrenildi:", ag.learn(u[7:])); continue
print(ag.chat(u)); print(" ", ag.trace)
if __name__ == "__main__":
main()