aip-agents-binary 0.5.25__py3-none-macosx_13_0_arm64.whl → 0.6.8__py3-none-macosx_13_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. aip_agents/agent/__init__.py +44 -4
  2. aip_agents/agent/base_langgraph_agent.py +163 -74
  3. aip_agents/agent/base_langgraph_agent.pyi +3 -2
  4. aip_agents/agent/langgraph_memory_enhancer_agent.py +368 -34
  5. aip_agents/agent/langgraph_memory_enhancer_agent.pyi +3 -2
  6. aip_agents/agent/langgraph_react_agent.py +329 -22
  7. aip_agents/agent/langgraph_react_agent.pyi +41 -2
  8. aip_agents/examples/hello_world_ptc.py +49 -0
  9. aip_agents/examples/hello_world_ptc.pyi +5 -0
  10. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  11. aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
  12. aip_agents/examples/hello_world_tool_output_client.py +9 -0
  13. aip_agents/examples/tools/multiply_tool.py +43 -0
  14. aip_agents/examples/tools/multiply_tool.pyi +18 -0
  15. aip_agents/guardrails/engines/base.py +6 -6
  16. aip_agents/mcp/client/__init__.py +38 -2
  17. aip_agents/mcp/client/connection_manager.py +36 -1
  18. aip_agents/mcp/client/connection_manager.pyi +3 -0
  19. aip_agents/mcp/client/persistent_session.py +318 -68
  20. aip_agents/mcp/client/persistent_session.pyi +9 -0
  21. aip_agents/mcp/client/transports.py +37 -2
  22. aip_agents/mcp/client/transports.pyi +9 -0
  23. aip_agents/memory/adapters/base_adapter.py +98 -0
  24. aip_agents/memory/adapters/base_adapter.pyi +25 -0
  25. aip_agents/ptc/__init__.py +87 -0
  26. aip_agents/ptc/__init__.pyi +14 -0
  27. aip_agents/ptc/custom_tools.py +473 -0
  28. aip_agents/ptc/custom_tools.pyi +184 -0
  29. aip_agents/ptc/custom_tools_payload.py +400 -0
  30. aip_agents/ptc/custom_tools_payload.pyi +31 -0
  31. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  32. aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
  33. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
  34. aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
  35. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
  36. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
  37. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  38. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
  39. aip_agents/ptc/doc_gen.py +122 -0
  40. aip_agents/ptc/doc_gen.pyi +40 -0
  41. aip_agents/ptc/exceptions.py +57 -0
  42. aip_agents/ptc/exceptions.pyi +37 -0
  43. aip_agents/ptc/executor.py +261 -0
  44. aip_agents/ptc/executor.pyi +99 -0
  45. aip_agents/ptc/mcp/__init__.py +45 -0
  46. aip_agents/ptc/mcp/__init__.pyi +7 -0
  47. aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
  48. aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
  49. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  50. aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
  51. aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
  52. aip_agents/ptc/naming.py +196 -0
  53. aip_agents/ptc/naming.pyi +85 -0
  54. aip_agents/ptc/payload.py +26 -0
  55. aip_agents/ptc/payload.pyi +15 -0
  56. aip_agents/ptc/prompt_builder.py +673 -0
  57. aip_agents/ptc/prompt_builder.pyi +59 -0
  58. aip_agents/ptc/ptc_helper.py +16 -0
  59. aip_agents/ptc/ptc_helper.pyi +1 -0
  60. aip_agents/ptc/sandbox_bridge.py +256 -0
  61. aip_agents/ptc/sandbox_bridge.pyi +38 -0
  62. aip_agents/ptc/template_utils.py +33 -0
  63. aip_agents/ptc/template_utils.pyi +13 -0
  64. aip_agents/ptc/templates/__init__.py +1 -0
  65. aip_agents/ptc/templates/__init__.pyi +0 -0
  66. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  67. aip_agents/ptc/tool_def_helpers.py +101 -0
  68. aip_agents/ptc/tool_def_helpers.pyi +38 -0
  69. aip_agents/ptc/tool_enrichment.py +163 -0
  70. aip_agents/ptc/tool_enrichment.pyi +60 -0
  71. aip_agents/sandbox/__init__.py +43 -0
  72. aip_agents/sandbox/__init__.pyi +5 -0
  73. aip_agents/sandbox/defaults.py +205 -0
  74. aip_agents/sandbox/defaults.pyi +30 -0
  75. aip_agents/sandbox/e2b_runtime.py +295 -0
  76. aip_agents/sandbox/e2b_runtime.pyi +57 -0
  77. aip_agents/sandbox/template_builder.py +131 -0
  78. aip_agents/sandbox/template_builder.pyi +36 -0
  79. aip_agents/sandbox/types.py +24 -0
  80. aip_agents/sandbox/types.pyi +14 -0
  81. aip_agents/sandbox/validation.py +50 -0
  82. aip_agents/sandbox/validation.pyi +20 -0
  83. aip_agents/sentry/sentry.py +29 -8
  84. aip_agents/sentry/sentry.pyi +3 -2
  85. aip_agents/tools/__init__.py +13 -2
  86. aip_agents/tools/__init__.pyi +3 -1
  87. aip_agents/tools/browser_use/browser_use_tool.py +8 -0
  88. aip_agents/tools/browser_use/streaming.py +2 -0
  89. aip_agents/tools/date_range_tool.py +554 -0
  90. aip_agents/tools/date_range_tool.pyi +21 -0
  91. aip_agents/tools/execute_ptc_code.py +357 -0
  92. aip_agents/tools/execute_ptc_code.pyi +90 -0
  93. aip_agents/tools/memory_search/__init__.py +8 -1
  94. aip_agents/tools/memory_search/__init__.pyi +3 -3
  95. aip_agents/tools/memory_search/mem0.py +114 -1
  96. aip_agents/tools/memory_search/mem0.pyi +11 -1
  97. aip_agents/tools/memory_search/schema.py +33 -0
  98. aip_agents/tools/memory_search/schema.pyi +10 -0
  99. aip_agents/tools/memory_search_tool.py +8 -0
  100. aip_agents/tools/memory_search_tool.pyi +2 -2
  101. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
  102. aip_agents/utils/langgraph/tool_output_management.py +80 -0
  103. aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
  104. {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +9 -19
  105. {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +107 -41
  106. {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/WHEEL +1 -1
  107. aip_agents/examples/demo_memory_recall.py +0 -401
  108. aip_agents/examples/demo_memory_recall.pyi +0 -58
  109. {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/top_level.txt +0 -0
@@ -18,14 +18,14 @@ from collections.abc import Awaitable, Callable, Sequence
18
18
  from dataclasses import asdict, dataclass
19
19
  from functools import reduce
20
20
  from textwrap import dedent
21
- from typing import TYPE_CHECKING, Annotated, Any
21
+ from typing import TYPE_CHECKING, Annotated, Any, cast
22
22
 
23
- from deprecated import deprecated
23
+ from deprecated import deprecated # type: ignore[import-untyped]
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from aip_agents.guardrails.manager import GuardrailManager
27
- from gllm_core.event import EventEmitter
28
- from gllm_core.schema import Chunk
27
+ from gllm_core.event import EventEmitter # type: ignore[import-untyped]
28
+ from gllm_core.schema import Chunk # type: ignore[import-untyped]
29
29
  from langchain_core.language_models import BaseChatModel
30
30
  from langchain_core.messages import (
31
31
  AIMessage,
@@ -54,7 +54,7 @@ from aip_agents.schema.a2a import A2AStreamEventType
54
54
  from aip_agents.schema.hitl import ApprovalDecision, HitlMetadata
55
55
  from aip_agents.schema.langgraph import ToolCallResult, ToolStorageParams
56
56
  from aip_agents.schema.step_limit import MaxStepsExceededError, StepLimitConfig
57
- from aip_agents.tools.memory_search_tool import MEMORY_SEARCH_TOOL_NAME
57
+ from aip_agents.tools.memory_search_tool import MEMORY_DELETE_TOOL_NAME, MEMORY_SEARCH_TOOL_NAME
58
58
  from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY
59
59
  from aip_agents.utils import add_references_chunks
60
60
  from aip_agents.utils.langgraph import (
@@ -87,6 +87,9 @@ from aip_agents.utils.token_usage_helper import (
87
87
  extract_token_usage_from_tool_output,
88
88
  )
89
89
 
90
+ if TYPE_CHECKING:
91
+ from aip_agents.ptc import PTCSandboxConfig
92
+
90
93
  logger = get_logger(__name__)
91
94
 
92
95
  # Default instruction for ReAct agents
@@ -165,6 +168,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
165
168
  middlewares: Sequence[AgentMiddleware] | None = None,
166
169
  guardrail: GuardrailManager | None = None,
167
170
  step_limit_config: StepLimitConfig | None = None,
171
+ ptc_config: PTCSandboxConfig | None = None,
168
172
  **kwargs: Any,
169
173
  ):
170
174
  """Initialize the LangGraph ReAct Agent.
@@ -193,6 +197,11 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
193
197
  input/output filtering during agent execution.
194
198
  enable_pii: Optional toggle to enable PII handling for tool inputs and outputs.
195
199
  step_limit_config: Optional configuration for step limits and delegation depth.
200
+ ptc_config: Optional configuration for PTC sandbox execution. See PTCSandboxConfig
201
+ for available options including enabled flag, sandbox timeout, and template settings.
202
+ PTC is enabled when ptc_config is not None and ptc_config.enabled is True.
203
+ When enabled, prompt guidance is automatically injected into the agent's instruction.
204
+ PTC runs in a sandbox only; there is no in-process trusted PTC path.
196
205
  **kwargs: Additional keyword arguments passed to BaseLangGraphAgent.
197
206
  """
198
207
  # Use LangGraph's standard AgentState for ReAct
@@ -212,6 +221,12 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
212
221
  **kwargs,
213
222
  )
214
223
 
224
+ if self.model is None and self.lm_invoker is None:
225
+ logger.warning(
226
+ "Agent '%s': Model and LM invoker are both unset. Calls that require a model will fail.",
227
+ self.name,
228
+ )
229
+
215
230
  # Handle tool output management
216
231
  self.tool_output_manager = tool_output_manager
217
232
  self._pii_handlers_by_thread: dict[str, ToolPIIHandler] = {}
@@ -233,6 +248,18 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
233
248
 
234
249
  self.step_limit_config = step_limit_config
235
250
 
251
+ # Initialize PTC state (Programmatic Tool Calling)
252
+ self._ptc_config: PTCSandboxConfig | None = None
253
+ self._ptc_tool_synced = False
254
+ self._ptc_tool: BaseTool | None = None
255
+ self._ptc_prompt_hash: str = ""
256
+ # Capture instruction after middleware setup so middleware prompts are preserved
257
+ self._original_instruction: str = self.instruction
258
+
259
+ # Enable PTC if requested via constructor
260
+ if ptc_config is not None and ptc_config.enabled:
261
+ self.enable_ptc(ptc_config)
262
+
236
263
  def _setup_middleware(
237
264
  self,
238
265
  planning: bool,
@@ -256,7 +283,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
256
283
 
257
284
  # Auto-configure TodoListMiddleware if planning enabled
258
285
  if planning:
259
- middleware_list.append(TodoListMiddleware())
286
+ middleware_list.append(cast(AgentMiddleware, TodoListMiddleware()))
260
287
 
261
288
  # Auto-configure GuardrailMiddleware if guardrail provided
262
289
  if guardrail:
@@ -419,9 +446,9 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
419
446
  )
420
447
 
421
448
  def _rebuild_resolved_tools(self) -> None:
422
- """Rebuild resolved tools including middleware tools.
449
+ """Rebuild resolved tools including middleware and PTC tools.
423
450
 
424
- Overrides base class to ensure middleware tools are preserved
451
+ Overrides base class to ensure middleware tools and the PTC tool are preserved
425
452
  when tools are rebuilt (e.g., after update_regular_tools).
426
453
  """
427
454
  # Call base class to rebuild with regular, a2a, delegation, and mcp tools
@@ -431,6 +458,10 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
431
458
  if hasattr(self, "_middleware_tools") and self._middleware_tools:
432
459
  self.resolved_tools.extend(self._middleware_tools)
433
460
 
461
+ # Add PTC tool if synced
462
+ if hasattr(self, "_ptc_tool") and self._ptc_tool is not None:
463
+ self.resolved_tools.append(self._ptc_tool)
464
+
434
465
  def _handle_tool_artifacts(
435
466
  self, tool_output: Any, pending_artifacts: list[dict[str, Any]]
436
467
  ) -> tuple[str, list[dict[str, Any]]]:
@@ -553,6 +584,29 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
553
584
 
554
585
  return memory_node
555
586
 
587
+ def _should_save_interaction(self, final_state: dict[str, Any] | None) -> bool:
588
+ """Return True when interaction should be saved to memory."""
589
+ if self._contains_memory_delete_action(final_state):
590
+ logger.info("Memory: Skipping save_interaction due to memory delete action in state.")
591
+ return False
592
+ return True
593
+
594
+ @staticmethod
595
+ def _contains_memory_delete_action(final_state: dict[str, Any] | None) -> bool:
596
+ """Return True when final state includes a delete memory action block."""
597
+ if not isinstance(final_state, dict):
598
+ return False
599
+ messages = final_state.get("messages")
600
+ if not isinstance(messages, list):
601
+ return False
602
+ for message in messages:
603
+ content = getattr(message, "content", None)
604
+ if not isinstance(content, str):
605
+ continue
606
+ if "<MEMORY_ACTION>" in content and "action=delete" in content:
607
+ return True
608
+ return False
609
+
556
610
  def _extract_user_query_from_messages(self, messages: list[Any]) -> str | None:
557
611
  """Get latest user query string from a list of messages.
558
612
 
@@ -732,7 +786,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
732
786
  pending_artifacts: list[dict[str, Any]] = state.get("artifacts") or []
733
787
  reference_updates: list[Chunk] = []
734
788
  tool_map = {tool.name: tool for tool in self.resolved_tools}
735
- pii_mapping = {}
789
+ pii_mapping: dict[str, str] = {}
736
790
 
737
791
  aggregated_metadata_delta: dict[str, Any] = {}
738
792
  total_tools_token_usage: list[UsageMetadata] = []
@@ -756,7 +810,8 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
756
810
  ),
757
811
  )
758
812
 
759
- tasks = [asyncio.create_task(run_tool(tc)) for tc in last_message.tool_calls]
813
+ normalized_tool_calls = [self._normalize_tool_call(tc) for tc in last_message.tool_calls]
814
+ tasks = [asyncio.create_task(run_tool(tc)) for tc in normalized_tool_calls]
760
815
 
761
816
  for coro in asyncio.as_completed(tasks):
762
817
  tool_result = await coro
@@ -779,6 +834,31 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
779
834
  pii_mapping,
780
835
  )
781
836
 
837
+ def _normalize_tool_call(self, tool_call: Any) -> dict[str, Any]:
838
+ """Normalize tool call inputs into a dict with required keys."""
839
+ if isinstance(tool_call, dict):
840
+ normalized = dict(tool_call)
841
+ elif hasattr(tool_call, "model_dump"):
842
+ normalized = tool_call.model_dump()
843
+ elif hasattr(tool_call, "dict"):
844
+ normalized = tool_call.dict()
845
+ elif hasattr(tool_call, "name") and hasattr(tool_call, "args"):
846
+ normalized = {
847
+ "id": getattr(tool_call, "id", None),
848
+ "name": getattr(tool_call, "name", None),
849
+ "args": getattr(tool_call, "args", None),
850
+ }
851
+ else:
852
+ raise TypeError("Tool call must be a dict-like object or ToolCall instance.")
853
+
854
+ if not isinstance(normalized, dict):
855
+ raise TypeError("Tool call normalization did not produce a dict.")
856
+
857
+ if "name" not in normalized or "args" not in normalized:
858
+ raise TypeError("Tool call must include 'name' and 'args' fields.")
859
+
860
+ return normalized
861
+
782
862
  def _accumulate_tool_result( # noqa: PLR0913
783
863
  self,
784
864
  tool_result: Any,
@@ -787,7 +867,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
787
867
  aggregated_metadata_delta: dict[str, Any],
788
868
  reference_updates: list[Chunk],
789
869
  total_tools_token_usage: list[UsageMetadata],
790
- pii_mapping: dict[str, str] | None,
870
+ pii_mapping: dict[str, str],
791
871
  ) -> None: # noqa: PLR0913
792
872
  """Accumulate results from a single tool call.
793
873
 
@@ -1233,13 +1313,16 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1233
1313
 
1234
1314
  # Create enhanced tool configuration with output management
1235
1315
  tool_config = self._create_enhanced_tool_config(config, state, tool_call["name"], tool_call_id)
1316
+ if not isinstance(tool_config, dict):
1317
+ raise TypeError("Tool configuration must be a dictionary.")
1318
+ tool_config_runnable = tool_config
1236
1319
 
1237
1320
  arun_streaming_method = getattr(tool, TOOL_RUN_STREAMING_METHOD, None)
1238
1321
 
1239
1322
  if arun_streaming_method and callable(arun_streaming_method):
1240
1323
  tool_output = await self._execute_tool_with_streaming(tool, tool_call, tool_config)
1241
1324
  else:
1242
- tool_output = await tool.ainvoke(resolved_args, tool_config)
1325
+ tool_output = await tool.ainvoke(resolved_args, tool_config_runnable)
1243
1326
 
1244
1327
  references = extract_references_from_tool(tool, tool_output)
1245
1328
 
@@ -1513,7 +1596,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1513
1596
  tool_call: dict[str, Any],
1514
1597
  execution_time: float,
1515
1598
  pending_artifacts: list[dict[str, Any]],
1516
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1599
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1517
1600
  """Process tool output into messages, artifacts, and metadata.
1518
1601
 
1519
1602
  Args:
@@ -1541,7 +1624,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1541
1624
 
1542
1625
  def _handle_command_output(
1543
1626
  self, tool_output: Command, tool_call: dict[str, Any], execution_time: float, metadata_delta: dict[str, Any]
1544
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1627
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1545
1628
  """Handle Command type tool outputs.
1546
1629
 
1547
1630
  Args:
@@ -1570,7 +1653,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1570
1653
 
1571
1654
  def _handle_string_output(
1572
1655
  self, tool_output: str, tool_call: dict[str, Any], execution_time: float
1573
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1656
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1574
1657
  """Handle string type tool outputs.
1575
1658
 
1576
1659
  Args:
@@ -1596,7 +1679,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1596
1679
  execution_time: float,
1597
1680
  pending_artifacts: list[dict[str, Any]],
1598
1681
  metadata_delta: dict[str, Any],
1599
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1682
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1600
1683
  """Handle legacy dict and other tool outputs.
1601
1684
 
1602
1685
  Args:
@@ -1694,8 +1777,11 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1694
1777
  self._emit_default_tool_call_event(writer, tool_name, tool_call_id, tool_args)
1695
1778
 
1696
1779
  streaming_kwargs = self._build_streaming_kwargs(tool_args, tool_config)
1780
+ arun_streaming_method = getattr(tool, TOOL_RUN_STREAMING_METHOD, None)
1781
+ if not callable(arun_streaming_method):
1782
+ raise RuntimeError(f"Tool '{tool_name}' does not implement streaming.")
1697
1783
 
1698
- async for chunk in tool.arun_streaming(**streaming_kwargs):
1784
+ async for chunk in arun_streaming_method(**streaming_kwargs):
1699
1785
  final_output, saw_tool_result = self._handle_streaming_chunk(
1700
1786
  chunk=chunk,
1701
1787
  writer=writer,
@@ -2125,6 +2211,9 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2125
2211
 
2126
2212
  effective_event_emitter = state.get("event_emitter") or self.event_emitter
2127
2213
 
2214
+ if self.lm_invoker is None:
2215
+ raise RuntimeError("LM invoker is required for this execution path.")
2216
+
2128
2217
  if self.resolved_tools:
2129
2218
  self.lm_invoker.set_tools(self.resolved_tools)
2130
2219
 
@@ -2183,6 +2272,9 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2183
2272
  ):
2184
2273
  langchain_prompt = [SystemMessage(content=enhanced_instruction)] + list(current_messages)
2185
2274
 
2275
+ if self.model is None:
2276
+ raise RuntimeError("Model is required for this execution path.")
2277
+
2186
2278
  model_with_tools = self.model.bind_tools(self.resolved_tools) if self.resolved_tools else self.model
2187
2279
 
2188
2280
  ai_message = await model_with_tools.ainvoke(langchain_prompt, config)
@@ -2208,11 +2300,12 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2208
2300
  """
2209
2301
  try:
2210
2302
  tool_cfgs = metadata.get(TOOL_CONFIGS_KEY, {})
2211
- per_tool_config = tool_cfgs.get(MEMORY_SEARCH_TOOL_NAME)
2212
- if not isinstance(per_tool_config, dict):
2213
- per_tool_config = {}
2214
- per_tool_config["user_id"] = memory_user_id
2215
- tool_cfgs[MEMORY_SEARCH_TOOL_NAME] = per_tool_config
2303
+ for tool_name in (MEMORY_SEARCH_TOOL_NAME, MEMORY_DELETE_TOOL_NAME):
2304
+ per_tool_config = tool_cfgs.get(tool_name)
2305
+ if not isinstance(per_tool_config, dict):
2306
+ per_tool_config = {}
2307
+ per_tool_config["user_id"] = memory_user_id
2308
+ tool_cfgs[tool_name] = per_tool_config
2216
2309
  metadata[TOOL_CONFIGS_KEY] = tool_cfgs
2217
2310
  except Exception as e:
2218
2311
  # Non-fatal; metadata injection is best-effort
@@ -2576,6 +2669,220 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2576
2669
  if current_thread_id:
2577
2670
  self._pii_handlers_by_thread.pop(current_thread_id, None)
2578
2671
 
2672
+ # ==========================================================================
2673
+ # Programmatic Tool Calling (PTC) Methods
2674
+ # ==========================================================================
2675
+
2676
+ def add_mcp_server(self, mcp_config: dict[str, dict[str, Any]]) -> None:
2677
+ """Add MCP servers and refresh PTC tool state if needed."""
2678
+ super().add_mcp_server(mcp_config)
2679
+
2680
+ if not self._ptc_config or not self._ptc_config.enabled:
2681
+ return
2682
+
2683
+ if self._ptc_tool is not None:
2684
+ self._ptc_tool = None
2685
+
2686
+ self._ptc_tool_synced = False
2687
+ logger.debug(f"Agent '{self.name}': PTC tool will resync after MCP changes")
2688
+
2689
+ def enable_ptc(self, config: PTCSandboxConfig | None = None) -> None:
2690
+ """Enable Programmatic Tool Calling (PTC) for this agent.
2691
+
2692
+ PTC allows the LLM to execute Python code that calls MCP tools
2693
+ programmatically inside a sandboxed environment. This is useful for
2694
+ chaining multiple tool calls with local data processing.
2695
+
2696
+ The execute_ptc_code tool is automatically added to the agent's tools
2697
+ after MCP servers are configured. If no MCP servers are configured,
2698
+ the tool sync is deferred until servers are added.
2699
+
2700
+ Args:
2701
+ config: Optional configuration for PTC sandbox execution.
2702
+ See PTCSandboxConfig for options like enabled flag and sandbox_timeout.
2703
+ If None is passed, a default config with enabled=True will be created.
2704
+
2705
+ Example:
2706
+ agent.enable_ptc(PTCSandboxConfig(enabled=True))
2707
+ agent.add_mcp_server({"yfinance": {...}})
2708
+ # execute_ptc_code tool is now available
2709
+
2710
+ Note:
2711
+ PTC can also be enabled via the constructor by passing
2712
+ ptc_config=PTCSandboxConfig(enabled=True, ...).
2713
+ """
2714
+ # Lazy import to avoid circular dependencies
2715
+ from aip_agents.ptc.executor import PTCSandboxConfig
2716
+
2717
+ self._ptc_config = config or PTCSandboxConfig()
2718
+ self._ptc_config.enabled = True
2719
+ self._ptc_tool_synced = False
2720
+
2721
+ logger.info(f"Agent '{self.name}': PTC enabled")
2722
+
2723
+ # Attempt to sync PTC tool if MCP client is available
2724
+ self._sync_ptc_tool()
2725
+
2726
+ def _enrich_custom_tool_metadata(self) -> None:
2727
+ """Enrich custom tool definitions with metadata from agent's tools.
2728
+
2729
+ This method matches custom tool definitions in ptc_config with actual tool
2730
+ objects from self.tools and enriches them with description and input_schema.
2731
+ Called at PTC sync time to ensure tool definitions have accurate metadata.
2732
+
2733
+ The matching is done by comparing tool names (both original and sanitized).
2734
+
2735
+ Note: This method modifies self._ptc_config.custom_tools.tools in-place.
2736
+ """
2737
+ if not self._ptc_config:
2738
+ return
2739
+
2740
+ # Lazy import to avoid circular dependencies
2741
+ from aip_agents.ptc import enrich_custom_tools_from_agent
2742
+
2743
+ enrich_custom_tools_from_agent(
2744
+ self._ptc_config.custom_tools,
2745
+ self.tools,
2746
+ agent_name=self.name,
2747
+ )
2748
+
2749
+ def _sync_ptc_tool(self) -> None:
2750
+ """Build and register the execute_ptc_code tool when MCP or custom tools are available.
2751
+
2752
+ This method is called after enable_ptc() and after MCP servers are added.
2753
+ It creates the execute_ptc_code tool using the current MCP client
2754
+ configuration and/or custom tools config and adds it to the agent's resolved tools.
2755
+
2756
+ The tool is only created once. Subsequent calls are no-ops if the tool
2757
+ has already been synced.
2758
+ """
2759
+ if not self._ptc_config or not self._ptc_config.enabled:
2760
+ return
2761
+
2762
+ if self._ptc_tool_synced:
2763
+ return
2764
+
2765
+ # Check if we have custom tools enabled
2766
+ has_custom_tools = self._ptc_config.custom_tools.enabled and self._ptc_config.custom_tools.tools
2767
+ has_mcp_servers = bool(self.mcp_config)
2768
+
2769
+ if has_mcp_servers:
2770
+ if not self.mcp_client:
2771
+ logger.debug(f"Agent '{self.name}': PTC tool sync deferred - no MCP client yet")
2772
+ return
2773
+
2774
+ if not self.mcp_client.is_initialized:
2775
+ logger.debug(f"Agent '{self.name}': PTC tool sync deferred - MCP client not initialized")
2776
+ return
2777
+
2778
+ mcp_client_to_use = self.mcp_client
2779
+ elif has_custom_tools:
2780
+ mcp_client_to_use = None
2781
+ else:
2782
+ return
2783
+
2784
+ # Enrich custom tool definitions with metadata from actual tool objects
2785
+ if has_custom_tools:
2786
+ self._enrich_custom_tool_metadata()
2787
+
2788
+ # Lazy import to avoid circular dependencies
2789
+ from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool
2790
+
2791
+ if has_custom_tools and not mcp_client_to_use:
2792
+ logger.info(f"Agent '{self.name}': Syncing PTC tool with custom tools only (no MCP)")
2793
+ else:
2794
+ logger.info(f"Agent '{self.name}': Syncing PTC tool with MCP client")
2795
+
2796
+ # Create the execute_ptc_code tool with agent's tool configs
2797
+ self._ptc_tool = create_execute_ptc_code_tool(
2798
+ mcp_client_to_use, self._ptc_config, agent_tool_configs=self.tool_configs
2799
+ )
2800
+
2801
+ # Rebuild graph to include PTC tool
2802
+ self._rebuild_graph()
2803
+
2804
+ self._ptc_tool_synced = True
2805
+ logger.info(f"Agent '{self.name}': PTC tool synced successfully")
2806
+
2807
+ # Sync PTC prompt guidance
2808
+ self._sync_ptc_prompt()
2809
+
2810
+ def _sync_ptc_prompt(self) -> None:
2811
+ """Sync PTC usage guidance into the agent instruction.
2812
+
2813
+ This method builds and injects a PTC usage block into the agent's
2814
+ instruction when PTC is enabled. The prompt is refreshed when MCP
2815
+ or custom tools configuration changes (detected via hash).
2816
+ """
2817
+ if not self._ptc_config or not self._ptc_config.enabled:
2818
+ return
2819
+
2820
+ # Check if we have custom tools enabled
2821
+ has_custom_tools = self._ptc_config.custom_tools.enabled and self._ptc_config.custom_tools.tools
2822
+
2823
+ # For custom-only configs, allow None mcp_client
2824
+ # For MCP-only or MCP+custom, require mcp_client
2825
+ if not has_custom_tools and not self.mcp_client:
2826
+ return
2827
+
2828
+ # Lazy import to avoid circular dependencies
2829
+ from aip_agents.ptc.prompt_builder import build_ptc_prompt, compute_ptc_prompt_hash
2830
+
2831
+ # Get prompt config and custom tools config from PTC sandbox config
2832
+ prompt_config = self._ptc_config.prompt if self._ptc_config else None
2833
+ custom_tools_config = self._ptc_config.custom_tools if self._ptc_config else None
2834
+
2835
+ # Use mcp_client if available, None for custom-only
2836
+ mcp_client_to_use = self.mcp_client if self.mcp_client else None
2837
+
2838
+ # Check if MCP or custom tools config has changed
2839
+ current_hash = compute_ptc_prompt_hash(
2840
+ mcp_client_to_use, config=prompt_config, custom_tools_config=custom_tools_config
2841
+ )
2842
+ if current_hash == self._ptc_prompt_hash:
2843
+ logger.debug(f"Agent '{self.name}': PTC prompt unchanged, skipping refresh")
2844
+ return
2845
+
2846
+ # Build and inject the prompt
2847
+ ptc_prompt = build_ptc_prompt(mcp_client_to_use, config=prompt_config, custom_tools_config=custom_tools_config)
2848
+
2849
+ # Rebuild instruction from original + PTC guidance
2850
+ self.instruction = f"{self._original_instruction}\n\n{ptc_prompt}"
2851
+ self._ptc_prompt_hash = current_hash
2852
+
2853
+ logger.info(f"Agent '{self.name}': PTC prompt guidance injected")
2854
+
2855
+ async def _register_mcp_tools(self) -> None:
2856
+ """Override to sync PTC tool after MCP tools are registered.
2857
+
2858
+ This extends the base implementation to ensure the execute_ptc_code
2859
+ tool is added after MCP servers are initialized.
2860
+ """
2861
+ await super()._register_mcp_tools()
2862
+
2863
+ # Sync PTC tool after MCP tools are registered
2864
+ if self._ptc_config and self._ptc_config.enabled and not self._ptc_tool_synced:
2865
+ self._sync_ptc_tool()
2866
+
2867
+ async def cleanup(self) -> None:
2868
+ """Cleanup agent resources including PTC sandbox.
2869
+
2870
+ Extends base cleanup to also cleanup the PTC sandbox runtime if
2871
+ execute_ptc_code tool was created.
2872
+ """
2873
+ # Cleanup PTC tool's sandbox runtime if present
2874
+ if self._ptc_tool is not None:
2875
+ try:
2876
+ cleanup_method = getattr(self._ptc_tool, "cleanup", None)
2877
+ if cleanup_method and callable(cleanup_method):
2878
+ await cleanup_method()
2879
+ logger.debug(f"Agent '{self.name}': PTC sandbox cleanup completed")
2880
+ except Exception as e:
2881
+ logger.warning(f"Agent '{self.name}': Error during PTC sandbox cleanup: {e}")
2882
+
2883
+ # Call parent cleanup for MCP client
2884
+ await super().cleanup()
2885
+
2579
2886
  def _format_graph_output(self, final_state_result: dict[str, Any]) -> Any:
2580
2887
  """Convert final graph state to user-friendly output.
2581
2888
 
@@ -6,11 +6,12 @@ from aip_agents.guardrails.manager import GuardrailManager as GuardrailManager
6
6
  from aip_agents.middleware.base import AgentMiddleware as AgentMiddleware, ModelRequest as ModelRequest
7
7
  from aip_agents.middleware.manager import MiddlewareManager as MiddlewareManager
8
8
  from aip_agents.middleware.todolist import TodoList as TodoList, TodoListMiddleware as TodoListMiddleware
9
+ from aip_agents.ptc import PTCSandboxConfig as PTCSandboxConfig
9
10
  from aip_agents.schema.a2a import A2AStreamEventType as A2AStreamEventType
10
11
  from aip_agents.schema.hitl import ApprovalDecision as ApprovalDecision, HitlMetadata as HitlMetadata
11
12
  from aip_agents.schema.langgraph import ToolCallResult as ToolCallResult, ToolStorageParams as ToolStorageParams
12
13
  from aip_agents.schema.step_limit import MaxStepsExceededError as MaxStepsExceededError, StepLimitConfig as StepLimitConfig
13
- from aip_agents.tools.memory_search_tool import MEMORY_SEARCH_TOOL_NAME as MEMORY_SEARCH_TOOL_NAME
14
+ from aip_agents.tools.memory_search_tool import MEMORY_DELETE_TOOL_NAME as MEMORY_DELETE_TOOL_NAME, MEMORY_SEARCH_TOOL_NAME as MEMORY_SEARCH_TOOL_NAME
14
15
  from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY as TOOL_CONFIGS_KEY
15
16
  from aip_agents.utils import add_references_chunks as add_references_chunks
16
17
  from aip_agents.utils.langgraph import convert_langchain_messages_to_gllm_messages as convert_langchain_messages_to_gllm_messages, convert_lm_output_to_langchain_message as convert_lm_output_to_langchain_message
@@ -86,7 +87,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
86
87
  """
87
88
  tool_output_manager: Incomplete
88
89
  step_limit_config: Incomplete
89
- def __init__(self, name: str, instruction: str = ..., model: BaseChatModel | str | Any | None = None, tools: Sequence[BaseTool] | None = None, agents: Sequence[Any] | None = None, description: str | None = None, thread_id_key: str = 'thread_id', event_emitter: EventEmitter | None = None, tool_output_manager: ToolOutputManager | None = None, planning: bool = False, middlewares: Sequence[AgentMiddleware] | None = None, guardrail: GuardrailManager | None = None, step_limit_config: StepLimitConfig | None = None, **kwargs: Any) -> None:
90
+ def __init__(self, name: str, instruction: str = ..., model: BaseChatModel | str | Any | None = None, tools: Sequence[BaseTool] | None = None, agents: Sequence[Any] | None = None, description: str | None = None, thread_id_key: str = 'thread_id', event_emitter: EventEmitter | None = None, tool_output_manager: ToolOutputManager | None = None, planning: bool = False, middlewares: Sequence[AgentMiddleware] | None = None, guardrail: GuardrailManager | None = None, step_limit_config: StepLimitConfig | None = None, ptc_config: PTCSandboxConfig | None = None, **kwargs: Any) -> None:
90
91
  """Initialize the LangGraph ReAct Agent.
91
92
 
92
93
  Args:
@@ -113,6 +114,11 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
113
114
  input/output filtering during agent execution.
114
115
  enable_pii: Optional toggle to enable PII handling for tool inputs and outputs.
115
116
  step_limit_config: Optional configuration for step limits and delegation depth.
117
+ ptc_config: Optional configuration for PTC sandbox execution. See PTCSandboxConfig
118
+ for available options including enabled flag, sandbox timeout, and template settings.
119
+ PTC is enabled when ptc_config is not None and ptc_config.enabled is True.
120
+ When enabled, prompt guidance is automatically injected into the agent's instruction.
121
+ PTC runs in a sandbox only; there is no in-process trusted PTC path.
116
122
  **kwargs: Additional keyword arguments passed to BaseLangGraphAgent.
117
123
  """
118
124
  def define_graph(self, graph_builder: StateGraph) -> CompiledStateGraph:
@@ -124,6 +130,39 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
124
130
  Returns:
125
131
  Compiled LangGraph ready for execution.
126
132
  """
133
+ def add_mcp_server(self, mcp_config: dict[str, dict[str, Any]]) -> None:
134
+ """Add MCP servers and refresh PTC tool state if needed."""
135
+ def enable_ptc(self, config: PTCSandboxConfig | None = None) -> None:
136
+ '''Enable Programmatic Tool Calling (PTC) for this agent.
137
+
138
+ PTC allows the LLM to execute Python code that calls MCP tools
139
+ programmatically inside a sandboxed environment. This is useful for
140
+ chaining multiple tool calls with local data processing.
141
+
142
+ The execute_ptc_code tool is automatically added to the agent\'s tools
143
+ after MCP servers are configured. If no MCP servers are configured,
144
+ the tool sync is deferred until servers are added.
145
+
146
+ Args:
147
+ config: Optional configuration for PTC sandbox execution.
148
+ See PTCSandboxConfig for options like enabled flag and sandbox_timeout.
149
+ If None is passed, a default config with enabled=True will be created.
150
+
151
+ Example:
152
+ agent.enable_ptc(PTCSandboxConfig(enabled=True))
153
+ agent.add_mcp_server({"yfinance": {...}})
154
+ # execute_ptc_code tool is now available
155
+
156
+ Note:
157
+ PTC can also be enabled via the constructor by passing
158
+ ptc_config=PTCSandboxConfig(enabled=True, ...).
159
+ '''
160
+ async def cleanup(self) -> None:
161
+ """Cleanup agent resources including PTC sandbox.
162
+
163
+ Extends base cleanup to also cleanup the PTC sandbox runtime if
164
+ execute_ptc_code tool was created.
165
+ """
127
166
 
128
167
  class LangGraphAgent(LangGraphReactAgent):
129
168
  """Alias for LangGraphReactAgent."""
@@ -0,0 +1,49 @@
1
+ """Minimal PTC hello world example.
2
+
3
+ Required environment variables:
4
+ - OPENAI_API_KEY
5
+ - E2B_API_KEY
6
+ """
7
+
8
+ import asyncio
9
+
10
+ from aip_agents.agent import LangGraphReactAgent
11
+ from aip_agents.ptc import PromptConfig, PTCSandboxConfig
12
+
13
+
14
+ async def main() -> None:
15
+ """Run a hello-world PTC flow."""
16
+ instruction = (
17
+ "You are a helpful assistant with access to execute_ptc_code. "
18
+ "Use execute_ptc_code to run Python and print output. "
19
+ "The tool returns JSON with ok/stdout/stderr/exit_code."
20
+ )
21
+
22
+ agent = LangGraphReactAgent(
23
+ name="ptc_hello_world",
24
+ instruction=instruction,
25
+ model="openai/gpt-5.2",
26
+ ptc_config=PTCSandboxConfig(enabled=True, sandbox_timeout=180.0, prompt=PromptConfig(mode="index")),
27
+ )
28
+ agent.add_mcp_server(
29
+ {
30
+ "deepwiki": {
31
+ "transport": "streamable-http",
32
+ "url": "https://mcp.deepwiki.com/mcp",
33
+ "headers": {},
34
+ "timeout": 60.0,
35
+ }
36
+ }
37
+ )
38
+
39
+ try:
40
+ response = await agent.arun(
41
+ query="Use execute_ptc_code to print 'Hello, world!' and count the number of words in the output of deepwiki.read_wiki_structure('anthropics/claude-code')."
42
+ )
43
+ print("execute_ptc_code output:", response["output"])
44
+ finally:
45
+ await agent.cleanup()
46
+
47
+
48
+ if __name__ == "__main__":
49
+ asyncio.run(main())
@@ -0,0 +1,5 @@
1
+ from aip_agents.agent import LangGraphReactAgent as LangGraphReactAgent
2
+ from aip_agents.ptc import PTCSandboxConfig as PTCSandboxConfig, PromptConfig as PromptConfig
3
+
4
+ async def main() -> None:
5
+ """Run a hello-world PTC flow."""