aip-agents-binary 0.0.0__py3-none-any.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 (447) hide show
  1. aip_agents/__init__.py +66 -0
  2. aip_agents/a2a/__init__.py +19 -0
  3. aip_agents/a2a/server/__init__.py +10 -0
  4. aip_agents/a2a/server/base_executor.py +1125 -0
  5. aip_agents/a2a/server/google_adk_executor.py +198 -0
  6. aip_agents/a2a/server/langflow_executor.py +180 -0
  7. aip_agents/a2a/server/langgraph_executor.py +270 -0
  8. aip_agents/a2a/types.py +232 -0
  9. aip_agents/a2ui/__init__.py +26 -0
  10. aip_agents/a2ui/catalog_provider.py +109 -0
  11. aip_agents/a2ui/examples/0.8/booking_form.json +1 -0
  12. aip_agents/a2ui/examples/0.8/confirmation.json +1 -0
  13. aip_agents/a2ui/examples/0.8/single_column_list.json +1 -0
  14. aip_agents/a2ui/examples/0.9/booking_form.json +1 -0
  15. aip_agents/a2ui/examples/0.9/confirmation.json +1 -0
  16. aip_agents/a2ui/examples/0.9/single_column_list.json +1 -0
  17. aip_agents/a2ui/middleware.py +754 -0
  18. aip_agents/a2ui/models.py +153 -0
  19. aip_agents/a2ui/public_url.py +138 -0
  20. aip_agents/a2ui/stream_filter.py +67 -0
  21. aip_agents/agent/__init__.py +72 -0
  22. aip_agents/agent/base_agent.py +1437 -0
  23. aip_agents/agent/base_langgraph_agent.py +4048 -0
  24. aip_agents/agent/google_adk_agent.py +932 -0
  25. aip_agents/agent/google_adk_constants.py +11 -0
  26. aip_agents/agent/graph_runtime.py +185 -0
  27. aip_agents/agent/hitl/__init__.py +24 -0
  28. aip_agents/agent/hitl/config.py +28 -0
  29. aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
  30. aip_agents/agent/hitl/manager.py +532 -0
  31. aip_agents/agent/hitl/models.py +18 -0
  32. aip_agents/agent/hitl/prompt/__init__.py +9 -0
  33. aip_agents/agent/hitl/prompt/base.py +42 -0
  34. aip_agents/agent/hitl/prompt/deferred.py +73 -0
  35. aip_agents/agent/hitl/registry.py +149 -0
  36. aip_agents/agent/interface.py +138 -0
  37. aip_agents/agent/interfaces.py +65 -0
  38. aip_agents/agent/langflow_agent.py +464 -0
  39. aip_agents/agent/langgraph_react_agent.py +3741 -0
  40. aip_agents/agent/model_invocation_adapter.py +346 -0
  41. aip_agents/agent/system_instruction_context.py +34 -0
  42. aip_agents/agent/tool_execution_engine.py +587 -0
  43. aip_agents/audio_interface/__init__.py +30 -0
  44. aip_agents/audio_interface/audio_agent_adapter.py +254 -0
  45. aip_agents/audio_interface/config.py +146 -0
  46. aip_agents/audio_interface/errors.py +15 -0
  47. aip_agents/audio_interface/livekit_audio_session.py +504 -0
  48. aip_agents/audio_interface/livekit_realtime_audio_session.py +142 -0
  49. aip_agents/audio_interface/session_factory.py +40 -0
  50. aip_agents/clients/__init__.py +10 -0
  51. aip_agents/clients/langflow/__init__.py +10 -0
  52. aip_agents/clients/langflow/client.py +477 -0
  53. aip_agents/clients/langflow/types.py +18 -0
  54. aip_agents/common/__init__.py +5 -0
  55. aip_agents/common/callable_introspection.py +17 -0
  56. aip_agents/constants.py +23 -0
  57. aip_agents/context/__init__.py +5 -0
  58. aip_agents/context/budget.py +237 -0
  59. aip_agents/context/compaction.py +339 -0
  60. aip_agents/context/metadata.py +169 -0
  61. aip_agents/context/offload.py +449 -0
  62. aip_agents/context/preparation.py +196 -0
  63. aip_agents/context/prompt.py +61 -0
  64. aip_agents/context/runtime.py +149 -0
  65. aip_agents/context/session_recall.py +69 -0
  66. aip_agents/context/state.py +48 -0
  67. aip_agents/context/utils.py +44 -0
  68. aip_agents/credentials/manager.py +132 -0
  69. aip_agents/examples/__init__.py +5 -0
  70. aip_agents/examples/audio_livekit_session.py +116 -0
  71. aip_agents/examples/compare_streaming_client.py +783 -0
  72. aip_agents/examples/compare_streaming_server.py +141 -0
  73. aip_agents/examples/gl_connectors_smoke.py +148 -0
  74. aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
  75. aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
  76. aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
  77. aip_agents/examples/hello_world_a2a_google_adk_server.py +78 -0
  78. aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
  79. aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
  80. aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
  81. aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
  82. aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
  83. aip_agents/examples/hello_world_a2a_langchain_reference_server.py +104 -0
  84. aip_agents/examples/hello_world_a2a_langchain_server.py +78 -0
  85. aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +77 -0
  86. aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
  87. aip_agents/examples/hello_world_a2a_langflow_server.py +80 -0
  88. aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
  89. aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
  90. aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +91 -0
  91. aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
  92. aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
  93. aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
  94. aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
  95. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
  96. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
  97. aip_agents/examples/hello_world_a2a_langgraph_server.py +83 -0
  98. aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +78 -0
  99. aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +131 -0
  100. aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
  101. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
  102. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +250 -0
  103. aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
  104. aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +79 -0
  105. aip_agents/examples/hello_world_filesystem.py +162 -0
  106. aip_agents/examples/hello_world_google_adk.py +41 -0
  107. aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
  108. aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
  109. aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
  110. aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
  111. aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
  112. aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
  113. aip_agents/examples/hello_world_google_adk_stream.py +44 -0
  114. aip_agents/examples/hello_world_langchain.py +28 -0
  115. aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
  116. aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
  117. aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
  118. aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
  119. aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
  120. aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
  121. aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
  122. aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
  123. aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
  124. aip_agents/examples/hello_world_langchain_stream.py +36 -0
  125. aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
  126. aip_agents/examples/hello_world_langflow_agent.py +163 -0
  127. aip_agents/examples/hello_world_langgraph.py +39 -0
  128. aip_agents/examples/hello_world_langgraph_gl_connector_twitter.py +44 -0
  129. aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
  130. aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
  131. aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
  132. aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
  133. aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
  134. aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
  135. aip_agents/examples/hello_world_langgraph_skills.py +62 -0
  136. aip_agents/examples/hello_world_langgraph_stream.py +43 -0
  137. aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
  138. aip_agents/examples/hello_world_model_switch_cli.py +210 -0
  139. aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
  140. aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
  141. aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
  142. aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
  143. aip_agents/examples/hello_world_multi_agent_tool_output_client.py +57 -0
  144. aip_agents/examples/hello_world_multi_agent_tool_output_server.py +131 -0
  145. aip_agents/examples/hello_world_pii_logger.py +21 -0
  146. aip_agents/examples/hello_world_ptc.py +49 -0
  147. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  148. aip_agents/examples/hello_world_sentry.py +133 -0
  149. aip_agents/examples/hello_world_step_limits.py +273 -0
  150. aip_agents/examples/hello_world_stock_a2a_server.py +100 -0
  151. aip_agents/examples/hello_world_tool_output_client.py +55 -0
  152. aip_agents/examples/hello_world_tool_output_server.py +113 -0
  153. aip_agents/examples/hitl_demo.py +724 -0
  154. aip_agents/examples/mcp_configs/configs.py +63 -0
  155. aip_agents/examples/mcp_servers/common.py +76 -0
  156. aip_agents/examples/mcp_servers/mcp_name.py +29 -0
  157. aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
  158. aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
  159. aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
  160. aip_agents/examples/mcp_servers/mcp_time.py +10 -0
  161. aip_agents/examples/pii_demo_langgraph_client.py +69 -0
  162. aip_agents/examples/pii_demo_langgraph_server.py +125 -0
  163. aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
  164. aip_agents/examples/pii_demo_multi_agent_server.py +246 -0
  165. aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
  166. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +87 -0
  167. aip_agents/examples/tools/__init__.py +27 -0
  168. aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
  169. aip_agents/examples/tools/adk_weather_tool.py +60 -0
  170. aip_agents/examples/tools/data_generator_tool.py +103 -0
  171. aip_agents/examples/tools/data_visualization_tool.py +312 -0
  172. aip_agents/examples/tools/image_artifact_tool.py +136 -0
  173. aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
  174. aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
  175. aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
  176. aip_agents/examples/tools/langchain_weather_tool.py +48 -0
  177. aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
  178. aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
  179. aip_agents/examples/tools/multiply_tool.py +43 -0
  180. aip_agents/examples/tools/pii_demo_tools.py +189 -0
  181. aip_agents/examples/tools/random_chart_tool.py +142 -0
  182. aip_agents/examples/tools/serper_tool.py +202 -0
  183. aip_agents/examples/tools/stock_tools.py +82 -0
  184. aip_agents/examples/tools/table_generator_tool.py +167 -0
  185. aip_agents/examples/tools/time_tool.py +82 -0
  186. aip_agents/examples/tools/weather_forecast_tool.py +38 -0
  187. aip_agents/guardrails/__init__.py +83 -0
  188. aip_agents/guardrails/engines/__init__.py +69 -0
  189. aip_agents/guardrails/engines/base.py +90 -0
  190. aip_agents/guardrails/engines/nemo.py +101 -0
  191. aip_agents/guardrails/engines/phrase_matcher.py +113 -0
  192. aip_agents/guardrails/exceptions.py +39 -0
  193. aip_agents/guardrails/manager.py +163 -0
  194. aip_agents/guardrails/middleware.py +199 -0
  195. aip_agents/guardrails/schemas.py +63 -0
  196. aip_agents/guardrails/utils.py +45 -0
  197. aip_agents/integration/__init__.py +43 -0
  198. aip_agents/integration/agent.py +58 -0
  199. aip_agents/integration/guardrails.py +52 -0
  200. aip_agents/integration/hitl.py +71 -0
  201. aip_agents/integration/ptc.py +95 -0
  202. aip_agents/integration/skills.py +37 -0
  203. aip_agents/integration/storage.py +81 -0
  204. aip_agents/integration/version.py +103 -0
  205. aip_agents/mcp/__init__.py +1 -0
  206. aip_agents/mcp/client/__init__.py +60 -0
  207. aip_agents/mcp/client/base_mcp_client.py +377 -0
  208. aip_agents/mcp/client/connection_manager.py +231 -0
  209. aip_agents/mcp/client/errors.py +119 -0
  210. aip_agents/mcp/client/google_adk/__init__.py +45 -0
  211. aip_agents/mcp/client/google_adk/client.py +383 -0
  212. aip_agents/mcp/client/langchain/__init__.py +11 -0
  213. aip_agents/mcp/client/langchain/client.py +265 -0
  214. aip_agents/mcp/client/persistent_session.py +704 -0
  215. aip_agents/mcp/client/session_pool.py +358 -0
  216. aip_agents/mcp/client/transports.py +288 -0
  217. aip_agents/mcp/utils/__init__.py +7 -0
  218. aip_agents/mcp/utils/config_validator.py +139 -0
  219. aip_agents/memory/__init__.py +14 -0
  220. aip_agents/memory/adapters/__init__.py +10 -0
  221. aip_agents/memory/adapters/base_adapter.py +815 -0
  222. aip_agents/memory/adapters/mem0.py +286 -0
  223. aip_agents/memory/base.py +84 -0
  224. aip_agents/memory/constants.py +49 -0
  225. aip_agents/memory/factory.py +86 -0
  226. aip_agents/memory/guidance.py +20 -0
  227. aip_agents/memory/simple_memory.py +47 -0
  228. aip_agents/middleware/__init__.py +65 -0
  229. aip_agents/middleware/backends/__init__.py +72 -0
  230. aip_agents/middleware/backends/in_memory.py +578 -0
  231. aip_agents/middleware/backends/local_disk.py +1203 -0
  232. aip_agents/middleware/backends/protocol.py +353 -0
  233. aip_agents/middleware/backends/sandbox.py +1431 -0
  234. aip_agents/middleware/backends/sandbox_files.py +113 -0
  235. aip_agents/middleware/backends/utils.py +293 -0
  236. aip_agents/middleware/base.py +239 -0
  237. aip_agents/middleware/filesystem.py +690 -0
  238. aip_agents/middleware/manager.py +291 -0
  239. aip_agents/middleware/memory.py +532 -0
  240. aip_agents/middleware/pii_middleware.py +192 -0
  241. aip_agents/middleware/schema.py +19 -0
  242. aip_agents/middleware/skills.py +872 -0
  243. aip_agents/middleware/task_delegation.py +1656 -0
  244. aip_agents/middleware/todolist.py +276 -0
  245. aip_agents/middleware/tools/__init__.py +23 -0
  246. aip_agents/middleware/tools/edit_file.py +70 -0
  247. aip_agents/middleware/tools/execute.py +428 -0
  248. aip_agents/middleware/tools/grep_file.py +79 -0
  249. aip_agents/middleware/tools/ls.py +71 -0
  250. aip_agents/middleware/tools/read_file.py +239 -0
  251. aip_agents/middleware/tools/skill_load.py +193 -0
  252. aip_agents/middleware/tools/skill_resource.py +338 -0
  253. aip_agents/middleware/tools/write_file.py +114 -0
  254. aip_agents/middleware/vision.py +322 -0
  255. aip_agents/ptc/__init__.py +87 -0
  256. aip_agents/ptc/custom_tools.py +511 -0
  257. aip_agents/ptc/custom_tools_payload.py +499 -0
  258. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  259. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +24 -0
  260. aip_agents/ptc/custom_tools_templates/custom_init.py.template +26 -0
  261. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +149 -0
  262. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +112 -0
  263. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  264. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +87 -0
  265. aip_agents/ptc/doc_gen.py +122 -0
  266. aip_agents/ptc/exceptions.py +57 -0
  267. aip_agents/ptc/executor.py +627 -0
  268. aip_agents/ptc/mcp/__init__.py +45 -0
  269. aip_agents/ptc/mcp/sandbox_bridge.py +679 -0
  270. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  271. aip_agents/ptc/mcp/templates/mcp_client.py.template +240 -0
  272. aip_agents/ptc/naming.py +430 -0
  273. aip_agents/ptc/payload.py +26 -0
  274. aip_agents/ptc/prompt_builder.py +674 -0
  275. aip_agents/ptc/ptc_helper.py +16 -0
  276. aip_agents/ptc/sandbox_bridge.py +297 -0
  277. aip_agents/ptc/template_utils.py +33 -0
  278. aip_agents/ptc/templates/__init__.py +1 -0
  279. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  280. aip_agents/ptc/tool_def_helpers.py +101 -0
  281. aip_agents/ptc/tool_enrichment.py +163 -0
  282. aip_agents/resilience/__init__.py +10 -0
  283. aip_agents/resilience/base.py +46 -0
  284. aip_agents/resilience/tool.py +1661 -0
  285. aip_agents/sandbox/__init__.py +62 -0
  286. aip_agents/sandbox/defaults.py +220 -0
  287. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1-datasaur.Dockerfile +120 -0
  288. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1.Dockerfile +100 -0
  289. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2-datasaur.Dockerfile +143 -0
  290. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2.Dockerfile +119 -0
  291. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3-datasaur.Dockerfile +173 -0
  292. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3.Dockerfile +130 -0
  293. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.Dockerfile +164 -0
  294. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.patch_js_context.py +41 -0
  295. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.requirements.txt +29 -0
  296. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2.Dockerfile +87 -0
  297. aip_agents/sandbox/e2b_runtime.py +27 -0
  298. aip_agents/sandbox/image_registry.py +98 -0
  299. aip_agents/sandbox/providers/__init__.py +76 -0
  300. aip_agents/sandbox/providers/base.py +392 -0
  301. aip_agents/sandbox/providers/bedrock.py +28 -0
  302. aip_agents/sandbox/providers/e2b.py +230 -0
  303. aip_agents/sandbox/providers/opensandbox.py +163 -0
  304. aip_agents/sandbox/sandbox_runtime.py +967 -0
  305. aip_agents/sandbox/template_builder.py +240 -0
  306. aip_agents/sandbox/types.py +141 -0
  307. aip_agents/sandbox/validation.py +50 -0
  308. aip_agents/schema/__init__.py +106 -0
  309. aip_agents/schema/a2a.py +58 -0
  310. aip_agents/schema/agent.py +117 -0
  311. aip_agents/schema/artifacts.py +75 -0
  312. aip_agents/schema/attachments.py +425 -0
  313. aip_agents/schema/hitl.py +157 -0
  314. aip_agents/schema/langgraph.py +47 -0
  315. aip_agents/schema/model_id.py +103 -0
  316. aip_agents/schema/model_usage.py +50 -0
  317. aip_agents/schema/runtime_metadata.py +78 -0
  318. aip_agents/schema/step_limit.py +108 -0
  319. aip_agents/schema/storage.py +40 -0
  320. aip_agents/schema/tool_payloads.py +68 -0
  321. aip_agents/sentry/__init__.py +11 -0
  322. aip_agents/sentry/sentry.py +267 -0
  323. aip_agents/skills/__init__.py +30 -0
  324. aip_agents/skills/errors.py +110 -0
  325. aip_agents/skills/installer.py +479 -0
  326. aip_agents/skills/models.py +769 -0
  327. aip_agents/skills/stager.py +224 -0
  328. aip_agents/skills/validation.py +53 -0
  329. aip_agents/storage/__init__.py +41 -0
  330. aip_agents/storage/base.py +85 -0
  331. aip_agents/storage/clients/__init__.py +12 -0
  332. aip_agents/storage/clients/minio_client.py +318 -0
  333. aip_agents/storage/config.py +62 -0
  334. aip_agents/storage/providers/__init__.py +15 -0
  335. aip_agents/storage/providers/base.py +106 -0
  336. aip_agents/storage/providers/memory.py +114 -0
  337. aip_agents/storage/providers/object_storage.py +214 -0
  338. aip_agents/tools/__init__.py +65 -0
  339. aip_agents/tools/agent_schedule_tool.py +392 -0
  340. aip_agents/tools/browser_use/__init__.py +83 -0
  341. aip_agents/tools/browser_use/action_parser.py +104 -0
  342. aip_agents/tools/browser_use/browser_use_tool.py +1118 -0
  343. aip_agents/tools/browser_use/llm_config.py +120 -0
  344. aip_agents/tools/browser_use/minio_storage.py +203 -0
  345. aip_agents/tools/browser_use/schemas.py +121 -0
  346. aip_agents/tools/browser_use/session.py +13 -0
  347. aip_agents/tools/browser_use/session_errors.py +132 -0
  348. aip_agents/tools/browser_use/steel_session_recording.py +321 -0
  349. aip_agents/tools/browser_use/streaming.py +263 -0
  350. aip_agents/tools/browser_use/structured_data_parser.py +257 -0
  351. aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
  352. aip_agents/tools/browser_use/types.py +37 -0
  353. aip_agents/tools/code_sandbox/__init__.py +48 -0
  354. aip_agents/tools/code_sandbox/code_sandbox_tool.py +350 -0
  355. aip_agents/tools/code_sandbox/constant.py +13 -0
  356. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +346 -0
  357. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +65 -0
  358. aip_agents/tools/computer_use/__init__.py +75 -0
  359. aip_agents/tools/computer_use/computer_use_tool.py +910 -0
  360. aip_agents/tools/computer_use/schemas.py +78 -0
  361. aip_agents/tools/computer_use/streaming.py +198 -0
  362. aip_agents/tools/computer_use/types.py +32 -0
  363. aip_agents/tools/constants.py +177 -0
  364. aip_agents/tools/date_range_tool.py +554 -0
  365. aip_agents/tools/document_loader/__init__.py +44 -0
  366. aip_agents/tools/document_loader/base_reader.py +302 -0
  367. aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
  368. aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
  369. aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
  370. aip_agents/tools/document_loader/pdf_splitter.py +169 -0
  371. aip_agents/tools/execute_ptc_code.py +582 -0
  372. aip_agents/tools/gl_connector/__init__.py +5 -0
  373. aip_agents/tools/gl_connector/tool.py +518 -0
  374. aip_agents/tools/gl_connector_tools.py +170 -0
  375. aip_agents/tools/memory_search/__init__.py +29 -0
  376. aip_agents/tools/memory_search/base.py +213 -0
  377. aip_agents/tools/memory_search/mem0.py +424 -0
  378. aip_agents/tools/memory_search/schema.py +81 -0
  379. aip_agents/tools/memory_search_tool.py +34 -0
  380. aip_agents/tools/streaming_common.py +577 -0
  381. aip_agents/tools/time_tool.py +117 -0
  382. aip_agents/tools/tool_config_injector.py +300 -0
  383. aip_agents/tools/web_search/__init__.py +15 -0
  384. aip_agents/tools/web_search/serper_tool.py +187 -0
  385. aip_agents/types/__init__.py +73 -0
  386. aip_agents/types/a2a_events.py +13 -0
  387. aip_agents/utils/__init__.py +91 -0
  388. aip_agents/utils/a2a_connector.py +1758 -0
  389. aip_agents/utils/artifact_helpers.py +542 -0
  390. aip_agents/utils/artifacts/__init__.py +20 -0
  391. aip_agents/utils/artifacts/tracker.py +367 -0
  392. aip_agents/utils/attachment_helpers.py +178 -0
  393. aip_agents/utils/constants.py +22 -0
  394. aip_agents/utils/datetime/__init__.py +34 -0
  395. aip_agents/utils/datetime/normalization.py +231 -0
  396. aip_agents/utils/datetime/timezone.py +206 -0
  397. aip_agents/utils/env_loader.py +27 -0
  398. aip_agents/utils/event_handler_registry.py +58 -0
  399. aip_agents/utils/file_prompt_utils.py +276 -0
  400. aip_agents/utils/file_watchers/__init__.py +33 -0
  401. aip_agents/utils/file_watchers/base.py +46 -0
  402. aip_agents/utils/file_watchers/e2b.py +128 -0
  403. aip_agents/utils/file_watchers/factory.py +39 -0
  404. aip_agents/utils/file_watchers/ls_diff.py +88 -0
  405. aip_agents/utils/file_watchers/sandbox.py +66 -0
  406. aip_agents/utils/file_watchers/watchdog.py +97 -0
  407. aip_agents/utils/final_response_builder.py +211 -0
  408. aip_agents/utils/formatter_llm_client.py +258 -0
  409. aip_agents/utils/langgraph/__init__.py +19 -0
  410. aip_agents/utils/langgraph/converter.py +399 -0
  411. aip_agents/utils/langgraph/delegation_context.py +22 -0
  412. aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
  413. aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
  414. aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
  415. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1329 -0
  416. aip_agents/utils/langgraph/tool_output_management.py +1047 -0
  417. aip_agents/utils/logger.py +195 -0
  418. aip_agents/utils/metadata/__init__.py +27 -0
  419. aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
  420. aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
  421. aip_agents/utils/metadata/activity_narrative/builder.py +822 -0
  422. aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
  423. aip_agents/utils/metadata/activity_narrative/context.py +49 -0
  424. aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
  425. aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
  426. aip_agents/utils/metadata/schemas/__init__.py +16 -0
  427. aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
  428. aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
  429. aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
  430. aip_agents/utils/metadata_helper.py +363 -0
  431. aip_agents/utils/name_preprocessor/__init__.py +17 -0
  432. aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
  433. aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
  434. aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
  435. aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
  436. aip_agents/utils/pii/__init__.py +29 -0
  437. aip_agents/utils/pii/pii_handler.py +555 -0
  438. aip_agents/utils/pii/pii_helper.py +225 -0
  439. aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
  440. aip_agents/utils/reference_helper.py +273 -0
  441. aip_agents/utils/sse_chunk_transformer.py +836 -0
  442. aip_agents/utils/step_limit_manager.py +265 -0
  443. aip_agents/utils/token_usage_helper.py +531 -0
  444. aip_agents_binary-0.0.0.dist-info/METADATA +825 -0
  445. aip_agents_binary-0.0.0.dist-info/RECORD +447 -0
  446. aip_agents_binary-0.0.0.dist-info/WHEEL +5 -0
  447. aip_agents_binary-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1437 @@
