BPM Logo
Studiu de caz • Agenți AI & Tool Calling

Put your money where your mouth is

De la „buzzwords” la practică: Cum construim un agent AI minimal care scrie, testează și își corectează singur codul într-un mediu izolat.

REST API, Tools, Agenți...

Auzim la tot pasul o mulțime de buzzwords. Pentru a nu relua de fiecare dată explicațiile de la zero legate de cum se testează sau instalează un modul, am structurat deja întreaga colecție de ghiduri pe bpm.creez.ro.

Așa că în exemplul de azi am decis să lucrez direct cu „materialul clientului”:

  • Am deschis o instanță curată de Windows Sandbox.
  • Am instalat Python și Ollama conform pașilor din articolele anterioare.

A durat doar 5 minute? Desigur că nu. 😂

Mi-am dat seama abia mai târziu că în Sandbox nu aveam nici măcar un Notepad la îndemână, așa că a trebuit să trag repede o versiune portabilă de editor de text în folderul partajat. Cu toate astea, la final am avut tot ce ne trebuie pentru a crea un script minim — cea mai pură și apropiată formă a unui agent AI.

Cum funcționează „magia” din spate?

Am testat mai multe variante și modele AI pentru a găsi limita inferioară de la care scriptul devine cu adevărat stabil. Pragul minim funcțional la care am ajuns a fost modelul qwen3:8b (care ocupă cca. 5.2 GB — memoria RAM sau VRAM minimă de care ai nevoie activă).

Dar fix despre asta este vorba: am testat iterativ până a ieșit! Scriptul face exact același lucru: AI-ul vorbește cu AI-ul și o dă la perete până când rezolvă cerința primită.

📖 Standardul din spatele uneltelor (Function Calling) Magia prin care modelul AI înțelege când și cum să apeleze funcții din sistemul local este documentat oficial în specificațiile OpenAI:
https://developers.openai.com/api/docs/guides/function-calling
Rulare Agent Python în Terminal
Rularea agentului în terminal – procesul de gândire și apelare a uneltelor locale.
🐍 Vezi codul sursă complet Python (Agent Coder cu Tool Calling) [ Apasă pentru a extinde ]
import json
import os
import subprocess
import time
import httpx
from openai import OpenAI

# ===========================================================================
# 0. CONFIGURARE CLIENT LOCAL (RUTARE CĂTRE PROXMOX / LLAMA.CPP)
# ===========================================================================
client = OpenAI(
    base_url=f"http://localhost:11434/v1",
    api_key="nu-conteaza-pentru-local",  # Serverul local nu o verifică, dar librăria o cere
)

# ===========================================================================
# 1. DEFINIREA UNELTELOR (FUNCTIILOR PYTHON PE CARE AI-UL LE POATE APELA)
# ===========================================================================


def scrie_cod_in_fisier(nume_fisier: str, cod: str) -> str:
    """Scrie sau suprascrie un fișier local cu codul primit."""
    try:
        with open(nume_fisier, "w", encoding="utf-8") as f:
            f.write(cod)
        return f"[Unealtă] Codul a fost salvat cu succes în '{nume_fisier}'."
    except Exception as e:
        return f"[Unealtă] Eroare la scrierea fișierului '{nume_fisier}': {str(e)}"


def ruleaza_script_python(nume_fisier: str) -> str:
    """Rulează un script Python local și returnează ce afișează în terminal."""
    if not os.path.exists(nume_fisier):
        return (
            f"[Unealtă Rulare] Eroare: Fișierul '{nume_fisier}' nu există pe"
            " disc."
        )

    try:
        # Rulăm scriptul folosind executabilul curent 'python' sau 'py'
        # Capturăm output-ul text (stdout) și erorile (stderr)
        rezultat = subprocess.run(
            [
                "py",
                nume_fisier,
            ],  # Schimbă în ["python", nume_fisier] dacă ești pe Linux/Mac
            capture_output=True,
            text=True,
            timeout=15,
        )

        if rezultat.returncode == 0:
            return f"[Unealtă Rulare] Succes!\nIeșire:\n{rezultat.stdout}"
        else:
            return f"[Unealtă Rulare] Eroare la rulare (Cod {rezultat.returncode}):\n{rezultat.stderr}"

    except subprocess.TimeoutExpired:
        return (
            "[Unealtă Rulare] Eroare: Scriptul a durat prea mult și a primit"
            " Timeout."
        )
    except Exception as e:
        return f"[Unealtă Rulare] Eroare neașteptată de sistem: {str(e)}"


