Skip to content

Latest commit

Β 

History

History
1181 lines (1026 loc) Β· 32.4 KB

File metadata and controls

1181 lines (1026 loc) Β· 32.4 KB

PlateERAG Backend Node Editor μ‹œμŠ€ν…œ κ°€μ΄λ“œ

πŸ“– κ°œμš”

PlateERAG Backend의 Node EditorλŠ” μ‹œκ°μ  μ›Œν¬ν”Œλ‘œμš° νŽΈμ§‘κΈ°λ₯Ό μœ„ν•œ λ°±μ—”λ“œ μ‹œμŠ€ν…œμž…λ‹ˆλ‹€. 각 λ…Έλ“œλŠ” νŠΉμ • κΈ°λŠ₯을 μˆ˜ν–‰ν•˜λŠ” 독립적인 μ‹€ν–‰ λ‹¨μœ„μ΄λ©°, 이듀을 μ—°κ²°ν•˜μ—¬ λ³΅μž‘ν•œ 데이터 처리 μ›Œν¬ν”Œλ‘œμš°λ₯Ό ꡬ성할 수 μžˆμŠ΅λ‹ˆλ‹€.

🎯 핡심 νŠΉμ§•

  • λ…Έλ“œ 기반 μ•„ν‚€ν…μ²˜: 각 λ…Έλ“œλŠ” ν•˜λ‚˜μ˜ κΈ°λŠ₯을 λ‹΄λ‹Ήν•˜λŠ” 독립적인 μ‹€ν–‰ λ‹¨μœ„
  • μžλ™ λ…Έλ“œ 발견: nodes/ ν΄λ”μ˜ λ…Έλ“œλ“€μ„ μžλ™μœΌλ‘œ νƒμ§€ν•˜κ³  등둝
  • νƒ€μž… μ‹œμŠ€ν…œ: κ°•λ ₯ν•œ νƒ€μž… 검증 및 포트 μ‹œμŠ€ν…œ
  • μ›Œν¬ν”Œλ‘œμš° μ‹€ν–‰: μœ„μƒ 정렬을 ν†΅ν•œ 효율적인 λ…Έλ“œ μ‹€ν–‰
  • ν™•μž₯μ„±: μƒˆλ‘œμš΄ λ…Έλ“œλ₯Ό μ‰½κ²Œ μΆ”κ°€ν•  수 μžˆλŠ” ν”ŒλŸ¬κ·ΈμΈ μ•„ν‚€ν…μ²˜
  • μΉ΄ν…Œκ³ λ¦¬ μ‹œμŠ€ν…œ: λ…Έλ“œλ₯Ό κΈ°λŠ₯λ³„λ‘œ λΆ„λ₯˜ν•˜μ—¬ 관리
  • λ§€κ°œλ³€μˆ˜ 검증: λŸ°νƒ€μž„ 전에 λ…Έλ“œ λ§€κ°œλ³€μˆ˜ μœ νš¨μ„± 검증
  • JSON μŠ€νŽ™ 생성: ν”„λ‘ νŠΈμ—”λ“œλ₯Ό μœ„ν•œ λ…Έλ“œ μŠ€νŽ™ μžλ™ 생성

πŸ—οΈ Node Editor μ•„ν‚€ν…μ²˜

폴더 ꡬ쑰

editor/
β”œβ”€β”€ README.md                    # πŸ“– 이 λ¬Έμ„œ
β”œβ”€β”€ __init__.py                  # πŸ”§ 에디터 νŒ¨ν‚€μ§€ μ΄ˆκΈ°ν™”
β”œβ”€β”€ model/                       # πŸ“‹ 데이터 λͺ¨λΈ
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── node.py                  # πŸ”— λ…Έλ“œ νƒ€μž… μ •μ˜ 및 검증
β”œβ”€β”€ node_composer.py             # 🎼 λ…Έλ“œ 탐지 및 등둝 μ‹œμŠ€ν…œ
β”œβ”€β”€ workflow_executor.py         # πŸ”„ μ›Œν¬ν”Œλ‘œμš° μ‹€ν–‰ μ—”μ§„
└── nodes/                       # πŸ“‚ λ…Έλ“œ κ΅¬ν˜„ 폴더
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ chat/                    # πŸ’¬ μ±„νŒ… λͺ¨λΈ λ…Έλ“œ
    β”‚   β”œβ”€β”€ __init__.py
    β”‚   └── chat_openai.py       # πŸ€– OpenAI μ±„νŒ… λ…Έλ“œ
    β”œβ”€β”€ math/                    # πŸ”’ μˆ˜ν•™ μ—°μ‚° λ…Έλ“œ
    β”‚   β”œβ”€β”€ __init__.py
    β”‚   β”œβ”€β”€ math_add.py          # βž• λ§μ…ˆ λ…Έλ“œ
    β”‚   β”œβ”€β”€ math_multiply.py     # βœ–οΈ κ³±μ…ˆ λ…Έλ“œ
    β”‚   └── math_subtract.py     # βž– λΊ„μ…ˆ λ…Έλ“œ
    └── tool/                    # πŸ”§ μœ ν‹Έλ¦¬ν‹° λ…Έλ“œ
        β”œβ”€β”€ __init__.py
        β”œβ”€β”€ input_int.py         # πŸ“₯ μ •μˆ˜ μž…λ ₯ λ…Έλ“œ
        β”œβ”€β”€ input_str.py         # πŸ“₯ λ¬Έμžμ—΄ μž…λ ₯ λ…Έλ“œ
        β”œβ”€β”€ print_any.py         # πŸ“€ 좜λ ₯ λ…Έλ“œ
        └── test_validation.py   # πŸ§ͺ 검증 ν…ŒμŠ€νŠΈ λ…Έλ“œ

μ•„ν‚€ν…μ²˜ κ΅¬μ„±μš”μ†Œ

