whatap-python 2.1.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.
- whatap/LICENSE +0 -0
- whatap/README.rst +49 -0
- whatap/__init__.py +923 -0
- whatap/__main__.py +4 -0
- whatap/agent/darwin/amd64/whatap_python +0 -0
- whatap/agent/darwin/arm64/whatap_python +0 -0
- whatap/agent/linux/amd64/whatap_python +0 -0
- whatap/agent/linux/arm64/whatap_python +0 -0
- whatap/agent/windows/whatap_python.exe +0 -0
- whatap/bootstrap/__init__.py +0 -0
- whatap/bootstrap/sitecustomize.py +19 -0
- whatap/build.py +4 -0
- whatap/conf/__init__.py +0 -0
- whatap/conf/configuration.py +280 -0
- whatap/conf/configure.py +105 -0
- whatap/conf/license.py +49 -0
- whatap/control/__init__.py +0 -0
- whatap/counter/__init__.py +14 -0
- whatap/counter/counter_manager.py +45 -0
- whatap/counter/tasks/__init__.py +3 -0
- whatap/counter/tasks/base_task.py +26 -0
- whatap/counter/tasks/llm_evaluator_task.py +501 -0
- whatap/counter/tasks/llm_log_sink_task.py +309 -0
- whatap/counter/tasks/llm_stat_task.py +78 -0
- whatap/counter/tasks/openfiledescriptor.py +67 -0
- whatap/io/__init__.py +1 -0
- whatap/io/data_inputx.py +161 -0
- whatap/io/data_outputx.py +262 -0
- whatap/llm/__init__.py +17 -0
- whatap/llm/definitions.py +43 -0
- whatap/llm/evaluators/__init__.py +136 -0
- whatap/llm/evaluators/base.py +114 -0
- whatap/llm/evaluators/builtins/__init__.py +91 -0
- whatap/llm/evaluators/builtins/answer_relevance.py +46 -0
- whatap/llm/evaluators/builtins/combined_judge.py +271 -0
- whatap/llm/evaluators/builtins/factuality.py +71 -0
- whatap/llm/evaluators/builtins/hallucination.py +97 -0
- whatap/llm/evaluators/builtins/llm_judge.py +516 -0
- whatap/llm/evaluators/builtins/pii_leak.py +214 -0
- whatap/llm/evaluators/builtins/prompt_injection.py +71 -0
- whatap/llm/evaluators/builtins/toxicity.py +53 -0
- whatap/llm/evaluators/builtins/url_scan.py +194 -0
- whatap/llm/evaluators/registry.py +192 -0
- whatap/llm/evaluators/sampler.py +83 -0
- whatap/llm/evaluators/scope.py +334 -0
- whatap/llm/features.py +66 -0
- whatap/llm/log_sink_packs/__init__.py +9 -0
- whatap/llm/log_sink_packs/llm_input_message.py +16 -0
- whatap/llm/log_sink_packs/llm_log_sink_pack.py +72 -0
- whatap/llm/log_sink_packs/llm_output_message.py +19 -0
- whatap/llm/log_sink_packs/llm_step_eval_status.py +94 -0
- whatap/llm/log_sink_packs/llm_step_status.py +118 -0
- whatap/llm/log_sink_packs/llm_system_message.py +16 -0
- whatap/llm/log_sink_packs/llm_tool_calls.py +44 -0
- whatap/llm/log_sink_packs/llm_tool_results.py +16 -0
- whatap/llm/log_sink_packs/llm_tx_status.py +108 -0
- whatap/llm/pricing.py +236 -0
- whatap/llm/prompt_meta.py +288 -0
- whatap/llm/providers/__init__.py +0 -0
- whatap/llm/providers/anthropic/__init__.py +37 -0
- whatap/llm/providers/anthropic/messages/__init__.py +0 -0
- whatap/llm/providers/anthropic/messages/messages.py +70 -0
- whatap/llm/providers/anthropic/messages/messages_context.py +76 -0
- whatap/llm/providers/anthropic/messages/messages_extractor.py +126 -0
- whatap/llm/providers/interceptor.py +182 -0
- whatap/llm/providers/openai/__init__.py +133 -0
- whatap/llm/providers/openai/chat/__init__.py +0 -0
- whatap/llm/providers/openai/chat/chat.py +82 -0
- whatap/llm/providers/openai/chat/chat_context.py +78 -0
- whatap/llm/providers/openai/chat/chat_extractor.py +127 -0
- whatap/llm/providers/openai/completions/__init__.py +0 -0
- whatap/llm/providers/openai/completions/completions.py +70 -0
- whatap/llm/providers/openai/completions/completions_context.py +31 -0
- whatap/llm/providers/openai/completions/completions_extractor.py +61 -0
- whatap/llm/providers/openai/content_parser.py +41 -0
- whatap/llm/providers/openai/embeddings/__init__.py +0 -0
- whatap/llm/providers/openai/embeddings/embeddings.py +59 -0
- whatap/llm/providers/openai/embeddings/embeddings_context.py +25 -0
- whatap/llm/providers/openai/embeddings/embeddings_extractor.py +26 -0
- whatap/llm/providers/openai/responses/__init__.py +0 -0
- whatap/llm/providers/openai/responses/responses.py +70 -0
- whatap/llm/providers/openai/responses/responses_context.py +88 -0
- whatap/llm/providers/openai/responses/responses_extractor.py +126 -0
- whatap/llm/providers/stream_accumulator.py +73 -0
- whatap/llm/stats/__init__.py +35 -0
- whatap/llm/stats/active_stat.py +86 -0
- whatap/llm/stats/answer_relevance_eval_stat.py +10 -0
- whatap/llm/stats/api_status_stat.py +35 -0
- whatap/llm/stats/base_stat.py +107 -0
- whatap/llm/stats/combined_judge_eval_stat.py +11 -0
- whatap/llm/stats/error_stat.py +59 -0
- whatap/llm/stats/eval_stat.py +225 -0
- whatap/llm/stats/factuality_eval_stat.py +10 -0
- whatap/llm/stats/feature_stat.py +104 -0
- whatap/llm/stats/finish_stat.py +105 -0
- whatap/llm/stats/hallucination_eval_stat.py +10 -0
- whatap/llm/stats/meter.py +18 -0
- whatap/llm/stats/perf_stat.py +117 -0
- whatap/llm/stats/pii_leak_eval_stat.py +12 -0
- whatap/llm/stats/prompt_injection_eval_stat.py +10 -0
- whatap/llm/stats/token_usage_stat.py +133 -0
- whatap/llm/stats/toxicity_eval_stat.py +10 -0
- whatap/llm/stats/url_scan_eval_stat.py +12 -0
- whatap/net/__init__.py +0 -0
- whatap/net/async_sender.py +107 -0
- whatap/net/packet_enum.py +44 -0
- whatap/net/packet_type_enum.py +31 -0
- whatap/net/param_def.py +69 -0
- whatap/net/stackhelper.py +87 -0
- whatap/net/udp_session.py +394 -0
- whatap/net/udp_thread.py +54 -0
- whatap/pack/__init__.py +0 -0
- whatap/pack/logSinkPack.py +77 -0
- whatap/pack/pack.py +34 -0
- whatap/pack/pack_enum.py +41 -0
- whatap/pack/tagCountPack.py +61 -0
- whatap/scripts/__init__.py +208 -0
- whatap/trace/__init__.py +12 -0
- whatap/trace/mod/__init__.py +0 -0
- whatap/trace/mod/amqp/__init__.py +0 -0
- whatap/trace/mod/amqp/kombu.py +122 -0
- whatap/trace/mod/amqp/pika.py +62 -0
- whatap/trace/mod/application/__init__.py +0 -0
- whatap/trace/mod/application/bottle.py +34 -0
- whatap/trace/mod/application/celery.py +81 -0
- whatap/trace/mod/application/cherrypy.py +30 -0
- whatap/trace/mod/application/django.py +287 -0
- whatap/trace/mod/application/django_asgi.py +266 -0
- whatap/trace/mod/application/django_py3.py +251 -0
- whatap/trace/mod/application/fastapi/__init__.py +31 -0
- whatap/trace/mod/application/fastapi/endpoint.py +73 -0
- whatap/trace/mod/application/fastapi/exception_log.py +63 -0
- whatap/trace/mod/application/fastapi/instrumentation.py +204 -0
- whatap/trace/mod/application/fastapi/scope.py +115 -0
- whatap/trace/mod/application/fastapi/transaction.py +67 -0
- whatap/trace/mod/application/flask.py +52 -0
- whatap/trace/mod/application/frappe.py +224 -0
- whatap/trace/mod/application/graphql.py +170 -0
- whatap/trace/mod/application/nameko.py +39 -0
- whatap/trace/mod/application/odoo.py +63 -0
- whatap/trace/mod/application/starlette.py +126 -0
- whatap/trace/mod/application/tornado.py +163 -0
- whatap/trace/mod/application/wsgi.py +195 -0
- whatap/trace/mod/database/__init__.py +0 -0
- whatap/trace/mod/database/cxoracle.py +49 -0
- whatap/trace/mod/database/mongo.py +169 -0
- whatap/trace/mod/database/mysql.py +80 -0
- whatap/trace/mod/database/neo4j.py +90 -0
- whatap/trace/mod/database/psycopg2.py +45 -0
- whatap/trace/mod/database/psycopg3.py +359 -0
- whatap/trace/mod/database/redis.py +122 -0
- whatap/trace/mod/database/sqlalchemy.py +213 -0
- whatap/trace/mod/database/sqlite3.py +130 -0
- whatap/trace/mod/database/util.py +630 -0
- whatap/trace/mod/email/__init__.py +0 -0
- whatap/trace/mod/email/smtp.py +78 -0
- whatap/trace/mod/httpc/__init__.py +0 -0
- whatap/trace/mod/httpc/django.py +31 -0
- whatap/trace/mod/httpc/httplib.py +70 -0
- whatap/trace/mod/httpc/httpx.py +62 -0
- whatap/trace/mod/httpc/requests.py +20 -0
- whatap/trace/mod/httpc/urllib3.py +27 -0
- whatap/trace/mod/httpc/util.py +388 -0
- whatap/trace/mod/logging.py +161 -0
- whatap/trace/mod/plugin.py +84 -0
- whatap/trace/mod/standalone/__init__.py +0 -0
- whatap/trace/mod/standalone/multiple.py +293 -0
- whatap/trace/mod/standalone/single.py +135 -0
- whatap/trace/simple_trace_context.py +18 -0
- whatap/trace/trace_context.py +212 -0
- whatap/trace/trace_context_manager.py +244 -0
- whatap/trace/trace_error.py +84 -0
- whatap/trace/trace_handler.py +89 -0
- whatap/trace/trace_import.py +91 -0
- whatap/trace/trace_module_definition.py +156 -0
- whatap/util/__init__.py +0 -0
- whatap/util/bit_util.py +49 -0
- whatap/util/cardinality/__init__.py +0 -0
- whatap/util/cardinality/hyperloglog.py +84 -0
- whatap/util/cardinality/murmurhash.py +20 -0
- whatap/util/cardinality/registerset.py +60 -0
- whatap/util/compare_util.py +19 -0
- whatap/util/date_util.py +55 -0
- whatap/util/debug_util.py +73 -0
- whatap/util/escape_literal_sql.py +233 -0
- whatap/util/frame_util.py +20 -0
- whatap/util/hash_util.py +103 -0
- whatap/util/hexa32.py +66 -0
- whatap/util/int_set.py +199 -0
- whatap/util/ip_util.py +63 -0
- whatap/util/keygen.py +11 -0
- whatap/util/linked_list.py +113 -0
- whatap/util/linked_map.py +359 -0
- whatap/util/metering_util.py +103 -0
- whatap/util/request_double_queue.py +68 -0
- whatap/util/request_queue.py +60 -0
- whatap/util/string_util.py +20 -0
- whatap/util/throttle_util.py +99 -0
- whatap/util/userid_util.py +134 -0
- whatap/value/__init__.py +1 -0
- whatap/value/blob_value.py +38 -0
- whatap/value/boolean_value.py +33 -0
- whatap/value/decimal_value.py +36 -0
- whatap/value/double_summary.py +86 -0
- whatap/value/double_value.py +33 -0
- whatap/value/float_array.py +42 -0
- whatap/value/float_value.py +34 -0
- whatap/value/int_array.py +42 -0
- whatap/value/ip4_value.py +50 -0
- whatap/value/list_value.py +105 -0
- whatap/value/long_array.py +44 -0
- whatap/value/long_summary.py +83 -0
- whatap/value/map_value.py +154 -0
- whatap/value/null_value.py +21 -0
- whatap/value/number_value.py +33 -0
- whatap/value/summary_value.py +39 -0
- whatap/value/text_array.py +58 -0
- whatap/value/text_hash_value.py +37 -0
- whatap/value/text_value.py +43 -0
- whatap/value/value.py +26 -0
- whatap/value/value_enum.py +80 -0
- whatap/whatap.conf +14 -0
- whatap_python-2.1.0.dist-info/METADATA +87 -0
- whatap_python-2.1.0.dist-info/RECORD +227 -0
- whatap_python-2.1.0.dist-info/WHEEL +5 -0
- whatap_python-2.1.0.dist-info/entry_points.txt +6 -0
- whatap_python-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from whatap import DateUtil
|
|
5
|
+
from whatap import logging
|
|
6
|
+
from whatap.pack import logSinkPack
|
|
7
|
+
from whatap.conf.configure import Configure as conf
|
|
8
|
+
from whatap.trace.trace_context_manager import TraceContextManager
|
|
9
|
+
|
|
10
|
+
import whatap.io as whatapio
|
|
11
|
+
import whatap.net.async_sender as async_sender
|
|
12
|
+
|
|
13
|
+
from whatap.llm.log_sink_packs.llm_log_sink_pack import LlmLogSinkPack
|
|
14
|
+
from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
|
|
15
|
+
from whatap.llm.log_sink_packs.llm_system_message import LlmSystemMessage
|
|
16
|
+
from whatap.llm.log_sink_packs.llm_input_message import LlmInputMessage
|
|
17
|
+
from whatap.llm.log_sink_packs.llm_output_message import LlmOutputMessage
|
|
18
|
+
from whatap.llm.log_sink_packs.llm_tool_calls import LlmToolCalls
|
|
19
|
+
from whatap.llm.log_sink_packs.llm_tool_results import LlmToolResults
|
|
20
|
+
from whatap.llm.log_sink_packs.llm_tx_status import LlmTxStatus
|
|
21
|
+
|
|
22
|
+
_MAX_CONTENT_BYTES = 20000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LlmLogSinkTask(object):
|
|
26
|
+
"""LLM logsink pack 송출 파이프라인.
|
|
27
|
+
|
|
28
|
+
LLM 호출 결과 pack 을 받아 즉시 송출. 평가 (LLM-judge 등) 는 별도 파이프라인
|
|
29
|
+
(``LlmEvaluatorTask``) 에서 비동기로 처리되며, 평가 결과는 ``LlmStepEvalStatus``
|
|
30
|
+
pack (``llm_log_type=llm_step_eval_status``) 으로 송출된다 — 원본 LlmStepStatus 와
|
|
31
|
+
동일한 fields/tags + 평가 점수 5 필드. (txid, step_id) 결합 키로 백엔드 사후 결합.
|
|
32
|
+
|
|
33
|
+
이 단순한 모델 덕분에 hold/timeout/callback 등의 복잡도가 모두 제거됨.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_instance = None
|
|
37
|
+
_lock = threading.Lock()
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self._q = queue.Queue(4096)
|
|
41
|
+
self._started = False
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def get_instance(cls):
|
|
45
|
+
if cls._instance is None:
|
|
46
|
+
with cls._lock:
|
|
47
|
+
if cls._instance is None:
|
|
48
|
+
cls._instance = cls()
|
|
49
|
+
return cls._instance
|
|
50
|
+
|
|
51
|
+
def dispatch(self, pack):
|
|
52
|
+
"""LLM 호출 결과 pack 을 즉시 송출 + 평가 큐 트리거.
|
|
53
|
+
|
|
54
|
+
평가 워커 안에서 호출된 judge LLM call 인 경우 (intercept 가 fire 한 케이스):
|
|
55
|
+
- operation_type='whatap_evaluation' / prompt_version='v1' 고정
|
|
56
|
+
- parent (user) step 의 (txid, step_id, index) 그대로 propagate
|
|
57
|
+
→ dashboard 에서 같은 transaction 으로 묶여 보임
|
|
58
|
+
- eval enqueue 는 재귀 가드가 자동 차단
|
|
59
|
+
"""
|
|
60
|
+
if not isinstance(pack, LlmStepStatus):
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
ctx = getattr(pack, '_trace_ctx', None) or TraceContextManager.getLocalContext()
|
|
64
|
+
pack.set_context(ctx)
|
|
65
|
+
|
|
66
|
+
if not pack.operation_type and ctx:
|
|
67
|
+
pack.operation_type = getattr(ctx, '_llm_operation_type', '') or 'unknown'
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
pack.calculate_cost()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logging.warning('[LLM] calculate_cost failed: model=%s, input_tokens=%s, output_tokens=%s, cached_tokens=%s, error=%s'
|
|
73
|
+
% (pack.model, pack.input_tokens, pack.output_tokens, pack.cached_tokens, e),
|
|
74
|
+
extra={'id': 'LLM024'})
|
|
75
|
+
|
|
76
|
+
# ── judge LLM call (eval 워커 안에서 발생) — 고정 라벨 + parent step ids ──
|
|
77
|
+
try:
|
|
78
|
+
from whatap.counter.tasks.llm_evaluator_task import (
|
|
79
|
+
_is_in_evaluator_worker, _get_eval_worker_state,
|
|
80
|
+
)
|
|
81
|
+
if _is_in_evaluator_worker():
|
|
82
|
+
state = _get_eval_worker_state() or {}
|
|
83
|
+
pack.operation_type = 'whatap_evaluation'
|
|
84
|
+
pack.prompt_version = 'v1'
|
|
85
|
+
if state.get('parent_txid'):
|
|
86
|
+
pack.txid = state['parent_txid']
|
|
87
|
+
if state.get('parent_step_id'):
|
|
88
|
+
pack.step_id = state['parent_step_id']
|
|
89
|
+
if state.get('parent_index') is not None:
|
|
90
|
+
pack.index = state['parent_index']
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# tx_summary 누적
|
|
95
|
+
self._accumulate_tx_summary(ctx, pack)
|
|
96
|
+
|
|
97
|
+
if ctx and not pack.success and pack.error_type:
|
|
98
|
+
ctx._llm_last_error_type = pack.error_type
|
|
99
|
+
ctx._llm_last_error_model = pack.model or 'unknown'
|
|
100
|
+
ctx._llm_last_error_provider = pack.provider or ''
|
|
101
|
+
ctx._llm_last_error_op_type = pack.operation_type or 'unknown'
|
|
102
|
+
ctx._llm_last_error_url = pack.url or ''
|
|
103
|
+
ctx._llm_last_error_prompt_version = (
|
|
104
|
+
getattr(pack, 'prompt_version', 'v1') or 'v1')
|
|
105
|
+
|
|
106
|
+
# pack 즉시 송출 — eval 결과 기다리지 않음
|
|
107
|
+
self._enqueue_for_send(pack)
|
|
108
|
+
|
|
109
|
+
# 평가 큐 트리거 — worker 가 pack 을 복제해 LlmStepEvalStatus 로 만들어 점수 채워 송출
|
|
110
|
+
# judge 호출 인 경우 enqueue_evaluation 의 재귀 가드가 차단
|
|
111
|
+
try:
|
|
112
|
+
from whatap.counter.tasks.llm_evaluator_task import enqueue_evaluation
|
|
113
|
+
enqueue_evaluation(pack)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logging.warning('[LLM] eval enqueue failed: %s' % e, extra={'id': 'LLM034'})
|
|
116
|
+
|
|
117
|
+
def _enqueue_for_send(self, pack):
|
|
118
|
+
"""완성된 pack 을 송출 큐에 적재. queue full 이면 drop + LLM025."""
|
|
119
|
+
self._ensure_started()
|
|
120
|
+
if self._q.full():
|
|
121
|
+
logging.warning('[LLM] send queue full, pack dropped: model=%s' % pack.model,
|
|
122
|
+
extra={'id': 'LLM025'})
|
|
123
|
+
return
|
|
124
|
+
self._q.put(pack)
|
|
125
|
+
|
|
126
|
+
def send_tx_status(self, ctx):
|
|
127
|
+
if not ctx:
|
|
128
|
+
return
|
|
129
|
+
if not getattr(conf, 'llm_enabled', False):
|
|
130
|
+
return
|
|
131
|
+
tx = getattr(ctx, '_llm_tx_status', None)
|
|
132
|
+
if not tx or tx.call_count == 0:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
self._send_log_sink(tx)
|
|
137
|
+
# 트랜잭션 경계에서 LLM 버퍼에 남아있는 데이터 flush (큐를 거쳐 순서 보장)
|
|
138
|
+
async_sender.flush_llm_relaypack()
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logging.warning('[LLM] send_llm_tx_status failed: %s' % e, extra={'id': 'LLM022'})
|
|
141
|
+
|
|
142
|
+
# ── internal ──
|
|
143
|
+
|
|
144
|
+
def _ensure_started(self):
|
|
145
|
+
if self._started:
|
|
146
|
+
return
|
|
147
|
+
with self._lock:
|
|
148
|
+
if self._started:
|
|
149
|
+
return
|
|
150
|
+
self._started = True
|
|
151
|
+
t = threading.Thread(target=self._run, daemon=True)
|
|
152
|
+
t.start()
|
|
153
|
+
|
|
154
|
+
def _run(self):
|
|
155
|
+
while True:
|
|
156
|
+
pack = self._q.get()
|
|
157
|
+
if not pack:
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
self._process_pack(pack)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logging.warning('[LLM] process failed: %s' % e, extra={'id': 'LLM003'})
|
|
163
|
+
|
|
164
|
+
def _process_pack(self, pack):
|
|
165
|
+
# llm_step_status
|
|
166
|
+
self._send_log_sink(pack)
|
|
167
|
+
|
|
168
|
+
# system_message
|
|
169
|
+
for text in getattr(pack, 'system_texts', None) or []:
|
|
170
|
+
self._send_log_sink(LlmSystemMessage.from_pack(pack, system_text=text))
|
|
171
|
+
|
|
172
|
+
# input_message
|
|
173
|
+
prompt_text = getattr(pack, 'prompt_text', '') or ''
|
|
174
|
+
self._send_log_sink(LlmInputMessage.from_pack(pack, prompt_text=prompt_text))
|
|
175
|
+
|
|
176
|
+
# output_message
|
|
177
|
+
completion_text = getattr(pack, 'completion_text', '') or ''
|
|
178
|
+
reasoning_text = getattr(pack, 'reasoning_text', '') or ''
|
|
179
|
+
self._send_log_sink(LlmOutputMessage.from_pack(pack,
|
|
180
|
+
completion_text=completion_text, reasoning_text=reasoning_text))
|
|
181
|
+
|
|
182
|
+
# tool
|
|
183
|
+
tool_calls_text = getattr(pack, 'tool_calls_text', '') or ''
|
|
184
|
+
if tool_calls_text:
|
|
185
|
+
self._send_log_sink(LlmToolCalls.from_pack(pack, tool_calls_text=tool_calls_text))
|
|
186
|
+
|
|
187
|
+
# tool_result
|
|
188
|
+
tool_results_text = getattr(pack, 'tool_results_text', '') or ''
|
|
189
|
+
if tool_results_text:
|
|
190
|
+
self._send_log_sink(LlmToolResults.from_pack(pack, tool_results_text=tool_results_text))
|
|
191
|
+
|
|
192
|
+
def _accumulate_tx_summary(self, ctx, pack):
|
|
193
|
+
if not ctx:
|
|
194
|
+
return
|
|
195
|
+
tx = getattr(ctx, '_llm_tx_status', None)
|
|
196
|
+
if tx is None:
|
|
197
|
+
tx = LlmTxStatus()
|
|
198
|
+
tx.txid = str(ctx.id)
|
|
199
|
+
ctx._llm_tx_status = tx
|
|
200
|
+
tx.accumulate(pack)
|
|
201
|
+
|
|
202
|
+
def _send_log_sink(self, data):
|
|
203
|
+
tags = data.tags()
|
|
204
|
+
fields = data.fields()
|
|
205
|
+
content = data.content()
|
|
206
|
+
if content:
|
|
207
|
+
self._send_chunked(tags, fields, content, data.index)
|
|
208
|
+
else:
|
|
209
|
+
self._send_pack(tags, fields, '', data.index)
|
|
210
|
+
|
|
211
|
+
def _send_pack(self, tags, fields, content, index):
|
|
212
|
+
try:
|
|
213
|
+
tags = {k: v for k, v in tags.items() if v is not None}
|
|
214
|
+
fields = {k: v for k, v in fields.items() if v is not None}
|
|
215
|
+
fields['index'] = index
|
|
216
|
+
if content:
|
|
217
|
+
encoded = content.encode('utf-8', errors='replace')
|
|
218
|
+
if len(encoded) > _MAX_CONTENT_BYTES:
|
|
219
|
+
content = encoded[:_MAX_CONTENT_BYTES].decode('utf-8', errors='replace')
|
|
220
|
+
p = logSinkPack.getLogSinkPack(
|
|
221
|
+
t=DateUtil.now(),
|
|
222
|
+
category=LlmLogSinkPack.CATEGORY,
|
|
223
|
+
tags=tags,
|
|
224
|
+
fields={},
|
|
225
|
+
line=DateUtil.now(),
|
|
226
|
+
content=content
|
|
227
|
+
)
|
|
228
|
+
for k, v in fields.items():
|
|
229
|
+
if isinstance(v, (int, float)):
|
|
230
|
+
p.fields.putAuto(k, v)
|
|
231
|
+
else:
|
|
232
|
+
p.fields.putString(k, str(v))
|
|
233
|
+
|
|
234
|
+
p.pcode = conf.PCODE
|
|
235
|
+
bout = whatapio.DataOutputX()
|
|
236
|
+
bout.writePack(p, None)
|
|
237
|
+
packbytes = bout.toByteArray()
|
|
238
|
+
async_sender.send_llm_relaypack(packbytes)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logging.warning('[LLM] send_pack failed: %s' % e, extra={'id': 'LLM001'})
|
|
241
|
+
|
|
242
|
+
def _send_chunked(self, tags, fields, content, index):
|
|
243
|
+
try:
|
|
244
|
+
if not content:
|
|
245
|
+
self._send_pack(tags, fields, '', index)
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
tags = {k: v for k, v in tags.items() if v is not None}
|
|
249
|
+
fields = {k: v for k, v in fields.items() if v is not None}
|
|
250
|
+
|
|
251
|
+
content_bytes = content.encode('utf-8', errors='replace')
|
|
252
|
+
total_bytes = len(content_bytes)
|
|
253
|
+
|
|
254
|
+
if total_bytes <= _MAX_CONTENT_BYTES:
|
|
255
|
+
chunk_fields = dict(fields)
|
|
256
|
+
chunk_fields["chunk_index"] = 0
|
|
257
|
+
chunk_fields["chunk_total"] = 1
|
|
258
|
+
self._send_pack(tags, chunk_fields, content, index)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
raw_chunks = []
|
|
262
|
+
i = 0
|
|
263
|
+
while i < total_bytes:
|
|
264
|
+
end = min(i + _MAX_CONTENT_BYTES, total_bytes)
|
|
265
|
+
if end < total_bytes:
|
|
266
|
+
while end > i and (content_bytes[end] & 0xC0) == 0x80:
|
|
267
|
+
end -= 1
|
|
268
|
+
raw_chunks.append(content_bytes[i:end].decode('utf-8', errors='replace'))
|
|
269
|
+
i = end
|
|
270
|
+
|
|
271
|
+
chunk_total = len(raw_chunks)
|
|
272
|
+
for idx, chunk in enumerate(raw_chunks):
|
|
273
|
+
chunk_fields = dict(fields)
|
|
274
|
+
chunk_fields["chunk_index"] = idx
|
|
275
|
+
chunk_fields["chunk_total"] = chunk_total
|
|
276
|
+
self._send_pack(tags, chunk_fields, chunk, index)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logging.warning('[LLM] send_chunked failed: %s' % e, extra={'id': 'LLM002'})
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ── module-level API ──
|
|
282
|
+
|
|
283
|
+
def dispatch_llm_pack(pack):
|
|
284
|
+
LlmLogSinkTask.get_instance().dispatch(pack)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def send_llm_tx_status(ctx):
|
|
288
|
+
LlmLogSinkTask.get_instance().send_tx_status(ctx)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def send_llm_pack(metadata):
|
|
292
|
+
pack = LlmStepStatus()
|
|
293
|
+
for key, val in metadata.items():
|
|
294
|
+
if hasattr(pack, key):
|
|
295
|
+
setattr(pack, key, val)
|
|
296
|
+
dispatch_llm_pack(pack)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def dispatch_llm_evaluation_pack(pack):
|
|
300
|
+
"""LlmStepEvalStatus pack 을 LogSink 인프라로 송출.
|
|
301
|
+
|
|
302
|
+
LlmEvaluatorTask._run_one 이 원본 LlmStepStatus 를 복제해 만든 LlmStepEvalStatus 를
|
|
303
|
+
이 함수로 송출. (txid, step_id) 결합 키로 백엔드에서 LlmStepStatus 와 사후 결합.
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
LlmLogSinkTask.get_instance()._send_log_sink(pack)
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logging.warning('[LLM] dispatch_llm_evaluation_pack failed: %s' % e,
|
|
309
|
+
extra={'id': 'LLM037'})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
from threading import Thread
|
|
4
|
+
|
|
5
|
+
from whatap.llm.stats import LLM_STAT_CLASSES
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LlmStatTask(Thread):
|
|
9
|
+
_instance = None
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super(LlmStatTask, self).__init__()
|
|
13
|
+
self.stats = list()
|
|
14
|
+
self._stat_map = {}
|
|
15
|
+
self.last_executed = {}
|
|
16
|
+
LlmStatTask._instance = self
|
|
17
|
+
|
|
18
|
+
def _register_stats(self):
|
|
19
|
+
for cls in LLM_STAT_CLASSES:
|
|
20
|
+
stat = cls()
|
|
21
|
+
self.stats.append(stat)
|
|
22
|
+
self._stat_map[stat.name()] = stat
|
|
23
|
+
self.last_executed[stat.name()] = 0
|
|
24
|
+
|
|
25
|
+
def run(self):
|
|
26
|
+
self._register_stats()
|
|
27
|
+
|
|
28
|
+
while True:
|
|
29
|
+
current_time = time.time()
|
|
30
|
+
time.sleep(1)
|
|
31
|
+
for stat in self.stats:
|
|
32
|
+
last_executed_time = self.last_executed[stat.name()]
|
|
33
|
+
interval = stat.interval()
|
|
34
|
+
|
|
35
|
+
if current_time - last_executed_time >= interval:
|
|
36
|
+
try:
|
|
37
|
+
self.last_executed[stat.name()] = current_time
|
|
38
|
+
stat.process()
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logging.debug(e, extra={'id': 'WA182'}, exc_info=True)
|
|
41
|
+
|
|
42
|
+
def notify(self, pack):
|
|
43
|
+
from whatap.conf.configure import Configure as conf
|
|
44
|
+
if not getattr(conf, 'llm_enabled', False):
|
|
45
|
+
return
|
|
46
|
+
for stat in self.stats:
|
|
47
|
+
try:
|
|
48
|
+
stat.update_from_pack(pack)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logging.debug(e, extra={'id': 'WA183'}, exc_info=True)
|
|
51
|
+
|
|
52
|
+
if pack.success:
|
|
53
|
+
from whatap.llm.stats.meter import Meter
|
|
54
|
+
Meter.increment()
|
|
55
|
+
|
|
56
|
+
def flush_last_error(self, ctx):
|
|
57
|
+
last_error_type = getattr(ctx, '_llm_last_error_type', None)
|
|
58
|
+
if not last_error_type:
|
|
59
|
+
return
|
|
60
|
+
from whatap.conf.configure import Configure as conf
|
|
61
|
+
if not getattr(conf, 'llm_enabled', False):
|
|
62
|
+
return
|
|
63
|
+
stat = self._stat_map.get('ErrorStat')
|
|
64
|
+
if stat:
|
|
65
|
+
stat.update_last_error(
|
|
66
|
+
getattr(ctx, '_llm_last_error_model', 'unknown'),
|
|
67
|
+
getattr(ctx, '_llm_last_error_provider', ''),
|
|
68
|
+
getattr(ctx, '_llm_last_error_op_type', 'unknown'),
|
|
69
|
+
url=getattr(ctx, '_llm_last_error_url', ''),
|
|
70
|
+
error_type=last_error_type,
|
|
71
|
+
prompt_version=getattr(ctx, '_llm_last_error_prompt_version', 'v1'),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def get_stat(cls, name):
|
|
76
|
+
if cls._instance is None:
|
|
77
|
+
return None
|
|
78
|
+
return cls._instance._stat_map.get(name)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from whatap.counter.tasks.base_task import BaseTask
|
|
2
|
+
from whatap.pack import tagCountPack
|
|
3
|
+
from whatap import DateUtil
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import resource
|
|
8
|
+
HAS_RESOURCE = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
HAS_RESOURCE = False
|
|
11
|
+
|
|
12
|
+
currentpid = os.getpid()
|
|
13
|
+
|
|
14
|
+
if HAS_RESOURCE:
|
|
15
|
+
soft_limit, _= resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
16
|
+
else:
|
|
17
|
+
soft_limit = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OpenFileDescriptorTask(BaseTask):
|
|
21
|
+
|
|
22
|
+
def interval(self):
|
|
23
|
+
from whatap.conf.configure import Configure as conf
|
|
24
|
+
return conf.open_file_descriptor_interval
|
|
25
|
+
|
|
26
|
+
def process(self):
|
|
27
|
+
from whatap.conf.configure import Configure as conf
|
|
28
|
+
if not conf.open_file_descriptor_enabled:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
currentnofile = self.currentNofile()
|
|
32
|
+
if soft_limit:
|
|
33
|
+
current_nofile_pct = float(currentnofile) / float(soft_limit) * float(100)
|
|
34
|
+
else:
|
|
35
|
+
current_nofile_pct = "N/A"
|
|
36
|
+
category = "app_filedescriptor"
|
|
37
|
+
tags = dict(pid=currentpid)
|
|
38
|
+
fields = dict(max_nofile = soft_limit,
|
|
39
|
+
currnet_nofile=currentnofile,
|
|
40
|
+
current_nofile_pct=current_nofile_pct )
|
|
41
|
+
|
|
42
|
+
p = tagCountPack.getTagCountPack(
|
|
43
|
+
t=DateUtil.now(),
|
|
44
|
+
category=f"{category}",
|
|
45
|
+
tags=tags,
|
|
46
|
+
fields=fields
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.send_pack(p)
|
|
50
|
+
|
|
51
|
+
def currentNofile(self):
|
|
52
|
+
if not HAS_RESOURCE:
|
|
53
|
+
try:
|
|
54
|
+
import psutil
|
|
55
|
+
process = psutil.Process(currentpid)
|
|
56
|
+
return len(process.open_files())
|
|
57
|
+
except ImportError:
|
|
58
|
+
return "N/A"
|
|
59
|
+
except Exception:
|
|
60
|
+
return "N/A"
|
|
61
|
+
|
|
62
|
+
fd_directory = f'/proc/{currentpid}/fd'
|
|
63
|
+
try:
|
|
64
|
+
fd_count = len(os.listdir(fd_directory))
|
|
65
|
+
return fd_count
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
return "N/A"
|
whatap/io/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from whatap.io.data_outputx import DataOutputX
|
whatap/io/data_inputx.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import struct
|
|
3
|
+
from whatap.pack.pack_enum import PackEnum
|
|
4
|
+
|
|
5
|
+
from whatap.value.value_enum import ValueEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataInputX(object):
|
|
9
|
+
def __init__(self, v):
|
|
10
|
+
self.buffer = io.BytesIO(v)
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def toInt(buf, pos):
|
|
14
|
+
ch1 = buf[pos] & 0xff
|
|
15
|
+
ch2 = buf[pos + 1] & 0xff
|
|
16
|
+
ch3 = buf[pos + 2] & 0xff
|
|
17
|
+
ch4 = buf[pos + 3] & 0xff
|
|
18
|
+
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0))
|
|
19
|
+
|
|
20
|
+
def readPack(self):
|
|
21
|
+
type = self.readShort()
|
|
22
|
+
return PackEnum.create(type).read(self)
|
|
23
|
+
|
|
24
|
+
def readValue(self):
|
|
25
|
+
type = self.readByte()
|
|
26
|
+
return ValueEnum.create(type).read(self)
|
|
27
|
+
|
|
28
|
+
# TODO
|
|
29
|
+
# def available(self):
|
|
30
|
+
# return len(self.buffer.getvalue())- self.offset > 0
|
|
31
|
+
|
|
32
|
+
# TODO
|
|
33
|
+
# def readStep(self):
|
|
34
|
+
# raise
|
|
35
|
+
# # byte type = this.inner.readByte();
|
|
36
|
+
# # return StepEnum.create(type).read(this);
|
|
37
|
+
# return None
|
|
38
|
+
|
|
39
|
+
def readIntBytes(self, max):
|
|
40
|
+
ln = self.readInt()
|
|
41
|
+
if ln < 0 or ln > max:
|
|
42
|
+
raise Exception('error')
|
|
43
|
+
return self.buffer.read(ln)
|
|
44
|
+
|
|
45
|
+
def readBoolean(self):
|
|
46
|
+
v = self.buffer.read(1)
|
|
47
|
+
return struct.unpack('>?', v)[0]
|
|
48
|
+
|
|
49
|
+
def readByte(self):
|
|
50
|
+
v = self.buffer.read(1)
|
|
51
|
+
return struct.unpack('>b', v)[0]
|
|
52
|
+
|
|
53
|
+
def readShort(self):
|
|
54
|
+
v = self.buffer.read(2)
|
|
55
|
+
return struct.unpack('>h', v)[0]
|
|
56
|
+
|
|
57
|
+
def readInt3(self):
|
|
58
|
+
s = self.buffer.read(3)
|
|
59
|
+
chrs = struct.unpack('>bBB', s)
|
|
60
|
+
ch1 = (chrs[0])
|
|
61
|
+
ch2 = (chrs[1])
|
|
62
|
+
ch3 = (chrs[2])
|
|
63
|
+
return (ch1 << 16) + (ch2 << 8) + ch3
|
|
64
|
+
|
|
65
|
+
def readInt(self):
|
|
66
|
+
v = self.buffer.read(4)
|
|
67
|
+
|
|
68
|
+
return struct.unpack('>i', v)[0]
|
|
69
|
+
|
|
70
|
+
def readLong5(self):
|
|
71
|
+
s = self.buffer.read(5)
|
|
72
|
+
chrs = struct.unpack('>bBBBB', s)
|
|
73
|
+
ch1 = (chrs[0])
|
|
74
|
+
ch2 = (chrs[1])
|
|
75
|
+
ch3 = (chrs[2])
|
|
76
|
+
ch4 = (chrs[3])
|
|
77
|
+
ch5 = (chrs[4])
|
|
78
|
+
return ((ch1 << 32) + (ch2 << 24) + (ch3 << 16) + (ch4 << 8) + (ch5))
|
|
79
|
+
|
|
80
|
+
def readLong(self):
|
|
81
|
+
v = self.buffer.read(8)
|
|
82
|
+
return struct.unpack('>q', v)[0]
|
|
83
|
+
|
|
84
|
+
def readFloat(self):
|
|
85
|
+
v = self.buffer.read(4)
|
|
86
|
+
return struct.unpack('>f', v)[0]
|
|
87
|
+
|
|
88
|
+
def readDouble(self):
|
|
89
|
+
v = self.buffer.read(8)
|
|
90
|
+
return struct.unpack('>d', v)[0]
|
|
91
|
+
|
|
92
|
+
def readDecimal(self):
|
|
93
|
+
ln = self.readByte()
|
|
94
|
+
if ln == 0:
|
|
95
|
+
return 0
|
|
96
|
+
elif ln == 1:
|
|
97
|
+
return self.readByte()
|
|
98
|
+
elif ln == 2:
|
|
99
|
+
return self.readShort()
|
|
100
|
+
elif ln == 3:
|
|
101
|
+
return self.readInt3()
|
|
102
|
+
elif ln == 4:
|
|
103
|
+
return self.readInt()
|
|
104
|
+
elif ln == 5:
|
|
105
|
+
return self.readLong5()
|
|
106
|
+
else:
|
|
107
|
+
return self.readLong()
|
|
108
|
+
|
|
109
|
+
def read(self, ln):
|
|
110
|
+
return self.buffer.read(ln)
|
|
111
|
+
|
|
112
|
+
def readBlob(self):
|
|
113
|
+
baselen = self.readByte() & 0xff
|
|
114
|
+
if baselen == 255:
|
|
115
|
+
ln = self.readShort() & 0xffff
|
|
116
|
+
return self.buffer.read(ln)
|
|
117
|
+
elif baselen == 254:
|
|
118
|
+
ln = self.readInt()
|
|
119
|
+
return self.buffer.read(ln)
|
|
120
|
+
elif baselen == 0:
|
|
121
|
+
return []
|
|
122
|
+
else:
|
|
123
|
+
return self.buffer.read(baselen)
|
|
124
|
+
|
|
125
|
+
def readText(self):
|
|
126
|
+
arr = self.readBlob()
|
|
127
|
+
if not len(arr):
|
|
128
|
+
return ''
|
|
129
|
+
else:
|
|
130
|
+
return arr.decode("utf-8")
|
|
131
|
+
|
|
132
|
+
def toByteArray(self):
|
|
133
|
+
return self.buffer.getvalue()
|
|
134
|
+
|
|
135
|
+
def readFloatArray(self):
|
|
136
|
+
ln = self.readShort()
|
|
137
|
+
data = []
|
|
138
|
+
for _ in range(ln):
|
|
139
|
+
data.append(self.readFloat())
|
|
140
|
+
return data
|
|
141
|
+
|
|
142
|
+
def readIntArray(self):
|
|
143
|
+
ln = self.readShort()
|
|
144
|
+
data = []
|
|
145
|
+
for _ in range(ln):
|
|
146
|
+
data.append(self.readInt())
|
|
147
|
+
return data
|
|
148
|
+
|
|
149
|
+
def readLongArray(self):
|
|
150
|
+
ln = self.readShort()
|
|
151
|
+
data = []
|
|
152
|
+
for _ in range(ln):
|
|
153
|
+
data.append(self.readLong())
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
def readDecimalArray(self):
|
|
157
|
+
ln = self.readShort()
|
|
158
|
+
data = []
|
|
159
|
+
for _ in range(ln):
|
|
160
|
+
data.append(self.readDecimal())
|
|
161
|
+
return data
|