Skip to content

Commit 87d85fc

Browse files
committed
Support SDK-local custom tool preload
1 parent acdbcd3 commit 87d85fc

13 files changed

Lines changed: 565 additions & 60 deletions

python/composio/core/models/custom_tool.py

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def _create_tool(
156156
execute: CustomToolExecuteFn,
157157
extends_toolkit: t.Optional[str] = None,
158158
output_params: t.Optional[t.Type[BaseModel]] = None,
159+
preload: t.Optional[bool] = None,
159160
) -> CustomTool:
160161
"""Internal: create and validate a CustomTool."""
161162
context = "experimental.tool"
@@ -218,6 +219,7 @@ def _create_tool(
218219
output_schema=output_schema,
219220
input_params=input_params,
220221
execute=execute,
222+
preload=preload,
221223
)
222224

223225

@@ -258,6 +260,7 @@ def _infer_tool_from_function(
258260
description: t.Optional[str] = None,
259261
extends_toolkit: t.Optional[str] = None,
260262
output_params: t.Optional[t.Type[BaseModel]] = None,
263+
preload: t.Optional[bool] = None,
261264
annotation_locals: t.Optional[t.Mapping[str, t.Any]] = None,
262265
) -> CustomTool:
263266
"""Create a CustomTool by inferring metadata from a decorated function.
@@ -341,6 +344,7 @@ def execute(input: t.Any, ctx: t.Any) -> t.Dict[str, t.Any]:
341344
execute=execute,
342345
extends_toolkit=extends_toolkit,
343346
output_params=output_params,
347+
preload=preload,
344348
)
345349

346350

@@ -372,7 +376,14 @@ def search_code(input: SearchInput, ctx):
372376
return {"results": []}
373377
"""
374378

375-
def __init__(self, *, slug: str, name: str, description: str) -> None:
379+
def __init__(
380+
self,
381+
*,
382+
slug: str,
383+
name: str,
384+
description: str,
385+
preload: t.Optional[bool] = None,
386+
) -> None:
376387
context = "experimental.Toolkit"
377388
_validate_slug(slug, context)
378389
if not name:
@@ -383,6 +394,7 @@ def __init__(self, *, slug: str, name: str, description: str) -> None:
383394
self.slug = slug
384395
self.name = name
385396
self.description = description
397+
self.preload = preload
386398
self._tools: t.List[CustomTool] = []
387399

388400
@property
@@ -400,6 +412,7 @@ def tool(
400412
name: t.Optional[str] = None,
401413
description: t.Optional[str] = None,
402414
output_params: t.Optional[t.Type[BaseModel]] = None,
415+
preload: t.Optional[bool] = None,
403416
) -> t.Callable[[t.Callable[..., t.Any]], CustomTool]: ...
404417

405418
def tool(
@@ -410,6 +423,7 @@ def tool(
410423
name: t.Optional[str] = None,
411424
description: t.Optional[str] = None,
412425
output_params: t.Optional[t.Type[BaseModel]] = None,
426+
preload: t.Optional[bool] = None,
413427
) -> t.Union[CustomTool, t.Callable[[t.Callable[..., t.Any]], CustomTool]]:
414428
"""Decorator to add a tool to this toolkit.
415429
@@ -425,6 +439,7 @@ def decorator(f: t.Callable[..., t.Any]) -> CustomTool:
425439
name=name,
426440
description=description,
427441
output_params=output_params,
442+
preload=preload,
428443
annotation_locals=annotation_locals,
429444
# No extends_toolkit for toolkit tools
430445
)
@@ -441,6 +456,7 @@ def decorator(f: t.Callable[..., t.Any]) -> CustomTool:
441456
name=name,
442457
description=description,
443458
output_params=output_params,
459+
preload=preload,
444460
annotation_locals=_get_caller_locals(),
445461
)
446462
_validate_slug_length(
@@ -479,6 +495,7 @@ def tool(
479495
description: t.Optional[str] = None,
480496
extends_toolkit: t.Optional[str] = None,
481497
output_params: t.Optional[t.Type[BaseModel]] = None,
498+
preload: t.Optional[bool] = None,
482499
) -> t.Callable[[t.Callable[..., t.Any]], CustomTool]: ...
483500

484501
def tool(
@@ -490,6 +507,7 @@ def tool(
490507
description: t.Optional[str] = None,
491508
extends_toolkit: t.Optional[str] = None,
492509
output_params: t.Optional[t.Type[BaseModel]] = None,
510+
preload: t.Optional[bool] = None,
493511
) -> t.Union[CustomTool, t.Callable[[t.Callable[..., t.Any]], CustomTool]]:
494512
"""Decorator to create a custom tool from a function.
495513
@@ -526,6 +544,7 @@ def decorator(f: t.Callable[..., t.Any]) -> CustomTool:
526544
description=description,
527545
extends_toolkit=extends_toolkit,
528546
output_params=output_params,
547+
preload=preload,
529548
annotation_locals=annotation_locals,
530549
)
531550

