Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
byeongwook.choi
2026-09-21 13:23:37 +09:00
co-authored by Claude Fable 5.1
commit 11ae3629b2
453 changed files with 259183 additions and 0 deletions
View File
+31
View File
@@ -0,0 +1,31 @@
# 질의 확장용 도메인 용어 사전 초기 시드 (계획서 §5.6)
# 형식 — 용어: [동의어/관련 테이블/T-Code ...]
#
# 이 사전은 두 곳에서 같이 쓰인다 (config/glossary.py):
# 1) query/expand.py — 검색 질의 확장 ("입고"로 물어도 GR·MSEG·101 로 색인된 문서를 찾는다)
# 2) wiki_out/run.py — concepts/ 위키 문서 생성
# 양방향으로 동작한다: 대표어로도, 동의어 중 아무거나로도 질의가 걸린다.
총계정원장: [G/L, GL, 원장, ACDOCA, ACDOCT, SKAT, FAGLL03, FS10N]
전표: [회계전표, BKPF, BSEG, ACDOCA, FB03]
반제: [클리어링, 미결정리, BSIS, BSAS, F-03, F.13]
제조원가: [원가계산, CO-PC, 오더결산, COEP, COSS, CKMLHD, KKS1, KO88]
코스트센터: [CCTR, CSKS, KSB1]
회계단위: [회사코드, BUKRS, T001]
계정과목: [계정, RACCT, SAKNR, SKA1, SKAT]
사업영역: [BUSA, GSBER]
기능영역: [FAREA, RFAREA]
손익: [P/L, 손익계산서, GVTYP]
잔액: [기말잔액, 이월, HSL, BALANCE]
# MM — 샘플에 ZMMR* 프로그램이 있어 함께 시드한다 (계속 확장 대상)
입고: [GR, Goods Receipt, 101, MSEG, MKPF, MB51, MIGO]
출고: [GI, Goods Issue, 201, 261, MSEG, MB1A]
자재: [품목, 자재마스터, MATNR, MARA, MAKT, MM03]
자재문서: [물자문서, MBLNR, MKPF, MSEG, MB51]
구매오더: [PO, Purchase Order, EBELN, EKKO, EKPO, ME23N]
구매요청: [PR, Purchase Requisition, BANFN, EBAN, ME53N]
재고: [재고수량, LABST, MARD, MBEW, MMBE]
이동유형: [BWART, 이동타입, movement type]
플랜트: [WERKS, 사업장, T001W]
저장위치: [LGORT, 창고, T001L]
공급업체: [벤더, LIFNR, LFA1, XK03]
송장: [인보이스, Invoice, RBKP, RSEG, MIRO]
+67
View File
@@ -0,0 +1,67 @@
"""도메인 용어 사전 로더 (계획서 §5.6, 수정사항 4번).
`config/domain_glossary.yaml` 은 의존성을 늘리지 않으려고 YAML 파서 없이 읽는다.
지원 형식은 한 줄 리스트뿐이다:
총계정원장: [G/L, GL, 원장, ACDOCA, SKAT, FAGLL03]
질의 확장(query/expand.py)과 concepts 위키 생성(wiki_out/run.py)이 같은 사전을 쓰도록
로딩을 여기로 모았다 (이전에는 wiki_out/run.py 안에 인라인 정규식으로만 있었다).
"""
from __future__ import annotations
import re
from functools import lru_cache
from pathlib import Path
from .settings import ROOT
# "용어: [동의어, 동의어]" — 주석(#)·빈 줄·중첩 키(tags:)는 건너뛴다
GLOSSARY_LINE = re.compile(r"^([^#:\s][^:]*):\s*\[(.*)\]\s*$")
DEFAULT_PATH = ROOT / "config" / "domain_glossary.yaml"
def parse_glossary(text: str) -> dict[str, list[str]]:
"""사전 텍스트 → {대표어: [동의어...]}"""
out: dict[str, list[str]] = {}
for line in text.splitlines():
m = GLOSSARY_LINE.match(line.strip())
if not m:
continue
term = m.group(1).strip()
syns = [t.strip() for t in m.group(2).split(",") if t.strip()]
if term:
out[term] = syns
return out
@lru_cache(maxsize=4)
def load_glossary(path: Path | None = None) -> dict[str, list[str]]:
p = path or DEFAULT_PATH
if not p.exists():
return {}
return parse_glossary(p.read_text(encoding="utf-8"))
@lru_cache(maxsize=4)
def synonym_map(path: Path | None = None) -> dict[str, list[str]]:
"""양방향 동의어 맵 — 대표어로도, 동의어로도 질의가 들어올 수 있다.
"입고" → [GR, MSEG, 101 …] 뿐 아니라 "GR" → [입고, MSEG …] 도 성립해야 한다.
키는 대문자 정규화(한글은 그대로)해 비교한다.
"""
glossary = load_glossary(path)
groups: list[set[str]] = [{term, *syns} for term, syns in glossary.items()]
out: dict[str, list[str]] = {}
for group in groups:
for member in group:
key = member.upper()
others = [m for m in group if m.upper() != key]
if not others:
continue
bucket = out.setdefault(key, [])
for o in others:
if o not in bucket:
bucket.append(o)
return out
+57
View File
@@ -0,0 +1,57 @@
"""전역 설정. 모든 외부 접점(LLM/임베딩/DB/서버)은 환경변수로만 결정한다."""
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
ROOT = Path(__file__).resolve().parent.parent
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=ROOT / ".env", env_file_encoding="utf-8", extra="ignore")
# DB — 기본은 SQLite(개발/폐쇄망 무의존). PostgreSQL 전환은 ASSUMPTIONS.md 참고.
database_url: str = f"sqlite:///{ROOT / 'data' / 'index.db'}"
# LLM (Stage 3 요약) — OpenAI 호환 엔드포인트 가정
llm_base_url: str = ""
llm_api_key: str = ""
llm_model: str = "glm-5.2"
# 요약 배치의 동시 LLM 호출 수. DB 쓰기는 메인 스레드에 남기고 호출만 병렬로 돈다.
llm_concurrency: int = 4
llm_timeout_s: int = 180
# 429/5xx 재시도 — 동시성을 올리면 429 가 늘어나므로 백오프와 같이 써야 한다
llm_max_retries: int = 5
llm_backoff_base: float = 2.0
llm_backoff_max: float = 60.0
# /ingest 가 뒤이어 돌리는 요약의 백엔드:
# api — 환경변수 키로 호출 (키가 유효할 때)
# file — 프롬프트를 data/llm_jobs/ 에 쌓아두고 사람·에이전트가 채운다 (키 불필요)
# off — 자동 요약을 하지 않는다 (배치로만 돌린다)
summarize_backend: str = "api"
# 임베딩 — 모델 미정(§10). 비워두면 벡터 검색 비활성(FTS만 사용).
embed_base_url: str = ""
embed_api_key: str = ""
embed_model: str = ""
# 질의 서비스
index_host: str = "127.0.0.1"
index_port: int = 8100
# 데이터 디렉토리
data_raw: Path = ROOT / "data" / "raw"
data_normalized: Path = ROOT / "data" / "normalized"
data_parsed: Path = ROOT / "data" / "parsed"
data_summaries: Path = ROOT / "data" / "summaries"
# file 백엔드의 프롬프트/응답 큐 — LLM 키 없이 요약을 돌리는 입구 (summarize/jobs.py)
data_llm_jobs: Path = ROOT / "data" / "llm_jobs"
# 위키 (계획서 v4 §5.7 — OKF v0.2 출력, 의미 검색의 1차 대상)
wiki_dir: Path = ROOT / "wiki"
# 위키 검색 백엔드: qmd | pg (M3.5 파일럿으로 확정 — 계획서 §6.5)
# "pg" 는 내장 DB 검색 경로를 뜻함. 현 구현은 SQLite FTS5 (ASSUMPTIONS.md §1).
wiki_search_backend: str = "pg"
qmd_url: str = "http://127.0.0.1:8181"
settings = Settings()
+40
View File
@@ -0,0 +1,40 @@
# 허용 업무 태그 초기 시드 (계획서 §5.6) — 첫 300본 요약 후 proposed_tags 집계로 확장
tags:
# FI
- 전표
- 총계정원장
- 계정잔액
- 반제
- 자동반제
- 미결관리
- 결산
- 월별비교
- 채권관리
- 채무관리
- 지급처리
- 세금계산서
- 부가세
- 원천세
- 고정자산
- 예산관리
- 자금관리
- 인사연동
- 급여전기
- 경비처리
# CO
- 제조원가
- 표준원가
- 오더결산
- 차이분석
- 배부
- 코스트센터
- 수익성분석
- 내부오더
# 공통
- 인터페이스
- 배치작업
- 마스터관리
- 권한관리
- 리포트
- 대사작업
- 마감작업