google-adk 0.0.1__py3-none-any.whl → 0.0.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 (312) hide show
  1. google/adk/__init__.py +20 -0
  2. google/adk/agents/__init__.py +32 -0
  3. google/adk/agents/active_streaming_tool.py +38 -0
  4. google/adk/agents/base_agent.py +345 -0
  5. google/adk/agents/callback_context.py +112 -0
  6. google/adk/agents/invocation_context.py +181 -0
  7. google/adk/agents/langgraph_agent.py +140 -0
  8. google/adk/agents/live_request_queue.py +64 -0
  9. google/adk/agents/llm_agent.py +376 -0
  10. google/adk/agents/loop_agent.py +62 -0
  11. google/adk/agents/parallel_agent.py +96 -0
  12. google/adk/agents/readonly_context.py +46 -0
  13. google/adk/agents/remote_agent.py +50 -0
  14. google/adk/agents/run_config.py +87 -0
  15. google/adk/agents/sequential_agent.py +45 -0
  16. google/adk/agents/transcription_entry.py +34 -0
  17. google/adk/artifacts/__init__.py +23 -0
  18. google/adk/artifacts/base_artifact_service.py +128 -0
  19. google/adk/artifacts/gcs_artifact_service.py +195 -0
  20. google/adk/artifacts/in_memory_artifact_service.py +133 -0
  21. google/adk/auth/__init__.py +22 -0
  22. google/adk/auth/auth_credential.py +220 -0
  23. google/adk/auth/auth_handler.py +268 -0
  24. google/adk/auth/auth_preprocessor.py +116 -0
  25. google/adk/auth/auth_schemes.py +67 -0
  26. google/adk/auth/auth_tool.py +55 -0
  27. google/adk/cli/__init__.py +15 -0
  28. google/adk/cli/__main__.py +18 -0
  29. google/adk/cli/agent_graph.py +122 -0
  30. google/adk/cli/browser/adk_favicon.svg +17 -0
  31. google/adk/cli/browser/assets/audio-processor.js +51 -0
  32. google/adk/cli/browser/assets/config/runtime-config.json +3 -0
  33. google/adk/cli/browser/index.html +33 -0
  34. google/adk/cli/browser/main-XUU6OGCC.js +75 -0
  35. google/adk/cli/browser/polyfills-FFHMD2TL.js +18 -0
  36. google/adk/cli/browser/styles-4VDSPQ37.css +17 -0
  37. google/adk/cli/cli.py +181 -0
  38. google/adk/cli/cli_deploy.py +181 -0
  39. google/adk/cli/cli_eval.py +282 -0
  40. google/adk/cli/cli_tools_click.py +479 -0
  41. google/adk/cli/fast_api.py +774 -0
  42. google/adk/cli/media_streamer/__init__.py +19 -0
  43. google/adk/cli/media_streamer/index.html +228 -0
  44. google/adk/cli/utils/__init__.py +49 -0
  45. google/adk/cli/utils/envs.py +57 -0
  46. google/adk/cli/utils/evals.py +93 -0
  47. google/adk/cli/utils/logs.py +72 -0
  48. google/adk/code_executors/__init__.py +49 -0
  49. google/adk/code_executors/base_code_executor.py +97 -0
  50. google/adk/code_executors/code_execution_utils.py +256 -0
  51. google/adk/code_executors/code_executor_context.py +202 -0
  52. google/adk/code_executors/container_code_executor.py +196 -0
  53. google/adk/code_executors/unsafe_local_code_executor.py +71 -0
  54. google/adk/code_executors/vertex_ai_code_executor.py +234 -0
  55. google/adk/evaluation/__init__.py +31 -0
  56. google/adk/evaluation/agent_evaluator.py +329 -0
  57. google/adk/evaluation/evaluation_constants.py +24 -0
  58. google/adk/evaluation/evaluation_generator.py +270 -0
  59. google/adk/evaluation/response_evaluator.py +135 -0
  60. google/adk/evaluation/trajectory_evaluator.py +184 -0
  61. google/adk/events/__init__.py +21 -0
  62. google/adk/events/event.py +130 -0
  63. google/adk/events/event_actions.py +55 -0
  64. google/adk/examples/__init__.py +28 -0
  65. google/adk/examples/base_example_provider.py +35 -0
  66. google/adk/examples/example.py +27 -0
  67. google/adk/examples/example_util.py +123 -0
  68. google/adk/examples/vertex_ai_example_store.py +104 -0
  69. google/adk/flows/__init__.py +14 -0
  70. google/adk/flows/llm_flows/__init__.py +20 -0
  71. google/adk/flows/llm_flows/_base_llm_processor.py +52 -0
  72. google/adk/flows/llm_flows/_code_execution.py +458 -0
  73. google/adk/flows/llm_flows/_nl_planning.py +129 -0
  74. google/adk/flows/llm_flows/agent_transfer.py +132 -0
  75. google/adk/flows/llm_flows/audio_transcriber.py +109 -0
  76. google/adk/flows/llm_flows/auto_flow.py +49 -0
  77. google/adk/flows/llm_flows/base_llm_flow.py +559 -0
  78. google/adk/flows/llm_flows/basic.py +72 -0
  79. google/adk/flows/llm_flows/contents.py +370 -0
  80. google/adk/flows/llm_flows/functions.py +486 -0
  81. google/adk/flows/llm_flows/identity.py +47 -0
  82. google/adk/flows/llm_flows/instructions.py +137 -0
  83. google/adk/flows/llm_flows/single_flow.py +57 -0
  84. google/adk/memory/__init__.py +35 -0
  85. google/adk/memory/base_memory_service.py +74 -0
  86. google/adk/memory/in_memory_memory_service.py +62 -0
  87. google/adk/memory/vertex_ai_rag_memory_service.py +177 -0
  88. google/adk/models/__init__.py +31 -0
  89. google/adk/models/anthropic_llm.py +243 -0
  90. google/adk/models/base_llm.py +87 -0
  91. google/adk/models/base_llm_connection.py +76 -0
  92. google/adk/models/gemini_llm_connection.py +200 -0
  93. google/adk/models/google_llm.py +331 -0
  94. google/adk/models/lite_llm.py +673 -0
  95. google/adk/models/llm_request.py +98 -0
  96. google/adk/models/llm_response.py +111 -0
  97. google/adk/models/registry.py +102 -0
  98. google/adk/planners/__init__.py +23 -0
  99. google/adk/planners/base_planner.py +66 -0
  100. google/adk/planners/built_in_planner.py +75 -0
  101. google/adk/planners/plan_re_act_planner.py +208 -0
  102. google/adk/runners.py +456 -0
  103. google/adk/sessions/__init__.py +41 -0
  104. google/adk/sessions/base_session_service.py +133 -0
  105. google/adk/sessions/database_session_service.py +522 -0
  106. google/adk/sessions/in_memory_session_service.py +206 -0
  107. google/adk/sessions/session.py +54 -0
  108. google/adk/sessions/state.py +71 -0
  109. google/adk/sessions/vertex_ai_session_service.py +356 -0
  110. google/adk/telemetry.py +189 -0
  111. google/adk/tests/__init__.py +14 -0
  112. google/adk/tests/integration/.env.example +10 -0
  113. google/adk/tests/integration/__init__.py +18 -0
  114. google/adk/tests/integration/conftest.py +119 -0
  115. google/adk/tests/integration/fixture/__init__.py +14 -0
  116. google/adk/tests/integration/fixture/agent_with_config/__init__.py +15 -0
  117. google/adk/tests/integration/fixture/agent_with_config/agent.py +88 -0
  118. google/adk/tests/integration/fixture/callback_agent/__init__.py +15 -0
  119. google/adk/tests/integration/fixture/callback_agent/agent.py +105 -0
  120. google/adk/tests/integration/fixture/context_update_test/OWNERS +1 -0
  121. google/adk/tests/integration/fixture/context_update_test/__init__.py +15 -0
  122. google/adk/tests/integration/fixture/context_update_test/agent.py +43 -0
  123. google/adk/tests/integration/fixture/context_update_test/successful_test.session.json +582 -0
  124. google/adk/tests/integration/fixture/context_variable_agent/__init__.py +15 -0
  125. google/adk/tests/integration/fixture/context_variable_agent/agent.py +115 -0
  126. google/adk/tests/integration/fixture/customer_support_ma/__init__.py +15 -0
  127. google/adk/tests/integration/fixture/customer_support_ma/agent.py +172 -0
  128. google/adk/tests/integration/fixture/ecommerce_customer_service_agent/__init__.py +15 -0
  129. google/adk/tests/integration/fixture/ecommerce_customer_service_agent/agent.py +338 -0
  130. google/adk/tests/integration/fixture/ecommerce_customer_service_agent/order_query.test.json +69 -0
  131. google/adk/tests/integration/fixture/ecommerce_customer_service_agent/test_config.json +6 -0
  132. google/adk/tests/integration/fixture/flow_complex_spark/__init__.py +15 -0
  133. google/adk/tests/integration/fixture/flow_complex_spark/agent.py +182 -0
  134. google/adk/tests/integration/fixture/flow_complex_spark/sample.debug.log +243 -0
  135. google/adk/tests/integration/fixture/flow_complex_spark/sample.session.json +190 -0
  136. google/adk/tests/integration/fixture/hello_world_agent/__init__.py +15 -0
  137. google/adk/tests/integration/fixture/hello_world_agent/agent.py +95 -0
  138. google/adk/tests/integration/fixture/hello_world_agent/roll_die.test.json +24 -0
  139. google/adk/tests/integration/fixture/hello_world_agent/test_config.json +6 -0
  140. google/adk/tests/integration/fixture/home_automation_agent/__init__.py +15 -0
  141. google/adk/tests/integration/fixture/home_automation_agent/agent.py +304 -0
  142. google/adk/tests/integration/fixture/home_automation_agent/simple_test.test.json +5 -0
  143. google/adk/tests/integration/fixture/home_automation_agent/simple_test2.test.json +5 -0
  144. google/adk/tests/integration/fixture/home_automation_agent/test_config.json +5 -0
  145. google/adk/tests/integration/fixture/home_automation_agent/test_files/dependent_tool_calls.test.json +18 -0
  146. google/adk/tests/integration/fixture/home_automation_agent/test_files/memorizing_past_events/eval_data.test.json +17 -0
  147. google/adk/tests/integration/fixture/home_automation_agent/test_files/memorizing_past_events/test_config.json +6 -0
  148. google/adk/tests/integration/fixture/home_automation_agent/test_files/simple_multi_turn_conversation.test.json +18 -0
  149. google/adk/tests/integration/fixture/home_automation_agent/test_files/simple_test.test.json +17 -0
  150. google/adk/tests/integration/fixture/home_automation_agent/test_files/simple_test2.test.json +5 -0
  151. google/adk/tests/integration/fixture/home_automation_agent/test_files/test_config.json +5 -0
  152. google/adk/tests/integration/fixture/tool_agent/__init__.py +15 -0
  153. google/adk/tests/integration/fixture/tool_agent/agent.py +218 -0
  154. google/adk/tests/integration/fixture/tool_agent/files/Agent_test_plan.pdf +0 -0
  155. google/adk/tests/integration/fixture/trip_planner_agent/__init__.py +15 -0
  156. google/adk/tests/integration/fixture/trip_planner_agent/agent.py +110 -0
  157. google/adk/tests/integration/fixture/trip_planner_agent/initial.session.json +13 -0
  158. google/adk/tests/integration/fixture/trip_planner_agent/test_config.json +5 -0
  159. google/adk/tests/integration/fixture/trip_planner_agent/test_files/initial.session.json +13 -0
  160. google/adk/tests/integration/fixture/trip_planner_agent/test_files/test_config.json +5 -0
  161. google/adk/tests/integration/fixture/trip_planner_agent/test_files/trip_inquiry_sub_agent.test.json +7 -0
  162. google/adk/tests/integration/fixture/trip_planner_agent/trip_inquiry.test.json +19 -0
  163. google/adk/tests/integration/models/__init__.py +14 -0
  164. google/adk/tests/integration/models/test_google_llm.py +65 -0
  165. google/adk/tests/integration/test_callback.py +70 -0
  166. google/adk/tests/integration/test_context_variable.py +67 -0
  167. google/adk/tests/integration/test_evalute_agent_in_fixture.py +76 -0
  168. google/adk/tests/integration/test_multi_agent.py +28 -0
  169. google/adk/tests/integration/test_multi_turn.py +42 -0
  170. google/adk/tests/integration/test_single_agent.py +23 -0
  171. google/adk/tests/integration/test_sub_agent.py +26 -0
  172. google/adk/tests/integration/test_system_instruction.py +177 -0
  173. google/adk/tests/integration/test_tools.py +287 -0
  174. google/adk/tests/integration/test_with_test_file.py +34 -0
  175. google/adk/tests/integration/tools/__init__.py +14 -0
  176. google/adk/tests/integration/utils/__init__.py +16 -0
  177. google/adk/tests/integration/utils/asserts.py +75 -0
  178. google/adk/tests/integration/utils/test_runner.py +97 -0
  179. google/adk/tests/unittests/__init__.py +14 -0
  180. google/adk/tests/unittests/agents/__init__.py +14 -0
  181. google/adk/tests/unittests/agents/test_base_agent.py +407 -0
  182. google/adk/tests/unittests/agents/test_langgraph_agent.py +191 -0
  183. google/adk/tests/unittests/agents/test_llm_agent_callbacks.py +138 -0
  184. google/adk/tests/unittests/agents/test_llm_agent_fields.py +231 -0
  185. google/adk/tests/unittests/agents/test_loop_agent.py +136 -0
  186. google/adk/tests/unittests/agents/test_parallel_agent.py +92 -0
  187. google/adk/tests/unittests/agents/test_sequential_agent.py +114 -0
  188. google/adk/tests/unittests/artifacts/__init__.py +14 -0
  189. google/adk/tests/unittests/artifacts/test_artifact_service.py +276 -0
  190. google/adk/tests/unittests/auth/test_auth_handler.py +575 -0
  191. google/adk/tests/unittests/conftest.py +73 -0
  192. google/adk/tests/unittests/fast_api/__init__.py +14 -0
  193. google/adk/tests/unittests/fast_api/test_fast_api.py +269 -0
  194. google/adk/tests/unittests/flows/__init__.py +14 -0
  195. google/adk/tests/unittests/flows/llm_flows/__init__.py +14 -0
  196. google/adk/tests/unittests/flows/llm_flows/_test_examples.py +142 -0
  197. google/adk/tests/unittests/flows/llm_flows/test_agent_transfer.py +311 -0
  198. google/adk/tests/unittests/flows/llm_flows/test_functions_long_running.py +244 -0
  199. google/adk/tests/unittests/flows/llm_flows/test_functions_request_euc.py +346 -0
  200. google/adk/tests/unittests/flows/llm_flows/test_functions_sequential.py +93 -0
  201. google/adk/tests/unittests/flows/llm_flows/test_functions_simple.py +258 -0
  202. google/adk/tests/unittests/flows/llm_flows/test_identity.py +66 -0
  203. google/adk/tests/unittests/flows/llm_flows/test_instructions.py +164 -0
  204. google/adk/tests/unittests/flows/llm_flows/test_model_callbacks.py +142 -0
  205. google/adk/tests/unittests/flows/llm_flows/test_other_configs.py +46 -0
  206. google/adk/tests/unittests/flows/llm_flows/test_tool_callbacks.py +269 -0
  207. google/adk/tests/unittests/models/__init__.py +14 -0
  208. google/adk/tests/unittests/models/test_google_llm.py +224 -0
  209. google/adk/tests/unittests/models/test_litellm.py +804 -0
  210. google/adk/tests/unittests/models/test_models.py +60 -0
  211. google/adk/tests/unittests/sessions/__init__.py +14 -0
  212. google/adk/tests/unittests/sessions/test_session_service.py +227 -0
  213. google/adk/tests/unittests/sessions/test_vertex_ai_session_service.py +246 -0
  214. google/adk/tests/unittests/streaming/__init__.py +14 -0
  215. google/adk/tests/unittests/streaming/test_streaming.py +50 -0
  216. google/adk/tests/unittests/tools/__init__.py +14 -0
  217. google/adk/tests/unittests/tools/apihub_tool/clients/test_apihub_client.py +499 -0
  218. google/adk/tests/unittests/tools/apihub_tool/test_apihub_toolset.py +204 -0
  219. google/adk/tests/unittests/tools/application_integration_tool/clients/test_connections_client.py +600 -0
  220. google/adk/tests/unittests/tools/application_integration_tool/clients/test_integration_client.py +630 -0
  221. google/adk/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py +345 -0
  222. google/adk/tests/unittests/tools/google_api_tool/__init__.py +13 -0
  223. google/adk/tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py +657 -0
  224. google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_auto_auth_credential_exchanger.py +145 -0
  225. google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_base_auth_credential_exchanger.py +68 -0
  226. google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_oauth2_exchanger.py +153 -0
  227. google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_service_account_exchanger.py +196 -0
  228. google/adk/tests/unittests/tools/openapi_tool/auth/test_auth_helper.py +573 -0
  229. google/adk/tests/unittests/tools/openapi_tool/common/test_common.py +436 -0
  230. google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test.yaml +1367 -0
  231. google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_openapi_spec_parser.py +628 -0
  232. google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_openapi_toolset.py +139 -0
  233. google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_operation_parser.py +406 -0
  234. google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py +966 -0
  235. google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_tool_auth_handler.py +201 -0
  236. google/adk/tests/unittests/tools/retrieval/__init__.py +14 -0
  237. google/adk/tests/unittests/tools/retrieval/test_vertex_ai_rag_retrieval.py +147 -0
  238. google/adk/tests/unittests/tools/test_agent_tool.py +167 -0
  239. google/adk/tests/unittests/tools/test_base_tool.py +141 -0
  240. google/adk/tests/unittests/tools/test_build_function_declaration.py +277 -0
  241. google/adk/tests/unittests/utils.py +304 -0
  242. google/adk/tools/__init__.py +51 -0
  243. google/adk/tools/_automatic_function_calling_util.py +346 -0
  244. google/adk/tools/agent_tool.py +176 -0
  245. google/adk/tools/apihub_tool/__init__.py +19 -0
  246. google/adk/tools/apihub_tool/apihub_toolset.py +209 -0
  247. google/adk/tools/apihub_tool/clients/__init__.py +13 -0
  248. google/adk/tools/apihub_tool/clients/apihub_client.py +332 -0
  249. google/adk/tools/apihub_tool/clients/secret_client.py +115 -0
  250. google/adk/tools/application_integration_tool/__init__.py +19 -0
  251. google/adk/tools/application_integration_tool/application_integration_toolset.py +230 -0
  252. google/adk/tools/application_integration_tool/clients/connections_client.py +903 -0
  253. google/adk/tools/application_integration_tool/clients/integration_client.py +253 -0
  254. google/adk/tools/base_tool.py +144 -0
  255. google/adk/tools/built_in_code_execution_tool.py +59 -0
  256. google/adk/tools/crewai_tool.py +72 -0
  257. google/adk/tools/example_tool.py +62 -0
  258. google/adk/tools/exit_loop_tool.py +23 -0
  259. google/adk/tools/function_parameter_parse_util.py +307 -0
  260. google/adk/tools/function_tool.py +87 -0
  261. google/adk/tools/get_user_choice_tool.py +28 -0
  262. google/adk/tools/google_api_tool/__init__.py +14 -0
  263. google/adk/tools/google_api_tool/google_api_tool.py +59 -0
  264. google/adk/tools/google_api_tool/google_api_tool_set.py +107 -0
  265. google/adk/tools/google_api_tool/google_api_tool_sets.py +55 -0
  266. google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +521 -0
  267. google/adk/tools/google_search_tool.py +68 -0
  268. google/adk/tools/langchain_tool.py +86 -0
  269. google/adk/tools/load_artifacts_tool.py +113 -0
  270. google/adk/tools/load_memory_tool.py +58 -0
  271. google/adk/tools/load_web_page.py +41 -0
  272. google/adk/tools/long_running_tool.py +39 -0
  273. google/adk/tools/mcp_tool/__init__.py +42 -0
  274. google/adk/tools/mcp_tool/conversion_utils.py +161 -0
  275. google/adk/tools/mcp_tool/mcp_tool.py +113 -0
  276. google/adk/tools/mcp_tool/mcp_toolset.py +272 -0
  277. google/adk/tools/openapi_tool/__init__.py +21 -0
  278. google/adk/tools/openapi_tool/auth/__init__.py +19 -0
  279. google/adk/tools/openapi_tool/auth/auth_helpers.py +498 -0
  280. google/adk/tools/openapi_tool/auth/credential_exchangers/__init__.py +25 -0
  281. google/adk/tools/openapi_tool/auth/credential_exchangers/auto_auth_credential_exchanger.py +105 -0
  282. google/adk/tools/openapi_tool/auth/credential_exchangers/base_credential_exchanger.py +55 -0
  283. google/adk/tools/openapi_tool/auth/credential_exchangers/oauth2_exchanger.py +117 -0
  284. google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py +97 -0
  285. google/adk/tools/openapi_tool/common/__init__.py +19 -0
  286. google/adk/tools/openapi_tool/common/common.py +300 -0
  287. google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +32 -0
  288. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +231 -0
  289. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +144 -0
  290. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +260 -0
  291. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +496 -0
  292. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +268 -0
  293. google/adk/tools/preload_memory_tool.py +72 -0
  294. google/adk/tools/retrieval/__init__.py +36 -0
  295. google/adk/tools/retrieval/base_retrieval_tool.py +37 -0
  296. google/adk/tools/retrieval/files_retrieval.py +33 -0
  297. google/adk/tools/retrieval/llama_index_retrieval.py +41 -0
  298. google/adk/tools/retrieval/vertex_ai_rag_retrieval.py +107 -0
  299. google/adk/tools/tool_context.py +90 -0
  300. google/adk/tools/toolbox_tool.py +46 -0
  301. google/adk/tools/transfer_to_agent_tool.py +21 -0
  302. google/adk/tools/vertex_ai_search_tool.py +96 -0
  303. google/adk/version.py +16 -0
  304. google_adk-0.0.1.dist-info/LICENSE.txt → google_adk-0.0.2.dist-info/LICENSE +32 -0
  305. google_adk-0.0.2.dist-info/METADATA +73 -0
  306. google_adk-0.0.2.dist-info/RECORD +308 -0
  307. {google_adk-0.0.1.dist-info → google_adk-0.0.2.dist-info}/WHEEL +1 -2
  308. google_adk-0.0.2.dist-info/entry_points.txt +3 -0
  309. agent_kit/__init__.py +0 -0
  310. google_adk-0.0.1.dist-info/METADATA +0 -15
  311. google_adk-0.0.1.dist-info/RECORD +0 -6
  312. google_adk-0.0.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,774 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import importlib
