aip-agents-binary 0.5.21__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 (149) hide show
  1. aip_agents/agent/__init__.py +44 -4
  2. aip_agents/agent/base_langgraph_agent.py +169 -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 +424 -35
  7. aip_agents/agent/langgraph_react_agent.pyi +46 -2
  8. aip_agents/examples/{hello_world_langgraph_bosa_twitter.py → hello_world_langgraph_gl_connector_twitter.py} +10 -7
  9. aip_agents/examples/hello_world_langgraph_gl_connector_twitter.pyi +5 -0
  10. aip_agents/examples/hello_world_ptc.py +49 -0
  11. aip_agents/examples/hello_world_ptc.pyi +5 -0
  12. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  13. aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
  14. aip_agents/examples/hello_world_sentry.py +2 -2
  15. aip_agents/examples/hello_world_tool_output_client.py +9 -0
  16. aip_agents/examples/tools/multiply_tool.py +43 -0
  17. aip_agents/examples/tools/multiply_tool.pyi +18 -0
  18. aip_agents/guardrails/__init__.py +83 -0
  19. aip_agents/guardrails/__init__.pyi +6 -0
  20. aip_agents/guardrails/engines/__init__.py +69 -0
  21. aip_agents/guardrails/engines/__init__.pyi +4 -0
  22. aip_agents/guardrails/engines/base.py +90 -0
  23. aip_agents/guardrails/engines/base.pyi +61 -0
  24. aip_agents/guardrails/engines/nemo.py +101 -0
  25. aip_agents/guardrails/engines/nemo.pyi +46 -0
  26. aip_agents/guardrails/engines/phrase_matcher.py +113 -0
  27. aip_agents/guardrails/engines/phrase_matcher.pyi +48 -0
  28. aip_agents/guardrails/exceptions.py +39 -0
  29. aip_agents/guardrails/exceptions.pyi +23 -0
  30. aip_agents/guardrails/manager.py +163 -0
  31. aip_agents/guardrails/manager.pyi +42 -0
  32. aip_agents/guardrails/middleware.py +199 -0
  33. aip_agents/guardrails/middleware.pyi +87 -0
  34. aip_agents/guardrails/schemas.py +63 -0
  35. aip_agents/guardrails/schemas.pyi +43 -0
  36. aip_agents/guardrails/utils.py +45 -0
  37. aip_agents/guardrails/utils.pyi +19 -0
  38. aip_agents/mcp/client/__init__.py +38 -2
  39. aip_agents/mcp/client/connection_manager.py +36 -1
  40. aip_agents/mcp/client/connection_manager.pyi +3 -0
  41. aip_agents/mcp/client/persistent_session.py +318 -65
  42. aip_agents/mcp/client/persistent_session.pyi +9 -0
  43. aip_agents/mcp/client/transports.py +52 -4
  44. aip_agents/mcp/client/transports.pyi +9 -0
  45. aip_agents/memory/adapters/base_adapter.py +98 -0
  46. aip_agents/memory/adapters/base_adapter.pyi +25 -0
  47. aip_agents/middleware/base.py +8 -0
  48. aip_agents/middleware/base.pyi +4 -0
  49. aip_agents/middleware/manager.py +22 -0
  50. aip_agents/middleware/manager.pyi +4 -0
  51. aip_agents/ptc/__init__.py +87 -0
  52. aip_agents/ptc/__init__.pyi +14 -0
  53. aip_agents/ptc/custom_tools.py +473 -0
  54. aip_agents/ptc/custom_tools.pyi +184 -0
  55. aip_agents/ptc/custom_tools_payload.py +400 -0
  56. aip_agents/ptc/custom_tools_payload.pyi +31 -0
  57. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  58. aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
  59. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
  60. aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
  61. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
  62. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
  63. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  64. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
  65. aip_agents/ptc/doc_gen.py +122 -0
  66. aip_agents/ptc/doc_gen.pyi +40 -0
  67. aip_agents/ptc/exceptions.py +57 -0
  68. aip_agents/ptc/exceptions.pyi +37 -0
  69. aip_agents/ptc/executor.py +261 -0
  70. aip_agents/ptc/executor.pyi +99 -0
  71. aip_agents/ptc/mcp/__init__.py +45 -0
  72. aip_agents/ptc/mcp/__init__.pyi +7 -0
  73. aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
  74. aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
  75. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  76. aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
  77. aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
  78. aip_agents/ptc/naming.py +196 -0
  79. aip_agents/ptc/naming.pyi +85 -0
  80. aip_agents/ptc/payload.py +26 -0
  81. aip_agents/ptc/payload.pyi +15 -0
  82. aip_agents/ptc/prompt_builder.py +673 -0
  83. aip_agents/ptc/prompt_builder.pyi +59 -0
  84. aip_agents/ptc/ptc_helper.py +16 -0
  85. aip_agents/ptc/ptc_helper.pyi +1 -0
  86. aip_agents/ptc/sandbox_bridge.py +256 -0
  87. aip_agents/ptc/sandbox_bridge.pyi +38 -0
  88. aip_agents/ptc/template_utils.py +33 -0
  89. aip_agents/ptc/template_utils.pyi +13 -0
  90. aip_agents/ptc/templates/__init__.py +1 -0
  91. aip_agents/ptc/templates/__init__.pyi +0 -0
  92. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  93. aip_agents/ptc/tool_def_helpers.py +101 -0
  94. aip_agents/ptc/tool_def_helpers.pyi +38 -0
  95. aip_agents/ptc/tool_enrichment.py +163 -0
  96. aip_agents/ptc/tool_enrichment.pyi +60 -0
  97. aip_agents/sandbox/__init__.py +43 -0
  98. aip_agents/sandbox/__init__.pyi +5 -0
  99. aip_agents/sandbox/defaults.py +205 -0
  100. aip_agents/sandbox/defaults.pyi +30 -0
  101. aip_agents/sandbox/e2b_runtime.py +295 -0
  102. aip_agents/sandbox/e2b_runtime.pyi +57 -0
  103. aip_agents/sandbox/template_builder.py +131 -0
  104. aip_agents/sandbox/template_builder.pyi +36 -0
  105. aip_agents/sandbox/types.py +24 -0
  106. aip_agents/sandbox/types.pyi +14 -0
  107. aip_agents/sandbox/validation.py +50 -0
  108. aip_agents/sandbox/validation.pyi +20 -0
  109. aip_agents/sentry/__init__.py +1 -1
  110. aip_agents/sentry/sentry.py +33 -12
  111. aip_agents/sentry/sentry.pyi +5 -4
  112. aip_agents/tools/__init__.py +20 -3
  113. aip_agents/tools/__init__.pyi +4 -2
  114. aip_agents/tools/browser_use/browser_use_tool.py +8 -0
  115. aip_agents/tools/browser_use/streaming.py +2 -0
  116. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +80 -31
  117. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.pyi +25 -9
  118. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +6 -6
  119. aip_agents/tools/constants.py +24 -12
  120. aip_agents/tools/constants.pyi +14 -11
  121. aip_agents/tools/date_range_tool.py +554 -0
  122. aip_agents/tools/date_range_tool.pyi +21 -0
  123. aip_agents/tools/execute_ptc_code.py +357 -0
  124. aip_agents/tools/execute_ptc_code.pyi +90 -0
  125. aip_agents/tools/gl_connector/__init__.py +1 -1
  126. aip_agents/tools/gl_connector/tool.py +62 -30
  127. aip_agents/tools/gl_connector/tool.pyi +3 -3
  128. aip_agents/tools/gl_connector_tools.py +119 -0
  129. aip_agents/tools/gl_connector_tools.pyi +39 -0
  130. aip_agents/tools/memory_search/__init__.py +8 -1
  131. aip_agents/tools/memory_search/__init__.pyi +3 -3
  132. aip_agents/tools/memory_search/mem0.py +114 -1
  133. aip_agents/tools/memory_search/mem0.pyi +11 -1
  134. aip_agents/tools/memory_search/schema.py +33 -0
  135. aip_agents/tools/memory_search/schema.pyi +10 -0
  136. aip_agents/tools/memory_search_tool.py +8 -0
  137. aip_agents/tools/memory_search_tool.pyi +2 -2
  138. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
  139. aip_agents/utils/langgraph/tool_output_management.py +80 -0
  140. aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
  141. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +14 -22
  142. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +144 -58
  143. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/WHEEL +1 -1
  144. aip_agents/examples/demo_memory_recall.py +0 -401
  145. aip_agents/examples/demo_memory_recall.pyi +0 -58
  146. aip_agents/examples/hello_world_langgraph_bosa_twitter.pyi +0 -5
  147. aip_agents/tools/bosa_tools.py +0 -105
  148. aip_agents/tools/bosa_tools.pyi +0 -37
  149. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/top_level.txt +0 -0
