lucidicai 2.1.3__py3-none-any.whl → 3.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.
- lucidicai/__init__.py +32 -390
- lucidicai/api/client.py +31 -2
- lucidicai/api/resources/__init__.py +16 -1
- lucidicai/api/resources/dataset.py +422 -82
- lucidicai/api/resources/evals.py +209 -0
- lucidicai/api/resources/event.py +399 -27
- lucidicai/api/resources/experiment.py +108 -0
- lucidicai/api/resources/feature_flag.py +78 -0
- lucidicai/api/resources/prompt.py +84 -0
- lucidicai/api/resources/session.py +545 -38
- lucidicai/client.py +408 -480
- lucidicai/core/config.py +73 -48
- lucidicai/core/errors.py +3 -3
- lucidicai/sdk/bound_decorators.py +321 -0
- lucidicai/sdk/context.py +20 -2
- lucidicai/sdk/decorators.py +283 -74
- lucidicai/sdk/event.py +538 -36
- lucidicai/sdk/event_builder.py +2 -4
- lucidicai/sdk/features/dataset.py +391 -1
- lucidicai/sdk/features/feature_flag.py +344 -3
- lucidicai/sdk/init.py +49 -347
- lucidicai/sdk/session.py +502 -0
- lucidicai/sdk/shutdown_manager.py +103 -46
- lucidicai/session_obj.py +321 -0
- lucidicai/telemetry/context_capture_processor.py +13 -6
- lucidicai/telemetry/extract.py +60 -63
- lucidicai/telemetry/litellm_bridge.py +3 -44
- lucidicai/telemetry/lucidic_exporter.py +143 -131
- lucidicai/telemetry/openai_agents_instrumentor.py +2 -2
- lucidicai/telemetry/openai_patch.py +7 -6
- lucidicai/telemetry/telemetry_manager.py +183 -0
- lucidicai/telemetry/utils/model_pricing.py +21 -30
- lucidicai/telemetry/utils/provider.py +77 -0
- lucidicai/utils/images.py +27 -11
- lucidicai/utils/serialization.py +27 -0
- {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/METADATA +1 -1
- {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/RECORD +39 -29
- {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/WHEEL +0 -0
- {lucidicai-2.1.3.dist-info → lucidicai-3.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Evals resource API operations."""
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
from ..client import HttpClient
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("Lucidic")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _truncate_id(id_str: Optional[str]) -> str:
|
|
12
|
+
"""Truncate ID for logging."""
|
|
13
|
+
if not id_str:
|
|
14
|
+
return "None"
|
|
15
|
+
return f"{id_str[:8]}..." if len(id_str) > 8 else id_str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _infer_result_type(result: Any) -> str:
|
|
19
|
+
"""Infer result type from Python value.
|
|
20
|
+
|
|
21
|
+
Note: bool must be checked first because bool is a subclass of int in Python.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
result: The evaluation result value.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The result type string: "boolean", "number", or "string".
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If result is not a supported type.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(result, bool):
|
|
33
|
+
return "boolean"
|
|
34
|
+
elif isinstance(result, (int, float)):
|
|
35
|
+
return "number"
|
|
36
|
+
elif isinstance(result, str):
|
|
37
|
+
return "string"
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Unsupported result type: {type(result).__name__}. "
|
|
41
|
+
"Must be bool, int, float, or str."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validate_result_type(result: Any, result_type: str) -> bool:
|
|
46
|
+
"""Validate that result matches the specified result_type.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
result: The evaluation result value.
|
|
50
|
+
result_type: The expected type ("boolean", "number", "string").
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if the result matches the type, False otherwise.
|
|
54
|
+
"""
|
|
55
|
+
if result_type == "boolean":
|
|
56
|
+
return isinstance(result, bool)
|
|
57
|
+
elif result_type == "number":
|
|
58
|
+
# Check for bool first since bool is subclass of int
|
|
59
|
+
return isinstance(result, (int, float)) and not isinstance(result, bool)
|
|
60
|
+
elif result_type == "string":
|
|
61
|
+
return isinstance(result, str)
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EvalsResource:
|
|
66
|
+
"""Handle evaluation-related API operations."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, http: HttpClient, production: bool = False):
|
|
69
|
+
"""Initialize evals resource.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
http: HTTP client instance
|
|
73
|
+
production: Whether to suppress errors in production mode
|
|
74
|
+
"""
|
|
75
|
+
self.http = http
|
|
76
|
+
self._production = production
|
|
77
|
+
|
|
78
|
+
def emit(
|
|
79
|
+
self,
|
|
80
|
+
result: Union[bool, int, float, str],
|
|
81
|
+
name: Optional[str] = None,
|
|
82
|
+
description: Optional[str] = None,
|
|
83
|
+
result_type: Optional[str] = None,
|
|
84
|
+
session_id: Optional[str] = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Fire-and-forget evaluation submission that returns instantly.
|
|
87
|
+
|
|
88
|
+
This function returns immediately while the actual evaluation
|
|
89
|
+
submission happens in a background thread. Perfect for non-blocking
|
|
90
|
+
evaluation logging.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
result: The evaluation result. Can be bool, int, float, or str.
|
|
94
|
+
name: Optional name for the evaluation. If not provided, the backend
|
|
95
|
+
will generate a default name based on the result type.
|
|
96
|
+
description: Optional description of the evaluation.
|
|
97
|
+
result_type: Optional explicit result type ("boolean", "number", "string").
|
|
98
|
+
If not provided, it will be inferred from the result value.
|
|
99
|
+
session_id: Optional session ID. If not provided, uses the current
|
|
100
|
+
session from context.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
# Basic usage - type inferred
|
|
104
|
+
client.evals.emit(result=True)
|
|
105
|
+
client.evals.emit(result=0.95)
|
|
106
|
+
client.evals.emit(result="excellent")
|
|
107
|
+
|
|
108
|
+
# With name and description
|
|
109
|
+
client.evals.emit(
|
|
110
|
+
result=True,
|
|
111
|
+
name="task_completed",
|
|
112
|
+
description="User task was successful"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Explicit session_id
|
|
116
|
+
client.evals.emit(result=0.87, name="accuracy_score", session_id="abc-123")
|
|
117
|
+
"""
|
|
118
|
+
from ...sdk.context import current_session_id
|
|
119
|
+
|
|
120
|
+
# Capture session from context if not provided
|
|
121
|
+
captured_session_id = session_id
|
|
122
|
+
if not captured_session_id:
|
|
123
|
+
captured_session_id = current_session_id.get(None)
|
|
124
|
+
|
|
125
|
+
if not captured_session_id:
|
|
126
|
+
logger.debug("[EvalsResource] No active session for emit()")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Infer or validate result_type
|
|
130
|
+
try:
|
|
131
|
+
if result_type is None:
|
|
132
|
+
inferred_type = _infer_result_type(result)
|
|
133
|
+
else:
|
|
134
|
+
# Validate that result matches the explicit type
|
|
135
|
+
if not _validate_result_type(result, result_type):
|
|
136
|
+
error_msg = (
|
|
137
|
+
f"Result type mismatch: result is {type(result).__name__} "
|
|
138
|
+
f"but result_type is '{result_type}'"
|
|
139
|
+
)
|
|
140
|
+
if self._production:
|
|
141
|
+
logger.error(f"[EvalsResource] {error_msg}")
|
|
142
|
+
return
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(error_msg)
|
|
145
|
+
inferred_type = result_type
|
|
146
|
+
except ValueError as e:
|
|
147
|
+
if self._production:
|
|
148
|
+
logger.error(f"[EvalsResource] {e}")
|
|
149
|
+
return
|
|
150
|
+
else:
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
# Capture all data for background thread
|
|
154
|
+
captured_result = result
|
|
155
|
+
captured_name = name
|
|
156
|
+
captured_description = description
|
|
157
|
+
captured_type = inferred_type
|
|
158
|
+
|
|
159
|
+
def _background_emit():
|
|
160
|
+
try:
|
|
161
|
+
params: Dict[str, Any] = {
|
|
162
|
+
"session_id": captured_session_id,
|
|
163
|
+
"result": captured_result,
|
|
164
|
+
"result_type": captured_type,
|
|
165
|
+
}
|
|
166
|
+
if captured_name is not None:
|
|
167
|
+
params["name"] = captured_name
|
|
168
|
+
if captured_description is not None:
|
|
169
|
+
params["description"] = captured_description
|
|
170
|
+
|
|
171
|
+
self._create_eval(params)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.debug(f"[EvalsResource] Background emit() failed: {e}")
|
|
174
|
+
|
|
175
|
+
# Start background thread
|
|
176
|
+
thread = threading.Thread(target=_background_emit, daemon=True)
|
|
177
|
+
thread.start()
|
|
178
|
+
|
|
179
|
+
def _create_eval(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
180
|
+
"""Send evaluation to backend API.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
params: Evaluation parameters including:
|
|
184
|
+
- session_id: Session ID
|
|
185
|
+
- result: Evaluation result value
|
|
186
|
+
- result_type: Type of result ("boolean", "number", "string")
|
|
187
|
+
- name: Optional evaluation name
|
|
188
|
+
- description: Optional description
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
API response (typically empty for 201 Created)
|
|
192
|
+
"""
|
|
193
|
+
session_id = params.get("session_id")
|
|
194
|
+
name = params.get("name")
|
|
195
|
+
result_type = params.get("result_type")
|
|
196
|
+
logger.debug(
|
|
197
|
+
f"[Evals] _create_eval() called - "
|
|
198
|
+
f"session_id={_truncate_id(session_id)}, name={name!r}, "
|
|
199
|
+
f"result_type={result_type!r}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
response = self.http.post("sdk/evals", params)
|
|
203
|
+
|
|
204
|
+
logger.debug(
|
|
205
|
+
f"[Evals] _create_eval() response - "
|
|
206
|
+
f"session_id={_truncate_id(session_id)}, "
|
|
207
|
+
f"response_keys={list(response.keys()) if response else 'None'}"
|
|
208
|
+
)
|
|
209
|
+
return response
|
lucidicai/api/resources/event.py
CHANGED
|
@@ -1,24 +1,297 @@
|
|
|
1
1
|
"""Event resource API operations."""
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
2
6
|
from typing import Any, Dict, Optional
|
|
3
|
-
from datetime import datetime
|
|
4
7
|
|
|
5
8
|
from ..client import HttpClient
|
|
6
9
|
|
|
10
|
+
logger = logging.getLogger("Lucidic")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _truncate_id(id_str: Optional[str]) -> str:
|
|
14
|
+
"""Truncate ID for logging."""
|
|
15
|
+
if not id_str:
|
|
16
|
+
return "None"
|
|
17
|
+
return f"{id_str[:8]}..." if len(id_str) > 8 else id_str
|
|
18
|
+
|
|
7
19
|
|
|
8
20
|
class EventResource:
|
|
9
21
|
"""Handle event-related API operations."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, http: HttpClient):
|
|
22
|
+
|
|
23
|
+
def __init__(self, http: HttpClient, production: bool = False):
|
|
12
24
|
"""Initialize event resource.
|
|
13
|
-
|
|
25
|
+
|
|
14
26
|
Args:
|
|
15
27
|
http: HTTP client instance
|
|
28
|
+
production: Whether to suppress errors in production mode
|
|
16
29
|
"""
|
|
17
30
|
self.http = http
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
self._production = production
|
|
32
|
+
|
|
33
|
+
# ==================== High-Level Event Methods ====================
|
|
34
|
+
|
|
35
|
+
def create(
|
|
36
|
+
self,
|
|
37
|
+
type: str = "generic",
|
|
38
|
+
event_id: Optional[str] = None,
|
|
39
|
+
session_id: Optional[str] = None,
|
|
40
|
+
**kwargs,
|
|
41
|
+
) -> str:
|
|
20
42
|
"""Create a new event.
|
|
21
|
-
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
type: Event type (e.g., "llm_generation", "function_call", "error_traceback", "generic")
|
|
46
|
+
event_id: Optional client event ID (auto-generated if not provided)
|
|
47
|
+
session_id: Optional session ID (uses current context if not provided)
|
|
48
|
+
**kwargs: Event-specific fields
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Event ID (client-generated or provided UUID)
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
event_id = client.events.create(
|
|
55
|
+
type="custom_event",
|
|
56
|
+
data={"key": "value"}
|
|
57
|
+
)
|
|
58
|
+
"""
|
|
59
|
+
from ...sdk.context import current_session_id, current_parent_event_id
|
|
60
|
+
from ...sdk.event_builder import EventBuilder
|
|
61
|
+
|
|
62
|
+
# Generate event ID if not provided
|
|
63
|
+
client_event_id = event_id or str(uuid.uuid4())
|
|
64
|
+
|
|
65
|
+
# Get session from context if not provided
|
|
66
|
+
if not session_id:
|
|
67
|
+
session_id = current_session_id.get(None)
|
|
68
|
+
|
|
69
|
+
if not session_id:
|
|
70
|
+
logger.debug("[EventResource] No active session for create()")
|
|
71
|
+
return client_event_id
|
|
72
|
+
|
|
73
|
+
# Get parent event ID from context
|
|
74
|
+
parent_event_id = None
|
|
75
|
+
try:
|
|
76
|
+
parent_event_id = current_parent_event_id.get(None)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# Build event request
|
|
81
|
+
params = {
|
|
82
|
+
"type": type,
|
|
83
|
+
"event_id": client_event_id,
|
|
84
|
+
"parent_event_id": parent_event_id,
|
|
85
|
+
"session_id": session_id,
|
|
86
|
+
"occurred_at": kwargs.pop("occurred_at", None)
|
|
87
|
+
or datetime.now(timezone.utc).isoformat(),
|
|
88
|
+
**kwargs,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
event_request = EventBuilder.build(params)
|
|
93
|
+
self.create_event(event_request)
|
|
94
|
+
logger.debug(f"[EventResource] Created event {client_event_id[:8]}...")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
if self._production:
|
|
97
|
+
logger.error(f"[EventResource] Failed to create event: {e}")
|
|
98
|
+
else:
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
return client_event_id
|
|
102
|
+
|
|
103
|
+
async def acreate(
|
|
104
|
+
self,
|
|
105
|
+
type: str = "generic",
|
|
106
|
+
event_id: Optional[str] = None,
|
|
107
|
+
session_id: Optional[str] = None,
|
|
108
|
+
**kwargs,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Create a new event (async version).
|
|
111
|
+
|
|
112
|
+
See create() for full documentation.
|
|
113
|
+
"""
|
|
114
|
+
from ...sdk.context import current_session_id, current_parent_event_id
|
|
115
|
+
from ...sdk.event_builder import EventBuilder
|
|
116
|
+
|
|
117
|
+
client_event_id = event_id or str(uuid.uuid4())
|
|
118
|
+
|
|
119
|
+
if not session_id:
|
|
120
|
+
session_id = current_session_id.get(None)
|
|
121
|
+
|
|
122
|
+
if not session_id:
|
|
123
|
+
logger.debug("[EventResource] No active session for acreate()")
|
|
124
|
+
return client_event_id
|
|
125
|
+
|
|
126
|
+
parent_event_id = None
|
|
127
|
+
try:
|
|
128
|
+
parent_event_id = current_parent_event_id.get(None)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
params = {
|
|
133
|
+
"type": type,
|
|
134
|
+
"event_id": client_event_id,
|
|
135
|
+
"parent_event_id": parent_event_id,
|
|
136
|
+
"session_id": session_id,
|
|
137
|
+
"occurred_at": kwargs.pop("occurred_at", None)
|
|
138
|
+
or datetime.now(timezone.utc).isoformat(),
|
|
139
|
+
**kwargs,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
event_request = EventBuilder.build(params)
|
|
144
|
+
await self.acreate_event(event_request)
|
|
145
|
+
logger.debug(f"[EventResource] Created async event {client_event_id[:8]}...")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
if self._production:
|
|
148
|
+
logger.error(f"[EventResource] Failed to create async event: {e}")
|
|
149
|
+
else:
|
|
150
|
+
raise
|
|
151
|
+
|
|
152
|
+
return client_event_id
|
|
153
|
+
|
|
154
|
+
def emit(
|
|
155
|
+
self,
|
|
156
|
+
type: str = "generic",
|
|
157
|
+
event_id: Optional[str] = None,
|
|
158
|
+
session_id: Optional[str] = None,
|
|
159
|
+
**kwargs,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Fire-and-forget event creation that returns instantly.
|
|
162
|
+
|
|
163
|
+
This function returns immediately with an event ID, while the actual
|
|
164
|
+
event creation happens in a background thread. Perfect for hot path
|
|
165
|
+
telemetry where latency is critical.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
type: Event type (e.g., "llm_generation", "function_call", "generic")
|
|
169
|
+
event_id: Optional client event ID (auto-generated if not provided)
|
|
170
|
+
session_id: Optional session ID (uses current context if not provided)
|
|
171
|
+
**kwargs: Event-specific fields
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Event ID (client-generated or provided UUID) - returned immediately
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
client.events.emit(type="log", message="Something happened")
|
|
178
|
+
"""
|
|
179
|
+
from ...sdk.context import current_session_id, current_parent_event_id
|
|
180
|
+
|
|
181
|
+
# Pre-generate event ID for instant return
|
|
182
|
+
client_event_id = event_id or str(uuid.uuid4())
|
|
183
|
+
|
|
184
|
+
# Capture context variables BEFORE creating the thread
|
|
185
|
+
captured_parent_id = kwargs.get("parent_event_id")
|
|
186
|
+
if captured_parent_id is None:
|
|
187
|
+
try:
|
|
188
|
+
captured_parent_id = current_parent_event_id.get(None)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# Get session from context if not provided
|
|
193
|
+
captured_session_id = session_id
|
|
194
|
+
if not captured_session_id:
|
|
195
|
+
captured_session_id = current_session_id.get(None)
|
|
196
|
+
|
|
197
|
+
if not captured_session_id:
|
|
198
|
+
logger.debug("[EventResource] No active session for emit()")
|
|
199
|
+
return client_event_id
|
|
200
|
+
|
|
201
|
+
# Capture all data for background thread
|
|
202
|
+
captured_kwargs = dict(kwargs)
|
|
203
|
+
captured_kwargs["parent_event_id"] = captured_parent_id
|
|
204
|
+
|
|
205
|
+
def _background_create():
|
|
206
|
+
try:
|
|
207
|
+
self.create(
|
|
208
|
+
type=type,
|
|
209
|
+
event_id=client_event_id,
|
|
210
|
+
session_id=captured_session_id,
|
|
211
|
+
**captured_kwargs,
|
|
212
|
+
)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.debug(f"[EventResource] Background emit() failed: {e}")
|
|
215
|
+
|
|
216
|
+
# Start background thread
|
|
217
|
+
thread = threading.Thread(target=_background_create, daemon=True)
|
|
218
|
+
thread.start()
|
|
219
|
+
|
|
220
|
+
return client_event_id
|
|
221
|
+
|
|
222
|
+
def create_error(
|
|
223
|
+
self,
|
|
224
|
+
error: Any,
|
|
225
|
+
parent_event_id: Optional[str] = None,
|
|
226
|
+
**kwargs,
|
|
227
|
+
) -> str:
|
|
228
|
+
"""Create an error traceback event.
|
|
229
|
+
|
|
230
|
+
Convenience method for creating error events with proper traceback information.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
error: The error message or exception object
|
|
234
|
+
parent_event_id: Optional parent event ID for nesting
|
|
235
|
+
**kwargs: Additional event parameters
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Event ID of the created error event
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
try:
|
|
242
|
+
risky_operation()
|
|
243
|
+
except Exception as e:
|
|
244
|
+
client.events.create_error(e)
|
|
245
|
+
"""
|
|
246
|
+
import traceback as tb
|
|
247
|
+
|
|
248
|
+
if isinstance(error, Exception):
|
|
249
|
+
error_str = str(error)
|
|
250
|
+
traceback_str = tb.format_exc()
|
|
251
|
+
else:
|
|
252
|
+
error_str = str(error)
|
|
253
|
+
traceback_str = kwargs.pop("traceback", "")
|
|
254
|
+
|
|
255
|
+
return self.create(
|
|
256
|
+
type="error_traceback",
|
|
257
|
+
error=error_str,
|
|
258
|
+
traceback=traceback_str,
|
|
259
|
+
parent_event_id=parent_event_id,
|
|
260
|
+
**kwargs,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
async def acreate_error(
|
|
264
|
+
self,
|
|
265
|
+
error: Any,
|
|
266
|
+
parent_event_id: Optional[str] = None,
|
|
267
|
+
**kwargs,
|
|
268
|
+
) -> str:
|
|
269
|
+
"""Create an error traceback event (async version).
|
|
270
|
+
|
|
271
|
+
See create_error() for full documentation.
|
|
272
|
+
"""
|
|
273
|
+
import traceback as tb
|
|
274
|
+
|
|
275
|
+
if isinstance(error, Exception):
|
|
276
|
+
error_str = str(error)
|
|
277
|
+
traceback_str = tb.format_exc()
|
|
278
|
+
else:
|
|
279
|
+
error_str = str(error)
|
|
280
|
+
traceback_str = kwargs.pop("traceback", "")
|
|
281
|
+
|
|
282
|
+
return await self.acreate(
|
|
283
|
+
type="error_traceback",
|
|
284
|
+
error=error_str,
|
|
285
|
+
traceback=traceback_str,
|
|
286
|
+
parent_event_id=parent_event_id,
|
|
287
|
+
**kwargs,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# ==================== Low-Level HTTP Methods ====================
|
|
291
|
+
|
|
292
|
+
def create_event(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
293
|
+
"""Create a new event via API.
|
|
294
|
+
|
|
22
295
|
Args:
|
|
23
296
|
params: Event parameters including:
|
|
24
297
|
- client_event_id: Client-generated event ID
|
|
@@ -27,36 +300,81 @@ class EventResource:
|
|
|
27
300
|
- occurred_at: When the event occurred
|
|
28
301
|
- payload: Event payload
|
|
29
302
|
- etc.
|
|
30
|
-
|
|
303
|
+
|
|
31
304
|
Returns:
|
|
32
305
|
API response with optional blob_url for large payloads
|
|
33
306
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
307
|
+
event_id = params.get("client_event_id")
|
|
308
|
+
session_id = params.get("session_id")
|
|
309
|
+
event_type = params.get("type")
|
|
310
|
+
parent_id = params.get("client_parent_event_id")
|
|
311
|
+
logger.debug(
|
|
312
|
+
f"[Event] create_event() called - "
|
|
313
|
+
f"event_id={_truncate_id(event_id)}, session_id={_truncate_id(session_id)}, "
|
|
314
|
+
f"type={event_type!r}, parent_id={_truncate_id(parent_id)}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
response = self.http.post("events", params)
|
|
318
|
+
|
|
319
|
+
resp_event_id = response.get("event_id") if response else None
|
|
320
|
+
logger.debug(
|
|
321
|
+
f"[Event] create_event() response - "
|
|
322
|
+
f"event_id={_truncate_id(resp_event_id)}, response_keys={list(response.keys()) if response else 'None'}"
|
|
323
|
+
)
|
|
324
|
+
return response
|
|
325
|
+
|
|
326
|
+
async def acreate_event(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
327
|
+
"""Create a new event via API (asynchronous).
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
params: Event parameters
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
API response with optional blob_url for large payloads
|
|
334
|
+
"""
|
|
335
|
+
event_id = params.get("client_event_id")
|
|
336
|
+
session_id = params.get("session_id")
|
|
337
|
+
event_type = params.get("type")
|
|
338
|
+
parent_id = params.get("client_parent_event_id")
|
|
339
|
+
logger.debug(
|
|
340
|
+
f"[Event] acreate_event() called - "
|
|
341
|
+
f"event_id={_truncate_id(event_id)}, session_id={_truncate_id(session_id)}, "
|
|
342
|
+
f"type={event_type!r}, parent_id={_truncate_id(parent_id)}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
response = await self.http.apost("events", params)
|
|
346
|
+
|
|
347
|
+
resp_event_id = response.get("event_id") if response else None
|
|
348
|
+
logger.debug(
|
|
349
|
+
f"[Event] acreate_event() response - "
|
|
350
|
+
f"event_id={_truncate_id(resp_event_id)}, response_keys={list(response.keys()) if response else 'None'}"
|
|
351
|
+
)
|
|
352
|
+
return response
|
|
353
|
+
|
|
354
|
+
def get(self, event_id: str) -> Dict[str, Any]:
|
|
37
355
|
"""Get an event by ID.
|
|
38
|
-
|
|
356
|
+
|
|
39
357
|
Args:
|
|
40
358
|
event_id: Event ID
|
|
41
|
-
|
|
359
|
+
|
|
42
360
|
Returns:
|
|
43
361
|
Event data
|
|
44
362
|
"""
|
|
45
363
|
return self.http.get(f"events/{event_id}")
|
|
46
|
-
|
|
47
|
-
def
|
|
364
|
+
|
|
365
|
+
def update(self, event_id: str, **updates) -> Dict[str, Any]:
|
|
48
366
|
"""Update an existing event.
|
|
49
|
-
|
|
367
|
+
|
|
50
368
|
Args:
|
|
51
369
|
event_id: Event ID
|
|
52
|
-
updates: Fields to update
|
|
53
|
-
|
|
370
|
+
**updates: Fields to update
|
|
371
|
+
|
|
54
372
|
Returns:
|
|
55
373
|
Updated event data
|
|
56
374
|
"""
|
|
57
375
|
return self.http.put(f"events/{event_id}", updates)
|
|
58
|
-
|
|
59
|
-
def
|
|
376
|
+
|
|
377
|
+
def list(
|
|
60
378
|
self,
|
|
61
379
|
session_id: Optional[str] = None,
|
|
62
380
|
event_type: Optional[str] = None,
|
|
@@ -64,25 +382,79 @@ class EventResource:
|
|
|
64
382
|
offset: int = 0
|
|
65
383
|
) -> Dict[str, Any]:
|
|
66
384
|
"""List events with optional filters.
|
|
67
|
-
|
|
385
|
+
|
|
68
386
|
Args:
|
|
69
387
|
session_id: Filter by session ID
|
|
70
388
|
event_type: Filter by event type
|
|
71
389
|
limit: Maximum number of events to return
|
|
72
390
|
offset: Pagination offset
|
|
73
|
-
|
|
391
|
+
|
|
74
392
|
Returns:
|
|
75
393
|
List of events and pagination info
|
|
76
394
|
"""
|
|
77
|
-
params = {
|
|
395
|
+
params: Dict[str, Any] = {
|
|
78
396
|
"limit": limit,
|
|
79
397
|
"offset": offset
|
|
80
398
|
}
|
|
81
|
-
|
|
399
|
+
|
|
82
400
|
if session_id:
|
|
83
401
|
params["session_id"] = session_id
|
|
84
|
-
|
|
402
|
+
|
|
85
403
|
if event_type:
|
|
86
404
|
params["type"] = event_type
|
|
87
|
-
|
|
88
|
-
return self.http.get("events", params)
|
|
405
|
+
|
|
406
|
+
return self.http.get("events", params)
|
|
407
|
+
|
|
408
|
+
async def aget(self, event_id: str) -> Dict[str, Any]:
|
|
409
|
+
"""Get an event by ID (asynchronous).
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
event_id: Event ID
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Event data
|
|
416
|
+
"""
|
|
417
|
+
return await self.http.aget(f"events/{event_id}")
|
|
418
|
+
|
|
419
|
+
async def aupdate(self, event_id: str, **updates) -> Dict[str, Any]:
|
|
420
|
+
"""Update an existing event (asynchronous).
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
event_id: Event ID
|
|
424
|
+
**updates: Fields to update
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Updated event data
|
|
428
|
+
"""
|
|
429
|
+
return await self.http.aput(f"events/{event_id}", updates)
|
|
430
|
+
|
|
431
|
+
async def alist(
|
|
432
|
+
self,
|
|
433
|
+
session_id: Optional[str] = None,
|
|
434
|
+
event_type: Optional[str] = None,
|
|
435
|
+
limit: int = 100,
|
|
436
|
+
offset: int = 0
|
|
437
|
+
) -> Dict[str, Any]:
|
|
438
|
+
"""List events with optional filters (asynchronous).
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
session_id: Filter by session ID
|
|
442
|
+
event_type: Filter by event type
|
|
443
|
+
limit: Maximum number of events to return
|
|
444
|
+
offset: Pagination offset
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
List of events and pagination info
|
|
448
|
+
"""
|
|
449
|
+
params: Dict[str, Any] = {
|
|
450
|
+
"limit": limit,
|
|
451
|
+
"offset": offset
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if session_id:
|
|
455
|
+
params["session_id"] = session_id
|
|
456
|
+
|
|
457
|
+
if event_type:
|
|
458
|
+
params["type"] = event_type
|
|
459
|
+
|
|
460
|
+
return await self.http.aget("events", params)
|