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,127 @@
|
|
|
1
|
+
"""OpenAI Chat Completions 응답에서 토큰 사용량 및 완성 텍스트를 추출하는 모듈."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from whatap.llm.features import LlmFeature
|
|
5
|
+
from whatap.llm.providers.stream_accumulator import StreamAccumulator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def extract_usage(usage):
|
|
9
|
+
"""usage 객체에서 토큰 사용량 상세 정보를 딕셔너리로 추출한다."""
|
|
10
|
+
input_t = getattr(usage, "prompt_tokens", 0) or 0
|
|
11
|
+
output_t = getattr(usage, "completion_tokens", 0) or 0
|
|
12
|
+
prompt_d = getattr(usage, "prompt_tokens_details", None)
|
|
13
|
+
comp_d = getattr(usage, "completion_tokens_details", None)
|
|
14
|
+
return {
|
|
15
|
+
"input_tokens": input_t,
|
|
16
|
+
"output_tokens": output_t,
|
|
17
|
+
"total_tokens_count": input_t + output_t,
|
|
18
|
+
"cached_tokens": getattr(prompt_d, "cached_tokens", 0) or 0,
|
|
19
|
+
"audio_input_tokens": getattr(prompt_d, "audio_tokens", 0) or 0,
|
|
20
|
+
"reasoning_tokens": getattr(comp_d, "reasoning_tokens", 0) or 0,
|
|
21
|
+
"audio_output_tokens": getattr(comp_d, "audio_tokens", 0) or 0,
|
|
22
|
+
"accepted_prediction_tokens": getattr(comp_d, "accepted_prediction_tokens", 0) or 0,
|
|
23
|
+
"rejected_prediction_tokens": getattr(comp_d, "rejected_prediction_tokens", 0) or 0,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def finalize(response, pack, features):
|
|
28
|
+
"""비스트리밍 Chat 응답에서 완성 텍스트, 토큰, 도구 호출 정보를 팩에 기록한다."""
|
|
29
|
+
texts, reasonings = [], []
|
|
30
|
+
for choice in response.choices:
|
|
31
|
+
if not pack.finish_reason:
|
|
32
|
+
pack.finish_reason = getattr(choice, 'finish_reason', None)
|
|
33
|
+
texts.append(choice.message.content or "")
|
|
34
|
+
reasonings.append(
|
|
35
|
+
getattr(choice.message, "reasoning_content", "") or
|
|
36
|
+
getattr(choice.message, "reasoning", "") or "")
|
|
37
|
+
tc = getattr(choice.message, "tool_calls", None)
|
|
38
|
+
if tc and LlmFeature.TOOL_USE not in features:
|
|
39
|
+
features.append(LlmFeature.TOOL_USE)
|
|
40
|
+
try:
|
|
41
|
+
pack.tool_calls_text = json.dumps([t.model_dump() for t in tc], ensure_ascii=False)
|
|
42
|
+
except Exception:
|
|
43
|
+
pack.tool_calls_text = str(tc)
|
|
44
|
+
|
|
45
|
+
reasoning = "".join(reasonings)
|
|
46
|
+
if reasoning and LlmFeature.REASONING not in features:
|
|
47
|
+
features.append(LlmFeature.REASONING)
|
|
48
|
+
|
|
49
|
+
pack.features = ",".join(features)
|
|
50
|
+
pack.set_tokens(extract_usage(getattr(response, "usage", None)))
|
|
51
|
+
pack.completion_text = "".join(texts)
|
|
52
|
+
if reasoning:
|
|
53
|
+
pack.reasoning_text = reasoning
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ChatStream(StreamAccumulator):
|
|
57
|
+
"""OpenAI Chat 스트리밍 응답 청크를 누적하는 어큐뮬레이터."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, pack, active_key):
|
|
60
|
+
super().__init__(pack, active_key)
|
|
61
|
+
self.text = ""
|
|
62
|
+
self.reasoning = ""
|
|
63
|
+
self.usage = {}
|
|
64
|
+
self.tool_chunks = {}
|
|
65
|
+
self.has_tool = False
|
|
66
|
+
self.finish_reason = None
|
|
67
|
+
|
|
68
|
+
def on_chunk(self, chunk):
|
|
69
|
+
usage = getattr(chunk, "usage", None)
|
|
70
|
+
if usage:
|
|
71
|
+
self.usage = extract_usage(usage)
|
|
72
|
+
|
|
73
|
+
choices = getattr(chunk, 'choices', None)
|
|
74
|
+
if not choices:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
finish_reason = getattr(choices[0], 'finish_reason', None)
|
|
78
|
+
if finish_reason:
|
|
79
|
+
self.finish_reason = finish_reason
|
|
80
|
+
|
|
81
|
+
delta = choices[0].delta
|
|
82
|
+
|
|
83
|
+
content = getattr(delta, "content", None) or ""
|
|
84
|
+
if content:
|
|
85
|
+
self.on_first_token()
|
|
86
|
+
self.text += content
|
|
87
|
+
|
|
88
|
+
reasoning = (
|
|
89
|
+
getattr(delta, "reasoning_content", None) or
|
|
90
|
+
getattr(delta, "reasoning", None) or "")
|
|
91
|
+
if reasoning:
|
|
92
|
+
self.reasoning += reasoning
|
|
93
|
+
|
|
94
|
+
if getattr(delta, "tool_calls", None):
|
|
95
|
+
self.has_tool = True
|
|
96
|
+
for tc in delta.tool_calls:
|
|
97
|
+
idx = tc.index
|
|
98
|
+
if idx not in self.tool_chunks:
|
|
99
|
+
self.tool_chunks[idx] = {"id": "", "function": "", "arguments": ""}
|
|
100
|
+
if tc.id:
|
|
101
|
+
self.tool_chunks[idx]["id"] = tc.id
|
|
102
|
+
fn = getattr(tc, "function", None)
|
|
103
|
+
if fn:
|
|
104
|
+
if fn.name:
|
|
105
|
+
self.tool_chunks[idx]["function"] = fn.name
|
|
106
|
+
if fn.arguments:
|
|
107
|
+
self.tool_chunks[idx]["arguments"] += fn.arguments
|
|
108
|
+
|
|
109
|
+
def _apply(self):
|
|
110
|
+
pack = self.pack
|
|
111
|
+
pack.finish_reason = self.finish_reason
|
|
112
|
+
if self.has_tool:
|
|
113
|
+
parts = [f for f in pack.features.split(",") if f]
|
|
114
|
+
if LlmFeature.TOOL_USE not in parts:
|
|
115
|
+
parts.append(LlmFeature.TOOL_USE)
|
|
116
|
+
pack.features = ",".join(parts)
|
|
117
|
+
if self.reasoning:
|
|
118
|
+
parts = [f for f in pack.features.split(",") if f]
|
|
119
|
+
if LlmFeature.REASONING not in parts:
|
|
120
|
+
parts.append(LlmFeature.REASONING)
|
|
121
|
+
pack.features = ",".join(parts)
|
|
122
|
+
if self.tool_chunks:
|
|
123
|
+
pack.tool_calls_text = json.dumps(list(self.tool_chunks.values()), ensure_ascii=False)
|
|
124
|
+
pack.set_tokens(self.usage)
|
|
125
|
+
pack.completion_text = self.text
|
|
126
|
+
if self.reasoning:
|
|
127
|
+
pack.reasoning_text = self.reasoning
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""OpenAI 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.completions.completions_context import build_context
|
|
10
|
+
from whatap.llm.providers.openai.completions.completions_extractor import finalize, CompletionsStream
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def intercept_completions(fn, *args, **kwargs):
|
|
14
|
+
"""Completions API 동기 인터셉트."""
|
|
15
|
+
pack, ctx = build_context(kwargs)
|
|
16
|
+
capture_client(pack, ctx, args)
|
|
17
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
18
|
+
|
|
19
|
+
before_call(pack, active_key)
|
|
20
|
+
_stream_returned = False
|
|
21
|
+
try:
|
|
22
|
+
try:
|
|
23
|
+
response = fn(*args, **kwargs)
|
|
24
|
+
except Exception as err:
|
|
25
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
26
|
+
raise
|
|
27
|
+
finally:
|
|
28
|
+
if ctx:
|
|
29
|
+
ctx._llm_httpc_pending = False
|
|
30
|
+
|
|
31
|
+
after_call(pack, ctx)
|
|
32
|
+
if pack.stream:
|
|
33
|
+
_stream_returned = True
|
|
34
|
+
return sync_stream(response, CompletionsStream(pack, active_key))
|
|
35
|
+
finalize(response, pack)
|
|
36
|
+
finalize_non_streaming(pack, active_key)
|
|
37
|
+
return response
|
|
38
|
+
finally:
|
|
39
|
+
if not _stream_returned:
|
|
40
|
+
_ensure_end(pack, active_key)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def intercept_completions_async(fn, *args, **kwargs):
|
|
44
|
+
"""Completions API 비동기 인터셉트."""
|
|
45
|
+
pack, ctx = build_context(kwargs)
|
|
46
|
+
capture_client(pack, ctx, args)
|
|
47
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
48
|
+
|
|
49
|
+
before_call(pack, active_key)
|
|
50
|
+
_stream_returned = False
|
|
51
|
+
try:
|
|
52
|
+
try:
|
|
53
|
+
response = await fn(*args, **kwargs)
|
|
54
|
+
except Exception as err:
|
|
55
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
56
|
+
raise
|
|
57
|
+
finally:
|
|
58
|
+
if ctx:
|
|
59
|
+
ctx._llm_httpc_pending = False
|
|
60
|
+
|
|
61
|
+
after_call(pack, ctx)
|
|
62
|
+
if pack.stream:
|
|
63
|
+
_stream_returned = True
|
|
64
|
+
return async_stream(response, CompletionsStream(pack, active_key))
|
|
65
|
+
finalize(response, pack)
|
|
66
|
+
finalize_non_streaming(pack, active_key)
|
|
67
|
+
return response
|
|
68
|
+
finally:
|
|
69
|
+
if not _stream_returned:
|
|
70
|
+
_ensure_end(pack, active_key)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""OpenAI Completions API 요청 파싱."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from whatap.trace.trace_context_manager import TraceContextManager
|
|
5
|
+
from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_context(kwargs):
|
|
9
|
+
"""Completions kwargs에서 LlmStepStatus 팩을 생성한다."""
|
|
10
|
+
model = kwargs.get("model")
|
|
11
|
+
stream = kwargs.get("stream", False)
|
|
12
|
+
prompt = kwargs.get("prompt", "")
|
|
13
|
+
|
|
14
|
+
if isinstance(prompt, list):
|
|
15
|
+
prompt_text = json.dumps(prompt, ensure_ascii=False)
|
|
16
|
+
else:
|
|
17
|
+
prompt_text = str(prompt) if prompt else ""
|
|
18
|
+
|
|
19
|
+
pack = LlmStepStatus()
|
|
20
|
+
pack.model = model
|
|
21
|
+
pack.prompt_text = prompt_text
|
|
22
|
+
pack.stream = stream
|
|
23
|
+
if kwargs.get("temperature") is not None:
|
|
24
|
+
pack.temperature = kwargs["temperature"]
|
|
25
|
+
|
|
26
|
+
ctx = TraceContextManager.getLocalContext()
|
|
27
|
+
if ctx:
|
|
28
|
+
ctx._llm_httpc_pending = True
|
|
29
|
+
ctx._llm_model = model
|
|
30
|
+
|
|
31
|
+
return pack, ctx
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""OpenAI Completions API 응답 추출."""
|
|
2
|
+
from whatap.llm.providers.stream_accumulator import StreamAccumulator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def extract_usage(usage):
|
|
6
|
+
"""usage 객체에서 토큰 사용량을 추출한다."""
|
|
7
|
+
if not usage:
|
|
8
|
+
return {}
|
|
9
|
+
input_t = getattr(usage, "prompt_tokens", 0) or 0
|
|
10
|
+
output_t = getattr(usage, "completion_tokens", 0) or 0
|
|
11
|
+
return {
|
|
12
|
+
"input_tokens": input_t,
|
|
13
|
+
"output_tokens": output_t,
|
|
14
|
+
"total_tokens_count": input_t + output_t,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def finalize(response, pack):
|
|
19
|
+
"""비스트리밍 Completions 응답에서 텍스트와 토큰을 팩에 기록한다."""
|
|
20
|
+
texts = []
|
|
21
|
+
for choice in response.choices:
|
|
22
|
+
if not pack.finish_reason:
|
|
23
|
+
pack.finish_reason = getattr(choice, 'finish_reason', None)
|
|
24
|
+
texts.append(getattr(choice, 'text', '') or '')
|
|
25
|
+
|
|
26
|
+
pack.set_tokens(extract_usage(getattr(response, "usage", None)))
|
|
27
|
+
pack.completion_text = "".join(texts)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CompletionsStream(StreamAccumulator):
|
|
31
|
+
"""OpenAI Completions 스트리밍 응답 청크를 누적하는 어큐뮬레이터."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, pack, active_key):
|
|
34
|
+
super().__init__(pack, active_key)
|
|
35
|
+
self.text = ""
|
|
36
|
+
self.usage = {}
|
|
37
|
+
self.finish_reason = None
|
|
38
|
+
|
|
39
|
+
def on_chunk(self, chunk):
|
|
40
|
+
usage = getattr(chunk, "usage", None)
|
|
41
|
+
if usage:
|
|
42
|
+
self.usage = extract_usage(usage)
|
|
43
|
+
|
|
44
|
+
choices = getattr(chunk, 'choices', None)
|
|
45
|
+
if not choices:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
finish_reason = getattr(choices[0], 'finish_reason', None)
|
|
49
|
+
if finish_reason:
|
|
50
|
+
self.finish_reason = finish_reason
|
|
51
|
+
|
|
52
|
+
text = getattr(choices[0], 'text', '') or ''
|
|
53
|
+
if text:
|
|
54
|
+
self.on_first_token()
|
|
55
|
+
self.text += text
|
|
56
|
+
|
|
57
|
+
def _apply(self):
|
|
58
|
+
pack = self.pack
|
|
59
|
+
pack.finish_reason = self.finish_reason
|
|
60
|
+
pack.set_tokens(self.usage)
|
|
61
|
+
pack.completion_text = self.text
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""OpenAI 컨텐츠 블록 파싱.
|
|
2
|
+
|
|
3
|
+
OpenAI API의 content 필드는 문자열 또는 블록 리스트(text, image_url 등)로 올 수 있다.
|
|
4
|
+
이 모듈은 다양한 content 형태를 텍스트로 변환한다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _block_to_dict(block):
|
|
9
|
+
"""Pydantic 모델 등 비dict 블록을 dict로 변환."""
|
|
10
|
+
if isinstance(block, dict):
|
|
11
|
+
return block
|
|
12
|
+
if hasattr(block, 'model_dump'):
|
|
13
|
+
try:
|
|
14
|
+
return block.model_dump()
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
if hasattr(block, '__dict__'):
|
|
18
|
+
return dict(vars(block))
|
|
19
|
+
return {"type": str(getattr(block, 'type', ''))}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_content_text(content):
|
|
23
|
+
"""content를 텍스트로 변환. 이미지/오디오 블록은 [IMAGE], [AUDIO]로 치환."""
|
|
24
|
+
if isinstance(content, str):
|
|
25
|
+
return content
|
|
26
|
+
if isinstance(content, list):
|
|
27
|
+
parts = []
|
|
28
|
+
for block in content:
|
|
29
|
+
if not isinstance(block, dict):
|
|
30
|
+
block = _block_to_dict(block)
|
|
31
|
+
block_type = block.get("type", "")
|
|
32
|
+
if block_type in ("text", "input_text"):
|
|
33
|
+
parts.append(block.get("text", ""))
|
|
34
|
+
elif block_type in ("image_url", "input_image"):
|
|
35
|
+
parts.append("[IMAGE]")
|
|
36
|
+
elif block_type == "input_audio":
|
|
37
|
+
parts.append("[AUDIO]")
|
|
38
|
+
elif block_type:
|
|
39
|
+
parts.append("[{}]".format(block_type.upper()))
|
|
40
|
+
return "".join(parts)
|
|
41
|
+
return str(content) if content else ""
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""OpenAI Embeddings 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.openai.embeddings.embeddings_context import build_context
|
|
9
|
+
from whatap.llm.providers.openai.embeddings.embeddings_extractor import finalize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def intercept_embeddings(fn, *args, **kwargs):
|
|
13
|
+
"""OpenAI Embeddings 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
|
|
14
|
+
pack, ctx = build_context(kwargs)
|
|
15
|
+
capture_client(pack, ctx, args)
|
|
16
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
17
|
+
|
|
18
|
+
before_call(pack, active_key)
|
|
19
|
+
try:
|
|
20
|
+
try:
|
|
21
|
+
response = fn(*args, **kwargs)
|
|
22
|
+
except Exception as err:
|
|
23
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
24
|
+
raise
|
|
25
|
+
finally:
|
|
26
|
+
if ctx:
|
|
27
|
+
ctx._llm_httpc_pending = False
|
|
28
|
+
|
|
29
|
+
after_call(pack, ctx)
|
|
30
|
+
finalize(response, pack, kwargs)
|
|
31
|
+
finalize_non_streaming(pack, active_key)
|
|
32
|
+
return response
|
|
33
|
+
finally:
|
|
34
|
+
_ensure_end(pack, active_key)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def intercept_embeddings_async(fn, *args, **kwargs):
|
|
38
|
+
"""OpenAI Embeddings 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
|
|
39
|
+
pack, ctx = build_context(kwargs)
|
|
40
|
+
capture_client(pack, ctx, args)
|
|
41
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
42
|
+
|
|
43
|
+
before_call(pack, active_key)
|
|
44
|
+
try:
|
|
45
|
+
try:
|
|
46
|
+
response = await fn(*args, **kwargs)
|
|
47
|
+
except Exception as err:
|
|
48
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
49
|
+
raise
|
|
50
|
+
finally:
|
|
51
|
+
if ctx:
|
|
52
|
+
ctx._llm_httpc_pending = False
|
|
53
|
+
|
|
54
|
+
after_call(pack, ctx)
|
|
55
|
+
finalize(response, pack, kwargs)
|
|
56
|
+
finalize_non_streaming(pack, active_key)
|
|
57
|
+
return response
|
|
58
|
+
finally:
|
|
59
|
+
_ensure_end(pack, active_key)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""OpenAI Embeddings API 요청에서 컨텍스트를 구성하는 모듈."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from whatap.trace.trace_context_manager import TraceContextManager
|
|
5
|
+
from whatap.llm.log_sink_packs.llm_step_status import LlmStepStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_context(kwargs):
|
|
9
|
+
"""Embeddings kwargs로부터 LlmStepStatus 팩과 트레이스 컨텍스트를 구성한다."""
|
|
10
|
+
input_data = kwargs.get("input", "")
|
|
11
|
+
model = kwargs.get("model")
|
|
12
|
+
|
|
13
|
+
inputs = input_data if isinstance(input_data, list) else [input_data]
|
|
14
|
+
|
|
15
|
+
pack = LlmStepStatus()
|
|
16
|
+
pack.model = model
|
|
17
|
+
pack.prompt_text = json.dumps(inputs, ensure_ascii=False)
|
|
18
|
+
pack.stream = False
|
|
19
|
+
|
|
20
|
+
ctx = TraceContextManager.getLocalContext()
|
|
21
|
+
if ctx:
|
|
22
|
+
ctx._llm_httpc_pending = True
|
|
23
|
+
ctx._llm_model = model
|
|
24
|
+
|
|
25
|
+
return pack, ctx
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""OpenAI Embeddings 응답에서 토큰 사용량과 임베딩 정보를 추출하는 모듈."""
|
|
2
|
+
from whatap import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def finalize(response, pack, kwargs):
|
|
6
|
+
"""Embeddings 응답에서 토큰 수, 임베딩 개수, 차원 수를 팩에 기록한다."""
|
|
7
|
+
usage = getattr(response, "usage", None)
|
|
8
|
+
input_tokens = getattr(usage, "prompt_tokens", 0) or 0
|
|
9
|
+
|
|
10
|
+
embedding_count = 0
|
|
11
|
+
dimensions = 0
|
|
12
|
+
try:
|
|
13
|
+
data = getattr(response, "data", [])
|
|
14
|
+
embedding_count = len(data)
|
|
15
|
+
if data and kwargs.get("encoding_format") != "base64":
|
|
16
|
+
dimensions = len(data[0].embedding)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
logging.warning('[LLM] embeddings data extraction failed: %s' % e, extra={'id': 'LLM010'})
|
|
19
|
+
|
|
20
|
+
pack.set_tokens({
|
|
21
|
+
"input_tokens": input_tokens,
|
|
22
|
+
"output_tokens": 0,
|
|
23
|
+
"total_tokens_count": getattr(usage, "total_tokens", 0) or 0,
|
|
24
|
+
"embedding_count": embedding_count,
|
|
25
|
+
"dimensions": dimensions,
|
|
26
|
+
})
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""OpenAI Responses 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.responses.responses_context import build_context
|
|
10
|
+
from whatap.llm.providers.openai.responses.responses_extractor import finalize, ResponsesStream
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def intercept_responses_create(fn, *args, **kwargs):
|
|
14
|
+
"""OpenAI Responses 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
|
|
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
|
+
before_call(pack, active_key)
|
|
20
|
+
_stream_returned = False
|
|
21
|
+
try:
|
|
22
|
+
try:
|
|
23
|
+
response = fn(*args, **kwargs)
|
|
24
|
+
except Exception as err:
|
|
25
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
26
|
+
raise
|
|
27
|
+
finally:
|
|
28
|
+
if ctx:
|
|
29
|
+
ctx._llm_httpc_pending = False
|
|
30
|
+
|
|
31
|
+
after_call(pack, ctx)
|
|
32
|
+
if stream:
|
|
33
|
+
_stream_returned = True
|
|
34
|
+
return sync_stream(response, ResponsesStream(pack, active_key))
|
|
35
|
+
finalize(response, pack, features)
|
|
36
|
+
finalize_non_streaming(pack, active_key)
|
|
37
|
+
return response
|
|
38
|
+
finally:
|
|
39
|
+
if not _stream_returned:
|
|
40
|
+
_ensure_end(pack, active_key)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def intercept_responses_create_async(fn, *args, **kwargs):
|
|
44
|
+
"""OpenAI Responses 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
|
|
45
|
+
pack, ctx, features, stream = build_context(kwargs)
|
|
46
|
+
capture_client(pack, ctx, args)
|
|
47
|
+
active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
|
|
48
|
+
|
|
49
|
+
before_call(pack, active_key)
|
|
50
|
+
_stream_returned = False
|
|
51
|
+
try:
|
|
52
|
+
try:
|
|
53
|
+
response = await fn(*args, **kwargs)
|
|
54
|
+
except Exception as err:
|
|
55
|
+
handle_error(pack, err, active_key, OpenAIError)
|
|
56
|
+
raise
|
|
57
|
+
finally:
|
|
58
|
+
if ctx:
|
|
59
|
+
ctx._llm_httpc_pending = False
|
|
60
|
+
|
|
61
|
+
after_call(pack, ctx)
|
|
62
|
+
if stream:
|
|
63
|
+
_stream_returned = True
|
|
64
|
+
return async_stream(response, ResponsesStream(pack, active_key))
|
|
65
|
+
finalize(response, pack, features)
|
|
66
|
+
finalize_non_streaming(pack, active_key)
|
|
67
|
+
return response
|
|
68
|
+
finally:
|
|
69
|
+
if not _stream_returned:
|
|
70
|
+
_ensure_end(pack, active_key)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""OpenAI Responses API 요청에서 컨텍스트를 구성하는 모듈."""
|
|
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, _block_to_dict
|
|
8
|
+
from whatap.trace.trace_context_manager import TraceContextManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _msg_to_dict(msg):
|
|
12
|
+
"""메시지 객체를 딕셔너리로 변환한다."""
|
|
13
|
+
if isinstance(msg, dict):
|
|
14
|
+
return msg
|
|
15
|
+
if hasattr(msg, 'model_dump'):
|
|
16
|
+
try:
|
|
17
|
+
return msg.model_dump()
|
|
18
|
+
except Exception as e:
|
|
19
|
+
logging.warning('[LLM] _msg_to_dict model_dump failed: %s' % e, extra={'id': 'LLM014'})
|
|
20
|
+
if hasattr(msg, '__dict__'):
|
|
21
|
+
return dict(vars(msg))
|
|
22
|
+
return {"type": str(getattr(msg, 'type', 'unknown'))}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_context(kwargs):
|
|
26
|
+
"""Responses kwargs로부터 LlmStepStatus 팩과 트레이스 컨텍스트를 구성한다."""
|
|
27
|
+
input_data = kwargs.get("input", "")
|
|
28
|
+
model = kwargs.get("model")
|
|
29
|
+
stream = kwargs.get("stream", False)
|
|
30
|
+
instructions = kwargs.get("instructions")
|
|
31
|
+
|
|
32
|
+
system_parts = [instructions] if instructions else []
|
|
33
|
+
has_vision = False
|
|
34
|
+
tool_results = []
|
|
35
|
+
|
|
36
|
+
if isinstance(input_data, str):
|
|
37
|
+
prompt_text = input_data
|
|
38
|
+
elif isinstance(input_data, list):
|
|
39
|
+
prompt_msgs = []
|
|
40
|
+
for raw_msg in input_data:
|
|
41
|
+
msg = _msg_to_dict(raw_msg)
|
|
42
|
+
role = msg.get("role", "user")
|
|
43
|
+
content = msg.get("content")
|
|
44
|
+
if role == "system":
|
|
45
|
+
system_parts.append(content or "")
|
|
46
|
+
else:
|
|
47
|
+
sanitized = dict(msg)
|
|
48
|
+
if isinstance(content, list):
|
|
49
|
+
if not has_vision:
|
|
50
|
+
for block in content:
|
|
51
|
+
if _block_to_dict(block).get("type") == "input_image":
|
|
52
|
+
has_vision = True
|
|
53
|
+
break
|
|
54
|
+
sanitized["content"] = extract_content_text(content)
|
|
55
|
+
prompt_msgs.append(sanitized)
|
|
56
|
+
if msg.get("type") == "function_call_output":
|
|
57
|
+
tool_results.append({"call_id": msg.get("call_id", ""), "output": msg.get("output", "")})
|
|
58
|
+
try:
|
|
59
|
+
prompt_text = json.dumps(prompt_msgs, ensure_ascii=False) if prompt_msgs else ""
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logging.warning('[LLM] responses prompt serialization failed: %s' % e, extra={'id': 'LLM005'})
|
|
62
|
+
prompt_text = ""
|
|
63
|
+
else:
|
|
64
|
+
prompt_text = str(input_data)
|
|
65
|
+
|
|
66
|
+
features = []
|
|
67
|
+
if kwargs.get("reasoning"):
|
|
68
|
+
features.append(LlmFeature.REASONING)
|
|
69
|
+
if has_vision:
|
|
70
|
+
features.append(LlmFeature.VISION)
|
|
71
|
+
|
|
72
|
+
pack = LlmStepStatus()
|
|
73
|
+
pack.model = model
|
|
74
|
+
pack.prompt_text = prompt_text
|
|
75
|
+
pack.system_texts = system_parts
|
|
76
|
+
pack.stream = stream
|
|
77
|
+
pack.features = ",".join(features)
|
|
78
|
+
if kwargs.get("temperature") is not None:
|
|
79
|
+
pack.temperature = kwargs["temperature"]
|
|
80
|
+
if tool_results:
|
|
81
|
+
pack.tool_results_text = json.dumps(tool_results, ensure_ascii=False)
|
|
82
|
+
|
|
83
|
+
ctx = TraceContextManager.getLocalContext()
|
|
84
|
+
if ctx:
|
|
85
|
+
ctx._llm_httpc_pending = True
|
|
86
|
+
ctx._llm_model = model
|
|
87
|
+
|
|
88
|
+
return pack, ctx, features, stream
|