@@ -6,8 +6,11 @@ Authors:
6
6
  Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
7
7
  Fachriza Adhiatma (fachriza.d.adhiatma@gdplabs.id)
8
8
  Raymond Christopher (raymond.christopher@gdplabs.id)
9
+ Reinhart Linanda (reinhart.linanda@gdplabs.id)
9
10
  """
10
11
 
12
+ from __future__ import annotations
13
+
11
14
  import asyncio
12
15
  import time
13
16
  import uuid
@@ -15,11 +18,14 @@ from collections.abc import Awaitable, Callable, Sequence
15
18
  from dataclasses import asdict, dataclass
16
19
  from functools import reduce
17
20
  from textwrap import dedent
18
- from typing import Annotated, Any
21
+ from typing import TYPE_CHECKING, Annotated, Any, cast
22
+
23
+ from deprecated import deprecated # type: ignore[import-untyped]
19
24
 
20
- from deprecated import deprecated
21
- from gllm_core.event import EventEmitter
22
- from gllm_core.schema import Chunk
25
+ if TYPE_CHECKING:
26
+ from aip_agents.guardrails.manager import GuardrailManager
27
+ from gllm_core.event import EventEmitter # type: ignore[import-untyped]
28
+ from gllm_core.schema import Chunk # type: ignore[import-untyped]
23
29
  from langchain_core.language_models import BaseChatModel
24
30
  from langchain_core.messages import (
25
31
  AIMessage,
@@ -48,7 +54,7 @@ from aip_agents.schema.a2a import A2AStreamEventType
48
54
  from aip_agents.schema.hitl import ApprovalDecision, HitlMetadata
49
55
  from aip_agents.schema.langgraph import ToolCallResult, ToolStorageParams
50
56
  from aip_agents.schema.step_limit import MaxStepsExceededError, StepLimitConfig
51
- 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
52
58
  from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY
53
59
  from aip_agents.utils import add_references_chunks
54
60
  from aip_agents.utils.langgraph import (
@@ -81,6 +87,9 @@ from aip_agents.utils.token_usage_helper import (
81
87
  extract_token_usage_from_tool_output,
82
88
  )
83
89
 
90
+ if TYPE_CHECKING:
91
+ from aip_agents.ptc import PTCSandboxConfig
92
+
84
93
  logger = get_logger(__name__)
85
94
 
86
95
  # Default instruction for ReAct agents
@@ -157,7 +166,9 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
157
166
  tool_output_manager: ToolOutputManager | None = None,
158
167
  planning: bool = False,
159
168
  middlewares: Sequence[AgentMiddleware] | None = None,
169
+ guardrail: GuardrailManager | None = None,
160
170
  step_limit_config: StepLimitConfig | None = None,
171
+ ptc_config: PTCSandboxConfig | None = None,
161
172
  **kwargs: Any,
162
173
  ):
163
174
  """Initialize the LangGraph ReAct Agent.
