Laborator practic: clasificare și rutare sigură în Python

Pregătește un proiect Python pe Ubuntu sau Kubuntu, construiește un joc de clasificare, testează o componentă de rutare deterministă și documentează primul tău agent.

Laboratorul 0130 min de lectură

90–120 de minute · Ubuntu sau Kubuntu · GPU-ul nu este necesar · Aproximativ 100 MB

Ce vei construi

Acest laborator transformă noțiunile teoretice în două programe mici, pe care le poți inspecta integral. Nu presupune experiență anterioară cu programarea: fiecare instrument și comandă sunt explicate înainte de utilizare.

  1. un spațiu de lucru Linux organizat și reproductibil, numit frecvent workspace în industria software;
  2. un joc mic în terminal care clasifică sisteme folosind etichetele lecției;
  3. o componentă de rutare deterministă, numită frecvent router, care trimite fiecare cerere către categoria potrivită;
  4. șapte teste automate, adică verificări executate de program pentru a confirma că funcțiile se comportă așa cum am stabilit;
  5. primul profil al agentului și primul Architecture Decision Record — ADR, adică un document scurt care notează o decizie tehnică și motivul ei.

Primul program este un joc de clasificare care prezintă situații software și cere alegerea etichetelor potrivite. Al doilea program este componenta de rutare: el nu rezolvă cererea, ci decide ce tip de tratament trebuie să primească. Regula de siguranță va impune aprobarea unui om pentru acțiuni precum plata sau ștergerea.

Aceste programe nu sunt încă agenți AI. Ele formează însă un punct de referință, numit în industrie baseline: o soluție simplă, măsurabilă și verificabilă cu care vom compara versiunile viitoare. Astfel vom putea demonstra ce adaugă un model lingvistic, un instrument, memoria sau o buclă de agent.

De ce nu instalăm încă un LLM?

Un LLMlarge language model, în română model lingvistic de mari dimensiuni — ar introduce comportament probabilistic înainte să înțelegem structura programului. Începem cu cod determinist pentru a putea urmări fiecare decizie și pentru a păstra regulile de siguranță explicite.

1. Cerințe

Înainte de instalare, verificăm mediul în care vom lucra. Laboratorul folosește terminalul din sistemul de operare bazat pe Linux — Ubuntu sau Kubuntu —, adică aplicația în care introduci comenzi text. Comenzile sunt utile deoarece pot fi repetate exact și pot fi incluse ulterior în documentație sau scripturi.

Ce trebuie să știi despre sudo

sudo execută o comandă cu drepturi administrative. Sistemul îți poate cere parola contului; când o tastezi, terminalul nu afișează caractere sau asteriscuri. Folosește sudo doar pentru comenzile explicate și nu copia comenzi administrative din surse necunoscute.

  • Ubuntu, Kubuntu sau o distribuție Debian apropiată;
  • un cont de utilizator pe calculatorul tău care poate instala programe, adică un cont cu drepturi administrative (folosește sudo). Este vorba despre contul cu care te autentifici în Ubuntu sau Kubuntu; nu îți trebuie niciun cont creat pe vreun site pentru acest curs;
  • terminal;
  • aproximativ 100 MB liberi;
  • niciun GPU necesar.

Comenzile folosesc instrumente Linux și biblioteca standard Python.

Alte platforme: pe Windows folosește WSL2 cu Ubuntu. Pe macOS, pașii Python și Git sunt similari, dar instalează git, jq și tree prin Homebrew, nu prin apt. Suportul oficial al laboratorului rămâne Ubuntu/Kubuntu.

În curând publicăm un tutorial dedicat pentru utilizatorii de Windows și macOS, astfel încât să poată rula laboratoarele fără dificultate.

2. Instalează instrumentele de bază

În această etapă instalăm un set mic de instrumente generale. Pentru acest laborator limbajul de programare este Python și pe el îl vom folosi ca să scriem programele; celelalte instrumente și comenzi ne ajută să izolăm proiectul, să urmărim modificările și să inspectăm rezultatele.

Python 3

Python este un limbaj de programare general: cu el se scriu deopotrivă scripturi de câteva rânduri, aplicații web și programe de calcul științific. Se citește aproape ca engleza obișnuită, nu cere declararea tipurilor și nu trebuie compilat înainte de rulare — scrii fișierul și îl execuți imediat, ceea ce scurtează mult drumul de la idee la rezultat.

În AI a devenit limbajul implicit fiindcă aproape toate bibliotecile importante sunt disponibile aici, iar exemplele din documentație, cursuri și lucrări sunt scrise tot în Python. Nu este singurul posibil — agenți de producție se scriu și în TypeScript, Java, Go sau Rust — dar rămâne cea mai practică alegere pentru primele experimente. Versiunea folosită în acest curs este Python 3; exemplele mai vechi de pe internet, scrise pentru Python 2, nu mai rulează.

