Lang2SQL의 hooks 시스템은 그래프 엔진 없이도 관측성(observability)을 제공하기 위한 최소 레이어입니다. Flow/Component 실행 과정에서 이벤트를 발행하고, 사용자는 hook 구현체로 이를 수집/출력/전송할 수 있습니다.
핵심 컨셉은 단 하나입니다:
“실행 중 무슨 일이 일어났는지(Event)를 hook이 받는다.”
Event는 Flow/Component 실행 중 발생한 “관측 단위”입니다.
@dataclass
class Event:
name: str # e.g., "component.run" / "flow.run"
component: str # e.g., "KeywordTableRetriever" / "SequentialFlow"
phase: Literal["start", "end", "error"]
ts: float # unix timestamp
duration_ms: Optional[float] = None
input_summary: Optional[str] = None
output_summary: Optional[str] = None
error: Optional[str] = None
data: dict[str, Any] = field(default_factory=dict)-
name- 이벤트 종류를 나타내는 문자열
- 예:
"component.run","flow.run"
-
component- 이벤트를 발생시킨 실행 단위 이름
- 예:
"KeywordTableRetriever","SequentialFlow"
-
phase"start" | "end" | "error"
-
ts- 이벤트 발생 시간(Unix timestamp)
-
duration_msend/error에서만 주로 채움(실행 시간)
-
input_summary,output_summary- 디버깅을 위한 “사람이 읽기 쉬운” 요약 문자열
-
error- 실패 시 오류 요약 문자열
-
data- UI/필터링/테스트/추가 메타를 위한 구조화 payload
- 기본은 빈 dict이며, 필요할 때만 채우는 것을 권장합니다.
TraceHook은 이벤트를 받는 인터페이스입니다.
class TraceHook(Protocol):
def on_event(self, event: Event) -> None: ...- Lang2SQL의 Flow/Component는 실행 시점에
hook.on_event(Event(...))형태로 이벤트를 발행합니다. - hook은 옵션이며, 없으면
NullHook이 사용됩니다.
class NullHook:
def on_event(self, event: Event) -> None:
return- 기본값
- 아무 것도 하지 않습니다.
- hook 비용을 없애고 싶을 때 항상 안전한 기본 구현입니다.
class MemoryHook:
def __init__(self) -> None:
self.events: list[Event] = []
def on_event(self, event: Event) -> None:
self.events.append(event)
def clear(self) -> None:
self.events.clear()
def snapshot(self) -> list[Event]:
return list(self.events)- 이벤트를 메모리에 누적합니다.
- 테스트/디버깅에 가장 유용합니다.
from lang2sql.core.hooks import MemoryHook
from lang2sql.flows.baseline import SequentialFlow
hook = MemoryHook()
flow = SequentialFlow(steps=[...], hook=hook)
out = flow.run("지난달 매출")
for e in hook.snapshot():
print(e.name, e.phase, e.component, e.duration_ms, e.error)- 보통은 테스트에서만
clear()가 필요합니다. (케이스 간 이벤트 섞임 방지) - 일반 사용자는 보통 “요청 1회 → hook 1개 생성” 패턴으로 충분합니다.
예:
hook = MemoryHook()
out = flow.run_query("q") # 여기서만 쓰고 끝
events = hook.snapshot()def now() -> float:
return time.time()- timestamp 생성에 사용됩니다.
def ms(start: float, end: float) -> float:
return (end - start) * 1000.0- duration(ms) 계산에 사용됩니다.
def summarize(x: Any, max_len: int = 240) -> str:
...- repr(x)를 기반으로 요약 문자열을 만들고 길이를 제한합니다.
- 이벤트의
input_summary/output_summary에 사용됩니다.
MemoryHook은 테스트용입니다. 운영에서는 보통 다음 형태로 확장합니다.
LoggingHook: JSON 로그로 남기기OTelHook: OpenTelemetry span으로 전송FilteringHook: 특정 component만 샘플링/필터링
관측성 제어는 hook 구현체에서 하고, Flow/Component 로직은 비즈니스에 집중하는 것이 기본 철학입니다.
Lang2SQL 예외 시스템은 두 목표를 가집니다.
- 도메인 에러는 도메인 타입으로 유지한다.
- 외부/일반 예외는 “어디서 터졌는지”가 보이도록 표준 래핑한다.
class Lang2SQLError(Exception):
"""Base error for lang2sql."""- Lang2SQL에서 발생하는 모든 도메인 예외의 베이스입니다.
BaseComponent/BaseFlow는 일반적으로 Lang2SQLError는 그대로 다시 raise합니다.
class IntegrationMissingError(Lang2SQLError):
def __init__(self, integration: str, extra: str | None = None, hint: str | None = None):
...- 선택적 의존성(optional integration)이 필요한데 설치되어 있지 않을 때
예:
faissretriever를 쓰는데faiss가 설치되어 있지 않음
extra가 있으면 설치 힌트를 포함합니다.
예 메시지:
Missing optional integration: faiss. Install with: pip install 'lang2sql[faiss]'
class ValidationError(Lang2SQLError):
pass- SQL 검증 실패, 정책상 금지 쿼리, 스키마 불일치 등
- “유저 입력/생성 결과가 유효하지 않다”에 해당하는 에러를 담는 대표 도메인 예외
class ContractError(Lang2SQLError):
"""Raised when a component violates a required call/return contract."""
pass- Lang2SQL이 요구하는 호출/반환 계약을 위반했을 때
- 예:
RunContext -> RunContext계약인데None또는int를 반환
이 에러는 “사용자 코드 버그를 빨리 발견(fail-fast)”하기 위한 타입입니다.
class ComponentError(Lang2SQLError):
def __init__(self, component: str, message: str, *, cause: Exception | None = None):
self.component = component
self.cause = cause
super().__init__(f"[{component}] {message}")- “일반 예외(ValueError, KeyError 등)”를 도메인 레이어로 끌어올 때 사용합니다.
- 어떤 컴포넌트에서 터졌는지 식별 가능하게 만듭니다.
- 원본 예외를 보존합니다.
- 테스트/디버깅에서 error chain을 확인할 수 있습니다.
(현재 BaseComponent 설계 기준)
-
Lang2SQLError계열- 그대로 이벤트에 기록하고 그대로 raise
-
그 외 모든 예외
- 이벤트에 기록하고
ComponentError(..., cause=e)로 래핑하여 raise
- 이벤트에 기록하고
즉:
- 도메인 예외는 “정상적인 실패”로 취급
- 일반 예외는 “버그/예상 밖 실패”로 표준화
- “사용자 입력/정책/검증 실패”는
ValidationError - “의존성 설치 문제”는
IntegrationMissingError - “계약 위반(반환 타입/호출 규약)”은
ContractError - “외부 라이브러리/예상 밖 예외”는
ComponentError로 래핑되어 올라오는 것을 기본으로 합니다.