@@ -178,10 +189,19 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
178
189
  planning: Enable planning capabilities with TodoListMiddleware. Defaults to False.
179
190
  middlewares: Optional sequence of custom middleware to COMPOSE (not override) with built-in middleware.
180
191
  Execution order: [TodoListMiddleware (if planning=True),
192
+ GuardrailMiddleware (if guardrail provided),
181
193
  ...custom middlewares in order provided]
182
194
  All middleware hooks execute - this extends capabilities, never replaces them.
195
+ guardrail: Optional GuardrailManager for content filtering and safety checks.
196
+ When provided, automatically wraps in GuardrailMiddleware for transparent
197
+ input/output filtering during agent execution.
183
198
  enable_pii: Optional toggle to enable PII handling for tool inputs and outputs.
184
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.
185
205
  **kwargs: Additional keyword arguments passed to BaseLangGraphAgent.
186
206
  """
187
207
  # Use LangGraph's standard AgentState for ReAct
@@ -201,6 +221,12 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
201
221
  **kwargs,
202
222
  )
203
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
+
204
230
  # Handle tool output management
205
231
  self.tool_output_manager = tool_output_manager
206
232
  self._pii_handlers_by_thread: dict[str, ToolPIIHandler] = {}
@@ -212,6 +238,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
212
238
  # Setup middleware
213
239
  self._middleware_manager = self._setup_middleware(
214
240
  planning=planning,
241
+ guardrail=guardrail,
215
242
  custom_middlewares=middlewares,
216
243
  )
217
244
 
@@ -221,18 +248,32 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
221
248
 
222
249
  self.step_limit_config = step_limit_config
223
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
+
224
263
  def _setup_middleware(
225
264
  self,
226
265
  planning: bool,
266
+ guardrail: GuardrailManager | None,
227
267
  custom_middlewares: Sequence[AgentMiddleware] | None,
228
268
  ) -> MiddlewareManager | None:
229
269
  """Setup middleware based on configuration.
