tutorial tehnic · C7

LangGraph: workflow-uri cu stare, routere și agenți multipli

De la agent RAG individual la thread multi-agent controlat. LangGraph nu este un model AI — este un framework pentru controlul fluxului: stare, noduri, muchii, decizii, bucle, oprire. În C7 îl folosim pentru a construi un thread în care mai mulți agenți văd conversația anterioară și sunt coordonați de un router.

📓 C7_01_langgraph_multi_agent_thread.ipynb 📦 pip install langgraph 🔗 depinde de C6: core/agent.py ⏱ ~60 min
C6text politic → agent RAG → răspuns
C7state → router → agent_node → state update → router → … → END

01De ce avem nevoie de LangGraph

În C6 am construit un agent RAG individual. Un text politic intra în sistem, agentul recupera context din FAISS și producea un răspuns. Totul se întâmpla într-un singur apel — nu existau decizii intermediare, nici reluări, nici alți agenți în joc.

În C7 cerința se schimbă. Vrem mai mulți agenți care intervin într-o conversație: fiecare are rolul lui, fiecare vede ce s-a spus înainte, iar ordinea intervențiilor nu este fixă. Problema nu mai este cum generăm un răspuns, ci cum controlăm pașii: cine vorbește primul, cine urmează, când ne oprim, ce se păstrează între intervenții.

C6 text politic agent RAG răspuns flux liniar · un agent · fără decizii C7 stimulus router agent_node state update router → … → END mai mulți agenți · stare comună · ordine controlată de router
Fig. 1 — C6: un agent produce un răspuns. C7: un graf orchestrează mai mulți agenți, păstrează starea conversației și decide la fiecare pas.

Generarea de text rămâne treaba modelului (din C6). LangGraph adaugă controlul: stare partajată, decizii pe baza ei, repetare până la o condiție de oprire. Aceasta este diferența fundamentală față de C6.

02Ce este LangGraph

LangGraph este un framework pentru construirea de workflow-uri AI ca grafuri. Un graf are noduri, muchii și o stare comună. Fiecare nod este o funcție Python care primește starea curentă și returnează modificările aduse. Muchiile definesc cum trece fluxul de la un nod la altul — fie fix („mereu A → B"), fie pe baza unei decizii („dacă X, atunci A; altfel B").

Mesajul central. LangGraph nu face modelul mai inteligent. Face fluxul explicit, controlabil și inspectabil. La fiecare pas se vede ce stare a citit nodul, ce a modificat și ce decizie a luat. Pentru cercetare socială asta înseamnă auditabilitate completă — putem reconstrui pas cu pas cum s-a ajuns la o conversație simulată.

Trei consecințe practice ale acestei alegeri arhitecturale, pentru un proiect ca EchoChamber:

  • Reproducibilitate. Cu același state inițial și aceleași noduri, graful produce aceeași execuție. Conversațiile simulate pot fi rerulate și verificate.
  • Modularitate. Schimbi un nod, restul rămâne neatins. Înlocuiești routerul round-robin cu un router LLM — graful funcționează în continuare.
  • Inspecție. La fiecare pas poți printa starea. Nu este o cutie neagră — este un grafic Python pe care îl execuți pas cu pas.

03De la chain la graph

În cursurile anterioare am întâlnit două forme mai simple de a apela un model. LangGraph este forma a treia. Tabelul de mai jos arată când este suficientă fiecare.

Tip de fluxFormăCând este suficient
LLM callinput → LLM → outputrăspuns unic, fără context structurat
Chain liniarinput → pas 1 → pas 2 → outputpași ficși, ordine cunoscută dinainte
LangGraphstate → node → decision → node / ENDdecizii, bucle, agenți multipli, stare

Un chain este bun când ordinea pașilor este fixă și cunoscută dinainte. Un graph este necesar când sistemul trebuie să decidă la runtime ce se întâmplă mai departe — cine intervine, când se oprește, dacă reia un pas.

Concret în C7: nu putem ști dinainte ce va spune fiecare agent. Dar putem să spunem grafului cum să decidă cine vorbește la fiecare pas și când să se oprească. Aceste reguli devin parte din graf, nu din codul aplicației.

