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.
Files changed (227) hide show
  1. whatap/LICENSE +0 -0
  2. whatap/README.rst +49 -0
  3. whatap/__init__.py +923 -0
  4. whatap/__main__.py +4 -0
  5. whatap/agent/darwin/amd64/whatap_python +0 -0
  6. whatap/agent/darwin/arm64/whatap_python +0 -0
  7. whatap/agent/linux/amd64/whatap_python +0 -0
  8. whatap/agent/linux/arm64/whatap_python +0 -0
  9. whatap/agent/windows/whatap_python.exe +0 -0
  10. whatap/bootstrap/__init__.py +0 -0
  11. whatap/bootstrap/sitecustomize.py +19 -0
  12. whatap/build.py +4 -0
  13. whatap/conf/__init__.py +0 -0
  14. whatap/conf/configuration.py +280 -0
  15. whatap/conf/configure.py +105 -0
  16. whatap/conf/license.py +49 -0
  17. whatap/control/__init__.py +0 -0
  18. whatap/counter/__init__.py +14 -0
  19. whatap/counter/counter_manager.py +45 -0
  20. whatap/counter/tasks/__init__.py +3 -0
  21. whatap/counter/tasks/base_task.py +26 -0
  22. whatap/counter/tasks/llm_evaluator_task.py +501 -0
  23. whatap/counter/tasks/llm_log_sink_task.py +309 -0
  24. whatap/counter/tasks/llm_stat_task.py +78 -0
  25. whatap/counter/tasks/openfiledescriptor.py +67 -0
  26. whatap/io/__init__.py +1 -0
  27. whatap/io/data_inputx.py +161 -0
  28. whatap/io/data_outputx.py +262 -0
  29. whatap/llm/__init__.py +17 -0
  30. whatap/llm/definitions.py +43 -0
  31. whatap/llm/evaluators/__init__.py +136 -0
  32. whatap/llm/evaluators/base.py +114 -0
  33. whatap/llm/evaluators/builtins/__init__.py +91 -0
  34. whatap/llm/evaluators/builtins/answer_relevance.py +46 -0
  35. whatap/llm/evaluators/builtins/combined_judge.py +271 -0
  36. whatap/llm/evaluators/builtins/factuality.py +71 -0
  37. whatap/llm/evaluators/builtins/hallucination.py +97 -0
  38. whatap/llm/evaluators/builtins/llm_judge.py +516 -0
  39. whatap/llm/evaluators/builtins/pii_leak.py +214 -0
  40. whatap/llm/evaluators/builtins/prompt_injection.py +71 -0
  41. whatap/llm/evaluators/builtins/toxicity.py +53 -0
  42. whatap/llm/evaluators/builtins/url_scan.py +194 -0
  43. whatap/llm/evaluators/registry.py +192 -0
  44. whatap/llm/evaluators/sampler.py +83 -0
  45. whatap/llm/evaluators/scope.py +334 -0
  46. whatap/llm/features.py +66 -0
  47. whatap/llm/log_sink_packs/__init__.py +9 -0
  48. whatap/llm/log_sink_packs/llm_input_message.py +16 -0
  49. whatap/llm/log_sink_packs/llm_log_sink_pack.py +72 -0
  50. whatap/llm/log_sink_packs/llm_output_message.py +19 -0
  51. whatap/llm/log_sink_packs/llm_step_eval_status.py +94 -0
  52. whatap/llm/log_sink_packs/llm_step_status.py +118 -0
  53. whatap/llm/log_sink_packs/llm_system_message.py +16 -0
  54. whatap/llm/log_sink_packs/llm_tool_calls.py +44 -0
  55. whatap/llm/log_sink_packs/llm_tool_results.py +16 -0
  56. whatap/llm/log_sink_packs/llm_tx_status.py +108 -0
  57. whatap/llm/pricing.py +236 -0
  58. whatap/llm/prompt_meta.py +288 -0
  59. whatap/llm/providers/__init__.py +0 -0
  60. whatap/llm/providers/anthropic/__init__.py +37 -0
  61. whatap/llm/providers/anthropic/messages/__init__.py +0 -0
  62. whatap/llm/providers/anthropic/messages/messages.py +70 -0
  63. whatap/llm/providers/anthropic/messages/messages_context.py +76 -0
  64. whatap/llm/providers/anthropic/messages/messages_extractor.py +126 -0
  65. whatap/llm/providers/interceptor.py +182 -0
  66. whatap/llm/providers/openai/__init__.py +133 -0
  67. whatap/llm/providers/openai/chat/__init__.py +0 -0
  68. whatap/llm/providers/openai/chat/chat.py +82 -0
  69. whatap/llm/providers/openai/chat/chat_context.py +78 -0
  70. whatap/llm/providers/openai/chat/chat_extractor.py +127 -0
  71. whatap/llm/providers/openai/completions/__init__.py +0 -0
  72. whatap/llm/providers/openai/completions/completions.py +70 -0
  73. whatap/llm/providers/openai/completions/completions_context.py +31 -0
  74. whatap/llm/providers/openai/completions/completions_extractor.py +61 -0
  75. whatap/llm/providers/openai/content_parser.py +41 -0
  76. whatap/llm/providers/openai/embeddings/__init__.py +0 -0
  77. whatap/llm/providers/openai/embeddings/embeddings.py +59 -0
  78. whatap/llm/providers/openai/embeddings/embeddings_context.py +25 -0
  79. whatap/llm/providers/openai/embeddings/embeddings_extractor.py +26 -0
  80. whatap/llm/providers/openai/responses/__init__.py +0 -0
  81. whatap/llm/providers/openai/responses/responses.py +70 -0
  82. whatap/llm/providers/openai/responses/responses_context.py +88 -0
  83. whatap/llm/providers/openai/responses/responses_extractor.py +126 -0
  84. whatap/llm/providers/stream_accumulator.py +73 -0
  85. whatap/llm/stats/__init__.py +35 -0
  86. whatap/llm/stats/active_stat.py +86 -0
  87. whatap/llm/stats/answer_relevance_eval_stat.py +10 -0
  88. whatap/llm/stats/api_status_stat.py +35 -0
  89. whatap/llm/stats/base_stat.py +107 -0
  90. whatap/llm/stats/combined_judge_eval_stat.py +11 -0
  91. whatap/llm/stats/error_stat.py +59 -0
  92. whatap/llm/stats/eval_stat.py +225 -0
  93. whatap/llm/stats/factuality_eval_stat.py +10 -0
  94. whatap/llm/stats/feature_stat.py +104 -0
  95. whatap/llm/stats/finish_stat.py +105 -0
  96. whatap/llm/stats/hallucination_eval_stat.py +10 -0
  97. whatap/llm/stats/meter.py +18 -0
  98. whatap/llm/stats/perf_stat.py +117 -0
  99. whatap/llm/stats/pii_leak_eval_stat.py +12 -0
  100. whatap/llm/stats/prompt_injection_eval_stat.py +10 -0
  101. whatap/llm/stats/token_usage_stat.py +133 -0
  102. whatap/llm/stats/toxicity_eval_stat.py +10 -0
  103. whatap/llm/stats/url_scan_eval_stat.py +12 -0
  104. whatap/net/__init__.py +0 -0
  105. whatap/net/async_sender.py +107 -0
  106. whatap/net/packet_enum.py +44 -0
  107. whatap/net/packet_type_enum.py +31 -0
  108. whatap/net/param_def.py +69 -0
  109. whatap/net/stackhelper.py +87 -0
  110. whatap/net/udp_session.py +394 -0
  111. whatap/net/udp_thread.py +54 -0
  112. whatap/pack/__init__.py +0 -0
  113. whatap/pack/logSinkPack.py +77 -0
  114. whatap/pack/pack.py +34 -0
  115. whatap/pack/pack_enum.py +41 -0
  116. whatap/pack/tagCountPack.py +61 -0
  117. whatap/scripts/__init__.py +208 -0
  118. whatap/trace/__init__.py +12 -0
  119. whatap/trace/mod/__init__.py +0 -0
  120. whatap/trace/mod/amqp/__init__.py +0 -0
  121. whatap/trace/mod/amqp/kombu.py +122 -0
  122. whatap/trace/mod/amqp/pika.py +62 -0
  123. whatap/trace/mod/application/__init__.py +0 -0
  124. whatap/trace/mod/application/bottle.py +34 -0
  125. whatap/trace/mod/application/celery.py +81 -0
  126. whatap/trace/mod/application/cherrypy.py +30 -0
  127. whatap/trace/mod/application/django.py +287 -0
  128. whatap/trace/mod/application/django_asgi.py +266 -0
  129. whatap/trace/mod/application/django_py3.py +251 -0
  130. whatap/trace/mod/application/fastapi/__init__.py +31 -0
  131. whatap/trace/mod/application/fastapi/endpoint.py +73 -0
  132. whatap/trace/mod/application/fastapi/exception_log.py +63 -0
  133. whatap/trace/mod/application/fastapi/instrumentation.py +204 -0
  134. whatap/trace/mod/application/fastapi/scope.py +115 -0
  135. whatap/trace/mod/application/fastapi/transaction.py +67 -0
  136. whatap/trace/mod/application/flask.py +52 -0
  137. whatap/trace/mod/application/frappe.py +224 -0
  138. whatap/trace/mod/application/graphql.py +170 -0
  139. whatap/trace/mod/application/nameko.py +39 -0
  140. whatap/trace/mod/application/odoo.py +63 -0
  141. whatap/trace/mod/application/starlette.py +126 -0
  142. whatap/trace/mod/application/tornado.py +163 -0
  143. whatap/trace/mod/application/wsgi.py +195 -0
  144. whatap/trace/mod/database/__init__.py +0 -0
  145. whatap/trace/mod/database/cxoracle.py +49 -0
  146. whatap/trace/mod/database/mongo.py +169 -0
  147. whatap/trace/mod/database/mysql.py +80 -0
  148. whatap/trace/mod/database/neo4j.py +90 -0
  149. whatap/trace/mod/database/psycopg2.py +45 -0
  150. whatap/trace/mod/database/psycopg3.py +359 -0
  151. whatap/trace/mod/database/redis.py +122 -0
  152. whatap/trace/mod/database/sqlalchemy.py +213 -0
  153. whatap/trace/mod/database/sqlite3.py +130 -0
  154. whatap/trace/mod/database/util.py +630 -0
  155. whatap/trace/mod/email/__init__.py +0 -0
  156. whatap/trace/mod/email/smtp.py +78 -0
  157. whatap/trace/mod/httpc/__init__.py +0 -0
  158. whatap/trace/mod/httpc/django.py +31 -0
  159. whatap/trace/mod/httpc/httplib.py +70 -0
  160. whatap/trace/mod/httpc/httpx.py +62 -0
  161. whatap/trace/mod/httpc/requests.py +20 -0
  162. whatap/trace/mod/httpc/urllib3.py +27 -0
  163. whatap/trace/mod/httpc/util.py +388 -0
  164. whatap/trace/mod/logging.py +161 -0
  165. whatap/trace/mod/plugin.py +84 -0
  166. whatap/trace/mod/standalone/__init__.py +0 -0
  167. whatap/trace/mod/standalone/multiple.py +293 -0
  168. whatap/trace/mod/standalone/single.py +135 -0
  169. whatap/trace/simple_trace_context.py +18 -0
  170. whatap/trace/trace_context.py +212 -0
  171. whatap/trace/trace_context_manager.py +244 -0
  172. whatap/trace/trace_error.py +84 -0
  173. whatap/trace/trace_handler.py +89 -0
  174. whatap/trace/trace_import.py +91 -0
  175. whatap/trace/trace_module_definition.py +156 -0
  176. whatap/util/__init__.py +0 -0
  177. whatap/util/bit_util.py +49 -0
  178. whatap/util/cardinality/__init__.py +0 -0
  179. whatap/util/cardinality/hyperloglog.py +84 -0
  180. whatap/util/cardinality/murmurhash.py +20 -0
  181. whatap/util/cardinality/registerset.py +60 -0
  182. whatap/util/compare_util.py +19 -0
  183. whatap/util/date_util.py +55 -0
  184. whatap/util/debug_util.py +73 -0
  185. whatap/util/escape_literal_sql.py +233 -0
  186. whatap/util/frame_util.py +20 -0
  187. whatap/util/hash_util.py +103 -0
  188. whatap/util/hexa32.py +66 -0
  189. whatap/util/int_set.py +199 -0
  190. whatap/util/ip_util.py +63 -0
  191. whatap/util/keygen.py +11 -0
  192. whatap/util/linked_list.py +113 -0
  193. whatap/util/linked_map.py +359 -0
  194. whatap/util/metering_util.py +103 -0
  195. whatap/util/request_double_queue.py +68 -0
  196. whatap/util/request_queue.py +60 -0
  197. whatap/util/string_util.py +20 -0
  198. whatap/util/throttle_util.py +99 -0
  199. whatap/util/userid_util.py +134 -0
  200. whatap/value/__init__.py +1 -0
  201. whatap/value/blob_value.py +38 -0
  202. whatap/value/boolean_value.py +33 -0
  203. whatap/value/decimal_value.py +36 -0
  204. whatap/value/double_summary.py +86 -0
  205. whatap/value/double_value.py +33 -0
  206. whatap/value/float_array.py +42 -0
  207. whatap/value/float_value.py +34 -0
  208. whatap/value/int_array.py +42 -0
  209. whatap/value/ip4_value.py +50 -0
  210. whatap/value/list_value.py +105 -0
  211. whatap/value/long_array.py +44 -0
  212. whatap/value/long_summary.py +83 -0
  213. whatap/value/map_value.py +154 -0
  214. whatap/value/null_value.py +21 -0
  215. whatap/value/number_value.py +33 -0
  216. whatap/value/summary_value.py +39 -0
  217. whatap/value/text_array.py +58 -0
  218. whatap/value/text_hash_value.py +37 -0
  219. whatap/value/text_value.py +43 -0
  220. whatap/value/value.py +26 -0
  221. whatap/value/value_enum.py +80 -0
  222. whatap/whatap.conf +14 -0
  223. whatap_python-2.1.0.dist-info/METADATA +87 -0
  224. whatap_python-2.1.0.dist-info/RECORD +227 -0
  225. whatap_python-2.1.0.dist-info/WHEEL +5 -0
  226. whatap_python-2.1.0.dist-info/entry_points.txt +6 -0
  227. 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