venv și pip

venv vine de la virtual environment, în română mediu virtual. Comanda creează în proiect un folder (la noi .venv) care conține o copie a interpretorului Python și un loc propriu pentru biblioteci. Cât timp mediul este activat, tot ce instalezi ajunge acolo, nu în Python-ul întregului sistem: proiectul rămâne curat, poate fi refăcut identic pe alt calculator, iar două proiecte pot folosi versiuni diferite ale aceleiași biblioteci fără să se încurce între ele.

pip instalează biblioteci Python. Chiar dacă această lecție folosește doar biblioteca standard, pregătim structura corectă pentru etapele următoare.

Git

Git păstrează istoricul fișierelor local: poți vedea ce s-a schimbat și poți reveni la o versiune anterioară. Folderul pe care Git îl urmărește împreună cu istoricul său este numit în industrie repository sau, prescurtat, repo. Git nu este același lucru cu GitHub sau GitLab; acestea sunt servicii online unde poți găzdui un repository. Un cont online este opțional pentru acest laborator.

jq

JSON este un format text folosit pentru a transmite date structurate între programe. jq citește, formatează și filtrează JSON direct în terminal. Îl vom folosi pentru a extrage o singură valoare din răspunsul componentei de rutare.

tree

tree afișează folderele și fișierele sub forma unui arbore. Este util pentru a vedea structura proiectului fără să deschizi manual fiecare folder.

apt

apt este managerul de pachete folosit de Ubuntu și Kubuntu. El descarcă programele din sursele configurate ale sistemului și urmărește actualizările lor.

Programarea agenților pentru începători: Python este un limbaj de programare, o bibliotecă este cod pregătit dinainte pe care îl poți refolosi, iar dimensiunea ecosistemelor de pachete diferă de la un limbaj la altul

Ce fac cele două comenzi

  • sudo apt update actualizează catalogul local de pachete. Nu actualizează și programele deja instalate.
  • sudo apt install -y ... instalează pachetele enumerate. Opțiunea -y confirmă automat întrebarea de instalare.
sudo apt update
sudo apt install -y python3 python3-venv python3-pip git jq tree

După instalare, cerem fiecărui program să-și afișeze versiunea. Este un mod simplu de a verifica dacă executabilul există și dacă terminalul îl găsește:

python3 --version
git --version
jq --version
tree --version

Numerele exacte pot fi diferite. Important este ca fiecare comandă să afișeze un nume și o versiune.

Dacă apare mesajul „command not found”, instrumentul nu a fost instalat ori terminalul nu îl poate găsi. Revino la instalare, citește mesajele afișate de apt și repetă verificarea.

3. Pregătește folderul proiectului și mediul Python

În continuare vom crea un folder principal al proiectului și subfolderele care separă programele, testele, datele și documentația. Programatorii numesc adesea folderul principal project root, iar întregul spațiu de lucru poate fi numit workspace. Organizarea nu schimbă rezultatul programului, dar face proiectul mai ușor de înțeles, testat și extins.

src/codul programelor
tests/teste automate
data/date de lucru viitoare
docs/lecții și decizii arhitecturale
configs/configurații
skills/proceduri reutilizabile viitoare
evaluations/cazuri și rezultate de evaluare
logs/jurnale produse la rulare

Cum citești comenzile

  • mkdir -p creează foldere. Opțiunea -p creează și folderele intermediare, iar dacă acestea există deja nu dă eroare.
  • ~ înseamnă folderul personal al utilizatorului, de exemplu /home/ana.
  • cd schimbă folderul curent al terminalului.
  • Bara inversă \ continuă aceeași comandă pe linia următoare, pentru lizibilitate.
  • python3 -m venv .venv creează un mediu Python izolat în folderul ascuns .venv.
  • source .venv/bin/activate activează acel mediu pentru sesiunea curentă de terminal.
  • python -m pip install --upgrade pip actualizează instalatorul de biblioteci din mediul izolat, nu pe cel al întregului sistem.
  • git init transformă folderul într-un repository Git local.
mkdir -p ~/ai-agents-zero-to-expert
cd ~/ai-agents-zero-to-expert

mkdir -p \
  src \
  tests \
  data \
  docs/lessons \
  docs/architecture-decisions \
  configs \
  skills \
  evaluations \
  logs

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

git config --global init.defaultBranch main
git init

Dacă activarea a reușit, promptul terminalului începe cu (.venv). Acest marcaj arată că Python și pip rulează acum în mediul proiectului. Pentru a ieși din el mai târziu, rulezi deactivate.

3.1 Creează .gitignore

.gitignore este un fișier text care îi spune lui Git ce fișiere sau foldere să nu includă în istoricul repository-ului. Programatorii spun că acele fișiere sunt ignorate de Git. Fișierul nu le ascunde pe calculator, nu le protejează cu parolă și nu le șterge; el previne doar adăugarea lor accidentală prin git add.