# ===========================================================================
# 2. DEFINIREA SCHEMEI JSON PENTRU OPENAI (HARTA UNELTELOR)
# ===========================================================================

unelte_openai = [
    {
        "type": "function",
        "function": {
            "name": "scrie_cod_in_fisier",
            "description": (
                "Scrie sau creează un fișier local cu cod Python. Folosește-o"
                " ori de câte ori trebuie să generezi cod sau să salvezi un"
                " script."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "nume_fisier": {
                        "type": "string",
                        "description": (
                            "Numele fișierului în care se salvează codul (ex:"
                            " 'calcul.py')."
                        ),
                    },
                    "cod": {
                        "type": "string",
                        "description": (
                            "Codul sursă complet în Python care va fi scris în"
                            " acel fișier."
                        ),
                    },
                },
                "required": ["nume_fisier", "cod"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "ruleaza_script_python",
            "description": (
                "Rulează un fișier Python local existent pe disc și colectează"
                " rezultatul printat în terminal sau erorile aruncate."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "nume_fisier": {
                        "type": "string",
                        "description": (
                            "Numele exact al fișierului de pe disc care"
                            " trebuie executat (ex: 'calcul.py')."
                        ),
                    }
                },
                "required": ["nume_fisier"],
            },
        },
    },
]

# ===========================================================================
# 3. BUCLA PRINCIPALĂ A AGENTULUI (ORCHESTRAREA ȘI PARTEA DIDACTICĂ)
# ===========================================================================