πŸ—‚οΈ μΉ΄ν…Œκ³ λ¦¬ 및 κΈ°λŠ₯ μ‹œμŠ€ν…œ

πŸ“‹ μΉ΄ν…Œκ³ λ¦¬ λͺ©λ‘

ν˜„μž¬ μ§€μ›ν•˜λŠ” μΉ΄ν…Œκ³ λ¦¬λ“€:

μΉ΄ν…Œκ³ λ¦¬ ID μΉ΄ν…Œκ³ λ¦¬ 이름 μ•„μ΄μ½˜ μ„€λͺ…
langchain LangChain SiLangchain LangChain 라이브러리 기반 λ…Έλ“œ
polar POLAR POLAR POLAR μ‹œμŠ€ν…œ μ „μš© λ…Έλ“œ
utilities Utilities LuWrench μœ ν‹Έλ¦¬ν‹° 및 도ꡬ λ…Έλ“œ
math Math LuWrench μˆ˜ν•™ μ—°μ‚° λ…Έλ“œ

πŸ”§ κΈ°λŠ₯ λͺ©λ‘

각 μΉ΄ν…Œκ³ λ¦¬ λ‚΄μ—μ„œ μ‚¬μš©ν•  수 μžˆλŠ” κΈ°λŠ₯λ“€:

κΈ°λŠ₯ ID κΈ°λŠ₯ 이름 μ„€λͺ…
agents Agent LangChain μ—μ΄μ „νŠΈ
cache Cache 캐싱 μ‹œμŠ€ν…œ
chain Chain LangChain 체인
chat_models Chat Model μ±„νŒ… λͺ¨λΈ
document_loaders Document Loader λ¬Έμ„œ λ‘œλ”
embeddings Embedding μž„λ² λ”© λͺ¨λΈ
graph Graph κ·Έλž˜ν”„ 처리
memory Memory λ©”λͺ¨λ¦¬ μ‹œμŠ€ν…œ
moderation Moderation μ½˜ν…μΈ  κ²€μ—΄
output_parsers Output Parser 좜λ ₯ νŒŒμ„œ
tools Tool 도ꡬ λ…Έλ“œ
arithmetic Arithmetic μˆ˜ν•™ μ—°μ‚°
endnode End Node μ’…λ£Œ λ…Έλ“œ
startnode Start Node μ‹œμž‘ λ…Έλ“œ

🎯 λ…Έλ“œ νƒ€μž… μ‹œμŠ€ν…œ

πŸ”Œ 포트 νƒ€μž…

λ…Έλ“œ κ°„ 데이터 전솑을 μœ„ν•œ 포트 νƒ€μž…λ“€:

νƒ€μž… μ„€λͺ… μ˜ˆμ‹œ
INT μ •μˆ˜ 42, -10, 0
STR λ¬Έμžμ—΄ "Hello", "World"
FLOAT λΆ€λ™μ†Œμˆ˜μ  3.14, -0.5, 1.0
BOOL 뢈린 true, false
ANY λͺ¨λ“  νƒ€μž… λͺ¨λ“  데이터 νƒ€μž… ν—ˆμš©

πŸ“Š λ§€κ°œλ³€μˆ˜ νƒ€μž…

λ…Έλ“œ 섀정을 μœ„ν•œ λ§€κ°œλ³€μˆ˜ νƒ€μž…λ“€:

νƒ€μž… μ„€λͺ… μΆ”κ°€ 속성
STRING λ¬Έμžμ—΄ λ§€κ°œλ³€μˆ˜ -
INTEGER μ •μˆ˜ λ§€κ°œλ³€μˆ˜ min, max, step
FLOAT λΆ€λ™μ†Œμˆ˜μ  λ§€κ°œλ³€μˆ˜ min, max, step
BOOLEAN 뢈린 λ§€κ°œλ³€μˆ˜ -

🎚️ λ§€κ°œλ³€μˆ˜ κ³ κΈ‰ μ„€μ •

parameters = [
    {
        "id": "temperature",
        "name": "Temperature",
        "type": "FLOAT",
        "value": 0.7,
        "required": False,
        "optional": True,      # κ³ κΈ‰ λͺ¨λ“œμ—μ„œλ§Œ ν‘œμ‹œ
        "min": 0.0,
        "max": 2.0,
        "step": 0.1
    },
    {
        "id": "model",
        "name": "Model",
        "type": "STRING",
        "value": "gpt-3.5-turbo",
        "required": True,
        "options": [           # λ“œλ‘­λ‹€μš΄ μ˜΅μ…˜
            {"value": "gpt-3.5-turbo", "label": "GPT-3.5 Turbo"},
            {"value": "gpt-4", "label": "GPT-4"}
        ]
    }
]

πŸš€ μƒˆλ‘œμš΄ λ…Έλ“œ μΆ”κ°€ν•˜κΈ° (Step-by-Step)

🎯 Step 1: λ…Έλ“œ 파일 생성

μƒˆλ‘œμš΄ λ…Έλ“œλ₯Ό nodes/ ν΄λ”μ˜ μ μ ˆν•œ μΉ΄ν…Œκ³ λ¦¬μ— μƒμ„±ν•©λ‹ˆλ‹€.

μ˜ˆμ‹œ: κ°„λ‹¨ν•œ λ¬Έμžμ—΄ μ—°κ²° λ…Έλ“œ

파일: editor/nodes/tool/string_concat.py

"""
λ¬Έμžμ—΄ μ—°κ²° λ…Έλ“œ

두 개의 λ¬Έμžμ—΄μ„ μž…λ ₯λ°›μ•„ μ—°κ²°ν•œ κ²°κ³Όλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
"""

from editor.node_composer import Node

