whatap-python 2.2.2__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
Binary file
Binary file
Binary file
Binary file
whatap/build.py CHANGED
@@ -1,4 +1,4 @@
1
1
  app = 'Python'
2
2
  name = 'whatap-python'
3
- version = '2.2.2'
4
- release_date = '20261019'
3
+ version = '2.3.0'
4
+ release_date = '20261112'
@@ -433,6 +433,8 @@ Configuration = {
433
433
  # 않는다 — 줄을 지우면 재기동 전까지 마지막 값이 그대로 남아 계속 차단된다.
434
434
  "llm_blocked_users": "",
435
435
 
436
+ # >>> LLM-EVAL: 평가(Evaluation) 설정 키 — 평가 기능을 떼면 이 블록째 지운다
437
+ # (whatap/llm/evaluators/REMOVAL.md).
436
438
  # ── LLM Evaluation ──
437
439
  # 평가 파이프라인 마스터 토글. false면 enqueue/worker가 동작하지 않음.
438
440
  "llm_eval_enabled": False,
@@ -490,26 +492,44 @@ Configuration = {
490
492
 
491
493
  # ── conf 기반 default judge (앱 코드 0) ──
492
494
  # llm-as-judge 평가자(combined_judge 등)는 평가용 LLM 호출에 자체 자격증명이 필요하다.
493
- # judge 해석 순서: ① evaluator(judge_fn=) → ② set_default_judge_fn() → ③ 계측이
494
- # 캡처한 사용자 client 인스턴스 재사용 → ④ ctx.provider 매칭 후 환경변수 키.
495
- # 요청 스코프(요청마다 client 생성·종료) 앱이나 OAuth 전용 환경에서는 ③(평가는 요청이
496
- # 끝난 뒤 비동기로 도는데 그 시점엔 client 가 이미 닫힘)·④(환경변수 키 없음)가 모두
497
- # 막혀, 앱에서 set_default_judge_fn 을 직접 호출해야만 했다. 아래 키를 채우면
498
- # 부트스트랩이 그 값으로 default judge 를 자동 등록해 — 앱 코드 수정 없이 conf 만으로 —
499
- # 평가가 돈다. 코드로 set_default_judge_fn 을 이미 호출했으면 그쪽이 우선(conf 미적용).
495
+ # judge 해석 순서: ① evaluator(judge_fn=) → ② set_default_judge_fn() / 아래 conf →
496
+ # ③ 계측이 사용자 호출에서 떠 둔 엔드포인트·자격증명 스냅샷 → ④ ctx.provider 매칭 후
497
+ # 환경변수 키. 보통 ③ 이 자동으로 풀린다(앱과 같은 엔드포인트·자격증명·모델). OAuth 라
498
+ # 평문 키가 없거나 다른 모델·엔드포인트로 평가하려면 아래 키를 채운다 — 부트스트랩이 그
499
+ # 값으로 default judge 를 자동 등록해 앱 코드 수정 없이 conf 만으로 평가가 돈다.
500
+ # 코드로 set_default_judge_fn 을 이미 호출했으면 그쪽이 우선(conf 미적용).
500
501
  #
501
- # llm_eval_judge_provider : "openai" | "anthropic". 미지정 시 base_url 이 있으면
502
- # openai(호환)로 간주, 없으면 자동 등록 안 함(③/④ 유지).
502
+ # Bedrock: Bedrock 을 부르는 앱(boto3·aioboto3·AnthropicBedrock·LiteLLM)은 ③ 으로
503
+ # judge 도 Bedrock Runtime(Converse)으로 간다 — 앱과 같은 리전·엔드포인트·자격증명·모델.
504
+ # 다른 앱(OpenAI 등)도 judge 만 Bedrock 으로 보내려면 provider=bedrock + model(+region).
505
+ #
506
+ # llm_eval_judge_provider : "openai" | "anthropic" | "bedrock". 미지정 시 base_url 이
507
+ # 있으면 openai(호환)로 간주, 없으면 자동 등록 안 함(③/④ 유지).
503
508
  # llm_eval_judge_model : judge 모델. 미지정 시 provider 별 default
504
- # (openai=gpt-4o-mini, anthropic=claude-3-5-haiku-latest).
509
+ # (openai=gpt-4o-mini, anthropic=claude-haiku-5-5).
510
+ # provider 나 base_url 과 함께 넣어야 적용된다 — 모델만
511
+ # 넣으면 무시하고 LLM056 경고(③ 은 계속 앱 모델로 평가).
512
+ # bedrock 은 **필수** — modelId 또는 추론 프로파일 ID
513
+ # (예: us.anthropic.claude-haiku-4-5-20251001-v1:0). 계정·리전마다
514
+ # 열린 모델이 달라 기본값을 두지 않는다.
505
515
  # llm_eval_judge_api_key_env : judge 키가 담긴 환경변수 이름(권장 — 키를 설정 파일에
506
516
  # 직접 두지 않음). 예: "WHATAP_LLM_EVAL_JUDGE_API_KEY".
507
517
  # llm_eval_judge_api_key : (대안) judge API 키 직접 지정. _env 가 있으면 그쪽 우선.
508
- # 로컬 vLLM 등 키가 불필요하면 "EMPTY".
518
+ # 로컬 vLLM 등 키가 불필요하면 "EMPTY". bedrock 엔 쓰지 않는다
519
+ # (AWS 자격증명으로 서명 — env / 프로파일 / IAM 역할).
509
520
  # llm_eval_judge_base_url : (선택) OpenAI 호환 엔드포인트 / 프록시 / 내부 게이트웨이.
521
+ # bedrock 이면 Bedrock Runtime endpoint_url(VPC 엔드포인트 등).
522
+ # llm_eval_judge_region : (bedrock) AWS 리전. 미지정 시 AWS 기본 해석
523
+ # (AWS_DEFAULT_REGION / ~/.aws/config — botocore 는
524
+ # AWS_REGION 을 읽지 않는다).
525
+ # llm_eval_judge_aws_profile : (bedrock) ~/.aws 프로파일 이름. 미지정 시 AWS 기본 자격증명
526
+ # 체인(env → 공유 자격증명 파일 → IAM 역할).
510
527
  "llm_eval_judge_provider": "",
511
528
  "llm_eval_judge_model": "",
512
529
  "llm_eval_judge_api_key_env": "",
513
530
  "llm_eval_judge_api_key": "",
514
531
  "llm_eval_judge_base_url": "",
532
+ "llm_eval_judge_region": "",
533
+ "llm_eval_judge_aws_profile": "",
534
+ # <<< LLM-EVAL
515
535
  }
@@ -22,8 +22,9 @@
22
22
 
23
23
  평가는 fire-and-forget. LlmStepStatus 송출과 완전 독립 — 사용자 트랜잭션 영향 0.
24
24
  모든 예외는 swallow 후 [LLM] 로그로만 기록.
25
+
26
+ 코어(계측·송출)는 이 모듈을 직접 부르지 않는다 — ``whatap.llm.evaluators.hooks`` 를 거친다.
25
27
  """
26
- import contextvars
27
28
  import queue
28
29
  import threading
29
30
  import time
@@ -32,6 +33,9 @@ from whatap import logging
32
33
  from whatap.conf.configure import Configure as conf
33
34
 
34
35
  from whatap.llm.evaluators.base import EvaluatorContext, EvaluatorResult
36
+ from whatap.llm.evaluators.hooks import (
37
+ conf_truthy as _conf_truthy, enter_judge_call, exit_judge_call, is_judge_call,
38
+ )
35
39
  from whatap.llm.evaluators.registry import EvaluatorRegistry
36
40
  from whatap.llm.evaluators.sampler import EvaluatorSampler
37
41
  from whatap.llm.log_sink_packs.llm_step_eval_status import LlmStepEvalStatus
@@ -40,23 +44,6 @@ from whatap.llm.log_sink_packs.llm_step_eval_status import LlmStepEvalStatus
40
44
  _DEFAULT_BUFFER_LIMIT = 1000
41
45
  _DEFAULT_WORKERS = 4
42
46
 
43
-
44
- def _conf_truthy(name, default=False):
45
- """Configure attr 을 bool 로 안전 변환.
46
-
47
- Configure.setProperty 가 config file 에서 읽은 문자열 'true'/'false' 를 그대로
48
- setattr 해서 string 으로 저장 → 'false' 도 truthy 로 평가되는 reload 버그 우회.
49
- """
50
- try:
51
- val = getattr(conf, name, default)
52
- except Exception:
53
- return bool(default)
54
- if isinstance(val, bool):
55
- return val
56
- if isinstance(val, str):
57
- return val.strip().lower() in ('true', 'yes', '1', 'on')
58
- return bool(val)
59
-
60
47
  _scope_ignored_warned = [False]
61
48
 
62
49
 
@@ -98,44 +85,6 @@ _LABEL_TO_FIELD = {
98
85
  'combined_judge': 'eval_combined_judge',
99
86
  }
100
87
 
101
- # 평가 워커 안인지 판별하는 플래그 — 두 저장소 동시 사용:
102
- # - thread-local : sync 워커 스레드 자체에서 발생한 LLM 호출 인지용
103
- # - ContextVar : async judge 가 ``run_coroutine_threadsafe`` 로 user loop 에 dispatch
104
- # 된 task 의 context 에서도 인지되도록 propagate
105
- # 무한 재귀 방지 (judge 호출이 다시 평가 큐에 안 들어가도록) + intercept 가 judge 호출도
106
- # 정상 추적 (단, 재귀 가드 덕분에 다시 평가 enqueue 되지 않음) 의 두 가지 목적.
107
- _eval_worker_local = threading.local()
108
- _eval_worker_cv = contextvars.ContextVar('whatap_eval_worker', default=False)
109
-
110
-
111
- def _is_in_evaluator_worker():
112
- """평가 워커 안인지 — thread-local 또는 ContextVar 기준."""
113
- if getattr(_eval_worker_local, 'in_eval', False):
114
- return True
115
- cv_val = _eval_worker_cv.get()
116
- if isinstance(cv_val, dict):
117
- return bool(cv_val.get('in_eval'))
118
- return bool(cv_val)
119
-
120
-
121
- def _get_eval_worker_state():
122
- """현재 eval worker state 반환. 없으면 None.
123
-
124
- 구조: {'in_eval', 'parent_txid', 'parent_step_id', 'parent_index'}.
125
- CV 우선, fallback TL.
126
- """
127
- cv_val = _eval_worker_cv.get()
128
- if isinstance(cv_val, dict) and cv_val.get('in_eval'):
129
- return cv_val
130
- if getattr(_eval_worker_local, 'in_eval', False):
131
- return {
132
- 'in_eval': True,
133
- 'parent_txid': getattr(_eval_worker_local, 'parent_txid', None),
134
- 'parent_step_id': getattr(_eval_worker_local, 'parent_step_id', None),
135
- 'parent_index': getattr(_eval_worker_local, 'parent_index', None),
136
- }
137
- return None
138
-
139
88
 
140
89
  # EvalStat 인스턴스 캐시.
141
90
  _eval_stat_cache = [None]
@@ -190,10 +139,12 @@ class LlmEvaluatorTask(object):
190
139
  if not _conf_truthy('llm_eval_enabled'):
191
140
  _warn_scope_ignored_once()
192
141
  return
193
- if _is_in_evaluator_worker():
142
+ if is_judge_call():
194
143
  return # judge 평가자의 자기 호출이 다시 평가 큐에 들어가는 무한 재귀 차단
195
144
  if pack is None:
196
145
  return
146
+ if _is_embedding_call(pack):
147
+ return # 평가할 텍스트 응답이 없다 — judge 가 임베딩 모델로 불려 실패만 쌓인다
197
148
 
198
149
  # conf.llm_eval_evaluators 기반 빌트인 자동 register (1회 — 안에서 fast-path).
199
150
  # is_empty() 검사 전에 호출해야 첫 enqueue 가 skip 안 됨.
@@ -262,27 +213,37 @@ class LlmEvaluatorTask(object):
262
213
  continue
263
214
  try:
264
215
  merged, scope_designated = self._merge_evaluators(scope_evs)
265
- ctx_txid = pack.txid
266
- # 샘플링 통과한 평가자만 모아서 한 워커 task 로 묶음 — 한 LLM 호출당
267
- # LlmStepEvalStatus pack 1개만 송출되도록.
268
- #
269
- # 스코프로 지정된 평가자는 샘플링 면제 (항상 실행). 구간을 지정한 것
270
- # 자체가 "이 구간은 다 본다" 는 선언이고, 거기에 sample_rate 를 또
271
- # 곱하면 "데코레이터 붙였는데 10% 만 평가" 가 된다.
272
- active = []
273
- for evaluator in merged:
274
- if not evaluator.LABEL:
275
- continue
276
- if not scope_designated \
277
- and not self._sampler.should_run(evaluator, ctx_txid):
278
- continue
279
- active.append(evaluator)
216
+ # 고른 평가자를 한 워커 task 로 묶음 — 한 LLM 호출당 LlmStepEvalStatus
217
+ # pack 1개만 송출되도록.
218
+ active = self._select_active(merged, scope_designated, pack)
280
219
  if active:
281
220
  self._executor.submit(self._safe_run_pack, active, pack)
282
221
  except Exception as e:
283
222
  logging.warning('[LLM] eval dispatch failed: %s' % e,
284
223
  extra={'id': 'LLM031'})
285
224
 
225
+ def _select_active(self, evaluators, scope_designated, pack):
226
+ """이 pack 에 돌릴 평가자.
227
+
228
+ - 응답 텍스트가 없는 호출(도구만 부른 단계 · 실패한 호출)은 judge 가 빈 답을 채점하게
229
+ 되므로 judge 평가자를 건너뛴다. 규칙 기반 평가자는 그대로 돈다. 에이전트라면 최종
230
+ 답변 단계의 입력에 앞 단계의 도구 결과가 모두 들어가므로 프롬프트 인젝션도 거기서 본다.
231
+ - 스코프로 지정된 평가자는 샘플링 면제 (항상 실행). 구간을 지정한 것 자체가
232
+ "이 구간은 다 본다" 는 선언이고, 거기에 sample_rate 를 또 곱하면 "데코레이터
233
+ 붙였는데 10% 만 평가" 가 된다.
234
+ """
235
+ has_response = _has_response_text(pack)
236
+ active = []
237
+ for evaluator in evaluators:
238
+ if not evaluator.LABEL:
239
+ continue
240
+ if not has_response and getattr(evaluator, 'USES_LLM_JUDGE', False):
241
+ continue
242
+ if not scope_designated and not self._sampler.should_run(evaluator, pack.txid):
243
+ continue
244
+ active.append(evaluator)
245
+ return active
246
+
286
247
  @staticmethod
287
248
  def _merge_evaluators(scope_evs=None):
288
249
  """이번 pack 에 실행할 평가자 목록 결정.
@@ -308,27 +269,11 @@ class LlmEvaluatorTask(object):
308
269
 
309
270
  모든 예외 swallow + 재귀 방지 + 부모 step 키 propagation.
310
271
 
311
- TL/CV 에 다음 stash:
312
- - in_eval=True (재귀 가드)
313
- - parent_txid / parent_step_id / parent_index : 원본 user step 의 키
314
- ``llm_eval_track_judge_calls=true`` 일 때 judge LLM 호출 pack 의 결합 키를
315
- user step 과 동일하게 override 해서 dashboard 에서 같은 transaction 으로
316
- 보이도록 함.
317
-
318
- TL 은 워커 스레드 자체에서 발생한 LLM 호출 (sync judge), CV 는 dispatched coro
319
- (async judge, user loop 으로 dispatch) 에서 propagate 되도록.
272
+ 평가하는 동안 judge 호출 표시(``hooks.enter_judge_call``)를 켠다 — 재귀 가드이자,
273
+ ``llm_eval_track_judge_calls=true`` 일 때 judge LLM 호출 pack 의 결합 키를 user
274
+ step 과 같게 덮어 dashboard 에서 같은 transaction 으로 보이게 하는 좌표.
320
275
  """
321
- state = {
322
- 'in_eval': True,
323
- 'parent_txid': pack.txid,
324
- 'parent_step_id': pack.step_id,
325
- 'parent_index': pack.index,
326
- }
327
- _eval_worker_local.in_eval = True
328
- _eval_worker_local.parent_txid = pack.txid
329
- _eval_worker_local.parent_step_id = pack.step_id
330
- _eval_worker_local.parent_index = pack.index
331
- cv_token = _eval_worker_cv.set(state)
276
+ token = enter_judge_call(pack)
332
277
  try:
333
278
  self._run_pack(evaluators, pack)
334
279
  except Exception as e:
@@ -336,11 +281,7 @@ class LlmEvaluatorTask(object):
336
281
  % (len(evaluators), e),
337
282
  extra={'id': 'LLM036'})
338
283
  finally:
339
- _eval_worker_cv.reset(cv_token)
340
- _eval_worker_local.in_eval = False
341
- _eval_worker_local.parent_txid = None
342
- _eval_worker_local.parent_step_id = None
343
- _eval_worker_local.parent_index = None
284
+ exit_judge_call(token)
344
285
 
345
286
  def _run_pack(self, evaluators, pack):
346
287
  """한 pack 의 모든 활성 evaluator 를 순차 실행 + 점수 모아서 단일
@@ -543,5 +484,32 @@ def build_eval_context_from_pack(pack):
543
484
  event_loop=None,
544
485
  judge_base_url=getattr(pack, '_llm_judge_base_url', None),
545
486
  judge_api_key=getattr(pack, '_llm_judge_api_key', None),
546
- judge_kind=getattr(pack, '_llm_judge_kind', None),
487
+ judge_kind=getattr(pack, '_llm_judge_kind', None) or _bedrock_kind(pack),
488
+ judge_aws=getattr(pack, '_llm_judge_aws', None),
547
489
  )