230
270
 
231
- Creates auto-configured middleware (planning) and composes
271
+ Creates auto-configured middleware (planning, guardrails) and composes
232
272
  with custom middleware if provided.
233
273
 
234
274
  Args:
235
275
  planning: Whether to enable TodoListMiddleware.
276
+ guardrail: Optional GuardrailManager to wrap in GuardrailMiddleware.
236
277
  custom_middlewares: Optional custom middlewares to append.
237
278
 
238
279
  Returns:
@@ -242,7 +283,13 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
242
283
 
243
284
  # Auto-configure TodoListMiddleware if planning enabled
244
285
  if planning:
245
- middleware_list.append(TodoListMiddleware())
286
+ middleware_list.append(cast(AgentMiddleware, TodoListMiddleware()))
287
+
288
+ # Auto-configure GuardrailMiddleware if guardrail provided
289
+ if guardrail:
290
+ from aip_agents.guardrails.middleware import GuardrailMiddleware
291
+
292
+ middleware_list.append(GuardrailMiddleware(guardrail))
246
293
 
247
294
  # Append custom middlewares
248
295
  if custom_middlewares:
@@ -399,9 +446,9 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
399
446
  )
400
447
 
401
448
  def _rebuild_resolved_tools(self) -> None:
402
- """Rebuild resolved tools including middleware tools.
449
+ """Rebuild resolved tools including middleware and PTC tools.
403
450
 
404
- Overrides base class to ensure middleware tools are preserved
451
+ Overrides base class to ensure middleware tools and the PTC tool are preserved
405
452
  when tools are rebuilt (e.g., after update_regular_tools).
406
453
  """
407
454
  # Call base class to rebuild with regular, a2a, delegation, and mcp tools
@@ -411,6 +458,10 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
411
458
  if hasattr(self, "_middleware_tools") and self._middleware_tools:
412
459
  self.resolved_tools.extend(self._middleware_tools)
413
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
+
414
465
  def _handle_tool_artifacts(
415
466
  self, tool_output: Any, pending_artifacts: list[dict[str, Any]]
416
467
  ) -> tuple[str, list[dict[str, Any]]]:
@@ -533,6 +584,29 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
533
584
 
534
585
  return memory_node
535
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
+
536
610
  def _extract_user_query_from_messages(self, messages: list[Any]) -> str | None:
537
611
  """Get latest user query string from a list of messages.
538
612
 
@@ -579,14 +653,29 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
579
653
  current_messages = state["messages"]
580
654
 
581
655
  # Execute LLM call
