Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ permissions:
contents: read

jobs:
demo-scripts:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Check out repository
uses: actions/checkout@v7
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false
- name: Test demo decisions without external services
run: python3 -m unittest discover -s scripts/demo -p 'test_*.py' -v

backend:
runs-on: ubuntu-latest
timeout-minutes: 15
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,15 @@ python3 scripts/demo/seed_demo_corpus.py
- 기본 계정은 `demo@docgrid.local`이며 비밀번호(12자 이상)를 입력받습니다. 계정이 없으면 그 비밀번호로
가입하고, 있으면 로그인합니다. `--email`(`DOCGRID_DEMO_EMAIL`)과 `DOCGRID_DEMO_PASSWORD`로 바꿀 수 있습니다.
- 파일 해시로 만든 `Idempotency-Key`를 보내므로 다시 실행해도 같은 계정에 문서가 중복 생성되지 않습니다.
- INDEXED까지 최대 10분(`--timeout`) 기다린 뒤 요약과 예시 검색 1건을 출력합니다. 진행되지 않으면 Worker
- INDEXED까지 최대 10분(`--timeout`) 기다린 뒤 고정 질문 3개의 기대 문서와 원본 15건의 해시를 검사합니다. 진행되지 않으면 Worker
꺼짐, 임베딩 서버 미준비 같은 원인 후보를 안내합니다. 전체 옵션은 `--help`로 확인합니다.

2026-10-16 새 DB에서 실행한 출력입니다(MacBook Pro M5, BGE-M3 준비 완료, Ollama 미실행). `…`는 생략한 부분입니다.
현재 기본 범위는 `search`이며 생성 답변은 미검증으로 표시합니다. 검색 실패·빈 결과·원본 해시 불일치는
실패 종료합니다. 실제 생성 답변·인용까지 검사하려면 `--scope rag`를 지정합니다.
실행별 원장은 `.demo/reports/<run-id>/`에 저장하며, 자세한 판정은 [예제 검증 안내](examples/demo-corpus/README.md)를 참고하세요.

아래는 판정 개선 전인 2026-10-16 새 DB에서 실행한 당시 출력입니다(MacBook Pro M5, BGE-M3 준비 완료,
Ollama 미실행). 현재 출력 형식과 다르며, `…`는 생략한 부분입니다.

```text
[계정] demo@docgrid.local 회원가입 완료
Expand All @@ -322,7 +327,7 @@ INDEXED: 15건 · 인덱싱 실패: 0건 · 시간 안에 끝나지 않음: 0건
[RAG] LLM 답변 생성 실패 → 대체 답변 표시(2.0초 대기): AI 답변 생성이 지연되고 있습니다. 가장 관련도 높은 문서에서 다음 내용을 찾았습니다: "재택근무 운영 규정 … 제3조(재택근무 횟수) 1. 재택근무는 주 2일까지 허용한다. …
```

Ollama와 `qwen2.5:7b`가 준비되어 있으면 마지막 줄이 `[RAG] 답변 생성 완료(…초 대기): …`로 바뀝니다.
`--scope rag`에서 Ollama와 `qwen2.5:7b`가 준비되어 있으면 실제 답변·인용 검사 결과를 확인할 수 있습니다.
같은 계정이나 새로 가입한 계정으로 웹(`http://localhost:3000`)에 로그인해 다음 질문을 검색해 봅니다.
예제 문서는 PUBLIC으로 올라가므로 모든 로그인 사용자가 검색할 수 있습니다.

Expand Down
43 changes: 43 additions & 0 deletions docs/design/demo-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 예제 시연 검증과 실행 원장

## 목적과 경계

예제 15건이 INDEXED여도 검색 HTTP 오류나 빈 결과를 성공으로 종료하던 판정을 수정한다.
기존 인증·멱등 업로드·인덱싱 대기는 재사용하며, 생성 모델의 품질 평가나 OpenSQL HA 검증은 포함하지 않는다.

## 판정