class StringConcatNode(Node):
    # μΉ΄ν…Œκ³ λ¦¬ 및 κΈ°λŠ₯ μ •μ˜
    categoryId = "utilities"        # λ°˜λ“œμ‹œ CATEGORIES_LABEL_MAP에 μ‘΄μž¬ν•΄μ•Ό 함
    functionId = "tools"           # λ°˜λ“œμ‹œ FUNCTION_LABEL_MAP에 μ‘΄μž¬ν•΄μ•Ό 함
    
    # λ…Έλ“œ κΈ°λ³Έ 정보
    nodeId = "tool/string_concat"   # 고유 μ‹λ³„μž (μΉ΄ν…Œκ³ λ¦¬/λ…Έλ“œλͺ… ν˜•μ‹ ꢌμž₯)
    nodeName = "String Concat"      # μ‚¬μš©μžμ—κ²Œ ν‘œμ‹œλ  이름
    description = "두 개의 λ¬Έμžμ—΄μ„ μž…λ ₯λ°›μ•„ μ—°κ²°ν•œ κ²°κ³Όλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. κ΅¬λΆ„μžλ₯Ό μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€."
    tags = ["string", "concatenation", "text", "join", "utility"]  # 검색 νƒœκ·Έ
    
    # μž…λ ₯ 포트 μ •μ˜
    inputs = [
        {
            "id": "str1",           # 포트 고유 μ‹λ³„μž
            "name": "String 1",     # 포트 ν‘œμ‹œ 이름
            "type": "STR",          # 데이터 νƒ€μž…
            "required": True,       # ν•„μˆ˜ μž…λ ₯
            "multi": False          # 닀쀑 μ—°κ²° λΉ„ν—ˆμš©
        },
        {
            "id": "str2",
            "name": "String 2",
            "type": "STR",
            "required": True,
            "multi": False
        }
    ]
    
    # 좜λ ₯ 포트 μ •μ˜
    outputs = [
        {
            "id": "result",
            "name": "Result",
            "type": "STR"
        }
    ]
    
    # λ§€κ°œλ³€μˆ˜ μ •μ˜
    parameters = [
        {
            "id": "separator",
            "name": "Separator",
            "type": "STRING",
            "value": " ",          # κΈ°λ³Έκ°’: 곡백
            "required": False,     # ν•„μˆ˜ μ•„λ‹˜
            "optional": False      # κΈ°λ³Έ λͺ¨λ“œμ—μ„œ ν‘œμ‹œ
        }
    ]
    
    def execute(self, str1: str, str2: str, separator: str = " ") -> str:
        """
        λ…Έλ“œ μ‹€ν–‰ λ©”μ„œλ“œ
        
        Args:
            str1: 첫 번째 λ¬Έμžμ—΄
            str2: 두 번째 λ¬Έμžμ—΄
            separator: κ΅¬λΆ„μž (κΈ°λ³Έκ°’: 곡백)
            
        Returns:
            str: μ—°κ²°λœ λ¬Έμžμ—΄
        """
        return f"{str1}{separator}{str2}"

πŸ”§ Step 2: λ…Έλ“œ 등둝 확인

λ…Έλ“œκ°€ μ •μ˜λ˜λ©΄ μžλ™μœΌλ‘œ λ“±λ‘λ©λ‹ˆλ‹€. 등둝 성곡 μ‹œ λ‹€μŒκ³Ό 같은 λ©”μ‹œμ§€κ°€ 좜λ ₯λ©λ‹ˆλ‹€:

-> λ…Έλ“œ 'String Concat' 등둝 μ™„λ£Œ.

등둝 μ‹€νŒ¨ μ‹œ λ‹€μŒκ³Ό 같은 였λ₯˜κ°€ 좜λ ₯λ©λ‹ˆλ‹€:

[Node Registration Failed] Node 'StringConcatNode': 'categoryId' is invalid.
-> Assigned value: 'invalid_category' (Allowed values: ['langchain', 'polar', 'utilities', 'math'])

πŸ§ͺ Step 3: λ…Έλ“œ ν…ŒμŠ€νŠΈ

μƒˆλ‘œμš΄ λ…Έλ“œλ₯Ό ν…ŒμŠ€νŠΈν•˜λŠ” 방법:

# test_string_concat.py
from editor.nodes.tool.string_concat import StringConcatNode

def test_string_concat():
    node = StringConcatNode()
    
    # κΈ°λ³Έ κ΅¬λΆ„μž ν…ŒμŠ€νŠΈ
    result = node.execute("Hello", "World")
    assert result == "Hello World"
    
    # μ‚¬μš©μž μ •μ˜ κ΅¬λΆ„μž ν…ŒμŠ€νŠΈ
    result = node.execute("Hello", "World", separator="-")
    assert result == "Hello-World"
    
    print("βœ… String Concat Node ν…ŒμŠ€νŠΈ 톡과")

if __name__ == "__main__":
    test_string_concat()

🎨 Step 4: κ³ κΈ‰ λ…Έλ“œ νŒ¨ν„΄

1. μƒνƒœλ₯Ό κ°€μ§„ λ…Έλ“œ

class CounterNode(Node):
    categoryId = "utilities"
    functionId = "tools"
    nodeId = "tool/counter"
    nodeName = "Counter"
    description = "호좜될 λ•Œλ§ˆλ‹€ 카운트λ₯Ό μ¦κ°€μ‹œν‚€λŠ” λ…Έλ“œμž…λ‹ˆλ‹€."
    tags = ["counter", "state", "increment"]
    
    inputs = [
        {
            "id": "trigger",
            "name": "Trigger",
            "type": "ANY",
            "required": True,
            "multi": False
        }
    ]
    
    outputs = [
        {
            "id": "count",
            "name": "Count",
            "type": "INT"
        }
    ]
    
    parameters = [
        {
            "id": "start_value",
            "name": "Start Value",
            "type": "INTEGER",
            "value": 0,
            "required": False
        }
    ]
    
    def __init__(self):
        super().__init__()
        self.count = 0
    
    def execute(self, trigger: any, start_value: int = 0) -> int:
        if self.count == 0:
            self.count = start_value
        self.count += 1
        return self.count

