Download app/tars_memoria.py from OpenTARS/openTARS: direct link, hf CLI and curl.
- Browser
- Download file 8.27 kB
-
https://huggingface.co/OpenTARS/openTARS/resolve/main/app/tars_memoria.py
- Command line
-
hf download hf://OpenTARS/openTARS/app/tars_memoria.py
-
curl -L -o tars_memoria.py https://huggingface.co/OpenTARS/openTARS/resolve/main/app/tars_memoria.py
8.27 kB
| """openTARS: memória (preferências que a pessoa pediu pra lembrar). | |
| Memória = frases curtas ("meu navegador é o Brave", "a pasta de projetos | |
| é ~/dev"). Vão no prompt de todo pedido. Ficam em | |
| ~/.config/opentars/memoria.json. (Endurance Neo: as rotinas agendadas | |
| saíram; até a 4.0 este arquivo era o tars_rotinas.py.)""" | |
| import datetime as dt | |
| import json | |
| import os | |
| import re | |
| import threading | |
| import time | |
| import unicodedata | |
| from pathlib import Path | |
| import tars_plataforma as plataforma | |
| def pasta_config(): | |
| return plataforma.pastas()["config"] | |
| def guardar_quebrado(caminho): | |
| """Arquivo que não abre como JSON: guarda uma cópia (.corrompido) antes que o próximo salvamento o sobrescreva.""" | |
| try: | |
| os.replace(caminho, str(caminho) + ".corrompido") | |
| except OSError: | |
| pass | |
| def _ler(caminho, padrao): | |
| try: | |
| return json.loads(Path(caminho).read_text(encoding="utf-8")) | |
| except OSError: | |
| return padrao | |
| except ValueError: | |
| guardar_quebrado(caminho) | |
| return padrao | |
| def gravar_texto_atomico(caminho, texto, modo=0o600): | |
| """Escreve num arquivo temporário ÚNICO (0600) na mesma pasta, fsync e rename: duas gravações ao mesmo tempo | |
| não se misturam e uma queda no meio não deixa o arquivo pela metade.""" | |
| import tempfile | |
| caminho = Path(caminho) | |
| caminho.parent.mkdir(parents=True, exist_ok=True) | |
| fd, temporario = tempfile.mkstemp(prefix="." + caminho.name + ".", suffix=".tmp", dir=str(caminho.parent)) | |
| try: | |
| with os.fdopen(fd, "w", encoding="utf-8") as f: | |
| f.write(texto) | |
| f.flush() | |
| os.fsync(f.fileno()) | |
| os.chmod(temporario, modo) | |
| for tentativa in range(50): | |
| try: | |
| os.replace(temporario, caminho) | |
| break | |
| except PermissionError: # Windows: alguém lendo o arquivo agora bloqueia a troca por um instante | |
| if os.name != "nt" or tentativa == 49: | |
| raise | |
| time.sleep(0.02) | |
| except BaseException: | |
| try: | |
| os.unlink(temporario) | |
| except OSError: | |
| pass | |
| raise | |
| def _gravar(caminho, dados): | |
| gravar_texto_atomico(caminho, json.dumps(dados, ensure_ascii=False, indent=1)) | |
| def _normalizar(texto): | |
| texto = unicodedata.normalize("NFKD", str(texto or "").lower()) | |
| return " ".join("".join(c for c in texto if not unicodedata.combining(c)).split()) | |
| LIMITE_MEMORIAS = 40 | |
| LIMITE_CHARS_MEMORIA = 200 | |
| class Memoria: | |
| def __init__(self, caminho=None, agora=None): | |
| self.caminho = Path(caminho or pasta_config() / "memoria.json") | |
| self.agora = agora or dt.datetime.now | |
| self._trava = threading.Lock() | |
| def listar(self): | |
| return [m["texto"] for m in _ler(self.caminho, []) if isinstance(m, dict) and m.get("texto")] | |
| def lembrar(self, texto): | |
| """Guarda (ou atualiza, se já houver uma parecida). Devolve (ok, erro).""" | |
| texto = " ".join(str(texto or "").split()) | |
| if not texto: | |
| return False, "empty" | |
| if len(texto) > LIMITE_CHARS_MEMORIA: | |
| return False, f"too long (max {LIMITE_CHARS_MEMORIA} characters): keep it short" | |
| with self._trava: | |
| itens = [m for m in _ler(self.caminho, []) if isinstance(m, dict) and m.get("texto")] | |
| chave = _normalizar(texto) | |
| itens = [m for m in itens if _normalizar(m["texto"]) != chave] | |
| itens.append({"texto": texto, "quando": self.agora().isoformat(timespec="seconds")}) | |
| _gravar(self.caminho, itens[-LIMITE_MEMORIAS:]) | |
| return True, None | |
| def esquecer(self, alvo): | |
| """Tira as memórias que contêm 'alvo' (ou todas com '*'). Devolve as tiradas.""" | |
| chave = _normalizar(alvo) | |
| if not chave: | |
| return [] | |
| with self._trava: | |
| itens = [m for m in _ler(self.caminho, []) if isinstance(m, dict) and m.get("texto")] | |
| fora = [m["texto"] for m in itens if chave == "*" or chave in _normalizar(m["texto"])] | |
| if fora: | |
| _gravar(self.caminho, [m for m in itens if m["texto"] not in fora]) | |
| return fora | |
| def para_prompt(self): | |
| itens = self.listar() | |
| if not itens: | |
| return "" | |
| linhas = "\n".join(f"- {x}" for x in itens) | |
| return ("\nWHAT THE USER ASKED YOU TO REMEMBER (use it: e.g. their browser, folders, names; " | |
| "don't ask again what is here):\n" + linhas + "\n") | |
| # ------------------------------------------------------------ | |
| # O pedido fala de lembrar/esquecer? (aí a IA ganha essas ferramentas) | |
| # ------------------------------------------------------------ | |
| _RE_MEMORIA = re.compile( | |
| r"\b(lembr(?!ete)\w*|memoriz\w*|esquec\w*|remember\w*|memori[sz]e\w*|forget\w*|recuerd\w*|olvid\w*|" | |
| r"souvien\w*|retien\w*|oubli\w*|merk\w*|vergiss\w*|erinner\w*|what do you know about me|" | |
| r"o que (voce )?sabe (sobre|de) mim|" | |
| # "keep in memory", "save it to memory", "na memória", "salve na memoria" (a palavra memória em si já pede a ferramenta) | |
| r"memor(?:y|ies|ia|ias)|memoire|speicher\w*|" | |
| r"keep in mind|bear in mind|note (?:that|this|down)|from now on|" | |
| r"tenha em mente|de agora em diante|daqui pra frente|anote?\w* que|guard[ae]\w* (?:que|isso|essa|esse))\b") | |
| def fala_de_memoria(texto): | |
| return bool(_RE_MEMORIA.search(_normalizar(texto))) or intencao_de_lembrar(texto) is not None | |
| # ------------------------------------------------------------ | |
| # O pedido MANDA salvar uma coisa? (aí o openTARS salva, mesmo que o modelo não chame a ferramenta) | |
| # ------------------------------------------------------------ | |
| # Só o imperativo no COMEÇO do pedido ("lembra que...", "keep in memory ..."): "você lembra?", "lembre-me de ligar | |
| # amanhã" (lembrete, não preferência) e frases no meio de outro assunto não contam. | |
| _FATO = r"(?P<fato>[^\n]{3,300})" | |
| _PEDE = r"(?:(?:por favor|please|pf|pfv|s'il te pla[iî]t|por favor,?)[ ,]+)?" | |
| _RE_LEMBRAR = [re.compile(p, re.IGNORECASE) for p in ( | |
| # en: "keep/save/put this in memory (that) X", "remember (that) X", "keep in mind (that) X", "note (that) X" | |
| r"^" + _PEDE + r"(?:keep|save|store|put|add|write|record)\s+(?:(?:this|that|it)\s+)?(?:in|to|into|on)\s+(?:your\s+|my\s+|the\s+)?" | |
| r"memor(?:y|ies)\s*(?:that\b|:|,|-)?\s*" + _FATO, | |
| r"^" + _PEDE + r"(?:remember|memorize|memorise)\s*(?:that\b|this\b|:|,|-)?\s*" + _FATO, | |
| r"^" + _PEDE + r"(?:keep|bear)\s+in\s+mind\s*(?:that\b|:|,|-)?\s*" + _FATO, | |
| r"^" + _PEDE + r"note\s+(?:down\s+)?(?:that\b|this\b|:)\s*" + _FATO, | |
| # pt: "lembre/guarde/salve/anote (na memória) que X", "tenha em mente que X" | |
| r"^" + _PEDE + r"(?:guard[ae]|salv[ae]|anot[ae]|grav[ae]|registr[ae])\s+(?:isso\s+|essa\s+|esse\s+)?(?:na|em|a)?\s*mem[oó]ria\s*" | |
| r"(?:que\b|:|,|-)?\s*" + _FATO, | |
| r"^" + _PEDE + r"(?:guard[ae]|anot[ae]|memoriz[ae]|lembr[ae](?!-?me\b)|lembra(?!-?me\b))(?:-se)?\s+(?:de\s+)?(?:que\b|isso\b|:|,|-)\s*" + _FATO, | |
| r"^" + _PEDE + r"(?:memoriz[ae])\s+" + _FATO, | |
| r"^" + _PEDE + r"tenha\s+em\s+mente\s*(?:que\b|:|,|-)?\s*" + _FATO, | |
| # es / fr / de | |
| r"^" + _PEDE + r"(?:recuerda|memoriza|guarda\s+en\s+(?:la\s+)?memoria|ten\s+en\s+cuenta)\s*(?:que\b|:|,|-)?\s*" + _FATO, | |
| r"^" + _PEDE + r"(?:souviens-toi|rappelle-toi|retiens|m[ée]morise|garde\s+en\s+m[ée]moire)\s*(?:que\b|:|,|-)?\s*" + _FATO, | |
| r"^" + _PEDE + r"(?:merk\s+dir|merke\s+dir|erinnere\s+dich\s+daran)\s*,?\s*(?:dass\b|:|,|-)?\s*" + _FATO, | |
| )] | |
| def intencao_de_lembrar(texto): | |
| """O fato que o pedido manda salvar ("keep in memory i use brave as my browser" -> "i use brave as my browser"), | |
| ou None. Pergunta ("você lembra?") e lembrete ("lembre-me de...") não são pedido de salvar.""" | |
| texto = (texto or "").strip() | |
| if not texto or texto.endswith("?"): | |
| return None | |
| primeira = texto.split("\n", 1)[0] if "\n" in texto[:300] else texto | |
| for padrao in _RE_LEMBRAR: | |
| m = padrao.match(primeira) | |
| if m: | |
| fato = m.group("fato").strip(" \t:,;-–—\"'`") | |
| fato = re.sub(r"^(?:que|that|dass|isso|this)\s+", "", fato, flags=re.IGNORECASE).strip() | |
| if len(fato) >= 3: | |
| return fato | |
| return None | |