def ruleaza_agent_coder(cerinta_utilizator: str, max_iteratii: int = 5):
    # Promptul de sistem setează personalitatea și regulile de comportament ale agentului
    prompt_sistem = (
        "Ești un agent AI autonom specializat în programare. Sarcina ta este"
        " să scrii cod, să îl salvezi în fișiere locale și SĂ ÎL RULEZI"
        " obligatoriu folosind uneltele puse la dispoziție pentru a te asigura"
        " că funcționează corect și nu conține bug-uri. Dacă terminalul îți"
        " returnează o eroare la rulare, analizeaz-o, corectează codul și"
        " rulează-l din nou. Nu te opri până când codul nu rulează cu succes și"
        " îndeplinește cerința utilizatorului. Răspunde în limba română la"
        " final."
    )

    # Inițializăm memoria de lucru (istoria conversației)
    istoric_conversatie = [
        {"role": "system", "content": prompt_sistem},
        {"role": "user", "content": cerinta_utilizator},
    ]

    print(f"🚀 Pornire agent pentru sarcina: '{cerinta_utilizator}'\n")

    iteratie = 1

    while iteratie <= max_iteratii:
        print("\n" + "=" * 60)
        print(f"📊 [Iterația {iteratie}/{max_iteratii}]")
        print(
            "📋 Dimensiune Istoric/Context actual:"
            f" {len(str(istoric_conversatie))} caractere"
        )
        print("=" * 60)

        print("\n🤖 Agentul trimite contextul și gândește...")

        # Măsurăm timpul în care procesorul din Proxmox calculează răspunsul
        timp_start_api = time.time()

        try:
            # Solicitare către serverul local cu un timeout masiv de 10 minute
            raspuns_llm = client.chat.completions.create(
                model="qwen3:8b",  # llama.cpp folosește implicit modelul deja încărcat
                messages=istoric_conversatie,
                tools=unelte_openai,
                tool_choice="auto",
                timeout=httpx.Timeout(600.0, connect=10.0),
                temperature=0.0,
            )
        except Exception as e:
            print(
                "\n❌ Eroare de comunicare sau Timeout după"
                f" {time.time() - timp_start_api:.2f} secunde!"
            )
            print(f"Detalii eroare: {e}")
            break

        timp_total_api = time.time() - timp_start_api
        print(
            f"⏱️ Răspuns primit de la server în {timp_total_api:.2f} secunde."
        )

        mesaj_agent = raspuns_llm.choices[0].message

        # --- SECȚIUNEA DIDACTIC REVEAL: Ce se ascunde în mintea LLM-ului ---
        print(
            "\n--- 🧠 [LOG INTRINSIC] Ce a generat „creierul” LLM în această"
            " fază: ---"
        )
        if mesaj_agent.content:
            print(f"📝 Text/Gândire: {mesaj_agent.content}")
        if mesaj_agent.tool_calls:
            print("🛠️ Intenție detectată! Modelul cere rularea unei unelte:")
            for tool_call in mesaj_agent.tool_calls:
                print(f"   -> Numele uneltei: {tool_call.function.name}")
                print(
                    "   -> Argumente brute primite:"
                    f" {tool_call.function.arguments}"
                )
        print("-" * 60)
        # ------------------------------------------------------------------

        # Salvăm răspunsul modelului în istoricul conversației
        istoric_conversatie.append(mesaj_agent)

        # Verificăm dacă modelul a decis să ruleze unelte
        if mesaj_agent.tool_calls:
            for tool_call in mesaj_agent.tool_calls:
                nume_functie = tool_call.function.name

                # Încercăm să parsăm argumentele venite ca text de la LLM în obiect JSON
                try:
                    argumente = json.loads(tool_call.function.arguments)
                except Exception as e:
                    print(
                        "❌ Modelul a generat un JSON invalid pentru"
                        f" argumente: {e}"
                    )
                    argumente = {}

                print(
                    "\n⚙️ Execut unealta locală în Python:"
                    f" '{nume_functie}'..."
                )
                timp_start_unealta = time.time()

                # Ruterul local de unelte
                if nume_functie == "scrie_cod_in_fisier":
                    rezultat_actiune = scrie_cod_in_fisier(
                        nume_fisier=argumente.get(
                            "nume_fisier", "script_eroare.py"
                        ),
                        cod=argumente.get("cod", ""),
                    )
                elif nume_functie == "ruleaza_script_python":
                    rezultat_actiune = ruleaza_script_python(
                        nume_fisier=argumente.get("nume_fisier", "")
                    )
                else:
                    rezultat_actiune = (
                        f"Eroare: Unealta '{nume_functie}' nu este definită în"
                        " sistem."
                    )

                timp_total_unealta = time.time() - timp_start_unealta
                print(rezultat_actiune)
                print(
                    "⏱️ Execuția uneltei în sistemul local a durat"
                    f" {timp_total_unealta:.4f} secunde."
                )

                # IMPORTANT: Trimitem feedback-ul înapoi în memorie cu rolul "tool"
                istoric_conversatie.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "name": nume_functie,
                    "content": rezultat_actiune,
                })
        else:
            # Dacă nu a cerut nicio unealtă, înseamnă că modelul consideră că a terminat treaba
            print(f"\n🤖 Răspunsul Final al Agentului:\n{mesaj_agent.content}")
            print("\n✅ Agentul a considerat sarcina finalizată cu succes!")
            break

        iteratie += 1


# ===========================================================================
# 4. LANSAREA SARCINII (CITIRE DE LA TASTATURĂ)
# ===========================================================================
if __name__ == "__main__":
    # Citire cerință
    cerinta = input("✍️ Introdu cerința pentru agent: ").strip()

    # Citire număr de iterații cu tratare de eroare (pentru siguranță în caz că introduci text)
    iteratii_input = input(
        "🔢 Introdu numărul maxim de iterații (implicit 5): "
    ).strip()

    if iteratii_input.isdigit():
        max_iter = int(iteratii_input)
    else:
        print("⚠️ Intrare nevalidă pentru iterații. Se folosește valoarea implicită (5).")
        max_iter = 5

    # Apelare agent
    if cerinta:
        ruleaza_agent_coder(cerinta, max_iter)
    else:
        print("❌ Nu ai introdus nicio cerință! Scriptul s-a oprit.")
🇪🇺 Notă de transparență (EU AI Act): Ca sa fie UE fericita, acest articol și exemplul de cod aferent au fost redactate și structurate cu asistența Inteligenței Artificiale.