2. 쑰건뢀 μ‹€ν–‰ λ…Έλ“œ

class ConditionalNode(Node):
    categoryId = "utilities"
    functionId = "tools"
    nodeId = "tool/conditional"
    nodeName = "Conditional"
    description = "쑰건에 따라 λ‹€λ₯Έ 값을 λ°˜ν™˜ν•˜λŠ” λ…Έλ“œμž…λ‹ˆλ‹€."
    tags = ["conditional", "if", "logic", "branch"]
    
    inputs = [
        {
            "id": "condition",
            "name": "Condition",
            "type": "BOOL",
            "required": True,
            "multi": False
        },
        {
            "id": "true_value",
            "name": "True Value",
            "type": "ANY",
            "required": True,
            "multi": False
        },
        {
            "id": "false_value",
            "name": "False Value",
            "type": "ANY",
            "required": True,
            "multi": False
        }
    ]
    
    outputs = [
        {
            "id": "result",
            "name": "Result",
            "type": "ANY"
        }
    ]
    
    parameters = []
    
    def execute(self, condition: bool, true_value: any, false_value: any) -> any:
        return true_value if condition else false_value

3. 배치 처리 λ…Έλ“œ

from typing import List

class BatchProcessNode(Node):
    categoryId = "utilities"
    functionId = "tools"
    nodeId = "tool/batch_process"
    nodeName = "Batch Process"
    description = "μ—¬λŸ¬ μž…λ ₯을 배치둜 μ²˜λ¦¬ν•˜λŠ” λ…Έλ“œμž…λ‹ˆλ‹€."
    tags = ["batch", "process", "list", "multiple"]
    
    inputs = [
        {
            "id": "items",
            "name": "Items",
            "type": "ANY",
            "required": True,
            "multi": True  # 닀쀑 μž…λ ₯ ν—ˆμš©
        }
    ]
    
    outputs = [
        {
            "id": "results",
            "name": "Results",
            "type": "ANY"
        }
    ]
    
    parameters = [
        {
            "id": "batch_size",
            "name": "Batch Size",
            "type": "INTEGER",
            "value": 10,
            "required": False,
            "min": 1,
            "max": 100
        }
    ]
    
    def execute(self, items: List[any], batch_size: int = 10) -> List[any]:
        results = []
        
        # 배치 λ‹¨μœ„λ‘œ 처리
        for i in range(0, len(items), batch_size):
            batch = items[i:i + batch_size]
            # 배치 처리 둜직
            processed_batch = [self.process_item(item) for item in batch]
            results.extend(processed_batch)
        
        return results
    
    def process_item(self, item: any) -> any:
        """κ°œλ³„ μ•„μ΄ν…œ 처리 둜직"""
        return item  # μ‹€μ œ 처리 둜직 κ΅¬ν˜„

4. μ™ΈλΆ€ API 호좜 λ…Έλ“œ

import requests
from typing import Dict, Any

class HttpRequestNode(Node):
    categoryId = "utilities"
    functionId = "tools"
    nodeId = "tool/http_request"
    nodeName = "HTTP Request"
    description = "HTTP μš”μ²­μ„ 보내고 응닡을 λ°›λŠ” λ…Έλ“œμž…λ‹ˆλ‹€."
    tags = ["http", "request", "api", "web", "rest"]
    
    inputs = [
        {
            "id": "url",
            "name": "URL",
            "type": "STR",
            "required": True,
            "multi": False
        },
        {
            "id": "data",
            "name": "Request Data",
            "type": "ANY",
            "required": False,
            "multi": False
        }
    ]
    
    outputs = [
        {
            "id": "response",
            "name": "Response",
            "type": "ANY"
        },
        {
            "id": "status_code",
            "name": "Status Code",
            "type": "INT"
        }
    ]
    
    parameters = [
        {
            "id": "method",
            "name": "Method",
            "type": "STRING",
            "value": "GET",
            "required": True,
            "options": [
                {"value": "GET", "label": "GET"},
                {"value": "POST", "label": "POST"},
                {"value": "PUT", "label": "PUT"},
                {"value": "DELETE", "label": "DELETE"}
            ]
        },
        {
            "id": "timeout",
            "name": "Timeout",
            "type": "INTEGER",
            "value": 30,
            "required": False,
            "optional": True,
            "min": 1,
            "max": 300
        }
    ]
    
    def execute(self, url: str, data: any = None, method: str = "GET", timeout: int = 30) -> Dict[str, Any]:
        try:
            response = requests.request(
                method=method,
                url=url,
                json=data if data else None,
                timeout=timeout
            )
            
            return {
                "response": response.json() if response.headers.get('content-type', '').startswith('application/json') else response.text,
                "status_code": response.status_code
            }
        except Exception as e:
            return {
                "response": {"error": str(e)},
                "status_code": -1
            }

5. 데이터 λ³€ν™˜ λ…Έλ“œ

import json
from typing import Dict, Any

