Skip to content

Commit 54b3dd1

Browse files
committed
fix(ai/tools): module tool 자동등록 — _MODULE_CORE 수동 whitelist 제거
라이브 검증 중 2 버그 발견: 1. pastInsight(stockCode) 호출 시 stockCode 누락 에러 원인: _splitKwargs 가 _MODULE_CORE 수동 whitelist 에 의존. pastInsight/sectorInsights 가 여기 없어서 모든 kwargs drop. 근본: _splitKwargs 를 inspect.signature 자동 추출로 교체. 2. search tool 이 Company.search 로 잘못 바인딩 (stockCode 요구) 원인: _autoDiscover 가 Company-bound 를 module-level 보다 우선. Company.search (c 스코프) vs dartlab.search (시장 전체) 충돌. 근본: module-level 우선 등록, Company 는 중복이면 skip. AI tool 의 기본 의미 = 시장 전체 / 종목 비의존. 라이브 검증 (4건 전부 정상): - pastInsight('005930') → dict - gather news + target='한국 경제' → 100 rows - search(query) → 시장 전체 공시 검색 - analysis override wacc=9.0 → assumptions.wacc 9.0 반영 회귀 116/116 통과.
1 parent 2f2299e commit 54b3dd1

1 file changed

Lines changed: 39 additions & 27 deletions

File tree

src/dartlab/ai/tools/__init__.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -71,33 +71,20 @@ class AITool:
7171
def _autoDiscover() -> dict[str, tuple[str, str]]:
7272
"""dartlab.__all__ + Company 공개 method 자동 순회 + 블랙리스트.
7373
74-
우선순위: Company-bound (_xxxImpl 또는 직접 method) > module-level.
74+
우선순위: **module-level (시장 전체) > Company-bound (개별 종목)**.
75+
이유: AI tool 은 기본이 "시장 전체 / 종목 비의존" 의미. 같은 이름이 둘 다 있으면
76+
module 이 더 범용. 예: `search` = dartlab.search (시장 공시) vs Company.search
77+
(이 회사 공시) — AI 는 전자가 기본 유용.
7578
"""
7679
import dartlab
7780
from dartlab.providers.dart.company import Company as _C
7881

7982
tools: dict[str, tuple[str, str]] = {}
8083

81-
# 1. Company-bound — dual-access _xxxImpl 또는 직접 method
82-
for attr in dir(_C):
83-
if attr.startswith("_") or attr in _BLACKLIST:
84-
continue
85-
# dual-access property (_xxxImpl 존재) 우선
86-
if getattr(_C, f"_{attr}Impl", None) is not None:
87-
tools[attr] = ("company", attr)
88-
continue
89-
# 직접 method (gather, quant 등)
90-
obj = getattr(_C, attr, None)
91-
if callable(obj) and (inspect.isfunction(obj) or inspect.ismethod(obj)):
92-
# Company 인스턴스에서만 의미 있는 method 만 (수업 attribute, dunder 제외)
93-
if not isinstance(obj, type):
94-
tools[attr] = ("company", attr)
95-
96-
# 2. Module-level — Company 에 없는 것만 (scan/macro/search 등)
97-
# 주의: dartlab.scan/macro/quant 는 _CallableModule (ismodule=True) — 명시적 허용.
84+
# 1. Module-level 먼저
9885
_MODULE_WHITELIST = {"scan", "macro", "quant", "gather", "industry", "topdown"}
9986
for name in getattr(dartlab, "__all__", []):
100-
if name in _BLACKLIST or name.startswith("_") or name in tools:
87+
if name in _BLACKLIST or name.startswith("_"):
10188
continue
10289
obj = getattr(dartlab, name, None)
10390
if obj is None or inspect.isclass(obj):
@@ -108,6 +95,20 @@ def _autoDiscover() -> dict[str, tuple[str, str]]:
10895
continue
10996
tools[name] = ("module", name)
11097

98+
# 2. Company-bound — module 에 없는 것만 (analysis/credit/review/show/... 은 여기서)
99+
for attr in dir(_C):
100+
if attr.startswith("_") or attr in _BLACKLIST or attr in tools:
101+
continue
102+
# dual-access property (_xxxImpl 존재) 우선
103+
if getattr(_C, f"_{attr}Impl", None) is not None:
104+
tools[attr] = ("company", attr)
105+
continue
106+
# 직접 method (gather, quant 등)
107+
obj = getattr(_C, attr, None)
108+
if callable(obj) and (inspect.isfunction(obj) or inspect.ismethod(obj)):
109+
if not isinstance(obj, type):
110+
tools[attr] = ("company", attr)
111+
111112
# 3. searchName → searchCompany (AI 친화적 이름)
112113
if "searchName" in tools:
113114
tools["searchCompany"] = tools.pop("searchName")
@@ -445,16 +446,27 @@ def _paramType(param: inspect.Parameter) -> str:
445446
# ── Module tool post-processing ───────────────────────────
446447

447448

448-
_MODULE_CORE: dict[str, set[str]] = {
449-
"scan": {"axis", "target"},
450-
"macro": {"axis", "target", "market"},
451-
"search": {"query", "corp", "start", "end", "topK", "scope"},
452-
"searchName": {"keyword"},
453-
}
449+
def _splitKwargs(target: str, kwargs: dict) -> tuple[dict, dict]:
450+
"""함수 시그니처 자동 분석으로 허용 파라미터 / 추가 파라미터 분리.
454451
452+
수동 whitelist 금지 원칙 (CAPABILITIES 자동). pastInsight/sectorInsights
453+
포함 모든 module-level tool 에 일관 적용.
454+
"""
455+
import dartlab
455456

456-
def _splitKwargs(target: str, kwargs: dict) -> tuple[dict, dict]:
457-
core_keys = _MODULE_CORE.get(target, set())
457+
fn = _resolveCallable("module", target)
458+
if fn is None:
459+
return {}, kwargs
460+
try:
461+
sig = inspect.signature(fn)
462+
core_keys = {
463+
n
464+
for n, p in sig.parameters.items()
465+
if n not in ("self", "cls")
466+
and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
467+
}
468+
except (ValueError, TypeError):
469+
core_keys = set()
458470
core = {k: v for k, v in kwargs.items() if k in core_keys and v is not None}
459471
post = {k: v for k, v in kwargs.items() if k not in core_keys and v is not None}
460472
return core, post

0 commit comments

Comments
 (0)