@@ -82,6 +82,29 @@ def _to_period_index_if_possible(obj: Any) -> Any:
8282 return obj
8383
8484
85+ # Max forecast rows returned inline before truncation (NB-22). Normal horizons
86+ # (<= a few dozen) are never affected; a 1000-step forecast would otherwise
87+ # flood the client with ~30KB+ of inline JSON.
88+ _MAX_PREDICTION_ROWS = 500
89+
90+
91+ def _cap_prediction_rows (result : dict ) -> tuple [dict , dict | None ]:
92+ """Cap an index-keyed prediction dict, returning (capped, truncation_note)."""
93+ if not isinstance (result , dict ) or len (result ) <= _MAX_PREDICTION_ROWS :
94+ return result , None
95+ total = len (result )
96+ kept = dict (list (result .items ())[:_MAX_PREDICTION_ROWS ])
97+ note = {
98+ "shown" : _MAX_PREDICTION_ROWS ,
99+ "total" : total ,
100+ "note" : (
101+ "forecast truncated; request a smaller horizon or use save_data to write "
102+ "the full series to a file"
103+ ),
104+ }
105+ return kept , note
106+
107+
85108def _get_index_frequency_metadata (
86109 index : pd .Index ,
87110 fallback : str | None = None ,
@@ -492,6 +515,7 @@ def predict(
492515 elif obj_type in ("transformer" , "clusterer" ):
493516 is_transformer = True
494517
518+ dropped_y_warning = None
495519 try :
496520 if fh is None and not (is_classifier_or_regressor or is_transformer ):
497521 fh = list (range (1 , 13 ))
@@ -500,7 +524,21 @@ def predict(
500524 if X is not None :
501525 kwargs ["X" ] = X
502526 if y is not None :
503- kwargs ["y" ] = y
527+ # y at predict is only for annotators; forwarding it to a
528+ # forecaster raised a raw "unexpected keyword argument 'y'"
529+ # TypeError (NB-18). Only pass it when predict accepts it.
530+ accepts_y = False
531+ try :
532+ accepts_y = "y" in inspect .signature (instance .predict ).parameters
533+ except (ValueError , TypeError ):
534+ accepts_y = False
535+ if accepts_y :
536+ kwargs ["y" ] = y
537+ else :
538+ dropped_y_warning = (
539+ f"y was ignored: { obj_type or 'this estimator' } .predict() does not "
540+ "accept y (it is only used by annotators/detectors)."
541+ )
504542
505543 if is_classifier_or_regressor :
506544 # Classifiers take X in predict (X is the feature matrix)
@@ -542,20 +580,25 @@ def predict(
542580
543581 from sktime_mcp .server import sanitize_for_json
544582
583+ truncated_note = None
545584 if isinstance (predictions , pd .Series ):
546585 predictions_copy = predictions .copy ()
547586 predictions_copy .index = predictions_copy .index .astype (str )
548- result = predictions_copy .to_dict ()
587+ result , truncated_note = _cap_prediction_rows ( predictions_copy .to_dict () )
549588 elif isinstance (predictions , pd .DataFrame ):
550589 predictions_copy = predictions .copy ()
551590 predictions_copy .index = predictions_copy .index .astype (str )
552- # Need to handle multiindex columns if they exist (like in predict_interval)
591+ # Flatten multiindex columns ( predict_interval/quantiles) for JSON.
553592 if isinstance (predictions_copy .columns , pd .MultiIndex ):
554- # Flatten multiindex for JSON serialization
555593 predictions_copy .columns = [
556594 "_" .join (map (str , col )) for col in predictions_copy .columns .values
557595 ]
558- result = predictions_copy .to_dict (orient = "list" )
596+ # orient="index" keeps the time index as the key so interval /
597+ # variance values map to time points, consistent with predict
598+ # (NB-21). orient="list" dropped the index entirely.
599+ result , truncated_note = _cap_prediction_rows (
600+ predictions_copy .to_dict (orient = "index" )
601+ )
559602 else :
560603 result = sanitize_for_json (predictions )
561604
@@ -574,6 +617,10 @@ def predict(
574617 out ["alpha" ] = alpha
575618 else :
576619 out ["predictions" ] = result
620+ if truncated_note :
621+ out ["predictions_truncated" ] = truncated_note
622+ if dropped_y_warning :
623+ out ["warnings" ] = [dropped_y_warning ]
577624 return out
578625 except Exception as e :
579626 return {"success" : False , "error" : str (e )}
0 commit comments