class DataTransformNode(Node):
    categoryId = "utilities"
    functionId = "tools"
    nodeId = "tool/data_transform"
    nodeName = "Data Transform"
    description = "데이터λ₯Ό λ‹€λ₯Έ ν˜•μ‹μœΌλ‘œ λ³€ν™˜ν•˜λŠ” λ…Έλ“œμž…λ‹ˆλ‹€."
    tags = ["transform", "convert", "data", "format"]
    
    inputs = [
        {
            "id": "data",
            "name": "Input Data",
            "type": "ANY",
            "required": True,
            "multi": False
        }
    ]
    
    outputs = [
        {
            "id": "transformed_data",
            "name": "Transformed Data",
            "type": "ANY"
        }
    ]
    
    parameters = [
        {
            "id": "transform_type",
            "name": "Transform Type",
            "type": "STRING",
            "value": "to_json",
            "required": True,
            "options": [
                {"value": "to_json", "label": "To JSON"},
                {"value": "to_string", "label": "To String"},
                {"value": "to_upper", "label": "To Uppercase"},
                {"value": "to_lower", "label": "To Lowercase"}
            ]
        }
    ]
    
    def execute(self, data: any, transform_type: str = "to_json") -> any:
        try:
            if transform_type == "to_json":
                return json.dumps(data, ensure_ascii=False, indent=2)
            elif transform_type == "to_string":
                return str(data)
            elif transform_type == "to_upper":
                return str(data).upper()
            elif transform_type == "to_lower":
                return str(data).lower()
            else:
                return data
        except Exception as e:
            return {"error": str(e)}

πŸ”„ Step 5: λ…Έλ“œ ν…ŒμŠ€νŠΈ 및 검증

1. λ‹¨μœ„ ν…ŒμŠ€νŠΈ μž‘μ„±

# test_nodes.py
import pytest
from editor.nodes.tool.string_concat import StringConcatNode
from editor.nodes.tool.conditional import ConditionalNode

class TestStringConcatNode:
    def setup_method(self):
        self.node = StringConcatNode()
    
    def test_basic_concat(self):
        result = self.node.execute("Hello", "World")
        assert result == "Hello World"
    
    def test_custom_separator(self):
        result = self.node.execute("Hello", "World", separator="-")
        assert result == "Hello-World"
    
    def test_empty_strings(self):
        result = self.node.execute("", "")
        assert result == " "

class TestConditionalNode:
    def setup_method(self):
        self.node = ConditionalNode()
    
    def test_true_condition(self):
        result = self.node.execute(True, "yes", "no")
        assert result == "yes"
    
    def test_false_condition(self):
        result = self.node.execute(False, "yes", "no")
        assert result == "no"

2. λ…Έλ“œ 검증 도ꡬ

# node_validator.py
from editor.node_composer import get_node_registry, run_discovery