04Concepte fundamentale LangGraph

Înainte să construim primul graf, avem nevoie de un vocabular minim. Cinci concepte acoperă tot ce vom face în C7.

ConceptDefiniție simplăExemplu C7
Statedatele care circulă prin grafstimulus, messages, current_turn
Nodefuncție Python care modifică state-ulrouter_node, agent_node
Edgetrecere fixă între noduriagent_node → router
Conditional edgealegere pe baza state-uluirouter → agent sau router → END
START / ENDintrarea și ieșirea grafuluiSTART → router; router → END

State este memoria temporară a grafului. Toate nodurile citesc din ea și returnează ce modifică. Două noduri nu comunică direct între ele — comunicarea se face prin state. Această alegere face graful inspectabil: la orice moment, întreaga stare este vizibilă.

START node_A decision node_B node_C END
Fig. 2 — Cele cinci primitive într-un graf simplu. Săgețile pline = edges fixe. decision este nodul care implementează o muchie condițională, alegând între B și C.

05Primul graf — fără LLM

Începem fără LLM ca să separăm mecanica LangGraph de generarea textului. Exemplul: un graf care numără cuvintele dintr-un text. Niciun apel către un model, niciun API key.

first_graph.py — START → count_words → END
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

# 1. State — datele care circulă prin graf
class TextState(TypedDict):
    text:  str
    count: int

# 2. Node — funcție care primește state și returnează modificări
def count_words(state: TextState) -> dict:
    n = len(state["text"].split())
    return {"count": n}      # doar ce s-a schimbat

# 3. Construim graful
workflow = StateGraph(TextState)
workflow.add_node("count_words", count_words)
workflow.add_edge(START, "count_words")
workflow.add_edge("count_words", END)

# 4. Compilăm și rulăm
graph = workflow.compile()
result = graph.invoke({"text": "LangGraph orchestrează workflow-uri AI.", "count": 0})
print(result)
# → {'text': 'LangGraph orchestrează workflow-uri AI.', 'count': 4}

Patru pași, fiecare cu un rol clar: definim ce date circulă (State), ce face un pas (Node), cum se leagă pașii (edges), apoi compilăm și rulăm. Toate grafurile LangGraph respectă această structură, oricât de complexe ar deveni.

Notă despre return. Nodul returnează doar câmpurile pe care le modifică, nu state-ul întreg. LangGraph aplică automat actualizările peste state-ul existent. text rămâne neschimbat fiindcă nodul nu l-a returnat.

06Graf cu decizie

Acum adăugăm prima decizie. În funcție de lungimea textului, graful merge la short_response sau la long_response. Aceasta este o muchie condițională.

decision_graph.py — START → choose_path → short / long → END
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class LengthState(TypedDict):
    text:   str
    result: str

# Noduri — ramurile
def short_response(state: LengthState) -> dict:
    return {"result": "Text scurt."}

def long_response(state: LengthState) -> dict:
    return {"result": "Text lung."}

# Routerul — decide ramura, returnează numele etichetei
def choose_path(state: LengthState) -> str:
    if len(state["text"].split()) < 10:
        return "short"
    return "long"

# Construim graful
workflow = StateGraph(LengthState)
workflow.add_node("short_response", short_response)
workflow.add_node("long_response",  long_response)

# Muchie condițională cu mapping explicit etichetă → nod
workflow.add_conditional_edges(
    START,
    choose_path,
    {"short": "short_response", "long": "long_response"},
)
workflow.add_edge("short_response", END)
workflow.add_edge("long_response",  END)

graph = workflow.compile()
graph.invoke({"text": "Salut lume.", "result": ""})
# → {'text': 'Salut lume.', 'result': 'Text scurt.'}

Mapping-ul explicit contează. Dicționarul {"short": "short_response", "long": "long_response"} spune lui LangGraph: „dacă choose_path returnează 'short', mergi la nodul short_response". Fără mapping, vizualizarea grafului nu poate desena muchiile corect.

Muchia condițională este mecanismul prin care un graf decide. Tot restul C7 se construiește în jurul ei: routerul nostru este o funcție exact ca choose_path, doar că în loc de „short" / „long" returnează numele agentului care urmează să vorbească.

