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
124 changes: 30 additions & 94 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,101 +1,37 @@
# ============================================================================
# doc-graph-agent — 환경 변수 예제
# ============================================================================
# 사용법:
# cp .env.example .env
# .env 의 각 값을 채운 후, .env 는 절대 commit 하지 않는다 (.gitignore)
#
# 관련 이슈: #7 환경 부트스트랩 / #8 Spike 검증
# ============================================================================

# doc-graph-agent .env 예시
# 이 파일을 .env 로 복사 후 실제 값 채움

# ----------------------------------------------------------------------------
# Neo4j (Aura Free 또는 로컬)
# ----------------------------------------------------------------------------
# Aura Free 사용 시: console.neo4j.io 에서 인스턴스 생성 후 다운로드된
# Neo4j-xxxxx.txt 파일에서 복사. neo4j+s:// 스킴은 TLS + bolt 통합.
#
# 형식 예: neo4j+s://abcd1234.databases.neo4j.io
NEO4J_URI=neo4j+s://YOUR_INSTANCE_ID.databases.neo4j.io
# ============================================
# Neo4j Aura (W4 이후 필수)
# ============================================
NEO4J_URI=neo4j+s://<your-instance-id>.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=YOUR_AUTO_GENERATED_PASSWORD
# Aura Free 는 데이터베이스 이름이 'neo4j' 로 고정됨
NEO4J_PASSWORD=<your-password>
NEO4J_DATABASE=neo4j

# Aura 메타데이터 (연결엔 불필요, 관리/로깅용)
AURA_INSTANCEID=
AURA_INSTANCENAME=

# ----------------------------------------------------------------------------
# SSL 인증서 (Windows + 회사 보안 SW 환경에서 필요할 수 있음)
# ----------------------------------------------------------------------------
# 증상: Neo4j 연결 시
# ssl.SSLCertVerificationError: self-signed certificate in certificate chain
# 원인: 회사 보안 SW / 백신이 Windows 시스템 인증서 저장소에 self-signed
# 루트 인증서를 끼워넣고 HTTPS 트래픽을 가로채는 경우.
# 해결: certifi 패키지의 깨끗한 CA 번들 경로를 SSL_CERT_FILE 에 박는다.
#
# uv add certifi
# uv run python -c "import certifi; print(certifi.where())"
#
# 위 경로를 시스템 환경변수 SSL_CERT_FILE 에 등록하거나 아래 줄을 활성화.
# (시스템 환경변수가 우선이므로 .env 에 두는 건 fallback 용)
# SSL_CERT_FILE=C:\path\to\.venv\Lib\site-packages\certifi\cacert.pem


# ----------------------------------------------------------------------------
# LLM — Kimi (Moonshot) — OpenAI 호환 SDK 로 호출
# ----------------------------------------------------------------------------
# 멘토 임시 배부 키 또는 본인 발급 키.
# 4주차 이후 운영 방식은 #5 에서 추적 중.
#
# base_url 주의 (#8 시행착오):
# - 글로벌(영어 콘솔, platform.moonshot.ai 가입): https://api.moonshot.ai/v1
# - 중국 본사 (platform.moonshot.cn 가입): https://api.moonshot.cn/v1
# 잘못된 base_url + 키 조합은 401 Invalid Authentication 으로 떨어진다.
KIMI_API_KEY=sk-YOUR_KIMI_KEY
KIMI_BASE_URL=https://api.moonshot.ai/v1
# 모델명 — 멘토 권장 또는 비용 효율 기준으로 결정
# ============================================
# LLM provider — prod / W4 정성 검증용 (Kimi 기본)
# ============================================
KIMI_API_KEY=<your-moonshot-or-openrouter-key>
KIMI_BASE_URL=https://api.moonshot.cn/v1
KIMI_MODEL=moonshot-v1-8k

# ============================================
# LLM provider — #127 백조 정량 평가용 (OpenRouter 통한 다수 모델)
# ============================================
# scripts/run_qa_eval.py --llm-model / --judge-model 인자와 함께 사용
OPENROUTER_API_KEY=sk-or-v1-<your-openrouter-key>

# 측정 대상 LLM 모델 (OpenRouter ID):
# - openai/gpt-5-mini
# - deepseek/deepseek-v3.2
# - moonshotai/kimi-k2.5
# - x-ai/grok-4.20
#
# Judge LLM 모델:
# - anthropic/claude-haiku-4.5 (권장, 편향 방지)

