Feature/rpc detection and fixes#1
Conversation
- RPC-style API에서 verb-resource 기반 CRUD workflow 관계 감지 (Layer 4) - DTO 타입 매칭으로 cross-controller COMPLEMENTARY 관계 감지 - _group_by_resource: 하드코딩 대신 동적 prefix threshold로 버전/라우팅 prefix 자동 스킵 - _detect_rpc_patterns를 _detect_rpc_crud_workflows + _detect_rpc_dto_links로 분리 - serialization.py: save_graph에 encoding="utf-8" 추가
Phase A — L0 Knowledge Base - graph_tool_call.ingest.io_contract: swagger schema → produces/consumes leaf 필드 결정론적 추출 - graph_tool_call.ontology.llm_provider: enrich_tool_semantics 메서드 (per-tool semantic 주석), ToolEnrichment / FieldSemantic / PairHint dataclass, max_tokens 명시, reference_tools 분리 - graph_tool_call.tool_graph: search_tools gateway 가 prerequisites / relations 를 LLM 에 노출 (retrieve_with_scores 사용) Phase B — Stage 3 Plan Runner - graph_tool_call.plan: Plan / PlanStep / ExecutionTrace 스키마 - graph_tool_call.plan.binding: BindingResolver (dotted + [N] + native type 보존) - graph_tool_call.plan.runner: PlanRunner (streaming + non-streaming) - tests: 29/29 pass (binding 21 + runner 8) docs/architecture-plan-and-execute.md: 5-layer 설계 문서.
target tool 에서 출발해 required consumes 를 bottom-up 재귀로 해소:
- entities (Stage 1 output) 로 직접 바인딩
- 없으면 graph 에서 semantic_tag / field_name 으로 producer 검색 후
prereq step 추가 (재귀)
- 없으면 UnsatisfiableFieldError
v1 범위 (설계 §16.6):
- Linear chain (fan-out/조건/parallel 은 v2+)
- 여러 producer 중 첫 번째 픽 (disambiguation 은 Phase D)
- [*] wildcard → [0] 변환 (단일 선택)
- max_depth 기본 5 (cyclic guard)
공개 API:
graph_tool_call.plan.PathSynthesizer(graph_dict).synthesize(
target=..., entities=..., goal=...) -> Plan
단위 테스트 13/13 pass (전체 plan 패키지 42/42):
- trivial (no required / entity-only)
- 2/3-step chain (semantic match, field_name fallback)
- unsatisfiable / unknown target / cycle / max_depth
- semantic 우선순위, self-producer 제외
Stage 1 — graph_tool_call.plan.intent
parse_intent(requirement, catalog, llm) -> ParsedIntent:
- LLM 1회로 {target, entities, confidence, output_shape} 구조화
- catalog 는 retrieval 상위 K개 ToolCatalogEntry (ai_metadata 활용:
one_line_summary / when_to_use / consumes_tags / canonical_action)
- target 이 catalog 에 없으면 IntentParseError (hallucination 차단)
- confidence 0~1 clamp, output_shape 검증 + single fallback
Stage 4 — graph_tool_call.plan.response
synthesize_success_response / synthesize_failure_response:
- ExecutionTrace → 자연어 답변 (한국어 기본)
- 성공/실패 프롬프트 분리 (실패 시 failed_step / error / partial 전달)
- char_limit 으로 큰 응답 truncate
…enrichment prompt PathSynthesizer 가 chain 결정 시 ``kind=data`` (비즈니스 값, producer chain 가능) 와 ``kind=context`` (ambient config, chain 거부 — entity 또는 collection default 만 사용) 를 분리해야 무관 chain (예: locale/siteNo 의 producer 까지 끌어오기) 을 막을 수 있다. enrichment prompt 에 두 분류 가이드를 명시해 LLM 이 새 도구의 consume 을 정확히 분류하게 한다. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hard constraints
Stage 1 (parse_intent) 의 4가지 robustness 보강:
* Vocabulary fuzzy validation — LLM 이 ``search_keyword`` 대신
``search_keyword_name`` 같은 인접 표현을 만들면 ratio≥0.8 로 coerce,
그 외엔 drop. 잘못된 entity key 는 downstream cycle / unsatisfied
field 로 이어져 silent 한 잘못된 plan 을 만든다.
* Multi-turn ``seed_entities`` 인자 — popup-driven 흐름에서 직전 turn
의 결정값을 carry forward. prompt 에 명시 + 코드 안전망 (LLM 이
seed 무시 시에도 합쳐 줌). 새 turn 의 명시 entity 는 같은 키의
seed 를 override.
* ``enum_mappings`` prompt section — 운영자가 등록한 ``{field: {code:
label}}`` 를 catalog scope 안에서만 노출 (전체 노출 시 prompt 폭주).
HC5 으로 enum field 는 코드 (left side) 만 entity 값으로 허용.
* Hard constraint 강화 — ``do not force-fit`` 같은 약한 부정 대신
``DO NOT`` 4개 (HC1-HC4) 명시: identifier 필드에 자연어 phrase
금지, vocab 외 키 발명 금지, 동일 값 여러 필드 금지, 값 변환 금지.
…eys hint
PlanRunner 가 step 응답을 context 에 저장하기 전에 한 단계 envelope 을
peel 하는 휴리스틱 추가. 흔한 backend 패턴 ``{code, message, payload:
{...}, timestamp}`` 에서 swagger 가 envelope 을 안 적었을 때, downstream
binding ``${s1.searchDataList[*].goodsNo}`` 가 ``payload`` 안의 데이터로
자연스럽게 풀리게 한다.
조건 (5가지 모두 충족 시에만 unwrap):
1. response 가 dict, root key 2개 이상
2. 정확히 1개의 dict-typed root value (wrapper 후보)
3. 나머지 root value 모두 scalar / null
4. expected_root_keys (= produces[].json_path 의 first segment) 가
response root 에 하나도 없음
5. wrapper 안에 expected_root_keys 중 하나라도 존재
조건이 strict 해서 false unwrap 은 거의 없음. wrapper 이름은 안 봄
(``payload``/``data``/``result`` 모두 동일하게 동작 — backend fit 아님).
PlanStep 에 ``response_root_keys: list[str]`` 필드 추가 — 합성 시점에
synthesizer 가 채워두면 runtime 에 unwrap detect 가 schema 비교
가능. 채워지지 않으면 unwrap skip (안전 default).
…option, enum popup
PathSynthesizer 가 ``produces`` 매칭만으로 chain 을 결정하던 흐름에
의미 신호를 단계적으로 도입. 핵심 변화 4가지:
* ``context_defaults`` 인자 — collection-level ambient values
(locale/site/tenant) 운영자가 한 번 등록 → ``kind=context`` 필드
자동 채움. swagger 가 optional 로 표시했지만 backend 가 사실상
필수로 요구하는 환경값을 cover.
* Chain eligibility filter — producer 가 ``canonical_action ∈
{search, read}`` 이고 ``primary_resource`` 가 target 의 도메인
(target.primary_resource + consumes semantic prefix) 안일 때만
chain. ``produces`` 매칭으로 무관 도구 (예: claim_cost calculator)
가 끌려오는 false positive 방지. ai_metadata 부분 enriched / 미
enriched 케이스는 fallback 통과해 graph quality 가 sparse 한
collection 도 회귀 없이 동작.
* Dynamic option detection — required-data field 의 producer 가
single-hop 으로 호출 가능 (모든 input 이 entity / context_defaults
로 채워짐) + canonical_action='read' + json_path 가 array 면
``DynamicOptionRequired`` (UnsatisfiableField 의 subclass) 를 던져
호출자가 producer 를 부분 실행해 옵션 list 를 사용자한테 popup.
chain 으로 임의로 [0] 인덱스 박는 패턴을 막고 사용자 의도 우선.
* ``enum_field_names`` — 운영자가 enum 매핑 등록한 field 는 chain
안 만들고 즉시 ``UnsatisfiableField`` raise (popup 으로 풀어야).
enum 코드 같은 환경 값을 chain 으로 풀 때 끌려오는 무관 도구
(응답에 같은 코드명 우연히 포함된 도구) 차단.
부수: ``response_root_keys`` 자동 채움 (PlanRunner 의 envelope unwrap
힌트) + producer ranking 정책 docstring 정리.
…ieval
Adds a graphify-mode pipeline alongside the existing retrieval engine — the
graph itself carries the relationship signal, so search and plan synthesis
need no embeddings and no per-query LLM calls. All decisions are decided at
ingest time and persisted as edge attrs.
Library
- Confidence enum (EXTRACTED / INFERRED / AMBIGUOUS); add_relation now
accepts confidence/conf_score/layer/evidence as optional kwargs (legacy
callers unchanged).
- New graph_tool_call.graphify subpackage:
* ingest_openapi_graphify(schemas, raw_spec=None) buckets each
DetectedRelation by layer + score and persists confidence per edge.
* preserve_refs_for_detection rescues layer-1 shared-schema signal that
ingest_openapi inlined (Spring/SpringDoc specs depend on this).
* _apply_pair_hints derives graphify edges from each tool's
ai_metadata.pairs_well_with (single source-of-truth: ai_metadata; edges
re-derived on every rebuild).
* retrieve_graphify: BM25-seeded confidence-weighted BFS, intent-aware
relation weighting, render_subgraph_text packages results into a
NODE/EDGE text block bounded by token_budget.
- ingest/openapi.py:
* Content-type fallback (application/*+json, */*, first available) for
response/request body schemas — needed for SpringDoc-emitted */* APIs.
* Wrapper-object/array query parameters (Spring @ModelAttribute) are
expanded into their inner properties or dropped when those properties
are already exposed as siblings.
- plan/synthesizer.py:
* _find_producer redesigned around combined graph + schema signals
(semantic_exact 100, graph_EXTRACTED 50, field_exact 40, ...) — graph
edges are first-class, not a fallback chain.
* Echo-back filter excludes producers that merely relay an input field.
* Loose field-name matching (case + separator folded) for
cross-naming-convention coverage.
* Cycle policy A: visiting set passed to _find_producer so cycle-prone
candidates are skipped and the chain reroutes around them.
* F2 + Cycle policy B: unmet required fields surface as
${user_input.<field>} placeholders instead of raising; recursion
failures (MaxDepth/Cyclic) on a producer fall through to the same
placeholder so plan synthesis never aborts midway.
* Plan.metadata.user_input_slots collects every placeholder for the
runner / UI to prompt with.
Tests passing across touched modules: ingest_openapi, dependency,
retrieval, plan, graph_engine.
lint:
- ruff check . 23건 수정 (E501/F401/F841/F402/N806/N818/I001/UP035)
- ruff format . 전체 적용
- examples/xgen_workflow_gateway.py 구문 오류 수정 (lambda **kwargs 위치)
- tests/test_dependency.py 중복 정의 제거
- tests/test_gateway_*.py 옵셔널 import 에 # noqa: E402
신규:
- graph_tool_call/ingest/io_contract.py — extract_leaves (xgen-workflow 의존)
- docs/* — api-reference / benchmarks / cli / integrations / roadmap
- benchmarks/results/models/{README,bonsai-8b-q1_0}.md
- .pre-commit-config.yaml
- examples/test_bonsai_tool_calling.py
기타:
- .gitignore — benchmarks/results/benchmark_*.json (자동 출력 무시)
main 의 lint/docs/pre-commit 작업과 우리 feature 브랜치 통합. 충돌은 main 우선 (-X theirs) — main 이 이미 lint/docs 정리 완료. benchmarks/results/benchmark_e2e_*.json 은 .gitignore 처리.
|
@da2sy-dev 님, 큰 PR 작성하시느라 고생하셨습니다. 설계 문서( 다만 머지 전에 함께 확인하면 좋을 결함을 몇 가지 발견했습니다. 직접 검증 가능한 재현 스크립트도 함께 첨부합니다. 🔴 CRITICAL #1 —
|
CRITICAL #1 — ${user_input.x} ↔ context["input"] 이름 불일치 해소 - PlanRunner 가 input_context 를 'input' / 'user_input' 두 키로 alias 등록. - synthesizer 의 F2/Cycle-policy fallback (${user_input.<field>}) 이 정상 resolve. - 영향: F2 fallback 으로 합성된 plan 이 첫 step 부터 abort 되던 회귀 fix. CRITICAL #2 — ExecutionTrace.steps 가 항상 빈 리스트 - PlanCompleted / PlanAborted 이벤트에 trace_steps 필드 추가. - run_stream 이 종결 이벤트에 누적된 StepTrace 를 실어 보냄. - run() 은 종결 이벤트의 trace_steps 를 추출해 ExecutionTrace.steps 채움. - 영향: run_stream 안 쓰는 caller 도 step 단위 trace 받을 수 있음. 단위 테스트 신규 추가 (42 PASS) — plan/graphify 모듈 cover - tests/test_plan_runner.py — CRITICAL #1, #2 회귀 테스트 + 핵심 동작 - tests/test_plan_synthesizer.py — 합성/체이닝/F2 fallback/normalize 등 - tests/test_plan_binding.py — placeholder resolution + 에러 동작 - tests/test_io_contract.py — extract_leaves + query/path enum 추출 회귀 - tests/test_dependency_verbs.py — _VERB_TO_INTENT 'reg' 매핑 기타 - synthesizer.py: _normalize_field_name 중복 정의 제거 (첫 정의가 dead code 였음). - dependency.py: _VERB_TO_INTENT 에 'reg' 추가 (regGoodsApprove → write). - io_contract.py: query/path/header parameter 의 enum 추출 (이전엔 body 만).
코드 리뷰의 _VERB_TO_INTENT['reg'] 누락과 동일 패턴 — sibling vocabulary 인 _ANNOTATION_BY_VERB (MCP annotation 추론용) 도 register 계열이 통째로 빠져 있었다. 동작 자체는 망가지지 않지만 registerUser / insertOrder 같은 도구가 MCP 클라이언트에 read_only_hint / destructive_hint 힌트 못 받음. 회귀 테스트 추가: 두 dict 간 register 계열 커버리지 일관성 검증.
CI 의 'ruff format --check .' 실패 해소. 동작 변경 없음.
- response.py: _SUCCESS_PROMPT에 count/total 처리 지침 추가 — totalCount 등 명시적 total 필드가 있으면 사용하고, 없으면 "N개 등록" 같은 단정 금지. result_char_limit 2000→4000으로 늘려 list 응답 truncate 완화. - ingest/openapi.py: Swagger 2 / OpenAPI 3 둘 다 path 파라미터를 무조건 required=True로 마킹. 많은 spec이 명시 안 해도 URL placeholder라 호출 시 반드시 값 필요. synthesizer가 빈 entity로 plan 생성하던 회귀 차단.
1. Plan-and-Execute 합성기 추가 (
graph_tool_call/plan/)4-stage 파이프라인: Intent → Synthesize → Execute → Response.
metadata.produces/consumes의 backward chain 으로 prereq 자동 합성.왜: 도구 수가 많은 (수백개) API 오케스트레이션에서 ReAct 의 한계 — 매 step LLM 호출 → 비용·속도·audit 측면 모두 약함. PathSynthesizer 는 그래프가 이미 가진
produces/consumes를 backward 추적해 chain 생성, LLM 호출 0회 (Stage 2/3). 결정론 → 같은 입력에 같은 plan, audit 가능.2. Graphify 모드 (
graph_tool_call/graphify/)Confidenceenum (EXTRACTED/INFERRED/AMBIGUOUS) — 모든 엣지에 신뢰도 라벨ingest_openapi_graphify—detect_dependencies결과 +ai_metadata.pairs_well_with를 graph edge 로 통합 (단일 source-of-truth)retrieve_graphify— BM25 시드 → confidence-가중 BFS → token-budgeted text. 임베딩 0.왜: 기존
RetrievalEngine14단계 (BM25 + embedding + wRRF + reranker + MMR + 다수 boost) 는 신호 채널 충돌이 잦음. 코드 코멘트도 인정 — "Graph noise from degrading BM25/embedding precision" (engine.py:206) — 결국 graph 가 wRRF 에서 분리되어 candidate injection 만으로 격리됨.대안 대비 채택 이유: 임베딩 기반은 매 쿼리 모델 호출 + 인덱스 동기화 부담. graphify 는 build-time 한 번 신뢰도 부여 → 검색은 그래프 traversal 만. 단순 + audit 가능.
호환성: 기존
RetrievalEngine그대로 유지. graphify 는 opt-in.3. Layer 4 RPC pattern detector (
analyze/dependency.py)왜: 기존 layer 1~3 은 REST 가정 (path-hierarchy /
$ref/ CRUD). Spring/SpringDoc 의 RPC 스타일 (/v1/basket/optionChange, 동사+명사 path) 에서는 거의 못 잡음.Layer 4: controller prefix 자동 감지 + DTO 매칭으로 같은 controller 의 도구를
INFERRED로 묶음.4.
ingest_openapicontent-type fallback왜: 기존 코드가 응답 스키마를
application/json만 봄. SpringDoc 이 default 로 emit 하는*/*미디어타입에서 응답 스키마 추출 0건 —produces인덱스 비어 PathSynthesizer 가 prereq 을 영원히 못 찾는 결함.Fallback 순서:
application/json→application/*+json→*/*→ 첫 미디어타입.5. Wrapper-DTO query parameter 처리
왜: Spring
@ModelAttribute가 wrapper 객체와 inner 필드를 query param 으로 동시 노출 (예:searchRequest(object) +searchKeyword(inner)). 현재 코드가 wrapper 자체를ToolParameter로 잡으면 PathSynthesizer 가 wrapper 의 producer 를 영원히 못 찾음.type=object/type=array of object인 query parameter 가 inner properties 를 sibling 으로 노출하면 wrapper 제거, 아니면 inner 로 펼침.호환성
전부 additive:
add_relation의 새 kwargs (confidence,conf_score,layer,evidence) 모두 optional, defaultNonegraph_dictJSON 형태 그대로, edge attrs 만 새 필드 추가검증