07Vizualizarea grafului

După compile(), LangGraph poate desena automat graful ca diagramă Mermaid. Este util pentru a verifica vizual că workflow-ul este construit corect — mai ales când avem multe noduri și muchii condiționate.

viz.py — desenare automată în Jupyter
from IPython.display import Image, display

# Imaginea PNG generată din graf (necesită conexiune pentru rendering)
display(Image(graph.get_graph().draw_mermaid_png()))

# Fallback: textul Mermaid brut, dacă rendering-ul nu merge
print(graph.get_graph().draw_mermaid())

Vizualizarea ajută să verificăm dacă workflow-ul este construit corect, înainte să-l rulăm. Dacă o muchie condițională nu apare în diagramă, înseamnă că mapping-ul lipsește sau este greșit. Cel mai bun debug este vizual.

Mai jos, varianta textuală Mermaid generată din graful cu decizie. Aceasta este reprezentarea pe care o desenează draw_mermaid_png():

graph.draw_mermaid() — output text
---
config:
  flowchart:
    curve: linear
---
graph TD;
        __start__([<p>__start__</p>]):::first
        short_response(short_response)
        long_response(long_response)
        __end__([<p>__end__</p>]):::last
        __start__ -.-> short_response;
        __start__ -.-> long_response;
        short_response --> __end__;
        long_response --> __end__;

08Ce este un thread conversațional

În C7 nu vrem un singur răspuns — vrem o conversație. O conversație, din punct de vedere al codului, este o listă de mesaje păstrată în State. Fiecare intervenție adaugă un mesaj la listă. Routerul citește lista ca să decidă cine urmează.

Un mesaj are o structură minimă:

