@@ -1122,6 +1122,34 @@ def _run_transform(
1122
1122
1123
1123
return run_create
1124
1124
1125
+ def _feedback_transform (
1126
+ self ,
1127
+ feedback : Union [ls_schemas .Feedback , dict ],
1128
+ ) -> dict :
1129
+ """Transform the given feedback object into a dictionary representation.
1130
+
1131
+ Args:
1132
+ feedback (Union[ls_schemas.Feedback, dict]): The feedback object to transform.
1133
+ update (bool, optional): Whether the payload is for an "update" event.
1134
+ copy (bool, optional): Whether to deepcopy feedback inputs/outputs.
1135
+ attachments_collector (Optional[dict[str, ls_schemas.Attachments]]):
1136
+ A dictionary to collect attachments. If not passed, attachments
1137
+ will be dropped.
1138
+
1139
+ Returns:
1140
+ dict: The transformed feedback object as a dictionary.
1141
+ """
1142
+ if hasattr (feedback , "dict" ) and callable (getattr (feedback , "dict" )):
1143
+ feedback_create : dict = feedback .dict () # type: ignore
1144
+ else :
1145
+ feedback_create = cast (dict , feedback )
1146
+ if "id" not in feedback_create :
1147
+ feedback_create ["id" ] = uuid .uuid4 ()
1148
+ elif isinstance (feedback_create ["id" ], str ):
1149
+ feedback_create ["id" ] = uuid .UUID (feedback_create ["id" ])
1150
+
1151
+ return feedback_create
1152
+
1125
1153
@staticmethod
1126
1154
def _insert_runtime_env (runs : Sequence [dict ]) -> None :
1127
1155
runtime_env = ls_env .get_runtime_environment ()
@@ -1408,14 +1436,15 @@ def _post_batch_ingest_runs(self, body: bytes, *, _context: str):
1408
1436
except Exception :
1409
1437
logger .warning (f"Failed to batch ingest runs: { repr (e )} " )
1410
1438
1411
- def multipart_ingest_runs (
1439
+ def multipart_ingest (
1412
1440
self ,
1413
1441
create : Optional [
1414
1442
Sequence [Union [ls_schemas .Run , ls_schemas .RunLikeDict , Dict ]]
1415
1443
] = None ,
1416
1444
update : Optional [
1417
1445
Sequence [Union [ls_schemas .Run , ls_schemas .RunLikeDict , Dict ]]
1418
1446
] = None ,
1447
+ feedback : Optional [Sequence [Union [ls_schemas .Feedback , Dict ]]] = None ,
1419
1448
* ,
1420
1449
pre_sampled : bool = False ,
1421
1450
) -> None :
@@ -1442,7 +1471,7 @@ def multipart_ingest_runs(
1442
1471
- The run objects MUST contain the dotted_order and trace_id fields
1443
1472
to be accepted by the API.
1444
1473
"""
1445
- if not create and not update :
1474
+ if not ( create or update or feedback ) :
1446
1475
return
1447
1476
# transform and convert to dicts
1448
1477
all_attachments : Dict [str , ls_schemas .Attachments ] = {}
@@ -1454,6 +1483,7 @@ def multipart_ingest_runs(
1454
1483
self ._run_transform (run , update = True , attachments_collector = all_attachments )
1455
1484
for run in update or EMPTY_SEQ
1456
1485
]
1486
+ feedback_dicts = [self ._feedback_transform (f ) for f in feedback or EMPTY_SEQ ]
1457
1487
# require trace_id and dotted_order
1458
1488
if create_dicts :
1459
1489
for run in create_dicts :
@@ -1491,21 +1521,26 @@ def multipart_ingest_runs(
1491
1521
if not pre_sampled :
1492
1522
create_dicts = self ._filter_for_sampling (create_dicts )
1493
1523
update_dicts = self ._filter_for_sampling (update_dicts , patch = True )
1494
- if not create_dicts and not update_dicts :
1524
+ if not create_dicts and not update_dicts and not feedback_dicts :
1495
1525
return
1496
1526
# insert runtime environment
1497
1527
self ._insert_runtime_env (create_dicts )
1498
1528
self ._insert_runtime_env (update_dicts )
1499
1529
# send the runs in multipart requests
1500
1530
acc_context : List [str ] = []
1501
1531
acc_parts : MultipartParts = []
1502
- for event , payloads in (("post" , create_dicts ), ("patch" , update_dicts )):
1532
+ for event , payloads in (
1533
+ ("post" , create_dicts ),
1534
+ ("patch" , update_dicts ),
1535
+ ("feedback" , feedback_dicts ),
1536
+ ):
1503
1537
for payload in payloads :
1504
1538
# collect fields to be sent as separate parts
1505
1539
fields = [
1506
1540
("inputs" , payload .pop ("inputs" , None )),
1507
1541
("outputs" , payload .pop ("outputs" , None )),
1508
1542
("events" , payload .pop ("events" , None )),
1543
+ ("feedback" , payload .pop ("feedback" , None )),
1509
1544
]
1510
1545
# encode the main run payload
1511
1546
payloadb = _dumps_json (payload )
@@ -4115,6 +4150,7 @@ def _submit_feedback(**kwargs):
4115
4150
),
4116
4151
feedback_source_type = ls_schemas .FeedbackSourceType .MODEL ,
4117
4152
project_id = project_id ,
4153
+ trace_id = run .trace_id if run else None ,
4118
4154
)
4119
4155
return results
4120
4156
@@ -4185,6 +4221,7 @@ def create_feedback(
4185
4221
project_id : Optional [ID_TYPE ] = None ,
4186
4222
comparative_experiment_id : Optional [ID_TYPE ] = None ,
4187
4223
feedback_group_id : Optional [ID_TYPE ] = None ,
4224
+ trace_id : Optional [ID_TYPE ] = None ,
4188
4225
** kwargs : Any ,
4189
4226
) -> ls_schemas .Feedback :
4190
4227
"""Create a feedback in the LangSmith API.
@@ -4194,6 +4231,8 @@ def create_feedback(
4194
4231
run_id : str or UUID
4195
4232
The ID of the run to provide feedback for. Either the run_id OR
4196
4233
the project_id must be provided.
4234
+ trace_id : str or UUID
4235
+ The trace ID of the run to provide feedback for. This is optional.
4197
4236
key : str
4198
4237
The name of the metric or 'aspect' this feedback is about.
4199
4238
score : float or int or bool or None, default=None
@@ -4241,66 +4280,87 @@ def create_feedback(
4241
4280
f" endpoint: { sorted (kwargs )} " ,
4242
4281
DeprecationWarning ,
4243
4282
)
4244
- if not isinstance (feedback_source_type , ls_schemas .FeedbackSourceType ):
4245
- feedback_source_type = ls_schemas .FeedbackSourceType (feedback_source_type )
4246
- if feedback_source_type == ls_schemas .FeedbackSourceType .API :
4247
- feedback_source : ls_schemas .FeedbackSourceBase = (
4248
- ls_schemas .APIFeedbackSource (metadata = source_info )
4283
+ try :
4284
+ if not isinstance (feedback_source_type , ls_schemas .FeedbackSourceType ):
4285
+ feedback_source_type = ls_schemas .FeedbackSourceType (
4286
+ feedback_source_type
4287
+ )
4288
+ if feedback_source_type == ls_schemas .FeedbackSourceType .API :
4289
+ feedback_source : ls_schemas .FeedbackSourceBase = (
4290
+ ls_schemas .APIFeedbackSource (metadata = source_info )
4291
+ )
4292
+ elif feedback_source_type == ls_schemas .FeedbackSourceType .MODEL :
4293
+ feedback_source = ls_schemas .ModelFeedbackSource (metadata = source_info )
4294
+ else :
4295
+ raise ValueError (f"Unknown feedback source type { feedback_source_type } " )
4296
+ feedback_source .metadata = (
4297
+ feedback_source .metadata if feedback_source .metadata is not None else {}
4249
4298
)
4250
- elif feedback_source_type == ls_schemas .FeedbackSourceType .MODEL :
4251
- feedback_source = ls_schemas .ModelFeedbackSource (metadata = source_info )
4252
- else :
4253
- raise ValueError (f"Unknown feedback source type { feedback_source_type } " )
4254
- feedback_source .metadata = (
4255
- feedback_source .metadata if feedback_source .metadata is not None else {}
4256
- )
4257
- if source_run_id is not None and "__run" not in feedback_source .metadata :
4258
- feedback_source .metadata ["__run" ] = {"run_id" : str (source_run_id )}
4259
- if feedback_source .metadata and "__run" in feedback_source .metadata :
4260
- # Validate that the linked run ID is a valid UUID
4261
- # Run info may be a base model or dict.
4262
- _run_meta : Union [dict , Any ] = feedback_source .metadata ["__run" ]
4263
- if hasattr (_run_meta , "dict" ) and callable (_run_meta ):
4264
- _run_meta = _run_meta .dict ()
4265
- if "run_id" in _run_meta :
4266
- _run_meta ["run_id" ] = str (
4267
- _as_uuid (
4268
- feedback_source .metadata ["__run" ]["run_id" ],
4269
- "feedback_source.metadata['__run']['run_id']" ,
4299
+ if source_run_id is not None and "__run" not in feedback_source .metadata :
4300
+ feedback_source .metadata ["__run" ] = {"run_id" : str (source_run_id )}
4301
+ if feedback_source .metadata and "__run" in feedback_source .metadata :
4302
+ # Validate that the linked run ID is a valid UUID
4303
+ # Run info may be a base model or dict.
4304
+ _run_meta : Union [dict , Any ] = feedback_source .metadata ["__run" ]
4305
+ if hasattr (_run_meta , "dict" ) and callable (_run_meta ):
4306
+ _run_meta = _run_meta .dict ()
4307
+ if "run_id" in _run_meta :
4308
+ _run_meta ["run_id" ] = str (
4309
+ _as_uuid (
4310
+ feedback_source .metadata ["__run" ]["run_id" ],
4311
+ "feedback_source.metadata['__run']['run_id']" ,
4312
+ )
4270
4313
)
4314
+ feedback_source .metadata ["__run" ] = _run_meta
4315
+ feedback = ls_schemas .FeedbackCreate (
4316
+ id = _ensure_uuid (feedback_id ),
4317
+ # If run_id is None, this is interpreted as session-level
4318
+ # feedback.
4319
+ run_id = _ensure_uuid (run_id , accept_null = True ),
4320
+ trace_id = _ensure_uuid (trace_id , accept_null = True ),
4321
+ key = key ,
4322
+ score = score ,
4323
+ value = value ,
4324
+ correction = correction ,
4325
+ comment = comment ,
4326
+ feedback_source = feedback_source ,
4327
+ created_at = datetime .datetime .now (datetime .timezone .utc ),
4328
+ modified_at = datetime .datetime .now (datetime .timezone .utc ),
4329
+ feedback_config = feedback_config ,
4330
+ session_id = _ensure_uuid (project_id , accept_null = True ),
4331
+ comparative_experiment_id = _ensure_uuid (
4332
+ comparative_experiment_id , accept_null = True
4333
+ ),
4334
+ feedback_group_id = _ensure_uuid (feedback_group_id , accept_null = True ),
4335
+ )
4336
+
4337
+ feedback_block = _dumps_json (feedback .dict (exclude_none = True ))
4338
+ use_multipart = (self .info .batch_ingest_config or {}).get (
4339
+ "use_multipart_endpoint" , False
4340
+ )
4341
+
4342
+ if (
4343
+ use_multipart
4344
+ and self .tracing_queue is not None
4345
+ and feedback .trace_id is not None
4346
+ ):
4347
+ self .tracing_queue .put (
4348
+ TracingQueueItem (str (feedback .id ), "feedback" , feedback )
4271
4349
)
4272
- feedback_source .metadata ["__run" ] = _run_meta
4273
- feedback = ls_schemas .FeedbackCreate (
4274
- id = _ensure_uuid (feedback_id ),
4275
- # If run_id is None, this is interpreted as session-level
4276
- # feedback.
4277
- run_id = _ensure_uuid (run_id , accept_null = True ),
4278
- key = key ,
4279
- score = score ,
4280
- value = value ,
4281
- correction = correction ,
4282
- comment = comment ,
4283
- feedback_source = feedback_source ,
4284
- created_at = datetime .datetime .now (datetime .timezone .utc ),
4285
- modified_at = datetime .datetime .now (datetime .timezone .utc ),
4286
- feedback_config = feedback_config ,
4287
- session_id = _ensure_uuid (project_id , accept_null = True ),
4288
- comparative_experiment_id = _ensure_uuid (
4289
- comparative_experiment_id , accept_null = True
4290
- ),
4291
- feedback_group_id = _ensure_uuid (feedback_group_id , accept_null = True ),
4292
- )
4293
- feedback_block = _dumps_json (feedback .dict (exclude_none = True ))
4294
- self .request_with_retries (
4295
- "POST" ,
4296
- "/feedback" ,
4297
- request_kwargs = {
4298
- "data" : feedback_block ,
4299
- },
4300
- stop_after_attempt = stop_after_attempt ,
4301
- retry_on = (ls_utils .LangSmithNotFoundError ,),
4302
- )
4303
- return ls_schemas .Feedback (** feedback .dict ())
4350
+ else :
4351
+ self .request_with_retries (
4352
+ "POST" ,
4353
+ "/feedback" ,
4354
+ request_kwargs = {
4355
+ "data" : feedback_block ,
4356
+ },
4357
+ stop_after_attempt = stop_after_attempt ,
4358
+ retry_on = (ls_utils .LangSmithNotFoundError ,),
4359
+ )
4360
+ return ls_schemas .Feedback (** feedback .dict ())
4361
+ except Exception as e :
4362
+ logger .error ("Error creating feedback" , exc_info = True )
4363
+ raise e
4304
4364
4305
4365
def update_feedback (
4306
4366
self ,
0 commit comments