@@ -537,6 +556,7 @@ def decorator(f: t.Callable[..., t.Any]) -> CustomTool:
537556
description=description,
538557
extends_toolkit=extends_toolkit,
539558
output_params=output_params,
559+
preload=preload,
540560
annotation_locals=_get_caller_locals(),
541561
)
542562
return decorator
@@ -547,7 +567,28 @@ def decorator(f: t.Callable[..., t.Any]) -> CustomTool:
547567
# ────────────────────────────────────────────────────────────────
548568

549569

550-
def serialize_custom_tools(tools: t.List[CustomTool]) -> t.List[t.Dict[str, t.Any]]:
570+
def _serialized_preload_value(
571+
preload: t.Optional[bool],
572+
inherited_preload: t.Optional[bool],
573+
default_preload: bool,
574+
) -> t.Optional[bool]:
575+
resolved = (
576+
preload
577+
if preload is not None
578+
else inherited_preload
579+
if inherited_preload is not None
580+
else default_preload
581+
)
582+
if preload is not None or inherited_preload is not None or default_preload:
583+
return resolved
584+
return None
585+
586+
587+
def serialize_custom_tools(
588+
tools: t.List[CustomTool],
589+
*,
590+
default_preload: bool = False,
591+
) -> t.List[t.Dict[str, t.Any]]:
551592
"""Serialize custom tools into the format expected by the backend."""
552593
result = []
553594
for tool in tools:
@@ -561,12 +602,19 @@ def serialize_custom_tools(tools: t.List[CustomTool]) -> t.List[t.Dict[str, t.An
561602
entry["output_schema"] = tool.output_schema
562603
if tool.extends_toolkit:
563604
entry["extends_toolkit"] = tool.extends_toolkit
605+
preload = _serialized_preload_value(
606+
tool.preload, inherited_preload=None, default_preload=default_preload
607+
)
608+
if preload is not None:
609+
entry["preload"] = preload
564610
result.append(entry)
565611
return result
566612

567613

568614
def serialize_custom_toolkits(
569615
toolkits: t.Sequence[ExperimentalToolkit],
616+
*,
617+
default_preload: bool = False,
570618
) -> t.List[t.Dict[str, t.Any]]:
571619
"""Serialize custom toolkits into the format expected by the backend."""
572620
result = []
@@ -581,15 +629,26 @@ def serialize_custom_toolkits(
581629
}
582630
if tool.output_schema:
583631
entry["output_schema"] = tool.output_schema
632+
preload = _serialized_preload_value(
633+
tool.preload,
634+
inherited_preload=tk.preload,
635+
default_preload=default_preload,
636+
)
637+
if preload is not None:
638+
entry["preload"] = preload
584639
toolkit_tools.append(entry)
585-
result.append(
586-
{
587-
"slug": tk.slug,
588-
"name": tk.name,
589-
"description": tk.description,
590-
"tools": toolkit_tools,
591-
}
640+
toolkit_entry: t.Dict[str, t.Any] = {
641+
"slug": tk.slug,
642+
"name": tk.name,
643+
"description": tk.description,
644+
"tools": toolkit_tools,
645+
}
646+
preload = _serialized_preload_value(
647+
tk.preload, inherited_preload=None, default_preload=default_preload
592648
)
649+
if preload is not None:
650+
toolkit_entry["preload"] = preload
651+
result.append(toolkit_entry)
593652
return result
594653

595654

@@ -729,23 +788,63 @@ def find_custom_tool_map_entry_by_final_slug(
729788
return custom_tools_map.by_final_slug.get(slug.upper())
730789

731790

791+
def assert_no_custom_tool_slugs_in_preload(
792+
preload_tools: t.Union[t.Sequence[str], t.Literal["all"], None],
793+
custom_tools_map: t.Optional[CustomToolsMap],
794+
) -> None:
795+
"""Reject legacy top-level preload of custom tool slugs."""
796+
if preload_tools is None or preload_tools == "all":
797+
return
798+
799+
custom_preload_slugs = []
800+
for slug in preload_tools:
801+
normalized = slug.upper()
802+
if normalized.startswith(LOCAL_TOOL_PREFIX) or (
803+
custom_tools_map is not None
804+
and (
805+
normalized in custom_tools_map.by_original_slug
806+
or normalized in custom_tools_map.by_final_slug
807+
)
808+
):
809+
custom_preload_slugs.append(slug)
810+
811+
if custom_preload_slugs:
812+
raise ValidationError(
813+
"Custom tool slugs are not supported in preload.tools: "
814+
f"{', '.join(custom_preload_slugs)}. Set preload=True on the SDK "
815+
"custom tool or custom toolkit definition instead."
816+
)
817+
818+
732819
def get_preloaded_custom_tool_slugs(
733-
tool_router_tools: t.Optional[t.Iterable[str]],
734820
custom_tools_map: t.Optional[CustomToolsMap],
821+
*,
822+
default_preload: bool = False,
735823
) -> t.List[str]:
736-
"""Return final custom tool slugs selected by the backend for preload."""
737-
if tool_router_tools is None or custom_tools_map is None:
824+
"""Return final custom tool slugs selected locally for preload."""
825+
if custom_tools_map is None:
738826
return []
739827

740828
seen: t.Set[str] = set()
741829
custom_tool_slugs: t.List[str] = []
742830

743-
for slug in tool_router_tools:
744-
entry = find_custom_tool_map_entry_by_final_slug(
745-
custom_tools_map,
746-
str(slug),
831+
for entry in custom_tools_map.by_final_slug.values():
832+
toolkit = next(
833+
(
834+
tk
835+
for tk in custom_tools_map.toolkits or []
836+
if entry.toolkit and tk.slug.lower() == entry.toolkit.lower()
837+
),
838+
None,
839+
)
840+
should_preload = (
841+
entry.handle.preload
842+
if entry.handle.preload is not None
843+
else toolkit.preload
844+
if toolkit is not None and toolkit.preload is not None
845+
else default_preload
747846
)
748-
if entry is None:
847+
if not should_preload:
749848
continue
750849

751850
final_slug_key = entry.final_slug.upper()

python/composio/core/models/custom_tool_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class CustomTool:
106106
execute: CustomToolExecuteFn
107107
extends_toolkit: t.Optional[str] = None
108108
output_schema: t.Optional[t.Dict[str, t.Any]] = None
109+
preload: t.Optional[bool] = None
109110

110111

111112
# ────────────────────────────────────────────────────────────────

python/composio/core/models/tool_router.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from composio.core.models.base import Resource
2020
from composio.core.models.custom_tool import (
2121
ExperimentalToolkit,
22+
assert_no_custom_tool_slugs_in_preload,
23+
build_custom_tools_map,
2224
build_custom_tools_map_from_response,
2325
get_preloaded_custom_tool_slugs,
2426
serialize_custom_tools,
@@ -627,6 +629,10 @@ def create(
627629
- 'assistive_prompt' (dict): Configuration for assistive prompt generation.
628630
- 'user_timezone' (str): IANA timezone identifier
629631
(e.g., "America/New_York", "Europe/London").
632+
- 'custom_tools' / 'custom_toolkits': SDK custom tools.
633+
Set preload=True on a custom tool or toolkit to expose
634+
it directly from session.tools(); otherwise custom tools
635+
remain search-only.
630636
Example: {'assistive_prompt': {'user_timezone': 'America/New_York'}}
631637
:return: Tool router session object
632638
@@ -716,6 +722,7 @@ def create(
716722
workbench=workbench,
717723
preload=preload,
718724
)
725+
default_custom_preload = preload is not None and preload.get("tools") == "all"
719726

720727
# Parse manage_connections config
721728
manage_connections = (
@@ -851,6 +858,8 @@ def create(
851858
# experimental.assistive_prompt_config.user_timezone
852859
custom_tools: t.Optional[t.List[CustomTool]] = None
853860
custom_toolkits: t.Optional[t.List[ExperimentalToolkit]] = None
861+
local_custom_tools_map: t.Optional[CustomToolsMap] = None
862+
inline_custom_tools_payload: t.Optional[t.Dict[str, t.Any]] = None
854863

855864
if experimental is not None:
856865
experimental_payload: t.Dict[str, t.Any] = {}
@@ -866,16 +875,36 @@ def create(
866875
# Serialize custom tools and toolkits for the backend
867876
custom_tools = experimental.get("custom_tools")
868877
custom_toolkits = experimental.get("custom_toolkits")
878+
if custom_tools or custom_toolkits:
879+
local_custom_tools_map = build_custom_tools_map(
880+
custom_tools or [],
881+
custom_toolkits,
882+
)
883+
assert_no_custom_tool_slugs_in_preload(
884+
preload.get("tools") if preload is not None else None,
885+
local_custom_tools_map,
886+
)
869887

870888
if custom_tools:
871889
experimental_payload["custom_tools"] = serialize_custom_tools(
872-
custom_tools
890+
custom_tools,
891+
default_preload=default_custom_preload,
873892
)
874893
if custom_toolkits:
875894
experimental_payload["custom_toolkits"] = serialize_custom_toolkits(
876-
custom_toolkits
895+
custom_toolkits,
896+
default_preload=default_custom_preload,
877897
)
878898

899+
if (
900+
"custom_tools" in experimental_payload
901+
or "custom_toolkits" in experimental_payload
902+
):
903+
inline_custom_tools_payload = {
904+
"custom_tools": experimental_payload.get("custom_tools"),
905+
"custom_toolkits": experimental_payload.get("custom_toolkits"),
906+
}
907+
879908
if experimental_payload:
880909
create_params["experimental"] = experimental_payload
881910

@@ -897,8 +926,8 @@ def create(
897926
experimental=session.experimental,
898927
)
899928
preloaded_custom_tool_slugs = get_preloaded_custom_tool_slugs(
900-
getattr(session, "tool_router_tools", None),
901929
custom_tools_map,
930+
default_preload=default_custom_preload,
902931
)
903932

904933
# Transform experimental response:
@@ -930,6 +959,7 @@ def create(
930959
user_id=user_id,
931960
preload=_session_preload_config(session),
932961
preloaded_custom_tool_slugs=preloaded_custom_tool_slugs,
962+
inline_custom_tools_payload=inline_custom_tools_payload,
933963
)
934964

935965
def use(self, session_id: str) -> ToolRouterSession[TTool, TToolCollection]:

0 commit comments

Comments
 (0)