1
+ """Base class for concrete agent implementations.
2
+
3
+ This class provides common functionalities like A2A client capabilities.
4
+
5
+ Authors:
6
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ import asyncio
11
+ from collections.abc import AsyncGenerator, Mapping
12
+ from contextvars import ContextVar
13
+ from importlib import import_module
14
+ from pathlib import Path
15
+ from types import MethodType
16
+ from typing import Any
17
+ from warnings import warn
18
+
19
+ import httpx
20
+ from a2a.server.apps import A2AStarletteApplication
21
+ from a2a.server.request_handlers import DefaultRequestHandler
22
+ from a2a.server.tasks import InMemoryTaskStore
23
+ from a2a.types import AgentCard
24
+ from gllm_core.schema import Tool as GLLMTool
25
+ from gllm_core.utils.retry import RetryConfig
26
+ from gllm_inference.lm_invoker.build_lm_invoker import build_lm_invoker
27
+ from gllm_inference.lm_invoker.lm_invoker import BaseLMInvoker
28
+ from gllm_inference.schema import ThinkingConfig
29
+
30
+ try:
31
+ from gllm_inference.schema import Thinking
32
+ except ImportError: # pragma: no cover
33
+ from gllm_inference.schema import Reasoning as Thinking # pragma: no cover
34
+ from gllm_inference.schema.enums import OutputTransformerType
35
+ from langchain_core.language_models import BaseChatModel
36
+ from langchain_core.tools import BaseTool
37
+ from starlette.applications import Starlette
38
+
39
+ from aip_agents.agent.interface import AgentInterface
40
+ from aip_agents.agent.model_invocation_adapter import LMInvokerRuntimePolicy
41
+ from aip_agents.credentials.manager import CredentialsManager
42
+ from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
43
+ from aip_agents.schema.agent import A2AClientConfig, AgentConfig, BaseAgentConfig, CredentialType
44
+ from aip_agents.schema.model_id import ModelId, ModelProvider
45
+ from aip_agents.utils.a2a_connector import A2AConnector
46
+ from aip_agents.utils.logger import get_logger
47
+ from aip_agents.utils.name_preprocessor.name_preprocessor import NamePreprocessor
48
+
49
+ logger = get_logger(__name__)
50
+
51
+
52
+ def _get_agent_executor_mapping() -> dict[str, str]:
53
+ """Map agent class names to their executor import paths."""
54
+ return {
55
+ "LangGraphReactAgent": "aip_agents.a2a.server.langgraph_executor.LangGraphA2AExecutor",
56
+ "LangGraphAgent": "aip_agents.a2a.server.langgraph_executor.LangGraphA2AExecutor",
57
+ "LangChainAgent": "aip_agents.a2a.server.langgraph_executor.LangGraphA2AExecutor",
58
+ "LangflowAgent": "aip_agents.a2a.server.langflow_executor.LangflowA2AExecutor",
59
+ "GoogleADKAgent": "aip_agents.a2a.server.google_adk_executor.GoogleADKExecutor",
60
+ }
61
+
62
+
63
+ AGENT_EXECUTOR_MAPPING = _get_agent_executor_mapping()
64
+
65
+
66
+ def _load_executor_class(candidate: Any) -> type[Any]:
67
+ """Resolve an executor class from a dotted path or a direct reference.
68
+
69
+ Args:
70
+ candidate (Any): Either a string path (e.g., "module.Class") or a class/type object.
71
+
72
+ Returns:
73
+ type[Any]: The resolved executor class.
74
+ """
75
+ if isinstance(candidate, str):
76
+ module_name, class_name = candidate.rsplit(".", 1)
77
+ module = import_module(module_name)
78
+ return getattr(module, class_name)
79
+
80
+ if isinstance(candidate, type): # Already a class reference
81
+ return candidate
82
+
83
+ if callable(candidate):
84
+ return candidate # type: ignore[return-value]
85
+
86
+ raise TypeError(f"Unsupported executor mapping entry: {candidate!r}")
87
+
88
+
89
+ def _get_executor_class_for_agent(agent: "BaseAgent") -> type[Any]:
90
+ """Resolve the appropriate executor class for the given agent instance.
91
+
92
+ Args:
93
+ agent (BaseAgent): The agent instance to find an executor for.
94
+
95
+ Returns:
96
+ type[Any]: The appropriate executor class for the agent.
97
+ """
98
+ for cls in agent.__class__.__mro__:
99
+ executor_path = AGENT_EXECUTOR_MAPPING.get(cls.__name__)
100
+ if executor_path:
101
+ return _load_executor_class(executor_path)
102
+ raise KeyError(f"No A2A executor registered for agent class '{agent.__class__.__name__}'")
103
+
104
+
105
+ DEFAULT_RETRY_CONFIG = RetryConfig(max_retries=5, timeout=240.0)
106
+
107
+ LM_EXCLUDE_FIELDS = {
108
+ "a2ui",
109
+ "lm_base_url",
110
+ "lm_api_key",
111
+ "lm_name",
112
+ "lm_provider",
113
+ "lm_hyperparameters",
114
+ "lm_retry_config",
115
+ "lm_credentials",
116
+ }
117
+
118
+ CUSTOM_PROVIDERS = {
119
+ "openai-compatible/": ModelProvider.OPENAI_COMPATIBLE,
120
+ "azure-openai/": ModelProvider.AZURE_OPENAI,
121
+ }
122
+
123
+ OPENAI_COMPATIBLE_PREFIX = "openai-compatible/"
124
+ OPENAI_CHAT_COMPLETIONS_PROVIDER = "openai-chat-completions"
125
+
126
+ OUTPUT_ANALYTICS_KEY = "output_analytics"
127
+ OUTPUT_TRANSFORMER_KEY = "output_transformer"
128
+ ANTHROPIC_PROMPT_CACHE_CONFIG_KEY = "anthropic_prompt_cache"
129
+ CACHE_CONTROL_KEY = "cache_control"
130
+ _AIP_ANTHROPIC_CACHE_USAGE = ContextVar("aip_anthropic_cache_usage", default=None)
131
+ DEFAULT_ANTHROPIC_CACHE_CONTROL = {"type": "ephemeral"}
132
+
133
+
134
+ def _thinking_text(thinking: Thinking) -> str:
135
+ """Return reasoning text across Thinking/Reasoning schema variants."""
136
+ return str(getattr(thinking, "thinking", getattr(thinking, "reasoning", "")) or "")
137
+
138
+
139
+ def _make_thinking(*, id_value: str, thinking_value: str) -> Thinking:
140
+ """Build a Thinking/Reasoning object across gllm-inference schema variants."""
141
+ fields = getattr(Thinking, "model_fields", {})
142
+ if "thinking" in fields:
143
+ return Thinking(id=id_value, thinking=thinking_value)
144
+ return Thinking(id=id_value, reasoning=thinking_value) # pragma: no cover
145
+
146
+
147
+ def _reasoning_block_text(block: Any) -> str:
148
+ """Extract reasoning text from OpenAI summary/content blocks."""
149
+ if isinstance(block, dict):
150
+ value = block.get("text", "") # pragma: no cover
151
+ else:
152
+ value = getattr(block, "text", "")
153
+ return value if isinstance(value, str) else ""
154
+
155
+
156
+ def _is_enabled_reasoning_invocation(invoker_self: Any) -> bool:
157
+ """Return whether runtime thinking is enabled on an invoker instance."""
158
+ return bool(getattr(getattr(invoker_self, "thinking", None), "enabled", False))
159
+
160
+
161
+ def _existing_thinking_keys(output: Any) -> set[tuple[str, str]]:
162
+ """Build a stable key set for already-added reasoning blocks."""
163
+ return {
164
+ (str(getattr(thinking, "id", "") or ""), _thinking_text(thinking))
165
+ for thinking in (getattr(output, "thinkings", None) or [])
166
+ }
167
+
168
+
169
+ def _reasoning_blocks(item: Any) -> list[Any]:
170
+ """Return OpenAI reasoning summary/content blocks as a flat list."""
171
+ return [
172
+ *(getattr(item, "summary", None) or []),
173
+ *(getattr(item, "content", None) or []),
174
+ ]
175
+
176
+
177
+ def _recover_reasoning_thinkings(item: Any, output: Any) -> list[Thinking]:
178
+ """Recover non-duplicate reasoning thinkings from OpenAI reasoning items."""
179
+ if getattr(item, "type", None) != "reasoning":
180
+ return [] # pragma: no cover
181
+
182
+ item_id = str(getattr(item, "id", "") or "")
183
+ seen = _existing_thinking_keys(output)
184
+ recovered: list[Thinking] = []
185
+
186
+ for block in _reasoning_blocks(item):
187
+ text = _reasoning_block_text(block)
188
+ if not text.strip():
189
+ continue # pragma: no cover
190
+
191
+ key = (item_id, text)
192
+ if key in seen:
193
+ continue
194
+
195
+ seen.add(key)
196
+ recovered.append(_make_thinking(id_value=item_id, thinking_value=text))
197
+
198
+ return recovered
199
+
200
+
201
+ def _build_openai_reasoning_patch(original_process_thinkings: Any) -> Any:
202
+ """Build an instance-method wrapper for OpenAI reasoning extraction."""
203
+
204
+ def _patched_process_thinkings(invoker_self: Any, item: Any, output: Any) -> None:
205
+ original_process_thinkings(item, output)
206
+
207
+ if not _is_enabled_reasoning_invocation(invoker_self):
208
+ return # pragma: no cover
209
+
210
+ recovered = _recover_reasoning_thinkings(item, output)
211
+ if recovered:
212
+ output.add_thinking(recovered)
213
+
214
+ return _patched_process_thinkings
215
+
216
+
217
+ def _anthropic_usage_int(usage: Any, attr: str) -> int:
218
+ return int(getattr(usage, attr, 0) or 0)
219
+
220
+
221
+ def _anthropic_cache_creation_tokens(usage: Any) -> int:
222
+ direct = _anthropic_usage_int(usage, "cache_creation_input_tokens")
223
+ cache_creation = getattr(usage, "cache_creation", None)
224
+ return direct or (
225
+ _anthropic_usage_int(cache_creation, "ephemeral_5m_input_tokens")
226
+ + _anthropic_usage_int(cache_creation, "ephemeral_1h_input_tokens")
227
+ )
228
+
229
+
230
+ def _anthropic_cache_input_details(usage: Any) -> dict[str, int]:
231
+ creation_tokens = _anthropic_cache_creation_tokens(usage)
232
+ read_tokens = _anthropic_usage_int(usage, "cache_read_input_tokens")
233
+ if not creation_tokens and not read_tokens:
234
+ return {}
235
+ return {
236
+ "cache_creation_input_tokens": creation_tokens,
237
+ "cache_read_input_tokens": read_tokens,
238
+ "cached_tokens": read_tokens,
239
+ }
240
+
241
+
242
+ def _detail_values(detail: Any) -> dict[str, int]:
243
+ if detail is None:
244
+ return {}
245
+ if isinstance(detail, dict):
246
+ return {str(key): int(value or 0) for key, value in detail.items() if isinstance(value, int | float)}
247
+ if hasattr(detail, "model_dump"):
248
+ dumped = detail.model_dump()
249
+ if isinstance(dumped, dict):
250
+ return {str(key): int(value or 0) for key, value in dumped.items() if isinstance(value, int | float)}
251
+ if hasattr(detail, "__dict__") and isinstance(detail.__dict__, dict):
252
+ return {str(key): int(value or 0) for key, value in detail.__dict__.items() if isinstance(value, int | float)}
253
+ return {}
254
+
255
+
256
+ def _merge_anthropic_cache_usage(output: Any, raw_usage: Any) -> None:
257
+ cache_details = _anthropic_cache_input_details(raw_usage)
258
+ if not cache_details:
259
+ return
260
+
261
+ token_usage = getattr(output, "token_usage", None)
262
+ if isinstance(token_usage, dict):
263
+ input_tokens = int(token_usage.get("input_tokens", 0) or 0)
264
+ output_tokens = int(token_usage.get("output_tokens", 0) or 0)
265
+ input_details = _detail_values(token_usage.get("input_token_details"))
266
+ output_details = _detail_values(token_usage.get("output_token_details"))
267
+ else:
268
+ input_tokens = int(getattr(token_usage, "input_tokens", 0) or 0)
269
+ output_tokens = int(getattr(token_usage, "output_tokens", 0) or 0)
270
+ input_details = _detail_values(getattr(token_usage, "input_token_details", None))
271
+ output_details = _detail_values(getattr(token_usage, "output_token_details", None))
272
+
273
+ input_details.update(cache_details)
274
+ patched_usage: dict[str, Any] = {
275
+ "input_tokens": input_tokens,
276
+ "output_tokens": output_tokens,
277
+ "input_token_details": input_details,
278
+ }
279
+ if output_details:
280
+ patched_usage["output_token_details"] = output_details
281
+ output.token_usage = patched_usage
282
+
283
+
284
+ class BaseAgent(AgentInterface):
285
+ """Base class for agents, providing common A2A client method implementations.
286
+
287
+ Concrete agent implementations (e.g., LangGraphAgent, GoogleADKAgent)
288
+ should inherit from this class if they need to utilize the shared A2A
289
+ client functionalities.
290
+
291
+ This class now supports flexible model handling:
292
+ - model: Optional[Any] - can be an lm_invoker, string/ModelId, LangChain BaseChatModel, or other types
293
+ - Automatically sets self.lm_invoker if an lm_invoker is provided or can be built
294
+ - Stores the original model in self.model for subclass use
295
+ - Enhanced credential support with automatic type detection
296
+ """
297
+
298
+ def __init__( # noqa: PLR0913
299
+ self,
300
+ name: str,
301
+ instruction: str,
302
+ description: str | None = None,
303
+ model: Any | None = None,
304
+ tools: list[Any] | None = None,
305
+ config: BaseAgentConfig | dict[str, Any] | None = None,
306
+ tool_configs: dict[str, Any] | None = None,
307
+ **kwargs: Any,
308
+ ):
309
+ """Initializes the BaseAgent.
310
+
311
+ Args:
312
+ name: The name of the agent.
313
+ instruction: The core directive or system prompt for the agent.
314
+ description: Human-readable description. Defaults to instruction if not provided.
315
+ model: The model to use. Can be:
316
+ - BaseLMInvoker instance (will be set as self.lm_invoker)
317
+ - String or ModelId (will build an lm_invoker)
318
+ - LangChain BaseChatModel (will be stored in self.model)
319
+ - Any other type (will be stored in self.model)
320
+ tools: List of tools available to the agent.
321
+ config: Additional configuration for the agent. Can be a BaseAgentConfig instance or dict.
322
+ tool_configs: Default tool configurations applied to all tool calls from this agent.
323
+ **kwargs: Additional keyword arguments for AgentInterface.
324
+ """
325
+ # Convert config to BaseAgentConfig if it's a dict (backward compatibility)
326
+ processed_config = self._process_config(config, tools)
327
+
328
+ # Process model parameter to set up lm_invoker and model attributes
329
+ processed_lm_invoker, processed_model = self._process_model_parameter(
330
+ name, model, tools or [], processed_config
331
+ )
332
+
333
+ # Pass the lm_invoker to the parent class
334
+ super().__init__(
335
+ name=name,
336
+ instruction=instruction,
337
+ description=description,
338
+ lm_invoker=processed_lm_invoker,
339
+ config=processed_config,
340
+ **kwargs,
341
+ )
342
+
343
+ # Store processed model and other attributes
344
+ self.model = processed_model
345
+ self.tools = tools or []
346
+ # Private MCP configuration to prevent tampering and maintain sync
347
+ self._mcp_config: dict[str, dict[str, Any]] = {}
348
+ self.tool_configs = tool_configs or {}
349
+
350
+ self._mcp_tools_initialized: bool = False
351
+ self.mcp_client: BaseMCPClient | None = None
352
+ self._mcp_init_lock: asyncio.Lock = asyncio.Lock()
353
+
354
+ self.name_preprocessor = self.get_name_preprocessor()
355
+
356
+ def get_name_preprocessor(self) -> NamePreprocessor:
357
+ """Get the name preprocessor based on the provider.
358
+
359
+ This will be used to correct the agent name and tool name. (mostly tool name)
360
+
361
+ Returns:
362
+ NamePreprocessor: The name preprocessor for the model.
363
+ """
364
+ return NamePreprocessor(self.model_provider)
365
+
366
+ @property
367
+ def model_provider(self) -> str:
368
+ """Get the provider of the model with simplified logic.
369
+
370
+ Returns:
371
+ str: The provider of the model.
372
+ """
373
+ if hasattr(self, "lm_invoker") and self.lm_invoker is not None:
374
+ return self.lm_invoker.model_provider
375
+
376
+ if hasattr(self, "model") and self.model is not None:
377
+ return self._detect_provider_from_model(self.model)
378
+
379
+ return "unknown"
380
+
381
+ def _detect_provider_from_model(self, model: Any) -> str:
382
+ """Detect provider from model object.
383
+
384
+ Args:
385
+ model: The model object.
386
+
387
+ Returns:
388
+ str: The provider of the model.
389
+ """
390
+ if isinstance(model, str):
391
+ return self._detect_provider_from_string(model)
392
+
393
+ if hasattr(model, "__class__"):
394
+ return self._detect_provider_from_class(model.__class__.__name__)
395
+
396
+ return "unknown"
397
+
398
+ def _detect_provider_from_string(self, model_str: str) -> str:
399
+ """Detect provider from model string.
400
+
401
+ Args:
402
+ model_str: The model string.
403
+
404
+ Returns:
405
+ str: The provider of the model.
406
+ """
407
+ model_lower = model_str.lower()
408
+
409
+ if model_lower.startswith(("gemini", "google")):
410
+ return "google"
411
+
412
+ if "/" in model_str:
413
+ return model_str.split("/")[0]
414
+ if ":" in model_str:
415
+ return model_str.split(":")[0]
416
+ return model_str
417
+
418
+ def _detect_provider_from_class(self, class_name: str) -> str:
419
+ """Detect provider from class name.
420
+
421
+ Args:
422
+ class_name: The class name.
423
+
424
+ Returns:
425
+ str: The provider of the model.
426
+ """
427
+ class_name_lower = class_name.lower()
428
+
429
+ provider_mappings = {"openai": "openai", "google": "google", "vertex": "google", "anthropic": "anthropic"}
430
+
431
+ for keyword, provider in provider_mappings.items():
432
+ if keyword in class_name_lower:
433
+ return provider
434
+
435
+ return "unknown"
436
+
437
+ def _process_config(
438
+ self, config: BaseAgentConfig | dict[str, Any] | None, tools: list[Any] | None = None
439
+ ) -> BaseAgentConfig | None:
440
+ """Process config parameter to ensure it's a BaseAgentConfig instance.
441
+
442
+ Args:
443
+ config: Configuration parameter that can be dict, BaseAgentConfig, or None.
444
+ tools: List of tools to include in config if not already present.
445
+
446
+ Returns:
447
+ BaseAgentConfig instance or None.
448
+ """
449
+ if config is None:
450
+ if tools:
451
+ return AgentConfig(tools=tools)
452
+ return None
453
+
454
+ if isinstance(config, BaseAgentConfig):
455
+ if tools and not config.tools:
456
+ config.tools = tools
457
+ return config
458
+
459
+ if isinstance(config, dict):
460
+ config_dict = config.copy()
461
+
462
+ if tools and "tools" not in config_dict:
463
+ config_dict["tools"] = tools
464
+
465
+ if "lm_hyperparameters" in config_dict and "default_hyperparameters" not in config_dict:
466
+ config_dict["default_hyperparameters"] = config_dict.pop("lm_hyperparameters")
467
+
468
+ return AgentConfig(**config_dict)
469
+
470
+ raise TypeError(f"Config must be BaseAgentConfig, dict, or None, got {type(config)}")
471
+
472
+ def _extract_credentials_from_config(
473
+ self, config: BaseAgentConfig | None
474
+ ) -> tuple[CredentialType, str | dict[str, Any] | None]:
475
+ """Extract and auto-detect credentials from config with ultra-simple logic.
476
+
477
+ This method supports multiple credential formats with automatic type detection:
478
+ - New lm_credentials field: Auto-detects type based on content
479
+ - Legacy lm_api_key field: For backward compatibility
480
+
481
+ Auto-detection logic:
482
+ - Dict: Passed through as-is (CredentialType.DICT)
483
+ - String + file exists: Treated as file path (CredentialType.FILE)
484
+ - String + file doesn't exist: Treated as API key (CredentialType.API_KEY)
485
+
486
+ Args:
487
+ config: Configuration object.
488
+
489
+ Returns:
490
+ Tuple containing:
491
+ - credential_type: CredentialType enum value
492
+ - credentials: The extracted credentials or None if not found
493
+ """
494
+ if not config:
495
+ return CredentialType.API_KEY, None
496
+
497
+ if hasattr(config, "lm_credentials") and config.lm_credentials is not None:
498
+ detected_type, formatted_creds = self._auto_detect_credential_type(config.lm_credentials)
499
+ return detected_type, formatted_creds
500
+
501
+ if hasattr(config, "lm_api_key") and config.lm_api_key:
502
+ warn(
503
+ (
504
+ "The lm_api_key is deprecated as of version 0.5.0. "
505
+ "Use lm_credentials instead which supports auto-detection of API keys, "
506
+ "file paths, and dictionary credentials."
507
+ ),
508
+ DeprecationWarning,
509
+ stacklevel=2,
510
+ )
511
+ return CredentialType.API_KEY, config.lm_api_key
512
+
513
+ return CredentialType.API_KEY, None
514
+
515
+ def _auto_detect_credential_type(self, credentials: Any) -> tuple[CredentialType, Any]:
516
+ """Automatically detect credential type using simple file existence rules.
517
+
518
+ This method uses ultra-simple detection logic:
519
+ 1. If credentials is dict -> CredentialType.DICT (Bedrock, LangChain credentials)
520
+ 2. If credentials is string and exists on disk -> CredentialType.FILE
521
+ 3. Everything else -> CredentialType.API_KEY (simple fallback)
522
+
523
+ Args:
524
+ credentials: Raw credentials from config.
525
+
526
+ Returns:
527
+ Tuple containing:
528
+ - credential_type: CredentialType enum value
529
+ - formatted_credentials: The credentials in the detected format
530
+ """
531
+ if isinstance(credentials, dict):
532
+ return CredentialType.DICT, credentials
533
+
534
+ if isinstance(credentials, str):
535
+ if not credentials.strip():
536
+ return CredentialType.API_KEY, credentials
537
+
538
+ try:
539
+ path = Path(credentials)
540
+ if path.exists():
541
+ return CredentialType.FILE, credentials
542
+ except (ValueError, OSError):
543
+ pass
544
+
545
+ return CredentialType.API_KEY, credentials
546
+
547
+ return CredentialType.API_KEY, str(credentials)
548
+
549
+ @property
550
+ def mcp_config(self) -> dict[str, dict[str, Any]]:
551
+ """Read-only view of MCP configuration.
552
+
553
+ Returns a copy to prevent direct mutation; use add_mcp_server() for changes.
554
+ """
555
+ return self._mcp_config.copy()
556
+
557
+ @mcp_config.setter
558
+ def mcp_config(self, value: dict[str, dict[str, Any]]) -> None:
559
+ """Set MCP configuration and maintain synchronization.
560
+
561
+ Automatically resets initialization flag and recreates client to ensure consistency.
562
+ Prefer using add_mcp_server() for proper validation.
563
+
564
+ Args:
565
+ value (dict[str, dict[str, Any]]): The MCP configuration to set.
566
+ """
567
+ if not isinstance(value, dict):
568
+ raise ValueError("mcp_config must be a dict[str, dict[str, Any]]")
569
+ self._mcp_config = value.copy()
570
+ # Reset flag and recreate client object to maintain sync. This is lightweight
571
+ # (no connections created) and safe to perform synchronously. Actual session
572
+ # initialization remains lazy in the event loop via _ensure_mcp_tools_initialized().
573
+ self._mcp_tools_initialized = False
574
+ if self._mcp_config:
575
+ self._initialize_mcp_client()
576
+ else:
577
+ # Clear client for empty config
578
+ self.mcp_client = None
579
+
580
+ def _get_credentials(self, model: str | ModelId, config: BaseAgentConfig | None) -> str | dict[str, Any] | None:
581
+ """Get credentials for the model with enhanced type support.
582
+
583
+ This method now supports multiple credential formats through the new
584
+ _extract_credentials_from_config method while maintaining backward compatibility.
585
+
586
+ Args:
587
+ model: Model identifier.
588
+ config: Configuration object.
589
+
590
+ Returns:
591
+ Credentials if found, None otherwise. Can be:
592
+ - str: For API keys or file paths
593
+ - dict: For structured credentials (Bedrock, LangChain)
594
+ """
595
+ credentials = None
596
+
597
+ if config:
598
+ _, credentials = self._extract_credentials_from_config(config)
599
+
600
+ if not credentials:
601
+ credentials = CredentialsManager.get_credentials(model)
602
+
603
+ return credentials
604
+
605
+ def _extract_retry_config(self, config: BaseAgentConfig | None, use_default: bool = True) -> RetryConfig | None:
606
+ """Extract and process retry config from agent config.
607
+
608
+ Args:
609
+ config: Configuration object.
610
+ use_default: If True, return a default RetryConfig when none is found.
611
+
612
+ Returns:
613
+ RetryConfig instance if found, default RetryConfig if use_default=True and none found, None otherwise.
614
+ """
615
+ if config and isinstance(config, AgentConfig) and config.lm_retry_config:
616
+ if isinstance(config.lm_retry_config, dict):
617
+ return RetryConfig(**config.lm_retry_config)
618
+ return config.lm_retry_config
619
+
620
+ if use_default:
621
+ return DEFAULT_RETRY_CONFIG
622
+
623
+ return None
624
+
625
+ def _update_config_with_tools(
626
+ self, tools: list[Any], config: BaseAgentConfig | None = None
627
+ ) -> dict[str, Any] | None:
628
+ """Update config with tools if not already present and convert to dict for lm_invoker.
629
+
630
+ Args:
631
+ tools: List of tools.
632
+ config: Configuration object.
633
+
634
+ Returns:
635
+ Configuration dictionary for lm_invoker.
636
+ """
637
+ lm_invoker_tools = self._prepare_tools_for_lm_invoker(tools)
638
+
639
+ if config is None:
640
+ config_dict = {"tools": lm_invoker_tools} if lm_invoker_tools else {}
641
+ config_dict["retry_config"] = self._extract_retry_config(config)
642
+ return config_dict
643
+
644
+ # Convert BaseAgentConfig to dict, excluding LM-specific fields
645
+ config_dict = config.model_dump(
646
+ exclude_none=True,
647
+ exclude=LM_EXCLUDE_FIELDS,
648
+ )
649
+ # Preserve extra config objects such as gllm ThinkingConfig; model_dump()
650
+ # serializes extras to dicts, but build_lm_invoker expects the original objects.
651
+ if getattr(config, "model_extra", None):
652
+ config_dict.update(config.model_extra or {})
653
+ if isinstance(config, AgentConfig) and config.lm_hyperparameters:
654
+ config_dict["default_hyperparameters"] = config.lm_hyperparameters
655
+
656
+ config_dict["retry_config"] = self._extract_retry_config(config)
657
+
658
+ if lm_invoker_tools:
659
+ config_dict["tools"] = lm_invoker_tools
660
+
661
+ return config_dict if config_dict else None
662
+
663
+ @staticmethod
664
+ def _resolve_tool_func(tool: BaseTool) -> Any:
665
+ """Extract the executable function from a LangChain tool.
666
+
667
+ Checks ``func``, ``coroutine``, ``_run``, and ``_arun`` in order.
668
+
669
+ Args:
670
+ tool: The LangChain tool to extract the function from.
671
+
672
+ Returns:
673
+ The function if found, or ``None``.
674
+ """
675
+ func = getattr(tool, "func", None) or getattr(tool, "coroutine", None)
676
+ if callable(func):
677
+ return func
678
+ func = getattr(tool, "_run", None) or getattr(tool, "_arun", None)
679
+ return func if callable(func) else None
680
+
681
+ @staticmethod
682
+ def _resolve_input_schema(tool: BaseTool) -> Any:
683
+ """Resolve the input schema for a LangChain tool.
684
+
685
+ Tries ``args_schema``, ``get_input_schema()``, then ``tool_call_schema``.
686
+
687
+ Args:
688
+ tool: The LangChain tool to resolve the schema for.
689
+
690
+ Returns:
691
+ The resolved schema, or ``None`` if no schema is available.
692
+ """
693
+ schema = getattr(tool, "args_schema", None)
694
+ if schema is not None:
695
+ return schema
696
+
697
+ if hasattr(tool, "get_input_schema"):
698
+ try:
699
+ return tool.get_input_schema()
700
+ except Exception as e:
701
+ logger.warning("Failed to get input schema for tool '%s': %s", tool.name, e)
702
+
703
+ return getattr(tool, "tool_call_schema", None)
704
+
705
+ def _convert_langchain_tool(self, tool: BaseTool) -> GLLMTool:
706
+ """Convert a single LangChain BaseTool to a GLLMTool for LM schema generation.
707
+
708
+ Extracts only name, description, input_schema, and func. LangChain wrapper
709
+ state (callbacks, metadata, config_schema) is intentionally discarded —
710
+ GLLMTools are for LM schema/description only; LangGraph retains ownership
711
+ of tool execution.
712
+
713
+ Falls back to ``GLLMTool.from_langchain`` when no input schema can be
714
+ resolved.
715
+
716
+ Args:
717
+ tool: The LangChain tool to convert.
718
+
719
+ Returns:
720
+ A ``GLLMTool`` instance.
721
+ """
722
+ input_schema = self._resolve_input_schema(tool)
723
+
724
+ if input_schema is not None:
725
+ func = self._resolve_tool_func(tool)
726
+ return GLLMTool(
727
+ name=tool.name,
728
+ description=tool.description,
729
+ input_schema=input_schema,
730
+ func=func,
731
+ is_async=asyncio.iscoroutinefunction(func) if callable(func) else False,
732
+ )
733
+
734
+ return GLLMTool.from_langchain(tool)
735
+
736
+ def _prepare_tools_for_lm_invoker(self, tools: list[Any] | None) -> list[Any]:
737
+ """Convert LangChain tools to gllm-core tools for LM invokers only.
738
+
739
+ Converts each LangChain BaseTool to a GLLMTool via _convert_langchain_tool.
740
+ Wrapper state (metadata, callbacks, config_schema) is intentionally stripped
741
+ — GLLMTools are for LM schema/description only; LangGraph retains ownership
742
+ of tool execution. Non-BaseTool items are passed through unchanged.
743
+
744
+ Args:
745
+ tools: List of tools (LangChain BaseTool or other) to convert.
746
+
747
+ Returns:
748
+ List of tools ready for LM invoker configuration.
749
+ """
750
+ if not tools:
751
+ return []
752
+
753
+ prepared_tools: list[Any] = []
754
+ for tool in tools:
755
+ if isinstance(tool, BaseTool):
756
+ prepared_tools.append(self._convert_langchain_tool(tool))
757
+ else:
758
+ prepared_tools.append(tool)
759
+
760
+ return prepared_tools
761
+
762
+ def _finalize_lm_invoker_config(self, tools: list[Any], config: BaseAgentConfig | None) -> dict[str, Any]:
763
+ """Finalize lm_invoker config with tools and default output settings.
764
+
765
+ Ensures generated lm_invoker config includes default values when omitted:
766
+ - output_analytics=True
767
+ - output_transformer=OutputTransformerType.THINK_TAG
768
+
769
+ Args:
770
+ tools: List of tools.
771
+ config: Configuration object for lm_invoker.
772
+
773
+ Returns:
774
+ Configuration dictionary for lm_invoker.
775
+ """
776
+ processed_config = self._update_config_with_tools(tools, config)
777
+ if processed_config is None:
778
+ processed_config = {}
779
+
780
+ if processed_config.get(OUTPUT_ANALYTICS_KEY) is None:
781
+ processed_config[OUTPUT_ANALYTICS_KEY] = True
782
+
783
+ if processed_config.get(OUTPUT_TRANSFORMER_KEY) is None:
784
+ processed_config[OUTPUT_TRANSFORMER_KEY] = OutputTransformerType.THINK_TAG
785
+
786
+ return processed_config
787
+
788
+ def _apply_default_thinking_config(self, model: str | ModelId, config: dict[str, Any]) -> None:
789
+ """Enable provider thinking by default for models that can expose it.
790
+
791
+ Explicit caller configuration always wins, including ``thinking=False``.
792
+ """
793
+ if "thinking" in config:
794
+ return
795
+
796
+ provider, model_name = self._provider_and_model_name(model)
797
+ if provider == ModelProvider.OPENAI and self._is_openai_reasoning_model(model_name):
798
+ config["thinking"] = ThinkingConfig(enabled=True, kwargs={"effort": "low", "summary": "detailed"})
799
+ elif provider == ModelProvider.GOOGLE_GENAI and model_name.startswith(("gemini-2.5", "gemini-3")):
800
+ config["thinking"] = ThinkingConfig(enabled=True, kwargs={"thinking_budget": 512})
801
+
802
+ def _apply_anthropic_prompt_cache_config(self, model: str | ModelId, config: dict[str, Any]) -> None:
803
+ """Apply native Anthropic prompt-cache defaults without touching routed providers."""
804
+ provider, _model_name = self._provider_and_model_name(model)
805
+ prompt_cache_config = config.get(ANTHROPIC_PROMPT_CACHE_CONFIG_KEY, True)
806
+ if provider != ModelProvider.ANTHROPIC:
807
+ return
808
+ if prompt_cache_config is False:
809
+ config.pop(ANTHROPIC_PROMPT_CACHE_CONFIG_KEY, None)
810
+ return
811
+
812
+ config.pop(ANTHROPIC_PROMPT_CACHE_CONFIG_KEY, None)
813
+
814
+ hyperparameters = config.get("default_hyperparameters")
815
+ if hyperparameters is None:
816
+ hyperparameters = {}
817
+ elif isinstance(hyperparameters, Mapping):
818
+ hyperparameters = dict(hyperparameters)
819
+ else:
820
+ raise TypeError(
821
+ "default_hyperparameters must be a dict when Anthropic cache defaults are requested; "
822
+ f"got {type(hyperparameters).__name__}."
823
+ )
824
+
825
+ if isinstance(prompt_cache_config, dict):
826
+ hyperparameters[CACHE_CONTROL_KEY] = dict(prompt_cache_config) # pragma: no cover
827
+ elif CACHE_CONTROL_KEY not in hyperparameters:
828
+ hyperparameters[CACHE_CONTROL_KEY] = dict(DEFAULT_ANTHROPIC_CACHE_CONTROL)
829
+
830
+ if hyperparameters:
831
+ config["default_hyperparameters"] = hyperparameters
832
+
833
+ def _provider_and_model_name(self, model: str | ModelId) -> tuple[str | None, str]:
834
+ """Return a normalized provider and model name for a model identifier."""
835
+ if isinstance(model, ModelId):
836
+ return model.provider, model.name.lower()
837
+
838
+ model_text = model.strip().lower()
839
+ provider = self._detect_provider_from_string(model_text)
840
+ if "/" in model_text:
841
+ model_name = model_text.rsplit("/", 1)[-1]
842
+ elif ":" in model_text:
843
+ model_name = model_text.rsplit(":", 1)[-1]
844
+ else:
845
+ model_name = model_text
846
+
847
+ return provider, model_name
848
+
849
+ @staticmethod
850
+ def _is_openai_reasoning_model(model_name: str) -> bool:
851
+ """Return True when an OpenAI model supports reasoning summaries."""
852
+ return model_name.startswith(("gpt-5", "o1", "o3", "o4"))
853
+
854
+ def _setup_lm_invoker_param(
855
+ self, model: str | ModelId, tools: list[Any], config: BaseAgentConfig | None = None
856
+ ) -> tuple[ModelId | str, str | None, dict[str, Any] | None]:
857
+ """Setup parameter for build_lm_invoker.
858
+
859
+ Args:
860
+ model (str | ModelId): The model identifier.
861
+ tools (list[Any]): List of tools.
862
+ config (BaseAgentConfig | None): Configuration object.
863
+
864
+ Returns:
865
+ - model_id: ModelId | str
866
+ - credentials: str | None
867
+ - config: dict[str, Any] | None
868
+ as tuple
869
+ """
870
+ model_id: str | ModelId = model
871
+ credentials: str | None = None
872
+ processed_as_custom = False
873
+ credential_lookup_model: str | ModelId = model
874
+
875
+ if isinstance(model, str):
876
+ for prefix, provider in CUSTOM_PROVIDERS.items():
877
+ if model.startswith(prefix):
878
+ processed_as_custom = True
879
+
880
+ if not config or not isinstance(config, AgentConfig):
881
+ raise ValueError(f"AgentConfig is required for model '{model}'")
882
+
883
+ try:
884
+ model_id = ModelId.from_string(model)
885
+ if prefix == OPENAI_COMPATIBLE_PREFIX:
886
+ model_id = ModelId( # pragma: no cover
887
+ provider=OPENAI_CHAT_COMPLETIONS_PROVIDER,
888
+ name=model_id.name,
889
+ path=model_id.path or config.lm_base_url,
890
+ )
891
+ except ValueError as e:
892
+ base_url = config.lm_base_url
893
+ if not base_url:
894
+ raise ValueError(f"lm_base_url in AgentConfig is required for model '{model}'") from e
895
+
896
+ model_name = model.removeprefix(prefix)
897
+ normalized_provider = (
898
+ OPENAI_CHAT_COMPLETIONS_PROVIDER if prefix == OPENAI_COMPATIBLE_PREFIX else provider
899
+ )
900
+ model_id = ModelId(provider=normalized_provider, name=model_name, path=base_url)
901
+
902
+ # Extract credentials using new enhanced method
903
+ _, credentials = self._extract_credentials_from_config(config)
904
+ if prefix == OPENAI_COMPATIBLE_PREFIX and credentials is None:
905
+ credential_lookup_model = model # pragma: no cover
906
+ break
907
+
908
+ if not processed_as_custom or credentials is None:
909
+ credentials = self._get_credentials(credential_lookup_model if credentials is None else model_id, config)
910
+
911
+ processed_config = self._finalize_lm_invoker_config(tools, config)
912
+ self._apply_default_thinking_config(model_id, processed_config)
913
+ self._apply_anthropic_prompt_cache_config(model_id, processed_config)
914
+ return model_id, credentials, processed_config
915
+
916
+ def _process_model_parameter(
917
+ self, agent_name: str, model: Any | None, tools: list[Any], config: BaseAgentConfig | None = None
918
+ ) -> tuple[Any | None, Any | None]:
919
+ """Process the model parameter and determine lm_invoker and model attributes.
920
+
921
+ Args:
922
+ agent_name: The name of the agent (for logging).
923
+ model: The model parameter from initialization.
924
+ tools: List of tools for lm_invoker configuration.
925
+ config: Configuration object.
926
+
927
+ Returns:
928
+ Tuple of (lm_invoker, processed_model) where:
929
+ - lm_invoker: Built LM Invoker if model is string/ModelId or existing BaseLMInvoker, None otherwise
930
+ - processed_model: LangChain model if it's a BaseChatModel,
931
+ original model for other types, None if lm_invoker was created
932
+ or if model is None
933
+ """
934
+ # If model is already an lm_invoker instance
935
+ if BaseLMInvoker and isinstance(model, BaseLMInvoker):
936
+ logger.debug(f"Agent '{agent_name}': Using provided LM Invoker: {model.__class__.__name__}")
937
+ self._apply_lm_invoker_runtime_patches_for_invoker(model)
938
+ return model, None
939
+
940
+ # Check if model is a string or ModelId - build lm_invoker
941
+ if isinstance(model, str) or (ModelId and isinstance(model, ModelId)):
942
+ if not build_lm_invoker:
943
+ logger.warning(
944
+ f"Agent '{agent_name}': gllm-inference not available, cannot build LM Invoker from {model}"
945
+ )
946
+ return None, model
947
+
948
+ logger.info(f"Agent '{agent_name}': Building LM Invoker from model identifier: {model}")
949
+ try:
950
+ model_id, credentials, preprocessed_config = self._setup_lm_invoker_param(model, tools, config)
951
+
952
+ lm_invoker = build_lm_invoker(
953
+ model_id=model_id,
954
+ credentials=credentials,
955
+ config=preprocessed_config,
956
+ )
957
+ self._apply_lm_invoker_runtime_patches(model_id, lm_invoker)
958
+ return lm_invoker, None
959
+ except Exception as e:
960
+ logger.error(f"Agent '{agent_name}': Failed to build LM Invoker from {model}: {e}")
961
+ raise RuntimeError(f"Failed to build LM Invoker from model '{model}': {e}") from e
962
+
963
+ # If it's a LangChain model, use it directly
964
+ elif BaseChatModel and hasattr(model, "__class__") and issubclass(model.__class__, BaseChatModel):
965
+ logger.debug(f"Agent '{agent_name}': Using provided LangChain model: {model.__class__.__name__}")
966
+ return None, model
967
+
968
+ # If model is None, that's acceptable for some use cases
969
+ elif model is None:
970
+ logger.debug(f"Agent '{agent_name}': No model provided")
971
+ return None, None
972
+
973
+ # For any other type, store as model (e.g., Google ADK agent instance)
974
+ else:
975
+ logger.debug(f"Agent '{agent_name}': Using provided model of type: {type(model)}")
976
+ return None, model
977
+
978
+ def _apply_lm_invoker_runtime_patches(self, model: str | ModelId, lm_invoker: Any) -> None:
979
+ """Apply aip-agents-local runtime fixes to a built LM invoker.
980
+
981
+ These policy-layer patches let aip-agents ship narrow provider workarounds
982
+ without waiting for upstream gllm-inference changes.
983
+ """
984
+ self._lm_invoker_runtime_policy().apply_to_built_invoker(model, lm_invoker)
985
+
986
+ def _apply_lm_invoker_runtime_patches_for_invoker(self, lm_invoker: Any) -> None:
987
+ """Apply runtime patches to a caller-provided ``BaseLMInvoker`` instance.
988
+
989
+ Mirrors ``_apply_lm_invoker_runtime_patches`` for the path where the
990
+ caller already constructed an invoker. Reads the provider and model
991
+ name from the invoker's own properties to avoid coupling to the
992
+ ``ModelId`` serialization format used by different package shims.
993
+ """
994
+ self._lm_invoker_runtime_policy().apply_to_provided_invoker(lm_invoker)
995
+
996
+ def _lm_invoker_runtime_policy(self) -> LMInvokerRuntimePolicy:
997
+ """Build the private model-invocation runtime policy delegate."""
998
+ return LMInvokerRuntimePolicy(
999
+ provider_and_model_name=self._provider_and_model_name,
1000
+ provider_and_model_name_for_invoker=self._provider_and_model_name_for_invoker,
1001
+ is_openai_reasoning_model=self._is_openai_reasoning_model,
1002
+ patch_openai_reasoning_thinking_extraction=self._patch_openai_reasoning_thinking_extraction,
1003
+ patch_anthropic_cache_usage=self._patch_anthropic_cache_usage,
1004
+ )
1005
+
1006
+ def _patch_anthropic_cache_usage(self, lm_invoker: Any) -> None:
1007
+ """Preserve Anthropic prompt-cache counters dropped by current gllm-inference builds."""
1008
+ if getattr(lm_invoker, "_aip_anthropic_cache_usage_patch_applied", False):
1009
+ return
1010
+
1011
+ messages = getattr(getattr(getattr(lm_invoker, "client", None), "beta", None), "messages", None)
1012
+ original_create = getattr(messages, "create", None)
1013
+ original_invoke = getattr(lm_invoker, "invoke", None)
1014
+ if not callable(original_create) or not callable(original_invoke):
1015
+ return
1016
+
1017
+ async def _patched_create(*args: Any, **kwargs: Any) -> Any:
1018
+ response = await original_create(*args, **kwargs)
1019
+ _AIP_ANTHROPIC_CACHE_USAGE.set(getattr(response, "usage", None))
1020
+ return response
1021
+
1022
+ async def _patched_invoke(*args: Any, **kwargs: Any) -> Any:
1023
+ token = _AIP_ANTHROPIC_CACHE_USAGE.set(None)
1024
+ try:
1025
+ output = await original_invoke(*args, **kwargs)
1026
+ finally:
1027
+ latest_usage = _AIP_ANTHROPIC_CACHE_USAGE.get()
1028
+ _AIP_ANTHROPIC_CACHE_USAGE.reset(token)
1029
+
1030
+ _merge_anthropic_cache_usage(output, latest_usage)
1031
+ return output
1032
+
1033
+ messages.create = _patched_create
1034
+ lm_invoker.invoke = _patched_invoke
1035
+ lm_invoker._aip_anthropic_cache_usage_patch_applied = True
1036
+
1037
+ def _provider_and_model_name_for_invoker(self, lm_invoker: Any) -> tuple[str | None, str]:
1038
+ """Return normalized provider/model_name for a caller-provided invoker.
1039
+
1040
+ Prefers the invoker's own provider/name properties to avoid coupling
1041
+ to ModelId serialization format, which differs between
1042
+ ``aip-agents`` and ``gllm-inference`` shims.
1043
+ """
1044
+ provider = getattr(lm_invoker, "model_provider", None)
1045
+ model_name = getattr(lm_invoker, "model_name", None)
1046
+ if provider and model_name:
1047
+ return provider, model_name.lower()
1048
+
1049
+ model_id = getattr(lm_invoker, "model_id", None)
1050
+ if model_id is not None:
1051
+ return self._provider_and_model_name(str(model_id)) # pragma: no cover
1052
+
1053
+ return None, ""
1054
+
1055
+ def _patch_openai_reasoning_thinking_extraction(self, lm_invoker: Any) -> None:
1056
+ """Recover OpenAI reasoning text from ``content`` when upstream omits it.
1057
+
1058
+ Some OpenAI Responses API reasoning items expose recoverable text in
1059
+ ``content`` while leaving ``summary`` empty. Older gllm-inference builds
1060
+ only read ``summary``, which drops provider reasoning before the SSE layer
1061
+ ever sees it. This instance-local patch preserves aip-agents behavior
1062
+ without mutating the upstream package globally.
1063
+ """
1064
+ if getattr(lm_invoker, "_aip_openai_reasoning_patch_applied", False):
1065
+ return # pragma: no cover
1066
+
1067
+ original_process_thinkings = getattr(lm_invoker, "_process_thinkings", None)
1068
+ if not callable(original_process_thinkings):
1069
+ return
1070
+
1071
+ lm_invoker._process_thinkings = MethodType(
1072
+ _build_openai_reasoning_patch(original_process_thinkings), lm_invoker
1073
+ )
1074
+ lm_invoker._aip_openai_reasoning_patch_applied = True
1075
+
1076
+ def to_a2a(self, agent_card: AgentCard, **kwargs: Any) -> Starlette:
1077
+ """Converts the agent to an A2A-compatible ASGI application.
1078
+
1079
+ This implementation provides a base setup for A2A server components.
1080
+ Subclasses can override this method if they need custom executor
1081
+ or task store implementations.
1082
+
1083
+ Args:
1084
+ agent_card: The agent card to use for the A2A application.
1085
+ **kwargs: Additional keyword arguments for ASGI application configuration.
1086
+
1087
+ Returns:
1088
+ A Starlette ASGI application that can be used with any ASGI server.
1089
+ """
1090
+ # Use provided task store or create default in-memory store
1091
+ task_store = kwargs.get("task_store", InMemoryTaskStore())
1092
+
1093
+ # Create default request handler if not provided
1094
+ try:
1095
+ executor_cls = _get_executor_class_for_agent(self)
1096
+ except KeyError as exc:
1097
+ raise ValueError(f"No A2A executor registered for agent type '{self.__class__.__name__}'.") from exc
1098
+
1099
+ agent_executor = executor_cls(self)
1100
+ request_handler = kwargs.get(
1101
+ "request_handler",
1102
+ DefaultRequestHandler(
1103
+ agent_executor=agent_executor,
1104
+ task_store=task_store,
1105
+ ),
1106
+ )
1107
+
1108
+ # Create A2A application
1109
+ a2a_app = A2AStarletteApplication(agent_card=agent_card, http_handler=request_handler)
1110
+
1111
+ # Get base routes from A2A app
1112
+ routes = a2a_app.routes()
1113
+
1114
+ # Add any additional routes if provided
1115
+ if "routes" in kwargs:
1116
+ routes.extend(kwargs["routes"])
1117
+
1118
+ # Create and return Starlette application
1119
+ return Starlette(routes=routes)
1120
+
1121
+ @classmethod
1122
+ def discover_agents(cls, a2a_config: A2AClientConfig, **kwargs: Any) -> list[AgentCard]:
1123
+ """Discover agents from the URLs specified in a2a_config.discovery_urls.
1124
+
1125
+ This concrete implementation fetches and parses .well-known/agent.json
1126
+ from each discovery URL to build a list of available agents.
1127
+
1128
+ Args:
1129
+ a2a_config: Configuration containing discovery URLs and other A2A settings.
1130
+ **kwargs: Additional keyword arguments (unused in this implementation).
1131
+
1132
+ Returns:
1133
+ A list of AgentCard objects representing discovered agents.
1134
+ """
1135
+ discovered_cards: list[AgentCard] = []
1136
+
1137
+ if not a2a_config or not a2a_config.discovery_urls:
1138
+ logger.debug("No discovery URLs configured")
1139
+ return discovered_cards
1140
+
1141
+ httpx_client_options = {}
1142
+ if a2a_config.httpx_client_options:
1143
+ httpx_client_options = a2a_config.httpx_client_options.model_dump(exclude_none=True)
1144
+
1145
+ with httpx.Client(**httpx_client_options) as client:
1146
+ for base_url in a2a_config.discovery_urls:
1147
+ try:
1148
+ agent_json_url = f"{base_url.rstrip('/')}/.well-known/agent.json"
1149
+
1150
+ response = client.get(agent_json_url)
1151
+ response.raise_for_status()
1152
+
1153
+ try:
1154
+ agent_card = AgentCard.model_validate(response.json())
1155
+ discovered_cards.append(agent_card)
1156
+ logger.info(f"Successfully discovered agent '{agent_card.name}' at {base_url}")
1157
+ except Exception as parse_error:
1158
+ logger.error(f"Error parsing agent card from {agent_json_url}: {parse_error}")
1159
+ continue
1160
+
1161
+ except Exception as e:
1162
+ logger.error(f"Unexpected error discovering agents from {base_url}: {e}")
1163
+ continue
1164
+
1165
+ agent_list = "\n".join([f"{i + 1}. {agent.name}" for i, agent in enumerate(discovered_cards)])
1166
+ logger.info(f"Discovered agents ({len(discovered_cards)} Agents): \n{agent_list}")
1167
+ return discovered_cards
1168
+
1169
+ def send_to_agent(
1170
+ self,
1171
+ agent_card: AgentCard,
1172
+ message: str | dict[str, Any],
1173
+ **kwargs: Any,
1174
+ ) -> dict[str, Any]:
1175
+ """Synchronously sends a message to another agent using the A2A protocol.
1176
+
1177
+ This method is a synchronous wrapper around asend_to_agent. It handles the creation
1178
+ of an event loop if one doesn't exist, and manages the asynchronous call internally.
1179
+
1180
+ Args:
1181
+ agent_card: The AgentCard instance containing the target agent's details including
1182
+ URL, authentication requirements, and capabilities.
1183
+ message: The message to send to the agent. Can be either a string for simple text
1184
+ messages or a dictionary for structured data.
1185
+ **kwargs: Additional keyword arguments passed to asend_to_agent.
1186
+
1187
+ Returns:
1188
+ A dictionary containing the response details:
1189
+ - status (str): 'success' or 'error'
1190
+ - content (str): Extracted text content from the response
1191
+ - task_id (str, optional): ID of the created/updated task
1192
+ - task_state (str, optional): Current state of the task
1193
+ - raw_response (str): Complete JSON response from the A2A client
1194
+ - error_type (str, optional): Type of error if status is 'error'
1195
+ - message (str, optional): Error message if status is 'error'
1196
+
1197
+ Raises:
1198
+ RuntimeError: If called from within an existing event loop or if asend_to_agent
1199
+ encounters an unhandled exception.
1200
+ """
1201
+ try:
1202
+ return A2AConnector.send_to_agent(agent_card, message, **kwargs)
1203
+ except RuntimeError as e:
1204
+ raise RuntimeError(f"Agent '{self.name}': Error in sync 'send_to_agent'. Original error: {e}") from e
1205
+
1206
+ async def asend_to_agent(
1207
+ self,
1208
+ agent_card: AgentCard,
1209
+ message: str | dict[str, Any],
1210
+ **kwargs: Any,
1211
+ ) -> dict[str, Any]:
1212
+ """Asynchronously sends a message to another agent using the A2A protocol.
1213
+
1214
+ This method handles the core A2A communication logic, creating and sending properly
1215
+ formatted A2A messages and processing the responses.
1216
+
1217
+ Args:
1218
+ agent_card: The AgentCard instance containing the target agent's details including
1219
+ URL, authentication requirements, and capabilities.
1220
+ message: The message to send to the agent. Can be either a string for simple text
1221
+ messages or a dictionary for structured data.
1222
+ **kwargs: Additional keyword arguments.
1223
+
1224
+ Returns:
1225
+ A dictionary containing the response details:
1226
+ - status (str): 'success' or 'error'
1227
+ - content (str): Extracted text content from the response
1228
+ - task_id (str, optional): ID of the created/updated task
1229
+ - task_state (str, optional): Current state of the task
1230
+ - raw_response (str): Complete JSON response from the A2A client
1231
+ - error_type (str, optional): Type of error if status is 'error'
1232
+ - message (str, optional): Error message if status is 'error'
1233
+
1234
+ Raises:
1235
+ httpx.HTTPError: If there's an HTTP-related error during the request.
1236
+ Exception: For any other unexpected errors during message sending or processing.
1237
+ """
1238
+ return await A2AConnector.asend_to_agent(agent_card, message, **kwargs)
1239
+
1240
+ async def astream_to_agent(
1241
+ self,
1242
+ agent_card: AgentCard,
1243
+ message: str | dict[str, Any],
1244
+ **kwargs: Any,
1245
+ ) -> AsyncGenerator[dict[str, Any], None]:
1246
+ """Asynchronously sends a streaming message to another agent using the A2A protocol.
1247
+
1248
+ This method supports streaming responses from the target agent, yielding chunks of
1249
+ the response as they become available. It handles various types of streaming events
1250
+ including task status updates, artifact updates, and message parts.
1251
+
1252
+ Args:
1253
+ agent_card: The AgentCard instance containing the target agent's details including
1254
+ URL, authentication requirements, and capabilities.
1255
+ message: The message to send to the agent. Can be either a string for simple text
1256
+ messages or a dictionary for structured data.
1257
+ **kwargs: Additional keyword arguments.
1258
+
1259
+ Yields:
1260
+ Dictionaries containing streaming response chunks:
1261
+ For successful chunks:
1262
+ - status (str): 'success'
1263
+ - content (str): Extracted text content from the chunk
1264
+ - task_id (str): ID of the associated task
1265
+ - task_state (str): Current state of the task
1266
+ - final (bool): Whether this is the final chunk
1267
+ - artifact_name (str, optional): Name of the artifact if chunk is an artifact update
1268
+ For error chunks:
1269
+ - status (str): 'error'
1270
+ - error_type (str): Type of error encountered
1271
+ - message (str): Error description
1272
+
1273
+ Raises:
1274
+ httpx.HTTPError: If there's an HTTP-related error during the streaming request.
1275
+ Exception: For any other unexpected errors during message streaming or processing.
1276
+ """
1277
+ # Default to the richer A2A event payload so integrations (e.g. HITL streaming) receive
1278
+ async for chunk in A2AConnector.astream_to_agent(agent_card, message, **kwargs):
1279
+ yield chunk
1280
+
1281
+ @staticmethod
1282
+ def format_agent_description(agent_card: AgentCard) -> str:
1283
+ """Format the description of an agent card including skills information.
1284
+
1285
+ Args:
1286
+ agent_card (AgentCard): The agent card to format.
1287
+
1288
+ Returns:
1289
+ str: The formatted description including skills.
1290
+ """
1291
+ # Start with the base description
1292
+ formatted_description = agent_card.description or ""
1293
+
1294
+ if agent_card.skills:
1295
+ formatted_description += "\n\nSkills:"
1296
+ for skill in agent_card.skills:
1297
+ formatted_description += f"\n• {skill.name}: {skill.description}"
1298
+
1299
+ if skill.tags:
1300
+ tags_str = ", ".join(skill.tags)
1301
+ formatted_description += f" (Tags: {tags_str})"
1302
+
1303
+ if skill.examples:
1304
+ formatted_description += "\n Examples:"
1305
+ for example in skill.examples:
1306
+ formatted_description += f"\n - {example}"
1307
+
1308
+ return formatted_description
1309
+
1310
+ def add_mcp_server(self, mcp_config: dict[str, dict[str, Any]]) -> None:
1311
+ """Adds MCP servers to the agent.
1312
+
1313
+ Args:
1314
+ mcp_config: A dictionary containing MCP server configurations.
1315
+
1316
+ Raises:
1317
+ ValueError: If the MCP configuration is empty or None.
1318
+ KeyError: If a server with the same name already exists in the MCP configuration.
1319
+ """
1320
+ if not mcp_config:
1321
+ raise ValueError("MCP configuration must not be empty or None")
1322
+
1323
+ for server_name, config in mcp_config.items():
1324
+ if server_name in self.mcp_config:
1325
+ raise KeyError(f"Server '{server_name}' already exists in MCP configuration")
1326
+ if not isinstance(config, dict):
1327
+ raise ValueError(f"Configuration for server '{server_name}' must be a dictionary")
1328
+ if not config:
1329
+ raise ValueError(f"Configuration for server '{server_name}' must not be empty")
1330
+
1331
+ # Validate that either URL or command is present
1332
+ required_keys = ["url"] if "url" in config else ["command"]
1333
+ if not any(key in config for key in required_keys):
1334
+ raise ValueError(
1335
+ f"Server '{server_name}' missing required configuration: must have either 'url' or 'command'"
1336
+ )
1337
+
1338
+ self._mcp_config.update(mcp_config)
1339
+ # Initialize/recreate MCP client object (lightweight, no sessions yet)
1340
+ # This satisfies existing unit tests and keeps lazy async session init intact.
1341
+ self._initialize_mcp_client()
1342
+ # Mark that we need to initialize MCP tools (lazy initialization)
1343
+ self._mcp_tools_initialized = False
1344
+
1345
+ def _initialize_mcp_client(self) -> None:
1346
+ """Initialize/recreate MCP client with current config.
1347
+
1348
+ To be implemented by child agents as each agent type has its own MCP client.
1349
+ For agents that don't support MCP (e.g., LangflowAgent), this can be a no-op.
1350
+ """
1351
+ # Default implementation is no-op for agents that don't support MCP
1352
+ pass
1353
+
1354
+ def _set_mcp_client_safely(self, new_client: BaseMCPClient | None) -> None:
1355
+ """Replace current MCP client with cleanup of the previous instance.
1356
+
1357
+ This helper ensures we don't leak persistent sessions when recreating the client.
1358
+ It attempts asynchronous cleanup when an event loop is running, otherwise performs
1359
+ a synchronous cleanup using asyncio.run.
1360
+
1361
+ Args:
1362
+ new_client (BaseMCPClient | None): The new MCP client to set, or None to clear the current client.
1363
+ """
1364
+ prev_client = self.mcp_client
1365
+ self.mcp_client = new_client
1366
+ if prev_client is not None and prev_client is not new_client:
1367
+ try:
1368
+ loop = asyncio.get_running_loop()
1369
+ loop.create_task(prev_client.cleanup())
1370
+ except RuntimeError:
1371
+ try:
1372
+ asyncio.run(prev_client.cleanup())
1373
+ except Exception:
1374
+ # Best-effort cleanup; ignore failures
1375
+ pass
1376
+
1377
+ async def _ensure_mcp_tools_initialized(self) -> None:
1378
+ """Ensure MCP tools are initialized lazily (one-time operation).
1379
+
1380
+ This method ensures MCP tools are initialized only once during the first run,
1381
+ avoiding event loop issues by doing initialization in the correct event loop.
1382
+ """
1383
+ if self._mcp_tools_initialized:
1384
+ return
1385
+
1386
+ # Prevent concurrent initialization across simultaneous runs
1387
+ async with self._mcp_init_lock:
1388
+ if self._mcp_tools_initialized:
1389
+ return
1390
+
1391
+ await self._handle_mcp_client_initialization()
1392
+
1393
+ # If we have a client at this point, proceed with tool registration
1394
+ if self.mcp_client is not None:
1395
+ await self._perform_mcp_tool_registration()
1396
+
1397
+ async def _handle_mcp_client_initialization(self) -> None:
1398
+ """Handle MCP client initialization based on current state.
1399
+
1400
+ Sets the initialization flag appropriately for different scenarios.
1401
+ """
1402
+ if self.mcp_client is not None:
1403
+ return # Client already exists
1404
+
1405
+ if not self._mcp_config:
1406
+ # No config at all - skip and mark as initialized to avoid repeated logs
1407
+ logger.debug(f"Agent '{self.name}': MCP client not configured; skipping MCP tool registration")
1408
+ self._mcp_tools_initialized = True
1409
+ return
1410
+
1411
+ # Config exists but no client - try to initialize
1412
+ self._initialize_mcp_client()
1413
+ if self.mcp_client is None:
1414
+ # Still no client after init - agent type doesn't support MCP
1415
+ logger.warning(f"Agent '{self.name}': MCP config present but agent type doesn't support MCP")
1416
+
1417
+ async def _perform_mcp_tool_registration(self) -> None:
1418
+ """Perform the actual MCP tool registration.
1419
+
1420
+ Raises:
1421
+ RuntimeError: If tool registration fails
1422
+ """
1423
+ try:
1424
+ await self._register_mcp_tools()
1425
+ self._mcp_tools_initialized = True
1426
+ except Exception as e:
1427
+ logger.error(f"Agent '{self.name}': Failed to initialize MCP tools: {e}", exc_info=True)
1428
+ raise RuntimeError(f"Agent '{self.name}': MCP tool initialization failed: {e}") from e
1429
+
1430
+ async def _register_mcp_tools(self) -> None:
1431
+ """Register MCP tools with the agent.
1432
+
1433
+ To be implemented by child agents as each agent type has its own way
1434
+ of registering tools. For agents that don't support MCP, this can be a no-op.
1435
+ """
1436
+ # Default implementation is no-op for agents that don't support MCP
1437
+ pass