ag2 0.10.2__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 (423) hide show
  1. ag2-0.10.2.dist-info/METADATA +819 -0
  2. ag2-0.10.2.dist-info/RECORD +423 -0
  3. ag2-0.10.2.dist-info/WHEEL +4 -0
  4. ag2-0.10.2.dist-info/licenses/LICENSE +201 -0
  5. ag2-0.10.2.dist-info/licenses/NOTICE.md +19 -0
  6. autogen/__init__.py +88 -0
  7. autogen/_website/__init__.py +3 -0
  8. autogen/_website/generate_api_references.py +426 -0
  9. autogen/_website/generate_mkdocs.py +1216 -0
  10. autogen/_website/notebook_processor.py +475 -0
  11. autogen/_website/process_notebooks.py +656 -0
  12. autogen/_website/utils.py +413 -0
  13. autogen/a2a/__init__.py +36 -0
  14. autogen/a2a/agent_executor.py +86 -0
  15. autogen/a2a/client.py +357 -0
  16. autogen/a2a/errors.py +18 -0
  17. autogen/a2a/httpx_client_factory.py +79 -0
  18. autogen/a2a/server.py +221 -0
  19. autogen/a2a/utils.py +207 -0
  20. autogen/agentchat/__init__.py +47 -0
  21. autogen/agentchat/agent.py +180 -0
  22. autogen/agentchat/assistant_agent.py +86 -0
  23. autogen/agentchat/chat.py +325 -0
  24. autogen/agentchat/contrib/__init__.py +5 -0
  25. autogen/agentchat/contrib/agent_eval/README.md +7 -0
  26. autogen/agentchat/contrib/agent_eval/agent_eval.py +108 -0
  27. autogen/agentchat/contrib/agent_eval/criterion.py +43 -0
  28. autogen/agentchat/contrib/agent_eval/critic_agent.py +44 -0
  29. autogen/agentchat/contrib/agent_eval/quantifier_agent.py +39 -0
  30. autogen/agentchat/contrib/agent_eval/subcritic_agent.py +45 -0
  31. autogen/agentchat/contrib/agent_eval/task.py +42 -0
  32. autogen/agentchat/contrib/agent_optimizer.py +432 -0
  33. autogen/agentchat/contrib/capabilities/__init__.py +5 -0
  34. autogen/agentchat/contrib/capabilities/agent_capability.py +20 -0
  35. autogen/agentchat/contrib/capabilities/generate_images.py +301 -0
  36. autogen/agentchat/contrib/capabilities/teachability.py +393 -0
  37. autogen/agentchat/contrib/capabilities/text_compressors.py +66 -0
  38. autogen/agentchat/contrib/capabilities/tools_capability.py +22 -0
  39. autogen/agentchat/contrib/capabilities/transform_messages.py +93 -0
  40. autogen/agentchat/contrib/capabilities/transforms.py +578 -0
  41. autogen/agentchat/contrib/capabilities/transforms_util.py +122 -0
  42. autogen/agentchat/contrib/capabilities/vision_capability.py +215 -0
  43. autogen/agentchat/contrib/captainagent/__init__.py +9 -0
  44. autogen/agentchat/contrib/captainagent/agent_builder.py +790 -0
  45. autogen/agentchat/contrib/captainagent/captainagent.py +514 -0
  46. autogen/agentchat/contrib/captainagent/tool_retriever.py +334 -0
  47. autogen/agentchat/contrib/captainagent/tools/README.md +44 -0
  48. autogen/agentchat/contrib/captainagent/tools/__init__.py +5 -0
  49. autogen/agentchat/contrib/captainagent/tools/data_analysis/calculate_correlation.py +40 -0
  50. autogen/agentchat/contrib/captainagent/tools/data_analysis/calculate_skewness_and_kurtosis.py +28 -0
  51. autogen/agentchat/contrib/captainagent/tools/data_analysis/detect_outlier_iqr.py +28 -0
  52. autogen/agentchat/contrib/captainagent/tools/data_analysis/detect_outlier_zscore.py +28 -0
  53. autogen/agentchat/contrib/captainagent/tools/data_analysis/explore_csv.py +21 -0
  54. autogen/agentchat/contrib/captainagent/tools/data_analysis/shapiro_wilk_test.py +30 -0
  55. autogen/agentchat/contrib/captainagent/tools/information_retrieval/arxiv_download.py +27 -0
  56. autogen/agentchat/contrib/captainagent/tools/information_retrieval/arxiv_search.py +53 -0
  57. autogen/agentchat/contrib/captainagent/tools/information_retrieval/extract_pdf_image.py +53 -0
  58. autogen/agentchat/contrib/captainagent/tools/information_retrieval/extract_pdf_text.py +38 -0
  59. autogen/agentchat/contrib/captainagent/tools/information_retrieval/get_wikipedia_text.py +21 -0
  60. autogen/agentchat/contrib/captainagent/tools/information_retrieval/get_youtube_caption.py +34 -0
  61. autogen/agentchat/contrib/captainagent/tools/information_retrieval/image_qa.py +60 -0
  62. autogen/agentchat/contrib/captainagent/tools/information_retrieval/optical_character_recognition.py +61 -0
  63. autogen/agentchat/contrib/captainagent/tools/information_retrieval/perform_web_search.py +47 -0
  64. autogen/agentchat/contrib/captainagent/tools/information_retrieval/scrape_wikipedia_tables.py +33 -0
  65. autogen/agentchat/contrib/captainagent/tools/information_retrieval/transcribe_audio_file.py +21 -0
  66. autogen/agentchat/contrib/captainagent/tools/information_retrieval/youtube_download.py +35 -0
  67. autogen/agentchat/contrib/captainagent/tools/math/calculate_circle_area_from_diameter.py +21 -0
  68. autogen/agentchat/contrib/captainagent/tools/math/calculate_day_of_the_week.py +18 -0
  69. autogen/agentchat/contrib/captainagent/tools/math/calculate_fraction_sum.py +28 -0
  70. autogen/agentchat/contrib/captainagent/tools/math/calculate_matrix_power.py +31 -0
  71. autogen/agentchat/contrib/captainagent/tools/math/calculate_reflected_point.py +16 -0
  72. autogen/agentchat/contrib/captainagent/tools/math/complex_numbers_product.py +25 -0
  73. autogen/agentchat/contrib/captainagent/tools/math/compute_currency_conversion.py +23 -0
  74. autogen/agentchat/contrib/captainagent/tools/math/count_distinct_permutations.py +27 -0
  75. autogen/agentchat/contrib/captainagent/tools/math/evaluate_expression.py +28 -0
  76. autogen/agentchat/contrib/captainagent/tools/math/find_continuity_point.py +34 -0
  77. autogen/agentchat/contrib/captainagent/tools/math/fraction_to_mixed_numbers.py +39 -0
  78. autogen/agentchat/contrib/captainagent/tools/math/modular_inverse_sum.py +23 -0
  79. autogen/agentchat/contrib/captainagent/tools/math/simplify_mixed_numbers.py +36 -0
  80. autogen/agentchat/contrib/captainagent/tools/math/sum_of_digit_factorials.py +15 -0
  81. autogen/agentchat/contrib/captainagent/tools/math/sum_of_primes_below.py +15 -0
  82. autogen/agentchat/contrib/captainagent/tools/requirements.txt +10 -0
  83. autogen/agentchat/contrib/captainagent/tools/tool_description.tsv +34 -0
  84. autogen/agentchat/contrib/gpt_assistant_agent.py +526 -0
  85. autogen/agentchat/contrib/graph_rag/__init__.py +9 -0
  86. autogen/agentchat/contrib/graph_rag/document.py +29 -0
  87. autogen/agentchat/contrib/graph_rag/falkor_graph_query_engine.py +167 -0
  88. autogen/agentchat/contrib/graph_rag/falkor_graph_rag_capability.py +103 -0
  89. autogen/agentchat/contrib/graph_rag/graph_query_engine.py +53 -0
  90. autogen/agentchat/contrib/graph_rag/graph_rag_capability.py +63 -0
  91. autogen/agentchat/contrib/graph_rag/neo4j_graph_query_engine.py +263 -0
  92. autogen/agentchat/contrib/graph_rag/neo4j_graph_rag_capability.py +83 -0
  93. autogen/agentchat/contrib/graph_rag/neo4j_native_graph_query_engine.py +210 -0
  94. autogen/agentchat/contrib/graph_rag/neo4j_native_graph_rag_capability.py +93 -0
  95. autogen/agentchat/contrib/img_utils.py +397 -0
  96. autogen/agentchat/contrib/llamaindex_conversable_agent.py +117 -0
  97. autogen/agentchat/contrib/llava_agent.py +189 -0
  98. autogen/agentchat/contrib/math_user_proxy_agent.py +464 -0
  99. autogen/agentchat/contrib/multimodal_conversable_agent.py +125 -0
  100. autogen/agentchat/contrib/qdrant_retrieve_user_proxy_agent.py +325 -0
  101. autogen/agentchat/contrib/rag/__init__.py +10 -0
  102. autogen/agentchat/contrib/rag/chromadb_query_engine.py +268 -0
  103. autogen/agentchat/contrib/rag/llamaindex_query_engine.py +195 -0
  104. autogen/agentchat/contrib/rag/mongodb_query_engine.py +319 -0
  105. autogen/agentchat/contrib/rag/query_engine.py +76 -0
  106. autogen/agentchat/contrib/retrieve_assistant_agent.py +59 -0
  107. autogen/agentchat/contrib/retrieve_user_proxy_agent.py +704 -0
  108. autogen/agentchat/contrib/society_of_mind_agent.py +200 -0
  109. autogen/agentchat/contrib/swarm_agent.py +1404 -0
  110. autogen/agentchat/contrib/text_analyzer_agent.py +79 -0
  111. autogen/agentchat/contrib/vectordb/__init__.py +5 -0
  112. autogen/agentchat/contrib/vectordb/base.py +224 -0
  113. autogen/agentchat/contrib/vectordb/chromadb.py +316 -0
  114. autogen/agentchat/contrib/vectordb/couchbase.py +405 -0
  115. autogen/agentchat/contrib/vectordb/mongodb.py +551 -0
  116. autogen/agentchat/contrib/vectordb/pgvectordb.py +927 -0
  117. autogen/agentchat/contrib/vectordb/qdrant.py +320 -0
  118. autogen/agentchat/contrib/vectordb/utils.py +126 -0
  119. autogen/agentchat/contrib/web_surfer.py +304 -0
  120. autogen/agentchat/conversable_agent.py +4307 -0
  121. autogen/agentchat/group/__init__.py +67 -0
  122. autogen/agentchat/group/available_condition.py +91 -0
  123. autogen/agentchat/group/context_condition.py +77 -0
  124. autogen/agentchat/group/context_expression.py +238 -0
  125. autogen/agentchat/group/context_str.py +39 -0
  126. autogen/agentchat/group/context_variables.py +182 -0
  127. autogen/agentchat/group/events/transition_events.py +111 -0
  128. autogen/agentchat/group/group_tool_executor.py +324 -0
  129. autogen/agentchat/group/group_utils.py +659 -0
  130. autogen/agentchat/group/guardrails.py +179 -0
  131. autogen/agentchat/group/handoffs.py +303 -0
  132. autogen/agentchat/group/llm_condition.py +93 -0
  133. autogen/agentchat/group/multi_agent_chat.py +291 -0
  134. autogen/agentchat/group/on_condition.py +55 -0
  135. autogen/agentchat/group/on_context_condition.py +51 -0
  136. autogen/agentchat/group/patterns/__init__.py +18 -0
  137. autogen/agentchat/group/patterns/auto.py +160 -0
  138. autogen/agentchat/group/patterns/manual.py +177 -0
  139. autogen/agentchat/group/patterns/pattern.py +295 -0
  140. autogen/agentchat/group/patterns/random.py +106 -0
  141. autogen/agentchat/group/patterns/round_robin.py +117 -0
  142. autogen/agentchat/group/reply_result.py +24 -0
  143. autogen/agentchat/group/safeguards/__init__.py +21 -0
  144. autogen/agentchat/group/safeguards/api.py +241 -0
  145. autogen/agentchat/group/safeguards/enforcer.py +1158 -0
  146. autogen/agentchat/group/safeguards/events.py +140 -0
  147. autogen/agentchat/group/safeguards/validator.py +435 -0
  148. autogen/agentchat/group/speaker_selection_result.py +41 -0
  149. autogen/agentchat/group/targets/__init__.py +4 -0
  150. autogen/agentchat/group/targets/function_target.py +245 -0
  151. autogen/agentchat/group/targets/group_chat_target.py +133 -0
  152. autogen/agentchat/group/targets/group_manager_target.py +151 -0
  153. autogen/agentchat/group/targets/transition_target.py +424 -0
  154. autogen/agentchat/group/targets/transition_utils.py +6 -0
  155. autogen/agentchat/groupchat.py +1832 -0
  156. autogen/agentchat/realtime/__init__.py +3 -0
  157. autogen/agentchat/realtime/experimental/__init__.py +20 -0
  158. autogen/agentchat/realtime/experimental/audio_adapters/__init__.py +8 -0
  159. autogen/agentchat/realtime/experimental/audio_adapters/twilio_audio_adapter.py +148 -0
  160. autogen/agentchat/realtime/experimental/audio_adapters/websocket_audio_adapter.py +139 -0
  161. autogen/agentchat/realtime/experimental/audio_observer.py +42 -0
  162. autogen/agentchat/realtime/experimental/clients/__init__.py +15 -0
  163. autogen/agentchat/realtime/experimental/clients/gemini/__init__.py +7 -0
  164. autogen/agentchat/realtime/experimental/clients/gemini/client.py +274 -0
  165. autogen/agentchat/realtime/experimental/clients/oai/__init__.py +8 -0
  166. autogen/agentchat/realtime/experimental/clients/oai/base_client.py +220 -0
  167. autogen/agentchat/realtime/experimental/clients/oai/rtc_client.py +243 -0
  168. autogen/agentchat/realtime/experimental/clients/oai/utils.py +48 -0
  169. autogen/agentchat/realtime/experimental/clients/realtime_client.py +191 -0
  170. autogen/agentchat/realtime/experimental/function_observer.py +84 -0
  171. autogen/agentchat/realtime/experimental/realtime_agent.py +158 -0
  172. autogen/agentchat/realtime/experimental/realtime_events.py +42 -0
  173. autogen/agentchat/realtime/experimental/realtime_observer.py +100 -0
  174. autogen/agentchat/realtime/experimental/realtime_swarm.py +533 -0
  175. autogen/agentchat/realtime/experimental/websockets.py +21 -0
  176. autogen/agentchat/realtime_agent/__init__.py +21 -0
  177. autogen/agentchat/user_proxy_agent.py +114 -0
  178. autogen/agentchat/utils.py +206 -0
  179. autogen/agents/__init__.py +3 -0
  180. autogen/agents/contrib/__init__.py +10 -0
  181. autogen/agents/contrib/time/__init__.py +8 -0
  182. autogen/agents/contrib/time/time_reply_agent.py +74 -0
  183. autogen/agents/contrib/time/time_tool_agent.py +52 -0
  184. autogen/agents/experimental/__init__.py +27 -0
  185. autogen/agents/experimental/deep_research/__init__.py +7 -0
  186. autogen/agents/experimental/deep_research/deep_research.py +52 -0
  187. autogen/agents/experimental/discord/__init__.py +7 -0
  188. autogen/agents/experimental/discord/discord.py +66 -0
  189. autogen/agents/experimental/document_agent/__init__.py +19 -0
  190. autogen/agents/experimental/document_agent/chroma_query_engine.py +301 -0
  191. autogen/agents/experimental/document_agent/docling_doc_ingest_agent.py +113 -0
  192. autogen/agents/experimental/document_agent/document_agent.py +643 -0
  193. autogen/agents/experimental/document_agent/document_conditions.py +50 -0
  194. autogen/agents/experimental/document_agent/document_utils.py +376 -0
  195. autogen/agents/experimental/document_agent/inmemory_query_engine.py +214 -0
  196. autogen/agents/experimental/document_agent/parser_utils.py +134 -0
  197. autogen/agents/experimental/document_agent/url_utils.py +417 -0
  198. autogen/agents/experimental/reasoning/__init__.py +7 -0
  199. autogen/agents/experimental/reasoning/reasoning_agent.py +1178 -0
  200. autogen/agents/experimental/slack/__init__.py +7 -0
  201. autogen/agents/experimental/slack/slack.py +73 -0
  202. autogen/agents/experimental/telegram/__init__.py +7 -0
  203. autogen/agents/experimental/telegram/telegram.py +76 -0
  204. autogen/agents/experimental/websurfer/__init__.py +7 -0
  205. autogen/agents/experimental/websurfer/websurfer.py +70 -0
  206. autogen/agents/experimental/wikipedia/__init__.py +7 -0
  207. autogen/agents/experimental/wikipedia/wikipedia.py +88 -0
  208. autogen/browser_utils.py +309 -0
  209. autogen/cache/__init__.py +10 -0
  210. autogen/cache/abstract_cache_base.py +71 -0
  211. autogen/cache/cache.py +203 -0
  212. autogen/cache/cache_factory.py +88 -0
  213. autogen/cache/cosmos_db_cache.py +144 -0
  214. autogen/cache/disk_cache.py +97 -0
  215. autogen/cache/in_memory_cache.py +54 -0
  216. autogen/cache/redis_cache.py +119 -0
  217. autogen/code_utils.py +598 -0
  218. autogen/coding/__init__.py +30 -0
  219. autogen/coding/base.py +120 -0
  220. autogen/coding/docker_commandline_code_executor.py +283 -0
  221. autogen/coding/factory.py +56 -0
  222. autogen/coding/func_with_reqs.py +203 -0
  223. autogen/coding/jupyter/__init__.py +23 -0
  224. autogen/coding/jupyter/base.py +36 -0
  225. autogen/coding/jupyter/docker_jupyter_server.py +160 -0
  226. autogen/coding/jupyter/embedded_ipython_code_executor.py +182 -0
  227. autogen/coding/jupyter/import_utils.py +82 -0
  228. autogen/coding/jupyter/jupyter_client.py +224 -0
  229. autogen/coding/jupyter/jupyter_code_executor.py +154 -0
  230. autogen/coding/jupyter/local_jupyter_server.py +164 -0
  231. autogen/coding/local_commandline_code_executor.py +341 -0
  232. autogen/coding/markdown_code_extractor.py +44 -0
  233. autogen/coding/utils.py +55 -0
  234. autogen/coding/yepcode_code_executor.py +197 -0
  235. autogen/doc_utils.py +35 -0
  236. autogen/environments/__init__.py +10 -0
  237. autogen/environments/docker_python_environment.py +365 -0
  238. autogen/environments/python_environment.py +125 -0
  239. autogen/environments/system_python_environment.py +85 -0
  240. autogen/environments/venv_python_environment.py +220 -0
  241. autogen/environments/working_directory.py +74 -0
  242. autogen/events/__init__.py +7 -0
  243. autogen/events/agent_events.py +1016 -0
  244. autogen/events/base_event.py +100 -0
  245. autogen/events/client_events.py +168 -0
  246. autogen/events/helpers.py +44 -0
  247. autogen/events/print_event.py +45 -0
  248. autogen/exception_utils.py +73 -0
  249. autogen/extensions/__init__.py +5 -0
  250. autogen/fast_depends/__init__.py +16 -0
  251. autogen/fast_depends/_compat.py +75 -0
  252. autogen/fast_depends/core/__init__.py +14 -0
  253. autogen/fast_depends/core/build.py +206 -0
  254. autogen/fast_depends/core/model.py +527 -0
  255. autogen/fast_depends/dependencies/__init__.py +15 -0
  256. autogen/fast_depends/dependencies/model.py +30 -0
  257. autogen/fast_depends/dependencies/provider.py +40 -0
  258. autogen/fast_depends/library/__init__.py +10 -0
  259. autogen/fast_depends/library/model.py +46 -0
  260. autogen/fast_depends/py.typed +6 -0
  261. autogen/fast_depends/schema.py +66 -0
  262. autogen/fast_depends/use.py +272 -0
  263. autogen/fast_depends/utils.py +177 -0
  264. autogen/formatting_utils.py +83 -0
  265. autogen/function_utils.py +13 -0
  266. autogen/graph_utils.py +173 -0
  267. autogen/import_utils.py +539 -0
  268. autogen/interop/__init__.py +22 -0
  269. autogen/interop/crewai/__init__.py +7 -0
  270. autogen/interop/crewai/crewai.py +88 -0
  271. autogen/interop/interoperability.py +71 -0
  272. autogen/interop/interoperable.py +46 -0
  273. autogen/interop/langchain/__init__.py +8 -0
  274. autogen/interop/langchain/langchain_chat_model_factory.py +156 -0
  275. autogen/interop/langchain/langchain_tool.py +78 -0
  276. autogen/interop/litellm/__init__.py +7 -0
  277. autogen/interop/litellm/litellm_config_factory.py +178 -0
  278. autogen/interop/pydantic_ai/__init__.py +7 -0
  279. autogen/interop/pydantic_ai/pydantic_ai.py +172 -0
  280. autogen/interop/registry.py +70 -0
  281. autogen/io/__init__.py +15 -0
  282. autogen/io/base.py +151 -0
  283. autogen/io/console.py +56 -0
  284. autogen/io/processors/__init__.py +12 -0
  285. autogen/io/processors/base.py +21 -0
  286. autogen/io/processors/console_event_processor.py +61 -0
  287. autogen/io/run_response.py +294 -0
  288. autogen/io/thread_io_stream.py +63 -0
  289. autogen/io/websockets.py +214 -0
  290. autogen/json_utils.py +42 -0
  291. autogen/llm_clients/MIGRATION_TO_V2.md +782 -0
  292. autogen/llm_clients/__init__.py +77 -0
  293. autogen/llm_clients/client_v2.py +122 -0
  294. autogen/llm_clients/models/__init__.py +55 -0
  295. autogen/llm_clients/models/content_blocks.py +389 -0
  296. autogen/llm_clients/models/unified_message.py +145 -0
  297. autogen/llm_clients/models/unified_response.py +83 -0
  298. autogen/llm_clients/openai_completions_client.py +444 -0
  299. autogen/llm_config/__init__.py +11 -0
  300. autogen/llm_config/client.py +59 -0
  301. autogen/llm_config/config.py +461 -0
  302. autogen/llm_config/entry.py +169 -0
  303. autogen/llm_config/types.py +37 -0
  304. autogen/llm_config/utils.py +223 -0
  305. autogen/logger/__init__.py +11 -0
  306. autogen/logger/base_logger.py +129 -0
  307. autogen/logger/file_logger.py +262 -0
  308. autogen/logger/logger_factory.py +42 -0
  309. autogen/logger/logger_utils.py +57 -0
  310. autogen/logger/sqlite_logger.py +524 -0
  311. autogen/math_utils.py +338 -0
  312. autogen/mcp/__init__.py +7 -0
  313. autogen/mcp/__main__.py +78 -0
  314. autogen/mcp/helpers.py +45 -0
  315. autogen/mcp/mcp_client.py +349 -0
  316. autogen/mcp/mcp_proxy/__init__.py +19 -0
  317. autogen/mcp/mcp_proxy/fastapi_code_generator_helpers.py +62 -0
  318. autogen/mcp/mcp_proxy/mcp_proxy.py +577 -0
  319. autogen/mcp/mcp_proxy/operation_grouping.py +166 -0
  320. autogen/mcp/mcp_proxy/operation_renaming.py +110 -0
  321. autogen/mcp/mcp_proxy/patch_fastapi_code_generator.py +98 -0
  322. autogen/mcp/mcp_proxy/security.py +399 -0
  323. autogen/mcp/mcp_proxy/security_schema_visitor.py +37 -0
  324. autogen/messages/__init__.py +7 -0
  325. autogen/messages/agent_messages.py +946 -0
  326. autogen/messages/base_message.py +108 -0
  327. autogen/messages/client_messages.py +172 -0
  328. autogen/messages/print_message.py +48 -0
  329. autogen/oai/__init__.py +61 -0
  330. autogen/oai/anthropic.py +1516 -0
  331. autogen/oai/bedrock.py +800 -0
  332. autogen/oai/cerebras.py +302 -0
  333. autogen/oai/client.py +1658 -0
  334. autogen/oai/client_utils.py +196 -0
  335. autogen/oai/cohere.py +494 -0
  336. autogen/oai/gemini.py +1045 -0
  337. autogen/oai/gemini_types.py +156 -0
  338. autogen/oai/groq.py +319 -0
  339. autogen/oai/mistral.py +311 -0
  340. autogen/oai/oai_models/__init__.py +23 -0
  341. autogen/oai/oai_models/_models.py +16 -0
  342. autogen/oai/oai_models/chat_completion.py +86 -0
  343. autogen/oai/oai_models/chat_completion_audio.py +32 -0
  344. autogen/oai/oai_models/chat_completion_message.py +97 -0
  345. autogen/oai/oai_models/chat_completion_message_tool_call.py +60 -0
  346. autogen/oai/oai_models/chat_completion_token_logprob.py +62 -0
  347. autogen/oai/oai_models/completion_usage.py +59 -0
  348. autogen/oai/ollama.py +657 -0
  349. autogen/oai/openai_responses.py +451 -0
  350. autogen/oai/openai_utils.py +897 -0
  351. autogen/oai/together.py +387 -0
  352. autogen/remote/__init__.py +18 -0
  353. autogen/remote/agent.py +199 -0
  354. autogen/remote/agent_service.py +197 -0
  355. autogen/remote/errors.py +17 -0
  356. autogen/remote/httpx_client_factory.py +131 -0
  357. autogen/remote/protocol.py +37 -0
  358. autogen/remote/retry.py +102 -0
  359. autogen/remote/runtime.py +96 -0
  360. autogen/retrieve_utils.py +490 -0
  361. autogen/runtime_logging.py +161 -0
  362. autogen/testing/__init__.py +12 -0
  363. autogen/testing/messages.py +45 -0
  364. autogen/testing/test_agent.py +111 -0
  365. autogen/token_count_utils.py +280 -0
  366. autogen/tools/__init__.py +20 -0
  367. autogen/tools/contrib/__init__.py +9 -0
  368. autogen/tools/contrib/time/__init__.py +7 -0
  369. autogen/tools/contrib/time/time.py +40 -0
  370. autogen/tools/dependency_injection.py +249 -0
  371. autogen/tools/experimental/__init__.py +54 -0
  372. autogen/tools/experimental/browser_use/__init__.py +7 -0
  373. autogen/tools/experimental/browser_use/browser_use.py +154 -0
  374. autogen/tools/experimental/code_execution/__init__.py +7 -0
  375. autogen/tools/experimental/code_execution/python_code_execution.py +86 -0
  376. autogen/tools/experimental/crawl4ai/__init__.py +7 -0
  377. autogen/tools/experimental/crawl4ai/crawl4ai.py +150 -0
  378. autogen/tools/experimental/deep_research/__init__.py +7 -0
  379. autogen/tools/experimental/deep_research/deep_research.py +329 -0
  380. autogen/tools/experimental/duckduckgo/__init__.py +7 -0
  381. autogen/tools/experimental/duckduckgo/duckduckgo_search.py +103 -0
  382. autogen/tools/experimental/firecrawl/__init__.py +7 -0
  383. autogen/tools/experimental/firecrawl/firecrawl_tool.py +836 -0
  384. autogen/tools/experimental/google/__init__.py +14 -0
  385. autogen/tools/experimental/google/authentication/__init__.py +11 -0
  386. autogen/tools/experimental/google/authentication/credentials_hosted_provider.py +43 -0
  387. autogen/tools/experimental/google/authentication/credentials_local_provider.py +91 -0
  388. autogen/tools/experimental/google/authentication/credentials_provider.py +35 -0
  389. autogen/tools/experimental/google/drive/__init__.py +9 -0
  390. autogen/tools/experimental/google/drive/drive_functions.py +124 -0
  391. autogen/tools/experimental/google/drive/toolkit.py +88 -0
  392. autogen/tools/experimental/google/model.py +17 -0
  393. autogen/tools/experimental/google/toolkit_protocol.py +19 -0
  394. autogen/tools/experimental/google_search/__init__.py +8 -0
  395. autogen/tools/experimental/google_search/google_search.py +93 -0
  396. autogen/tools/experimental/google_search/youtube_search.py +181 -0
  397. autogen/tools/experimental/messageplatform/__init__.py +17 -0
  398. autogen/tools/experimental/messageplatform/discord/__init__.py +7 -0
  399. autogen/tools/experimental/messageplatform/discord/discord.py +284 -0
  400. autogen/tools/experimental/messageplatform/slack/__init__.py +7 -0
  401. autogen/tools/experimental/messageplatform/slack/slack.py +385 -0
  402. autogen/tools/experimental/messageplatform/telegram/__init__.py +7 -0
  403. autogen/tools/experimental/messageplatform/telegram/telegram.py +271 -0
  404. autogen/tools/experimental/perplexity/__init__.py +7 -0
  405. autogen/tools/experimental/perplexity/perplexity_search.py +249 -0
  406. autogen/tools/experimental/reliable/__init__.py +10 -0
  407. autogen/tools/experimental/reliable/reliable.py +1311 -0
  408. autogen/tools/experimental/searxng/__init__.py +7 -0
  409. autogen/tools/experimental/searxng/searxng_search.py +142 -0
  410. autogen/tools/experimental/tavily/__init__.py +7 -0
  411. autogen/tools/experimental/tavily/tavily_search.py +176 -0
  412. autogen/tools/experimental/web_search_preview/__init__.py +7 -0
  413. autogen/tools/experimental/web_search_preview/web_search_preview.py +120 -0
  414. autogen/tools/experimental/wikipedia/__init__.py +7 -0
  415. autogen/tools/experimental/wikipedia/wikipedia.py +284 -0
  416. autogen/tools/function_utils.py +412 -0
  417. autogen/tools/tool.py +188 -0
  418. autogen/tools/toolkit.py +86 -0
  419. autogen/types.py +29 -0
  420. autogen/version.py +7 -0
  421. templates/client_template/main.jinja2 +72 -0
  422. templates/config_template/config.jinja2 +7 -0
  423. templates/main.jinja2 +61 -0
@@ -0,0 +1,1158 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import re
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+ from ....code_utils import content_str
13
+ from ....io.base import IOStream
14
+ from ....llm_config import LLMConfig
15
+ from ...conversable_agent import ConversableAgent
16
+ from ...groupchat import GroupChatManager
17
+ from ..guardrails import LLMGuardrail, RegexGuardrail
18
+ from ..targets.transition_target import TransitionTarget
19
+ from .events import SafeguardEvent
20
+
21
+
22
+ class SafeguardEnforcer:
23
+ """Main safeguard enforcer - executes safeguard policies"""
24
+
25
+ @staticmethod
26
+ def _stringify_content(value: Any) -> str:
27
+ if isinstance(value, (str, list)) or value is None:
28
+ try:
29
+ return content_str(value)
30
+ except (TypeError, ValueError, AssertionError):
31
+ pass
32
+ return "" if value is None else str(value)
33
+
34
+ def __init__(
35
+ self,
36
+ policy: dict[str, Any] | str,
37
+ safeguard_llm_config: LLMConfig | dict[str, Any] | None = None,
38
+ mask_llm_config: LLMConfig | dict[str, Any] | None = None,
39
+ groupchat_manager: GroupChatManager | None = None,
40
+ agents: list[ConversableAgent] | None = None,
41
+ ):
42
+ """Initialize the safeguard enforcer.
43
+
44
+ Args:
45
+ policy: Safeguard policy dict or path to JSON file
46
+ safeguard_llm_config: LLM configuration for safeguard checks
47
+ mask_llm_config: LLM configuration for masking
48
+ groupchat_manager: GroupChat manager instance for group chat scenarios
49
+ agents: List of conversable agents to apply safeguards to
50
+ """
51
+ self.policy = self._load_policy(policy)
52
+ self.safeguard_llm_config = safeguard_llm_config
53
+ self.mask_llm_config = mask_llm_config
54
+ self.groupchat_manager = groupchat_manager
55
+ self.agents = agents
56
+ self.group_tool_executor = None
57
+ if self.groupchat_manager:
58
+ for agent in self.groupchat_manager.groupchat.agents:
59
+ if agent.name == "_Group_Tool_Executor":
60
+ self.group_tool_executor = agent # type: ignore[assignment]
61
+ break
62
+
63
+ # Validate policy format before proceeding
64
+ self._validate_policy()
65
+
66
+ # Create mask agent for content masking
67
+ if self.mask_llm_config:
68
+ from ...conversable_agent import ConversableAgent
69
+
70
+ self.mask_agent = ConversableAgent(
71
+ name="mask_agent",
72
+ system_message="You are a agent responsible for masking sensitive information.",
73
+ llm_config=self.mask_llm_config,
74
+ human_input_mode="NEVER",
75
+ max_consecutive_auto_reply=1,
76
+ )
77
+
78
+ # Parse safeguard rules
79
+ self.inter_agent_rules = self._parse_inter_agent_rules()
80
+ self.environment_rules = self._parse_environment_rules()
81
+
82
+ # Send load event
83
+ self._send_safeguard_event(
84
+ event_type="load",
85
+ message=f"Loaded {len(self.inter_agent_rules)} inter-agent and {len(self.environment_rules)} environment safeguard rules",
86
+ )
87
+
88
+ def _send_safeguard_event(
89
+ self,
90
+ event_type: str,
91
+ message: str,
92
+ source_agent: str | None = None,
93
+ target_agent: str | None = None,
94
+ guardrail_type: str | None = None,
95
+ action: str | None = None,
96
+ content_preview: str | None = None,
97
+ ) -> None:
98
+ """Send a safeguard event to the IOStream."""
99
+ iostream = IOStream.get_default()
100
+ event = SafeguardEvent(
101
+ event_type=event_type,
102
+ message=message,
103
+ source_agent=source_agent,
104
+ target_agent=target_agent,
105
+ guardrail_type=guardrail_type,
106
+ action=action,
107
+ content_preview=content_preview,
108
+ )
109
+ iostream.send(event)
110
+
111
+ def _load_policy(self, policy: dict[str, Any] | str) -> dict[str, Any]:
112
+ """Load policy from file or use provided dict."""
113
+ if isinstance(policy, str):
114
+ with open(policy) as f:
115
+ result: dict[str, Any] = json.load(f)
116
+ return result
117
+ return policy
118
+
119
+ def _validate_policy(self) -> None:
120
+ """Validate policy format and syntax."""
121
+ from .validator import SafeguardValidator
122
+
123
+ validator = SafeguardValidator(self.policy)
124
+ validator.validate_policy_structure()
125
+
126
+ def _parse_inter_agent_rules(self) -> list[dict[str, Any]]:
127
+ """Parse inter-agent safeguard rules from policy."""
128
+ rules = []
129
+ inter_agent = self.policy.get("inter_agent_safeguards", {})
130
+
131
+ # Agent transitions
132
+ for rule in inter_agent.get("agent_transitions", []):
133
+ # Create guardrail based on check_method
134
+ check_method = rule.get("check_method", "regex")
135
+ guardrail: LLMGuardrail | RegexGuardrail | None = None
136
+ action = rule.get("violation_response", rule.get("action", "block")) # Support both field names
137
+
138
+ if check_method == "llm":
139
+ if not self.safeguard_llm_config:
140
+ raise ValueError(
141
+ f"safeguard_llm_config is required for LLM-based guardrail: {rule['message_source']} -> {rule['message_destination']}"
142
+ )
143
+
144
+ # Handle different LLM check types
145
+ if "custom_prompt" in rule:
146
+ # Custom prompt for LLM guardrail
147
+ condition = rule["custom_prompt"]
148
+
149
+ elif "disallow_item" in rule:
150
+ # Disallow items for LLM guardrail
151
+ condition = f"Check if this content contains any of these disallowed categories: {', '.join(rule['disallow_item'])}"
152
+
153
+ else:
154
+ raise ValueError(
155
+ f"Either custom_prompt or disallow_item must be provided for LLM guardrail: {rule['message_source']} -> {rule['message_destination']}"
156
+ )
157
+
158
+ # Create LLM guardrail - handle dict config by converting to LLMConfig
159
+ llm_config = self.safeguard_llm_config
160
+ if isinstance(llm_config, dict):
161
+ llm_config = LLMConfig(config_list=[llm_config])
162
+
163
+ guardrail = LLMGuardrail(
164
+ name=f"llm_guard_{rule['message_source']}_{rule['message_destination']}",
165
+ condition=condition,
166
+ target=TransitionTarget(),
167
+ llm_config=llm_config,
168
+ activation_message=rule.get("activation_message", "LLM detected violation"),
169
+ )
170
+
171
+ elif check_method == "regex":
172
+ if "pattern" in rule:
173
+ # Regex pattern guardrail
174
+ guardrail = RegexGuardrail(
175
+ name=f"regex_guard_{rule['message_source']}_{rule['message_destination']}",
176
+ condition=rule["pattern"],
177
+ target=TransitionTarget(),
178
+ activation_message=rule.get("activation_message", "Regex pattern matched"),
179
+ )
180
+
181
+ # Add rule with guardrail
182
+ parsed_rule = {
183
+ "type": "agent_transition",
184
+ "source": rule["message_source"],
185
+ "target": rule["message_destination"],
186
+ "action": action,
187
+ "guardrail": guardrail,
188
+ "activation_message": rule.get("activation_message", "Content blocked by safeguard"),
189
+ }
190
+
191
+ # Keep legacy fields for backward compatibility
192
+ if "disallow_item" in rule:
193
+ parsed_rule["disallow"] = rule["disallow_item"]
194
+ if "pattern" in rule:
195
+ parsed_rule["pattern"] = rule["pattern"]
196
+ if "custom_prompt" in rule:
197
+ parsed_rule["custom_prompt"] = rule["custom_prompt"]
198
+
199
+ rules.append(parsed_rule)
200
+
201
+ # Groupchat message check
202
+ if "groupchat_message_check" in inter_agent:
203
+ rule = inter_agent["groupchat_message_check"]
204
+ rules.append({
205
+ "type": "groupchat_message",
206
+ "source": "*",
207
+ "target": "*",
208
+ "action": rule.get("pet_action", "block"),
209
+ "disallow": rule.get("disallow_item", []),
210
+ })
211
+
212
+ return rules
213
+
214
+ def _parse_environment_rules(self) -> list[dict[str, Any]]:
215
+ """Parse agent-environment safeguard rules from policy."""
216
+ rules = []
217
+ env_rules = self.policy.get("agent_environment_safeguards", {})
218
+
219
+ # Tool interaction rules
220
+ for rule in env_rules.get("tool_interaction", []):
221
+ check_method = rule.get("check_method", "regex") # default to regex for backward compatibility
222
+ action = rule.get("violation_response", rule.get("action", "block"))
223
+
224
+ if check_method == "llm":
225
+ # LLM-based tool interaction rule - requires message_source/message_destination
226
+ if "message_source" not in rule or "message_destination" not in rule:
227
+ raise ValueError(
228
+ "tool_interaction with check_method 'llm' must have 'message_source' and 'message_destination'"
229
+ )
230
+
231
+ parsed_rule = {
232
+ "type": "tool_interaction",
233
+ "message_source": rule["message_source"],
234
+ "message_destination": rule["message_destination"],
235
+ "check_method": "llm",
236
+ "action": action,
237
+ "activation_message": rule.get("activation_message", "LLM blocked tool output"),
238
+ }
239
+
240
+ # Add LLM-specific parameters
241
+ if "custom_prompt" in rule:
242
+ parsed_rule["custom_prompt"] = rule["custom_prompt"]
243
+ elif "disallow_item" in rule:
244
+ parsed_rule["disallow"] = rule["disallow_item"]
245
+
246
+ rules.append(parsed_rule)
247
+
248
+ elif check_method == "regex":
249
+ # Regex pattern-based rule - now requires message_source/message_destination
250
+ if "message_source" not in rule or "message_destination" not in rule:
251
+ raise ValueError(
252
+ "tool_interaction with check_method 'regex' must have 'message_source' and 'message_destination'"
253
+ )
254
+ if "pattern" not in rule:
255
+ raise ValueError("tool_interaction with check_method 'regex' must have 'pattern'")
256
+
257
+ rules.append({
258
+ "type": "tool_interaction",
259
+ "message_source": rule["message_source"],
260
+ "message_destination": rule["message_destination"],
261
+ "check_method": "regex",
262
+ "pattern": rule["pattern"],
263
+ "action": action,
264
+ "activation_message": rule.get("activation_message", "Content blocked by safeguard"),
265
+ })
266
+ else:
267
+ raise ValueError(
268
+ "tool_interaction rule must have check_method 'llm' or 'regex' with appropriate parameters"
269
+ )
270
+
271
+ # LLM interaction rules
272
+ for rule in env_rules.get("llm_interaction", []):
273
+ check_method = rule.get("check_method", "regex") # default to regex for backward compatibility
274
+ action = rule.get("action", "block")
275
+
276
+ # All llm_interaction rules now require message_source/message_destination
277
+ if "message_source" not in rule or "message_destination" not in rule:
278
+ raise ValueError("llm_interaction rule must have 'message_source' and 'message_destination'")
279
+
280
+ if check_method == "llm":
281
+ # LLM-based LLM interaction rule
282
+ parsed_rule = {
283
+ "type": "llm_interaction",
284
+ "message_source": rule["message_source"],
285
+ "message_destination": rule["message_destination"],
286
+ "check_method": "llm",
287
+ "action": action,
288
+ "activation_message": rule.get("activation_message", "LLM blocked content"),
289
+ }
290
+
291
+ # Add LLM-specific parameters
292
+ if "custom_prompt" in rule:
293
+ parsed_rule["custom_prompt"] = rule["custom_prompt"]
294
+ elif "disallow_item" in rule:
295
+ parsed_rule["disallow_item"] = rule["disallow_item"]
296
+ else:
297
+ raise ValueError(
298
+ "llm_interaction with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
299
+ )
300
+
301
+ rules.append(parsed_rule)
302
+
303
+ elif check_method == "regex":
304
+ # Regex-based LLM interaction rule
305
+ if "pattern" not in rule:
306
+ raise ValueError("llm_interaction with check_method 'regex' must have 'pattern'")
307
+
308
+ rules.append({
309
+ "type": "llm_interaction",
310
+ "message_source": rule["message_source"],
311
+ "message_destination": rule["message_destination"],
312
+ "check_method": "regex",
313
+ "pattern": rule["pattern"],
314
+ "action": action,
315
+ "activation_message": rule.get("activation_message", "Content blocked by safeguard"),
316
+ })
317
+ else:
318
+ raise ValueError(
319
+ "llm_interaction rule must have check_method 'llm' or 'regex' with appropriate parameters"
320
+ )
321
+
322
+ # User interaction rules
323
+ for rule in env_rules.get("user_interaction", []):
324
+ check_method = rule.get("check_method", "llm") # default to llm for backward compatibility
325
+ action = rule.get("action", "block")
326
+
327
+ # All user_interaction rules now require message_source/message_destination
328
+ if "message_source" not in rule or "message_destination" not in rule:
329
+ raise ValueError("user_interaction rule must have 'message_source' and 'message_destination'")
330
+
331
+ if check_method == "llm":
332
+ # LLM-based user interaction rule
333
+ parsed_rule = {
334
+ "type": "user_interaction",
335
+ "message_source": rule["message_source"],
336
+ "message_destination": rule["message_destination"],
337
+ "check_method": "llm",
338
+ "action": action,
339
+ }
340
+
341
+ # Add LLM-specific parameters
342
+ if "custom_prompt" in rule:
343
+ parsed_rule["custom_prompt"] = rule["custom_prompt"]
344
+ elif "disallow_item" in rule:
345
+ parsed_rule["disallow_item"] = rule["disallow_item"]
346
+ else:
347
+ raise ValueError(
348
+ "user_interaction with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
349
+ )
350
+
351
+ rules.append(parsed_rule)
352
+
353
+ elif check_method == "regex":
354
+ # Regex-based user interaction rule
355
+ if "pattern" not in rule:
356
+ raise ValueError("user_interaction with check_method 'regex' must have 'pattern'")
357
+
358
+ rules.append({
359
+ "type": "user_interaction",
360
+ "message_source": rule["message_source"],
361
+ "message_destination": rule["message_destination"],
362
+ "check_method": "regex",
363
+ "pattern": rule["pattern"],
364
+ "action": action,
365
+ })
366
+ else:
367
+ raise ValueError(
368
+ "user_interaction rule must have check_method 'llm' or 'regex' with appropriate parameters"
369
+ )
370
+
371
+ return rules
372
+
373
+ def create_agent_hooks(self, agent_name: str) -> dict[str, Callable[..., Any]]:
374
+ """Create hook functions for a specific agent, only for rule types that exist."""
375
+ hooks = {}
376
+
377
+ # Check if we have any tool interaction rules that apply to this agent
378
+ if agent_name == "_Group_Tool_Executor":
379
+ # group tool executor is running all tools, so we need to check all tool interaction rules
380
+ agent_tool_rules = [rule for rule in self.environment_rules if rule["type"] == "tool_interaction"]
381
+ else:
382
+ agent_tool_rules = [
383
+ rule
384
+ for rule in self.environment_rules
385
+ if rule["type"] == "tool_interaction"
386
+ and (
387
+ rule.get("message_destination") == agent_name
388
+ or rule.get("message_source") == agent_name
389
+ or rule.get("agent_name") == agent_name
390
+ or "message_destination" not in rule
391
+ )
392
+ ]
393
+ if agent_tool_rules:
394
+
395
+ def tool_input_hook(tool_input: dict[str, Any]) -> dict[str, Any] | None:
396
+ result = self._check_tool_interaction(agent_name, tool_input, "input")
397
+ return result if result is not None else tool_input
398
+
399
+ def tool_output_hook(tool_input: dict[str, Any]) -> dict[str, Any] | None:
400
+ result = self._check_tool_interaction(agent_name, tool_input, "output")
401
+ return result if result is not None else tool_input
402
+
403
+ hooks["safeguard_tool_inputs"] = tool_input_hook
404
+ hooks["safeguard_tool_outputs"] = tool_output_hook
405
+
406
+ # Check if we have any LLM interaction rules that apply to this agent
407
+ agent_llm_rules = [
408
+ rule
409
+ for rule in self.environment_rules
410
+ if rule["type"] == "llm_interaction"
411
+ and (
412
+ rule.get("message_destination") == agent_name
413
+ or rule.get("message_source") == agent_name
414
+ or rule.get("agent_name") == agent_name
415
+ or "message_destination" not in rule
416
+ )
417
+ ] # Simple pattern rules apply to all
418
+
419
+ if agent_llm_rules:
420
+
421
+ def llm_input_hook(tool_input: dict[str, Any]) -> dict[str, Any] | None:
422
+ # Extract messages from the data structure if needed
423
+ messages = tool_input if isinstance(tool_input, list) else tool_input.get("messages", tool_input)
424
+ result = self._check_llm_interaction(agent_name, messages, "input")
425
+ if isinstance(result, list) and isinstance(tool_input, dict) and "messages" in tool_input:
426
+ return {**tool_input, "messages": result}
427
+ elif isinstance(result, dict):
428
+ return result
429
+ elif result is not None and not isinstance(result, dict):
430
+ # Convert string or other types to dict format
431
+ return {"content": str(result), "role": "function"}
432
+ elif result is not None and isinstance(result, dict) and result != tool_input:
433
+ # Return the modified dict result
434
+ return result
435
+ return tool_input
436
+
437
+ def llm_output_hook(tool_input: dict[str, Any]) -> dict[str, Any] | None:
438
+ result = self._check_llm_interaction(agent_name, tool_input, "output")
439
+ if isinstance(result, dict):
440
+ return result
441
+ elif result is not None and not isinstance(result, dict):
442
+ # Convert string or other types to dict format
443
+ return {"content": str(result), "role": "function"}
444
+ elif result is not None and isinstance(result, dict) and result != tool_input:
445
+ # Return the modified dict result
446
+ return result
447
+ return tool_input
448
+
449
+ hooks["safeguard_llm_inputs"] = llm_input_hook
450
+ hooks["safeguard_llm_outputs"] = llm_output_hook
451
+
452
+ # Check if we have any user interaction rules that apply to this agent
453
+ agent_user_rules = [
454
+ rule
455
+ for rule in self.environment_rules
456
+ if rule["type"] == "user_interaction" and rule.get("message_destination") == agent_name
457
+ ]
458
+
459
+ if agent_user_rules:
460
+
461
+ def human_input_hook(tool_input: dict[str, Any]) -> dict[str, Any] | None:
462
+ # Extract human input from data structure
463
+ human_input = tool_input.get("content", str(tool_input))
464
+ result = self._check_user_interaction(agent_name, human_input)
465
+ if result != human_input and isinstance(tool_input, dict):
466
+ return {**tool_input, "content": result}
467
+ return tool_input if result == human_input else {"content": result}
468
+
469
+ hooks["safeguard_human_inputs"] = human_input_hook
470
+
471
+ # Check if we have any inter-agent rules that apply to this agent
472
+ # Note: For group chats, inter-agent communication is handled by GroupChat._run_inter_agent_guardrails()
473
+ # But for direct agent-to-agent communication, we need the process_message_before_send hook
474
+ agent_inter_rules = [
475
+ rule
476
+ for rule in self.inter_agent_rules
477
+ if (
478
+ rule.get("source") == agent_name
479
+ or rule.get("target") == agent_name
480
+ or rule.get("source") == "*"
481
+ or rule.get("target") == "*"
482
+ )
483
+ ]
484
+
485
+ if agent_inter_rules:
486
+
487
+ def message_before_send_hook(
488
+ sender: Any, message: dict[str, Any] | str, recipient: Any, silent: Any = None
489
+ ) -> dict[str, Any] | str:
490
+ _ = silent # Unused parameter
491
+ result = self._check_inter_agent_communication(sender.name, recipient.name, message)
492
+ return result
493
+
494
+ hooks["process_message_before_send"] = message_before_send_hook # type: ignore[assignment]
495
+
496
+ return hooks
497
+
498
+ def _check_llm_violation(
499
+ self, content: str, disallow_items: list[str] | None = None, custom_prompt: str | None = None
500
+ ) -> tuple[bool, str]:
501
+ """Check if content violates LLM-based safeguard conditions."""
502
+ if not self.safeguard_llm_config:
503
+ raise ValueError("safeguard_llm_config is required for LLM-based safeguard checks")
504
+
505
+ # Determine condition based on available parameters
506
+ if custom_prompt:
507
+ condition = custom_prompt
508
+ elif disallow_items:
509
+ condition = (
510
+ f"Check if this content contains any of these disallowed categories: {', '.join(disallow_items)}"
511
+ )
512
+ else:
513
+ raise ValueError("Either custom_prompt or disallow_items must be provided")
514
+
515
+ # Create LLM guardrail for checking
516
+ # Handle dict config by converting to LLMConfig
517
+ llm_config = self.safeguard_llm_config
518
+ if isinstance(llm_config, dict):
519
+ llm_config = LLMConfig(config_list=[llm_config])
520
+
521
+ from ..targets.transition_target import TransitionTarget
522
+
523
+ guardrail = LLMGuardrail(
524
+ name="temp_safeguard_check",
525
+ condition=condition,
526
+ target=TransitionTarget(),
527
+ llm_config=llm_config,
528
+ activation_message="Content violates safeguard conditions",
529
+ )
530
+
531
+ try:
532
+ result = guardrail.check(content)
533
+ return result.activated, result.justification
534
+ except Exception as e:
535
+ raise RuntimeError(f"Safeguard check failed: {e}")
536
+
537
+ def _check_regex_violation(self, content: str, pattern: str) -> tuple[bool, str]:
538
+ """Check if content matches a regex pattern."""
539
+ try:
540
+ if re.search(pattern, content, re.IGNORECASE):
541
+ return True, f"Content matched pattern: {pattern}"
542
+ except re.error as e:
543
+ raise ValueError(f"Invalid regex pattern '{pattern}': {e}")
544
+
545
+ return False, "No pattern match"
546
+
547
+ def _apply_action(
548
+ self,
549
+ action: str,
550
+ content: str | dict[str, Any] | list[Any],
551
+ disallow_items: list[str],
552
+ explanation: str,
553
+ custom_message: str | None = None,
554
+ pattern: str | None = None,
555
+ guardrail_type: str | None = None,
556
+ source_agent: str | None = None,
557
+ target_agent: str | None = None,
558
+ content_preview: str | None = None,
559
+ ) -> str | dict[str, Any] | list[Any]:
560
+ """Apply the specified action to content."""
561
+ message = custom_message or explanation
562
+
563
+ if action == "block":
564
+ self._send_safeguard_event(
565
+ event_type="action",
566
+ message=f"BLOCKED: {message}",
567
+ action="block",
568
+ source_agent=source_agent,
569
+ target_agent=target_agent,
570
+ content_preview=content_preview,
571
+ )
572
+ return self._handle_blocked_content(content, message)
573
+ elif action == "mask":
574
+ self._send_safeguard_event(
575
+ event_type="action",
576
+ message=f"MASKED: {message}",
577
+ action="mask",
578
+ source_agent=source_agent,
579
+ target_agent=target_agent,
580
+ content_preview=content_preview,
581
+ )
582
+
583
+ def mask_func(text: str) -> str:
584
+ return self._mask_content(text, disallow_items, explanation, pattern)
585
+
586
+ return self._handle_masked_content(content, mask_func)
587
+ elif action == "warning":
588
+ self._send_safeguard_event(
589
+ event_type="action",
590
+ message=f"WARNING: {message}",
591
+ action="warning",
592
+ source_agent=source_agent,
593
+ target_agent=target_agent,
594
+ content_preview=content_preview,
595
+ )
596
+ return content
597
+ else:
598
+ return content
599
+
600
+ def _mask_content(
601
+ self, content: str, disallow_items: list[str], explanation: str, pattern: str | None = None
602
+ ) -> str:
603
+ """Mask sensitive content using LLM, pattern-based, or simple replacement."""
604
+ # If we have a specific pattern from a regex guardrail, use it first
605
+ if pattern:
606
+ try:
607
+ masked = re.sub(pattern, "[SENSITIVE_INFO]", content, flags=re.IGNORECASE)
608
+ if masked != content: # Only return if something was actually masked
609
+ return masked
610
+ except re.error as e:
611
+ raise ValueError(f"Pattern masking failed: {e}")
612
+
613
+ # Try LLM-based masking if available
614
+ if self.mask_agent and disallow_items:
615
+ mask_prompt = f"""
616
+ Mask the sensitive information in this content with [SENSITIVE_INFO]:
617
+
618
+ Content: {content}
619
+ Sensitive categories: {", ".join(disallow_items)}
620
+ Reason: {explanation}
621
+
622
+ Return only the masked content, nothing else.
623
+ """
624
+
625
+ try:
626
+ response = self.mask_agent.generate_oai_reply(messages=[{"role": "user", "content": mask_prompt}])
627
+
628
+ if response[0] and response[1]:
629
+ masked = response[1].get("content", content) if isinstance(response[1], dict) else str(response[1])
630
+ return masked
631
+ except Exception as e:
632
+ raise ValueError(f"LLM masking failed: {e}")
633
+
634
+ return masked
635
+
636
+ def _handle_blocked_content(
637
+ self, content: str | dict[str, Any] | list[Any], block_message: str
638
+ ) -> str | dict[str, Any] | list[Any]:
639
+ """Handle blocked content based on its structure."""
640
+ block_msg = f"🛡️ BLOCKED: {block_message}"
641
+
642
+ if isinstance(content, dict):
643
+ blocked_content = content.copy()
644
+
645
+ # Handle tool_responses (like in tool outputs)
646
+ if "tool_responses" in blocked_content and blocked_content["tool_responses"]:
647
+ blocked_content["content"] = block_msg
648
+ blocked_content["tool_responses"] = [
649
+ {**response, "content": block_msg} for response in blocked_content["tool_responses"]
650
+ ]
651
+ # Handle tool_calls (like in tool inputs)
652
+ elif "tool_calls" in blocked_content and blocked_content["tool_calls"]:
653
+ blocked_content["tool_calls"] = [
654
+ {**tool_call, "function": {**tool_call["function"], "arguments": json.dumps({"error": block_msg})}}
655
+ for tool_call in blocked_content["tool_calls"]
656
+ ]
657
+ # Handle regular content
658
+ elif "content" in blocked_content:
659
+ blocked_content["content"] = block_msg
660
+ # Handle arguments (for some tool formats)
661
+ elif "arguments" in blocked_content:
662
+ blocked_content["arguments"] = block_msg
663
+ else:
664
+ # Default case - add content field
665
+ blocked_content["content"] = block_msg
666
+
667
+ return blocked_content
668
+
669
+ elif isinstance(content, list):
670
+ # Handle list of messages (like LLM inputs)
671
+ blocked_list = []
672
+ for item in content:
673
+ if isinstance(item, dict):
674
+ blocked_item = item.copy()
675
+ if "content" in blocked_item:
676
+ blocked_item["content"] = block_msg
677
+ if "tool_calls" in blocked_item:
678
+ blocked_item["tool_calls"] = [
679
+ {
680
+ **tool_call,
681
+ "function": {**tool_call["function"], "arguments": json.dumps({"error": block_msg})},
682
+ }
683
+ for tool_call in blocked_item["tool_calls"]
684
+ ]
685
+ if "tool_responses" in blocked_item:
686
+ blocked_item["tool_responses"] = [
687
+ {**response, "content": block_msg} for response in blocked_item["tool_responses"]
688
+ ]
689
+ blocked_list.append(blocked_item)
690
+ else:
691
+ blocked_list.append({"content": block_msg, "role": "function"})
692
+ return blocked_list
693
+
694
+ else:
695
+ # String or other content - return as function message
696
+ return {"content": block_msg, "role": "function"}
697
+
698
+ def _handle_masked_content(
699
+ self, content: str | dict[str, Any] | list[Any], mask_func: Callable[[str], str]
700
+ ) -> str | dict[str, Any] | list[Any]:
701
+ """Handle masked content based on its structure."""
702
+ if isinstance(content, dict):
703
+ masked_content = content.copy()
704
+
705
+ # Handle tool_responses
706
+ if "tool_responses" in masked_content and masked_content["tool_responses"]:
707
+ if "content" in masked_content:
708
+ masked_content["content"] = mask_func(self._stringify_content(masked_content.get("content")))
709
+ masked_content["tool_responses"] = [
710
+ {
711
+ **response,
712
+ "content": mask_func(self._stringify_content(response.get("content"))),
713
+ }
714
+ for response in masked_content["tool_responses"]
715
+ ]
716
+ # Handle tool_calls
717
+ elif "tool_calls" in masked_content and masked_content["tool_calls"]:
718
+ masked_content["tool_calls"] = [
719
+ {
720
+ **tool_call,
721
+ "function": {
722
+ **tool_call["function"],
723
+ "arguments": mask_func(self._stringify_content(tool_call["function"].get("arguments"))),
724
+ },
725
+ }
726
+ for tool_call in masked_content["tool_calls"]
727
+ ]
728
+ # Handle regular content
729
+ elif "content" in masked_content:
730
+ masked_content["content"] = mask_func(self._stringify_content(masked_content.get("content")))
731
+ # Handle arguments
732
+ elif "arguments" in masked_content:
733
+ masked_content["arguments"] = mask_func(self._stringify_content(masked_content.get("arguments")))
734
+
735
+ return masked_content
736
+
737
+ elif isinstance(content, list):
738
+ # Handle list of messages
739
+ masked_list = []
740
+ for item in content:
741
+ if isinstance(item, dict):
742
+ masked_item = item.copy()
743
+ if "content" in masked_item:
744
+ masked_item["content"] = mask_func(self._stringify_content(masked_item.get("content")))
745
+ if "tool_calls" in masked_item:
746
+ masked_item["tool_calls"] = [
747
+ {
748
+ **tool_call,
749
+ "function": {
750
+ **tool_call["function"],
751
+ "arguments": mask_func(
752
+ self._stringify_content(tool_call["function"].get("arguments"))
753
+ ),
754
+ },
755
+ }
756
+ for tool_call in masked_item["tool_calls"]
757
+ ]
758
+ if "tool_responses" in masked_item:
759
+ masked_item["tool_responses"] = [
760
+ {
761
+ **response,
762
+ "content": mask_func(self._stringify_content(response.get("content"))),
763
+ }
764
+ for response in masked_item["tool_responses"]
765
+ ]
766
+ masked_list.append(masked_item)
767
+ else:
768
+ # For non-dict items, wrap the masked content in a dict
769
+ masked_item_content: str = mask_func(self._stringify_content(item))
770
+ masked_list.append({"content": masked_item_content, "role": "function"})
771
+ return masked_list
772
+
773
+ else:
774
+ # String content
775
+ return mask_func(self._stringify_content(content))
776
+
777
+ def _check_inter_agent_communication(
778
+ self, sender_name: str, recipient_name: str, message: str | dict[str, Any]
779
+ ) -> str | dict[str, Any]:
780
+ """Check inter-agent communication."""
781
+ if isinstance(message, dict):
782
+ if "tool_calls" in message and isinstance(message["tool_calls"], list):
783
+ # Extract arguments from all tool calls and combine them
784
+ tool_args = []
785
+ for tool_call in message["tool_calls"]:
786
+ if "function" in tool_call and "arguments" in tool_call["function"]:
787
+ tool_args.append(tool_call["function"]["arguments"])
788
+ content_to_check = " | ".join(tool_args) if tool_args else ""
789
+ elif "tool_responses" in message and isinstance(message["tool_responses"], list):
790
+ # Extract content from all tool responses and combine them
791
+ tool_contents = []
792
+ for tool_response in message["tool_responses"]:
793
+ if "content" in tool_response:
794
+ tool_contents.append(str(tool_response["content"]))
795
+ content_to_check = " | ".join(tool_contents) if tool_contents else ""
796
+ else:
797
+ content_to_check = str(message.get("content", ""))
798
+ elif isinstance(message, str):
799
+ content_to_check = message
800
+ else:
801
+ raise ValueError("Message must be a dictionary or a string")
802
+
803
+ for rule in self.inter_agent_rules:
804
+ if rule["type"] == "agent_transition":
805
+ # Check if this rule applies
806
+ source_match = rule["source"] == "*" or rule["source"] == sender_name
807
+ target_match = rule["target"] == "*" or rule["target"] == recipient_name
808
+
809
+ if source_match and target_match:
810
+ # Prepare content preview
811
+ content_preview = str(content_to_check)[:100] + ("..." if len(str(content_to_check)) > 100 else "")
812
+
813
+ # Use guardrail if available
814
+ if "guardrail" in rule and rule["guardrail"]:
815
+ # Send single check event with guardrail info
816
+ self._send_safeguard_event(
817
+ event_type="check",
818
+ message="Checking inter-agent communication",
819
+ source_agent=sender_name,
820
+ target_agent=recipient_name,
821
+ guardrail_type=type(rule["guardrail"]).__name__,
822
+ # action=rule.get('action', 'N/A'),
823
+ content_preview=content_preview,
824
+ )
825
+
826
+ try:
827
+ result = rule["guardrail"].check(content_to_check)
828
+ if result.activated:
829
+ self._send_safeguard_event(
830
+ event_type="violation",
831
+ message=f"VIOLATION DETECTED: {result.justification}",
832
+ source_agent=sender_name,
833
+ target_agent=recipient_name,
834
+ guardrail_type=type(rule["guardrail"]).__name__,
835
+ content_preview=content_preview,
836
+ )
837
+ # Pass the pattern if it's a regex guardrail
838
+ pattern = rule.get("pattern") if isinstance(rule["guardrail"], RegexGuardrail) else None
839
+ action_result = self._apply_action(
840
+ action=rule["action"],
841
+ content=message,
842
+ disallow_items=[],
843
+ explanation=result.justification,
844
+ custom_message=rule.get("activation_message", result.justification),
845
+ pattern=pattern,
846
+ guardrail_type=type(rule["guardrail"]).__name__,
847
+ source_agent=sender_name,
848
+ target_agent=recipient_name,
849
+ content_preview=content_preview,
850
+ )
851
+ if isinstance(action_result, (str, dict)):
852
+ return action_result
853
+ else:
854
+ return message
855
+ else:
856
+ # Content passed - no additional event needed, already sent check event above
857
+ pass
858
+ except Exception as e:
859
+ raise ValueError(f"Guardrail check failed: {e}")
860
+
861
+ # Handle legacy pattern-based rules
862
+ elif "pattern" in rule and rule["pattern"]:
863
+ # Send single check event for pattern-based rules
864
+ self._send_safeguard_event(
865
+ event_type="check",
866
+ message="Checking inter-agent communication",
867
+ source_agent=sender_name,
868
+ target_agent=recipient_name,
869
+ guardrail_type="RegexGuardrail",
870
+ # action=rule.get('action', 'N/A'),
871
+ content_preview=content_preview,
872
+ )
873
+ is_violation, explanation = self._check_regex_violation(content_to_check, rule["pattern"])
874
+ if is_violation:
875
+ result_value = self._apply_action(
876
+ action=rule["action"],
877
+ content=message,
878
+ disallow_items=[],
879
+ explanation=explanation,
880
+ custom_message=rule.get("activation_message"),
881
+ pattern=rule["pattern"],
882
+ guardrail_type="RegexGuardrail",
883
+ source_agent=sender_name,
884
+ target_agent=recipient_name,
885
+ content_preview=content_preview,
886
+ )
887
+ if isinstance(result_value, (str, dict)):
888
+ return result_value
889
+ else:
890
+ return message
891
+ else:
892
+ pass
893
+
894
+ # Handle legacy disallow-based rules and custom prompts
895
+ elif "disallow" in rule or "custom_prompt" in rule:
896
+ # Send single check event for LLM-based legacy rules
897
+ self._send_safeguard_event(
898
+ event_type="check",
899
+ message="Checking inter-agent communication",
900
+ source_agent=sender_name,
901
+ target_agent=recipient_name,
902
+ guardrail_type="LLMGuardrail",
903
+ # action=rule.get('action', 'N/A'),
904
+ content_preview=content_preview,
905
+ )
906
+ if "custom_prompt" in rule:
907
+ is_violation, explanation = self._check_llm_violation(
908
+ content_to_check, custom_prompt=rule["custom_prompt"]
909
+ )
910
+ else:
911
+ is_violation, explanation = self._check_llm_violation(
912
+ content_to_check, disallow_items=rule["disallow"]
913
+ )
914
+
915
+ if is_violation:
916
+ result_value = self._apply_action(
917
+ action=rule["action"],
918
+ content=message,
919
+ disallow_items=rule.get("disallow", []),
920
+ explanation=explanation,
921
+ custom_message=None,
922
+ pattern=None,
923
+ guardrail_type="LLMGuardrail",
924
+ source_agent=sender_name,
925
+ target_agent=recipient_name,
926
+ content_preview=content_preview,
927
+ )
928
+ if isinstance(result_value, (str, dict)):
929
+ return result_value
930
+ else:
931
+ return message
932
+ else:
933
+ pass
934
+
935
+ return message
936
+
937
+ def _check_interaction(
938
+ self,
939
+ interaction_type: str,
940
+ source_name: str,
941
+ dest_name: str,
942
+ content: str,
943
+ data: str | dict[str, Any] | list[dict[str, Any]],
944
+ context_info: str,
945
+ ) -> str | dict[str, Any] | list[dict[str, Any]] | None:
946
+ """Unified method to check any type of interaction."""
947
+ for rule in self.environment_rules:
948
+ if (
949
+ rule["type"] == interaction_type
950
+ and "message_source" in rule
951
+ and "message_destination" in rule
952
+ and rule["message_source"] == source_name
953
+ and rule["message_destination"] == dest_name
954
+ ):
955
+ content_preview = content[:100] + ("..." if len(content) > 100 else "")
956
+ check_method = rule.get("check_method", "regex")
957
+ guardrail_type = "LLMGuardrail" if check_method == "llm" else "RegexGuardrail"
958
+
959
+ # Send check event
960
+ self._send_safeguard_event(
961
+ event_type="check",
962
+ message=f"Checking {interaction_type.replace('_', ' ')}: {context_info}",
963
+ source_agent=source_name,
964
+ target_agent=dest_name,
965
+ guardrail_type=guardrail_type,
966
+ content_preview=content_preview,
967
+ )
968
+
969
+ # Perform check based on method
970
+ is_violation, explanation = self._perform_check(rule, content, check_method)
971
+
972
+ if is_violation:
973
+ # Send violation event
974
+ self._send_safeguard_event(
975
+ event_type="violation",
976
+ message=f"{guardrail_type.replace('Guardrail', '').upper()} VIOLATION: {explanation}",
977
+ source_agent=source_name,
978
+ target_agent=dest_name,
979
+ guardrail_type=guardrail_type,
980
+ content_preview=content_preview,
981
+ )
982
+
983
+ # Apply action
984
+ result = self._apply_action(
985
+ action=rule["action"],
986
+ content=data,
987
+ disallow_items=rule.get("disallow_item", []),
988
+ explanation=explanation,
989
+ custom_message=rule.get("activation_message"),
990
+ pattern=rule.get("pattern"),
991
+ guardrail_type=guardrail_type,
992
+ source_agent=source_name,
993
+ target_agent=dest_name,
994
+ content_preview=content_preview,
995
+ )
996
+ return result
997
+
998
+ return None
999
+
1000
+ def _perform_check(self, rule: dict[str, Any], content: str, check_method: str) -> tuple[bool, str]:
1001
+ """Perform the actual check based on the method."""
1002
+ if check_method == "llm":
1003
+ if not self.safeguard_llm_config:
1004
+ raise ValueError(
1005
+ f"safeguard_llm_config is required for LLM-based {rule['type']} rule: {rule['message_source']} -> {rule['message_destination']}"
1006
+ )
1007
+
1008
+ if "custom_prompt" in rule:
1009
+ return self._check_llm_violation(content, custom_prompt=rule["custom_prompt"])
1010
+ elif "disallow_item" in rule:
1011
+ return self._check_llm_violation(content, disallow_items=rule["disallow_item"])
1012
+ else:
1013
+ raise ValueError(
1014
+ f"Either custom_prompt or disallow_item must be provided for LLM-based {rule['type']}: {rule['message_source']} -> {rule['message_destination']}"
1015
+ )
1016
+
1017
+ elif check_method == "regex":
1018
+ if "pattern" not in rule:
1019
+ raise ValueError(
1020
+ f"pattern is required for regex-based {rule['type']}: {rule['message_source']} -> {rule['message_destination']}"
1021
+ )
1022
+ return self._check_regex_violation(content, rule["pattern"])
1023
+
1024
+ else:
1025
+ raise ValueError(f"Unsupported check_method: {check_method}")
1026
+
1027
+ def _check_tool_interaction(self, agent_name: str, data: dict[str, Any], direction: str) -> dict[str, Any]:
1028
+ """Check tool interactions."""
1029
+ # Extract tool name from data
1030
+ tool_name = data.get("name", data.get("tool_name", ""))
1031
+
1032
+ # Resolve the actual agent name if this is GroupToolExecutor
1033
+ actual_agent_name = agent_name
1034
+ if agent_name == "_Group_Tool_Executor" and self.group_tool_executor:
1035
+ # Get the original tool caller from GroupToolExecutor
1036
+ originator = self.group_tool_executor.get_tool_call_originator() # type: ignore[attr-defined]
1037
+ if originator:
1038
+ actual_agent_name = originator
1039
+
1040
+ # Determine source/destination based on direction
1041
+ if direction == "output":
1042
+ source_name, dest_name = tool_name, actual_agent_name
1043
+ content = str(data.get("content", ""))
1044
+ else: # input
1045
+ source_name, dest_name = actual_agent_name, tool_name
1046
+ content = str(data.get("arguments", ""))
1047
+
1048
+ result = self._check_interaction(
1049
+ interaction_type="tool_interaction",
1050
+ source_name=source_name,
1051
+ dest_name=dest_name,
1052
+ content=content,
1053
+ data=data,
1054
+ context_info=f"{actual_agent_name} <-> {tool_name} ({direction})",
1055
+ )
1056
+
1057
+ if result is not None:
1058
+ if isinstance(result, dict):
1059
+ return result
1060
+ else:
1061
+ # Convert string or list result back to dict format
1062
+ return {"content": str(result), "name": tool_name}
1063
+ return data
1064
+
1065
+ def _check_llm_interaction(
1066
+ self, agent_name: str, data: str | dict[str, Any] | list[dict[str, Any]], direction: str
1067
+ ) -> str | dict[str, Any] | list[dict[str, Any]]:
1068
+ """Check LLM interactions."""
1069
+ content = str(data)
1070
+
1071
+ # Determine source/destination based on direction
1072
+ if direction == "input":
1073
+ source_name, dest_name = agent_name, "llm"
1074
+ else: # output
1075
+ source_name, dest_name = "llm", agent_name
1076
+
1077
+ result = self._check_interaction(
1078
+ interaction_type="llm_interaction",
1079
+ source_name=source_name,
1080
+ dest_name=dest_name,
1081
+ content=content,
1082
+ data=data,
1083
+ context_info=f"{agent_name} <-> llm ({direction})",
1084
+ )
1085
+
1086
+ return result if result is not None else data
1087
+
1088
+ def _check_user_interaction(self, agent_name: str, user_input: str) -> str | None:
1089
+ """Check user interactions."""
1090
+ result = self._check_interaction(
1091
+ interaction_type="user_interaction",
1092
+ source_name="user",
1093
+ dest_name=agent_name,
1094
+ content=user_input,
1095
+ data=user_input,
1096
+ context_info=f"user <-> {agent_name}",
1097
+ )
1098
+
1099
+ if result is not None and isinstance(result, str):
1100
+ return result
1101
+ return user_input
1102
+
1103
+ def check_and_act(
1104
+ self, src_agent_name: str, dst_agent_name: str, message_content: str | dict[str, Any]
1105
+ ) -> str | dict[str, Any] | None:
1106
+ """Check and act on inter-agent communication for GroupChat integration.
1107
+
1108
+ This method is called by GroupChat._run_inter_agent_guardrails to check
1109
+ messages between agents and potentially modify or block them.
1110
+
1111
+ Args:
1112
+ src_agent_name: Name of the source agent
1113
+ dst_agent_name: Name of the destination agent
1114
+ message_content: The message content to check
1115
+
1116
+ Returns:
1117
+ Optional replacement message if a safeguard triggers, None otherwise
1118
+ """
1119
+ # Handle GroupToolExecutor transparency for safeguards
1120
+ if src_agent_name == "_Group_Tool_Executor":
1121
+ actual_src_agent_name = self._resolve_tool_executor_source(src_agent_name, self.group_tool_executor)
1122
+ else:
1123
+ actual_src_agent_name = src_agent_name
1124
+
1125
+ # Store original message for comparison
1126
+ original_message = message_content
1127
+
1128
+ result = self._check_inter_agent_communication(actual_src_agent_name, dst_agent_name, message_content)
1129
+
1130
+ # Check if the result is different from the original
1131
+ if result != original_message:
1132
+ return result
1133
+
1134
+ return None
1135
+
1136
+ def _resolve_tool_executor_source(self, src_agent_name: str, tool_executor: Any = None) -> str:
1137
+ """Resolve the actual source agent when GroupToolExecutor is involved.
1138
+
1139
+ When src_agent_name is "_Group_Tool_Executor", get the original agent who called the tool.
1140
+
1141
+ Args:
1142
+ src_agent_name: The source agent name from the communication
1143
+ tool_executor: GroupToolExecutor instance for getting originator
1144
+
1145
+ Returns:
1146
+ The actual source agent name (original tool caller for tool responses)
1147
+ """
1148
+ if src_agent_name != "_Group_Tool_Executor":
1149
+ return src_agent_name
1150
+
1151
+ # Handle GroupToolExecutor - get the original tool caller
1152
+ if tool_executor and hasattr(tool_executor, "get_tool_call_originator"):
1153
+ originator = tool_executor.get_tool_call_originator()
1154
+ if originator:
1155
+ return originator # type: ignore[no-any-return]
1156
+
1157
+ # Fallback: Could not determine original caller
1158
+ return "tool_executor"