Add execution incidents, action bursts and shared Flash history. Expose verification and Explorer settings with streamed check results. Support graceful cancellation, recording recovery and bundled UI resources. Add CI quality checks and clean up comments and documentation.
175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from collections.abc import Callable
|
|
import inspect
|
|
import time
|
|
from typing import Annotated, Any, get_args, get_origin
|
|
from unittest.mock import MagicMock, Mock
|
|
import uuid
|
|
|
|
from langchain_core.messages import ToolMessage
|
|
from langchain_core.tools import BaseTool
|
|
from langchain_core.tools.base import InjectedToolCallId
|
|
from langgraph.prebuilt import InjectedState
|
|
from pydantic import BaseModel
|
|
|
|
from artemis.context import ArtemisContext
|
|
from artemis.data_engine import engine as engine_mod
|
|
from artemis.data_engine.trace import CURRENT_TRACE_ID, smart_serialize
|
|
from artemis.graph.state import State
|
|
from artemis.tools.types import CyFunctionDetector
|
|
|
|
|
|
class ToolWrapper(BaseModel):
|
|
"""Wrapper holding a tool factory and lifecycle callbacks."""
|
|
|
|
model_config = {"ignored_types": (CyFunctionDetector,)}
|
|
tool_fn_getter: Callable[[ArtemisContext], BaseTool]
|
|
on_success_fn: Callable[..., str]
|
|
on_failure_fn: Callable[..., str]
|
|
is_available_fn: Callable[[ArtemisContext], bool] | None = None
|
|
|
|
|
|
class CompositeToolWrapper(ToolWrapper):
|
|
"""Wrapper holding a composite tool factory and lifecycle callbacks."""
|
|
|
|
composite_tools_fn_getter: Callable[[ArtemisContext], list[BaseTool]]
|
|
|
|
|
|
# pylint: disable=too-many-branches,too-many-locals,too-many-statements
|
|
async def invoke_tool_with_injection(
|
|
tool: BaseTool,
|
|
args: dict,
|
|
tool_call_id: str,
|
|
state: State | None = None,
|
|
record_trace: bool = True,
|
|
) -> Any:
|
|
"""Invokes a LangChain tool, manually injecting arguments marked with
|
|
|
|
InjectedState or InjectedToolCallId if calling the underlying function
|
|
directly.
|
|
Automatically records type='tool' trace in DataEngine.
|
|
"""
|
|
final_args = dict(args)
|
|
|
|
if isinstance(tool, (Mock, MagicMock)):
|
|
has_real_coro = hasattr(tool, "coroutine") and not isinstance(
|
|
tool.coroutine, (Mock, MagicMock)
|
|
)
|
|
has_real_func = hasattr(tool, "func") and not isinstance(tool.func, (Mock, MagicMock))
|
|
if not has_real_coro and not has_real_func:
|
|
if (
|
|
state is not None
|
|
and hasattr(tool, "args")
|
|
and isinstance(tool.args, dict)
|
|
and "state" in tool.args
|
|
):
|
|
final_args["state"] = state
|
|
return await tool.ainvoke(final_args)
|
|
|
|
# For StructuredTool, the original function might be in tool.func or tool.coroutine
|
|
func = None
|
|
if hasattr(tool, "coroutine") and tool.coroutine:
|
|
func = tool.coroutine
|
|
elif hasattr(tool, "func") and tool.func:
|
|
func = tool.func
|
|
|
|
if func:
|
|
try:
|
|
sig = inspect.signature(func)
|
|
for param_name, param in sig.parameters.items():
|
|
annotation = param.annotation
|
|
# Handle Annotated types
|
|
if get_origin(annotation) is Annotated:
|
|
args_list = get_args(annotation)
|
|
if InjectedState in args_list:
|
|
final_args[param_name] = state
|
|
elif InjectedToolCallId in args_list:
|
|
final_args[param_name] = tool_call_id
|
|
except (ValueError, TypeError):
|
|
# If inspection fails, fall back to just using provided args
|
|
pass
|
|
|
|
trace_id = uuid.uuid4()
|
|
parent_id = CURRENT_TRACE_ID.get()
|
|
token = CURRENT_TRACE_ID.set(trace_id)
|
|
start_time = time.time()
|
|
|
|
live_engine = getattr(engine_mod, "_CURRENT_DATA_ENGINE", None)
|
|
|
|
if record_trace and live_engine:
|
|
step_id = getattr(live_engine, "current_step_id", None)
|
|
live_engine.record_trace(
|
|
type="tool",
|
|
name=tool.name,
|
|
payload={"args": {k: smart_serialize(v) for k, v in args.items()}},
|
|
status="running",
|
|
parent_trace_id=parent_id,
|
|
step_id=step_id,
|
|
trace_id=trace_id,
|
|
)
|
|
|
|
execution_status = "running"
|
|
execution_result = None
|
|
execution_error = None
|
|
try:
|
|
# Call the function
|
|
if inspect.iscoroutinefunction(func):
|
|
result = await func(**final_args)
|
|
else:
|
|
result = func(**final_args)
|
|
execution_status = "success"
|
|
execution_result = result
|
|
return result
|
|
except Exception as e:
|
|
execution_status = "failed"
|
|
execution_error = e
|
|
raise e
|
|
finally:
|
|
duration = time.time() - start_time
|
|
if record_trace and live_engine:
|
|
step_id = getattr(live_engine, "current_step_id", None)
|
|
payload = {"args": {k: smart_serialize(v) for k, v in args.items()}}
|
|
|
|
if execution_status == "success":
|
|
payload["result"] = smart_serialize(get_tool_result_content(execution_result))
|
|
elif execution_status == "failed":
|
|
payload["error"] = str(execution_error)
|
|
else:
|
|
execution_status = "terminated"
|
|
payload["error"] = "Execution terminated unexpectedly."
|
|
|
|
live_engine.record_trace(
|
|
type="tool",
|
|
name=tool.name,
|
|
payload=payload,
|
|
status=execution_status,
|
|
duration=duration,
|
|
parent_trace_id=parent_id,
|
|
step_id=step_id,
|
|
trace_id=trace_id,
|
|
)
|
|
CURRENT_TRACE_ID.reset(token)
|
|
else:
|
|
# Fallback to ainvoke if no direct func available
|
|
return await tool.ainvoke(final_args)
|
|
|
|
|
|
def get_tool_result_content(result: Any) -> Any:
|
|
"""Extracts the content from a tool result, handling ToolMessage returns."""
|
|
if isinstance(result, ToolMessage):
|
|
return result.content
|
|
return result
|