Skip to content

Commit d8e1002

Browse files
committed
feat!: split track_metrics_of into sync and async (track_metrics_of_async) variants
feat: add optional graph_key to all LDAIConfigTracker track_* methods for graph correlation feat: add track_tool_call/track_tool_calls to LDAIConfigTracker feat: add graph_key property to AIGraphTracker feat: make AIGraphTracker.track_total_tokens accept Optional[TokenUsage], skip when None or total <= 0 feat: add LangChainHelper.get_tool_calls_from_response and sum_token_usage_from_messages feat: extract OpenAIHelper.get_ai_usage_from_response; delegate get_ai_metrics_from_response to it refactor: remove node-scoped methods from AIGraphTracker (track_node_invocation, track_tool_call, track_node_judge_response) refactor: use time.time_ns() for sub-millisecond precision in duration calculations
1 parent e2df180 commit d8e1002

9 files changed

Lines changed: 367 additions & 139 deletions

File tree

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_helper.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,44 @@ def get_ai_usage_from_response(response: Any) -> Optional[TokenUsage]:
114114
)
115115
return None
116116

117+
@staticmethod
118+
def get_tool_calls_from_response(response: Any) -> List[str]:
119+
"""
120+
Get tool call names from a LangChain provider response.
121+
122+
:param response: The response from the LangChain model
123+
:return: List of tool names in order, or empty list if none
124+
"""
125+
names: List[str] = []
126+
if hasattr(response, 'tool_calls') and response.tool_calls:
127+
for tc in response.tool_calls:
128+
n = tc.get('name')
129+
if n:
130+
names.append(str(n))
131+
return names
132+
133+
@staticmethod
134+
def sum_token_usage_from_messages(messages: List[Any]) -> Optional[TokenUsage]:
135+
"""
136+
Sum token usage across LangChain messages using :meth:`get_ai_usage_from_response` per message.
137+
138+
:param messages: List of message objects (e.g. from a graph state)
139+
:return: Aggregated TokenUsage, or None if no usage on any message
140+
"""
141+
in_sum = 0
142+
out_sum = 0
143+
total_sum = 0
144+
for m in messages:
145+
u = LangChainHelper.get_ai_usage_from_response(m)
146+
if u is None:
147+
continue
148+
in_sum += u.input
149+
out_sum += u.output
150+
total_sum += u.total
151+
if in_sum == 0 and out_sum == 0 and total_sum == 0:
152+
return None
153+
return TokenUsage(total=total_sum, input=in_sum, output=out_sum)
154+
117155
@staticmethod
118156
def get_ai_metrics_from_response(response: Any) -> LDAIMetrics:
119157
"""

packages/ai-providers/server-ai-openai/src/ldai_openai/openai_helper.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ def convert_messages_to_openai(messages: List[LDMessage]) -> Iterable[ChatComple
2626
[{'role': msg.role, 'content': msg.content} for msg in messages],
2727
)
2828

29+
@staticmethod
30+
def get_ai_usage_from_response(response: Any) -> Optional[TokenUsage]:
31+
"""
32+
Get token usage from an OpenAI provider response.
33+
34+
:param response: The response from the OpenAI model
35+
:return: TokenUsage if any usage data is present, else None
36+
"""
37+
usage: Optional[TokenUsage] = None
38+
if hasattr(response, 'usage') and response.usage is not None:
39+
u = response.usage
40+
usage = TokenUsage(
41+
total=getattr(u, 'total_tokens', 0),
42+
input=getattr(u, 'prompt_tokens', 0),
43+
output=getattr(u, 'completion_tokens', 0),
44+
)
45+
if usage is not None and usage.total == 0 and usage.input == 0 and usage.output == 0:
46+
return None
47+
return usage
48+
2949
@staticmethod
3050
def get_ai_metrics_from_response(response: Any) -> LDAIMetrics:
3151
"""
@@ -34,11 +54,5 @@ def get_ai_metrics_from_response(response: Any) -> LDAIMetrics:
3454
:param response: The response from the OpenAI chat completions API
3555
:return: LDAIMetrics with success status and token usage
3656
"""
37-
usage: Optional[TokenUsage] = None
38-
if hasattr(response, 'usage') and response.usage:
39-
usage = TokenUsage(
40-
total=response.usage.total_tokens or 0,
41-
input=response.usage.prompt_tokens or 0,
42-
output=response.usage.completion_tokens or 0,
43-
)
57+
usage = OpenAIHelper.get_ai_usage_from_response(response)
4458
return LDAIMetrics(success=True, usage=usage)

packages/sdk/server-ai/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ async def main():
150150
# Create LangChain model from configuration
151151
llm = await LangChainProvider.create_langchain_model(ai_config)
152152

153-
# Use with tracking
154-
response = await ai_config.tracker.track_metrics_of(
153+
# Use with tracking (sync invoke)
154+
response = ai_config.tracker.track_metrics_of(
155155
lambda: llm.invoke(messages),
156156
lambda result: LangChainProvider.get_ai_metrics_from_response(result)
157157
)
@@ -190,7 +190,7 @@ async def main():
190190
temperature=ai_config.model.get_parameter('temperature') if ai_config.model else 0.5,
191191
)
192192

193-
result = await ai_config.tracker.track_metrics_of(
193+
result = await ai_config.tracker.track_metrics_of_async(
194194
call_custom_provider,
195195
map_custom_provider_metrics
196196
)

packages/sdk/server-ai/src/ldai/judge/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def evaluate(
7171
messages = self._construct_evaluation_messages(input_text, output_text)
7272
assert self._evaluation_response_structure is not None
7373

74-
response = await self._ai_config_tracker.track_metrics_of(
74+
response = await self._ai_config_tracker.track_metrics_of_async(
7575
lambda: self._model_runner.invoke_structured_model(messages, self._evaluation_response_structure),
7676
lambda result: result.metrics,
7777
)

packages/sdk/server-ai/src/ldai/managed_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def invoke(self, prompt: str) -> ModelResponse:
4848
config_messages = self._ai_config.messages or []
4949
all_messages = config_messages + self._messages
5050

51-
response = await self._tracker.track_metrics_of(
51+
response = await self._tracker.track_metrics_of_async(
5252
lambda: self._model_runner.invoke_model(all_messages),
5353
lambda result: result.metrics,
5454
)

packages/sdk/server-ai/src/ldai/providers/runner_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def _with_fallback(
7777
continue
7878
result = fn(provider_factory)
7979
if result is not None:
80-
log.debug(f"Successfully created capability using provider '{provider_type}'")
80+
log.debug(f"Successfully invoked create function with provider '{provider_type}'")
8181
return result
8282
except Exception as exc:
8383
log.warning(f"Provider '{provider_type}' failed: {exc}")

0 commit comments

Comments
 (0)