490
+
491
+
492
+ def _has_response_text(pack):
493
+ """judge 가 채점할 응답 텍스트가 있는가 — 도구만 부른 단계 · 실패한 호출에는 없다."""
494
+ return bool(str(getattr(pack, 'completion_text', '') or '').strip())
495
+
496
+
497
+ def _is_embedding_call(pack):
498
+ """평가할 응답이 없는 임베딩 호출인가.
499
+
500
+ OpenAI 는 operation_type 이 embedding 이다. Bedrock InvokeModel 은 모델과 무관하게 chat 으로
501
+ 분류되므로 모델 ID 로 가른다(amazon.titan-embed-* · cohere.embed-* · amazon.nova-*-embeddings-*
502
+ 등). 텍스트 응답이 있으면 임베딩으로 보지 않는다.
503
+ """
504
+ if str(getattr(pack, 'operation_type', '') or '').lower() == 'embedding':
505
+ return True
506
+ model = str(getattr(pack, 'model', '') or '').lower()
507
+ return 'embed' in model and not str(getattr(pack, 'completion_text', '') or '').strip()
508
+
509
+
510
+ def _bedrock_kind(pack):
511
+ """스냅샷이 없는 Bedrock 호출(LiteLLM·직접 HTTP — 떠 둘 client 가 없다)도 Bedrock judge 로.
512
+
513
+ 계측이 못박은 provider 키로 가른다. 리전·엔드포인트는 judge 가 호스트에서 읽는다.
514
+ """
515
+ return 'bedrock' if getattr(pack, '_provider_key', None) == 'bedrock' else None
@@ -11,7 +11,7 @@ import whatap.io as whatapio
11
11
  import whatap.net.async_sender as async_sender
