autobyteus 1.1.8__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +6 -2
  2. autobyteus/agent/handlers/inter_agent_message_event_handler.py +17 -19
  3. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +6 -3
  4. autobyteus/agent/handlers/tool_result_event_handler.py +61 -18
  5. autobyteus/agent/handlers/user_input_message_event_handler.py +19 -10
  6. autobyteus/agent/hooks/base_phase_hook.py +17 -0
  7. autobyteus/agent/hooks/hook_registry.py +15 -27
  8. autobyteus/agent/input_processor/base_user_input_processor.py +17 -1
  9. autobyteus/agent/input_processor/processor_registry.py +15 -27
  10. autobyteus/agent/llm_response_processor/base_processor.py +17 -1
  11. autobyteus/agent/llm_response_processor/processor_registry.py +15 -24
  12. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +14 -0
  13. autobyteus/agent/message/agent_input_user_message.py +15 -2
  14. autobyteus/agent/message/send_message_to.py +1 -1
  15. autobyteus/agent/processor_option.py +17 -0
  16. autobyteus/agent/sender_type.py +1 -0
  17. autobyteus/agent/system_prompt_processor/base_processor.py +17 -1
  18. autobyteus/agent/system_prompt_processor/processor_registry.py +15 -27
  19. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +10 -0
  20. autobyteus/agent/tool_execution_result_processor/base_processor.py +17 -1
  21. autobyteus/agent/tool_execution_result_processor/processor_registry.py +15 -1
  22. autobyteus/agent/workspace/base_workspace.py +1 -1
  23. autobyteus/agent/workspace/workspace_definition.py +1 -1
  24. autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +1 -1
  25. autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +2 -2
  26. autobyteus/agent_team/task_notification/__init__.py +4 -0
  27. autobyteus/agent_team/task_notification/activation_policy.py +70 -0
  28. autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +56 -122
  29. autobyteus/agent_team/task_notification/task_activator.py +66 -0
  30. autobyteus/cli/agent_team_tui/state.py +17 -20
  31. autobyteus/cli/agent_team_tui/widgets/focus_pane.py +1 -1
  32. autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +1 -1
  33. autobyteus/clients/__init__.py +10 -0
  34. autobyteus/clients/autobyteus_client.py +318 -0
  35. autobyteus/clients/cert_utils.py +105 -0
  36. autobyteus/clients/certificates/cert.pem +34 -0
  37. autobyteus/events/event_types.py +2 -2
  38. autobyteus/llm/api/autobyteus_llm.py +1 -1
  39. autobyteus/llm/api/gemini_llm.py +45 -54
  40. autobyteus/llm/api/qwen_llm.py +25 -0
  41. autobyteus/llm/api/zhipu_llm.py +26 -0
  42. autobyteus/llm/autobyteus_provider.py +9 -3
  43. autobyteus/llm/llm_factory.py +39 -0
  44. autobyteus/llm/ollama_provider_resolver.py +1 -0
  45. autobyteus/llm/providers.py +1 -0
  46. autobyteus/llm/token_counter/token_counter_factory.py +3 -0
  47. autobyteus/llm/token_counter/zhipu_token_counter.py +24 -0
  48. autobyteus/multimedia/audio/api/autobyteus_audio_client.py +5 -2
  49. autobyteus/multimedia/audio/api/gemini_audio_client.py +84 -153
  50. autobyteus/multimedia/audio/audio_client_factory.py +47 -22
  51. autobyteus/multimedia/audio/audio_model.py +13 -6
  52. autobyteus/multimedia/audio/autobyteus_audio_provider.py +9 -3
  53. autobyteus/multimedia/audio/base_audio_client.py +3 -1
  54. autobyteus/multimedia/image/api/autobyteus_image_client.py +13 -6
  55. autobyteus/multimedia/image/api/gemini_image_client.py +72 -130
  56. autobyteus/multimedia/image/api/openai_image_client.py +4 -2
  57. autobyteus/multimedia/image/autobyteus_image_provider.py +9 -3
  58. autobyteus/multimedia/image/base_image_client.py +6 -2
  59. autobyteus/multimedia/image/image_client_factory.py +20 -19
  60. autobyteus/multimedia/image/image_model.py +13 -6
  61. autobyteus/multimedia/providers.py +1 -0
  62. autobyteus/task_management/__init__.py +10 -10
  63. autobyteus/task_management/base_task_board.py +14 -6
  64. autobyteus/task_management/converters/__init__.py +0 -2
  65. autobyteus/task_management/converters/task_board_converter.py +7 -16
  66. autobyteus/task_management/events.py +6 -6
  67. autobyteus/task_management/in_memory_task_board.py +48 -38
  68. autobyteus/task_management/schemas/__init__.py +2 -2
  69. autobyteus/task_management/schemas/{plan_definition.py → task_definition.py} +6 -7
  70. autobyteus/task_management/schemas/task_status_report.py +1 -2
  71. autobyteus/task_management/task.py +60 -0
  72. autobyteus/task_management/tools/__init__.py +6 -2
  73. autobyteus/task_management/tools/assign_task_to.py +125 -0
  74. autobyteus/task_management/tools/get_my_tasks.py +80 -0
  75. autobyteus/task_management/tools/get_task_board_status.py +3 -3
  76. autobyteus/task_management/tools/publish_task.py +77 -0
  77. autobyteus/task_management/tools/publish_tasks.py +74 -0
  78. autobyteus/task_management/tools/update_task_status.py +5 -5
  79. autobyteus/tools/__init__.py +54 -16
  80. autobyteus/tools/base_tool.py +4 -4
  81. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +1 -1
  82. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +1 -1
  83. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +1 -1
  84. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +1 -1
  85. autobyteus/tools/browser/standalone/navigate_to.py +1 -1
  86. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +1 -1
  87. autobyteus/tools/browser/standalone/webpage_image_downloader.py +1 -1
  88. autobyteus/tools/browser/standalone/webpage_reader.py +1 -1
  89. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +1 -1
  90. autobyteus/tools/download_media_tool.py +136 -0
  91. autobyteus/tools/file/file_editor.py +200 -0
  92. autobyteus/tools/functional_tool.py +1 -1
  93. autobyteus/tools/google_search.py +1 -1
  94. autobyteus/tools/mcp/factory.py +1 -1
  95. autobyteus/tools/mcp/schema_mapper.py +1 -1
  96. autobyteus/tools/mcp/tool.py +1 -1
  97. autobyteus/tools/multimedia/__init__.py +2 -0
  98. autobyteus/tools/multimedia/audio_tools.py +10 -20
  99. autobyteus/tools/multimedia/image_tools.py +21 -22
  100. autobyteus/tools/multimedia/media_reader_tool.py +117 -0
  101. autobyteus/tools/pydantic_schema_converter.py +1 -1
  102. autobyteus/tools/registry/tool_definition.py +1 -1
  103. autobyteus/tools/timer.py +1 -1
  104. autobyteus/tools/tool_meta.py +1 -1
  105. autobyteus/tools/usage/formatters/default_json_example_formatter.py +1 -1
  106. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +1 -1
  107. autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +59 -3
  108. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +1 -1
  109. autobyteus/tools/usage/formatters/google_json_example_formatter.py +1 -1
  110. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +1 -1
  111. autobyteus/tools/usage/parsers/_string_decoders.py +18 -0
  112. autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +9 -1
  113. autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +15 -1
  114. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +4 -1
  115. autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +4 -1
  116. autobyteus/{tools → utils}/parameter_schema.py +1 -1
  117. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/METADATA +4 -3
  118. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/RECORD +122 -108
  119. examples/run_poem_writer.py +1 -1
  120. autobyteus/task_management/converters/task_plan_converter.py +0 -48
  121. autobyteus/task_management/task_plan.py +0 -110
  122. autobyteus/task_management/tools/publish_task_plan.py +0 -101
  123. autobyteus/tools/image_downloader.py +0 -99
  124. autobyteus/tools/pdf_downloader.py +0 -89
  125. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/WHEEL +0 -0
  126. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/licenses/LICENSE +0 -0
  127. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,110 +0,0 @@
