Pular para o conteúdo

Sequência de requisição

A sequência de interações que trata um único turno do usuário através de POST /chat. O handler FastAPI executa o turno no grafo LangGraph compilado; qual API de grafo ele usa depende da negociação de conteúdo. Uma requisição JSON simples (qualquer Accept que não seja text/event-stream) é executada via ainvoke e retorna um ChatResponse. Uma requisição que carrega Accept: text/event-stream é executada via astream e retorna um fluxo de server-sent-events com eventos de execução por nó; o Grafo de Execução do Agente na single-page app de demonstração consome esse fluxo. A variante de streaming é a segunda sequência abaixo. De qualquer forma, os guardrails não são uma camada que a API orquestra em torno do grafo - eles rodam como nós do grafo. guardrail_pre roda depois de intake; guardrail_post roda depois de generate_response. Uma decisão de pré-guardrail de falha é levada adiante no estado e faz curto-circuito na chamada ao LLM dentro de generate_response; o turno ainda flui por cada nó subsequente, então até um turno de recusa ou de sem correspondência volta como uma mensagem do assistente no estado final do grafo. Spans do OpenTelemetry são abertos em cada nó e ao redor da chamada ao LLM.

Consulte c4-container.md para a decomposição estática e c4-component.md para a visão de nós e módulos.

sequenceDiagram
  autonumber

  actor User
  participant API as FastAPI /chat
  participant Graph as LangGraph agent graph
  participant RAG as RAG store
  participant LLM as LLMClient
  participant Provider as LLM Provider

  User->>API: POST /chat {messages, locale, thread_id?}
  activate API
  API->>Graph: ainvoke(state, config={thread_id, model})
  activate Graph

  Note over Graph: node intake - mints trace id, increments turn

  Note over Graph: node guardrail_pre - input-validation,<br/>PII redaction, escalation, rule-based scope,<br/>optional judge scope. Failing decisions are<br/>carried on the state.

  alt clean turn and RAG enabled
    Graph->>RAG: retrieve_context: embed query, query(top_k)
    activate RAG
    RAG-->>Graph: top_k hits (top_k from RETRIEVAL_TOP_K)
    deactivate RAG
  else pre-guardrail failure present
    Note over Graph: conditional edge skips retrieve_context<br/>(rejected input is never embedded)
  end

  Note over Graph: node generate_response

  alt pre-guardrail failure or no-match
    Note over Graph: short-circuit, no LLM call:<br/>escalation template / refusal /<br/>no-context refusal
  else proceed to generation
    Graph->>LLM: complete(system + messages, params)
    activate LLM
    LLM->>Provider: HTTPS request
    Provider-->>LLM: completion
    LLM-->>Graph: completion (tokens_in, tokens_out, latency)
    deactivate LLM
    Note over Graph: extract + verify citations
  end

  Note over Graph: node guardrail_post - missing-citation check,<br/>persona-stability check (flag-only, never blocks)

  Note over Graph: node closing - finalises turn

  Graph-->>API: final state (messages, guardrail_decisions, cost)
  deactivate Graph
  API-->>User: 200 ChatResponse {content, citations,<br/>guardrail_decisions, cost, status=complete}
  deactivate API

Quando o grafo é compilado com HITL habilitado, um nó review_response fica entre generate_response e guardrail_post. Um rascunho de alto risco mas não agudo pausa o grafo via interrupt() do LangGraph; o turno é retomado por uma chamada separada POST /chat/resume.

sequenceDiagram
  autonumber

  actor User
  actor Reviewer as Human reviewer
  participant API as FastAPI
  participant Graph as LangGraph agent graph (HITL)
  participant Saver as Checkpointer

  User->>API: POST /chat {messages, locale}
  activate API
  API->>Graph: ainvoke(state, config={thread_id})
  activate Graph
  Note over Graph: intake -> guardrail_pre -><br/>[retrieve_context] -> generate_response
  Note over Graph: node review_response - assess_review_need(state)
  alt verdict = clear
    Note over Graph: no-op, flows to guardrail_post then closing
    Graph-->>API: final state (status=complete)
  else verdict = needs review
    Graph->>Saver: persist paused state
    Graph-->>API: __interrupt__ with ReviewInterruptPayload
  end
  deactivate Graph
  API-->>User: 200 {status=paused_for_review, review, content=draft}
  deactivate API

  Reviewer->>API: POST /chat/resume {thread_id, decision, edited_text?}
  activate API
  API->>Graph: aget_state then ainvoke(Command(resume=...))
  activate Graph
  Note over Graph: review_response re-enters, applies<br/>approve / edit / reject -> guardrail_post -> closing
  Graph-->>API: final state (status=complete, hitl-review decision)
  deactivate Graph
  API-->>Reviewer: 200 ChatResponse {content, guardrail_decisions}
  deactivate API

Quando a requisição pede text/event-stream, o handler conduz o mesmo grafo compilado através de astream em vez de ainvoke e mapeia cada evento por nó do LangGraph para um registro de server-sent-events. O fluxo abre com um evento graph_topology (para que a SPA desenhe o conjunto real de nós antes que qualquer nó rode), depois emite um par node_started / node_completed por nó executado e um node_completed skipped sintetizado por nó condicional genuinamente ignorado, e termina com um evento terminal turn_completed carregando o ChatResponse completo. Uma falha após o primeiro byte é um evento error dentro do fluxo; uma falha antes do primeiro byte é um erro HTTP normal. Consulte ADR-0009 para o schema de eventos.

sequenceDiagram
  autonumber

  actor Client as SSE client (demo SPA)
  participant API as FastAPI /chat
  participant Graph as LangGraph agent graph

  Client->>API: POST /chat (Accept: text/event-stream)
  activate API
  Note over API: content negotiation selects the SSE path;<br/>build the graph_topology payload before streaming
  API-->>Client: 200 text/event-stream (Cache-Control: no-cache,<br/>X-Accel-Buffering: no)
  API-->>Client: event: graph_topology (real node set + edges + flags)
  API->>Graph: astream(state, config={thread_id, model})
  activate Graph

  loop per executed node, in graph order
    Graph-->>API: node lifecycle event
    API-->>Client: event: node_started {node, run_id, ts_ms}
    Note over Graph: node body runs (guardrails / retrieval /<br/>generation), spans opened as in the JSON path
    Graph-->>API: node lifecycle event
    API-->>Client: event: node_completed {node, status=executed,<br/>duration_ms}
  end

  opt a conditional node was genuinely bypassed
    Note over API: diff topology vs nodes that emitted events
    API-->>Client: event: node_completed {status=skipped,<br/>duration_ms=0}
  end

  Graph-->>API: final state
  deactivate Graph
  API-->>Client: event: turn_completed { ...full ChatResponse... }
  deactivate API

Um turno HITL por streaming encerra seu fluxo de /chat com um evento paused (carregando o ReviewInterruptPayload) em vez de turn_completed, e fecha; o turno continua em um novo fluxo SSE POST /chat/resume que abre com seu próprio evento graph_topology, reemite os nós pós-pausa e termina com turn_completed carregando um human_wait_ms no nível do envelope.