lucidicai 3.0.0__py3-none-any.whl → 3.1.1__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 CHANGED
@@ -35,7 +35,7 @@ from .core.errors import (
35
35
  )
36
36
 
37
37
  # Version
38
- __version__ = "3.0.0"
38
+ __version__ = "3.1.1"
39
39
 
40
40
  # All exports
41
41
  __all__ = [
@@ -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/client.py CHANGED
@@ -30,6 +30,7 @@ from .api.resources.dataset import DatasetResource
30
30
  from .api.resources.experiment import ExperimentResource
31
31
  from .api.resources.prompt import PromptResource
32
32
  from .api.resources.feature_flag import FeatureFlagResource
33
+ from .api.resources.evals import EvalsResource
33
34
  from .core.config import SDKConfig
34
35
  from .core.errors import LucidicError
35
36
  from .session_obj import Session
@@ -145,6 +146,7 @@ class LucidicAI:
145
146
  "experiments": ExperimentResource(self._http, self._config.agent_id, self._production),
146
147
  "prompts": PromptResource(self._http, self._production),
147
148
  "feature_flags": FeatureFlagResource(self._http, self._config.agent_id, self._production),
149
+ "evals": EvalsResource(self._http, self._production),
148
150
  }
149
151
 
150
152
  # Active sessions for this client