# ----------------------------------------------------------------------------
# LLM — GPT (ablation 전용)
# ----------------------------------------------------------------------------
# #1 길 4 보너스 ablation 측정에서만 사용. 일상 작업에는 불필요.
OPENAI_API_KEY=sk-YOUR_OPENAI_KEY
OPENAI_MODEL=gpt-4o-mini


# ----------------------------------------------------------------------------
# Embedding — NED + Dedup (#14) 에서 사용
# ----------------------------------------------------------------------------
# 옵션 A: bge-m3 로컬 (HuggingFace, 한국어 강함, 무료)
# 옵션 B: OpenAI text-embedding-3-small (간단, 유료)
# Spike 단계에서는 후순위. W3 진입 전 결정.
EMBEDDING_PROVIDER=bge-m3
EMBEDDING_MODEL=BAAI/bge-m3


# ----------------------------------------------------------------------------
# Opik (Comet) — 트레이싱 + 평가 (W5)
# ----------------------------------------------------------------------------
# #6 에서 발급. Workspace 는 본인 Comet 계정의 workspace 슬러그.
OPIK_URL_OVERRIDE=https://www.comet.com/opik/api
OPIK_API_KEY=YOUR_OPIK_API_KEY
OPIK_WORKSPACE=YOUR_WORKSPACE
OPIK_PROJECT_NAME=doc-graph-agent


# ----------------------------------------------------------------------------
# 평가 토글 (#25 LLM 어댑터 이후 활성화)
# ----------------------------------------------------------------------------
# gpt | kimi — 평가 경로의 LLM 선택. ablation 측정 시 토글.
EVAL_LLM=kimi


# ----------------------------------------------------------------------------
# 기타
# ----------------------------------------------------------------------------
# 로깅 레벨: DEBUG | INFO | WARNING | ERROR
LOG_LEVEL=INFO
# ============================================
# OpenAI 직결 (기본 fallback, 선택)
# ============================================
# OPENAI_API_KEY=sk-...
80 changes: 71 additions & 9 deletions agent/llm_client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
"""LLM 클라이언트 thin wrapper.

OpenAI SDK 호환 (Kimi, OpenRouter, OpenAI 모두 동일 코드로).
환경변수로 provider 토글:
OpenAI SDK 호환 — Kimi, OpenRouter, OpenAI, DeepSeek, Grok 모두 동일 코드로.

## 환경변수 설정 방식 (#127 멘토링 평가 토글)

### 방식 1: 기본 KIMI_* (prod / W4 정성 검증)
- KIMI_API_KEY / KIMI_BASE_URL / KIMI_MODEL

Spike (#8) 검증용 최소 구현. 정식화는 #7 W2 환경 부트스트랩에서.
### 방식 2: 평가 진입점에서 configure_llm()으로 명시 토글
- LLMConfig(api_key, base_url, model) 을 직접 주입
- scripts/run_qa_eval.py 에서 --llm-model / --llm-base-url / --llm-api-key-env 인자로 토글

호출 안 하면 KIMI_* 환경변수 사용 (기존 동작 유지).
"""

from __future__ import annotations

import logging
import os
from dataclasses import dataclass

Expand All @@ -18,17 +26,20 @@

load_dotenv()

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class LLMConfig:
"""LLM 호출 설정. 환경변수로부터 로드."""
"""LLM 호출 설정."""

api_key: str
base_url: str
model: str

@classmethod
def from_env(cls) -> "LLMConfig":
"""기본 — KIMI_* 환경변수에서 로드."""
api_key = os.environ.get("KIMI_API_KEY", "")
base_url = os.environ.get("KIMI_BASE_URL", "https://api.moonshot.cn/v1")
model = os.environ.get("KIMI_MODEL", "moonshot-v1-8k")
Expand All @@ -39,17 +50,70 @@ def from_env(cls) -> "LLMConfig":
)
return cls(api_key=api_key, base_url=base_url, model=model)