582
- if self.lm_invoker:
583
- result = await self._handle_lm_invoker_call(current_messages, state, config)
584
- elif isinstance(self.model, BaseChatModel):
585
- result = await self._handle_langchain_model_call(current_messages, state, config)
586
- else:
587
- raise ValueError(
588
- f"Agent '{self.name}': No valid LMInvoker or LangChain model configured for ReAct agent node."
589
- )
656
+ try:
657
+ if self.lm_invoker:
658
+ result = await self._handle_lm_invoker_call(current_messages, state, config)
659
+ elif isinstance(self.model, BaseChatModel):
660
+ result = await self._handle_langchain_model_call(current_messages, state, config)
661
+ else:
662
+ raise ValueError(
663
+ f"Agent '{self.name}': No valid LMInvoker or LangChain model configured for ReAct agent node."
664
+ )
665
+ except Exception as e:
666
+ # Lazy import to support optional guardrails dependency
667
+ from aip_agents.guardrails.exceptions import GuardrailViolationError
668
+
669
+ if isinstance(e, GuardrailViolationError):
670
+ return {
671
+ "messages": [
672
+ AIMessage(
673
+ content=f"⚠️ Guardrail violation: {e.result.reason}",
674
+ response_metadata={"finish_reason": "stop"},
675
+ )
676
+ ]
677
+ }
678
+ raise
590
679
 
591
680
  # Increment step counter after successful execution
592
681
  manager.increment_step()
@@ -697,7 +786,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
697
786
  pending_artifacts: list[dict[str, Any]] = state.get("artifacts") or []
698
787
  reference_updates: list[Chunk] = []
699
788
  tool_map = {tool.name: tool for tool in self.resolved_tools}
700
- pii_mapping = {}
789
+ pii_mapping: dict[str, str] = {}
701
790
 
702
791
  aggregated_metadata_delta: dict[str, Any] = {}
703
792
  total_tools_token_usage: list[UsageMetadata] = []
@@ -721,7 +810,8 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
721
810
  ),
722
811
  )
723
812
 
724
- 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]
725
815
 
726
816
  for coro in asyncio.as_completed(tasks):
727
817
  tool_result = await coro
@@ -744,6 +834,31 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
744
834
  pii_mapping,
745
835
  )
746
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
+
747
862
  def _accumulate_tool_result( # noqa: PLR0913
748
863
  self,
749
864
  tool_result: Any,
@@ -752,7 +867,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
752
867
  aggregated_metadata_delta: dict[str, Any],
753
868
  reference_updates: list[Chunk],
754
869
  total_tools_token_usage: list[UsageMetadata],
755
- pii_mapping: dict[str, str] | None,
870
+ pii_mapping: dict[str, str],
756
871
  ) -> None: # noqa: PLR0913
757
872
  """Accumulate results from a single tool call.
758
873
 
@@ -1198,13 +1313,16 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1198
1313
 
1199
1314
  # Create enhanced tool configuration with output management
1200
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
1201
1319
 
1202
1320
  arun_streaming_method = getattr(tool, TOOL_RUN_STREAMING_METHOD, None)
1203
1321
 
1204
1322
  if arun_streaming_method and callable(arun_streaming_method):
1205
1323
  tool_output = await self._execute_tool_with_streaming(tool, tool_call, tool_config)
1206
1324
  else:
1207
- tool_output = await tool.ainvoke(resolved_args, tool_config)
1325
+ tool_output = await tool.ainvoke(resolved_args, tool_config_runnable)
1208
1326
 
1209
1327
  references = extract_references_from_tool(tool, tool_output)
1210
1328
 
@@ -1478,7 +1596,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1478
1596
  tool_call: dict[str, Any],
1479
1597
  execution_time: float,
1480
1598
  pending_artifacts: list[dict[str, Any]],
1481
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1599
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1482
1600
  """Process tool output into messages, artifacts, and metadata.
1483
1601
 
1484
1602
  Args:
@@ -1506,7 +1624,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1506
1624
 