@@ -271,6 +273,17 @@ class LucidicAI:
271
273
  """
272
274
  return self._resources["datasets"]
273
275
 
276
+ @property
277
+ def evals(self) -> EvalsResource:
278
+ """Access evals resource for submitting evaluation results.
279
+
280
+ Example:
281
+ client.evals.emit(result=True, name="task_success")
282
+ client.evals.emit(result=0.95, name="accuracy")
283
+ client.evals.emit(result="excellent", name="quality")
284
+ """
285
+ return self._resources["evals"]
286
+
274
287
  # ==================== Decorators ====================
275
288
 
276
289
  def event(
@@ -51,7 +51,7 @@ def _emit_event_to_client(
51
51
  "session_id": session_id,
52
52
  **event_data,
53
53
  }
54
- response = client._resources["events"].create_event(event_payload)
54
+ response = client._resources["events"].create(**event_payload)
55
55
  return response.get("event_id") if response else None
56
56
  except Exception as e:
57
57
  debug(f"[Decorator] Failed to emit event: {e}")
@@ -81,7 +81,7 @@ async def _aemit_event_to_client(
81
81
  "session_id": session_id,
82
82
  **event_data,
83
83
  }
84
- response = await client._resources["events"].acreate_event(event_payload)
84
+ response = await client._resources["events"].acreate(**event_payload)
85
85
  return response.get("event_id") if response else None
86
86
  except Exception as e:
87
87
  debug(f"[Decorator] Failed to emit async event: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lucidicai
3
- Version: 3.0.0
3
+ Version: 3.1.1
4
4
  Summary: Lucidic AI Python SDK
5
5
  Author: Andy Liang
6
6
  Author-email: andy@lucidic.ai
@@ -1,6 +1,6 @@
1
- lucidicai/__init__.py,sha256=2tzRM0UYAxeeYzqqMrxnKyINWqKtxg3gbX6beieIeFs,1180
1
+ lucidicai/__init__.py,sha256=83k3iIyPlLhhZ_fRyy54x4Ry9WbRhNTK1bczhvHSj_A,1180
2
2
  lucidicai/action.py,sha256=sPRd1hTIVXDqnvG9ZXWEipUFh0bsXcE0Fm7RVqmVccM,237
3
- lucidicai/client.py,sha256=XxJ4NYfSi1mlgLC9XjLL9TmwpXpgvKMLzi7zzZazuRU,14180
3
+ lucidicai/client.py,sha256=WnkUeo_Z0uP4xh66gNC6MJhYdyhRpjC61OBjHEJLHq4,14674
4
4
  lucidicai/constants.py,sha256=zN8O7TjoRHRlaGa9CZUWppS73rhzKGwaEkF9XMTV0Cg,1160
5
5
  lucidicai/context.py,sha256=ruEXAndSv0gQ-YEXLlC4Fx6NNbaylfp_dZxbpwmLZSA,4622
6
6
  lucidicai/dataset.py,sha256=wu25X02JyWkht_yQabgQpGZFfzbNTxG6tf5k9ol8Amo,4005
@@ -22,6 +22,7 @@ lucidicai/api/__init__.py,sha256=UOYuFZupG0TgzMAxbLNgpodDXhDRXBgMva8ZblgBN9Y,31
22
22
  lucidicai/api/client.py,sha256=0Ia5cXo5OKifhP-63TPAWPuy2bWCzR9VN4BpWIMo34w,13212
23
23
  lucidicai/api/resources/__init__.py,sha256=DDgviDW3Su-G1ofkZGlaJMc2pzYJqrbBnEruNg1whCM,416
24
24
  lucidicai/api/resources/dataset.py,sha256=I6g9ah4vaqEH1jyeouBn7xvC0oAuDNPeyl-bmtNj-T0,17400
25
+ lucidicai/api/resources/evals.py,sha256=_3nLE6dMLht844mWw7kl_hctjv5JIuC6MP06YWUgLnI,7235
25
26
  lucidicai/api/resources/event.py,sha256=GTIU5sIbLNTWAHk4rB120xWTRkhnraz9JNfamEygyNo,14267
26
27
  lucidicai/api/resources/experiment.py,sha256=fOIKJ5d89bHJBVZ3wjbhY_6XF3kLHz9TE3BVPA5pNpA,3563
27
28
  lucidicai/api/resources/feature_flag.py,sha256=ii412DIkZCEAhrXdGydcpQKveqGlFq4NlgdmWQnU83c,2259
@@ -51,7 +52,7 @@ lucidicai/providers/universal_image_interceptor.py,sha256=7d-hw4xihRwvvA1AP8-vqY
51
52
  lucidicai/sdk/__init__.py,sha256=UrkV9FYbZkBxaX9qwxGbCJdXp-JqMpn0_u-huO9Y-ec,32
52
53
  lucidicai/sdk/bound_decorators.py,sha256=SzmNZwORhArXL9D8T8BpPltT-jQ-tVpy71t8bJWOIU0,12151
53
54
  lucidicai/sdk/context.py,sha256=y58_C9JlBML_xFPUbmAn6WuxsnM03bECJ2pKBWz0TuQ,10386
54
- lucidicai/sdk/decorators.py,sha256=-MBMCn-vED2x3ioWD3MYo0_FfMdr6LbfpekURH7Cpvw,15660
55
+ lucidicai/sdk/decorators.py,sha256=FV0259ki6x26d2-YKZJwkUNzHain-QGghq94OzAk2Os,15652
55
56
  lucidicai/sdk/error_boundary.py,sha256=IPr5wS9rS7ZQNgEaBwK53UaixAm6L2rijKKFfxcxjUI,9190
56
57
  lucidicai/sdk/event.py,sha256=hpBJfqKteOuQKoZfhxQfbeVOrdmR8wCcQc8P6658VRo,22658
57
58
  lucidicai/sdk/event_builder.py,sha256=Z376RKlStM7IBcAm5LKgTDh3x_fjmcvkWltUrjZ6RAc,10304
@@ -90,7 +91,7 @@ lucidicai/utils/images.py,sha256=z8mlIKgFfrIbuk-l4L2rB62uw_uPO79sHPXPY7eLu2A,128
90
91
  lucidicai/utils/logger.py,sha256=R3B3gSee64F6UVHUrShihBq_O7W7bgfrBiVDXTO3Isg,4777
91
92
  lucidicai/utils/queue.py,sha256=8DQwnGw7pINEJ0dNSkB0PhdPW-iBQQ-YZg23poe4umE,17323
92
93
  lucidicai/utils/serialization.py,sha256=KdOREZd7XBxFBAZ86DePMfYPzSVyKr4RcgUa82aFxrs,820
93
- lucidicai-3.0.0.dist-info/METADATA,sha256=KY6XOzFlxTySIfjrzQvPa3SI50ecJp0_86qEq-Or4xw,902
94
- lucidicai-3.0.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
95
- lucidicai-3.0.0.dist-info/top_level.txt,sha256=vSSdM3lclF4I5tyVC0xxUk8eIRnnYXMe1hW-eO91HUo,10
96
- lucidicai-3.0.0.dist-info/RECORD,,
94
+ lucidicai-3.1.1.dist-info/METADATA,sha256=OCfPUqhEyRmBCQWTOoKUwbw3hR7T6JNZY9XtxyUbXxE,902
95
+ lucidicai-3.1.1.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
96
+ lucidicai-3.1.1.dist-info/top_level.txt,sha256=vSSdM3lclF4I5tyVC0xxUk8eIRnnYXMe1hW-eO91HUo,10
97
+ lucidicai-3.1.1.dist-info/RECORD,,