Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c164b51
perf(rag): KB 검색 fan-out을 제한 병렬화
HyungminYoon1 Jul 22, 2026
120d120
docs(rag): fan-out 실행과 지연 계측 계약 정리
HyungminYoon1 Jul 22, 2026
287742a
test(ci): Gateway의 Workflow runtime import 경계 검증
HyungminYoon1 Jul 22, 2026
93744c2
fix(ci): LLM node runtime import를 지연 로딩
HyungminYoon1 Jul 22, 2026
56959b4
fix(rag): fan-out 프로세스 자원 경계 보강
HyungminYoon1 Jul 23, 2026
ed111a9
test(rag): safe-no-result 지연 비노출 경계 보강
HyungminYoon1 Jul 23, 2026
c9a3b65
docs(rag): fan-out 운영 및 추적 경계 갱신
HyungminYoon1 Jul 23, 2026
70c32cd
fix(rag): 후속 SQL 취소와 절대 기한을 강제
HyungminYoon1 Jul 23, 2026
b14d771
docs(rag): SQL별 절대 기한 계약을 명시
HyungminYoon1 Jul 23, 2026
efcd1c8
fix(rag): DB checkout와 주입 세션 경계를 보강
HyungminYoon1 Jul 23, 2026
47c621d
docs(rag): checkout 및 주입 세션 계약을 명시
HyungminYoon1 Jul 23, 2026
305be27
fix(ci): Gateway와 RAG worker import 경계를 분리
HyungminYoon1 Jul 23, 2026
80d6904
docs(rag): CI import 경계 검증 기준을 갱신
HyungminYoon1 Jul 23, 2026
ff076ee
fix(rag): fan-out admission과 계약 CI 선택을 보강
HyungminYoon1 Jul 26, 2026
236ce4b
docs(rag): fan-out admission과 CI 계약을 명시
HyungminYoon1 Jul 26, 2026
0dcc923
test(rag): public history 검색 계약을 최신화
HyungminYoon1 Jul 29, 2026
2b485b9
docs(rag): 최신 dev 검증 기준을 갱신
HyungminYoon1 Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test-knowledge-runtime-postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ on:
- "apps/shared/tests/domain/test_knowledge_runtime_candidates.py"
- "apps/shared/tests/services/test_knowledge_permission_runtime_bulk.py"
- "apps/workflow_engine/adapters/knowledge_runtime_candidates.py"
- "apps/workflow_engine/adapters/rag_retrieval_connection_acquirer.py"
- "apps/workflow_engine/adapters/rag_retrieval_executor.py"
- "apps/workflow_engine/adapters/rag_retrieval_session.py"
- "apps/workflow_engine/application/rag_retrieval_fanout.py"
- "apps/workflow_engine/application/runtime_retrieval/**"
- "apps/workflow_engine/composition/runtime_retrieval.py"
- "apps/workflow_engine/tests/adapters/test_postgres_knowledge_runtime_candidate_adapter.py"
- "apps/workflow_engine/tests/adapters/test_rag_retrieval_session_postgres.py"
- ".github/workflows/test-knowledge-runtime-postgres.yml"

permissions:
Expand Down Expand Up @@ -95,4 +100,5 @@ jobs:
apps/workflow_engine/.venv/bin/python -m pytest
apps/shared/tests/db/test_knowledge_runtime_snapshot_disposable_postgres.py
apps/workflow_engine/tests/adapters/test_postgres_knowledge_runtime_candidate_adapter.py
apps/workflow_engine/tests/adapters/test_rag_retrieval_session_postgres.py
-q
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Gateway와 Workflow worker 패키지의 import 경계를 검증한다."""

from __future__ import annotations

import os
from pathlib import Path
import subprocess
import sys


REPOSITORY_ROOT = Path(__file__).resolve().parents[4]


def test_llm_entity_import_does_not_load_worker_runtime() -> None:
"""Data-only schema import must not require worker-only dependencies."""

script = """
import sys

from apps.workflow_engine.workflow.nodes.llm.entities import LLMNodeData

assert LLMNodeData.__name__ == "LLMNodeData"
assert "apps.workflow_engine.workflow.nodes.llm.llm_node" not in sys.modules
"""
environment = os.environ.copy()
existing_pythonpath = environment.get("PYTHONPATH")
environment["PYTHONPATH"] = os.pathsep.join(
value
for value in (str(REPOSITORY_ROOT), existing_pythonpath)
if value
)

completed = subprocess.run(
[sys.executable, "-c", script],
cwd=REPOSITORY_ROOT,
env=environment,
capture_output=True,
text=True,
check=False,
)

assert completed.returncode == 0, completed.stderr


def test_node_factory_import_does_not_require_gevent_runtime() -> None:
"""Gateway-side graph validation must not require worker-only gevent."""
Comment on lines +45 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gateway import 경계 테스트를 관련 소스 변경에도 실행하세요

apps/workflow_engine/workflow/nodes/llm/__init__.py나 새 RAG 어댑터만 수정하는 후속 PR은 classify_paths()에서 workflow_tests만 선택하므로, apps/gateway/tests에 둔 이 회귀 테스트는 실행되지 않습니다. 그 결과 LLMNode가 다시 eager import되어 Gateway에 worker 전용 gevent 의존성이 유입되는 변경도 선택 CI를 통과할 수 있으므로, 이 테스트를 Workflow Engine 선택 대상에 두거나 해당 import-boundary 소스가 바뀌면 Gateway 테스트도 선택해야 합니다.

AGENTS.md reference: AGENTS.md:L206-L207

Useful? React with 👍 / 👎.


script = """
import sys