1507
1625
  def _handle_command_output(
1508
1626
  self, tool_output: Command, tool_call: dict[str, Any], execution_time: float, metadata_delta: dict[str, Any]
1509
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1627
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1510
1628
  """Handle Command type tool outputs.
1511
1629
 
1512
1630
  Args:
@@ -1535,7 +1653,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1535
1653
 
1536
1654
  def _handle_string_output(
1537
1655
  self, tool_output: str, tool_call: dict[str, Any], execution_time: float
1538
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1656
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1539
1657
  """Handle string type tool outputs.
1540
1658
 
1541
1659
  Args:
@@ -1561,7 +1679,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1561
1679
  execution_time: float,
1562
1680
  pending_artifacts: list[dict[str, Any]],
1563
1681
  metadata_delta: dict[str, Any],
1564
- ) -> tuple[list[BaseMessage], list[dict[str, Any]], dict[str, Any]]:
1682
+ ) -> tuple[list[ToolMessage], list[dict[str, Any]], dict[str, Any]]:
1565
1683
  """Handle legacy dict and other tool outputs.
1566
1684
 
1567
1685
  Args:
@@ -1659,8 +1777,11 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1659
1777
  self._emit_default_tool_call_event(writer, tool_name, tool_call_id, tool_args)
1660
1778
 
1661
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.")
1662
1783
 
1663
- async for chunk in tool.arun_streaming(**streaming_kwargs):
1784
+ async for chunk in arun_streaming_method(**streaming_kwargs):
1664
1785
  final_output, saw_tool_result = self._handle_streaming_chunk(
1665
1786
  chunk=chunk,
1666
1787
  writer=writer,
@@ -1954,6 +2075,47 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1954
2075
  )
1955
2076
  writer(a2a_event)
1956
2077
 
2078
+ async def _execute_abefore_model_hook(self, state: dict[str, Any]) -> None:
2079
+ """Asynchronously execute abefore_model middleware hook and update state.
2080
+
2081
+ Args:
2082
+ state: Current agent state to potentially update.
2083
+ """
2084
+ if self._middleware_manager:
2085
+ try:
2086
+ before_updates = await self._middleware_manager.abefore_model(state)
2087
+ if before_updates:
2088
+ state.update(before_updates)
2089
+ except Exception as e:
2090
+ # Lazy import to support optional guardrails dependency
2091
+ from aip_agents.guardrails.exceptions import GuardrailViolationError
2092
+
2093
+ if isinstance(e, GuardrailViolationError):
2094
+ # Re-raise guardrail violations to be caught by the agent node
2095
+ raise
2096
+ logger.error(f"Agent '{self.name}': Middleware abefore_model hook failed: {e}")
2097
+
2098
+ async def _execute_aafter_model_hook(self, state_updates: dict[str, Any], state: dict[str, Any]) -> None:
2099
+ """Asynchronously execute aafter_model middleware hook.
2100
+
2101
+ Args:
2102
+ state_updates: Updates to be merged into state.
2103
+ state: Current agent state for context.
2104
+ """
2105
+ if self._middleware_manager:
2106
+ try:
2107
+ after_updates = await self._middleware_manager.aafter_model(state)
2108
+ if after_updates:
2109
+ state_updates.update(after_updates)
2110
+ except Exception as e:
2111
+ # Lazy import to support optional guardrails dependency
2112
+ from aip_agents.guardrails.exceptions import GuardrailViolationError
2113
+
2114
+ if isinstance(e, GuardrailViolationError):
2115
+ # Re-raise guardrail violations
2116
+ raise
2117
+ logger.error(f"Agent '{self.name}': Middleware aafter_model hook failed: {e}")
2118
+
1957
2119
  def _execute_before_model_hook(self, state: dict[str, Any]) -> None:
1958
2120
  """Execute before_model middleware hook and update state.
1959
2121
 
@@ -1966,6 +2128,12 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
1966
2128
  if before_updates:
1967
2129
  state.update(before_updates)
1968
2130
  except Exception as e:
