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,76 @@
|
|
|
1
|
+
"""Anthropic Messages API 요청에서 컨텍스트를 구성하는 모듈."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from whatap.llm.features import LlmFeature
|
|
5
|
+
from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
|
|
6
|
+
from whatap.trace.trace_context_manager import TraceContextManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _classify_input_features(kwargs):
|
|
10
|
+
"""요청 kwargs에서 vision, document, reasoning 등 입력 피처를 분류한다."""
|
|
11
|
+
features = []
|
|
12
|
+
has_vision, has_document = False, False
|
|
13
|
+
for msg in kwargs.get("messages", []):
|
|
14
|
+
content = msg.get("content")
|
|
15
|
+
if isinstance(content, list):
|
|
16
|
+
for block in content:
|
|
17
|
+
if isinstance(block, dict):
|
|
18
|
+
bt = block.get("type", "")
|
|
19
|
+
if bt == "image":
|
|
20
|
+
has_vision = True
|
|
21
|
+
elif bt == "document":
|
|
22
|
+
has_document = True
|
|
23
|
+
if has_vision and has_document:
|
|
24
|
+
break
|
|
25
|
+
if has_vision:
|
|
26
|
+
features.append(LlmFeature.VISION)
|
|
27
|
+
if has_document:
|
|
28
|
+
features.append(LlmFeature.DOCUMENT)
|
|
29
|
+
thinking = kwargs.get("thinking")
|
|
30
|
+
if thinking and thinking.get("type") == "enabled":
|
|
31
|
+
features.append(LlmFeature.REASONING)
|
|
32
|
+
return features
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _extract_input_text(messages, system=None):
|
|
36
|
+
"""메시지 목록과 시스템 프롬프트에서 텍스트를 추출한다."""
|
|
37
|
+
if isinstance(system, list):
|
|
38
|
+
parts = []
|
|
39
|
+
for block in system:
|
|
40
|
+
if isinstance(block, dict):
|
|
41
|
+
parts.append(block.get("text", ""))
|
|
42
|
+
elif hasattr(block, 'text'):
|
|
43
|
+
parts.append(block.text)
|
|
44
|
+
else:
|
|
45
|
+
parts.append(str(block))
|
|
46
|
+
system_text = "\n".join(p for p in parts if p)
|
|
47
|
+
else:
|
|
48
|
+
system_text = system or ""
|
|
49
|
+
prompt_text = json.dumps(messages, ensure_ascii=False) if messages else ""
|
|
50
|
+
return system_text, prompt_text
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_context(kwargs):
|
|
54
|
+
"""Messages kwargs로부터 LlmStepStatus 팩과 트레이스 컨텍스트를 구성한다."""
|
|
55
|
+
model = kwargs.get("model")
|
|
56
|
+
stream = kwargs.get("stream", False)
|
|
57
|
+
features = _classify_input_features(kwargs)
|
|
58
|
+
|
|
59
|
+
system_text, prompt_text = _extract_input_text(
|
|
60
|
+
kwargs.get("messages", []), system=kwargs.get("system"))
|
|
61
|
+
|
|
62
|
+
pack = LlmStepStatus()
|
|
63
|
+
pack.model = model
|
|
64
|
+
pack.prompt_text = prompt_text
|
|
65
|
+
pack.system_texts = [system_text] if system_text else []
|
|
66
|
+
pack.stream = stream
|
|
67
|
+
pack.features = ",".join(features)
|
|
68
|
+
if kwargs.get("temperature") is not None:
|
|
69
|
+
pack.temperature = kwargs["temperature"]
|
|
70
|
+
|
|
71
|
+
ctx = TraceContextManager.getLocalContext()
|
|
72
|
+
if ctx:
|
|
73
|
+
ctx._llm_httpc_pending = True
|
|
74
|
+
ctx._llm_model = model
|
|
75
|
+
|
|
76
|
+
return pack, ctx, features, stream
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Anthropic Messages 응답에서 토큰 사용량 및 완성 텍스트를 추출하는 모듈."""
|
|
2
|
+
from whatap import logging
|
|
3
|
+
from whatap.llm.features import LlmFeature
|
|
4
|
+
from whatap.llm.providers.stream_accumulator import StreamAccumulator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _extract_usage(response):
|
|
8
|
+
"""응답 객체에서 캐시 토큰 포함 사용량 정보를 딕셔너리로 추출한다."""
|
|
9
|
+
usage = getattr(response, "usage", None)
|
|
10
|
+
inp = getattr(usage, "input_tokens", 0) or 0
|
|
11
|
+
out = getattr(usage, "output_tokens", 0) or 0
|
|
12
|
+
cc = getattr(usage, "cache_creation_input_tokens", 0) or 0
|
|
13
|
+
cr = getattr(usage, "cache_read_input_tokens", 0) or 0
|
|
14
|
+
return {
|
|
15
|
+
"input_tokens": inp, "output_tokens": out,
|
|
16
|
+
"total_tokens_count": inp + out + cc + cr,
|
|
17
|
+
"cache_creation_input_tokens": cc, "cache_read_input_tokens": cr,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _extract_response_text(response):
|
|
22
|
+
"""응답 content 블록에서 텍스트와 thinking 내용을 분리 추출한다."""
|
|
23
|
+
try:
|
|
24
|
+
parts, thinking = [], []
|
|
25
|
+
for block in response.content:
|
|
26
|
+
if hasattr(block, 'type') and block.type == 'thinking':
|
|
27
|
+
thinking.append(getattr(block, 'thinking', ''))
|
|
28
|
+
elif hasattr(block, 'text'):
|
|
29
|
+
parts.append(block.text)
|
|
30
|
+
return "".join(parts), "".join(thinking)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logging.warning('[LLM] anthropic response text extraction failed: %s' % e, extra={'id': 'LLM030'})
|
|
33
|
+
return "", ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _classify_response_features(response_content, features):
|
|
37
|
+
"""응답 content에서 tool_use, computer_use 등 출력 피처를 분류한다."""
|
|
38
|
+
for block in response_content:
|
|
39
|
+
bt = getattr(block, 'type', '')
|
|
40
|
+
if bt == 'tool_use':
|
|
41
|
+
name = getattr(block, 'name', '')
|
|
42
|
+
tag = LlmFeature.COMPUTER_USE if 'computer' in name else LlmFeature.TOOL_USE
|
|
43
|
+
if tag not in features:
|
|
44
|
+
features.append(tag)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def finalize(response, pack, features):
|
|
48
|
+
"""비스트리밍 Messages 응답에서 완성 텍스트, 토큰, 피처 정보를 팩에 기록한다."""
|
|
49
|
+
_classify_response_features(response.content, features)
|
|
50
|
+
pack.features = ",".join(features)
|
|
51
|
+
text, reasoning = _extract_response_text(response)
|
|
52
|
+
pack.set_tokens(_extract_usage(response))
|
|
53
|
+
pack.completion_text = text
|
|
54
|
+
pack.finish_reason = getattr(response, 'stop_reason', None)
|
|
55
|
+
if reasoning:
|
|
56
|
+
pack.reasoning_text = reasoning
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AnthropicStream(StreamAccumulator):
|
|
60
|
+
"""Anthropic Messages 스트리밍 이벤트를 누적하는 어큐뮬레이터."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, pack, active_key, features):
|
|
63
|
+
super().__init__(pack, active_key)
|
|
64
|
+
self.features = features
|
|
65
|
+
self.text = ""
|
|
66
|
+
self.reasoning = ""
|
|
67
|
+
self.block_type = None
|
|
68
|
+
self.block_name = None
|
|
69
|
+
self.input_tokens = 0
|
|
70
|
+
self.output_tokens = 0
|
|
71
|
+
self.cache_creation = 0
|
|
72
|
+
self.cache_read = 0
|
|
73
|
+
|
|
74
|
+
def on_chunk(self, event):
|
|
75
|
+
t = getattr(event, 'type', '')
|
|
76
|
+
if t == 'message_start':
|
|
77
|
+
usage = getattr(getattr(event, 'message', None), 'usage', None)
|
|
78
|
+
if usage:
|
|
79
|
+
self.input_tokens = getattr(usage, 'input_tokens', 0) or 0
|
|
80
|
+
self.cache_creation = getattr(usage, 'cache_creation_input_tokens', 0) or 0
|
|
81
|
+
self.cache_read = getattr(usage, 'cache_read_input_tokens', 0) or 0
|
|
82
|
+
elif t == 'content_block_start':
|
|
83
|
+
block = getattr(event, 'content_block', None)
|
|
84
|
+
self.block_type = getattr(block, 'type', None)
|
|
85
|
+
self.block_name = getattr(block, 'name', None)
|
|
86
|
+
if self.block_type == 'tool_use':
|
|
87
|
+
tag = (LlmFeature.COMPUTER_USE
|
|
88
|
+
if self.block_name and 'computer' in self.block_name
|
|
89
|
+
else LlmFeature.TOOL_USE)
|
|
90
|
+
if tag not in self.features:
|
|
91
|
+
self.features.append(tag)
|
|
92
|
+
elif t == 'content_block_delta':
|
|
93
|
+
delta = getattr(event, 'delta', None)
|
|
94
|
+
if self.block_type == 'thinking':
|
|
95
|
+
self.reasoning += getattr(delta, 'thinking', '') or ''
|
|
96
|
+
elif self.block_type == 'text':
|
|
97
|
+
text = getattr(delta, 'text', '') or ''
|
|
98
|
+
if text:
|
|
99
|
+
self.on_first_token()
|
|
100
|
+
self.text += text
|
|
101
|
+
elif t == 'content_block_stop':
|
|
102
|
+
self.block_type = None
|
|
103
|
+
self.block_name = None
|
|
104
|
+
elif t == 'message_delta':
|
|
105
|
+
delta_obj = getattr(event, 'delta', None)
|
|
106
|
+
if delta_obj:
|
|
107
|
+
stop_reason = getattr(delta_obj, 'stop_reason', None)
|
|
108
|
+
if stop_reason:
|
|
109
|
+
self.pack.finish_reason = stop_reason
|
|
110
|
+
usage = getattr(event, 'usage', None)
|
|
111
|
+
if usage:
|
|
112
|
+
self.output_tokens = getattr(usage, 'output_tokens', 0) or 0
|
|
113
|
+
|
|
114
|
+
def _apply(self):
|
|
115
|
+
pack = self.pack
|
|
116
|
+
pack.features = ",".join(self.features)
|
|
117
|
+
pack.set_tokens({
|
|
118
|
+
"input_tokens": self.input_tokens,
|
|
119
|
+
"output_tokens": self.output_tokens,
|
|
120
|
+
"total_tokens_count": self.input_tokens + self.output_tokens + self.cache_creation + self.cache_read,
|
|
121
|
+
"cache_creation_input_tokens": self.cache_creation,
|
|
122
|
+
"cache_read_input_tokens": self.cache_read,
|
|
123
|
+
})
|
|
124
|
+
pack.completion_text = self.text
|
|
125
|
+
if self.reasoning:
|
|
126
|
+
pack.reasoning_text = self.reasoning
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""LLM 인터셉트 공통 라이프사이클 함수.
|
|
2
|
+
|
|
3
|
+
API 호출 전후 처리 흐름:
|
|
4
|
+
before_call → fn() → after_call → finalize_non_streaming
|
|
5
|
+
에러 시: before_call → fn() → handle_error
|
|
6
|
+
"""
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from whatap.counter.tasks.llm_log_sink_task import dispatch_llm_pack
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def capture_client(pack, ctx, args):
|
|
13
|
+
"""인터셉트 시점에 호출된 LLM client 객체 + (async 면) event loop 를 캡처.
|
|
14
|
+
|
|
15
|
+
OpenAI/Anthropic SDK 모두 ``client.<resource>.<method>(...)`` 형태 메서드 호출이며,
|
|
16
|
+
인터셉트의 args[0] 은 그 resource 인스턴스. resource 의 ``_client`` 어트리뷰트로
|
|
17
|
+
상위 client (OpenAI/AsyncOpenAI/Anthropic/AsyncAnthropic) 에 접근 가능.
|
|
18
|
+
|
|
19
|
+
이걸 pack 에 stash 해두면 LLM judge 평가자가 사용자의 그 client 를 그대로 재사용 —
|
|
20
|
+
별도 sync client / httpx.Client 생성 없이 user 의 instance 를 그대로 호출.
|
|
21
|
+
|
|
22
|
+
async client 인 경우 ``asyncio.get_running_loop()`` 도 같이 캡처. 평가 워커 스레드는
|
|
23
|
+
sync 환경이라 그 loop 으로 ``run_coroutine_threadsafe`` dispatch 해야 user 의
|
|
24
|
+
AsyncClient (loop bind) 를 안전하게 재사용 가능.
|
|
25
|
+
|
|
26
|
+
실패해도 silently 무시 (인터셉트 자체는 절대 깨면 안 됨).
|
|
27
|
+
"""
|
|
28
|
+
# Event loop 먼저 — sync 호출이면 RuntimeError 무시
|
|
29
|
+
try:
|
|
30
|
+
import asyncio
|
|
31
|
+
pack._llm_event_loop = asyncio.get_running_loop()
|
|
32
|
+
if ctx is not None:
|
|
33
|
+
ctx._llm_event_loop = pack._llm_event_loop
|
|
34
|
+
except RuntimeError:
|
|
35
|
+
pass
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
if not args:
|
|
40
|
+
return
|
|
41
|
+
try:
|
|
42
|
+
resource = args[0]
|
|
43
|
+
llm_client = getattr(resource, '_client', None)
|
|
44
|
+
if llm_client is not None:
|
|
45
|
+
pack._llm_client = llm_client
|
|
46
|
+
if ctx is not None:
|
|
47
|
+
ctx._llm_client = llm_client
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# prompt_meta 스코프에서 (name, version) 가져와 pack 의 operation_type / prompt_version
|
|
52
|
+
# 으로 set. 데코레이터/scope 미적용 시 ('default', 'v1').
|
|
53
|
+
try:
|
|
54
|
+
from whatap.llm.prompt_meta import get_prompt_meta
|
|
55
|
+
name, version = get_prompt_meta()
|
|
56
|
+
pack.operation_type = name
|
|
57
|
+
pack.prompt_version = version
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# ★ judge LLM call (eval 워커 안에서 발생한 호출) 인 경우 — operation_type 을
|
|
62
|
+
# 'whatap_evaluation' 로 + parent step ids 를 user step 으로 override.
|
|
63
|
+
# capture_client 시점에 set 해야 (intercept_create 의 active_key 가 이 직후 계산되므로)
|
|
64
|
+
# 메트릭 (active_stat / perf_stat / token_usage_stat) 의 operation_type 라벨이
|
|
65
|
+
# 'whatap_evaluation' 로 정확하게 분리됨. dispatch 에서 override 하면 active_key 가 stale.
|
|
66
|
+
try:
|
|
67
|
+
from whatap.counter.tasks.llm_evaluator_task import (
|
|
68
|
+
_is_in_evaluator_worker, _get_eval_worker_state,
|
|
69
|
+
)
|
|
70
|
+
if _is_in_evaluator_worker():
|
|
71
|
+
state = _get_eval_worker_state() or {}
|
|
72
|
+
pack.operation_type = 'whatap_evaluation'
|
|
73
|
+
pack.prompt_version = 'v1'
|
|
74
|
+
if state.get('parent_txid'):
|
|
75
|
+
pack.txid = state['parent_txid']
|
|
76
|
+
if state.get('parent_step_id'):
|
|
77
|
+
pack.step_id = state['parent_step_id']
|
|
78
|
+
if state.get('parent_index') is not None:
|
|
79
|
+
pack.index = state['parent_index']
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _stat_task():
|
|
85
|
+
"""LlmStatTask 싱글톤 인스턴스 반환. 미초기화 시 None."""
|
|
86
|
+
from whatap.counter.tasks.llm_stat_task import LlmStatTask
|
|
87
|
+
return LlmStatTask._instance
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _active_stat():
|
|
91
|
+
"""ActiveStat 인스턴스 반환. LLM 동시 호출 수 추적용."""
|
|
92
|
+
from whatap.counter.tasks.llm_stat_task import LlmStatTask
|
|
93
|
+
return LlmStatTask.get_stat('ActiveStat')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def before_call(pack, active_key):
|
|
97
|
+
"""API 호출 전: active 카운터 증가 + 시작 시간 기록 + 순차 인덱스 할당."""
|
|
98
|
+
pack._active_ended = False
|
|
99
|
+
stat = _active_stat()
|
|
100
|
+
if stat:
|
|
101
|
+
stat.on_start(*active_key)
|
|
102
|
+
pack._start_time = time.monotonic()
|
|
103
|
+
from whatap.trace.trace_context_manager import TraceContextManager
|
|
104
|
+
ctx = TraceContextManager.getLocalContext()
|
|
105
|
+
if ctx:
|
|
106
|
+
pack._trace_ctx = ctx
|
|
107
|
+
idx = getattr(ctx, '_llm_call_index', 0)
|
|
108
|
+
pack.index = idx
|
|
109
|
+
ctx._llm_call_index = idx + 1
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _ensure_end(pack, active_key):
|
|
113
|
+
"""on_end 멱등 호출 — 이미 호출되었으면 무시."""
|
|
114
|
+
if getattr(pack, '_active_ended', False):
|
|
115
|
+
return
|
|
116
|
+
pack._active_ended = True
|
|
117
|
+
stat = _active_stat()
|
|
118
|
+
if stat:
|
|
119
|
+
stat.on_end(*active_key, host=pack.provider or '')
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def handle_error(pack, err, active_key, api_error_cls):
|
|
123
|
+
"""API 호출 실패 시: 에러 기록 (cause chain 포함) → 로그싱크 전송 → active 카운터 감소."""
|
|
124
|
+
try:
|
|
125
|
+
error_type = "api_error" if isinstance(err, api_error_cls) else "program_error"
|
|
126
|
+
# OpenAI SDK 의 APIConnectionError 등은 str(err)='Connection error.' 처럼 흐릿함.
|
|
127
|
+
# __cause__/__context__ 까지 풀어 진짜 원인 노출.
|
|
128
|
+
try:
|
|
129
|
+
chain = _format_exception_chain(err)
|
|
130
|
+
except Exception:
|
|
131
|
+
chain = '%s: %s' % (type(err).__name__, err)
|
|
132
|
+
pack.error = chain[:1500]
|
|
133
|
+
pack.error_type = error_type
|
|
134
|
+
_dispatch(pack)
|
|
135
|
+
finally:
|
|
136
|
+
_ensure_end(pack, active_key)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _format_exception_chain(e):
|
|
140
|
+
"""예외 체인 (__cause__/__context__) 끝까지 펼쳐 한 줄로 직렬화."""
|
|
141
|
+
parts = []
|
|
142
|
+
seen = set()
|
|
143
|
+
cur = e
|
|
144
|
+
while cur is not None:
|
|
145
|
+
if id(cur) in seen:
|
|
146
|
+
break
|
|
147
|
+
seen.add(id(cur))
|
|
148
|
+
msg = str(cur).strip() or '(no message)'
|
|
149
|
+
parts.append('%s: %s' % (type(cur).__name__, msg))
|
|
150
|
+
cur = cur.__cause__ or cur.__context__
|
|
151
|
+
if len(parts) > 5:
|
|
152
|
+
parts.append('...')
|
|
153
|
+
break
|
|
154
|
+
return ' | caused by '.join(parts)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def after_call(pack, ctx):
|
|
158
|
+
"""API 호출 후: trace context에서 provider/url 추출 + active host 갱신."""
|
|
159
|
+
if ctx:
|
|
160
|
+
ctx._llm_httpc_pending = False
|
|
161
|
+
pack.set_context(ctx)
|
|
162
|
+
stat = _active_stat()
|
|
163
|
+
if stat:
|
|
164
|
+
stat.set_host(pack.model, pack.provider, pack.url)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def finalize_non_streaming(pack, active_key):
|
|
168
|
+
"""비스트리밍 응답 완료: latency 계산 → 로그싱크 전송 → active 카운터 감소."""
|
|
169
|
+
try:
|
|
170
|
+
pack.latency = round((time.monotonic() - pack._start_time) * 1000)
|
|
171
|
+
pack.success = True
|
|
172
|
+
_dispatch(pack)
|
|
173
|
+
finally:
|
|
174
|
+
_ensure_end(pack, active_key)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _dispatch(pack):
|
|
178
|
+
"""로그싱크팩 전송 + 메트릭 stat 업데이트 통합 호출."""
|
|
179
|
+
dispatch_llm_pack(pack)
|
|
180
|
+
inst = _stat_task()
|
|
181
|
+
if inst:
|
|
182
|
+
inst.notify(pack)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from whatap import logging
|
|
2
|
+
from whatap.conf.configure import Configure as conf
|
|
3
|
+
from whatap.trace.trace_handler import trace_handler, async_trace_handler
|
|
4
|
+
from whatap.llm.providers.openai.chat.chat import intercept_create, intercept_create_async
|
|
5
|
+
from whatap.llm.providers.openai.responses.responses import intercept_responses_create, intercept_responses_create_async
|
|
6
|
+
from whatap.llm.providers.openai.embeddings.embeddings import intercept_embeddings, intercept_embeddings_async
|
|
7
|
+
from whatap.llm.providers.openai.completions.completions import intercept_completions, intercept_completions_async
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def instrument_openai(module):
|
|
11
|
+
|
|
12
|
+
if not conf.llm_enabled:
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
def create_wrapper(fn):
|
|
16
|
+
@trace_handler(fn)
|
|
17
|
+
def trace(*args, **kwargs):
|
|
18
|
+
return intercept_create(fn, *args, **kwargs)
|
|
19
|
+
return trace
|
|
20
|
+
|
|
21
|
+
def async_create_wrapper(fn):
|
|
22
|
+
@async_trace_handler(fn)
|
|
23
|
+
async def trace(*args, **kwargs):
|
|
24
|
+
return await intercept_create_async(fn, *args, **kwargs)
|
|
25
|
+
return trace
|
|
26
|
+
|
|
27
|
+
def embeddings_wrapper(fn):
|
|
28
|
+
@trace_handler(fn)
|
|
29
|
+
def trace(*args, **kwargs):
|
|
30
|
+
return intercept_embeddings(fn, *args, **kwargs)
|
|
31
|
+
return trace
|
|
32
|
+
|
|
33
|
+
def async_embeddings_wrapper(fn):
|
|
34
|
+
@async_trace_handler(fn)
|
|
35
|
+
async def trace(*args, **kwargs):
|
|
36
|
+
return await intercept_embeddings_async(fn, *args, **kwargs)
|
|
37
|
+
return trace
|
|
38
|
+
|
|
39
|
+
def completions_wrapper(fn):
|
|
40
|
+
@trace_handler(fn)
|
|
41
|
+
def trace(*args, **kwargs):
|
|
42
|
+
return intercept_completions(fn, *args, **kwargs)
|
|
43
|
+
return trace
|
|
44
|
+
|
|
45
|
+
def async_completions_wrapper(fn):
|
|
46
|
+
@async_trace_handler(fn)
|
|
47
|
+
async def trace(*args, **kwargs):
|
|
48
|
+
return await intercept_completions_async(fn, *args, **kwargs)
|
|
49
|
+
return trace
|
|
50
|
+
|
|
51
|
+
def responses_wrapper(fn):
|
|
52
|
+
@trace_handler(fn)
|
|
53
|
+
def trace(*args, **kwargs):
|
|
54
|
+
return intercept_responses_create(fn, *args, **kwargs)
|
|
55
|
+
return trace
|
|
56
|
+
|
|
57
|
+
def async_responses_wrapper(fn):
|
|
58
|
+
@async_trace_handler(fn)
|
|
59
|
+
async def trace(*args, **kwargs):
|
|
60
|
+
return await intercept_responses_create_async(fn, *args, **kwargs)
|
|
61
|
+
return trace
|
|
62
|
+
|
|
63
|
+
# Sync: openai.resources.chat.completions.Completions.create
|
|
64
|
+
if (hasattr(module, 'resources') and
|
|
65
|
+
hasattr(module.resources, 'chat') and
|
|
66
|
+
hasattr(module.resources.chat, 'completions') and
|
|
67
|
+
hasattr(module.resources.chat.completions, 'Completions') and
|
|
68
|
+
hasattr(module.resources.chat.completions.Completions, 'create')):
|
|
69
|
+
original_create = module.resources.chat.completions.Completions.create
|
|
70
|
+
module.resources.chat.completions.Completions.create = create_wrapper(original_create)
|
|
71
|
+
|
|
72
|
+
# Async: openai.resources.chat.completions.AsyncCompletions.create
|
|
73
|
+
if (hasattr(module, 'resources') and
|
|
74
|
+
hasattr(module.resources, 'chat') and
|
|
75
|
+
hasattr(module.resources.chat, 'completions') and
|
|
76
|
+
hasattr(module.resources.chat.completions, 'AsyncCompletions') and
|
|
77
|
+
hasattr(module.resources.chat.completions.AsyncCompletions, 'create')):
|
|
78
|
+
original_async_create = module.resources.chat.completions.AsyncCompletions.create
|
|
79
|
+
module.resources.chat.completions.AsyncCompletions.create = async_create_wrapper(original_async_create)
|
|
80
|
+
|
|
81
|
+
# Sync: openai.resources.completions.Completions.create
|
|
82
|
+
if (hasattr(module, 'resources') and
|
|
83
|
+
hasattr(module.resources, 'completions') and
|
|
84
|
+
hasattr(module.resources.completions, 'Completions') and
|
|
85
|
+
hasattr(module.resources.completions.Completions, 'create')):
|
|
86
|
+
original_completions = module.resources.completions.Completions.create
|
|
87
|
+
module.resources.completions.Completions.create = completions_wrapper(original_completions)
|
|
88
|
+
|
|
89
|
+
# Async: openai.resources.completions.AsyncCompletions.create
|
|
90
|
+
if (hasattr(module, 'resources') and
|
|
91
|
+
hasattr(module.resources, 'completions') and
|
|
92
|
+
hasattr(module.resources.completions, 'AsyncCompletions') and
|
|
93
|
+
hasattr(module.resources.completions.AsyncCompletions, 'create')):
|
|
94
|
+
original_async_completions = module.resources.completions.AsyncCompletions.create
|
|
95
|
+
module.resources.completions.AsyncCompletions.create = async_completions_wrapper(original_async_completions)
|
|
96
|
+
|
|
97
|
+
# Sync: openai.resources.embeddings.Embeddings.create
|
|
98
|
+
if (hasattr(module, 'resources') and
|
|
99
|
+
hasattr(module.resources, 'embeddings') and
|
|
100
|
+
hasattr(module.resources.embeddings, 'Embeddings') and
|
|
101
|
+
hasattr(module.resources.embeddings.Embeddings, 'create')):
|
|
102
|
+
original_embeddings = module.resources.embeddings.Embeddings.create
|
|
103
|
+
module.resources.embeddings.Embeddings.create = embeddings_wrapper(original_embeddings)
|
|
104
|
+
|
|
105
|
+
# Async: openai.resources.embeddings.AsyncEmbeddings.create
|
|
106
|
+
if (hasattr(module, 'resources') and
|
|
107
|
+
hasattr(module.resources, 'embeddings') and
|
|
108
|
+
hasattr(module.resources.embeddings, 'AsyncEmbeddings') and
|
|
109
|
+
hasattr(module.resources.embeddings.AsyncEmbeddings, 'create')):
|
|
110
|
+
original_async_embeddings = module.resources.embeddings.AsyncEmbeddings.create
|
|
111
|
+
module.resources.embeddings.AsyncEmbeddings.create = async_embeddings_wrapper(original_async_embeddings)
|
|
112
|
+
|
|
113
|
+
# Sync: openai.resources.responses.responses.Responses.create (Responses API)
|
|
114
|
+
try:
|
|
115
|
+
from openai.resources.responses.responses import Responses as ResponsesCls
|
|
116
|
+
if hasattr(ResponsesCls, 'create'):
|
|
117
|
+
original_responses_create = ResponsesCls.create
|
|
118
|
+
ResponsesCls.create = responses_wrapper(original_responses_create)
|
|
119
|
+
except ImportError:
|
|
120
|
+
pass
|
|
121
|
+
except AttributeError as e:
|
|
122
|
+
logging.warning('[LLM] Responses API patch failed: %s' % e, extra={'id': 'LLM020'})
|
|
123
|
+
|
|
124
|
+
# Async: openai.resources.responses.responses.AsyncResponses.create
|
|
125
|
+
try:
|
|
126
|
+
from openai.resources.responses.responses import AsyncResponses as AsyncResponsesCls
|
|
127
|
+
if hasattr(AsyncResponsesCls, 'create'):
|
|
128
|
+
original_async_responses_create = AsyncResponsesCls.create
|
|
129
|
+
AsyncResponsesCls.create = async_responses_wrapper(original_async_responses_create)
|
|
130
|
+
except ImportError:
|
|
131
|
+
pass
|
|
132
|
+
except AttributeError as e:
|
|
133
|
+
logging.warning('[LLM] Async Responses API patch failed: %s' % e, extra={'id': 'LLM021'})
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""OpenAI Chat Completions API 호출을 인터셉트하는 모듈."""
|
|
2
|
+
from openai import OpenAIError
|
|
3
|
+
|
|
4
|
+
from whatap.llm.providers.interceptor import (
|
|
5
|
+
before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
|
|
6
|
+
capture_client,
|
|
7
|
+
)
|
|
8
|
+
from whatap.llm.providers.stream_accumulator import sync_stream, async_stream
|
|
9
|
+
from whatap.llm.providers.openai.chat.chat_context import build_context
|
|
10
|
+
from whatap.llm.providers.openai.chat.chat_extractor import finalize, ChatStream
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def intercept_create(fn, *args, **kwargs):
|
|
14
|
+
"""OpenAI Chat Completions 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
|
|
15
|
+
pack, ctx, features, stream = build_context(kwargs)
|
|
16
|
+
capture_client(pack, ctx, args)
|
|
17
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
18
|
+
|
|
19
|
+
if stream:
|
|
20
|
+
opts = dict(kwargs.get("stream_options") or {})
|
|
21
|
+
if not opts.get("include_usage"):
|
|
22
|
+
opts["include_usage"] = True
|
|
23
|
+
kwargs["stream_options"] = opts
|
|
24
|
+
|
|
25
|
+
before_call(pack, active_key)
|
|
26
|
+
_stream_returned = False
|
|
27
|
+
try:
|
|
28
|
+
try:
|
|
29
|
+
response = fn(*args, **kwargs)
|
|
30
|
+
except Exception as err:
|
|
31
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
32
|
+
raise
|
|
33
|
+
finally:
|
|
34
|
+
if ctx:
|
|
35
|
+
ctx._llm_httpc_pending = False
|
|
36
|
+
|
|
37
|
+
after_call(pack, ctx)
|
|
38
|
+
if stream:
|
|
39
|
+
_stream_returned = True
|
|
40
|
+
return sync_stream(response, ChatStream(pack, active_key))
|
|
41
|
+
finalize(response, pack, features)
|
|
42
|
+
finalize_non_streaming(pack, active_key)
|
|
43
|
+
return response
|
|
44
|
+
finally:
|
|
45
|
+
if not _stream_returned:
|
|
46
|
+
_ensure_end(pack, active_key)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def intercept_create_async(fn, *args, **kwargs):
|
|
50
|
+
"""OpenAI Chat Completions 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
|
|
51
|
+
pack, ctx, features, stream = build_context(kwargs)
|
|
52
|
+
capture_client(pack, ctx, args)
|
|
53
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
54
|
+
|
|
55
|
+
if stream:
|
|
56
|
+
opts = dict(kwargs.get("stream_options") or {})
|
|
57
|
+
if not opts.get("include_usage"):
|
|
58
|
+
opts["include_usage"] = True
|
|
59
|
+
kwargs["stream_options"] = opts
|
|
60
|
+
|
|
61
|
+
before_call(pack, active_key)
|
|
62
|
+
_stream_returned = False
|
|
63
|
+
try:
|
|
64
|
+
try:
|
|
65
|
+
response = await fn(*args, **kwargs)
|
|
66
|
+
except Exception as err:
|
|
67
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
68
|
+
raise
|
|
69
|
+
finally:
|
|
70
|
+
if ctx:
|
|
71
|
+
ctx._llm_httpc_pending = False
|
|
72
|
+
|
|
73
|
+
after_call(pack, ctx)
|
|
74
|
+
if stream:
|
|
75
|
+
_stream_returned = True
|
|
76
|
+
return async_stream(response, ChatStream(pack, active_key))
|
|
77
|
+
finalize(response, pack, features)
|
|
78
|
+
finalize_non_streaming(pack, active_key)
|
|
79
|
+
return response
|
|
80
|
+
finally:
|
|
81
|
+
if not _stream_returned:
|
|
82
|
+
_ensure_end(pack, active_key)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""OpenAI Chat Completions 요청에서 컨텍스트를 구성하는 모듈."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from whatap import logging
|
|
5
|
+
from whatap.llm.features import LlmFeature
|
|
6
|
+
from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
|
|
7
|
+
from whatap.llm.providers.openai.content_parser import extract_content_text
|
|
8
|
+
from whatap.trace.trace_context_manager import TraceContextManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_context(kwargs):
|
|
12
|
+
"""Chat kwargs로부터 LlmStepStatus 팩과 트레이스 컨텍스트를 구성한다."""
|
|
13
|
+
messages = kwargs.get("messages", [])
|
|
14
|
+
model = kwargs.get("model")
|
|
15
|
+
stream = kwargs.get("stream", False)
|
|
16
|
+
|
|
17
|
+
system_parts, prompt_msgs, tool_results = [], [], []
|
|
18
|
+
features, has_vision = [], False
|
|
19
|
+
|
|
20
|
+
for msg in messages:
|
|
21
|
+
if not isinstance(msg, dict):
|
|
22
|
+
try:
|
|
23
|
+
msg = msg.model_dump()
|
|
24
|
+
except Exception:
|
|
25
|
+
msg = dict(msg)
|
|
26
|
+
role = msg.get("role", "user")
|
|
27
|
+
content = msg.get("content")
|
|
28
|
+
if role == "system":
|
|
29
|
+
system_parts.append(extract_content_text(content))
|
|
30
|
+
elif role == "tool":
|
|
31
|
+
text = extract_content_text(content)
|
|
32
|
+
tool_results.append({"tool_call_id": msg.get("tool_call_id", ""), "content": text})
|
|
33
|
+
sanitized = dict(msg)
|
|
34
|
+
if isinstance(content, list):
|
|
35
|
+
sanitized["content"] = text
|
|
36
|
+
prompt_msgs.append(sanitized)
|
|
37
|
+
else:
|
|
38
|
+
sanitized = dict(msg)
|
|
39
|
+
if isinstance(content, list):
|
|
40
|
+
if not has_vision:
|
|
41
|
+
for block in content:
|
|
42
|
+
if isinstance(block, dict) and block.get("type") == "image_url":
|
|
43
|
+
has_vision = True
|
|
44
|
+
break
|
|
45
|
+
sanitized["content"] = extract_content_text(content)
|
|
46
|
+
prompt_msgs.append(sanitized)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
prompt_text = json.dumps(prompt_msgs, ensure_ascii=False) if prompt_msgs else ""
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logging.warning('[LLM] chat prompt serialization failed: %s' % e, extra={'id': 'LLM004'})
|
|
52
|
+
prompt_text = ""
|
|
53
|
+
|
|
54
|
+
if has_vision:
|
|
55
|
+
features.append(LlmFeature.VISION)
|
|
56
|
+
if kwargs.get("reasoning_effort"):
|
|
57
|
+
features.append(LlmFeature.REASONING)
|
|
58
|
+
rf = kwargs.get("response_format")
|
|
59
|
+
if isinstance(rf, dict) and rf.get("type") == "json_schema":
|
|
60
|
+
features.append(LlmFeature.STRUCTURED_OUTPUT)
|
|
61
|
+
|
|
62
|
+
pack = LlmStepStatus()
|
|
63
|
+
pack.model = model
|
|
64
|
+
pack.prompt_text = prompt_text
|
|
65
|
+
pack.system_texts = system_parts
|
|
66
|
+
pack.stream = stream
|
|
67
|
+
pack.features = ",".join(features)
|
|
68
|
+
if kwargs.get("temperature") is not None:
|
|
69
|
+
pack.temperature = kwargs["temperature"]
|
|
70
|
+
if tool_results:
|
|
71
|
+
pack.tool_results_text = json.dumps(tool_results, ensure_ascii=False)
|
|
72
|
+
|
|
73
|
+
ctx = TraceContextManager.getLocalContext()
|
|
74
|
+
if ctx:
|
|
75
|
+
ctx._llm_httpc_pending = True
|
|
76
|
+
ctx._llm_model = model
|
|
77
|
+
|
|
78
|
+
return pack, ctx, features, stream
|