class BlockGeventImport:
def find_spec(self, fullname, path=None, target=None):
if fullname == "gevent" or fullname.startswith("gevent."):
raise ModuleNotFoundError("blocked worker-only gevent dependency")
return None


sys.meta_path.insert(0, BlockGeventImport())

from apps.workflow_engine.workflow.core.workflow_node_factory import NodeFactory

assert "llmNode" in NodeFactory.NODE_REGISTRY
assert "gevent" not in sys.modules
"""
environment = os.environ.copy()
existing_pythonpath = environment.get("PYTHONPATH")
environment["PYTHONPATH"] = os.pathsep.join(
value
for value in (str(REPOSITORY_ROOT), existing_pythonpath)
if value
)

completed = subprocess.run(
[sys.executable, "-c", script],
cwd=REPOSITORY_ROOT,
env=environment,
capture_output=True,
text=True,
check=False,
)

assert completed.returncode == 0, completed.stderr
21 changes: 21 additions & 0 deletions apps/shared/services/tracing/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,11 @@
"total_tokens",
},
"rag": {
"candidate_resolution_latency_ms",
"citation_ids",
"context_token_estimate",
"document_ids",
"evidence_policy_latency_ms",
"evidence_sufficient",
"fanout_concurrency",
"fanout_timeout_seconds",
Expand All @@ -262,10 +264,12 @@
"permission_filter_applied",
"latency_ms",
"partial_result",
"query_embedding_latency_ms",
"query_rewrite_applied",
"query_rewrite_strategy",
"raw_content_returned",
"rag_mode",
"retrieval_fanout_latency_ms",
"retrieval_payload_id",
"retrieval_strategy",
"retrieved_chunk_summary_truncated",
Expand All @@ -275,6 +279,7 @@
"score_summary",
"selected_kb_count",
"selected_kb_count_bucket",
"slowest_search_latency_ms",
"source_tier_policy",
"source_tier_used",
"stored_result_count",
Expand Down Expand Up @@ -303,6 +308,14 @@
"latency_ms",
},
}
RAG_STAGE_LATENCY_FIELDS = {
"candidate_resolution_latency_ms",
"query_embedding_latency_ms",
"retrieval_fanout_latency_ms",
"slowest_search_latency_ms",
"evidence_policy_latency_ms",
}
MAX_RAG_STAGE_LATENCY_MS = 300_000
RAG_RESULT_FIELDS = {
"document_id",
"chunk_id",
Expand Down Expand Up @@ -780,6 +793,14 @@ def _sanitize_rag_section(cls, value: Any) -> dict[str, Any]:
sanitized: dict[str, Any] = {}
for key in SPAN_SECTION_FIELDS["rag"]:
if key in safe_value:
if key in RAG_STAGE_LATENCY_FIELDS:
latency = safe_value[key]
if (
type(latency) is int
and 0 <= latency <= MAX_RAG_STAGE_LATENCY_MS
):
sanitized[key] = latency
continue
sanitized_value = cls._sanitize_allowed_value(safe_value[key])
if sanitized_value is not None:
sanitized[key] = sanitized_value
Expand Down
37 changes: 37 additions & 0 deletions apps/shared/tests/services/test_tracing_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,43 @@ def test_rag_span_metadata_preserves_evidence_summary_fields_only():
assert "raw_rewritten_query" not in metadata["rag"]


def test_rag_span_metadata_allows_only_bounded_integer_stage_latencies():
metadata = TraceMetadataSanitizer.sanitize_span_metadata(
"llmNode",
{
"rag": {
"candidate_resolution_latency_ms": 0,
"query_embedding_latency_ms": 12,
"retrieval_fanout_latency_ms": 34,
"slowest_search_latency_ms": 56,
"evidence_policy_latency_ms": 300_000,
"per_kb_latency_ms": {"hidden-resource": 99},
}
},
)

assert metadata["rag"] == {
"candidate_resolution_latency_ms": 0,
"query_embedding_latency_ms": 12,
"retrieval_fanout_latency_ms": 34,
"slowest_search_latency_ms": 56,
"evidence_policy_latency_ms": 300_000,
}


@pytest.mark.parametrize(
"invalid_value",
[True, -1, 1.5, "12", float("nan"), float("inf"), 300_001],
)
def test_rag_span_metadata_drops_invalid_stage_latency(invalid_value):
metadata = TraceMetadataSanitizer.sanitize_span_metadata(
"llmNode",
{"rag": {"retrieval_fanout_latency_ms": invalid_value}},
)

assert metadata.get("rag", {}) == {}


def test_trace_detail_metadata_view_hides_error_message_and_sanitizes_metadata():
run = SimpleNamespace(
id=uuid.uuid4(),
Expand Down
Loading
Loading