Download code/norovox_embed/agent.py from emrevrg/Norovox-Embed-Zero: direct link, hf CLI and curl.
- Browser
- Download file 17.7 kB
-
https://huggingface.co/emrevrg/Norovox-Embed-Zero/resolve/main/code/norovox_embed/agent.py
- Command line
-
hf download hf://emrevrg/Norovox-Embed-Zero/code/norovox_embed/agent.py
-
curl -L -o agent.py https://huggingface.co/emrevrg/Norovox-Embed-Zero/resolve/main/code/norovox_embed/agent.py
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} | |
| 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() | |