2131
+ # Lazy import to support optional guardrails dependency
2132
+ from aip_agents.guardrails.exceptions import GuardrailViolationError
2133
+
2134
+ if isinstance(e, GuardrailViolationError):
2135
+ # Re-raise guardrail violations to be caught by the agent node
2136
+ raise
1969
2137
  logger.error(f"Agent '{self.name}': Middleware before_model hook failed: {e}")
1970
2138
 
1971
2139
  def _execute_modify_model_request_hook(
@@ -2029,7 +2197,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2029
2197
  dict[str, Any]: A dictionary containing the new messages and updated token usage.
2030
2198
  """
2031
2199
  # Execute before_model middleware hook
2032
- self._execute_before_model_hook(state)
2200
+ await self._execute_abefore_model_hook(state)
2033
2201
 
2034
2202
  # Build tool output aware instruction
2035
2203
  enhanced_instruction = self._build_tool_output_aware_instruction(self.instruction, state, config)
@@ -2043,6 +2211,9 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2043
2211
 
2044
2212
  effective_event_emitter = state.get("event_emitter") or self.event_emitter
2045
2213
 
2214
+ if self.lm_invoker is None:
2215
+ raise RuntimeError("LM invoker is required for this execution path.")
2216
+
2046
2217
  if self.resolved_tools:
2047
2218
  self.lm_invoker.set_tools(self.resolved_tools)
2048
2219
 
@@ -2063,7 +2234,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2063
2234
  state_updates.update(token_usage_updates)
2064
2235
 
2065
2236
  # Execute after_model middleware hook
2066
- self._execute_after_model_hook(state_updates, state)
2237
+ await self._execute_aafter_model_hook(state_updates, state)
2067
2238
 
2068
2239
  return state_updates
2069
2240
 
@@ -2081,7 +2252,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2081
2252
  dict[str, Any]: A dictionary containing the new messages and updated token usage.
2082
2253
  """
2083
2254
  # Execute before_model middleware hook
2084
- self._execute_before_model_hook(state)
2255
+ await self._execute_abefore_model_hook(state)
2085
2256
 
2086
2257
  # Build tool output aware instruction
2087
2258
  enhanced_instruction = self._build_tool_output_aware_instruction(self.instruction, state, config)
@@ -2101,6 +2272,9 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2101
2272
  ):
2102
2273
  langchain_prompt = [SystemMessage(content=enhanced_instruction)] + list(current_messages)
2103
2274
 
2275
+ if self.model is None:
2276
+ raise RuntimeError("Model is required for this execution path.")
2277
+
2104
2278
  model_with_tools = self.model.bind_tools(self.resolved_tools) if self.resolved_tools else self.model
2105
2279
 
2106
2280
  ai_message = await model_with_tools.ainvoke(langchain_prompt, config)
@@ -2113,7 +2287,7 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2113
2287
  state_updates.update(token_usage_updates)
2114
2288
 
2115
2289
  # Execute after_model middleware hook
2116
- self._execute_after_model_hook(state_updates, state)
2290
+ await self._execute_aafter_model_hook(state_updates, state)
2117
2291
 
2118
2292
  return state_updates
2119
2293
 
@@ -2126,11 +2300,12 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2126
2300
  """
2127
2301
  try:
2128
2302
  tool_cfgs = metadata.get(TOOL_CONFIGS_KEY, {})
2129
- per_tool_config = tool_cfgs.get(MEMORY_SEARCH_TOOL_NAME)
2130
- if not isinstance(per_tool_config, dict):
2131
- per_tool_config = {}
2132
- per_tool_config["user_id"] = memory_user_id
2133
- 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
2134
2309
  metadata[TOOL_CONFIGS_KEY] = tool_cfgs
2135
2310
  except Exception as e:
2136
2311
  # Non-fatal; metadata injection is best-effort
@@ -2494,6 +2669,220 @@ class LangGraphReactAgent(LangGraphHitLMixin, BaseLangGraphAgent):
2494
2669
  if current_thread_id:
2495
2670
  self._pii_handlers_by_thread.pop(current_thread_id, None)
2496
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
+
2497
2886
  def _format_graph_output(self, final_state_result: dict[str, Any]) -> Any:
2498
2887
  """Convert final graph state to user-friendly output.
2499
2888