12
12
 
13
13
  # 표준 의존 방향: counter.tasks → whatap.llm. 여기서는 whatap.llm 의 **데이터 계층**
14
- # (log_sink_packs) 만 모듈레벨로 import 한다. providers/evaluators/stats 같은 오케스트레이션
14
+ # (log_sink_packs) 만 모듈레벨로 import 한다. providers/stats 등 오케스트레이션
15
15
  # 계층은 이 모듈을 (함수 스코프로) 되-import 하므로, 그쪽을 여기서 모듈레벨 import 하면
16
16
  # 부트스트랩 순환이 된다 — 하지 말 것.
17
17
  from whatap.llm.log_sink_packs.llm_log_sink_pack import LlmLogSinkPack, normalize_id
@@ -29,14 +29,15 @@ _MAX_CONTENT_BYTES = 20000
29
29
  class LlmLogSinkTask(object):
30
30
  """LLM logsink pack 송출 파이프라인.
31
31
 
32
- LLM 호출 결과 pack 을 받아 즉시 송출. 평가 (LLM-judge 등) 는 별도 파이프라인
33
- (``LlmEvaluatorTask``) 에서 비동기로 처리되며, 평가 결과는 ``LlmStepEvalStatus``
34
- pack (``llm_log_type=llm_step_eval_status``) 으로 송출된다 — 원본 LlmStepStatus 와
35
- 동일한 fields/tags + 평가 점수 5 필드. (txid, step_id) 결합 키로 백엔드 사후 결합.
36
-
37
- 이 단순한 모델 덕분에 hold/timeout/callback 등의 복잡도가 모두 제거됨.
32
+ LLM 호출 결과 pack 을 받아 즉시 송출한다. 결과를 기다리는 후처리가 없으므로
33
+ hold/timeout/callback 등의 복잡도가 없다.
38
34
  """