@classmethod
def from_args(
cls,
model: str | None = None,
base_url: str | None = None,
api_key_env: str = "KIMI_API_KEY",
) -> "LLMConfig":
"""평가 진입점용 — CLI 인자로 명시 토글 (#127).

Args:
model: 모델 ID (예: "deepseek/deepseek-v3.2")
base_url: OpenAI-호환 endpoint (예: "https://openrouter.ai/api/v1")
api_key_env: API 키 환경변수 이름 (예: "OPENROUTER_API_KEY")
"""
api_key = os.environ.get(api_key_env, "")
if not api_key:
raise RuntimeError(f"환경변수 {api_key_env} 미설정 — LLM 토글 불가")
if not model:
raise RuntimeError("--llm-model 인자 필요")
if not base_url:
raise RuntimeError("--llm-base-url 인자 필요")
return cls(api_key=api_key, base_url=base_url, model=model)


# ── Runtime active config (#127) ───────────────────────────
# configure_llm() 호출 전: None → LLMClient() 호출 시 from_env() 사용
# configure_llm() 호출 후: 명시 config 사용 → 모든 LLMClient 인스턴스 영향
_active_config: LLMConfig | None = None


def configure_llm(
model: str | None = None,
base_url: str | None = None,
api_key_env: str = "KIMI_API_KEY",
) -> None:
"""평가 진입점에서 LLM 을 일회성으로 재설정 (#127).

호출 안 하면 기존 KIMI_* 환경변수 사용 (prod / W4 정성 검증 동작 유지).

호출 시 모든 후속 LLMClient() 인스턴스가 이 config 를 사용 →
retrieval.text2cypher / local_retriever / router 자동 토글.
"""
global _active_config
_active_config = LLMConfig.from_args(
model=model, base_url=base_url, api_key_env=api_key_env
)
logger.info(
"LLM 재설정: model=%s base_url=%s api_key_env=%s",
_active_config.model, _active_config.base_url, api_key_env,
)


class LLMClient:
"""OpenAI SDK 기반 thin wrapper.

- 단일 진입점: chat(messages, **kwargs)
- tenacity 로 재시도 (네트워크 / 일시적 5xx 대비)
- 토큰 / latency 로깅은 #24 Opik 통합에서 추가
- tenacity 로 재시도
- configure_llm() 호출됐으면 그 config 사용, 안 됐으면 from_env()
"""

def __init__(self, config: LLMConfig | None = None) -> None:
self.config = config or LLMConfig.from_env()
if config is None:
config = _active_config if _active_config is not None else LLMConfig.from_env()
self.config = config
self._client = OpenAI(
api_key=self.config.api_key,
base_url=self.config.base_url,
Expand All @@ -68,7 +132,6 @@ def chat(
max_tokens: int = 1024,
response_format: dict | None = None,
) -> str:
"""단순 텍스트 응답 반환. Spike 단계 최소 인터페이스."""
kwargs: dict = {
"model": self.config.model,
"messages": messages,
Expand All @@ -82,7 +145,6 @@ def chat(
return resp.choices[0].message.content or ""

def hello(self) -> str:
"""헬스체크 1회 호출. Spike #8 단계 3 검증용."""
return self.chat(
messages=[{"role": "user", "content": "한 단어로 'pong' 만 답하세요."}],
max_tokens=16,
Expand Down
25 changes: 25 additions & 0 deletions eval/dataset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# QA Datasets — doc-graph-agent

## VectorRAG QA (40 고)

`vectorrag_qa.json` — doc-summary-agent 의 40 QA 그대로 복사.
- factual / numerical / summary / negative 유형
- 한와/DS/미래에셋/농협/금감원 5 문서
- 텍스트 chunk 검색으로 답변 가능한 질의 (RAG 강점)

## GraphRAG QA (40 계획)

`graphrag_qa.json` — 그래프 구조 활용 질의 (Graph 강점).

### 작성 패턴
1. **1-hop 관계** (7) — entity A 와 함께 언급된 B
2. **2-hop 다중** (7) — entity A → chunk → entity B → chunk → entity C
3. **교집합** (7) — 두 entity 의 공통 속성
4. **집계/통계** (7) — 전체 그래프 최대/최소/평균
5. **필터 + 집계** (6) — doc_type / doc_year 필터 후 집계
6. **메타데이터** (6) — 문서 개수, 섹션 구조, 첥크 분포

### 작성 순서
1. `scripts/explore_graph.py` 실행 → 그래프 메타 추출
2. 결과를 보면서 40 QA 작성
3. 각 QA 의 정답은 Cypher 쿼리로 곀증 가능해야 함
Loading