| 단계 | search 필수 | rag 필수 | 통과 기준 |
| --- | --- | --- | --- |
| 업로드 | 예 | 예 | 서로 다른 문서 ID 15건 |
| 인덱싱 | 예 | 예 | INDEXED 15건 |
| 검색 | 예 | 예 | 고정 질문 3개에서 기대 문서가 topK 3 안에 포함, DOCX 표 본문 확인 |
| 원본 | 예 | 예 | 파일 API 바이트 15건의 길이·SHA-256 일치 |
| 생성 답변 | 아니오 | 예 | SUCCESS·비어 있지 않은 답변·검색 후보 문서/Chunk에 속한 인용 |
| 재실행 | 이전 원장 지정 시 | 이전 원장 지정 시 | 같은 대상·계정·예제 fingerprint와 문서 ID 일치 |

PASS/FAIL/NOT_RUN을 구분하며 필수 단계가 NOT_RUN이어도 성공 종료하지 않는다.
search 범위는 RAG 검증을 생략하는 것이며 백엔드 RAG Worker를 비활성화하지 않는다.
`--query`를 지정하면 고정 기대 문서 검증 대신 사용자 질문 1건의 비어 있지 않은 결과만 검사한다.
따라서 이 경우 고정 검색 3/3 통과를 주장하지 않는다.

## 실행 순서

```text
예제 파일·checks.json 고정
→ 임베딩 준비 확인
→ 기존 계정 준비·파일 해시 키 업로드
→ INDEXED 대기
→ 기대 검색·본문 + 파일 API 해시 대조
→ 선택 RAG / 선택 이전 문서 ID 비교
→ 필수 단계 전체 PASS일 때만 종료 코드 0
```

## 원장과 민감정보

- 실행마다 `.demo/reports/<run-id>/`를 새로 만든다. 지정한 결과 디렉터리가 이미 있으면 거부한다.
- `events.jsonl`은 안전한 상태 이벤트를 즉시 flush하고 `summary.json`은 최종 판정·건수·시간·문서 ID를 보관한다.
- 예제·검증 질문 fingerprint, 소스 revision, topK와 실행 범위를 남긴다. 실제 모델 revision은 이 원장만으로 증명하지 않는다.
- 대상 주소·계정은 원문 대신 비교용 fingerprint를 사용한다. 원장은 기본적으로 로컬 Git 제외 경로에 둔다.
- 비밀번호·토큰·이메일·서버 URL은 출력 전에 제거한다. 검색 원문·모델 답변·서버 임의 오류 본문을 기록하지 않는다.
- 문서 ID 재사용은 해당 API 결과 비교이며, 이 검사만으로 DB 내부 Job 중복 0을 주장하지 않는다.
44 changes: 44 additions & 0 deletions docs/test-results/demo-verification-20261018/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 예제 시연 판정 회귀 검증

## 목적

예제 인덱싱이 완료됐어도 검색 HTTP 오류·빈 결과·원본 불일치가 있으면 실패 종료해야 한다.
수정 전 잘못된 성공을 재현한 뒤 선택한 범위의 필수 단계로 종료 코드를 결정하도록 수정했다.

## 테스트 환경

- 로컬 Python 3.9.6, 수정 전 `b6ee78bfebfcc8d23b4ee039e2852faf6679fb98`.
- 수정 구현 `775d64f`, 최종 회귀 소스 `defe0ec`.
- 실제 예제 파일 15건(PDF 5·DOCX 5·HWPX 5)을 읽으며 API는 제어 응답으로 대체한다.
- 실제 DB·BGE-M3·Ollama 호출은 0건이다. 실제 모델 E2E나 원격 OpenSQL HA 검증이 아니다.

## 테스트 방법

```bash
python3 -m unittest discover -s scripts/demo -p 'test_*.py' -v
```

1. 수정 전 스크립트에 인덱싱 완료·검색 HTTP 500 또는 빈 결과를 주입한다.
2. 수정 후 같은 오류, 기대 문서 누락·표 본문 누락·파일 불일치·RAG 실패/미완료/잘못된 인용을 검사한다.
3. 정상 search, 제어된 rag 성공, 이전 문서 ID 비교, 기존 원장 보존, 출력 전 마스킹, HTTP 오류 코드와 크기 제한을 확인한다.

