File size: 4,552 Bytes
50efdc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""
src/graph_builder.py
LangGraph StateGraph ํŒฉํ† ๋ฆฌ โ€” ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ ์ •์˜ ๋ฐ ์ปดํŒŒ์ผ
"""
import uuid
import argparse

from src.logging_config import setup_logging
setup_logging()  # Must be called before any other src.* imports

from langgraph.graph import StateGraph, END
from langgraph.types import Send
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver

from src.config import get_config
from src.graph.state import AgentState
from src.graph.router import router_node
from src.graph.worker_a import worker_a_node
from src.graph.worker_b import worker_b_node
from src.graph.human_loop import human_loop_node
from src.graph.synthesizer import synthesizer_node
from src.preprocess import preprocess_email


def _auto_approve_node(state: AgentState) -> dict:
    """HITL ๋น„ํ™œ์„ฑ(ํ‰๊ฐ€ ์ „์šฉ) ๊ฒฝ๋กœ์—์„œ worker_a์˜ PENDING_APPROVAL์„ ์Šน์ธ๋จ(SUCCESS)์œผ๋กœ
    ํ‘œ์‹œํ•œ๋‹ค. ์šด์˜์˜ human_loop๊ณผ ๋‹ฌ๋ฆฌ DB(SQLite) UPDATE๋Š” ์ ˆ๋Œ€ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค โ€”
    '์Šน์ธ๋˜์—ˆ๋‹ค๊ณ  ๊ฐ€์ •'ํ–ˆ์„ ๋•Œ์˜ ๋‹ต์žฅ ํ’ˆ์งˆ๋งŒ ์ธก์ •ํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค.
    (golden_response๋„ PENDING_APPROVAL์„ 'completed successfully'๋กœ ๋ Œ๋”ํ•˜๋ฏ€๋กœ ์ •ํ•ฉ)."""
    if state.get("erp_action_status") == "PENDING_APPROVAL":
        return {
            "erp_action_status":       "SUCCESS",
            "requires_human_approval": False,
            "human_approved":          True,
        }
    return {}


def _build_state_graph(*, hitl: bool = True) -> StateGraph:
    """StateGraph ๊ตฌ์กฐ๋งŒ ๋นŒ๋“œ (์ฒดํฌํฌ์ธํ„ฐ ์—†์Œ) โ€” sync/async ๊ณต์šฉ.

    hitl=True  : worker_a โ†’ (PENDING_APPROVAL์ด๋ฉด) human_loop โ†’ synthesizer.
    hitl=False : ํ‰๊ฐ€ ์ „์šฉ โ€” worker_a โ†’ auto_approve(์Šน์ธ ๊ฐ€์ •, DB ๋ฏธ๋ณ€๊ฒฝ) โ†’ synthesizer.
    """
    builder = StateGraph(AgentState)

    builder.add_node("router", router_node)
    builder.add_node("worker_a", worker_a_node)
    builder.add_node("worker_b", worker_b_node)
    builder.add_node("synthesizer", synthesizer_node)

    if hitl:
        builder.add_node("human_loop", human_loop_node)
    else:
        builder.add_node("auto_approve", _auto_approve_node)

    builder.set_entry_point("router")

    def route_after_router(state: AgentState):
        intent = state["intent"]
        if intent == "ACTION_ONLY":
            return [Send("worker_a", state)]
        elif intent == "QA_ONLY":
            return [Send("worker_b", state)]
        else:  # BOTH
            return [Send("worker_a", state), Send("worker_b", state)]

    builder.add_conditional_edges("router", route_after_router, ["worker_a", "worker_b"])

    if hitl:
        _hitl_config_enabled = get_config().feature_flags.human_in_the_loop

        def route_after_worker_a(state: AgentState) -> str:
            if state.get("erp_action_status") == "PENDING_APPROVAL" and _hitl_config_enabled:
                return "human_loop"
            return "synthesizer"

        builder.add_conditional_edges("worker_a", route_after_worker_a)
        builder.add_edge("human_loop", "synthesizer")
    else:
        # ํ‰๊ฐ€ ์ „์šฉ: ์Šน์ธ์„ ๊ฐ€์ •(PENDING_APPROVALโ†’SUCCESS, DB ๋ฏธ๋ณ€๊ฒฝ)ํ•œ ๋’ค ํ•ฉ์„ฑ.
        builder.add_edge("worker_a", "auto_approve")
        builder.add_edge("auto_approve", "synthesizer")

    builder.add_edge("worker_b", "synthesizer")
    builder.add_edge("synthesizer", END)

    return builder


def build_graph():
    """SqliteSaver(sync)๋กœ ๊ทธ๋ž˜ํ”„ ๋นŒ๋“œ โ€” CLI / ํ…Œ์ŠคํŠธ ์ „์šฉ"""
    checkpoint_db = get_config().paths.checkpoint_db
    conn = sqlite3.connect(checkpoint_db, check_same_thread=False)
    memory = SqliteSaver(conn)
    return _build_state_graph().compile(checkpointer=memory)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="SAP ERP AI Agent")
    parser.add_argument("--input", required=True, help="์ด๋ฉ”์ผ ํ…์ŠคํŠธ ์ž…๋ ฅ")
    args = parser.parse_args()

    graph = build_graph()
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}

    # LangGraph ์ง„์ž… ์ „ ์ด๋ฉ”์ผ ์ „์ฒ˜๋ฆฌ โ€” ๋ฐœ์‹ ์ž/์ˆ˜์‹ ์ž/์š”์ฒญ ์‚ฌํ•ญ/์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐํ™”
    email_ctx = preprocess_email(args.input)

    result = graph.invoke(
        {
            "user_input":     args.input,
            "email_context":  email_ctx.model_dump(),
            "error_messages": [],
            "requires_human_approval": False,
            "human_approved": None,
        },
        config=config,
    )
    print("=== ์ตœ์ข… ์‘๋‹ต ===")
    print(result.get("final_response", "์‘๋‹ต ์—†์Œ"))