def validate_all_nodes():
    """λͺ¨λ“  λ…Έλ“œμ˜ μœ νš¨μ„±μ„ κ²€μ¦ν•©λ‹ˆλ‹€."""
    run_discovery()
    registry = get_node_registry()
    
    print(f"총 {len(registry)}개의 λ…Έλ“œκ°€ λ“±λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
    
    # μΉ΄ν…Œκ³ λ¦¬λ³„ λΆ„λ₯˜
    categories = {}
    for node in registry:
        cat_id = node['categoryId']
        if cat_id not in categories:
            categories[cat_id] = []
        categories[cat_id].append(node)
    
    # 검증 κ²°κ³Ό 좜λ ₯
    for cat_id, nodes in categories.items():
        print(f"\nπŸ“‚ {cat_id}: {len(nodes)}개 λ…Έλ“œ")
        for node in nodes:
            print(f"  - {node['nodeName']} ({node['id']})")
            
            # μž…λ ₯/좜λ ₯ 검증
            if not node['inputs'] and not node['outputs']:
                print(f"    ⚠️  μž…λ ₯/좜λ ₯이 λͺ¨λ‘ μ—†μŠ΅λ‹ˆλ‹€.")
            
            # λ§€κ°œλ³€μˆ˜ 검증
            for param in node['parameters']:
                if param.get('required') and param.get('optional'):
                    print(f"    ❌ λ§€κ°œλ³€μˆ˜ '{param['id']}': required와 optional이 λͺ¨λ‘ Trueμž…λ‹ˆλ‹€.")

if __name__ == "__main__":
    validate_all_nodes()

πŸ—οΈ μƒˆλ‘œμš΄ μΉ΄ν…Œκ³ λ¦¬ μΆ”κ°€ν•˜κΈ°

🎯 Step 1: μΉ΄ν…Œκ³ λ¦¬ μ •μ˜

editor/model/node.pyμ—μ„œ μƒˆλ‘œμš΄ μΉ΄ν…Œκ³ λ¦¬λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€:

CATEGORIES_LABEL_MAP = {
    'langchain': 'LangChain',
    'polar': 'POLAR',
    'utilities': 'Utilities',
    'math': 'Math',
    'database': 'Database',    # μƒˆλ‘œμš΄ μΉ΄ν…Œκ³ λ¦¬ μΆ”κ°€
    'network': 'Network',      # μƒˆλ‘œμš΄ μΉ΄ν…Œκ³ λ¦¬ μΆ”κ°€
    # ...
}

ICON_LABEL_MAP = {
    'langchain': 'SiLangchain',
    'polar': 'POLAR',
    'utilities': 'LuWrench',
    'math': 'LuWrench',
    'database': 'BiDatabase',   # μƒˆλ‘œμš΄ μ•„μ΄μ½˜ μΆ”κ°€
    'network': 'BiNetwork',     # μƒˆλ‘œμš΄ μ•„μ΄μ½˜ μΆ”κ°€
    # ...
}

πŸ”§ Step 2: κΈ°λŠ₯ μ •μ˜

ν•„μš”ν•œ 경우 μƒˆλ‘œμš΄ κΈ°λŠ₯도 μΆ”κ°€ν•©λ‹ˆλ‹€:

FUNCTION_LABEL_MAP = {
    # ...κΈ°μ‘΄ κΈ°λŠ₯λ“€...
    'sql': 'SQL',
    'nosql': 'NoSQL',
    'crud': 'CRUD',
    'http': 'HTTP',
    'websocket': 'WebSocket',
    'tcp': 'TCP',
    # ...
}

πŸ“ Step 3: 폴더 ꡬ쑰 생성

μƒˆλ‘œμš΄ μΉ΄ν…Œκ³ λ¦¬λ₯Ό μœ„ν•œ 폴더λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€:

editor/nodes/
β”œβ”€β”€ database/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ mysql_query.py
β”‚   β”œβ”€β”€ mongodb_find.py
β”‚   └── redis_get.py
└── network/
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ http_get.py
    β”œβ”€β”€ websocket_send.py
    └── tcp_connect.py

🎨 Step 4: μΉ΄ν…Œκ³ λ¦¬λ³„ λ…Έλ“œ μ˜ˆμ‹œ

λ°μ΄ν„°λ² μ΄μŠ€ λ…Έλ“œ μ˜ˆμ‹œ

# editor/nodes/database/mysql_query.py
import mysql.connector
from editor.node_composer import Node

class MySQLQueryNode(Node):
    categoryId = "database"
    functionId = "sql"
    nodeId = "database/mysql_query"
    nodeName = "MySQL Query"
    description = "MySQL λ°μ΄ν„°λ² μ΄μŠ€μ— 쿼리λ₯Ό μ‹€ν–‰ν•˜λŠ” λ…Έλ“œμž…λ‹ˆλ‹€."
    tags = ["mysql", "database", "sql", "query"]
    
    inputs = [
        {
            "id": "query",
            "name": "SQL Query",
            "type": "STR",
            "required": True,
            "multi": False
        }
    ]
    
    outputs = [
        {
            "id": "result",
            "name": "Query Result",
            "type": "ANY"
        }
    ]
    
    parameters = [
        {
            "id": "host",
            "name": "Host",
            "type": "STRING",
            "value": "localhost",
            "required": True
        },
        {
            "id": "port",
            "name": "Port",
            "type": "INTEGER",
            "value": 3306,
            "required": True
        },
        {
            "id": "database",
            "name": "Database",
            "type": "STRING",
            "value": "",
            "required": True
        },
        {
            "id": "username",
            "name": "Username",
            "type": "STRING",
            "value": "",
            "required": True
        },
        {
            "id": "password",
            "name": "Password",
            "type": "STRING",
            "value": "",
            "required": True
        }
    ]
    
    def execute(self, query: str, host: str, port: int, database: str, username: str, password: str):
        try:
            connection = mysql.connector.connect(
                host=host,
                port=port,
                database=database,
                user=username,
                password=password
            )
            
            cursor = connection.cursor()
            cursor.execute(query)
            
            if query.strip().upper().startswith('SELECT'):
                result = cursor.fetchall()
                columns = [desc[0] for desc in cursor.description]
                return {
                    "data": result,
                    "columns": columns
                }
            else:
                connection.commit()
                return {
                    "affected_rows": cursor.rowcount,
                    "message": "Query executed successfully"
                }
                
        except Exception as e:
            return {"error": str(e)}
        finally:
            if connection.is_connected():
                cursor.close()
                connection.close()

λ„€νŠΈμ›Œν¬ λ…Έλ“œ μ˜ˆμ‹œ

# editor/nodes/network/http_get.py
import requests
from editor.node_composer import Node

class HttpGetNode(Node):
    categoryId = "network"
    functionId = "http"
    nodeId = "network/http_get"
    nodeName = "HTTP GET"
    description = "HTTP GET μš”μ²­μ„ λ³΄λ‚΄λŠ” λ…Έλ“œμž…λ‹ˆλ‹€."
    tags = ["http", "get", "request", "api", "web"]
    
    inputs = [
        {
            "id": "url",
            "name": "URL",
            "type": "STR",
            "required": True,
            "multi": False
        }
    ]
    
    outputs = [
        {
            "id": "response",
            "name": "Response",
            "type": "ANY"
        },
        {
            "id": "status_code",
            "name": "Status Code",
            "type": "INT"
        }
    ]
    
    parameters = [
        {
            "id": "timeout",
            "name": "Timeout",
            "type": "INTEGER",
            "value": 30,
            "required": False,
            "min": 1,
            "max": 300
        },
        {
            "id": "headers",
            "name": "Headers",
            "type": "STRING",
            "value": "{}",
            "required": False,
            "optional": True
        }
    ]
    
    def execute(self, url: str, timeout: int = 30, headers: str = "{}"):
        try:
            import json
            headers_dict = json.loads(headers) if headers else {}
            
            response = requests.get(url, timeout=timeout, headers=headers_dict)
            
            try:
                response_data = response.json()
            except:
                response_data = response.text
            
            return {
                "response": response_data,
                "status_code": response.status_code
            }
        except Exception as e:
            return {
                "response": {"error": str(e)},
                "status_code": -1
            }

πŸ”„ μ›Œν¬ν”Œλ‘œμš° ꡬ쑰 및 특수 λ…Έλ“œ

πŸ“‹ μ›Œν¬ν”Œλ‘œμš° ν•„μˆ˜ ꡬ쑰

λͺ¨λ“  μ›Œν¬ν”Œλ‘œμš°λŠ” λ°˜λ“œμ‹œ λ‹€μŒκ³Ό 같은 ꡬ쑰λ₯Ό κ°€μ Έμ•Ό ν•©λ‹ˆλ‹€:

μ‚¬μš©μž μž…λ ₯ (Interaction)
         ↓
    [Start Node]
         ↓
   [쀑간 처리 λ…Έλ“œλ“€]
         ↓
     [End Node]
         ↓
    μ‚¬μš©μž 좜λ ₯ (Result)

🎯 특수 λ…Έλ“œ: Start Node와 End Node

πŸš€ Start Node (μ‹œμž‘ λ…Έλ“œ)

Start NodeλŠ” μ›Œν¬ν”Œλ‘œμš°μ˜ μ§„μž…μ μ΄λ©°, μ‚¬μš©μžμ˜ Interaction을 λ°›μ•„λ“€μ΄λŠ” νŠΉμˆ˜ν•œ λ…Έλ“œμž…λ‹ˆλ‹€.

νŠΉμ§•
  • 단일성: μ›Œν¬ν”Œλ‘œμš°λ‹Ή λ°˜λ“œμ‹œ ν•˜λ‚˜λ§Œ μ‘΄μž¬ν•΄μ•Ό 함
  • μž…λ ₯ μ—†μŒ: μ™ΈλΆ€ ν¬νŠΈλ‘œλΆ€ν„° μž…λ ₯을 λ°›μ§€ μ•ŠμŒ
  • Interaction μ—°κ²°: μ‚¬μš©μžμ˜ μž…λ ₯(Interaction)을 직접 λ°›μŒ
  • κ³ μ • κΈ°λŠ₯: functionId = "startnode"둜 고정됨
Start Node μ˜ˆμ‹œ
class InputStringNode(Node):
    categoryId = "utilities"
    functionId = "startnode"        # κ³ μ •κ°’: μ‹œμž‘ λ…Έλ“œ
    nodeId = "tool/input_str"
    nodeName = "Input String"
    description = "μ‚¬μš©μžκ°€ μ„€μ •ν•œ λ¬Έμžμ—΄ 값을 좜λ ₯ν•˜λŠ” μž…λ ₯ λ…Έλ“œμž…λ‹ˆλ‹€. μ›Œν¬ν”Œλ‘œμš°μ—μ„œ ν…μŠ€νŠΈ λ°μ΄ν„°μ˜ μ‹œμž‘μ μœΌλ‘œ μ‚¬μš©λ©λ‹ˆλ‹€."
    tags = ["input", "string", "text", "parameter", "source", "start_node", "user_input"]
    
    inputs = []  # μž…λ ₯ 포트 μ—†μŒ
    outputs = [
        {
            "id": "result",
            "name": "Result",
            "type": "STR"
        }
    ]
    parameters = [
        {
            "id": "input_str",
            "name": "String",
            "type": "STRING",
            "value": "",
            "required": True
        }
    ]
    
    def execute(self, input_str: str) -> str:
        """μ‚¬μš©μž μž…λ ₯을 κ·ΈλŒ€λ‘œ 좜λ ₯"""
        return input_str

🏁 End Node (μ’…λ£Œ λ…Έλ“œ)

End NodeλŠ” μ›Œν¬ν”Œλ‘œμš°μ˜ 좜ꡬ점이며, μ΅œμ’… κ²°κ³Όλ₯Ό μ‚¬μš©μžμ—κ²Œ λ°˜ν™˜ν•˜λŠ” νŠΉμˆ˜ν•œ λ…Έλ“œμž…λ‹ˆλ‹€.

νŠΉμ§•
  • 단일성: μ›Œν¬ν”Œλ‘œμš°λ‹Ή λ°˜λ“œμ‹œ ν•˜λ‚˜λ§Œ μ‘΄μž¬ν•΄μ•Ό 함
  • 좜λ ₯ μ—†μŒ: λ‹€λ₯Έ λ…Έλ“œλ‘œ 좜λ ₯을 μ „λ‹¬ν•˜μ§€ μ•ŠμŒ
  • κ²°κ³Ό λ°˜ν™˜: μ›Œν¬ν”Œλ‘œμš°μ˜ μ΅œμ’… κ²°κ³Όλ₯Ό μ‚¬μš©μžμ—κ²Œ λ°˜ν™˜
  • κ³ μ • κΈ°λŠ₯: functionId = "endnode"둜 고정됨
End Node μ˜ˆμ‹œ
class OutputStringNode(Node):
    categoryId = "utilities"
    functionId = "endnode"          # κ³ μ •κ°’: μ’…λ£Œ λ…Έλ“œ
    nodeId = "tool/output_str"
    nodeName = "Output String"
    description = "μž…λ ₯받은 값을 μ΅œμ’… 결과둜 좜λ ₯ν•˜λŠ” μ’…λ£Œ λ…Έλ“œμž…λ‹ˆλ‹€. μ›Œν¬ν”Œλ‘œμš°μ˜ μ΅œμ’… 좜λ ₯점으둜 μ‚¬μš©λ©λ‹ˆλ‹€."
    tags = ["output", "string", "text", "result", "end_node", "final_output"]
    
    inputs = [
        {
            "id": "input",
            "name": "Input",
            "type": "ANY",
            "required": True,
            "multi": False
        }
    ]
    outputs = []  # 좜λ ₯ 포트 μ—†μŒ
    parameters = []
    
    def execute(self, input: any) -> any:
        """μž…λ ₯을 μ΅œμ’… 결과둜 λ°˜ν™˜"""
        return input

πŸ”„ μ›Œν¬ν”Œλ‘œμš° μ‹€ν–‰ 흐름

1. Interaction μž…λ ₯ 단계

# μ‚¬μš©μž μž…λ ₯ μ˜ˆμ‹œ
user_interaction = {
    "type": "text",
    "content": "μ•ˆλ…•ν•˜μ„Έμš”, λ‚ μ”¨λŠ” μ–΄λ–€κ°€μš”?",
    "timestamp": "2025-07-18T10:30:00Z"
}

2. Start Node μ‹€ν–‰

# Start Nodeκ°€ μ‚¬μš©μž μž…λ ₯을 처리
start_node_result = start_node.execute(user_interaction["content"])
# κ²°κ³Ό: "μ•ˆλ…•ν•˜μ„Έμš”, λ‚ μ”¨λŠ” μ–΄λ–€κ°€μš”?"

3. 쀑간 λ…Έλ“œ 처리

# 예: μ±„νŒ… λͺ¨λΈμ„ ν†΅ν•œ 응닡 생성
chat_node_result = chat_node.execute(start_node_result)
# κ²°κ³Ό: "μ•ˆλ…•ν•˜μ„Έμš”! 였늘 λ‚ μ”¨λŠ” λ§‘κ³  κΈ°μ˜¨μ€ 25λ„μž…λ‹ˆλ‹€."

4. End Node μ‹€ν–‰

# End Nodeκ°€ μ΅œμ’… κ²°κ³Όλ₯Ό μ‚¬μš©μžμ—κ²Œ λ°˜ν™˜
final_result = end_node.execute(chat_node_result)
# κ²°κ³Ό: "μ•ˆλ…•ν•˜μ„Έμš”! 였늘 λ‚ μ”¨λŠ” λ§‘κ³  κΈ°μ˜¨μ€ 25λ„μž…λ‹ˆλ‹€."

🎨 μ›Œν¬ν”Œλ‘œμš° ꡬ성 μ˜ˆμ‹œ

κ°„λ‹¨ν•œ μ±„νŒ… μ›Œν¬ν”Œλ‘œμš°

{
  "workflow_name": "Simple Chat",
  "workflow_id": "simple_chat",
  "nodes": [
    {
      "id": "start_node",
      "type": "tool/input_str",
      "data": {
        "nodeId": "tool/input_str",
        "parameters": {
          "input_str": "{{user_input}}"  // Interactionμ—μ„œ μžλ™ μ£Όμž…
        }
      }
    },
    {
      "id": "chat_node",
      "type": "chat/openai",
      "data": {
        "nodeId": "chat/openai",
        "parameters": {
          "model": "gpt-3.5-turbo",
          "temperature": 0.7
        }
      }
    },
    {
      "id": "end_node",
      "type": "tool/output_str",
      "data": {
        "nodeId": "tool/output_str",
        "parameters": {}
      }
    }
  ],
  "edges": [
    {
      "id": "edge1",
      "source": {"nodeId": "start_node", "portId": "result"},
      "target": {"nodeId": "chat_node", "portId": "text"}
    },
    {
      "id": "edge2",
      "source": {"nodeId": "chat_node", "portId": "result"},
      "target": {"nodeId": "end_node", "portId": "input"}
    }
  ]
}

λ³΅μž‘ν•œ RAG μ›Œν¬ν”Œλ‘œμš°

{
  "workflow_name": "RAG Chat",
  "workflow_id": "rag_chat",
  "nodes": [
    {
      "id": "start_node",
      "type": "tool/input_str",
      "data": {
        "nodeId": "tool/input_str",
        "parameters": {
          "input_str": "{{user_input}}"
        }
      }
    },
    {
      "id": "retrieval_node",
      "type": "rag/vector_search",
      "data": {
        "nodeId": "rag/vector_search",
        "parameters": {
          "collection_name": "knowledge_base",
          "top_k": 5
        }
      }
    },
    {
      "id": "context_merge_node",
      "type": "tool/string_concat",
      "data": {
        "nodeId": "tool/string_concat",
        "parameters": {
          "separator": "\n\n"
        }
      }
    },
    {
      "id": "chat_node",
      "type": "chat/openai",
      "data": {
        "nodeId": "chat/openai",
        "parameters": {
          "model": "gpt-4",
          "temperature": 0.3
        }
      }
    },
    {
      "id": "end_node",
      "type": "tool/output_str",
      "data": {
        "nodeId": "tool/output_str",
        "parameters": {}
      }
    }
  ],
  "edges": [
    {
      "id": "edge1",
      "source": {"nodeId": "start_node", "portId": "result"},
      "target": {"nodeId": "retrieval_node", "portId": "query"}
    },
    {
      "id": "edge2",
      "source": {"nodeId": "start_node", "portId": "result"},
      "target": {"nodeId": "context_merge_node", "portId": "str1"}
    },
    {
      "id": "edge3",
      "source": {"nodeId": "retrieval_node", "portId": "result"},
      "target": {"nodeId": "context_merge_node", "portId": "str2"}
    },
    {
      "id": "edge4",
      "source": {"nodeId": "context_merge_node", "portId": "result"},
      "target": {"nodeId": "chat_node", "portId": "text"}
    },
    {
      "id": "edge5",
      "source": {"nodeId": "chat_node", "portId": "result"},
      "target": {"nodeId": "end_node", "portId": "input"}
    }
  ]
}

🎯 Best Practices

1. Start Node 섀계

  • λ‹¨μˆœμ„±: λ³΅μž‘ν•œ 둜직 μ§€μ–‘, μž…λ ₯ λ³€ν™˜μ— 집쀑
  • μœ μ—°μ„±: λ‹€μ–‘ν•œ μž…λ ₯ ν˜•μ‹ 지원
  • 검증: μž…λ ₯ 데이터 μœ νš¨μ„± 검증

2. End Node 섀계

  • ν¬λ§·νŒ…: μ‚¬μš©μž μΉœν™”μ μΈ κ²°κ³Ό ν˜•μ‹
  • 메타데이터: μ‹€ν–‰ 정보, νƒ€μž„μŠ€νƒ¬ν”„ λ“± μΆ”κ°€
  • μ—λŸ¬ 처리: μ‹€ν–‰ 였λ₯˜ μ‹œ μ μ ˆν•œ μ—λŸ¬ λ©”μ‹œμ§€

3. μ›Œν¬ν”Œλ‘œμš° 섀계

  • μ„ ν˜•μ„±: Start β†’ 쀑간 β†’ End μ„ ν˜• ꡬ쑰 μœ μ§€
  • 검증: μ—°κ²°μ„± 및 ꡬ쑰 검증
  • ν…ŒμŠ€νŠΈ: λ‹€μ–‘ν•œ μž…λ ₯에 λŒ€ν•œ ν…ŒμŠ€νŠΈ