Fix: surface workflow container LLM usage (#27021)
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from flask import has_request_context
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.file import FILE_MODEL_IDENTITY, File, FileTransferMethod
|
||||
from core.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.__base.tool_runtime import ToolRuntime
|
||||
from core.tools.entities.tool_entities import (
|
||||
@@ -49,6 +50,7 @@ class WorkflowTool(Tool):
|
||||
self.workflow_entities = workflow_entities
|
||||
self.workflow_call_depth = workflow_call_depth
|
||||
self.label = label
|
||||
self._latest_usage = LLMUsage.empty_usage()
|
||||
|
||||
super().__init__(entity=entity, runtime=runtime)
|
||||
|
||||
@@ -84,10 +86,11 @@ class WorkflowTool(Tool):
|
||||
assert self.runtime.invoke_from is not None
|
||||
|
||||
user = self._resolve_user(user_id=user_id)
|
||||
|
||||
if user is None:
|
||||
raise ToolInvokeError("User not found")
|
||||
|
||||
self._latest_usage = LLMUsage.empty_usage()
|
||||
|
||||
result = generator.generate(
|
||||
app_model=app,
|
||||
workflow=workflow,
|
||||
@@ -111,9 +114,68 @@ class WorkflowTool(Tool):
|
||||
for file in files:
|
||||
yield self.create_file_message(file) # type: ignore
|
||||
|
||||
self._latest_usage = self._derive_usage_from_result(data)
|
||||
|
||||
yield self.create_text_message(json.dumps(outputs, ensure_ascii=False))
|
||||
yield self.create_json_message(outputs)
|
||||
|
||||
@property
|
||||
def latest_usage(self) -> LLMUsage:
|
||||
return self._latest_usage
|
||||
|
||||
@classmethod
|
||||
def _derive_usage_from_result(cls, data: Mapping[str, Any]) -> LLMUsage:
|
||||
usage_dict = cls._extract_usage_dict(data)
|
||||
if usage_dict is not None:
|
||||
return LLMUsage.from_metadata(cast(LLMUsageMetadata, dict(usage_dict)))
|
||||
|
||||
total_tokens = data.get("total_tokens")
|
||||
total_price = data.get("total_price")
|
||||
if total_tokens is None and total_price is None:
|
||||
return LLMUsage.empty_usage()
|
||||
|
||||
usage_metadata: dict[str, Any] = {}
|
||||
if total_tokens is not None:
|
||||
try:
|
||||
usage_metadata["total_tokens"] = int(str(total_tokens))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if total_price is not None:
|
||||
usage_metadata["total_price"] = str(total_price)
|
||||
currency = data.get("currency")
|
||||
if currency is not None:
|
||||
usage_metadata["currency"] = currency
|
||||
|
||||
if not usage_metadata:
|
||||
return LLMUsage.empty_usage()
|
||||
|
||||
return LLMUsage.from_metadata(cast(LLMUsageMetadata, usage_metadata))
|
||||
|
||||
@classmethod
|
||||
def _extract_usage_dict(cls, payload: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
||||
usage_candidate = payload.get("usage")
|
||||
if isinstance(usage_candidate, Mapping):
|
||||
return usage_candidate
|
||||
|
||||
metadata_candidate = payload.get("metadata")
|
||||
if isinstance(metadata_candidate, Mapping):
|
||||
usage_candidate = metadata_candidate.get("usage")
|
||||
if isinstance(usage_candidate, Mapping):
|
||||
return usage_candidate
|
||||
|
||||
for value in payload.values():
|
||||
if isinstance(value, Mapping):
|
||||
found = cls._extract_usage_dict(value)
|
||||
if found is not None:
|
||||
return found
|
||||
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
||||
for item in value:
|
||||
if isinstance(item, Mapping):
|
||||
found = cls._extract_usage_dict(item)
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
def fork_tool_runtime(self, runtime: ToolRuntime) -> "WorkflowTool":
|
||||
"""
|
||||
fork a new tool with metadata
|
||||
|
||||
Reference in New Issue
Block a user