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.
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +6 -2
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +17 -19
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +6 -3
- autobyteus/agent/handlers/tool_result_event_handler.py +61 -18
- autobyteus/agent/handlers/user_input_message_event_handler.py +19 -10
- autobyteus/agent/hooks/base_phase_hook.py +17 -0
- autobyteus/agent/hooks/hook_registry.py +15 -27
- autobyteus/agent/input_processor/base_user_input_processor.py +17 -1
- autobyteus/agent/input_processor/processor_registry.py +15 -27
- autobyteus/agent/llm_response_processor/base_processor.py +17 -1
- autobyteus/agent/llm_response_processor/processor_registry.py +15 -24
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +14 -0
- autobyteus/agent/message/agent_input_user_message.py +15 -2
- autobyteus/agent/message/send_message_to.py +1 -1
- autobyteus/agent/processor_option.py +17 -0
- autobyteus/agent/sender_type.py +1 -0
- autobyteus/agent/system_prompt_processor/base_processor.py +17 -1
- autobyteus/agent/system_prompt_processor/processor_registry.py +15 -27
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +10 -0
- autobyteus/agent/tool_execution_result_processor/base_processor.py +17 -1
- autobyteus/agent/tool_execution_result_processor/processor_registry.py +15 -1
- autobyteus/agent/workspace/base_workspace.py +1 -1
- autobyteus/agent/workspace/workspace_definition.py +1 -1
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +1 -1
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +2 -2
- autobyteus/agent_team/task_notification/__init__.py +4 -0
- autobyteus/agent_team/task_notification/activation_policy.py +70 -0
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +56 -122
- autobyteus/agent_team/task_notification/task_activator.py +66 -0
- autobyteus/cli/agent_team_tui/state.py +17 -20
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +1 -1
- autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +1 -1
- autobyteus/clients/__init__.py +10 -0
- autobyteus/clients/autobyteus_client.py +318 -0
- autobyteus/clients/cert_utils.py +105 -0
- autobyteus/clients/certificates/cert.pem +34 -0
- autobyteus/events/event_types.py +2 -2
- autobyteus/llm/api/autobyteus_llm.py +1 -1
- autobyteus/llm/api/gemini_llm.py +45 -54
- autobyteus/llm/api/qwen_llm.py +25 -0
- autobyteus/llm/api/zhipu_llm.py +26 -0
- autobyteus/llm/autobyteus_provider.py +9 -3
- autobyteus/llm/llm_factory.py +39 -0
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/token_counter_factory.py +3 -0
- autobyteus/llm/token_counter/zhipu_token_counter.py +24 -0
- autobyteus/multimedia/audio/api/autobyteus_audio_client.py +5 -2
- autobyteus/multimedia/audio/api/gemini_audio_client.py +84 -153
- autobyteus/multimedia/audio/audio_client_factory.py +47 -22
- autobyteus/multimedia/audio/audio_model.py +13 -6
- autobyteus/multimedia/audio/autobyteus_audio_provider.py +9 -3
- autobyteus/multimedia/audio/base_audio_client.py +3 -1
- autobyteus/multimedia/image/api/autobyteus_image_client.py +13 -6
- autobyteus/multimedia/image/api/gemini_image_client.py +72 -130
- autobyteus/multimedia/image/api/openai_image_client.py +4 -2
- autobyteus/multimedia/image/autobyteus_image_provider.py +9 -3
- autobyteus/multimedia/image/base_image_client.py +6 -2
- autobyteus/multimedia/image/image_client_factory.py +20 -19
- autobyteus/multimedia/image/image_model.py +13 -6
- autobyteus/multimedia/providers.py +1 -0
- autobyteus/task_management/__init__.py +10 -10
- autobyteus/task_management/base_task_board.py +14 -6
- autobyteus/task_management/converters/__init__.py +0 -2
- autobyteus/task_management/converters/task_board_converter.py +7 -16
- autobyteus/task_management/events.py +6 -6
- autobyteus/task_management/in_memory_task_board.py +48 -38
- autobyteus/task_management/schemas/__init__.py +2 -2
- autobyteus/task_management/schemas/{plan_definition.py → task_definition.py} +6 -7
- autobyteus/task_management/schemas/task_status_report.py +1 -2
- autobyteus/task_management/task.py +60 -0
- autobyteus/task_management/tools/__init__.py +6 -2
- autobyteus/task_management/tools/assign_task_to.py +125 -0
- autobyteus/task_management/tools/get_my_tasks.py +80 -0
- autobyteus/task_management/tools/get_task_board_status.py +3 -3
- autobyteus/task_management/tools/publish_task.py +77 -0
- autobyteus/task_management/tools/publish_tasks.py +74 -0
- autobyteus/task_management/tools/update_task_status.py +5 -5
- autobyteus/tools/__init__.py +54 -16
- autobyteus/tools/base_tool.py +4 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +1 -1
- autobyteus/tools/browser/standalone/navigate_to.py +1 -1
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +1 -1
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +1 -1
- autobyteus/tools/browser/standalone/webpage_reader.py +1 -1
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +1 -1
- autobyteus/tools/download_media_tool.py +136 -0
- autobyteus/tools/file/file_editor.py +200 -0
- autobyteus/tools/functional_tool.py +1 -1
- autobyteus/tools/google_search.py +1 -1
- autobyteus/tools/mcp/factory.py +1 -1
- autobyteus/tools/mcp/schema_mapper.py +1 -1
- autobyteus/tools/mcp/tool.py +1 -1
- autobyteus/tools/multimedia/__init__.py +2 -0
- autobyteus/tools/multimedia/audio_tools.py +10 -20
- autobyteus/tools/multimedia/image_tools.py +21 -22
- autobyteus/tools/multimedia/media_reader_tool.py +117 -0
- autobyteus/tools/pydantic_schema_converter.py +1 -1
- autobyteus/tools/registry/tool_definition.py +1 -1
- autobyteus/tools/timer.py +1 -1
- autobyteus/tools/tool_meta.py +1 -1
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +59 -3
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +1 -1
- autobyteus/tools/usage/parsers/_string_decoders.py +18 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +9 -1
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +15 -1
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +4 -1
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +4 -1
- autobyteus/{tools → utils}/parameter_schema.py +1 -1
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/METADATA +4 -3
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/RECORD +122 -108
- examples/run_poem_writer.py +1 -1
- autobyteus/task_management/converters/task_plan_converter.py +0 -48
- autobyteus/task_management/task_plan.py +0 -110
- autobyteus/task_management/tools/publish_task_plan.py +0 -101
- autobyteus/tools/image_downloader.py +0 -99
- autobyteus/tools/pdf_downloader.py +0 -89
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|