## 관측 결과

| 실행 ID | 목적 | 통과 / 실패 | 판정과 근거 |
| --- | --- | --- | --- |
| demo-verdict-baseline-01 | 수정 전 거짓 성공 | 0 / 2 | 실제 반환값이 두 경우 모두 0. [수정 전 결함 재현](baseline-01.md) |
| demo-verdict-regression-01 | 초기 판정 회귀 | 28 / 0 | [초기 로그](demo-verdict-regression-01.log) |
| demo-verdict-regression-02 | HTTP 경계 추가 | 30 / 0 | [독립 로그](demo-verdict-regression-02.log) |
| demo-verdict-regression-03 | ID 형식 보완 후 최종 회귀 | 32 / 0 | [최종 로그](demo-verdict-regression-03.log) |

정상 search 제어 회차는 인덱싱 15/15·고정 검색 3/3·원본 바이트 대조 15/15, RAG NOT_RUN으로 통과했다.
실제 모델을 통한 수치가 아니라 제어 응답의 판정 로직 결과다.

## 검증 과정에서 확인된 사항

- 원래 최종 판정은 INDEXED 개수만 보았으며, 검색 함수는 HTTP 오류를 출력하고 반환했다.
- HTTP 오류 본문을 모두 버리면 회원가입 판단에 필요한 USER-004 코드도 사라지므로, bounded 응답에서 코드를 해석하되 임의 메시지는 기록하지 않는다.
- 응답 직후 INDEXED라는 이유만으로 이전 업로드라고 추정하지 않고 이전 실행 문서 ID를 직접 비교한다.
- 로그는 생성 답변·검색 원문을 저장하지 않는다. 마스킹 시험은 비밀값·이메일·URL이 기록 전에 제거됐는지 확인한다.
- 이 회차는 Python 판정 회귀만 실행했다. 새 환경 실제 시연·원격 서비스·생성 모델·DB 내부 Job 수는 미검증이다.
- 원본 개인 환경 파일·기존 Docker 자원·공개 서비스에 변경을 가하지 않았다. 시험 임시 파일은 실행별 임시 디렉터리에서 정리했다.
12 changes: 12 additions & 0 deletions docs/test-results/demo-verification-20261018/baseline-01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 수정 전 검색 거짓 성공 재현

