1313from ldai import log
1414
1515if TYPE_CHECKING :
16- from ldai .providers .types import LDAIMetrics
16+ from ldai .providers .types import GraphMetrics , GraphMetricSummary , LDAIMetrics
1717
1818
1919class FeedbackKind (Enum ):
@@ -616,7 +616,11 @@ def _openai_to_token_usage(data: dict) -> TokenUsage:
616616
617617class AIGraphTracker :
618618 """
619- Tracks graph-level, node-level, and edge-level metrics for AI agent graph operations.
619+ Tracks graph-level metrics for AI agent graph operations.
620+
621+ Maintains an internal :class:`~ldai.providers.types.GraphMetricSummary`
622+ that is updated as tracking methods are called. Retrieve it via
623+ :meth:`get_summary`.
620624 """
621625
622626 def __init__ (
@@ -642,11 +646,22 @@ def __init__(
642646 self ._version = version
643647 self ._context = context
644648
649+ from ldai .providers .types import GraphMetricSummary
650+ self ._summary = GraphMetricSummary ()
651+
645652 @property
646653 def graph_key (self ) -> str :
647654 """Graph configuration key used in tracking payloads."""
648655 return self ._graph_key
649656
657+ def get_summary (self ) -> GraphMetricSummary :
658+ """
659+ Get the current summary of graph-level metrics.
660+
661+ :return: Summary of graph metrics tracked so far.
662+ """
663+ return self ._summary
664+
650665 def __get_track_data (self ):
651666 """
652667 Get tracking data for events.
@@ -664,6 +679,10 @@ def track_invocation_success(self) -> None:
664679 """
665680 Track a successful graph invocation.
666681 """
682+ if self ._summary .success is not None :
683+ log .warning ("Invocation status has already been tracked for this graph execution. %s" , self .__get_track_data ())
684+ return
685+ self ._summary .success = True
667686 self ._ld_client .track (
668687 "$ld:ai:graph:invocation_success" ,
669688 self ._context ,
@@ -675,6 +694,10 @@ def track_invocation_failure(self) -> None:
675694 """
676695 Track an unsuccessful graph invocation.
677696 """
697+ if self ._summary .success is not None :
698+ log .warning ("Invocation status has already been tracked for this graph execution. %s" , self .__get_track_data ())
699+ return
700+ self ._summary .success = False
678701 self ._ld_client .track (
679702 "$ld:ai:graph:invocation_failure" ,
680703 self ._context ,
@@ -688,6 +711,10 @@ def track_duration(self, duration: int) -> None:
688711
689712 :param duration: Duration in milliseconds.
690713 """
714+ if self ._summary .duration_ms is not None :
715+ log .warning ("Duration has already been tracked for this graph execution. %s" , self .__get_track_data ())
716+ return
717+ self ._summary .duration_ms = duration
691718 self ._ld_client .track (
692719 "$ld:ai:graph:duration:total" ,
693720 self ._context ,
@@ -703,6 +730,10 @@ def track_total_tokens(self, tokens: Optional[TokenUsage] = None) -> None:
703730 """
704731 if tokens is None or tokens .total <= 0 :
705732 return
733+ if self ._summary .usage is not None :
734+ log .warning ("Token usage has already been tracked for this graph execution. %s" , self .__get_track_data ())
735+ return
736+ self ._summary .usage = tokens
706737 self ._ld_client .track (
707738 "$ld:ai:graph:total_tokens" ,
708739 self ._context ,
@@ -716,6 +747,10 @@ def track_path(self, path: List[str]) -> None:
716747
717748 :param path: An array of configuration keys representing the sequence of nodes executed during graph traversal.
718749 """
750+ if self ._summary .path :
751+ log .warning ("Path has already been tracked for this graph execution. %s" , self .__get_track_data ())
752+ return
753+ self ._summary .path = list (path )
719754 track_data = {** self .__get_track_data (), "path" : path }
720755 self ._ld_client .track (
721756 "$ld:ai:graph:path" ,
@@ -780,3 +815,92 @@ def track_handoff_failure(self, source_key: str, target_key: str) -> None:
780815 track_data ,
781816 1 ,
782817 )
818+
819+ def _track_from_graph_metrics (
820+ self ,
821+ result : Any ,
822+ metrics_extractor : Callable [[Any ], Optional [GraphMetrics ]],
823+ elapsed_ms : int ,
824+ ) -> None :
825+ metrics : Optional [GraphMetrics ] = None
826+ try :
827+ metrics = metrics_extractor (result )
828+ except Exception as exc :
829+ log .warning ("Failed to extract graph metrics: %s" , exc )
830+
831+ if metrics is None :
832+ self .track_duration (elapsed_ms )
833+ return
834+
835+ self .track_duration (metrics .duration_ms if metrics .duration_ms is not None else elapsed_ms )
836+ if metrics .success :
837+ self .track_invocation_success ()
838+ else :
839+ self .track_invocation_failure ()
840+ if metrics .path :
841+ self .track_path (metrics .path )
842+ if metrics .usage is not None :
843+ self .track_total_tokens (metrics .usage )
844+
845+ def track_graph_metrics_of (
846+ self ,
847+ metrics_extractor : Callable [[Any ], Optional [GraphMetrics ]],
848+ func : Callable [[], Any ],
849+ ) -> Any :
850+ """
851+ Track graph-level metrics for a synchronous graph operation.
852+
853+ Times the operation, extracts :class:`~ldai.providers.types.GraphMetrics`
854+ via the provided extractor, and fires graph-level tracking events
855+ (path, duration, success/failure, total tokens).
856+
857+ If the extracted ``GraphMetrics`` has a non-``None`` ``duration_ms``,
858+ that value is used instead of the wall-clock elapsed time.
859+
860+ Node-level metrics are not tracked by this method.
861+
862+ For async operations, use :meth:`track_graph_metrics_of_async`.
863+
864+ :param metrics_extractor: Function that extracts GraphMetrics from the result
865+ :param func: Synchronous callable that runs the graph operation
866+ :return: The result of the operation
867+ """
868+ start_ns = time .perf_counter_ns ()
869+ try :
870+ result = func ()
871+ except Exception as err :
872+ duration = (time .perf_counter_ns () - start_ns ) // 1_000_000
873+ self .track_duration (duration )
874+ self .track_invocation_failure ()
875+ raise err
876+
877+ elapsed_ms = (time .perf_counter_ns () - start_ns ) // 1_000_000
878+ self ._track_from_graph_metrics (result , metrics_extractor , elapsed_ms )
879+ return result
880+
881+ async def track_graph_metrics_of_async (
882+ self ,
883+ metrics_extractor : Callable [[Any ], Optional [GraphMetrics ]],
884+ func : Callable [[], Any ],
885+ ) -> Any :
886+ """
887+ Track graph-level metrics for an async graph operation (``func`` is awaited).
888+
889+ Same event semantics as :meth:`track_graph_metrics_of`.
890+
891+ :param metrics_extractor: Function that extracts GraphMetrics from the result
892+ :param func: Async callable that runs the graph operation
893+ :return: The result of the operation
894+ """
895+ start_ns = time .perf_counter_ns ()
896+ try :
897+ result = await func ()
898+ except Exception as err :
899+ duration = (time .perf_counter_ns () - start_ns ) // 1_000_000
900+ self .track_duration (duration )
901+ self .track_invocation_failure ()
902+ raise err
903+
904+ elapsed_ms = (time .perf_counter_ns () - start_ns ) // 1_000_000
905+ self ._track_from_graph_metrics (result , metrics_extractor , elapsed_ms )
906+ return result
0 commit comments