Ignorăm mediul virtual, fișierele temporare Python, secretele din fișiere .env și jurnalele generate la rulare. Acestea pot fi mari, dependente de calculator sau sensibile și pot fi recreate.

Ce înseamnă sintaxa cat > fișier <<'EOF'

Aceasta este o metodă Linux numită here-document. Terminalul copiază toate liniile dintre prima linie și marcajul final EOF într-un fișier nou. O folosim pentru ca tu să poți crea exact conținutul necesar fără un editor separat.

cat > .gitignore <<'EOF'
.venv/
__pycache__/
*.py[cod]
.env
.env.*
logs/*
!logs/.gitkeep
.DS_Store
EOF

touch logs/.gitkeep

Regula logs/* ignoră conținutul folderului, iar !logs/.gitkeep păstrează fișierul gol care marchează existența folderului. Deoarece Git nu urmărește foldere goale, touch logs/.gitkeep creează acel fișier. Jurnalele propriu-zise, inclusiv fișierele .jsonl, nu ajung în istoricul Git.

Inspectează structura:

tree -a -L 2

4. Mini-joc: „Ce tip de sistem este?”

Vom crea acum primul program al laboratorului: un joc în terminal care selectează situații, primește etichetele alese de cursant, le compară cu o clasificare de referință și calculează un scor. Prin acest program transformăm teoria într-un comportament observabil: poți rula aceleași reguli, poți greși, poți vedea explicația și poți îmbunătăți rezultatul.

Programul este salvat într-un singur fișier Python. Un program mic de acest tip este numit frecvent script. Folosim numai biblioteca standard Python, adică modulele instalate împreună cu Python, fără pachete externe. Un modul este un fișier sau o componentă de cod care poate fi importată și reutilizată; instrucțiunea import aduce în program funcții definite într-un asemenea modul.

Structura programului înainte să-l copiezi

  1. Importuri: aduc funcții standard pentru argumentele din linia de comandă, alegeri aleatoare și structuri de date.
  2. VALID_TAGS: definește singurele etichete acceptate.
  3. Scenario: descrie forma fiecărui scenariu: nume, text, etichete și explicație.
  4. SCENARIOS: păstrează exemplele jocului.
  5. Funcțiile de parsare și scor: transformă textul introdus de utilizator în etichete și compară mulțimile rezultate.
  6. main(): rulează rundele și afișează rezultatul final.

Comanda următoare creează fișierul src/classification_game.py printr-un here-document. Textul dintre <<'PY' și marcajul final PY devine codul programului.

cat > src/classification_game.py <<'PY'
#!/usr/bin/env python3
"""Jocul de clasificare din Lectia 1.

Jocul arata ca etichetele se suprapun. Un sistem poate fi ML + workflow +
automat fara sa fie agent.
"""

from __future__ import annotations

import argparse
import random
from dataclasses import dataclass

VALID_TAGS = {"ALG", "ML", "GEN", "FLOW", "AGENT", "AUTO"}


@dataclass(frozen=True)
class Scenario:
    name: str
    description: str
    tags: frozenset[str]
    explanation: str


SCENARIOS = [
    Scenario(
        "Calculator",
        "Evalueaza 17 x 43.",
        frozenset({"ALG"}),
        "Un algoritm determinist transforma intrarea intr-o iesire exacta.",
    ),
    Scenario(
        "Backup nocturn",
        "Un job cron copiaza un folder pe alt disc in fiecare noapte.",
        frozenset({"ALG", "FLOW", "AUTO"}),
        "Este un workflow automat fix. Actioneaza fara aprobare imediata, dar nu este machine learning.",
    ),
    Scenario(
        "Rutarea spamului",
        "Un model invatat prezice spamul, iar sistemul de e-mail muta mesajul automat.",
        frozenset({"ALG", "ML", "FLOW", "AUTO"}),
        "Predictia este invatata, iar mutarea in folder o face o regula determinista.",
    ),
    Scenario(
        "Poezie dintr-o singura cerere",
        "Un model lingvistic scrie o poezie dintr-un singur prompt.",
        frozenset({"ML", "GEN"}),
        "Este inferenta generativa, dar nu exista o bucla cu instrumente, orientata spre un obiectiv.",
    ),
    Scenario(
        "Arbore de raspunsuri FAQ",
        "Un bot de suport urmeaza un arbore de decizie stabilit dinainte.",
        frozenset({"ALG", "FLOW"}),
        "Este automatizare conversationala, nu neaparat AI si nu un agent.",
    ),
    Scenario(
        "Investigator de facturi",
        "Sistemul interogheaza inregistrari, alege alta verificare cand lipsesc dovezi, redacteaza un mesaj si asteapta aprobarea.",
        frozenset({"ALG", "ML", "GEN", "FLOW", "AGENT"}),
        "Alege dinamic actiunile, dar nu trimite singur mesajul in exterior.",
    ),
    Scenario(
        "Aspirator robotic",
        "Cartografiaza camerele si schimba traseul cand intalneste un obstacol.",
        frozenset({"ALG", "ML", "AGENT", "AUTO"}),
        "Percepe si actioneaza intr-o bucla cu feedback. Implementarile pot folosi si reguli, si modele invatate.",
    ),
    Scenario(
        "Robot de sudura cu traseu fix",
        "Repeta acelasi traseu de sudura programat.",
        frozenset({"ALG", "FLOW", "AUTO"}),
        "Automatizarea fizica nu inseamna automat un agent adaptiv.",
    ),
    Scenario(
        "Motor clasic de sah",
        "Cauta mutarile posibile cu o logica de evaluare scrisa de oameni.",
        frozenset({"ALG", "AGENT"}),
        "Alege actiuni catre un obiectiv, fara sa aiba nevoie de machine learning.",
    ),
    Scenario(
        "Pipeline RAG de raspuns",
        "Recupereaza pasaje din documente si produce un raspuns cu citari, intr-o secventa fixa.",
        frozenset({"ALG", "ML", "GEN", "FLOW"}),
        "Un RAG obisnuit este de regula un pipeline. Devine agentic daca sistemul planifica si repeta cautarea in timpul rularii.",
    ),
    Scenario(
        "Asistent de cod",
        "Sugereaza cod, dar nu poate modifica fisiere si nu poate rula comenzi.",
        frozenset({"ML", "GEN"}),
        "Este un asistent, nu un agent care actioneaza.",
    ),
    Scenario(
        "Agent de programare",
        "Planifica modificari, editeaza fisiere, ruleaza teste, observa erorile si incearca din nou.",
        frozenset({"ALG", "ML", "GEN", "AGENT", "AUTO"}),
        "Are o bucla cu instrumente, orientata spre obiectiv, si modifica mediul.",
    ),
]


def parse_tags(raw: str) -> set[str]:
    """Transforma textul scris de utilizator in etichete valide, normalizate."""
    normalized = raw.upper().replace(",", " ").split()
    return {tag for tag in normalized if tag in VALID_TAGS}


def score_tags(expected: set[str] | frozenset[str], actual: set[str]) -> float:
    """Intoarce un scor de suprapunere, intre 0 si 1, care iarta greselile partiale."""
    union = set(expected) | actual
    if not union:
        return 1.0
    return len(set(expected) & actual) / len(union)


def print_reference() -> None:
    print("\nEtichete:")
    print("  ALG   comportamentul vine din reguli si calcule scrise de oameni")
    print("  ML    in sistem exista un model invatat din exemple")
    print("  GEN   produce continut nou")
    print("  FLOW  urmeaza un flux automat stabilit dinainte")
    print("  AGENT isi alege singur actiunile pentru un obiectiv")
    print("  AUTO  actioneaza fara aprobarea imediata a unui om")


def run_game(rounds: int, seed: int | None) -> None:
    rng = random.Random(seed)
    selected = SCENARIOS.copy()
    rng.shuffle(selected)
    selected = selected[: min(rounds, len(selected))]

    total = 0.0
    print("\nCE FEL DE SISTEM ESTE?")
    print("Scrie toate etichetele potrivite, separate prin spatii sau virgule.")
    print_reference()

    for index, scenario in enumerate(selected, start=1):
        print(f"\n[{index}/{len(selected)}] {scenario.name}")
        print(scenario.description)
        try:
            answer = input("Etichetele tale: ")
        except (EOFError, KeyboardInterrupt):
            print("\nJoc intrerupt. Nu s-a salvat nimic.")
            return
        actual = parse_tags(answer)
        round_score = score_tags(scenario.tags, actual)
        total += round_score

        expected = ", ".join(sorted(scenario.tags))
        print(f"Raspunsul cursului: {expected}")
        print(f"Explicatie: {scenario.explanation}")
        print(f"Scor runda: {round_score:.0%}")

    percentage = 100 * total / len(selected)
    print(f"\nScor final: {percentage:.1f}%")
    if percentage >= 85:
        print("Excelent: separi corect capabilitatile, controlul si autoritatea.")
    elif percentage >= 65:
        print("Inceput bun: reciteste diferenta dintre workflow, agent si autonomie.")
    else:
        print("Reia jocul dupa ce revezi harta celor trei axe.")


def list_scenarios() -> None:
    print_reference()
    for scenario in SCENARIOS:
        tags = ", ".join(sorted(scenario.tags))
        print(f"\n{scenario.name} [{tags}]\n  {scenario.description}\n  {scenario.explanation}")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--rounds", type=int, default=8)
    parser.add_argument("--seed", type=int)
    parser.add_argument("--list", action="store_true")
    args = parser.parse_args()

    if args.rounds < 1:
        parser.error("--rounds trebuie sa fie cel putin 1")

    if args.list:
        list_scenarios()
    else:
        run_game(args.rounds, args.seed)


if __name__ == "__main__":
    main()
PY

chmod +x src/classification_game.py
Deschide explicația principalelor elemente de cod

@dataclass(frozen=True) creează o structură de date compactă și nemodificabilă după inițializare. frozenset păstrează etichetele fără duplicate. Funcția parse_tags acceptă virgule, spații și litere mici sau mari. Funcția score_tags folosește indicele Jaccard: numărul etichetelor comune împărțit la numărul total de etichete distincte.

argparse permite opțiuni precum --rounds, --seed și --list. O valoare fixă de pornire (seed) produce aceeași ordine aleatoare, lucru util pentru teste și demonstrații reproductibile.

Rulează programul cu Python. Varianta implicită alege 8 dintre cele 12 scenarii; apasă Enter după ce introduci etichetele.

python src/classification_game.py

Pentru a le vedea pe toate, într-o ordine reproductibilă:

python src/classification_game.py --rounds 12 --seed 42

Afișează lista de referință rulând direct scriptul:

./src/classification_game.py --list

Dacă apeși Enter fără etichete, runda primește scor 0; programul nu se blochează.

Ce învață jocul

  • Etichetele se pot suprapune.
  • Un sistem poate fi automat fără să fie agent.
  • Un sistem poate folosi ML fără să genereze conținut.
  • Un agent poate exista fără LLM.
  • Clasificarea cere definiții și presupuneri explicite.

5. Testează logica jocului

Un test automat rulează o funcție cu o intrare cunoscută și verifică dacă rezultatul este cel așteptat. Testele nu demonstrează că programul nu are nicio eroare, dar previn reapariția unor erori deja cunoscute și ne permit să modificăm codul cu mai multă încredere.

Vom testa funcțiile mici, nu interacțiunea completă din terminal. Acest tip de test se numește unit test. Modulul unittest este inclus în Python.

Fișierul de mai jos conține patru verificări independente. El importă funcțiile din program, le oferă intrări controlate și compară rezultatele cu valorile așteptate. Creează fișierul de test cu următoarea comandă:

cat > tests/test_classification_game.py <<'PY'
import sys
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))

from classification_game import parse_tags, score_tags


class ClassificationGameTests(unittest.TestCase):
    def test_parse_tags_accepts_commas_spaces_and_case(self):
        self.assertEqual(parse_tags("ml, gen agent"), {"ML", "GEN", "AGENT"})

    def test_parse_tags_ignores_unknown_values(self):
        self.assertEqual(parse_tags("ML MAGIC FLOW"), {"ML", "FLOW"})

    def test_exact_score(self):
        self.assertEqual(score_tags({"ML", "GEN"}, {"ML", "GEN"}), 1.0)

    def test_partial_score(self):
        self.assertAlmostEqual(score_tags({"ML", "GEN"}, {"ML"}), 0.5)


if __name__ == "__main__":
    unittest.main()
PY

Rulează acum toate testele din folderul tests. Opțiunea -v înseamnă verbose și afișează numele fiecărei verificări, nu doar rezultatul final:

python -m unittest discover -s tests -v

Rezultatul intermediar trebuie să includă:

Ran 4 tests ... OK

Testarea apare încă din Lecția 1 deoarece agenții fiabili vor depinde de componente deterministe care trebuie să rămână corecte chiar când schimbăm modele și prompturi.

6. Proiect scurt: componentă de rutare deterministă

Al doilea program primește o cerere scrisă în limbaj obișnuit și o trimite către o categorie prestabilită. Componenta care face această alegere se numește în industrie router sau componentă de rutare. Nu este routerul de internet sau Wi-Fi din locuință; este o piesă de software care decide către ce funcție, serviciu ori proces trebuie direcționată o cerere.

Într-un agent viitor, componenta de rutare ar putea trimite o cerere către un calculator, un model lingvistic, o bază de date sau o coadă de aprobare. O coadă este o structură în care sarcinile așteaptă să fie prelucrate. În această lecție folosim reguli explicite, astfel încât fiecare decizie să poată fi citită și verificată.

Programul nu este încă un agent AI. El este primul nostru baseline, termen folosit în industrie pentru un punct de referință simplu cu care vom compara soluțiile viitoare. Dacă un model mai complex nu depășește baseline-ul la precizie, siguranță ori cost, complexitatea suplimentară nu este justificată.

6.1 Găsește greșeala: precedența regulilor

Analizează exemplul și deschide explicația

Ordinea regulilor contează atunci când o cerere conține mai multe intenții. Dacă programul verifică mai întâi cuvântul calculate, o cerere precum „calculate and pay” ar putea fi declarată sigură înainte să observe cuvântul pay. De aceea, regulile care blochează sau cer aprobare trebuie evaluate înaintea regulilor obișnuite.

Privește ordinea nesigură:

ROUTES = [
    ({"calculate"}, "calculation", False),
    ({"pay"}, "high-impact-action", True),
]

Dacă programul returnează prima regulă potrivită, cererea calculate and pay the invoice poate fi tratată drept calcul inofensiv și poate ocoli aprobarea. Aceasta este o eroare de securitate.

Implementarea corectată verifică mai întâi cuvintele cu impact mare: regulile de interdicție ori aprobare trebuie să aibă prioritate față de regulile obișnuite de permitere.

6.2 Creează implementarea sigură

Implementarea sigură separă cuvintele cu impact ridicat într-o mulțime proprie și le verifică înainte de rutele normale. Rezultatul este afișat ca JSON — un format text cu perechi de tip cheie–valoare, folosit frecvent pentru schimbul de date între programe. De exemplu, {"requires_human": true} spune explicit că este necesară aprobarea unui om.

Ce conține routerul

  • RouteDecision descrie ruta, motivul și necesitatea aprobării.
  • HIGH_IMPACT_KEYWORDS păstrează cuvintele care marchează acțiuni ce nu au voie să fie executate fără aprobare.
  • ROUTES definește categoriile obișnuite.
  • route_task normalizează cuvintele și aplică precedența de siguranță.
  • json.dumps transformă rezultatul în text JSON, ușor de procesat.
cat > src/task_router.py <<'PY'
#!/usr/bin/env python3
"""Un baseline determinist de rutare.

Clasifica o sarcina dupa reguli explicite si intoarce pasul urmator stabilit dinainte.
Nu are model invatat, nu are plan adaptiv, nu are instrumente si nu are bucla de actiune.
"""

from __future__ import annotations

import json
import sys
from dataclasses import asdict, dataclass


@dataclass(frozen=True)
class RouteDecision:
    route: str
    reason: str
    requires_human: bool


HIGH_IMPACT_KEYWORDS = {"send", "delete", "publish", "pay"}

ROUTES = [
    ({"calculate", "sum", "multiply", "total"}, "calculation", "Aritmetica exacta se face in cod determinist.", False),
    ({"summarize", "rewrite", "draft"}, "language-task", "Transformarea de limbaj poate folosi mai tarziu un model generativ.", True),
    ({"classify", "detect", "predict"}, "prediction-task", "Predictia poate folosi mai tarziu reguli sau machine learning.", True),
]


def route_task(task: str) -> RouteDecision:
    words = {word.strip(".,!?;:").lower() for word in task.split()}

    # Precedenta de siguranta: regulile de interdictie sau de aprobare sunt evaluate
    # inaintea rutelor obisnuite. Altfel, o cerere precum "calculate and pay the invoice"
    # s-ar potrivi intai cu "calculate" si ar ocoli gresit aprobarea umana.
    if words & HIGH_IMPACT_KEYWORDS:
        return RouteDecision(
            "high-impact-action",
            "Actiunile externe sau distructive cer aprobare explicita.",
            True,
        )

    for keywords, route, reason, requires_human in ROUTES:
        if words & keywords:
            return RouteDecision(route, reason, requires_human)

    return RouteDecision(
        "manual-review",
        "Nicio regula determinista nu se potriveste sigur cu aceasta sarcina.",
        True,
    )


def main() -> None:
    task = " ".join(sys.argv[1:]).strip()
    if not task:
        task = input("Descrie o sarcina: ").strip()
    if not task:
        raise SystemExit("Este nevoie de o sarcina, nu de un text gol.")

    decision = route_task(task)
    print(json.dumps(asdict(decision), indent=2))


if __name__ == "__main__":
    main()
PY

chmod +x src/task_router.py

Încearcă mai multe sarcini:

python src/task_router.py "calculate the invoice total"
python src/task_router.py "draft a polite reminder"
python src/task_router.py "delete the original files"
python src/task_router.py "investigate what happened"

Folosește jq pentru a citi un singur câmp din JSON:

python src/task_router.py "calculate and pay the invoice" | jq .requires_human

Simbolul | trimite ieșirea primei comenzi ca intrare pentru jq. Expresia .requires_human extrage doar câmpul respectiv din obiectul JSON. Rezultatul trebuie să fie true.

Rezultatul trebuie să fie:

true

6.3 Adaugă teste de regresie

Adăugăm acum un test de regresie pentru exact combinația care ar putea ocoli aprobarea. Un test de regresie fixează comportamentul corect după descoperirea unei erori, astfel încât o modificare viitoare să nu readucă problema.

cat > tests/test_task_router.py <<'PY'
import sys
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))

from task_router import route_task


class TaskRouterTests(unittest.TestCase):
    def test_calculation_is_low_risk_by_itself(self):
        result = route_task("calculate the invoice total")
        self.assertEqual(result.route, "calculation")
        self.assertFalse(result.requires_human)

    def test_high_impact_rule_overrides_calculation(self):
        result = route_task("calculate and pay the invoice")
        self.assertEqual(result.route, "high-impact-action")
        self.assertTrue(result.requires_human)

    def test_unknown_task_goes_to_manual_review(self):
        result = route_task("investigate what happened")
        self.assertEqual(result.route, "manual-review")
        self.assertTrue(result.requires_human)


if __name__ == "__main__":
    unittest.main()
PY

python -m unittest discover -s tests -v

Suita completă trebuie să raporteze 7 teste și să se termine cu OK.

De ce componenta de rutare nu este încă agent

Routerul:

  • nu se descurcă bine cu formulări pe care nu le-a mai întâlnit;
  • nu are obiectiv pe termen lung;
  • nu are acces la instrumente externe;
  • nu observă efectul unei acțiuni;
  • nu poate revizui un plan;
  • face o singură clasificare și se oprește.

Acesta este avantajul baseline-ului: comportamentul este transparent, testabil și comparabil cu versiuni viitoare.

7. Documentează agentul pe care îl vei construi

Ingineria unui agent nu înseamnă doar cod. Înainte să conectezi modele și instrumente, trebuie să definești cine îl folosește, ce rezultat urmărește, ce date poate citi și ce acțiuni necesită aprobare. Aceste decizii vor limita riscul și vor face evaluarea posibilă.

Vom crea două documente Markdown. Markdown este un format text simplu, în care titlurile încep cu #, listele cu -, iar casetele de bifat cu - [ ]. Fișierele pot fi citite deopotrivă de oameni și de programe: Git, editoare precum Obsidian, instrumente de dezvoltare.

7.1 Profilul inițial al agentului

Profilul agentului este o fișă de intenție și de limite. Nu alegem încă modelul sau framework-ul; definim problema, utilizatorul și nivelul inițial de autoritate.

cat > docs/agent-profile.md <<'EOF'
# Profilul agentului meu

## Nume provizoriu

`[Alege un nume temporar]`

## Utilizatorul vizat

`[Cine îl va folosi?]`

## Rezultatul dorit

`[Ce trebuie să devină mai ușor, rapid, sigur sau exact?]`

## Domeniu

`[Documente, cercetare, operațiuni, programare, media, știință etc.]`

## Informații pe care le-ar putea citi

- `[Sursa 1]`
- `[Sursa 2]`

## Acțiuni pe care le-ar putea pregăti

- `[Acțiunea 1]`
- `[Acțiunea 2]`

## Acțiuni executabile automat

- `[Începe doar cu acțiuni reversibile și cu risc redus]`

## Acțiuni care cer aprobare umană

- ștergere;
- plată;
- publicare;
- mesaje trimise extern;
- modificări ale înregistrărilor sensibile;
- `[Adaugă acțiuni specifice domeniului]`.

## Nivel inițial de autonomie

**Nivelul 1 — doar consultativ.**

## Dovezi de succes

`[Cum vei măsura că agentul a ajutat?]`

## Riscuri cunoscute

- răspunsuri nesusținute;
- permisiuni excesive;
- expunerea datelor private;
- selectarea greșită a instrumentului;
- `[Riscuri specifice proiectului]`.
EOF

7.2 Architecture Decision Record — ADR-0001

Un Architecture Decision Record — ADR — este un document scurt care notează o decizie importantă, contextul și consecințele sale. Îl folosim pentru a evita situația în care, peste câteva luni, nu mai știm de ce proiectul a fost construit într-un anumit mod.

cat > docs/architecture-decisions/ADR-0001-start-with-a-deterministic-baseline.md <<'EOF'
# ADR-0001: Începem cu un baseline determinist

## Stare

Acceptată

## Context

Proiectul va deveni treptat un agent AI. Dacă am începe direct cu un LLM, ar fi greu să separăm comportamentul modelului de comportamentul orchestrării.

## Decizie

Construim și testăm un router determinist înainte de a conecta un model.

## Consecințe

- Avem un baseline transparent.
- Putem măsura dacă AI aduce îmbunătățire reală.
- Regulile de siguranță rămân deterministe.
- Prima versiune are flexibilitate lingvistică redusă.
EOF

8. Alege traseul agentului tău

În această etapă alegi doar domeniul general. Toate traseele vor folosi componente comune — instrumente, stare, evaluări și permisiuni — dar exemplele de date și acțiuni vor diferi.

Traseu Exemplu de scop inițial
A — Documente și cercetare Caută în surse, extrage dovezi, pregătește un răspuns citat
B — Operațiuni de business Clasifică cereri, pregătește acțiuni și respectă aprobările
C — Programare Analizează repository-uri, propune modificări și rulează teste în sandbox
D — Media Organizează metadatele, transcrierile și căutarea semantică
E — Știință și inginerie Interoghează date și apelează simulatoare ori calcule validate
F — Domeniu propriu Definește utilizatorul, rezultatul și limitele de autoritate

Nu trebuie să alegi definitiv stack-ul, modelul sau framework-ul. Alegi doar problema și granițele inițiale.

9. Adaugă fișa lecției

Fișa lecției este un jurnal scurt al progresului. Ea te obligă să notezi ce poți explica, ce caz ți s-a părut dificil și unde proiectul tău ar trebui să folosească reguli, modele sau aprobare umană.

cat > docs/lessons/lesson-01.md <<'EOF'
# Fișa Lecției 1

## Concepte pe care le pot explica

- [ ] AI versus automatizare
- [ ] machine learning versus AI generativă
- [ ] model versus agent
- [ ] workflow versus agent
- [ ] agentic versus autonom
- [ ] clasificarea pe cele trei axe

## Cel mai dificil caz-limită

`[Scrie exemplul și motivul.]`

## Un pas al agentului meu care trebuie să folosească reguli

`[Răspuns]`

## Un pas care ar putea necesita model învățat

`[Răspuns]`

## O acțiune care trebuie aprobată

`[Răspuns]`
EOF

10. Salvează munca în Git

Git salvează versiuni ale proiectului sub forma unor commit-uri. Un commit este o înregistrare locală care conține fișierele selectate, autorul, momentul și un mesaj despre schimbare. Commit-urile rămân pe calculator până când alegi să le trimiți către un serviciu online, operație numită de obicei push.

Ai nevoie de cont GitHub?

Nu pentru acest pas. Poți folosi Git complet local. Un cont GitHub, GitLab sau Codeberg devine util dacă vrei backup online, colaborare sau publicarea proiectului. Nu încărca un proiect public înainte să verifici că fișierele secrete și datele personale sunt excluse.

Ce fac comenzile Git

  • git config --global user.name și user.email stabilesc autorul commit-urilor pe acest calculator.
  • git add . pregătește fișierele neignorate pentru următorul commit.
  • git status arată ce va fi inclus.
  • git commit -m "..." creează instantaneul și îi atribuie mesajul indicat.

Configurează identitatea înainte de primul commit. Omite primele două comenzi dacă sunt deja configurate.

git config --global user.name "Numele tău"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main

git add .
git status
git commit -m "Complete lesson 1 system classification lab"

Într-un repository public nu folosi o adresă de e-mail personală, dacă nu vrei să devină publică. GitHub și alte platforme oferă adrese de tip noreply, create tocmai ca să nu-ți expună adresa reală.

11. Exerciții practice

Exercițiile de mai jos consolidează exact pașii executați: clasificare, siguranță, baseline și organizarea proiectului. Fă cel puțin exercițiile de bază și de siguranță înainte de a considera laboratorul terminat.

Exercițiu de bază

Joacă toate cele 12 scenarii și obține peste 80%:

python src/classification_game.py --rounds 12

Exercițiu pentru cazuri-limită

Adaugă un Scenario discutabil și explică presupunerile în câmpul explanation.

Exercițiu pentru punctul de referință (baseline)

Adaugă o rută sigură în task_router.py și un test pentru ea.

Exercițiu de siguranță

Mută temporar verificarea de impact sub rutele obișnuite și rulează calculate and pay the invoice. Notează rezultatul nesigur, restaurează ordinea corectă și explică de ce regulile de aprobare au prioritate. Nu păstra versiunea nesigură.

Extensie avansată

Modifică jocul pentru a salva rezultate JSON Lines în logs/lesson01-results.jsonl. Confirmă cu:

git status --ignored

Fișierul trebuie să apară ca ignorat. Nu salva date personale sau sensibile.

12. Depanare

Depanarea începe prin citirea mesajului complet de eroare și verificarea folderului curent. Nu repeta la întâmplare comenzile cu sudo; folosește simptomele de mai jos pentru a identifica etapa care a eșuat.

python: command not found

În afara mediului virtual folosește python3. Reactivează mediul:

source ~/ai-agents-zero-to-expert/.venv/bin/activate

Crearea mediului virtual eșuează

sudo apt install --reinstall python3-venv

Git refuză commit-ul

Configurează numele și adresa ca în Secțiunea 10, apoi reîncearcă.

Scriptul nu este executabil

O singură dată, în laborator, scriptul este rulat direct: ./src/classification_game.py --list. Repară permisiunile:

chmod +x src/classification_game.py src/task_router.py

Poți rula întotdeauna prin Python:

python src/classification_game.py

Testele nu pot importa modulul

Asigură-te că ești în rădăcina proiectului:

cd ~/ai-agents-zero-to-expert
python -m unittest discover -s tests -v

13. Checklist de finalizare

Bifează fiecare punct numai după ce ai rulat comanda și ai verificat rezultatul. Checklist-ul confirmă că proiectul poate fi reluat în etapa următoare fără să lipsească vreun pas.