"""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)[^>]*>.*?", " ", 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= (kendi SearXNG sunucun) ya da NOROVOX_BRAVE_KEY= (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: ' 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 ") 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()