În Lecția 3 ai văzut că un sistem agentic nu este un singur program, ci un ansamblu de responsabilități: hardware, sistem de operare, drivere, software de accelerare, runtime-uri, modele, controller, instrumente, date, securitate, observabilitate, interfață și deployment. În laborator transformăm harta conceptuală în ceva verificabil pe propriul calculator.
Construim două artefacte diferite și tocmai diferența dintre ele este ideea centrală a laboratorului:
Înregistrează ce poate detecta acum calculatorul: sistem de operare, procesor, RAM, controlere grafice și câteva utilitare de diagnostic sau dezvoltare.
Înregistrează ce responsabilități are proiectul și ce decizii de arhitectură am luat. Unele alegeri pot rămâne intenționat neselectate.
Un program instalat nu devine automat parte din arhitectura proiectului. Invers, faptul că un program lipsește nu înseamnă automat că trebuie instalat. Mai întâi observăm, apoi descriem nevoile, iar abia după aceea alegem produse.
Înainte să rulezi comenzi, privește structura pe care o vei construi. Un începător lucrează mai ușor când știe unde trebuie să ajungă fiecare fișier.
ai-agents-zero-to-expert/
├── configs/
│ ├── lesson03_stack_manifest.json
│ └── lesson03_broken_stack_manifest.json
├── data/
│ └── lesson03/
│ └── system_inventory.json
├── diagrams/
│ ├── lesson03_stack.dot
│ └── lesson03_stack.png
├── docs/
│ ├── architecture-decisions/
│ │ └── ADR-0003-stack-before-products.md
│ └── lessons/
│ └── lesson-03/
│ ├── broken_manifest_report.md
│ ├── stack_manifest_report.md
│ └── system_inventory.md
├── logs/
│ └── lesson03/
├── src/
│ ├── lesson03_manifest_validator.py
│ └── lesson03_stack_inspector.py
└── tests/
└── test_lesson03_stack.py
Fișierele au trei roluri. Codul inspectează și validează. Datele și rapoartele păstrează observațiile și rezultatele. Documentația de arhitectură explică deciziile pe care le-am luat.
Un inventar tehnic poate deveni inutil de intruziv dacă adună „tot ce găsește”. Noi colectăm numai informația necesară pentru obiectivul lecției.
Programul va citi numele distribuției Linux, versiunea kernelului, arhitectura CPU, cantitatea totală de RAM, o descriere a procesorului, controlerele grafice raportate de magistrala PCI și starea câtorva utilitare. Nu va colecta numele utilizatorului, hostname-ul, adresele IP, numerele de serie, valorile variabilelor de mediu, istoricul browserului sau fișiere personale.
De asemenea, programul Python nu folosește sudo. Comenzile GPU pe care le executăm sunt interogări de diagnostic; nu configurăm dispozitivele.
PATH.PATH: este lista de foldere în care shell-ul caută executabile atunci când scrii numele unei comenzi. Dacă un program există pe disc, dar folderul lui nu se află în PATH, shell-ul poate spune totuși că nu găsește acea comandă.Nu memoriza o listă de nume. Reține pentru fiecare instrument întrebarea la care răspunde.
Rulează inspectorul și validatorul. Folosim numai biblioteca standard; laboratorul nu instalează pachete Python externe.
Păstrează istoricul proiectului și ne ajută să separăm artefactele revizuite de rezultatele specifice unei mașini.
lspci / pciutilsListează dispozitivele vizibile prin magistrala PCI. Îl folosim ca să vedem ce controlere VGA, 3D sau Display raportează sistemul.
jqFormatează și filtrează JSON direct în terminal, fără să modificăm fișierul.
treeAfișează folderele și fișierele sub formă de arbore și ne ajută să verificăm structura proiectului.
dotTransformă o descriere textuală a unui graf într-o imagine. Diagrama este documentație, nu cod care controlează agentul.
curlEste un client pentru protocoale de rețea, inclusiv HTTP. În laborator nu îl folosim pentru a trimite inventarul; inspectorul doar notează dacă este deja disponibil.
Laboratorul continuă același proiect din Lecțiile 1–2. Comenzile de creare a folderelor sunt sigure și dacă folderele există deja.
Încearcă mai întâi:
cd ~/ai-agents-zero-to-expert
pwd
Calea afișată de pwd trebuie să se termine în /ai-agents-zero-to-expert. Dacă cd spune că folderul nu există, creează-l și intră în el:
mkdir -p ~/ai-agents-zero-to-expert
cd ~/ai-agents-zero-to-expert
Acum creează folderele folosite până la această lecție:
mkdir -p \
src \
tests \
configs \
data/lesson03 \
diagrams \
logs/lesson03 \
docs/lessons/lesson-03 \
docs/architecture-decisions \
skills \
evaluations
mkdir -p creează părinții care lipsesc și nu eșuează dacă un folder există deja. Backslash-ul de la sfârșitul liniei spune shell-ului că aceeași comandă continuă pe linia următoare.
Privește fișierele existente, fără să presupui că proiectul tău arată identic cu al altui cursant:
find . -maxdepth 2 -type f | sort
git există.Instalăm instrumentele cu care construim și verificăm laboratorul. Nu instalăm un stack GPU și nici un model.
sudo apt update
sudo apt install -y \
python3 \
python3-venv \
git \
jq \
tree \
pciutils \
graphviz
apt update actualizează lista pachetelor disponibile; nu face upgrade tuturor programelor. Opțiunea -y confirmă automat instalarea. Dacă preferi să vezi lista înainte să accepți, elimină -y.
Verifică instrumentele pe care laboratorul chiar se bazează:
python3 --version
git --version
jq --version
tree --version
lspci --version
dot -V
Versiunile exacte diferă de la o mașină la alta. Graphviz poate scrie versiunea pe canalul standard de eroare (stderr); asta nu înseamnă că a eșuat.
Acum, după ce am verificat că Git este instalat, inițializează repository-ul. Dacă proiectul era deja un repository Git, comanda este sigură și nu șterge istoricul:
git init
git init înainte ca Git să existe. Instalăm și verificăm mai întâi programul, apoi îl folosim.curl aici: laboratorul vrea să distingă între ceea ce era deja prezent și ceea ce instalează el însuși. curl nu este necesar pentru pașii de bază; inspectorul îl va raporta dacă îl găsește.Laboratorul este verificat pentru Ubuntu/Kubuntu. Pe Windows, WSL2 cu Ubuntu este cea mai apropiată variantă de comenzile de aici. Pe macOS, multe comenzi Python și Git sunt similare, dar fișiere Linux precum /proc/meminfo nu există; laboratorul de inspecție trebuie adaptat și nu îl considerăm echivalent.
Acum python3-venv este instalat. Această ordine contează pentru cine începe direct cu Lecția 3.
if [ ! -d .venv ]; then
python3 -m venv .venv
fi
source .venv/bin/activate
which python
python --version
Dacă mediul este activ, which python trebuie să indice un executabil din .venv/bin/. Nu instalăm pachete Python externe, deci nu este necesar să facem upgrade la pip doar pentru acest laborator.
.venv înainte să instaleze python3-venv. Pe o instalare minimă de Ubuntu, acel pas putea eșua. Acum dependența este instalată înainte de utilizare..gitignoreInventarul descrie mașina pe care rulezi. Chiar dacă evităm datele de identitate, el rămâne un rezultat specific dispozitivului și nu trebuie publicat automat.
Pentru proiectul cursului, creează sau înlocuiește .gitignore cu setul cumulativ de reguli de mai jos:
cat > .gitignore <<'EOF'
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.env
.env.*
logs/*
!logs/.gitkeep
*.tmp
.DS_Store
# Laboratorul 3: output specific mașinii
/data/lesson03/system_inventory.json
/docs/lessons/lesson-03/system_inventory.md
EOF
touch logs/.gitkeep
Comanda cat > ... <<'EOF' este un here-document: toate liniile până la marcajul final EOF sunt scrise în fișier. Astfel poți reproduce exact configurația fără să deschizi un editor separat.
.gitignore nu criptează și nu șterge nimic. El îi spune lui Git ce fișiere neînregistrate să ignore. Un fișier deja urmărit de Git nu devine „neurmărit” doar pentru că apare ulterior în .gitignore; vom vedea remediul la depanare.
.venv, .env, cache-uri și loguri. O lecție cumulativă nu trebuie să presupună că protecțiile anterioare există dacă oferă explicit o cale de intrare directă.Acesta este primul program al laboratorului. El adună observații tehnice într-o structură JSON și produce apoi un raport Markdown ușor de citit.
Creează fișierul src/lesson03_stack_inspector.py cu următorul conținut:
#!/usr/bin/env python3
"""Inspectează un sistem Linux fără a colecta date de identitate sau de rețea."""
from __future__ import annotations
import argparse
import json
import os
import platform
import re
import shutil
import subprocess
from pathlib import Path
from typing import Any, Sequence
def read_text(path: Path) -> str:
"""Citește un fișier text cunoscut; dacă nu poate fi citit, întoarce șirul gol."""
try:
return path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
def parse_key_value_text(text: str) -> dict[str, str]:
"""Transformă linii KEY=VALUE, precum cele din /etc/os-release, într-un dicționar."""
result: dict[str, str] = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
result[key.strip()] = value.strip().strip('"').strip("'")
return result
def parse_memtotal_gib(text: str) -> float | None:
"""Întoarce RAM-ul total în GiB din textul /proc/meminfo."""
match = re.search(r"^MemTotal:\s+(\d+)\s+kB$", text, re.MULTILINE)
if not match:
return None
kib = int(match.group(1))
return round(kib / 1024 / 1024, 2)
def parse_cpu_model(text: str) -> str | None:
"""Extrage o descriere a procesorului fără a căuta câmpuri de tip serial number."""
for key in ("model name", "Hardware", "Processor"):
match = re.search(rf"^{re.escape(key)}\s*:\s*(.+)$", text, re.MULTILINE)
if match:
return match.group(1).strip()
return None
def parse_gpu_lines(text: str) -> list[str]:
"""Păstrează din lspci numai liniile pentru controlere VGA, 3D și Display."""
markers = ("VGA compatible controller", "3D controller", "Display controller")
entries = [line.strip() for line in text.splitlines() if any(marker in line for marker in markers)]
return sorted(set(entries))
def run_command(args: Sequence[str], timeout: int = 5) -> dict[str, Any]:
"""Rulează o comandă de diagnostic și întoarce o stare structurată, fără a propaga eroarea."""
executable = args[0]
resolved = shutil.which(executable)
if resolved is None:
return {
"available": False,
"command": executable,
"returncode": None,
"stdout": "",
"stderr": "command not found",
}
try:
completed = subprocess.run(
list(args),
check=False,
capture_output=True,
text=True,
timeout=timeout,
env={**os.environ, "LC_ALL": "C"},
)
except subprocess.TimeoutExpired:
return {
"available": True,
"command": executable,
"returncode": None,
"stdout": "",
"stderr": f"timed out after {timeout} seconds",
}
except OSError as exc:
return {
"available": True,
"command": executable,
"returncode": None,
"stdout": "",
"stderr": f"could not execute command: {exc}",
}
return {
"available": True,
"command": executable,
"returncode": completed.returncode,
"stdout": completed.stdout.strip(),
"stderr": completed.stderr.strip(),
}
def first_line(value: str) -> str | None:
for line in value.splitlines():
cleaned = line.strip()
if cleaned:
return cleaned
return None
def command_version(args: Sequence[str]) -> dict[str, Any]:
result = run_command(args)
combined = result["stdout"] or result["stderr"]
return {
"available": result["available"],
"version": first_line(combined),
"returncode": result["returncode"],
}
def query_summary(result: dict[str, Any], max_lines: int = 20) -> dict[str, Any]:
"""Păstrează doar informația necesară pentru raport, nu întregul rezultat al utilitarului."""
succeeded = result["returncode"] == 0
return {
"available": result["available"],
"query_succeeded": succeeded,
"summary": result["stdout"].splitlines()[:max_lines] if succeeded else [],
"error": first_line(result["stderr"]) if result["available"] and not succeeded else None,
}
def collect_inventory() -> dict[str, Any]:
os_release = parse_key_value_text(read_text(Path("/etc/os-release")))
meminfo = read_text(Path("/proc/meminfo"))
cpuinfo = read_text(Path("/proc/cpuinfo"))
lspci = run_command(["lspci"])
nvidia_query = run_command(
[
"nvidia-smi",
"--query-gpu=name,memory.total,driver_version",
"--format=csv,noheader,nounits",
]
)
# AMD SMI este interfața recomandată în prezent. ROCm SMI rămâne doar variantă de rezervă pentru sisteme vechi.
amd_query = run_command(["amd-smi", "static", "--vram"], timeout=8)
rocm_query: dict[str, Any] = {
"available": False,
"command": "rocm-smi",
"returncode": None,
"stdout": "",
"stderr": "not queried because amd-smi was available",
}
if not amd_query["available"]:
rocm_query = run_command(
["rocm-smi", "--showproductname", "--showmeminfo", "vram"], timeout=8
)
inventory: dict[str, Any] = {
"schema_version": 1,
"privacy_note": (
"Acest inventar omite intenționat numele utilizatorului, hostname-ul, adresele IP, "
"numerele de serie ale dispozitivelor și valorile variabilelor de mediu."
),
"system": {
"operating_system": os_release.get("PRETTY_NAME") or platform.system(),
"kernel": platform.release(),
"architecture": platform.machine(),
},
"hardware": {
"cpu_model": parse_cpu_model(cpuinfo),
"logical_cpu_count": os.cpu_count(),
"ram_gib": parse_memtotal_gib(meminfo),
"lspci": {
"available": lspci["available"],
"query_succeeded": lspci["returncode"] == 0,
"error": first_line(lspci["stderr"]) if lspci["returncode"] != 0 else None,
},
"gpu_pci_entries": parse_gpu_lines(lspci["stdout"]) if lspci["returncode"] == 0 else [],
},
"acceleration": {
"nvidia_smi": query_summary(nvidia_query),
"amd_smi": query_summary(amd_query),
"rocm_smi_legacy": query_summary(rocm_query),
"nvcc": command_version(["nvcc", "--version"]),
},
"development_tools": {
"python": command_version(["python3", "--version"]),
"git": command_version(["git", "--version"]),
"curl": command_version(["curl", "--version"]),
"docker": command_version(["docker", "--version"]),
"ollama": command_version(["ollama", "--version"]),
},
}
return inventory
def availability_label(value: dict[str, Any]) -> str:
return "detectat" if value.get("available") else "nedetectat"
def query_label(value: dict[str, Any]) -> str:
if value.get("query_succeeded"):
return "interogare reușită"
if value.get("available"):
return "interogare eșuată"
return "nedetectat"
def display_value(value: Any, fallback: str = "nedetectat") -> str:
return fallback if value is None else str(value)
def inventory_to_markdown(inventory: dict[str, Any]) -> str:
system = inventory["system"]
hardware = inventory["hardware"]
acceleration = inventory["acceleration"]
tools = inventory["development_tools"]
if not hardware["lspci"]["query_succeeded"]:
gpu_lines = "- `lspci` nu a putut fi interogat; nu trage concluzii despre existența unui GPU din acest rezultat."
elif hardware["gpu_pci_entries"]:
gpu_lines = "\n".join(f"- {entry}" for entry in hardware["gpu_pci_entries"])
else:
gpu_lines = "- `lspci` nu a raportat un controller VGA, 3D sau Display."
tool_lines = []
for name, details in tools.items():
version = details.get("version") or "versiune neraportată"
tool_lines.append(f"| {name} | {availability_label(details)} | {version} |")
ram = display_value(hardware["ram_gib"])
ram_display = f"{ram} GiB" if hardware["ram_gib"] is not None else ram
return f"""# Raportul inventarului de sistem — Laboratorul 3
Acest raport descrie capabilități tehnice detectate. Nu este o recomandare de a instala fiecare componentă lipsă.
## Sistem
| Câmp | Valoare |
|---|---|
| Sistem de operare | {system['operating_system']} |
| Kernel | {system['kernel']} |
| Arhitectură | {system['architecture']} |
| CPU-uri logice | {display_value(hardware['logical_cpu_count'])} |
| RAM | {ram_display} |
## Plăci grafice raportate prin magistrala PCI
{gpu_lines}
## Diagnostic de accelerare
| Diagnostic | Rezultat |
|---|---|
| NVIDIA `nvidia-smi` | {query_label(acceleration['nvidia_smi'])} |
| AMD `amd-smi` | {query_label(acceleration['amd_smi'])} |
| AMD `rocm-smi` legacy | {query_label(acceleration['rocm_smi_legacy'])} |
| Compilator CUDA `nvcc` | {availability_label(acceleration['nvcc'])} |
Faptul că un utilitar nu a fost detectat nu dovedește că hardware-ul lipsește. Comanda poate să nu fie instalată, să nu se afle în `PATH` sau să nu fie compatibilă cu această mașină.
## Instrumente de dezvoltare
| Instrument | Stare | Versiune / mesaj |
|---|---|---|
{chr(10).join(tool_lines)}
## Confidențialitate
{inventory['privacy_note']}
"""
def write_outputs(inventory: dict[str, Any], json_out: Path, markdown_out: Path) -> None:
json_out.parent.mkdir(parents=True, exist_ok=True)
markdown_out.parent.mkdir(parents=True, exist_ok=True)
json_out.write_text(json.dumps(inventory, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
markdown_out.write_text(inventory_to_markdown(inventory), encoding="utf-8")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Creează un inventar tehnic limitat la informațiile necesare Laboratorului 3."
)
parser.add_argument(
"--json-out",
type=Path,
default=Path("data/lesson03/system_inventory.json"),
help="calea fișierului JSON de ieșire",
)
parser.add_argument(
"--markdown-out",
type=Path,
default=Path("docs/lessons/lesson-03/system_inventory.md"),
help="calea raportului Markdown",
)
return parser
def main() -> int:
args = build_parser().parse_args()
inventory = collect_inventory()
write_outputs(inventory, args.json_out, args.markdown_out)
print(
json.dumps(
{
"status": "completed",
"json": str(args.json_out),
"report": str(args.markdown_out),
},
ensure_ascii=False,
indent=2,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Fă fișierul executabil și verifică mai întâi sintaxa, fără să rulezi încă inspecția:
chmod +x src/lesson03_stack_inspector.py
python -m py_compile src/lesson03_stack_inspector.py
Dacă py_compile nu afișează nimic, fișierul s-a compilat sintactic.
Nu încerca să înțelegi toate liniile dintr-o dată. Un parser este o funcție sau componentă care transformă text ori date brute într-o structură pe care programul o poate folosi. Citește programul în patru treceri:
parse_key_value_text, parse_memtotal_gib, parse_cpu_model și parse_gpu_lines transformă text brut în valori structurate.run_command verifică dacă utilitarul există, impune un timeout și transformă lipsa/eroarea într-un rezultat structurat, nu într-o oprire cu eroare.collect_inventory construiește obiectul unic care va deveni JSON.inventory_to_markdown transformă aceleași date într-un raport destinat omului.Observă o distincție importantă: dacă lspci nu poate rula, raportul nu spune „nu există GPU”. Spune că diagnosticul nu a putut fi executat. Absența dovezii nu devine automat dovadă de absență.
lspci simplu și lizibil. Dacă scrii un instrument de producție care procesează automat ieșirea lspci, folosește unul dintre formatele destinate scripturilor, precum lspci -m sau lspci -vmm; documentația lspci avertizează că formatul obișnuit se poate schimba între versiuni.nvidia-smi, amd-smi, rocm-smi și nvccnvidia-smi este un utilitar NVIDIA de monitorizare și administrare a GPU-urilor, bazat pe NVML. nvcc este compilatorul-driver din CUDA Toolkit. Un driver NVIDIA funcțional nu dovedește că ai instalat întregul CUDA Toolkit.
Pentru AMD, programul încearcă mai întâi amd-smi. AMD SMI este succesorul recomandat pentru ROCm SMI; rocm-smi este păstrat aici numai ca fallback pentru sisteme mai vechi. Nu instalăm niciunul în laborator doar ca să obținem un rezultat „verde”.
Acum executăm programul. Scopul nu este să obții aceleași valori ca alt cursant, ci să poți explica ce înseamnă fiecare categorie de rezultat.
python src/lesson03_stack_inspector.py
Mesajul final trebuie să aibă forma:
{
"status": "completed",
"json": "data/lesson03/system_inventory.json",
"report": "docs/lessons/lesson-03/system_inventory.md"
}
Ce dovedește acest mesaj: programul a terminat și a scris cele două fișiere. Nu dovedește că ai un GPU compatibil sau că toate utilitarele sunt instalate.
Inspectează JSON-ul complet și apoi câteva câmpuri:
jq . data/lesson03/system_inventory.json
jq -r '.system.operating_system' data/lesson03/system_inventory.json
jq '.hardware.gpu_pci_entries' data/lesson03/system_inventory.json
jq '.acceleration' data/lesson03/system_inventory.json
Citește raportul pentru om:
less docs/lessons/lesson-03/system_inventory.md
Ieși din less cu tasta q.
PATH;Programul nu folosește „magie”. Rulează câteva dintre comenzile pe care se bazează:
cat /etc/os-release
uname -r
uname -m
grep MemTotal /proc/meminfo
grep -m 1 -E 'model name|Hardware|Processor' /proc/cpuinfo
lspci | grep -Ei 'VGA|3D|Display' || true
Ultimul || true împiedică shell-ul să trateze lipsa unei potriviri grep drept oprirea întregului exercițiu. Dacă ai instalate utilitarele corespunzătoare, le poți interoga separat:
nvidia-smi
amd-smi static --vram
rocm-smi --showproductname --showmeminfo vram
nvcc --version
ollama --version
docker --version
Este normal ca unele comenzi să nu existe. Laboratorul tocmai te învață să înregistrezi o observație fără să inventezi explicația.
Al doilea program nu inspectează mașina. El verifică dacă documentul nostru de arhitectură conține toate responsabilitățile și câmpurile pe care le-am convenit.
Creează src/lesson03_manifest_validator.py:
#!/usr/bin/env python3
"""Validează manifestul responsabilităților din stack-ul AI al Laboratorului 3."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
REQUIRED_LAYERS = (
"hardware",
"operating_system",
"acceleration",
"language_runtime",
"ml_framework",
"model_artifact",
"inference_runtime",
"agent_controller",
"tools_integrations",
"data_state_memory",
"security_governance",
"observability_evaluation",
"interface",
"deployment",
)
ALLOWED_STATUSES = {"selected", "planned", "not_required_yet", "not_selected_yet"}
ALLOWED_DEPLOYMENTS = {"local", "cloud", "hybrid", "edge", "on_premise", "not_selected_yet"}
def load_json(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise ValueError("Rădăcina manifestului trebuie să fie un obiect JSON.")
return value
def validate_manifest(manifest: dict[str, Any]) -> list[str]:
errors: list[str] = []
if manifest.get("schema_version") != 1:
errors.append("schema_version trebuie să fie 1")
if not isinstance(manifest.get("system_purpose"), str) or not manifest["system_purpose"].strip():
errors.append("system_purpose trebuie să fie un șir de caractere nevid")
layers = manifest.get("layers")
if not isinstance(layers, dict):
return errors + ["layers trebuie să fie un obiect JSON"]
for layer_name in REQUIRED_LAYERS:
layer = layers.get(layer_name)
if not isinstance(layer, dict):
errors.append(f"strat lipsă sau invalid: {layer_name}")
continue
status = layer.get("status")
if status not in ALLOWED_STATUSES:
errors.append(
f"{layer_name}.status trebuie să fie una dintre valorile: "
+ ", ".join(sorted(ALLOWED_STATUSES))
)
decision = layer.get("decision")
if not isinstance(decision, str) or not decision.strip():
errors.append(f"{layer_name}.decision trebuie să fie un șir de caractere nevid")
reason = layer.get("reason")
if not isinstance(reason, str) or len(reason.strip()) < 12:
errors.append(
f"{layer_name}.reason trebuie să explice decizia în cel puțin 12 caractere"
)
deployment = layers.get("deployment")
if isinstance(deployment, dict):
choice = deployment.get("choice")
if choice not in ALLOWED_DEPLOYMENTS:
errors.append(
"deployment.choice trebuie să fie una dintre valorile: "
+ ", ".join(sorted(ALLOWED_DEPLOYMENTS))
)
boundaries = manifest.get("boundaries")
if not isinstance(boundaries, dict):
errors.append("boundaries trebuie să fie un obiect JSON")
else:
for field in ("may_read", "may_write", "requires_approval"):
if not isinstance(boundaries.get(field), list):
errors.append(f"boundaries.{field} trebuie să fie un array JSON")
return errors
def markdown_cell(value: Any) -> str:
"""Escapă caracterele care ar putea rupe un tabel Markdown."""
return str(value).replace("|", r"\|").replace("\n", " ")
def manifest_to_markdown(manifest: dict[str, Any], errors: list[str]) -> str:
lines = [
"# Validarea manifestului de stack — Laboratorul 3",
"",
f"**Scopul sistemului:** {manifest.get('system_purpose', 'lipsește')}",
"",
f"**Rezultatul validării:** {'PASS' if not errors else 'FAIL'}",
"",
]
if errors:
lines.extend(["## Erori", ""])
lines.extend(f"- {error}" for error in errors)
lines.append("")
lines.extend(
[
"## Deciziile pe straturi",
"",
"| Strat | Stare | Decizie |",
"|---|---|---|",
]
)
layers = manifest.get("layers", {})
if isinstance(layers, dict):
for layer_name in REQUIRED_LAYERS:
layer = layers.get(layer_name, {})
if isinstance(layer, dict):
lines.append(
"| "
+ " | ".join(
[
markdown_cell(layer_name),
markdown_cell(layer.get("status", "lipsește")),
markdown_cell(layer.get("decision", "lipsește")),
]
)
+ " |"
)
else:
lines.append(f"| {layer_name} | lipsește | lipsește |")
lines.extend(
[
"",
"Un manifest acceptat de validator are harta responsabilităților completă. "
"Validarea nu dovedește că produsele alese sunt compatibile, sigure sau instalate.",
"",
]
)
return "\n".join(lines)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Validează manifestul de stack din Laboratorul 3.")
parser.add_argument("manifest", type=Path, help="calea manifestului JSON")
parser.add_argument(
"--report",
type=Path,
default=Path("docs/lessons/lesson-03/stack_manifest_report.md"),
help="calea raportului Markdown de validare",
)
return parser
def main() -> int:
args = build_parser().parse_args()
try:
manifest = load_json(args.manifest)
except (OSError, json.JSONDecodeError, ValueError) as exc:
print(json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=False, indent=2))
return 2
errors = validate_manifest(manifest)
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(manifest_to_markdown(manifest, errors), encoding="utf-8")
print(
json.dumps(
{
"status": "valid" if not errors else "invalid",
"error_count": len(errors),
"errors": errors,
"report": str(args.report),
},
ensure_ascii=False,
indent=2,
)
)
return 0 if not errors else 1
if __name__ == "__main__":
raise SystemExit(main())
Verifică sintaxa:
chmod +x src/lesson03_manifest_validator.py
python -m py_compile src/lesson03_manifest_validator.py
În acest laborator, o schemă este setul de reguli care spune ce câmpuri trebuie să existe într-un document, ce tipuri de valori acceptăm și ce valori sunt permise în anumite câmpuri. Validatorul nostru implementează o schemă simplă direct în Python.
Validatorul poate demonstra că există toate cele 14 responsabilități, că valorile câmpului status aparțin unui set permis, că fiecare decizie are o justificare, că deployment-ul folosește o categorie cunoscută și că limitele de acces sunt liste JSON.
Nu poate demonstra că un GPU este compatibil cu o anumită versiune de framework, că licența unui model permite utilizarea comercială, că o arhitectură este sigură sau că răspunsurile unui model sunt bune. Acelea sunt întrebări diferite.
statusselected — am luat o decizie pentru etapa curentă;planned — intenționăm să introducem responsabilitatea mai târziu;not_required_yet — știm ce este responsabilitatea, dar etapa curentă nu are nevoie de ea;not_selected_yet — responsabilitatea va conta, dar nu avem încă suficiente dovezi pentru a alege un produs sau design.Ultimele două nu sunt erori. Ele sunt o formă de disciplină arhitecturală: „nu știm încă” este mai bun decât o alegere inventată.
Manifestul de mai jos selectează numai ce au justificat primele trei lecții. Nu introduce un model doar pentru că facem un curs despre agenți AI.
Creează configs/lesson03_stack_manifest.json:
{
"schema_version": 1,
"system_purpose": "Un agent local de învățare care păstrează deciziile explicite înainte de integrarea unui model.",
"layers": {
"hardware": {
"status": "selected",
"decision": "Folosim calculatorul Linux existent al cursantului.",
"reason": "Sarcinile deterministe din acest laborator nu necesită un accelerator AI dedicat."
},
"operating_system": {
"status": "selected",
"decision": "Ubuntu sau Kubuntu Linux.",
"reason": "Comenzile practice și instrucțiunile de instalare ale cursului vizează acest mediu."
},
"acceleration": {
"status": "not_required_yet",
"decision": "Laboratorul rulează pe CPU și doar înregistrează capabilitățile GPU detectate.",
"reason": "În acest laborator nu antrenăm și nu rulăm prin inferență un model neuronal."
},
"language_runtime": {
"status": "selected",
"decision": "Python 3 în mediul virtual al proiectului.",
"reason": "Laboratoarele anterioare folosesc deja programe Python inspectabile și teste automate."
},
"ml_framework": {
"status": "not_required_yet",
"decision": "Nu instalăm PyTorch, TensorFlow sau JAX în această lecție.",
"reason": "Laboratorul inspectează stack-ul și validează arhitectura, fără a rula un model neuronal."
},
"model_artifact": {
"status": "not_selected_yet",
"decision": "Nu am selectat încă un artefact de model.",
"reason": "Alegerea modelului trebuie să urmeze cerințele de sarcină, hardware, licență și evaluare."
},
"inference_runtime": {
"status": "not_selected_yet",
"decision": "Nu am selectat încă un runtime de inferență local sau la distanță.",
"reason": "Cursul nu a comparat încă modele și nu a evaluat inferența locală față de cea găzduită."
},
"agent_controller": {
"status": "selected",
"decision": "Continuăm cu o mașină de stare explicită scrisă în Python.",
"reason": "Un controller mic și explicit păstrează fluxul de control vizibil înainte de introducerea framework-urilor."
},
"tools_integrations": {
"status": "selected",
"decision": "Folosim comenzi locale de diagnostic cu rol de citire și fișiere locale.",
"reason": "Lecția are nevoie de capabilități inspectabile, fără acțiuni externe de scriere."
},
"data_state_memory": {
"status": "selected",
"decision": "Folosim fișiere JSON pentru date și rapoarte Markdown pentru citire umană.",
"reason": "Fișierele simple sunt suficiente și rămân ușor de inspectat, comparat și versionat."
},
"security_governance": {
"status": "selected",
"decision": "Nu colectăm username, hostname, adresă IP, numere de serie sau valori de secrete.",
"reason": "Un inventar hardware trebuie să reducă la minimum informațiile personale și sensibile pentru securitate."
},
"observability_evaluation": {
"status": "selected",
"decision": "Folosim rezultate structurate ale comenzilor, rapoarte de validare și teste automate.",
"reason": "Cursantul trebuie să poată explica ce a fost detectat și de ce a trecut sau a eșuat validarea."
},
"interface": {
"status": "selected",
"decision": "Folosim o interfață CLI cu ieșiri JSON și Markdown.",
"reason": "CLI-ul arată direct fișierele și erorile, fără a adăuga încă un framework web."
},
"deployment": {
"status": "selected",
"choice": "local",
"decision": "Rulăm în proiectul cumulativ de pe calculatorul cursantului.",
"reason": "Execuția locală este suficientă pentru un inventar numai pentru citire și un document de arhitectură."
}
},
"boundaries": {
"may_read": [
"/etc/os-release",
"/proc/meminfo",
"/proc/cpuinfo",
"rezultatul comenzii lspci pentru controlere grafice",
"rezultatul de diagnostic nvidia-smi, dacă utilitarul există",
"rezultatul de diagnostic amd-smi sau varianta de rezervă rocm-smi pentru sisteme mai vechi, dacă utilitarul există",
"rezultatele de versiune pentru nvcc, Python, Git, curl, Docker și Ollama, dacă utilitarele există"
],
"may_write": [
"data/lesson03/system_inventory.json",
"docs/lessons/lesson-03/system_inventory.md",
"docs/lessons/lesson-03/stack_manifest_report.md"
],
"requires_approval": [
"instalarea unui driver GPU",
"instalarea CUDA sau ROCm",
"descărcarea unui artefact de model",
"încărcarea inventarului în afara proiectului local"
]
}
}
Verifică JSON-ul:
jq . configs/lesson03_stack_manifest.json
Rulează validatorul:
python src/lesson03_manifest_validator.py \
configs/lesson03_stack_manifest.json
Rezultatul așteptat este:
{
"status": "valid",
"error_count": 0,
"errors": [],
"report": "docs/lessons/lesson-03/stack_manifest_report.md"
}
Ce demonstrează: manifestul acoperă structura cerută. Nu demonstrează că alegerile viitoare pentru model sau GPU au fost rezolvate; din contră, fișierul spune explicit unde alegerea este amânată.
Citește raportul:
less docs/lessons/lesson-03/stack_manifest_report.md
Acum separăm două tipuri de problemă: greșeală structurală, pe care o poate detecta codul, și raționament tehnic prost, care poate rămâne perfect valid JSON.
Creează configs/lesson03_broken_stack_manifest.json:
{
"schema_version": 1,
"system_purpose": "Instalăm toate produsele AI înainte să definim ce problemă vrem să rezolvăm.",
"layers": {
"hardware": {
"status": "selected",
"decision": "Orice calculator",
"reason": "Ar trebui să meargă"
},
"operating_system": {
"status": "selected",
"decision": "Linux",
"reason": "Linux este des întâlnit"
},
"acceleration": {
"status": "selected",
"decision": "Instalăm CUDA pe orice calculator",
"reason": "AI are întotdeauna nevoie de CUDA"
},
"language_runtime": {
"status": "selected",
"decision": "Python",
"reason": "Folosim Python acum"
},
"model_artifact": {
"status": "selected",
"decision": "Descărcăm cel mai mare model",
"reason": "Mai mare trebuie să însemne mai bun"
},
"inference_runtime": {
"status": "selected",
"decision": "Ollama",
"reason": "Este popular"
},
"agent_controller": {
"status": "selected",
"decision": "Modelul se va controla singur",
"reason": "Nu avem nevoie de controller"
},
"tools_integrations": {
"status": "selected",
"decision": "Îi dăm acces complet la shell",
"reason": "Autonomie maximă"
},
"data_state_memory": {
"status": "selected",
"decision": "Păstrăm fiecare conversație pentru totdeauna",
"reason": "Mai multă memorie este mereu mai bine"
},
"security_governance": {
"status": "selected",
"decision": "Dacă rulează local, este sigur",
"reason": "Nu mai trebuie verificat"
},
"interface": {
"status": "selected",
"decision": "Pagină de chat",
"reason": "Utilizatorilor le place chat-ul"
},
"deployment": {
"status": "selected",
"choice": "everywhere",
"decision": "Local și cloud în același timp",
"reason": "Flexibilitate maximă"
}
},
"boundaries": {
"may_read": "everything",
"may_write": ["everything"],
"requires_approval": []
}
}
Rulează validatorul și scrie raportul separat:
python src/lesson03_manifest_validator.py \
configs/lesson03_broken_stack_manifest.json \
--report docs/lessons/lesson-03/broken_manifest_report.md
printf 'exit status: %s\n' "$?"
Programul trebuie să întoarcă cod de ieșire (exit status) 1. Aici 1 nu înseamnă că validatorul s-a stricat; înseamnă că validatorul a funcționat și a găsit un manifest invalid.
Trebuie să găsească patru probleme structurale:
ml_framework;observability_evaluation;deployment.choice are valoarea nepermisă everywhere;boundaries.may_read este string, nu array.În plus, un om ar trebui să observe idei proaste pe care schema nu le poate respinge: „instalăm CUDA peste tot”, „cel mai mare model este automat cel mai bun”, „dăm shell complet”, „păstrăm fiecare conversație pentru totdeauna” sau „local înseamnă sigur”.
everywhere nu este un design de deployment.may_read: "everything" este și greșit structural, și periculos de larg.Un test bun trebuie să poată rula și pe un laptop Intel, și pe o mașină AMD, și într-un container fără GPU. De aceea parserele primesc text controlat în loc să ceară tuturor același hardware.
Creează tests/test_lesson03_stack.py:
from __future__ import annotations
import importlib.util
import json
import tempfile
import unittest
from unittest.mock import patch
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def load_module(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Nu am putut încărca {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
inspector = load_module("lesson03_stack_inspector", ROOT / "src" / "lesson03_stack_inspector.py")
validator = load_module("lesson03_manifest_validator", ROOT / "src" / "lesson03_manifest_validator.py")
class InspectorTests(unittest.TestCase):
def test_parse_key_value_text(self) -> None:
parsed = inspector.parse_key_value_text('NAME="Ubuntu"\nVERSION_ID="24.04"\n')
self.assertEqual(parsed["NAME"], "Ubuntu")
self.assertEqual(parsed["VERSION_ID"], "24.04")
def test_parse_memtotal_gib(self) -> None:
self.assertEqual(inspector.parse_memtotal_gib("MemTotal: 16777216 kB\n"), 16.0)
def test_parse_memtotal_gib_returns_none_for_invalid_text(self) -> None:
self.assertIsNone(inspector.parse_memtotal_gib("MemFree: 100 kB\n"))
def test_parse_gpu_lines_keeps_only_display_controllers(self) -> None:
text = (
"00:02.0 VGA compatible controller: Intel Corp\n"
"00:14.0 USB controller: Example\n"
"01:00.0 3D controller: NVIDIA Corp\n"
)
self.assertEqual(len(inspector.parse_gpu_lines(text)), 2)
def test_run_command_reports_missing_command_without_raising(self) -> None:
result = inspector.run_command(["lesson03-command-that-does-not-exist"])
self.assertFalse(result["available"])
self.assertIsNone(result["returncode"])
def test_query_label_distinguishes_failed_query(self) -> None:
self.assertEqual(
inspector.query_label({"available": True, "query_succeeded": True}),
"interogare reușită",
)
self.assertEqual(
inspector.query_label({"available": True, "query_succeeded": False}),
"interogare eșuată",
)
self.assertEqual(
inspector.query_label({"available": False, "query_succeeded": False}),
"nedetectat",
)
def test_collect_inventory_does_not_request_amd_asic_serial(self) -> None:
calls: list[list[str]] = []
def fake_run_command(args, timeout=5):
calls.append(list(args))
return {
"available": False,
"command": args[0],
"returncode": None,
"stdout": "",
"stderr": "command not found",
}
with patch.object(inspector, "run_command", side_effect=fake_run_command):
inspector.collect_inventory()
amd_calls = [args for args in calls if args and args[0] == "amd-smi"]
self.assertEqual(amd_calls, [["amd-smi", "static", "--vram"]])
self.assertNotIn("--asic", amd_calls[0])
def test_inventory_report_contains_privacy_note(self) -> None:
sample = {
"privacy_note": "Fără câmpuri de identitate.",
"system": {"operating_system": "TestOS", "kernel": "1", "architecture": "x86_64"},
"hardware": {
"logical_cpu_count": 4,
"ram_gib": 8.0,
"lspci": {"available": True, "query_succeeded": True, "error": None},
"gpu_pci_entries": [],
},
"acceleration": {
"nvidia_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
"amd_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
"rocm_smi_legacy": {"available": False, "query_succeeded": False, "summary": [], "error": None},
"nvcc": {"available": False, "version": None, "returncode": None},
},
"development_tools": {
"python": {"available": True, "version": "Python 3", "returncode": 0}
},
}
report = inspector.inventory_to_markdown(sample)
self.assertIn("Fără câmpuri de identitate.", report)
self.assertIn("nu a raportat un controller VGA, 3D sau Display", report)
def test_inventory_report_does_not_claim_no_gpu_when_lspci_failed(self) -> None:
sample = {
"privacy_note": "Test.",
"system": {"operating_system": "TestOS", "kernel": "1", "architecture": "x86_64"},
"hardware": {
"logical_cpu_count": 4,
"ram_gib": None,
"lspci": {"available": False, "query_succeeded": False, "error": "missing"},
"gpu_pci_entries": [],
},
"acceleration": {
"nvidia_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
"amd_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
"rocm_smi_legacy": {"available": False, "query_succeeded": False, "summary": [], "error": None},
"nvcc": {"available": False, "version": None, "returncode": None},
},
"development_tools": {},
}
report = inspector.inventory_to_markdown(sample)
self.assertIn("nu a putut fi interogat", report)
self.assertNotIn("nu a raportat un controller", report)
self.assertIn("| RAM | nedetectat |", report)
class ManifestTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.valid_manifest = json.loads(
(ROOT / "configs" / "lesson03_stack_manifest.json").read_text(encoding="utf-8")
)
def test_valid_manifest_passes(self) -> None:
self.assertEqual(validator.validate_manifest(self.valid_manifest), [])
def test_missing_layer_fails(self) -> None:
value = json.loads(json.dumps(self.valid_manifest))
del value["layers"]["security_governance"]
errors = validator.validate_manifest(value)
self.assertTrue(any("security_governance" in error for error in errors))
def test_invalid_status_fails(self) -> None:
value = json.loads(json.dumps(self.valid_manifest))
value["layers"]["hardware"]["status"] = "installed_maybe"
errors = validator.validate_manifest(value)
self.assertTrue(any("hardware.status" in error for error in errors))
def test_invalid_deployment_choice_fails(self) -> None:
value = json.loads(json.dumps(self.valid_manifest))
value["layers"]["deployment"]["choice"] = "everywhere"
errors = validator.validate_manifest(value)
self.assertTrue(any("deployment.choice" in error for error in errors))
def test_boundaries_must_be_arrays(self) -> None:
value = json.loads(json.dumps(self.valid_manifest))
value["boundaries"]["may_read"] = "everything"
errors = validator.validate_manifest(value)
self.assertIn("boundaries.may_read trebuie să fie un array JSON", errors)
def test_load_json_rejects_non_object_root(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "bad.json"
path.write_text("[]", encoding="utf-8")
with self.assertRaises(ValueError):
validator.load_json(path)
def test_missing_system_purpose_fails(self) -> None:
value = json.loads(json.dumps(self.valid_manifest))
value["system_purpose"] = ""
errors = validator.validate_manifest(value)
self.assertTrue(any("system_purpose" in error for error in errors))
def test_markdown_cell_escapes_table_separator(self) -> None:
self.assertEqual(validator.markdown_cell("local | cloud"), r"local \| cloud")
if __name__ == "__main__":
unittest.main()
Compilează întâi toate fișierele Python:
python -m py_compile \
src/lesson03_stack_inspector.py \
src/lesson03_manifest_validator.py \
tests/test_lesson03_stack.py
Rulează testele:
python -m unittest discover \
-s tests \
-p 'test_lesson03_*.py' \
-v
Rezultatul așteptat este:
Ran 17 tests
OK
Suite-ul verifică parsarea fișierelor Linux, conversia RAM, filtrarea liniilor GPU, tratarea unei comenzi inexistente, diferența dintre „lspci nu a rulat” și „a rulat dar n-a găsit controller”, nota de confidențialitate, faptul că AMD SMI nu este apelat cu opțiunea --asic care poate expune un serial, manifestul valid, straturi lipsă, valori status nepermise, deployment invalid, tipurile limitelor, rădăcina JSON, scopul sistemului și escaparea caracterului | în tabele Markdown.
Manifestul păstrează cele paisprezece responsabilități. Diagrama le grupează în cele șase zone pedagogice din Lecția 3, ca să poți vedea aceeași arhitectură la două niveluri de detaliu.
Creează diagrams/lesson03_stack.dot:
digraph ai_stack {
rankdir=BT;
graph [bgcolor="transparent", pad="0.2", nodesep="0.22", ranksep="0.3"];
node [shape=box, style="rounded", fontname="DejaVu Sans", fontsize=11, margin="0.16,0.10"];
edge [fontname="DejaVu Sans", fontsize=9];
hardware [label="Zona I · Hardware\nCPU · RAM · stocare · GPU · rețea"];
system [label="Zona II · Sistem și accelerare\nLinux · drivere · CUDA/ROCm"];
software [label="Zona III · Software de aplicație și ML\nPython · dependențe · framework-uri"];
model [label="Zona IV · Model și inferență\nartefact · runtime · server de model"];
agent [label="Zona V · Agent, instrumente și date\ncontroller · API-uri · stare · retrieval"];
control [label="Zona VI · Control și operare\nsecuritate · observabilitate · interfață · deployment"];
hardware -> system -> software -> model -> agent -> control;
}
Randează PNG-ul:
dot -Tpng \
diagrams/lesson03_stack.dot \
-o diagrams/lesson03_stack.png
file diagrams/lesson03_stack.png
ls -lh diagrams/lesson03_stack.png
Dacă lucrezi într-un desktop Linux, îl poți deschide cu:
xdg-open diagrams/lesson03_stack.png
Fișierul .dot este sursa editabilă; PNG-ul este rezultatul vizual. Spre deosebire de inventarul mașinii, diagrama este documentație revizuită și poate fi păstrată în Git.
Nu este suficient să avem un manifest. Vrem și o explicație scurtă a principiului care a condus la el: descriem responsabilitățile înainte să alegem produsele.
Creează docs/architecture-decisions/ADR-0003-stack-before-products.md:
# ADR-0003 — Descriem responsabilitățile stack-ului înainte să alegem produse
## Stare
Acceptat pentru proiectul de învățare.
## Context
Produsele AI combină adesea mai multe responsabilități, iar denumirile comerciale pot face componente diferite să pară interschimbabile. Instalarea prematură poate duce la drivere incompatibile, modele descărcate inutil, limite de securitate slabe și o arhitectură care nu corespunde sarcinii reale.
## Decizie
Proiectul va păstra un manifest al stack-ului care acoperă hardware-ul, sistemul de operare, accelerarea, runtime-ul limbajului, framework-ul ML, artefactul de model, runtime-ul de inferență, controllerul, instrumentele, datele, securitatea, observabilitatea, interfața și deployment-ul.
Un strat poate avea starea `not_required_yet` sau `not_selected_yet`. Alegerea unui produs trebuie justificată prin sarcină și constrângeri, nu prin popularitate.
## Consecințe
- Deciziile lipsă devin vizibile.
- Proiectul evită să trateze un singur produs drept „agentul complet”.
- Compatibilitatea trebuie verificată separat.
- Manifestul trebuie revizuit pe măsură ce lecțiile următoare introduc modele, retrieval, sandbox-uri și deployment.
ADR-ul este scurt intenționat. El trebuie să răspundă la patru întrebări: ce problemă avem, ce am decis, de ce am decis și ce consecințe acceptăm.
Înainte să înregistrezi munca în Git, verifică atât structura, cât și faptul că inventarul specific mașinii este ignorat.
tree -a -L 4 \
configs \
data/lesson03 \
diagrams \
docs/architecture-decisions \
docs/lessons/lesson-03 \
src \
tests
git status --short
Confirmă explicit regulile de ignorare:
git check-ignore -v data/lesson03/system_inventory.json
git check-ignore -v docs/lessons/lesson-03/system_inventory.md
Aceste două comenzi trebuie să indice regula din .gitignore. Manifestele, codul, testele, ADR-ul și diagrama trebuie să rămână vizibile pentru Git.
Git nu decide dacă un fișier este sigur de publicat. Tu trebuie să verifici întâi conținutul.
less configs/lesson03_stack_manifest.json
less docs/lessons/lesson-03/stack_manifest_report.md
less docs/lessons/lesson-03/broken_manifest_report.md
Dacă ești mulțumit de conținut, adaugă artefactele revizuite:
git add \
.gitignore \
logs/.gitkeep \
src/lesson03_stack_inspector.py \
src/lesson03_manifest_validator.py \
tests/test_lesson03_stack.py \
configs/lesson03_stack_manifest.json \
configs/lesson03_broken_stack_manifest.json \
diagrams/lesson03_stack.dot \
diagrams/lesson03_stack.png \
docs/architecture-decisions/ADR-0003-stack-before-products.md \
docs/lessons/lesson-03/stack_manifest_report.md \
docs/lessons/lesson-03/broken_manifest_report.md
git diff --cached --stat
git diff --cached
Ultimele două comenzi sunt deliberat înaintea commit-ului: îți arată exact ce urmează să înregistrezi.
git commit -m "lesson 3: map and validate the AI stack"
Dacă Git spune că identitatea nu este configurată, setează un nume și o adresă de e-mail potrivite modului în care vrei să apară istoricul. Nu publica o adresă personală dacă nu vrei să fie prezentă în commit-uri.
Acest checkpoint împiedică o confuzie foarte importantă: laboratorul descrie infrastructura unui viitor sistem agentic, dar nu a introdus încă un model lingvistic.
Controllerul cumulativ rămâne cel determinist introdus anterior. Lecția 3 îi adaugă în jur o hartă explicită a infrastructurii și responsabilităților.
Citește mesajul de eroare înainte să instalezi ceva. Multe probleme sunt de cale, pachet sau context de lucru, nu de „AI”.
cd: ... No such file or directoryReia secțiunea 4 pentru folderul proiectului, apoi secțiunea 5 pentru instalarea și inițializarea Git.
python3: command not found sau modulul venv lipseșteRulează sudo apt update și sudo apt install python3 python3-venv, apoi recreează/activează .venv.
lspci: command not foundInstalează pciutils: sudo apt install pciutils. Absența comenzii nu dovedește că nu ai placă grafică.
Dacă lspci a rulat, pot exista mai multe explicații: hardware virtualizat, clasificare neobișnuită a dispozitivului, arhitectură non-PCI sau un mediu care nu expune dispozitivul. Notează observația; nu inventa diagnosticul.
nvidia-smi nu existăNu instala imediat un driver. Verifică mai întâi hardware-ul cu lspci | grep -Ei 'NVIDIA|VGA|3D|Display' și consultă apoi documentația oficială pentru acel GPU și sistem de operare.
nvcc lipsește, dar nvidia-smi funcționeazăPoate fi perfect normal. nvidia-smi ține de utilitarele de management/driver, iar nvcc este compilatorul din CUDA Toolkit. Sunt componente diferite.
rocm-smi lipsește, dar ai AMDVerifică întâi amd-smi. AMD SMI este succesorul recomandat al ROCm SMI. Laboratorul nu îți cere să instalezi niciunul doar pentru a completa inventarul.
dot: command not foundInstalează Graphviz cu sudo apt install graphviz.
Este rezultatul așteptat. Validatorul întoarce codul 1 tocmai pentru că a găsit structura invalidă.
Asigură-te că ești în rădăcina proiectului, că .venv este activ și rulează exact comanda de test din secțiunea 13.
git statusVerifică regula cu git check-ignore -v data/lesson03/system_inventory.json. Dacă fișierul fusese deja urmărit de Git, elimină-l numai din index cu git rm --cached data/lesson03/system_inventory.json; copia locală rămâne pe disc.
Exercițiile mută atenția de la „urmez comenzile” la „pot lua singur o decizie”. Pentru traseul de bază, consideră obligatorii exercițiile A, E, G și H. Celelalte sunt extensii: sunt valoroase, dar nu trebuie terminate pentru a continua cursul.
Folosind system_inventory.md, scrie un paragraf care separă hardware-ul detectat, utilitarele detectate, suportul de accelerare confirmat, componentele nedetectate și concluziile pe care încă nu le poți formula.
Folosește verbe diferite: „a fost raportat”, „a fost găsită comanda”, „interogarea a reușit” și „nu avem încă suficiente dovezi”. Aceste expresii nu înseamnă același lucru.
Extinde inspectorul numai cu tipul sistemului de fișiere, capacitatea totală și capacitatea liberă. Nu înregistra nume de fișiere, foldere personale, seriale sau căi care includ numele utilizatorului. Scrie testele parserului înainte să folosești rezultate reale.
Noul test trebuie să poată verifica parserul folosind text artificial. Testul nu trebuie să depindă de dimensiunea SSD-ului tău.
Adaugă un obiect capabilities cu can_run_python, can_render_graphviz, nvidia_compute_confirmed și amd_compute_confirmed. Definește fiecare boolean din dovezi explicite, nu din numele producătorului.
Adaugă clasificarea datelor, numărul estimat de utilizatori, cerința offline, latența maximă acceptabilă, stocarea estimată și cerința de recuperare. Actualizează validatorul și testele.
Creează configs/lesson03_repaired_stack_manifest.json. Corectează atât problemele structurale, cât și raționamentul, fără să copiezi manifestul valid. Explică fiecare decizie în cuvintele tale.
Fără să instalezi nimic, compară un traseu Ollama local, un traseu vLLM local și un API de model găzduit. Pentru fiecare, notează cine răspunde de hardware, pe unde circulă datele, ce povară operațională apare și ce trebuie evaluat mai târziu.
Construiește o listă cu model GPU, driver GPU, platformă de accelerare, OS, arhitectură CPU, versiune Python, build-ul framework-ului, runtime de inferență, arhitectură de model, format de artefact și licență. Orice necunoscut rămâne explicit necunoscut.
Privește inventarul și decide dacă l-ai publica. Separă câmpurile sigure de distribuit, câmpurile pe care le-ai elimina și informațiile care devin sensibile atunci când sunt combinate.
Actualizează manifestul pentru un agent de documente/cercetare, operațiuni de business, coding, producție media sau date științifice/senzori. Schimbă numai decizii pe care primele trei lecții le pot justifica. Păstrează restul neselectat.
Deschide răspunsul numai după ce formulezi în minte o propoziție proprie.
De ce omite inspectorul hostname-ul și adresele IP?
Laboratorul are nevoie de capabilități tehnice, nu de identitatea dispozitivului sau locația lui în rețea. Colectarea minimă reduce riscul de confidențialitate și securitate.
Dacă nvidia-smi lipsește, înseamnă că nu există GPU NVIDIA?
Nu. Înseamnă că executabilul nu a fost găsit în PATH. Driverul poate lipsi, mediul poate fi virtualizat, utilitarul poate fi în altă locație sau hardware-ul poate să nu fie compatibil. Ai nevoie de alte dovezi.
De ce inventarul și manifestul sunt fișiere diferite?
Inventarul descrie observații despre mașina actuală. Manifestul descrie decizii și responsabilități ale proiectului. Dacă le amesteci, riști ca software-ul instalat întâmplător să dicteze arhitectura.
Ce dovedește validatorul manifestului?
Dovedește că forma documentului respectă schema simplă a laboratorului: straturi obligatorii, valori permise pentru status, justificări și limite cu tipul corect. Nu dovedește compatibilitatea, securitatea sau calitatea deciziilor.
De ce este util not_selected_yet?
Face vizibil faptul că responsabilitatea este cunoscută, dar dovezile nu sunt suficiente pentru o alegere. Astfel, lipsa deciziei nu este confundată cu neglijența și nici nu este umplută cu un produs ales la întâmplare.
Cum poate un manifest valid JSON să descrie un sistem nesigur?
JSON validează structura datelor. Textul «oferă agentului acces complet la shell» poate fi o valoare JSON perfect validă și, în același timp, o decizie periculoasă.
De ce verificăm nvcc separat de nvidia-smi?
Sunt componente diferite. nvidia-smi este un utilitar de management/monitorizare legat de driver și NVML; nvcc este compilatorul-driver din CUDA Toolkit.
De ce testele folosesc date artificiale și nu cer un anumit GPU?
Parserul și validatorul trebuie să fie reproductibile pe mașini diferite. Hardware-ul real este verificat separat ca integrare, iar rezultatul lui poate diferi legitim.
De ce păstrăm PNG-ul diagramei în Git, dar ignorăm inventarul?
PNG-ul este un artefact de documentație revizuit și comun proiectului. Inventarul este un rezultat de rulare specific unei mașini și poate dezvălui detalii tehnice. Rolul fișierului, nu faptul că a fost generat, decide politica.
Ce faci dacă inventarul spune că Ollama nu a fost detectat?
Înregistrezi observația și continui. Laboratorul nu are nevoie de Ollama. Un server de model se instalează după ce sarcina, modelul, hardware-ul, confidențialitatea și criteriile de evaluare justifică acea alegere.
Laboratorul este terminat când poți bifa atât execuția, cât și înțelegerea.
.venv este activ;Aceste note au fost verificate din nou în septembrie 2026 și explică de ce laboratorul folosește anumite formulări și comenzi.
amd-smi primul și păstrează rocm-smi numai ca variantă de rezervă pentru sisteme mai vechi. Folosim amd-smi static --vram, nu --asic, deoarece ieșirea ASIC poate include un număr de serie al dispozitivului.nvidia-smi este utilitarul de management/monitorizare bazat pe NVML; nvcc este compilatorul-driver din CUDA Toolkit. Prezența unuia nu dovedește prezența celuilalt.lspci: pentru cod de producție care parsează automat rezultatul, folosește formatele destinate procesării de către programe, precum -m sau -vmm. Laboratorul păstrează formatul simplu numai pentru lizibilitate didactică.dot este potrivit pentru grafuri direcționate stratificate și poate produce PNG direct din sursa DOT.git init poate fi rulat în siguranță într-un repository existent; iar .gitignore nu încetează să urmărească automat fișiere deja adăugate în Git.python3-venv înainte de a crea .venv.Documentație oficială consultată: AMD SMI CLI, ROCm release notes, NVIDIA nvidia-smi, NVIDIA nvcc, lspci, Graphviz dot, git init, gitignore și Python pe Ubuntu.
Aceste fișiere nu sunt exerciții de unică folosință. Ele vor fi actualizate când proiectul primește componente noi.
configs/lesson03_stack_manifest.json
docs/architecture-decisions/ADR-0003-stack-before-products.md
diagrams/lesson03_stack.dot
src/lesson03_stack_inspector.py
src/lesson03_manifest_validator.py
tests/test_lesson03_stack.py
Lecțiile următoare pot adăuga un artefact de model, un model server, framework ML, clienți API, componente RAG, baze de date, sandbox-uri, telemetrie structurată, o interfață web sau deployment la distanță. Fiecare schimbare trebuie să actualizeze manifestul cu motiv, limită și dovadă.