autobyteus 1.1.9__py3-none-any.whl → 1.2.1__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/context/agent_runtime_state.py +4 -0
- autobyteus/agent/events/notifiers.py +5 -1
- autobyteus/agent/message/send_message_to.py +5 -4
- autobyteus/agent/streaming/agent_event_stream.py +5 -0
- autobyteus/agent/streaming/stream_event_payloads.py +25 -0
- autobyteus/agent/streaming/stream_events.py +13 -1
- autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +4 -4
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +12 -12
- autobyteus/agent_team/context/agent_team_runtime_state.py +2 -2
- autobyteus/agent_team/streaming/agent_team_event_notifier.py +4 -4
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +3 -3
- autobyteus/agent_team/streaming/agent_team_stream_events.py +8 -8
- autobyteus/agent_team/task_notification/activation_policy.py +1 -1
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +22 -22
- autobyteus/agent_team/task_notification/task_notification_mode.py +1 -1
- autobyteus/cli/agent_team_tui/app.py +4 -4
- autobyteus/cli/agent_team_tui/state.py +8 -8
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +3 -3
- autobyteus/cli/agent_team_tui/widgets/shared.py +1 -1
- autobyteus/cli/agent_team_tui/widgets/{task_board_panel.py → task_plan_panel.py} +5 -5
- 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 +4 -3
- autobyteus/llm/api/autobyteus_llm.py +1 -1
- autobyteus/llm/api/zhipu_llm.py +26 -0
- autobyteus/llm/autobyteus_provider.py +1 -1
- autobyteus/llm/llm_factory.py +23 -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/__init__.py +3 -2
- autobyteus/multimedia/audio/api/autobyteus_audio_client.py +1 -1
- autobyteus/multimedia/audio/api/openai_audio_client.py +112 -0
- autobyteus/multimedia/audio/audio_client_factory.py +37 -0
- autobyteus/multimedia/audio/autobyteus_audio_provider.py +1 -1
- autobyteus/multimedia/image/api/autobyteus_image_client.py +1 -1
- autobyteus/multimedia/image/autobyteus_image_provider.py +1 -1
- autobyteus/multimedia/image/image_client_factory.py +1 -1
- autobyteus/task_management/__init__.py +44 -20
- autobyteus/task_management/{base_task_board.py → base_task_plan.py} +16 -13
- autobyteus/task_management/converters/__init__.py +2 -2
- autobyteus/task_management/converters/{task_board_converter.py → task_plan_converter.py} +13 -13
- autobyteus/task_management/events.py +7 -7
- autobyteus/task_management/{in_memory_task_board.py → in_memory_task_plan.py} +34 -22
- autobyteus/task_management/schemas/__init__.py +3 -0
- autobyteus/task_management/schemas/task_definition.py +1 -1
- autobyteus/task_management/schemas/task_status_report.py +3 -3
- autobyteus/task_management/schemas/todo_definition.py +15 -0
- autobyteus/task_management/todo.py +29 -0
- autobyteus/task_management/todo_list.py +75 -0
- autobyteus/task_management/tools/__init__.py +25 -7
- autobyteus/task_management/tools/task_tools/__init__.py +19 -0
- autobyteus/task_management/tools/task_tools/assign_task_to.py +125 -0
- autobyteus/task_management/tools/{publish_task.py → task_tools/create_task.py} +16 -18
- autobyteus/task_management/tools/{publish_tasks.py → task_tools/create_tasks.py} +19 -19
- autobyteus/task_management/tools/{get_my_tasks.py → task_tools/get_my_tasks.py} +15 -15
- autobyteus/task_management/tools/{get_task_board_status.py → task_tools/get_task_plan_status.py} +16 -16
- autobyteus/task_management/tools/{update_task_status.py → task_tools/update_task_status.py} +16 -16
- autobyteus/task_management/tools/todo_tools/__init__.py +18 -0
- autobyteus/task_management/tools/todo_tools/add_todo.py +78 -0
- autobyteus/task_management/tools/todo_tools/create_todo_list.py +79 -0
- autobyteus/task_management/tools/todo_tools/get_todo_list.py +55 -0
- autobyteus/task_management/tools/todo_tools/update_todo_status.py +85 -0
- autobyteus/tools/__init__.py +61 -21
- autobyteus/tools/bash/bash_executor.py +3 -3
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +5 -5
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +4 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -3
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -3
- autobyteus/tools/browser/standalone/navigate_to.py +13 -9
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +9 -5
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +10 -6
- autobyteus/tools/browser/standalone/webpage_reader.py +13 -9
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +9 -5
- autobyteus/tools/file/__init__.py +13 -0
- autobyteus/tools/file/edit_file.py +200 -0
- autobyteus/tools/file/list_directory.py +168 -0
- autobyteus/tools/file/{file_reader.py → read_file.py} +3 -3
- autobyteus/tools/file/search_files.py +188 -0
- autobyteus/tools/file/{file_writer.py → write_file.py} +3 -3
- autobyteus/tools/functional_tool.py +10 -8
- autobyteus/tools/mcp/tool.py +3 -3
- autobyteus/tools/mcp/tool_registrar.py +5 -2
- autobyteus/tools/multimedia/__init__.py +2 -1
- autobyteus/tools/multimedia/audio_tools.py +2 -2
- autobyteus/tools/multimedia/download_media_tool.py +136 -0
- autobyteus/tools/multimedia/image_tools.py +4 -4
- autobyteus/tools/multimedia/media_reader_tool.py +1 -1
- autobyteus/tools/registry/tool_definition.py +66 -13
- autobyteus/tools/registry/tool_registry.py +29 -0
- autobyteus/tools/search/__init__.py +17 -0
- autobyteus/tools/search/base_strategy.py +35 -0
- autobyteus/tools/search/client.py +24 -0
- autobyteus/tools/search/factory.py +81 -0
- autobyteus/tools/search/google_cse_strategy.py +68 -0
- autobyteus/tools/search/providers.py +10 -0
- autobyteus/tools/search/serpapi_strategy.py +65 -0
- autobyteus/tools/search/serper_strategy.py +87 -0
- autobyteus/tools/search_tool.py +83 -0
- autobyteus/tools/timer.py +4 -0
- autobyteus/tools/tool_meta.py +4 -24
- 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/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +1 -2
- {autobyteus-1.1.9.dist-info → autobyteus-1.2.1.dist-info}/METADATA +7 -6
- {autobyteus-1.1.9.dist-info → autobyteus-1.2.1.dist-info}/RECORD +117 -94
- examples/run_agentic_software_engineer.py +239 -0
- examples/run_poem_writer.py +3 -3
- autobyteus/person/__init__.py +0 -0
- autobyteus/person/examples/__init__.py +0 -0
- autobyteus/person/examples/sample_persons.py +0 -14
- autobyteus/person/examples/sample_roles.py +0 -14
- autobyteus/person/person.py +0 -29
- autobyteus/person/role.py +0 -14
- autobyteus/tools/google_search.py +0 -149
- autobyteus/tools/image_downloader.py +0 -99
- autobyteus/tools/pdf_downloader.py +0 -89
- {autobyteus-1.1.9.dist-info → autobyteus-1.2.1.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.9.dist-info → autobyteus-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.9.dist-info → autobyteus-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# file: autobyteus/examples/run_agentic_software_engineer.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# --- Boilerplate to make the script runnable from the project root ---
|
|
10
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
11
|
+
PACKAGE_ROOT = SCRIPT_DIR.parent
|
|
12
|
+
if str(PACKAGE_ROOT) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(PACKAGE_ROOT))
|
|
14
|
+
|
|
15
|
+
# Load environment variables from .env file in the project root
|
|
16
|
+
try:
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
env_file_path = PACKAGE_ROOT / ".env"
|
|
19
|
+
if env_file_path.exists():
|
|
20
|
+
load_dotenv(env_file_path)
|
|
21
|
+
print(f"Loaded environment variables from: {env_file_path}")
|
|
22
|
+
else:
|
|
23
|
+
print(f"Info: No .env file found at: {env_file_path}. Relying on exported environment variables.")
|
|
24
|
+
except ImportError:
|
|
25
|
+
print("Warning: python-dotenv not installed. Cannot load .env file.")
|
|
26
|
+
|
|
27
|
+
# --- Imports for the Agentic Software Engineer Example ---
|
|
28
|
+
try:
|
|
29
|
+
# Tool related imports
|
|
30
|
+
from autobyteus.tools.registry import default_tool_registry
|
|
31
|
+
from autobyteus.tools.tool_origin import ToolOrigin
|
|
32
|
+
# Import local tools to ensure they are registered
|
|
33
|
+
import autobyteus.tools.local_tools
|
|
34
|
+
|
|
35
|
+
# Workspace imports
|
|
36
|
+
from autobyteus.agent.workspace.local_workspace import LocalWorkspace
|
|
37
|
+
from autobyteus.agent.workspace.workspace_config import WorkspaceConfig
|
|
38
|
+
|
|
39
|
+
# For Agent creation
|
|
40
|
+
from autobyteus.agent.context.agent_config import AgentConfig
|
|
41
|
+
from autobyteus.llm.models import LLMModel
|
|
42
|
+
from autobyteus.llm.llm_factory import default_llm_factory, LLMFactory
|
|
43
|
+
from autobyteus.agent.factory.agent_factory import AgentFactory
|
|
44
|
+
from autobyteus.cli import agent_cli
|
|
45
|
+
except ImportError as e:
|
|
46
|
+
print(f"Error importing autobyteus components: {e}", file=sys.stderr)
|
|
47
|
+
print("Please ensure that the autobyteus library is installed and accessible.", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
# --- Logging Setup ---
|
|
51
|
+
logger = logging.getLogger("agentic_swe_example")
|
|
52
|
+
interactive_logger = logging.getLogger("autobyteus.cli.interactive")
|
|
53
|
+
|
|
54
|
+
def setup_logging(args: argparse.Namespace):
|
|
55
|
+
"""
|
|
56
|
+
Configures logging for the interactive session.
|
|
57
|
+
"""
|
|
58
|
+
loggers_to_clear = [
|
|
59
|
+
logging.getLogger(),
|
|
60
|
+
logging.getLogger("autobyteus"),
|
|
61
|
+
logging.getLogger("autobyteus.cli"),
|
|
62
|
+
interactive_logger,
|
|
63
|
+
]
|
|
64
|
+
for l in loggers_to_clear:
|
|
65
|
+
if l.hasHandlers():
|
|
66
|
+
for handler in l.handlers[:]:
|
|
67
|
+
l.removeHandler(handler)
|
|
68
|
+
if hasattr(handler, 'close'): handler.close()
|
|
69
|
+
|
|
70
|
+
script_log_level = logging.DEBUG if args.debug else logging.INFO
|
|
71
|
+
|
|
72
|
+
# 1. Handler for unformatted interactive output
|
|
73
|
+
interactive_handler = logging.StreamHandler(sys.stdout)
|
|
74
|
+
interactive_logger.addHandler(interactive_handler)
|
|
75
|
+
interactive_logger.setLevel(logging.INFO)
|
|
76
|
+
interactive_logger.propagate = False
|
|
77
|
+
|
|
78
|
+
# 2. Handler for formatted console logs
|
|
79
|
+
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
|
80
|
+
|
|
81
|
+
class FormattedConsoleFilter(logging.Filter):
|
|
82
|
+
def filter(self, record):
|
|
83
|
+
if record.name.startswith("agentic_swe_example") or record.name.startswith("autobyteus.cli"):
|
|
84
|
+
return True
|
|
85
|
+
if record.levelno >= logging.CRITICAL:
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
formatted_console_handler = logging.StreamHandler(sys.stdout)
|
|
90
|
+
formatted_console_handler.setFormatter(console_formatter)
|
|
91
|
+
formatted_console_handler.addFilter(FormattedConsoleFilter())
|
|
92
|
+
|
|
93
|
+
root_logger = logging.getLogger()
|
|
94
|
+
root_logger.addHandler(formatted_console_handler)
|
|
95
|
+
root_logger.setLevel(script_log_level)
|
|
96
|
+
|
|
97
|
+
# 3. Handler for the main agent log file
|
|
98
|
+
log_file_path = Path(args.agent_log_file).resolve()
|
|
99
|
+
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
agent_file_handler = logging.FileHandler(log_file_path, mode='w')
|
|
101
|
+
agent_file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s:%(lineno)d - %(message)s')
|
|
102
|
+
agent_file_handler.setFormatter(agent_file_formatter)
|
|
103
|
+
file_log_level = logging.DEBUG if args.debug else logging.INFO
|
|
104
|
+
|
|
105
|
+
autobyteus_logger = logging.getLogger("autobyteus")
|
|
106
|
+
autobyteus_logger.addHandler(agent_file_handler)
|
|
107
|
+
autobyteus_logger.setLevel(file_log_level)
|
|
108
|
+
autobyteus_logger.propagate = True
|
|
109
|
+
|
|
110
|
+
# 4. Configure `autobyteus.cli` package logging
|
|
111
|
+
cli_logger = logging.getLogger("autobyteus.cli")
|
|
112
|
+
cli_logger.setLevel(script_log_level)
|
|
113
|
+
cli_logger.propagate = True
|
|
114
|
+
|
|
115
|
+
logger.info(f"Core library logs (excluding CLI) redirected to: {log_file_path} (level: {logging.getLevelName(file_log_level)})")
|
|
116
|
+
|
|
117
|
+
# --- Environment Variable Checks ---
|
|
118
|
+
def check_required_env_vars():
|
|
119
|
+
"""Checks for environment variables required by this example. None are strictly required."""
|
|
120
|
+
logger.info("No specific environment variables are required, but ensure your chosen LLM provider's API key is set (e.g., GOOGLE_API_KEY).")
|
|
121
|
+
return {}
|
|
122
|
+
|
|
123
|
+
async def main(args: argparse.Namespace):
|
|
124
|
+
"""Main function to configure and run the Agentic Software Engineer."""
|
|
125
|
+
logger.info("--- Starting Agentic Software Engineer Example ---")
|
|
126
|
+
check_required_env_vars()
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# 1. Create a workspace for the agent.
|
|
130
|
+
workspace_path = Path(args.workspace_path).resolve()
|
|
131
|
+
workspace_path.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
logger.info(f"Agent workspace initialized at: {workspace_path}")
|
|
133
|
+
workspace_config = WorkspaceConfig(params={"root_path": str(workspace_path)})
|
|
134
|
+
workspace = LocalWorkspace(config=workspace_config)
|
|
135
|
+
|
|
136
|
+
# 2. Get all available local tools.
|
|
137
|
+
tool_registry = default_tool_registry
|
|
138
|
+
local_tool_defs = tool_registry.get_tools_by_origin(ToolOrigin.LOCAL)
|
|
139
|
+
local_tool_names = [tool_def.name for tool_def in local_tool_defs]
|
|
140
|
+
|
|
141
|
+
if not local_tool_names:
|
|
142
|
+
logger.error("No local tools were found in the registry. Cannot create agent.")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
logger.info(f"Creating instances for registered local tools: {local_tool_names}")
|
|
146
|
+
tools_for_agent = [tool_registry.create_tool(name) for name in local_tool_names]
|
|
147
|
+
|
|
148
|
+
# 3. Configure and create the agent.
|
|
149
|
+
try:
|
|
150
|
+
_ = LLMModel[args.llm_model]
|
|
151
|
+
except (KeyError, ValueError):
|
|
152
|
+
logger.error(f"LLM Model '{args.llm_model}' is not valid or ambiguous.", file=sys.stderr)
|
|
153
|
+
try:
|
|
154
|
+
LLMFactory.ensure_initialized()
|
|
155
|
+
print("\nAvailable LLM Models (use the 'Identifier' with --llm-model):")
|
|
156
|
+
all_models = sorted(list(LLMModel), key=lambda m: m.model_identifier)
|
|
157
|
+
if not all_models:
|
|
158
|
+
print(" No models found.")
|
|
159
|
+
for model in all_models:
|
|
160
|
+
print(f" - Display Name: {model.name:<30} Identifier: {model.model_identifier}")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(f"Additionally, an error occurred while listing models: {e}", file=sys.stderr)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
logger.info(f"Creating LLM instance for model: {args.llm_model}")
|
|
166
|
+
llm_instance = default_llm_factory.create_llm(model_identifier=args.llm_model)
|
|
167
|
+
|
|
168
|
+
# Load system prompt from file
|
|
169
|
+
prompt_path = SCRIPT_DIR / "prompts" / "agentic_software_engineer.prompt"
|
|
170
|
+
if not prompt_path.exists():
|
|
171
|
+
logger.error(f"System prompt file not found at: {prompt_path}")
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
with open(prompt_path, "r", encoding="utf-8") as f:
|
|
174
|
+
system_prompt = f.read()
|
|
175
|
+
logger.info(f"Loaded system prompt from: {prompt_path}")
|
|
176
|
+
|
|
177
|
+
agent_config = AgentConfig(
|
|
178
|
+
name="AgenticSoftwareDeveloper",
|
|
179
|
+
role="SoftwareEngineer",
|
|
180
|
+
description="An AI agent that can reason, plan, and execute software development tasks.",
|
|
181
|
+
llm_instance=llm_instance,
|
|
182
|
+
system_prompt=system_prompt,
|
|
183
|
+
tools=tools_for_agent,
|
|
184
|
+
workspace=workspace,
|
|
185
|
+
use_xml_tool_format=True, # As specified in the prompt
|
|
186
|
+
auto_execute_tools=False # Require user approval for safety
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
agent = AgentFactory().create_agent(config=agent_config)
|
|
190
|
+
logger.info(f"Agentic Software Engineer instance created: {agent.agent_id}")
|
|
191
|
+
|
|
192
|
+
# 4. Run the agent in an interactive CLI session.
|
|
193
|
+
logger.info(f"Starting interactive session for agent {agent.agent_id}...")
|
|
194
|
+
initial_prompt = f"Hello! I'm ready to work. My current working directory is `{workspace_path}`. What's the first task?"
|
|
195
|
+
await agent_cli.run(agent=agent, initial_prompt=initial_prompt, show_tool_logs=not args.no_tool_logs)
|
|
196
|
+
logger.info(f"Interactive session for agent {agent.agent_id} finished.")
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"An error occurred during the agent workflow: {e}", exc_info=True)
|
|
200
|
+
|
|
201
|
+
logger.info("--- Agentic Software Engineer Example Finished ---")
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
parser = argparse.ArgumentParser(description="Run the Agentic Software Engineer interactively.")
|
|
205
|
+
parser.add_argument("--llm-model", type=str, default="gemini-2.0-flash-", help=f"The LLM model identifier to use. Call --help-models for list.")
|
|
206
|
+
parser.add_argument("--workspace-path", type=str, default="./agent_workspace", help="Path to the agent's working directory. (Default: ./agent_workspace)")
|
|
207
|
+
parser.add_argument("--help-models", action="store_true", help="Display available LLM models and exit.")
|
|
208
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
|
|
209
|
+
parser.add_argument("--agent-log-file", type=str, default="./agent_logs_swe.txt",
|
|
210
|
+
help="Path to the log file for autobyteus.* library logs. (Default: ./agent_logs_swe.txt)")
|
|
211
|
+
parser.add_argument("--no-tool-logs", action="store_true",
|
|
212
|
+
help="Disable display of [Tool Log (...)] messages on the console by the agent_cli.")
|
|
213
|
+
|
|
214
|
+
if "--help-models" in sys.argv:
|
|
215
|
+
try:
|
|
216
|
+
LLMFactory.ensure_initialized()
|
|
217
|
+
print("Available LLM Models (use the 'Identifier' with --llm-model):")
|
|
218
|
+
all_models = sorted(list(LLMModel), key=lambda m: m.model_identifier)
|
|
219
|
+
if not all_models:
|
|
220
|
+
print(" No models found.")
|
|
221
|
+
for model in all_models:
|
|
222
|
+
print(f" - Display Name: {model.name:<30} Identifier: {model.model_identifier}")
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"Error listing models: {e}")
|
|
225
|
+
sys.exit(0)
|
|
226
|
+
|
|
227
|
+
parsed_args = parser.parse_args()
|
|
228
|
+
|
|
229
|
+
setup_logging(parsed_args)
|
|
230
|
+
check_required_env_vars()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
asyncio.run(main(parsed_args))
|
|
234
|
+
except (KeyboardInterrupt, SystemExit):
|
|
235
|
+
logger.info("Script interrupted by user. Exiting.")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"An unhandled error occurred at the top level: {e}", exc_info=True)
|
|
238
|
+
finally:
|
|
239
|
+
logger.info("Exiting script.")
|
examples/run_poem_writer.py
CHANGED
|
@@ -39,7 +39,7 @@ try:
|
|
|
39
39
|
from autobyteus.llm.llm_factory import default_llm_factory, LLMFactory
|
|
40
40
|
from autobyteus.agent.factory.agent_factory import AgentFactory
|
|
41
41
|
from autobyteus.cli import agent_cli
|
|
42
|
-
from autobyteus.tools.file.
|
|
42
|
+
from autobyteus.tools.file.write_file import write_file
|
|
43
43
|
# Import core workspace and schema components from the library
|
|
44
44
|
from autobyteus.agent.workspace import BaseAgentWorkspace, WorkspaceConfig
|
|
45
45
|
from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
@@ -176,8 +176,8 @@ async def main(args: argparse.Namespace):
|
|
|
176
176
|
workspace_base_path.mkdir(parents=True, exist_ok=True)
|
|
177
177
|
logger.info(f"Agent will be configured with a local workspace at: {workspace_base_path}")
|
|
178
178
|
|
|
179
|
-
# The
|
|
180
|
-
tools_for_agent = [
|
|
179
|
+
# The write_file tool is an instance ready to be used
|
|
180
|
+
tools_for_agent = [write_file]
|
|
181
181
|
|
|
182
182
|
# UPDATED: The system prompt now provides context about the workspace.
|
|
183
183
|
system_prompt = (
|
autobyteus/person/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from autobyteus.person.examples.sample_roles import RESEARCHER_ROLE, WRITER_ROLE
|
|
2
|
-
from autobyteus.person.person import Person
|
|
3
|
-
|
|
4
|
-
ANNA = Person(
|
|
5
|
-
name="Anna",
|
|
6
|
-
role=RESEARCHER_ROLE,
|
|
7
|
-
characteristics=["detail-oriented", "analytical", "curious"]
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
RYAN = Person(
|
|
11
|
-
name="Ryan",
|
|
12
|
-
role=WRITER_ROLE,
|
|
13
|
-
characteristics=["creative", "empathetic", "articulate"]
|
|
14
|
-
)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from autobyteus.person.role import Role
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
RESEARCHER_ROLE = Role(
|
|
5
|
-
name="Researcher",
|
|
6
|
-
skills=["data analysis", "literature review", "critical thinking"],
|
|
7
|
-
responsibilities=["gather information", "analyze data", "report findings"]
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
WRITER_ROLE = Role(
|
|
11
|
-
name="Writer",
|
|
12
|
-
skills=["content creation", "editing", "storytelling"],
|
|
13
|
-
responsibilities=["draft articles", "revise content", "adapt to different styles"]
|
|
14
|
-
)
|
autobyteus/person/person.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
from autobyteus.person.role import Role
|
|
3
|
-
|
|
4
|
-
class Person:
|
|
5
|
-
def __init__(self, name: str, role: Role, characteristics: List[str]):
|
|
6
|
-
self.name = name
|
|
7
|
-
self.role = role
|
|
8
|
-
self.characteristics = characteristics
|
|
9
|
-
self.tasks = []
|
|
10
|
-
|
|
11
|
-
def get_description(self) -> str:
|
|
12
|
-
characteristics_str = ", ".join(self.characteristics)
|
|
13
|
-
return (f"Person: {self.name}\n"
|
|
14
|
-
f"{self.role.get_description()}\n"
|
|
15
|
-
f"Characteristics: {characteristics_str}")
|
|
16
|
-
|
|
17
|
-
def __str__(self) -> str:
|
|
18
|
-
return f"{self.name} ({self.role.name})"
|
|
19
|
-
|
|
20
|
-
def assign_task(self, task):
|
|
21
|
-
if task not in self.tasks:
|
|
22
|
-
self.tasks.append(task)
|
|
23
|
-
|
|
24
|
-
def unassign_task(self, task):
|
|
25
|
-
if task in self.tasks:
|
|
26
|
-
self.tasks.remove(task)
|
|
27
|
-
|
|
28
|
-
def get_tasks(self):
|
|
29
|
-
return self.tasks
|
autobyteus/person/role.py
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
|
|
3
|
-
class Role:
|
|
4
|
-
def __init__(self, name: str, skills: List[str], responsibilities: List[str]):
|
|
5
|
-
self.name = name
|
|
6
|
-
self.skills = skills
|
|
7
|
-
self.responsibilities = responsibilities
|
|
8
|
-
|
|
9
|
-
def get_description(self) -> str:
|
|
10
|
-
skills_str = ", ".join(self.skills)
|
|
11
|
-
responsibilities_str = ", ".join(self.responsibilities)
|
|
12
|
-
return (f"Role: {self.name}\n"
|
|
13
|
-
f"Skills: {skills_str}\n"
|
|
14
|
-
f"Responsibilities: {responsibilities_str}")
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import aiohttp
|
|
5
|
-
from typing import Optional, TYPE_CHECKING, Any, Dict, List
|
|
6
|
-
|
|
7
|
-
from autobyteus.tools.base_tool import BaseTool
|
|
8
|
-
from autobyteus.tools.tool_config import ToolConfig
|
|
9
|
-
from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
10
|
-
from autobyteus.tools.tool_category import ToolCategory
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from autobyteus.agent.context import AgentContext
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
class GoogleSearch(BaseTool):
|
|
18
|
-
"""
|
|
19
|
-
Performs a Google search using the Serper.dev API and returns a structured summary of the results.
|
|
20
|
-
This tool requires a Serper API key, which should be set in the SERPER_API_KEY environment variable.
|
|
21
|
-
"""
|
|
22
|
-
CATEGORY = ToolCategory.WEB
|
|
23
|
-
API_URL = "https://google.serper.dev/search"
|
|
24
|
-
|
|
25
|
-
def __init__(self, config: Optional[ToolConfig] = None):
|
|
26
|
-
super().__init__(config=config)
|
|
27
|
-
self.api_key: Optional[str] = None
|
|
28
|
-
|
|
29
|
-
if config:
|
|
30
|
-
self.api_key = config.get('api_key')
|
|
31
|
-
|
|
32
|
-
if not self.api_key:
|
|
33
|
-
self.api_key = os.getenv("SERPER_API_KEY")
|
|
34
|
-
|
|
35
|
-
if not self.api_key:
|
|
36
|
-
raise ValueError(
|
|
37
|
-
"GoogleSearch tool requires a Serper API key. "
|
|
38
|
-
"Please provide it via the 'api_key' config parameter or set the 'SERPER_API_KEY' environment variable."
|
|
39
|
-
)
|
|
40
|
-
logger.debug("GoogleSearch (API-based) tool initialized.")
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def get_name(cls) -> str:
|
|
44
|
-
return "GoogleSearch"
|
|
45
|
-
|
|
46
|
-
@classmethod
|
|
47
|
-
def get_description(cls) -> str:
|
|
48
|
-
return (
|
|
49
|
-
"Searches Google for a given query using the Serper API. "
|
|
50
|
-
"Returns a concise, structured summary of search results, including direct answers and top organic links."
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
@classmethod
|
|
54
|
-
def get_argument_schema(cls) -> Optional[ParameterSchema]:
|
|
55
|
-
schema = ParameterSchema()
|
|
56
|
-
schema.add_parameter(ParameterDefinition(
|
|
57
|
-
name="query",
|
|
58
|
-
param_type=ParameterType.STRING,
|
|
59
|
-
description="The search query string.",
|
|
60
|
-
required=True
|
|
61
|
-
))
|
|
62
|
-
schema.add_parameter(ParameterDefinition(
|
|
63
|
-
name="num_results",
|
|
64
|
-
param_type=ParameterType.INTEGER,
|
|
65
|
-
description="The number of organic search results to return.",
|
|
66
|
-
required=False,
|
|
67
|
-
default_value=5,
|
|
68
|
-
min_value=1,
|
|
69
|
-
max_value=10
|
|
70
|
-
))
|
|
71
|
-
return schema
|
|
72
|
-
|
|
73
|
-
@classmethod
|
|
74
|
-
def get_config_schema(cls) -> Optional[ParameterSchema]:
|
|
75
|
-
schema = ParameterSchema()
|
|
76
|
-
schema.add_parameter(ParameterDefinition(
|
|
77
|
-
name="api_key",
|
|
78
|
-
param_type=ParameterType.STRING,
|
|
79
|
-
description="The API key for the Serper.dev service. Overrides the SERPER_API_KEY environment variable.",
|
|
80
|
-
required=False
|
|
81
|
-
))
|
|
82
|
-
return schema
|
|
83
|
-
|
|
84
|
-
def _format_results(self, data: Dict[str, Any]) -> str:
|
|
85
|
-
"""Formats the JSON response from Serper into a clean string for an LLM."""
|
|
86
|
-
summary_parts = []
|
|
87
|
-
|
|
88
|
-
# 1. Answer Box (most important for direct questions)
|
|
89
|
-
if "answerBox" in data:
|
|
90
|
-
answer_box = data["answerBox"]
|
|
91
|
-
title = answer_box.get("title", "")
|
|
92
|
-
snippet = answer_box.get("snippet") or answer_box.get("answer")
|
|
93
|
-
summary_parts.append(f"Direct Answer for '{title}':\n{snippet}")
|
|
94
|
-
|
|
95
|
-
# 2. Knowledge Graph (for entity information)
|
|
96
|
-
if "knowledgeGraph" in data:
|
|
97
|
-
kg = data["knowledgeGraph"]
|
|
98
|
-
title = kg.get("title", "")
|
|
99
|
-
description = kg.get("description")
|
|
100
|
-
summary_parts.append(f"Summary for '{title}':\n{description}")
|
|
101
|
-
|
|
102
|
-
# 3. Organic Results (the main search links)
|
|
103
|
-
if "organic" in data and data["organic"]:
|
|
104
|
-
organic_results = data["organic"]
|
|
105
|
-
results_str = "\n".join(
|
|
106
|
-
f"{i+1}. {result.get('title', 'No Title')}\n"
|
|
107
|
-
f" Link: {result.get('link', 'No Link')}\n"
|
|
108
|
-
f" Snippet: {result.get('snippet', 'No Snippet')}"
|
|
109
|
-
for i, result in enumerate(organic_results)
|
|
110
|
-
)
|
|
111
|
-
summary_parts.append(f"Search Results:\n{results_str}")
|
|
112
|
-
|
|
113
|
-
if not summary_parts:
|
|
114
|
-
return "No relevant information found for the query."
|
|
115
|
-
|
|
116
|
-
return "\n\n---\n\n".join(summary_parts)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
async def _execute(self, context: 'AgentContext', query: str, num_results: int = 5) -> str:
|
|
120
|
-
logger.info(f"Executing GoogleSearch (API) for agent {context.agent_id} with query: '{query}'")
|
|
121
|
-
|
|
122
|
-
headers = {
|
|
123
|
-
'X-API-KEY': self.api_key,
|
|
124
|
-
'Content-Type': 'application/json'
|
|
125
|
-
}
|
|
126
|
-
payload = json.dumps({
|
|
127
|
-
"q": query,
|
|
128
|
-
"num": num_results
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
async with aiohttp.ClientSession() as session:
|
|
133
|
-
async with session.post(self.API_URL, headers=headers, data=payload) as response:
|
|
134
|
-
if response.status == 200:
|
|
135
|
-
data = await response.json()
|
|
136
|
-
return self._format_results(data)
|
|
137
|
-
else:
|
|
138
|
-
error_text = await response.text()
|
|
139
|
-
logger.error(
|
|
140
|
-
f"Serper API returned a non-200 status code: {response.status}. "
|
|
141
|
-
f"Response: {error_text}"
|
|
142
|
-
)
|
|
143
|
-
raise RuntimeError(f"API request failed with status {response.status}: {error_text}")
|
|
144
|
-
except aiohttp.ClientError as e:
|
|
145
|
-
logger.error(f"Network error during GoogleSearch API call: {e}", exc_info=True)
|
|
146
|
-
raise RuntimeError(f"A network error occurred: {e}")
|
|
147
|
-
except Exception as e:
|
|
148
|
-
logger.error(f"An unexpected error occurred in GoogleSearch tool: {e}", exc_info=True)
|
|
149
|
-
raise
|
|
@@ -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.utils.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()
|