CâmpRol
agentnumele afișabil al agentului (ex. „Anti-sistem")
slugidentificatorul unic, folosit de router (ex. anti_sistem)
handlehandle-ul stilizat ca pe rețele sociale (ex. @LibertateRO99)
textconținutul intervenției, generat de agentul RAG
turnnumărul de ordine al intervenției în thread
thread_example.py — exemplu manual de thread
HANDLES = {
    "anti_sistem":     "@LibertateRO99",
    "conspirationist": "@AdevarulViu",
    "pro_european":    "@EuroOptimistRO",
}

messages = [
    {"agent": "Anti-sistem",  "slug": "anti_sistem",
     "text":  "Instituțiile par din nou rupte de oameni.", "turn": 1},
    {"agent": "Pro-european", "slug": "pro_european",
     "text":  "Trebuie să discutăm pe baza procedurilor și a dovezilor.", "turn": 2},
]

def thread_to_text(messages: list) -> str:
    lines = []
    for m in messages:
        handle = HANDLES.get(m["slug"], m["slug"])
        lines.append(f"Turn {m['turn']} — {handle}: {m['text']}")
    return "\n".join(lines)

print(thread_to_text(messages))
# Turn 1 — @LibertateRO99: Instituțiile par din nou rupte de oameni.
# Turn 2 — @EuroOptimistRO: Trebuie să discutăm pe baza procedurilor...

Pentru moment, mesajele sunt construite manual. Mai târziu, fiecare agent își va genera mesajul prin generate_agent_response() din C6. Funcția thread_to_text() este utilă atât pentru afișare, cât și pentru a trimite thread-ul anterior către un agent care urmează să răspundă.

09ThreadState — structura de date a conversației

ThreadState este State-ul specific lui C7. Toate câmpurile sunt simple: șiruri, liste, întregi. Fiecare nod citește din ThreadState și actualizează câmpurile relevante.

thread_state.py
from typing import TypedDict

class ThreadState(TypedDict):
    stimulus:     str        # inputul politic inițial
    messages:     list       # mesajele produse până acum
    active_slugs: list       # agenții care participă la thread
    total_turns:  int        # câte intervenții vrem în total
    current_turn: int        # câte au fost deja produse
    next_slug:    str        # agentul ales pentru intervenția următoare
    provider:     str        # "gemini" sau "deepseek"
    k:            int        # câte fragmente FAISS recuperăm
CâmpRolCine îl modifică
stimulustextul politic — fix pe toată durata thread-uluinimeni (inițializat)
messageslista de mesaje a conversațieiagent_node
active_slugsagenții participanți, în ordinea round-robinnimeni (inițializat)
total_turnsbugetul de intervențiinimeni (inițializat)
current_turncontorul de intervenții — citit de routeragent_node
next_slugslug-ul agentului ales pentru tura curentărouter_node
providerfurnizorul LLMnimeni (inițializat)
kparametrul top-k pentru retrievernimeni (inițializat)

Diviziunea muncii. Routerul setează doar next_slug. Agent node-ul adaugă la messages și incrementează current_turn. Restul câmpurilor rămân neschimbate pe tot parcursul thread-ului. Această separare clară face debugging-ul ușor.

10Router simplu — round-robin

Routerul este inima sistemului multi-agent. El decide la fiecare pas cine vorbește și când se oprește thread-ul. Pentru C7 obligatoriu folosim round-robin — agenții vorbesc pe rând, în ordinea din active_slugs. Este simplu, stabil și explicabil.

router_node pick_next_round_robin() state.current_turn END current_turn ≥ total_turns agentul următor altfel
Fig. 3 — Routerul decide pe baza unei singure verificări: a fost atins bugetul de ture? Dacă da, END. Dacă nu, alege agentul următor din rotație.
router.py — round-robin minimal
def pick_next_round_robin(active_slugs: list, current_turn: int) -> str:
    """Alege agentul următor: tura N → al N-lea agent (modulo nr. agenți)."""
    return active_slugs[current_turn % len(active_slugs)]

def router_node(state: ThreadState) -> dict:
    """Setează next_slug în state, sau '__end__' dacă thread-ul s-a terminat."""
    if state["current_turn"] >= state["total_turns"]:
        return {"next_slug": "__end__"}

    next_slug = pick_next_round_robin(
        state["active_slugs"],
        state["current_turn"],
    )
    return {"next_slug": next_slug}

def route_decision(state: ThreadState) -> str:
    """Muchia condițională: citește next_slug și-l returnează ca etichetă."""
    return state["next_slug"]

Trei funcții, trei roluri. pick_next_round_robin este pură — primește două argumente, returnează un slug. Testabilă izolat. router_node aplică logica pe state. route_decision este folosită ca conditional edge și citește doar next_slug. Separarea face routerul ușor de înlocuit (vezi secțiunea 15).

11Agent node — apelul către agentul C6

Routerul decide cine vorbește. Agent node-ul decide ce spune — printr-un apel către agentul RAG construit în C6. Nu rescriem nimic din C6: doar îl chemăm cu un input extins care include și thread-ul anterior.

Router

cine vorbește

Citește current_turn și active_slugs. Setează next_slug. Nu apelează LLM-ul.

Agent node

ce spune agentul

Citește stimulus și messages. Apelează generate_agent_response(). Returnează mesajul nou și incrementează current_turn.

agent_node.py — factory pentru noduri specializate
from core.agent import generate_agent_response

def make_agent_node(slug: str):
    """Factory: returnează un nod specializat pentru agentul `slug`."""

    def agent_node(state: ThreadState) -> dict:
        thread_ctx = thread_to_text(state["messages"])

        # input extins: stimulusul + thread-ul anterior
        extended_input = f"""[STIMULUS]
{state['stimulus']}

[THREAD ANTERIOR]
{thread_ctx if thread_ctx else "Prima intervenție."}

[SARCINĂ]
Răspunde ca agentul tău. Dacă există mesaje anterioare, reacționează la ele."""

        # apel către agentul RAG din C6
        result = generate_agent_response(
            agent_slug=slug,
            input_text=extended_input,
            provider=state["provider"],
            k=state["k"],
        )

        new_turn = state["current_turn"] + 1
        new_msg = {
            "agent": result["agent_name"],
            "slug":  slug,
            "text":  result["response"],
            "turn":  new_turn,
        }

        # întoarcem messages cu mesajul nou adăugat la sfârșit
        return {
            "messages":     state["messages"] + [new_msg],
            "current_turn": new_turn,
        }

    return agent_node

De ce factory. Avem 3-5 agenți. Un nod per agent. make_agent_node("anti_sistem") produce un nod care apelează agentul anti-sistem. make_agent_node("pro_european") produce alt nod. Alternativa — un singur nod care primește slug-ul prin state — ar amesteca routarea cu execuția și ar face graful mai greu de citit.

12Graful multi-agent complet

Conectăm router_node, route_decision și nodurile produse de make_agent_node într-un graf canonic. Fiecare agent se întoarce la router după ce a vorbit — pentru că routerul este singurul care știe dacă thread-ul continuă sau s-a oprit.

build_graph.py — graful multi-agent
from langgraph.graph import StateGraph, START, END

def build_graph(active_slugs: list[str]):
    workflow = StateGraph(ThreadState)

    # nodul router
    workflow.add_node("router", router_node)

    # câte un nod pentru fiecare agent activ
    for slug in active_slugs:
        workflow.add_node(slug, make_agent_node(slug))
        workflow.add_edge(slug, "router")     # agent → router (întotdeauna)

    # intrare
    workflow.add_edge(START, "router")

    # muchia condițională: router → un agent sau END
    mapping = {slug: slug for slug in active_slugs} | {"__end__": END}
    workflow.add_conditional_edges("router", route_decision, mapping)

    return workflow.compile()
START router pick_next() anti_sistem conspirationist pro_european END agent → router
Fig. 4 — Graful complet C7. Săgețile galbene: router → agent (muchie condițională, decisă la runtime). Săgețile gri întrerupte: agent → router (muchie fixă). Routerul este nodul central.

Topologia este aceeași indiferent de câți agenți avem — adăugarea unui al patrulea sau al cincilea agent înseamnă doar adăugarea unui nou nod și a unui mapping nou. Graful crește în lățime, nu în complexitate.

13Rulăm primul thread multi-agent

Compilăm graful și îl invocăm cu un state inițial. LangGraph se ocupă de restul: rulează routerul, decide următorul agent, îl rulează, se întoarce la router, repetă până când contorul ajunge la total_turns.

run_thread.py
graph = build_graph(["anti_sistem", "conspirationist", "pro_european"])

initial_state = {
    "stimulus":     "CCR a decis anularea alegerilor după suspiciuni privind influențe externe.",
    "messages":     [],
    "active_slugs": ["anti_sistem", "conspirationist", "pro_european"],
    "total_turns":  4,
    "current_turn": 0,
    "next_slug":    "",
    "provider":     "gemini",
    "k":            3,
}

result = graph.invoke(initial_state)
for m in result["messages"]:
    print(f"Turn {m['turn']} — {m['agent']}: {m['text']}")

Inputul este același pentru toți studenții, ca să putem compara conversațiile produse. Recomandat: „CCR a decis anularea alegerilor după suspiciuni privind influențe externe.". Trei agenți, patru ture, provider Gemini sau DeepSeek.

14Afișarea thread-ului

După graph.invoke(), result["messages"] conține lista de mesaje produse. O afișăm ca thread cu carduri colorate per agent — același stil pe care îl folosește aplicația finală. Tabelul Pandas nu este vizualizarea principală: thread-ul are sens ca succesiune de intervenții, nu ca rânduri într-o tabelă.

TURN 1 · @LIBERTATERO99 · ANTI-SISTEM
Instituțiile par din nou rupte de oameni. O decizie de o asemenea gravitate, luată fără explicații clare, întărește sentimentul că sistemul nu mai răspunde nimănui.
TURN 2 · @ADEVARULVIU · CONSPIRAȚIONIST
„Influențe externe" — exact așa numesc ei mereu mâinile pe care nu vor să le numească. Cine câștigă din anularea asta? Acolo este răspunsul, nu în comunicatele oficiale.
TURN 3 · @EUROOPTIMISTRO · PRO-EUROPEAN
CCR a aplicat o procedură constituțională. Putem critica decizia, dar haideți să nu confundăm controlul de constituționalitate cu o conspirație. Dovezile contează mai mult decât bănuielile.
TURN 4 · @LIBERTATERO99 · ANTI-SISTEM
Procedurile au existat și atunci când instituțiile au eșuat. Forma nu este o garanție de fond — cetățenii vor răspunsuri, nu lecții constituționale.

Patru intervenții, ordine A → B → C → A (round-robin), fiecare agent vede thread-ul anterior și reacționează. Textele de mai sus sunt reprezentative — outputul concret depinde de modelul ales și de fragmentele FAISS recuperate pentru fiecare rol.

Mini-task · interpretare

Rulează thread-ul cu același stimulus, dar inversează ordinea din active_slugs. Compară rezultatele: prima intervenție stabilește cadrul interpretativ pentru tot ce urmează?

15Router LLM — extensie opțională

Round-robin este robust, dar mecanic — agenții vorbesc în ordine fixă indiferent de ce s-a spus. Pentru conversații mai dinamice, putem înlocui routerul cu un apel LLM care alege agentul potrivit pe baza thread-ului. Aceasta este o extensie, nu o cerință obligatorie C7.

RouterAvantajRisc
round-robinstabil, explicabil, reproducibilpoate părea mecanic în conversații lungi
LLM routermai flexibil, contextualpoate alege repetat același agent · cost LLM la fiecare tură

Regulă obligatorie pentru router LLM. Nu alege același agent de două ori la rând. Fără această constrângere, modelul tinde să continue cu agentul curent („pentru că tocmai a vorbit despre subiectul potrivit") — și conversația devine monolog.

router_llm.py — extensie opțională
from core.agent import make_llm

def router_node_llm(state: ThreadState) -> dict:
    if state["current_turn"] >= state["total_turns"]:
        return {"next_slug": "__end__"}

    active = state["active_slugs"]
    if not state["messages"]:
        return {"next_slug": active[0]}

    # constrângerea hard: nu alege agentul care tocmai a vorbit
    last_slug  = state["messages"][-1]["slug"]
    candidates = [s for s in active if s != last_slug] or active

    prompt = f"""[STIMULUS]
{state["stimulus"]}

[THREAD]
{thread_to_text(state["messages"])}

[Agenți disponibili]
{chr(10).join(f"- {s}" for s in candidates)}

Alege agentul care ar trebui să răspundă următorul.
Răspunde DOAR cu slug-ul exact."""

    llm    = make_llm(provider=state["provider"], temperature=0.1)
    chosen = llm.invoke(prompt).content.strip().lower()
    return {"next_slug": chosen if chosen in candidates else candidates[0]}

Restul grafului rămâne identic. Schimbi un singur nod — router_noderouter_node_llm — și toți agenții continuă să funcționeze. Aceasta este modularitatea LangGraph: politica de routing este o componentă interschimbabilă.

16De la notebook la core/graph.py

Notebook-ul este pentru învățare și explorare. Aplicația are nevoie de funcții reutilizabile, importabile din alte module. Mutăm logica de mai sus într-un singur fișier core/graph.py cu un API minimal.

core/graph.py — API public
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from core.agent import generate_agent_response

# --- State ---------------------------------------------------------
class ThreadState(TypedDict):
    stimulus: str; messages: list; active_slugs: list
    total_turns: int; current_turn: int; next_slug: str
    provider: str; k: int

# --- Helpers -------------------------------------------------------
def thread_to_text(messages: list) -> str: ...

# --- Router --------------------------------------------------------
def router_node(state):    ...
def route_decision(state): return state["next_slug"]

# --- Agent node factory --------------------------------------------
def make_agent_node(slug: str): ...

# --- Builder + runner ---------------------------------------------
def build_graph(active_slugs): ...

def run_thread(stimulus: str, active_slugs: list,
               total_turns: int = 4, provider: str = "gemini",
               k: int = 3) -> dict:
    graph = build_graph(active_slugs)
    return graph.invoke({
        "stimulus": stimulus, "messages": [],
        "active_slugs": active_slugs, "total_turns": total_turns,
        "current_turn": 0, "next_slug": "",
        "provider": provider, "k": k,
    })

Test rapid din terminal — fiecare membru al echipei rulează această comandă ca să verifice că core/graph.py funcționează izolat:

test terminal — core/graph.py funcționează
python -m core.graph \
  --agents anti_sistem conspirationist pro_european \
  --text "CCR a decis anularea alegerilor după suspiciuni privind influențe externe." \
  --turns 4 \
  --provider gemini

17Integrare minimă în aplicație

app.py nu trebuie să cunoască LangGraph. Singurul lucru pe care îl face este să apeleze run_thread() și să afișeze mesajele. Aceasta este toată integrarea pentru C7.

stimulus + agenți + runde run_thread() messages afișare thread
Fig. 5 — Integrarea în app. Tot ce face aplicația este să colecteze 3 inputuri, să cheme o funcție și să afișeze rezultatul.
app/app.py — tab multi-agent minimal
from core.graph import run_thread

# În tab-ul „Multi-agent thread"
def on_run_click(stimulus, active_slugs, total_turns, provider):
    result = run_thread(
        stimulus=stimulus,
        active_slugs=active_slugs,
        total_turns=total_turns,
        provider=provider,
    )
    return result["messages"]   # UI le afișează ca thread cu carduri

Principiu de design. Aplicația este o interfață peste run_thread(). Toată logica LangGraph rămâne în core/graph.py. Dacă schimbi routerul sau topologia grafului, app.py nu se modifică deloc.

18Etică și limite

Agenții nu sunt persoane reale. Răspunsurile generate nu sunt opinii autentice ale vreunei grupări politice. Sistemul simulează poziții discursive construite din corpus — atât tehnic, cât și editorial. Orice prezentare publică a output-ului trebuie să clarifice această distincție.

DimensiuneRiscControl minim
Transparențăagenții pot părea persoane realedisclaimer explicit: agenți simulați
Factualitaterăspunsurile pot părea adevăr verificatseparăm răspunsul de contextul RAG în UI
Datecorpusul poate conține date personaleminimizare și curățare la C3
Amplificareconversația poate escalada conflictullimită de runde + review uman
Reprezentarerolurile pot fi confundate cu grupuri realeetichetăm „poziții discursive construite"
Responsabilitateoutputul poate fi reutilizat greșitdocs/ethics_and_limits.md + README
Mini-task · disclaimer

Scrie un disclaimer de 3–4 fraze pentru aplicația EchoChamber. Include: (1) ce este sistemul, (2) ce nu este, (3) pe ce date se bazează, (4) cum trebuie interpretat output-ul. Salvează în docs/ethics_and_limits.md.

19Seminar C7 — EchoChamber

Toată mecanica LangGraph de mai sus se concretizează în seminar într-o singură sesiune practică. La final, echipa are un thread multi-agent funcțional integrat în aplicație.

Ce construim concret

  1. Notebook

    notebooks/student_XX/C7_01_langgraph_multi_agent_thread.ipynb — parcurgem mecanica LangGraph, construim graful, rulăm un thread, afișăm rezultatul cu cardurile colorate per agent.

  2. Backend reutilizabil

    core/graph.py cu API-ul minimal: ThreadState, router_node, make_agent_node, build_graph, run_thread. Importabil din app.py.

  3. Tab multi-agent în aplicație

    Tab nou minimal: utilizatorul alege stimulusul, agenții și numărul de ture; apasă „Run"; thread-ul apare ca succesiune de carduri.

  4. Disclaimer și etică

    docs/ethics_and_limits.md — scriem o pagină scurtă cu disclaimer-ul și limitele sistemului. Aceasta este obligatorie înainte de orice demo public.

Cum se leagă de C6

C7 nu rescrie C6. LangGraph orchestrează agentul C6. Funcția generate_agent_response() din core/agent.py rămâne neatinsă; o chemăm dintr-un nod LangGraph. Dacă agentul C6 nu funcționează izolat, graful C7 nu are ce să controleze.

Test de validare înainte de a începe. python -m core.agent --agent anti_sistem --text "..." --provider gemini trebuie să returneze un răspuns ancorat în corpus. Dacă nu merge, repară C6 înainte de a continua cu C7.

20Livrabile C7

Individual

Notebook + interpretare

notebooks/student_XX/C7_01_langgraph_multi_agent_thread.ipynb

· 1 thread multi-agent rulat
· 3 agenți, 4 ture, același stimulus
· mini-interpretare (3–5 fraze) despre dinamica conversației

Echipă

Backend + app + docs

· core/graph.py cu run_thread() funcțional
· app/app.py cu tab multi-agent minimal
· docs/ethics_and_limits.md
· README.md actualizat

Criteriu de succes. La final, oricine descarcă repo-ul poate rula aplicația local, alege 3 agenți, pune un stimulus și obține un thread multi-agent în UI. Atât. Nu cerem dashboard, metrici complexe sau evaluare automată.

21Team work session — 60 de minute

Scop. Stabilizăm aplicația și pregătim trecerea de la Agent RAG simplu (C6) la thread multi-agent (C7). La final, echipa trebuie să poată rula aplicația local și să genereze un thread funcțional pentru cel puțin un grup de agenți.

Regula principală. Nu lucrează doi membri pe același fișier comun în același timp. Pentru fiecare fișier comun, un singur membru este responsabil.

Împărțirea rolurilor

Membru A

Roles lead

assets/roles/roles.yaml · role_XX.yaml

Verifică structura YAML, prezența câmpurilor slug, name, emoji, color, system. Rulează testul de încărcare.

Membru B

Retrieval lead

core/retriever.py · vectorstores/

Verifică existența index.faiss și index.pkl pentru agentul testat. Rulează python -m core.retriever ca test terminal.

Membru C

Agent backend lead

core/agent.py

Verifică generate_agent_response(). Rulează python -m core.agent --agent anti_sistem --text "..." --provider gemini.

Membru D

LangGraph / workflow lead

core/graph.py · notebook C7

Construiește sau verifică core/graph.py. Rulează python -m core.graph cu agenții, textul și provider-ul. Output așteptat: lista de mesaje.

Membru E

App lead

app/app.py

Pornește python app/app.py. Verifică tab-urile existente. Adaugă tab multi-agent care apelează run_thread().

Membru F

GitHub / issue lead

issues · commits · README

Verifică issues C6 deschise. Coordonează commit-urile pe fișierele comune. Nu modifică fișierele dacă alt membru lucrează pe ele.

Ordinea de verificare

Echipa nu începe cu aplicația. Ordinea este: roles → retriever → agent → graph → app → GitHub. Dacă un pas nu merge, nu treceți la următorul.

test comun pentru toată echipa
# Toate testele folosesc același input
INPUT="CCR a decis anularea alegerilor după suspiciuni privind influențe externe."
AGENT="anti_sistem"   # sau agentul pentru care echipa are sigur vectorstore

python -m core.retriever --agent $AGENT --query "$INPUT" --k 5
python -m core.agent     --agent $AGENT --text "$INPUT" --provider gemini
python -m core.graph     --agents anti_sistem conspirationist pro_european \
                          --text "$INPUT" --turns 4 --provider gemini

Ce NU facem azi

Skip

UI redesign

Tab-ul multi-agent este minimal. Restul interfeței rămâne neatinsă.

Skip

Dashboard, metrici complexe

Fără grafice, fără statistici. Doar thread-ul afișat ca succesiune de carduri.

Skip

RAGAS

Evaluarea automată RAG nu intră în C7. Rămâne demo profesor sau C8+.

Skip

n8n, MCP

Automatizări externe — nu fac parte din scopul C7. Demo separat dacă rămâne timp.

Skip

Scraping web

Stimulusul vine manual. RSS-ul rămâne pentru extensii viitoare.

Skip

Multi-agent debate complet

Round-robin cu 3 agenți, 4 ture, atât. Conversații lungi rămân pentru C8+.

Raport scurt la final

comentariu pe issue-ul C7
DONE C7 team work session

roles.yaml works:           yes / no
retriever works:            yes / no
agent.py works:             yes / no
graph.py / notebook works:  yes / no
app multi-agent tab works:  yes / no
ethics_and_limits.md:       yes / no

Observații:
- [orice problemă rămasă deschisă]
- [ce trebuie reluat în sesiunea următoare]

Resurse

Concluzie. LangGraph nu face modelul mai inteligent. Face fluxul explicit, controlabil și inspectabil. În C6 am produs răspunsuri individuale. În C7 am construit un thread multi-agent — agenții au roluri, văd conversația, sunt coordonați de un router. Aceasta este fundația pentru aplicația finală din C8.