17
+ import json
18
+ import logging
19
+ import os
20
+ from pathlib import Path
21
+ import re
22
+ import sys
23
+ import traceback
24
+ import typing
25
+ from typing import Any
26
+ from typing import List
27
+ from typing import Literal
28
+ from typing import Optional
29
+
30
+ import click
31
+ from fastapi import FastAPI
32
+ from fastapi import HTTPException
33
+ from fastapi import Query
34
+ from fastapi import Response
35
+ from fastapi.middleware.cors import CORSMiddleware
36
+ from fastapi.responses import FileResponse
37
+ from fastapi.responses import RedirectResponse
38
+ from fastapi.responses import StreamingResponse
39
+ from fastapi.staticfiles import StaticFiles
40
+ from fastapi.websockets import WebSocket
41
+ from fastapi.websockets import WebSocketDisconnect
42
+ from google.genai import types
43
+ import graphviz
44
+ from opentelemetry import trace
45
+ from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
46
+ from opentelemetry.sdk.trace import export
47
+ from opentelemetry.sdk.trace import ReadableSpan
48
+ from opentelemetry.sdk.trace import TracerProvider
49
+ from pydantic import BaseModel
50
+ from pydantic import ValidationError
51
+
52
+ from ..agents import RunConfig
53
+ from ..agents.live_request_queue import LiveRequest
54
+ from ..agents.live_request_queue import LiveRequestQueue
55
+ from ..agents.llm_agent import Agent
56
+ from ..agents.run_config import StreamingMode
57
+ from ..artifacts import InMemoryArtifactService
58
+ from ..events.event import Event
59
+ from ..runners import Runner
60
+ from ..sessions.database_session_service import DatabaseSessionService
61
+ from ..sessions.in_memory_session_service import InMemorySessionService
62
+ from ..sessions.session import Session
63
+ from ..sessions.vertex_ai_session_service import VertexAiSessionService
64
+ from .cli_eval import EVAL_SESSION_ID_PREFIX
65
+ from .cli_eval import EvalMetric
66
+ from .cli_eval import EvalMetricResult
67
+ from .cli_eval import EvalStatus
68
+ from .utils import create_empty_state
69
+ from .utils import envs
70
+ from .utils import evals
71
+
72
+ logger = logging.getLogger(__name__)
73
+
74
+ _EVAL_SET_FILE_EXTENSION = ".evalset.json"
75
+
76
+
77
+ class ApiServerSpanExporter(export.SpanExporter):
78
+
79
+ def __init__(self, trace_dict):
80
+ self.trace_dict = trace_dict
81
+
82
+ def export(
83
+ self, spans: typing.Sequence[ReadableSpan]
84
+ ) -> export.SpanExportResult:
85
+ for span in spans:
86
+ if (
87
+ span.name == "call_llm"
88
+ or span.name == "send_data"
89
+ or span.name.startswith("tool_response")
90
+ ):
91
+ attributes = dict(span.attributes)
92
+ attributes["trace_id"] = span.get_span_context().trace_id
93
+ attributes["span_id"] = span.get_span_context().span_id
94
+ if attributes.get("gcp.vertex.agent.event_id", None):
95
+ self.trace_dict[attributes["gcp.vertex.agent.event_id"]] = attributes
96
+ return export.SpanExportResult.SUCCESS
97
+
98
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
99
+ return True
100
+
101
+
102
+ class AgentRunRequest(BaseModel):
103
+ app_name: str
104
+ user_id: str
105
+ session_id: str
106
+ new_message: types.Content
107
+ streaming: bool = False
108
+
109
+
110
+ class AddSessionToEvalSetRequest(BaseModel):
111
+ eval_id: str
112
+ session_id: str
113
+ user_id: str
114
+
115
+
116
+ class RunEvalRequest(BaseModel):
117
+ eval_ids: list[str] # if empty, then all evals in the eval set are run.
118
+ eval_metrics: list[EvalMetric]
119
+
120
+
121
+ class RunEvalResult(BaseModel):
122
+ eval_set_id: str
123
+ eval_id: str
124
+ final_eval_status: EvalStatus
125
+ eval_metric_results: list[tuple[EvalMetric, EvalMetricResult]]
126
+ session_id: str
127
+
128
+
129
+ def get_fast_api_app(
130
+ *,
131
+ agent_dir: str,
132
+ session_db_url: str = "",
133
+ allow_origins: Optional[list[str]] = None,
134
+ web: bool,
135
+ ) -> FastAPI:
136
+ # InMemory tracing dict.
137
+ trace_dict: dict[str, Any] = {}
138
+
139
+ # Set up tracing in the FastAPI server.
140
+ provider = TracerProvider()
141
+ provider.add_span_processor(
142
+ export.SimpleSpanProcessor(ApiServerSpanExporter(trace_dict))
143
+ )
144
+ if os.environ.get("ADK_TRACE_TO_CLOUD", "0") == "1":
145
+ processor = export.BatchSpanProcessor(
146
+ CloudTraceSpanExporter(
147
+ project_id=os.environ.get("GOOGLE_CLOUD_PROJECT", "")
148
+ )
149
+ )
150
+ provider.add_span_processor(processor)
151
+
152
+ trace.set_tracer_provider(provider)
153
+
154
+ # Run the FastAPI server.
155
+ app = FastAPI()
156
+
157
+ if allow_origins:
158
+ app.add_middleware(
159
+ CORSMiddleware,
160
+ allow_origins=allow_origins,
161
+ allow_credentials=True,
162
+ allow_methods=["*"],
163
+ allow_headers=["*"],
164
+ )
165
+
166
+ if agent_dir not in sys.path:
167
+ sys.path.append(agent_dir)
168
+
169
+ runner_dict = {}
170
+ root_agent_dict = {}
171
+
172
+ # Build the Artifact service
173
+ artifact_service = InMemoryArtifactService()
174
+
175
+ # Build the Session service
176
+ agent_engine_id = ""
177
+ if session_db_url:
178
+ if session_db_url.startswith("agentengine://"):
179
+ # Create vertex session service
180
+ agent_engine_id = session_db_url.split("://")[1]
181
+ if not agent_engine_id:
182
+ raise click.ClickException("Agent engine id can not be empty.")
183
+ envs.load_dotenv_for_agent("", agent_dir)
184
+ session_service = VertexAiSessionService(
185
+ os.environ["GOOGLE_CLOUD_PROJECT"],
186
+ os.environ["GOOGLE_CLOUD_LOCATION"],
187
+ )
188
+ else:
189
+ session_service = DatabaseSessionService(db_url=session_db_url)
190
+ else:
191
+ session_service = InMemorySessionService()
192
+
193
+ @app.get("/list-apps")
194
+ def list_apps() -> list[str]:
195
+ base_path = Path.cwd() / agent_dir
196
+ if not base_path.exists():
197
+ raise HTTPException(status_code=404, detail="Path not found")
198
+ if not base_path.is_dir():
199
+ raise HTTPException(status_code=400, detail="Not a directory")
200
+ agent_names = [
201
+ x
202
+ for x in os.listdir(base_path)
203
+ if os.path.isdir(os.path.join(base_path, x))
204
+ and not x.startswith(".")
205
+ and x != "__pycache__"
206
+ ]
207
+ agent_names.sort()
208
+ return agent_names
209
+
210
+ @app.get("/debug/trace/{event_id}")
211
+ def get_trace_dict(event_id: str) -> Any:
212
+ event_dict = trace_dict.get(event_id, None)
213
+ if event_dict is None:
214
+ raise HTTPException(status_code=404, detail="Trace not found")
215
+ return event_dict
216
+
217
+ @app.get(
218
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}",
219
+ response_model_exclude_none=True,
220
+ )
221
+ def get_session(app_name: str, user_id: str, session_id: str) -> Session:
222
+ # Connect to managed session if agent_engine_id is set.
223
+ app_name = agent_engine_id if agent_engine_id else app_name
224
+ session = session_service.get_session(
225
+ app_name=app_name, user_id=user_id, session_id=session_id
226
+ )
227
+ if not session:
228
+ raise HTTPException(status_code=404, detail="Session not found")
229
+ return session
230
+
231
+ @app.get(
232
+ "/apps/{app_name}/users/{user_id}/sessions",
233
+ response_model_exclude_none=True,
234
+ )
235
+ def list_sessions(app_name: str, user_id: str) -> list[Session]:
236
+ # Connect to managed session if agent_engine_id is set.
237
+ app_name = agent_engine_id if agent_engine_id else app_name
238
+ return [
239
+ session
240
+ for session in session_service.list_sessions(
241
+ app_name=app_name, user_id=user_id
242
+ ).sessions
243
+ # Remove sessions that were generated as a part of Eval.
244
+ if not session.id.startswith(EVAL_SESSION_ID_PREFIX)
245
+ ]
246
+
247
+ @app.post(
248
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}",
249
+ response_model_exclude_none=True,
250
+ )
251
+ def create_session_with_id(
252
+ app_name: str,
253
+ user_id: str,
254
+ session_id: str,
255
+ state: Optional[dict[str, Any]] = None,
256
+ ) -> Session:
257
+ # Connect to managed session if agent_engine_id is set.
258
+ app_name = agent_engine_id if agent_engine_id else app_name
259
+ if (
260
+ session_service.get_session(
261
+ app_name=app_name, user_id=user_id, session_id=session_id
262
+ )
263
+ is not None
264
+ ):
265
+ logger.warning("Session already exists: %s", session_id)
266
+ raise HTTPException(
267
+ status_code=400, detail=f"Session already exists: {session_id}"
268
+ )
269
+
270
+ logger.info("New session created: %s", session_id)
271
+ return session_service.create_session(
272
+ app_name=app_name, user_id=user_id, state=state, session_id=session_id
273
+ )
274
+
275
+ @app.post(
276
+ "/apps/{app_name}/users/{user_id}/sessions",
277
+ response_model_exclude_none=True,
278
+ )
279
+ def create_session(
280
+ app_name: str,
281
+ user_id: str,
282
+ state: Optional[dict[str, Any]] = None,
283
+ ) -> Session:
284
+ # Connect to managed session if agent_engine_id is set.
285
+ app_name = agent_engine_id if agent_engine_id else app_name
286
+
287
+ logger.info("New session created")
288
+ return session_service.create_session(
289
+ app_name=app_name, user_id=user_id, state=state
290
+ )
291
+
292
+ def _get_eval_set_file_path(app_name, agent_dir, eval_set_id) -> str:
293
+ return os.path.join(
294
+ agent_dir,
295
+ app_name,
296
+ eval_set_id + _EVAL_SET_FILE_EXTENSION,
297
+ )
298
+
299
+ @app.post(
300
+ "/apps/{app_name}/eval_sets/{eval_set_id}",
301
+ response_model_exclude_none=True,
302
+ )
303
+ def create_eval_set(
304
+ app_name: str,
305
+ eval_set_id: str,
306
+ ):
307
+ """Creates an eval set, given the id."""
308
+ pattern = r"^[a-zA-Z0-9_]+$"
309
+ if not bool(re.fullmatch(pattern, eval_set_id)):
310
+ raise HTTPException(
311
+ status_code=400,
312
+ detail=(
313
+ f"Invalid eval set id. Eval set id should have the `{pattern}`"
314
+ " format"
315
+ ),
316
+ )
317
+ # Define the file path
318
+ new_eval_set_path = _get_eval_set_file_path(
319
+ app_name, agent_dir, eval_set_id
320
+ )
321
+
322
+ logger.info("Creating eval set file `%s`", new_eval_set_path)
323
+
324
+ if not os.path.exists(new_eval_set_path):
325
+ # Write the JSON string to the file
326
+ logger.info("Eval set file doesn't exist, we will create a new one.")
327
+ with open(new_eval_set_path, "w") as f:
328
+ empty_content = json.dumps([], indent=2)
329
+ f.write(empty_content)
330
+
331
+ @app.get(
332
+ "/apps/{app_name}/eval_sets",
333
+ response_model_exclude_none=True,
334
+ )
335
+ def list_eval_sets(app_name: str) -> list[str]:
336
+ """Lists all eval sets for the given app."""
337
+ eval_set_file_path = os.path.join(agent_dir, app_name)
338
+ eval_sets = []
339
+ for file in os.listdir(eval_set_file_path):
340
+ if file.endswith(_EVAL_SET_FILE_EXTENSION):
341
+ eval_sets.append(
342
+ os.path.basename(file).removesuffix(_EVAL_SET_FILE_EXTENSION)
343
+ )
344
+
345
+ return sorted(eval_sets)
346
+
347
+ @app.post(
348
+ "/apps/{app_name}/eval_sets/{eval_set_id}/add_session",
349
+ response_model_exclude_none=True,
350
+ )
351
+ def add_session_to_eval_set(
352
+ app_name: str, eval_set_id: str, req: AddSessionToEvalSetRequest
353
+ ):
354
+ pattern = r"^[a-zA-Z0-9_]+$"
355
+ if not bool(re.fullmatch(pattern, req.eval_id)):
356
+ raise HTTPException(
357
+ status_code=400,
358
+ detail=f"Invalid eval id. Eval id should have the `{pattern}` format",
359
+ )
360
+
361
+ # Get the session
362
+ session = session_service.get_session(
363
+ app_name=app_name, user_id=req.user_id, session_id=req.session_id
364
+ )
365
+ assert session, "Session not found."
366
+ # Load the eval set file data
367
+ eval_set_file_path = _get_eval_set_file_path(
368
+ app_name, agent_dir, eval_set_id
369
+ )
370
+ with open(eval_set_file_path, "r") as file:
371
+ eval_set_data = json.load(file) # Load JSON into a list
372
+
373
+ if [x for x in eval_set_data if x["name"] == req.eval_id]:
374
+ raise HTTPException(
375
+ status_code=400,
376
+ detail=(
377
+ f"Eval id `{req.eval_id}` already exists in `{eval_set_id}`"
378
+ " eval set."
379
+ ),
380
+ )
381
+
382
+ # Convert the session data to evaluation format
383
+ test_data = evals.convert_session_to_eval_format(session)
384
+
385
+ # Populate the session with initial session state.
386
+ initial_session_state = create_empty_state(_get_root_agent(app_name))
387
+
388
+ eval_set_data.append({
389
+ "name": req.eval_id,
390
+ "data": test_data,
391
+ "initial_session": {
392
+ "state": initial_session_state,
393
+ "app_name": app_name,
394
+ "user_id": req.user_id,
395
+ },
396
+ })
397
+ # Serialize the test data to JSON and write to the eval set file.
398
+ with open(eval_set_file_path, "w") as f:
399
+ f.write(json.dumps(eval_set_data, indent=2))
400
+
401
+ @app.get(
402
+ "/apps/{app_name}/eval_sets/{eval_set_id}/evals",
403
+ response_model_exclude_none=True,
404
+ )
405
+ def list_evals_in_eval_set(
406
+ app_name: str,
407
+ eval_set_id: str,
408
+ ) -> list[str]:
409
+ """Lists all evals in an eval set."""
410
+ # Load the eval set file data
411
+ eval_set_file_path = _get_eval_set_file_path(
412
+ app_name, agent_dir, eval_set_id
413
+ )
414
+ with open(eval_set_file_path, "r") as file:
415
+ eval_set_data = json.load(file) # Load JSON into a list
416
+
417
+ return sorted([x["name"] for x in eval_set_data])
418
+
419
+ @app.post(
420
+ "/apps/{app_name}/eval_sets/{eval_set_id}/run_eval",
421
+ response_model_exclude_none=True,
422
+ )
423
+ def run_eval(
424
+ app_name: str, eval_set_id: str, req: RunEvalRequest
425
+ ) -> list[RunEvalResult]:
426
+ from .cli_eval import run_evals
427
+
428
+ """Runs an eval given the details in the eval request."""
429
+ # Create a mapping from eval set file to all the evals that needed to be
430
+ # run.
431
+ eval_set_file_path = _get_eval_set_file_path(
432
+ app_name, agent_dir, eval_set_id
433
+ )
434
+ eval_set_to_evals = {eval_set_file_path: req.eval_ids}
435
+
436
+ if not req.eval_ids:
437
+ logger.info(
438
+ "Eval ids to run list is empty. We will all evals in the eval set."
439
+ )
440
+ root_agent = _get_root_agent(app_name)
441
+ eval_results = list(
442
+ run_evals(
443
+ eval_set_to_evals,
444
+ root_agent,
445
+ getattr(root_agent, "reset_data", None),
446
+ req.eval_metrics,
447
+ session_service=session_service,
448
+ artifact_service=artifact_service,
449
+ )
450
+ )
451
+
452
+ run_eval_results = []
453
+ for eval_result in eval_results:
454
+ run_eval_results.append(
455
+ RunEvalResult(
456
+ app_name=app_name,
457
+ eval_set_id=eval_set_id,
458
+ eval_id=eval_result.eval_id,
459
+ final_eval_status=eval_result.final_eval_status,
460
+ eval_metric_results=eval_result.eval_metric_results,
461
+ session_id=eval_result.session_id,
462
+ )
463
+ )
464
+ return run_eval_results
465
+
466
+ @app.delete("/apps/{app_name}/users/{user_id}/sessions/{session_id}")
467
+ def delete_session(app_name: str, user_id: str, session_id: str):
468
+ # Connect to managed session if agent_engine_id is set.
469
+ app_name = agent_engine_id if agent_engine_id else app_name
470
+ session_service.delete_session(
471
+ app_name=app_name, user_id=user_id, session_id=session_id
472
+ )
473
+
474
+ @app.get(
475
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}",
476
+ response_model_exclude_none=True,
477
+ )
478
+ def load_artifact(
479
+ app_name: str,
480
+ user_id: str,
481
+ session_id: str,
482
+ artifact_name: str,
483
+ version: Optional[int] = Query(None),
484
+ ) -> Optional[types.Part]:
485
+ app_name = agent_engine_id if agent_engine_id else app_name
486
+ artifact = artifact_service.load_artifact(
487
+ app_name=app_name,
488
+ user_id=user_id,
489
+ session_id=session_id,
490
+ filename=artifact_name,
491
+ version=version,
492
+ )
493
+ if not artifact:
494
+ raise HTTPException(status_code=404, detail="Artifact not found")
495
+ return artifact
496
+
497
+ @app.get(
498
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/{version_id}",
499
+ response_model_exclude_none=True,
500
+ )
501
+ def load_artifact_version(
502
+ app_name: str,
503
+ user_id: str,
504
+ session_id: str,
505
+ artifact_name: str,
506
+ version_id: int,
507
+ ) -> Optional[types.Part]:
508
+ app_name = agent_engine_id if agent_engine_id else app_name
509
+ artifact = artifact_service.load_artifact(
510
+ app_name=app_name,
511
+ user_id=user_id,
512
+ session_id=session_id,
513
+ filename=artifact_name,
514
+ version=version_id,
515
+ )
516
+ if not artifact:
517
+ raise HTTPException(status_code=404, detail="Artifact not found")
518
+ return artifact
519
+
520
+ @app.get(
521
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts",
522
+ response_model_exclude_none=True,
523
+ )
524
+ def list_artifact_names(
525
+ app_name: str, user_id: str, session_id: str
526
+ ) -> list[str]:
527
+ app_name = agent_engine_id if agent_engine_id else app_name
528
+ return artifact_service.list_artifact_keys(
529
+ app_name=app_name, user_id=user_id, session_id=session_id
530
+ )
531
+
532
+ @app.get(
533
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions",
534
+ response_model_exclude_none=True,
535
+ )
536
+ def list_artifact_versions(
537
+ app_name: str, user_id: str, session_id: str, artifact_name: str
538
+ ) -> list[int]:
539
+ app_name = agent_engine_id if agent_engine_id else app_name
540
+ return artifact_service.list_versions(
541
+ app_name=app_name,
542
+ user_id=user_id,
543
+ session_id=session_id,
544
+ filename=artifact_name,
545
+ )
546
+
547
+ @app.delete(
548
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}",
549
+ )
550
+ def delete_artifact(
551
+ app_name: str, user_id: str, session_id: str, artifact_name: str
552
+ ):
553
+ app_name = agent_engine_id if agent_engine_id else app_name
554
+ artifact_service.delete_artifact(
555
+ app_name=app_name,
556
+ user_id=user_id,
557
+ session_id=session_id,
558
+ filename=artifact_name,
559
+ )
560
+
561
+ @app.post("/run", response_model_exclude_none=True)
562
+ async def agent_run(req: AgentRunRequest) -> list[Event]:
563
+ # Connect to managed session if agent_engine_id is set.
564
+ app_id = agent_engine_id if agent_engine_id else req.app_name
565
+ session = session_service.get_session(
566
+ app_name=app_id, user_id=req.user_id, session_id=req.session_id
567
+ )
568
+ if not session:
569
+ raise HTTPException(status_code=404, detail="Session not found")
570
+ runner = _get_runner(req.app_name)
571
+ events = [
572
+ event
573
+ async for event in runner.run_async(
574
+ user_id=req.user_id,
575
+ session_id=req.session_id,
576
+ new_message=req.new_message,
577
+ )
578
+ ]
579
+ logger.info("Generated %s events in agent run: %s", len(events), events)
580
+ return events
581
+
582
+ @app.post("/run_sse")
583
+ async def agent_run_sse(req: AgentRunRequest) -> StreamingResponse:
584
+ # Connect to managed session if agent_engine_id is set.
585
+ app_id = agent_engine_id if agent_engine_id else req.app_name
586
+ # SSE endpoint
587
+ session = session_service.get_session(
588
+ app_name=app_id, user_id=req.user_id, session_id=req.session_id
589
+ )
590
+ if not session:
591
+ raise HTTPException(status_code=404, detail="Session not found")
592
+
593
+ # Convert the events to properly formatted SSE
594
+ async def event_generator():
595
+ try:
596
+ stream_mode = StreamingMode.SSE if req.streaming else StreamingMode.NONE
597
+ runner = _get_runner(req.app_name)
598
+ async for event in runner.run_async(
599
+ user_id=req.user_id,
600
+ session_id=req.session_id,
601
+ new_message=req.new_message,
602
+ run_config=RunConfig(streaming_mode=stream_mode),
603
+ ):
604
+ # Format as SSE data
605
+ sse_event = event.model_dump_json(exclude_none=True, by_alias=True)
606
+ logger.info("Generated event in agent run streaming: %s", sse_event)
607
+ yield f"data: {sse_event}\n\n"
608
+ except Exception as e:
609
+ logger.exception("Error in event_generator: %s", e)
610
+ # You might want to yield an error event here
611
+ yield f'data: {{"error": "{str(e)}"}}\n\n'
612
+
613
+ # Returns a streaming response with the proper media type for SSE
614
+ return StreamingResponse(
615
+ event_generator(),
616
+ media_type="text/event-stream",
617
+ )
618
+
619
+ @app.get(
620
+ "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}/graph",
621
+ response_model_exclude_none=True,
622
+ )
623
+ def get_event_graph(
624
+ app_name: str, user_id: str, session_id: str, event_id: str
625
+ ):
626
+ # Connect to managed session if agent_engine_id is set.
627
+ app_id = agent_engine_id if agent_engine_id else app_name
628
+ session = session_service.get_session(
629
+ app_name=app_id, user_id=user_id, session_id=session_id
630
+ )
631
+ session_events = session.events if session else []
632
+ event = next((x for x in session_events if x.id == event_id), None)
633
+ if not event:
634
+ return {}
635
+
636
+ from . import agent_graph
637
+
638
+ function_calls = event.get_function_calls()
639
+ function_responses = event.get_function_responses()
640
+ root_agent = _get_root_agent(app_name)
641
+ dot_graph = None
642
+ if function_calls:
643
+ function_call_highlights = []
644
+ for function_call in function_calls:
645
+ from_name = event.author
646
+ to_name = function_call.name
647
+ function_call_highlights.append((from_name, to_name))
648
+ dot_graph = agent_graph.get_agent_graph(
649
+ root_agent, function_call_highlights
650
+ )
651
+ elif function_responses:
652
+ function_responses_highlights = []
653
+ for function_response in function_responses:
654
+ from_name = function_response.name
655
+ to_name = event.author
656
+ function_responses_highlights.append((from_name, to_name))
657
+ dot_graph = agent_graph.get_agent_graph(
658
+ root_agent, function_responses_highlights
659
+ )
660
+ else:
661
+ from_name = event.author
662
+ to_name = ""
663
+ dot_graph = agent_graph.get_agent_graph(
664
+ root_agent, [(from_name, to_name)]
665
+ )
666
+ if dot_graph and isinstance(dot_graph, graphviz.Digraph):
667
+ return {"dot_src": dot_graph.source}
668
+ else:
669
+ return {}
670
+
671
+ @app.websocket("/run_live")
672
+ async def agent_live_run(
673
+ websocket: WebSocket,
674
+ app_name: str,
675
+ user_id: str,
676
+ session_id: str,
677
+ modalities: List[Literal["TEXT", "AUDIO"]] = Query(
678
+ default=["TEXT", "AUDIO"]
679
+ ), # Only allows "TEXT" or "AUDIO"
680
+ ) -> None:
681
+ await websocket.accept()
682
+
683
+ # Connect to managed session if agent_engine_id is set.
684
+ app_id = agent_engine_id if agent_engine_id else app_name
685
+ session = session_service.get_session(
686
+ app_name=app_id, user_id=user_id, session_id=session_id
687
+ )
688
+ if not session:
689
+ # Accept first so that the client is aware of connection establishment,
690
+ # then close with a specific code.
691
+ await websocket.close(code=1002, reason="Session not found")
692
+ return
693
+
694
+ live_request_queue = LiveRequestQueue()
695
+
696
+ async def forward_events():
697
+ runner = _get_runner(app_name)
698
+ async for event in runner.run_live(
699
+ session=session, live_request_queue=live_request_queue
700
+ ):
701
+ await websocket.send_text(
702
+ event.model_dump_json(exclude_none=True, by_alias=True)
703
+ )
704
+
705
+ async def process_messages():
706
+ try:
707
+ while True:
708
+ data = await websocket.receive_text()
709
+ # Validate and send the received message to the live queue.
710
+ live_request_queue.send(LiveRequest.model_validate_json(data))
711
+ except ValidationError as ve:
712
+ logger.error("Validation error in process_messages: %s", ve)
713
+
714
+ # Run both tasks concurrently and cancel all if one fails.
715
+ tasks = [
716
+ asyncio.create_task(forward_events()),
717
+ asyncio.create_task(process_messages()),
718
+ ]
719
+ done, pending = await asyncio.wait(
720
+ tasks, return_when=asyncio.FIRST_EXCEPTION
721
+ )
722
+ try:
723
+ # This will re-raise any exception from the completed tasks.
724
+ for task in done:
725
+ task.result()
726
+ except WebSocketDisconnect:
727
+ logger.info("Client disconnected during process_messages.")
728
+ except Exception as e:
729
+ logger.exception("Error during live websocket communication: %s", e)
730
+ traceback.print_exc()
731
+ finally:
732
+ for task in pending:
733
+ task.cancel()
734
+
735
+ def _get_root_agent(app_name: str) -> Agent:
736
+ """Returns the root agent for the given app."""
737
+ if app_name in root_agent_dict:
738
+ return root_agent_dict[app_name]
739
+ envs.load_dotenv_for_agent(os.path.basename(app_name), agent_dir)
740
+ agent_module = importlib.import_module(app_name)
741
+ root_agent: Agent = agent_module.agent.root_agent
742
+ root_agent_dict[app_name] = root_agent
743
+ return root_agent
744
+
745
+ def _get_runner(app_name: str) -> Runner:
746
+ """Returns the runner for the given app."""
747
+ if app_name in runner_dict:
748
+ return runner_dict[app_name]
749
+ root_agent = _get_root_agent(app_name)
750
+ runner = Runner(
751
+ app_name=agent_engine_id if agent_engine_id else app_name,
752
+ agent=root_agent,
753
+ artifact_service=artifact_service,
754
+ session_service=session_service,
755
+ )
756
+ runner_dict[app_name] = runner
757
+ return runner
758
+
759
+ if web:
760
+ BASE_DIR = Path(__file__).parent.resolve()
761
+ ANGULAR_DIST_PATH = BASE_DIR / "browser"
762
+
763
+ @app.get("/")
764
+ async def redirect_to_dev_ui():
765
+ return RedirectResponse("/dev-ui")
766
+
767
+ @app.get("/dev-ui")
768
+ async def dev_ui():
769
+ return FileResponse(BASE_DIR / "browser/index.html")
770
+
771
+ app.mount(
772
+ "/", StaticFiles(directory=ANGULAR_DIST_PATH, html=True), name="static"
773
+ )
774
+ return app