개발자 가이드

LangGraph 에이전트에 행위 전 검사 추가

LangGraph 실행 경로에는 도구 노드가 실행되기 전에 명시적인 결정 지점이 필요합니다.

검사가 하는 일

검사는 permit, deny, 또는 escalate 를 판단을 정한 조항 및 서명된 영수증과 함께 돌려줍니다. 검사는 조언형이며 실행을 거부하는 것은 그래프 경로입니다. 호스팅 검사는 요청을 메모리에서 평가하고 요청 내용을 보관하지 않습니다.

코드

다음 위치에 게시된 감사된 예시와 정확히 같습니다: public/examples/frameworks/.

"""LangGraph route that checks HANRIA before its execution node."""

from __future__ import annotations

import json
from typing import Any, TypedDict
from urllib.error import URLError
from urllib.request import Request, urlopen

from langgraph.graph import END, START, StateGraph

CHECK_URL = "https://check.hanria.ai/v1/check"
MANDATE = {
    "schema_version": "0.2-draft",
    "mandate_id": "read-text-files",
    "purpose": "Permit reads of text files under the example directory.",
    "default": "deny",
    "clauses": [
        {
            "id": "permit-example-read",
            "effect": "permit",
            "match": {
                "kind": ["file"],
                "verb": ["read"],
                "target_prefix": ["/tmp/example/"],
            },
        }
    ],
}


class ToolState(TypedDict, total=False):
    action: dict[str, Any]
    check_url: str
    decision: dict[str, Any]
    executed: bool
    result: str


def check_action(action: dict[str, Any], check_url: str = CHECK_URL) -> dict[str, Any]:
    """Return a HANRIA decision, mapping every transport or response failure to error."""
    body = json.dumps({"mandate": MANDATE, "action": action}).encode()
    request = Request(check_url, data=body, headers={"content-type": "application/json"})
    try:
        with urlopen(request, timeout=10) as response:
            decision = json.load(response)
    except (OSError, URLError, ValueError, json.JSONDecodeError) as error:
        return {"outcome": "error", "reason": f"HANRIA check failed: {error}"}
    if not isinstance(decision, dict) or decision.get("outcome") not in {
        "permit", "deny", "escalate", "error"
    }:
        return {"outcome": "error", "reason": "HANRIA returned an invalid decision"}
    return decision


def check_node(state: ToolState) -> ToolState:
    return {"decision": check_action(state["action"], state.get("check_url", CHECK_URL))}


def route_decision(state: ToolState) -> str:
    return "execute" if state["decision"].get("outcome") == "permit" else "refuse"


def execute_node(state: ToolState) -> ToolState:
    target = state["action"]["operation"]["target"]
    return {"executed": True, "result": f"tool would run for {target}"}


def refuse_node(state: ToolState) -> ToolState:
    decision = state["decision"]
    return {
        "executed": False,
        "result": f"tool refused: {decision.get('outcome', 'error')}: "
        f"{decision.get('reason', 'no reason returned')}",
    }


def build_graph():
    graph = StateGraph(ToolState)
    graph.add_node("check", check_node)
    graph.add_node("execute", execute_node)
    graph.add_node("refuse", refuse_node)
    graph.add_edge(START, "check")
    graph.add_conditional_edges("check", route_decision, {"execute": "execute", "refuse": "refuse"})
    graph.add_edge("execute", END)
    graph.add_edge("refuse", END)
    return graph.compile()


def example_action() -> dict[str, Any]:
    return {
        "schema_version": "0.1-draft",
        "requested_by": {"agent": "langgraph-example"},
        "operation": {"kind": "file", "verb": "read", "target": "/tmp/example/note.txt"},
        "justification": "Read the example note.",
    }


if __name__ == "__main__":
    graph = build_graph()
    print("Constructed HANRIA graph without invoking it.")

실행하기

langgraph.py 파일을 저장한 다음 해당 폴더에서 다음 명령을 실행하세요.

uv run --with langgraph==1.2.12 python langgraph.py

이 명령은 그래프를 구성하지만 호출하지는 않습니다. 애플리케이션에서 build_graph().invoke({"action": example_action()})를 호출합니다. 행위 딕셔너리는 그래프 상태의 "action" 키 아래에 둡니다. 검사 노드는 반드시 실행 노드 앞에 있어야 합니다.

모든 결과 처리하기

전송 실패, 잘못된 형식의 응답 또는 알 수 없는 결과는 error가 되므로 실행 노드를 선택할 수 없습니다.

참고 자료

검사 사용해 보기 · 서명된 영수증 검증하기 · OpenAPI 문서 · 에이전트용 요약