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.
- aip_agents/__init__.py +65 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1086 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/agent/__init__.py +27 -0
- aip_agents/agent/base_agent.py +970 -0
- aip_agents/agent/base_langgraph_agent.py +2942 -0
- aip_agents/agent/google_adk_agent.py +926 -0
- aip_agents/agent/google_adk_constants.py +6 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
- aip_agents/agent/langgraph_react_agent.py +2514 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/constants.py +23 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +142 -0
- aip_agents/examples/demo_memory_recall.py +401 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
- aip_agents/examples/hello_world_tool_output_client.py +46 -0
- aip_agents/examples/hello_world_tool_output_server.py +114 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +126 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/executor/agent_executor.py +473 -0
- aip_agents/executor/base.py +48 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +14 -0
- aip_agents/mcp/client/base_mcp_client.py +369 -0
- aip_agents/mcp/client/connection_manager.py +193 -0
- aip_agents/mcp/client/google_adk/__init__.py +11 -0
- aip_agents/mcp/client/google_adk/client.py +381 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +359 -0
- aip_agents/mcp/client/session_pool.py +351 -0
- aip_agents/mcp/client/transports.py +215 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +717 -0
- aip_agents/memory/adapters/mem0.py +84 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +17 -0
- aip_agents/middleware/base.py +88 -0
- aip_agents/middleware/manager.py +128 -0
- aip_agents/middleware/todolist.py +274 -0
- aip_agents/schema/__init__.py +69 -0
- aip_agents/schema/a2a.py +56 -0
- aip_agents/schema/agent.py +111 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +37 -0
- aip_agents/schema/model_id.py +97 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +151 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +33 -0
- aip_agents/tools/bosa_tools.py +105 -0
- aip_agents/tools/browser_use/__init__.py +82 -0
- aip_agents/tools/browser_use/action_parser.py +103 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +198 -0
- aip_agents/tools/browser_use/schemas.py +119 -0
- aip_agents/tools/browser_use/session.py +76 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +317 -0
- aip_agents/tools/browser_use/streaming.py +813 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +78 -0
- aip_agents/tools/code_sandbox/__init__.py +26 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
- aip_agents/tools/constants.py +165 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +351 -0
- aip_agents/tools/memory_search/__init__.py +22 -0
- aip_agents/tools/memory_search/base.py +200 -0
- aip_agents/tools/memory_search/mem0.py +258 -0
- aip_agents/tools/memory_search/schema.py +48 -0
- aip_agents/tools/memory_search_tool.py +26 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +70 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +79 -0
- aip_agents/utils/a2a_connector.py +1757 -0
- aip_agents/utils/artifact_helpers.py +502 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +176 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +231 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +128 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
- aip_agents/utils/langgraph/tool_output_management.py +967 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +358 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +25 -0
- aip_agents/utils/pii/pii_handler.py +397 -0
- aip_agents/utils/pii/pii_helper.py +207 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +831 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +156 -0
- aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
- aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
- aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
- 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)
|