39
35
 
36
+ # >>> LLM-EVAL: 평가(LLM-judge 등)는 별도 파이프라인(``LlmEvaluatorTask``)에서 비동기로 돌고,
37
+ # 결과는 ``LlmStepEvalStatus`` pack(``llm_log_type=llm_step_eval_status``)으로 따로 송출된다
38
+ # — 원본 LlmStepStatus 와 같은 fields/tags + 평가 점수. (txid, step_id) 결합 키로 백엔드 사후 결합.
39
+ # <<< LLM-EVAL
40
+
40
41
  _instance = None
41
42
  _lock = threading.Lock()
42
43
 
@@ -53,14 +54,7 @@ class LlmLogSinkTask(object):
53
54
  return cls._instance
54
55
 
55
56
  def dispatch(self, pack):
56
- """LLM 호출 결과 pack 을 즉시 송출 + 평가 큐 트리거.
57
-
58
- 평가 워커 안에서 호출된 judge LLM call 인 경우 (intercept 가 fire 한 케이스):
59
- - operation_type='whatap_evaluation' / prompt_version='v1' 고정
60
- - parent (user) step 의 (txid, step_id, index) 그대로 propagate
61
- → dashboard 에서 같은 transaction 으로 묶여 보임
62
- - eval enqueue 는 재귀 가드가 자동 차단
63
- """
57
+ """LLM 호출 결과 pack 을 즉시 송출한다."""
64
58
  if not isinstance(pack, LlmStepStatus):