1
- # file: autobyteus/autobyteus/task_management/task_plan.py
2
- """
3
- Defines the data structures for a task plan and its constituent tasks.
4
- These models represent the static, intended structure of a plan of action.
5
- """
6
- import logging
7
- import uuid
8
- from typing import List, Dict, Any
9
- from pydantic import BaseModel, Field, field_validator, model_validator
10
-
11
- # To avoid circular import, we use a string forward reference.
12
- from typing import TYPE_CHECKING
13
- if TYPE_CHECKING:
14
- from autobyteus.task_management.deliverable import FileDeliverable
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
- def generate_task_id():
19
- """Generates a unique task identifier."""
20
- return f"task_{uuid.uuid4().hex}"
21
-
22
- def generate_plan_id():
23
- """Generates a unique plan identifier."""
24
- return f"plan_{uuid.uuid4().hex}"
25
-
26
- class Task(BaseModel):
27
- """
28
- Represents a single, discrete unit of work within a larger TaskPlan.
29
- """
30
- task_name: str = Field(..., description="A short, unique, descriptive name for this task within the plan (e.g., 'setup_project', 'implement_scraper'). Used for defining dependencies.")
31
-
32
- task_id: str = Field(default_factory=generate_task_id, description="A unique system-generated identifier for this task within the plan.")
33
-
34
- assignee_name: str = Field(..., description="The unique name of the agent or sub-team responsible for executing this task (e.g., 'SoftwareEngineer', 'ResearchTeam').")
35
- description: str = Field(..., description="A clear and concise description of what this task entails.")
36
-
37
- dependencies: List[str] = Field(
38
- default_factory=list,
39
- description="A list of 'task_name' values for tasks that must be completed before this one can be started."
40
- )
41
-
42
- # This is the updated field as per user request.
43
- file_deliverables: List["FileDeliverable"] = Field(
44
- default_factory=list,
45
- description="A list of file deliverables that were produced as a result of completing this task."
46
- )
47
-
48
- @model_validator(mode='before')
49
- @classmethod
50
- def handle_local_id_compatibility(cls, data: Any) -> Any:
51
- """Handles backward compatibility for the 'local_id' field."""
52
- if isinstance(data, dict) and 'local_id' in data:
53
- data['task_name'] = data.pop('local_id')
54
- # Compatibility for old artifact field
55
- if isinstance(data, dict) and 'produced_artifact_ids' in data:
56
- del data['produced_artifact_ids']
57
- return data
58
-
59
- def model_post_init(self, __context: Any) -> None:
60
- """Called after the model is initialized and validated."""
61
- logger.debug(f"Task created: Name='{self.task_name}', SystemID='{self.task_id}', Assignee='{self.assignee_name}'")
62
-
63
-
64
- class TaskPlan(BaseModel):
65
- """
66
- Represents a complete, static plan for achieving a high-level goal.
67
- It is composed of a list of interconnected tasks.
68
- """
69
- plan_id: str = Field(default_factory=generate_plan_id, description="A unique system-generated identifier for this entire plan.")
70
-
71
- overall_goal: str = Field(..., description="The high-level objective that this plan is designed to achieve.")
72
- tasks: List[Task] = Field(..., description="The list of tasks that make up this plan.")
73
-
74
- @field_validator('tasks')
75
- def task_names_must_be_unique(cls, tasks: List[Task]) -> List[Task]:
76
- """Ensures that the LLM-provided task_names are unique within the plan."""
77
- seen_names = set()
78
- for task in tasks:
79
- if task.task_name in seen_names:
80
- raise ValueError(f"Duplicate task_name '{task.task_name}' found in task list. Each task_name must be unique within the plan.")
81
- seen_names.add(task.task_name)
82
- return tasks
83
-
84
- def hydrate_dependencies(self) -> 'TaskPlan':
85
- """
86
- Converts the dependency list of task_names to system-generated task_ids.
87
- This makes the plan internally consistent and ready for execution.
88
- """
89
- name_to_system_id_map = {task.task_name: task.task_id for task in self.tasks}
90
-
91
- for task in self.tasks:
92
- # Create a new list for the resolved dependency IDs
93
- resolved_deps = []
94
- for dep_name in task.dependencies:
95
- if dep_name not in name_to_system_id_map:
96
- raise ValueError(f"Task '{task.task_name}' has an invalid dependency: '{dep_name}' does not correspond to any task's name.")
97
- resolved_deps.append(name_to_system_id_map[dep_name])
98
- # Replace the old list of names with the new list of system_ids
99
- task.dependencies = resolved_deps
100
-
101
- logger.debug(f"TaskPlan '{self.plan_id}' successfully hydrated dependencies.")
102
- return self
103
-
104
- def model_post_init(self, __context: Any) -> None:
105
- """Called after the model is initialized and validated."""
106
- logger.debug(f"TaskPlan created: ID='{self.plan_id}', Tasks={len(self.tasks)}")
107
-
108
- # This is necessary for Pydantic v2 to correctly handle the recursive model
109
- from autobyteus.task_management.deliverable import FileDeliverable
110
- Task.model_rebuild()
@@ -1,101 +0,0 @@
1
- # file: autobyteus/autobyteus/task_management/tools/publish_task_plan.py
2
- import json
3
- import logging
4
- from typing import TYPE_CHECKING, Optional, Dict, Any
5
-
6
- from pydantic import ValidationError
7
-
8
- from autobyteus.tools.base_tool import BaseTool
9
- from autobyteus.tools.tool_category import ToolCategory
10
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
11
- from autobyteus.tools.pydantic_schema_converter import pydantic_to_parameter_schema
12
- from autobyteus.task_management.schemas import TaskPlanDefinitionSchema
13
- from autobyteus.task_management.converters import TaskPlanConverter, TaskBoardConverter
14
-
15
- if TYPE_CHECKING:
16
- from autobyteus.agent.context import AgentContext
17
- from autobyteus.agent_team.context import AgentTeamContext
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
- class PublishTaskPlan(BaseTool):
22
- """A tool for the coordinator to parse and load a generated plan into the TaskBoard."""
23
-
24
- CATEGORY = ToolCategory.TASK_MANAGEMENT
25
-
26
- @classmethod
27
- def get_name(cls) -> str:
28
- return "PublishTaskPlan"
29
-
30
- @classmethod
31
- def get_description(cls) -> str:
32
- return (
33
- "Parses a structured object representing a complete task plan, converts it into a "
34
- "system-ready format, and loads it onto the team's shared task board. "
35
- "This action resets the task board with the new plan. Upon success, it returns "
36
- "the initial status of the newly loaded task board. "
37
- "This tool should typically only be used by the team coordinator."
38
- )
39
-
40
- @classmethod
41
- def get_argument_schema(cls) -> Optional[ParameterSchema]:
42
- schema = ParameterSchema()
43
-
44
- # Convert the Pydantic model to our native ParameterSchema for the nested object
45
- plan_object_schema = pydantic_to_parameter_schema(TaskPlanDefinitionSchema)
46
-
47
- schema.add_parameter(ParameterDefinition(
48
- name="plan",
49
- param_type=ParameterType.OBJECT,
50
- description=(
51
- "A structured object representing a complete task plan. This object defines the overall goal "
52
- "and a list of tasks with their assignees, descriptions, and dependencies. "
53
- "Each task must have a unique name within the plan."
54
- ),
55
- required=True,
56
- object_schema=plan_object_schema
57
- ))
58
- return schema
59
-
60
- async def _execute(self, context: 'AgentContext', plan: Dict[str, Any]) -> str:
61
- """
62
- Executes the tool by validating the plan object, using a converter to create a TaskPlan,
63
- and loading it onto the task board.
64
- """
65
- logger.info(f"Agent '{context.agent_id}' is executing PublishTaskPlan.")
66
-
67
- team_context: Optional['AgentTeamContext'] = context.custom_data.get("team_context")
68
- if not team_context:
69
- error_msg = "Error: Team context is not available. Cannot access the task board."
70
- logger.error(f"Agent '{context.agent_id}': {error_msg}")
71
- return error_msg
72
-
73
- task_board = getattr(team_context.state, 'task_board', None)
74
- if not task_board:
75
- error_msg = "Error: Task board has not been initialized for this team."
76
- logger.error(f"Agent '{context.agent_id}': {error_msg}")
77
- return error_msg
78
-
79
- try:
80
- plan_definition_schema = TaskPlanDefinitionSchema(**plan)
81
- final_plan = TaskPlanConverter.from_schema(plan_definition_schema)
82
- except (ValidationError, ValueError) as e:
83
- error_msg = f"Invalid or inconsistent task plan provided: {e}"
84
- logger.warning(f"Agent '{context.agent_id}' provided an invalid plan for PublishTaskPlan: {error_msg}")
85
- return f"Error: {error_msg}"
86
- except Exception as e:
87
- error_msg = f"An unexpected error occurred during plan parsing or conversion: {e}"
88
- logger.error(f"Agent '{context.agent_id}': {error_msg}", exc_info=True)
89
- return f"Error: {error_msg}"
90
-
91
- if task_board.load_task_plan(final_plan):
92
- logger.info(f"Agent '{context.agent_id}': Task plan published successfully. Returning new board status.")
93
- status_report_schema = TaskBoardConverter.to_schema(task_board)
94
- if status_report_schema:
95
- return status_report_schema.model_dump_json(indent=2)
96
- else:
97
- return "Task plan published successfully, but could not generate status report."
98
- else:
99
- error_msg = "Failed to load task plan onto the board. This can happen if the board implementation rejects the plan."
100
- logger.error(f"Agent '{context.agent_id}': {error_msg}")
101
- return f"Error: {error_msg}"
@@ -1,99 +0,0 @@
1
- import os
2
- import aiohttp
3
- import logging
4
- from datetime import datetime
5
- from typing import Optional, TYPE_CHECKING, Any
6
-
7
- from autobyteus.tools.base_tool import BaseTool
8
- from autobyteus.tools.tool_config import ToolConfig
9
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
10
- from autobyteus.tools.tool_category import ToolCategory
11
- from PIL import Image
12
- from io import BytesIO
13
- from autobyteus.utils.file_utils import get_default_download_folder
14
- from autobyteus.events.event_types import EventType
15
-
16
- if TYPE_CHECKING:
17
- from autobyteus.agent.context import AgentContext
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
- class ImageDownloader(BaseTool):
22
- CATEGORY = ToolCategory.WEB
23
- supported_formats = ['.jpeg', '.jpg', '.gif', '.png', '.webp']
24
-
25
- def __init__(self, config: Optional[ToolConfig] = None):
26
- super().__init__(config=config)
27
-
28
- custom_download_folder = None
29
- if config:
30
- custom_download_folder = config.get('custom_download_folder')
31
-
32
- self.default_download_folder = get_default_download_folder()
33
- self.download_folder = custom_download_folder or self.default_download_folder
34
- self.last_downloaded_image = None
35
-
36
- # Explicitly subscribe the handler in the constructor
37
- self.subscribe(EventType.WEIBO_POST_COMPLETED, self.on_weibo_post_completed)
38
-
39
- logger.debug(f"ImageDownloader initialized with download_folder: {self.download_folder}")
40
-
41
- @classmethod
42
- def get_description(cls) -> str:
43
- return f"Downloads an image from a given URL. Supported formats: {', '.join(format.upper()[1:] for format in cls.supported_formats)}."
44
-
45
- @classmethod
46
- def get_argument_schema(cls) -> Optional[ParameterSchema]:
47
- schema = ParameterSchema()
48
- schema.add_parameter(ParameterDefinition(name="url", param_type=ParameterType.STRING, description=f"A direct URL to an image file (must end with {', '.join(cls.supported_formats)}).", required=True))
49
- schema.add_parameter(ParameterDefinition(name="folder", param_type=ParameterType.STRING, description="Optional. Custom directory path to save this specific image. Overrides instance default.", required=False))
50
- return schema
51
-
52
- @classmethod
53
- def get_config_schema(cls) -> Optional[ParameterSchema]:
54
- schema = ParameterSchema()
55
- schema.add_parameter(ParameterDefinition(name="custom_download_folder", param_type=ParameterType.STRING, description="Custom directory path where downloaded images will be saved by default.", required=False, default_value=None))
56
- return schema
57
-
58
- async def _execute(self, context: 'AgentContext', url: str, folder: Optional[str] = None) -> str:
59
- current_download_folder = folder or self.download_folder
60
- if not any(url.lower().endswith(fmt) for fmt in self.supported_formats):
61
- raise ValueError(f"Unsupported image format. URL must end with one of: {', '.join(self.supported_formats)}.")
62
-
63
- try:
64
- async with aiohttp.ClientSession() as session:
65
- async with session.get(url) as response:
66
- response.raise_for_status()
67
- image_bytes = await response.read()
68
-
69
- with Image.open(BytesIO(image_bytes)) as img:
70
- img.verify()
71
-
72
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
73
- extension = os.path.splitext(url)[1].lower() or ".png"
74
-
75
- filename = f"downloaded_image_{timestamp}{extension}"
76
- filepath = os.path.join(current_download_folder, filename)
77
-
78
- os.makedirs(current_download_folder, exist_ok=True)
79
- with open(filepath, 'wb') as f:
80
- f.write(image_bytes)
81
-
82
- self.last_downloaded_image = filepath
83
- logger.info(f"The image is downloaded and stored at: {filepath}")
84
- self.emit(EventType.IMAGE_DOWNLOADED, image_path=filepath)
85
- return f"The image is downloaded and stored at: {filepath}"
86
- except Exception as e:
87
- logger.error(f"Error processing image from {url}: {str(e)}", exc_info=True)
88
- raise ValueError(f"Error processing image from {url}: {str(e)}")
89
-
90
- def on_weibo_post_completed(self): # No **kwargs needed due to intelligent dispatch
91
- if self.last_downloaded_image and os.path.exists(self.last_downloaded_image):
92
- try:
93
- os.remove(self.last_downloaded_image)
94
- logger.info(f"Removed downloaded image: {self.last_downloaded_image} after Weibo post.")
95
- except Exception as e:
96
- logger.error(f"Failed to remove downloaded image: {self.last_downloaded_image}. Error: {str(e)}", exc_info=True)
97
- else:
98
- logger.debug("No last downloaded image to remove or image file not found.")
99
- self.last_downloaded_image = None
@@ -1,89 +0,0 @@
1
- # This was top-level, keep it there.
2
- import os
3
- import logging
4
- import asyncio
5
- import requests
6
- from datetime import datetime
7
- from typing import TYPE_CHECKING, Optional
8
-
9
- from autobyteus.tools import tool
10
- from autobyteus.tools.tool_category import ToolCategory
11
- from autobyteus.utils.file_utils import get_default_download_folder
12
-
13
- if TYPE_CHECKING:
14
- from autobyteus.agent.context import AgentContext
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
- @tool(name="PDFDownloader", category=ToolCategory.WEB)
19
- async def pdf_downloader( # function name can be pdf_downloader
20
- context: 'AgentContext',
21
- url: str,
22
- folder: Optional[str] = None
23
- ) -> str:
24
- """
25
- Downloads a PDF file from a given URL and saves it locally.
26
- 'url' is the URL of the PDF.
27
- 'folder' (optional) is a custom directory to save the PDF. If not given,
28
- uses the system's default download folder. Validates Content-Type.
29
- """
30
- logger.debug(f"Functional PDFDownloader tool for agent {context.agent_id}, URL: {url}, Folder: {folder}")
31
-
32
- current_download_folder = folder if folder else get_default_download_folder()
33
-
34
- try:
35
- loop = asyncio.get_event_loop()
36
- response = await loop.run_in_executor(None, lambda: requests.get(url, stream=True, timeout=30))
37
- response.raise_for_status()
38
-
39
- content_type = response.headers.get('Content-Type', '').lower()
40
- if 'application/pdf' not in content_type:
41
- response.close()
42
- raise ValueError(f"The URL does not point to a PDF file. Content-Type: {content_type}")
43
-
44
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
45
- filename_from_header = None
46
- if 'Content-Disposition' in response.headers:
47
- import re
48
- match = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?', response.headers['Content-Disposition'])
49
- if match: filename_from_header = match.group(1)
50
-
51
- if filename_from_header and filename_from_header.lower().endswith(".pdf"):
52
- import string
53
- valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
54
- filename_from_header = ''.join(c for c in filename_from_header if c in valid_chars)[:200]
55
- filename = f"{timestamp}_{filename_from_header}"
56
- else:
57
- filename = f"downloaded_pdf_{timestamp}.pdf"
58
-
59
- save_path = os.path.join(current_download_folder, filename)
60
- os.makedirs(current_download_folder, exist_ok=True)
61
-
62
- def download_and_save_sync():
63
- with open(save_path, 'wb') as file_handle:
64
- for chunk in response.iter_content(chunk_size=8192):
65
- file_handle.write(chunk)
66
- response.close()
67
-
68
- await loop.run_in_executor(None, download_and_save_sync)
69
-
70
- logger.info(f"PDF successfully downloaded and saved to {save_path}")
71
- return f"PDF successfully downloaded and saved to {save_path}"
72
- except requests.exceptions.Timeout:
73
- logger.error(f"Timeout downloading PDF from {url}", exc_info=True)
74
- return f"Error downloading PDF: Timeout occurred for URL {url}"
75
- except requests.exceptions.RequestException as e:
76
- logger.error(f"Error downloading PDF from {url}: {str(e)}", exc_info=True)
77
- return f"Error downloading PDF: {str(e)}"
78
- except ValueError as e:
79
- logger.error(f"Content type error for PDF from {url}: {str(e)}", exc_info=True)
80
- return str(e)
81
- except IOError as e:
82
- logger.error(f"Error saving PDF to {current_download_folder}: {str(e)}", exc_info=True)
83
- return f"Error saving PDF: {str(e)}"
84
- except Exception as e:
85
- logger.error(f"Unexpected error downloading PDF from {url}: {str(e)}", exc_info=True)
86
- return f"An unexpected error occurred: {str(e)}"
87
- finally:
88
- if 'response' in locals() and hasattr(response, 'close') and response.raw and not response.raw.closed:
89
- response.close()