- 실행 ID: `demo-verdict-baseline-01`
- 원본 revision: `b6ee78bfebfcc8d23b4ee039e2852faf6679fb98`
- 목적: 검색 HTTP 500·빈 결과가 성공 종료하는지 확인
- 환경: 로컬 Python 3.9.6, 제어 API 응답, 실제 DB·모델 호출 0건
- 로그 최종 기록 시각: 2026-10-18T14:13:13+09:00
- 방법: `baseline_verdict.py <수정 전 seed 파일>` 실행. 인덱싱 완료 1건을 제어하고 각 검색 응답을 주입
- 합격 기준: 각 검색 오류에서 종료 코드가 0이 아니어야 함
- 관측: 두 조건 모두 실제 반환값 0. 회귀 요구 검사 2개 중 통과 0·실패 2
- 해석: 수정 전 결함 재현 성공. 수정 전 코드가 요구 기준을 통과한 것은 아님
- 원본 로그: [baseline](demo-verdict-baseline-01.log)
44 changes: 44 additions & 0 deletions docs/test-results/demo-verification-20261018/baseline_verdict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""현재 seed의 종료 판정을 제어 응답으로 재현하며 실제 API는 호출하지 않는다."""
import contextlib
import importlib.util
import io
import sys
import unittest
from pathlib import Path
from unittest.mock import patch

path = Path(sys.argv.pop(1))
spec = importlib.util.spec_from_file_location('seed_baseline', path)
seed = importlib.util.module_from_spec(spec)
spec.loader.exec_module(seed)


class VerdictRegression(unittest.TestCase):
"""인덱싱 완료 뒤 검색 실패를 전체 실패로 판정해야 한다."""

def assert_search_failure(self, status, payload):
client = unittest.mock.Mock()
client.base_url = 'http://localhost:8080'
client.call.return_value = (200, {'data': {'documentStatus': 'INDEXED',
'currentVersion': {'status': 'INDEXED'}, 'processingVersion': None}})
client.call_json.return_value = (status, payload)
with contextlib.ExitStack() as stack:
stack.enter_context(contextlib.redirect_stdout(io.StringIO()))
for name, value in [('load_manifest', [{'file': 'fixture.pdf', 'title': '합성 문서'}]),
('DocGridClient', client), ('embedding_ready', True),
('authenticate', None), ('read_password', 'synthetic-only'),
('upload', {'documentId': 1}),
('wait_for_indexing', ({1: 'INDEXED'}, {1}))]:
stack.enter_context(patch.object(seed, name, return_value=value))
result = seed.run(['--skip-embedding-check', '--rag-wait', '0'])
self.assertNotEqual(result, 0)

def test_search_http_error_must_fail(self):
self.assert_search_failure(500, {'code': 'SYNTHETIC-ERROR'})

def test_empty_results_must_fail(self):
self.assert_search_failure(200, {'data': {'results': [], 'ragStatus': 'FAILED'}})


if __name__ == '__main__':
unittest.main(verbosity=2)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
test_empty_results_must_fail (__main__.VerdictRegression) ... FAIL
test_search_http_error_must_fail (__main__.VerdictRegression) ... FAIL

======================================================================
FAIL: test_empty_results_must_fail (__main__.VerdictRegression)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<로컬 시험 도구>/baseline_verdict.py", line 40, in test_empty_results_must_fail
self.assert_search_failure(200, {'data': {'results': [], 'ragStatus': 'FAILED'}})
File "<로컬 시험 도구>/baseline_verdict.py", line 34, in assert_search_failure
self.assertNotEqual(result, 0)
AssertionError: 0 == 0

======================================================================
FAIL: test_search_http_error_must_fail (__main__.VerdictRegression)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<로컬 시험 도구>/baseline_verdict.py", line 37, in test_search_http_error_must_fail
self.assert_search_failure(500, {'code': 'SYNTHETIC-ERROR'})
File "<로컬 시험 도구>/baseline_verdict.py", line 34, in assert_search_failure
self.assertNotEqual(result, 0)
AssertionError: 0 == 0

----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=2)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
test_changed_target_refuses_replay_before_upload (test_demo_verdict.DemoVerdictTest) ... ok
test_citation_outside_search_candidates_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_different_previous_document_id_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_download_hash_mismatch_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_empty_search_results_fail (test_demo_verdict.DemoVerdictTest) ... ok
test_event_redaction_happens_before_storage (test_demo_verdict.DemoVerdictTest) ... ok
test_existing_report_is_never_overwritten (test_demo_verdict.DemoVerdictTest) ... ok
test_full_search_scope_passes_without_rag_polling (test_demo_verdict.DemoVerdictTest) ... ok
test_missing_expected_document_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_previous_document_ids_are_compared (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_failed_is_not_success (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_incomplete_is_not_success (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_scope_requires_success_and_valid_citations (test_demo_verdict.DemoVerdictTest) ... ok
test_search_http_error_fails_after_all_documents_indexed (test_demo_verdict.DemoVerdictTest) ... ok
test_table_text_missing_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_unrun_required_stages_cannot_pass (test_demo_verdict.DemoVerdictTest) ... ok
test_claimed_or_retrying_job_is_processing (test_seed_demo_corpus.ClassifyTest) ... ok
test_failed_job (test_seed_demo_corpus.ClassifyTest) ... ok
test_indexed_document (test_seed_demo_corpus.ClassifyTest) ... ok
test_new_upload_waits_for_worker (test_seed_demo_corpus.ClassifyTest) ... ok
test_embedding_hint_first_when_server_not_ready (test_seed_demo_corpus.DiagnoseTest) ... ok
test_embedding_hint_for_failed_documents (test_seed_demo_corpus.DiagnoseTest) ... ok
test_no_hint_when_everything_indexed (test_seed_demo_corpus.DiagnoseTest) ... ok
test_slow_hint_when_worker_and_server_are_up (test_seed_demo_corpus.DiagnoseTest) ... ok
test_worker_hint_when_no_job_started (test_seed_demo_corpus.DiagnoseTest) ... ok
test_different_bytes_produce_different_keys (test_seed_demo_corpus.IdempotencyKeyTest) ... ok
test_same_bytes_produce_same_valid_key (test_seed_demo_corpus.IdempotencyKeyTest) ... ok
test_committed_corpus_is_complete (test_seed_demo_corpus.ManifestTest) ... ok

----------------------------------------------------------------------
Ran 28 tests in 0.147s

OK
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
실행 ID: demo-verdict-regression-02
목적: 거짓 성공 방지와 안전한 원장·HTTP 경계 회귀
환경: 로컬 Python 3.9.6, 제어 API 응답, 실제 DB·모델 미사용
합격 기준: 30개 검사 통과, 실패 0개
명령: python3 -m unittest discover -s scripts/demo -p test_*.py -v
시작 시각: 2026-10-18T14:18:32+09:00
test_changed_target_refuses_replay_before_upload (test_demo_verdict.DemoVerdictTest) ... ok
test_citation_outside_search_candidates_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_different_previous_document_id_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_download_hash_mismatch_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_empty_search_results_fail (test_demo_verdict.DemoVerdictTest) ... ok
test_event_redaction_happens_before_storage (test_demo_verdict.DemoVerdictTest) ... ok
test_existing_report_is_never_overwritten (test_demo_verdict.DemoVerdictTest) ... ok
test_full_search_scope_passes_without_rag_polling (test_demo_verdict.DemoVerdictTest) ... ok
test_missing_expected_document_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_previous_document_ids_are_compared (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_failed_is_not_success (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_incomplete_is_not_success (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_scope_requires_success_and_valid_citations (test_demo_verdict.DemoVerdictTest) ... ok
test_search_http_error_fails_after_all_documents_indexed (test_demo_verdict.DemoVerdictTest) ... ok
test_table_text_missing_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_unrun_required_stages_cannot_pass (test_demo_verdict.DemoVerdictTest) ... ok
test_http_error_preserves_code_for_signup_decision (test_demo_verdict.HttpBoundaryTest) ... ok
test_oversized_original_is_rejected (test_demo_verdict.HttpBoundaryTest) ... ok
test_claimed_or_retrying_job_is_processing (test_seed_demo_corpus.ClassifyTest) ... ok
test_failed_job (test_seed_demo_corpus.ClassifyTest) ... ok
test_indexed_document (test_seed_demo_corpus.ClassifyTest) ... ok
test_new_upload_waits_for_worker (test_seed_demo_corpus.ClassifyTest) ... ok
test_embedding_hint_first_when_server_not_ready (test_seed_demo_corpus.DiagnoseTest) ... ok
test_embedding_hint_for_failed_documents (test_seed_demo_corpus.DiagnoseTest) ... ok
test_no_hint_when_everything_indexed (test_seed_demo_corpus.DiagnoseTest) ... ok
test_slow_hint_when_worker_and_server_are_up (test_seed_demo_corpus.DiagnoseTest) ... ok
test_worker_hint_when_no_job_started (test_seed_demo_corpus.DiagnoseTest) ... ok
test_different_bytes_produce_different_keys (test_seed_demo_corpus.IdempotencyKeyTest) ... ok
test_same_bytes_produce_same_valid_key (test_seed_demo_corpus.IdempotencyKeyTest) ... ok
test_committed_corpus_is_complete (test_seed_demo_corpus.ManifestTest) ... ok

----------------------------------------------------------------------
Ran 30 tests in 0.174s

OK
종료 시각: 2026-10-18T14:18:32+09:00
종료 코드: 0
소요 시간: 0.250초
Loading
Loading