whatap-python 2.2.1rc1__py3-none-any.whl → 2.2.2__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.
- whatap/__init__.py +158 -54
- whatap/build.py +2 -2
- whatap/conf/configuration.py +72 -1
- whatap/counter/tasks/llm_stat_task.py +10 -0
- whatap/doctor.py +402 -0
- whatap/llm/control/__init__.py +59 -0
- whatap/llm/control/cut.py +519 -0
- whatap/llm/control/guard.py +217 -0
- whatap/llm/control/trajectory.py +524 -0
- whatap/llm/definitions.py +21 -1
- whatap/llm/instrument/_boundary.py +136 -22
- whatap/llm/log_sink_packs/llm_step_status.py +9 -0
- whatap/llm/pricing.py +16 -1
- whatap/llm/providers/anthropic/messages/messages.py +3 -2
- whatap/llm/providers/anthropic/messages/messages_stream.py +3 -2
- whatap/llm/providers/bedrock/__init__.py +92 -0
- whatap/llm/providers/bedrock/_content.py +81 -0
- whatap/llm/providers/bedrock/converse/__init__.py +2 -0
- whatap/llm/providers/bedrock/converse/converse_context.py +155 -0
- whatap/llm/providers/bedrock/converse/converse_extractor.py +196 -0
- whatap/llm/providers/bedrock/event_stream.py +254 -0
- whatap/llm/providers/bedrock/http.py +395 -0
- whatap/llm/providers/bedrock/invoke_model/__init__.py +2 -0
- whatap/llm/providers/bedrock/invoke_model/invoke_model_context.py +126 -0
- whatap/llm/providers/bedrock/invoke_model/invoke_model_extractor.py +763 -0
- whatap/llm/providers/bedrock/runtime.py +291 -0
- whatap/llm/providers/bedrock/step.py +76 -0
- whatap/llm/providers/interceptor.py +10 -1
- whatap/llm/providers/openai/chat/chat.py +71 -14
- whatap/llm/providers/openai/chat/chat_extractor.py +8 -1
- whatap/llm/providers/openai/completions/completions.py +3 -2
- whatap/llm/providers/openai/embeddings/embeddings.py +3 -2
- whatap/llm/providers/openai/responses/responses.py +3 -2
- whatap/llm/providers/stream_accumulator.py +11 -1
- whatap/llm/providers/workflow.py +87 -5
- whatap/llm/stats/traj_actx_stat.py +50 -1
- whatap/net/async_sender.py +22 -9
- whatap/net/param_def.py +6 -0
- whatap/net/udp_session.py +33 -0
- whatap/scripts/__init__.py +89 -0
- whatap/skills/whatap-python-install/AGENTS.md +29 -0
- whatap/skills/whatap-python-install/INSTALL.md +96 -0
- whatap/skills/whatap-python-install/SKILL.md +42 -0
- whatap/skills/whatap-python-install/references/config-keys.md +59 -0
- whatap/skills/whatap-python-install/references/frameworks.md +55 -0
- whatap/skills/whatap-python-install/references/llm-api-usage.md +81 -0
- whatap/skills/whatap-python-install/references/llm-evaluation.md +114 -0
- whatap/skills/whatap-python-install/references/llm-observability.md +123 -0
- whatap/skills/whatap-python-install/references/troubleshooting.md +41 -0
- whatap/trace/mod/httpc/httpx.py +26 -3
- whatap/trace/mod/httpc/httpx2.py +23 -0
- whatap/trace/mod/httpc/requests.py +16 -3
- whatap/trace/mod/httpc/stream_end.py +136 -0
- whatap/trace/mod/httpc/util.py +231 -13
- whatap/trace/trace_error.py +9 -2
- whatap/trace/trace_module_definition.py +15 -0
- whatap/util/native_process.py +55 -27
- {whatap_python-2.2.1rc1.dist-info → whatap_python-2.2.2.dist-info}/METADATA +1 -1
- {whatap_python-2.2.1rc1.dist-info → whatap_python-2.2.2.dist-info}/RECORD +62 -34
- {whatap_python-2.2.1rc1.dist-info → whatap_python-2.2.2.dist-info}/entry_points.txt +2 -0
- {whatap_python-2.2.1rc1.dist-info → whatap_python-2.2.2.dist-info}/WHEEL +0 -0
- {whatap_python-2.2.1rc1.dist-info → whatap_python-2.2.2.dist-info}/top_level.txt +0 -0
whatap/__init__.py
CHANGED
|
@@ -378,11 +378,20 @@ def _is_instrumentation_ignored(definition_key, ignore_set):
|
|
|
378
378
|
def hooks(home):
|
|
379
379
|
ignore_set = _build_ignore_instrumentation_set()
|
|
380
380
|
skipped_keys = []
|
|
381
|
+
# LLM 계측은 llm_enabled 일 때만 **등록**한다. instrument_* 함수들도 각자
|
|
382
|
+
# llm_enabled 를 보고 되돌아가지만, 등록만 해둬도 대상 모듈이 로드되는 순간
|
|
383
|
+
# import 훅이 ``whatap.llm.*`` 서브트리를 통째로 끌어온다 — 켜지도 않은 기능
|
|
384
|
+
# 때문에. ``botocore.client``(Bedrock) 처럼 LLM 과 무관한 앱도 흔히 쓰는 모듈이
|
|
385
|
+
# 대상에 들어오면서 이 비용이 실제로 문제가 된다.
|
|
386
|
+
# conf.init() 이 hooks() 보다 먼저 돌므로 여기서 llm_enabled 는 이미 확정이다.
|
|
387
|
+
llm_enabled = bool(getattr(conf, 'llm_enabled', False))
|
|
381
388
|
try:
|
|
382
389
|
for key, value_list in DEFINITION.items():
|
|
383
390
|
if _is_instrumentation_ignored(key, ignore_set):
|
|
384
391
|
skipped_keys.append(key)
|
|
385
392
|
continue
|
|
393
|
+
if key.startswith('llm.') and not llm_enabled:
|
|
394
|
+
continue
|
|
386
395
|
for value in value_list:
|
|
387
396
|
if len(value) >= 3 and isinstance(value[2], str):
|
|
388
397
|
module_path = value[2]
|
|
@@ -553,9 +562,13 @@ def agent():
|
|
|
553
562
|
llm_accesskey = (preview_whatap_conf("llm_accesskey")
|
|
554
563
|
or os.environ.get("llm_accesskey"))
|
|
555
564
|
if llm_accesskey:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
565
|
+
if sys.platform.startswith('linux'):
|
|
566
|
+
# APM 과 같은 이유로 소유 기록까지 호출 스레드에서 끝낸다.
|
|
567
|
+
go(llm=True)
|
|
568
|
+
else:
|
|
569
|
+
t = threading.Thread(target=go, kwargs={'llm': True})
|
|
570
|
+
t.setDaemon(True)
|
|
571
|
+
t.start()
|
|
559
572
|
else:
|
|
560
573
|
whatap_print("WHATAP: llm_enabled=true 이나 llm_accesskey 가 "
|
|
561
574
|
"없어 LLM 에이전트 기동을 건너뜁니다")
|
|
@@ -569,31 +582,89 @@ ARCH = {
|
|
|
569
582
|
'ARM': 'arm',
|
|
570
583
|
'AArch64': 'arm64',
|
|
571
584
|
'arm64': 'arm64',
|
|
572
|
-
'aarch64': 'arm64'
|
|
585
|
+
'aarch64': 'arm64',
|
|
586
|
+
# Windows 의 platform.machine() 은 **대문자**로 돌려준다('AMD64'/'ARM64').
|
|
587
|
+
# Windows 는 arch 하위 디렉토리를 쓰지 않아 지금은 이 표를 안 타지만, 빠져 있으면
|
|
588
|
+
# 누가 조회하는 순간 KeyError 로 기동이 통째로 죽는 종류의 구멍이라 채워 둔다.
|
|
589
|
+
'AMD64': 'amd64',
|
|
590
|
+
'ARM64': 'arm64',
|
|
591
|
+
'i386': '386',
|
|
573
592
|
}
|
|
574
593
|
|
|
575
594
|
AGENT_NAME = 'whatap_python'
|
|
576
595
|
|
|
577
596
|
|
|
597
|
+
def agent_binary_name():
|
|
598
|
+
"""OS 별 에이전트 실행 파일 이름."""
|
|
599
|
+
return AGENT_NAME + '.exe' if sys.platform == 'win32' else AGENT_NAME
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def llm_agent_reldir():
|
|
603
|
+
"""LLM 전용 ``whatap_python`` 이 놓이는 ``$WHATAP_HOME`` 하위 상대경로.
|
|
604
|
+
|
|
605
|
+
Unix 계열은 ``<os>/<arch>`` (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64).
|
|
606
|
+
**Windows 는 arch 하위를 쓰지 않는다** — x64 하나면 ARM64 Windows 에서도 에뮬레이션
|
|
607
|
+
으로 돌아서 아키텍처별 빌드를 나눌 이유가 없다. 번들 APM 쪽
|
|
608
|
+
``whatap/agent/windows/`` 가 평평한 것과 같은 이유이고, 같은 모양이다.
|
|
609
|
+
|
|
610
|
+
반환: ``(상대경로, 사유)``. 상대경로가 None 이면 사유에 왜 못 구하는지가 담긴다.
|
|
611
|
+
"""
|
|
612
|
+
system = platform.system().lower()
|
|
613
|
+
if sys.platform == 'win32':
|
|
614
|
+
return system, ''
|
|
615
|
+
machine = platform.machine()
|
|
616
|
+
arch = ARCH.get(machine)
|
|
617
|
+
if not arch:
|
|
618
|
+
return None, ('unsupported architecture {0!r} for the LLM agent '
|
|
619
|
+
'(known: {1})'.format(machine, ', '.join(sorted(set(ARCH.values())))))
|
|
620
|
+
return os.path.join(system, arch), ''
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def llm_agent_path():
|
|
624
|
+
"""LLM 전용 ``whatap_python`` 의 배포 위치. @see ``llm_agent_reldir``
|
|
578
625
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
626
|
+
반환: ``(경로, 사유)``. 경로가 None 이면 사유에 왜 못 쓰는지가 담긴다.
|
|
627
|
+
"""
|
|
628
|
+
reldir, reason = llm_agent_reldir()
|
|
629
|
+
if reldir is None:
|
|
630
|
+
return None, reason
|
|
631
|
+
home_path = os.environ.get('WHATAP_HOME')
|
|
632
|
+
if not home_path:
|
|
633
|
+
return None, 'WHATAP_HOME is not set'
|
|
634
|
+
path = os.path.join(home_path, reldir, agent_binary_name())
|
|
635
|
+
if not os.path.exists(path):
|
|
636
|
+
return None, ('LLM agent binary not found: {0} '
|
|
637
|
+
'(place the LLM {1} under $WHATAP_HOME/{2}/)'.format(
|
|
638
|
+
path, agent_binary_name(), reldir.replace(os.sep, '/')))
|
|
639
|
+
return path, ''
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _native_agent_spec(opts, llm_executable=None):
|
|
644
|
+
from whatap.util.native_process import APM, LLM, AgentSpec
|
|
645
|
+
if llm_executable:
|
|
646
|
+
# LLM 은 llm_agent_path() 의 전용 바이너리를 제자리에서 실행한다(@see _start_go).
|
|
647
|
+
source, mode = llm_executable, LLM
|
|
648
|
+
else:
|
|
649
|
+
source = os.path.join(os.path.dirname(__file__), 'agent', 'linux',
|
|
650
|
+
ARCH[platform.machine()], AGENT_NAME)
|
|
651
|
+
mode = APM
|
|
583
652
|
env = os.environ.copy()
|
|
584
653
|
env.update(opts)
|
|
585
|
-
return AgentSpec(os.environ['WHATAP_HOME'], source, env)
|
|
654
|
+
return AgentSpec(os.environ['WHATAP_HOME'], source, env, mode=mode)
|
|
586
655
|
|
|
587
656
|
|
|
588
|
-
def _native_agent_log_path():
|
|
589
|
-
|
|
657
|
+
def _native_agent_log_path(llm=False):
|
|
658
|
+
# LLM 은 별개 프로세스라 기동 실패 원인이 APM 로그와 섞이지 않게 나눈다.
|
|
659
|
+
name = AGENT_NAME + ('.llm.log' if llm else '.log')
|
|
660
|
+
return os.path.join(os.environ.get('WHATAP_HOME', '.'), 'logs', name)
|
|
590
661
|
|
|
591
662
|
|
|
592
|
-
def _open_native_agent_log():
|
|
663
|
+
def _open_native_agent_log(llm=False):
|
|
593
664
|
# managed 경로는 PIPE 를 읽지 않으므로 네이티브 stdout/stderr 를 파일로 남겨
|
|
594
665
|
# 기동 직후 실패 원인을 추적할 수 있게 한다.
|
|
595
666
|
try:
|
|
596
|
-
log_path = _native_agent_log_path()
|
|
667
|
+
log_path = _native_agent_log_path(llm)
|
|
597
668
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
598
669
|
return open(log_path, 'ab')
|
|
599
670
|
except Exception as e:
|
|
@@ -602,44 +673,61 @@ def _open_native_agent_log():
|
|
|
602
673
|
|
|
603
674
|
|
|
604
675
|
def go(batch=False, opts={}, llm=False):
|
|
605
|
-
if sys.platform.startswith('linux') and not batch
|
|
676
|
+
if sys.platform.startswith('linux') and not batch:
|
|
606
677
|
from whatap.util.native_process import FAILED, ensure_agent_running
|
|
678
|
+
label = 'LLM' if llm else 'APM'
|
|
679
|
+
log_prefix = 'WHATAP_LLM_AGENT' if llm else 'WHATAP_AGENT'
|
|
680
|
+
llm_executable = None
|
|
681
|
+
if llm:
|
|
682
|
+
# 바이너리가 없으면 락·/proc 탐색 전에 기존처럼 건너뛴다.
|
|
683
|
+
llm_executable, reason = llm_agent_path()
|
|
684
|
+
if llm_executable is None:
|
|
685
|
+
whatap_error_log(
|
|
686
|
+
'WHATAP: skipping LLM agent start — {0}'.format(reason),
|
|
687
|
+
log_id='WHATAP_LLM_AGENT_UNAVAILABLE')
|
|
688
|
+
return
|
|
607
689
|
|
|
608
690
|
def on_child_exit(pid, exit_code):
|
|
609
691
|
whatap_error_log(
|
|
610
|
-
'WHATAP: native
|
|
611
|
-
pid, exit_code, _native_agent_log_path()),
|
|
612
|
-
log_id='
|
|
692
|
+
'WHATAP: native {} exited (PID: {}, exit code: {}, log: {})'.format(
|
|
693
|
+
label, pid, exit_code, _native_agent_log_path(llm)),
|
|
694
|
+
log_id=log_prefix + '_EXITED')
|
|
613
695
|
|
|
614
696
|
try:
|
|
615
|
-
spec = _native_agent_spec(opts)
|
|
697
|
+
spec = _native_agent_spec(opts, llm_executable)
|
|
616
698
|
result = ensure_agent_running(
|
|
617
699
|
spec, lambda: _start_go(batch, opts, llm, managed=True),
|
|
618
700
|
on_child_exit=on_child_exit)
|
|
619
701
|
if result.status == FAILED:
|
|
620
|
-
whatap_error_log('WHATAP: native
|
|
621
|
-
log_id='
|
|
702
|
+
whatap_error_log('WHATAP: native {} startup failed'.format(label),
|
|
703
|
+
log_id=log_prefix + '_START_FAILED')
|
|
622
704
|
else:
|
|
623
|
-
whatap_print('WHATAP: native
|
|
624
|
-
result.status, result.pid))
|
|
705
|
+
whatap_print('WHATAP: native {} {} (PID: {})'.format(
|
|
706
|
+
label, result.status, result.pid))
|
|
625
707
|
except Exception as e:
|
|
626
|
-
whatap_error_log('WHATAP: native
|
|
627
|
-
log_id='
|
|
708
|
+
whatap_error_log('WHATAP: native {} startup failed: {}'.format(label, e),
|
|
709
|
+
log_id=log_prefix + '_START_FAILED')
|
|
628
710
|
return
|
|
629
711
|
return _start_go(batch, opts, llm)
|
|
630
712
|
|
|
631
713
|
|
|
632
714
|
def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
633
|
-
# LLM 에이전트는
|
|
634
|
-
#
|
|
635
|
-
#
|
|
636
|
-
#
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
715
|
+
# LLM 에이전트는 전용 whatap_python 이 $WHATAP_HOME 하위에 따로 배포된다
|
|
716
|
+
# (unix 는 <os>/<arch>, windows 는 windows/ — @see llm_agent_reldir).
|
|
717
|
+
# **OS 로 막지 않는다** — 그 자리에 바이너리가 놓였는지로 판단한다(그래야 windows
|
|
718
|
+
# 빌드가 배포되는 순간 코드 변경 없이 살아난다).
|
|
719
|
+
#
|
|
720
|
+
# 여기서 미리 걸러야 하는 이유: 그냥 흘려보내면 배포 분기로 빠져 **번들 APM
|
|
721
|
+
# 바이너리**를 골라 ``--llm`` 으로 띄우고, 심지어 APM 과 같은
|
|
722
|
+
# ``$WHATAP_HOME/whatap_python`` 에 배포해 서로를 덮는다(둘은 다른 바이너리다).
|
|
723
|
+
llm_agent_cmd = None
|
|
724
|
+
if llm:
|
|
725
|
+
llm_agent_cmd, reason = llm_agent_path()
|
|
726
|
+
if llm_agent_cmd is None:
|
|
727
|
+
whatap_error_log(
|
|
728
|
+
'WHATAP: skipping LLM agent start — {0}'.format(reason),
|
|
729
|
+
log_id='WHATAP_LLM_AGENT_UNAVAILABLE')
|
|
730
|
+
return
|
|
643
731
|
|
|
644
732
|
newenv=os.environ.copy()
|
|
645
733
|
newenv['WHATAP_VERSION'] = build.version
|
|
@@ -700,24 +788,20 @@ def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
|
700
788
|
home_path= os.environ.get(home)
|
|
701
789
|
|
|
702
790
|
# Windows uses .exe extension
|
|
703
|
-
|
|
791
|
+
binary_name = agent_binary_name()
|
|
704
792
|
|
|
705
|
-
# LLM mode
|
|
706
|
-
#
|
|
707
|
-
# It is executed in place from there rather than deployed
|
|
708
|
-
# package into $WHATAP_HOME/whatap_python, which the APM
|
|
709
|
-
# are
|
|
710
|
-
|
|
793
|
+
# LLM mode runs a dedicated whatap_python binary that ships separately under
|
|
794
|
+
# $WHATAP_HOME (unix: <os>/<arch>, windows: windows/ — @see llm_agent_reldir).
|
|
795
|
+
# It is executed in place from there rather than deployed
|
|
796
|
+
# from the bundled package into $WHATAP_HOME/whatap_python, which the APM
|
|
797
|
+
# agent uses (the two are distinct binaries and would otherwise clobber each
|
|
798
|
+
# other). **OS 로 가르지 않는다** — Windows 만 빼면 거기서 APM 바이너리를
|
|
799
|
+
# 골라 --llm 으로 띄우고 APM 의 배포 경로까지 덮어쓴다.
|
|
800
|
+
run_in_place = llm
|
|
711
801
|
|
|
712
802
|
if run_in_place:
|
|
713
|
-
|
|
714
|
-
source_agent =
|
|
715
|
-
if not os.path.exists(source_agent):
|
|
716
|
-
raise FileNotFoundError(
|
|
717
|
-
'LLM agent binary not found: {0} '
|
|
718
|
-
'(place the LLM {1} under $WHATAP_HOME/{2}/{3}/)'.format(
|
|
719
|
-
source_agent, agent_binary_name,
|
|
720
|
-
platform.system().lower(), ARCH[platform.machine()]))
|
|
803
|
+
# 경로·존재 확인은 _start_go() 진입부의 llm_agent_path() 가 이미 했다(단일 소스).
|
|
804
|
+
source_agent = llm_agent_cmd
|
|
721
805
|
# Ensure executable (best effort; ignore on read-only locations)
|
|
722
806
|
if not os.access(source_agent, os.X_OK):
|
|
723
807
|
try:
|
|
@@ -726,7 +810,7 @@ def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
|
726
810
|
whatap_print('WHATAP: chmod LLM agent binary failed: %s' % e)
|
|
727
811
|
agent_cmd = source_agent
|
|
728
812
|
else:
|
|
729
|
-
dest_agent = os.path.join(home_path,
|
|
813
|
+
dest_agent = os.path.join(home_path, binary_name)
|
|
730
814
|
|
|
731
815
|
# Try to remove existing binary
|
|
732
816
|
# Windows: File may still be locked even after waiting, ignore error and use existing binary
|
|
@@ -753,7 +837,7 @@ def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
|
753
837
|
source_cwd = os.path.join(os.path.join(os.path.dirname(__file__), 'agent'), platform.system().lower(),
|
|
754
838
|
ARCH[platform.machine()])
|
|
755
839
|
|
|
756
|
-
source_agent = os.path.join(source_cwd,
|
|
840
|
+
source_agent = os.path.join(source_cwd, binary_name)
|
|
757
841
|
|
|
758
842
|
# Deploy agent binary only if needed
|
|
759
843
|
if needs_deployment:
|
|
@@ -770,9 +854,9 @@ def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
|
770
854
|
|
|
771
855
|
# Windows uses absolute path, Unix/Linux uses relative path
|
|
772
856
|
if sys.platform == 'win32':
|
|
773
|
-
agent_cmd = os.path.join(home_path,
|
|
857
|
+
agent_cmd = os.path.join(home_path, binary_name)
|
|
774
858
|
else:
|
|
775
|
-
agent_cmd = './{0}'.format(
|
|
859
|
+
agent_cmd = './{0}'.format(binary_name)
|
|
776
860
|
|
|
777
861
|
sockfile_path = os.path.join(home_path, 'run')
|
|
778
862
|
if not os.path.exists(sockfile_path):
|
|
@@ -833,7 +917,7 @@ def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
|
833
917
|
creationflags=DETACHED_PROCESS,
|
|
834
918
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
835
919
|
elif managed:
|
|
836
|
-
go_out = _open_native_agent_log()
|
|
920
|
+
go_out = _open_native_agent_log(llm)
|
|
837
921
|
try:
|
|
838
922
|
process = subprocess.Popen(cmd_args,
|
|
839
923
|
cwd=home_path, env=newenv,
|
|
@@ -1188,6 +1272,26 @@ def _config_port():
|
|
|
1188
1272
|
|
|
1189
1273
|
|
|
1190
1274
|
def configLlmPort():
|
|
1275
|
+
if sys.platform.startswith('linux') and preview_whatap_conf('llm_enabled') == 'true':
|
|
1276
|
+
# 바이너리가 없으면 go(llm=True) 도 건너뛰므로 지킬 LLM 에이전트가 없다.
|
|
1277
|
+
llm_executable = llm_agent_path()[0]
|
|
1278
|
+
if llm_executable:
|
|
1279
|
+
from whatap.util.native_process import LLM, acquire_start_lock, configured_channel, find_existing_agent
|
|
1280
|
+
try:
|
|
1281
|
+
spec = _native_agent_spec({}, llm_executable)
|
|
1282
|
+
with acquire_start_lock(spec):
|
|
1283
|
+
if find_existing_agent(spec):
|
|
1284
|
+
# configPort() 와 같은 계약: 살아 있는 LLM 에이전트의 채널을 다시 쓰지 않는다.
|
|
1285
|
+
return configured_channel(spec.home, spec.environment,
|
|
1286
|
+
include_unix=False, mode=LLM)[1]
|
|
1287
|
+
return _config_llm_port()
|
|
1288
|
+
except Exception as e:
|
|
1289
|
+
# 락 타임아웃/다중 인스턴스 등은 CLI 를 죽이지 말고 기존 포트 할당으로 폴백
|
|
1290
|
+
whatap_print('WHATAP: live LLM agent check skipped, falling back to port file: %s' % e)
|
|
1291
|
+
return _config_llm_port()
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def _config_llm_port():
|
|
1191
1295
|
# force_llm_net_udp_port가 설정된 경우 lock 파일 무시하고 해당 포트 강제 사용
|
|
1192
1296
|
force_port_str = preview_whatap_conf("force_llm_net_udp_port")
|
|
1193
1297
|
if force_port_str and force_port_str not in ('false', 'False'):
|
whatap/build.py
CHANGED
whatap/conf/configuration.py
CHANGED
|
@@ -39,7 +39,7 @@ Configuration = {
|
|
|
39
39
|
# 해당 메서드 실행이 워크플로우가 되고 내부 LLM 호출은 스텝. workflow_plugin 참조.
|
|
40
40
|
"workflow_method_patterns": "",
|
|
41
41
|
# 트랜잭션(서비스) 진입점으로 자동 래핑할 메서드 패턴 (module:Class.method, 콤마 구분).
|
|
42
|
-
# 해당 메서드 호출이 하나의 트랜잭션이 된다(
|
|
42
|
+
# 해당 메서드 호출이 하나의 트랜잭션이 된다(구 @method_profiling 데코레이터 대체).
|
|
43
43
|
# 프레임워크 자동계측이 안 되는 진입점용. hook_service_patterns 모듈 참조.
|
|
44
44
|
"hook_service_patterns": "",
|
|
45
45
|
|
|
@@ -144,6 +144,26 @@ Configuration = {
|
|
|
144
144
|
# APM Go Agent 가 떠 있는 배포(apm_enabled=true)에서만 의미가 있다.
|
|
145
145
|
"llm_apm_txn_enabled": False,
|
|
146
146
|
|
|
147
|
+
# ── LLM 계측 킬스위치 (문제 발생 시 에이전트 다운그레이드 없이 되돌릴 레버) ──
|
|
148
|
+
#
|
|
149
|
+
# **여기 등록돼 있어야 whatap.conf 의 false 가 먹는다.** ``Configure.setProperty`` 는
|
|
150
|
+
# 이 표에 bool 기본값이 있는 키만 문자열 'false' → False 로 강제 변환한다. 미등록 키는
|
|
151
|
+
# ``'false'`` 문자열로 남고 ``bool('false') == True`` 라 게이트가 계속 열려 있다
|
|
152
|
+
# (실측으로 확인: 등록 키는 False, 미등록 키는 'false'/truthy).
|
|
153
|
+
#
|
|
154
|
+
# llm_workflow_enabled : 워크플로우/에이전트 경계 계측 전체 (구 키 llm_trajectory_enabled
|
|
155
|
+
# 와 AND — 어느 쪽을 false 로 줘도 꺼진다)
|
|
156
|
+
# mtrace_kanban_enabled: hermes 칸반 카드 경계 전파
|
|
157
|
+
# llm_thread_pool_propagation_enabled:
|
|
158
|
+
# Thread/ThreadPoolExecutor 컨텍스트 전파 몽키패치
|
|
159
|
+
# llm_httpc_step_enabled: 워크플로우 안의 httpc 스텝을 **전부** LLM 채널에도 싣는다
|
|
160
|
+
# (APM 쪽은 무관하게 그대로). false 면 LLM API httpc 만.
|
|
161
|
+
"llm_workflow_enabled": True,
|
|
162
|
+
"llm_trajectory_enabled": True,
|
|
163
|
+
"mtrace_kanban_enabled": True,
|
|
164
|
+
"llm_thread_pool_propagation_enabled": True,
|
|
165
|
+
"llm_httpc_step_enabled": True,
|
|
166
|
+
|
|
147
167
|
# 프로세스 종료 시 송출 큐를 비우는 최대 대기(초, 0 이면 대기 안 함). sender 스레드는
|
|
148
168
|
# daemon 이라 exit 시 즉사하므로, 원샷 CLI 에이전트처럼 단명하는 프로세스는 마지막
|
|
149
169
|
# TX_END 가 큐에 남은 채 사라질 수 있다. standalone 모듈은 자체 atexit 로 이걸
|
|
@@ -369,6 +389,50 @@ Configuration = {
|
|
|
369
389
|
# (llm_actx_bands)만 붙는다.
|
|
370
390
|
"llm_traj_actx_enabled": True,
|
|
371
391
|
|
|
392
|
+
# ── LLM 트래젝토리 제어 (ParamPack 905) ──
|
|
393
|
+
# 실행 중인 트래젝토리(mtid 가족)를 서버에서 중지시킨다. 스코프는 트랜잭션 되감기가
|
|
394
|
+
# 아니라 **진행 중 LLM 호출 끊기 + 이후 LLM 호출 차단** 이다 — 앱은 평범한 연결오류를
|
|
395
|
+
# 받고 스스로 응답한다(되감기는 gthread 에서 클라이언트 60초 행을 만들었다).
|
|
396
|
+
#
|
|
397
|
+
# llm_control_enabled: 기능 킬스위치. false 면 표시도 끊기도 하지 않고 905 에
|
|
398
|
+
# 204(지원하지 않음)로 답한다 — 400(대상 없음)으로 답하면 "왜 안 되지" 를 데이터
|
|
399
|
+
# 문제로 오독하게 된다.
|
|
400
|
+
# llm_control_async_cut_enabled: async 호출을 우리 태스크로 감쌀지. 코루틴은 소켓이
|
|
401
|
+
# 스택에 없어(sync 는 있다) 취소 핸들을 **미리** 만들어 두는 것이 유일한 수단이라,
|
|
402
|
+
# 중지 요청이 없어도 모든 async LLM 호출에 적용된다. 의심스러운 환경(비-asyncio
|
|
403
|
+
# 런타임 등)에서 이것만 끄면 async 는 "이후 호출 차단" 만 남는다(진행 중 호출은
|
|
404
|
+
# 그대로 끝난다). sync 경로는 이 값과 무관하게 동작한다.
|
|
405
|
+
# llm_control_stop_transaction_enabled: 중지 스코프. true(기본)면 진행 중 LLM 호출을
|
|
406
|
+
# 끊은 뒤 **그 트랜잭션을 실행 중인 주체까지 끝낸다**(async=태스크 취소,
|
|
407
|
+
# sync=스레드에 BaseException 주입). false 면 LLM 호출만 끊고 앱은 계속 돈다
|
|
408
|
+
# (이후 LLM 호출은 전부 실패하므로 LLM 작업량은 0).
|
|
409
|
+
# **끄는 것이 나은 경우: gunicorn gthread 워커.** 이전 PoC 실측에서 그 조합만 응답
|
|
410
|
+
# 없이 클라이언트가 60초 대기했다(sync/uvicorn 은 그 요청 500, 워커·옆 요청 무사).
|
|
411
|
+
"llm_control_enabled": True,
|
|
412
|
+
# llm_control_stop_thread_enabled: sync 스레드에 **예외 주입**까지 할 것인가(기본 off).
|
|
413
|
+
# 다른 종료 수단과 안전성이 다르다 — asyncio 취소는 await 지점, greenlet kill 은
|
|
414
|
+
# switch 지점, 협조적 체크포인트는 우리 프레임에서 발동하지만, 스레드 주입만
|
|
415
|
+
# **임의 바이트코드**에서 터진다(실측: 수동 락 구간에 걸리면 락이 영구 점유되고,
|
|
416
|
+
# 블로킹 시스템콜 중에는 아예 발동하지 않는다). 끄더라도 sync 앱은 협조적
|
|
417
|
+
# 체크포인트로 멈춘다 — 계측 경계(LLM 호출/HTTP/워크플로우)를 지나면 끝난다.
|
|
418
|
+
# 비계측 블로킹에 갇힌 스레드까지 강제 종료해야 하는 운영자만 켠다.
|
|
419
|
+
"llm_control_stop_transaction_enabled": True,
|
|
420
|
+
"llm_control_stop_thread_enabled": False,
|
|
421
|
+
"llm_control_async_cut_enabled": True,
|
|
422
|
+
# llm_blocked_users: **사용자 단위 차단**(콤마 구분 user_id 목록). 905 가 서버에서
|
|
423
|
+
# mtid 하나를 지목하는 일회성 사건이라면, 이쪽은 **설정이 곧 상태**다 — 여기 올라
|
|
424
|
+
# 있는 동안 그 사용자의 LLM 호출은 계속 막히고, 빼면 즉시 풀린다(설정 핫리로드
|
|
425
|
+
# 주기 30초). 집행 지점은 905 와 같다: 새 호출은 가드에서, SDK/앱 재시도는 httpc
|
|
426
|
+
# 체크포인트에서, 진행 중 호출은 1초 틱 스윕에서 끊는다.
|
|
427
|
+
# 대상 값은 ``ctx.login_id`` — set_user_id()/llm_user_header/llm_user_meta_key/
|
|
428
|
+
# hermes 4경로로 채워지는 그 user_id 이고, 로그싱크 팩의 user_id 태그와 같은 값이라
|
|
429
|
+
# 화면에서 본 값을 그대로 옮겨 적으면 된다. 대소문자 무시, 미식별('0')은 대상 아님.
|
|
430
|
+
# llm_blocked_users=u-1024,abuse@example.com
|
|
431
|
+
# **해제는 줄을 지우는 게 아니라 값을 비우는 것**(`llm_blocked_users=`)이다. 핫리로드
|
|
432
|
+
# (`Configure.load`)는 파일에 있는 키만 다시 세팅하고 사라진 키를 기본값으로 되돌리지
|
|
433
|
+
# 않는다 — 줄을 지우면 재기동 전까지 마지막 값이 그대로 남아 계속 차단된다.
|
|
434
|
+
"llm_blocked_users": "",
|
|
435
|
+
|
|
372
436
|
# ── LLM Evaluation ──
|
|
373
437
|
# 평가 파이프라인 마스터 토글. false면 enqueue/worker가 동작하지 않음.
|
|
374
438
|
"llm_eval_enabled": False,
|
|
@@ -417,6 +481,13 @@ Configuration = {
|
|
|
417
481
|
# 평가자만 적용되고 이 전역 목록은 적용되지 않는다.
|
|
418
482
|
"llm_eval_evaluators": "",
|
|
419
483
|
|
|
484
|
+
# 정형 평가자(pii_leak / url_scan)의 스캔 범위. 기본은 output_text(모델 응답)만 —
|
|
485
|
+
# "모델이 응답에 PII/URL 을 흘렸나(leak)" 관점이라 사용자가 프롬프트에 넣은 값은
|
|
486
|
+
# 대상이 아니다. true 로 두면 같은 LLM 호출을 input+output 한 덩어리로 보고
|
|
487
|
+
# input_text 도 함께 스캔 — 입력에만 있어도(예: 프롬프트에 붙은 카드번호/URL) 검출된다
|
|
488
|
+
# (프롬프트-사이드 DLP). LLM judge(combined_judge 등)에는 영향 없음.
|
|
489
|
+
"llm_eval_scan_input": False,
|
|
490
|
+
|
|
420
491
|
# ── conf 기반 default judge (앱 코드 0) ──
|
|
421
492
|
# llm-as-judge 평가자(combined_judge 등)는 평가용 LLM 호출에 자체 자격증명이 필요하다.
|
|
422
493
|
# judge 해석 순서: ① evaluator(judge_fn=) → ② set_default_judge_fn() → ③ 계측이
|
|
@@ -28,6 +28,16 @@ class LlmStatTask(Thread):
|
|
|
28
28
|
while True:
|
|
29
29
|
current_time = time.time()
|
|
30
30
|
time.sleep(1)
|
|
31
|
+
# 차단 사용자(llm_blocked_users)의 **진행 중** LLM 호출 끊기. 가드/체크포인트는
|
|
32
|
+
# "다음 호출" 만 막으므로, 설정으로 차단을 건 순간 이미 돌고 있는 스트리밍은
|
|
33
|
+
# 여기서 끊어야 운영자가 본 대로 동작한다. 목록이 비면(기본) getattr 한 번에
|
|
34
|
+
# 끝나고, 모집단도 진행 중 호출 레지스트리(0~수 개)라 틱 비용이 없다.
|
|
35
|
+
# 통계와 무관한 일이지만 LLM 모드에서 도는 주기 스레드가 여기뿐이다.
|
|
36
|
+
try:
|
|
37
|
+
from whatap.llm.control import enforce_blocked_users
|
|
38
|
+
enforce_blocked_users()
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logging.debug(e, extra={'id': 'WA186'}, exc_info=True)
|
|
31
41
|
for stat in self.stats:
|
|
32
42
|
last_executed_time = self.last_executed[stat.name()]
|
|
33
43
|
interval = stat.interval()
|