first commit
This commit is contained in:
commit
377bc51a5b
59
.gitignore
vendored
Normal file
59
.gitignore
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# === sub-módulos clonados ===
|
||||||
|
src/modules/
|
||||||
|
|
||||||
|
# === Autotools build artefacts ===
|
||||||
|
Makefile
|
||||||
|
Makefile.in
|
||||||
|
aclocal.m4
|
||||||
|
autom4te.cache/
|
||||||
|
config.log
|
||||||
|
config.status
|
||||||
|
configure
|
||||||
|
depcomp
|
||||||
|
install-sh
|
||||||
|
missing
|
||||||
|
py-compile
|
||||||
|
stamp-h1
|
||||||
|
|
||||||
|
# === Build & dist directories ===
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
!/dist/releases/
|
||||||
|
!/dist/releases/*
|
||||||
|
*.tar.gz
|
||||||
|
*.tar.bz2
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# === Python cache & venv ===
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
venv/
|
||||||
|
.env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# === Editor / OS junk ===
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# === Logs ===
|
||||||
|
*.log
|
||||||
|
nohup.out
|
||||||
|
|
||||||
|
# === IDE / workspace ===
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# === Backup copies ===
|
||||||
|
*.old
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
125
Makefile.am
Normal file
125
Makefile.am
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
SUBDIRS = src
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Caminhos e artefactos
|
||||||
|
# ===========================
|
||||||
|
TOP_DIR = $(shell pwd)
|
||||||
|
DIST_DIR = $(TOP_DIR)/dist
|
||||||
|
BUILD_DIR = $(TOP_DIR)/build
|
||||||
|
GIT_VER = $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.1-dev")
|
||||||
|
SRC_TAR = $(DIST_DIR)/neuro-$(GIT_VER)-src.tar.gz
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Configurações de Git
|
||||||
|
# ===========================
|
||||||
|
GIT_USER ?= "neo.webmaster.2@gmail.com"
|
||||||
|
GIT_EMAIL ?= "neo.webmaster.2@gmail.com"
|
||||||
|
GIT_REMOTE ?= "origin"
|
||||||
|
GIT_BRANCH ?= "main"
|
||||||
|
COMMIT_MSG ?= "Auto-commit via make git"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Alvos principais
|
||||||
|
# ===========================
|
||||||
|
.PHONY: all tarball git release run clean-local check-remote
|
||||||
|
|
||||||
|
all: $(DIST_DIR)
|
||||||
|
|
||||||
|
$(DIST_DIR):
|
||||||
|
@mkdir -p $(DIST_DIR)
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Empacotamento do código-fonte
|
||||||
|
# ===========================
|
||||||
|
tarball: $(SRC_TAR)
|
||||||
|
|
||||||
|
$(SRC_TAR):
|
||||||
|
@echo "[TAR] Empacotando código-fonte (versão $(GIT_VER))..."
|
||||||
|
@mkdir -p "$(DIST_DIR)" "$(BUILD_DIR)"
|
||||||
|
cd "$(TOP_DIR)" && tar \
|
||||||
|
--exclude="$(notdir $(SRC_TAR))" \
|
||||||
|
--exclude="$(DIST_DIR)" \
|
||||||
|
--exclude="$(BUILD_DIR)" \
|
||||||
|
--exclude='*/__pycache__' \
|
||||||
|
--exclude='*/.venv' \
|
||||||
|
--exclude='*/venv' \
|
||||||
|
--exclude='*.pyc' \
|
||||||
|
--exclude='*.pyo' \
|
||||||
|
--exclude='*.o' \
|
||||||
|
--exclude='*.a' \
|
||||||
|
--exclude='*.so' \
|
||||||
|
-czf "$(SRC_TAR)" .
|
||||||
|
@echo "[✔] Tarball gerado em $(SRC_TAR)"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Git (commit + push)
|
||||||
|
# ===========================
|
||||||
|
git: check-remote
|
||||||
|
@echo "📦 Commit automático → Gitea"
|
||||||
|
@git config user.name $(GIT_USER)
|
||||||
|
@git config user.email $(GIT_EMAIL)
|
||||||
|
@git rev-parse --abbrev-ref HEAD >/dev/null 2>&1 || true
|
||||||
|
@git add -A
|
||||||
|
@git commit -m "$$(echo '$(COMMIT_MSG)')" || echo "Nenhuma modificação para commitar."
|
||||||
|
@git push $(GIT_REMOTE) $(GIT_BRANCH)
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Release (Tarball + Tag)
|
||||||
|
# ===========================
|
||||||
|
release: tarball check-remote
|
||||||
|
@echo "🚀 Publicando build em dist/releases (versão: $(GIT_VER))"
|
||||||
|
@mkdir -p $(DIST_DIR)/releases
|
||||||
|
@cp $(SRC_TAR) $(DIST_DIR)/releases/ 2>/dev/null || echo "⚠️ Nenhum tarball encontrado. Execute 'make tarball' primeiro."
|
||||||
|
@echo "📦 Adicionando releases ignoradas (forçado)"
|
||||||
|
@git add -f $(DIST_DIR)/releases/* 2>/dev/null || echo "⚠️ Nenhum artefato novo para adicionar."
|
||||||
|
@git commit -m "Build automático: release $(GIT_VER)" || echo "Nenhum ficheiro novo para commitar."
|
||||||
|
@git push origin main
|
||||||
|
@TAG="$(GIT_VER)"; \
|
||||||
|
if git rev-parse "$$TAG" >/dev/null 2>&1; then \
|
||||||
|
echo "⚠️ Tag $$TAG já existente."; \
|
||||||
|
else \
|
||||||
|
echo "🏷 Criando tag $$TAG"; \
|
||||||
|
git tag -a "$$TAG" -m "Release automática $$TAG"; \
|
||||||
|
git push origin "$$TAG"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Git Remote (HTTPS → SSH Auto-Fix)
|
||||||
|
# ===========================
|
||||||
|
check-remote:
|
||||||
|
@REMOTE_URL=$$(git remote get-url $(GIT_REMOTE)); \
|
||||||
|
if echo $$REMOTE_URL | grep -q '^https://gitea\.neoricalex\.com'; then \
|
||||||
|
echo "⚠️ Repositório configurado com HTTPS:"; \
|
||||||
|
echo " $$REMOTE_URL"; \
|
||||||
|
echo "🔄 Convertendo para SSH (porta 2222)..."; \
|
||||||
|
SSH_URL=$$(echo $$REMOTE_URL | sed -E 's|https://gitea\.neoricalex\.com[:/]+|ssh://git@gitea.neoricalex.com:2222/|'); \
|
||||||
|
git remote set-url $(GIT_REMOTE) $$SSH_URL; \
|
||||||
|
echo "✅ Remote atualizado para:"; \
|
||||||
|
git remote -v; \
|
||||||
|
else \
|
||||||
|
echo "✅ Remote SSH já configurado:"; \
|
||||||
|
git remote -v | grep $(GIT_REMOTE); \
|
||||||
|
fi; \
|
||||||
|
echo "🔍 Testando conectividade SSH com Gitea..."; \
|
||||||
|
if ssh -T git@gitea.neoricalex.com -p 2222 2>&1 | grep -q "successfully authenticated"; then \
|
||||||
|
echo "✅ Conexão SSH funcional com Gitea."; \
|
||||||
|
else \
|
||||||
|
echo "❌ Falha na autenticação SSH com Gitea."; \
|
||||||
|
echo " Verifique a chave em ~/.ssh/id_ed25519.pub e nas SSH Keys do Gitea."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Teste
|
||||||
|
# ===========================
|
||||||
|
run:
|
||||||
|
@python3 src/root/__main__.py
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Limpeza
|
||||||
|
# ===========================
|
||||||
|
clean-local:
|
||||||
|
@echo "[CLEAN] Removendo diretórios temporários..."
|
||||||
|
@rm -rf $(BUILD_DIR)
|
||||||
|
@find $(DIST_DIR) -maxdepth 1 -type f ! -path "$(DIST_DIR)/releases/*" -delete 2>/dev/null || true
|
||||||
|
@echo "[✔] Limpeza concluída (releases preservadas)"
|
||||||
37
configure.ac
Normal file
37
configure.ac
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
AC_INIT([NEURO], [NEO_VERSION], [https://gitea.neoricalex.com/neo/neuro.git])
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Diretórios base
|
||||||
|
# ===========================
|
||||||
|
AC_CONFIG_AUX_DIR([.])
|
||||||
|
AC_SUBST([TOP_DIR], ['$(CURDIR)'])
|
||||||
|
AC_SUBST([BUILD_DIR], ['$(CURDIR)/build'])
|
||||||
|
AC_SUBST([DIST_DIR], ['$(CURDIR)/dist'])
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Versão dinâmica (Git)
|
||||||
|
# ===========================
|
||||||
|
m4_define([NEO_VERSION],
|
||||||
|
m4_esyscmd_s([git describe --tags --always --dirty 2>/dev/null || echo "0.1-dev"]))
|
||||||
|
AC_SUBST([NEO_VERSION])
|
||||||
|
|
||||||
|
# Caminho do tarball dinâmico (protegido contra M4 expansion)
|
||||||
|
AC_SUBST([SRC_TAR],
|
||||||
|
['$(CURDIR)/dist/neuro-@NEO_VERSION@-src.tar.gz'])
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Automake + Python
|
||||||
|
# ===========================
|
||||||
|
|
||||||
|
AM_INIT_AUTOMAKE([foreign dist-bzip2 no-dist-gzip])
|
||||||
|
AM_PATH_PYTHON([3.0])
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Arquivos Makefile
|
||||||
|
# ===========================
|
||||||
|
AC_CONFIG_FILES([
|
||||||
|
Makefile
|
||||||
|
src/Makefile
|
||||||
|
src/root/Makefile
|
||||||
|
])
|
||||||
|
AC_OUTPUT
|
||||||
18
src/Makefile.am
Normal file
18
src/Makefile.am
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
SUBDIRS = root
|
||||||
|
|
||||||
|
bin_SCRIPTS = neuro
|
||||||
|
CLEANFILES = $(bin_SCRIPTS)
|
||||||
|
EXTRA_DIST = neuro.in
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Substituição dinâmica
|
||||||
|
# ===========================
|
||||||
|
neuro: neuro.in Makefile
|
||||||
|
@which git >/dev/null || { echo "⚠️ Git não encontrado — instale-o manualmente."; exit 1; }
|
||||||
|
sed \
|
||||||
|
-e 's,[@]pythondir[@],$(pythondir),g' \
|
||||||
|
-e 's,[@]PACKAGE[@],$(PACKAGE),g' \
|
||||||
|
-e 's,[@]VERSION[@],$(VERSION),g' \
|
||||||
|
< $(srcdir)/neuro.in > neuro
|
||||||
|
chmod +x neuro
|
||||||
|
|
||||||
40
src/bootstrap.py
Normal file
40
src/bootstrap.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Application(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
for key in kwargs:
|
||||||
|
setattr(self, key, kwargs[key])
|
||||||
|
self.base_path = Path(__file__).resolve().parents[1]
|
||||||
|
self.venv_path = self.base_path / "venv"
|
||||||
|
self.python = self.venv_path / "bin" / "python"
|
||||||
|
self._hello()
|
||||||
|
|
||||||
|
def _hello(self):
|
||||||
|
print(f"NEURO {getattr(self, 'version', '')} — inicializador de ambiente")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self._ensure_venv()
|
||||||
|
self.launch_neuro()
|
||||||
|
|
||||||
|
def _ensure_venv(self):
|
||||||
|
os.chdir(self.base_path)
|
||||||
|
|
||||||
|
if not self.venv_path.exists():
|
||||||
|
print("[+] Criando ambiente Python...")
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "venv", str(self.venv_path)],
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("[=] Ambiente Python já existente.")
|
||||||
|
|
||||||
|
def launch_neuro(self):
|
||||||
|
neuro = self.base_path / "src" / "root" / "__main__.py"
|
||||||
|
subprocess.run([str(self.python), str(neuro)])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
11
src/neuro
Executable file
11
src/neuro
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(1, '/usr/local/local/lib/python3.12/dist-packages')
|
||||||
|
|
||||||
|
from bootstrap import Application
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = Application(package="neuro", version="0.1-dev")
|
||||||
|
app.run()
|
||||||
|
|
||||||
11
src/neuro.in
Normal file
11
src/neuro.in
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(1, '@pythondir@')
|
||||||
|
|
||||||
|
from bootstrap import Application
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = Application(package="@PACKAGE@", version="@VERSION@")
|
||||||
|
app.run()
|
||||||
|
|
||||||
1
src/requirements.lock
Normal file
1
src/requirements.lock
Normal file
@ -0,0 +1 @@
|
|||||||
|
ec72420df5dfbdce4111f715c96338df3b7cb75f58e478d2449c9720e560de8c
|
||||||
1
src/requirements.txt
Normal file
1
src/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
requests
|
||||||
16
src/root/Makefile.am
Normal file
16
src/root/Makefile.am
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# ===========================
|
||||||
|
# Módulos Python do neoricalex
|
||||||
|
# ===========================
|
||||||
|
|
||||||
|
# Diretório de instalação Python
|
||||||
|
neurodir = $(pythondir)
|
||||||
|
|
||||||
|
# Fontes do root
|
||||||
|
dist_neuro_PYTHON = \
|
||||||
|
__main__.py \
|
||||||
|
__init__.py
|
||||||
|
|
||||||
|
# Inclui o bootstrap (nível acima) como parte deste pacote
|
||||||
|
EXTRA_DIST = ../bootstrap.py
|
||||||
|
|
||||||
|
|
||||||
215
src/root/README.md
Normal file
215
src/root/README.md
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
Boa — agora tens o **Codex como “motor de edição/execução”**. A IA que vai interagir com ele pode ser “from scratch” *no teu estilo*: pequena no core, mas completa como sistema.
|
||||||
|
|
||||||
|
A minha sugestão: pensa nela como um **cérebro (orquestrador)** + **ferramentas (Codex + shell + git)** + **memória (estado)** + **protocolo de prompts**.
|
||||||
|
|
||||||
|
A arquitetura abaixo já nasce “Neuro-like” e dá para crescer sem refazer tudo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Papel do Codex no sistema
|
||||||
|
|
||||||
|
O Codex (CLI) é o teu **executor especializado** para:
|
||||||
|
|
||||||
|
* ler ficheiros / contexto do repo
|
||||||
|
* propor alterações
|
||||||
|
* aplicar patches
|
||||||
|
* (opcional) correr comandos/testes
|
||||||
|
|
||||||
|
Ou seja: o teu “LLM principal” (ou regras) decide *o que fazer*; o Codex faz *o trabalho pesado no repo*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Mínimo completo (mas bem fundacional)
|
||||||
|
|
||||||
|
### Componentes
|
||||||
|
|
||||||
|
1. **Kernel de loop** (observe → think → act → rest)
|
||||||
|
|
||||||
|
* lê intenção do utilizador
|
||||||
|
* monta contexto
|
||||||
|
* escolhe ação (Codex / shell / git / responder)
|
||||||
|
|
||||||
|
2. **State** (memória curta e longa)
|
||||||
|
|
||||||
|
* curta: última tarefa, decisões, paths tocados, erros
|
||||||
|
* longa: “project rules”, convenções do repo, preferências
|
||||||
|
|
||||||
|
3. **Tool layer** (wrappers)
|
||||||
|
|
||||||
|
* `CodexTool`: chama o binário do codex e captura output
|
||||||
|
* `GitTool`: status/diff/commit/branch
|
||||||
|
* `ShellTool`: executar comandos (com allowlist)
|
||||||
|
* `FileTool`: read/write/scan (se precisares fora do codex)
|
||||||
|
|
||||||
|
4. **Prompt Protocol** (muito importante)
|
||||||
|
Define um “contrato” estável entre o teu orquestrador e o Codex:
|
||||||
|
|
||||||
|
* **Objective**: o que queremos mudar
|
||||||
|
* **Constraints**: não quebrar build, seguir estilo, não tocar em X
|
||||||
|
* **Context**: ficheiros relevantes + output de testes + diffs
|
||||||
|
* **Expected Output**: patch/diff + comandos sugeridos
|
||||||
|
|
||||||
|
5. **Policy & Safety**
|
||||||
|
|
||||||
|
* allowlist de comandos
|
||||||
|
* limites de paths editáveis
|
||||||
|
* nunca executar coisas destrutivas sem “modo explícito”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Um blueprint que funciona mesmo sem “LLM externo”
|
||||||
|
|
||||||
|
Tu disseste “vamos fazer através de prompts” — perfeito.
|
||||||
|
|
||||||
|
Dá para ter 2 modos:
|
||||||
|
|
||||||
|
### Modo A — “IA determinística” (sem API)
|
||||||
|
|
||||||
|
* heurísticas + templates de prompt
|
||||||
|
* decisões baseadas em regras (ex.: se falha teste → pedir ao codex para corrigir)
|
||||||
|
* ótimo para bootstrap
|
||||||
|
|
||||||
|
### Modo B — “IA híbrida”
|
||||||
|
|
||||||
|
* o mesmo kernel, mas o “think/planning” pode usar OpenAI API depois (quando quiseres)
|
||||||
|
* o Codex continua sendo o executor
|
||||||
|
|
||||||
|
A sacada: **não acoplas o core à API**. A API vira só mais um “ThoughtProvider”.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Fluxo operacional recomendado
|
||||||
|
|
||||||
|
### 1) Observe
|
||||||
|
|
||||||
|
* `git status --porcelain`
|
||||||
|
* (opcional) ler ficheiros-chave (`README`, `pyproject`, `system/`)
|
||||||
|
* recolher logs/erros se houver
|
||||||
|
|
||||||
|
### 2) Think (planner)
|
||||||
|
|
||||||
|
* classificar tarefa: “refactor”, “bugfix”, “feature”, “docs”
|
||||||
|
* gerar “plan steps” pequeno (1–5 passos)
|
||||||
|
* escolher ferramenta: codex vs shell
|
||||||
|
|
||||||
|
### 3) Act (codex)
|
||||||
|
|
||||||
|
* chama codex com prompt estruturado
|
||||||
|
* exige output em formato “patch + rationale + commands”
|
||||||
|
* aplica patch
|
||||||
|
* corre comandos (pytest/mypy/etc) se permitido
|
||||||
|
|
||||||
|
### 4) Verify
|
||||||
|
|
||||||
|
* `git diff` + `git status`
|
||||||
|
* executa testes
|
||||||
|
* se falhar: loop volta com “erro + diff + logs” como contexto
|
||||||
|
|
||||||
|
### 5) Rest
|
||||||
|
|
||||||
|
* grava “worklog” e “memory frame” (json)
|
||||||
|
* atualiza “project_rules.json”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Estrutura de diretórios sugerida (bem limpa)
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/root/
|
||||||
|
ai/
|
||||||
|
__init__.py
|
||||||
|
kernel.py # loop observe/think/act
|
||||||
|
state.py # state + persistence
|
||||||
|
prompts.py # templates de prompt
|
||||||
|
tools/
|
||||||
|
codex.py # wrapper do binário
|
||||||
|
git.py
|
||||||
|
shell.py
|
||||||
|
files.py
|
||||||
|
policies.py # allowlist/paths
|
||||||
|
system/
|
||||||
|
env.py # ensure_codex já está
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) O “prompt contract” com o Codex (o segredo)
|
||||||
|
|
||||||
|
Usa sempre um prompt com esta estrutura (mesmo sem LLM externo):
|
||||||
|
|
||||||
|
* **ROLE**: “You are Codex, repo editor”
|
||||||
|
* **TASK**: objetivo claro
|
||||||
|
* **CONSTRAINTS**: estilo/paths/comandos permitidos
|
||||||
|
* **CONTEXT**: diffs, erros, ficheiros relevantes (mínimo necessário)
|
||||||
|
* **OUTPUT FORMAT**: obrigatório
|
||||||
|
|
||||||
|
Exemplo de output format (super útil):
|
||||||
|
|
||||||
|
* `PLAN:` bullets
|
||||||
|
* `PATCH:` unified diff
|
||||||
|
* `COMMANDS:` lista de comandos para validar
|
||||||
|
|
||||||
|
Isto torna o sistema “programável”.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Como “bem completa” sem virar monstro
|
||||||
|
|
||||||
|
Para ficar completa de verdade, adiciona 4 capacidades logo cedo:
|
||||||
|
|
||||||
|
1. **Controle de escopo**
|
||||||
|
|
||||||
|
* “só tocar em src/root/ai e system/”
|
||||||
|
* evitar alterações fora do perímetro
|
||||||
|
|
||||||
|
2. **Verificação automática**
|
||||||
|
|
||||||
|
* sempre rodar pelo menos `python -m compileall` ou `pytest -q` quando houver
|
||||||
|
|
||||||
|
3. **Journaling**
|
||||||
|
|
||||||
|
* `data/ai_runs/YYYYMMDD_HHMMSS.json`
|
||||||
|
* guarda prompts, diffs, outputs, comandos
|
||||||
|
|
||||||
|
4. **Rollback**
|
||||||
|
|
||||||
|
* se algo der ruim: `git checkout -- .` ou `git reset --hard HEAD` (somente em modo explícito)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Próximo passo prático (eu sugiro)
|
||||||
|
|
||||||
|
Vamos implementar em 3 “camadas” rápidas:
|
||||||
|
|
||||||
|
### Passo 1 — Tool wrapper (CodexTool)
|
||||||
|
|
||||||
|
* função `run(prompt, cwd, timeout)`
|
||||||
|
* captura stdout/stderr
|
||||||
|
* retorna objeto `{ok, text, rc}`
|
||||||
|
|
||||||
|
### Passo 2 — Kernel mínimo (1 loop)
|
||||||
|
|
||||||
|
* lê input do user
|
||||||
|
* monta prompt
|
||||||
|
* chama codex
|
||||||
|
* mostra diff/result
|
||||||
|
|
||||||
|
### Passo 3 — Verify loop
|
||||||
|
|
||||||
|
* roda testes
|
||||||
|
* se falha: chama codex de novo com logs + diff
|
||||||
|
|
||||||
|
Com isso, tens uma “IA” que já age como agente de manutenção do repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Escolha que muda tudo (preciso que decidas já, sem drama)
|
||||||
|
|
||||||
|
Queres que essa IA:
|
||||||
|
|
||||||
|
1. **Aplique mudanças automaticamente** (auto-patch + auto-test), ou
|
||||||
|
2. **Só proponha e tu confirmas** (human-in-the-loop)?
|
||||||
|
|
||||||
|
Eu recomendo começar em **(2)** por segurança e depois habilitar (1) por feature flag.
|
||||||
|
|
||||||
|
Se me disseres “auto” ou “confirm”, eu já te devolvo um esqueleto inicial (arquivos + classes + prompts) pronto para colares no repo.
|
||||||
0
src/root/__init__.py
Normal file
0
src/root/__init__.py
Normal file
25
src/root/__main__.py
Normal file
25
src/root/__main__.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from email.mime import base
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from system.env import ensure_codex
|
||||||
|
from system.requirements import ensure_requirements
|
||||||
|
from ai.cli import run_ai
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base = Path(__file__).resolve().parents[2]
|
||||||
|
python_bin = base / "venv" / "bin" / "python"
|
||||||
|
|
||||||
|
# 1. Dependências Python
|
||||||
|
ensure_requirements(python_bin)
|
||||||
|
|
||||||
|
codex_bin = ensure_codex(base)
|
||||||
|
print("codex:", codex_bin)
|
||||||
|
|
||||||
|
run_ai(base, codex_bin)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
0
src/root/ai/__init__.py
Normal file
0
src/root/ai/__init__.py
Normal file
43
src/root/ai/cli.py
Normal file
43
src/root/ai/cli.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# src/root/ai/cli.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .kernel import Kernel
|
||||||
|
from .policies import policies_from_env
|
||||||
|
from .tools.codex import CodexTool
|
||||||
|
from .tools.git import GitTool
|
||||||
|
from .tools.shell import ShellTool
|
||||||
|
|
||||||
|
|
||||||
|
def run_ai(repo_root: Path, codex_bin: Path, argv: list[str] | None = None) -> None:
|
||||||
|
ap = argparse.ArgumentParser(prog="neuro-ai")
|
||||||
|
ap.add_argument("--auto", action="store_true", help="auto-apply patches (dangerous)")
|
||||||
|
ap.add_argument("--once", action="store_true", help="run a single turn and exit")
|
||||||
|
ap.add_argument("--model", default=None, help="codex model (e.g. gpt-5.3-codex medium)")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
policies = policies_from_env(repo_root, auto_flag=args.auto)
|
||||||
|
|
||||||
|
kernel = Kernel(
|
||||||
|
repo_root=repo_root,
|
||||||
|
policies=policies,
|
||||||
|
codex=CodexTool(codex_bin, model=args.model),
|
||||||
|
git=GitTool(repo_root),
|
||||||
|
shell=ShellTool(policies),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[ai] mode={'AUTO' if policies.auto_apply else 'CONFIRM'}")
|
||||||
|
print("[ai] Enter a task (or empty to exit):")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("> ").strip()
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
if not user_input:
|
||||||
|
break
|
||||||
|
kernel.run_once(user_input)
|
||||||
|
if args.once:
|
||||||
|
break
|
||||||
137
src/root/ai/kernel.py
Normal file
137
src/root/ai/kernel.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# src/root/ai/kernel.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .policies import Policies
|
||||||
|
from .prompts import build_codex_prompt
|
||||||
|
from .state import RunLog, save_runlog
|
||||||
|
from .tools.codex import CodexTool
|
||||||
|
from .tools.git import GitTool
|
||||||
|
from .tools.shell import ShellTool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Kernel:
|
||||||
|
repo_root: Path
|
||||||
|
policies: Policies
|
||||||
|
codex: CodexTool
|
||||||
|
git: GitTool
|
||||||
|
shell: ShellTool
|
||||||
|
|
||||||
|
def _constraints_text(self) -> str:
|
||||||
|
return (
|
||||||
|
f"- Only modify these paths: {', '.join(self.policies.allowed_paths)}\n"
|
||||||
|
"- Output unified diff inside ```diff fences.\n"
|
||||||
|
"- Do not delete large files.\n"
|
||||||
|
"- Keep changes minimal and buildable.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _context_text(self) -> str:
|
||||||
|
snap = self.git.snapshot()
|
||||||
|
ctx = [
|
||||||
|
"Repo status (porcelain):",
|
||||||
|
snap.status_porcelain or "(clean)",
|
||||||
|
"",
|
||||||
|
"Current diff:",
|
||||||
|
snap.diff or "(no diff)",
|
||||||
|
]
|
||||||
|
return "\n".join(ctx)
|
||||||
|
|
||||||
|
def _extract_diff(self, codex_output: str) -> str:
|
||||||
|
# pega conteúdo dentro de ```diff ... ```
|
||||||
|
m = re.search(r"```diff\s*(.*?)```", codex_output, flags=re.DOTALL)
|
||||||
|
return (m.group(1).strip() + "\n") if m else ""
|
||||||
|
|
||||||
|
def run_once(self, user_input: str) -> None:
|
||||||
|
before = self.git.snapshot()
|
||||||
|
|
||||||
|
prompt = build_codex_prompt(
|
||||||
|
task=user_input,
|
||||||
|
constraints=self._constraints_text(),
|
||||||
|
context=self._context_text(),
|
||||||
|
)
|
||||||
|
|
||||||
|
codex_res = self.codex.run(prompt.task, cwd=self.repo_root)
|
||||||
|
if not codex_res.ok:
|
||||||
|
# tentativa fallback via stdin (se quiseres automático)
|
||||||
|
codex_res = self.codex.run_exec_stdin(prompt.task, cwd=self.repo_root)
|
||||||
|
|
||||||
|
print("[ai] codex failed (rc=%d)" % codex_res.rc)
|
||||||
|
print(codex_res.text)
|
||||||
|
# log mesmo assim
|
||||||
|
after = self.git.snapshot()
|
||||||
|
path = save_runlog(self.repo_root, RunLog(
|
||||||
|
ts="now",
|
||||||
|
user_input=user_input,
|
||||||
|
codex_prompt=prompt.task,
|
||||||
|
codex_output=codex_res.text,
|
||||||
|
before_status=before.status_porcelain,
|
||||||
|
before_diff=before.diff,
|
||||||
|
after_status=after.status_porcelain,
|
||||||
|
after_diff=after.diff,
|
||||||
|
applied=False,
|
||||||
|
notes="codex returned non-zero",
|
||||||
|
))
|
||||||
|
print(f"[ai] runlog: {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
diff = self._extract_diff(codex_res.text)
|
||||||
|
|
||||||
|
print("\n========== CODEX OUTPUT ==========\n")
|
||||||
|
print(codex_res.text)
|
||||||
|
|
||||||
|
if not diff.strip():
|
||||||
|
print("\n[ai] No PATCH found in codex output.")
|
||||||
|
after = self.git.snapshot()
|
||||||
|
path = save_runlog(self.repo_root, RunLog(
|
||||||
|
ts="now",
|
||||||
|
user_input=user_input,
|
||||||
|
codex_prompt=prompt.task,
|
||||||
|
codex_output=codex_res.text,
|
||||||
|
before_status=before.status_porcelain,
|
||||||
|
before_diff=before.diff,
|
||||||
|
after_status=after.status_porcelain,
|
||||||
|
after_diff=after.diff,
|
||||||
|
applied=False,
|
||||||
|
notes="no diff extracted",
|
||||||
|
))
|
||||||
|
print(f"[ai] runlog: {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Human-in-the-loop gate
|
||||||
|
applied = False
|
||||||
|
if self.policies.auto_apply:
|
||||||
|
print("\n[ai] AUTO mode enabled -> applying patch.")
|
||||||
|
self.git.apply_patch(diff)
|
||||||
|
applied = True
|
||||||
|
else:
|
||||||
|
print("\n[ai] Proposed PATCH extracted. Apply? [y/N] ", end="", flush=True)
|
||||||
|
ans = input().strip().lower()
|
||||||
|
if ans in ("y", "yes"):
|
||||||
|
self.git.apply_patch(diff)
|
||||||
|
applied = True
|
||||||
|
else:
|
||||||
|
print("[ai] Not applied.")
|
||||||
|
|
||||||
|
after = self.git.snapshot()
|
||||||
|
path = save_runlog(self.repo_root, RunLog(
|
||||||
|
ts="now",
|
||||||
|
user_input=user_input,
|
||||||
|
codex_prompt=prompt.task,
|
||||||
|
codex_output=codex_res.text,
|
||||||
|
before_status=before.status_porcelain,
|
||||||
|
before_diff=before.diff,
|
||||||
|
after_status=after.status_porcelain,
|
||||||
|
after_diff=after.diff,
|
||||||
|
applied=applied,
|
||||||
|
))
|
||||||
|
print(f"[ai] runlog: {path}")
|
||||||
|
|
||||||
|
if applied:
|
||||||
|
print("\n[ai] git status --porcelain:")
|
||||||
|
print(after.status_porcelain or "(clean)")
|
||||||
|
print("\n[ai] git diff:")
|
||||||
|
print(after.diff or "(no diff)")
|
||||||
34
src/root/ai/policies.py
Normal file
34
src/root/ai/policies.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# src/root/ai/policies.py
|
||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Policies:
|
||||||
|
auto_apply: bool
|
||||||
|
repo_root: Path
|
||||||
|
allowed_paths: tuple[str, ...]
|
||||||
|
allowed_shell_prefixes: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def policies_from_env(repo_root: Path, auto_flag: bool | None = None) -> Policies:
|
||||||
|
env_auto = os.getenv("NEURO_AUTO", "").strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
auto_apply = env_auto if auto_flag is None else auto_flag
|
||||||
|
|
||||||
|
return Policies(
|
||||||
|
auto_apply=auto_apply,
|
||||||
|
repo_root=repo_root,
|
||||||
|
# limita onde a IA pode mexer (ajusta depois)
|
||||||
|
allowed_paths=(
|
||||||
|
"src/root/ai/",
|
||||||
|
),
|
||||||
|
# allowlist de comandos (mínimo)
|
||||||
|
allowed_shell_prefixes=(
|
||||||
|
"python ",
|
||||||
|
"python3 ",
|
||||||
|
"pytest",
|
||||||
|
"git ",
|
||||||
|
),
|
||||||
|
)
|
||||||
35
src/root/ai/prompts.py
Normal file
35
src/root/ai/prompts.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# src/root/ai/prompts.py
|
||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptPack:
|
||||||
|
system: str
|
||||||
|
task: str
|
||||||
|
|
||||||
|
|
||||||
|
def build_codex_prompt(
|
||||||
|
task: str,
|
||||||
|
context: str,
|
||||||
|
constraints: str = ("- Do NOT run shell commands.\n"
|
||||||
|
"- Do NOT use network.\n"
|
||||||
|
"- Output unified diff inside ```diff fences.\n"
|
||||||
|
),
|
||||||
|
) -> PromptPack:
|
||||||
|
system = (
|
||||||
|
"You are Codex CLI acting as a repo editor.\n"
|
||||||
|
"Return output in EXACT sections:\n"
|
||||||
|
"PLAN:\n- ...\n"
|
||||||
|
"PATCH:\n```diff\n...unified diff...\n```\n"
|
||||||
|
"COMMANDS:\n- command\n"
|
||||||
|
"NOTES:\n...\n"
|
||||||
|
"If you cannot produce a patch, leave PATCH empty but explain in NOTES.\n"
|
||||||
|
)
|
||||||
|
full = (
|
||||||
|
f"{system}\n"
|
||||||
|
f"TASK:\n{task}\n\n"
|
||||||
|
f"CONSTRAINTS:\n{constraints}\n\n"
|
||||||
|
f"CONTEXT:\n{context}\n"
|
||||||
|
)
|
||||||
|
return PromptPack(system=system, task=full)
|
||||||
30
src/root/ai/state.py
Normal file
30
src/root/ai/state.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# src/root/ai/state.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RunLog:
|
||||||
|
ts: str
|
||||||
|
user_input: str
|
||||||
|
codex_prompt: str
|
||||||
|
codex_output: str
|
||||||
|
before_status: str
|
||||||
|
before_diff: str
|
||||||
|
after_status: str
|
||||||
|
after_diff: str
|
||||||
|
applied: bool
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def save_runlog(repo_root: Path, log: RunLog) -> Path:
|
||||||
|
outdir = repo_root / "data" / "ai_runs"
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
fname = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + ".json"
|
||||||
|
path = outdir / fname
|
||||||
|
path.write_text(json.dumps(asdict(log), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return path
|
||||||
0
src/root/ai/tools/__init__.py
Normal file
0
src/root/ai/tools/__init__.py
Normal file
102
src/root/ai/tools/codex.py
Normal file
102
src/root/ai/tools/codex.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# src/root/ai/tools/codex.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CodexResult:
|
||||||
|
ok: bool
|
||||||
|
rc: int
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
class CodexTool:
|
||||||
|
def __init__(self, codex_bin: Path, model: str | None = None):
|
||||||
|
self.codex_bin = codex_bin
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def run_exec(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
cwd: Path,
|
||||||
|
sandbox: str = "read-only",
|
||||||
|
approval: str = "never",
|
||||||
|
timeout_s: int = 1800,
|
||||||
|
) -> CodexResult:
|
||||||
|
"""
|
||||||
|
Usa: codex exec -C <DIR> -s read-only -a never [--model ...] "<PROMPT>"
|
||||||
|
"""
|
||||||
|
base_cmd = [
|
||||||
|
str(self.codex_bin),
|
||||||
|
"-C", str(cwd),
|
||||||
|
"-s", sandbox,
|
||||||
|
"-a", approval,
|
||||||
|
]
|
||||||
|
if self.model:
|
||||||
|
base_cmd += ["-m", self.model]
|
||||||
|
|
||||||
|
# 1) tenta passar prompt como argumento (mais comum)
|
||||||
|
cmd = base_cmd + ["exec", prompt]
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=str(cwd),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert p.stdout is not None
|
||||||
|
out, _ = p.communicate(timeout=timeout_s)
|
||||||
|
rc = p.returncode or 0
|
||||||
|
if rc == 0:
|
||||||
|
return CodexResult(True, rc, out)
|
||||||
|
# fallback se falhar
|
||||||
|
fallback_needed = True
|
||||||
|
return CodexResult(False, rc, out + ("\n[exec-arg failed]\n" if fallback_needed else ""))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return CodexResult(False, 124, "[TIMEOUT]\n")
|
||||||
|
|
||||||
|
def run_exec_stdin(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
cwd: Path,
|
||||||
|
sandbox: str = "read-only",
|
||||||
|
approval: str = "never",
|
||||||
|
timeout_s: int = 1800,
|
||||||
|
) -> CodexResult:
|
||||||
|
"""
|
||||||
|
Fallback: tenta enviar prompt via stdin (alguns CLIs aceitam quando prompt é omitido)
|
||||||
|
"""
|
||||||
|
base_cmd = [
|
||||||
|
str(self.codex_bin),
|
||||||
|
"-C", str(cwd),
|
||||||
|
"-s", sandbox,
|
||||||
|
"-a", approval,
|
||||||
|
"exec",
|
||||||
|
]
|
||||||
|
if self.model:
|
||||||
|
base_cmd = [
|
||||||
|
str(self.codex_bin),
|
||||||
|
"-C", str(cwd),
|
||||||
|
"-s", sandbox,
|
||||||
|
"-a", approval,
|
||||||
|
] + (["-m", self.model] if self.model else []) + ["exec"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(
|
||||||
|
base_cmd,
|
||||||
|
cwd=str(cwd),
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert p.stdin and p.stdout
|
||||||
|
out, _ = p.communicate(prompt, timeout=timeout_s)
|
||||||
|
rc = p.returncode or 0
|
||||||
|
return CodexResult(rc == 0, rc, out)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return CodexResult(False, 124, "[TIMEOUT]\n")
|
||||||
0
src/root/ai/tools/files.py
Normal file
0
src/root/ai/tools/files.py
Normal file
52
src/root/ai/tools/git.py
Normal file
52
src/root/ai/tools/git.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# src/root/ai/tools/git.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _run_git(args: list[str], cwd: Path) -> str:
|
||||||
|
out = subprocess.check_output(["git"] + args, cwd=str(cwd), text=True, stderr=subprocess.STDOUT)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GitSnapshot:
|
||||||
|
status_porcelain: str
|
||||||
|
diff: str
|
||||||
|
|
||||||
|
|
||||||
|
class GitTool:
|
||||||
|
def __init__(self, repo_root: Path):
|
||||||
|
self.repo_root = repo_root
|
||||||
|
|
||||||
|
def status_porcelain(self) -> str:
|
||||||
|
return _run_git(["status", "--porcelain"], self.repo_root)
|
||||||
|
|
||||||
|
def diff(self) -> str:
|
||||||
|
return _run_git(["diff"], self.repo_root)
|
||||||
|
|
||||||
|
def snapshot(self) -> GitSnapshot:
|
||||||
|
return GitSnapshot(
|
||||||
|
status_porcelain=self.status_porcelain(),
|
||||||
|
diff=self.diff(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_patch(self, patch_text: str) -> None:
|
||||||
|
# Aplica via stdin. Patch deve ser unified diff.
|
||||||
|
p = subprocess.Popen(
|
||||||
|
["git", "apply", "--whitespace=nowarn", "-"],
|
||||||
|
cwd=str(self.repo_root),
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert p.stdin
|
||||||
|
out, _ = p.communicate(patch_text)
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise RuntimeError(f"git apply failed:\n{out}")
|
||||||
|
|
||||||
|
def reset_hard(self) -> None:
|
||||||
|
_run_git(["reset", "--hard", "HEAD"], self.repo_root)
|
||||||
41
src/root/ai/tools/shell.py
Normal file
41
src/root/ai/tools/shell.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# src/root/ai/tools/shell.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from ..policies import Policies
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ShellResult:
|
||||||
|
ok: bool
|
||||||
|
rc: int
|
||||||
|
out: str
|
||||||
|
|
||||||
|
|
||||||
|
class ShellTool:
|
||||||
|
def __init__(self, policies: Policies):
|
||||||
|
self.policies = policies
|
||||||
|
|
||||||
|
def run(self, cmd: str, cwd: Path | None = None, timeout_s: int = 1200) -> ShellResult:
|
||||||
|
cmd = cmd.strip()
|
||||||
|
if not any(cmd.startswith(p) for p in self.policies.allowed_shell_prefixes):
|
||||||
|
return ShellResult(False, 126, f"Blocked command by policy: {cmd}")
|
||||||
|
|
||||||
|
p = subprocess.Popen(
|
||||||
|
["bash", "-lc", cmd],
|
||||||
|
cwd=str(cwd or self.policies.repo_root),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert p.stdout is not None
|
||||||
|
try:
|
||||||
|
out, _ = p.communicate(timeout=timeout_s)
|
||||||
|
rc = p.returncode or 0
|
||||||
|
return ShellResult(ok=(rc == 0), rc=rc, out=out)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
p.kill()
|
||||||
|
out = p.stdout.read() if p.stdout else ""
|
||||||
|
return ShellResult(False, 124, out + "\n[TIMEOUT]\n")
|
||||||
0
src/root/system/__init__.py
Normal file
0
src/root/system/__init__.py
Normal file
279
src/root/system/env.py
Normal file
279
src/root/system/env.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
# system/env.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CODEX_REPO = "https://github.com/openai/codex.git"
|
||||||
|
MIN_RUSTC = (1, 82, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _print(msg: str):
|
||||||
|
print(f"[codex] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: list[str], cwd: Path | None = None, env: dict | None = None):
|
||||||
|
_print(f"RUN: {' '.join(cmd)}")
|
||||||
|
p = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=str(cwd) if cwd else None,
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert p.stdout is not None
|
||||||
|
for line in p.stdout:
|
||||||
|
print(f"[codex] {line.rstrip()}", flush=True)
|
||||||
|
rc = p.wait()
|
||||||
|
if rc != 0:
|
||||||
|
raise RuntimeError(f"Command failed: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_capture(cmd: list[str], env: dict | None = None) -> str:
|
||||||
|
out = subprocess.check_output(cmd, env=env, text=True, stderr=subprocess.STDOUT)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _paths(base: Path):
|
||||||
|
cache = base / ".cache" / "codex"
|
||||||
|
root = cache / "repo"
|
||||||
|
workspace = root / "codex-rs"
|
||||||
|
exe = "codex.exe" if sys.platform.startswith("win") else "codex"
|
||||||
|
target_bin = workspace / "target" / "debug" / exe
|
||||||
|
return cache, root, workspace, target_bin
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rustc_version(line: str) -> tuple[int, int, int]:
|
||||||
|
# rustc 1.76.0 (....)
|
||||||
|
parts = line.split()
|
||||||
|
ver = parts[1].split(".")
|
||||||
|
return (int(ver[0]), int(ver[1]), int(ver[2]))
|
||||||
|
|
||||||
|
|
||||||
|
def _version_ge(a: tuple[int, int, int], b: tuple[int, int, int]) -> bool:
|
||||||
|
return a >= b
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_rust_min(env: dict) -> dict:
|
||||||
|
_print("Checking Rust toolchain...")
|
||||||
|
|
||||||
|
cargo_bin = Path.home() / ".cargo" / "bin"
|
||||||
|
env["PATH"] = f"{cargo_bin}:{env.get('PATH','')}"
|
||||||
|
|
||||||
|
if shutil.which("rustup") is None or shutil.which("cargo") is None or shutil.which("rustc") is None:
|
||||||
|
_print("Rust toolchain not fully found. Installing rustup...")
|
||||||
|
_run(
|
||||||
|
["bash", "-lc", "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"],
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
env["PATH"] = f"{cargo_bin}:{env.get('PATH','')}"
|
||||||
|
|
||||||
|
cur_line = _run_capture(["bash", "-lc", "rustc --version"], env=env)
|
||||||
|
cur = _parse_rustc_version(cur_line)
|
||||||
|
_print(f"rustc version: {cur[0]}.{cur[1]}.{cur[2]} (min required 1.82.0)")
|
||||||
|
|
||||||
|
if not _version_ge(cur, MIN_RUSTC):
|
||||||
|
_print("rustc is too old. Updating stable toolchain...")
|
||||||
|
_run(["bash", "-lc", "rustup toolchain install stable"], env=env)
|
||||||
|
_run(["bash", "-lc", "rustup default stable"], env=env)
|
||||||
|
_run(["bash", "-lc", "rustup update stable"], env=env)
|
||||||
|
|
||||||
|
cur_line2 = _run_capture(["bash", "-lc", "rustc --version"], env=env)
|
||||||
|
cur2 = _parse_rustc_version(cur_line2)
|
||||||
|
_print(f"rustc version after update: {cur2[0]}.{cur2[1]}.{cur2[2]}")
|
||||||
|
if not _version_ge(cur2, MIN_RUSTC):
|
||||||
|
raise RuntimeError(f"rustc still < 1.82.0 after update: {cur_line2}")
|
||||||
|
|
||||||
|
_print("Ensuring rustfmt + clippy...")
|
||||||
|
_run(["bash", "-lc", "rustup component add rustfmt"], env=env)
|
||||||
|
_run(["bash", "-lc", "rustup component add clippy"], env=env)
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _have_sudo() -> bool:
|
||||||
|
return shutil.which("sudo") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _sudo_noprompt_ok() -> bool:
|
||||||
|
if not _have_sudo():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
subprocess.check_call(["sudo", "-n", "true"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_pkg_manager() -> str | None:
|
||||||
|
for pm in ("apt-get", "dnf", "pacman", "zypper"):
|
||||||
|
if shutil.which(pm) is not None:
|
||||||
|
return pm
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _install_system_deps_for_linux_sandbox() -> None:
|
||||||
|
"""
|
||||||
|
Codex linux-sandbox precisa de:
|
||||||
|
- pkg-config
|
||||||
|
- libcap (dev headers + libcap.pc)
|
||||||
|
"""
|
||||||
|
pm = _detect_pkg_manager()
|
||||||
|
if pm is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Missing system deps (pkg-config/libcap). No supported package manager found.\n"
|
||||||
|
"Install manually: pkg-config + libcap development package (contains libcap.pc)."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not _have_sudo():
|
||||||
|
raise RuntimeError(
|
||||||
|
"Missing system deps (pkg-config/libcap) and sudo not found.\n"
|
||||||
|
"Install manually: pkg-config + libcap development package (contains libcap.pc)."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not _sudo_noprompt_ok():
|
||||||
|
# sem prompt: a ensure_codex não pode ficar parada pedindo senha
|
||||||
|
raise RuntimeError(
|
||||||
|
"Missing system deps (pkg-config/libcap). Need sudo privileges.\n"
|
||||||
|
"Run manually (then re-run neuro):\n"
|
||||||
|
" - Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y pkg-config libcap-dev\n"
|
||||||
|
" - Fedora: sudo dnf install -y pkgconf-pkg-config libcap-devel\n"
|
||||||
|
" - Arch: sudo pacman -S --needed pkgconf libcap"
|
||||||
|
)
|
||||||
|
|
||||||
|
_print(f"Installing system dependencies via {pm} (sudo -n)...")
|
||||||
|
|
||||||
|
if pm == "apt-get":
|
||||||
|
_run(["sudo", "-n", "apt-get", "update"])
|
||||||
|
_run(["sudo", "-n", "apt-get", "install", "-y", "pkg-config", "libcap-dev"])
|
||||||
|
elif pm == "dnf":
|
||||||
|
_run(["sudo", "-n", "dnf", "install", "-y", "pkgconf-pkg-config", "libcap-devel"])
|
||||||
|
elif pm == "pacman":
|
||||||
|
_run(["sudo", "-n", "pacman", "-S", "--needed", "--noconfirm", "pkgconf", "libcap"])
|
||||||
|
elif pm == "zypper":
|
||||||
|
_run(["sudo", "-n", "zypper", "--non-interactive", "install", "pkg-config", "libcap-devel"])
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unsupported package manager: {pm}")
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_linux_sandbox_deps(env: dict):
|
||||||
|
# Só faz sentido no Linux
|
||||||
|
if not sys.platform.startswith("linux"):
|
||||||
|
_print("Non-Linux platform detected; skipping linux-sandbox deps check.")
|
||||||
|
return
|
||||||
|
|
||||||
|
_print("Checking system deps for codex-linux-sandbox (pkg-config + libcap)...")
|
||||||
|
|
||||||
|
# pkg-config
|
||||||
|
if shutil.which("pkg-config") is None:
|
||||||
|
_print("pkg-config not found.")
|
||||||
|
_install_system_deps_for_linux_sandbox()
|
||||||
|
return
|
||||||
|
|
||||||
|
# libcap via pkg-config
|
||||||
|
try:
|
||||||
|
subprocess.check_call(["pkg-config", "--exists", "libcap"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
_print("libcap found via pkg-config.")
|
||||||
|
except Exception:
|
||||||
|
_print("libcap not found via pkg-config (libcap.pc missing).")
|
||||||
|
_install_system_deps_for_linux_sandbox()
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_just(env: dict):
|
||||||
|
if shutil.which("just") is None:
|
||||||
|
_print("Installing just...")
|
||||||
|
_run(["bash", "-lc", "cargo install just"], env=env)
|
||||||
|
else:
|
||||||
|
_print("just already installed.")
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_nextest(env: dict):
|
||||||
|
if shutil.which("cargo-nextest") is None:
|
||||||
|
_print("Installing cargo-nextest (optional)...")
|
||||||
|
_run(["bash", "-lc", "cargo install --locked cargo-nextest"], env=env)
|
||||||
|
else:
|
||||||
|
_print("cargo-nextest already installed.")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_codex(base: Path) -> Path:
|
||||||
|
_print("Ensuring Codex build...")
|
||||||
|
|
||||||
|
cache, root, workspace, target_bin = _paths(base)
|
||||||
|
cache.mkdir(parents=True, exist_ok=True)
|
||||||
|
env = dict(os.environ)
|
||||||
|
|
||||||
|
# Repo
|
||||||
|
if not root.exists():
|
||||||
|
_print("Cloning Codex repository...")
|
||||||
|
_run(["git", "clone", CODEX_REPO, str(root)], cwd=cache, env=env)
|
||||||
|
else:
|
||||||
|
_print("Repository already exists. Checking for updates...")
|
||||||
|
_run(["git", "fetch"], cwd=root, env=env)
|
||||||
|
_run(["git", "pull"], cwd=root, env=env)
|
||||||
|
|
||||||
|
if not workspace.exists():
|
||||||
|
raise RuntimeError(f"Expected workspace not found: {workspace}")
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
env = _ensure_rust_min(env)
|
||||||
|
|
||||||
|
# System deps (Linux sandbox)
|
||||||
|
_ensure_linux_sandbox_deps(env)
|
||||||
|
|
||||||
|
# Helper tools
|
||||||
|
_ensure_just(env)
|
||||||
|
_ensure_nextest(env)
|
||||||
|
|
||||||
|
# Build
|
||||||
|
_print("Building Codex...")
|
||||||
|
_run(["bash", "-lc", "cargo build"], cwd=workspace, env=env)
|
||||||
|
|
||||||
|
if not target_bin.exists():
|
||||||
|
raise RuntimeError(f"Codex build failed: binary not found at {target_bin}")
|
||||||
|
|
||||||
|
_print(f"Build complete: {target_bin}")
|
||||||
|
return target_bin
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_ollama():
|
||||||
|
if shutil.which("ollama"):
|
||||||
|
print("[=] Ollama encontrado.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("[+] Ollama não encontrado. A instalar...")
|
||||||
|
subprocess.run(
|
||||||
|
["bash", "-c", "curl -fsSL https://ollama.com/install.sh | sh"],
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ollama_running():
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["ollama", "list"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_ollama_serve():
|
||||||
|
if _ollama_running():
|
||||||
|
print("[=] Ollama serve já ativo.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("[+] A iniciar ollama serve...")
|
||||||
|
|
||||||
|
def _serve():
|
||||||
|
subprocess.run(["ollama", "serve"])
|
||||||
|
|
||||||
|
t = threading.Thread(target=_serve, daemon=True)
|
||||||
|
t.start()
|
||||||
|
time.sleep(2)
|
||||||
46
src/root/system/requirements.py
Normal file
46
src/root/system/requirements.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import hashlib
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_file(path: Path) -> str:
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with path.open("rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(8192), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_requirements(python_bin: Path):
|
||||||
|
"""
|
||||||
|
Instala dependências apenas se requirements.txt mudou.
|
||||||
|
Usa SEMPRE o Python do venv.
|
||||||
|
"""
|
||||||
|
base_path = Path(__file__).resolve().parents[2]
|
||||||
|
req_file = base_path / "requirements.txt"
|
||||||
|
lock_file = base_path / "requirements.lock"
|
||||||
|
|
||||||
|
if not req_file.exists():
|
||||||
|
print("[!] requirements.txt não encontrado. A ignorar.")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_hash = _hash_file(req_file)
|
||||||
|
|
||||||
|
if lock_file.exists() and lock_file.read_text().strip() == current_hash:
|
||||||
|
print("[=] Dependências Python já atualizadas.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("[+] Atualizando dependências Python...")
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[str(python_bin), "-m", "pip", "install", "--upgrade", "pip"],
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[str(python_bin), "-m", "pip", "install", "-r", str(req_file)],
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
lock_file.write_text(current_hash)
|
||||||
|
print("[✓] Dependências instaladas com sucesso.")
|
||||||
171
src/root/teste.py
Normal file
171
src/root/teste.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# mini_ai_3d_xor.py — MLP do zero (numpy) + Adam, softmax e backprop
|
||||||
|
# Dataset 3D XOR (paridade) para classificação binária em 2 classes.
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# ---------- util ----------
|
||||||
|
def accuracy(logits, y):
|
||||||
|
return (np.argmax(logits, axis=1) == y).mean()
|
||||||
|
|
||||||
|
# ---------- dataset (3D XOR / parity) ----------
|
||||||
|
def make_xor3d(n=4000, noise=0.25, seed=0):
|
||||||
|
"""
|
||||||
|
Gera XOR 3D por paridade:
|
||||||
|
label = 1 se (x>0) XOR (y>0) XOR (z>0) for True (ímpar de positivos)
|
||||||
|
label = 0 caso contrário (par de positivos)
|
||||||
|
|
||||||
|
X ~ (±1, ±1, ±1) com ruído gaussiano.
|
||||||
|
"""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
|
||||||
|
# bits 0/1 -> sinais -1/+1
|
||||||
|
bits = rng.integers(0, 2, size=(n, 3), dtype=np.int64) # (n,3) em {0,1}
|
||||||
|
signs = (bits * 2 - 1).astype(np.float32) # -> {-1, +1}
|
||||||
|
|
||||||
|
X = signs + rng.normal(0, noise, size=signs.shape).astype(np.float32)
|
||||||
|
|
||||||
|
# paridade (XOR) dos 3 bits
|
||||||
|
y = (bits[:, 0] ^ bits[:, 1] ^ bits[:, 2]).astype(np.int64) # (n,) em {0,1}
|
||||||
|
|
||||||
|
# normalização leve ajuda (opcional)
|
||||||
|
X = X.astype(np.float32)
|
||||||
|
return X, y
|
||||||
|
|
||||||
|
# ---------- layers ----------
|
||||||
|
class Linear:
|
||||||
|
def __init__(self, in_dim, out_dim, seed=0):
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
self.W = (rng.normal(0, 1, (in_dim, out_dim)).astype(np.float32) * np.sqrt(2.0 / in_dim))
|
||||||
|
self.b = np.zeros((1, out_dim), dtype=np.float32)
|
||||||
|
self.x = None
|
||||||
|
self.dW = np.zeros_like(self.W)
|
||||||
|
self.db = np.zeros_like(self.b)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
self.x = x
|
||||||
|
return x @ self.W + self.b
|
||||||
|
|
||||||
|
def backward(self, grad_out):
|
||||||
|
self.dW = self.x.T @ grad_out
|
||||||
|
self.db = grad_out.sum(axis=0, keepdims=True)
|
||||||
|
return grad_out @ self.W.T
|
||||||
|
|
||||||
|
class ReLU:
|
||||||
|
def __init__(self):
|
||||||
|
self.mask = None
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
self.mask = (x > 0).astype(np.float32)
|
||||||
|
return x * self.mask
|
||||||
|
|
||||||
|
def backward(self, grad_out):
|
||||||
|
return grad_out * self.mask
|
||||||
|
|
||||||
|
# ---------- loss: softmax cross-entropy ----------
|
||||||
|
def softmax(logits):
|
||||||
|
z = logits - logits.max(axis=1, keepdims=True)
|
||||||
|
expz = np.exp(z)
|
||||||
|
return expz / expz.sum(axis=1, keepdims=True)
|
||||||
|
|
||||||
|
def softmax_cross_entropy(logits, y):
|
||||||
|
probs = softmax(logits)
|
||||||
|
N = logits.shape[0]
|
||||||
|
loss = -np.log(probs[np.arange(N), y] + 1e-12).mean()
|
||||||
|
grad = probs
|
||||||
|
grad[np.arange(N), y] -= 1.0
|
||||||
|
grad /= N
|
||||||
|
return loss, grad
|
||||||
|
|
||||||
|
# ---------- optimizer: Adam ----------
|
||||||
|
class Adam:
|
||||||
|
def __init__(self, params, lr=2e-2, betas=(0.9, 0.999), eps=1e-8, weight_decay=1e-4):
|
||||||
|
self.params = params
|
||||||
|
self.lr = lr
|
||||||
|
self.b1, self.b2 = betas
|
||||||
|
self.eps = eps
|
||||||
|
self.wd = weight_decay
|
||||||
|
self.t = 0
|
||||||
|
self.m = [np.zeros_like(p) for (p, _) in params]
|
||||||
|
self.v = [np.zeros_like(p) for (p, _) in params]
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
self.t += 1
|
||||||
|
for i, (p, g) in enumerate(self.params):
|
||||||
|
g = g + self.wd * p # weight decay (tipo AdamW simples)
|
||||||
|
|
||||||
|
self.m[i] = self.b1 * self.m[i] + (1 - self.b1) * g
|
||||||
|
self.v[i] = self.b2 * self.v[i] + (1 - self.b2) * (g * g)
|
||||||
|
|
||||||
|
mhat = self.m[i] / (1 - self.b1 ** self.t)
|
||||||
|
vhat = self.v[i] / (1 - self.b2 ** self.t)
|
||||||
|
|
||||||
|
p -= self.lr * mhat / (np.sqrt(vhat) + self.eps)
|
||||||
|
|
||||||
|
# ---------- model ----------
|
||||||
|
class MLP:
|
||||||
|
def __init__(self, in_dim, hidden_dim, out_dim, seed=0):
|
||||||
|
self.l1 = Linear(in_dim, hidden_dim, seed=seed + 1)
|
||||||
|
self.a1 = ReLU()
|
||||||
|
self.l2 = Linear(hidden_dim, hidden_dim, seed=seed + 2)
|
||||||
|
self.a2 = ReLU()
|
||||||
|
self.l3 = Linear(hidden_dim, out_dim, seed=seed + 3)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
x = self.l1.forward(x)
|
||||||
|
x = self.a1.forward(x)
|
||||||
|
x = self.l2.forward(x)
|
||||||
|
x = self.a2.forward(x)
|
||||||
|
return self.l3.forward(x)
|
||||||
|
|
||||||
|
def backward(self, grad_logits):
|
||||||
|
g = self.l3.backward(grad_logits)
|
||||||
|
g = self.a2.backward(g)
|
||||||
|
g = self.l2.backward(g)
|
||||||
|
g = self.a1.backward(g)
|
||||||
|
_ = self.l1.backward(g)
|
||||||
|
|
||||||
|
def parameters(self):
|
||||||
|
return [
|
||||||
|
(self.l1.W, self.l1.dW), (self.l1.b, self.l1.db),
|
||||||
|
(self.l2.W, self.l2.dW), (self.l2.b, self.l2.db),
|
||||||
|
(self.l3.W, self.l3.dW), (self.l3.b, self.l3.db),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------- train ----------
|
||||||
|
def train(seed=42):
|
||||||
|
X, y = make_xor3d(n=6000, noise=0.30, seed=seed)
|
||||||
|
|
||||||
|
# split treino/val
|
||||||
|
n = X.shape[0]
|
||||||
|
p = np.random.default_rng(seed).permutation(n)
|
||||||
|
X, y = X[p], y[p]
|
||||||
|
|
||||||
|
n_train = int(0.8 * n)
|
||||||
|
Xtr, ytr = X[:n_train], y[:n_train]
|
||||||
|
Xva, yva = X[n_train:], y[n_train:]
|
||||||
|
|
||||||
|
model = MLP(in_dim=3, hidden_dim=64, out_dim=2, seed=seed)
|
||||||
|
opt = Adam(model.parameters(), lr=2e-2, weight_decay=1e-4)
|
||||||
|
|
||||||
|
rng = np.random.default_rng(seed + 123)
|
||||||
|
batch = 256
|
||||||
|
steps = 2000
|
||||||
|
|
||||||
|
for step in range(1, steps + 1):
|
||||||
|
idx = rng.integers(0, Xtr.shape[0], size=batch)
|
||||||
|
xb, yb = Xtr[idx], ytr[idx]
|
||||||
|
|
||||||
|
logits = model.forward(xb)
|
||||||
|
loss, grad = softmax_cross_entropy(logits, yb)
|
||||||
|
model.backward(grad)
|
||||||
|
opt.step()
|
||||||
|
|
||||||
|
if step % 200 == 0 or step == 1:
|
||||||
|
tr_logits = model.forward(Xtr)
|
||||||
|
va_logits = model.forward(Xva)
|
||||||
|
tr_acc = accuracy(tr_logits, ytr)
|
||||||
|
va_acc = accuracy(va_logits, yva)
|
||||||
|
print(f"step {step:4d} | loss {loss:.4f} | train_acc {tr_acc:.3f} | val_acc {va_acc:.3f}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
train()
|
||||||
Loading…
Reference in New Issue
Block a user