65
59
  return
66
60
 
@@ -77,23 +71,15 @@ class LlmLogSinkTask(object):
77
71
  % (pack.model, pack.input_tokens, pack.output_tokens, pack.cached_tokens, e),
78
72
  extra={'id': 'LLM024'})
79
73
 
80
- # ── judge LLM call (eval 워커 안에서 발생) — 고정 라벨 + parent step ids ──
74
+ # >>> LLM-EVAL: judge 호출(평가 워커 안에서 난 LLM 호출)이면 고정 라벨 + 부모(사용자) 스텝
75
+ # 좌표로 덮는다 — 위 set_context 가 현재 컨텍스트 좌표로 되돌렸으므로 다시 덮어야 한다.
76
+ # 그래야 dashboard 에서 사용자 호출과 같은 transaction 으로 묶여 보인다.
81
77
  try:
82
- from whatap.counter.tasks.llm_evaluator_task import (
83
- _is_in_evaluator_worker, _get_eval_worker_state,
84
- )
85
- if _is_in_evaluator_worker():
86
- state = _get_eval_worker_state() or {}
87
- pack.operation_type = 'whatap_evaluation'
88
- pack.prompt_version = 'v1'
89
- if state.get('parent_txid'):
90
- pack.txid = state['parent_txid']
91
- if state.get('parent_step_id'):
92
- pack.step_id = state['parent_step_id']
93
- if state.get('parent_index') is not None:
94
- pack.index = state['parent_index']
78
+ from whatap.llm.evaluators.hooks import mark_judge_call
79
+ mark_judge_call(pack)
95
80
  except Exception:
96
81
  pass
82
+ # <<< LLM-EVAL
97
83
 
98
84
  # tx_summary 누적
99
85
  self._accumulate_tx_summary(ctx, pack)
@@ -107,16 +93,17 @@ class LlmLogSinkTask(object):
107
93
  ctx._llm_last_error_prompt_version = (
108
94
  getattr(pack, 'prompt_version', 'v1') or 'v1')
109
95
 
110
- # pack 즉시 송출 — eval 결과 기다리지 않음
96
+ # pack 즉시 송출
111
97
  self._enqueue_for_send(pack)
112
98
 
113
- # 평가 큐 트리거 — worker 가 pack 을 복제해 LlmStepEvalStatus 로 만들어 점수 채워 송출
114
- # judge 호출 인 경우 enqueue_evaluation 의 재귀 가드가 차단
99
+ # >>> LLM-EVAL: 평가 큐 투입 — 워커가 pack 을 복제해 점수를 채워 LlmStepEvalStatus 로 송출.
100
+ # 원본 송출(위)은 평가 결과를 기다리지 않는다. judge 호출 자체는 재귀 가드가 걸러낸다.
115
101
  try:
116
- from whatap.counter.tasks.llm_evaluator_task import enqueue_evaluation
117
- enqueue_evaluation(pack)
102
+ from whatap.llm.evaluators.hooks import enqueue
103
+ enqueue(pack)
118
104
  except Exception as e:
119
105
  logging.warning('[LLM] eval enqueue failed: %s' % e, extra={'id': 'LLM034'})
106
+ # <<< LLM-EVAL
120
107
 
121
108
  def _enqueue_for_send(self, pack):
122
109
  """완성된 pack 을 송출 큐에 적재. queue full 이면 drop + LLM025."""
@@ -362,6 +349,7 @@ def send_llm_tx_status(ctx):
362
349
  LlmLogSinkTask.get_instance().send_tx_status(ctx)
363
350
 
364
351
 
352
+ # >>> LLM-EVAL: 평가 결과 pack 송출구 — 평가 워커와 submit_evaluation 만 부른다.
365
353
  def dispatch_llm_evaluation_pack(pack):
366
354
  """LlmStepEvalStatus pack 을 LogSink 인프라로 송출.
367
355
 
@@ -373,3 +361,4 @@ def dispatch_llm_evaluation_pack(pack):
373
361
  except Exception as e:
374
362
  logging.warning('[LLM] dispatch_llm_evaluation_pack failed: %s' % e,
375
363
  extra={'id': 'LLM037'})
364
+ # <<< LLM-EVAL
whatap/doctor.py CHANGED
@@ -229,7 +229,7 @@ def _llm_sdk_result(found):
229
229
 
230
230
 
231
231
  def check_llm_features(conf, sdk_present):
232
- """LLM 관측 on/off 와 Evaluation 상태를 요약한다(비용 경고 포함)."""
232
+ """LLM 관측 on/off 상태를 요약한다."""
233
233
  if conf is None or "__error__" in conf:
234
234
  return None
235
235
  llm_on = conf.get("llm_enabled", "false") == "true"
@@ -240,13 +240,18 @@ def check_llm_features(conf, sdk_present):
240
240
  "(references/llm-observability.md)")
241
241
  return Result(OK, "LLM 관측", "off")
242
242
 
243
+ result = Result(OK, "LLM 관측", "on")
244
+ # >>> LLM-EVAL: Evaluation 상태 요약(비용 경고 포함)
243
245
  if conf.get("llm_eval_enabled", "false") == "true":
244
246
  evs = conf.get("llm_eval_evaluators", "combined_judge, pii_leak, url_scan")
245
- return Result(WARN, "LLM Evaluation", "on / evaluators={0}".format(evs),
246
- hint="judge LLM 호출만큼 토큰·비용이 추가 발생. combined_judge 권장, "
247
- "필요시 llm_eval_sample_rate 로 비율 조절 (references/llm-evaluation.md)")
248
- return Result(OK, "LLM 관측", "on (Evaluation off)",
249
- hint="응답 품질 평가가 필요하면 llm_eval_enabled=true (references/llm-evaluation.md)")
247
+ result = Result(WARN, "LLM Evaluation", "on / evaluators={0}".format(evs),
248
+ hint="judge LLM 호출만큼 토큰·비용이 추가 발생. combined_judge 권장, "
249
+ "필요시 llm_eval_sample_rate 로 비율 조절 (references/llm-evaluation.md)")
250
+ else:
251
+ result = Result(OK, "LLM 관측", "on (Evaluation off)",
252
+ hint="응답 품질 평가가 필요하면 llm_eval_enabled=true (references/llm-evaluation.md)")
253
+ # <<< LLM-EVAL
254
+ return result
250
255
 
251
256
 
252
257
  def check_process():
@@ -279,7 +284,9 @@ _SCAN_PATTERNS = [
279
284
  ("workflow/agent", re.compile(r"@(workflow|agent|agent_name)\b|with\s+(workflow|agent|agent_name)\s*\(")),
280
285
  ("session/user", re.compile(r"set_(session|user)_id\s*\(|with\s+(session|user)\s*\(")),
281
286
  ("operation/prompt", re.compile(r"@prompt_meta\b|with\s+prompt_meta_scope\s*\(")),
287
+ # >>> LLM-EVAL: 고객 소스의 평가 API 사용
282
288
  ("evaluation", re.compile(r"@evaluate_with\b|evaluate_with\s*\(|with\s+evaluation_scope\s*\(")),
289
+ # <<< LLM-EVAL
283
290
  ("marker", re.compile(r"#\s*whatap")),
284
291
  ]
285
292
 
@@ -35,7 +35,6 @@ LangChain / LlamaIndex 등 AI 프레임워크 계측 시에도 framework 의 자
35
35
  저장: trace context (있으면) + ContextVar (항상). 양쪽 동시에 같은 stack 을 push/pop.
36
36
  중첩 안전 (LIFO stack). asyncio.create_task / TaskGroup 분기 시 ContextVar 가 inherit
37
37
  되어 sub-task 안에서 LLM 호출 시에도 같은 prompt_meta 가 보임.
38
- ``whatap.llm.evaluators.scope`` 와 같은 패턴.
39
38
  """
40
39
  import asyncio
41
40
  import contextvars
@@ -52,11 +52,14 @@ EvaluatorContext = namedtuple(
52
52
  # 인스턴스를 안 잡으므로 수명 무관. OAuth 등 못 읽으면 None → conf/env 로 폴백.
53
53
  'judge_base_url', # str|None — 앱 client 의 base_url
54
54
  'judge_api_key', # str|None — 앱 client 의 api_key
55
- 'judge_kind', # 'openai'|'anthropic'|None
55
+ 'judge_kind', # 'openai'|'anthropic'|'bedrock'|None
56
+ # Bedrock 은 API 키가 아니라 AWS 자격증명으로 서명한다 — 앱 client 의 리전·엔드포인트와
57
+ # botocore 자격증명 객체(닫히지 않고, 갱신형은 judge 시점에 스스로 갱신)를 담는다.
58
+ 'judge_aws', # dict|None — {'region', 'endpoint_url', 'profile', 'credentials'}
56
59
  ],
57
60
  )
58
- # 후행 optional 필드 defaults: event_loop, judge_base_url, judge_api_key, judge_kind
59
- EvaluatorContext.__new__.__defaults__ = (None, None, None, None)
61
+ # 후행 optional 필드 defaults: event_loop, judge_base_url, judge_api_key, judge_kind, judge_aws
62
+ EvaluatorContext.__new__.__defaults__ = (None, None, None, None, None)
60
63
 
61
64
 
62
65
  class EvaluatorResult(object):
@@ -47,7 +47,9 @@ LLM judge 기반 평가자는 별도 LLM 호출(judge call)로 점수를 산출
47
47
  Judge LLM 호출 헬퍼:
48
48
 
49
49
  make_openai_judge(client=..., model='gpt-4o-mini')
50
- make_anthropic_judge(client=..., model='claude-3-5-haiku-latest')
50
+ make_anthropic_judge(client=..., model='claude-haiku-5-5')
51
+ make_bedrock_judge(model='us.anthropic.claude-haiku-4-5-20251001-v1:0',
52
+ region_name='us-east-1') # Bedrock Runtime Converse (모델 필수)
51
53
 
52
54
  커스텀 judge 작성 시 ``LLMJudgeBase`` 상속하고 ``make_prompt`` / ``parse_judgment`` 만
53
55
  구현하면 된다.
@@ -56,6 +58,7 @@ from whatap.llm.evaluators.builtins.llm_judge import (
56
58
  LLMJudgeBase,
57
59
  make_openai_judge,
58
60
  make_anthropic_judge,
61
+ make_bedrock_judge,
59
62
  parse_json_response,
60
63
  set_default_judge_fn,
61
64
  get_default_judge_fn,
@@ -75,6 +78,7 @@ __all__ = [
75
78
  'LLMJudgeBase',
76
79
  'make_openai_judge',
77
80
  'make_anthropic_judge',
81
+ 'make_bedrock_judge',
78
82
  'parse_json_response',
79
83
  'set_default_judge_fn',
80
84
  'get_default_judge_fn',