Skip to content

Feature/rpc detection and fixes#1

Merged
SonAIengine merged 15 commits into
mainfrom
feature/rpc-detection-and-fixes
May 6, 2026
Merged

Feature/rpc detection and fixes#1
SonAIengine merged 15 commits into
mainfrom
feature/rpc-detection-and-fixes

Conversation

@da2sy-dev
Copy link
Copy Markdown
Collaborator

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/)

  • Confidence enum (EXTRACTED / INFERRED / AMBIGUOUS) — 모든 엣지에 신뢰도 라벨
  • ingest_openapi_graphifydetect_dependencies 결과 + ai_metadata.pairs_well_with 를 graph edge 로 통합 (단일 source-of-truth)
  • retrieve_graphify — BM25 시드 → confidence-가중 BFS → token-budgeted text. 임베딩 0.

: 기존 RetrievalEngine 14단계 (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_openapi content-type fallback

: 기존 코드가 응답 스키마를 application/json 만 봄. SpringDoc 이 default 로 emit 하는 */* 미디어타입에서 응답 스키마 추출 0건 — produces 인덱스 비어 PathSynthesizer 가 prereq 을 영원히 못 찾는 결함.

Fallback 순서: application/jsonapplication/*+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, default None
  • graph_dict JSON 형태 그대로, edge attrs 만 새 필드 추가
  • 기존 라이브러리 사용자 영향 0

검증

  • 라이브러리 단위 테스트: 64 PASS (graphify 신규 29 + 기존 35)
  • 실제 OpenAPI 3.1 spec (184 endpoint, RPC 스타일) 으로 검증
    • retrieval top-5 hit rate: 8/8 (자연어 쿼리)
    • latency: avg 4ms / p95 21ms

daehee and others added 9 commits April 7, 2026 18:11
- 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.
@da2sy-dev da2sy-dev requested a review from SonAIengine April 29, 2026 15:38
da2sy-dev added 2 commits May 3, 2026 07:58
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 처리.
@SonAIengine
Copy link
Copy Markdown
Owner

@da2sy-dev 님, 큰 PR 작성하시느라 고생하셨습니다. 설계 문서(docs/architecture-plan-and-execute.md)와 모듈 구조가 깔끔해서 의도 파악이 쉬웠습니다.

다만 머지 전에 함께 확인하면 좋을 결함을 몇 가지 발견했습니다. 직접 검증 가능한 재현 스크립트도 함께 첨부합니다.

🔴 CRITICAL #1${user_input.x}context["input"] 이름 불일치

PathSynthesizer의 F2 fallback 정책(synthesizer.py:389, 443)으로 합성되는 ${user_input.field} placeholder가 PlanRunnerinput_context 매핑(runner.py:154)과 어긋납니다.

재현:

from graph_tool_call.plan import Plan, PlanStep, PlanRunner

plan = Plan(id="t", goal="g", steps=[
    PlanStep(id="s1", tool="echo", args={"foo": "${user_input.foo}"}),
], output_binding="${s1}")

runner = PlanRunner(lambda n, a: {"echoed": a})
events = list(runner.run_stream(plan, input_context={"foo": "BAR"}))
# step.failed: "unknown source 'user_input' in binding ${...}: context has ['input']"

영향: cycle/unsatisfiable 처리 정책(Cycle policy A/B + F2)으로 만들어진 모든 plan이 첫 step부터 abort됩니다. 도구 호출 0회.

수정 방향: runner가 context["user_input"]도 받게 하거나, synthesizer가 ${input.field}로 합성하거나, input_context 외에 별도 user_input 매개변수 추가.

🔴 CRITICAL #2 — PlanRunner.run().steps 항상 빈 리스트

runner.py:289-291pass로 솔직히 표시해주신 부분인데, ExecutionTrace 스키마(steps: list[StepTrace]) 약속과 맞지 않습니다. run_stream을 쓰지 않는 caller는 step별 trace를 받을 수 없습니다.

재현:

plan = Plan(id="t", goal="g", steps=[
    PlanStep(id="s1", tool="echo", args={"x": "hello"}),
    PlanStep(id="s2", tool="echo", args={"y": "${s1.x}"}),
], output_binding="${s2}")
trace = PlanRunner(lambda n, a: a).run(plan)
print(trace.success, trace.steps)  # True, []

수정 방향: StepCompleted/StepFailed 이벤트에 StepTrace를 첨부해서 run이 누적할 수 있게 하거나, 별도 mutable list로 streaming 중 누적.

🟡 단위 테스트 누락

PR 본문은 "라이브러리 단위 테스트: 64 PASS (graphify 신규 29 + 기존 35)"라고 명시되어 있는데, 실제 PR diff의 tests/ 변경에는 신규 모듈(plan/, graphify/)을 import하는 테스트가 없습니다:

$ grep -rln "from graph_tool_call.plan\|from graph_tool_call.graphify" tests/
# 0 hits

신규 5,029줄 코드에 단위 테스트가 0개라 위 두 CRITICAL 결함이 CI를 통과했습니다. 신규 테스트 파일 commit이 누락된 건 아닌지 확인 부탁드립니다.

🟡 함수 중복 정의

plan/synthesizer.py:93:114_normalize_field_name이 두 번 정의되어 있어 첫 번째가 dead code입니다. 두 구현이 사실상 동일하지만 한 정의만 남기는 게 좋겠습니다.

🟢 작은 항목

  • _VERB_TO_INTENTreg (register 약어) 누락 — regGoodsApprove('approve', '') 로 잘못 분류
  • io_contract.py의 query/path parameter consume 추출 시 enum 정보 누락 (body 추출은 OK) — Pass 2 LLM이 가능 값 추정에 활용 못 함

잘 된 점

  • 설계 깊이 우수 (의사결정 근거 + 마이그레이션 + 운영 리스크 매트릭스까지 정리)
  • Echo-back filter, chain-eligibility, envelope unwrapping 같은 미묘한 디테일을 정확히 인지
  • graceful degradation 정책 (cycle → 다른 producer → user_input fallback) — binding 결함만 고치면 정말 좋은 디자인
  • 모든 변경 additive, transport-agnostic, v1 scope 명시적 한정 (premature abstraction 회피)

머지 전 부탁드릴 작업

  1. CRITICAL Feature/rpc detection and fixes #1, #2 수정
  2. 신규 모듈 단위 테스트 추가 (PR 본문 약속한 64개 — 위 두 결함 cover하는 케이스 포함)
  3. _normalize_field_name 중복 정의 제거

수고하셨습니다. 수정 후 다시 리뷰하겠습니다.

da2sy-dev added 4 commits May 3, 2026 18:00
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 생성하던 회귀 차단.
@SonAIengine SonAIengine merged commit 98b353d into main May 6, 2026
6 checks passed
da2sy-dev added a commit that referenced this pull request May 6, 2026
PR #1 (feature/rpc-detection-and-fixes) 머지 후 minor bump.
주요 변경:
  - feat(plan): Stage 4 prompt count 정확성 + path param required 강제
  - fix(core/tool): _ANNOTATION_BY_VERB 에 register/regist/reg/insert 추가
  - 코드 리뷰 결함 반영 (CRITICAL #1/#2 + 단위 테스트)

publish.yml 이 GitHub Release 발행 시 PyPI 에 자동 업로드.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants