aip-agents-binary 0.5.20__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 (280) hide show
  1. aip_agents/__init__.py +65 -0
  2. aip_agents/a2a/__init__.py +19 -0
  3. aip_agents/a2a/server/__init__.py +10 -0
  4. aip_agents/a2a/server/base_executor.py +1086 -0
  5. aip_agents/a2a/server/google_adk_executor.py +198 -0
  6. aip_agents/a2a/server/langflow_executor.py +180 -0
  7. aip_agents/a2a/server/langgraph_executor.py +270 -0
  8. aip_agents/a2a/types.py +232 -0
  9. aip_agents/agent/__init__.py +27 -0
  10. aip_agents/agent/base_agent.py +970 -0
  11. aip_agents/agent/base_langgraph_agent.py +2942 -0
  12. aip_agents/agent/google_adk_agent.py +926 -0
  13. aip_agents/agent/google_adk_constants.py +6 -0
  14. aip_agents/agent/hitl/__init__.py +24 -0
  15. aip_agents/agent/hitl/config.py +28 -0
  16. aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
  17. aip_agents/agent/hitl/manager.py +532 -0
  18. aip_agents/agent/hitl/models.py +18 -0
  19. aip_agents/agent/hitl/prompt/__init__.py +9 -0
  20. aip_agents/agent/hitl/prompt/base.py +42 -0
  21. aip_agents/agent/hitl/prompt/deferred.py +73 -0
  22. aip_agents/agent/hitl/registry.py +149 -0
  23. aip_agents/agent/interface.py +138 -0
  24. aip_agents/agent/interfaces.py +65 -0
  25. aip_agents/agent/langflow_agent.py +464 -0
  26. aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
  27. aip_agents/agent/langgraph_react_agent.py +2514 -0
  28. aip_agents/agent/system_instruction_context.py +34 -0
  29. aip_agents/clients/__init__.py +10 -0
  30. aip_agents/clients/langflow/__init__.py +10 -0
  31. aip_agents/clients/langflow/client.py +477 -0
  32. aip_agents/clients/langflow/types.py +18 -0
  33. aip_agents/constants.py +23 -0
  34. aip_agents/credentials/manager.py +132 -0
  35. aip_agents/examples/__init__.py +5 -0
  36. aip_agents/examples/compare_streaming_client.py +783 -0
  37. aip_agents/examples/compare_streaming_server.py +142 -0
  38. aip_agents/examples/demo_memory_recall.py +401 -0
  39. aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
  40. aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
  41. aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
  42. aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
  43. aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
  44. aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
  45. aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
  46. aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
  47. aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
  48. aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
  49. aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
  50. aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
  51. aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
  52. aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
  53. aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
  54. aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
  55. aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
  56. aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
  57. aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
  58. aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
  59. aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
  60. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
  61. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
  62. aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
  63. aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
  64. aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
  65. aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
  66. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
  67. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
  68. aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
  69. aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
  70. aip_agents/examples/hello_world_google_adk.py +41 -0
  71. aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
  72. aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
  73. aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
  74. aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
  75. aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
  76. aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
  77. aip_agents/examples/hello_world_google_adk_stream.py +44 -0
  78. aip_agents/examples/hello_world_langchain.py +28 -0
  79. aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
  80. aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
  81. aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
  82. aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
  83. aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
  84. aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
  85. aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
  86. aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
  87. aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
  88. aip_agents/examples/hello_world_langchain_stream.py +36 -0
  89. aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
  90. aip_agents/examples/hello_world_langflow_agent.py +163 -0
  91. aip_agents/examples/hello_world_langgraph.py +39 -0
  92. aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
  93. aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
  94. aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
  95. aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
  96. aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
  97. aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
  98. aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
  99. aip_agents/examples/hello_world_langgraph_stream.py +43 -0
  100. aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
  101. aip_agents/examples/hello_world_model_switch_cli.py +210 -0
  102. aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
  103. aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
  104. aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
  105. aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
  106. aip_agents/examples/hello_world_pii_logger.py +21 -0
  107. aip_agents/examples/hello_world_sentry.py +133 -0
  108. aip_agents/examples/hello_world_step_limits.py +273 -0
  109. aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
  110. aip_agents/examples/hello_world_tool_output_client.py +46 -0
  111. aip_agents/examples/hello_world_tool_output_server.py +114 -0
  112. aip_agents/examples/hitl_demo.py +724 -0
  113. aip_agents/examples/mcp_configs/configs.py +63 -0
  114. aip_agents/examples/mcp_servers/common.py +76 -0
  115. aip_agents/examples/mcp_servers/mcp_name.py +29 -0
  116. aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
  117. aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
  118. aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
  119. aip_agents/examples/mcp_servers/mcp_time.py +10 -0
  120. aip_agents/examples/pii_demo_langgraph_client.py +69 -0
  121. aip_agents/examples/pii_demo_langgraph_server.py +126 -0
  122. aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
  123. aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
  124. aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
  125. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
  126. aip_agents/examples/tools/__init__.py +27 -0
  127. aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
  128. aip_agents/examples/tools/adk_weather_tool.py +60 -0
  129. aip_agents/examples/tools/data_generator_tool.py +103 -0
  130. aip_agents/examples/tools/data_visualization_tool.py +312 -0
  131. aip_agents/examples/tools/image_artifact_tool.py +136 -0
  132. aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
  133. aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
  134. aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
  135. aip_agents/examples/tools/langchain_weather_tool.py +48 -0
  136. aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
  137. aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
  138. aip_agents/examples/tools/pii_demo_tools.py +189 -0
  139. aip_agents/examples/tools/random_chart_tool.py +142 -0
  140. aip_agents/examples/tools/serper_tool.py +202 -0
  141. aip_agents/examples/tools/stock_tools.py +82 -0
  142. aip_agents/examples/tools/table_generator_tool.py +167 -0
  143. aip_agents/examples/tools/time_tool.py +82 -0
  144. aip_agents/examples/tools/weather_forecast_tool.py +38 -0
  145. aip_agents/executor/agent_executor.py +473 -0
  146. aip_agents/executor/base.py +48 -0
  147. aip_agents/mcp/__init__.py +1 -0
  148. aip_agents/mcp/client/__init__.py +14 -0
  149. aip_agents/mcp/client/base_mcp_client.py +369 -0
  150. aip_agents/mcp/client/connection_manager.py +193 -0
  151. aip_agents/mcp/client/google_adk/__init__.py +11 -0
  152. aip_agents/mcp/client/google_adk/client.py +381 -0
  153. aip_agents/mcp/client/langchain/__init__.py +11 -0
  154. aip_agents/mcp/client/langchain/client.py +265 -0
  155. aip_agents/mcp/client/persistent_session.py +359 -0
  156. aip_agents/mcp/client/session_pool.py +351 -0
  157. aip_agents/mcp/client/transports.py +215 -0
  158. aip_agents/mcp/utils/__init__.py +7 -0
  159. aip_agents/mcp/utils/config_validator.py +139 -0
  160. aip_agents/memory/__init__.py +14 -0
  161. aip_agents/memory/adapters/__init__.py +10 -0
  162. aip_agents/memory/adapters/base_adapter.py +717 -0
  163. aip_agents/memory/adapters/mem0.py +84 -0
  164. aip_agents/memory/base.py +84 -0
  165. aip_agents/memory/constants.py +49 -0
  166. aip_agents/memory/factory.py +86 -0
  167. aip_agents/memory/guidance.py +20 -0
  168. aip_agents/memory/simple_memory.py +47 -0
  169. aip_agents/middleware/__init__.py +17 -0
  170. aip_agents/middleware/base.py +88 -0
  171. aip_agents/middleware/manager.py +128 -0
  172. aip_agents/middleware/todolist.py +274 -0
  173. aip_agents/schema/__init__.py +69 -0
  174. aip_agents/schema/a2a.py +56 -0
  175. aip_agents/schema/agent.py +111 -0
  176. aip_agents/schema/hitl.py +157 -0
  177. aip_agents/schema/langgraph.py +37 -0
  178. aip_agents/schema/model_id.py +97 -0
  179. aip_agents/schema/step_limit.py +108 -0
  180. aip_agents/schema/storage.py +40 -0
  181. aip_agents/sentry/__init__.py +11 -0
  182. aip_agents/sentry/sentry.py +151 -0
  183. aip_agents/storage/__init__.py +41 -0
  184. aip_agents/storage/base.py +85 -0
  185. aip_agents/storage/clients/__init__.py +12 -0
  186. aip_agents/storage/clients/minio_client.py +318 -0
  187. aip_agents/storage/config.py +62 -0
  188. aip_agents/storage/providers/__init__.py +15 -0
  189. aip_agents/storage/providers/base.py +106 -0
  190. aip_agents/storage/providers/memory.py +114 -0
  191. aip_agents/storage/providers/object_storage.py +214 -0
  192. aip_agents/tools/__init__.py +33 -0
  193. aip_agents/tools/bosa_tools.py +105 -0
  194. aip_agents/tools/browser_use/__init__.py +82 -0
  195. aip_agents/tools/browser_use/action_parser.py +103 -0
  196. aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
  197. aip_agents/tools/browser_use/llm_config.py +120 -0
  198. aip_agents/tools/browser_use/minio_storage.py +198 -0
  199. aip_agents/tools/browser_use/schemas.py +119 -0
  200. aip_agents/tools/browser_use/session.py +76 -0
  201. aip_agents/tools/browser_use/session_errors.py +132 -0
  202. aip_agents/tools/browser_use/steel_session_recording.py +317 -0
  203. aip_agents/tools/browser_use/streaming.py +813 -0
  204. aip_agents/tools/browser_use/structured_data_parser.py +257 -0
  205. aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
  206. aip_agents/tools/browser_use/types.py +78 -0
  207. aip_agents/tools/code_sandbox/__init__.py +26 -0
  208. aip_agents/tools/code_sandbox/constant.py +13 -0
  209. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
  210. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
  211. aip_agents/tools/constants.py +165 -0
  212. aip_agents/tools/document_loader/__init__.py +44 -0
  213. aip_agents/tools/document_loader/base_reader.py +302 -0
  214. aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
  215. aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
  216. aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
  217. aip_agents/tools/document_loader/pdf_splitter.py +169 -0
  218. aip_agents/tools/gl_connector/__init__.py +5 -0
  219. aip_agents/tools/gl_connector/tool.py +351 -0
  220. aip_agents/tools/memory_search/__init__.py +22 -0
  221. aip_agents/tools/memory_search/base.py +200 -0
  222. aip_agents/tools/memory_search/mem0.py +258 -0
  223. aip_agents/tools/memory_search/schema.py +48 -0
  224. aip_agents/tools/memory_search_tool.py +26 -0
  225. aip_agents/tools/time_tool.py +117 -0
  226. aip_agents/tools/tool_config_injector.py +300 -0
  227. aip_agents/tools/web_search/__init__.py +15 -0
  228. aip_agents/tools/web_search/serper_tool.py +187 -0
  229. aip_agents/types/__init__.py +70 -0
  230. aip_agents/types/a2a_events.py +13 -0
  231. aip_agents/utils/__init__.py +79 -0
  232. aip_agents/utils/a2a_connector.py +1757 -0
  233. aip_agents/utils/artifact_helpers.py +502 -0
  234. aip_agents/utils/constants.py +22 -0
  235. aip_agents/utils/datetime/__init__.py +34 -0
  236. aip_agents/utils/datetime/normalization.py +231 -0
  237. aip_agents/utils/datetime/timezone.py +206 -0
  238. aip_agents/utils/env_loader.py +27 -0
  239. aip_agents/utils/event_handler_registry.py +58 -0
  240. aip_agents/utils/file_prompt_utils.py +176 -0
  241. aip_agents/utils/final_response_builder.py +211 -0
  242. aip_agents/utils/formatter_llm_client.py +231 -0
  243. aip_agents/utils/langgraph/__init__.py +19 -0
  244. aip_agents/utils/langgraph/converter.py +128 -0
  245. aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
  246. aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
  247. aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
  248. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
  249. aip_agents/utils/langgraph/tool_output_management.py +967 -0
  250. aip_agents/utils/logger.py +195 -0
  251. aip_agents/utils/metadata/__init__.py +27 -0
  252. aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
  253. aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
  254. aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
  255. aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
  256. aip_agents/utils/metadata/activity_narrative/context.py +49 -0
  257. aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
  258. aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
  259. aip_agents/utils/metadata/schemas/__init__.py +16 -0
  260. aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
  261. aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
  262. aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
  263. aip_agents/utils/metadata_helper.py +358 -0
  264. aip_agents/utils/name_preprocessor/__init__.py +17 -0
  265. aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
  266. aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
  267. aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
  268. aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
  269. aip_agents/utils/pii/__init__.py +25 -0
  270. aip_agents/utils/pii/pii_handler.py +397 -0
  271. aip_agents/utils/pii/pii_helper.py +207 -0
  272. aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
  273. aip_agents/utils/reference_helper.py +273 -0
  274. aip_agents/utils/sse_chunk_transformer.py +831 -0
  275. aip_agents/utils/step_limit_manager.py +265 -0
  276. aip_agents/utils/token_usage_helper.py +156 -0
  277. aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
  278. aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
  279. aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
  280. aip_agents_binary-0.5.20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,317 @@
1
+ """Steel session recording helper built on Steel's HLS exports.
2
+
3
+ This module downloads Steel session HLS manifests, merges the associated
4
+ segments into a single MP4 file, and uploads the result to MinIO storage.
5
+ It replaces the earlier rrweb + Playwright conversion pipeline.
6
+
7
+ Authors:
8
+ Reinhart Linanda (reinhart.linanda@gdplabs.id)
9
+ """
10
+
11
+ import asyncio
12
+ import re
13
+ import shutil
14
+ import tempfile
15
+ import time
16
+ from io import BufferedWriter
17
+ from pathlib import Path
18
+ from urllib.parse import urljoin
19
+
20
+ import requests
21
+ from dotenv import load_dotenv
22
+
23
+ from aip_agents.tools.browser_use.minio_storage import MinIOStorage
24
+ from aip_agents.utils.logger import get_logger
25
+
26
+ load_dotenv()
27
+
28
+ logger = get_logger(__name__)
29
+
30
+ VIDEO_FILE_NAME_PREFIX = "session_"
31
+ MANIFEST_TEMP_SUFFIX = ".m3u8"
32
+
33
+
34
+ class SteelSessionRecorder:
35
+ """High-level helper to export Steel sessions via their HLS manifests.
36
+
37
+ This class provides a high-level interface for exporting Steel sessions via their HLS manifests.
38
+ It provides methods for sanitizing session IDs, building safe filenames, and generating video filenames.
39
+ It also provides methods for downloading and merging HLS manifests, and uploading videos to MinIO.
40
+ """
41
+
42
+ @staticmethod
43
+ def safe_session_id(session_id: str) -> str:
44
+ """Sanitize a session ID for filename usage.
45
+
46
+ Args:
47
+ session_id: The session ID to sanitize.
48
+
49
+ Returns:
50
+ str: The sanitized session ID.
51
+ """
52
+ return "".join(c for c in session_id if c.isalnum() or c in ("-", "_"))
53
+
54
+ @staticmethod
55
+ def safe_session_filename(session_id: str, extension: str) -> str:
56
+ """Build a safe filename for a session recording.
57
+
58
+ Args:
59
+ session_id: The session ID to build a filename for.
60
+ extension: The extension of the filename.
61
+
62
+ Returns:
63
+ str: The safe filename.
64
+ """
65
+ safe_id = SteelSessionRecorder.safe_session_id(session_id)
66
+ return f"{VIDEO_FILE_NAME_PREFIX}{safe_id}{extension}"
67
+
68
+ @staticmethod
69
+ def generate_video_filename(session_id: str, extension: str = ".mp4") -> str:
70
+ """Generate a filename for a session recording.
71
+
72
+ Args:
73
+ session_id: The session ID to generate a filename for.
74
+ extension: The extension of the filename.
75
+
76
+ Returns:
77
+ str: The generated filename.
78
+ """
79
+ normalized_extension = extension if extension.startswith(".") else f".{extension}"
80
+ return SteelSessionRecorder.safe_session_filename(session_id, normalized_extension)
81
+
82
+ @staticmethod
83
+ def _parse_manifest_entries(manifest_text: str, manifest_url: str) -> tuple[str | None, list[str]]:
84
+ """Parse manifest lines into init segment and media segment URLs.
85
+
86
+ Args:
87
+ manifest_text: The text of the manifest.
88
+ manifest_url: The URL of the manifest.
89
+
90
+ Returns:
91
+ tuple[str | None, list[str]]: The init segment URL and the list of media segment URLs.
92
+ """
93
+ init_url = None
94
+ segments: list[str] = []
95
+
96
+ for line in manifest_text.splitlines():
97
+ line = line.strip()
98
+ if not line:
99
+ continue
100
+
101
+ if not line.startswith("#"):
102
+ segments.append(urljoin(manifest_url, line))
103
+ continue
104
+
105
+ if line.startswith("#EXT-X-MAP"):
106
+ match = re.search(r'URI="([^"]+)"', line)
107
+ if match:
108
+ init_url = match.group(1)
109
+
110
+ if init_url:
111
+ init_url = urljoin(manifest_url, init_url)
112
+
113
+ return init_url, segments
114
+
115
+ @staticmethod
116
+ def _stream_url(session: requests.Session, url: str, headers: dict[str, str], output_file: BufferedWriter) -> None:
117
+ """Stream data from a URL into a buffered writer.
118
+
119
+ Args:
120
+ session: The requests session to use.
121
+ url: The URL to stream from.
122
+ headers: The headers to use.
123
+ output_file: The buffered writer to write the data to.
124
+ """
125
+ with session.get(url, headers=headers, stream=True, timeout=120) as resp:
126
+ resp.raise_for_status()
127
+
128
+ # Validate content type
129
+ content_type = resp.headers.get("content-type", "")
130
+ if not content_type.startswith(("video/", "application/octet-stream")):
131
+ logger.warning("Unexpected content type: %s", content_type)
132
+ return
133
+
134
+ for chunk in resp.iter_content(chunk_size=64 * 1024):
135
+ if chunk:
136
+ output_file.write(chunk)
137
+
138
+ @staticmethod
139
+ def _cleanup_temp_directory(video_dir: str) -> None:
140
+ """Clean up the temporary directory.
141
+
142
+ Args:
143
+ video_dir: The directory to clean up.
144
+ """
145
+ try:
146
+ shutil.rmtree(video_dir)
147
+ except OSError:
148
+ pass
149
+
150
+ def __init__(self, base_url: str, api_key: str):
151
+ """Initialize the recorder.
152
+
153
+ Args:
154
+ base_url: Steel API base URL.
155
+ api_key: Steel API key for authentication.
156
+ """
157
+ self.base_url = base_url
158
+ self.api_key = api_key
159
+
160
+ self._manifest_poll_timeout_minutes = 1
161
+ self._manifest_poll_interval_seconds = 2
162
+
163
+ try:
164
+ self.minio_storage = MinIOStorage()
165
+ except Exception as exc:
166
+ logger.warning("Failed to initialize MinIO storage: %s", exc)
167
+ self.minio_storage = None
168
+
169
+ def _download_manifest(self, session_id: str, dest: Path) -> str | None:
170
+ """Download the Steel HLS manifest for a session.
171
+
172
+ Args:
173
+ session_id: The session ID to download the manifest for.
174
+ dest: The destination path to save the manifest to.
175
+
176
+ Returns:
177
+ str | None: The URL of the manifest, or None if the base URL or API key is not set.
178
+
179
+ Raises:
180
+ requests.exceptions.RequestException: If the manifest is not ready after the timeout.
181
+ """
182
+ if not self.base_url or not self.api_key:
183
+ logger.warning("Base URL or API key not set, skipping download for session %s", session_id)
184
+ return None
185
+
186
+ url = f"{self.base_url.rstrip('/')}/v1/sessions/{session_id}/hls"
187
+ headers = {"steel-api-key": self.api_key}
188
+
189
+ deadline = time.monotonic() + (self._manifest_poll_timeout_minutes * 60)
190
+ attempt = 0
191
+ while True:
192
+ response = requests.get(url, headers=headers, timeout=60)
193
+ if response.ok:
194
+ dest.write_text(response.text)
195
+ return response.url
196
+
197
+ pollable = response.status_code in {404, 408, 429, 503}
198
+ attempt += 1
199
+ if not pollable or time.monotonic() >= deadline:
200
+ logger.warning(
201
+ "Manifest not ready for session %s after %.1f minutes, last status %d",
202
+ session_id,
203
+ self._manifest_poll_timeout_minutes,
204
+ response.status_code,
205
+ )
206
+ response.raise_for_status()
207
+
208
+ logger.debug(
209
+ "Manifest not ready for session %s (attempt %d, status %d); waiting %.1f seconds",
210
+ session_id,
211
+ attempt,
212
+ response.status_code,
213
+ self._manifest_poll_interval_seconds,
214
+ )
215
+ time.sleep(self._manifest_poll_interval_seconds)
216
+
217
+ def _merge_segments(self, manifest_path: Path, manifest_url: str, output_path: Path) -> bool:
218
+ """Merge HLS segments defined in a manifest into a single video file.
219
+
220
+ Args:
221
+ manifest_path: The path to the manifest.
222
+ manifest_url: The URL of the manifest.
223
+ output_path: The path to save the merged video to.
224
+
225
+ Returns:
226
+ bool: True if the segments were merged successfully, False otherwise.
227
+ """
228
+ if not self.api_key:
229
+ logger.warning("API key not set, skipping merge for session %s", manifest_url)
230
+ return False
231
+
232
+ headers = {"steel-api-key": self.api_key}
233
+ manifest_text = manifest_path.read_text()
234
+ init_url, segments = SteelSessionRecorder._parse_manifest_entries(manifest_text, manifest_url)
235
+
236
+ if not init_url or not segments:
237
+ logger.warning("No media segments found in manifest.")
238
+ return False
239
+
240
+ output_path.parent.mkdir(parents=True, exist_ok=True)
241
+
242
+ with requests.Session() as session, open(output_path, "wb") as output_file:
243
+ if init_url:
244
+ SteelSessionRecorder._stream_url(session, init_url, headers, output_file)
245
+
246
+ for segment_url in segments:
247
+ SteelSessionRecorder._stream_url(session, segment_url, headers, output_file)
248
+
249
+ return True
250
+
251
+ async def _download_and_merge_manifest(self, session_id: str, output_path: Path) -> bool:
252
+ """Download and merge the Steel HLS manifest into a single file.
253
+
254
+ Args:
255
+ session_id: The session ID to download the manifest for.
256
+ output_path: The path to save the merged video to.
257
+
258
+ Returns:
259
+ bool: True if the manifest was downloaded and merged successfully, False otherwise.
260
+ """
261
+ manifest_path = Path(tempfile.NamedTemporaryFile(suffix=MANIFEST_TEMP_SUFFIX, delete=False).name)
262
+
263
+ try:
264
+ manifest_url = await asyncio.to_thread(self._download_manifest, session_id, manifest_path)
265
+ if not manifest_url:
266
+ logger.warning("Failed to download manifest for session %s", session_id)
267
+ return False
268
+
269
+ merged = await asyncio.to_thread(self._merge_segments, manifest_path, manifest_url, output_path)
270
+ if not merged:
271
+ logger.warning("Failed to merge segments for session %s", session_id)
272
+ return False
273
+
274
+ return True
275
+ except Exception as exc:
276
+ logger.warning(
277
+ "Failed to generate video from HLS manifest for session %s: %s", session_id, exc, exc_info=True
278
+ )
279
+ return False
280
+ finally:
281
+ manifest_path.unlink(missing_ok=True)
282
+
283
+ async def _upload_video_to_minio(self, video_path: Path, video_filename: str) -> None:
284
+ """Upload a video file to MinIO storage.
285
+
286
+ Args:
287
+ video_path: The path to the video file to upload.
288
+ video_filename: The filename of the video to upload.
289
+ """
290
+ if not video_path.exists():
291
+ logger.warning("Video file not found: %s", video_path)
292
+ return
293
+
294
+ await asyncio.to_thread(self.minio_storage.upload_file, str(video_path), video_filename)
295
+
296
+ async def record_session_to_video(self, session_id: str) -> None:
297
+ """Download the HLS manifest and upload the merged video to MinIO.
298
+
299
+ Args:
300
+ session_id: The session ID to record.
301
+ """
302
+ if not self.minio_storage:
303
+ logger.warning("MinIO storage not available, skipping recording for %s", session_id)
304
+ return
305
+
306
+ video_dir = tempfile.mkdtemp()
307
+ try:
308
+ video_filename = SteelSessionRecorder.generate_video_filename(session_id, extension=".mp4")
309
+ output_path = Path(video_dir) / video_filename
310
+ merged = await self._download_and_merge_manifest(session_id, output_path)
311
+ if not merged:
312
+ logger.warning("Skipping upload because manifest merge failed for session %s", session_id)
313
+ return
314
+
315
+ await self._upload_video_to_minio(output_path, video_filename)
316
+ finally:
317
+ SteelSessionRecorder._cleanup_temp_directory(video_dir)