"""alpha-processing 연동 확인 — 앱이 실제로 쓰는 경로 그대로 왕복시킨다. web 컨테이너 안에서 실행한다 (verify.sh 가 stdin 으로 넣어 준다): exec -i sh -c 'cd /app && /opt/venv/bin/python -' < _probe.py 1. 표준 라이브러리만으로 텍스트가 박힌 1쪽짜리 PDF 를 만든다 (외부 도구 불필요). 2. alpha.__utils.processing_client.extract_text_from_file() 로 보낸다. → 파일은 Redis(FILE_STORAGE db)에 blob 으로 올라가고, gRPC 는 redis_key 와 추출 정책(pipeline=auto, vlm_model=granite_docling, page_break=\\f)만 나른다. 3. 돌아온 텍스트에 표식이 있으면 Redis·gRPC·Docling 경로가 모두 살아 있는 것이다. 4. 이어서 임베딩 엔드포인트도 왕복시킨다 — glossary 검색이 쓰는 경로다. (앱의 processing_client.embed_texts() 와 같은 API·함수·task 계약을 쓴다.) """ import os import sys import time import uuid MARKER = "PICONTINUUM" + uuid.uuid4().hex[:10].upper() def build_pdf(text): """xref 오프셋까지 직접 계산한 최소 PDF. Docling 이 임베디드 텍스트로 읽는다.""" body = f"BT /F1 24 Tf 72 700 Td ({text}) Tj ET\n".encode() objs = [ b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>", b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] " b"/Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>", b"<< /Length " + str(len(body)).encode() + b" >>\nstream\n" + body + b"endstream", b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", ] out = bytearray(b"%PDF-1.4\n") offsets = [] for i, obj in enumerate(objs, start=1): offsets.append(len(out)) out += f"{i} 0 obj\n".encode() + obj + b"\nendobj\n" xref_at = len(out) out += f"xref\n0 {len(objs) + 1}\n".encode() out += b"0000000000 65535 f \n" for off in offsets: out += f"{off:010d} 00000 n \n".encode() out += ( f"trailer\n<< /Size {len(objs) + 1} /Root 1 0 R >>\nstartxref\n{xref_at}\n".encode() + b"%%EOF\n" ) return bytes(out) def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alpha.settings") import django django.setup() from django.conf import settings from alpha.__utils.processing_client import extract_text_from_file target = "%s:%s" % ( getattr(settings, "GRPC_PROCESSING_IP", "-"), getattr(settings, "GRPC_PROCESSING_PORT", "-"), ) print("[probe] grpc_processing target = %s" % target) if target.startswith("-"): print("[probe] FAIL: GRPC_PROCESSING_IP/PORT 가 설정되지 않았다.") return 1 pdf = build_pdf(MARKER) print("[probe] PDF %d bytes, marker=%s" % (len(pdf), MARKER)) started = time.time() try: text = extract_text_from_file(pdf, "airgap-check.pdf", timeout=180) except Exception as exc: # noqa: BLE001 - 진단용이므로 원인을 그대로 보여준다 print("[probe] FAIL: 예외 %s: %s" % (type(exc).__name__, exc)) return 1 elapsed = time.time() - started if text is None: print("[probe] FAIL: 추출 실패 (None). alpha-processing 로그를 확인한다.") return 1 snippet = " ".join(text.split())[:200] print("[probe] %.1fs, %d chars: %s" % (elapsed, len(text), snippet)) if MARKER not in text.replace(" ", ""): print("[probe] FAIL: 추출은 됐지만 표식 %s 가 없다." % MARKER) return 1 print("[probe] OK: PDF 텍스트 추출 왕복 성공 (Redis blob → gRPC → Docling)") return check_embedding() def check_embedding(): """임베딩 엔드포인트 왕복 — 용어집 검색이 쓰는 경로다. 앱 이미지 버전에 따라 processing_client.embed_texts() 가 없을 수 있으므로 gRPC 스텁을 직접 호출한다. API·함수·task·dimension 계약은 동일하다. """ from alpha.__protobuf.alpha_grpc_stub import grpc_stub_function api, function, dimension = "apps.embedding.__api.embed", "embed", 768 started = time.time() try: response = grpc_stub_function( "grpc_processing", api, function, {"texts": ["업무 프로세스 개선", "process improvement"], "task": "query", "dimension": dimension}, timeout=180, ) or {} except Exception as exc: # noqa: BLE001 print("[probe] FAIL: 임베딩 호출 예외 %s: %s" % (type(exc).__name__, exc)) return 1 elapsed = time.time() - started if not response.get("success"): print("[probe] FAIL: 임베딩 실패 — %s" % (response.get("error") or "UNKNOWN")) return 1 data = response.get("data") or {} vectors = data.get("embeddings") if not isinstance(vectors, list) or len(vectors) != 2 or len(vectors[0]) != dimension: print("[probe] FAIL: 임베딩 응답 형태가 계약과 다르다 (%s)" % type(vectors).__name__) return 1 print("[probe] %.1fs, %d개 벡터 / %d차원, model=%s" % (elapsed, len(vectors), len(vectors[0]), data.get("model"))) print("[probe] OK: 임베딩 왕복 성공 (gRPC → ONNX)") return 0 sys.exit(main())